druid_widget_nursery/theme_loader/
widget.rs1use 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
11pub 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 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 }
83 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}