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    /// Whether the window is too small for the nav bar + main content.
67    is_condensed: bool,
68
69    /// Enables built in keyboard navigation
70    pub(super) keyboard_nav: bool,
71
72    /// Current status of the nav bar panel.
73    nav_bar: NavBar,
74
75    /// Scaling factor used by the application
76    scale_factor: f32,
77
78    /// Window focus state
79    pub(super) focused_window: Vec<window::Id>,
80
81    pub(super) theme_sub_counter: u64,
82    /// Last known system theme
83    pub(super) system_theme: Theme,
84
85    /// Configured theme mode
86    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    /// A regular application
124    Window,
125    /// A system application
126    System,
127    /// An applet
128    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    /// Whether the window is too small for the nav bar + main content.
196    #[must_use]
197    #[inline]
198    pub const fn is_condensed(&self) -> bool {
199        self.is_condensed
200    }
201
202    /// The scaling factor used by the application.
203    #[must_use]
204    #[inline]
205    pub const fn scale_factor(&self) -> f32 {
206        self.scale_factor
207    }
208
209    /// Enable or disable keyboard navigation
210    #[inline]
211    pub const fn set_keyboard_nav(&mut self, enabled: bool) {
212        self.keyboard_nav = enabled;
213    }
214
215    /// Enable or disable keyboard navigation
216    #[must_use]
217    #[inline]
218    pub const fn keyboard_nav(&self) -> bool {
219        self.keyboard_nav
220    }
221
222    /// Changes the scaling factor used by the application.
223    #[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    /// Set header bar title
230    #[inline]
231    pub fn set_header_title(&mut self, title: String) {
232        self.window.header_title = title;
233    }
234
235    #[inline]
236    /// Whether to show or hide the main window's content.
237    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    /// Call this whenever the scaling factor or window width has changed.
243    fn is_condensed_update(&mut self) {
244        // Nav bar (280px) + padding (8px) + content (360px)
245        let mut breakpoint = 280.0 + 8.0 + 360.0;
246        //TODO: the app may return None from the context_drawer function even if show_context is true
247        if self.window.show_context && !self.window.context_is_overlay {
248            // Context drawer min width (344px) + padding (8px)
249            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        // There is a conflict if the view is condensed and both the nav bar and context drawer are open on the same layer
258        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        // Content width (360px) + padding (8px)
269        let mut reserved_width = 360.0 + 8.0;
270        if has_nav {
271            // Navbar width (280px) + padding (8px)
272            reserved_width += 280.0 + 8.0;
273        }
274
275        #[allow(clippy::manual_clamp)]
276        // This logic is to ensure the context drawer does not take up too much of the content's space
277        // The minimum width is 344px and the maximum with is 480px
278        // We want to keep the content at least 360px until going down to the minimum width
279        (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        // Ensure nav bar is closed if condensed view and context drawer is opened
287        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    /// Whether the nav panel is visible or not
299    #[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        // Ensure context drawer is closed if condensed view and nav bar is opened
337        if self.condensed_conflict() {
338            self.window.show_context = false;
339            self.is_condensed_update();
340            // Sync nav bar state if the view is no longer condensed after closing the context drawer
341            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    /// Set the height of the main window.
359    pub(crate) const fn set_window_height(&mut self, new_height: f32) {
360        self.window.height = new_height;
361    }
362
363    #[inline]
364    /// Set the width of the main window.
365    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    /// Get the current system theme
372    pub const fn system_theme(&self) -> &Theme {
373        &self.system_theme
374    }
375
376    #[inline]
377    #[must_use]
378    /// Get the current system theme mode
379    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    /// Get the current focused window if it exists
426    #[must_use]
427    #[inline]
428    pub fn focused_window(&self) -> Option<window::Id> {
429        self.focused_window.last().copied()
430    }
431
432    /// Get the current focus chain of windows
433    #[must_use]
434    #[inline]
435    pub fn focus_chain(&self) -> &[window::Id] {
436        &self.focused_window
437    }
438
439    /// Whether the application should use a dark theme, according to the system
440    #[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    /// The [`Id`] of the main window
448    #[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    /// Reset the tracked main window to a new value
455    #[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    /// Whether the active theme's frosted (blur) styling should apply to this
539    /// app's surfaces.
540    #[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    /// Calculate suggested corners for each app type main window
604    #[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}