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}
44
45fn direction_from_locale(locale: &LanguageIdentifier) -> Direction {
46    match locale.character_direction() {
47        CharacterDirection::RTL => Direction::RightToLeft,
48        _ => Direction::LeftToRight,
49    }
50}
51
52fn apply_direction_class(cx: &mut EventContext, direction: Direction) {
53    let rtl = direction == Direction::RightToLeft;
54    let window_entities = cx.windows.keys().copied().collect::<Vec<_>>();
55
56    cx.with_current(Entity::root(), |cx| {
57        cx.toggle_class("rtl", rtl);
58    });
59
60    for window_entity in window_entities {
61        cx.with_current(window_entity, |cx| {
62            cx.toggle_class("rtl", rtl);
63        });
64    }
65}
66
67fn detect_theme() -> ThemeMode {
68    #[cfg(target_os = "linux")]
69    {
70        let mundy_prefs =
71            Preferences::once_blocking(Interest::ColorScheme, Duration::from_millis(100));
72
73        if let Some(preferences) = mundy_prefs
74            && preferences.color_scheme == mundy::ColorScheme::Dark
75        {
76            ThemeMode::DarkMode
77        } else {
78            ThemeMode::LightMode
79        }
80    }
81
82    #[cfg(not(target_os = "linux"))]
83    {
84        ThemeMode::LightMode
85    }
86}
87
88impl Environment {
89    pub(crate) fn new(cx: &mut Context) -> Self {
90        let locale: LanguageIdentifier =
91            sys_locale::get_locale().and_then(|l| l.parse().ok()).unwrap_or_default();
92        let caret_timer = cx.add_timer(Duration::from_millis(530), None, |cx, action| {
93            if matches!(action, TimerAction::Tick(_)) {
94                cx.emit(TextEvent::ToggleCaret);
95            }
96        });
97        let direction = direction_from_locale(&locale);
98        Self {
99            locale: Signal::new(locale.clone()),
100            direction: Signal::new(direction),
101            double_click_interval: Duration::from_millis(500),
102            tooltip_delay: Duration::from_millis(1500),
103            theme_mode: ThemeMode::default(),
104            system_theme_mode: detect_theme(),
105            caret_timer,
106            drag_distance: Signal::new(4),
107        }
108    }
109
110    /// Returns the effective (resolved) theme, substituting the OS theme when the
111    /// user preference is [`ThemeMode::System`].
112    pub fn effective_theme(&self) -> ThemeMode {
113        match self.theme_mode {
114            ThemeMode::System => self.system_theme_mode,
115            other => other,
116        }
117    }
118}
119
120/// Events for setting the state in the [Environment].
121pub enum EnvironmentEvent {
122    /// Set the locale used for the whole application.
123    SetLocale(LanguageIdentifier),
124    /// Set the text and layout direction used by the whole application.
125    SetDirection(Direction),
126    /// Set the default theme mode.
127    // TODO: add SetSysTheme event when the winit `set_theme` fixed.
128    SetThemeMode(ThemeMode),
129    /// Reset the locale to use the system provided locale.
130    UseSystemLocale,
131    /// Alternate between dark and light theme modes.
132    ToggleThemeMode,
133    /// Set the maximum interval between two clicks to be recognised as a double-click.
134    SetDoubleClickInterval(Duration),
135    /// Set the delay before a tooltip fades in.
136    SetTooltipDelay(Duration),
137    /// Set the distance the mouse has to be dragged to start a drag operation.
138    SetDragDistance(u32),
139}
140
141impl Model for Environment {
142    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
143        event.take(|event, _| match event {
144            EnvironmentEvent::SetLocale(locale) => {
145                self.locale.set(locale.clone());
146                let direction = direction_from_locale(&locale);
147                self.direction.set(direction);
148                apply_direction_class(cx, direction);
149                cx.reload_styles().unwrap();
150            }
151
152            EnvironmentEvent::SetDirection(direction) => {
153                self.direction.set_if_changed(direction);
154                apply_direction_class(cx, direction);
155                cx.reload_styles().unwrap();
156            }
157
158            EnvironmentEvent::SetThemeMode(theme) => {
159                self.theme_mode = theme;
160                let is_dark = self.effective_theme() == ThemeMode::DarkMode;
161                cx.with_current(Entity::root(), |cx| {
162                    cx.toggle_class("dark", is_dark);
163                });
164                cx.reload_styles().unwrap();
165            }
166
167            EnvironmentEvent::UseSystemLocale => {
168                let locale: LanguageIdentifier =
169                    sys_locale::get_locale().map(|l| l.parse().unwrap()).unwrap_or_default();
170                let direction = direction_from_locale(&locale);
171                self.locale.set(locale);
172                self.direction.set(direction);
173                apply_direction_class(cx, direction);
174                cx.reload_styles().unwrap();
175            }
176
177            EnvironmentEvent::ToggleThemeMode => {
178                let theme_mode = match self.theme_mode {
179                    ThemeMode::System => ThemeMode::System,
180                    ThemeMode::DarkMode => ThemeMode::LightMode,
181                    ThemeMode::LightMode => ThemeMode::DarkMode,
182                };
183
184                self.theme_mode = theme_mode;
185
186                let is_dark = self.effective_theme() == ThemeMode::DarkMode;
187                cx.with_current(Entity::root(), |cx| {
188                    cx.toggle_class("dark", is_dark);
189                });
190
191                cx.reload_styles().unwrap();
192            }
193
194            EnvironmentEvent::SetDoubleClickInterval(interval) => {
195                self.double_click_interval = interval;
196            }
197
198            EnvironmentEvent::SetTooltipDelay(delay) => {
199                self.tooltip_delay = delay;
200            }
201
202            EnvironmentEvent::SetDragDistance(distance) => {
203                self.drag_distance.set_if_changed(distance);
204            }
205        });
206
207        event.map(|event, _| match event {
208            WindowEvent::ThemeChanged(theme) => {
209                self.system_theme_mode = *theme;
210                if self.theme_mode == ThemeMode::System {
211                    let is_dark = self.system_theme_mode == ThemeMode::DarkMode;
212                    cx.with_current(Entity::root(), |cx| {
213                        cx.toggle_class("dark", is_dark);
214                    });
215                    cx.reload_styles().unwrap();
216                }
217            }
218            _ => (),
219        })
220    }
221}