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
4use unic_langid::CharacterDirection;
5use unic_langid::LanguageIdentifier;
6
7/// And enum which represents the current built-in theme mode.
8#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
9pub enum ThemeMode {
10    /// Follow the system theme.
11    #[default]
12    System,
13    /// The built-in vizia dark theme.
14    DarkMode,
15    /// The built-in vizia light theme.
16    LightMode,
17}
18
19use crate::{context::EventContext, events::Event};
20
21/// A model for system specific state which can be accessed by any model or view.
22pub struct Environment {
23    /// The locale used for localization.
24    pub locale: Signal<LanguageIdentifier>,
25    /// The text and layout direction used by the application.
26    pub direction: Signal<Direction>,
27    /// The maximum interval between two clicks to be recognised as a double-click.
28    pub double_click_interval: Duration,
29    /// The delay before a tooltip fades in.
30    pub tooltip_delay: Duration,
31    /// The user's theme preference (may be `System` to follow the OS).
32    pub theme_mode: ThemeMode,
33    /// The OS-reported system theme (always `DarkMode` or `LightMode`, never `System`).
34    pub system_theme_mode: ThemeMode,
35    /// The timer used to blink the caret of a textbox.
36    pub(crate) caret_timer: Timer,
37}
38
39fn direction_from_locale(locale: &LanguageIdentifier) -> Direction {
40    match locale.character_direction() {
41        CharacterDirection::RTL => Direction::RightToLeft,
42        _ => Direction::LeftToRight,
43    }
44}
45
46fn apply_direction_class(cx: &mut EventContext, direction: Direction) {
47    let rtl = direction == Direction::RightToLeft;
48    let window_entities = cx.windows.keys().copied().collect::<Vec<_>>();
49
50    cx.with_current(Entity::root(), |cx| {
51        cx.toggle_class("rtl", rtl);
52    });
53
54    for window_entity in window_entities {
55        cx.with_current(window_entity, |cx| {
56            cx.toggle_class("rtl", rtl);
57        });
58    }
59}
60
61impl Environment {
62    pub(crate) fn new(cx: &mut Context) -> Self {
63        let locale: LanguageIdentifier =
64            sys_locale::get_locale().and_then(|l| l.parse().ok()).unwrap_or_default();
65        let caret_timer = cx.add_timer(Duration::from_millis(530), None, |cx, action| {
66            if matches!(action, TimerAction::Tick(_)) {
67                cx.emit(TextEvent::ToggleCaret);
68            }
69        });
70        let direction = direction_from_locale(&locale);
71        Self {
72            locale: Signal::new(locale.clone()),
73            direction: Signal::new(direction),
74            double_click_interval: Duration::from_millis(500),
75            tooltip_delay: Duration::from_millis(1500),
76            theme_mode: ThemeMode::default(),
77            system_theme_mode: ThemeMode::LightMode,
78            caret_timer,
79        }
80    }
81
82    /// Returns the effective (resolved) theme, substituting the OS theme when the
83    /// user preference is [`ThemeMode::System`].
84    pub fn effective_theme(&self) -> ThemeMode {
85        match self.theme_mode {
86            ThemeMode::System => self.system_theme_mode,
87            other => other,
88        }
89    }
90}
91
92/// Events for setting the state in the [Environment].
93pub enum EnvironmentEvent {
94    /// Set the locale used for the whole application.
95    SetLocale(LanguageIdentifier),
96    /// Set the text and layout direction used by the whole application.
97    SetDirection(Direction),
98    /// Set the default theme mode.
99    // TODO: add SetSysTheme event when the winit `set_theme` fixed.
100    SetThemeMode(ThemeMode),
101    /// Reset the locale to use the system provided locale.
102    UseSystemLocale,
103    /// Alternate between dark and light theme modes.
104    ToggleThemeMode,
105    /// Set the maximum interval between two clicks to be recognised as a double-click.
106    SetDoubleClickInterval(Duration),
107    /// Set the delay before a tooltip fades in.
108    SetTooltipDelay(Duration),
109}
110
111impl Model for Environment {
112    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
113        event.take(|event, _| match event {
114            EnvironmentEvent::SetLocale(locale) => {
115                self.locale.set(locale.clone());
116                let direction = direction_from_locale(&locale);
117                self.direction.set(direction);
118                apply_direction_class(cx, direction);
119                cx.reload_styles().unwrap();
120            }
121
122            EnvironmentEvent::SetDirection(direction) => {
123                self.direction.set_if_changed(direction);
124                apply_direction_class(cx, direction);
125                cx.reload_styles().unwrap();
126            }
127
128            EnvironmentEvent::SetThemeMode(theme) => {
129                self.theme_mode = theme;
130                let is_dark = self.effective_theme() == ThemeMode::DarkMode;
131                cx.with_current(Entity::root(), |cx| {
132                    cx.toggle_class("dark", is_dark);
133                });
134                cx.reload_styles().unwrap();
135            }
136
137            EnvironmentEvent::UseSystemLocale => {
138                let locale: LanguageIdentifier =
139                    sys_locale::get_locale().map(|l| l.parse().unwrap()).unwrap_or_default();
140                let direction = direction_from_locale(&locale);
141                self.locale.set(locale);
142                self.direction.set(direction);
143                apply_direction_class(cx, direction);
144                cx.reload_styles().unwrap();
145            }
146
147            EnvironmentEvent::ToggleThemeMode => {
148                let theme_mode = match self.theme_mode {
149                    ThemeMode::System => ThemeMode::System,
150                    ThemeMode::DarkMode => ThemeMode::LightMode,
151                    ThemeMode::LightMode => ThemeMode::DarkMode,
152                };
153
154                self.theme_mode = theme_mode;
155
156                let is_dark = self.effective_theme() == ThemeMode::DarkMode;
157                cx.with_current(Entity::root(), |cx| {
158                    cx.toggle_class("dark", is_dark);
159                });
160
161                cx.reload_styles().unwrap();
162            }
163
164            EnvironmentEvent::SetDoubleClickInterval(interval) => {
165                self.double_click_interval = interval;
166            }
167
168            EnvironmentEvent::SetTooltipDelay(delay) => {
169                self.tooltip_delay = delay;
170            }
171        });
172
173        event.map(|event, _| match event {
174            WindowEvent::ThemeChanged(theme) => {
175                self.system_theme_mode = *theme;
176                if self.theme_mode == ThemeMode::System {
177                    let is_dark = self.system_theme_mode == ThemeMode::DarkMode;
178                    cx.with_current(Entity::root(), |cx| {
179                        cx.toggle_class("dark", is_dark);
180                    });
181                    cx.reload_styles().unwrap();
182                }
183            }
184            _ => (),
185        })
186    }
187}