1use std::collections::HashMap;
5
6use crate::widget::nav_bar;
7use cosmic_config::CosmicConfigEntry;
8use cosmic_theme::ThemeMode;
9use iced::{Limits, Size, window};
10use iced_core::window::Id;
11use palette::Srgba;
12use slotmap::Key;
13
14use crate::Theme;
15
16#[derive(Clone)]
18pub struct NavBar {
19 active: bool,
20 context_id: crate::widget::nav_bar::Id,
21 toggled: bool,
22 toggled_condensed: bool,
23}
24
25#[allow(clippy::struct_excessive_bools)]
27#[derive(Clone)]
28pub struct Window {
29 pub header_title: String,
31 pub use_template: bool,
32 pub content_container: bool,
33 pub context_is_overlay: bool,
34 pub sharp_corners: bool,
35 pub show_context: bool,
36 pub show_headerbar: bool,
37 pub show_window_menu: bool,
38 pub show_close: bool,
39 pub show_maximize: bool,
40 pub show_minimize: bool,
41 pub is_maximized: bool,
42 height: f32,
43 width: f32,
44}
45
46#[derive(Clone)]
48pub struct Core {
49 pub debug: bool,
51
52 pub(super) icon_theme_override: bool,
54
55 is_condensed: bool,
57
58 pub(super) keyboard_nav: bool,
60
61 nav_bar: NavBar,
63
64 scale_factor: f32,
66
67 pub(super) focused_window: Vec<window::Id>,
69
70 pub(super) theme_sub_counter: u64,
71 pub(super) system_theme: Theme,
73
74 pub(super) system_theme_mode: ThemeMode,
76
77 pub(super) portal_is_dark: Option<bool>,
78
79 pub(super) portal_accent: Option<Srgba>,
80
81 pub(super) portal_is_high_contrast: Option<bool>,
82
83 pub(super) title: HashMap<Id, String>,
84
85 pub window: Window,
86
87 #[cfg(feature = "applet")]
88 pub applet: crate::applet::Context,
89
90 #[cfg(feature = "single-instance")]
91 pub(crate) single_instance: bool,
92
93 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
94 pub(crate) settings_daemon: Option<cosmic_settings_daemon::CosmicSettingsDaemonProxy<'static>>,
95
96 pub(crate) main_window: Option<window::Id>,
97
98 pub(crate) exit_on_main_window_closed: bool,
99
100 pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
101
102 #[cfg(feature = "wayland")]
103 pub(crate) sync_window_border_radii_to_theme: bool,
104}
105
106impl Default for Core {
107 fn default() -> Self {
108 Self {
109 debug: false,
110 icon_theme_override: false,
111 is_condensed: false,
112 keyboard_nav: true,
113 nav_bar: NavBar {
114 active: true,
115 context_id: crate::widget::nav_bar::Id::null(),
116 toggled: true,
117 toggled_condensed: false,
118 },
119 scale_factor: 1.0,
120 title: HashMap::new(),
121 theme_sub_counter: 0,
122 system_theme: crate::theme::active(),
123 system_theme_mode: ThemeMode::config()
124 .map(|c| {
125 ThemeMode::get_entry(&c).unwrap_or_else(|(errors, mode)| {
126 for why in errors.into_iter().filter(cosmic_config::Error::is_err) {
127 tracing::error!(?why, "ThemeMode config entry error");
128 }
129 mode
130 })
131 })
132 .unwrap_or_default(),
133 window: Window {
134 header_title: String::new(),
135 use_template: true,
136 content_container: true,
137 context_is_overlay: true,
138 sharp_corners: false,
139 show_context: false,
140 show_headerbar: true,
141 show_close: true,
142 show_maximize: true,
143 show_minimize: true,
144 show_window_menu: false,
145 is_maximized: false,
146 height: 0.,
147 width: 0.,
148 },
149 focused_window: Vec::new(),
150 #[cfg(feature = "applet")]
151 applet: crate::applet::Context::default(),
152 #[cfg(feature = "single-instance")]
153 single_instance: false,
154 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
155 settings_daemon: None,
156 portal_is_dark: None,
157 portal_accent: None,
158 portal_is_high_contrast: None,
159 main_window: None,
160 exit_on_main_window_closed: true,
161 menu_bars: HashMap::new(),
162 #[cfg(feature = "wayland")]
163 sync_window_border_radii_to_theme: true,
164 }
165 }
166}
167
168impl Core {
169 #[must_use]
171 #[inline]
172 pub const fn is_condensed(&self) -> bool {
173 self.is_condensed
174 }
175
176 #[must_use]
178 #[inline]
179 pub const fn scale_factor(&self) -> f32 {
180 self.scale_factor
181 }
182
183 #[inline]
185 pub const fn set_keyboard_nav(&mut self, enabled: bool) {
186 self.keyboard_nav = enabled;
187 }
188
189 #[must_use]
191 #[inline]
192 pub const fn keyboard_nav(&self) -> bool {
193 self.keyboard_nav
194 }
195
196 #[cold]
198 pub(crate) fn set_scale_factor(&mut self, factor: f32) {
199 self.scale_factor = factor;
200 self.is_condensed_update();
201 }
202
203 #[inline]
205 pub fn set_header_title(&mut self, title: String) {
206 self.window.header_title = title;
207 }
208
209 #[inline]
210 pub(crate) fn show_content(&self) -> bool {
212 !self.is_condensed || !self.nav_bar.toggled_condensed
213 }
214
215 #[allow(clippy::cast_precision_loss)]
216 fn is_condensed_update(&mut self) {
218 let mut breakpoint = 280.0 + 8.0 + 360.0;
220 if self.window.show_context && !self.window.context_is_overlay {
222 breakpoint += 344.0 + 8.0;
224 };
225 self.is_condensed = (breakpoint * self.scale_factor) > self.window.width;
226 self.nav_bar_update();
227 }
228
229 #[inline]
230 fn condensed_conflict(&self) -> bool {
231 self.is_condensed
233 && self.nav_bar.toggled_condensed
234 && self.window.show_context
235 && !self.window.context_is_overlay
236 }
237
238 #[inline]
239 pub(crate) fn context_width(&self, has_nav: bool) -> f32 {
240 let window_width = self.window.width / self.scale_factor;
241
242 let mut reserved_width = 360.0 + 8.0;
244 if has_nav {
245 reserved_width += 280.0 + 8.0;
247 }
248
249 #[allow(clippy::manual_clamp)]
250 (window_width - reserved_width).min(480.0).max(344.0)
254 }
255
256 #[cold]
257 pub fn set_show_context(&mut self, show: bool) {
258 self.window.show_context = show;
259 self.is_condensed_update();
260 if self.condensed_conflict() {
262 self.nav_bar.toggled_condensed = false;
263 self.is_condensed_update();
264 }
265 }
266
267 #[inline]
268 pub fn main_window_is(&self, id: iced::window::Id) -> bool {
269 self.main_window_id().is_some_and(|main_id| main_id == id)
270 }
271
272 #[must_use]
274 #[inline]
275 pub const fn nav_bar_active(&self) -> bool {
276 self.nav_bar.active
277 }
278
279 #[inline]
280 pub fn nav_bar_toggle(&mut self) {
281 self.nav_bar.toggled = !self.nav_bar.toggled;
282 self.nav_bar_set_toggled_condensed(self.nav_bar.toggled);
283 }
284
285 #[inline]
286 pub fn nav_bar_toggle_condensed(&mut self) {
287 self.nav_bar_set_toggled_condensed(!self.nav_bar.toggled_condensed);
288 }
289
290 #[inline]
291 pub(crate) const fn nav_bar_context(&self) -> nav_bar::Id {
292 self.nav_bar.context_id
293 }
294
295 #[inline]
296 pub(crate) fn nav_bar_set_context(&mut self, id: nav_bar::Id) {
297 self.nav_bar.context_id = id;
298 }
299
300 #[inline]
301 pub fn nav_bar_set_toggled(&mut self, toggled: bool) {
302 self.nav_bar.toggled = toggled;
303 self.nav_bar_set_toggled_condensed(self.nav_bar.toggled);
304 }
305
306 #[cold]
307 pub(crate) fn nav_bar_set_toggled_condensed(&mut self, toggled: bool) {
308 self.nav_bar.toggled_condensed = toggled;
309 self.nav_bar_update();
310 if self.condensed_conflict() {
312 self.window.show_context = false;
313 self.is_condensed_update();
314 if !self.is_condensed {
316 self.nav_bar.toggled = toggled;
317 self.nav_bar_update();
318 }
319 }
320 }
321
322 #[inline]
323 pub(crate) fn nav_bar_update(&mut self) {
324 self.nav_bar.active = if self.is_condensed {
325 self.nav_bar.toggled_condensed
326 } else {
327 self.nav_bar.toggled
328 };
329 }
330
331 #[inline]
332 pub(crate) const fn set_window_height(&mut self, new_height: f32) {
334 self.window.height = new_height;
335 }
336
337 #[inline]
338 pub(crate) fn set_window_width(&mut self, new_width: f32) {
340 self.window.width = new_width;
341 self.is_condensed_update();
342 }
343
344 #[inline]
345 pub const fn system_theme(&self) -> &Theme {
347 &self.system_theme
348 }
349
350 #[inline]
351 #[must_use]
352 pub const fn system_theme_mode(&self) -> ThemeMode {
354 self.system_theme_mode
355 }
356
357 pub fn watch_config<
358 T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
359 >(
360 &self,
361 config_id: &'static str,
362 ) -> iced::Subscription<cosmic_config::Update<T>> {
363 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
364 if let Some(settings_daemon) = self.settings_daemon.clone() {
365 return cosmic_config::dbus::watcher_subscription(settings_daemon, config_id, false);
366 }
367 cosmic_config::config_subscription(
368 std::any::TypeId::of::<T>(),
369 std::borrow::Cow::Borrowed(config_id),
370 T::VERSION,
371 )
372 }
373
374 pub fn watch_state<
375 T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
376 >(
377 &self,
378 state_id: &'static str,
379 ) -> iced::Subscription<cosmic_config::Update<T>> {
380 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
381 if let Some(settings_daemon) = self.settings_daemon.clone() {
382 return cosmic_config::dbus::watcher_subscription(settings_daemon, state_id, true);
383 }
384 cosmic_config::config_subscription(
385 std::any::TypeId::of::<T>(),
386 std::borrow::Cow::Borrowed(state_id),
387 T::VERSION,
388 )
389 }
390
391 #[must_use]
393 #[inline]
394 pub fn focused_window(&self) -> Option<window::Id> {
395 self.focused_window.last().copied()
396 }
397
398 #[must_use]
400 #[inline]
401 pub fn focus_chain(&self) -> &[window::Id] {
402 &self.focused_window
403 }
404
405 #[must_use]
407 #[inline]
408 pub fn system_is_dark(&self) -> bool {
409 self.portal_is_dark
410 .unwrap_or(self.system_theme_mode.is_dark)
411 }
412
413 #[must_use]
415 #[inline]
416 pub fn main_window_id(&self) -> Option<window::Id> {
417 self.main_window.filter(|id| iced::window::Id::NONE != *id)
418 }
419
420 #[inline]
422 pub fn set_main_window_id(&mut self, mut id: Option<window::Id>) -> Option<window::Id> {
423 std::mem::swap(&mut self.main_window, &mut id);
424 id
425 }
426
427 #[cfg(feature = "winit")]
428 pub fn drag<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
429 let Some(id) = id.or(self.main_window) else {
430 return iced::Task::none();
431 };
432 crate::command::drag(id)
433 }
434
435 #[cfg(feature = "winit")]
436 pub fn maximize<M: Send + 'static>(
437 &self,
438 id: Option<window::Id>,
439 maximized: bool,
440 ) -> crate::app::Task<M> {
441 let Some(id) = id.or(self.main_window) else {
442 return iced::Task::none();
443 };
444 crate::command::maximize(id, maximized)
445 }
446
447 #[cfg(feature = "winit")]
448 pub fn minimize<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
449 let Some(id) = id.or(self.main_window) else {
450 return iced::Task::none();
451 };
452 crate::command::minimize(id)
453 }
454
455 #[cfg(feature = "winit")]
456 pub fn set_title<M: Send + 'static>(
457 &self,
458 id: Option<window::Id>,
459 title: String,
460 ) -> crate::app::Task<M> {
461 let Some(id) = id.or(self.main_window) else {
462 return iced::Task::none();
463 };
464 crate::command::set_title(id, title)
465 }
466
467 #[cfg(feature = "winit")]
468 pub fn set_windowed<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
469 let Some(id) = id.or(self.main_window) else {
470 return iced::Task::none();
471 };
472 crate::command::set_windowed(id)
473 }
474
475 #[cfg(feature = "winit")]
476 pub fn toggle_maximize<M: Send + 'static>(
477 &self,
478 id: Option<window::Id>,
479 ) -> crate::app::Task<M> {
480 let Some(id) = id.or(self.main_window) else {
481 return iced::Task::none();
482 };
483
484 crate::command::toggle_maximize(id)
485 }
486
487 #[cfg(feature = "wayland")]
489 pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) {
490 self.sync_window_border_radii_to_theme = sync;
491 }
492
493 #[cfg(feature = "wayland")]
494 pub fn sync_window_border_radii_to_theme(&self) -> bool {
495 self.sync_window_border_radii_to_theme
496 }
497}