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 pub suggested_bounds: Option<iced::Size>,
52 pub padding_overlap: f32,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum Size {
58 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)) .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 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 .transparent(true);
196 if let Some(theme) = self.theme() {
197 settings = settings.theme(theme);
198 }
199 settings.exit_on_close = true;
200 settings
201 }
202
203 #[must_use]
204 pub fn is_horizontal(&self) -> bool {
205 matches!(self.anchor, PanelAnchor::Top | PanelAnchor::Bottom)
206 }
207
208 pub fn icon_button_from_handle<'a, Message: Clone + 'static>(
209 &self,
210 icon: widget::icon::Handle,
211 ) -> crate::widget::Button<'a, Message> {
212 let suggested = self.suggested_size(icon.symbolic);
213 let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
214 let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
215 (applet_padding_major_axis, applet_padding_minor_axis)
216 } else {
217 (applet_padding_minor_axis, applet_padding_major_axis)
218 };
219 let symbolic = icon.symbolic;
220 let icon = widget::icon(icon)
221 .class(if symbolic {
222 theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style {
223 color: Some(theme.cosmic().background(theme.transparent).on.into()),
224 }))
225 } else {
226 theme::Svg::default()
227 })
228 .width(Length::Fixed(suggested.0 as f32))
229 .height(Length::Fixed(suggested.1 as f32));
230 self.button_from_element(icon, symbolic)
231 }
232
233 pub fn button_from_element<'a, Message: Clone + 'static>(
234 &self,
235 content: impl Into<Element<'a, Message>>,
236 use_symbolic_size: bool,
237 ) -> crate::widget::Button<'a, Message> {
238 let suggested = self.suggested_size(use_symbolic_size);
239 let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
240 let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
241 (applet_padding_major_axis, applet_padding_minor_axis)
242 } else {
243 (applet_padding_minor_axis, applet_padding_major_axis)
244 };
245
246 crate::widget::button::custom(layer_container(content).center(Length::Fill))
247 .width(Length::Fixed((suggested.0 + 2 * horizontal_padding) as f32))
248 .height(Length::Fixed((suggested.1 + 2 * vertical_padding) as f32))
249 .class(Button::AppletIcon)
250 }
251
252 pub fn text_button<'a, Message: Clone + 'static>(
253 &self,
254 text: impl Into<Text<'a, crate::Theme, crate::Renderer>>,
255 message: Message,
256 ) -> crate::widget::Button<'a, Message> {
257 let text = text.into();
258 let suggested = self.suggested_size(true);
259
260 let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true);
261 let (horizontal_padding, vertical_padding) = if self.is_horizontal() {
262 (applet_padding_major_axis, applet_padding_minor_axis)
263 } else {
264 (applet_padding_minor_axis, applet_padding_major_axis)
265 };
266 crate::widget::button::custom(
267 layer_container(
268 Text::from(text)
269 .height(Length::Fill)
270 .align_y(Alignment::Center),
271 )
272 .center_y(Length::Fixed(f32::from(suggested.1 + 2 * vertical_padding))),
273 )
274 .on_press_down(message)
275 .padding([0, horizontal_padding])
276 .class(crate::theme::Button::AppletIcon)
277 }
278
279 pub fn icon_button<'a, Message: Clone + 'static>(
280 &self,
281 icon_name: &'a str,
282 ) -> crate::widget::Button<'a, Message> {
283 let suggested_size = self.suggested_size(true);
284 self.icon_button_from_handle(
285 widget::icon::from_name(icon_name)
286 .symbolic(true)
287 .size(suggested_size.0)
288 .into(),
289 )
290 }
291
292 pub fn applet_tooltip<'a, Message: 'static>(
293 &self,
294 content: impl Into<Element<'a, Message>>,
295 tooltip: impl Into<Cow<'static, str>>,
296 has_popup: bool,
297 on_surface_action: impl Fn(crate::surface::Action<Message>) -> Message + 'static,
298 parent_id: Option<window::Id>,
299 ) -> crate::widget::wayland::tooltip::widget::Tooltip<'a, Message, Message> {
300 let window_id = *TOOLTIP_WINDOW_ID;
301 let subsurface_id = TOOLTIP_ID.clone();
302 let anchor = self.anchor;
303 let tooltip = tooltip.into();
304
305 crate::widget::wayland::tooltip::widget::Tooltip::<'a, Message, Message>::new(
306 content,
307 (!has_popup).then_some(move |bounds: Rectangle| {
308 let window_id = window_id;
309 let (popup_anchor, gravity) = match anchor {
310 PanelAnchor::Left => (Anchor::Right, Gravity::Right),
311 PanelAnchor::Right => (Anchor::Left, Gravity::Left),
312 PanelAnchor::Top => (Anchor::Bottom, Gravity::Bottom),
313 PanelAnchor::Bottom => (Anchor::Top, Gravity::Top),
314 };
315
316 SctkPopupSettings {
317 parent: parent_id.unwrap_or(window::Id::RESERVED),
318 id: window_id,
319 grab: false,
320 input_zone: Some(vec![Rectangle::new(
321 iced::Point::new(-1000., -1000.),
322 iced::Size::default(),
323 )]),
324 positioner: SctkPositioner {
325 size: None,
326 size_limits: Limits::NONE.min_width(1.).min_height(1.),
327 anchor_rect: Rectangle {
328 x: bounds.x.round() as i32,
329 y: bounds.y.round() as i32,
330 width: bounds.width.round() as i32,
331 height: bounds.height.round() as i32,
332 },
333 anchor: popup_anchor,
334 gravity,
335 constraint_adjustment: 15,
336 offset: (0, 0),
337 reactive: true,
338 },
339 parent_size: None,
340 close_with_children: true,
341 }
342 }),
343 move || {
344 Element::from(autosize::autosize(
345 layer_container(crate::widget::text(tooltip.clone()))
346 .layer(crate::cosmic_theme::Layer::Background)
347 .padding(4.),
348 subsurface_id.clone(),
349 ))
350 },
351 on_surface_action(crate::surface::Action::DestroyPopup(window_id)),
352 on_surface_action,
353 )
354 .delay(Duration::from_millis(100))
355 }
356
357 pub fn popup_container<'a, Message: 'static>(
359 &self,
360 content: impl Into<Element<'a, Message>>,
361 ) -> Autosize<'a, Message, crate::Theme, Renderer> {
362 let (vertical_align, horizontal_align) = match self.anchor {
363 PanelAnchor::Left => (Vertical::Center, Horizontal::Left),
364 PanelAnchor::Right => (Vertical::Center, Horizontal::Right),
365 PanelAnchor::Top => (Vertical::Top, Horizontal::Center),
366 PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center),
367 };
368
369 autosize(
370 Container::<Message, _, Renderer>::new(
371 Container::<Message, _, Renderer>::new(content).style(|theme| {
372 let cosmic = theme.cosmic();
373 let corners = cosmic.corner_radii;
374 let mut bg = cosmic.background(theme.transparent).base;
375 iced_widget::container::Style {
376 text_color: Some(cosmic.background(theme.transparent).on.into()),
377 background: Some(Color::from(bg).into()),
378 border: iced::Border {
379 radius: corners.radius_m.into(),
380 width: 1.0,
381 color: cosmic.background(theme.transparent).divider.into(),
382 },
383 shadow: Shadow::default(),
384 icon_color: Some(cosmic.background(theme.transparent).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, 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
530pub fn run<App: Application>(flags: App::Flags) -> iced::Result {
536 if let Some(fd) = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET")
537 .ok()
538 .and_then(|fd| fd.parse::<std::os::fd::RawFd>().ok())
539 {
540 let fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
541 if let Ok(flags) = rustix::io::fcntl_getfd(fd) {
542 let _ = rustix::io::fcntl_setfd(fd, flags | rustix::io::FdFlags::CLOEXEC);
543 }
544 }
545 let helper = Context::default();
546
547 let mut settings = helper.window_settings();
548 settings.resizable = None;
549
550 #[cfg(all(target_env = "gnu", not(target_os = "windows")))]
551 if let Some(threshold) = settings.default_mmap_threshold {
552 crate::malloc::limit_mmap_threshold(threshold);
553 }
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 core.app_type = crate::core::AppType::Applet;
563
564 window_settings.decorations = false;
565 window_settings.exit_on_close_request = true;
566 window_settings.resizable = false;
567 window_settings.resize_border = 0;
568
569 let no_main_window = core.main_window.is_none();
572 if no_main_window {
573 core.main_window = Some(iced_core::window::Id::RESERVED);
576 }
577 let mut app = iced::daemon(
578 BootData(Rc::new(RefCell::new(Some(BootDataInner::<App> {
579 flags,
580 core,
581 settings: window_settings,
582 })))),
583 cosmic::Cosmic::update,
584 cosmic::Cosmic::view,
585 );
586
587 app.subscription(cosmic::Cosmic::subscription)
588 .style(cosmic::Cosmic::style)
589 .theme(cosmic::Cosmic::theme)
590 .settings(iced_settings)
591 .run()
592}
593
594#[must_use]
595pub fn style() -> iced::theme::Style {
596 let theme = crate::theme::THEME.lock().unwrap();
597 iced::theme::Style {
598 background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
599 text_color: theme.cosmic().on_bg_color().into(),
600 icon_color: theme.cosmic().on_bg_color().into(),
601 }
602}
603
604pub fn menu_button<'a, Message: Clone + 'a>(
605 content: impl Into<Element<'a, Message>>,
606) -> crate::widget::Button<'a, Message> {
607 crate::widget::button::custom(content)
608 .class(Button::AppletMenu)
609 .padding(menu_control_padding())
610 .width(Length::Fill)
611}
612
613pub fn padded_control<'a, Message>(
614 content: impl Into<Element<'a, Message>>,
615) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> {
616 crate::widget::container(content)
617 .padding(menu_control_padding())
618 .width(Length::Fill)
619}
620
621pub fn menu_control_padding() -> Padding {
622 let guard = THEME.lock().unwrap();
623 let cosmic = guard.cosmic();
624 [cosmic.space_xxs(), cosmic.space_m()].into()
625}