Skip to main content

takumi_css/style/properties/
background_position.rs

1use crate::style::{ToCss, unexpected_token};
2use cssparser::{Parser, Token, match_ignore_ascii_case};
3use std::fmt;
4use taffy::{Point, Size};
5
6use super::background_image::parse_comma_list;
7use crate::style::{
8  Animatable, Color, CssSyntaxKind, CssToken, FromCss, Length, ListInterpolationStrategy,
9  MakeComputed, ParseResult, SizingContext, SpacePair, tw::TailwindPropertyParser,
10};
11
12/// Horizontal keywords for `background-position`.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum PositionKeywordX {
15  /// Align to the left edge.
16  Left,
17  /// Align to the horizontal center.
18  Center,
19  /// Align to the right edge.
20  Right,
21}
22
23/// Vertical keywords for `background-position`.
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum PositionKeywordY {
26  /// Align to the top edge.
27  Top,
28  /// Align to the vertical center.
29  Center,
30  /// Align to the bottom edge.
31  Bottom,
32}
33
34/// A single `background-position` component for an axis.
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum PositionComponent {
37  /// A horizontal keyword.
38  KeywordX(PositionKeywordX),
39  /// A vertical keyword.
40  KeywordY(PositionKeywordY),
41  /// An absolute length value.
42  Length(Length),
43}
44
45impl MakeComputed for PositionComponent {
46  fn make_computed(&mut self, sizing: &SizingContext) {
47    if let Self::Length(length) = self {
48      length.make_computed(sizing);
49    }
50  }
51}
52
53impl Animatable for PositionComponent {
54  fn interpolate(
55    &mut self,
56    from: &Self,
57    to: &Self,
58    progress: f32,
59    sizing: &SizingContext,
60    current_color: Color,
61  ) {
62    let mut length = Length::from(*from);
63    length.interpolate(
64      &Length::from(*from),
65      &Length::from(*to),
66      progress,
67      sizing,
68      current_color,
69    );
70    *self = PositionComponent::Length(length);
71  }
72}
73
74impl From<Length> for PositionComponent {
75  fn from(value: Length) -> Self {
76    PositionComponent::Length(value)
77  }
78}
79
80impl From<PositionComponent> for Length {
81  fn from(component: PositionComponent) -> Self {
82    match component {
83      PositionComponent::KeywordX(keyword) => match keyword {
84        PositionKeywordX::Center => Self::Percentage(50.0),
85        PositionKeywordX::Left => Self::Percentage(0.0),
86        PositionKeywordX::Right => Self::Percentage(100.0),
87      },
88      PositionComponent::KeywordY(keyword) => match keyword {
89        PositionKeywordY::Center => Self::Percentage(50.0),
90        PositionKeywordY::Top => Self::Percentage(0.0),
91        PositionKeywordY::Bottom => Self::Percentage(100.0),
92      },
93      PositionComponent::Length(length) => length,
94    }
95  }
96}
97
98/// Parsed position value for one layer-like CSS property.
99#[derive(Debug, Clone, Copy, PartialEq)]
100pub struct BackgroundPosition<const DEFAULT_TOP_LEFT: bool = true>(
101  pub SpacePair<PositionComponent>,
102);
103
104/// `object-position` value with a CSS initial value of `center center`.
105pub type ObjectPosition = BackgroundPosition<false>;
106/// `transform-origin` value with a CSS initial value of `center center`.
107pub type TransformOrigin = BackgroundPosition<false>;
108
109impl<const DEFAULT_TOP_LEFT: bool> MakeComputed for BackgroundPosition<DEFAULT_TOP_LEFT> {
110  fn make_computed(&mut self, sizing: &SizingContext) {
111    self.0.make_computed(sizing);
112  }
113}
114
115impl<const DEFAULT_TOP_LEFT: bool> Animatable for BackgroundPosition<DEFAULT_TOP_LEFT> {
116  fn list_interpolation_strategy() -> ListInterpolationStrategy {
117    ListInterpolationStrategy::RepeatToLcm
118  }
119
120  fn interpolate(
121    &mut self,
122    from: &Self,
123    to: &Self,
124    progress: f32,
125    sizing: &SizingContext,
126    current_color: Color,
127  ) {
128    let mut value = from.0;
129    value.interpolate(&from.0, &to.0, progress, sizing, current_color);
130    self.0 = value;
131  }
132}
133
134impl<const DEFAULT_TOP_LEFT: bool> BackgroundPosition<DEFAULT_TOP_LEFT> {
135  pub fn to_point(self, sizing: &SizingContext, border_box: Size<f32>) -> Point<f32> {
136    Point {
137      x: Length::from(self.0.x).to_px(sizing, border_box.width),
138      y: Length::from(self.0.y).to_px(sizing, border_box.height),
139    }
140  }
141}
142
143impl<const DEFAULT_TOP_LEFT: bool> TailwindPropertyParser for BackgroundPosition<DEFAULT_TOP_LEFT> {
144  fn parse_tw(token: &str) -> Option<Self> {
145    match token {
146      "top-left" => Some(Self(SpacePair::from_pair(
147        PositionComponent::KeywordX(PositionKeywordX::Left),
148        PositionComponent::KeywordY(PositionKeywordY::Top),
149      ))),
150      "top" => Some(Self(SpacePair::from_pair(
151        PositionComponent::KeywordX(PositionKeywordX::Center),
152        PositionComponent::KeywordY(PositionKeywordY::Top),
153      ))),
154      "top-right" => Some(Self(SpacePair::from_pair(
155        PositionComponent::KeywordX(PositionKeywordX::Right),
156        PositionComponent::KeywordY(PositionKeywordY::Top),
157      ))),
158      "left" => Some(Self(SpacePair::from_pair(
159        PositionComponent::KeywordX(PositionKeywordX::Left),
160        PositionComponent::KeywordY(PositionKeywordY::Center),
161      ))),
162      "center" => Some(Self(SpacePair::from_pair(
163        PositionComponent::KeywordX(PositionKeywordX::Center),
164        PositionComponent::KeywordY(PositionKeywordY::Center),
165      ))),
166      "right" => Some(Self(SpacePair::from_pair(
167        PositionComponent::KeywordX(PositionKeywordX::Right),
168        PositionComponent::KeywordY(PositionKeywordY::Center),
169      ))),
170      "bottom-left" => Some(Self(SpacePair::from_pair(
171        PositionComponent::KeywordX(PositionKeywordX::Left),
172        PositionComponent::KeywordY(PositionKeywordY::Bottom),
173      ))),
174      "bottom" => Some(Self(SpacePair::from_pair(
175        PositionComponent::KeywordX(PositionKeywordX::Center),
176        PositionComponent::KeywordY(PositionKeywordY::Bottom),
177      ))),
178      "bottom-right" => Some(Self(SpacePair::from_pair(
179        PositionComponent::KeywordX(PositionKeywordX::Right),
180        PositionComponent::KeywordY(PositionKeywordY::Bottom),
181      ))),
182      _ => None,
183    }
184  }
185}
186
187impl<const DEFAULT_TOP_LEFT: bool> Default for BackgroundPosition<DEFAULT_TOP_LEFT> {
188  fn default() -> Self {
189    if DEFAULT_TOP_LEFT {
190      Self(SpacePair::from_pair(
191        PositionComponent::KeywordX(PositionKeywordX::Left),
192        PositionComponent::KeywordY(PositionKeywordY::Top),
193      ))
194    } else {
195      Self(SpacePair::from_pair(
196        PositionComponent::KeywordX(PositionKeywordX::Center),
197        PositionComponent::KeywordY(PositionKeywordY::Center),
198      ))
199    }
200  }
201}
202
203impl<'i, const DEFAULT_TOP_LEFT: bool> FromCss<'i> for BackgroundPosition<DEFAULT_TOP_LEFT> {
204  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
205    let first = PositionComponent::from_css(input)?;
206    // If a second exists, parse it; otherwise, 1-value syntax means y=center
207    let second = input.try_parse(PositionComponent::from_css).ok();
208
209    let (x, y) = match (first, second) {
210      (PositionComponent::KeywordY(_), None) => {
211        (PositionComponent::KeywordX(PositionKeywordX::Center), first)
212      }
213      (PositionComponent::KeywordY(_), Some(second)) => (second, first),
214      (x, None) => (x, PositionComponent::KeywordY(PositionKeywordY::Center)),
215      (x, Some(y)) => (x, y),
216    };
217
218    Ok(BackgroundPosition(SpacePair::from_pair(x, y)))
219  }
220
221  const VALID_TOKENS: &'static [CssToken] = PositionComponent::VALID_TOKENS;
222}
223
224impl<'i> FromCss<'i> for PositionComponent {
225  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
226    if let Ok(v) = input.try_parse(Length::from_css) {
227      return Ok(v.into());
228    }
229
230    let location = input.current_source_location();
231    let token = input.next()?;
232    let Token::Ident(ident) = token else {
233      return Err(unexpected_token!(location, token));
234    };
235
236    match_ignore_ascii_case! {
237      &ident,
238      "left" => Ok(PositionComponent::KeywordX(PositionKeywordX::Left)),
239      "center" => Ok(PositionComponent::KeywordX(PositionKeywordX::Center)),
240      "right" => Ok(PositionComponent::KeywordX(PositionKeywordX::Right)),
241      "top" => Ok(PositionComponent::KeywordY(PositionKeywordY::Top)),
242      "bottom" => Ok(PositionComponent::KeywordY(PositionKeywordY::Bottom)),
243      _ => Err(unexpected_token!(location, token)),
244    }
245  }
246
247  const VALID_TOKENS: &'static [CssToken] = &[
248    CssToken::Keyword("left"),
249    CssToken::Keyword("center"),
250    CssToken::Keyword("right"),
251    CssToken::Keyword("top"),
252    CssToken::Keyword("bottom"),
253    CssToken::Syntax(CssSyntaxKind::Length),
254  ];
255}
256
257/// A list of `background-position` values (one per layer).
258pub type BackgroundPositions = Box<[BackgroundPosition]>;
259
260impl<'i> FromCss<'i> for BackgroundPositions {
261  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
262    parse_comma_list(input, BackgroundPosition::from_css)
263  }
264
265  const VALID_TOKENS: &'static [CssToken] = BackgroundPosition::<true>::VALID_TOKENS;
266}
267
268impl ToCss for PositionKeywordX {
269  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
270    match self {
271      Self::Left => dest.write_str("left"),
272      Self::Center => dest.write_str("center"),
273      Self::Right => dest.write_str("right"),
274    }
275  }
276}
277
278impl ToCss for PositionKeywordY {
279  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
280    match self {
281      Self::Top => dest.write_str("top"),
282      Self::Center => dest.write_str("center"),
283      Self::Bottom => dest.write_str("bottom"),
284    }
285  }
286}
287
288impl ToCss for PositionComponent {
289  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
290    match self {
291      Self::KeywordX(k) => k.to_css(dest),
292      Self::KeywordY(k) => k.to_css(dest),
293      Self::Length(l) => l.to_css(dest),
294    }
295  }
296}
297
298impl<const DEFAULT_TOP_LEFT: bool> ToCss for BackgroundPosition<DEFAULT_TOP_LEFT> {
299  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
300    self.0.to_css(dest)
301  }
302}