css_in_rs/
style_provider.rs

1use core::cell::RefCell;
2use std::{collections::btree_map::Entry, rc::Rc};
3
4#[doc_cfg(feature = "dioxus")]
5use dioxus::core::ScopeState;
6
7use doc_cfg::doc_cfg;
8
9use crate::{
10    backend::{Backend, CssGeneratorFn},
11    Classes, Theme,
12};
13
14/// Manages dynamically inserted styles. You should usually have exactly one.
15/// Generated classnames are only unique for a fixed [StyleProvider].
16///
17/// You will typically use [StyleProvider] in comination with the [`crate::make_styles!`]
18/// macro.
19///
20/// # Example
21/// ```no_run
22/// # use css_in_rs::{make_styles, EmptyTheme, StyleProvider};
23/// make_styles! {
24///     (_theme: EmptyTheme) -> MyClasses {
25///         ".my_class > span" {
26///             color: "red",
27///         },
28///     }
29/// }
30///
31/// fn main() {
32///     let style_provider = StyleProvider::quickstart_web(EmptyTheme);
33///     
34///     // inject the css styles
35///     let cls = style_provider.add_classes::<MyClasses>();
36///     let elem: &web_sys::Element = todo!(); // Some element
37///     elem.set_class_name(&cls.my_class);
38///     
39///     // inject it again; no change; will return the same classes
40///     let cls2 = style_provider.add_classes::<MyClasses>();
41///     assert_eq!(cls.my_class, cls2.my_class);
42/// }
43/// ```
44#[derive(Clone)]
45pub struct StyleProvider<T> {
46    inner: Rc<RefCell<Inner<T>>>,
47}
48
49impl<T: Theme> StyleProvider<T> {
50    /// Quickly sets up a [StyleProvider] for the given theme at the active document.
51    /// It will create a new `style` tag to the head and use it to mount new styles.
52    #[cfg(feature = "web-sys")]
53    pub fn quickstart_web(theme: T) -> Self {
54        let inner = Inner::quickstart_web(theme);
55        let inner = Rc::new(RefCell::new(inner));
56
57        StyleProvider { inner }
58    }
59
60    fn add_css_generator(&self, generator: CssGeneratorFn<T>) -> u64 {
61        self.inner.borrow_mut().add_css_generator(generator)
62    }
63
64    /// Mount new styles and returns the dynamically generated classnames.
65    /// If this style is already mounted, it won't be mounted again. The classnames
66    /// will be the same as last time.
67    pub fn add_classes<C>(&self) -> C
68    where
69        C: Classes<Theme = T>,
70    {
71        let start = self.add_css_generator(C::generate);
72        C::new(start)
73    }
74
75    /// Change the theme. All styles will be recomputed, but the classnames will
76    /// not change.
77    pub fn update_theme(&self, theme: T) {
78        self.inner.borrow_mut().update_theme(theme);
79    }
80
81    /// A convenience hook to mount styles and cache the classnames.
82    /// Note that the style will only be mounted once, even if you use this
83    /// hook from multiple components or your components will be used multiple
84    /// times. The classnames will be the same every time, as long as the
85    /// same [StyleProvider] is used.
86    #[doc_cfg(feature = "dioxus")]
87    pub fn use_styles<'a, C>(&self, cx: &'a ScopeState) -> &'a C
88    where
89        C: Classes<Theme = T>,
90    {
91        cx.use_hook(|| self.add_classes())
92    }
93}
94
95struct CssGenerator<T> {
96    generator: CssGeneratorFn<T>,
97    start: u64,
98    stop: u64,
99}
100
101impl<T: Theme> CssGenerator<T> {
102    fn generate(&self, theme: &T, css: &mut String) {
103        let mut counter = self.start;
104        (self.generator)(theme, css, &mut counter);
105        assert_eq!(counter, self.stop);
106    }
107}
108
109struct Inner<T> {
110    backend: Box<dyn Backend<T>>,
111    current_theme: T,
112    generators: Vec<CssGenerator<T>>,
113    generator_to_idx: std::collections::BTreeMap<CssGeneratorFn<T>, usize>,
114    counter: u64,
115}
116
117impl<T: Theme> Inner<T> {
118    #[cfg(feature = "web-sys")]
119    pub fn quickstart_web(theme: T) -> Self {
120        let backend = crate::backend::web::WebSysBackend::quickstart();
121        Self::new_with_backend(backend, theme)
122    }
123
124    pub fn new_with_backend<B: Backend<T>>(backend: B, theme: T) -> Self {
125        let backend = Box::new(backend);
126        Self {
127            backend,
128            current_theme: theme,
129            generators: Default::default(),
130            generator_to_idx: Default::default(),
131            counter: 0,
132        }
133    }
134
135    pub fn add_css_generator(&mut self, generator: CssGeneratorFn<T>) -> u64 {
136        debug_assert_eq!(self.generator_to_idx.len(), self.generators.len());
137
138        match self.generator_to_idx.entry(generator) {
139            Entry::Vacant(vac) => {
140                vac.insert(self.generators.len());
141            }
142            Entry::Occupied(occ) => {
143                let idx = *occ.get();
144                return self.generators[idx].start;
145            }
146        }
147
148        let start = self.counter;
149        self.backend
150            .run_css_generator(generator, &self.current_theme, &mut self.counter);
151        let stop = self.counter;
152        let generator = CssGenerator {
153            generator,
154            start,
155            stop,
156        };
157
158        self.generators.push(generator);
159        start
160    }
161
162    fn update(&mut self) {
163        let mut css = String::default();
164        for generator in &self.generators {
165            generator.generate(&self.current_theme, &mut css);
166        }
167
168        self.backend.replace_all(css);
169    }
170
171    pub fn update_theme(&mut self, theme: T) {
172        if !self.current_theme.fast_cmp(&theme) {
173            self.current_theme = theme;
174            self.update();
175        }
176    }
177}