Skip to main content

azul_css/props/style/
effects.rs

1//! CSS properties for visual effects like opacity, blending, and cursor style.
2
3use alloc::string::{String, ToString};
4use core::fmt;
5
6#[cfg(feature = "parser")]
7use crate::props::basic::{
8    error::{InvalidValueErr, InvalidValueErrOwned},
9    length::parse_percentage_value,
10};
11use crate::props::{
12    basic::length::{PercentageParseError, PercentageValue},
13    formatter::PrintAsCssValue,
14};
15
16// -- Opacity --
17
18/// Represents an `opacity` attribute, a value from 0.0 to 1.0.
19#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[repr(C)]
21pub struct StyleOpacity {
22    pub inner: PercentageValue,
23}
24
25impl Default for StyleOpacity {
26    fn default() -> Self {
27        StyleOpacity {
28            inner: PercentageValue::const_new(100),
29        }
30    }
31}
32
33impl PrintAsCssValue for StyleOpacity {
34    fn print_as_css_value(&self) -> String {
35        format!("{}", self.inner.normalized())
36    }
37}
38
39#[cfg(feature = "parser")]
40impl_percentage_value!(StyleOpacity);
41
42// -- Mix Blend Mode --
43
44/// Represents a `visibility` attribute, controlling element visibility.
45#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
46#[repr(C)]
47pub enum StyleVisibility {
48    Visible,
49    Hidden,
50    Collapse,
51}
52
53impl Default for StyleVisibility {
54    fn default() -> StyleVisibility {
55        StyleVisibility::Visible
56    }
57}
58
59impl PrintAsCssValue for StyleVisibility {
60    fn print_as_css_value(&self) -> String {
61        String::from(match self {
62            Self::Visible => "visible",
63            Self::Hidden => "hidden",
64            Self::Collapse => "collapse",
65        })
66    }
67}
68
69// -- Mix Blend Mode --
70
71/// Represents a `mix-blend-mode` attribute, which determines how an element's
72/// content should blend with the content of the element's parent.
73#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
74#[repr(C)]
75pub enum StyleMixBlendMode {
76    Normal,
77    Multiply,
78    Screen,
79    Overlay,
80    Darken,
81    Lighten,
82    ColorDodge,
83    ColorBurn,
84    HardLight,
85    SoftLight,
86    Difference,
87    Exclusion,
88    Hue,
89    Saturation,
90    Color,
91    Luminosity,
92}
93
94impl Default for StyleMixBlendMode {
95    fn default() -> StyleMixBlendMode {
96        StyleMixBlendMode::Normal
97    }
98}
99
100impl fmt::Display for StyleMixBlendMode {
101    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
102        write!(
103            f,
104            "{}",
105            match self {
106                Self::Normal => "normal",
107                Self::Multiply => "multiply",
108                Self::Screen => "screen",
109                Self::Overlay => "overlay",
110                Self::Darken => "darken",
111                Self::Lighten => "lighten",
112                Self::ColorDodge => "color-dodge",
113                Self::ColorBurn => "color-burn",
114                Self::HardLight => "hard-light",
115                Self::SoftLight => "soft-light",
116                Self::Difference => "difference",
117                Self::Exclusion => "exclusion",
118                Self::Hue => "hue",
119                Self::Saturation => "saturation",
120                Self::Color => "color",
121                Self::Luminosity => "luminosity",
122            }
123        )
124    }
125}
126
127impl PrintAsCssValue for StyleMixBlendMode {
128    fn print_as_css_value(&self) -> String {
129        self.to_string()
130    }
131}
132
133// -- Cursor --
134
135/// Represents a `cursor` attribute, defining the mouse cursor to be displayed
136/// when pointing over an element.
137#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
138#[repr(C)]
139pub enum StyleCursor {
140    Alias,
141    AllScroll,
142    Cell,
143    ColResize,
144    ContextMenu,
145    Copy,
146    Crosshair,
147    Default,
148    EResize,
149    EwResize,
150    Grab,
151    Grabbing,
152    Help,
153    Move,
154    NResize,
155    NsResize,
156    NeswResize,
157    NwseResize,
158    Pointer,
159    Progress,
160    RowResize,
161    SResize,
162    SeResize,
163    Text,
164    Unset,
165    VerticalText,
166    WResize,
167    Wait,
168    ZoomIn,
169    ZoomOut,
170}
171
172impl Default for StyleCursor {
173    fn default() -> StyleCursor {
174        StyleCursor::Default
175    }
176}
177
178impl PrintAsCssValue for StyleCursor {
179    fn print_as_css_value(&self) -> String {
180        String::from(match self {
181            Self::Alias => "alias",
182            Self::AllScroll => "all-scroll",
183            Self::Cell => "cell",
184            Self::ColResize => "col-resize",
185            Self::ContextMenu => "context-menu",
186            Self::Copy => "copy",
187            Self::Crosshair => "crosshair",
188            Self::Default => "default",
189            Self::EResize => "e-resize",
190            Self::EwResize => "ew-resize",
191            Self::Grab => "grab",
192            Self::Grabbing => "grabbing",
193            Self::Help => "help",
194            Self::Move => "move",
195            Self::NResize => "n-resize",
196            Self::NsResize => "ns-resize",
197            Self::NeswResize => "nesw-resize",
198            Self::NwseResize => "nwse-resize",
199            Self::Pointer => "pointer",
200            Self::Progress => "progress",
201            Self::RowResize => "row-resize",
202            Self::SResize => "s-resize",
203            Self::SeResize => "se-resize",
204            Self::Text => "text",
205            Self::Unset => "unset",
206            Self::VerticalText => "vertical-text",
207            Self::WResize => "w-resize",
208            Self::Wait => "wait",
209            Self::ZoomIn => "zoom-in",
210            Self::ZoomOut => "zoom-out",
211        })
212    }
213}
214
215// --- PARSERS ---
216
217#[cfg(feature = "parser")]
218pub mod parsers {
219    use super::*;
220    use crate::props::basic::error::{InvalidValueErr, InvalidValueErrOwned};
221
222    // -- Opacity Parser --
223
224    #[derive(Clone, PartialEq)]
225    pub enum OpacityParseError<'a> {
226        ParsePercentage(PercentageParseError, &'a str),
227        OutOfRange(&'a str),
228    }
229    impl_debug_as_display!(OpacityParseError<'a>);
230    impl_display! { OpacityParseError<'a>, {
231        ParsePercentage(e, s) => format!("Invalid opacity value \"{}\": {}", s, e),
232        OutOfRange(s) => format!("Invalid opacity value \"{}\": must be between 0 and 1", s),
233    }}
234
235    #[derive(Debug, Clone, PartialEq)]
236    pub enum OpacityParseErrorOwned {
237        ParsePercentage(PercentageParseError, String),
238        OutOfRange(String),
239    }
240
241    impl<'a> OpacityParseError<'a> {
242        pub fn to_contained(&self) -> OpacityParseErrorOwned {
243            match self {
244                Self::ParsePercentage(err, s) => {
245                    OpacityParseErrorOwned::ParsePercentage(err.clone(), s.to_string())
246                }
247                Self::OutOfRange(s) => OpacityParseErrorOwned::OutOfRange(s.to_string()),
248            }
249        }
250    }
251
252    impl OpacityParseErrorOwned {
253        pub fn to_shared<'a>(&'a self) -> OpacityParseError<'a> {
254            match self {
255                Self::ParsePercentage(err, s) => {
256                    OpacityParseError::ParsePercentage(err.clone(), s.as_str())
257                }
258                Self::OutOfRange(s) => OpacityParseError::OutOfRange(s.as_str()),
259            }
260        }
261    }
262
263    pub fn parse_style_opacity<'a>(input: &'a str) -> Result<StyleOpacity, OpacityParseError<'a>> {
264        let val = parse_percentage_value(input)
265            .map_err(|e| OpacityParseError::ParsePercentage(e, input))?;
266
267        let normalized = val.normalized();
268        if normalized < 0.0 || normalized > 1.0 {
269            return Err(OpacityParseError::OutOfRange(input));
270        }
271
272        Ok(StyleOpacity { inner: val })
273    }
274
275    // -- Visibility Parser --
276
277    #[derive(Clone, PartialEq)]
278    pub enum StyleVisibilityParseError<'a> {
279        InvalidValue(InvalidValueErr<'a>),
280    }
281    impl_debug_as_display!(StyleVisibilityParseError<'a>);
282    impl_display! { StyleVisibilityParseError<'a>, {
283        InvalidValue(e) => format!("Invalid visibility value: \"{}\"", e.0),
284    }}
285    impl_from!(InvalidValueErr<'a>, StyleVisibilityParseError::InvalidValue);
286
287    #[derive(Debug, Clone, PartialEq)]
288    pub enum StyleVisibilityParseErrorOwned {
289        InvalidValue(InvalidValueErrOwned),
290    }
291
292    impl<'a> StyleVisibilityParseError<'a> {
293        pub fn to_contained(&self) -> StyleVisibilityParseErrorOwned {
294            match self {
295                Self::InvalidValue(e) => {
296                    StyleVisibilityParseErrorOwned::InvalidValue(e.to_contained())
297                }
298            }
299        }
300    }
301
302    impl StyleVisibilityParseErrorOwned {
303        pub fn to_shared<'a>(&'a self) -> StyleVisibilityParseError<'a> {
304            match self {
305                Self::InvalidValue(e) => StyleVisibilityParseError::InvalidValue(e.to_shared()),
306            }
307        }
308    }
309
310    pub fn parse_style_visibility<'a>(
311        input: &'a str,
312    ) -> Result<StyleVisibility, StyleVisibilityParseError<'a>> {
313        let input = input.trim();
314        match input {
315            "visible" => Ok(StyleVisibility::Visible),
316            "hidden" => Ok(StyleVisibility::Hidden),
317            "collapse" => Ok(StyleVisibility::Collapse),
318            _ => Err(InvalidValueErr(input).into()),
319        }
320    }
321
322    // -- Mix Blend Mode Parser --
323
324    #[derive(Clone, PartialEq)]
325    pub enum MixBlendModeParseError<'a> {
326        InvalidValue(InvalidValueErr<'a>),
327    }
328    impl_debug_as_display!(MixBlendModeParseError<'a>);
329    impl_display! { MixBlendModeParseError<'a>, {
330        InvalidValue(e) => format!("Invalid mix-blend-mode value: \"{}\"", e.0),
331    }}
332    impl_from!(InvalidValueErr<'a>, MixBlendModeParseError::InvalidValue);
333
334    #[derive(Debug, Clone, PartialEq)]
335    pub enum MixBlendModeParseErrorOwned {
336        InvalidValue(InvalidValueErrOwned),
337    }
338
339    impl<'a> MixBlendModeParseError<'a> {
340        pub fn to_contained(&self) -> MixBlendModeParseErrorOwned {
341            match self {
342                Self::InvalidValue(e) => {
343                    MixBlendModeParseErrorOwned::InvalidValue(e.to_contained())
344                }
345            }
346        }
347    }
348
349    impl MixBlendModeParseErrorOwned {
350        pub fn to_shared<'a>(&'a self) -> MixBlendModeParseError<'a> {
351            match self {
352                Self::InvalidValue(e) => MixBlendModeParseError::InvalidValue(e.to_shared()),
353            }
354        }
355    }
356
357    pub fn parse_style_mix_blend_mode<'a>(
358        input: &'a str,
359    ) -> Result<StyleMixBlendMode, MixBlendModeParseError<'a>> {
360        let input = input.trim();
361        match input {
362            "normal" => Ok(StyleMixBlendMode::Normal),
363            "multiply" => Ok(StyleMixBlendMode::Multiply),
364            "screen" => Ok(StyleMixBlendMode::Screen),
365            "overlay" => Ok(StyleMixBlendMode::Overlay),
366            "darken" => Ok(StyleMixBlendMode::Darken),
367            "lighten" => Ok(StyleMixBlendMode::Lighten),
368            "color-dodge" => Ok(StyleMixBlendMode::ColorDodge),
369            "color-burn" => Ok(StyleMixBlendMode::ColorBurn),
370            "hard-light" => Ok(StyleMixBlendMode::HardLight),
371            "soft-light" => Ok(StyleMixBlendMode::SoftLight),
372            "difference" => Ok(StyleMixBlendMode::Difference),
373            "exclusion" => Ok(StyleMixBlendMode::Exclusion),
374            "hue" => Ok(StyleMixBlendMode::Hue),
375            "saturation" => Ok(StyleMixBlendMode::Saturation),
376            "color" => Ok(StyleMixBlendMode::Color),
377            "luminosity" => Ok(StyleMixBlendMode::Luminosity),
378            _ => Err(InvalidValueErr(input).into()),
379        }
380    }
381
382    // -- Cursor Parser --
383
384    #[derive(Clone, PartialEq)]
385    pub enum CursorParseError<'a> {
386        InvalidValue(InvalidValueErr<'a>),
387    }
388    impl_debug_as_display!(CursorParseError<'a>);
389    impl_display! { CursorParseError<'a>, {
390        InvalidValue(e) => format!("Invalid cursor value: \"{}\"", e.0),
391    }}
392    impl_from!(InvalidValueErr<'a>, CursorParseError::InvalidValue);
393
394    #[derive(Debug, Clone, PartialEq)]
395    pub enum CursorParseErrorOwned {
396        InvalidValue(InvalidValueErrOwned),
397    }
398
399    impl<'a> CursorParseError<'a> {
400        pub fn to_contained(&self) -> CursorParseErrorOwned {
401            match self {
402                Self::InvalidValue(e) => CursorParseErrorOwned::InvalidValue(e.to_contained()),
403            }
404        }
405    }
406
407    impl CursorParseErrorOwned {
408        pub fn to_shared<'a>(&'a self) -> CursorParseError<'a> {
409            match self {
410                Self::InvalidValue(e) => CursorParseError::InvalidValue(e.to_shared()),
411            }
412        }
413    }
414
415    pub fn parse_style_cursor<'a>(input: &'a str) -> Result<StyleCursor, CursorParseError<'a>> {
416        let input = input.trim();
417        match input {
418            "alias" => Ok(StyleCursor::Alias),
419            "all-scroll" => Ok(StyleCursor::AllScroll),
420            "cell" => Ok(StyleCursor::Cell),
421            "col-resize" => Ok(StyleCursor::ColResize),
422            "context-menu" => Ok(StyleCursor::ContextMenu),
423            "copy" => Ok(StyleCursor::Copy),
424            "crosshair" => Ok(StyleCursor::Crosshair),
425            "default" => Ok(StyleCursor::Default),
426            "e-resize" => Ok(StyleCursor::EResize),
427            "ew-resize" => Ok(StyleCursor::EwResize),
428            "grab" => Ok(StyleCursor::Grab),
429            "grabbing" => Ok(StyleCursor::Grabbing),
430            "help" => Ok(StyleCursor::Help),
431            "move" => Ok(StyleCursor::Move),
432            "n-resize" => Ok(StyleCursor::NResize),
433            "ns-resize" => Ok(StyleCursor::NsResize),
434            "nesw-resize" => Ok(StyleCursor::NeswResize),
435            "nwse-resize" => Ok(StyleCursor::NwseResize),
436            "pointer" => Ok(StyleCursor::Pointer),
437            "progress" => Ok(StyleCursor::Progress),
438            "row-resize" => Ok(StyleCursor::RowResize),
439            "s-resize" => Ok(StyleCursor::SResize),
440            "se-resize" => Ok(StyleCursor::SeResize),
441            "text" => Ok(StyleCursor::Text),
442            "unset" => Ok(StyleCursor::Unset),
443            "vertical-text" => Ok(StyleCursor::VerticalText),
444            "w-resize" => Ok(StyleCursor::WResize),
445            "wait" => Ok(StyleCursor::Wait),
446            "zoom-in" => Ok(StyleCursor::ZoomIn),
447            "zoom-out" => Ok(StyleCursor::ZoomOut),
448            _ => Err(InvalidValueErr(input).into()),
449        }
450    }
451}
452
453#[cfg(feature = "parser")]
454pub use self::parsers::*;
455
456#[cfg(all(test, feature = "parser"))]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn test_parse_opacity() {
462        assert_eq!(parse_style_opacity("0.5").unwrap().inner.normalized(), 0.5);
463        assert_eq!(parse_style_opacity("1").unwrap().inner.normalized(), 1.0);
464        assert_eq!(parse_style_opacity("50%").unwrap().inner.normalized(), 0.5);
465        assert_eq!(parse_style_opacity("0").unwrap().inner.normalized(), 0.0);
466        assert_eq!(
467            parse_style_opacity("  75%  ").unwrap().inner.normalized(),
468            0.75
469        );
470        assert!(parse_style_opacity("1.1").is_err());
471        assert!(parse_style_opacity("-0.1").is_err());
472        assert!(parse_style_opacity("auto").is_err());
473    }
474
475    #[test]
476    fn test_parse_mix_blend_mode() {
477        assert_eq!(
478            parse_style_mix_blend_mode("multiply").unwrap(),
479            StyleMixBlendMode::Multiply
480        );
481        assert_eq!(
482            parse_style_mix_blend_mode("screen").unwrap(),
483            StyleMixBlendMode::Screen
484        );
485        assert_eq!(
486            parse_style_mix_blend_mode("color-dodge").unwrap(),
487            StyleMixBlendMode::ColorDodge
488        );
489        assert!(parse_style_mix_blend_mode("mix").is_err());
490    }
491
492    #[test]
493    fn test_parse_visibility() {
494        assert_eq!(
495            parse_style_visibility("visible").unwrap(),
496            StyleVisibility::Visible
497        );
498        assert_eq!(
499            parse_style_visibility("hidden").unwrap(),
500            StyleVisibility::Hidden
501        );
502        assert_eq!(
503            parse_style_visibility("collapse").unwrap(),
504            StyleVisibility::Collapse
505        );
506        assert_eq!(
507            parse_style_visibility("  visible  ").unwrap(),
508            StyleVisibility::Visible
509        );
510        assert!(parse_style_visibility("none").is_err());
511        assert!(parse_style_visibility("show").is_err());
512    }
513
514    #[test]
515    fn test_parse_cursor() {
516        assert_eq!(parse_style_cursor("pointer").unwrap(), StyleCursor::Pointer);
517        assert_eq!(parse_style_cursor("wait").unwrap(), StyleCursor::Wait);
518        assert_eq!(
519            parse_style_cursor("col-resize").unwrap(),
520            StyleCursor::ColResize
521        );
522        assert_eq!(parse_style_cursor("  text  ").unwrap(), StyleCursor::Text);
523        assert!(parse_style_cursor("hand").is_err()); // "hand" is a legacy IE value
524    }
525}