1mod action;
10pub use action::Action;
11use cosmic_config::CosmicConfigEntry;
12pub mod context_drawer;
13pub use context_drawer::{ContextDrawer, context_drawer};
14pub mod cosmic;
15#[cfg(all(feature = "winit", feature = "multi-window"))]
16pub(crate) mod multi_window;
17pub mod settings;
18
19pub type Task<M> = iced::Task<crate::Action<M>>;
20
21pub use crate::Core;
22use crate::prelude::*;
23use crate::theme::THEME;
24use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover};
25use apply::Apply;
26use iced::window;
27use iced::{Length, Subscription};
28pub use settings::Settings;
29use std::borrow::Cow;
30
31#[cold]
32pub(crate) fn iced_settings<App: Application>(
33 settings: Settings,
34 flags: App::Flags,
35) -> (iced::Settings, (Core, App::Flags), iced::window::Settings) {
36 preload_fonts();
37
38 let mut core = Core::default();
39 core.debug = settings.debug;
40 core.icon_theme_override = settings.default_icon_theme.is_some();
41 core.set_scale_factor(settings.scale_factor);
42 core.set_window_width(settings.size.width);
43 core.set_window_height(settings.size.height);
44
45 if let Some(icon_theme) = settings.default_icon_theme {
46 crate::icon_theme::set_default(icon_theme);
47 } else {
48 crate::icon_theme::set_default(crate::config::icon_theme());
49 }
50
51 THEME.lock().unwrap().set_theme(settings.theme.theme_type);
52
53 if settings.no_main_window {
54 core.main_window = Some(iced::window::Id::NONE);
55 }
56
57 let mut iced = iced::Settings::default();
58
59 iced.antialiasing = settings.antialiasing;
60 iced.default_font = settings.default_font;
61 iced.default_text_size = iced::Pixels(settings.default_text_size);
62 let exit_on_close = settings.exit_on_close;
63 iced.is_daemon = false;
64 iced.exit_on_close_request = settings.is_daemon;
65 let mut window_settings = iced::window::Settings::default();
66 window_settings.exit_on_close_request = exit_on_close;
67 iced.id = Some(App::APP_ID.to_owned());
68 #[cfg(target_os = "linux")]
69 {
70 window_settings.platform_specific.application_id = App::APP_ID.to_string();
71 }
72 core.exit_on_main_window_closed = exit_on_close;
73
74 if let Some(border_size) = settings.resizable {
75 window_settings.resize_border = border_size as u32;
76 window_settings.resizable = true;
77 }
78 window_settings.decorations = !settings.client_decorations;
79 window_settings.size = settings.size;
80 let min_size = settings.size_limits.min();
81 if min_size != iced::Size::ZERO {
82 window_settings.min_size = Some(min_size);
83 }
84 let max_size = settings.size_limits.max();
85 if max_size != iced::Size::INFINITY {
86 window_settings.max_size = Some(max_size);
87 }
88
89 window_settings.transparent = settings.transparent;
90 (iced, (core, flags), window_settings)
91}
92
93pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result {
99 #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
100 if let Some(threshold) = settings.default_mmap_threshold {
101 crate::malloc::limit_mmap_threshold(threshold);
102 }
103
104 let default_font = settings.default_font;
105 let (settings, mut flags, window_settings) = iced_settings::<App>(settings, flags);
106 #[cfg(not(feature = "multi-window"))]
107 {
108 flags.0.main_window = Some(iced::window::Id::RESERVED);
109 iced::application(
110 cosmic::Cosmic::title,
111 cosmic::Cosmic::update,
112 cosmic::Cosmic::view,
113 )
114 .subscription(cosmic::Cosmic::subscription)
115 .style(cosmic::Cosmic::style)
116 .theme(cosmic::Cosmic::theme)
117 .window_size((500.0, 800.0))
118 .settings(settings)
119 .window(window_settings)
120 .run_with(move || cosmic::Cosmic::<App>::init(flags))
121 }
122 #[cfg(feature = "multi-window")]
123 {
124 let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>(
125 cosmic::Cosmic::title,
126 cosmic::Cosmic::update,
127 cosmic::Cosmic::view,
128 );
129 if flags.0.main_window.is_none() {
130 app = app.window(window_settings);
131 flags.0.main_window = Some(iced_core::window::Id::RESERVED);
132 }
133 app.subscription(cosmic::Cosmic::subscription)
134 .style(cosmic::Cosmic::style)
135 .theme(cosmic::Cosmic::theme)
136 .settings(settings)
137 .run_with(move || cosmic::Cosmic::<App>::init(flags))
138 }
139}
140
141#[cfg(feature = "single-instance")]
142pub fn run_single_instance<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result
148where
149 App::Flags: CosmicFlags,
150 App::Message: Clone + std::fmt::Debug + Send + 'static,
151{
152 use std::collections::HashMap;
153
154 let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();
155
156 let override_single = std::env::var("COSMIC_SINGLE_INSTANCE")
157 .map(|v| &v.to_lowercase() == "false" || &v == "0")
158 .unwrap_or_default();
159 if override_single {
160 return run::<App>(settings, flags);
161 }
162
163 let path: String = format!("/{}", App::APP_ID.replace('.', "/"));
164
165 let Ok(conn) = zbus::blocking::Connection::session() else {
166 tracing::warn!("Failed to connect to dbus");
167 return run::<App>(settings, flags);
168 };
169
170 if crate::dbus_activation::DbusActivationInterfaceProxyBlocking::builder(&conn)
171 .destination(App::APP_ID)
172 .ok()
173 .and_then(|b| b.path(path).ok())
174 .and_then(|b| b.destination(App::APP_ID).ok())
175 .and_then(|b| b.build().ok())
176 .is_some_and(|mut p| {
177 let res = {
178 let mut platform_data = HashMap::new();
179 if let Some(activation_token) = activation_token {
180 platform_data.insert("activation-token", activation_token.into());
181 }
182 if let Ok(startup_id) = std::env::var("DESKTOP_STARTUP_ID") {
183 platform_data.insert("desktop-startup-id", startup_id.into());
184 }
185 if let Some(action) = flags.action() {
186 let action = action.to_string();
187 p.activate_action(&action, flags.args(), platform_data)
188 } else {
189 p.activate(platform_data)
190 }
191 };
192 match res {
193 Ok(()) => {
194 tracing::info!("Successfully activated another instance");
195 true
196 }
197 Err(err) => {
198 tracing::warn!(?err, "Failed to activate another instance");
199 false
200 }
201 }
202 })
203 {
204 tracing::info!("Another instance is running");
205 Ok(())
206 } else {
207 let (settings, mut flags, window_settings) = iced_settings::<App>(settings, flags);
208 flags.0.single_instance = true;
209
210 #[cfg(not(feature = "multi-window"))]
211 {
212 iced::application(
213 cosmic::Cosmic::title,
214 cosmic::Cosmic::update,
215 cosmic::Cosmic::view,
216 )
217 .subscription(cosmic::Cosmic::subscription)
218 .style(cosmic::Cosmic::style)
219 .theme(cosmic::Cosmic::theme)
220 .window_size((500.0, 800.0))
221 .settings(settings)
222 .window(window_settings)
223 .run_with(move || cosmic::Cosmic::<App>::init(flags))
224 }
225 #[cfg(feature = "multi-window")]
226 {
227 let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>(
228 cosmic::Cosmic::title,
229 cosmic::Cosmic::update,
230 cosmic::Cosmic::view,
231 );
232 if flags.0.main_window.is_none() {
233 app = app.window(window_settings);
234 flags.0.main_window = Some(iced_core::window::Id::RESERVED);
235 }
236 app.subscription(cosmic::Cosmic::subscription)
237 .style(cosmic::Cosmic::style)
238 .theme(cosmic::Cosmic::theme)
239 .settings(settings)
240 .run_with(move || cosmic::Cosmic::<App>::init(flags))
241 }
242 }
243}
244
245pub trait CosmicFlags {
246 type SubCommand: ToString + std::fmt::Debug + Clone + Send + 'static;
247 type Args: Into<Vec<String>> + std::fmt::Debug + Clone + Send + 'static;
248 #[must_use]
249 fn action(&self) -> Option<&Self::SubCommand> {
250 None
251 }
252
253 #[must_use]
254 fn args(&self) -> Vec<&str> {
255 Vec::new()
256 }
257}
258
259#[allow(unused_variables)]
261pub trait Application
262where
263 Self: Sized + 'static,
264{
265 type Executor: iced_futures::Executor;
267
268 type Flags;
270
271 type Message: Clone + std::fmt::Debug + Send + 'static;
273
274 const APP_ID: &'static str;
278
279 fn core(&self) -> &Core;
281
282 fn core_mut(&mut self) -> &mut Core;
284
285 fn init(core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>);
287
288 fn context_drawer(&self) -> Option<ContextDrawer<Self::Message>> {
291 None
292 }
293
294 fn dialog(&self) -> Option<Element<Self::Message>> {
296 None
297 }
298
299 fn footer(&self) -> Option<Element<Self::Message>> {
301 None
302 }
303
304 fn header_start(&self) -> Vec<Element<Self::Message>> {
306 Vec::new()
307 }
308
309 fn header_center(&self) -> Vec<Element<Self::Message>> {
311 Vec::new()
312 }
313
314 fn header_end(&self) -> Vec<Element<Self::Message>> {
316 Vec::new()
317 }
318
319 fn nav_bar(&self) -> Option<Element<crate::Action<Self::Message>>> {
321 if !self.core().nav_bar_active() {
322 return None;
323 }
324
325 let nav_model = self.nav_model()?;
326
327 let mut nav =
328 crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id)))
329 .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id)))
330 .context_menu(self.nav_context_menu(self.core().nav_bar_context()))
331 .into_container()
332 .width(iced::Length::Shrink)
334 .height(iced::Length::Shrink);
335
336 if !self.core().is_condensed() {
337 nav = nav.max_width(280);
338 }
339
340 Some(Element::from(nav))
341 }
342
343 fn nav_context_menu(
345 &self,
346 id: nav_bar::Id,
347 ) -> Option<Vec<menu::Tree<crate::Action<Self::Message>>>> {
348 None
349 }
350
351 fn nav_model(&self) -> Option<&nav_bar::Model> {
353 None
354 }
355
356 fn on_app_exit(&mut self) -> Option<Self::Message> {
358 None
359 }
360
361 fn on_close_requested(&self, id: window::Id) -> Option<Self::Message> {
363 None
364 }
365
366 fn on_context_drawer(&mut self) -> Task<Self::Message> {
368 Task::none()
369 }
370
371 fn on_escape(&mut self) -> Task<Self::Message> {
373 Task::none()
374 }
375
376 fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
378 Task::none()
379 }
380
381 fn on_nav_context(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
383 Task::none()
384 }
385
386 fn on_search(&mut self) -> Task<Self::Message> {
388 Task::none()
389 }
390
391 fn on_window_resize(&mut self, id: window::Id, width: f32, height: f32) {}
393
394 fn subscription(&self) -> Subscription<Self::Message> {
396 Subscription::none()
397 }
398
399 fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
401 Task::none()
402 }
403
404 fn system_theme_update(
406 &mut self,
407 keys: &[&'static str],
408 new_theme: &cosmic_theme::Theme,
409 ) -> Task<Self::Message> {
410 Task::none()
411 }
412
413 fn system_theme_mode_update(
415 &mut self,
416 keys: &[&'static str],
417 new_theme: &cosmic_theme::ThemeMode,
418 ) -> Task<Self::Message> {
419 Task::none()
420 }
421
422 fn view(&self) -> Element<Self::Message>;
424
425 fn view_window(&self, id: window::Id) -> Element<Self::Message> {
427 panic!("no view for window {id:?}");
428 }
429
430 fn style(&self) -> Option<iced_runtime::Appearance> {
432 None
433 }
434
435 #[cfg(feature = "single-instance")]
437 fn dbus_activation(&mut self, msg: crate::dbus_activation::Message) -> Task<Self::Message> {
438 Task::none()
439 }
440}
441
442pub trait ApplicationExt: Application {
444 fn drag(&mut self) -> Task<Self::Message>;
446
447 fn maximize(&mut self) -> Task<Self::Message>;
449
450 fn minimize(&mut self) -> Task<Self::Message>;
452 #[cfg(not(feature = "multi-window"))]
455 fn title(&self) -> &str;
456
457 #[cfg(feature = "multi-window")]
458 fn title(&self, id: window::Id) -> &str;
460
461 fn set_show_context(&mut self, show: bool) {
463 self.core_mut().set_show_context(show);
464 }
465
466 fn set_header_title(&mut self, title: String) {
468 self.core_mut().set_header_title(title);
469 }
470
471 #[cfg(not(feature = "multi-window"))]
472 fn set_window_title(&mut self, title: String) -> Task<Self::Message>;
474
475 #[cfg(feature = "multi-window")]
476 fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message>;
478
479 fn view_main(&self) -> Element<crate::Action<Self::Message>>;
481
482 fn watch_config<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq>(
483 &self,
484 id: &'static str,
485 ) -> iced::Subscription<cosmic_config::Update<T>> {
486 self.core().watch_config(id)
487 }
488
489 fn watch_state<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq>(
490 &self,
491 id: &'static str,
492 ) -> iced::Subscription<cosmic_config::Update<T>> {
493 self.core().watch_state(id)
494 }
495}
496
497impl<App: Application> ApplicationExt for App {
498 fn drag(&mut self) -> Task<Self::Message> {
499 self.core().drag(None)
500 }
501
502 fn maximize(&mut self) -> Task<Self::Message> {
503 self.core().maximize(None, true)
504 }
505
506 fn minimize(&mut self) -> Task<Self::Message> {
507 self.core().minimize(None)
508 }
509
510 #[cfg(feature = "multi-window")]
511 fn title(&self, id: window::Id) -> &str {
512 self.core().title.get(&id).map_or("", |s| s.as_str())
513 }
514
515 #[cfg(not(feature = "multi-window"))]
516 fn title(&self) -> &str {
517 self.core()
518 .main_window_id()
519 .and_then(|id| self.core().title.get(&id).map(std::string::String::as_str))
520 .unwrap_or("")
521 }
522
523 #[cfg(feature = "multi-window")]
524 fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message> {
525 self.core_mut().title.insert(id, title.clone());
526 self.core().set_title(Some(id), title)
527 }
528
529 #[cfg(not(feature = "multi-window"))]
530 fn set_window_title(&mut self, title: String) -> Task<Self::Message> {
531 let Some(id) = self.core().main_window_id() else {
532 return Task::none();
533 };
534
535 self.core_mut().title.insert(id, title.clone());
536 Task::none()
537 }
538
539 #[allow(clippy::too_many_lines)]
540 fn view_main(&self) -> Element<crate::Action<Self::Message>> {
542 let core = self.core();
543 let is_condensed = core.is_condensed();
544 let sharp_corners = core.window.sharp_corners;
547 let content_container = core.window.content_container;
548 let show_context = core.window.show_context;
549 let nav_bar_active = core.nav_bar_active();
550 let focused = core
551 .focused_window()
552 .is_some_and(|i| Some(i) == self.core().main_window_id());
553
554 let border_padding = if sharp_corners { 8 } else { 7 };
555
556 let main_content_padding = if !content_container {
557 [0, 0, 0, 0]
558 } else {
559 let right_padding = if show_context { 0 } else { border_padding };
560 let left_padding = if nav_bar_active { 0 } else { border_padding };
561
562 [0, right_padding, 0, left_padding]
563 };
564
565 let content_row = crate::widget::row::with_children({
566 let mut widgets = Vec::with_capacity(3);
567
568 let has_nav = if let Some(nav) = self
570 .nav_bar()
571 .map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar")))
572 {
573 widgets.push(
574 container(nav)
575 .padding([
576 0,
577 if is_condensed { border_padding } else { 8 },
578 border_padding,
579 border_padding,
580 ])
581 .into(),
582 );
583 true
584 } else {
585 false
586 };
587
588 if self.nav_model().is_none() || core.show_content() {
589 let main_content = self.view();
590
591 let context_width = core.context_width(has_nav);
593 if core.window.context_is_overlay && show_context {
594 if let Some(context) = self.context_drawer() {
595 widgets.push(
596 crate::widget::context_drawer(
597 context.title,
598 context.header_actions,
599 context.header,
600 context.footer,
601 context.on_close,
602 main_content,
603 context.content,
604 context_width,
605 )
606 .apply(|drawer| {
607 Element::from(id_container(
608 drawer,
609 iced_core::id::Id::new("COSMIC_context_drawer"),
610 ))
611 })
612 .apply(container)
613 .padding([0, if content_container { border_padding } else { 0 }, 0, 0])
614 .apply(Element::from)
615 .map(crate::Action::App),
616 );
617 } else {
618 widgets.push(
619 container(main_content.map(crate::Action::App))
620 .padding(main_content_padding)
621 .into(),
622 );
623 }
624 } else {
625 widgets.push(
627 container(main_content.map(crate::Action::App))
628 .padding(main_content_padding)
629 .into(),
630 );
631 if let Some(context) = self.context_drawer() {
632 widgets.push(
633 crate::widget::ContextDrawer::new_inner(
634 context.title,
635 context.header_actions,
636 context.header,
637 context.footer,
638 context.content,
639 context.on_close,
640 context_width,
641 )
642 .apply(Element::from)
643 .map(crate::Action::App)
644 .apply(container)
645 .width(context_width)
646 .apply(|drawer| {
647 Element::from(id_container(
648 drawer,
649 iced_core::id::Id::new("COSMIC_context_drawer"),
650 ))
651 })
652 .apply(container)
653 .padding(if content_container {
654 [0, border_padding, border_padding, border_padding]
655 } else {
656 [0, 0, 0, 0]
657 })
658 .into(),
659 )
660 } else {
661 widgets.push(horizontal_space().width(Length::Shrink).into());
663 }
664 }
665 }
666
667 widgets
668 });
669 let content_col = crate::widget::column::with_capacity(2)
670 .push(content_row)
671 .push_maybe(self.footer().map(|footer| {
672 container(footer.map(crate::Action::App)).padding([
673 0,
674 border_padding,
675 border_padding,
676 border_padding,
677 ])
678 }));
679 let content: Element<_> = if content_container {
680 content_col
681 .apply(container)
682 .width(iced::Length::Fill)
683 .height(iced::Length::Fill)
684 .class(crate::theme::Container::WindowBackground)
685 .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
686 .into()
687 } else {
688 content_col.into()
689 };
690
691 let window_corner_radius = crate::theme::active()
693 .cosmic()
694 .radius_s()
695 .map(|x| if x < 4.0 { x } else { x + 4.0 });
696
697 let view_column = crate::widget::column::with_capacity(2)
698 .push_maybe(if core.window.show_headerbar {
699 Some({
700 let mut header = crate::widget::header_bar()
701 .focused(focused)
702 .maximized(sharp_corners)
703 .title(&core.window.header_title)
704 .on_drag(crate::Action::Cosmic(Action::Drag))
705 .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu))
706 .on_double_click(crate::Action::Cosmic(Action::Maximize));
707
708 if self.nav_model().is_some() {
709 let toggle = crate::widget::nav_bar_toggle()
710 .active(core.nav_bar_active())
711 .selected(focused)
712 .on_toggle(if is_condensed {
713 crate::Action::Cosmic(Action::ToggleNavBarCondensed)
714 } else {
715 crate::Action::Cosmic(Action::ToggleNavBar)
716 });
717
718 header = header.start(toggle);
719 }
720
721 if core.window.show_close {
722 header = header.on_close(crate::Action::Cosmic(Action::Close));
723 }
724
725 if core.window.show_maximize && crate::config::show_maximize() {
726 header = header.on_maximize(crate::Action::Cosmic(Action::Maximize));
727 }
728
729 if core.window.show_minimize && crate::config::show_minimize() {
730 header = header.on_minimize(crate::Action::Cosmic(Action::Minimize));
731 }
732
733 for element in self.header_start() {
734 header = header.start(element.map(crate::Action::App));
735 }
736
737 for element in self.header_center() {
738 header = header.center(element.map(crate::Action::App));
739 }
740
741 for element in self.header_end() {
742 header = header.end(element.map(crate::Action::App));
743 }
744
745 if content_container {
746 header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header")))
747 } else {
748 header
750 .apply(container)
751 .class(crate::theme::Container::custom(move |theme| {
752 container::Style {
753 background: Some(iced::Background::Color(
754 theme.cosmic().background.base.into(),
755 )),
756 border: iced::Border {
757 radius: [
758 window_corner_radius[0] - 1.0,
759 window_corner_radius[1] - 1.0,
760 theme.cosmic().radius_0()[2],
761 theme.cosmic().radius_0()[3],
762 ]
763 .into(),
764 ..Default::default()
765 },
766 ..Default::default()
767 }
768 }))
769 .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header")))
770 }
771 })
772 } else {
773 None
774 })
775 .push(content)
777 .apply(container)
778 .padding(if sharp_corners { 0 } else { 1 })
779 .class(crate::theme::Container::custom(move |theme| {
780 container::Style {
781 background: if content_container {
782 Some(iced::Background::Color(
783 theme.cosmic().background.base.into(),
784 ))
785 } else {
786 None
787 },
788 border: iced::Border {
789 color: theme.cosmic().bg_divider().into(),
790 width: if sharp_corners { 0.0 } else { 1.0 },
791 radius: window_corner_radius.into(),
792 },
793 ..Default::default()
794 }
795 }));
796
797 let mut popover = popover(view_column).modal(true);
800 if let Some(dialog) = self
801 .dialog()
802 .map(|w| Element::from(id_container(w, iced_core::id::Id::new("COSMIC_dialog"))))
803 {
804 popover = popover.popup(dialog.map(crate::Action::App));
805 }
806
807 let view_element: Element<_> = popover.into();
808 view_element.debug(core.debug)
809 }
810}
811
812const EMBEDDED_FONTS: &[&[u8]] = &[
813 include_bytes!("../../res/open-sans/OpenSans-Light.ttf"),
814 include_bytes!("../../res/open-sans/OpenSans-Regular.ttf"),
815 include_bytes!("../../res/open-sans/OpenSans-Semibold.ttf"),
816 include_bytes!("../../res/open-sans/OpenSans-Bold.ttf"),
817 include_bytes!("../../res/open-sans/OpenSans-ExtraBold.ttf"),
818 include_bytes!("../../res/noto/NotoSansMono-Regular.ttf"),
819 include_bytes!("../../res/noto/NotoSansMono-Bold.ttf"),
820];
821
822#[cold]
823fn preload_fonts() {
824 let mut font_system = iced::advanced::graphics::text::font_system()
825 .write()
826 .unwrap();
827
828 EMBEDDED_FONTS
829 .iter()
830 .for_each(move |font| font_system.load_font(Cow::Borrowed(font)));
831}