feat(app-list): implement workspace and configured-output filtering

Implement optional filtering of apps by active workspace or configured
output. The filter_top_levels config option accepts None (no filtering),
ActiveWorkspace (workspace-only), or ConfiguredOutput (monitor and
workspace filtering).

Signed-off-by: Tobias Schaffner <tobiasschaffner87@outlook.com>
This commit is contained in:
Tobias Schaffner 2026-01-01 17:00:34 +01:00
parent 2852f3cc16
commit 8ea267abfe

View file

@ -182,6 +182,7 @@ impl DockItem {
is_focused: bool, is_focused: bool,
dot_border_radius: [f32; 4], dot_border_radius: [f32; 4],
window_id: window::Id, window_id: window::Id,
filter: Option<&dyn Fn(&ToplevelInfo) -> bool>,
) -> Element<'_, Message> { ) -> Element<'_, Message> {
let Self { let Self {
toplevels, toplevels,
@ -190,6 +191,16 @@ impl DockItem {
.. ..
} = self; } = self;
let filtered_toplevels: Vec<_> = if let Some(filter_fn) = filter {
toplevels
.iter()
.filter(|(info, _)| filter_fn(info))
.collect()
} else {
toplevels.iter().collect()
};
let toplevel_count = filtered_toplevels.len();
let app_icon = AppletIconData::new(applet); let app_icon = AppletIconData::new(applet);
let cosmic_icon = fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default()) let cosmic_icon = fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default())
@ -200,7 +211,7 @@ impl DockItem {
.height(app_icon.icon_size.into()); .height(app_icon.icon_size.into());
let indicator = { let indicator = {
let container = if toplevels.len() <= 1 { let container = if toplevel_count <= 1 {
vertical_space().height(Length::Fixed(0.0)) vertical_space().height(Length::Fixed(0.0))
} else { } else {
match applet.anchor { match applet.anchor {
@ -215,7 +226,7 @@ impl DockItem {
.apply(container) .apply(container)
.padding(app_icon.dot_radius); .padding(app_icon.dot_radius);
if toplevels.is_empty() { if toplevel_count == 0 {
container container
} else { } else {
container.class(theme::Container::custom(move |theme| container::Style { container.class(theme::Container::custom(move |theme| container::Style {
@ -272,10 +283,10 @@ impl DockItem {
let icon_button: Element<_> = if interaction_enabled { let icon_button: Element<_> = if interaction_enabled {
mouse_area( mouse_area(
icon_button icon_button
.on_press_maybe(if toplevels.is_empty() { .on_press_maybe(if toplevel_count == 0 {
launch_on_preferred_gpu(desktop_info, gpus) launch_on_preferred_gpu(desktop_info, gpus)
} else if toplevels.len() == 1 { } else if toplevel_count == 1 {
toplevels filtered_toplevels
.first() .first()
.map(|t| Message::Toggle(t.0.foreign_toplevel.clone())) .map(|t| Message::Toggle(t.0.foreign_toplevel.clone()))
} else { } else {
@ -635,6 +646,32 @@ impl CosmicAppList {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
} }
fn is_on_current_monitor_and_workspace(&self, toplevel_info: &ToplevelInfo) -> bool {
use cosmic_app_list_config::ToplevelFilter;
let on_active_workspace = self.active_workspaces.is_empty()
|| toplevel_info.workspace.is_empty()
|| self.active_workspaces
.iter()
.any(|workspace| toplevel_info.workspace.contains(workspace));
match &self.config.filter_top_levels {
None => true,
Some(ToplevelFilter::ActiveWorkspace) => on_active_workspace,
Some(ToplevelFilter::ConfiguredOutput) => {
let on_active_output = self
.output_list
.iter()
.find(|(_, info)| info.name.as_ref() == Some(&self.core.applet.output_name))
.map_or(true, |(active_output, _)| {
toplevel_info.output.iter().any(|output| output == active_output)
});
on_active_output && on_active_workspace
}
}
}
// Update pinned items using the cached desktop entries as a source. // Update pinned items using the cached desktop entries as a source.
fn update_pinned_list(&mut self) { fn update_pinned_list(&mut self) {
self.pinned_list = find_desktop_entries(&self.desktop_entries, &self.config.favorites) self.pinned_list = find_desktop_entries(&self.desktop_entries, &self.config.favorites)
@ -1715,6 +1752,12 @@ impl cosmic::Application for CosmicAppList {
.iter() .iter()
.rev() .rev()
.map(|dock_item| { .map(|dock_item| {
let filtered_is_focused = dock_item
.toplevels
.iter()
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
self.core self.core
.applet .applet
.applet_tooltip::<Message>( .applet_tooltip::<Message>(
@ -1724,12 +1767,10 @@ impl cosmic::Application for CosmicAppList {
self.popup.is_none(), self.popup.is_none(),
self.config.enable_drag_source, self.config.enable_drag_source,
self.gpus.as_deref(), self.gpus.as_deref(),
dock_item filtered_is_focused,
.toplevels
.iter()
.any(|y| focused_item.contains(&y.0.foreign_toplevel)),
dot_radius, dot_radius,
self.core.main_window_id().unwrap(), self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
), ),
dock_item dock_item
.desktop_info .desktop_info
@ -1772,6 +1813,12 @@ impl cosmic::Application for CosmicAppList {
.as_ref() .as_ref()
.and_then(|o| o.dock_item.as_ref().map(|item| (item, o.preview_index))) .and_then(|o| o.dock_item.as_ref().map(|item| (item, o.preview_index)))
{ {
let filtered_is_focused = item
.toplevels
.iter()
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
favorites.insert( favorites.insert(
index.min(favorites.len()), index.min(favorites.len()),
item.as_icon( item.as_icon(
@ -1780,11 +1827,10 @@ impl cosmic::Application for CosmicAppList {
false, false,
self.config.enable_drag_source, self.config.enable_drag_source,
self.gpus.as_deref(), self.gpus.as_deref(),
item.toplevels filtered_is_focused,
.iter()
.any(|y| focused_item.contains(&y.0.foreign_toplevel)),
dot_radius, dot_radius,
self.core.main_window_id().unwrap(), self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
), ),
); );
} else if self.is_listening_for_dnd && self.pinned_list.is_empty() { } else if self.is_listening_for_dnd && self.pinned_list.is_empty() {
@ -1799,9 +1845,19 @@ impl cosmic::Application for CosmicAppList {
); );
} }
let filtered_active_list: Vec<_> = self
.active_list
.iter()
.filter(|dock_item| {
dock_item.toplevels.iter().any(|(toplevel_info, _)| {
self.is_on_current_monitor_and_workspace(toplevel_info)
})
})
.collect();
let mut active: Vec<_> = let mut active: Vec<_> =
self.active_list[..active_popup_cutoff.map_or(self.active_list.len(), |n| { filtered_active_list[..active_popup_cutoff.map_or(filtered_active_list.len(), |n| {
if n < self.active_list.len() { if n < filtered_active_list.len() {
n.saturating_sub(1) n.saturating_sub(1)
} else { } else {
n n
@ -1809,6 +1865,12 @@ impl cosmic::Application for CosmicAppList {
})] })]
.iter() .iter()
.map(|dock_item| { .map(|dock_item| {
let filtered_is_focused = dock_item
.toplevels
.iter()
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
self.core self.core
.applet .applet
.applet_tooltip( .applet_tooltip(
@ -1818,12 +1880,10 @@ impl cosmic::Application for CosmicAppList {
self.popup.is_none(), self.popup.is_none(),
self.config.enable_drag_source, self.config.enable_drag_source,
self.gpus.as_deref(), self.gpus.as_deref(),
dock_item filtered_is_focused,
.toplevels
.iter()
.any(|y| focused_item.contains(&y.0.foreign_toplevel)),
dot_radius, dot_radius,
self.core.main_window_id().unwrap(), self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
), ),
dock_item dock_item
.desktop_info .desktop_info
@ -1838,7 +1898,7 @@ impl cosmic::Application for CosmicAppList {
}) })
.collect(); .collect();
if active_popup_cutoff.is_some_and(|n| n < self.active_list.len()) { if active_popup_cutoff.is_some_and(|n| n < filtered_active_list.len()) {
// button to show more active // button to show more active
let icon = match self.core.applet.anchor { let icon = match self.core.applet.anchor {
PanelAnchor::Bottom => "go-up-symbolic", PanelAnchor::Bottom => "go-up-symbolic",
@ -1987,14 +2047,7 @@ impl cosmic::Application for CosmicAppList {
.. ..
}) = self.popup.as_ref().filter(|p| id == p.id) }) = self.popup.as_ref().filter(|p| id == p.id)
{ {
let ( let (dock_item, is_pinned) = match self.pinned_list.iter().find(|i| i.id == *id) {
DockItem {
toplevels,
desktop_info,
..
},
is_pinned,
) = match self.pinned_list.iter().find(|i| i.id == *id) {
Some(e) => (e, true), Some(e) => (e, true),
None => match self.active_list.iter().find(|i| i.id == *id) { None => match self.active_list.iter().find(|i| i.id == *id) {
Some(e) => (e, false), Some(e) => (e, false),
@ -2002,6 +2055,18 @@ impl cosmic::Application for CosmicAppList {
}, },
}; };
// Filter toplevels to only show windows on current monitor and workspace
let filtered_toplevels: Vec<_> = dock_item
.toplevels
.iter()
.filter(|(toplevel_info, _)| {
self.is_on_current_monitor_and_workspace(toplevel_info)
})
.collect();
let toplevels = &filtered_toplevels;
let desktop_info = &dock_item.desktop_info;
match popup_type { match popup_type {
PopupType::RightClickMenu => { PopupType::RightClickMenu => {
fn menu_button<'a, Message: Clone + 'a>( fn menu_button<'a, Message: Clone + 'a>(
@ -2203,19 +2268,34 @@ impl cosmic::Application for CosmicAppList {
let focused_item = self.currently_active_toplevel(); let focused_item = self.currently_active_toplevel();
let dot_radius = theme.cosmic().radius_xs(); let dot_radius = theme.cosmic().radius_xs();
// show the overflow popup for active list
let active: Vec<_> = self let filtered_active_list: Vec<_> = self
.active_list .active_list
.iter()
.filter(|dock_item| {
dock_item.toplevels.iter().any(|(toplevel_info, _)| {
self.is_on_current_monitor_and_workspace(toplevel_info)
})
})
.collect();
let active: Vec<_> = filtered_active_list
.iter() .iter()
.rev() .rev()
.take(active_popup_cutoff.map_or(self.active_list.len(), |n| { .take(active_popup_cutoff.map_or(filtered_active_list.len(), |n| {
if n < self.active_list.len() { if n < filtered_active_list.len() {
self.active_list.len() - n + 1 filtered_active_list.len() - n + 1
} else { } else {
0 0
} }
})) }))
.map(|dock_item| { .map(|dock_item| {
let filtered_is_focused = dock_item
.toplevels
.iter()
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
self.core self.core
.applet .applet
.applet_tooltip( .applet_tooltip(
@ -2225,12 +2305,10 @@ impl cosmic::Application for CosmicAppList {
self.popup.is_none(), self.popup.is_none(),
self.config.enable_drag_source, self.config.enable_drag_source,
self.gpus.as_deref(), self.gpus.as_deref(),
dock_item filtered_is_focused,
.toplevels
.iter()
.any(|y| focused_item.contains(&y.0.foreign_toplevel)),
dot_radius, dot_radius,
id, id,
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
), ),
dock_item dock_item
.desktop_info .desktop_info
@ -2290,6 +2368,7 @@ impl cosmic::Application for CosmicAppList {
let focused_item = self.currently_active_toplevel(); let focused_item = self.currently_active_toplevel();
let dot_radius = theme.cosmic().radius_xs(); let dot_radius = theme.cosmic().radius_xs();
// show the overflow popup for favorites list // show the overflow popup for favorites list
let mut favorite_to_remove = if let Some(cutoff) = favorite_popup_cutoff { let mut favorite_to_remove = if let Some(cutoff) = favorite_popup_cutoff {
if cutoff < self.pinned_list.len() { if cutoff < self.pinned_list.len() {
self.pinned_list.len() - cutoff + 1 self.pinned_list.len() - cutoff + 1
@ -2319,6 +2398,12 @@ impl cosmic::Application for CosmicAppList {
.iter() .iter()
.rev() .rev()
.map(|dock_item| { .map(|dock_item| {
let filtered_is_focused = dock_item
.toplevels
.iter()
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
self.core self.core
.applet .applet
.applet_tooltip( .applet_tooltip(
@ -2328,12 +2413,10 @@ impl cosmic::Application for CosmicAppList {
self.popup.is_none(), self.popup.is_none(),
self.config.enable_drag_source, self.config.enable_drag_source,
self.gpus.as_deref(), self.gpus.as_deref(),
dock_item filtered_is_focused,
.toplevels
.iter()
.any(|y| focused_item.contains(&y.0.foreign_toplevel)),
dot_radius, dot_radius,
id, id,
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
), ),
dock_item dock_item
.desktop_info .desktop_info