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.
|
/// Move one step down (toward the end) inside the enabled toolbar list.
|
||||||
ToolbarMoveDown(ToolbarAction),
|
ToolbarMoveDown(ToolbarAction),
|
||||||
ToolbarReset,
|
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),
|
SetTypeToSearch(TypeToSearch),
|
||||||
SystemThemeModeChange,
|
SystemThemeModeChange,
|
||||||
Size(window::Id, Size),
|
Size(window::Id, Size),
|
||||||
|
|
@ -846,6 +850,11 @@ pub struct App {
|
||||||
nav_bar_context_id: segmented_button::Entity,
|
nav_bar_context_id: segmented_button::Entity,
|
||||||
nav_model: segmented_button::SingleSelectModel,
|
nav_model: segmented_button::SingleSelectModel,
|
||||||
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
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>,
|
config_handler: Option<cosmic_config::Config>,
|
||||||
state_handler: Option<cosmic_config::Config>,
|
state_handler: Option<cosmic_config::Config>,
|
||||||
config: Config,
|
config: Config,
|
||||||
|
|
@ -1800,6 +1809,7 @@ impl App {
|
||||||
|
|
||||||
fn update_config(&mut self) -> Task<Message> {
|
fn update_config(&mut self) -> Task<Message> {
|
||||||
self.update_nav_model();
|
self.update_nav_model();
|
||||||
|
self.rebuild_toolbar_model();
|
||||||
// Tabs are collected first to placate the borrowck
|
// Tabs are collected first to placate the borrowck
|
||||||
let tabs: Box<[_]> = self.tab_model.iter().collect();
|
let tabs: Box<[_]> = self.tab_model.iter().collect();
|
||||||
// Update main conf and each tab with the new config
|
// Update main conf and each tab with the new config
|
||||||
|
|
@ -1813,6 +1823,49 @@ impl App {
|
||||||
Task::batch(commands)
|
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> {
|
fn update_desktop(&mut self) -> Task<Message> {
|
||||||
let needs_reload: Box<[_]> = (self.tab_model.iter())
|
let needs_reload: Box<[_]> = (self.tab_model.iter())
|
||||||
.filter_map(|entity| {
|
.filter_map(|entity| {
|
||||||
|
|
@ -2688,6 +2741,7 @@ impl Application for App {
|
||||||
nav_bar_context_id: segmented_button::Entity::null(),
|
nav_bar_context_id: segmented_button::Entity::null(),
|
||||||
nav_model: segmented_button::ModelBuilder::default().build(),
|
nav_model: segmented_button::ModelBuilder::default().build(),
|
||||||
tab_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,
|
config_handler: flags.config_handler,
|
||||||
state_handler: flags.state_handler,
|
state_handler: flags.state_handler,
|
||||||
config: flags.config,
|
config: flags.config,
|
||||||
|
|
@ -4661,6 +4715,24 @@ impl Application for App {
|
||||||
config_set!(toolbar, default_toolbar());
|
config_set!(toolbar, default_toolbar());
|
||||||
return self.update_config();
|
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) => {
|
Message::SetTypeToSearch(type_to_search) => {
|
||||||
config_set!(type_to_search, type_to_search);
|
config_set!(type_to_search, type_to_search);
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
|
|
@ -6805,52 +6877,23 @@ impl Application for App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yoda phase 3: Dolphin-style quick actions toolbar. Items are
|
// Yoda phase 3: Dolphin-style quick actions toolbar via
|
||||||
// rendered from self.config.toolbar (Vec<ToolbarAction>) — the user
|
// segmented_button::horizontal — the same widget that powers the
|
||||||
// picks the set AND the order via direct drag-drop on the toolbar.
|
// tab bar, so its built-in drag reorder works reliably (unlike the
|
||||||
// Short click = action (shared Action::message dispatch); drag past
|
// generic dnd_source+dnd_destination pairing we tried earlier).
|
||||||
// the default 8px threshold = reorder (ToolbarReorder message).
|
// 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() {
|
if !self.config.toolbar.is_empty() {
|
||||||
use cosmic::iced::clipboard::dnd::DndAction as DndAct;
|
let toolbar = widget::segmented_button::horizontal(&self.toolbar_model)
|
||||||
let clipboard_has = self.clipboard_has_content();
|
.button_height(32)
|
||||||
let buttons: Vec<Element<_>> = self
|
.button_spacing(space_xxs)
|
||||||
.config
|
.minimum_button_width(36)
|
||||||
.toolbar
|
.maximum_button_width(36)
|
||||||
.iter()
|
.enable_tab_drag(String::from("x-cosmic-files/toolbar-dnd"))
|
||||||
.map(|a| {
|
.on_reorder(Message::ToolbarTabReorder)
|
||||||
let action = *a;
|
.tab_drag_threshold(8.)
|
||||||
let (icon, label, msg) = toolbar_action_ui(action);
|
.on_activate(Message::ToolbarTabActivate);
|
||||||
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);
|
|
||||||
tab_column = tab_column.push(
|
tab_column = tab_column.push(
|
||||||
widget::container(toolbar)
|
widget::container(toolbar)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue