Skip to content

Commit

Permalink
Add triple-click support (#1512)
Browse files Browse the repository at this point in the history
  • Loading branch information
trevyn authored Apr 19, 2022
1 parent 4231a53 commit 2932c36
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 🔧
Expand Down
3 changes: 3 additions & 0 deletions egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions egui/src/data/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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.
Expand All @@ -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),
Expand Down
25 changes: 22 additions & 3 deletions egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -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>,
}
Expand All @@ -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![],
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand All @@ -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));
}
}
22 changes: 22 additions & 0 deletions egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -79,6 +82,7 @@ impl std::fmt::Debug for Response {
hovered,
clicked,
double_clicked,
triple_clicked,
dragged,
drag_released,
is_pointer_button_down_on,
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
82 changes: 81 additions & 1 deletion egui/src/widgets/text_edit/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1216,13 +1223,55 @@ 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),
prefer_next_row: false,
}
}

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 {
Expand All @@ -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() {
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion egui_demo_lib/src/apps/demo/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
);

Expand All @@ -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",
Expand Down

0 comments on commit 2932c36

Please sign in to comment.