segmented_button: add on_double_click callback

Fires in addition to on_activate when the same entity is left-clicked
twice within 400 ms. Lets applications bind quick actions (e.g. rename
a tab) without blocking the normal single-click activation path.

- New Widget::on_double_click builder mirroring on_activate/on_close.
- last_click field on LocalState for timestamp tracking.
- Detection branch in the mouse handler after on_activate fires, so the
  target entity is already focused when the callback runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lionel DARNIS 2026-04-21 21:26:35 +02:00
parent 1d98eee6de
commit 108441ef61

View file

@ -173,6 +173,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>>,
@ -232,6 +236,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,
@ -354,6 +359,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));
@ -912,6 +927,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(),
@ -1383,6 +1399,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;
}
@ -2387,6 +2430,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
@ -2532,6 +2578,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,