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