1
//! Shared types for CSS shadow properties (used by both `box-shadow` and `text-shadow`).
2

            
3
use alloc::string::{String, ToString};
4
use core::fmt;
5
use crate::corety::AzString;
6

            
7
use crate::props::{
8
    basic::{
9
        color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
10
        pixel::{
11
            parse_pixel_value_no_percent, CssPixelValueParseError, CssPixelValueParseErrorOwned,
12
            PixelValueNoPercent,
13
        },
14
    },
15
    formatter::PrintAsCssValue,
16
};
17

            
18
/// What direction should a `box-shadow` be clipped in (inset or outset).
19
#[derive(Debug, Default, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
20
#[repr(C)]
21
pub enum BoxShadowClipMode {
22
    #[default]
23
    Outset,
24
    Inset,
25
}
26

            
27
impl fmt::Display for BoxShadowClipMode {
28
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29
        match self {
30
            BoxShadowClipMode::Outset => Ok(()), // Outset is the default, not written
31
            BoxShadowClipMode::Inset => write!(f, "inset"),
32
        }
33
    }
34
}
35

            
36
/// Represents a single CSS shadow value, shared by both `box-shadow` and `text-shadow`.
37
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38
#[repr(C)]
39
pub struct StyleBoxShadow {
40
    pub offset_x: PixelValueNoPercent,
41
    pub offset_y: PixelValueNoPercent,
42
    pub blur_radius: PixelValueNoPercent,
43
    pub spread_radius: PixelValueNoPercent,
44
    pub clip_mode: BoxShadowClipMode,
45
    pub color: ColorU,
46
}
47

            
48
impl Default for StyleBoxShadow {
49
61
    fn default() -> Self {
50
61
        Self {
51
61
            offset_x: PixelValueNoPercent::default(),
52
61
            offset_y: PixelValueNoPercent::default(),
53
61
            blur_radius: PixelValueNoPercent::default(),
54
61
            spread_radius: PixelValueNoPercent::default(),
55
61
            clip_mode: BoxShadowClipMode::default(),
56
61
            color: ColorU::BLACK,
57
61
        }
58
61
    }
59
}
60

            
61
impl StyleBoxShadow {
62
    /// Scales the pixel values of the shadow for a given DPI factor.
63
    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
64
        self.offset_x.scale_for_dpi(scale_factor);
65
        self.offset_y.scale_for_dpi(scale_factor);
66
        self.blur_radius.scale_for_dpi(scale_factor);
67
        self.spread_radius.scale_for_dpi(scale_factor);
68
    }
69
}
70

            
71
impl PrintAsCssValue for StyleBoxShadow {
72
    fn print_as_css_value(&self) -> String {
73
        let mut components = Vec::new();
74

            
75
        if self.clip_mode == BoxShadowClipMode::Inset {
76
            components.push("inset".to_string());
77
        }
78
        components.push(self.offset_x.to_string());
79
        components.push(self.offset_y.to_string());
80

            
81
        // Only print blur, spread, and color if they are not default, for brevity
82
        if self.blur_radius.inner.number.get() != 0.0
83
            || self.spread_radius.inner.number.get() != 0.0
84
        {
85
            components.push(self.blur_radius.to_string());
86
        }
87
        if self.spread_radius.inner.number.get() != 0.0 {
88
            components.push(self.spread_radius.to_string());
89
        }
90
        if self.color != ColorU::BLACK {
91
            // Assuming black is the default
92
            components.push(self.color.to_hash());
93
        }
94

            
95
        components.join(" ")
96
    }
97
}
98

            
99
// Formatting to Rust code for StyleBoxShadow
100
impl crate::format_rust_code::FormatAsRustCode for StyleBoxShadow {
101
    fn format_as_rust_code(&self, tabs: usize) -> String {
102
        let t = String::from("    ").repeat(tabs);
103
        format!(
104
            "StyleBoxShadow {{\r\n{}    offset_x: {},\r\n{}    offset_y: {},\r\n{}    color: \
105
             {},\r\n{}    blur_radius: {},\r\n{}    spread_radius: {},\r\n{}    clip_mode: \
106
             BoxShadowClipMode::{:?},\r\n{}}}",
107
            t,
108
            crate::format_rust_code::format_pixel_value_no_percent(&self.offset_x),
109
            t,
110
            crate::format_rust_code::format_pixel_value_no_percent(&self.offset_y),
111
            t,
112
            crate::format_rust_code::format_color_value(&self.color),
113
            t,
114
            crate::format_rust_code::format_pixel_value_no_percent(&self.blur_radius),
115
            t,
116
            crate::format_rust_code::format_pixel_value_no_percent(&self.spread_radius),
117
            t,
118
            self.clip_mode,
119
            t
120
        )
121
    }
122
}
123

            
124
// --- PARSER ---
125

            
126
/// Error returned when parsing a CSS shadow value fails.
127
#[derive(Clone, PartialEq)]
128
pub enum CssShadowParseError<'a> {
129
    TooManyOrTooFewComponents(&'a str),
130
    ValueParseErr(CssPixelValueParseError<'a>),
131
    ColorParseError(CssColorParseError<'a>),
132
}
133

            
134
impl_debug_as_display!(CssShadowParseError<'a>);
135
impl_display! { CssShadowParseError<'a>, {
136
    TooManyOrTooFewComponents(e) => format!("Expected 2 to 4 length values for box-shadow, found an invalid number of components in: \"{}\"", e),
137
    ValueParseErr(e) => format!("Invalid length value in box-shadow: {}", e),
138
    ColorParseError(e) => format!("Invalid color value in box-shadow: {}", e),
139
}}
140

            
141
impl_from!(
142
    CssPixelValueParseError<'a>,
143
    CssShadowParseError::ValueParseErr
144
);
145
impl_from!(CssColorParseError<'a>, CssShadowParseError::ColorParseError);
146

            
147
/// Owned version of `CssShadowParseError`.
148
#[derive(Debug, Clone, PartialEq)]
149
#[repr(C, u8)]
150
pub enum CssShadowParseErrorOwned {
151
    TooManyOrTooFewComponents(AzString),
152
    ValueParseErr(CssPixelValueParseErrorOwned),
153
    ColorParseError(CssColorParseErrorOwned),
154
}
155

            
156
impl<'a> CssShadowParseError<'a> {
157
    /// Converts the borrowed error into an owned version for storage.
158
    pub fn to_contained(&self) -> CssShadowParseErrorOwned {
159
        match self {
160
            CssShadowParseError::TooManyOrTooFewComponents(s) => {
161
                CssShadowParseErrorOwned::TooManyOrTooFewComponents(s.to_string().into())
162
            }
163
            CssShadowParseError::ValueParseErr(e) => {
164
                CssShadowParseErrorOwned::ValueParseErr(e.to_contained())
165
            }
166
            CssShadowParseError::ColorParseError(e) => {
167
                CssShadowParseErrorOwned::ColorParseError(e.to_contained())
168
            }
169
        }
170
    }
171
}
172

            
173
impl CssShadowParseErrorOwned {
174
    /// Converts the owned error back into a borrowed version.
175
    pub fn to_shared<'a>(&'a self) -> CssShadowParseError<'a> {
176
        match self {
177
            CssShadowParseErrorOwned::TooManyOrTooFewComponents(s) => {
178
                CssShadowParseError::TooManyOrTooFewComponents(s.as_str())
179
            }
180
            CssShadowParseErrorOwned::ValueParseErr(e) => {
181
                CssShadowParseError::ValueParseErr(e.to_shared())
182
            }
183
            CssShadowParseErrorOwned::ColorParseError(e) => {
184
                CssShadowParseError::ColorParseError(e.to_shared())
185
            }
186
        }
187
    }
188
}
189

            
190
/// Parses a CSS box-shadow, such as `"5px 10px #888 inset"`.
191
///
192
/// Note: This parser does not handle the `none` keyword, as that is handled by the
193
/// `CssPropertyValue` enum wrapper. It also does not handle comma-separated lists
194
/// of multiple shadows; it only parses a single shadow value.
195
#[cfg(feature = "parser")]
196
61
pub fn parse_style_box_shadow<'a>(
197
61
    input: &'a str,
198
61
) -> Result<StyleBoxShadow, CssShadowParseError<'a>> {
199
61
    let mut parts: Vec<&str> = input.split_whitespace().collect();
200
61
    let mut shadow = StyleBoxShadow::default();
201

            
202
    // The `inset` keyword can appear anywhere. Find it, set the flag, and remove it.
203
254
    if let Some(pos) = parts.iter().position(|&p| p == "inset") {
204
2
        shadow.clip_mode = BoxShadowClipMode::Inset;
205
2
        parts.remove(pos);
206
59
    }
207

            
208
    // The color can also be anywhere. Find it, set the color, and remove it.
209
    // It's the only part that isn't a length. We iterate from the back because
210
    // it's slightly more common for the color to be last.
211
61
    if let Some((pos, color)) = parts
212
61
        .iter()
213
61
        .enumerate()
214
61
        .rev()
215
111
        .find_map(|(i, p)| parse_css_color(p).ok().map(|c| (i, c)))
216
49
    {
217
49
        shadow.color = color;
218
49
        parts.remove(pos);
219
56
    }
220

            
221
    // The remaining parts must be 2, 3, or 4 length values.
222
61
    match parts.len() {
223
60
        2..=4 => {
224
52
            shadow.offset_x = parse_pixel_value_no_percent(parts[0])?;
225
51
            shadow.offset_y = parse_pixel_value_no_percent(parts[1])?;
226
51
            if parts.len() > 2 {
227
47
                shadow.blur_radius = parse_pixel_value_no_percent(parts[2])?;
228
4
            }
229
50
            if parts.len() > 3 {
230
1
                shadow.spread_radius = parse_pixel_value_no_percent(parts[3])?;
231
49
            }
232
        }
233
9
        _ => return Err(CssShadowParseError::TooManyOrTooFewComponents(input)),
234
    }
235

            
236
50
    Ok(shadow)
237
61
}
238

            
239
#[cfg(all(test, feature = "parser"))]
240
mod tests {
241
    use super::*;
242
    use crate::props::basic::pixel::PixelValue;
243

            
244
17
    fn px_no_percent(val: f32) -> PixelValueNoPercent {
245
17
        PixelValueNoPercent {
246
17
            inner: PixelValue::px(val),
247
17
        }
248
17
    }
249

            
250
    #[test]
251
1
    fn test_parse_box_shadow_simple() {
252
1
        let result = parse_style_box_shadow("10px 5px").unwrap();
253
1
        assert_eq!(result.offset_x, px_no_percent(10.0));
254
1
        assert_eq!(result.offset_y, px_no_percent(5.0));
255
1
        assert_eq!(result.blur_radius, px_no_percent(0.0));
256
1
        assert_eq!(result.spread_radius, px_no_percent(0.0));
257
1
        assert_eq!(result.color, ColorU::BLACK);
258
1
        assert_eq!(result.clip_mode, BoxShadowClipMode::Outset);
259
1
    }
260

            
261
    #[test]
262
1
    fn test_parse_box_shadow_with_color() {
263
1
        let result = parse_style_box_shadow("10px 5px #888").unwrap();
264
1
        assert_eq!(result.offset_x, px_no_percent(10.0));
265
1
        assert_eq!(result.offset_y, px_no_percent(5.0));
266
1
        assert_eq!(result.color, ColorU::new_rgb(0x88, 0x88, 0x88));
267
1
    }
268

            
269
    #[test]
270
1
    fn test_parse_box_shadow_with_blur() {
271
1
        let result = parse_style_box_shadow("5px 10px 20px").unwrap();
272
1
        assert_eq!(result.offset_x, px_no_percent(5.0));
273
1
        assert_eq!(result.offset_y, px_no_percent(10.0));
274
1
        assert_eq!(result.blur_radius, px_no_percent(20.0));
275
1
    }
276

            
277
    #[test]
278
1
    fn test_parse_box_shadow_with_spread() {
279
1
        let result = parse_style_box_shadow("2px 2px 2px 1px rgba(0,0,0,0.2)").unwrap();
280
1
        assert_eq!(result.offset_x, px_no_percent(2.0));
281
1
        assert_eq!(result.offset_y, px_no_percent(2.0));
282
1
        assert_eq!(result.blur_radius, px_no_percent(2.0));
283
1
        assert_eq!(result.spread_radius, px_no_percent(1.0));
284
1
        assert_eq!(result.color, ColorU::new(0, 0, 0, 51));
285
1
    }
286

            
287
    #[test]
288
1
    fn test_parse_box_shadow_inset() {
289
1
        let result = parse_style_box_shadow("inset 0 0 10px #000").unwrap();
290
1
        assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
291
1
        assert_eq!(result.offset_x, px_no_percent(0.0));
292
1
        assert_eq!(result.offset_y, px_no_percent(0.0));
293
1
        assert_eq!(result.blur_radius, px_no_percent(10.0));
294
1
        assert_eq!(result.color, ColorU::BLACK);
295
1
    }
296

            
297
    #[test]
298
1
    fn test_parse_box_shadow_mixed_order() {
299
1
        let result = parse_style_box_shadow("5px 1em red inset").unwrap();
300
1
        assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
301
1
        assert_eq!(result.offset_x, px_no_percent(5.0));
302
1
        assert_eq!(
303
            result.offset_y,
304
1
            PixelValueNoPercent {
305
1
                inner: PixelValue::em(1.0)
306
1
            }
307
        );
308
1
        assert_eq!(result.color, ColorU::RED);
309
1
    }
310

            
311
    #[test]
312
1
    fn test_parse_box_shadow_invalid() {
313
1
        assert!(parse_style_box_shadow("10px").is_err());
314
1
        assert!(parse_style_box_shadow("10px 5px 4px 3px 2px").is_err());
315
        // Two colors: rposition picks "blue" as the color, leaving "red" which
316
        // fails to parse as a pixel value.
317
1
        assert!(parse_style_box_shadow("10px 5px red blue").is_err());
318
1
        assert!(parse_style_box_shadow("10% 5px").is_err()); // No percent allowed
319
1
    }
320
}