Skip to main content

azul_css/props/layout/
position.rs

1//! CSS properties for positioning elements: `position`, `top`, `right`,
2//! `bottom`, `left`, and `z-index`. Types defined here are consumed by the
3//! layout solver to resolve positioned elements.
4
5use alloc::string::{String, ToString};
6use crate::corety::AzString;
7
8#[cfg(feature = "parser")]
9use crate::props::basic::pixel::parse_pixel_value;
10use crate::props::{
11    basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
12    formatter::PrintAsCssValue,
13    macros::PixelValueTaker,
14};
15
16// --- LayoutPosition ---
17
18/// Represents a `position` attribute - default: `Static`
19#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(C)]
21#[derive(Default)]
22pub enum LayoutPosition {
23    #[default]
24    Static,
25    Relative,
26    Absolute,
27    Fixed,
28    Sticky,
29}
30
31impl LayoutPosition {
32    #[must_use] pub fn is_positioned(&self) -> bool {
33        *self != Self::Static
34    }
35}
36
37
38impl PrintAsCssValue for LayoutPosition {
39    fn print_as_css_value(&self) -> String {
40        String::from(match self {
41            Self::Static => "static",
42            Self::Relative => "relative",
43            Self::Absolute => "absolute",
44            Self::Fixed => "fixed",
45            Self::Sticky => "sticky",
46        })
47    }
48}
49
50impl_enum_fmt!(LayoutPosition, Static, Fixed, Absolute, Relative, Sticky);
51
52// -- Parser for LayoutPosition
53
54#[derive(Clone, PartialEq, Eq)]
55pub enum LayoutPositionParseError<'a> {
56    InvalidValue(&'a str),
57}
58
59impl_debug_as_display!(LayoutPositionParseError<'a>);
60impl_display! { LayoutPositionParseError<'a>, {
61    InvalidValue(val) => format!("Invalid position value: \"{}\"", val),
62}}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65#[repr(C, u8)]
66pub enum LayoutPositionParseErrorOwned {
67    InvalidValue(AzString),
68}
69
70impl LayoutPositionParseError<'_> {
71    #[must_use] pub fn to_contained(&self) -> LayoutPositionParseErrorOwned {
72        match self {
73            LayoutPositionParseError::InvalidValue(s) => {
74                LayoutPositionParseErrorOwned::InvalidValue((*s).to_string().into())
75            }
76        }
77    }
78}
79
80impl LayoutPositionParseErrorOwned {
81    #[must_use] pub fn to_shared(&self) -> LayoutPositionParseError<'_> {
82        match self {
83            Self::InvalidValue(s) => {
84                LayoutPositionParseError::InvalidValue(s.as_str())
85            }
86        }
87    }
88}
89
90#[cfg(feature = "parser")]
91/// # Errors
92///
93/// Returns an error if `input` is not a valid CSS `position` value.
94pub fn parse_layout_position(
95    input: &str,
96) -> Result<LayoutPosition, LayoutPositionParseError<'_>> {
97    let input = input.trim();
98    match input {
99        "static" => Ok(LayoutPosition::Static),
100        "relative" => Ok(LayoutPosition::Relative),
101        "absolute" => Ok(LayoutPosition::Absolute),
102        "fixed" => Ok(LayoutPosition::Fixed),
103        "sticky" => Ok(LayoutPosition::Sticky),
104        _ => Err(LayoutPositionParseError::InvalidValue(input)),
105    }
106}
107
108// --- Offset Properties (top, right, bottom, left) ---
109
110macro_rules! define_position_property {
111    ($struct_name:ident) => {
112        #[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
113        #[repr(C)]
114        pub struct $struct_name {
115            pub inner: PixelValue,
116        }
117
118        impl ::core::fmt::Debug for $struct_name {
119            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
120                write!(f, "{}", self.inner)
121            }
122        }
123
124        impl PixelValueTaker for $struct_name {
125            fn from_pixel_value(inner: PixelValue) -> Self {
126                Self { inner }
127            }
128        }
129
130        impl_pixel_value!($struct_name);
131
132        impl PrintAsCssValue for $struct_name {
133            fn print_as_css_value(&self) -> String {
134                format!("{}", self.inner)
135            }
136        }
137    };
138}
139
140/// Represents the CSS `top` offset property for positioned elements.
141define_position_property!(LayoutTop);
142/// Represents the CSS `right` offset property for positioned elements.
143define_position_property!(LayoutRight);
144/// Represents the CSS `bottom` offset property for positioned elements.
145define_position_property!(LayoutInsetBottom);
146/// Represents the CSS `left` offset property for positioned elements.
147define_position_property!(LayoutLeft);
148
149// -- Parse error types and parsers for offset properties (top, right, bottom, left)
150
151macro_rules! define_offset_parse_error {
152    ($struct_name:ident, $error_name:ident, $error_owned_name:ident, $parse_fn:ident) => {
153        #[derive(Clone, PartialEq, Eq)]
154        pub enum $error_name<'a> {
155            PixelValue(CssPixelValueParseError<'a>),
156        }
157        impl_debug_as_display!($error_name<'a>);
158        impl_display! { $error_name<'a>, { PixelValue(e) => format!("{}", e), }}
159        impl_from!(CssPixelValueParseError<'a>, $error_name::PixelValue);
160
161        #[derive(Debug, Clone, PartialEq, Eq)]
162        #[repr(C, u8)]
163        pub enum $error_owned_name {
164            PixelValue(CssPixelValueParseErrorOwned),
165        }
166        impl $error_name<'_> {
167            #[must_use] pub fn to_contained(&self) -> $error_owned_name {
168                match self {
169                    $error_name::PixelValue(e) => {
170                        $error_owned_name::PixelValue(e.to_contained())
171                    }
172                }
173            }
174        }
175        impl $error_owned_name {
176            #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
177                match self {
178                    $error_owned_name::PixelValue(e) => {
179                        $error_name::PixelValue(e.to_shared())
180                    }
181                }
182            }
183        }
184
185        #[cfg(feature = "parser")]
186        /// # Errors
187        ///
188        /// Returns an error if `input` is not a valid CSS value for this property.
189        pub fn $parse_fn(input: &str) -> Result<$struct_name, $error_name<'_>> {
190            parse_pixel_value(input)
191                .map(|v| $struct_name { inner: v })
192                .map_err(Into::into)
193        }
194    };
195}
196
197define_offset_parse_error!(LayoutTop, LayoutTopParseError, LayoutTopParseErrorOwned, parse_layout_top);
198define_offset_parse_error!(LayoutRight, LayoutRightParseError, LayoutRightParseErrorOwned, parse_layout_right);
199define_offset_parse_error!(LayoutInsetBottom, LayoutInsetBottomParseError, LayoutInsetBottomParseErrorOwned, parse_layout_bottom);
200define_offset_parse_error!(LayoutLeft, LayoutLeftParseError, LayoutLeftParseErrorOwned, parse_layout_left);
201
202// --- LayoutZIndex ---
203
204/// Represents a `z-index` attribute - controls stacking order of positioned elements
205#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
206#[repr(C, u8)]
207#[derive(Default)]
208pub enum LayoutZIndex {
209    #[default]
210    Auto,
211    Integer(i32),
212}
213
214// Formatting to Rust code
215impl crate::codegen::format::FormatAsRustCode for LayoutZIndex {
216    fn format_as_rust_code(&self, _tabs: usize) -> String {
217        match self {
218            Self::Auto => String::from("LayoutZIndex::Auto"),
219            Self::Integer(val) => {
220                format!("LayoutZIndex::Integer({val})")
221            }
222        }
223    }
224}
225
226
227impl PrintAsCssValue for LayoutZIndex {
228    fn print_as_css_value(&self) -> String {
229        match self {
230            Self::Auto => String::from("auto"),
231            Self::Integer(val) => val.to_string(),
232        }
233    }
234}
235
236// -- Parser for LayoutZIndex
237
238#[derive(Clone, PartialEq, Eq)]
239pub enum LayoutZIndexParseError<'a> {
240    InvalidValue(&'a str),
241    ParseInt(::core::num::ParseIntError, &'a str),
242}
243impl_debug_as_display!(LayoutZIndexParseError<'a>);
244impl_display! { LayoutZIndexParseError<'a>, {
245    InvalidValue(val) => format!("Invalid z-index value: \"{}\"", val),
246    ParseInt(e, s) => format!("Invalid z-index integer \"{}\": {}", s, e),
247}}
248
249/// Wrapper for `ParseIntError` that stores the error message and original
250/// input as owned strings for FFI compatibility.
251#[derive(Debug, Clone, PartialEq, Eq)]
252#[repr(C)]
253pub struct ParseIntErrorWithInput {
254    /// The stringified parse error (e.g. "invalid digit found in string").
255    pub error: AzString,
256    /// The original input string that failed to parse.
257    pub input: AzString,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
261#[repr(C, u8)]
262pub enum LayoutZIndexParseErrorOwned {
263    InvalidValue(AzString),
264    ParseInt(ParseIntErrorWithInput),
265}
266
267impl LayoutZIndexParseError<'_> {
268    #[must_use] pub fn to_contained(&self) -> LayoutZIndexParseErrorOwned {
269        match self {
270            LayoutZIndexParseError::InvalidValue(s) => {
271                LayoutZIndexParseErrorOwned::InvalidValue((*s).to_string().into())
272            }
273            LayoutZIndexParseError::ParseInt(e, s) => {
274                LayoutZIndexParseErrorOwned::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: (*s).to_string().into() })
275            }
276        }
277    }
278}
279
280impl LayoutZIndexParseErrorOwned {
281    /// Converts back to the borrowed error type.
282    ///
283    /// **Note:** This conversion is lossy for `ParseInt` — the original
284    /// `core::num::ParseIntError` cannot be reconstructed from its string
285    /// representation, so `ParseInt` is mapped to `InvalidValue` instead.
286    #[must_use] pub fn to_shared(&self) -> LayoutZIndexParseError<'_> {
287        match self {
288            Self::InvalidValue(s) => {
289                LayoutZIndexParseError::InvalidValue(s.as_str())
290            }
291            Self::ParseInt(e) => {
292                // We can't reconstruct ParseIntError, so use InvalidValue
293                LayoutZIndexParseError::InvalidValue(e.input.as_str())
294            }
295        }
296    }
297}
298
299#[cfg(feature = "parser")]
300/// # Errors
301///
302/// Returns an error if `input` is not a valid CSS `z-index` value.
303pub fn parse_layout_z_index(
304    input: &str,
305) -> Result<LayoutZIndex, LayoutZIndexParseError<'_>> {
306    let input = input.trim();
307    if input == "auto" {
308        return Ok(LayoutZIndex::Auto);
309    }
310
311    match input.parse::<i32>() {
312        Ok(val) => Ok(LayoutZIndex::Integer(val)),
313        Err(e) => Err(LayoutZIndexParseError::ParseInt(e, input)),
314    }
315}
316
317#[cfg(all(test, feature = "parser"))]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_parse_layout_position() {
323        assert_eq!(
324            parse_layout_position("static").unwrap(),
325            LayoutPosition::Static
326        );
327        assert_eq!(
328            parse_layout_position("relative").unwrap(),
329            LayoutPosition::Relative
330        );
331        assert_eq!(
332            parse_layout_position("absolute").unwrap(),
333            LayoutPosition::Absolute
334        );
335        assert_eq!(
336            parse_layout_position("fixed").unwrap(),
337            LayoutPosition::Fixed
338        );
339        assert_eq!(
340            parse_layout_position("sticky").unwrap(),
341            LayoutPosition::Sticky
342        );
343    }
344
345    #[test]
346    fn test_parse_layout_position_whitespace() {
347        assert_eq!(
348            parse_layout_position("  absolute  ").unwrap(),
349            LayoutPosition::Absolute
350        );
351    }
352
353    #[test]
354    fn test_parse_layout_position_invalid() {
355        assert!(parse_layout_position("").is_err());
356        assert!(parse_layout_position("absolutely").is_err());
357    }
358
359    #[test]
360    fn test_parse_layout_z_index() {
361        assert_eq!(parse_layout_z_index("auto").unwrap(), LayoutZIndex::Auto);
362        assert_eq!(
363            parse_layout_z_index("10").unwrap(),
364            LayoutZIndex::Integer(10)
365        );
366        assert_eq!(parse_layout_z_index("0").unwrap(), LayoutZIndex::Integer(0));
367        assert_eq!(
368            parse_layout_z_index("-5").unwrap(),
369            LayoutZIndex::Integer(-5)
370        );
371        assert_eq!(
372            parse_layout_z_index("  999  ").unwrap(),
373            LayoutZIndex::Integer(999)
374        );
375    }
376
377    #[test]
378    fn test_parse_layout_z_index_invalid() {
379        assert!(parse_layout_z_index("10px").is_err());
380        assert!(parse_layout_z_index("1.5").is_err());
381        assert!(parse_layout_z_index("none").is_err());
382        assert!(parse_layout_z_index("").is_err());
383    }
384
385    #[test]
386    fn test_parse_offsets() {
387        assert_eq!(
388            parse_layout_top("10px").unwrap(),
389            LayoutTop {
390                inner: PixelValue::px(10.0)
391            }
392        );
393        assert_eq!(
394            parse_layout_right("5%").unwrap(),
395            LayoutRight {
396                inner: PixelValue::percent(5.0)
397            }
398        );
399        assert_eq!(
400            parse_layout_bottom("2.5em").unwrap(),
401            LayoutInsetBottom {
402                inner: PixelValue::em(2.5)
403            }
404        );
405        assert_eq!(
406            parse_layout_left("0").unwrap(),
407            LayoutLeft {
408                inner: PixelValue::px(0.0)
409            }
410        );
411    }
412
413    #[test]
414    fn test_parse_offsets_invalid() {
415        // The simple `parse_pixel_value` does not handle `auto`.
416        assert!(parse_layout_top("auto").is_err());
417        assert!(parse_layout_right("").is_err());
418        // Liberal parsing accepts whitespace between number and unit
419        assert!(parse_layout_bottom("10 px").is_ok());
420        assert!(parse_layout_left("ten pixels").is_err());
421    }
422}