Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add triple-click support #1512

Merged
merged 3 commits into from
Apr 19, 2022
Merged
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
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