Skip to main content

cosmic/
core.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use 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/// Status of the nav bar and its panels.
18#[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/// COSMIC-specific settings for windows.
27#[allow(clippy::struct_excessive_bools)]
28#[derive(Clone)]
29pub struct Window {
30    /// Label to display as header bar title.
31    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    /// Automatically apply effect to regular windows
53    Window,
54    /// Automatically apply effect to popups
55    Popup,
56    /// Automatically apply effect to system interface elements (layer shell surfaces)
57    System,
58}
59
60/// COSMIC-specific application settings
61#[derive(Clone)]
62pub struct Core {
63    /// Enables debug features in cosmic/iced.
64    pub debug: bool,
65
66    /// Disables loading the icon theme from cosmic-config.
67    pub(super) icon_theme_override: bool,
68
69    /// Whether the window is too small for the nav bar + main content.
70    is_condensed: bool,
71
72    /// Enables built in keyboard navigation
73    pub(super) keyboard_nav: bool,
74
75    /// Current status of the nav bar panel.
76    nav_bar: NavBar,
77
78    /// Scaling factor used by the application
79    scale_factor: f32,
80
81    /// Window focus state
82    pub(super) focused_window: Vec<window::Id>,
83
84    pub(super) theme_sub_counter: u64,
85    /// Last known system theme
86    pub(super) system_theme: Theme,
87
88    /// Configured theme mode
89    pub(super) system_theme_mode: ThemeMode,
90
91    pub(super) portal_is_dark: Option<bool>,
92
93    pub(super) portal_accent: Option<Srgba>,
94
95    pub(super) portal_is_high_contrast: Option<bool>,
96
97    pub(super) title: HashMap<Id, String>,
98
99    pub window: Window,
100
101    #[cfg(feature = "applet")]
102    pub applet: crate::applet::Context,
103
104    #[cfg(feature = "single-instance")]
105    pub(crate) single_instance: bool,
106
107    #[cfg(all(feature = "dbus-config", target_os = "linux"))]
108    pub(crate) settings_daemon: Option<cosmic_settings_daemon::CosmicSettingsDaemonProxy<'static>>,
109
110    pub(crate) main_window: Option<window::Id>,
111
112    pub(crate) exit_on_main_window_closed: bool,
113
114    pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
115
116    pub(crate) auto_blur: BitFlags<Auto>,
117
118    pub(crate) auto_corner_radius: BitFlags<Auto>,
119
120    pub(crate) app_type: AppType,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum AppType {
125    /// A regular application
126    Window,
127    /// A system application
128    System,
129    /// An applet
130    Applet,
131}
132
133impl Default for Core {
134    fn default() -> Self {
135        Self {
136            debug: false,
137            icon_theme_override: false,
138            is_condensed: false,
139            keyboard_nav: true,
140            nav_bar: NavBar {
141                active: true,
142                context_id: crate::widget::nav_bar::Id::null(),
143                toggled: true,
144                toggled_condensed: false,
145            },
146            scale_factor: 1.0,
147            title: HashMap::new(),
148            theme_sub_counter: 0,
149            system_theme: crate::theme::active(),
150            system_theme_mode: ThemeMode::config()
151                .map(|c| {
152                    ThemeMode::get_entry(&c).unwrap_or_else(|(errors, mode)| {
153                        for why in errors.into_iter().filter(cosmic_config::Error::is_err) {
154                            tracing::error!(?why, "ThemeMode config entry error");
155                        }
156                        mode
157                    })
158                })
159                .unwrap_or_default(),
160            window: Window {
161                header_title: String::new(),
162                use_template: true,
163                content_container: true,
164                context_is_overlay: true,
165                sharp_corners: false,
166                show_context: false,
167                show_headerbar: true,
168                show_close: true,
169                show_maximize: true,
170                show_minimize: true,
171                show_window_menu: false,
172                is_maximized: false,
173                height: 0.,
174                width: 0.,
175                border_padding: None,
176            },
177            focused_window: Vec::new(),
178            #[cfg(feature = "applet")]
179            applet: crate::applet::Context::default(),
180            #[cfg(feature = "single-instance")]
181            single_instance: false,
182            #[cfg(all(feature = "dbus-config", target_os = "linux"))]
183            settings_daemon: None,
184            portal_is_dark: None,
185            portal_accent: None,
186            portal_is_high_contrast: None,
187            main_window: None,
188            exit_on_main_window_closed: true,
189            menu_bars: HashMap::new(),
190            auto_blur: Auto::System | Auto::Popup | Auto::Window,
191            auto_corner_radius: Auto::System | Auto::Popup | Auto::Window,
192            app_type: AppType::Window,
193        }
194    }
195}
196
197impl Core {
198    /// Whether the window is too small for the nav bar + main content.
199    #[must_use]
200    #[inline]
201    pub const fn is_condensed(&self) -> bool {
202        self.is_condensed
203    }
204
205    /// The scaling factor used by the application.
206    #[must_use]
207    #[inline]
208    pub const fn scale_factor(&self) -> f32 {
209        self.scale_factor
210    }
211
212    /// Enable or disable keyboard navigation
213    #[inline]
214    pub const fn set_keyboard_nav(&mut self, enabled: bool) {
215        self.keyboard_nav = enabled;
216    }
217
218    /// Enable or disable keyboard navigation
219    #[must_use]
220    #[inline]
221    pub const fn keyboard_nav(&self) -> bool {
222        self.keyboard_nav
223    }
224
225    /// Changes the scaling factor used by the application.
226    #[cold]
227    pub(crate) fn set_scale_factor(&mut self, factor: f32) {
228        self.scale_factor = factor;
229        self.is_condensed_update();
230    }
231
232    /// Set header bar title
233    #[inline]
234    pub fn set_header_title(&mut self, title: String) {
235        self.window.header_title = title;
236    }
237
238    #[inline]
239    /// Whether to show or hide the main window's content.
240    pub(crate) fn show_content(&self) -> bool {
241        !self.is_condensed || !self.nav_bar.toggled_condensed
242    }
243
244    #[allow(clippy::cast_precision_loss)]
245    /// Call this whenever the scaling factor or window width has changed.
246    fn is_condensed_update(&mut self) {
247        // Nav bar (280px) + padding (8px) + content (360px)
248        let mut breakpoint = 280.0 + 8.0 + 360.0;
249        //TODO: the app may return None from the context_drawer function even if show_context is true
250        if self.window.show_context && !self.window.context_is_overlay {
251            // Context drawer min width (344px) + padding (8px)
252            breakpoint += 344.0 + 8.0;
253        };
254        self.is_condensed = (breakpoint * self.scale_factor) > self.window.width;
255        self.nav_bar_update();
256    }
257
258    #[inline]
259    fn condensed_conflict(&self) -> bool {
260        // There is a conflict if the view is condensed and both the nav bar and context drawer are open on the same layer
261        self.is_condensed
262            && self.nav_bar.toggled_condensed
263            && self.window.show_context
264            && !self.window.context_is_overlay
265    }
266
267    #[inline]
268    pub(crate) fn context_width(&self, has_nav: bool) -> f32 {
269        let window_width = self.window.width / self.scale_factor;
270
271        // Content width (360px) + padding (8px)
272        let mut reserved_width = 360.0 + 8.0;
273        if has_nav {
274            // Navbar width (280px) + padding (8px)
275            reserved_width += 280.0 + 8.0;
276        }
277
278        #[allow(clippy::manual_clamp)]
279        // This logic is to ensure the context drawer does not take up too much of the content's space
280        // The minimum width is 344px and the maximum with is 480px
281        // We want to keep the content at least 360px until going down to the minimum width
282        (window_width - reserved_width).min(480.0).max(344.0)
283    }
284
285    #[cold]
286    pub fn set_show_context(&mut self, show: bool) {
287        self.window.show_context = show;
288        self.is_condensed_update();
289        // Ensure nav bar is closed if condensed view and context drawer is opened
290        if self.condensed_conflict() {
291            self.nav_bar.toggled_condensed = false;
292            self.is_condensed_update();
293        }
294    }
295
296    #[inline]
297    pub fn main_window_is(&self, id: iced::window::Id) -> bool {
298        self.main_window_id().is_some_and(|main_id| main_id == id)
299    }
300
301    /// Whether the nav panel is visible or not
302    #[must_use]
303    #[inline]
304    pub const fn nav_bar_active(&self) -> bool {
305        self.nav_bar.active
306    }
307
308    #[inline]
309    pub fn nav_bar_toggle(&mut self) {
310        self.nav_bar.toggled = !self.nav_bar.toggled;
311        self.nav_bar_set_toggled_condensed(self.nav_bar.toggled);
312    }
313
314    #[inline]
315    pub fn nav_bar_toggle_condensed(&mut self) {
316        self.nav_bar_set_toggled_condensed(!self.nav_bar.toggled_condensed);
317    }
318
319    #[inline]
320    pub(crate) const fn nav_bar_context(&self) -> nav_bar::Id {
321        self.nav_bar.context_id
322    }
323
324    #[inline]
325    pub(crate) fn nav_bar_set_context(&mut self, id: nav_bar::Id) {
326        self.nav_bar.context_id = id;
327    }
328
329    #[inline]
330    pub fn nav_bar_set_toggled(&mut self, toggled: bool) {
331        self.nav_bar.toggled = toggled;
332        self.nav_bar_set_toggled_condensed(self.nav_bar.toggled);
333    }
334
335    #[cold]
336    pub(crate) fn nav_bar_set_toggled_condensed(&mut self, toggled: bool) {
337        self.nav_bar.toggled_condensed = toggled;
338        self.nav_bar_update();
339        // Ensure context drawer is closed if condensed view and nav bar is opened
340        if self.condensed_conflict() {
341            self.window.show_context = false;
342            self.is_condensed_update();
343            // Sync nav bar state if the view is no longer condensed after closing the context drawer
344            if !self.is_condensed {
345                self.nav_bar.toggled = toggled;
346                self.nav_bar_update();
347            }
348        }
349    }
350
351    #[inline]
352    pub(crate) fn nav_bar_update(&mut self) {
353        self.nav_bar.active = if self.is_condensed {
354            self.nav_bar.toggled_condensed
355        } else {
356            self.nav_bar.toggled
357        };
358    }
359
360    #[inline]
361    /// Set the height of the main window.
362    pub(crate) const fn set_window_height(&mut self, new_height: f32) {
363        self.window.height = new_height;
364    }
365
366    #[inline]
367    /// Set the width of the main window.
368    pub(crate) fn set_window_width(&mut self, new_width: f32) {
369        self.window.width = new_width;
370        self.is_condensed_update();
371    }
372
373    #[inline]
374    /// Get the current system theme
375    pub const fn system_theme(&self) -> &Theme {
376        &self.system_theme
377    }
378
379    #[inline]
380    #[must_use]
381    /// Get the current system theme mode
382    pub const fn system_theme_mode(&self) -> ThemeMode {
383        self.system_theme_mode
384    }
385
386    pub fn watch_config<
387        T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
388    >(
389        &self,
390        config_id: &'static str,
391    ) -> iced::Subscription<cosmic_config::Update<T>> {
392        #[cfg(all(feature = "dbus-config", target_os = "linux"))]
393        if let Some(settings_daemon) = self.settings_daemon.as_ref() {
394            return cosmic_config::dbus::watcher_subscription(
395                settings_daemon.clone(),
396                config_id,
397                false,
398            );
399        }
400        cosmic_config::config_subscription(
401            std::any::TypeId::of::<T>(),
402            std::borrow::Cow::Borrowed(config_id),
403            T::VERSION,
404        )
405    }
406
407    pub fn watch_state<
408        T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
409    >(
410        &self,
411        state_id: &'static str,
412    ) -> iced::Subscription<cosmic_config::Update<T>> {
413        #[cfg(all(feature = "dbus-config", target_os = "linux"))]
414        if let Some(settings_daemon) = self.settings_daemon.as_ref() {
415            return cosmic_config::dbus::watcher_subscription(
416                settings_daemon.clone(),
417                state_id,
418                true,
419            );
420        }
421        cosmic_config::config_subscription(
422            std::any::TypeId::of::<T>(),
423            std::borrow::Cow::Borrowed(state_id),
424            T::VERSION,
425        )
426    }
427
428    /// Get the current focused window if it exists
429    #[must_use]
430    #[inline]
431    pub fn focused_window(&self) -> Option<window::Id> {
432        self.focused_window.last().copied()
433    }
434
435    /// Get the current focus chain of windows
436    #[must_use]
437    #[inline]
438    pub fn focus_chain(&self) -> &[window::Id] {
439        &self.focused_window
440    }
441
442    /// Whether the application should use a dark theme, according to the system
443    #[must_use]
444    #[inline]
445    pub fn system_is_dark(&self) -> bool {
446        self.portal_is_dark
447            .unwrap_or(self.system_theme_mode.is_dark)
448    }
449
450    /// The [`Id`] of the main window
451    #[must_use]
452    #[inline]
453    pub fn main_window_id(&self) -> Option<window::Id> {
454        self.main_window.filter(|id| iced::window::Id::NONE != *id)
455    }
456
457    /// Reset the tracked main window to a new value
458    #[inline]
459    pub fn set_main_window_id(&mut self, mut id: Option<window::Id>) -> Option<window::Id> {
460        std::mem::swap(&mut self.main_window, &mut id);
461        id
462    }
463
464    #[cfg(feature = "winit")]
465    pub fn drag<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
466        let Some(id) = id.or(self.main_window) else {
467            return iced::Task::none();
468        };
469        crate::command::drag(id)
470    }
471
472    #[cfg(feature = "winit")]
473    pub fn maximize<M: Send + 'static>(
474        &self,
475        id: Option<window::Id>,
476        maximized: bool,
477    ) -> crate::app::Task<M> {
478        let Some(id) = id.or(self.main_window) else {
479            return iced::Task::none();
480        };
481        crate::command::maximize(id, maximized)
482    }
483
484    #[cfg(feature = "winit")]
485    pub fn minimize<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
486        let Some(id) = id.or(self.main_window) else {
487            return iced::Task::none();
488        };
489        crate::command::minimize(id)
490    }
491
492    #[cfg(feature = "winit")]
493    pub fn set_title<M: Send + 'static>(
494        &self,
495        id: Option<window::Id>,
496        title: String,
497    ) -> crate::app::Task<M> {
498        let Some(id) = id.or(self.main_window) else {
499            return iced::Task::none();
500        };
501        crate::command::set_title(id, title)
502    }
503
504    #[cfg(feature = "winit")]
505    pub fn set_windowed<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
506        let Some(id) = id.or(self.main_window) else {
507            return iced::Task::none();
508        };
509        crate::command::set_windowed(id)
510    }
511
512    #[cfg(feature = "winit")]
513    pub fn toggle_maximize<M: Send + 'static>(
514        &self,
515        id: Option<window::Id>,
516    ) -> crate::app::Task<M> {
517        let Some(id) = id.or(self.main_window) else {
518            return iced::Task::none();
519        };
520
521        crate::command::toggle_maximize(id)
522    }
523
524    #[cfg(wayland_platform)]
525    pub fn sync_window_border_radii_to_theme(&self) -> bool {
526        match self.app_type {
527            AppType::Window => self.auto_corner_radius.contains(Auto::Window),
528            AppType::System => self.auto_corner_radius.contains(Auto::System),
529            AppType::Applet => false,
530        }
531    }
532
533    pub fn set_auto_blur(&mut self, auto_blur: BitFlags<Auto>) {
534        self.auto_blur = auto_blur;
535    }
536
537    pub fn auto_blur(&self) -> BitFlags<Auto> {
538        self.auto_blur
539    }
540
541    pub fn set_auto_corner_radius(&mut self, auto_corner_radius: BitFlags<Auto>) {
542        self.auto_corner_radius = auto_corner_radius;
543    }
544
545    pub fn auto_corner_radius(&self) -> BitFlags<Auto> {
546        self.auto_corner_radius
547    }
548
549    pub fn set_app_type(&mut self, app_type: AppType) {
550        self.app_type = app_type;
551    }
552
553    pub fn app_type(&self) -> AppType {
554        self.app_type
555    }
556
557    #[must_use]
558    #[cfg(feature = "winit")]
559    pub fn blur(
560        &self,
561        theme: &Theme,
562        surface_id_wrapper: Option<iced_winit::SurfaceIdWrapper>,
563    ) -> bool {
564        use iced_winit::SurfaceIdWrapper;
565        let theme = theme.cosmic();
566        match surface_id_wrapper {
567            Some(SurfaceIdWrapper::LayerSurface(_)) => {
568                theme.frosted_system_interface && self.auto_blur.contains(Auto::System)
569            }
570            Some(SurfaceIdWrapper::Window(_)) => {
571                theme.frosted_windows && self.auto_blur.contains(Auto::Window)
572            }
573            Some(SurfaceIdWrapper::Popup(_))
574                if matches!(self.app_type, AppType::Window | AppType::System) =>
575            {
576                theme.frosted_windows && self.auto_blur.contains(Auto::Popup)
577            }
578            Some(SurfaceIdWrapper::Popup(_)) if matches!(self.app_type, AppType::Applet) => {
579                theme.frosted_applets && self.auto_blur.contains(Auto::Popup)
580            }
581            None => match self.app_type {
582                AppType::Window => theme.frosted_windows && self.auto_blur.contains(Auto::Window),
583                AppType::System => {
584                    theme.frosted_system_interface && self.auto_blur.contains(Auto::System)
585                }
586                AppType::Applet => false,
587            },
588            _ => false,
589        }
590    }
591
592    /// Calculate suggested corners for each app type main window
593    #[must_use]
594    #[cfg(wayland_platform)]
595    pub fn corners(
596        &self,
597        theme: &Theme,
598        rounded: bool,
599    ) -> Option<iced_runtime::platform_specific::wayland::CornerRadius> {
600        if !self.sync_window_border_radii_to_theme() {
601            return None;
602        }
603        let theme = theme.cosmic();
604        let ret = if let AppType::Applet = self.app_type {
605            let radius_l = theme.radius_l();
606            iced_runtime::platform_specific::wayland::CornerRadius {
607                top_left: radius_l[0].round() as u32,
608                top_right: radius_l[1].round() as u32,
609                bottom_right: radius_l[2].round() as u32,
610                bottom_left: radius_l[3].round() as u32,
611            }
612        } else if let AppType::Window = self.app_type
613            && !rounded
614        {
615            let radius_0 = theme.radius_0();
616            iced_runtime::platform_specific::wayland::CornerRadius {
617                top_left: radius_0[0].round() as u32,
618                top_right: radius_0[1].round() as u32,
619                bottom_right: radius_0[2].round() as u32,
620                bottom_left: radius_0[3].round() as u32,
621            }
622        } else {
623            let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
624            iced_runtime::platform_specific::wayland::CornerRadius {
625                top_left: radius_s[0].round() as u32,
626                top_right: radius_s[1].round() as u32,
627                bottom_right: radius_s[2].round() as u32,
628                bottom_left: radius_s[3].round() as u32,
629            }
630        };
631        Some(ret)
632    }
633}