Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions crates/egui/src/input_state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,14 @@ pub struct PointerState {
/// This could also be the trigger point for a long-touch.
pub(crate) started_decidedly_dragging: bool,

/// The click count that a click ending the latest press would have:
/// 2 for the second press of a double-click, 3 for the third press of a triple-click, etc.
///
/// Unlike [`Click::count`] this is known already at the start of the press,
/// which is what e.g. double-click-and-drag text selection needs.
#[cfg_attr(feature = "serde", serde(default))]
press_click_count: usize,

/// Where did the last click originate?
/// `None` if no mouse click occurred.
last_click_pos: Option<Pos2>,
Expand Down Expand Up @@ -1084,6 +1092,7 @@ impl Default for PointerState {
press_start_time: None,
has_moved_too_much_for_a_click: false,
started_decidedly_dragging: false,
press_click_count: 0,
last_click_pos: None,
last_click_time: f64::NEG_INFINITY,
last_last_click_time: f64::NEG_INFINITY,
Expand Down Expand Up @@ -1151,6 +1160,26 @@ impl PointerState {
self.press_origin = Some(pos);
self.press_start_time = Some(time);
self.has_moved_too_much_for_a_click = false;

// Would a click ending this press be a double- or triple-click?
// Uses the same heuristics as the click counting on release.
let close_to_last_click = self.last_click_pos.is_some_and(|last_pos| {
last_pos.distance_sq(pos)
< self.options.max_click_dist * self.options.max_click_dist
});
self.press_click_count = if close_to_last_click
&& (time - self.last_last_click_time)
< (self.options.max_double_click_delay * 2.0)
{
3
} else if close_to_last_click
&& (time - self.last_click_time) < self.options.max_double_click_delay
{
2
} else {
1
};

self.pointer_events.push(PointerEvent::Pressed {
position: pos,
button,
Expand Down Expand Up @@ -1363,6 +1392,16 @@ impl PointerState {
(self.time - self.last_click_time) as f32
}

/// The click count that a click ending the latest press would have:
/// 2 for the second press of a double-click, 3 for the third press of a triple-click, etc.
///
/// Unlike [`Self::button_double_clicked`] this is known already at the start of the press,
/// which is what e.g. double-click-and-drag text selection needs.
#[inline(always)]
pub(crate) fn press_click_count(&self) -> usize {
self.press_click_count
}

/// Was any pointer button pressed (`!down -> down`) this frame?
///
/// This can sometimes return `true` even if `any_down() == false`
Expand Down Expand Up @@ -1669,6 +1708,7 @@ impl PointerState {
press_start_time,
has_moved_too_much_for_a_click,
started_decidedly_dragging,
press_click_count,
last_click_pos,
last_click_time,
last_last_click_time,
Expand All @@ -1695,6 +1735,7 @@ impl PointerState {
ui.label(format!(
"started_decidedly_dragging: {started_decidedly_dragging}"
));
ui.label(format!("press_click_count: {press_click_count}"));
ui.label(format!("last_click_pos: {last_click_pos:#?}"));
ui.label(format!("last_click_time: {last_click_time:#?}"));
ui.label(format!("last_last_click_time: {last_last_click_time:#?}"));
Expand Down
96 changes: 94 additions & 2 deletions crates/egui/src/text_selection/label_text_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{

use super::{
TextCursorState,
text_cursor_state::cursor_rect,
text_cursor_state::{SelectGranularity, cursor_rect, extend_granular_select, select_unit_at},
visuals::{RowVertexIndices, paint_text_selection},
};

Expand Down Expand Up @@ -61,6 +61,22 @@ impl std::fmt::Debug for WidgetTextCursor {
}
}

/// A label selection that started with a double- or triple-click,
/// remembered so that dragging extends it by whole words or lines.
///
/// Both ends of the anchor word/line are always in the same widget,
/// but the drag may extend the selection into other widgets.
#[derive(Clone, Copy, Debug)]
struct GranularDrag {
granularity: SelectGranularity,

/// Start of the word/line that was initially clicked.
anchor_min: WidgetTextCursor,

/// End of the word/line that was initially clicked.
anchor_max: WidgetTextCursor,
}

#[derive(Clone, Copy, Debug)]
struct CurrentSelection {
/// The selection is in this layer.
Expand Down Expand Up @@ -101,6 +117,10 @@ struct ViewportLabelSelectionState {
/// Are we in drag-to-select state?
is_dragging: bool,

/// Set if the current selection started with a double- or triple-click,
/// so that dragging extends the selection by whole words or lines.
granular_drag: Option<GranularDrag>,

/// Have we reached the widget containing the primary selection?
has_reached_primary: bool,

Expand All @@ -125,6 +145,7 @@ impl Default for ViewportLabelSelectionState {
selection_bbox_this_frame: Rect::NOTHING,
any_hovered: Default::default(),
is_dragging: Default::default(),
granular_drag: Default::default(),
has_reached_primary: Default::default(),
has_reached_secondary: Default::default(),
text_to_copy: Default::default(),
Expand Down Expand Up @@ -201,6 +222,10 @@ impl ViewportLabelSelectionState {
if ui.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
// Maybe a new selection is about to begin, but the old one is over:
// state.selection = None; // TODO(emilk): this makes sense, but doesn't work as expected.

// If this press is a double- or triple-click on a label,
// `on_label` will set this again later this pass:
self.granular_drag = None;
}

self.selection_bbox_last_frame = self.selection_bbox_this_frame;
Expand Down Expand Up @@ -373,7 +398,53 @@ impl ViewportLabelSelectionState {

let new_primary = if response.contains_pointer() {
// Dragging into this widget - easy case:
Some(galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2()))
let cursor_at_pointer =
galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2());

if let Some(granular) = &self.granular_drag {
// The selection started with a double- or triple-click,
// so extend it by whole words or lines:
if response.id == granular.anchor_min.widget_id {
// We are in the same widget as the anchor word/line:
let anchor = CCursorRange::two(
granular.anchor_min.ccursor,
granular.anchor_max.ccursor,
);
let range = extend_granular_select(
granular.granularity,
anchor,
galley.text(),
cursor_at_pointer,
);
selection.secondary = WidgetTextCursor::new(
response.id,
range.secondary,
global_from_galley,
galley,
);
Some(range.primary)
} else {
// The drag has left the anchor's widget.
// Are we before or after the anchor? Decide by screen position:
let after_anchor = granular.anchor_max.pos.y < pointer_pos.y
|| (granular.anchor_min.pos.y <= pointer_pos.y
&& granular.anchor_max.pos.x <= pointer_pos.x);

let unit =
select_unit_at(granular.granularity, galley.text(), cursor_at_pointer);
let [unit_min, unit_max] = unit.sorted_cursors();

if after_anchor {
selection.secondary = granular.anchor_min;
Some(unit_max)
} else {
selection.secondary = granular.anchor_max;
Some(unit_min)
}
}
} else {
Some(cursor_at_pointer)
}
} else if is_in_same_column
&& !self.has_reached_primary
&& selection.primary.pos.y <= selection.secondary.pos.y
Expand Down Expand Up @@ -560,6 +631,27 @@ impl ViewportLabelSelectionState {
// Actual drag-to-select happens elsewhere.
let dragged = false;
cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley, dragged);

// If this was a double- or triple-click, remember the clicked word/line
// so that dragging extends the selection by that granularity:
if let Some(granular) = cursor_state.granular_drag() {
let [anchor_min, anchor_max] = granular.anchor.sorted_cursors();
self.granular_drag = Some(GranularDrag {
granularity: granular.granularity,
anchor_min: WidgetTextCursor::new(
response.id,
anchor_min,
global_from_galley,
galley,
),
anchor_max: WidgetTextCursor::new(
response.id,
anchor_max,
global_from_galley,
galley,
),
});
}
}

if let Some(mut cursor_range) = cursor_state.range(galley) {
Expand Down
Loading
Loading