vizia_core/
environment.rs1use 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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
13pub enum ThemeMode {
14 #[default]
16 System,
17 DarkMode,
19 LightMode,
21}
22
23use crate::{context::EventContext, events::Event};
24
25pub struct Environment {
27 pub locale: Signal<LanguageIdentifier>,
29 pub direction: Signal<Direction>,
31 pub double_click_interval: Duration,
33 pub tooltip_delay: Duration,
35 pub theme_mode: ThemeMode,
37 pub system_theme_mode: ThemeMode,
39 pub(crate) caret_timer: Timer,
41 pub drag_distance: Signal<u32>,
43 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 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
125pub enum EnvironmentEvent {
127 SetLocale(LanguageIdentifier),
129 SetDirection(Direction),
131 SetThemeMode(ThemeMode),
134 UseSystemLocale,
136 ToggleThemeMode,
138 SetDoubleClickInterval(Duration),
140 SetTooltipDelay(Duration),
142 SetDragDistance(u32),
144 SetDebugLayout(bool),
146 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}