yoda: toolbar as segmented_button for working drag reorder
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<SingleSelect>
- 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<ToolbarAction>
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).
This commit is contained in:
parent
af843d204d
commit
94c3e6c551
1 changed files with 88 additions and 45 deletions
133
src/app.rs
133
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<segmented_button::SingleSelect>,
|
||||
/// 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<segmented_button::SingleSelect>,
|
||||
config_handler: Option<cosmic_config::Config>,
|
||||
state_handler: Option<cosmic_config::Config>,
|
||||
config: Config,
|
||||
|
|
@ -1800,6 +1809,7 @@ impl App {
|
|||
|
||||
fn update_config(&mut self) -> Task<Message> {
|
||||
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::<ToolbarAction>(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<Message> {
|
||||
let new_toolbar: Vec<ToolbarAction> = self
|
||||
.toolbar_model
|
||||
.iter()
|
||||
.filter_map(|e| self.toolbar_model.data::<ToolbarAction>(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<Message> {
|
||||
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::<ToolbarAction>(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<ToolbarAction>) — 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<Element<_>> = 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::<Message, ToolbarActionPayload>(tooltip)
|
||||
.drag_content(move || ToolbarActionPayload(action.to_u8()));
|
||||
widget::dnd_destination(
|
||||
source,
|
||||
vec![std::borrow::Cow::Borrowed(TOOLBAR_MIME)],
|
||||
)
|
||||
.data_received_for::<ToolbarActionPayload>(
|
||||
move |payload: Option<ToolbarActionPayload>| {
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue