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 pub(super) icon_theme_override: bool,
68
69 is_condensed: bool,
71
72 pub(super) keyboard_nav: bool,
74
75 nav_bar: NavBar,
77
78 scale_factor: f32,
80
81 pub(super) focused_window: Vec<window::Id>,
83
84 pub(super) theme_sub_counter: u64,
85 pub(super) system_theme: Theme,
87
88 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 Window,
127 System,
129 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 #[must_use]
200 #[inline]
201 pub const fn is_condensed(&self) -> bool {
202 self.is_condensed
203 }
204
205 #[must_use]
207 #[inline]
208 pub const fn scale_factor(&self) -> f32 {
209 self.scale_factor
210 }
211
212 #[inline]
214 pub const fn set_keyboard_nav(&mut self, enabled: bool) {
215 self.keyboard_nav = enabled;
216 }
217
218 #[must_use]
220 #[inline]
221 pub const fn keyboard_nav(&self) -> bool {
222 self.keyboard_nav
223 }
224
225 #[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 #[inline]
234 pub fn set_header_title(&mut self, title: String) {
235 self.window.header_title = title;
236 }
237
238 #[inline]
239 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 fn is_condensed_update(&mut self) {
247 let mut breakpoint = 280.0 + 8.0 + 360.0;
249 if self.window.show_context && !self.window.context_is_overlay {
251 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 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 let mut reserved_width = 360.0 + 8.0;
273 if has_nav {
274 reserved_width += 280.0 + 8.0;
276 }
277
278 #[allow(clippy::manual_clamp)]
279 (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 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 #[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 if self.condensed_conflict() {
341 self.window.show_context = false;
342 self.is_condensed_update();
343 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 pub(crate) const fn set_window_height(&mut self, new_height: f32) {
363 self.window.height = new_height;
364 }
365
366 #[inline]
367 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 pub const fn system_theme(&self) -> &Theme {
376 &self.system_theme
377 }
378
379 #[inline]
380 #[must_use]
381 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 #[must_use]
430 #[inline]
431 pub fn focused_window(&self) -> Option<window::Id> {
432 self.focused_window.last().copied()
433 }
434
435 #[must_use]
437 #[inline]
438 pub fn focus_chain(&self) -> &[window::Id] {
439 &self.focused_window
440 }
441
442 #[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 #[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 #[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 #[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}