//! CSS position values. use super::length::LengthPercentage; use super::percentage::Percentage; use crate::error::{ParserError, PrinterError}; use crate::macros::enum_property; use crate::printer::Printer; use crate::targets::Browsers; use crate::traits::{IsCompatible, Parse, ToCss, Zero}; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; #[cfg(feature = "serde")] use crate::serialization::ValueWrapper; /// A CSS [``](https://www.w3.org/TR/css3-values/#position) value, /// as used in the `background-position` property, gradients, masks, etc. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub struct Position { /// The x-position. pub x: HorizontalPosition, /// The y-position. pub y: VerticalPosition, } impl Position { /// Returns a `Position` with both the x and y set to `center`. pub fn center() -> Position { Position { x: HorizontalPosition::Center, y: VerticalPosition::Center, } } /// Returns whether both the x and y positions are centered. pub fn is_center(&self) -> bool { self.x.is_center() && self.y.is_center() } /// Returns whether both the x and y positions are zero. pub fn is_zero(&self) -> bool { self.x.is_zero() && self.y.is_zero() } } impl Default for Position { fn default() -> Position { Position { x: HorizontalPosition::Length(LengthPercentage::Percentage(Percentage(0.0))), y: VerticalPosition::Length(LengthPercentage::Percentage(Percentage(0.0))), } } } impl<'i> Parse<'i> for Position { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { match input.try_parse(HorizontalPosition::parse) { Ok(HorizontalPosition::Center) => { // Try parsing a vertical position next. if let Ok(y) = input.try_parse(VerticalPosition::parse) { return Ok(Position { x: HorizontalPosition::Center, y, }); } // If it didn't work, assume the first actually represents a y position, // and the next is an x position. e.g. `center left` rather than `left center`. let x = input.try_parse(HorizontalPosition::parse).unwrap_or(HorizontalPosition::Center); let y = VerticalPosition::Center; return Ok(Position { x, y }); } Ok(x @ HorizontalPosition::Length(_)) => { // If we got a length as the first component, then the second must // be a keyword or length (not a side offset). if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) { let y = VerticalPosition::Side { side: y_keyword, offset: None, }; return Ok(Position { x, y }); } if let Ok(y_lp) = input.try_parse(LengthPercentage::parse) { let y = VerticalPosition::Length(y_lp); return Ok(Position { x, y }); } let y = VerticalPosition::Center; let _ = input.try_parse(|i| i.expect_ident_matching("center")); return Ok(Position { x, y }); } Ok(HorizontalPosition::Side { side: x_keyword, offset: lp, }) => { // If we got a horizontal side keyword (and optional offset), expect another for the vertical side. // e.g. `left center` or `left 20px center` if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() { let x = HorizontalPosition::Side { side: x_keyword, offset: lp, }; let y = VerticalPosition::Center; return Ok(Position { x, y }); } // e.g. `left top`, `left top 20px`, `left 20px top`, or `left 20px top 20px` if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) { let y_lp = input.try_parse(LengthPercentage::parse).ok(); let x = HorizontalPosition::Side { side: x_keyword, offset: lp, }; let y = VerticalPosition::Side { side: y_keyword, offset: y_lp, }; return Ok(Position { x, y }); } // If we didn't get a vertical side keyword (e.g. `left 20px`), then apply the offset to the vertical side. let x = HorizontalPosition::Side { side: x_keyword, offset: None, }; let y = lp.map_or(VerticalPosition::Center, VerticalPosition::Length); return Ok(Position { x, y }); } _ => {} } // If the horizontal position didn't parse, then it must be out of order. Try vertical position keyword. let y_keyword = VerticalPositionKeyword::parse(input)?; let lp_and_x_pos: Result<_, ParseError<()>> = input.try_parse(|i| { let y_lp = i.try_parse(LengthPercentage::parse).ok(); if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) { let x_lp = i.try_parse(LengthPercentage::parse).ok(); let x_pos = HorizontalPosition::Side { side: x_keyword, offset: x_lp, }; return Ok((y_lp, x_pos)); } i.expect_ident_matching("center")?; let x_pos = HorizontalPosition::Center; Ok((y_lp, x_pos)) }); if let Ok((y_lp, x)) = lp_and_x_pos { let y = VerticalPosition::Side { side: y_keyword, offset: y_lp, }; return Ok(Position { x, y }); } let x = HorizontalPosition::Center; let y = VerticalPosition::Side { side: y_keyword, offset: None, }; Ok(Position { x, y }) } } impl ToCss for Position { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match (&self.x, &self.y) { (x_pos @ &HorizontalPosition::Side { side, offset: Some(_) }, &VerticalPosition::Length(ref y_lp)) if side != HorizontalPositionKeyword::Left => { x_pos.to_css(dest)?; dest.write_str(" top ")?; y_lp.to_css(dest) } (x_pos @ &HorizontalPosition::Side { side, offset: Some(_) }, y) if side != HorizontalPositionKeyword::Left && y.is_center() => { // If there is a side keyword with an offset, "center" must be a keyword not a percentage. x_pos.to_css(dest)?; dest.write_str(" center") } (&HorizontalPosition::Length(ref x_lp), y_pos @ &VerticalPosition::Side { side, offset: Some(_) }) if side != VerticalPositionKeyword::Top => { dest.write_str("left ")?; x_lp.to_css(dest)?; dest.write_str(" ")?; y_pos.to_css(dest) } (x, y) if x.is_center() && y.is_center() => { // `center center` => 50% x.to_css(dest) } (&HorizontalPosition::Length(ref x_lp), y) if y.is_center() => { // `center` is assumed if omitted. x_lp.to_css(dest) } (&HorizontalPosition::Side { side, offset: None }, y) if y.is_center() => { let p: LengthPercentage = side.into(); p.to_css(dest) } (x, y_pos @ &VerticalPosition::Side { offset: None, .. }) if x.is_center() => y_pos.to_css(dest), (&HorizontalPosition::Side { side: x, offset: None }, &VerticalPosition::Side { side: y, offset: None }) => { let x: LengthPercentage = x.into(); let y: LengthPercentage = y.into(); x.to_css(dest)?; dest.write_str(" ")?; y.to_css(dest) } (x_pos, y_pos) => { let zero = LengthPercentage::zero(); let fifty = LengthPercentage::Percentage(Percentage(0.5)); let x_len = match &x_pos { HorizontalPosition::Side { side: HorizontalPositionKeyword::Left, offset, } => { if let Some(len) = offset { if len.is_zero() { Some(&zero) } else { Some(len) } } else { Some(&zero) } } HorizontalPosition::Length(len) if len.is_zero() => Some(&zero), HorizontalPosition::Length(len) => Some(len), HorizontalPosition::Center => Some(&fifty), _ => None, }; let y_len = match &y_pos { VerticalPosition::Side { side: VerticalPositionKeyword::Top, offset, } => { if let Some(len) = offset { if len.is_zero() { Some(&zero) } else { Some(len) } } else { Some(&zero) } } VerticalPosition::Length(len) if len.is_zero() => Some(&zero), VerticalPosition::Length(len) => Some(len), VerticalPosition::Center => Some(&fifty), _ => None, }; if let (Some(x), Some(y)) = (x_len, y_len) { x.to_css(dest)?; dest.write_str(" ")?; y.to_css(dest) } else { x_pos.to_css(dest)?; dest.write_str(" ")?; y_pos.to_css(dest) } } } } } impl IsCompatible for Position { fn is_compatible(&self, _browsers: Browsers) -> bool { true } } /// A component within a [Position](Position) value, representing a position /// along either the horizontal or vertical axis of a box. /// /// This type is generic over side keywords. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum PositionComponent { /// The `center` keyword. Center, /// A length or percentage from the top-left corner of the box. #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::"))] Length(LengthPercentage), /// A side keyword with an optional offset. Side { /// A side keyword. side: S, /// Offset from the side. offset: Option, }, } impl PositionComponent { fn is_center(&self) -> bool { match self { PositionComponent::Center => true, PositionComponent::Length(LengthPercentage::Percentage(Percentage(p))) => *p == 0.5, _ => false, } } fn is_zero(&self) -> bool { matches!(self, PositionComponent::Length(len) if len.is_zero()) } } impl<'i, S: Parse<'i>> Parse<'i> for PositionComponent { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() { return Ok(PositionComponent::Center); } if let Ok(lp) = input.try_parse(|input| LengthPercentage::parse(input)) { return Ok(PositionComponent::Length(lp)); } let side = S::parse(input)?; let offset = input.try_parse(|input| LengthPercentage::parse(input)).ok(); Ok(PositionComponent::Side { side, offset }) } } impl ToCss for PositionComponent { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { use PositionComponent::*; match &self { Center => { if dest.minify { dest.write_str("50%") } else { dest.write_str("center") } } Length(lp) => lp.to_css(dest), Side { side, offset } => { side.to_css(dest)?; if let Some(lp) = offset { dest.write_str(" ")?; lp.to_css(dest)?; } Ok(()) } } } } enum_property! { /// A horizontal position keyword. pub enum HorizontalPositionKeyword { /// The `left` keyword. Left, /// The `right` keyword. Right, } } impl Into for HorizontalPositionKeyword { fn into(self) -> LengthPercentage { match self { HorizontalPositionKeyword::Left => LengthPercentage::zero(), HorizontalPositionKeyword::Right => LengthPercentage::Percentage(Percentage(1.0)), } } } enum_property! { /// A vertical position keyword. pub enum VerticalPositionKeyword { /// The `top` keyword. Top, /// The `bottom` keyword. Bottom, } } impl Into for VerticalPositionKeyword { fn into(self) -> LengthPercentage { match self { VerticalPositionKeyword::Top => LengthPercentage::zero(), VerticalPositionKeyword::Bottom => LengthPercentage::Percentage(Percentage(1.0)), } } } /// A horizontal position component. pub type HorizontalPosition = PositionComponent; /// A vertical position component. pub type VerticalPosition = PositionComponent;