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