yoda: dock icon hover magnification (macOS Tahoe-style, phase B v1)

First pass at the signature macOS Dock effect — the icon under the
pointer grows, adjacent icons stay at base size. Full fisheye (smooth
bell-curve scaling on neighbors) can be a later iteration.

Changes in cosmic-app-list/src/app.rs:
- CosmicAppList gains a hovered_dock_item: Option<DockItemId>
  auto-initialized to None via #[derive(Default)].
- New Message::DockItemHover(Option<DockItemId>) handled in update()
  by just writing the field; view() then reads it to decide scale.
- DockItem::as_icon gains an icon_scale: f32 parameter. Inside it the
  cosmic_icon width/height = (base_icon_size * icon_scale) clamped
  to u16; indicator dot and other surrounding layout stay at base
  size so only the icon visually bulges.
- New App::icon_scale_for(id) helper: 1.3 if Some(id) == hovered,
  1.0 otherwise. Single place to tune the magnification factor.
- The two main dock rows (favorites + filtered_active_list) wrap
  their rendered applet_tooltip in widget::mouse_area with
  on_enter(DockItemHover(Some(id))) / on_exit(DockItemHover(None))
  and call icon_scale_for before rendering.
- The three remaining as_icon call sites (DnD preview, favorites
  overflow popup, active overflow popup) pass icon_scale = 1.0 —
  hover magnification on those surfaces would look jittery and isn't
  needed anyway.

Build: cargo build --release -p cosmic-app-list (≈ 7s). Binary
installed at /usr/local/bin/cosmic-app-list, backup kept as
.pre-magnification.
This commit is contained in:
Lionel DARNIS 2026-04-24 12:39:19 +02:00 committed by Lionel DARNIS
parent 03c302d138
commit d090e60370

View file

@ -187,6 +187,11 @@ impl DockItem {
dot_border_radius: [f32; 4],
window_id: window::Id,
filter: Option<&dyn Fn(&ToplevelInfo) -> bool>,
// Yoda: multiplier on the computed icon size (1.0 = default,
// >1.0 = magnified e.g. on hover for the macOS Tahoe effect).
// Applied to the icon's rendered width/height only — indicator
// dot and surrounding layout stay at base size.
icon_scale: f32,
) -> Element<'_, Message> {
let Self {
toplevels,
@ -205,17 +210,35 @@ impl DockItem {
};
let toplevel_count = filtered_toplevels.len();
// Cairo-like : pastille plus petite + atténuée quand toutes les fenêtres
// de cette app sont minimisées.
let all_minimized = toplevel_count > 0
&& filtered_toplevels
.iter()
.all(|(info, _)| info.state.contains(&State::Minimized));
let app_icon = AppletIconData::new(applet);
// Yoda: scaled icon size for hover magnification. Clamped so
// tiny floats don't round to 0 and huge ones stay within u16.
let scaled_icon_size = ((f32::from(app_icon.icon_size) * icon_scale).round() as i32)
.clamp(1, u16::MAX as i32) as u16;
let cosmic_icon = cosmic::widget::icon(
fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default()).as_cosmic_icon(),
)
// sets the preferred icon size variant
.size(128)
.width(app_icon.icon_size.into())
.height(app_icon.icon_size.into());
.width(scaled_icon_size.into())
.height(scaled_icon_size.into());
let indicator = {
// Padding réduit quand minimisée → pastille plus petite.
let effective_radius = if all_minimized {
(app_icon.dot_radius * 0.55).max(1.0)
} else {
app_icon.dot_radius
};
let container = if toplevel_count <= 1 {
vertical_space().height(Length::Fixed(0.0))
} else {
@ -229,22 +252,34 @@ impl DockItem {
}
}
.apply(container)
.padding(app_icon.dot_radius);
.padding(effective_radius);
if toplevel_count == 0 {
container
} else {
container.class(theme::Container::custom(move |theme| container::Style {
background: if is_focused {
Some(Background::Color(theme.cosmic().accent_color().into()))
container.class(theme::Container::custom(move |theme| {
let cosmic = theme.cosmic();
let accent: iced::Color = cosmic.accent_color().into();
let on_bg: iced::Color = cosmic.on_bg_color().into();
// Teinte neutre atténuée quand toutes les fenêtres sont minimisées.
let muted = iced::Color { a: 0.45, ..on_bg };
let fill = if all_minimized {
muted
} else if is_focused {
accent
} else {
Some(Background::Color(theme.cosmic().on_bg_color().into()))
},
border: Border {
radius: dot_border_radius.into(),
on_bg
};
container::Style {
background: Some(Background::Color(fill)),
border: Border {
radius: dot_border_radius.into(),
..Default::default()
},
..Default::default()
},
..Default::default()
}
}))
}
};
@ -374,6 +409,9 @@ struct CosmicAppList {
output_list: FxHashMap<WlOutput, OutputInfo>,
locales: Vec<String>,
hovered_toplevel: Option<ExtForeignToplevelHandleV1>,
/// Yoda: which dock icon the pointer is currently over (for hover
/// magnification). None = no dock icon hovered.
hovered_dock_item: Option<DockItemId>,
overflow_favorites_popup: Option<window::Id>,
overflow_active_popup: Option<window::Id>,
}
@ -389,6 +427,9 @@ enum Message {
Wayland(WaylandUpdate),
PinApp(u32),
UnpinApp(u32),
/// Yoda: pointer entered (Some) or left (None) a dock icon — drives
/// the macOS Tahoe-style hover magnification effect.
DockItemHover(Option<DockItemId>),
Popup(u32, window::Id),
Pressed(window::Id),
ToplevelListPopup(u32, window::Id),
@ -647,6 +688,17 @@ impl CosmicAppList {
.collect::<Vec<_>>();
}
/// Yoda: macOS-Tahoe-style hover magnification. Returns the icon
/// size multiplier for a given dock item — 1.3× when the pointer
/// is over it, 1.0× otherwise. Called from the view() icon builders.
fn icon_scale_for(&self, id: &DockItemId) -> f32 {
if self.hovered_dock_item.as_ref() == Some(id) {
1.3
} else {
1.0
}
}
fn is_on_current_monitor_and_workspace(&self, toplevel_info: &ToplevelInfo) -> bool {
use cosmic_app_list_config::ToplevelFilter;
@ -1582,6 +1634,9 @@ impl cosmic::Application for CosmicAppList {
Message::GpuRequest(gpus) => {
self.gpus = gpus;
}
Message::DockItemHover(id) => {
self.hovered_dock_item = id;
}
Message::OpenActive => {
let create_new = self.overflow_active_popup.is_none();
let mut cmds = vec![self.close_popups()];
@ -1763,7 +1818,9 @@ impl cosmic::Application for CosmicAppList {
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
self.core
let dock_id = dock_item.id;
let icon_scale = self.icon_scale_for(&DockItemId::from(dock_id));
let tooltip = self.core
.applet
.applet_tooltip::<Message>(
dock_item.as_icon(
@ -1776,6 +1833,7 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
icon_scale,
),
dock_item
.desktop_info
@ -1785,7 +1843,10 @@ impl cosmic::Application for CosmicAppList {
self.popup.is_some(),
Message::Surface,
None,
)
);
cosmic::widget::mouse_area(tooltip)
.on_enter(Message::DockItemHover(Some(DockItemId::from(dock_id))))
.on_exit(Message::DockItemHover(None))
.into()
})
.collect();
@ -1836,6 +1897,10 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
// Yoda: no magnification on DnD-preview icons — these
// float around as the user drags, so static 1.0 is
// less visually confusing.
1.0,
),
);
} else if self.is_listening_for_dnd && self.pinned_list.is_empty() {
@ -1875,8 +1940,10 @@ impl cosmic::Application for CosmicAppList {
.iter()
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
let dock_id = dock_item.id;
let icon_scale = self.icon_scale_for(&DockItemId::from(dock_id));
self.core
let tooltip = self.core
.applet
.applet_tooltip(
dock_item.as_icon(
@ -1889,6 +1956,7 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
icon_scale,
),
dock_item
.desktop_info
@ -1898,7 +1966,10 @@ impl cosmic::Application for CosmicAppList {
self.popup.is_some(),
Message::Surface,
None,
)
);
cosmic::widget::mouse_area(tooltip)
.on_enter(Message::DockItemHover(Some(DockItemId::from(dock_id))))
.on_exit(Message::DockItemHover(None))
.into()
})
.collect();
@ -2313,6 +2384,10 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
id,
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
// Yoda: icons in the overflow popup are
// already smaller-grid — keep them at 1.0
// so the popup doesn't reshuffle on hover.
1.0,
),
dock_item
.desktop_info
@ -2421,6 +2496,10 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
id,
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
// Yoda: popup icons stay at 1.0 (hover
// magnification is applied to the main
// dock row only).
1.0,
),
dock_item
.desktop_info