Skip to main content

azul_css/props/layout/
shape.rs

1//! CSS properties for flowing content around shapes (CSS Shapes Module).
2
3use alloc::string::{String, ToString};
4
5use crate::{
6    props::{
7        basic::{
8            length::{parse_float_value, FloatValue},
9            pixel::{
10                parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned,
11                PixelValue,
12            },
13        },
14        formatter::PrintAsCssValue,
15    },
16    shape::CssShape,
17};
18
19/// CSS shape-outside property for wrapping text around shapes
20#[derive(Debug, Clone, PartialEq)]
21#[repr(C, u8)]
22pub enum ShapeOutside {
23    None,
24    Shape(CssShape),
25}
26
27impl Eq for ShapeOutside {}
28impl core::hash::Hash for ShapeOutside {
29    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
30        core::mem::discriminant(self).hash(state);
31        if let ShapeOutside::Shape(s) = self {
32            s.hash(state);
33        }
34    }
35}
36impl PartialOrd for ShapeOutside {
37    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
38        Some(self.cmp(other))
39    }
40}
41impl Ord for ShapeOutside {
42    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
43        match (self, other) {
44            (ShapeOutside::None, ShapeOutside::None) => core::cmp::Ordering::Equal,
45            (ShapeOutside::None, ShapeOutside::Shape(_)) => core::cmp::Ordering::Less,
46            (ShapeOutside::Shape(_), ShapeOutside::None) => core::cmp::Ordering::Greater,
47            (ShapeOutside::Shape(a), ShapeOutside::Shape(b)) => a.cmp(b),
48        }
49    }
50}
51
52impl Default for ShapeOutside {
53    fn default() -> Self {
54        Self::None
55    }
56}
57
58impl PrintAsCssValue for ShapeOutside {
59    fn print_as_css_value(&self) -> String {
60        match self {
61            Self::None => "none".to_string(),
62            Self::Shape(shape) => format!("{:?}", shape), // TODO: Proper CSS formatting
63        }
64    }
65}
66
67/// CSS shape-inside property for flowing text within shapes
68#[derive(Debug, Clone, PartialEq)]
69#[repr(C, u8)]
70pub enum ShapeInside {
71    None,
72    Shape(CssShape),
73}
74
75impl Eq for ShapeInside {}
76impl core::hash::Hash for ShapeInside {
77    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
78        core::mem::discriminant(self).hash(state);
79        if let ShapeInside::Shape(s) = self {
80            s.hash(state);
81        }
82    }
83}
84impl PartialOrd for ShapeInside {
85    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
86        Some(self.cmp(other))
87    }
88}
89impl Ord for ShapeInside {
90    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
91        match (self, other) {
92            (ShapeInside::None, ShapeInside::None) => core::cmp::Ordering::Equal,
93            (ShapeInside::None, ShapeInside::Shape(_)) => core::cmp::Ordering::Less,
94            (ShapeInside::Shape(_), ShapeInside::None) => core::cmp::Ordering::Greater,
95            (ShapeInside::Shape(a), ShapeInside::Shape(b)) => a.cmp(b),
96        }
97    }
98}
99
100impl Default for ShapeInside {
101    fn default() -> Self {
102        Self::None
103    }
104}
105
106impl PrintAsCssValue for ShapeInside {
107    fn print_as_css_value(&self) -> String {
108        match self {
109            Self::None => "none".to_string(),
110            Self::Shape(shape) => format!("{:?}", shape), // TODO: Proper CSS formatting
111        }
112    }
113}
114
115/// CSS clip-path property for clipping element rendering
116#[derive(Debug, Clone, PartialEq)]
117#[repr(C, u8)]
118pub enum ClipPath {
119    None,
120    Shape(CssShape),
121}
122
123impl Eq for ClipPath {}
124impl core::hash::Hash for ClipPath {
125    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
126        core::mem::discriminant(self).hash(state);
127        if let ClipPath::Shape(s) = self {
128            s.hash(state);
129        }
130    }
131}
132impl PartialOrd for ClipPath {
133    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
134        Some(self.cmp(other))
135    }
136}
137impl Ord for ClipPath {
138    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
139        match (self, other) {
140            (ClipPath::None, ClipPath::None) => core::cmp::Ordering::Equal,
141            (ClipPath::None, ClipPath::Shape(_)) => core::cmp::Ordering::Less,
142            (ClipPath::Shape(_), ClipPath::None) => core::cmp::Ordering::Greater,
143            (ClipPath::Shape(a), ClipPath::Shape(b)) => a.cmp(b),
144        }
145    }
146}
147
148impl Default for ClipPath {
149    fn default() -> Self {
150        Self::None
151    }
152}
153
154impl PrintAsCssValue for ClipPath {
155    fn print_as_css_value(&self) -> String {
156        match self {
157            Self::None => "none".to_string(),
158            Self::Shape(shape) => format!("{:?}", shape), // TODO: Proper CSS formatting
159        }
160    }
161}
162
163#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
164#[repr(C)]
165pub struct ShapeMargin {
166    pub inner: PixelValue,
167}
168
169impl Default for ShapeMargin {
170    fn default() -> Self {
171        Self {
172            inner: PixelValue::zero(),
173        }
174    }
175}
176
177impl PrintAsCssValue for ShapeMargin {
178    fn print_as_css_value(&self) -> String {
179        self.inner.print_as_css_value()
180    }
181}
182
183#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
184#[repr(C)]
185pub struct ShapeImageThreshold {
186    pub inner: FloatValue,
187}
188
189impl Default for ShapeImageThreshold {
190    fn default() -> Self {
191        Self {
192            inner: FloatValue::const_new(0),
193        }
194    }
195}
196
197impl PrintAsCssValue for ShapeImageThreshold {
198    fn print_as_css_value(&self) -> String {
199        self.inner.to_string()
200    }
201}
202
203// Formatting to Rust code
204impl crate::format_rust_code::FormatAsRustCode for ShapeOutside {
205    fn format_as_rust_code(&self, _tabs: usize) -> String {
206        match self {
207            ShapeOutside::None => String::from("ShapeOutside::None"),
208            ShapeOutside::Shape(_s) => String::from("ShapeOutside::Shape(/* ... */)"), // TODO
209        }
210    }
211}
212
213impl crate::format_rust_code::FormatAsRustCode for ShapeInside {
214    fn format_as_rust_code(&self, _tabs: usize) -> String {
215        match self {
216            ShapeInside::None => String::from("ShapeInside::None"),
217            ShapeInside::Shape(_s) => String::from("ShapeInside::Shape(/* ... */)"), // TODO
218        }
219    }
220}
221
222impl crate::format_rust_code::FormatAsRustCode for ClipPath {
223    fn format_as_rust_code(&self, _tabs: usize) -> String {
224        match self {
225            ClipPath::None => String::from("ClipPath::None"),
226            ClipPath::Shape(_s) => String::from("ClipPath::Shape(/* ... */)"), // TODO
227        }
228    }
229}
230
231impl crate::format_rust_code::FormatAsRustCode for ShapeMargin {
232    fn format_as_rust_code(&self, _tabs: usize) -> String {
233        format!(
234            "ShapeMargin {{ inner: {} }}",
235            crate::format_rust_code::format_pixel_value(&self.inner)
236        )
237    }
238}
239
240impl crate::format_rust_code::FormatAsRustCode for ShapeImageThreshold {
241    fn format_as_rust_code(&self, _tabs: usize) -> String {
242        format!(
243            "ShapeImageThreshold {{ inner: {} }}",
244            crate::format_rust_code::format_float_value(&self.inner)
245        )
246    }
247}
248
249// --- PARSERS ---
250#[cfg(feature = "parser")]
251mod parser {
252    use core::num::ParseFloatError;
253
254    use super::*;
255    use crate::shape_parser::{parse_shape, ShapeParseError};
256
257    /// Parser for shape-outside property
258    pub fn parse_shape_outside(input: &str) -> Result<ShapeOutside, ShapeParseError> {
259        let trimmed = input.trim();
260        if trimmed == "none" {
261            Ok(ShapeOutside::None)
262        } else {
263            let shape = parse_shape(trimmed)?;
264            Ok(ShapeOutside::Shape(shape))
265        }
266    }
267
268    /// Parser for shape-inside property
269    pub fn parse_shape_inside(input: &str) -> Result<ShapeInside, ShapeParseError> {
270        let trimmed = input.trim();
271        if trimmed == "none" {
272            Ok(ShapeInside::None)
273        } else {
274            let shape = parse_shape(trimmed)?;
275            Ok(ShapeInside::Shape(shape))
276        }
277    }
278
279    /// Parser for clip-path property
280    pub fn parse_clip_path(input: &str) -> Result<ClipPath, ShapeParseError> {
281        let trimmed = input.trim();
282        if trimmed == "none" {
283            Ok(ClipPath::None)
284        } else {
285            let shape = parse_shape(trimmed)?;
286            Ok(ClipPath::Shape(shape))
287        }
288    }
289
290    // Parsers for margin and threshold
291    pub fn parse_shape_margin(input: &str) -> Result<ShapeMargin, CssPixelValueParseError> {
292        Ok(ShapeMargin {
293            inner: parse_pixel_value(input)?,
294        })
295    }
296
297    pub fn parse_shape_image_threshold(
298        input: &str,
299    ) -> Result<ShapeImageThreshold, ParseFloatError> {
300        let val = parse_float_value(input)?;
301        // value should be clamped between 0.0 and 1.0
302        let clamped = val.get().max(0.0).min(1.0);
303        Ok(ShapeImageThreshold {
304            inner: FloatValue::new(clamped),
305        })
306    }
307}
308
309#[cfg(feature = "parser")]
310pub use parser::*;
311
312#[cfg(all(test, feature = "parser"))]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_parse_shape_properties() {
318        // Test shape-outside
319        assert!(matches!(
320            parse_shape_outside("none").unwrap(),
321            ShapeOutside::None
322        ));
323        assert!(matches!(
324            parse_shape_outside("circle(50px)").unwrap(),
325            ShapeOutside::Shape(_)
326        ));
327
328        // Test shape-inside
329        assert!(matches!(
330            parse_shape_inside("none").unwrap(),
331            ShapeInside::None
332        ));
333        assert!(matches!(
334            parse_shape_inside("circle(100px at 50px 50px)").unwrap(),
335            ShapeInside::Shape(_)
336        ));
337
338        // Test clip-path
339        assert!(matches!(parse_clip_path("none").unwrap(), ClipPath::None));
340        assert!(matches!(
341            parse_clip_path("polygon(0 0, 100px 0, 100px 100px, 0 100px)").unwrap(),
342            ClipPath::Shape(_)
343        ));
344
345        // Test existing properties
346        assert_eq!(
347            parse_shape_margin("10px").unwrap().inner,
348            PixelValue::px(10.0)
349        );
350        assert_eq!(parse_shape_image_threshold("0.5").unwrap().inner.get(), 0.5);
351    }
352}