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 iced::{Limits, Size, window};
10use iced_core::window::Id;
11use palette::Srgba;
12use slotmap::Key;
13
14use crate::Theme;
15
16/// Status of the nav bar and its panels.
17#[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/// COSMIC-specific settings for windows.
26#[allow(clippy::struct_excessive_bools)]
27#[derive(Clone)]
28pub struct Window {
29    /// Label to display as header bar title.
30    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/// COSMIC-specific application settings
47#[derive(Clone)]
48pub struct Core {
49    /// Enables debug features in cosmic/iced.
50    pub debug: bool,
51
52    /// Disables loading the icon theme from cosmic-config.
53    pub(super) icon_theme_override: bool,
54
55    /// Whether the window is too small for the nav bar + main content.
56    is_condensed: bool,
57
58    /// Enables built in keyboard navigation
59    pub(super) keyboard_nav: bool,
60
61    /// Current status of the nav bar panel.
62    nav_bar: NavBar,
63
64    /// Scaling factor used by the application
65    scale_factor: f32,
66
67    /// Window focus state
68    pub(super) focused_window: Vec<window::Id>,
69
70    pub(super) theme_sub_counter: u64,
71    /// Last known system theme
72    pub(super) system_theme: Theme,
73
74    /// Configured theme mode
75    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    /// Whether the window is too small for the nav bar + main content.
170    #[must_use]
171    #[inline]
172    pub const fn is_condensed(&self) -> bool {
173        self.is_condensed
174    }
175
176    /// The scaling factor used by the application.
177    #[must_use]
178    #[inline]
179    pub const fn scale_factor(&self) -> f32 {
180        self.scale_factor
181    }
182
183    /// Enable or disable keyboard navigation
184    #[inline]
185    pub const fn set_keyboard_nav(&mut self, enabled: bool) {
186        self.keyboard_nav = enabled;
187    }
188
189    /// Enable or disable keyboard navigation
190    #[must_use]
191    #[inline]
192    pub const fn keyboard_nav(&self) -> bool {
193        self.keyboard_nav
194    }
195
196    /// Changes the scaling factor used by the application.
197    #[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    /// Set header bar title
204    #[inline]
205    pub fn set_header_title(&mut self, title: String) {
206        self.window.header_title = title;
207    }
208
209    #[inline]
210    /// Whether to show or hide the main window's content.
211    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    /// Call this whenever the scaling factor or window width has changed.
217    fn is_condensed_update(&mut self) {
218        // Nav bar (280px) + padding (8px) + content (360px)
219        let mut breakpoint = 280.0 + 8.0 + 360.0;
220        //TODO: the app may return None from the context_drawer function even if show_context is true
221        if self.window.show_context && !self.window.context_is_overlay {
222            // Context drawer min width (344px) + padding (8px)
223            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        // There is a conflict if the view is condensed and both the nav bar and context drawer are open on the same layer
232        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        // Content width (360px) + padding (8px)
243        let mut reserved_width = 360.0 + 8.0;
244        if has_nav {
245            // Navbar width (280px) + padding (8px)
246            reserved_width += 280.0 + 8.0;
247        }
248
249        #[allow(clippy::manual_clamp)]
250        // This logic is to ensure the context drawer does not take up too much of the content's space
251        // The minimum width is 344px and the maximum with is 480px
252        // We want to keep the content at least 360px until going down to the minimum width
253        (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        // Ensure nav bar is closed if condensed view and context drawer is opened
261        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    /// Whether the nav panel is visible or not
273    #[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        // Ensure context drawer is closed if condensed view and nav bar is opened
311        if self.condensed_conflict() {
312            self.window.show_context = false;
313            self.is_condensed_update();
314            // Sync nav bar state if the view is no longer condensed after closing the context drawer
315            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    /// Set the height of the main window.
333    pub(crate) const fn set_window_height(&mut self, new_height: f32) {
334        self.window.height = new_height;
335    }
336
337    #[inline]
338    /// Set the width of the main window.
339    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    /// Get the current system theme
346    pub const fn system_theme(&self) -> &Theme {
347        &self.system_theme
348    }
349
350    #[inline]
351    #[must_use]
352    /// Get the current system theme mode
353    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.as_ref() {
365            return cosmic_config::dbus::watcher_subscription(
366                settings_daemon.clone(),
367                config_id,
368                false,
369            );
370        }
371        cosmic_config::config_subscription(
372            std::any::TypeId::of::<T>(),
373            std::borrow::Cow::Borrowed(config_id),
374            T::VERSION,
375        )
376    }
377
378    pub fn watch_state<
379        T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
380    >(
381        &self,
382        state_id: &'static str,
383    ) -> iced::Subscription<cosmic_config::Update<T>> {
384        #[cfg(all(feature = "dbus-config", target_os = "linux"))]
385        if let Some(settings_daemon) = self.settings_daemon.as_ref() {
386            return cosmic_config::dbus::watcher_subscription(
387                settings_daemon.clone(),
388                state_id,
389                true,
390            );
391        }
392        cosmic_config::config_subscription(
393            std::any::TypeId::of::<T>(),
394            std::borrow::Cow::Borrowed(state_id),
395            T::VERSION,
396        )
397    }
398
399    /// Get the current focused window if it exists
400    #[must_use]
401    #[inline]
402    pub fn focused_window(&self) -> Option<window::Id> {
403        self.focused_window.last().copied()
404    }
405
406    /// Get the current focus chain of windows
407    #[must_use]
408    #[inline]
409    pub fn focus_chain(&self) -> &[window::Id] {
410        &self.focused_window
411    }
412
413    /// Whether the application should use a dark theme, according to the system
414    #[must_use]
415    #[inline]
416    pub fn system_is_dark(&self) -> bool {
417        self.portal_is_dark
418            .unwrap_or(self.system_theme_mode.is_dark)
419    }
420
421    /// The [`Id`] of the main window
422    #[must_use]
423    #[inline]
424    pub fn main_window_id(&self) -> Option<window::Id> {
425        self.main_window.filter(|id| iced::window::Id::NONE != *id)
426    }
427
428    /// Reset the tracked main window to a new value
429    #[inline]
430    pub fn set_main_window_id(&mut self, mut id: Option<window::Id>) -> Option<window::Id> {
431        std::mem::swap(&mut self.main_window, &mut id);
432        id
433    }
434
435    #[cfg(feature = "winit")]
436    pub fn drag<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
437        let Some(id) = id.or(self.main_window) else {
438            return iced::Task::none();
439        };
440        crate::command::drag(id)
441    }
442
443    #[cfg(feature = "winit")]
444    pub fn maximize<M: Send + 'static>(
445        &self,
446        id: Option<window::Id>,
447        maximized: bool,
448    ) -> crate::app::Task<M> {
449        let Some(id) = id.or(self.main_window) else {
450            return iced::Task::none();
451        };
452        crate::command::maximize(id, maximized)
453    }
454
455    #[cfg(feature = "winit")]
456    pub fn minimize<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
457        let Some(id) = id.or(self.main_window) else {
458            return iced::Task::none();
459        };
460        crate::command::minimize(id)
461    }
462
463    #[cfg(feature = "winit")]
464    pub fn set_title<M: Send + 'static>(
465        &self,
466        id: Option<window::Id>,
467        title: String,
468    ) -> crate::app::Task<M> {
469        let Some(id) = id.or(self.main_window) else {
470            return iced::Task::none();
471        };
472        crate::command::set_title(id, title)
473    }
474
475    #[cfg(feature = "winit")]
476    pub fn set_windowed<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
477        let Some(id) = id.or(self.main_window) else {
478            return iced::Task::none();
479        };
480        crate::command::set_windowed(id)
481    }
482
483    #[cfg(feature = "winit")]
484    pub fn toggle_maximize<M: Send + 'static>(
485        &self,
486        id: Option<window::Id>,
487    ) -> crate::app::Task<M> {
488        let Some(id) = id.or(self.main_window) else {
489            return iced::Task::none();
490        };
491
492        crate::command::toggle_maximize(id)
493    }
494
495    // TODO should we emit tasks setting the corner radius or unsetting it if this is changed?
496    #[cfg(feature = "wayland")]
497    pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) {
498        self.sync_window_border_radii_to_theme = sync;
499    }
500
501    #[cfg(feature = "wayland")]
502    pub fn sync_window_border_radii_to_theme(&self) -> bool {
503        self.sync_window_border_radii_to_theme
504    }
505}