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

            
7
use alloc::string::{String, ToString};
8

            
9
use 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)]
27
pub enum ShapeOutside {
28
    #[default]
29
    None,
30
    Shape(CssShape),
31
}
32

            
33
impl Eq for ShapeOutside {}
34
impl 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
}
42
impl PartialOrd for ShapeOutside {
43
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
44
        Some(self.cmp(other))
45
    }
46
}
47
impl 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

            
59
impl 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)]
72
pub enum ShapeInside {
73
    #[default]
74
    None,
75
    Shape(CssShape),
76
}
77

            
78
impl Eq for ShapeInside {}
79
impl 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
}
87
impl PartialOrd for ShapeInside {
88
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
89
        Some(self.cmp(other))
90
    }
91
}
92
impl 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

            
104
impl 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)]
117
pub enum ClipPath {
118
    #[default]
119
    None,
120
    Shape(CssShape),
121
}
122

            
123
impl Eq for ClipPath {}
124
impl 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
}
132
impl PartialOrd for ClipPath {
133
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
134
        Some(self.cmp(other))
135
    }
136
}
137
impl 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

            
149
impl 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)]
161
pub struct ShapeMargin {
162
    pub inner: PixelValue,
163
}
164

            
165
impl Default for ShapeMargin {
166
    fn default() -> Self {
167
        Self {
168
            inner: PixelValue::zero(),
169
        }
170
    }
171
}
172

            
173
impl 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)]
182
pub struct ShapeImageThreshold {
183
    pub inner: FloatValue,
184
}
185

            
186
impl Default for ShapeImageThreshold {
187
    fn default() -> Self {
188
        Self {
189
            inner: FloatValue::const_new(0),
190
        }
191
    }
192
}
193

            
194
impl PrintAsCssValue for ShapeImageThreshold {
195
    fn print_as_css_value(&self) -> String {
196
        self.inner.to_string()
197
    }
198
}
199

            
200
// Formatting to Rust code
201
impl 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

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

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

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

            
252
impl 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")]
263
pub 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
2
    pub fn parse_shape_outside(input: &str) -> Result<ShapeOutside, ShapeParseError> {
271
2
        let trimmed = input.trim();
272
2
        if trimmed == "none" {
273
1
            Ok(ShapeOutside::None)
274
        } else {
275
1
            let shape = parse_shape(trimmed)?;
276
1
            Ok(ShapeOutside::Shape(shape))
277
        }
278
2
    }
279

            
280
    /// Parser for shape-inside property
281
2
    pub fn parse_shape_inside(input: &str) -> Result<ShapeInside, ShapeParseError> {
282
2
        let trimmed = input.trim();
283
2
        if trimmed == "none" {
284
1
            Ok(ShapeInside::None)
285
        } else {
286
1
            let shape = parse_shape(trimmed)?;
287
1
            Ok(ShapeInside::Shape(shape))
288
        }
289
2
    }
290

            
291
    /// Parser for clip-path property
292
2
    pub fn parse_clip_path(input: &str) -> Result<ClipPath, ShapeParseError> {
293
2
        let trimmed = input.trim();
294
2
        if trimmed == "none" {
295
1
            Ok(ClipPath::None)
296
        } else {
297
1
            let shape = parse_shape(trimmed)?;
298
1
            Ok(ClipPath::Shape(shape))
299
        }
300
2
    }
301

            
302
    /// Parser for shape-margin property
303
1
    pub fn parse_shape_margin(input: &str) -> Result<ShapeMargin, CssPixelValueParseError<'_>> {
304
        Ok(ShapeMargin {
305
1
            inner: parse_pixel_value(input)?,
306
        })
307
1
    }
308

            
309
    /// Parser for shape-image-threshold property
310
1
    pub fn parse_shape_image_threshold(
311
1
        input: &str,
312
1
    ) -> Result<ShapeImageThreshold, ParseFloatError> {
313
1
        let val = parse_float_value(input)?;
314
        // value should be clamped between 0.0 and 1.0
315
1
        let clamped = val.get().clamp(0.0, 1.0);
316
1
        Ok(ShapeImageThreshold {
317
1
            inner: FloatValue::new(clamped),
318
1
        })
319
1
    }
320
}
321

            
322
#[cfg(feature = "parser")]
323
pub use parser::*;
324

            
325
#[cfg(all(test, feature = "parser"))]
326
mod tests {
327
    use super::*;
328

            
329
    #[test]
330
1
    fn test_parse_shape_properties() {
331
        // Test shape-outside
332
1
        assert!(matches!(
333
1
            parse_shape_outside("none").unwrap(),
334
            ShapeOutside::None
335
        ));
336
1
        assert!(matches!(
337
1
            parse_shape_outside("circle(50px)").unwrap(),
338
            ShapeOutside::Shape(_)
339
        ));
340

            
341
        // Test shape-inside
342
1
        assert!(matches!(
343
1
            parse_shape_inside("none").unwrap(),
344
            ShapeInside::None
345
        ));
346
1
        assert!(matches!(
347
1
            parse_shape_inside("circle(100px at 50px 50px)").unwrap(),
348
            ShapeInside::Shape(_)
349
        ));
350

            
351
        // Test clip-path
352
1
        assert!(matches!(parse_clip_path("none").unwrap(), ClipPath::None));
353
1
        assert!(matches!(
354
1
            parse_clip_path("polygon(0 0, 100px 0, 100px 100px, 0 100px)").unwrap(),
355
            ClipPath::Shape(_)
356
        ));
357

            
358
        // Test existing properties
359
1
        assert_eq!(
360
1
            parse_shape_margin("10px").unwrap().inner,
361
1
            PixelValue::px(10.0)
362
        );
363
1
        assert_eq!(parse_shape_image_threshold("0.5").unwrap().inner.get(), 0.5);
364
1
    }
365
}