From 923da384b3f532e50d893046b11854e4c73d51f3 Mon Sep 17 00:00:00 2001 From: trevyn <230691+trevyn@users.noreply.github.com> Date: Mon, 18 Apr 2022 19:22:48 +0300 Subject: [PATCH 1/2] Add triple-click support --- CHANGELOG.md | 1 + egui/src/context.rs | 3 + egui/src/data/output.rs | 4 ++ egui/src/input_state.rs | 25 +++++++- egui/src/response.rs | 22 +++++++ egui/src/widgets/text_edit/builder.rs | 82 ++++++++++++++++++++++++++- egui_demo_lib/src/apps/demo/tests.rs | 5 +- 7 files changed, 137 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afb56641c92..3bc98bab57c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * Added `Frame::outer_margin`. * Added `Painter::hline` and `Painter::vline`. * Added `Link` and `ui.link` ([#1506](https://github.com/emilk/egui/pull/1506)). +* Added triple-click support. Triple-clicking a TextEdit field will select the whole paragraph. ### Changed 🔧 * `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)). diff --git a/egui/src/context.rs b/egui/src/context.rs index aaf28263853..1c3e50b8bba 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -327,6 +327,7 @@ impl Context { hovered, clicked: Default::default(), double_clicked: Default::default(), + triple_clicked: Default::default(), dragged: false, drag_released: false, is_pointer_button_down_on: false, @@ -410,6 +411,8 @@ impl Context { response.clicked[click.button as usize] = clicked; response.double_clicked[click.button as usize] = clicked && click.is_double(); + response.triple_clicked[click.button as usize] = + clicked && click.is_triple(); } } } diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 8f826cd25b1..0688b59c6e4 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -88,6 +88,7 @@ impl PlatformOutput { match event { OutputEvent::Clicked(widget_info) | OutputEvent::DoubleClicked(widget_info) + | OutputEvent::TripleClicked(widget_info) | OutputEvent::FocusGained(widget_info) | OutputEvent::TextSelectionChanged(widget_info) | OutputEvent::ValueChanged(widget_info) => { @@ -291,6 +292,8 @@ pub enum OutputEvent { Clicked(WidgetInfo), // A widget was double-clicked. DoubleClicked(WidgetInfo), + // A widget was triple-clicked. + TripleClicked(WidgetInfo), /// A widget gained keyboard focus (by tab key). FocusGained(WidgetInfo), // Text selection was updated. @@ -304,6 +307,7 @@ impl std::fmt::Debug for OutputEvent { match self { Self::Clicked(wi) => write!(f, "Clicked({:?})", wi), Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi), + Self::TripleClicked(wi) => write!(f, "TripleClicked({:?})", wi), Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi), Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi), Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi), diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 87e59cd49f4..85712a78b13 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -341,7 +341,7 @@ impl InputState { pub(crate) struct Click { pub pos: Pos2, pub button: PointerButton, - /// 1 or 2 (double-click) + /// 1 or 2 (double-click) or 3 (triple-click) pub count: u32, /// Allows you to check for e.g. shift-click pub modifiers: Modifiers, @@ -351,6 +351,9 @@ impl Click { pub fn is_double(&self) -> bool { self.count == 2 } + pub fn is_triple(&self) -> bool { + self.count == 3 + } } #[derive(Clone, Debug, PartialEq)] @@ -421,6 +424,10 @@ pub struct PointerState { /// Used to check for double-clicks. last_click_time: f64, + /// When did the pointer get click two clicks ago? + /// Used to check for triple-clicks. + last_last_click_time: f64, + /// All button events that occurred this frame pub(crate) pointer_events: Vec, } @@ -439,6 +446,7 @@ impl Default for PointerState { press_start_time: None, has_moved_too_much_for_a_click: false, last_click_time: std::f64::NEG_INFINITY, + last_last_click_time: std::f64::NEG_INFINITY, pointer_events: vec![], } } @@ -500,8 +508,17 @@ impl PointerState { let click = if clicked { let double_click = (time - self.last_click_time) < MAX_DOUBLE_CLICK_DELAY; - let count = if double_click { 2 } else { 1 }; - + let triple_click = + (time - self.last_last_click_time) < (MAX_DOUBLE_CLICK_DELAY * 2.0); + let count = if triple_click { + 3 + } else if double_click { + 2 + } else { + 1 + }; + + self.last_last_click_time = self.last_click_time; self.last_click_time = time; Some(Click { @@ -789,6 +806,7 @@ impl PointerState { press_start_time, has_moved_too_much_for_a_click, last_click_time, + last_last_click_time, pointer_events, } = self; @@ -807,6 +825,7 @@ impl PointerState { has_moved_too_much_for_a_click )); ui.label(format!("last_click_time: {:#?}", last_click_time)); + ui.label(format!("last_last_click_time: {:#?}", last_last_click_time)); ui.label(format!("pointer_events: {:?}", pointer_events)); } } diff --git a/egui/src/response.rs b/egui/src/response.rs index b7a76b54c22..21bc01aa2fb 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -47,6 +47,9 @@ pub struct Response { /// The thing was double-clicked. pub(crate) double_clicked: [bool; NUM_POINTER_BUTTONS], + /// The thing was triple-clicked. + pub(crate) triple_clicked: [bool; NUM_POINTER_BUTTONS], + /// The widgets is being dragged pub(crate) dragged: bool, @@ -79,6 +82,7 @@ impl std::fmt::Debug for Response { hovered, clicked, double_clicked, + triple_clicked, dragged, drag_released, is_pointer_button_down_on, @@ -94,6 +98,7 @@ impl std::fmt::Debug for Response { .field("hovered", hovered) .field("clicked", clicked) .field("double_clicked", double_clicked) + .field("triple_clicked", triple_clicked) .field("dragged", dragged) .field("drag_released", drag_released) .field("is_pointer_button_down_on", is_pointer_button_down_on) @@ -138,11 +143,21 @@ impl Response { self.double_clicked[PointerButton::Primary as usize] } + /// Returns true if this widget was triple-clicked this frame by the primary button. + pub fn triple_clicked(&self) -> bool { + self.triple_clicked[PointerButton::Primary as usize] + } + /// Returns true if this widget was double-clicked this frame by the given button. pub fn double_clicked_by(&self, button: PointerButton) -> bool { self.double_clicked[button as usize] } + /// Returns true if this widget was triple-clicked this frame by the given button. + pub fn triple_clicked_by(&self, button: PointerButton) -> bool { + self.triple_clicked[button as usize] + } + /// `true` if there was a click *outside* this widget this frame. pub fn clicked_elsewhere(&self) -> bool { // We do not use self.clicked(), because we want to catch all clicks within our frame, @@ -475,6 +490,8 @@ impl Response { Some(OutputEvent::Clicked(make_info())) } else if self.double_clicked() { Some(OutputEvent::DoubleClicked(make_info())) + } else if self.triple_clicked() { + Some(OutputEvent::TripleClicked(make_info())) } else if self.gained_focus() { Some(OutputEvent::FocusGained(make_info())) } else if self.changed { @@ -536,6 +553,11 @@ impl Response { self.double_clicked[1] || other.double_clicked[1], self.double_clicked[2] || other.double_clicked[2], ], + triple_clicked: [ + self.triple_clicked[0] || other.triple_clicked[0], + self.triple_clicked[1] || other.triple_clicked[1], + self.triple_clicked[2] || other.triple_clicked[2], + ], dragged: self.dragged || other.dragged, drag_released: self.drag_released || other.drag_released, is_pointer_button_down_on: self.is_pointer_button_down_on diff --git a/egui/src/widgets/text_edit/builder.rs b/egui/src/widgets/text_edit/builder.rs index f65c2f603a1..a451d13da77 100644 --- a/egui/src/widgets/text_edit/builder.rs +++ b/egui/src/widgets/text_edit/builder.rs @@ -430,7 +430,6 @@ impl<'t> TextEdit<'t> { ui.output().mutable_text_under_cursor = true; } - // TODO: triple-click to select whole paragraph // TODO: drag selected text to either move or clone (ctrl on windows, alt on mac) let singleline_offset = vec2(state.singleline_offset, 0.0); let cursor_at_pointer = @@ -459,6 +458,14 @@ impl<'t> TextEdit<'t> { primary: galley.from_ccursor(ccursor_range.primary), secondary: galley.from_ccursor(ccursor_range.secondary), })); + } else if response.triple_clicked() { + // Select line: + let center = cursor_at_pointer; + let ccursor_range = select_line_at(text.as_ref(), center.ccursor); + state.set_cursor_range(Some(CursorRange { + primary: galley.from_ccursor(ccursor_range.primary), + secondary: galley.from_ccursor(ccursor_range.secondary), + })); } else if allow_drag_to_select { if response.hovered() && ui.input().pointer.any_pressed() { ui.memory().request_focus(id); @@ -1216,6 +1223,41 @@ fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange { } } +fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange { + if ccursor.index == 0 { + CCursorRange::two(ccursor, ccursor_next_line(text, ccursor)) + } else { + let it = text.chars(); + let mut it = it.skip(ccursor.index - 1); + if let Some(char_before_cursor) = it.next() { + if let Some(char_after_cursor) = it.next() { + if is_line_char(char_before_cursor) && is_line_char(char_after_cursor) { + let min = ccursor_previous_line(text, ccursor + 1); + let max = ccursor_next_line(text, min); + CCursorRange::two(min, max) + } else if is_line_char(char_before_cursor) { + let min = ccursor_previous_line(text, ccursor); + let max = ccursor_next_line(text, min); + CCursorRange::two(min, max) + } else if is_line_char(char_after_cursor) { + let max = ccursor_next_line(text, ccursor); + CCursorRange::two(ccursor, max) + } else { + let min = ccursor_previous_line(text, ccursor); + let max = ccursor_next_line(text, ccursor); + CCursorRange::two(min, max) + } + } else { + let min = ccursor_previous_line(text, ccursor); + CCursorRange::two(min, ccursor) + } + } else { + let max = ccursor_next_line(text, ccursor); + CCursorRange::two(ccursor, max) + } + } +} + fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor { CCursor { index: next_word_boundary_char_index(text.chars(), ccursor.index), @@ -1223,6 +1265,13 @@ fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor { } } +fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor { + CCursor { + index: next_line_boundary_char_index(text.chars(), ccursor.index), + prefer_next_row: false, + } +} + fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor { let num_chars = text.chars().count(); CCursor { @@ -1232,6 +1281,15 @@ fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor { } } +fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor { + let num_chars = text.chars().count(); + CCursor { + index: num_chars + - next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index), + prefer_next_row: true, + } +} + fn next_word_boundary_char_index(it: impl Iterator, mut index: usize) -> usize { let mut it = it.skip(index); if let Some(_first) = it.next() { @@ -1250,10 +1308,32 @@ fn next_word_boundary_char_index(it: impl Iterator, mut index: usiz index } +fn next_line_boundary_char_index(it: impl Iterator, mut index: usize) -> usize { + let mut it = it.skip(index); + if let Some(_first) = it.next() { + index += 1; + + if let Some(second) = it.next() { + index += 1; + for next in it { + if is_line_char(next) != is_line_char(second) { + break; + } + index += 1; + } + } + } + index +} + fn is_word_char(c: char) -> bool { c.is_ascii_alphanumeric() || c == '_' } +fn is_line_char(c: char) -> bool { + c != '\r' && c != '\n' +} + /// Accepts and returns character offset (NOT byte offset!). fn find_line_start(text: &str, current_index: CCursor) -> CCursor { // We know that new lines, '\n', are a single byte char, but we have to diff --git a/egui_demo_lib/src/apps/demo/tests.rs b/egui_demo_lib/src/apps/demo/tests.rs index f6335cce7c9..d5d6d7999e0 100644 --- a/egui_demo_lib/src/apps/demo/tests.rs +++ b/egui_demo_lib/src/apps/demo/tests.rs @@ -332,7 +332,7 @@ impl super::View for InputTest { }); let response = ui.add( - egui::Button::new("Click, double-click or drag me with any mouse button") + egui::Button::new("Click, double-click, triple-click or drag me with any mouse button") .sense(egui::Sense::click_and_drag()), ); @@ -348,6 +348,9 @@ impl super::View for InputTest { if response.double_clicked_by(button) { new_info += &format!("Double-clicked by {:?} button\n", button); } + if response.triple_clicked_by(button) { + new_info += &format!("Triple-clicked by {:?} button\n", button); + } if response.dragged_by(button) { new_info += &format!( "Dragged by {:?} button, delta: {:?}\n", From 922dc1f9095f29e03e24fc26d7d9d6814dd0968c Mon Sep 17 00:00:00 2001 From: trevyn <230691+trevyn@users.noreply.github.com> Date: Tue, 19 Apr 2022 14:15:58 +0300 Subject: [PATCH 2/2] `!is_linebreak` instead of `is_line_char` --- egui/src/widgets/text_edit/builder.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/egui/src/widgets/text_edit/builder.rs b/egui/src/widgets/text_edit/builder.rs index a451d13da77..f92a6f91025 100644 --- a/egui/src/widgets/text_edit/builder.rs +++ b/egui/src/widgets/text_edit/builder.rs @@ -1231,15 +1231,15 @@ fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange { let mut it = it.skip(ccursor.index - 1); if let Some(char_before_cursor) = it.next() { if let Some(char_after_cursor) = it.next() { - if is_line_char(char_before_cursor) && is_line_char(char_after_cursor) { + if (!is_linebreak(char_before_cursor)) && (!is_linebreak(char_after_cursor)) { let min = ccursor_previous_line(text, ccursor + 1); let max = ccursor_next_line(text, min); CCursorRange::two(min, max) - } else if is_line_char(char_before_cursor) { + } else if !is_linebreak(char_before_cursor) { let min = ccursor_previous_line(text, ccursor); let max = ccursor_next_line(text, min); CCursorRange::two(min, max) - } else if is_line_char(char_after_cursor) { + } else if !is_linebreak(char_after_cursor) { let max = ccursor_next_line(text, ccursor); CCursorRange::two(ccursor, max) } else { @@ -1316,7 +1316,7 @@ fn next_line_boundary_char_index(it: impl Iterator, mut index: usiz if let Some(second) = it.next() { index += 1; for next in it { - if is_line_char(next) != is_line_char(second) { + if is_linebreak(next) != is_linebreak(second) { break; } index += 1; @@ -1330,8 +1330,8 @@ fn is_word_char(c: char) -> bool { c.is_ascii_alphanumeric() || c == '_' } -fn is_line_char(c: char) -> bool { - c != '\r' && c != '\n' +fn is_linebreak(c: char) -> bool { + c == '\r' || c == '\n' } /// Accepts and returns character offset (NOT byte offset!).