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    height: f32,
42    width: f32,
43}
44
45/// COSMIC-specific application settings
46#[derive(Clone)]
47pub struct Core {
48    /// Enables debug features in cosmic/iced.
49    pub debug: bool,
50
51    /// Disables loading the icon theme from cosmic-config.
52    pub(super) icon_theme_override: bool,
53
54    /// Whether the window is too small for the nav bar + main content.
55    is_condensed: bool,
56
57    /// Enables built in keyboard navigation
58    pub(super) keyboard_nav: bool,
59
60    /// Current status of the nav bar panel.
61    nav_bar: NavBar,
62
63    /// Scaling factor used by the application
64    scale_factor: f32,
65
66    /// Window focus state
67    pub(super) focused_window: Option<window::Id>,
68
69    pub(super) theme_sub_counter: u64,
70    /// Last known system theme
71    pub(super) system_theme: Theme,
72
73    /// Configured theme mode
74    pub(super) system_theme_mode: ThemeMode,
75
76    pub(super) portal_is_dark: Option<bool>,
77
78    pub(super) portal_accent: Option<Srgba>,
79
80    pub(super) portal_is_high_contrast: Option<bool>,
81
82    pub(super) title: HashMap<Id, String>,
83
84    pub window: Window,
85
86    #[cfg(feature = "applet")]
87    pub applet: crate::applet::Context,
88
89    #[cfg(feature = "single-instance")]
90    pub(crate) single_instance: bool,
91
92    #[cfg(feature = "dbus-config")]
93    pub(crate) settings_daemon: Option<cosmic_settings_daemon::CosmicSettingsDaemonProxy<'static>>,
94
95    pub(crate) main_window: Option<window::Id>,
96
97    pub(crate) exit_on_main_window_closed: bool,
98
99    pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
100}
101
102impl Default for Core {
103    fn default() -> Self {
104        Self {
105            debug: false,
106            icon_theme_override: false,
107            is_condensed: false,
108            keyboard_nav: true,
109            nav_bar: NavBar {
110                active: true,
111                context_id: crate::widget::nav_bar::Id::null(),
112                toggled: true,
113                toggled_condensed: false,
114            },
115            scale_factor: 1.0,
116            title: HashMap::new(),
117            theme_sub_counter: 0,
118            system_theme: crate::theme::active(),
119            system_theme_mode: ThemeMode::config()
120                .map(|c| {
121                    ThemeMode::get_entry(&c).unwrap_or_else(|(errors, mode)| {
122                        for why in errors.into_iter().filter(cosmic_config::Error::is_err) {
123                            tracing::error!(?why, "ThemeMode config entry error");
124                        }
125                        mode
126                    })
127                })
128                .unwrap_or_default(),
129            window: Window {
130                header_title: String::new(),
131                use_template: true,
132                content_container: true,
133                context_is_overlay: true,
134                sharp_corners: false,
135                show_context: false,
136                show_headerbar: true,
137                show_close: true,
138                show_maximize: true,
139                show_minimize: true,
140                show_window_menu: false,
141                height: 0.,
142                width: 0.,
143            },
144            focused_window: None,
145            #[cfg(feature = "applet")]
146            applet: crate::applet::Context::default(),
147            #[cfg(feature = "single-instance")]
148            single_instance: false,
149            #[cfg(feature = "dbus-config")]
150            settings_daemon: None,
151            portal_is_dark: None,
152            portal_accent: None,
153            portal_is_high_contrast: None,
154            main_window: None,
155            exit_on_main_window_closed: true,
156            menu_bars: HashMap::new(),
157        }
158    }
159}
160
161impl Core {
162    /// Whether the window is too small for the nav bar + main content.
163    #[must_use]
164    #[inline]
165    pub const fn is_condensed(&self) -> bool {
166        self.is_condensed
167    }
168
169    /// The scaling factor used by the application.
170    #[must_use]
171    #[inline]
172    pub const fn scale_factor(&self) -> f32 {
173        self.scale_factor
174    }
175
176    /// Enable or disable keyboard navigation
177    #[inline]
178    pub const fn set_keyboard_nav(&mut self, enabled: bool) {
179        self.keyboard_nav = enabled;
180    }
181
182    /// Enable or disable keyboard navigation
183    #[must_use]
184    #[inline]
185    pub const fn keyboard_nav(&self) -> bool {
186        self.keyboard_nav
187    }
188
189    /// Changes the scaling factor used by the application.
190    #[cold]
191    pub(crate) fn set_scale_factor(&mut self, factor: f32) {
192        self.scale_factor = factor;
193        self.is_condensed_update();
194    }
195
196    /// Set header bar title
197    #[inline]
198    pub fn set_header_title(&mut self, title: String) {
199        self.window.header_title = title;
200    }
201
202    #[inline]
203    /// Whether to show or hide the main window's content.
204    pub(crate) fn show_content(&self) -> bool {
205        !self.is_condensed || !self.nav_bar.toggled_condensed
206    }
207
208    #[allow(clippy::cast_precision_loss)]
209    /// Call this whenever the scaling factor or window width has changed.
210    fn is_condensed_update(&mut self) {
211        // Nav bar (280px) + padding (8px) + content (360px)
212        let mut breakpoint = 280.0 + 8.0 + 360.0;
213        //TODO: the app may return None from the context_drawer function even if show_context is true
214        if self.window.show_context && !self.window.context_is_overlay {
215            // Context drawer min width (344px) + padding (8px)
216            breakpoint += 344.0 + 8.0;
217        };
218        self.is_condensed = (breakpoint * self.scale_factor) > self.window.width;
219        self.nav_bar_update();
220    }
221
222    #[inline]
223    fn condensed_conflict(&self) -> bool {
224        // There is a conflict if the view is condensed and both the nav bar and context drawer are open on the same layer
225        self.is_condensed
226            && self.nav_bar.toggled_condensed
227            && self.window.show_context
228            && !self.window.context_is_overlay
229    }
230
231    #[inline]
232    pub(crate) fn context_width(&self, has_nav: bool) -> f32 {
233        let window_width = self.window.width / self.scale_factor;
234
235        // Content width (360px) + padding (8px)
236        let mut reserved_width = 360.0 + 8.0;
237        if has_nav {
238            // Navbar width (280px) + padding (8px)
239            reserved_width += 280.0 + 8.0;
240        }
241
242        #[allow(clippy::manual_clamp)]
243        // This logic is to ensure the context drawer does not take up too much of the content's space
244        // The minimum width is 344px and the maximum with is 480px
245        // We want to keep the content at least 360px until going down to the minimum width
246        (window_width - reserved_width).min(480.0).max(344.0)
247    }
248
249    #[cold]
250    pub fn set_show_context(&mut self, show: bool) {
251        self.window.show_context = show;
252        self.is_condensed_update();
253        // Ensure nav bar is closed if condensed view and context drawer is opened
254        if self.condensed_conflict() {
255            self.nav_bar.toggled_condensed = false;
256            self.is_condensed_update();
257        }
258    }
259
260    #[inline]
261    pub fn main_window_is(&self, id: iced::window::Id) -> bool {
262        self.main_window_id().is_some_and(|main_id| main_id == id)
263    }
264
265    /// Whether the nav panel is visible or not
266    #[must_use]
267    #[inline]
268    pub const fn nav_bar_active(&self) -> bool {
269        self.nav_bar.active
270    }
271
272    #[inline]
273    pub fn nav_bar_toggle(&mut self) {
274        self.nav_bar.toggled = !self.nav_bar.toggled;
275        self.nav_bar_set_toggled_condensed(self.nav_bar.toggled);
276    }
277
278    #[inline]
279    pub fn nav_bar_toggle_condensed(&mut self) {
280        self.nav_bar_set_toggled_condensed(!self.nav_bar.toggled_condensed);
281    }
282
283    #[inline]
284    pub(crate) const fn nav_bar_context(&self) -> nav_bar::Id {
285        self.nav_bar.context_id
286    }
287
288    #[inline]
289    pub(crate) fn nav_bar_set_context(&mut self, id: nav_bar::Id) {
290        self.nav_bar.context_id = id;
291    }
292
293    #[inline]
294    pub fn nav_bar_set_toggled(&mut self, toggled: bool) {
295        self.nav_bar.toggled = toggled;
296        self.nav_bar_set_toggled_condensed(self.nav_bar.toggled);
297    }
298
299    #[cold]
300    pub(crate) fn nav_bar_set_toggled_condensed(&mut self, toggled: bool) {
301        self.nav_bar.toggled_condensed = toggled;
302        self.nav_bar_update();
303        // Ensure context drawer is closed if condensed view and nav bar is opened
304        if self.condensed_conflict() {
305            self.window.show_context = false;
306            self.is_condensed_update();
307            // Sync nav bar state if the view is no longer condensed after closing the context drawer
308            if !self.is_condensed {
309                self.nav_bar.toggled = toggled;
310                self.nav_bar_update();
311            }
312        }
313    }
314
315    #[inline]
316    pub(crate) fn nav_bar_update(&mut self) {
317        self.nav_bar.active = if self.is_condensed {
318            self.nav_bar.toggled_condensed
319        } else {
320            self.nav_bar.toggled
321        };
322    }
323
324    #[inline]
325    /// Set the height of the main window.
326    pub(crate) const fn set_window_height(&mut self, new_height: f32) {
327        self.window.height = new_height;
328    }
329
330    #[inline]
331    /// Set the width of the main window.
332    pub(crate) fn set_window_width(&mut self, new_width: f32) {
333        self.window.width = new_width;
334        self.is_condensed_update();
335    }
336
337    #[inline]
338    /// Get the current system theme
339    pub const fn system_theme(&self) -> &Theme {
340        &self.system_theme
341    }
342
343    #[inline]
344    #[must_use]
345    /// Get the current system theme mode
346    pub const fn system_theme_mode(&self) -> ThemeMode {
347        self.system_theme_mode
348    }
349
350    pub fn watch_config<
351        T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
352    >(
353        &self,
354        config_id: &'static str,
355    ) -> iced::Subscription<cosmic_config::Update<T>> {
356        #[cfg(feature = "dbus-config")]
357        if let Some(settings_daemon) = self.settings_daemon.clone() {
358            return cosmic_config::dbus::watcher_subscription(settings_daemon, config_id, false);
359        }
360        cosmic_config::config_subscription(
361            std::any::TypeId::of::<T>(),
362            std::borrow::Cow::Borrowed(config_id),
363            T::VERSION,
364        )
365    }
366
367    pub fn watch_state<
368        T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq,
369    >(
370        &self,
371        state_id: &'static str,
372    ) -> iced::Subscription<cosmic_config::Update<T>> {
373        #[cfg(feature = "dbus-config")]
374        if let Some(settings_daemon) = self.settings_daemon.clone() {
375            return cosmic_config::dbus::watcher_subscription(settings_daemon, state_id, true);
376        }
377        cosmic_config::config_subscription(
378            std::any::TypeId::of::<T>(),
379            std::borrow::Cow::Borrowed(state_id),
380            T::VERSION,
381        )
382    }
383
384    /// Get the current focused window if it exists
385    #[must_use]
386    #[inline]
387    pub const fn focused_window(&self) -> Option<window::Id> {
388        self.focused_window
389    }
390
391    /// Whether the application should use a dark theme, according to the system
392    #[must_use]
393    #[inline]
394    pub fn system_is_dark(&self) -> bool {
395        self.portal_is_dark
396            .unwrap_or(self.system_theme_mode.is_dark)
397    }
398
399    /// The [`Id`] of the main window
400    #[must_use]
401    #[inline]
402    pub fn main_window_id(&self) -> Option<window::Id> {
403        self.main_window.filter(|id| iced::window::Id::NONE != *id)
404    }
405
406    /// Reset the tracked main window to a new value
407    #[inline]
408    pub fn set_main_window_id(&mut self, mut id: Option<window::Id>) -> Option<window::Id> {
409        std::mem::swap(&mut self.main_window, &mut id);
410        id
411    }
412
413    #[cfg(feature = "winit")]
414    pub fn drag<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
415        let Some(id) = id.or(self.main_window) else {
416            return iced::Task::none();
417        };
418        crate::command::drag(id)
419    }
420
421    #[cfg(feature = "winit")]
422    pub fn maximize<M: Send + 'static>(
423        &self,
424        id: Option<window::Id>,
425        maximized: bool,
426    ) -> crate::app::Task<M> {
427        let Some(id) = id.or(self.main_window) else {
428            return iced::Task::none();
429        };
430        crate::command::maximize(id, maximized)
431    }
432
433    #[cfg(feature = "winit")]
434    pub fn minimize<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
435        let Some(id) = id.or(self.main_window) else {
436            return iced::Task::none();
437        };
438        crate::command::minimize(id)
439    }
440
441    #[cfg(feature = "winit")]
442    pub fn set_title<M: Send + 'static>(
443        &self,
444        id: Option<window::Id>,
445        title: String,
446    ) -> crate::app::Task<M> {
447        let Some(id) = id.or(self.main_window) else {
448            return iced::Task::none();
449        };
450        crate::command::set_title(id, title)
451    }
452
453    #[cfg(feature = "winit")]
454    pub fn set_windowed<M: Send + 'static>(&self, id: Option<window::Id>) -> crate::app::Task<M> {
455        let Some(id) = id.or(self.main_window) else {
456            return iced::Task::none();
457        };
458        crate::command::set_windowed(id)
459    }
460
461    #[cfg(feature = "winit")]
462    pub fn toggle_maximize<M: Send + 'static>(
463        &self,
464        id: Option<window::Id>,
465    ) -> crate::app::Task<M> {
466        let Some(id) = id.or(self.main_window) else {
467            return iced::Task::none();
468        };
469
470        crate::command::toggle_maximize(id)
471    }
472}