From 5cb0dbb0c58821798247be7c26cacb03f17641d5 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 12:39:19 +0200 Subject: [PATCH] yoda: dock icon hover magnification (macOS Tahoe-style, phase B v1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 auto-initialized to None via #[derive(Default)]. - New Message::DockItemHover(Option) 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. --- cosmic-app-list/src/app.rs | 111 +++++++++++++++++++++++++++++++------ 1 file changed, 95 insertions(+), 16 deletions(-) diff --git a/cosmic-app-list/src/app.rs b/cosmic-app-list/src/app.rs index 932ffd6c..a6b967f7 100755 --- a/cosmic-app-list/src/app.rs +++ b/cosmic-app-list/src/app.rs @@ -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, locales: Vec, hovered_toplevel: Option, + /// Yoda: which dock icon the pointer is currently over (for hover + /// magnification). None = no dock icon hovered. + hovered_dock_item: Option, overflow_favorites_popup: Option, overflow_active_popup: Option, } @@ -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), Popup(u32, window::Id), Pressed(window::Id), ToplevelListPopup(u32, window::Id), @@ -647,6 +688,17 @@ impl CosmicAppList { .collect::>(); } + /// 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::( 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