feat(segmented_button): on_double_click + internal tab reorder (squashed)
Squash of 2 yoda commits: -108441efsegmented_button: add on_double_click callback -a322516fsegmented_button: fix internal tab reorder end-to-end
This commit is contained in:
parent
ea17ada931
commit
87510782ae
1 changed files with 61 additions and 6 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue