From ad8b4b899ec9e01715061297f54d84acad142a1c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 14:44:37 +0100 Subject: [PATCH 1/3] Added `count` field to `Press` --- crates/bevy_picking/src/events.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 252bc896f4dc3..609614f01d10a 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -293,6 +293,8 @@ pub struct Press { pub button: PointerButton, /// Information about the picking intersection. pub hit: HitData, + /// Number of consecutive presses, starting at `1`. + pub count: u8, } /// Fires when a pointer button is released over the [target entity](EntityEvent::event_target). From 811ba58f344e1ba48f9b5365c4ab43c30fd94dfa Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 15:18:13 +0100 Subject: [PATCH 2/3] Added multi-press count to `Press` entity events. This is to allow click then press and drag actions. Text inputs no long observe `Click` events, instead they check `count` on press. --- crates/bevy_picking/src/events.rs | 19 +++++-- crates/bevy_ui_widgets/src/text_input.rs | 71 ++++-------------------- 2 files changed, 27 insertions(+), 63 deletions(-) diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 609614f01d10a..18dcf72e31be2 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -478,8 +478,10 @@ pub struct Scroll { pub struct PointerButtonState { /// Stores the press location and start time for each button currently being pressed by the pointer. pub pressing: EntityHashMap<(Location, Instant, HitData)>, + /// Stores the latest press time and count for each pressed entity. + pub press_counts: EntityHashMap<(Instant, u8)>, /// Stores the latest click time and count for each clicked entity. - pub clicking: EntityHashMap<(Instant, u8)>, + pub click_counts: EntityHashMap<(Instant, u8)>, /// Stores the starting and current locations for each entity currently being dragged by the pointer. pub dragging: EntityHashMap, /// Stores the hit data for each entity currently being dragged over by the pointer. @@ -922,6 +924,9 @@ pub fn pointer_events( match action { PointerAction::Press(button) => { let state = pointer_state.get_mut(pointer_id, button); + state + .press_counts + .retain(|_, (last_press, _)| now - *last_press <= MULTI_CLICK_DURATION); // If it's a press, emit a Pressed event and mark the hovered entities as pressed for (hovered_entity, hit) in hover_map @@ -929,12 +934,18 @@ pub fn pointer_events( .iter() .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) { + let count = state + .press_counts + .get(&hovered_entity) + .map_or(1, |(_, count)| count.saturating_add(1)); + state.press_counts.insert(hovered_entity, (now, count)); let pressed_event = Pointer::new( pointer_id, location.clone(), Press { button, hit: hit.clone(), + count, }, hovered_entity, ); @@ -949,7 +960,7 @@ pub fn pointer_events( PointerAction::Release(button) => { let state = pointer_state.get_mut(pointer_id, button); state - .clicking + .click_counts .retain(|_, (last_click, _)| now - *last_click <= MULTI_CLICK_DURATION); // Emit Click and Release events on all the previously hovered entities. @@ -961,10 +972,10 @@ pub fn pointer_events( // If this pointer previously pressed the hovered entity, emit a Click event if let Some((_, press_instant, _)) = state.pressing.get(&hovered_entity) { let count = state - .clicking + .click_counts .get(&hovered_entity) .map_or(1, |(_, count)| count.saturating_add(1)); - state.clicking.insert(hovered_entity, (now, count)); + state.click_counts.insert(hovered_entity, (now, count)); let click_event = Pointer::new( pointer_id, location.clone(), diff --git a/crates/bevy_ui_widgets/src/text_input.rs b/crates/bevy_ui_widgets/src/text_input.rs index fd7a2e6df181c..f980692d15c27 100644 --- a/crates/bevy_ui_widgets/src/text_input.rs +++ b/crates/bevy_ui_widgets/src/text_input.rs @@ -15,7 +15,7 @@ use bevy_input_focus::{ FocusCause, FocusGained, FocusLost, FocusedInput, InputFocus, InputFocusSystems, }; use bevy_math::Vec2; -use bevy_picking::events::{Click, Drag, Pointer, Press, Release}; +use bevy_picking::events::{Drag, Pointer, Press, Release}; use bevy_picking::pointer::PointerButton; use bevy_text::{EditableText, PreeditCursor, TextEdit}; use bevy_ui::widget::{scroll_editable_text, update_editable_text_layout, TextScroll}; @@ -190,69 +190,23 @@ fn on_pointer_press( return; }; - editable_text - .pending_edits - .push(if keys.pressed(Key::Shift) { - TextEdit::ShiftClickExtension - } else { - TextEdit::MoveToPoint - }(local_pos)); - - input_focus.set(press.entity, FocusCause::Pressed); - - press.propagate(false); -} - -/// System that processes pointer click events into text edit actions for [`EditableText`] widgets. -/// -/// `Click`s follow `Press`, so multi-click `TextEdit`s are queued after those from the corresponding `Press`. -/// -/// Note that this does not immediately apply the edits; they are queued up in [`EditableText::pending_edits`], -/// and then applied later by the [`apply_text_edits`](`bevy_text::apply_text_edits`) system. -fn on_pointer_click( - mut click: On>, - mut text_input_query: Query<( - &mut EditableText, - &ComputedNode, - &ComputedUiRenderTargetInfo, - &UiGlobalTransform, - &TextScroll, - )>, - ui_scale: Res, -) { - if click.button != PointerButton::Primary { - return; - } - - let Ok((mut editable_text, node, target, transform, text_scroll)) = - text_input_query.get_mut(click.entity) - else { - return; - }; - - if editable_text.is_composing() { - // The IME is active; all input needs to be routed there, including pointer multi-clicks. - return; - } - - let Some(local_pos) = transform.try_inverse().map(|inverse| { - inverse - .transform_point2(click.pointer_location.position * target.scale_factor() / ui_scale.0) - - node.content_box().min - + text_scroll.0 - }) else { - return; - }; - - match click.count { + match press.count { 1 => { - // No special processing required for single clicks. Presses set the cursor position and are handled by `on_pointer_press`. + editable_text + .pending_edits + .push(if keys.pressed(Key::Shift) { + TextEdit::ShiftClickExtension + } else { + TextEdit::MoveToPoint + }(local_pos)); } 2 => editable_text.queue_edit(TextEdit::SelectWordAtPoint(local_pos)), _ => editable_text.queue_edit(TextEdit::SelectAll), } - click.propagate(false); + input_focus.set(press.entity, FocusCause::Pressed); + + press.propagate(false); } /// System that processes pointer drag events into text edit actions for [`EditableText`] widgets. @@ -540,7 +494,6 @@ impl Plugin for EditableTextInputPlugin { .add_observer(on_pointer_press) .add_observer(on_focus_lost_clear_ime) .add_observer(on_focus_select_all) - .add_observer(on_pointer_click) .add_systems( PreUpdate, ( From c8efd3633e5684c35cb129f451f3566f7d5f291f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 15:46:47 +0100 Subject: [PATCH 3/3] Removed the extra map tracking press counts. We only need one counter if we update the click count on presses. --- crates/bevy_picking/src/events.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 18dcf72e31be2..4c7164be3f56f 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -478,10 +478,8 @@ pub struct Scroll { pub struct PointerButtonState { /// Stores the press location and start time for each button currently being pressed by the pointer. pub pressing: EntityHashMap<(Location, Instant, HitData)>, - /// Stores the latest press time and count for each pressed entity. - pub press_counts: EntityHashMap<(Instant, u8)>, /// Stores the latest click time and count for each clicked entity. - pub click_counts: EntityHashMap<(Instant, u8)>, + pub clicking: EntityHashMap<(Instant, u8)>, /// Stores the starting and current locations for each entity currently being dragged by the pointer. pub dragging: EntityHashMap, /// Stores the hit data for each entity currently being dragged over by the pointer. @@ -925,8 +923,8 @@ pub fn pointer_events( PointerAction::Press(button) => { let state = pointer_state.get_mut(pointer_id, button); state - .press_counts - .retain(|_, (last_press, _)| now - *last_press <= MULTI_CLICK_DURATION); + .clicking + .retain(|_, (last_click, _)| now - *last_click <= MULTI_CLICK_DURATION); // If it's a press, emit a Pressed event and mark the hovered entities as pressed for (hovered_entity, hit) in hover_map @@ -935,10 +933,10 @@ pub fn pointer_events( .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) { let count = state - .press_counts + .clicking .get(&hovered_entity) .map_or(1, |(_, count)| count.saturating_add(1)); - state.press_counts.insert(hovered_entity, (now, count)); + state.clicking.insert(hovered_entity, (now, count)); let pressed_event = Pointer::new( pointer_id, location.clone(), @@ -960,7 +958,7 @@ pub fn pointer_events( PointerAction::Release(button) => { let state = pointer_state.get_mut(pointer_id, button); state - .click_counts + .clicking .retain(|_, (last_click, _)| now - *last_click <= MULTI_CLICK_DURATION); // Emit Click and Release events on all the previously hovered entities. @@ -972,10 +970,10 @@ pub fn pointer_events( // If this pointer previously pressed the hovered entity, emit a Click event if let Some((_, press_instant, _)) = state.pressing.get(&hovered_entity) { let count = state - .click_counts + .clicking .get(&hovered_entity) - .map_or(1, |(_, count)| count.saturating_add(1)); - state.click_counts.insert(hovered_entity, (now, count)); + .map_or(1, |(_, count)| *count); + state.clicking.insert(hovered_entity, (now, count)); let click_event = Pointer::new( pointer_id, location.clone(),