Skip to main content

fc_dioxus/core/
class.rs

1//! Class composition using upstream's BEM vocabulary.
2
3use std::fmt;
4
5/// A component's single declaration of its upstream class prefix.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub struct Bem(&'static str);
8
9impl Bem {
10    /// Declare a block that exists in upstream's CSS.
11    #[must_use]
12    pub const fn new(block: &'static str) -> Self {
13        Self(block)
14    }
15    /// The block class.
16    #[must_use]
17    pub const fn block(self) -> &'static str {
18        self.0
19    }
20    /// A part belonging to this block.
21    #[must_use]
22    pub fn element(self, element: &str) -> String {
23        format!("{}__{element}", self.0)
24    }
25    /// A modifier with no enum of its own, for a component composing another's block.
26    #[must_use]
27    pub(crate) fn modifier_raw(self, suffix: &str) -> String {
28        format!("{}--{suffix}", self.0)
29    }
30    /// The class for a typed modifier, or none for the bare block.
31    #[must_use]
32    pub fn modifier<M: Modifier>(self, modifier: M) -> Option<String> {
33        modifier
34            .suffix()
35            .map(|suffix| format!("{}--{suffix}", self.0))
36    }
37}
38
39/// An enum whose variants are exactly the CSS modifiers for one block.
40pub trait Modifier: Copy + Eq + Default + fmt::Debug + 'static {
41    /// Every variant, including the bare default.
42    const ALL: &'static [Self];
43    /// The upstream block to which these modifiers belong.
44    const BEM: Bem;
45    /// The portion after `--`, absent for the default.
46    fn suffix(self) -> Option<&'static str>;
47}
48
49/// A class attribute built in one string allocation.
50#[derive(Debug, Default, Clone, PartialEq, Eq)]
51pub struct ClassList(String);
52
53impl ClassList {
54    /// Start with a base class.
55    #[must_use]
56    pub fn new(base: &str) -> Self {
57        Self(base.to_owned())
58    }
59    /// Append whitespace-separated classes.
60    #[must_use]
61    pub fn with(mut self, class: impl AsRef<str>) -> Self {
62        let class = class.as_ref().trim();
63        if !class.is_empty() {
64            if !self.0.is_empty() {
65                self.0.push(' ');
66            }
67            self.0.push_str(class);
68        }
69        self
70    }
71    /// Append a class when a condition holds.
72    #[must_use]
73    pub fn with_if(self, condition: bool, class: impl AsRef<str>) -> Self {
74        if condition {
75            self.with(class)
76        } else {
77            self
78        }
79    }
80    /// Append the non-default modifier without allocating an intermediate string.
81    #[must_use]
82    pub fn with_modifier<M: Modifier>(mut self, bem: Bem, modifier: M) -> Self {
83        if let Some(suffix) = modifier.suffix() {
84            if !self.0.is_empty() {
85                self.0.push(' ');
86            }
87            self.0.push_str(bem.block());
88            self.0.push_str("--");
89            self.0.push_str(suffix);
90        }
91        self
92    }
93    /// Append consumer classes after the component's classes.
94    #[must_use]
95    pub fn extend(self, extra: Option<&str>) -> Self {
96        match extra {
97            Some(class) => self.with(class),
98            None => self,
99        }
100    }
101}
102
103impl fmt::Display for ClassList {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        f.write_str(&self.0)
106    }
107}
108
109/// A string did not name a declared modifier.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct ParseModifierError;
112impl fmt::Display for ParseModifierError {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.write_str("unknown CSS modifier")
115    }
116}
117impl std::error::Error for ParseModifierError {}
118
119/// Declare a modifier vocabulary once, including parsing and coverage enumeration.
120///
121/// The `for` argument is the component's BEM constant, so its prefix is not duplicated.
122/// Empty input parses as the bare variant.
123#[macro_export]
124macro_rules! fc_modifier {
125    (
126        $(#[$meta:meta])* $vis:vis enum $name:ident for $bem:ident {
127            $(#[$default_doc:meta])* $default:ident => None,
128            $($(#[$variant_doc:meta])* $variant:ident => $suffix:literal),* $(,)?
129        }
130    ) => {
131        $(#[$meta])*
132        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
133        #[non_exhaustive]
134        $vis enum $name {
135            $(#[$default_doc])*
136            #[default]
137            $default,
138            $($(#[$variant_doc])* $variant),*
139        }
140        impl $crate::core::class::Modifier for $name {
141            const ALL: &'static [Self] = &[Self::$default, $(Self::$variant),*];
142            const BEM: $crate::core::class::Bem = $bem;
143            fn suffix(self) -> Option<&'static str> {
144                match self { Self::$default => None, $(Self::$variant => Some($suffix)),* }
145            }
146        }
147        impl ::std::fmt::Display for $name {
148            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
149                f.write_str($crate::core::class::Modifier::suffix(*self).unwrap_or(""))
150            }
151        }
152        impl ::std::str::FromStr for $name {
153            type Err = $crate::core::class::ParseModifierError;
154            fn from_str(value: &str) -> Result<Self, Self::Err> {
155                match value { "" => Ok(Self::$default), $($suffix => Ok(Self::$variant),)* _ => Err($crate::core::class::ParseModifierError) }
156            }
157        }
158    };
159}