Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions cosmic-applet-status-area/src/components/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ impl App {
fn resize_window(&self) -> app::Task<Msg> {
let icon_size = self.core.applet.suggested_size(true).0 as u32
+ self.core.applet.suggested_padding(true).1 as u32 * 2;
let n = self.menus.len() as u32;
// Size to the visible count so a Passive item does not widen the panel.
let n = self.visible_menus().count() as u32;
window::resize(
self.core.main_window_id().unwrap(),
iced::Size::new(1.max(icon_size * n) as f32, icon_size as f32),
Expand All @@ -84,7 +85,8 @@ impl App {
let button_total_size = self.core.applet.suggested_size(true).0
+ self.core.applet.suggested_padding(true).1 * 2;

let menu_count = self.menus.len();
// Count only visible items so the skip/take split matches the rendered row.
let menu_count = self.visible_menus().count();

let btn_count = max_major_axis_len / button_total_size as u32;
if btn_count >= menu_count as u32 {
Expand All @@ -98,14 +100,26 @@ impl App {
}
}

/// The menus rendered as icons, in id order, with Passive items excluded
/// from every position/index/count computation so the popup anchor math
/// stays in sync with the on-screen row.
fn visible_menus(
&self,
) -> impl DoubleEndedIterator<Item = (usize, &status_menu::State)> + Clone {
self.menus
.iter()
.filter(|(_, m)| !m.is_passive())
.map(|(id, m)| (*id, m))
}

fn view_overflow_popup(&self) -> cosmic::Element<'_, Msg> {
// Render the overflow popup with the menus that are not shown in the main view
let overflow_index = self.overflow_index().unwrap_or(0);
let children = self.menus.iter().skip(overflow_index).map(|(id, menu)| {
let children = self.visible_menus().skip(overflow_index).map(|(id, menu)| {
mouse_area(
menu_icon_button(&self.core.applet, &menu).on_press_down(Msg::TogglePopup(*id)),
menu_icon_button(&self.core.applet, &menu).on_press_down(Msg::TogglePopup(id)),
)
.on_enter(Msg::Hovered(*id))
.on_enter(Msg::Hovered(id))
.into()
});

Expand Down Expand Up @@ -238,7 +252,10 @@ impl cosmic::Application for App {
cmds.push(destroy_popup(popup_id));
}
let popup_id = self.next_popup_id();
let i = self.menus.keys().position(|&i| i == id).unwrap();
// Anchor within the visible set; a Passive/absent id has no slot — bail.
let Some(i) = self.visible_menus().position(|(mid, _)| mid == id) else {
return Task::none();
};
let (i, parent) = self
.overflow_index()
.and_then(|overflow_i| {
Expand Down Expand Up @@ -327,7 +344,10 @@ impl cosmic::Application for App {
return Task::none();
}
let popup_id = self.next_popup_id();
let i = self.menus.keys().position(|&i| i == id).unwrap();
// Anchor within the visible set; a Passive/absent id has no slot — bail.
let Some(i) = self.visible_menus().position(|(mid, _)| mid == id) else {
return Task::none();
};

let (i, parent) = self
.overflow_index()
Expand Down Expand Up @@ -467,14 +487,15 @@ impl cosmic::Application for App {
fn view(&self) -> cosmic::Element<'_, Msg> {
let overflow_index = self.overflow_index();

// Bind first: inlining into `take(...)` would borrow `self` twice.
let visible_count = self.visible_menus().count();
let children = self
.menus
.iter()
.take(overflow_index.unwrap_or(self.menus.len()))
.visible_menus()
.take(overflow_index.unwrap_or(visible_count))
.map(|(id, menu)| {
mouse_area(menu_icon_button(&self.core.applet, &menu).on_press(Msg::Activate(*id)))
.on_right_press(Msg::TogglePopup(*id))
.on_enter(Msg::Hovered(*id))
mouse_area(menu_icon_button(&self.core.applet, &menu).on_press(Msg::Activate(id)))
.on_right_press(Msg::TogglePopup(id))
.on_enter(Msg::Hovered(id))
.into()
});

Expand Down
109 changes: 105 additions & 4 deletions cosmic-applet-status-area/src/components/status_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use cosmic::{
};
use std::path::{Path, PathBuf};

use crate::subscriptions::status_notifier_item::{IconUpdate, Layout, StatusNotifierItem};
use crate::subscriptions::status_notifier_item::{Icon, IconUpdate, Layout, StatusNotifierItem};

#[derive(Clone, Debug)]
pub enum Msg {
Expand All @@ -27,9 +27,35 @@ pub struct State {
// TODO handle icon with multiple sizes?
icon_handle: icon::Handle,
icon_theme_path: Option<PathBuf>,
/// Latest Status; defaults to "Active" so an item is visible before its first IconUpdate.
status: String,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an enum that represents each possible state.

click_event: Option<(i32, bool)>,
}

/// An item is hidden only when it explicitly reports Status "Passive"; any other value stays visible.
fn status_is_passive(status: &str) -> bool {
status == "Passive"
}

/// Choose the icon name + pixmap source, preferring the attention icon only
/// while status is "NeedsAttention"; otherwise fall through to the normal icon.
fn pick_icon_source(update: &IconUpdate) -> (String, Option<Vec<Icon>>) {
let attention_active = update.status == "NeedsAttention";
if attention_active && update.attention_name.is_some() {
(
update.attention_name.clone().unwrap_or_default(),
update.attention_pixmap.clone(),
)
} else if attention_active && update.attention_pixmap.is_some() {
(String::new(), update.attention_pixmap.clone())
} else {
(
update.name.clone().unwrap_or_default(),
update.pixmap.clone(),
)
}
}

impl State {
pub fn new(item: StatusNotifierItem) -> (Self, iced::Task<Msg>) {
(
Expand All @@ -41,6 +67,7 @@ impl State {
.prefer_svg(true)
.handle(),
icon_theme_path: None,
status: "Active".to_string(),
click_event: None,
},
iced::Task::none(),
Expand All @@ -64,12 +91,15 @@ impl State {
iced::Task::none()
}
Msg::Icon(update) => {
let icon_name = update.name.unwrap_or_default();
self.icon_theme_path = update.theme_path;
self.status = update.status.clone();
self.icon_theme_path = update.theme_path.clone();

// Prefer the attention icon while asking for attention; otherwise the normal icon.
let (icon_name, pixmap) = pick_icon_source(&update);

// Use the icon pixmap if an icon was not defined by name.
if icon_name.is_empty() {
let icon_pixmap = update.pixmap.and_then(|icons| icons
let icon_pixmap = pixmap.and_then(|icons| icons
.into_iter()
.max_by_key(|i| (i.width, i.height))
.map(|mut i| {
Expand Down Expand Up @@ -153,6 +183,11 @@ impl State {
self.item.name()
}

/// Whether the item is Passive and so must be hidden from the panel.
pub fn is_passive(&self) -> bool {
status_is_passive(&self.status)
}

pub fn icon_handle(&self) -> &icon::Handle {
&self.icon_handle
}
Expand Down Expand Up @@ -281,3 +316,69 @@ fn row_button(content: Vec<cosmic::Element<Msg>>) -> cosmic::widget::Button<Msg>
.width(iced::Length::Fill),
)
}

#[cfg(test)]
mod tests {
use super::*;

fn update(
status: &str,
name: Option<&str>,
attention_name: Option<&str>,
attention_pixmap: Option<Vec<Icon>>,
) -> IconUpdate {
IconUpdate {
name: name.map(str::to_string),
pixmap: None,
theme_path: None,
status: status.to_string(),
attention_name: attention_name.map(str::to_string),
attention_pixmap,
}
}

#[test]
fn is_passive_true_only_for_passive() {
assert!(status_is_passive("Passive"));
for s in ["Active", "NeedsAttention", "", "passive", "garbage"] {
assert!(
!status_is_passive(s),
"{s:?} must NOT be treated as Passive"
);
}
}

#[test]
fn attention_icon_preferred_when_needs_attention() {
let u = update("NeedsAttention", Some("normal"), Some("attn"), None);
let (name, _pixmap) = pick_icon_source(&u);
assert_eq!(name, "attn");
}

#[test]
fn attention_name_ignored_when_not_needs_attention() {
let u = update("Active", Some("normal"), Some("attn"), None);
let (name, _pixmap) = pick_icon_source(&u);
assert_eq!(name, "normal");
}

#[test]
fn needs_attention_without_attention_icon_falls_back() {
let u = update("NeedsAttention", Some("normal"), None, None);
let (name, _pixmap) = pick_icon_source(&u);
assert_eq!(name, "normal");
}

#[test]
fn needs_attention_attention_pixmap_only_forces_pixmap_branch() {
let pm = vec![Icon {
width: 1,
height: 1,
bytes: vec![0, 0, 0, 0],
}];
let u = update("NeedsAttention", Some("normal"), None, Some(pm.clone()));
let (name, pixmap) = pick_icon_source(&u);
assert_eq!(name, "", "empty name forces the pixmap branch");
assert!(pixmap.is_some_and(|p| p.len() == 1));
}
}
Loading
Loading