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}
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 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
120pub enum EnvironmentEvent {
122 SetLocale(LanguageIdentifier),
124 SetDirection(Direction),
126 SetThemeMode(ThemeMode),
129 UseSystemLocale,
131 ToggleThemeMode,
133 SetDoubleClickInterval(Duration),
135 SetTooltipDelay(Duration),
137 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}