feat(segmented_button): on_double_click + internal tab reorder (squashed)

Squash of 2 yoda commits:
- 108441ef segmented_button: add on_double_click callback
- a322516f segmented_button: fix internal tab reorder end-to-end
This commit is contained in:
Lionel DARNIS 2026-05-25 13:01:53 +02:00
parent ea17ada931
commit 87510782ae

View file

@ -175,6 +175,10 @@ where
pub(super) on_context: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
#[setters(skip)]
pub(super) on_middle_press: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
/// Emits the ID of the item that was double-clicked with the left button.
/// Fires in addition to `on_activate` (which keeps firing on each click).
#[setters(skip)]
pub(super) on_double_click: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
#[setters(skip)]
pub(super) on_dnd_drop:
Option<Box<dyn Fn(Entity, Vec<u8>, String, DndAction) -> Message + 'static>>,
@ -234,6 +238,7 @@ where
on_close: None,
on_context: None,
on_middle_press: None,
on_double_click: None,
on_dnd_drop: None,
on_dnd_enter: None,
on_dnd_leave: None,
@ -356,6 +361,16 @@ where
self
}
/// Emitted when a tab is double-clicked with the left mouse button.
/// Fires in addition to `on_activate`, which keeps firing on each click.
pub fn on_double_click<T>(mut self, on_double_click: T) -> Self
where
T: Fn(Entity) -> Message + 'static,
{
self.on_double_click = Some(Box::new(on_double_click));
self
}
/// Enable drag-and-drop support for tabs using the provided payload builder.
pub fn enable_tab_drag(mut self, mime: String) -> Self {
self.tab_drag = Some(TabDragSource::new(mime));
@ -393,11 +408,12 @@ where
{
return None;
}
let position = state
.drop_hint
.filter(|hint| hint.entity == target)
.map(|hint| InsertPosition::from(hint.side))
.unwrap_or_else(|| self.default_insert_position(dragged, target));
// Always use positional swap (Konsole/Firefox/Chrome semantics):
// dropping onto any part of a different tab swaps it with the dragged
// tab. drop_hint.side-based Before/After is counter-intuitive: dropping
// A (pos 0) on the left half of B (pos 1) resolves to "Before B" which,
// after removing A, lands at pos 0 — so the tab appears not to move.
let position = self.default_insert_position(dragged, target);
Some(ReorderEvent {
dragged,
target,
@ -914,6 +930,7 @@ where
hovered: Default::default(),
known_length: Default::default(),
middle_clicked: Default::default(),
last_click: None,
internal_layout: Default::default(),
context_cursor: Point::default(),
show_context: Default::default(),
@ -1187,7 +1204,14 @@ where
.dnd_state
.drag_offer
.as_ref()
.is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
.is_some_and(|offer| offer.selected_action.contains(DndAction::Move))
// Self-drop fallback: some compositors (cosmic-comp
// observed) do not emit OfferEvent::SelectedAction for
// internal drags, leaving selected_action empty.
// dragging_tab is only set by start_tab_drag on this
// same widget, so this covers the self-drop case
// safely; mime and on_reorder are checked below.
|| state.dragging_tab.is_some();
let pending_reorder = if allow_reorder
&& self.on_reorder.is_some()
&& self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
@ -1385,6 +1409,33 @@ where
state.set_focused();
state.focused_item = Item::Tab(key);
state.pressed_item = None;
// Double-click detection on the same entity
// within 400 ms — fires after on_activate so
// the tab is already focused when the handler
// runs.
if let Some(on_double_click) =
self.on_double_click.as_ref()
{
let now = Instant::now();
let is_double = match state.last_click {
Some((prev, t)) => {
prev == key
&& now.duration_since(t)
< Duration::from_millis(400)
}
None => false,
};
state.last_click = if is_double {
None
} else {
Some((key, now))
};
if is_double {
shell.publish(on_double_click(key));
}
}
shell.capture_event();
return;
}
@ -2391,6 +2442,9 @@ pub struct LocalState {
hovered: Item,
/// The ID of the button that was middle-clicked, but not yet released.
middle_clicked: Option<Item>,
/// Entity and timestamp of the most recent left-click activation, used
/// to detect double-clicks on the same tab.
last_click: Option<(Entity, Instant)>,
/// Last known length of the model.
pub(super) known_length: usize,
/// Dimensions of internal buttons when shrinking
@ -2536,6 +2590,7 @@ mod tests {
hovered: Item::default(),
known_length: 0,
middle_clicked: None,
last_click: None,
internal_layout: Vec::new(),
context_cursor: Point::ORIGIN,
show_context: None,