Skip to main content

cosmic/applet/
mod.rs

1#[cfg(feature = "applet-token")]
2pub mod token;
3
4use crate::app::{BootData, BootDataInner, cosmic, iced_settings};
5use crate::cctk::sctk;
6use crate::theme::{self, Button, THEME, system_dark, system_light};
7use crate::widget::autosize::{self, Autosize, autosize};
8use crate::widget::column::Column;
9use crate::widget::row::Row;
10use crate::widget::space::{horizontal, vertical};
11use crate::widget::{self, layer_container};
12use crate::{Application, Element, Renderer};
13
14pub use cosmic_panel_config;
15use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize};
16use iced::alignment::{Alignment, Horizontal, Vertical};
17use iced::widget::Container;
18use iced::{self, Color, Length, Limits, Rectangle, window};
19use iced_core::{Padding, Shadow};
20use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
21use iced_widget::Text;
22use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
23use std::borrow::Cow;
24use std::cell::RefCell;
25use std::num::NonZeroU32;
26use std::rc::Rc;
27use std::sync::LazyLock;
28use std::time::Duration;
29use tracing::info;
30
31pub mod column;
32pub mod row;
33
34static AUTOSIZE_ID: LazyLock<iced::id::Id> =
35    LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize"));
36static AUTOSIZE_MAIN_ID: LazyLock<iced::id::Id> =
37    LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main"));
38static TOOLTIP_ID: LazyLock<crate::widget::Id> = LazyLock::new(|| iced::id::Id::new("subsurface"));
39pub(crate) static TOOLTIP_WINDOW_ID: LazyLock<window::Id> = LazyLock::new(window::Id::unique);
40
41#[derive(Debug, Clone)]
42pub struct Context {
43    pub size: Size,
44    pub anchor: PanelAnchor,
45    pub spacing: u32,
46    pub background: CosmicPanelBackground,
47    pub output_name: String,
48    pub panel_type: PanelType,
49    /// Includes the configured size of the window.
50    /// This can be used by apples to handle overflow themselves.
51    pub suggested_bounds: Option<iced::Size>,
52    /// Ratio of overlap for applet padding.
53    pub padding_overlap: f32,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum Size {
58    // (width, height)
59    Hardcoded((u16, u16)),
60    PanelSize(PanelSize),
61}
62#[derive(Clone, Debug, PartialEq)]
63pub enum PanelType {
64    Panel,
65    Dock,
66    Other(String),
67}
68
69impl ToString for PanelType {
70    fn to_string(&self) -> String {
71        match self {
72            Self::Panel => "Panel".to_string(),
73            Self::Dock => "Dock".to_string(),
74            Self::Other(other) => other.clone(),
75        }
76    }
77}
78
79impl From<String> for PanelType {
80    fn from(value: String) -> Self {
81        match value.as_str() {
82            "Panel" => PanelType::Panel,
83            "Dock" => PanelType::Dock,
84            _ => PanelType::Other(value),
85        }
86    }
87}
88
89impl Default for Context {
90    fn default() -> Self {
91        Self {
92            size: Size::PanelSize(
93                std::env::var("COSMIC_PANEL_SIZE")
94                    .ok()
95                    .and_then(|size| ron::from_str(size.as_str()).ok())
96                    .unwrap_or(PanelSize::S),
97            ),
98            anchor: std::env::var("COSMIC_PANEL_ANCHOR")
99                .ok()
100                .and_then(|size| ron::from_str(size.as_str()).ok())
101                .unwrap_or(PanelAnchor::Top),
102            spacing: std::env::var("COSMIC_PANEL_SPACING")
103                .ok()
104                .and_then(|size| ron::from_str(size.as_str()).ok())
105                .unwrap_or(4),
106            background: std::env::var("COSMIC_PANEL_BACKGROUND")
107                .ok()
108                .and_then(|size| ron::from_str(size.as_str()).ok())
109                .unwrap_or(CosmicPanelBackground::ThemeDefault),
110            output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(),
111            panel_type: PanelType::from(std::env::var("COSMIC_PANEL_NAME").unwrap_or_default()),
112            padding_overlap: str::parse(
113                &std::env::var("COSMIC_PANEL_PADDING_OVERLAP").unwrap_or_default(),
114            )
115            .unwrap_or(0.0),
116            suggested_bounds: None,
117        }
118    }
119}
120
121impl Context {
122    #[must_use]
123    pub fn suggested_size(&self, is_symbolic: bool) -> (u16, u16) {
124        match &self.size {
125            Size::PanelSize(size) => {
126                let s = size.get_applet_icon_size(is_symbolic) as u16;
127                (s, s)
128            }
129            Size::Hardcoded((width, height)) => (*width, *height),
130        }
131    }
132
133    #[must_use]
134    pub fn suggested_window_size(&self) -> (NonZeroU32, NonZeroU32) {
135        let suggested = self.suggested_size(true);
136        let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
137        let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
138            (applet_padding_major_axis, applet_padding_minor_axis)
139        } else {
140            (applet_padding_minor_axis, applet_padding_major_axis)
141        };
142
143        let configured_width = self
144            .suggested_bounds
145            .as_ref()
146            .and_then(|c| NonZeroU32::new(c.width as u32)) // TODO: should this be physical size instead of logical?
147            .unwrap_or_else(|| {
148                NonZeroU32::new(suggested.0 as u32 + horizontal_padding as u32 * 2).unwrap()
149            });
150
151        let configured_height = self
152            .suggested_bounds
153            .as_ref()
154            .and_then(|c| NonZeroU32::new(c.height as u32))
155            .unwrap_or_else(|| {
156                NonZeroU32::new(suggested.1 as u32 + vertical_padding as u32 * 2).unwrap()
157            });
158        info!("{configured_height:?}");
159        (configured_width, configured_height)
160    }
161
162    #[must_use]
163    pub fn suggested_padding(&self, is_symbolic: bool) -> (u16, u16) {
164        match &self.size {
165            Size::PanelSize(size) => (
166                size.get_applet_shrinkable_padding(is_symbolic),
167                size.get_applet_padding(is_symbolic),
168            ),
169            Size::Hardcoded(_) => (12, 8),
170        }
171    }
172
173    // Set the default window size. Helper for application init with hardcoded size.
174    pub fn window_size(&mut self, width: u16, height: u16) {
175        self.size = Size::Hardcoded((width, height));
176    }
177
178    #[allow(clippy::cast_precision_loss)]
179    pub fn window_settings(&self) -> crate::app::Settings {
180        let (width, height) = self.suggested_size(true);
181        let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
182        let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
183            (applet_padding_major_axis, applet_padding_minor_axis)
184        } else {
185            (applet_padding_minor_axis, applet_padding_major_axis)
186        };
187
188        let width = f32::from(width) + horizontal_padding as f32 * 2.;
189        let height = f32::from(height) + vertical_padding as f32 * 2.;
190        let mut settings = crate::app::Settings::default()
191            .size(iced_core::Size::new(width, height))
192            .size_limits(Limits::NONE.min_height(height).min_width(width))
193            .resizable(None)
194            .default_text_size(14.0)
195            .default_font(crate::font::default())
196            .transparent(true);
197        if let Some(theme) = self.theme() {
198            settings = settings.theme(theme);
199        }
200        settings.exit_on_close = true;
201        settings
202    }
203
204    #[must_use]
205    pub fn is_horizontal(&self) -> bool {
206        matches!(self.anchor, PanelAnchor::Top | PanelAnchor::Bottom)
207    }
208
209    pub fn icon_button_from_handle<'a, Message: Clone + 'static>(
210        &self,
211        icon: widget::icon::Handle,
212    ) -> crate::widget::Button<'a, Message> {
213        let suggested = self.suggested_size(icon.symbolic);
214        let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
215        let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
216            (applet_padding_major_axis, applet_padding_minor_axis)
217        } else {
218            (applet_padding_minor_axis, applet_padding_major_axis)
219        };
220        let symbolic = icon.symbolic;
221        let icon = widget::icon(icon)
222            .class(if symbolic {
223                theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style {
224                    color: Some(theme.cosmic().background.on.into()),
225                }))
226            } else {
227                theme::Svg::default()
228            })
229            .width(Length::Fixed(suggested.0 as f32))
230            .height(Length::Fixed(suggested.1 as f32));
231        self.button_from_element(icon, symbolic)
232    }
233
234    pub fn button_from_element<'a, Message: Clone + 'static>(
235        &self,
236        content: impl Into<Element<'a, Message>>,
237        use_symbolic_size: bool,
238    ) -> crate::widget::Button<'a, Message> {
239        let suggested = self.suggested_size(use_symbolic_size);
240        let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
241        let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
242            (applet_padding_major_axis, applet_padding_minor_axis)
243        } else {
244            (applet_padding_minor_axis, applet_padding_major_axis)
245        };
246
247        crate::widget::button::custom(layer_container(content).center(Length::Fill))
248            .width(Length::Fixed((suggested.0 + 2 * horizontal_padding) as f32))
249            .height(Length::Fixed((suggested.1 + 2 * vertical_padding) as f32))
250            .class(Button::AppletIcon)
251    }
252
253    pub fn text_button<'a, Message: Clone + 'static>(
254        &self,
255        text: impl Into<Text<'a, crate::Theme, crate::Renderer>>,
256        message: Message,
257    ) -> crate::widget::Button<'a, Message> {
258        let text = text.into();
259        let suggested = self.suggested_size(true);
260
261        let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
262        let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
263            (applet_padding_major_axis, applet_padding_minor_axis)
264        } else {
265            (applet_padding_minor_axis, applet_padding_major_axis)
266        };
267        crate::widget::button::custom(
268            layer_container(
269                Text::from(text)
270                    .height(Length::Fill)
271                    .align_y(Alignment::Center),
272            )
273            .center_y(Length::Fixed(f32::from(suggested.1 + 2 * vertical_padding))),
274        )
275        .on_press_down(message)
276        .padding([0, horizontal_padding])
277        .class(crate::theme::Button::AppletIcon)
278    }
279
280    pub fn icon_button<'a, Message: Clone + 'static>(
281        &self,
282        icon_name: &'a str,
283    ) -> crate::widget::Button<'a, Message> {
284        let suggested_size = self.suggested_size(true);
285        self.icon_button_from_handle(
286            widget::icon::from_name(icon_name)
287                .symbolic(true)
288                .size(suggested_size.0)
289                .into(),
290        )
291    }
292
293    pub fn applet_tooltip<'a, Message: 'static>(
294        &self,
295        content: impl Into<Element<'a, Message>>,
296        tooltip: impl Into<Cow<'static, str>>,
297        has_popup: bool,
298        on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static,
299        parent_id: Option<window::Id>,
300    ) -> crate::widget::wayland::tooltip::widget::Tooltip<'a, Message, Message> {
301        let window_id = *TOOLTIP_WINDOW_ID;
302        let subsurface_id = TOOLTIP_ID.clone();
303        let anchor = self.anchor;
304        let tooltip = tooltip.into();
305
306        crate::widget::wayland::tooltip::widget::Tooltip::<'a, Message, Message>::new(
307            content,
308            (!has_popup).then_some(move |bounds: Rectangle| {
309                let window_id = window_id;
310                let (popup_anchor, gravity) = match anchor {
311                    PanelAnchor::Left => (Anchor::Right, Gravity::Right),
312                    PanelAnchor::Right => (Anchor::Left, Gravity::Left),
313                    PanelAnchor::Top => (Anchor::Bottom, Gravity::Bottom),
314                    PanelAnchor::Bottom => (Anchor::Top, Gravity::Top),
315                };
316
317                SctkPopupSettings {
318                    parent: parent_id.unwrap_or(window::Id::RESERVED),
319                    id: window_id,
320                    grab: false,
321                    input_zone: Some(vec![Rectangle::new(
322                        iced::Point::new(-1000., -1000.),
323                        iced::Size::default(),
324                    )]),
325                    positioner: SctkPositioner {
326                        size: None,
327                        size_limits: Limits::NONE.min_width(1.).min_height(1.),
328                        anchor_rect: Rectangle {
329                            x: bounds.x.round() as i32,
330                            y: bounds.y.round() as i32,
331                            width: bounds.width.round() as i32,
332                            height: bounds.height.round() as i32,
333                        },
334                        anchor: popup_anchor,
335                        gravity,
336                        constraint_adjustment: 15,
337                        offset: (0, 0),
338                        reactive: true,
339                    },
340                    parent_size: None,
341                    close_with_children: true,
342                }
343            }),
344            move || {
345                Element::from(autosize::autosize(
346                    layer_container(crate::widget::text(tooltip.clone()))
347                        .layer(crate::cosmic_theme::Layer::Background)
348                        .padding(4.),
349                    subsurface_id.clone(),
350                ))
351            },
352            on_surface_action(crate::surface::Action::DestroyPopup(window_id)),
353            on_surface_action,
354        )
355        .delay(Duration::from_millis(100))
356    }
357
358    // TODO popup container which tracks the size of itself and requests the popup to resize to match
359    pub fn popup_container<'a, Message: 'static>(
360        &self,
361        content: impl Into<Element<'a, Message>>,
362    ) -> Autosize<'a, Message, crate::Theme, Renderer> {
363        let (vertical_align, horizontal_align) = match self.anchor {
364            PanelAnchor::Left => (Vertical::Center, Horizontal::Left),
365            PanelAnchor::Right => (Vertical::Center, Horizontal::Right),
366            PanelAnchor::Top => (Vertical::Top, Horizontal::Center),
367            PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center),
368        };
369
370        autosize(
371            Container::<Message, _, Renderer>::new(
372                Container::<Message, _, Renderer>::new(content).style(|theme| {
373                    let cosmic = theme.cosmic();
374                    let corners = cosmic.corner_radii;
375                    iced_widget::container::Style {
376                        text_color: Some(cosmic.background.on.into()),
377                        background: Some(Color::from(cosmic.background.base).into()),
378                        border: iced::Border {
379                            radius: corners.radius_m.into(),
380                            width: 1.0,
381                            color: cosmic.background.divider.into(),
382                        },
383                        shadow: Shadow::default(),
384                        icon_color: Some(cosmic.background.on.into()),
385                        snap: true,
386                    }
387                }),
388            )
389            .height(Length::Shrink)
390            .align_x(horizontal_align)
391            .align_y(vertical_align),
392            AUTOSIZE_ID.clone(),
393        )
394        .limits(
395            Limits::NONE
396                .min_height(1.)
397                .min_width(360.0)
398                .max_width(360.0)
399                .max_height(1000.0),
400        )
401    }
402
403    #[must_use]
404    #[allow(clippy::cast_possible_wrap)]
405    pub fn get_popup_settings(
406        &self,
407        parent: window::Id,
408        id: window::Id,
409        size: Option<(u32, u32)>,
410        width_padding: Option<i32>,
411        height_padding: Option<i32>,
412    ) -> SctkPopupSettings {
413        let (width, height) = self.suggested_size(true);
414        let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
415        let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
416            (applet_padding_major_axis, applet_padding_minor_axis)
417        } else {
418            (applet_padding_minor_axis, applet_padding_major_axis)
419        };
420        let pixel_offset = 4;
421        let (offset, anchor, gravity) = match self.anchor {
422            PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right),
423            PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left),
424            PanelAnchor::Top => ((0, pixel_offset), Anchor::Bottom, Gravity::Bottom),
425            PanelAnchor::Bottom => ((0, -pixel_offset), Anchor::Top, Gravity::Top),
426        };
427        SctkPopupSettings {
428            parent,
429            id,
430            positioner: SctkPositioner {
431                anchor,
432                gravity,
433                offset,
434                size,
435                anchor_rect: Rectangle {
436                    x: 0,
437                    y: 0,
438                    width: width_padding.unwrap_or(horizontal_padding as i32) * 2
439                        + i32::from(width),
440                    height: height_padding.unwrap_or(vertical_padding as i32) * 2
441                        + i32::from(height),
442                },
443                reactive: true,
444                constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y
445                size_limits: Limits::NONE
446                    .min_height(1.0)
447                    .min_width(360.0)
448                    .max_width(360.0)
449                    .max_height(1080.0),
450            },
451            parent_size: None,
452            grab: true,
453            close_with_children: false,
454            input_zone: None,
455        }
456    }
457
458    pub fn autosize_window<'a, Message: 'static>(
459        &self,
460        content: impl Into<Element<'a, Message>>,
461    ) -> Autosize<'a, Message, crate::Theme, crate::Renderer> {
462        let force_configured = matches!(&self.panel_type, PanelType::Other(n) if n.is_empty());
463        let w = autosize(content, AUTOSIZE_MAIN_ID.clone());
464        let mut limits = Limits::NONE;
465        let suggested_window_size = self.suggested_window_size();
466
467        if let Some(width) = self
468            .suggested_bounds
469            .as_ref()
470            .filter(|c| c.width as i32 > 0)
471            .map(|c| c.width)
472        {
473            limits = limits.width(width);
474        }
475        if let Some(height) = self
476            .suggested_bounds
477            .as_ref()
478            .filter(|c| c.height as i32 > 0)
479            .map(|c| c.height)
480        {
481            limits = limits.height(height);
482        }
483
484        w.limits(limits)
485    }
486
487    #[must_use]
488    pub fn theme(&self) -> Option<theme::Theme> {
489        match self.background {
490            CosmicPanelBackground::Dark => {
491                let mut theme = system_dark();
492                theme.theme_type.prefer_dark(Some(true));
493                Some(theme)
494            }
495            CosmicPanelBackground::Light => {
496                let mut theme = system_light();
497                theme.theme_type.prefer_dark(Some(false));
498                Some(theme)
499            }
500            _ => Some(theme::system_preference()),
501        }
502    }
503
504    pub fn text<'a>(&self, msg: impl Into<Cow<'a, str>>) -> crate::widget::Text<'a, crate::Theme> {
505        let msg = msg.into();
506        let t = match self.size {
507            Size::Hardcoded(_) => crate::widget::text,
508            Size::PanelSize(ref s) => {
509                let size = s.get_applet_icon_size_with_padding(false);
510
511                let size_threshold_small = PanelSize::S.get_applet_icon_size_with_padding(false);
512                let size_threshold_medium = PanelSize::M.get_applet_icon_size_with_padding(false);
513                let size_threshold_large = PanelSize::L.get_applet_icon_size_with_padding(false);
514
515                if size <= size_threshold_small {
516                    crate::widget::text::body
517                } else if size <= size_threshold_medium {
518                    crate::widget::text::title4
519                } else if size <= size_threshold_large {
520                    crate::widget::text::title3
521                } else {
522                    crate::widget::text::title2
523                }
524            }
525        };
526        t(msg).font(crate::font::default())
527    }
528}
529
530/// Launch the application with the given settings.
531///
532/// # Errors
533///
534/// Returns error on application failure.
535pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
536    let helper = Context::default();
537
538    let mut settings = helper.window_settings();
539    settings.resizable = None;
540
541    #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
542    if let Some(threshold) = settings.default_mmap_threshold {
543        crate::malloc::limit_mmap_threshold(threshold);
544    }
545
546    if let Some(icon_theme) = settings.default_icon_theme.as_ref() {
547        crate::icon_theme::set_default(icon_theme.clone());
548    }
549
550    THEME
551        .lock()
552        .unwrap()
553        .set_theme(settings.theme.theme_type.clone());
554
555    let (iced_settings, (mut core, flags), mut window_settings) =
556        iced_settings::<App>(settings, flags);
557    core.window.show_headerbar = false;
558    core.window.sharp_corners = true;
559    core.window.show_maximize = false;
560    core.window.show_minimize = false;
561    core.window.use_template = false;
562
563    window_settings.decorations = false;
564    window_settings.exit_on_close_request = true;
565    window_settings.resizable = false;
566    window_settings.resize_border = 0;
567
568    // TODO make multi-window not mandatory
569
570    let no_main_window = core.main_window.is_none();
571    if no_main_window {
572        // TODO still apply window settings?
573        // window_settings = window_settings.clone();
574        core.main_window = Some(iced_core::window::Id::RESERVED);
575    }
576    let mut app = iced::daemon(
577        BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
578            flags,
579            core,
580            settings: window_settings,
581        })))),
582        cosmic::Cosmic::update,
583        cosmic::Cosmic::view,
584    );
585
586    app.subscription(cosmic::Cosmic::subscription)
587        .style(cosmic::Cosmic::style)
588        .theme(cosmic::Cosmic::theme)
589        .settings(iced_settings)
590        .run()
591}
592
593#[must_use]
594pub fn style() -> iced::theme::Style {
595    let theme = crate::theme::THEME.lock().unwrap();
596    iced::theme::Style {
597        background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
598        text_color: theme.cosmic().on_bg_color().into(),
599        icon_color: theme.cosmic().on_bg_color().into(),
600    }
601}
602
603pub fn menu_button<'a, Message: Clone + 'a>(
604    content: impl Into<Element<'a, Message>>,
605) -> crate::widget::Button<'a, Message> {
606    crate::widget::button::custom(content)
607        .class(Button::AppletMenu)
608        .padding(menu_control_padding())
609        .width(Length::Fill)
610}
611
612pub fn padded_control<'a, Message>(
613    content: impl Into<Element<'a, Message>>,
614) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> {
615    crate::widget::container(content)
616        .padding(menu_control_padding())
617        .width(Length::Fill)
618}
619
620pub fn menu_control_padding() -> Padding {
621    let guard = THEME.lock().unwrap();
622    let cosmic = guard.cosmic();
623    [cosmic.space_xxs(), cosmic.space_m()].into()
624}