Skip to main content

azul_css/props/layout/
shape.rs

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