1use std::borrow::Borrow;
5use std::collections::{HashMap, HashSet};
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(all(feature = "wayland", target_os = "linux"))]
12use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState};
13use cosmic_theme::ThemeMode;
14#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))]
15use iced::Application as IcedApplication;
16#[cfg(all(feature = "wayland", target_os = "linux"))]
17use iced::event::wayland;
18use iced::{Task, theme, window};
19use iced_futures::event::listen_with;
20#[cfg(all(feature = "wayland", target_os = "linux"))]
21use iced_winit::SurfaceIdWrapper;
22use palette::color_difference::EuclideanDistance;
23
24#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
25#[non_exhaustive]
26pub enum WindowingSystem {
27 UiKit,
28 AppKit,
29 Orbital,
30 OhosNdk,
31 Xlib,
32 Xcb,
33 Wayland,
34 Drm,
35 Gbm,
36 Win32,
37 WinRt,
38 Web,
39 WebCanvas,
40 WebOffscreenCanvas,
41 AndroidNdk,
42 Haiku,
43}
44
45pub(crate) static WINDOWING_SYSTEM: std::sync::OnceLock<WindowingSystem> =
46 std::sync::OnceLock::new();
47
48pub fn windowing_system() -> Option<WindowingSystem> {
49 WINDOWING_SYSTEM.get().copied()
50}
51
52fn init_windowing_system<M>(handle: window::raw_window_handle::WindowHandle) -> crate::Action<M> {
53 let raw = handle.as_ref();
54 let system = match raw {
55 window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit,
56 window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit,
57 window::raw_window_handle::RawWindowHandle::Orbital(_) => WindowingSystem::Orbital,
58 window::raw_window_handle::RawWindowHandle::OhosNdk(_) => WindowingSystem::OhosNdk,
59 window::raw_window_handle::RawWindowHandle::Xlib(_) => WindowingSystem::Xlib,
60 window::raw_window_handle::RawWindowHandle::Xcb(_) => WindowingSystem::Xcb,
61 window::raw_window_handle::RawWindowHandle::Wayland(_) => WindowingSystem::Wayland,
62 window::raw_window_handle::RawWindowHandle::Web(_) => WindowingSystem::Web,
63 window::raw_window_handle::RawWindowHandle::WebCanvas(_) => WindowingSystem::WebCanvas,
64 window::raw_window_handle::RawWindowHandle::WebOffscreenCanvas(_) => {
65 WindowingSystem::WebOffscreenCanvas
66 }
67 window::raw_window_handle::RawWindowHandle::AndroidNdk(_) => WindowingSystem::AndroidNdk,
68 window::raw_window_handle::RawWindowHandle::Haiku(_) => WindowingSystem::Haiku,
69 window::raw_window_handle::RawWindowHandle::Drm(_) => WindowingSystem::Drm,
70 window::raw_window_handle::RawWindowHandle::Gbm(_) => WindowingSystem::Gbm,
71 window::raw_window_handle::RawWindowHandle::Win32(_) => WindowingSystem::Win32,
72 window::raw_window_handle::RawWindowHandle::WinRt(_) => WindowingSystem::WinRt,
73 _ => {
74 tracing::warn!("Unknown windowing system: {raw:?}");
75 return crate::Action::Cosmic(Action::WindowingSystemInitialized);
76 }
77 };
78
79 _ = WINDOWING_SYSTEM.set(system);
80 crate::Action::Cosmic(Action::WindowingSystemInitialized)
81}
82
83#[derive(Default)]
84pub struct Cosmic<App: Application> {
85 pub app: App,
86 #[cfg(all(feature = "wayland", target_os = "linux"))]
87 pub surface_views: HashMap<
88 window::Id,
89 (
90 Option<window::Id>,
91 SurfaceIdWrapper,
92 Box<dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action<App::Message>>>,
93 ),
94 >,
95 pub tracked_windows: HashSet<window::Id>,
96 pub opened_surfaces: HashMap<window::Id, u32>,
97}
98
99impl<T: Application> Cosmic<T>
100where
101 T::Message: Send + 'static,
102{
103 pub fn init(
104 (mut core, flags): (Core, T::Flags),
105 ) -> (Self, iced::Task<crate::Action<T::Message>>) {
106 #[cfg(all(feature = "dbus-config", target_os = "linux"))]
107 {
108 use iced_futures::futures::executor::block_on;
109 core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok();
110 }
111 let id = core.main_window_id().unwrap_or(window::Id::RESERVED);
112
113 let (model, command) = T::init(core, flags);
114
115 (
116 Self::new(model),
117 Task::batch([
118 command,
119 iced_runtime::window::run_with_handle(id, init_windowing_system),
120 ]),
121 )
122 }
123
124 #[cfg(not(feature = "multi-window"))]
125 pub fn title(&self) -> String {
126 self.app.title().to_string()
127 }
128
129 #[cfg(feature = "multi-window")]
130 pub fn title(&self, id: window::Id) -> String {
131 self.app.title(id).to_string()
132 }
133
134 #[allow(clippy::too_many_lines)]
135 pub fn surface_update(
136 &mut self,
137 _surface_message: crate::surface::Action,
138 ) -> iced::Task<crate::Action<T::Message>> {
139 #[cfg(feature = "surface-message")]
140 match _surface_message {
141 #[cfg(all(feature = "wayland", target_os = "linux"))]
142 crate::surface::Action::AppSubsurface(settings, view) => {
143 let Some(settings) = std::sync::Arc::try_unwrap(settings)
144 .ok()
145 .and_then(|s| s.downcast::<Box<dyn Fn(&mut T) -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else {
146 tracing::error!("Invalid settings for subsurface");
147 return Task::none();
148 };
149
150 if let Some(view) = view.and_then(|view| {
151 match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
152 dyn for<'a> Fn(&'a T) -> Element<'a, crate::Action<T::Message>>
153 + Send
154 + Sync,
155 >>() {
156 Ok(v) => Some(v),
157 Err(err) => {
158 tracing::error!("Invalid view for subsurface view: {err:?}");
159
160 None
161 }
162 }
163 }) {
164 let settings = settings(&mut self.app);
165
166 self.get_subsurface(settings, *view)
167 } else {
168 iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app))
169 }
170 }
171 #[cfg(all(feature = "wayland", target_os = "linux"))]
172 crate::surface::Action::Subsurface(settings, view) => {
173 let Some(settings) = std::sync::Arc::try_unwrap(settings)
174 .ok()
175 .and_then(|s| s.downcast::<Box<dyn Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else {
176 tracing::error!("Invalid settings for subsurface");
177 return Task::none();
178 };
179
180 if let Some(view) = view.and_then(|view| {
181 match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
182 dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
183 >>() {
184 Ok(v) => Some(v),
185 Err(err) => {
186 tracing::error!("Invalid view for subsurface view: {err:?}");
187
188 None
189 }
190 }
191 }) {
192 let settings = settings();
193
194 self.get_subsurface(settings, Box::new(move |_| view()))
195 } else {
196 iced_winit::commands::subsurface::get_subsurface(settings())
197 }
198 }
199 #[cfg(all(feature = "wayland", target_os = "linux"))]
200 crate::surface::Action::AppPopup(settings, view) => {
201 let Some(settings) = std::sync::Arc::try_unwrap(settings)
202 .ok()
203 .and_then(|s| s.downcast::<Box<dyn Fn(&mut T) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else {
204 tracing::error!("Invalid settings for popup");
205 return Task::none();
206 };
207
208 if let Some(view) = view.and_then(|view| {
209 match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
210 dyn for<'a> Fn(&'a T) -> Element<'a, crate::Action<T::Message>>
211 + Send
212 + Sync,
213 >>() {
214 Ok(v) => Some(v),
215 Err(err) => {
216 tracing::error!("Invalid view for subsurface view: {err:?}");
217 None
218 }
219 }
220 }) {
221 let settings = settings(&mut self.app);
222
223 self.get_popup(settings, *view)
224 } else {
225 iced_winit::commands::popup::get_popup(settings(&mut self.app))
226 }
227 }
228 #[cfg(all(feature = "wayland", target_os = "linux"))]
229 crate::surface::Action::DestroyPopup(id) => {
230 iced_winit::commands::popup::destroy_popup(id)
231 }
232 #[cfg(all(feature = "wayland", target_os = "linux"))]
233 crate::surface::Action::DestroyTooltipPopup => {
234 #[cfg(feature = "applet")]
235 {
236 iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID)
237 }
238 #[cfg(not(feature = "applet"))]
239 {
240 Task::none()
241 }
242 }
243 #[cfg(all(feature = "wayland", target_os = "linux"))]
244 crate::surface::Action::DestroySubsurface(id) => {
245 iced_winit::commands::subsurface::destroy_subsurface(id)
246 }
247 #[cfg(all(feature = "wayland", target_os = "linux"))]
248 crate::surface::Action::DestroyWindow(id) => iced::window::close(id),
249 crate::surface::Action::ResponsiveMenuBar {
250 menu_bar,
251 limits,
252 size,
253 } => {
254 let core = self.app.core_mut();
255 core.menu_bars.insert(menu_bar, (limits, size));
256 iced::Task::none()
257 }
258 #[cfg(all(feature = "wayland", target_os = "linux"))]
259 crate::surface::Action::Popup(settings, view) => {
260 let Some(settings) = std::sync::Arc::try_unwrap(settings)
261 .ok()
262 .and_then(|s| s.downcast::<Box<dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else {
263 tracing::error!("Invalid settings for popup");
264 return Task::none();
265 };
266
267 if let Some(view) = view.and_then(|view| {
268 match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
269 dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
270 >>() {
271 Ok(v) => Some(v),
272 Err(err) => {
273 tracing::error!("Invalid view for subsurface view: {err:?}");
274 None
275 }
276 }
277 }) {
278 let settings = settings();
279
280 self.get_popup(settings, Box::new(move |_| view()))
281 } else {
282 iced_winit::commands::popup::get_popup(settings())
283 }
284 }
285 #[cfg(all(feature = "wayland", target_os = "linux"))]
286 crate::surface::Action::AppWindow(id, settings, view) => {
287 let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| {
288 s.downcast::<Box<dyn Fn(&mut T) -> iced::window::Settings + Send + Sync>>()
289 .ok()
290 }) else {
291 tracing::error!("Invalid settings for AppWindow");
292 return Task::none();
293 };
294
295 if let Some(view) = view.and_then(|view| {
296 match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
297 dyn for<'a> Fn(&'a T) -> Element<'a, crate::Action<T::Message>>
298 + Send
299 + Sync,
300 >>() {
301 Ok(v) => Some(v),
302 Err(err) => {
303 tracing::error!("Invalid view for AppWindow: {err:?}");
304 None
305 }
306 }
307 }) {
308 let settings = settings(&mut self.app);
309 self.tracked_windows.insert(id);
310
311 self.get_window(id, settings, *view)
312 } else {
313 let settings = settings(&mut self.app);
314
315 self.tracked_windows.insert(id);
316 iced_runtime::task::oneshot(|channel| {
317 iced_runtime::Action::Window(iced_runtime::window::Action::Open(
318 id, settings, channel,
319 ))
320 })
321 .discard()
322 }
323 }
324 #[cfg(all(feature = "wayland", target_os = "linux"))]
325 crate::surface::Action::Window(id, settings, view) => {
326 let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| {
327 s.downcast::<Box<dyn Fn() -> iced::window::Settings + Send + Sync>>()
328 .ok()
329 }) else {
330 tracing::error!("Invalid settings for Window");
331 return Task::none();
332 };
333
334 if let Some(view) = view.and_then(|view| {
335 match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
336 dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
337 >>() {
338 Ok(v) => Some(v),
339 Err(err) => {
340 tracing::error!("Invalid view for Window: {err:?}");
341 None
342 }
343 }
344 }) {
345 let settings = settings();
346 self.tracked_windows.insert(id);
347
348 self.get_window(id, settings, Box::new(move |_| view()))
349 } else {
350 let settings = settings();
351
352 self.tracked_windows.insert(id);
353
354 iced_runtime::task::oneshot(|channel| {
355 iced_runtime::Action::Window(iced_runtime::window::Action::Open(
356 id, settings, channel,
357 ))
358 })
359 .discard()
360 }
361 }
362
363 crate::surface::Action::Ignore => iced::Task::none(),
364 crate::surface::Action::Task(f) => {
365 f().map(|sm| crate::Action::Cosmic(Action::Surface(sm)))
366 }
367 _ => iced::Task::none(),
368 }
369
370 #[cfg(not(feature = "surface-message"))]
371 iced::Task::none()
372 }
373
374 pub fn update(
375 &mut self,
376 message: crate::Action<T::Message>,
377 ) -> iced::Task<crate::Action<T::Message>> {
378 let message = match message {
379 crate::Action::App(message) => self.app.update(message),
380 crate::Action::Cosmic(message) => self.cosmic_update(message),
381 crate::Action::None => iced::Task::none(),
382 #[cfg(feature = "single-instance")]
383 crate::Action::DbusActivation(message) => {
384 let mut task = self.app.dbus_activation(message);
385
386 if let Some(id) = self.app.core().main_window_id() {
387 let unminimize = iced_runtime::window::minimize::<()>(id, false);
388 task = task.chain(unminimize.discard());
389 }
390
391 task
392 }
393 };
394
395 #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
396 crate::malloc::trim(0);
397
398 message
399 }
400
401 #[cfg(not(feature = "multi-window"))]
402 pub fn scale_factor(&self) -> f64 {
403 f64::from(self.app.core().scale_factor())
404 }
405
406 #[cfg(feature = "multi-window")]
407 pub fn scale_factor(&self, _id: window::Id) -> f64 {
408 f64::from(self.app.core().scale_factor())
409 }
410
411 pub fn style(&self, theme: &Theme) -> theme::Style {
412 if let Some(style) = self.app.style() {
413 style
414 } else if self.app.core().window.is_maximized {
415 let theme = THEME.lock().unwrap();
416 crate::style::iced::application::style(theme.borrow())
417 } else {
418 let theme = THEME.lock().unwrap();
419
420 theme::Style {
421 background_color: iced_core::Color::TRANSPARENT,
422 icon_color: theme.cosmic().on_bg_color().into(),
423 text_color: theme.cosmic().on_bg_color().into(),
424 }
425 }
426 }
427
428 #[allow(clippy::too_many_lines)]
429 #[cold]
430 pub fn subscription(&self) -> Subscription<crate::Action<T::Message>> {
431 let window_events = listen_with(|event, _, id| {
432 match event {
433 iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => {
434 return Some(Action::WindowResize(id, width, height));
435 }
436 iced::Event::Window(window::Event::Opened { .. }) => {
437 return Some(Action::Opened(id));
438 }
439 iced::Event::Window(window::Event::Closed) => {
440 return Some(Action::SurfaceClosed(id));
441 }
442 iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)),
443 iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)),
444 #[cfg(all(feature = "wayland", target_os = "linux"))]
445 iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
446 match event {
447 wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
448 | wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => {
449 return Some(Action::SurfaceClosed(id));
450 }
451 #[cfg(feature = "applet")]
452 wayland::Event::Window(
453 iced::event::wayland::WindowEvent::SuggestedBounds(b),
454 ) => {
455 return Some(Action::SuggestedBounds(b));
456 }
457 #[cfg(all(feature = "wayland", target_os = "linux"))]
458 wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState(
459 s,
460 )) => {
461 return Some(Action::WindowState(id, s));
462 }
463 _ => (),
464 }
465 }
466 _ => (),
467 }
468
469 None
470 });
471
472 let mut subscriptions = vec![
473 self.app.subscription().map(crate::Action::App),
474 self.app
475 .core()
476 .watch_config::<crate::config::CosmicTk>(crate::config::ID)
477 .map(|update| {
478 for why in update
479 .errors
480 .into_iter()
481 .filter(cosmic_config::Error::is_err)
482 {
483 if let cosmic_config::Error::GetKey(_, err) = &why {
484 if err.kind() == std::io::ErrorKind::NotFound {
485 continue;
487 }
488 }
489 tracing::error!(?why, "cosmic toolkit config update error");
490 }
491
492 crate::Action::Cosmic(Action::ToolkitConfig(update.config))
493 }),
494 self.app
495 .core()
496 .watch_config::<cosmic_theme::Theme>(
497 if if let ThemeType::System { prefer_dark, .. } =
498 THEME.lock().unwrap().theme_type
499 {
500 prefer_dark
501 } else {
502 None
503 }
504 .unwrap_or_else(|| self.app.core().system_theme_mode.is_dark)
505 {
506 cosmic_theme::DARK_THEME_ID
507 } else {
508 cosmic_theme::LIGHT_THEME_ID
509 },
510 )
511 .map(|update| {
512 for why in update
513 .errors
514 .into_iter()
515 .filter(cosmic_config::Error::is_err)
516 {
517 tracing::error!(?why, "cosmic theme config update error");
518 }
519 Action::SystemThemeChange(
520 update.keys,
521 crate::theme::Theme::system(Arc::new(update.config)),
522 )
523 })
524 .map(crate::Action::Cosmic),
525 self.app
526 .core()
527 .watch_config::<ThemeMode>(cosmic_theme::THEME_MODE_ID)
528 .map(|update| {
529 for error in update
530 .errors
531 .into_iter()
532 .filter(cosmic_config::Error::is_err)
533 {
534 tracing::error!(?error, "error reading system theme mode update");
535 }
536 Action::SystemThemeModeChange(update.keys, update.config)
537 })
538 .map(crate::Action::Cosmic),
539 window_events.map(crate::Action::Cosmic),
540 #[cfg(feature = "xdg-portal")]
541 crate::theme::portal::desktop_settings()
542 .map(Action::DesktopSettings)
543 .map(crate::Action::Cosmic),
544 ];
545
546 if self.app.core().keyboard_nav {
547 subscriptions.push(
548 keyboard_nav::subscription()
549 .map(Action::KeyboardNav)
550 .map(crate::Action::Cosmic),
551 );
552 }
553
554 #[cfg(feature = "single-instance")]
555 if self.app.core().single_instance {
556 subscriptions.push(crate::dbus_activation::subscription::<T>());
557 }
558
559 Subscription::batch(subscriptions)
560 }
561
562 #[cfg(not(feature = "multi-window"))]
563 pub fn theme(&self) -> Theme {
564 crate::theme::active()
565 }
566
567 #[cfg(feature = "multi-window")]
568 pub fn theme(&self, _id: window::Id) -> Theme {
569 crate::theme::active()
570 }
571
572 #[cfg(feature = "multi-window")]
573 pub fn view(&self, id: window::Id) -> Element<'_, crate::Action<T::Message>> {
574 #[cfg(all(feature = "wayland", target_os = "linux"))]
575 if let Some((_, _, v)) = self.surface_views.get(&id) {
576 return v(&self.app);
577 }
578 if self
579 .app
580 .core()
581 .main_window_id()
582 .is_none_or(|main_id| main_id != id)
583 {
584 return self.app.view_window(id).map(crate::Action::App);
585 }
586
587 let view = if self.app.core().window.use_template {
588 self.app.view_main()
589 } else {
590 self.app.view().map(crate::Action::App)
591 };
592
593 #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
594 crate::malloc::trim(0);
595
596 view
597 }
598
599 #[cfg(not(feature = "multi-window"))]
600 pub fn view(&self) -> Element<crate::Action<T::Message>> {
601 let view = self.app.view_main();
602
603 #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
604 crate::malloc::trim(0);
605
606 view
607 }
608}
609
610impl<T: Application> Cosmic<T> {
611 #[allow(clippy::unused_self)]
612 #[cold]
613 pub fn close(&mut self) -> iced::Task<crate::Action<T::Message>> {
614 if let Some(id) = self.app.core().main_window_id() {
615 iced::window::close(id)
616 } else {
617 iced::Task::none()
618 }
619 }
620
621 #[allow(clippy::too_many_lines)]
622 fn cosmic_update(&mut self, message: Action) -> iced::Task<crate::Action<T::Message>> {
623 match message {
624 Action::WindowMaximized(id, maximized) => {
625 #[cfg(not(all(feature = "wayland", target_os = "linux")))]
626 if self
627 .app
628 .core()
629 .main_window_id()
630 .is_some_and(|main_id| main_id == id)
631 {
632 self.app.core_mut().window.sharp_corners = maximized;
633 }
634 }
635
636 Action::WindowResize(id, width, height) => {
637 if self
638 .app
639 .core()
640 .main_window_id()
641 .is_some_and(|main_id| main_id == id)
642 {
643 self.app.core_mut().set_window_width(width);
644 self.app.core_mut().set_window_height(height);
645 }
646
647 self.app.on_window_resize(id, width, height);
648
649 return iced::window::is_maximized(id).map(move |maximized| {
651 crate::Action::Cosmic(Action::WindowMaximized(id, maximized))
652 });
653 }
654
655 #[cfg(all(feature = "wayland", target_os = "linux"))]
656 Action::WindowState(id, state) => {
657 if self
658 .app
659 .core()
660 .main_window_id()
661 .is_some_and(|main_id| main_id == id)
662 {
663 self.app.core_mut().window.sharp_corners = state.intersects(
664 WindowState::MAXIMIZED
665 | WindowState::FULLSCREEN
666 | WindowState::TILED
667 | WindowState::TILED_RIGHT
668 | WindowState::TILED_LEFT
669 | WindowState::TILED_TOP
670 | WindowState::TILED_BOTTOM,
671 );
672 self.app.core_mut().window.is_maximized =
673 state.intersects(WindowState::MAXIMIZED | WindowState::FULLSCREEN);
674 }
675 if self.app.core().sync_window_border_radii_to_theme() {
676 use iced_runtime::platform_specific::wayland::CornerRadius;
677 use iced_winit::platform_specific::commands::corner_radius::corner_radius;
678
679 let theme = THEME.lock().unwrap();
680 let t = theme.cosmic();
681 let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
682 let cur_rad = CornerRadius {
683 top_left: radii[0].round() as u32,
684 top_right: radii[1].round() as u32,
685 bottom_right: radii[2].round() as u32,
686 bottom_left: radii[3].round() as u32,
687 };
688 let rounded = !self.app.core().window.sharp_corners;
689 return Task::batch([corner_radius(
690 id,
691 if rounded {
692 Some(cur_rad)
693 } else {
694 let rad_0 = t.radius_0();
695 Some(CornerRadius {
696 top_left: rad_0[0].round() as u32,
697 top_right: rad_0[1].round() as u32,
698 bottom_right: rad_0[2].round() as u32,
699 bottom_left: rad_0[3].round() as u32,
700 })
701 },
702 )
703 .discard()]);
704 }
705 }
706
707 #[cfg(all(feature = "wayland", target_os = "linux"))]
708 Action::WmCapabilities(id, capabilities) => {
709 if self
710 .app
711 .core()
712 .main_window_id()
713 .is_some_and(|main_id| main_id == id)
714 {
715 self.app.core_mut().window.show_maximize =
716 capabilities.contains(WindowManagerCapabilities::MAXIMIZE);
717 self.app.core_mut().window.show_minimize =
718 capabilities.contains(WindowManagerCapabilities::MINIMIZE);
719 self.app.core_mut().window.show_window_menu =
720 capabilities.contains(WindowManagerCapabilities::WINDOW_MENU);
721 }
722 }
723
724 Action::KeyboardNav(message) => match message {
725 keyboard_nav::Action::FocusNext => {
726 return iced::widget::operation::focus_next().map(crate::Action::Cosmic);
727 }
728 keyboard_nav::Action::FocusPrevious => {
729 return iced::widget::operation::focus_previous().map(crate::Action::Cosmic);
730 }
731 keyboard_nav::Action::Escape => return self.app.on_escape(),
732 keyboard_nav::Action::Search => return self.app.on_search(),
733
734 keyboard_nav::Action::Fullscreen => return self.app.core().toggle_maximize(None),
735 },
736
737 Action::ContextDrawer(show) => {
738 self.app.core_mut().set_show_context(show);
739 return self.app.on_context_drawer();
740 }
741
742 Action::Drag => return self.app.core().drag(None),
743
744 Action::Minimize => return self.app.core().minimize(None),
745
746 Action::Maximize => return self.app.core().toggle_maximize(None),
747
748 Action::NavBar(key) => {
749 self.app.core_mut().nav_bar_set_toggled_condensed(false);
750 return self.app.on_nav_select(key);
751 }
752
753 Action::NavBarContext(key) => {
754 self.app.core_mut().nav_bar_set_context(key);
755 return self.app.on_nav_context(key);
756 }
757
758 Action::ToggleNavBar => {
759 self.app.core_mut().nav_bar_toggle();
760 }
761
762 Action::ToggleNavBarCondensed => {
763 self.app.core_mut().nav_bar_toggle_condensed();
764 }
765
766 Action::AppThemeChange(mut theme) => {
767 if let ThemeType::System { theme: _, .. } = theme.theme_type {
768 self.app.core_mut().theme_sub_counter += 1;
769
770 let portal_accent = self.app.core().portal_accent;
771 if let Some(a) = portal_accent {
772 let t_inner = theme.cosmic();
773 if a.distance_squared(*t_inner.accent_color()) > 0.00001 {
774 theme = Theme::system(Arc::new(t_inner.with_accent(a)));
775 }
776 };
777 }
778
779 THEME.lock().unwrap().set_theme(theme.theme_type);
780 }
781
782 Action::SystemThemeChange(keys, theme) => {
783 let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark();
784 if cur_is_dark != theme.cosmic().is_dark {
786 return iced::Task::none();
787 }
788 let cmd = self.app.system_theme_update(&keys, theme.cosmic());
789 self.app.core_mut().system_theme = theme.clone();
791 let portal_accent = self.app.core().portal_accent;
792 {
793 let mut cosmic_theme = THEME.lock().unwrap();
794
795 if let ThemeType::System {
797 theme: _,
798 prefer_dark,
799 } = cosmic_theme.theme_type
800 {
801 let mut new_theme = if let Some(a) = portal_accent {
802 let t_inner = theme.cosmic();
803 if a.distance_squared(*t_inner.accent_color()) > 0.00001 {
804 Theme::system(Arc::new(t_inner.with_accent(a)))
805 } else {
806 theme
807 }
808 } else {
809 theme
810 };
811 new_theme.theme_type.prefer_dark(prefer_dark);
812
813 cosmic_theme.set_theme(new_theme.theme_type);
814 #[cfg(all(feature = "wayland", target_os = "linux"))]
815 if self.app.core().sync_window_border_radii_to_theme() {
816 use iced_runtime::platform_specific::wayland::CornerRadius;
817 use iced_winit::platform_specific::commands::corner_radius::corner_radius;
818
819 let t = cosmic_theme.cosmic();
820
821 let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
822 let cur_rad = CornerRadius {
823 top_left: radii[0].round() as u32,
824 top_right: radii[1].round() as u32,
825 bottom_right: radii[2].round() as u32,
826 bottom_left: radii[3].round() as u32,
827 };
828
829 let rounded = !self.app.core().window.sharp_corners;
830 let main_window_id = self
832 .app
833 .core()
834 .main_window_id()
835 .unwrap_or(window::Id::RESERVED);
836 let mut cmds = vec![
837 corner_radius(
838 main_window_id,
839 if rounded {
840 Some(cur_rad)
841 } else {
842 let rad_0 = t.radius_0();
843 Some(CornerRadius {
844 top_left: rad_0[0].round() as u32,
845 top_right: rad_0[1].round() as u32,
846 bottom_right: rad_0[2].round() as u32,
847 bottom_left: rad_0[3].round() as u32,
848 })
849 },
850 )
851 .discard(),
852 ];
853 for (id, (_, surface_type, _)) in self.surface_views.iter() {
855 if let SurfaceIdWrapper::Window(_) = surface_type {
856 cmds.push(
857 corner_radius(
858 *id,
859 if rounded {
860 Some(cur_rad)
861 } else {
862 let rad_0 = t.radius_0();
863 Some(CornerRadius {
864 top_left: rad_0[0].round() as u32,
865 top_right: rad_0[1].round() as u32,
866 bottom_right: rad_0[2].round() as u32,
867 bottom_left: rad_0[3].round() as u32,
868 })
869 },
870 )
871 .discard(),
872 );
873 }
874 }
875 for id in self.tracked_windows.iter() {
877 cmds.push(
878 corner_radius(
879 *id,
880 if rounded {
881 Some(cur_rad)
882 } else {
883 let rad_0 = t.radius_0();
884 Some(CornerRadius {
885 top_left: rad_0[0].round() as u32,
886 top_right: rad_0[1].round() as u32,
887 bottom_right: rad_0[2].round() as u32,
888 bottom_left: rad_0[3].round() as u32,
889 })
890 },
891 )
892 .discard(),
893 );
894 }
895
896 return Task::batch(cmds);
897 }
898 }
899 }
900
901 return cmd;
902 }
903
904 Action::ScaleFactor(factor) => {
905 self.app.core_mut().set_scale_factor(factor);
906 }
907
908 Action::Close => {
909 return match self.app.on_app_exit() {
910 Some(message) => self.app.update(message),
911 None => self.close(),
912 };
913 }
914 Action::SystemThemeModeChange(keys, mode) => {
915 if match THEME.lock().unwrap().theme_type {
916 ThemeType::System {
917 theme: _,
918 prefer_dark,
919 } => prefer_dark.is_some(),
920 _ => false,
921 } {
922 return iced::Task::none();
923 }
924 let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)];
925
926 let core = self.app.core_mut();
927 core.system_theme_mode = mode;
928 let is_dark = core.system_is_dark();
929 let changed = core.system_theme_mode.is_dark != is_dark
930 || core.portal_is_dark != Some(is_dark)
931 || core.system_theme.cosmic().is_dark != is_dark;
932 if changed {
933 core.theme_sub_counter += 1;
934 let mut new_theme = if is_dark {
935 crate::theme::system_dark()
936 } else {
937 crate::theme::system_light()
938 };
939 cmds.push(self.app.system_theme_update(&[], new_theme.cosmic()));
940
941 let core = self.app.core_mut();
942 new_theme = if let Some(a) = core.portal_accent {
943 let t_inner = new_theme.cosmic();
944 if a.distance_squared(*t_inner.accent_color()) > 0.00001 {
945 Theme::system(Arc::new(t_inner.with_accent(a)))
946 } else {
947 new_theme
948 }
949 } else {
950 new_theme
951 };
952
953 core.system_theme = new_theme.clone();
954 {
955 let mut cosmic_theme = THEME.lock().unwrap();
956
957 if let ThemeType::System { .. } = cosmic_theme.theme_type {
959 cosmic_theme.set_theme(new_theme.theme_type);
960 #[cfg(all(feature = "wayland", target_os = "linux"))]
961 if self.app.core().sync_window_border_radii_to_theme() {
962 use iced_runtime::platform_specific::wayland::CornerRadius;
963 use iced_winit::platform_specific::commands::corner_radius::corner_radius;
964
965 let t = cosmic_theme.cosmic();
966
967 let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
968 let cur_rad = CornerRadius {
969 top_left: radii[0].round() as u32,
970 top_right: radii[1].round() as u32,
971 bottom_right: radii[2].round() as u32,
972 bottom_left: radii[3].round() as u32,
973 };
974 let rounded = !self.app.core().window.sharp_corners;
975
976 let main_window_id = self
978 .app
979 .core()
980 .main_window_id()
981 .unwrap_or(window::Id::RESERVED);
982 let mut cmds = vec![
983 corner_radius(
984 main_window_id,
985 if rounded {
986 Some(cur_rad)
987 } else {
988 let rad_0 = t.radius_0();
989 Some(CornerRadius {
990 top_left: rad_0[0].round() as u32,
991 top_right: rad_0[1].round() as u32,
992 bottom_right: rad_0[2].round() as u32,
993 bottom_left: rad_0[3].round() as u32,
994 })
995 },
996 )
997 .discard(),
998 ];
999 for (id, (_, surface_type, _)) in self.surface_views.iter() {
1001 if let SurfaceIdWrapper::Window(_) = surface_type {
1002 cmds.push(
1003 corner_radius(
1004 *id,
1005 if rounded {
1006 Some(cur_rad)
1007 } else {
1008 let rad_0 = t.radius_0();
1009 Some(CornerRadius {
1010 top_left: rad_0[0].round() as u32,
1011 top_right: rad_0[1].round() as u32,
1012 bottom_right: rad_0[2].round() as u32,
1013 bottom_left: rad_0[3].round() as u32,
1014 })
1015 },
1016 )
1017 .discard(),
1018 );
1019 }
1020 }
1021 for id in self.tracked_windows.iter() {
1023 cmds.push(
1024 corner_radius(
1025 *id,
1026 if rounded {
1027 Some(cur_rad)
1028 } else {
1029 let rad_0 = t.radius_0();
1030 Some(CornerRadius {
1031 top_left: rad_0[0].round() as u32,
1032 top_right: rad_0[1].round() as u32,
1033 bottom_right: rad_0[2].round() as u32,
1034 bottom_left: rad_0[3].round() as u32,
1035 })
1036 },
1037 )
1038 .discard(),
1039 );
1040 }
1041
1042 return Task::batch(cmds);
1043 }
1044 }
1045 }
1046 }
1047 return Task::batch(cmds);
1048 }
1049 Action::Activate(_token) => {
1050 if let Some(id) = self.app.core().main_window_id() {
1051 let mut task = iced_runtime::window::minimize(id, false);
1053
1054 #[cfg(all(feature = "wayland", target_os = "linux"))]
1055 {
1056 task = task.chain(
1057 iced_winit::platform_specific::commands::activation::activate(
1058 id,
1059 #[allow(clippy::used_underscore_binding)]
1060 _token,
1061 ),
1062 )
1063 }
1064
1065 #[cfg(not(all(feature = "wayland", target_os = "linux")))]
1066 {
1067 task = task.chain(iced_runtime::window::gain_focus(id));
1068 }
1069
1070 return task;
1071 }
1072 }
1073
1074 Action::Surface(action) => return self.surface_update(action),
1075
1076 Action::SurfaceClosed(id) => {
1077 if self.opened_surfaces.get_mut(&id).is_some_and(|v| {
1078 *v = v.saturating_sub(1);
1079 *v == 0
1080 }) {
1081 self.opened_surfaces.remove(&id);
1082 #[cfg(all(feature = "wayland", target_os = "linux"))]
1083 self.surface_views.remove(&id);
1084 self.tracked_windows.remove(&id);
1085 }
1086
1087 let mut ret = if let Some(msg) = self.app.on_close_requested(id) {
1088 self.app.update(msg)
1089 } else {
1090 Task::none()
1091 };
1092 let core = self.app.core();
1093 if core.exit_on_main_window_closed
1094 && core.main_window_id().is_some_and(|m_id| id == m_id)
1095 {
1096 ret = Task::batch([iced::exit::<crate::Action<T::Message>>()]);
1097 }
1098 return ret;
1099 }
1100
1101 Action::ShowWindowMenu => {
1102 if let Some(id) = self.app.core().main_window_id() {
1103 return iced::window::show_system_menu(id);
1104 }
1105 }
1106
1107 #[cfg(feature = "single-instance")]
1108 Action::DbusConnection(conn) => {
1109 return self.app.dbus_connection(conn);
1110 }
1111
1112 #[cfg(feature = "xdg-portal")]
1113 Action::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => {
1114 use ashpd::desktop::settings::ColorScheme;
1115 if match THEME.lock().unwrap().theme_type {
1116 ThemeType::System {
1117 theme: _,
1118 prefer_dark,
1119 } => prefer_dark.is_some(),
1120 _ => false,
1121 } {
1122 return iced::Task::none();
1123 }
1124 let is_dark = match s {
1125 ColorScheme::NoPreference => None,
1126 ColorScheme::PreferDark => Some(true),
1127 ColorScheme::PreferLight => Some(false),
1128 };
1129 let core = self.app.core_mut();
1130
1131 core.portal_is_dark = is_dark;
1132 let is_dark = core.system_is_dark();
1133 let changed = core.system_theme_mode.is_dark != is_dark
1134 || core.portal_is_dark != Some(is_dark)
1135 || core.system_theme.cosmic().is_dark != is_dark;
1136
1137 if changed {
1138 core.theme_sub_counter += 1;
1139 let new_theme = if is_dark {
1140 crate::theme::system_dark()
1141 } else {
1142 crate::theme::system_light()
1143 };
1144 core.system_theme = new_theme.clone();
1145 {
1146 let mut cosmic_theme = THEME.lock().unwrap();
1147
1148 if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type {
1150 cosmic_theme.set_theme(new_theme.theme_type);
1151 }
1152 }
1153 }
1154 }
1155 #[cfg(feature = "xdg-portal")]
1156 Action::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => {
1157 use palette::Srgba;
1158 let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0);
1159 let core = self.app.core_mut();
1160 core.portal_accent = Some(c);
1161 let cur_accent = core.system_theme.cosmic().accent_color();
1162
1163 if cur_accent.distance_squared(*c) < 0.00001 {
1164 return iced::Task::none();
1166 }
1167
1168 {
1169 let mut cosmic_theme = THEME.lock().unwrap();
1170
1171 if let ThemeType::System {
1173 theme: t,
1174 prefer_dark,
1175 } = cosmic_theme.theme_type.clone()
1176 {
1177 cosmic_theme.set_theme(ThemeType::System {
1178 theme: Arc::new(t.with_accent(c)),
1179 prefer_dark,
1180 });
1181 }
1182 }
1183 }
1184 #[cfg(feature = "xdg-portal")]
1185 Action::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => {
1186 }
1188
1189 Action::ToolkitConfig(config) => {
1190 if !self.app.core().icon_theme_override
1192 && crate::icon_theme::default() != config.icon_theme
1193 {
1194 crate::icon_theme::set_default(config.icon_theme.clone());
1195 }
1196
1197 *crate::config::COSMIC_TK.write().unwrap() = config;
1198 }
1199
1200 Action::Focus(f) => {
1201 #[cfg(all(
1202 feature = "wayland",
1203 feature = "multi-window",
1204 feature = "surface-message",
1205 target_os = "linux"
1206 ))]
1207 if let Some((
1208 parent,
1209 SurfaceIdWrapper::Subsurface(_) | SurfaceIdWrapper::Popup(_),
1210 _,
1211 )) = self.surface_views.get(&f)
1212 {
1213 if parent.is_some_and(|p| self.app.core().focused_window.last() == Some(&p)) {
1216 self.app.core_mut().focused_window.push(f);
1217 return iced::Task::none();
1218 } else {
1219 let mut parent_chain = vec![f];
1221 let mut cur = *parent;
1222 while let Some(p) = cur {
1223 parent_chain.push(p);
1224 cur = self
1225 .surface_views
1226 .get(&p)
1227 .and_then(|(parent, _, _)| *parent);
1228 }
1229 parent_chain.reverse();
1230 self.app.core_mut().focused_window = parent_chain;
1231 return iced::Task::none();
1232 }
1233 }
1234 self.app.core_mut().focused_window = vec![f];
1235 }
1236
1237 Action::Unfocus(id) => {
1238 let core = self.app.core_mut();
1239 if core.focused_window().as_ref().is_some_and(|cur| *cur == id) {
1240 core.focused_window.pop();
1241 }
1242 }
1243 #[cfg(feature = "applet")]
1244 Action::SuggestedBounds(b) => {
1245 tracing::info!("Suggested bounds: {b:?}");
1246 let core = self.app.core_mut();
1247 core.applet.suggested_bounds = b;
1248 }
1249 Action::Opened(id) => {
1250 #[cfg(all(feature = "wayland", target_os = "linux"))]
1251 if self.app.core().sync_window_border_radii_to_theme() {
1252 use iced_runtime::platform_specific::wayland::CornerRadius;
1253 use iced_winit::platform_specific::commands::corner_radius::corner_radius;
1254
1255 let theme = THEME.lock().unwrap();
1256 let t = theme.cosmic();
1257 let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 });
1258 let cur_rad = CornerRadius {
1259 top_left: radii[0].round() as u32,
1260 top_right: radii[1].round() as u32,
1261 bottom_right: radii[2].round() as u32,
1262 bottom_left: radii[3].round() as u32,
1263 };
1264 let rounded = !self.app.core().window.sharp_corners;
1266
1267 return Task::batch([
1268 corner_radius(
1269 id,
1270 if rounded {
1271 Some(cur_rad)
1272 } else {
1273 let rad_0 = t.radius_0();
1274 Some(CornerRadius {
1275 top_left: rad_0[0].round() as u32,
1276 top_right: rad_0[1].round() as u32,
1277 bottom_right: rad_0[2].round() as u32,
1278 bottom_left: rad_0[3].round() as u32,
1279 })
1280 },
1281 )
1282 .discard(),
1283 iced_runtime::window::run_with_handle(id, init_windowing_system),
1284 ]);
1285 }
1286 return iced_runtime::window::run_with_handle(id, init_windowing_system);
1287 }
1288 _ => {}
1289 }
1290
1291 iced::Task::none()
1292 }
1293}
1294
1295impl<App: Application> Cosmic<App> {
1296 pub fn new(app: App) -> Self {
1297 Self {
1298 app,
1299 #[cfg(all(feature = "wayland", target_os = "linux"))]
1300 surface_views: HashMap::new(),
1301 tracked_windows: HashSet::new(),
1302 opened_surfaces: HashMap::new(),
1303 }
1304 }
1305
1306 #[cfg(all(feature = "wayland", target_os = "linux"))]
1307 pub fn get_subsurface(
1309 &mut self,
1310 settings: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings,
1311 view: Box<
1312 dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action<App::Message>> + Send + Sync,
1313 >,
1314 ) -> Task<crate::Action<App::Message>> {
1315 use iced_winit::commands::subsurface::get_subsurface;
1316
1317 *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1;
1318 self.surface_views.insert(
1319 settings.id,
1320 (
1321 Some(settings.parent),
1322 SurfaceIdWrapper::Subsurface(settings.id),
1323 view,
1324 ),
1325 );
1326 get_subsurface(settings)
1327 }
1328
1329 #[cfg(all(feature = "wayland", target_os = "linux"))]
1330 pub fn get_popup(
1332 &mut self,
1333 settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings,
1334 view: Box<
1335 dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action<App::Message>> + Send + Sync,
1336 >,
1337 ) -> Task<crate::Action<App::Message>> {
1338 use iced_winit::commands::popup::get_popup;
1339 *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1;
1340 self.surface_views.insert(
1341 settings.id,
1342 (
1343 Some(settings.parent),
1344 SurfaceIdWrapper::Popup(settings.id),
1345 view,
1346 ),
1347 );
1348 get_popup(settings)
1349 }
1350
1351 #[cfg(all(feature = "wayland", target_os = "linux"))]
1352 pub fn get_window(
1354 &mut self,
1355 id: iced::window::Id,
1356 settings: iced::window::Settings,
1357 view: Box<
1358 dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action<App::Message>> + Send + Sync,
1359 >,
1360 ) -> Task<crate::Action<App::Message>> {
1361 use iced_winit::SurfaceIdWrapper;
1362 *self.opened_surfaces.entry(id).or_insert(0) += 1;
1363 self.surface_views.insert(
1364 id,
1365 (
1366 None, SurfaceIdWrapper::Window(id),
1368 view,
1369 ),
1370 );
1371 iced_runtime::task::oneshot(|channel| {
1372 iced_runtime::Action::Window(iced_runtime::window::Action::Open(id, settings, channel))
1373 })
1374 .discard()
1375 }
1376}