floem_css/
provider.rs

1use std::path::PathBuf;
2use std::rc::Rc;
3
4use crossbeam_channel::{Receiver, Sender};
5use floem::ext_event::create_signal_from_channel;
6use floem::reactive::{create_effect, provide_context, RwSignal, SignalGet, SignalUpdate};
7use floem::IntoView;
8
9use crate::error::ThemeError;
10use crate::observer::FileObserver;
11use crate::parser::parse_css;
12use crate::style::StyleMap;
13use crate::ProviderOptions;
14
15pub struct StyleProvider {
16    path: PathBuf,
17    channel: (Sender<()>, Receiver<()>),
18    pub(crate) map: RwSignal<StyleMap>,
19    #[allow(unused)]
20    observer: FileObserver,
21}
22
23impl StyleProvider {
24    /// # Errors
25    ///
26    /// Returns `ThemeError` if `path` cannot be read
27    pub fn new(options: ProviderOptions) -> Result<Self, ThemeError> {
28        Self::try_from(options)
29    }
30
31    /// # Errors
32    /// Errors if path cannot be read
33    ///
34    /// # Panics
35    /// Panics only in debug mode if time is flowing into wrong direction
36    fn reload(&self) -> Result<(), ThemeError> {
37        let now = std::time::SystemTime::now();
38        let styles_str = floem_css_parser::read_styles(&self.path)?;
39        let s = styles_str.clone();
40        std::thread::spawn(move || {
41            floem_css_parser::analyze(&s);
42        });
43        let parsed_styles = parse_css(&styles_str);
44        if parsed_styles.is_empty() {
45            log::warn!("Styles parsed but no styles found");
46        }
47        self.map.update(|map| {
48            map.clear();
49            let _ = std::mem::replace(map, parsed_styles);
50        });
51        {
52            let elaps = std::time::SystemTime::now()
53                .duration_since(now)
54                .expect("Time is going backwards");
55            if elaps.as_millis() == 0 {
56                log::debug!("Styles parsed in {}μs", elaps.as_micros());
57            } else {
58                log::debug!("Styles parsed in {}ms", elaps.as_millis());
59            }
60        }
61        Ok(())
62    }
63}
64
65impl TryFrom<ProviderOptions> for StyleProvider {
66    type Error = ThemeError;
67    fn try_from(options: ProviderOptions) -> Result<Self, Self::Error> {
68        let channel = crossbeam_channel::unbounded();
69        let observer = FileObserver::new(&options.path, channel.0.clone(), options.recursive)?;
70        let theme = Self {
71            path: options.path,
72            observer,
73            channel,
74            map: RwSignal::new(StyleMap::new_const()),
75        };
76        Ok(theme)
77    }
78}
79
80/// Wrapper function that provides all necessary things in context
81/// for hot reloading to work
82///
83/// ## Example
84///
85/// ### style.css
86/// ```css
87/// body {
88///     flex-grow: 1;
89/// }
90///
91/// my-header {
92///     font-size: 32px;
93///     font-weight: 600;
94/// }
95/// ```
96///### main.rs
97/// ```rust
98/// use floem::views::{container, text};
99/// use floem::IntoView;
100/// use floem_css::{theme_provider, ProviderOptions, StyleCss};
101///
102/// fn main() {
103///     // Styles are read from this path.
104///     // Modify the css file to instantly see changes in app.
105///     // Path can point to file or folder.
106///     let options = ProviderOptions {
107///         path: "./examples/style.css".into(),
108///         ..Default::default()
109///     };
110///
111///     // Wrap your app in theme_provider and launch
112///     floem::launch(|| theme_provider(main_view, options))
113/// }
114///
115/// fn main_view() -> impl IntoView {
116///     let my_text = text("Change my style").css("my-header");
117///     container(my_text).css("body")
118/// }
119/// ```
120///
121/// # Panics
122///
123/// Panics if options path doesn't exist in filesystem or is otherwise unreadable
124pub fn theme_provider<V, F>(child: F, options: ProviderOptions) -> V
125where
126    F: Fn() -> V,
127    V: IntoView + 'static,
128{
129    let theme = StyleProvider::new(options).expect("Invalid theme path");
130    theme.reload().expect("Cannot load theme");
131    let observer_event = create_signal_from_channel(theme.channel.1.clone());
132    let rc_theme = Rc::new(theme);
133    provide_context(rc_theme.clone());
134    create_effect(move |_| {
135        if observer_event.get().is_some() {
136            if let Err(e) = rc_theme.reload() {
137                log::error!("Cannot reload theme: {e}");
138            }
139        }
140    });
141    child()
142}