1use std::collections::HashMap;
5
6use crate::widget::nav_bar;
7use cosmic_config::CosmicConfigEntry;
8use cosmic_theme::ThemeMode;
9use enumflags2::{self, BitFlags, bitflags};
10use iced::{Limits, Size, window};
11use iced_core::window::Id;
12use palette::Srgba;
13use slotmap::Key;
14
15use crate::Theme;
16
17#[derive(Clone)]
19pub struct NavBar {
20 active: bool,
21 context_id: crate::widget::nav_bar::Id,
22 toggled: bool,
23 toggled_condensed: bool,
24}
25
26#[allow(clippy::struct_excessive_bools)]
28#[derive(Clone)]
29pub struct Window {
30 pub header_title: String,
32 pub use_template: bool,
33 pub content_container: bool,
34 pub context_is_overlay: bool,
35 pub sharp_corners: bool,
36 pub show_context: bool,
37 pub show_headerbar: bool,
38 pub show_window_menu: bool,
39 pub show_close: bool,
40 pub show_maximize: bool,
41 pub show_minimize: bool,
42 pub is_maximized: bool,
43 pub border_padding: Option<u16>,
44 height: f32,
45 width: f32,
46}
47
48#[bitflags]
49#[repr(u8)]
50#[derive(Copy, Clone, Debug, PartialEq)]
51pub enum Auto {
52 Window,
54 Popup,
56 System,
58}
59
60#[derive(Clone)]
62pub struct Core {
63 pub debug: bool,
65
66 is_condensed: bool,
68
69 pub(super) keyboard_nav: bool,
71
72 nav_bar: NavBar,
74
75 scale_factor: f32,
77
78 pub(super) focused_window: Vec<window::Id>,
80
81 pub(super) theme_sub_counter: u64,
82 pub(super) system_theme: Theme,
84
85 pub(super) system_theme_mode: ThemeMode,
87
88 pub(super) portal_is_dark: Option<bool>,
89
90 pub(super) portal_accent: Option<Srgba>,
91
92 pub(super) portal_is_high_contrast: Option<bool>,
93
94 pub(super) title: HashMap<Id, String>,
95
96 pub window: Window,
97
98 #[cfg(feature = "applet")]
99 pub applet: crate::applet::Context,
100
101 #[cfg(feature = "single-instance")]
102 pub(crate) single_instance: bool,
103
104 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
105 pub(crate) settings_daemon:
106 Option<cosmic_settings_daemon_zbus::CosmicSettingsDaemonProxy<'static>>,
107
108 pub(crate) main_window: Option<window::Id>,
109
110 pub(crate) exit_on_main_window_closed: bool,
111
112 pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
113
114 pub(crate) auto_blur: BitFlags<Auto>,
115
116 pub(crate) auto_corner_radius: BitFlags<Auto>,
117
118 pub(crate) app_type: AppType,
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum AppType {
123 Window,
125 System,
127 Applet,
129}
130
131impl Default for Core {
132 fn default() -> Self {
133 Self {
134 debug: false,
135 is_condensed: false,
136 keyboard_nav: true,
137 nav_bar: NavBar {
138 active: true,
139 context_id: crate::widget::nav_bar::Id::null(),
140 toggled: true,
141 toggled_condensed: false,
142 },
143 scale_factor: 1.0,
144 title: HashMap::new(),
145 theme_sub_counter: 0,
146 system_theme: crate::theme::active(),
147 system_theme_mode: ThemeMode::config()
148 .map(|c| {
149 ThemeMode::get_entry(&c).unwrap_or_else(|(errors, mode)| {
150 for why in errors.into_iter().filter(cosmic_config::Error::is_err) {
151 tracing::error!(?why, "ThemeMode config entry error");
152 }
153 mode
154 })
155 })
156 .unwrap_or_default(),
157 window: Window {
158 header_title: String::new(),
159 use_template: true,
160 content_container: true,
161 context_is_overlay: true,
162 sharp_corners: false,
163 show_context: false,
164 show_headerbar: true,
165 show_close: true,
166 show_maximize: true,
167 show_minimize: true,
168 show_window_menu: false,
169 is_maximized: false,
170 height: 0.,
171 width: 0.,
172 border_padding: None,
173 },
174 focused_window: Vec::new(),
175 #[cfg(feature = "applet")]
176 applet: crate::applet::Context::default(),
177 #[cfg(feature = "single-instance")]
178 single_instance: false,
179 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
180 settings_daemon: None,
181 portal_is_dark: None,
182 portal_accent: None,
183 portal_is_high_contrast: None,
184 main_window: None,
185 exit_on_main_window_closed: true,
186 menu_bars: HashMap::new(),
187 auto_blur: Auto::System | Auto::Popup | Auto::Window,
188 auto_corner_radius: Auto::System | Auto::Popup | Auto::Window,
189 app_type: AppType::Window,
190 }
191 }
192}
193
194impl Core {
195 #[must_use]
197 #[inline]
198 pub const fn is_condensed(&self) -> bool {
199 self.is_condensed
200 }
201
202 #[must_use]
204 #[inline]
205 pub const fn scale_factor(&self) -> f32 {
206 self.scale_factor
207 }
208
209 #[inline]
211 pub const fn set_keyboard_nav(&mut self, enabled: bool) {
212 self.keyboard_nav = enabled;
213 }
214
215 #[must_use]
217 #[inline]
218 pub const fn keyboard_nav(&self) -> bool {
219 self.keyboard_nav
220 }
221
222 #[cold]
224 pub(crate) fn set_scale_factor(&mut self, factor: f32) {
225 self.scale_factor = factor;
226 self.is_condensed_update();
227 }
228
229 #[inline]
231 pub fn set_header_title(&mut self, title: String) {
232 self.window.header_title = title;
233 }
234
235 #[inline]
236 pub(crate) fn show_content(&self) -> bool {
238 !self.is_condensed || !self.nav_bar.toggled_condensed
239 }
240
241 #[allow(clippy::cast_precision_loss)]
242 fn is_condensed_update(&mut self) {
244 let mut breakpoint = 280.0 + 8.0 + 360.0;
246 if self.window.show_context && !self.window.context_is_overlay {
248 breakpoint += 344.0 + 8.0;
250 };
251 self.is_condensed = (breakpoint * self.scale_factor) > self.window.width;
252 self.nav_bar_update();
253 }
254
255 #[inline]
256 fn condensed_conflict(&self) -> bool {
257 self.is_condensed
259 && self.nav_bar.toggled_condensed
260 && self.window.show_context
261 && !self.window.context_is_overlay
262 }
263
264 #[inline]
265 pub(crate) fn context_width(&self, has_nav: bool) -> f32 {
266 let window_width = self.window.width / self.scale_factor;
267
268 let mut reserved_width = 360.0 + 8.0;
270 if has_nav {
271 reserved_width += 280.0 + 8.0;
273 }
274
275 #[allow(clippy::manual_clamp)]
276 (window_width - reserved_width).min(480.0).max(344.0)
280 }
281
282 #[cold]
283 pub fn set_show_context(&mut self, show: bool) {
284 self.window.show_context = show;
285 self.is_condensed_update();
286 if self.condensed_conflict() {
288 self.nav_bar.toggled_condensed = false;
289 self.is_condensed_update();
290 }
291 }
292
293 #[inline]
294 pub fn main_window_is(&self, id: iced::window::Id) -> bool {
295 self.main_window_id().is_some_and(|main_id| main_id == id)
296 }
297
298 #[must_use]
300 #[inline]
301 pub const fn nav_bar_active(&self) -> bool {
302 self.nav_bar.active
303 }
304
305 #[inline]
306 pub fn nav_bar_toggle(&mut self) {
307 self.nav_bar.toggled = !self.nav_bar.toggled;
308 self.nav_bar_set_toggled_condensed(self.nav_bar.toggled);
309 }
310
311 #[inline]
312 pub fn nav_bar_toggle_condensed(&mut self) {
313 self.nav_bar_set_toggled_condensed(!self.nav_bar.toggled_condensed);
314 }
315
316 #[inline]
317 pub(crate) const fn nav_bar_context(&self) -> nav_bar::Id {
318 self.nav_bar.context_id
319 }
320
321 #[inline]
322 pub(crate) fn nav_bar_set_context(&mut self, id: nav_bar::Id) {
323 self.nav_bar.context_id = id;
324 }
325
326 #[inline]
327 pub fn nav_bar_set_toggled(&mut self, toggled: bool) {
328 self.nav_bar.toggled = toggled;
329 self.nav_bar_set_toggled_condensed(self.nav_bar.toggled);
330 }
331
332 #[cold]
333 pub(crate) fn nav_bar_set_toggled_condensed(&mut self, toggled: bool) {
334 self.nav_bar.toggled_condensed = toggled;
335 self.nav_bar_update();
336 if self.condensed_conflict() {
338 self.window.show_context = false;
339 self.is_condensed_update();
340 if !self.is_condensed {
342 self.nav_bar.toggled = toggled;
343 self.nav_bar_update();
344 }
345 }
346 }
347
348 #[inline]
349 pub(crate) fn nav_bar_update(&mut self) {
350 self.nav_bar.active = if self.is_condensed {
351 self.nav_bar.toggled_condensed
352 } else {
353 self.nav_bar.toggled
354 };
355 }
356
357 #[inline]
358 pub(crate) const fn set_window_height(&mut self, new_height: f32) {
360 self.window.height = new_height;
361 }
362
363 #[inline]
364 pub(crate) fn set_window_width(&mut self, new_width: f32) {
366 self.window.width = new_width;
367 self.is_condensed_update();
368 }
369
370 #[inline]
371 pub const fn system_theme(&self) -> &Theme {
373 &self.system_theme
374 }
375
376 #[inline]
377 #[must_use]
378 pub const fn system_theme_mode(&self) -> ThemeMode {
380 self.system_theme_mode
381 }
382
383 pub fn watch_config<
384 T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
385 >(
386 &self,
387 config_id: &'static str,
388 ) -> iced::Subscription<cosmic_config::Update<T>> {
389 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
390 if let Some(settings_daemon) = self.settings_daemon.as_ref() {
391 return cosmic_config::dbus::watcher_subscription(
392 settings_daemon.clone(),
393 config_id,
394 false,
395 );
396 }
397 cosmic_config::config_subscription(
398 std::any::TypeId::of::<T>(),
399 std::borrow::Cow::Borrowed(config_id),
400 T::VERSION,
401 )
402 }
403
404 pub fn watch_state<
405 T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
406 >(
407 &self,
408 state_id: &'static str,
409 ) -> iced::Subscription<cosmic_config::Update<T>> {
410 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
411 if let Some(settings_daemon) = self.settings_daemon.as_ref() {
412 return cosmic_config::dbus::watcher_subscription(
413 settings_daemon.clone(),
414 state_id,
415 true,
416 );
417 }
418 cosmic_config::config_subscription(
419 std::any::TypeId::of::<T>(),
420 std::borrow::Cow::Borrowed(state_id),
421 T::VERSION,
422 )
423 }
424
425 #[must_use]
427 #[inline]
428 pub fn focused_window(&self) -> Option<window::Id> {
429 self.focused_window.last().copied()
430 }
431
432 #[must_use]
434 #[inline]
435 pub fn focus_chain(&self) -> &[window::Id] {
436 &self.focused_window
437 }
438
439 #[must_use]
441 #[inline]
442 pub fn system_is_dark(&self) -> bool {
443 self.portal_is_dark
444 .unwrap_or(self.system_theme_mode.is_dark)
445 }
446
447 #[must_use]
449 #[inline]
450 pub fn main_window_id(&self) -> Option<window::Id> {
451 self.main_window.filter(|id| iced::window::Id::NONE != *id)
452 }
453
454 #[inline]
456 pub fn set_main_window_id(&mut self, mut id: Option<window::Id>) -> Option<window::Id> {
457 std::mem::swap(&mut self.main_window, &mut id);
458 id
459 }
460
461 #[cfg(feature = "winit")]
462 pub fn drag<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
463 let Some(id) = id.or(self.main_window) else {
464 return iced::Task::none();
465 };
466 crate::command::drag(id)
467 }
468
469 #[cfg(feature = "winit")]
470 pub fn maximize<M: Send + 'static>(
471 &self,
472 id: Option<window::Id>,
473 maximized: bool,
474 ) -> crate::app::Task<M> {
475 let Some(id) = id.or(self.main_window) else {
476 return iced::Task::none();
477 };
478 crate::command::maximize(id, maximized)
479 }
480
481 #[cfg(feature = "winit")]
482 pub fn minimize<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
483 let Some(id) = id.or(self.main_window) else {
484 return iced::Task::none();
485 };
486 crate::command::minimize(id)
487 }
488
489 #[cfg(feature = "winit")]
490 pub fn set_title<M: Send + 'static>(
491 &self,
492 id: Option<window::Id>,
493 title: String,
494 ) -> crate::app::Task<M> {
495 let Some(id) = id.or(self.main_window) else {
496 return iced::Task::none();
497 };
498 crate::command::set_title(id, title)
499 }
500
501 #[cfg(feature = "winit")]
502 pub fn set_windowed<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
503 let Some(id) = id.or(self.main_window) else {
504 return iced::Task::none();
505 };
506 crate::command::set_windowed(id)
507 }
508
509 #[cfg(feature = "winit")]
510 pub fn toggle_maximize<M: Send + 'static>(
511 &self,
512 id: Option<window::Id>,
513 ) -> crate::app::Task<M> {
514 let Some(id) = id.or(self.main_window) else {
515 return iced::Task::none();
516 };
517
518 crate::command::toggle_maximize(id)
519 }
520
521 #[cfg(wayland_platform)]
522 pub fn sync_window_border_radii_to_theme(&self) -> bool {
523 match self.app_type {
524 AppType::Window => self.auto_corner_radius.contains(Auto::Window),
525 AppType::System => self.auto_corner_radius.contains(Auto::System),
526 AppType::Applet => false,
527 }
528 }
529
530 pub fn set_auto_blur(&mut self, auto_blur: BitFlags<Auto>) {
531 self.auto_blur = auto_blur;
532 }
533
534 pub fn auto_blur(&self) -> BitFlags<Auto> {
535 self.auto_blur
536 }
537
538 #[must_use]
541 pub fn frosted(&self, theme: &cosmic_theme::Theme) -> bool {
542 if self.auto_blur.is_empty() {
543 return false;
544 }
545 match self.app_type {
546 AppType::Window => theme.frosted_windows,
547 AppType::System => theme.frosted_system_interface,
548 AppType::Applet => theme.frosted_applets,
549 }
550 }
551
552 pub fn set_auto_corner_radius(&mut self, auto_corner_radius: BitFlags<Auto>) {
553 self.auto_corner_radius = auto_corner_radius;
554 }
555
556 pub fn auto_corner_radius(&self) -> BitFlags<Auto> {
557 self.auto_corner_radius
558 }
559
560 pub fn set_app_type(&mut self, app_type: AppType) {
561 self.app_type = app_type;
562 }
563
564 pub fn app_type(&self) -> AppType {
565 self.app_type
566 }
567
568 #[must_use]
569 #[cfg(feature = "winit")]
570 pub fn blur(
571 &self,
572 theme: &Theme,
573 surface_id_wrapper: Option<iced_winit::SurfaceIdWrapper>,
574 ) -> bool {
575 use iced_winit::SurfaceIdWrapper;
576 let theme = theme.cosmic();
577 match surface_id_wrapper {
578 Some(SurfaceIdWrapper::LayerSurface(_)) => {
579 theme.frosted_system_interface && self.auto_blur.contains(Auto::System)
580 }
581 Some(SurfaceIdWrapper::Window(_)) => {
582 theme.frosted_windows && self.auto_blur.contains(Auto::Window)
583 }
584 Some(SurfaceIdWrapper::Popup(_)) => {
585 self.auto_blur.contains(Auto::Popup)
586 && match self.app_type {
587 AppType::Window => theme.frosted_windows,
588 AppType::System => theme.frosted_system_interface,
589 AppType::Applet => theme.frosted_applets,
590 }
591 }
592 None => match self.app_type {
593 AppType::Window => theme.frosted_windows && self.auto_blur.contains(Auto::Window),
594 AppType::System => {
595 theme.frosted_system_interface && self.auto_blur.contains(Auto::System)
596 }
597 AppType::Applet => false,
598 },
599 _ => false,
600 }
601 }
602
603 #[must_use]
605 #[cfg(wayland_platform)]
606 pub fn corners(
607 &self,
608 theme: &Theme,
609 rounded: bool,
610 ) -> Option<iced_runtime::platform_specific::wayland::CornerRadius> {
611 if !self.sync_window_border_radii_to_theme() {
612 return None;
613 }
614 let theme = theme.cosmic();
615 let ret = if let AppType::Applet = self.app_type {
616 let radius_l = theme.radius_l();
617 iced_runtime::platform_specific::wayland::CornerRadius {
618 top_left: radius_l[0].round() as u32,
619 top_right: radius_l[1].round() as u32,
620 bottom_right: radius_l[2].round() as u32,
621 bottom_left: radius_l[3].round() as u32,
622 }
623 } else if let AppType::Window = self.app_type
624 && !rounded
625 {
626 let radius_0 = theme.radius_0();
627 iced_runtime::platform_specific::wayland::CornerRadius {
628 top_left: radius_0[0].round() as u32,
629 top_right: radius_0[1].round() as u32,
630 bottom_right: radius_0[2].round() as u32,
631 bottom_left: radius_0[3].round() as u32,
632 }
633 } else {
634 let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
635 iced_runtime::platform_specific::wayland::CornerRadius {
636 top_left: radius_s[0].round() as u32,
637 top_right: radius_s[1].round() as u32,
638 bottom_right: radius_s[2].round() as u32,
639 bottom_left: radius_s[3].round() as u32,
640 }
641 };
642 Some(ret)
643 }
644}