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#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
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 Self::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            (Self::None, Self::None) => core::cmp::Ordering::Equal,
51            (Self::None, Self::Shape(_)) => core::cmp::Ordering::Less,
52            (Self::Shape(_), Self::None) => core::cmp::Ordering::Greater,
53            (Self::Shape(a), Self::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#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
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 Self::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            (Self::None, Self::None) => core::cmp::Ordering::Equal,
96            (Self::None, Self::Shape(_)) => core::cmp::Ordering::Less,
97            (Self::Shape(_), Self::None) => core::cmp::Ordering::Greater,
98            (Self::Shape(a), Self::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#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
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 Self::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            (Self::None, Self::None) => core::cmp::Ordering::Equal,
141            (Self::None, Self::Shape(_)) => core::cmp::Ordering::Less,
142            (Self::Shape(_), Self::None) => core::cmp::Ordering::Greater,
143            (Self::Shape(a), Self::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::codegen::format::FormatAsRustCode for ShapeOutside {
202    fn format_as_rust_code(&self, _tabs: usize) -> String {
203        match self {
204            Self::None => String::from("ShapeOutside::None"),
205            Self::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::codegen::format::FormatAsRustCode for ShapeInside {
216    fn format_as_rust_code(&self, _tabs: usize) -> String {
217        match self {
218            Self::None => String::from("ShapeInside::None"),
219            Self::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::codegen::format::FormatAsRustCode for ClipPath {
230    fn format_as_rust_code(&self, _tabs: usize) -> String {
231        match self {
232            Self::None => String::from("ClipPath::None"),
233            Self::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::codegen::format::FormatAsRustCode for ShapeMargin {
244    fn format_as_rust_code(&self, _tabs: usize) -> String {
245        format!(
246            "ShapeMargin {{ inner: {} }}",
247            crate::codegen::format::format_pixel_value(&self.inner)
248        )
249    }
250}
251
252impl crate::codegen::format::FormatAsRustCode for ShapeImageThreshold {
253    fn format_as_rust_code(&self, _tabs: usize) -> String {
254        format!(
255            "ShapeImageThreshold {{ inner: {} }}",
256            crate::codegen::format::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    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
267    use super::*;
268    use crate::shape_parser::{parse_shape, ShapeParseError};
269
270    /// Parser for shape-outside property
271    /// # Errors
272    ///
273    /// Returns an error if `input` is not a valid CSS `shape-outside` value.
274    pub fn parse_shape_outside(input: &str) -> Result<ShapeOutside, ShapeParseError> {
275        let trimmed = input.trim();
276        if trimmed == "none" {
277            Ok(ShapeOutside::None)
278        } else {
279            let shape = parse_shape(trimmed)?;
280            Ok(ShapeOutside::Shape(shape))
281        }
282    }
283
284    /// Parser for shape-inside property
285    /// # Errors
286    ///
287    /// Returns an error if `input` is not a valid CSS `shape-inside` value.
288    pub fn parse_shape_inside(input: &str) -> Result<ShapeInside, ShapeParseError> {
289        let trimmed = input.trim();
290        if trimmed == "none" {
291            Ok(ShapeInside::None)
292        } else {
293            let shape = parse_shape(trimmed)?;
294            Ok(ShapeInside::Shape(shape))
295        }
296    }
297
298    /// Parser for clip-path property
299    /// # Errors
300    ///
301    /// Returns an error if `input` is not a valid CSS `clip-path` value.
302    pub fn parse_clip_path(input: &str) -> Result<ClipPath, ShapeParseError> {
303        let trimmed = input.trim();
304        if trimmed == "none" {
305            Ok(ClipPath::None)
306        } else {
307            let shape = parse_shape(trimmed)?;
308            Ok(ClipPath::Shape(shape))
309        }
310    }
311
312    /// Parser for shape-margin property
313    /// # Errors
314    ///
315    /// Returns an error if `input` is not a valid CSS `shape-margin` value.
316    pub fn parse_shape_margin(input: &str) -> Result<ShapeMargin, CssPixelValueParseError<'_>> {
317        Ok(ShapeMargin {
318            inner: parse_pixel_value(input)?,
319        })
320    }
321
322    /// Parser for shape-image-threshold property
323    /// # Errors
324    ///
325    /// Returns an error if `input` is not a valid CSS `shape-image-threshold` value.
326    pub fn parse_shape_image_threshold(
327        input: &str,
328    ) -> Result<ShapeImageThreshold, ParseFloatError> {
329        let val = parse_float_value(input)?;
330        // value should be clamped between 0.0 and 1.0
331        let clamped = val.get().clamp(0.0, 1.0);
332        Ok(ShapeImageThreshold {
333            inner: FloatValue::new(clamped),
334        })
335    }
336}
337
338#[cfg(feature = "parser")]
339pub use parser::*;
340
341#[cfg(all(test, feature = "parser"))]
342mod tests {
343    // Tests assert that parsed values equal the exact source literals.
344    #![allow(clippy::float_cmp)]
345    use super::*;
346
347    #[test]
348    fn test_parse_shape_properties() {
349        // Test shape-outside
350        assert!(matches!(
351            parse_shape_outside("none").unwrap(),
352            ShapeOutside::None
353        ));
354        assert!(matches!(
355            parse_shape_outside("circle(50px)").unwrap(),
356            ShapeOutside::Shape(_)
357        ));
358
359        // Test shape-inside
360        assert!(matches!(
361            parse_shape_inside("none").unwrap(),
362            ShapeInside::None
363        ));
364        assert!(matches!(
365            parse_shape_inside("circle(100px at 50px 50px)").unwrap(),
366            ShapeInside::Shape(_)
367        ));
368
369        // Test clip-path
370        assert!(matches!(parse_clip_path("none").unwrap(), ClipPath::None));
371        assert!(matches!(
372            parse_clip_path("polygon(0 0, 100px 0, 100px 100px, 0 100px)").unwrap(),
373            ClipPath::Shape(_)
374        ));
375
376        // Test existing properties
377        assert_eq!(
378            parse_shape_margin("10px").unwrap().inner,
379            PixelValue::px(10.0)
380        );
381        assert_eq!(parse_shape_image_threshold("0.5").unwrap().inner.get(), 0.5);
382    }
383}