lightningcss/values/
position.rs

1//! CSS position values.
2
3use super::length::LengthPercentage;
4use super::percentage::Percentage;
5use crate::error::{ParserError, PrinterError};
6use crate::macros::enum_property;
7use crate::printer::Printer;
8use crate::targets::Browsers;
9use crate::traits::{IsCompatible, Parse, ToCss, Zero};
10#[cfg(feature = "visitor")]
11use crate::visitor::Visit;
12use cssparser::*;
13
14#[cfg(feature = "serde")]
15use crate::serialization::ValueWrapper;
16
17/// A CSS [`<position>`](https://www.w3.org/TR/css3-values/#position) value,
18/// as used in the `background-position` property, gradients, masks, etc.
19#[derive(Debug, Clone, PartialEq)]
20#[cfg_attr(feature = "visitor", derive(Visit))]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
23#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
24pub struct Position {
25  /// The x-position.
26  pub x: HorizontalPosition,
27  /// The y-position.
28  pub y: VerticalPosition,
29}
30
31impl Position {
32  /// Returns a `Position` with both the x and y set to `center`.
33  pub fn center() -> Position {
34    Position {
35      x: HorizontalPosition::Center,
36      y: VerticalPosition::Center,
37    }
38  }
39
40  /// Returns whether both the x and y positions are centered.
41  pub fn is_center(&self) -> bool {
42    self.x.is_center() && self.y.is_center()
43  }
44
45  /// Returns whether both the x and y positions are zero.
46  pub fn is_zero(&self) -> bool {
47    self.x.is_zero() && self.y.is_zero()
48  }
49}
50
51impl Default for Position {
52  fn default() -> Position {
53    Position {
54      x: HorizontalPosition::Length(LengthPercentage::Percentage(Percentage(0.0))),
55      y: VerticalPosition::Length(LengthPercentage::Percentage(Percentage(0.0))),
56    }
57  }
58}
59
60impl<'i> Parse<'i> for Position {
61  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
62    match input.try_parse(HorizontalPosition::parse) {
63      Ok(HorizontalPosition::Center) => {
64        // Try parsing a vertical position next.
65        if let Ok(y) = input.try_parse(VerticalPosition::parse) {
66          return Ok(Position {
67            x: HorizontalPosition::Center,
68            y,
69          });
70        }
71
72        // If it didn't work, assume the first actually represents a y position,
73        // and the next is an x position. e.g. `center left` rather than `left center`.
74        let x = input.try_parse(HorizontalPosition::parse).unwrap_or(HorizontalPosition::Center);
75        let y = VerticalPosition::Center;
76        return Ok(Position { x, y });
77      }
78      Ok(x @ HorizontalPosition::Length(_)) => {
79        // If we got a length as the first component, then the second must
80        // be a keyword or length (not a side offset).
81        if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
82          let y = VerticalPosition::Side {
83            side: y_keyword,
84            offset: None,
85          };
86          return Ok(Position { x, y });
87        }
88        if let Ok(y_lp) = input.try_parse(LengthPercentage::parse) {
89          let y = VerticalPosition::Length(y_lp);
90          return Ok(Position { x, y });
91        }
92        let y = VerticalPosition::Center;
93        let _ = input.try_parse(|i| i.expect_ident_matching("center"));
94        return Ok(Position { x, y });
95      }
96      Ok(HorizontalPosition::Side {
97        side: x_keyword,
98        offset: lp,
99      }) => {
100        // If we got a horizontal side keyword (and optional offset), expect another for the vertical side.
101        // e.g. `left center` or `left 20px center`
102        if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
103          let x = HorizontalPosition::Side {
104            side: x_keyword,
105            offset: lp,
106          };
107          let y = VerticalPosition::Center;
108          return Ok(Position { x, y });
109        }
110
111        // e.g. `left top`, `left top 20px`, `left 20px top`, or `left 20px top 20px`
112        if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
113          let y_lp = input.try_parse(LengthPercentage::parse).ok();
114          let x = HorizontalPosition::Side {
115            side: x_keyword,
116            offset: lp,
117          };
118          let y = VerticalPosition::Side {
119            side: y_keyword,
120            offset: y_lp,
121          };
122          return Ok(Position { x, y });
123        }
124
125        // If we didn't get a vertical side keyword (e.g. `left 20px`), then apply the offset to the vertical side.
126        let x = HorizontalPosition::Side {
127          side: x_keyword,
128          offset: None,
129        };
130        let y = lp.map_or(VerticalPosition::Center, VerticalPosition::Length);
131        return Ok(Position { x, y });
132      }
133      _ => {}
134    }
135
136    // If the horizontal position didn't parse, then it must be out of order. Try vertical position keyword.
137    let y_keyword = VerticalPositionKeyword::parse(input)?;
138    let lp_and_x_pos: Result<_, ParseError<()>> = input.try_parse(|i| {
139      let y_lp = i.try_parse(LengthPercentage::parse).ok();
140      if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) {
141        let x_lp = i.try_parse(LengthPercentage::parse).ok();
142        let x_pos = HorizontalPosition::Side {
143          side: x_keyword,
144          offset: x_lp,
145        };
146        return Ok((y_lp, x_pos));
147      }
148      i.expect_ident_matching("center")?;
149      let x_pos = HorizontalPosition::Center;
150      Ok((y_lp, x_pos))
151    });
152
153    if let Ok((y_lp, x)) = lp_and_x_pos {
154      let y = VerticalPosition::Side {
155        side: y_keyword,
156        offset: y_lp,
157      };
158      return Ok(Position { x, y });
159    }
160
161    let x = HorizontalPosition::Center;
162    let y = VerticalPosition::Side {
163      side: y_keyword,
164      offset: None,
165    };
166    Ok(Position { x, y })
167  }
168}
169
170impl ToCss for Position {
171  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
172  where
173    W: std::fmt::Write,
174  {
175    match (&self.x, &self.y) {
176      (x_pos @ &HorizontalPosition::Side { side, offset: Some(_) }, &VerticalPosition::Length(ref y_lp))
177        if side != HorizontalPositionKeyword::Left =>
178      {
179        x_pos.to_css(dest)?;
180        dest.write_str(" top ")?;
181        y_lp.to_css(dest)
182      }
183      (x_pos @ &HorizontalPosition::Side { side, offset: Some(_) }, y)
184        if side != HorizontalPositionKeyword::Left && y.is_center() =>
185      {
186        // If there is a side keyword with an offset, "center" must be a keyword not a percentage.
187        x_pos.to_css(dest)?;
188        dest.write_str(" center")
189      }
190      (&HorizontalPosition::Length(ref x_lp), y_pos @ &VerticalPosition::Side { side, offset: Some(_) })
191        if side != VerticalPositionKeyword::Top =>
192      {
193        dest.write_str("left ")?;
194        x_lp.to_css(dest)?;
195        dest.write_str(" ")?;
196        y_pos.to_css(dest)
197      }
198      (x, y) if x.is_center() && y.is_center() => {
199        // `center center` => 50%
200        x.to_css(dest)
201      }
202      (&HorizontalPosition::Length(ref x_lp), y) if y.is_center() => {
203        // `center` is assumed if omitted.
204        x_lp.to_css(dest)
205      }
206      (&HorizontalPosition::Side { side, offset: None }, y) if y.is_center() => {
207        let p: LengthPercentage = side.into();
208        p.to_css(dest)
209      }
210      (x, y_pos @ &VerticalPosition::Side { offset: None, .. }) if x.is_center() => y_pos.to_css(dest),
211      (&HorizontalPosition::Side { side: x, offset: None }, &VerticalPosition::Side { side: y, offset: None }) => {
212        let x: LengthPercentage = x.into();
213        let y: LengthPercentage = y.into();
214        x.to_css(dest)?;
215        dest.write_str(" ")?;
216        y.to_css(dest)
217      }
218      (x_pos, y_pos) => {
219        let zero = LengthPercentage::zero();
220        let fifty = LengthPercentage::Percentage(Percentage(0.5));
221        let x_len = match &x_pos {
222          HorizontalPosition::Side {
223            side: HorizontalPositionKeyword::Left,
224            offset,
225          } => {
226            if let Some(len) = offset {
227              if len.is_zero() {
228                Some(&zero)
229              } else {
230                Some(len)
231              }
232            } else {
233              Some(&zero)
234            }
235          }
236          HorizontalPosition::Length(len) if len.is_zero() => Some(&zero),
237          HorizontalPosition::Length(len) => Some(len),
238          HorizontalPosition::Center => Some(&fifty),
239          _ => None,
240        };
241
242        let y_len = match &y_pos {
243          VerticalPosition::Side {
244            side: VerticalPositionKeyword::Top,
245            offset,
246          } => {
247            if let Some(len) = offset {
248              if len.is_zero() {
249                Some(&zero)
250              } else {
251                Some(len)
252              }
253            } else {
254              Some(&zero)
255            }
256          }
257          VerticalPosition::Length(len) if len.is_zero() => Some(&zero),
258          VerticalPosition::Length(len) => Some(len),
259          VerticalPosition::Center => Some(&fifty),
260          _ => None,
261        };
262
263        if let (Some(x), Some(y)) = (x_len, y_len) {
264          x.to_css(dest)?;
265          dest.write_str(" ")?;
266          y.to_css(dest)
267        } else {
268          x_pos.to_css(dest)?;
269          dest.write_str(" ")?;
270          y_pos.to_css(dest)
271        }
272      }
273    }
274  }
275}
276
277impl IsCompatible for Position {
278  fn is_compatible(&self, _browsers: Browsers) -> bool {
279    true
280  }
281}
282
283/// A component within a [Position](Position) value, representing a position
284/// along either the horizontal or vertical axis of a box.
285///
286/// This type is generic over side keywords.
287#[derive(Debug, Clone, PartialEq)]
288#[cfg_attr(feature = "visitor", derive(Visit))]
289#[cfg_attr(
290  feature = "serde",
291  derive(serde::Serialize, serde::Deserialize),
292  serde(tag = "type", rename_all = "kebab-case")
293)]
294#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
295#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
296pub enum PositionComponent<S> {
297  /// The `center` keyword.
298  Center,
299  /// A length or percentage from the top-left corner of the box.
300  #[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
301  Length(LengthPercentage),
302  /// A side keyword with an optional offset.
303  Side {
304    /// A side keyword.
305    side: S,
306    /// Offset from the side.
307    offset: Option<LengthPercentage>,
308  },
309}
310
311impl<S> PositionComponent<S> {
312  fn is_center(&self) -> bool {
313    match self {
314      PositionComponent::Center => true,
315      PositionComponent::Length(LengthPercentage::Percentage(Percentage(p))) => *p == 0.5,
316      _ => false,
317    }
318  }
319
320  fn is_zero(&self) -> bool {
321    matches!(self, PositionComponent::Length(len) if len.is_zero())
322  }
323}
324
325impl<'i, S: Parse<'i>> Parse<'i> for PositionComponent<S> {
326  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
327    if input.try_parse(|i| i.expect_ident_matching("center")).is_ok() {
328      return Ok(PositionComponent::Center);
329    }
330
331    if let Ok(lp) = input.try_parse(|input| LengthPercentage::parse(input)) {
332      return Ok(PositionComponent::Length(lp));
333    }
334
335    let side = S::parse(input)?;
336    let offset = input.try_parse(|input| LengthPercentage::parse(input)).ok();
337    Ok(PositionComponent::Side { side, offset })
338  }
339}
340
341impl<S: ToCss> ToCss for PositionComponent<S> {
342  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
343  where
344    W: std::fmt::Write,
345  {
346    use PositionComponent::*;
347    match &self {
348      Center => {
349        if dest.minify {
350          dest.write_str("50%")
351        } else {
352          dest.write_str("center")
353        }
354      }
355      Length(lp) => lp.to_css(dest),
356      Side { side, offset } => {
357        side.to_css(dest)?;
358        if let Some(lp) = offset {
359          dest.write_str(" ")?;
360          lp.to_css(dest)?;
361        }
362        Ok(())
363      }
364    }
365  }
366}
367
368enum_property! {
369  /// A horizontal position keyword.
370  pub enum HorizontalPositionKeyword {
371    /// The `left` keyword.
372    Left,
373    /// The `right` keyword.
374    Right,
375  }
376}
377
378impl Into<LengthPercentage> for HorizontalPositionKeyword {
379  fn into(self) -> LengthPercentage {
380    match self {
381      HorizontalPositionKeyword::Left => LengthPercentage::zero(),
382      HorizontalPositionKeyword::Right => LengthPercentage::Percentage(Percentage(1.0)),
383    }
384  }
385}
386
387enum_property! {
388  /// A vertical position keyword.
389  pub enum VerticalPositionKeyword {
390    /// The `top` keyword.
391    Top,
392    /// The `bottom` keyword.
393    Bottom,
394  }
395}
396
397impl Into<LengthPercentage> for VerticalPositionKeyword {
398  fn into(self) -> LengthPercentage {
399    match self {
400      VerticalPositionKeyword::Top => LengthPercentage::zero(),
401      VerticalPositionKeyword::Bottom => LengthPercentage::Percentage(Percentage(1.0)),
402    }
403  }
404}
405
406/// A horizontal position component.
407pub type HorizontalPosition = PositionComponent<HorizontalPositionKeyword>;
408
409/// A vertical position component.
410pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>;