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;
553 let maximized = core.window.is_maximized;
554 let content_container = core.window.content_container;
555 let show_context = core.window.show_context;
556 let nav_bar_active = core.nav_bar_active();
557 let focused = core
558 .focus_chain()
559 .iter()
560 .any(|i| Some(*i) == self.core().main_window_id());
561
562 let border_padding = if maximized { 8 } else { 7 };
563
564 let main_content_padding = if !content_container {
565 [0, 0, 0, 0]
566 } else {
567 let right_padding = if show_context { 0 } else { border_padding };
568 let left_padding = if nav_bar_active { 0 } else { border_padding };
569
570 [0, right_padding, 0, left_padding]
571 };
572
573 let content_row = crate::widget::row::with_children({
574 let mut widgets = Vec::with_capacity(3);
575
576 let has_nav = if let Some(nav) = self
578 .nav_bar()
579 .map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar")))
580 {
581 widgets.push(
582 container(nav)
583 .padding([
584 0,
585 if is_condensed { border_padding } else { 8 },
586 border_padding,
587 border_padding,
588 ])
589 .into(),
590 );
591 true
592 } else {
593 false
594 };
595
596 if self.nav_model().is_none() || core.show_content() {
597 let main_content = self.view();
598
599 let context_width = core.context_width(has_nav);
601 if core.window.context_is_overlay && show_context {
602 if let Some(context) = self.context_drawer() {
603 widgets.push(
604 crate::widget::context_drawer(
605 context.title,
606 context.header_actions,
607 context.header,
608 context.footer,
609 context.on_close,
610 main_content,
611 context.content,
612 context_width,
613 )
614 .apply(|drawer| {
615 Element::from(id_container(
616 drawer,
617 iced_core::id::Id::new("COSMIC_context_drawer"),
618 ))
619 })
620 .apply(container)
621 .padding([0, if content_container { border_padding } else { 0 }, 0, 0])
622 .apply(Element::from)
623 .map(crate::Action::App),
624 );
625 } else {
626 widgets.push(
627 container(main_content.map(crate::Action::App))
628 .padding(main_content_padding)
629 .into(),
630 );
631 }
632 } else {
633 widgets.push(
635 container(main_content.map(crate::Action::App))
636 .padding(main_content_padding)
637 .into(),
638 );
639 if let Some(context) = self.context_drawer() {
640 widgets.push(
641 crate::widget::ContextDrawer::new_inner(
642 context.title,
643 context.header_actions,
644 context.header,
645 context.footer,
646 context.content,
647 context.on_close,
648 context_width,
649 )
650 .apply(Element::from)
651 .map(crate::Action::App)
652 .apply(container)
653 .width(context_width)
654 .apply(|drawer| {
655 Element::from(id_container(
656 drawer,
657 iced_core::id::Id::new("COSMIC_context_drawer"),
658 ))
659 })
660 .apply(container)
661 .padding(if content_container {
662 [0, border_padding, border_padding, border_padding]
663 } else {
664 [0, 0, 0, 0]
665 })
666 .into(),
667 )
668 } else {
669 widgets.push(horizontal_space().width(Length::Shrink).into());
671 }
672 }
673 }
674
675 widgets
676 });
677 let content_col = crate::widget::column::with_capacity(2)
678 .push(content_row)
679 .push_maybe(self.footer().map(|footer| {
680 container(footer.map(crate::Action::App)).padding([
681 0,
682 border_padding,
683 border_padding,
684 border_padding,
685 ])
686 }));
687 let content: Element<_> = if content_container {
688 content_col
689 .apply(container)
690 .width(iced::Length::Fill)
691 .height(iced::Length::Fill)
692 .class(crate::theme::Container::WindowBackground)
693 .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
694 .into()
695 } else {
696 content_col.into()
697 };
698
699 let window_corner_radius = if sharp_corners {
701 crate::theme::active().cosmic().radius_0()
702 } else {
703 crate::theme::active()
704 .cosmic()
705 .radius_s()
706 .map(|x| if x < 4.0 { x } else { x + 4.0 })
707 };
708
709 let view_column = crate::widget::column::with_capacity(2)
710 .push_maybe(if core.window.show_headerbar {
711 Some({
712 let mut header = crate::widget::header_bar()
713 .focused(focused)
714 .maximized(maximized)
715 .sharp_corners(sharp_corners)
716 .title(&core.window.header_title)
717 .on_drag(crate::Action::Cosmic(Action::Drag))
718 .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu))
719 .on_double_click(crate::Action::Cosmic(Action::Maximize))
720 .is_condensed(is_condensed);
721
722 if self.nav_model().is_some() {
723 let toggle = crate::widget::nav_bar_toggle()
724 .active(core.nav_bar_active())
725 .selected(focused)
726 .on_toggle(if is_condensed {
727 crate::Action::Cosmic(Action::ToggleNavBarCondensed)
728 } else {
729 crate::Action::Cosmic(Action::ToggleNavBar)
730 });
731
732 header = header.start(toggle);
733 }
734
735 if core.window.show_close {
736 header = header.on_close(crate::Action::Cosmic(Action::Close));
737 }
738
739 if core.window.show_maximize && crate::config::show_maximize() {
740 header = header.on_maximize(crate::Action::Cosmic(Action::Maximize));
741 }
742
743 if core.window.show_minimize && crate::config::show_minimize() {
744 header = header.on_minimize(crate::Action::Cosmic(Action::Minimize));
745 }
746
747 for element in self.header_start() {
748 header = header.start(element.map(crate::Action::App));
749 }
750
751 for element in self.header_center() {
752 header = header.center(element.map(crate::Action::App));
753 }
754
755 for element in self.header_end() {
756 header = header.end(element.map(crate::Action::App));
757 }
758
759 if content_container {
760 header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header")))
761 } else {
762 header
764 .apply(container)
765 .class(crate::theme::Container::custom(move |theme| {
766 let cosmic = theme.cosmic();
767 container::Style {
768 background: Some(iced::Background::Color(
769 cosmic.background.base.into(),
770 )),
771 border: iced::Border {
772 radius: [
773 (window_corner_radius[0] - 1.0).max(0.0),
774 (window_corner_radius[1] - 1.0).max(0.0),
775 cosmic.radius_0()[2],
776 cosmic.radius_0()[3],
777 ]
778 .into(),
779 ..Default::default()
780 },
781 ..Default::default()
782 }
783 }))
784 .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header")))
785 }
786 })
787 } else {
788 None
789 })
790 .push(content)
792 .apply(container)
793 .padding(if maximized { 0 } else { 1 })
794 .class(crate::theme::Container::custom(move |theme| {
795 container::Style {
796 background: if content_container {
797 Some(iced::Background::Color(
798 theme.cosmic().background.base.into(),
799 ))
800 } else {
801 None
802 },
803 border: iced::Border {
804 color: theme.cosmic().bg_divider().into(),
805 width: if maximized { 0.0 } else { 1.0 },
806 radius: window_corner_radius.into(),
807 },
808 ..Default::default()
809 }
810 }));
811
812 let mut popover = popover(view_column).modal(true);
815 if let Some(dialog) = self
816 .dialog()
817 .map(|w| Element::from(id_container(w, iced_core::id::Id::new("COSMIC_dialog"))))
818 {
819 popover = popover.popup(dialog.map(crate::Action::App));
820 }
821
822 let view_element: Element<_> = popover.into();
823 view_element.debug(core.debug)
824 }
825}
826
827const EMBEDDED_FONTS: &[&[u8]] = &[
828 include_bytes!("../../res/open-sans/OpenSans-Light.ttf"),
829 include_bytes!("../../res/open-sans/OpenSans-Regular.ttf"),
830 include_bytes!("../../res/open-sans/OpenSans-Semibold.ttf"),
831 include_bytes!("../../res/open-sans/OpenSans-Bold.ttf"),
832 include_bytes!("../../res/open-sans/OpenSans-ExtraBold.ttf"),
833 include_bytes!("../../res/noto/NotoSansMono-Regular.ttf"),
834 include_bytes!("../../res/noto/NotoSansMono-Bold.ttf"),
835];
836
837#[cold]
838fn preload_fonts() {
839 let mut font_system = iced::advanced::graphics::text::font_system()
840 .write()
841 .unwrap();
842
843 EMBEDDED_FONTS
844 .iter()
845 .for_each(move |font| font_system.load_font(Cow::Borrowed(font)));
846}