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 #[cfg(feature = "single-instance")]
445 fn dbus_connection(&mut self, conn: zbus::Connection) -> Task<Self::Message> {
446 Task::none()
447 }
448}
449
450pub trait ApplicationExt: Application {
452 fn drag(&mut self) -> Task<Self::Message>;
454
455 fn maximize(&mut self) -> Task<Self::Message>;
457
458 fn minimize(&mut self) -> Task<Self::Message>;
460 #[cfg(not(feature = "multi-window"))]
463 fn title(&self) -> &str;
464
465 #[cfg(feature = "multi-window")]
466 fn title(&self, id: window::Id) -> &str;
468
469 fn set_show_context(&mut self, show: bool) {
471 self.core_mut().set_show_context(show);
472 }
473
474 fn set_header_title(&mut self, title: String) {
476 self.core_mut().set_header_title(title);
477 }
478
479 #[cfg(not(feature = "multi-window"))]
480 fn set_window_title(&mut self, title: String) -> Task<Self::Message>;
482
483 #[cfg(feature = "multi-window")]
484 fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message>;
486
487 fn view_main(&self) -> Element<crate::Action<Self::Message>>;
489
490 fn watch_config<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq>(
491 &self,
492 id: &'static str,
493 ) -> iced::Subscription<cosmic_config::Update<T>> {
494 self.core().watch_config(id)
495 }
496
497 fn watch_state<T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq>(
498 &self,
499 id: &'static str,
500 ) -> iced::Subscription<cosmic_config::Update<T>> {
501 self.core().watch_state(id)
502 }
503}
504
505impl<App: Application> ApplicationExt for App {
506 fn drag(&mut self) -> Task<Self::Message> {
507 self.core().drag(None)
508 }
509
510 fn maximize(&mut self) -> Task<Self::Message> {
511 self.core().maximize(None, true)
512 }
513
514 fn minimize(&mut self) -> Task<Self::Message> {
515 self.core().minimize(None)
516 }
517
518 #[cfg(feature = "multi-window")]
519 fn title(&self, id: window::Id) -> &str {
520 self.core().title.get(&id).map_or("", |s| s.as_str())
521 }
522
523 #[cfg(not(feature = "multi-window"))]
524 fn title(&self) -> &str {
525 self.core()
526 .main_window_id()
527 .and_then(|id| self.core().title.get(&id).map(std::string::String::as_str))
528 .unwrap_or("")
529 }
530
531 #[cfg(feature = "multi-window")]
532 fn set_window_title(&mut self, title: String, id: window::Id) -> Task<Self::Message> {
533 self.core_mut().title.insert(id, title.clone());
534 self.core().set_title(Some(id), title)
535 }
536
537 #[cfg(not(feature = "multi-window"))]
538 fn set_window_title(&mut self, title: String) -> Task<Self::Message> {
539 let Some(id) = self.core().main_window_id() else {
540 return Task::none();
541 };
542
543 self.core_mut().title.insert(id, title.clone());
544 Task::none()
545 }
546
547 #[allow(clippy::too_many_lines)]
548 fn view_main(&self) -> Element<crate::Action<Self::Message>> {
550 let core = self.core();
551 let is_condensed = core.is_condensed();
552 let sharp_corners = core.window.sharp_corners;
555 let content_container = core.window.content_container;
556 let show_context = core.window.show_context;
557 let nav_bar_active = core.nav_bar_active();
558 let focused = core
559 .focus_chain()
560 .iter()
561 .any(|i| Some(*i) == self.core().main_window_id());
562
563 let border_padding = if sharp_corners { 8 } else { 7 };
564
565 let main_content_padding = if !content_container {
566 [0, 0, 0, 0]
567 } else {
568 let right_padding = if show_context { 0 } else { border_padding };
569 let left_padding = if nav_bar_active { 0 } else { border_padding };
570
571 [0, right_padding, 0, left_padding]
572 };
573
574 let content_row = crate::widget::row::with_children({
575 let mut widgets = Vec::with_capacity(3);
576
577 let has_nav = if let Some(nav) = self
579 .nav_bar()
580 .map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar")))
581 {
582 widgets.push(
583 container(nav)
584 .padding([
585 0,
586 if is_condensed { border_padding } else { 8 },
587 border_padding,
588 border_padding,
589 ])
590 .into(),
591 );
592 true
593 } else {
594 false
595 };
596
597 if self.nav_model().is_none() || core.show_content() {
598 let main_content = self.view();
599
600 let context_width = core.context_width(has_nav);
602 if core.window.context_is_overlay && show_context {
603 if let Some(context) = self.context_drawer() {
604 widgets.push(
605 crate::widget::context_drawer(
606 context.title,
607 context.header_actions,
608 context.header,
609 context.footer,
610 context.on_close,
611 main_content,
612 context.content,
613 context_width,
614 )
615 .apply(|drawer| {
616 Element::from(id_container(
617 drawer,
618 iced_core::id::Id::new("COSMIC_context_drawer"),
619 ))
620 })
621 .apply(container)
622 .padding([0, if content_container { border_padding } else { 0 }, 0, 0])
623 .apply(Element::from)
624 .map(crate::Action::App),
625 );
626 } else {
627 widgets.push(
628 container(main_content.map(crate::Action::App))
629 .padding(main_content_padding)
630 .into(),
631 );
632 }
633 } else {
634 widgets.push(
636 container(main_content.map(crate::Action::App))
637 .padding(main_content_padding)
638 .into(),
639 );
640 if let Some(context) = self.context_drawer() {
641 widgets.push(
642 crate::widget::ContextDrawer::new_inner(
643 context.title,
644 context.header_actions,
645 context.header,
646 context.footer,
647 context.content,
648 context.on_close,
649 context_width,
650 )
651 .apply(Element::from)
652 .map(crate::Action::App)
653 .apply(container)
654 .width(context_width)
655 .apply(|drawer| {
656 Element::from(id_container(
657 drawer,
658 iced_core::id::Id::new("COSMIC_context_drawer"),
659 ))
660 })
661 .apply(container)
662 .padding(if content_container {
663 [0, border_padding, border_padding, border_padding]
664 } else {
665 [0, 0, 0, 0]
666 })
667 .into(),
668 )
669 } else {
670 widgets.push(horizontal_space().width(Length::Shrink).into());
672 }
673 }
674 }
675
676 widgets
677 });
678 let content_col = crate::widget::column::with_capacity(2)
679 .push(content_row)
680 .push_maybe(self.footer().map(|footer| {
681 container(footer.map(crate::Action::App)).padding([
682 0,
683 border_padding,
684 border_padding,
685 border_padding,
686 ])
687 }));
688 let content: Element<_> = if content_container {
689 content_col
690 .apply(container)
691 .width(iced::Length::Fill)
692 .height(iced::Length::Fill)
693 .class(crate::theme::Container::WindowBackground)
694 .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
695 .into()
696 } else {
697 content_col.into()
698 };
699
700 let window_corner_radius = crate::theme::active()
702 .cosmic()
703 .radius_s()
704 .map(|x| if x < 4.0 { x } else { x + 4.0 });
705
706 let view_column = crate::widget::column::with_capacity(2)
707 .push_maybe(if core.window.show_headerbar {
708 Some({
709 let mut header = crate::widget::header_bar()
710 .focused(focused)
711 .maximized(sharp_corners)
712 .title(&core.window.header_title)
713 .on_drag(crate::Action::Cosmic(Action::Drag))
714 .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu))
715 .on_double_click(crate::Action::Cosmic(Action::Maximize))
716 .is_condensed(is_condensed);
717
718 if self.nav_model().is_some() {
719 let toggle = crate::widget::nav_bar_toggle()
720 .active(core.nav_bar_active())
721 .selected(focused)
722 .on_toggle(if is_condensed {
723 crate::Action::Cosmic(Action::ToggleNavBarCondensed)
724 } else {
725 crate::Action::Cosmic(Action::ToggleNavBar)
726 });
727
728 header = header.start(toggle);
729 }
730
731 if core.window.show_close {
732 header = header.on_close(crate::Action::Cosmic(Action::Close));
733 }
734
735 if core.window.show_maximize && crate::config::show_maximize() {
736 header = header.on_maximize(crate::Action::Cosmic(Action::Maximize));
737 }
738
739 if core.window.show_minimize && crate::config::show_minimize() {
740 header = header.on_minimize(crate::Action::Cosmic(Action::Minimize));
741 }
742
743 for element in self.header_start() {
744 header = header.start(element.map(crate::Action::App));
745 }
746
747 for element in self.header_center() {
748 header = header.center(element.map(crate::Action::App));
749 }
750
751 for element in self.header_end() {
752 header = header.end(element.map(crate::Action::App));
753 }
754
755 if content_container {
756 header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header")))
757 } else {
758 header
760 .apply(container)
761 .class(crate::theme::Container::custom(move |theme| {
762 container::Style {
763 background: Some(iced::Background::Color(
764 theme.cosmic().background.base.into(),
765 )),
766 border: iced::Border {
767 radius: [
768 window_corner_radius[0] - 1.0,
769 window_corner_radius[1] - 1.0,
770 theme.cosmic().radius_0()[2],
771 theme.cosmic().radius_0()[3],
772 ]
773 .into(),
774 ..Default::default()
775 },
776 ..Default::default()
777 }
778 }))
779 .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header")))
780 }
781 })
782 } else {
783 None
784 })
785 .push(content)
787 .apply(container)
788 .padding(if sharp_corners { 0 } else { 1 })
789 .class(crate::theme::Container::custom(move |theme| {
790 container::Style {
791 background: if content_container {
792 Some(iced::Background::Color(
793 theme.cosmic().background.base.into(),
794 ))
795 } else {
796 None
797 },
798 border: iced::Border {
799 color: theme.cosmic().bg_divider().into(),
800 width: if sharp_corners { 0.0 } else { 1.0 },
801 radius: window_corner_radius.into(),
802 },
803 ..Default::default()
804 }
805 }));
806
807 let mut popover = popover(view_column).modal(true);
810 if let Some(dialog) = self
811 .dialog()
812 .map(|w| Element::from(id_container(w, iced_core::id::Id::new("COSMIC_dialog"))))
813 {
814 popover = popover.popup(dialog.map(crate::Action::App));
815 }
816
817 let view_element: Element<_> = popover.into();
818 view_element.debug(core.debug)
819 }
820}
821
822const EMBEDDED_FONTS: &[&[u8]] = &[
823 include_bytes!("../../res/open-sans/OpenSans-Light.ttf"),
824 include_bytes!("../../res/open-sans/OpenSans-Regular.ttf"),
825 include_bytes!("../../res/open-sans/OpenSans-Semibold.ttf"),
826 include_bytes!("../../res/open-sans/OpenSans-Bold.ttf"),
827 include_bytes!("../../res/open-sans/OpenSans-ExtraBold.ttf"),
828 include_bytes!("../../res/noto/NotoSansMono-Regular.ttf"),
829 include_bytes!("../../res/noto/NotoSansMono-Bold.ttf"),
830];
831
832#[cold]
833fn preload_fonts() {
834 let mut font_system = iced::advanced::graphics::text::font_system()
835 .write()
836 .unwrap();
837
838 EMBEDDED_FONTS
839 .iter()
840 .for_each(move |font| font_system.load_font(Cow::Borrowed(font)));
841}