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 .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 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, 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 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 let no_main_window = core.main_window.is_none();
571 if no_main_window {
572 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}