adui_dioxus/foundation/
semantic.rs

1//! Semantic classNames and styles system aligned with Ant Design 6.0.
2//!
3//! This module provides a type-safe way to apply custom classes and styles
4//! to specific semantic parts of components.
5
6use std::collections::HashMap;
7use std::hash::Hash;
8
9/// A collection of CSS class names keyed by semantic slot name.
10///
11/// Used to customize the styling of specific parts of a component.
12///
13/// # Example
14///
15/// ```rust,ignore
16/// use adui_dioxus::foundation::SemanticClassNames;
17///
18/// #[derive(Hash, Eq, PartialEq, Clone, Copy)]
19/// enum ButtonSemantic {
20///     Root,
21///     Icon,
22///     Content,
23/// }
24///
25/// let mut class_names = SemanticClassNames::new();
26/// class_names.set(ButtonSemantic::Root, "my-custom-button");
27/// class_names.set(ButtonSemantic::Icon, "my-custom-icon");
28/// ```
29#[derive(Clone, Debug, Default)]
30pub struct SemanticClassNames<S: Hash + Eq> {
31    values: HashMap<S, String>,
32}
33
34impl<S: Hash + Eq> SemanticClassNames<S> {
35    /// Create an empty semantic class names collection.
36    pub fn new() -> Self {
37        Self {
38            values: HashMap::new(),
39        }
40    }
41
42    /// Set the class name for a specific semantic slot.
43    pub fn set(&mut self, slot: S, class_name: impl Into<String>) {
44        self.values.insert(slot, class_name.into());
45    }
46
47    /// Get the class name for a specific semantic slot, if set.
48    pub fn get(&self, slot: &S) -> Option<&str> {
49        self.values.get(slot).map(|s| s.as_str())
50    }
51
52    /// Get the class name for a slot or return an empty string.
53    pub fn get_or_empty(&self, slot: &S) -> &str {
54        self.values.get(slot).map(|s| s.as_str()).unwrap_or("")
55    }
56
57    /// Check if a semantic slot has a class name.
58    pub fn contains(&self, slot: &S) -> bool {
59        self.values.contains_key(slot)
60    }
61
62    /// Create from an iterator of (slot, class_name) pairs.
63    pub fn from_iter<I, N>(iter: I) -> Self
64    where
65        I: IntoIterator<Item = (S, N)>,
66        N: Into<String>,
67    {
68        Self {
69            values: iter.into_iter().map(|(k, v)| (k, v.into())).collect(),
70        }
71    }
72}
73
74impl<S: Hash + Eq> PartialEq for SemanticClassNames<S> {
75    fn eq(&self, other: &Self) -> bool {
76        self.values == other.values
77    }
78}
79
80/// A collection of inline CSS styles keyed by semantic slot name.
81///
82/// Used to customize the styling of specific parts of a component.
83///
84/// # Example
85///
86/// ```rust,ignore
87/// use adui_dioxus::foundation::SemanticStyles;
88///
89/// #[derive(Hash, Eq, PartialEq, Clone, Copy)]
90/// enum ModalSemantic {
91///     Root,
92///     Header,
93///     Body,
94///     Footer,
95/// }
96///
97/// let mut styles = SemanticStyles::new();
98/// styles.set(ModalSemantic::Body, "padding: 24px; background: #f0f0f0;");
99/// ```
100#[derive(Clone, Debug, Default)]
101pub struct SemanticStyles<S: Hash + Eq> {
102    values: HashMap<S, String>,
103}
104
105impl<S: Hash + Eq> SemanticStyles<S> {
106    /// Create an empty semantic styles collection.
107    pub fn new() -> Self {
108        Self {
109            values: HashMap::new(),
110        }
111    }
112
113    /// Set the inline style for a specific semantic slot.
114    pub fn set(&mut self, slot: S, style: impl Into<String>) {
115        self.values.insert(slot, style.into());
116    }
117
118    /// Get the inline style for a specific semantic slot, if set.
119    pub fn get(&self, slot: &S) -> Option<&str> {
120        self.values.get(slot).map(|s| s.as_str())
121    }
122
123    /// Get the style for a slot or return an empty string.
124    pub fn get_or_empty(&self, slot: &S) -> &str {
125        self.values.get(slot).map(|s| s.as_str()).unwrap_or("")
126    }
127
128    /// Check if a semantic slot has a style.
129    pub fn contains(&self, slot: &S) -> bool {
130        self.values.contains_key(slot)
131    }
132
133    /// Create from an iterator of (slot, style) pairs.
134    pub fn from_iter<I, N>(iter: I) -> Self
135    where
136        I: IntoIterator<Item = (S, N)>,
137        N: Into<String>,
138    {
139        Self {
140            values: iter.into_iter().map(|(k, v)| (k, v.into())).collect(),
141        }
142    }
143}
144
145impl<S: Hash + Eq> PartialEq for SemanticStyles<S> {
146    fn eq(&self, other: &Self) -> bool {
147        self.values == other.values
148    }
149}
150
151// ============================================================================
152// Pre-defined semantic slot enums for common components
153// ============================================================================
154
155/// Semantic slots for Button component.
156#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
157pub enum ButtonSemantic {
158    Root,
159    Icon,
160    Content,
161}
162
163/// Semantic slots for Input component.
164#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
165pub enum InputSemantic {
166    Root,
167    Prefix,
168    Suffix,
169    Input,
170    Count,
171}
172
173/// Semantic slots for Select component.
174#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
175pub enum SelectSemantic {
176    Root,
177    Prefix,
178    Suffix,
179}
180
181/// Semantic slots for Select popup.
182#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
183pub enum SelectPopupSemantic {
184    Root,
185    List,
186    ListItem,
187}
188
189/// Semantic slots for Modal component.
190#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
191pub enum ModalSemantic {
192    Root,
193    Header,
194    Body,
195    Footer,
196    Container,
197    Title,
198    Wrapper,
199    Mask,
200}
201
202/// Semantic slots for Table component.
203#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
204pub enum TableSemantic {
205    Section,
206    Title,
207    Footer,
208    Content,
209    Root,
210}
211
212/// Semantic slots for Table body/header.
213#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
214pub enum TablePartSemantic {
215    Wrapper,
216    Cell,
217    Row,
218}
219
220/// Semantic slots for Tabs component.
221#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
222pub enum TabsSemantic {
223    Root,
224    Item,
225    Indicator,
226    Content,
227    Header,
228}
229
230/// Semantic slots for Collapse component.
231#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
232pub enum CollapseSemantic {
233    Root,
234    Header,
235    Title,
236    Body,
237    Icon,
238}
239
240/// Semantic slots for Form component.
241#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
242pub enum FormSemantic {
243    Root,
244    Label,
245    Content,
246}
247
248/// Semantic slots for Descriptions component.
249#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
250pub enum DescriptionsSemantic {
251    Root,
252    Header,
253    Title,
254    Extra,
255    Label,
256    Content,
257}
258
259/// Semantic slots for Timeline component.
260#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
261pub enum TimelineSemantic {
262    Root,
263    Item,
264    ItemTitle,
265    ItemContent,
266    Indicator,
267}
268
269/// Semantic slots for Anchor component.
270#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
271pub enum AnchorSemantic {
272    Root,
273    Item,
274    ItemTitle,
275    Indicator,
276}
277
278/// Semantic slots for Message component.
279#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
280pub enum MessageSemantic {
281    Root,
282    Content,
283    Icon,
284}
285
286/// Semantic slots for Notification component.
287#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
288pub enum NotificationSemantic {
289    Root,
290    Title,
291    Description,
292    Icon,
293    CloseButton,
294}
295
296// ============================================================================
297// Type aliases for convenience
298// ============================================================================
299
300pub type ButtonClassNames = SemanticClassNames<ButtonSemantic>;
301pub type ButtonStyles = SemanticStyles<ButtonSemantic>;
302
303pub type InputClassNames = SemanticClassNames<InputSemantic>;
304pub type InputStyles = SemanticStyles<InputSemantic>;
305
306pub type SelectClassNames = SemanticClassNames<SelectSemantic>;
307pub type SelectStyles = SemanticStyles<SelectSemantic>;
308
309pub type ModalClassNames = SemanticClassNames<ModalSemantic>;
310pub type ModalStyles = SemanticStyles<ModalSemantic>;
311
312pub type TableClassNames = SemanticClassNames<TableSemantic>;
313pub type TableStyles = SemanticStyles<TableSemantic>;
314
315pub type TabsClassNames = SemanticClassNames<TabsSemantic>;
316pub type TabsStyles = SemanticStyles<TabsSemantic>;
317
318pub type CollapseClassNames = SemanticClassNames<CollapseSemantic>;
319pub type CollapseStyles = SemanticStyles<CollapseSemantic>;
320
321pub type FormClassNames = SemanticClassNames<FormSemantic>;
322pub type FormStyles = SemanticStyles<FormSemantic>;
323
324// ============================================================================
325// Helper trait for merging class names
326// ============================================================================
327
328/// Extension trait for building class lists with semantic classes.
329pub trait ClassListExt {
330    /// Push a semantic class if it exists.
331    fn push_semantic<S: Hash + Eq>(&mut self, class_names: &Option<SemanticClassNames<S>>, slot: S);
332}
333
334impl ClassListExt for Vec<String> {
335    fn push_semantic<S: Hash + Eq>(
336        &mut self,
337        class_names: &Option<SemanticClassNames<S>>,
338        slot: S,
339    ) {
340        if let Some(cn) = class_names {
341            if let Some(class) = cn.get(&slot) {
342                if !class.is_empty() {
343                    self.push(class.to_string());
344                }
345            }
346        }
347    }
348}
349
350/// Extension trait for building style strings with semantic styles.
351pub trait StyleStringExt {
352    /// Append a semantic style if it exists.
353    fn append_semantic<S: Hash + Eq>(&mut self, styles: &Option<SemanticStyles<S>>, slot: S);
354}
355
356impl StyleStringExt for String {
357    fn append_semantic<S: Hash + Eq>(&mut self, styles: &Option<SemanticStyles<S>>, slot: S) {
358        if let Some(s) = styles {
359            if let Some(style) = s.get(&slot) {
360                if !style.is_empty() {
361                    if !self.is_empty() && !self.ends_with(';') {
362                        self.push(';');
363                    }
364                    self.push_str(style);
365                }
366            }
367        }
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn semantic_class_names_basic_operations() {
377        let mut cn = SemanticClassNames::<ButtonSemantic>::new();
378        assert!(cn.get(&ButtonSemantic::Root).is_none());
379
380        cn.set(ButtonSemantic::Root, "custom-root");
381        assert_eq!(cn.get(&ButtonSemantic::Root), Some("custom-root"));
382        assert!(cn.contains(&ButtonSemantic::Root));
383        assert!(!cn.contains(&ButtonSemantic::Icon));
384    }
385
386    #[test]
387    fn semantic_styles_basic_operations() {
388        let mut styles = SemanticStyles::<ModalSemantic>::new();
389        assert!(styles.get(&ModalSemantic::Body).is_none());
390
391        styles.set(ModalSemantic::Body, "padding: 24px;");
392        assert_eq!(styles.get(&ModalSemantic::Body), Some("padding: 24px;"));
393    }
394
395    #[test]
396    fn class_list_ext_works() {
397        let mut cn = SemanticClassNames::<ButtonSemantic>::new();
398        cn.set(ButtonSemantic::Root, "my-button");
399
400        let mut classes = vec!["adui-btn".to_string()];
401        classes.push_semantic(&Some(cn), ButtonSemantic::Root);
402        assert_eq!(classes, vec!["adui-btn", "my-button"]);
403    }
404
405    #[test]
406    fn style_string_ext_works() {
407        let mut styles = SemanticStyles::<ModalSemantic>::new();
408        styles.set(ModalSemantic::Body, "padding: 24px");
409
410        let mut style_str = "background: white".to_string();
411        style_str.append_semantic(&Some(styles), ModalSemantic::Body);
412        assert_eq!(style_str, "background: white;padding: 24px");
413    }
414}
415
416
417
418