From 94c3e6c5512cad63412af98e9351eb36792a8871 Mon Sep 17 00:00:00 2001 From: leyoda Date: Fri, 24 Apr 2026 11:03:05 +0200 Subject: [PATCH] yoda: toolbar as segmented_button for working drag reorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generic dnd_source+dnd_destination pairing didn't reliably fire on intra-window reorders in this setup, while segmented_button's built-in drag (same primitive powering tab_bar, which does work) is proven. Switched the toolbar rendering to segmented_button::horizontal with drag enabled — each segment carries its ToolbarAction as data. App state: - new toolbar_model: segmented_button::Model - rebuild_toolbar_model() mirrors config.toolbar into the model on every update_config (including the initial app.update_config at startup) - sync_toolbar_config_from_model() is the reverse: walk the model's entity order after a reorder, write the new Vec directly via config.set_toolbar (without calling update_config so we don't rebuild the model and wipe the reorder the user just did) Messages: - ToolbarTabActivate(Entity): look up action via model.data(), clear the model's active selection (segmented_button single-select would keep the last click highlighted; we don't want that for action buttons), dispatch the action's message. - ToolbarTabReorder(ReorderEvent): model.reorder then sync. View: - replaces the row-of-dnd-wrapped-icon-buttons with segmented_button::horizontal(&self.toolbar_model) .enable_tab_drag("x-cosmic-files/toolbar-dnd") .on_reorder(...) .on_activate(...) - fixed 36-px square buttons so it still looks toolbar-y rather than stretched pill-segmented-control Kept: Settings panel ↑↓/add/remove UI (no regression). Removed: dnd_source/dnd_destination wrappers from the toolbar (but the ToolbarActionPayload + MIME constant remain in case Settings DnD gets unstuck later). --- src/app.rs | 133 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 45 deletions(-) diff --git a/src/app.rs b/src/app.rs index c742244..ef2c8e2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -556,6 +556,10 @@ pub enum Message { /// Move one step down (toward the end) inside the enabled toolbar list. ToolbarMoveDown(ToolbarAction), ToolbarReset, + /// Click on a toolbar button (via segmented_button activation). + ToolbarTabActivate(segmented_button::Entity), + /// Drag-reorder inside the toolbar (via segmented_button drag). + ToolbarTabReorder(segmented_button::ReorderEvent), SetTypeToSearch(TypeToSearch), SystemThemeModeChange, Size(window::Id, Size), @@ -846,6 +850,11 @@ pub struct App { nav_bar_context_id: segmented_button::Entity, nav_model: segmented_button::SingleSelectModel, tab_model: segmented_button::Model, + /// Yoda phase 3: segmented_button model mirroring config.toolbar so the + /// toolbar row gets free drag-reorder + click activation (same widget + /// that powers the tab bar, its reorder is proven to work in this + /// setup unlike the generic dnd_source/dnd_destination wrappers). + toolbar_model: segmented_button::Model, config_handler: Option, state_handler: Option, config: Config, @@ -1800,6 +1809,7 @@ impl App { fn update_config(&mut self) -> Task { self.update_nav_model(); + self.rebuild_toolbar_model(); // Tabs are collected first to placate the borrowck let tabs: Box<[_]> = self.tab_model.iter().collect(); // Update main conf and each tab with the new config @@ -1813,6 +1823,49 @@ impl App { Task::batch(commands) } + /// Yoda phase 3: rebuild `toolbar_model` so it matches `config.toolbar`. + /// Called on init and on every config update. Each entity carries the + /// associated `ToolbarAction` as data so click/reorder handlers can + /// round-trip Entity → ToolbarAction without maintaining a side map. + fn rebuild_toolbar_model(&mut self) { + self.toolbar_model.clear(); + for action in self.config.toolbar.iter().copied() { + let (icon_name, label, _msg) = toolbar_action_ui(action); + self.toolbar_model + .insert() + .icon(widget::icon::from_name(icon_name).size(16).icon()) + .text(label) + .data::(action); + } + } + + /// Yoda phase 3: after a drag-reorder, sync `config.toolbar` with the + /// new entity order in `toolbar_model`. Inlines what `config_set!` + /// would do (the macro lives inside update()). + fn sync_toolbar_config_from_model(&mut self) -> Task { + let new_toolbar: Vec = self + .toolbar_model + .iter() + .filter_map(|e| self.toolbar_model.data::(e).copied()) + .collect(); + if new_toolbar == self.config.toolbar { + return Task::none(); + } + match self.config_handler.as_ref() { + Some(h) => { + if let Err(err) = self.config.set_toolbar(h, new_toolbar) { + log::warn!("failed to save toolbar order: {err}"); + } + } + None => self.config.toolbar = new_toolbar, + } + // Don't call update_config() — that would rebuild the + // toolbar_model from config and undo the reorder the user just + // made. The model already has the new order; config is just + // catching up for persistence. + Task::none() + } + fn update_desktop(&mut self) -> Task { let needs_reload: Box<[_]> = (self.tab_model.iter()) .filter_map(|entity| { @@ -2688,6 +2741,7 @@ impl Application for App { nav_bar_context_id: segmented_button::Entity::null(), nav_model: segmented_button::ModelBuilder::default().build(), tab_model: segmented_button::ModelBuilder::default().build(), + toolbar_model: segmented_button::ModelBuilder::default().build(), config_handler: flags.config_handler, state_handler: flags.state_handler, config: flags.config, @@ -4661,6 +4715,24 @@ impl Application for App { config_set!(toolbar, default_toolbar()); return self.update_config(); } + Message::ToolbarTabActivate(entity) => { + // Dispatch the stored ToolbarAction's message, then clear + // the "active" selection so the button doesn't stay + // highlighted after a click (we use segmented_button for + // layout/drag but toolbar buttons are action-firing, not + // a mutual-exclusive choice). + let action = self.toolbar_model.data::(entity).copied(); + self.toolbar_model.deactivate(); + if let Some(action) = action { + let (_, _, msg) = toolbar_action_ui(action); + return self.update(msg); + } + return Task::none(); + } + Message::ToolbarTabReorder(event) => { + let _ = self.toolbar_model.reorder(event.dragged, event.target, event.position); + return self.sync_toolbar_config_from_model(); + } Message::SetTypeToSearch(type_to_search) => { config_set!(type_to_search, type_to_search); return self.update_config(); @@ -6805,52 +6877,23 @@ impl Application for App { ); } - // Yoda phase 3: Dolphin-style quick actions toolbar. Items are - // rendered from self.config.toolbar (Vec) — the user - // picks the set AND the order via direct drag-drop on the toolbar. - // Short click = action (shared Action::message dispatch); drag past - // the default 8px threshold = reorder (ToolbarReorder message). + // Yoda phase 3: Dolphin-style quick actions toolbar via + // segmented_button::horizontal — the same widget that powers the + // tab bar, so its built-in drag reorder works reliably (unlike the + // generic dnd_source+dnd_destination pairing we tried earlier). + // Short click = action (ToolbarTabActivate → dispatch the stored + // ToolbarAction's message). Drag past threshold = reorder + // (ToolbarTabReorder → model.reorder + sync to config). if !self.config.toolbar.is_empty() { - use cosmic::iced::clipboard::dnd::DndAction as DndAct; - let clipboard_has = self.clipboard_has_content(); - let buttons: Vec> = self - .config - .toolbar - .iter() - .map(|a| { - let action = *a; - let (icon, label, msg) = toolbar_action_ui(action); - let enabled = !matches!(action, ToolbarAction::Paste) || clipboard_has; - let btn = widget::button::icon(widget::icon::from_name(icon).size(16)); - let btn = if enabled { btn.on_press(msg) } else { btn }; - let tooltip = widget::tooltip( - btn, - widget::text::body(label), - widget::tooltip::Position::Bottom, - ); - let source = widget::dnd_source::(tooltip) - .drag_content(move || ToolbarActionPayload(action.to_u8())); - widget::dnd_destination( - source, - vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)], - ) - .data_received_for::( - move |payload: Option| { - match payload.and_then(|p| ToolbarAction::from_u8(p.0)) { - Some(src) if src != action => { - Message::ToolbarReorder { src, target: action } - } - _ => Message::ToolbarReorder { src: action, target: action }, - } - }, - ) - .action(DndAct::Move) - .into() - }) - .collect(); - let toolbar = widget::row::with_children(buttons) - .spacing(space_xxs) - .align_y(Alignment::Center); + let toolbar = widget::segmented_button::horizontal(&self.toolbar_model) + .button_height(32) + .button_spacing(space_xxs) + .minimum_button_width(36) + .maximum_button_width(36) + .enable_tab_drag(String::from("x-cosmic-files/toolbar-dnd")) + .on_reorder(Message::ToolbarTabReorder) + .tab_drag_threshold(8.) + .on_activate(Message::ToolbarTabActivate); tab_column = tab_column.push( widget::container(toolbar) .width(Length::Fill)