| //! 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 [`<position>`](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<Self, ParseError<'i, ParserError<'i>>> { |
| 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<W>(&self, dest: &mut Printer<W>) -> 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<S> { |
| /// The `center` keyword. |
| Center, |
| /// A length or percentage from the top-left corner of the box. |
| #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))] |
| Length(LengthPercentage), |
| /// A side keyword with an optional offset. |
| Side { |
| /// A side keyword. |
| side: S, |
| /// Offset from the side. |
| offset: Option<LengthPercentage>, |
| }, |
| } |
| |
| impl<S> PositionComponent<S> { |
| 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<S> { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| 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<S: ToCss> ToCss for PositionComponent<S> { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> 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<LengthPercentage> 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<LengthPercentage> 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<HorizontalPositionKeyword>; |
| |
| /// A vertical position component. |
| pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>; |