cosmic/app/
cosmic.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use std::borrow::Borrow;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use super::{Action, Application, ApplicationExt, Subscription};
9use crate::theme::{THEME, Theme, ThemeType};
10use crate::{Core, Element, keyboard_nav};
11#[cfg(feature = "wayland")]
12use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
13use cosmic_theme::ThemeMode;
14#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
15use iced::Application as IcedApplication;
16#[cfg(feature = "wayland")]
17use iced::event::wayland;
18use iced::{Task, window};
19use iced_futures::event::listen_with;
20use palette::color_difference::EuclideanDistance;
21
22#[derive(Default)]
23pub struct Cosmic<App: Application> {
24    pub app: App,
25    #[cfg(feature = "wayland")]
26    pub surface_views: HashMap<
27        window::Id,
28        Box<dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action<App::Message>>>,
29    >,
30}
31
32impl<T: Application> Cosmic<T>
33where
34    T::Message: Send + 'static,
35{
36    pub fn init(
37        (mut core, flags): (Core, T::Flags),
38    ) -> (Self, iced::Task<crate::Action<T::Message>>) {
39        #[cfg(feature = "dbus-config")]
40        {
41            use iced_futures::futures::executor::block_on;
42            core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok();
43        }
44
45        let (model, command) = T::init(core, flags);
46
47        (Self::new(model), command)
48    }
49
50    #[cfg(not(feature = "multi-window"))]
51    pub fn title(&self) -> String {
52        self.app.title().to_string()
53    }
54
55    #[cfg(feature = "multi-window")]
56    pub fn title(&self, id: window::Id) -> String {
57        self.app.title(id).to_string()
58    }
59
60    pub fn surface_update(
61        &mut self,
62        _surface_message: crate::surface::Action,
63    ) -> iced::Task<crate::Action<T::Message>> {
64        #[cfg(feature = "surface-message")]
65        match _surface_message {
66            #[cfg(feature = "wayland")]
67            crate::surface::Action::AppSubsurface(settings, view) => {
68                let Some(settings) = std::sync::Arc::try_unwrap(settings)
69                                                                        .ok()
70                                                                        .and_then(|s| s.downcast::<Box<dyn Fn(&mut T) -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else {
71                                                                        tracing::error!("Invalid settings for subsurface");
72                                                                        return Task::none();
73                                                                    };
74
75                if let Some(view) = view.and_then(|view| {
76                    match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
77                        dyn for<'a> Fn(&'a T) -> Element<'a, crate::Action<T::Message>>
78                            + Send
79                            + Sync,
80                    >>() {
81                        Ok(v) => Some(v),
82                        Err(err) => {
83                            tracing::error!("Invalid view for subsurface view: {err:?}");
84
85                            None
86                        }
87                    }
88                }) {
89                    let settings = settings(&mut self.app);
90                    self.get_subsurface(settings, *view)
91                } else {
92                    iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app))
93                }
94            }
95            #[cfg(feature = "wayland")]
96            crate::surface::Action::Subsurface(settings, view) => {
97                let Some(settings) = std::sync::Arc::try_unwrap(settings)
98                                                                        .ok()
99                                                                        .and_then(|s| s.downcast::<Box<dyn Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else {
100                                                                        tracing::error!("Invalid settings for subsurface");
101                                                                        return Task::none();
102                                                                    };
103
104                if let Some(view) = view.and_then(|view| {
105                    match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
106                            dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
107                        >>() {
108                            Ok(v) => Some(v),
109                            Err(err) => {
110                                tracing::error!("Invalid view for subsurface view: {err:?}");
111
112                                None
113                            }
114                        }
115                }) {
116                    let settings = settings();
117                    self.get_subsurface(settings, Box::new(move |_| view()))
118                } else {
119                    iced_winit::commands::subsurface::get_subsurface(settings())
120                }
121            }
122            #[cfg(feature = "wayland")]
123            crate::surface::Action::AppPopup(settings, view) => {
124                let Some(settings) = std::sync::Arc::try_unwrap(settings)
125                                                                        .ok()
126                                                                        .and_then(|s| s.downcast::<Box<dyn Fn(&mut T) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else {
127                                                                        tracing::error!("Invalid settings for popup");
128                                                                        return Task::none();
129                                                                    };
130
131                if let Some(view) = view.and_then(|view| {
132                    match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
133                        dyn for<'a> Fn(&'a T) -> Element<'a, crate::Action<T::Message>>
134                            + Send
135                            + Sync,
136                    >>() {
137                        Ok(v) => Some(v),
138                        Err(err) => {
139                            tracing::error!("Invalid view for subsurface view: {err:?}");
140                            None
141                        }
142                    }
143                }) {
144                    let settings = settings(&mut self.app);
145
146                    self.get_popup(settings, *view)
147                } else {
148                    iced_winit::commands::popup::get_popup(settings(&mut self.app))
149                }
150            }
151            #[cfg(feature = "wayland")]
152            crate::surface::Action::DestroyPopup(id) => {
153                iced_winit::commands::popup::destroy_popup(id)
154            }
155            #[cfg(feature = "wayland")]
156            crate::surface::Action::DestroySubsurface(id) => {
157                iced_winit::commands::subsurface::destroy_subsurface(id)
158            }
159            crate::surface::Action::ResponsiveMenuBar {
160                menu_bar,
161                limits,
162                size,
163            } => {
164                let core = self.app.core_mut();
165                core.menu_bars.insert(menu_bar, (limits, size));
166                iced::Task::none()
167            }
168            #[cfg(feature = "wayland")]
169            crate::surface::Action::Popup(settings, view) => {
170                let Some(settings) = std::sync::Arc::try_unwrap(settings)
171                                                                        .ok()
172                                                                        .and_then(|s| s.downcast::<Box<dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else {
173                                                                        tracing::error!("Invalid settings for popup");
174                                                                        return Task::none();
175                                                                    };
176
177                if let Some(view) = view.and_then(|view| {
178                    match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
179                            dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
180                        >>() {
181                            Ok(v) => Some(v),
182                            Err(err) => {
183                                tracing::error!("Invalid view for subsurface view: {err:?}");
184                                None
185                            }
186                        }
187                }) {
188                    let settings = settings();
189
190                    self.get_popup(settings, Box::new(move |_| view()))
191                } else {
192                    iced_winit::commands::popup::get_popup(settings())
193                }
194            }
195            crate::surface::Action::Ignore => iced::Task::none(),
196            crate::surface::Action::Task(f) => {
197                f().map(|sm| crate::Action::Cosmic(Action::Surface(sm)))
198            }
199            _ => iced::Task::none(),
200        }
201
202        #[cfg(not(feature = "surface-message"))]
203        iced::Task::none()
204    }
205
206    pub fn update(
207        &mut self,
208        message: crate::Action<T::Message>,
209    ) -> iced::Task<crate::Action<T::Message>> {
210        let message = match message {
211            crate::Action::App(message) => self.app.update(message),
212            crate::Action::Cosmic(message) => self.cosmic_update(message),
213            crate::Action::None => iced::Task::none(),
214            #[cfg(feature = "single-instance")]
215            crate::Action::DbusActivation(message) => self.app.dbus_activation(message),
216        };
217
218        #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
219        crate::malloc::trim(0);
220
221        message
222    }
223
224    #[cfg(not(feature = "multi-window"))]
225    pub fn scale_factor(&self) -> f64 {
226        f64::from(self.app.core().scale_factor())
227    }
228
229    #[cfg(feature = "multi-window")]
230    pub fn scale_factor(&self, _id: window::Id) -> f64 {
231        f64::from(self.app.core().scale_factor())
232    }
233
234    pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance {
235        if let Some(style) = self.app.style() {
236            style
237        } else if self.app.core().window.sharp_corners {
238            let theme = THEME.lock().unwrap();
239            crate::style::iced::application::appearance(theme.borrow())
240        } else {
241            let theme = THEME.lock().unwrap();
242            iced_runtime::Appearance {
243                background_color: iced_core::Color::TRANSPARENT,
244                icon_color: theme.cosmic().on_bg_color().into(),
245                text_color: theme.cosmic().on_bg_color().into(),
246            }
247        }
248    }
249
250    #[allow(clippy::too_many_lines)]
251    #[cold]
252    pub fn subscription(&self) -> Subscription<crate::Action<T::Message>> {
253        let window_events = listen_with(|event, _, id| {
254            match event {
255                iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => {
256                    return Some(Action::WindowResize(id, width, height));
257                }
258                iced::Event::Window(window::Event::Closed) => {
259                    return Some(Action::SurfaceClosed(id));
260                }
261                iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)),
262                iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)),
263                #[cfg(feature = "wayland")]
264                iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
265                    match event {
266                        wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
267                        | wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => {
268                            return Some(Action::SurfaceClosed(id));
269                        }
270                        #[cfg(feature = "applet")]
271                        wayland::Event::Window(
272                            iced::event::wayland::WindowEvent::SuggestedBounds(b),
273                        ) => {
274                            return Some(Action::SuggestedBounds(b));
275                        }
276                        _ => (),
277                    }
278                }
279                _ => (),
280            }
281
282            None
283        });
284
285        let mut subscriptions = vec![
286            self.app.subscription().map(crate::Action::App),
287            self.app
288                .core()
289                .watch_config::<crate::config::CosmicTk>(crate::config::ID)
290                .map(|update| {
291                    for why in update
292                        .errors
293                        .into_iter()
294                        .filter(cosmic_config::Error::is_err)
295                    {
296                        tracing::error!(?why, "cosmic toolkit config update error");
297                    }
298
299                    crate::Action::Cosmic(Action::ToolkitConfig(update.config))
300                }),
301            self.app
302                .core()
303                .watch_config::<cosmic_theme::Theme>(
304                    if if let ThemeType::System { prefer_dark, .. } =
305                        THEME.lock().unwrap().theme_type
306                    {
307                        prefer_dark
308                    } else {
309                        None
310                    }
311                    .unwrap_or_else(|| self.app.core().system_theme_mode.is_dark)
312                    {
313                        cosmic_theme::DARK_THEME_ID
314                    } else {
315                        cosmic_theme::LIGHT_THEME_ID
316                    },
317                )
318                .map(|update| {
319                    for why in update
320                        .errors
321                        .into_iter()
322                        .filter(cosmic_config::Error::is_err)
323                    {
324                        tracing::error!(?why, "cosmic theme config update error");
325                    }
326                    Action::SystemThemeChange(
327                        update.keys,
328                        crate::theme::Theme::system(Arc::new(update.config)),
329                    )
330                })
331                .map(crate::Action::Cosmic),
332            self.app
333                .core()
334                .watch_config::<ThemeMode>(cosmic_theme::THEME_MODE_ID)
335                .map(|update| {
336                    for error in update
337                        .errors
338                        .into_iter()
339                        .filter(cosmic_config::Error::is_err)
340                    {
341                        tracing::error!(?error, "error reading system theme mode update");
342                    }
343                    Action::SystemThemeModeChange(update.keys, update.config)
344                })
345                .map(crate::Action::Cosmic),
346            window_events.map(crate::Action::Cosmic),
347            #[cfg(feature = "xdg-portal")]
348            crate::theme::portal::desktop_settings()
349                .map(Action::DesktopSettings)
350                .map(crate::Action::Cosmic),
351        ];
352
353        if self.app.core().keyboard_nav {
354            subscriptions.push(
355                keyboard_nav::subscription()
356                    .map(Action::KeyboardNav)
357                    .map(crate::Action::Cosmic),
358            );
359        }
360
361        #[cfg(feature = "single-instance")]
362        if self.app.core().single_instance {
363            subscriptions.push(crate::dbus_activation::subscription::<T>());
364        }
365
366        Subscription::batch(subscriptions)
367    }
368
369    #[cfg(not(feature = "multi-window"))]
370    pub fn theme(&self) -> Theme {
371        crate::theme::active()
372    }
373
374    #[cfg(feature = "multi-window")]
375    pub fn theme(&self, _id: window::Id) -> Theme {
376        crate::theme::active()
377    }
378
379    #[cfg(feature = "multi-window")]
380    pub fn view(&self, id: window::Id) -> Element<crate::Action<T::Message>> {
381        #[cfg(feature = "wayland")]
382        if let Some(v) = self.surface_views.get(&id) {
383            return v(&self.app);
384        }
385        if !self
386            .app
387            .core()
388            .main_window_id()
389            .is_some_and(|main_id| main_id == id)
390        {
391            return self.app.view_window(id).map(crate::Action::App);
392        }
393
394        let view = if self.app.core().window.use_template {
395            self.app.view_main()
396        } else {
397            self.app.view().map(crate::Action::App)
398        };
399
400        #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
401        crate::malloc::trim(0);
402
403        view
404    }
405
406    #[cfg(not(feature = "multi-window"))]
407    pub fn view(&self) -> Element<crate::Action<T::Message>> {
408        let view = self.app.view_main();
409
410        #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
411        crate::malloc::trim(0);
412
413        view
414    }
415}
416
417impl<T: Application> Cosmic<T> {
418    #[allow(clippy::unused_self)]
419    #[cold]
420    pub fn close(&mut self) -> iced::Task<crate::Action<T::Message>> {
421        if let Some(id) = self.app.core().main_window_id() {
422            iced::window::close(id)
423        } else {
424            iced::Task::none()
425        }
426    }
427
428    #[allow(clippy::too_many_lines)]
429    fn cosmic_update(&mut self, message: Action) -> iced::Task<crate::Action<T::Message>> {
430        match message {
431            Action::WindowMaximized(id, maximized) => {
432                if self
433                    .app
434                    .core()
435                    .main_window_id()
436                    .is_some_and(|main_id| main_id == id)
437                {
438                    self.app.core_mut().window.sharp_corners = maximized;
439                }
440            }
441
442            Action::WindowResize(id, width, height) => {
443                if self
444                    .app
445                    .core()
446                    .main_window_id()
447                    .is_some_and(|main_id| main_id == id)
448                {
449                    self.app.core_mut().set_window_width(width);
450                    self.app.core_mut().set_window_height(height);
451                }
452
453                self.app.on_window_resize(id, width, height);
454
455                //TODO: more efficient test of maximized (winit has no event for maximize if set by the OS)
456                return iced::window::get_maximized(id).map(move |maximized| {
457                    crate::Action::Cosmic(Action::WindowMaximized(id, maximized))
458                });
459            }
460
461            #[cfg(feature = "wayland")]
462            Action::WindowState(id, state) => {
463                if self
464                    .app
465                    .core()
466                    .main_window_id()
467                    .is_some_and(|main_id| main_id == id)
468                {
469                    self.app.core_mut().window.sharp_corners = state.intersects(
470                        WindowState::MAXIMIZED
471                            | WindowState::FULLSCREEN
472                            | WindowState::TILED
473                            | WindowState::TILED_RIGHT
474                            | WindowState::TILED_LEFT
475                            | WindowState::TILED_TOP
476                            | WindowState::TILED_BOTTOM,
477                    );
478                }
479            }
480
481            #[cfg(feature = "wayland")]
482            Action::WmCapabilities(id, capabilities) => {
483                if self
484                    .app
485                    .core()
486                    .main_window_id()
487                    .is_some_and(|main_id| main_id == id)
488                {
489                    self.app.core_mut().window.show_maximize =
490                        capabilities.contains(WindowManagerCapabilities::MAXIMIZE);
491                    self.app.core_mut().window.show_minimize =
492                        capabilities.contains(WindowManagerCapabilities::MINIMIZE);
493                    self.app.core_mut().window.show_window_menu =
494                        capabilities.contains(WindowManagerCapabilities::WINDOW_MENU);
495                }
496            }
497
498            Action::KeyboardNav(message) => match message {
499                keyboard_nav::Action::FocusNext => {
500                    return iced::widget::focus_next().map(crate::Action::Cosmic);
501                }
502                keyboard_nav::Action::FocusPrevious => {
503                    return iced::widget::focus_previous().map(crate::Action::Cosmic);
504                }
505                keyboard_nav::Action::Escape => return self.app.on_escape(),
506                keyboard_nav::Action::Search => return self.app.on_search(),
507
508                keyboard_nav::Action::Fullscreen => return self.app.core().toggle_maximize(None),
509            },
510
511            Action::ContextDrawer(show) => {
512                self.app.core_mut().set_show_context(show);
513                return self.app.on_context_drawer();
514            }
515
516            Action::Drag => return self.app.core().drag(None),
517
518            Action::Minimize => return self.app.core().minimize(None),
519
520            Action::Maximize => return self.app.core().toggle_maximize(None),
521
522            Action::NavBar(key) => {
523                self.app.core_mut().nav_bar_set_toggled_condensed(false);
524                return self.app.on_nav_select(key);
525            }
526
527            Action::NavBarContext(key) => {
528                self.app.core_mut().nav_bar_set_context(key);
529                return self.app.on_nav_context(key);
530            }
531
532            Action::ToggleNavBar => {
533                self.app.core_mut().nav_bar_toggle();
534            }
535
536            Action::ToggleNavBarCondensed => {
537                self.app.core_mut().nav_bar_toggle_condensed();
538            }
539
540            Action::AppThemeChange(mut theme) => {
541                if let ThemeType::System { theme: _, .. } = theme.theme_type {
542                    self.app.core_mut().theme_sub_counter += 1;
543
544                    let portal_accent = self.app.core().portal_accent;
545                    if let Some(a) = portal_accent {
546                        let t_inner = theme.cosmic();
547                        if a.distance_squared(*t_inner.accent_color()) > 0.00001 {
548                            theme = Theme::system(Arc::new(t_inner.with_accent(a)));
549                        }
550                    };
551                }
552
553                THEME.lock().unwrap().set_theme(theme.theme_type);
554            }
555
556            Action::SystemThemeChange(keys, theme) => {
557                let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark();
558                // Ignore updates if the current theme mode does not match.
559                if cur_is_dark != theme.cosmic().is_dark {
560                    return iced::Task::none();
561                }
562                let cmd = self.app.system_theme_update(&keys, theme.cosmic());
563                // Record the last-known system theme in event that the current theme is custom.
564                self.app.core_mut().system_theme = theme.clone();
565                let portal_accent = self.app.core().portal_accent;
566                {
567                    let mut cosmic_theme = THEME.lock().unwrap();
568
569                    // Only apply update if the theme is set to load a system theme
570                    if let ThemeType::System {
571                        theme: _,
572                        prefer_dark,
573                    } = cosmic_theme.theme_type
574                    {
575                        let mut new_theme = if let Some(a) = portal_accent {
576                            let t_inner = theme.cosmic();
577                            if a.distance_squared(*t_inner.accent_color()) > 0.00001 {
578                                Theme::system(Arc::new(t_inner.with_accent(a)))
579                            } else {
580                                theme
581                            }
582                        } else {
583                            theme
584                        };
585                        new_theme.theme_type.prefer_dark(prefer_dark);
586
587                        cosmic_theme.set_theme(new_theme.theme_type);
588                    }
589                }
590
591                return cmd;
592            }
593
594            Action::ScaleFactor(factor) => {
595                self.app.core_mut().set_scale_factor(factor);
596            }
597
598            Action::Close => {
599                return match self.app.on_app_exit() {
600                    Some(message) => self.app.update(message),
601                    None => self.close(),
602                };
603            }
604            Action::SystemThemeModeChange(keys, mode) => {
605                if match THEME.lock().unwrap().theme_type {
606                    ThemeType::System {
607                        theme: _,
608                        prefer_dark,
609                    } => prefer_dark.is_some(),
610                    _ => false,
611                } {
612                    return iced::Task::none();
613                }
614                let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)];
615
616                let core = self.app.core_mut();
617                let prev_is_dark = core.system_is_dark();
618                core.system_theme_mode = mode;
619                let is_dark = core.system_is_dark();
620                let changed = prev_is_dark != is_dark;
621                if changed {
622                    core.theme_sub_counter += 1;
623                    let mut new_theme = if is_dark {
624                        crate::theme::system_dark()
625                    } else {
626                        crate::theme::system_light()
627                    };
628                    cmds.push(self.app.system_theme_update(&[], new_theme.cosmic()));
629
630                    let core = self.app.core_mut();
631                    new_theme = if let Some(a) = core.portal_accent {
632                        let t_inner = new_theme.cosmic();
633                        if a.distance_squared(*t_inner.accent_color()) > 0.00001 {
634                            Theme::system(Arc::new(t_inner.with_accent(a)))
635                        } else {
636                            new_theme
637                        }
638                    } else {
639                        new_theme
640                    };
641
642                    core.system_theme = new_theme.clone();
643                    {
644                        let mut cosmic_theme = THEME.lock().unwrap();
645                        // Only apply update if the theme is set to load a system theme
646                        if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type {
647                            cosmic_theme.set_theme(new_theme.theme_type);
648                        }
649                    }
650                }
651                return Task::batch(cmds);
652            }
653            Action::Activate(_token) =>
654            {
655                #[cfg(feature = "wayland")]
656                if let Some(id) = self.app.core().main_window_id() {
657                    return iced_winit::platform_specific::commands::activation::activate(
658                        id,
659                        #[allow(clippy::used_underscore_binding)]
660                        _token,
661                    );
662                }
663            }
664
665            Action::Surface(action) => return self.surface_update(action),
666
667            Action::SurfaceClosed(id) => {
668                let mut ret = if let Some(msg) = self.app.on_close_requested(id) {
669                    self.app.update(msg)
670                } else {
671                    Task::none()
672                };
673                let core = self.app.core();
674                if core.exit_on_main_window_closed
675                    && core.main_window_id().is_some_and(|m_id| id == m_id)
676                {
677                    ret = Task::batch(vec![iced::exit::<crate::Action<T::Message>>()]);
678                }
679                return ret;
680            }
681
682            Action::ShowWindowMenu => {
683                if let Some(id) = self.app.core().main_window_id() {
684                    return iced::window::show_system_menu(id);
685                }
686            }
687
688            #[cfg(feature = "xdg-portal")]
689            Action::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => {
690                use ashpd::desktop::settings::ColorScheme;
691                if match THEME.lock().unwrap().theme_type {
692                    ThemeType::System {
693                        theme: _,
694                        prefer_dark,
695                    } => prefer_dark.is_some(),
696                    _ => false,
697                } {
698                    return iced::Task::none();
699                }
700                let is_dark = match s {
701                    ColorScheme::NoPreference => None,
702                    ColorScheme::PreferDark => Some(true),
703                    ColorScheme::PreferLight => Some(false),
704                };
705                let core = self.app.core_mut();
706                let prev_is_dark = core.system_is_dark();
707                core.portal_is_dark = is_dark;
708                let is_dark = core.system_is_dark();
709                let changed = prev_is_dark != is_dark;
710                if changed {
711                    core.theme_sub_counter += 1;
712                    let new_theme = if is_dark {
713                        crate::theme::system_dark()
714                    } else {
715                        crate::theme::system_light()
716                    };
717                    core.system_theme = new_theme.clone();
718                    {
719                        let mut cosmic_theme = THEME.lock().unwrap();
720
721                        // Only apply update if the theme is set to load a system theme
722                        if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type {
723                            cosmic_theme.set_theme(new_theme.theme_type);
724                        }
725                    }
726                }
727            }
728            #[cfg(feature = "xdg-portal")]
729            Action::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => {
730                use palette::Srgba;
731                let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0);
732                let core = self.app.core_mut();
733                core.portal_accent = Some(c);
734                let cur_accent = core.system_theme.cosmic().accent_color();
735
736                if cur_accent.distance_squared(*c) < 0.00001 {
737                    // skip calculations if we already have the same color
738                    return iced::Task::none();
739                }
740
741                {
742                    let mut cosmic_theme = THEME.lock().unwrap();
743
744                    // Only apply update if the theme is set to load a system theme
745                    if let ThemeType::System {
746                        theme: t,
747                        prefer_dark,
748                    } = cosmic_theme.theme_type.clone()
749                    {
750                        cosmic_theme.set_theme(ThemeType::System {
751                            theme: Arc::new(t.with_accent(c)),
752                            prefer_dark,
753                        });
754                    }
755                }
756            }
757            #[cfg(feature = "xdg-portal")]
758            Action::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => {
759                // TODO when high contrast is integrated in settings and all custom themes
760            }
761
762            Action::ToolkitConfig(config) => {
763                // Change the icon theme if not defined by the application.
764                if !self.app.core().icon_theme_override
765                    && crate::icon_theme::default() != config.icon_theme
766                {
767                    crate::icon_theme::set_default(config.icon_theme.clone());
768                }
769
770                *crate::config::COSMIC_TK.write().unwrap() = config;
771            }
772
773            Action::Focus(f) => {
774                self.app.core_mut().focused_window = Some(f);
775            }
776
777            Action::Unfocus(id) => {
778                let core = self.app.core_mut();
779                if core.focused_window.as_ref().is_some_and(|cur| *cur == id) {
780                    core.focused_window = None;
781                }
782            }
783            #[cfg(feature = "applet")]
784            Action::SuggestedBounds(b) => {
785                tracing::info!("Suggested bounds: {b:?}");
786                let core = self.app.core_mut();
787                core.applet.suggested_bounds = b;
788            }
789            _ => {}
790        }
791
792        iced::Task::none()
793    }
794}
795
796impl<App: Application> Cosmic<App> {
797    pub fn new(app: App) -> Self {
798        Self {
799            app,
800            #[cfg(feature = "wayland")]
801            surface_views: HashMap::new(),
802        }
803    }
804
805    #[cfg(feature = "wayland")]
806    /// Create a subsurface
807    pub fn get_subsurface(
808        &mut self,
809        settings: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings,
810        view: Box<
811            dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action<App::Message>> + Send + Sync,
812        >,
813    ) -> Task<crate::Action<App::Message>> {
814        use iced_winit::commands::subsurface::get_subsurface;
815
816        self.surface_views.insert(settings.id, view);
817        get_subsurface(settings)
818    }
819
820    #[cfg(feature = "wayland")]
821    /// Create a subsurface
822    pub fn get_popup(
823        &mut self,
824        settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings,
825        view: Box<
826            dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action<App::Message>> + Send + Sync,
827        >,
828    ) -> Task<crate::Action<App::Message>> {
829        use iced_winit::commands::popup::get_popup;
830
831        self.surface_views.insert(settings.id, view);
832        get_popup(settings)
833    }
834}