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

            
5
use alloc::string::{String, ToString};
6
use crate::corety::AzString;
7

            
8
#[cfg(feature = "parser")]
9
use crate::props::basic::pixel::parse_pixel_value;
10
use 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)]
22
pub enum LayoutPosition {
23
    #[default]
24
    Static,
25
    Relative,
26
    Absolute,
27
    Fixed,
28
    Sticky,
29
}
30

            
31
impl LayoutPosition {
32
70
    pub fn is_positioned(&self) -> bool {
33
70
        *self != LayoutPosition::Static
34
70
    }
35
}
36

            
37

            
38
impl 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

            
50
impl_enum_fmt!(LayoutPosition, Static, Fixed, Absolute, Relative, Sticky);
51

            
52
// -- Parser for LayoutPosition
53

            
54
#[derive(Clone, PartialEq)]
55
pub enum LayoutPositionParseError<'a> {
56
    InvalidValue(&'a str),
57
}
58

            
59
impl_debug_as_display!(LayoutPositionParseError<'a>);
60
impl_display! { LayoutPositionParseError<'a>, {
61
    InvalidValue(val) => format!("Invalid position value: \"{}\"", val),
62
}}
63

            
64
#[derive(Debug, Clone, PartialEq)]
65
#[repr(C, u8)]
66
pub enum LayoutPositionParseErrorOwned {
67
    InvalidValue(AzString),
68
}
69

            
70
impl<'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

            
80
impl 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")]
91
168
pub fn parse_layout_position<'a>(
92
168
    input: &'a str,
93
168
) -> Result<LayoutPosition, LayoutPositionParseError<'a>> {
94
168
    let input = input.trim();
95
168
    match input {
96
168
        "static" => Ok(LayoutPosition::Static),
97
167
        "relative" => Ok(LayoutPosition::Relative),
98
105
        "absolute" => Ok(LayoutPosition::Absolute),
99
23
        "fixed" => Ok(LayoutPosition::Fixed),
100
3
        "sticky" => Ok(LayoutPosition::Sticky),
101
2
        _ => Err(LayoutPositionParseError::InvalidValue(input)),
102
    }
103
168
}
104

            
105
// --- Offset Properties (top, right, bottom, left) ---
106

            
107
macro_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.
138
define_position_property!(LayoutTop);
139
/// Represents the CSS `right` offset property for positioned elements.
140
define_position_property!(LayoutRight);
141
/// Represents the CSS `bottom` offset property for positioned elements.
142
define_position_property!(LayoutInsetBottom);
143
/// Represents the CSS `left` offset property for positioned elements.
144
define_position_property!(LayoutLeft);
145

            
146
// -- Parse error types and parsers for offset properties (top, right, bottom, left)
147

            
148
macro_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
187
        pub fn $parse_fn<'a>(input: &'a str) -> Result<$struct_name, $error_name<'a>> {
184
187
            parse_pixel_value(input)
185
187
                .map(|v| $struct_name { inner: v })
186
187
                .map_err(Into::into)
187
187
        }
188
    };
189
}
190

            
191
define_offset_parse_error!(LayoutTop, LayoutTopParseError, LayoutTopParseErrorOwned, parse_layout_top);
192
define_offset_parse_error!(LayoutRight, LayoutRightParseError, LayoutRightParseErrorOwned, parse_layout_right);
193
define_offset_parse_error!(LayoutInsetBottom, LayoutInsetBottomParseError, LayoutInsetBottomParseErrorOwned, parse_layout_bottom);
194
define_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)]
202
pub enum LayoutZIndex {
203
    #[default]
204
    Auto,
205
    Integer(i32),
206
}
207

            
208
// Formatting to Rust code
209
impl 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

            
221
impl 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)]
233
pub enum LayoutZIndexParseError<'a> {
234
    InvalidValue(&'a str),
235
    ParseInt(::core::num::ParseIntError, &'a str),
236
}
237
impl_debug_as_display!(LayoutZIndexParseError<'a>);
238
impl_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)]
247
pub 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)]
256
pub enum LayoutZIndexParseErrorOwned {
257
    InvalidValue(AzString),
258
    ParseInt(ParseIntErrorWithInput),
259
}
260

            
261
impl<'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

            
274
impl 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")]
294
28
pub fn parse_layout_z_index<'a>(
295
28
    input: &'a str,
296
28
) -> Result<LayoutZIndex, LayoutZIndexParseError<'a>> {
297
28
    let input = input.trim();
298
28
    if input == "auto" {
299
1
        return Ok(LayoutZIndex::Auto);
300
27
    }
301

            
302
27
    match input.parse::<i32>() {
303
23
        Ok(val) => Ok(LayoutZIndex::Integer(val)),
304
4
        Err(e) => Err(LayoutZIndexParseError::ParseInt(e, input)),
305
    }
306
28
}
307

            
308
#[cfg(all(test, feature = "parser"))]
309
mod tests {
310
    use super::*;
311

            
312
    #[test]
313
1
    fn test_parse_layout_position() {
314
1
        assert_eq!(
315
1
            parse_layout_position("static").unwrap(),
316
            LayoutPosition::Static
317
        );
318
1
        assert_eq!(
319
1
            parse_layout_position("relative").unwrap(),
320
            LayoutPosition::Relative
321
        );
322
1
        assert_eq!(
323
1
            parse_layout_position("absolute").unwrap(),
324
            LayoutPosition::Absolute
325
        );
326
1
        assert_eq!(
327
1
            parse_layout_position("fixed").unwrap(),
328
            LayoutPosition::Fixed
329
        );
330
1
        assert_eq!(
331
1
            parse_layout_position("sticky").unwrap(),
332
            LayoutPosition::Sticky
333
        );
334
1
    }
335

            
336
    #[test]
337
1
    fn test_parse_layout_position_whitespace() {
338
1
        assert_eq!(
339
1
            parse_layout_position("  absolute  ").unwrap(),
340
            LayoutPosition::Absolute
341
        );
342
1
    }
343

            
344
    #[test]
345
1
    fn test_parse_layout_position_invalid() {
346
1
        assert!(parse_layout_position("").is_err());
347
1
        assert!(parse_layout_position("absolutely").is_err());
348
1
    }
349

            
350
    #[test]
351
1
    fn test_parse_layout_z_index() {
352
1
        assert_eq!(parse_layout_z_index("auto").unwrap(), LayoutZIndex::Auto);
353
1
        assert_eq!(
354
1
            parse_layout_z_index("10").unwrap(),
355
            LayoutZIndex::Integer(10)
356
        );
357
1
        assert_eq!(parse_layout_z_index("0").unwrap(), LayoutZIndex::Integer(0));
358
1
        assert_eq!(
359
1
            parse_layout_z_index("-5").unwrap(),
360
            LayoutZIndex::Integer(-5)
361
        );
362
1
        assert_eq!(
363
1
            parse_layout_z_index("  999  ").unwrap(),
364
            LayoutZIndex::Integer(999)
365
        );
366
1
    }
367

            
368
    #[test]
369
1
    fn test_parse_layout_z_index_invalid() {
370
1
        assert!(parse_layout_z_index("10px").is_err());
371
1
        assert!(parse_layout_z_index("1.5").is_err());
372
1
        assert!(parse_layout_z_index("none").is_err());
373
1
        assert!(parse_layout_z_index("").is_err());
374
1
    }
375

            
376
    #[test]
377
1
    fn test_parse_offsets() {
378
1
        assert_eq!(
379
1
            parse_layout_top("10px").unwrap(),
380
1
            LayoutTop {
381
1
                inner: PixelValue::px(10.0)
382
1
            }
383
        );
384
1
        assert_eq!(
385
1
            parse_layout_right("5%").unwrap(),
386
1
            LayoutRight {
387
1
                inner: PixelValue::percent(5.0)
388
1
            }
389
        );
390
1
        assert_eq!(
391
1
            parse_layout_bottom("2.5em").unwrap(),
392
1
            LayoutInsetBottom {
393
1
                inner: PixelValue::em(2.5)
394
1
            }
395
        );
396
1
        assert_eq!(
397
1
            parse_layout_left("0").unwrap(),
398
1
            LayoutLeft {
399
1
                inner: PixelValue::px(0.0)
400
1
            }
401
        );
402
1
    }
403

            
404
    #[test]
405
1
    fn test_parse_offsets_invalid() {
406
        // The simple `parse_pixel_value` does not handle `auto`.
407
1
        assert!(parse_layout_top("auto").is_err());
408
1
        assert!(parse_layout_right("").is_err());
409
        // Liberal parsing accepts whitespace between number and unit
410
1
        assert!(parse_layout_bottom("10 px").is_ok());
411
1
        assert!(parse_layout_left("ten pixels").is_err());
412
1
    }
413
}