Skip to main content

vizia_core/
environment.rs

1//! A model for system specific state which can be accessed by any model or view.
2use crate::prelude::*;
3
4#[cfg(target_os = "linux")]
5use mundy::Interest;
6#[cfg(target_os = "linux")]
7use mundy::Preferences;
8use unic_langid::CharacterDirection;
9use unic_langid::LanguageIdentifier;
10
11/// And enum which represents the current built-in theme mode.
12#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
13pub enum ThemeMode {
14    /// Follow the system theme.
15    #[default]
16    System,
17    /// The built-in vizia dark theme.
18    DarkMode,
19    /// The built-in vizia light theme.
20    LightMode,
21}
22
23use crate::{context::EventContext, events::Event};
24
25/// A model for system specific state which can be accessed by any model or view.
26pub struct Environment {
27    /// The locale used for localization.
28    pub locale: Signal<LanguageIdentifier>,
29    /// The text and layout direction used by the application.
30    pub direction: Signal<Direction>,
31    /// The maximum interval between two clicks to be recognised as a double-click.
32    pub double_click_interval: Duration,
33    /// The delay before a tooltip fades in.
34    pub tooltip_delay: Duration,
35    /// The user's theme preference (may be `System` to follow the OS).
36    pub theme_mode: ThemeMode,
37    /// The OS-reported system theme (always `DarkMode` or `LightMode`, never `System`).
38    pub system_theme_mode: ThemeMode,
39    /// The timer used to blink the caret of a textbox.
40    pub(crate) caret_timer: Timer,
41    /// The distance the mouse has to be dragged to start a drag operation.
42    pub drag_distance: Signal<u32>,
43    /// Whether the layout debug overlay is enabled. When set, views which underwent layout in the
44    /// most recent layout pass are outlined in orange until the next layout pass.
45    pub debug_layout: bool,
46}
47
48fn direction_from_locale(locale: &LanguageIdentifier) -> Direction {
49    match locale.character_direction() {
50        CharacterDirection::RTL => Direction::RightToLeft,
51        _ => Direction::LeftToRight,
52    }
53}
54
55fn apply_direction_class(cx: &mut EventContext, direction: Direction) {
56    let rtl = direction == Direction::RightToLeft;
57    let window_entities = cx.windows.keys().copied().collect::<Vec<_>>();
58
59    cx.with_current(Entity::root(), |cx| {
60        cx.toggle_class("rtl", rtl);
61    });
62
63    for window_entity in window_entities {
64        cx.with_current(window_entity, |cx| {
65            cx.toggle_class("rtl", rtl);
66        });
67    }
68}
69
70fn detect_theme() -> ThemeMode {
71    #[cfg(target_os = "linux")]
72    {
73        let mundy_prefs =
74            Preferences::once_blocking(Interest::ColorScheme, Duration::from_millis(100));
75
76        if let Some(preferences) = mundy_prefs
77            && preferences.color_scheme == mundy::ColorScheme::Dark
78        {
79            ThemeMode::DarkMode
80        } else {
81            ThemeMode::LightMode
82        }
83    }
84
85    #[cfg(not(target_os = "linux"))]
86    {
87        ThemeMode::LightMode
88    }
89}
90
91impl Environment {
92    pub(crate) fn new(cx: &mut Context) -> Self {
93        let locale: LanguageIdentifier =
94            sys_locale::get_locale().and_then(|l| l.parse().ok()).unwrap_or_default();
95        let caret_timer = cx.add_timer(Duration::from_millis(530), None, |cx, action| {
96            if matches!(action, TimerAction::Tick(_)) {
97                cx.emit(TextEvent::ToggleCaret);
98            }
99        });
100        let direction = direction_from_locale(&locale);
101        cx.style.debug_layout = false;
102        Self {
103            locale: Signal::new(locale.clone()),
104            direction: Signal::new(direction),
105            double_click_interval: Duration::from_millis(500),
106            tooltip_delay: Duration::from_millis(1500),
107            theme_mode: ThemeMode::default(),
108            system_theme_mode: detect_theme(),
109            caret_timer,
110            drag_distance: Signal::new(4),
111            debug_layout: false,
112        }
113    }
114
115    /// Returns the effective (resolved) theme, substituting the OS theme when the
116    /// user preference is [`ThemeMode::System`].
117    pub fn effective_theme(&self) -> ThemeMode {
118        match self.theme_mode {
119            ThemeMode::System => self.system_theme_mode,
120            other => other,
121        }
122    }
123}
124
125/// Events for setting the state in the [Environment].
126pub enum EnvironmentEvent {
127    /// Set the locale used for the whole application.
128    SetLocale(LanguageIdentifier),
129    /// Set the text and layout direction used by the whole application.
130    SetDirection(Direction),
131    /// Set the default theme mode.
132    // TODO: add SetSysTheme event when the winit `set_theme` fixed.
133    SetThemeMode(ThemeMode),
134    /// Reset the locale to use the system provided locale.
135    UseSystemLocale,
136    /// Alternate between dark and light theme modes.
137    ToggleThemeMode,
138    /// Set the maximum interval between two clicks to be recognised as a double-click.
139    SetDoubleClickInterval(Duration),
140    /// Set the delay before a tooltip fades in.
141    SetTooltipDelay(Duration),
142    /// Set the distance the mouse has to be dragged to start a drag operation.
143    SetDragDistance(u32),
144    /// Enable or disable the layout debug overlay.
145    SetDebugLayout(bool),
146    /// Toggle the layout debug overlay on or off.
147    ToggleDebugLayout,
148}
149
150impl Model for Environment {
151    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
152        event.take(|event, _| match event {
153            EnvironmentEvent::SetLocale(locale) => {
154                self.locale.set(locale.clone());
155                let direction = direction_from_locale(&locale);
156                self.direction.set(direction);
157                apply_direction_class(cx, direction);
158                cx.reload_styles().unwrap();
159            }
160
161            EnvironmentEvent::SetDirection(direction) => {
162                self.direction.set_if_changed(direction);
163                apply_direction_class(cx, direction);
164                cx.reload_styles().unwrap();
165            }
166
167            EnvironmentEvent::SetThemeMode(theme) => {
168                self.theme_mode = theme;
169                let is_dark = self.effective_theme() == ThemeMode::DarkMode;
170                cx.with_current(Entity::root(), |cx| {
171                    cx.toggle_class("dark", is_dark);
172                });
173                cx.reload_styles().unwrap();
174            }
175
176            EnvironmentEvent::UseSystemLocale => {
177                let locale: LanguageIdentifier =
178                    sys_locale::get_locale().map(|l| l.parse().unwrap()).unwrap_or_default();
179                let direction = direction_from_locale(&locale);
180                self.locale.set(locale);
181                self.direction.set(direction);
182                apply_direction_class(cx, direction);
183                cx.reload_styles().unwrap();
184            }
185
186            EnvironmentEvent::ToggleThemeMode => {
187                let theme_mode = match self.theme_mode {
188                    ThemeMode::System => ThemeMode::System,
189                    ThemeMode::DarkMode => ThemeMode::LightMode,
190                    ThemeMode::LightMode => ThemeMode::DarkMode,
191                };
192
193                self.theme_mode = theme_mode;
194
195                let is_dark = self.effective_theme() == ThemeMode::DarkMode;
196                cx.with_current(Entity::root(), |cx| {
197                    cx.toggle_class("dark", is_dark);
198                });
199
200                cx.reload_styles().unwrap();
201            }
202
203            EnvironmentEvent::SetDoubleClickInterval(interval) => {
204                self.double_click_interval = interval;
205            }
206
207            EnvironmentEvent::SetTooltipDelay(delay) => {
208                self.tooltip_delay = delay;
209            }
210
211            EnvironmentEvent::SetDragDistance(distance) => {
212                self.drag_distance.set_if_changed(distance);
213            }
214
215            EnvironmentEvent::SetDebugLayout(enabled) => {
216                self.debug_layout = enabled;
217                cx.style.debug_layout = enabled;
218                if !enabled {
219                    cx.style.laid_out.clear();
220                }
221                cx.needs_redraw();
222            }
223
224            EnvironmentEvent::ToggleDebugLayout => {
225                self.debug_layout = !self.debug_layout;
226                cx.style.debug_layout = self.debug_layout;
227                if !self.debug_layout {
228                    cx.style.laid_out.clear();
229                }
230                cx.needs_redraw();
231            }
232        });
233
234        event.map(|event, _| match event {
235            WindowEvent::ThemeChanged(theme) => {
236                self.system_theme_mode = *theme;
237                if self.theme_mode == ThemeMode::System {
238                    let is_dark = self.system_theme_mode == ThemeMode::DarkMode;
239                    cx.with_current(Entity::root(), |cx| {
240                        cx.toggle_class("dark", is_dark);
241                    });
242                    cx.reload_styles().unwrap();
243                }
244            }
245            _ => (),
246        })
247    }
248}