druid_widget_nursery/theme_loader/
widget.rs

1use std::path::PathBuf;
2
3#[cfg(feature = "notify")]
4use druid::ExtEventSink;
5use druid::{widget::prelude::*, Selector};
6
7use crate::theme_loader::{LoadableTheme, ThemeLoadError};
8
9pub const RELOAD_THEME: Selector<()> = Selector::new("runebender.theme-loader-reload");
10
11/// A widget that loads a theme from file and applies it to the [`Env`].
12///
13/// This can optionally reload the theme when it changes, if the `notify`
14/// feature is enabled.
15pub struct ThemeLoader<T, W> {
16    theme_path: PathBuf,
17    theme: T,
18    current_env: Option<Env>,
19    inner: W,
20}
21
22impl<T: LoadableTheme, W> ThemeLoader<T, W> {
23    /// Create a new `ThemeLoader`.
24    ///
25    /// The `path` argument should be a path to the theme file. The `them`
26    /// argument is a theme generated by the [`loadable_theme!`] macro.
27    ///
28    /// [`loadable_theme!`] crate::loadable_theme
29    pub fn new(path: impl Into<PathBuf>, theme: T, inner: W) -> Self {
30        ThemeLoader {
31            theme_path: path.into(),
32            theme,
33            inner,
34            current_env: None,
35        }
36    }
37
38    fn add_env_to_theme(&mut self, env: &Env) -> Result<Env, ThemeLoadError> {
39        let file_contents = std::fs::read_to_string(&self.theme_path)?;
40        let contents = iter_items(&file_contents).collect::<Result<_, _>>()?;
41        self.theme.load(&contents, env)
42    }
43
44    fn reload_theme_and_log_errors(&mut self, env: &Env) {
45        match self.add_env_to_theme(env) {
46            Ok(new_env) => self.current_env = Some(new_env),
47            Err(e) => log::error!("error loading theme file: {}", e),
48        }
49    }
50}
51
52fn iter_items(s: &str) -> impl Iterator<Item = Result<(&str, &str), ThemeLoadError>> {
53    s.lines().filter_map(|line| {
54        if line.trim().is_empty() {
55            None
56        } else {
57            let mut split = line.split(':');
58            match (split.next(), split.next(), split.next()) {
59                (Some(key), Some(val), None) => Some(Ok((key.trim(), val.trim()))),
60                _ => Some(Err(ThemeLoadError::ParseThemeLineError(line.to_string()))),
61            }
62        }
63    })
64}
65
66impl<T, S, W> Widget<T> for ThemeLoader<S, W>
67where
68    T: Data,
69    S: LoadableTheme,
70    W: Widget<T>,
71{
72    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
73        match event {
74            Event::WindowConnected => {
75                #[cfg(feature = "notify")]
76                {
77                    let event_snk = ctx.get_external_handle();
78                    start_watcher(event_snk, self.theme_path.clone(), ctx.widget_id());
79                }
80                self.reload_theme_and_log_errors(env);
81                // spin up our watcher thread
82            }
83            // FIXME: this is when we receive the command back from the watcher thread
84            Event::Command(cmd) if cmd.is(RELOAD_THEME) => {
85                log::info!("reloading theme");
86                self.reload_theme_and_log_errors(env);
87                ctx.request_layout();
88                ctx.set_handled();
89            }
90            _ => (),
91        }
92        let child_env = self.current_env.as_ref().unwrap_or(env);
93        self.inner.event(ctx, event, data, child_env);
94    }
95
96    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
97        let child_env = self.current_env.as_ref().unwrap_or(env);
98        self.inner.lifecycle(ctx, event, data, child_env)
99    }
100
101    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) {
102        if ctx.env_changed() {
103            self.reload_theme_and_log_errors(env);
104        }
105
106        let child_env = self.current_env.as_ref().unwrap_or(env);
107        self.inner.update(ctx, old_data, data, child_env);
108    }
109
110    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
111        let child_env = self.current_env.as_ref().unwrap_or(env);
112        self.inner.layout(ctx, bc, data, child_env)
113    }
114
115    fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
116        let child_env = self.current_env.as_ref().unwrap_or(env);
117        self.inner.paint(ctx, data, child_env);
118    }
119}
120
121#[cfg(feature = "notify")]
122fn start_watcher(sink: ExtEventSink, path: PathBuf, target: WidgetId) {
123    use notify::{DebouncedEvent, RecursiveMode, Watcher};
124    use std::sync::mpsc;
125    use std::time::Duration;
126    std::thread::spawn(move || {
127        let (tx, rx) = mpsc::channel();
128        let mut watcher = notify::watcher(tx, Duration::from_millis(500)).unwrap();
129        if let Err(e) = watcher.watch(&path, RecursiveMode::NonRecursive) {
130            log::error!(
131                "theme watcher failed to watch path '{}': '{}'",
132                path.to_string_lossy(),
133                e
134            );
135            return;
136        }
137
138        loop {
139            match rx.recv() {
140                Ok(DebouncedEvent::Write(_)) => {
141                    log::info!("sending reload command");
142                    if sink.submit_command(RELOAD_THEME, (), target).is_err() {
143                        break;
144                    }
145                }
146                Ok(other) => log::debug!("other event {:?}", other),
147                Err(e) => {
148                    log::error!("watch error: {:?}", e);
149                    break;
150                }
151            }
152        }
153    });
154}