Add triple-click support (#1512)

This commit is contained in:
trevyn 2022-04-19 16:14:55 +03:00 committed by GitHub
parent 4231a5303b
commit 2932c36238
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 136 additions and 4 deletions

View file

@ -16,6 +16,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 ([#1512](https://github.com/emilk/egui/pull/1512)).
* Added `Plot::x_grid_spacer` and `Plot::y_grid_spacer` for custom grid spacing ([#1180](https://github.com/emilk/egui/pull/1180)).
### Changed 🔧

View file

@ -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();
}
}
}

View file

@ -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),

View file

@ -349,7 +349,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,
@ -359,6 +359,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)]
@ -429,6 +432,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<PointerEvent>,
}
@ -447,6 +454,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![],
}
}
@ -508,8 +516,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 {
@ -797,6 +814,7 @@ impl PointerState {
press_start_time,
has_moved_too_much_for_a_click,
last_click_time,
last_last_click_time,
pointer_events,
} = self;
@ -815,6 +833,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));
}
}

View file

@ -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

View file

@ -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_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_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_linebreak(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<Item = char>, 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<Item = char>, mut index: usiz
index
}
fn next_line_boundary_char_index(it: impl Iterator<Item = char>, 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_linebreak(next) != is_linebreak(second) {
break;
}
index += 1;
}
}
}
index
}
fn is_word_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
fn is_linebreak(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

View file

@ -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",