Add triple-click support (#1512)
This commit is contained in:
parent
4231a5303b
commit
2932c36238
7 changed files with 136 additions and 4 deletions
|
@ -16,6 +16,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
||||||
* Added `Frame::outer_margin`.
|
* Added `Frame::outer_margin`.
|
||||||
* Added `Painter::hline` and `Painter::vline`.
|
* Added `Painter::hline` and `Painter::vline`.
|
||||||
* Added `Link` and `ui.link` ([#1506](https://github.com/emilk/egui/pull/1506)).
|
* 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)).
|
* Added `Plot::x_grid_spacer` and `Plot::y_grid_spacer` for custom grid spacing ([#1180](https://github.com/emilk/egui/pull/1180)).
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
|
|
|
@ -327,6 +327,7 @@ impl Context {
|
||||||
hovered,
|
hovered,
|
||||||
clicked: Default::default(),
|
clicked: Default::default(),
|
||||||
double_clicked: Default::default(),
|
double_clicked: Default::default(),
|
||||||
|
triple_clicked: Default::default(),
|
||||||
dragged: false,
|
dragged: false,
|
||||||
drag_released: false,
|
drag_released: false,
|
||||||
is_pointer_button_down_on: false,
|
is_pointer_button_down_on: false,
|
||||||
|
@ -410,6 +411,8 @@ impl Context {
|
||||||
response.clicked[click.button as usize] = clicked;
|
response.clicked[click.button as usize] = clicked;
|
||||||
response.double_clicked[click.button as usize] =
|
response.double_clicked[click.button as usize] =
|
||||||
clicked && click.is_double();
|
clicked && click.is_double();
|
||||||
|
response.triple_clicked[click.button as usize] =
|
||||||
|
clicked && click.is_triple();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ impl PlatformOutput {
|
||||||
match event {
|
match event {
|
||||||
OutputEvent::Clicked(widget_info)
|
OutputEvent::Clicked(widget_info)
|
||||||
| OutputEvent::DoubleClicked(widget_info)
|
| OutputEvent::DoubleClicked(widget_info)
|
||||||
|
| OutputEvent::TripleClicked(widget_info)
|
||||||
| OutputEvent::FocusGained(widget_info)
|
| OutputEvent::FocusGained(widget_info)
|
||||||
| OutputEvent::TextSelectionChanged(widget_info)
|
| OutputEvent::TextSelectionChanged(widget_info)
|
||||||
| OutputEvent::ValueChanged(widget_info) => {
|
| OutputEvent::ValueChanged(widget_info) => {
|
||||||
|
@ -291,6 +292,8 @@ pub enum OutputEvent {
|
||||||
Clicked(WidgetInfo),
|
Clicked(WidgetInfo),
|
||||||
// A widget was double-clicked.
|
// A widget was double-clicked.
|
||||||
DoubleClicked(WidgetInfo),
|
DoubleClicked(WidgetInfo),
|
||||||
|
// A widget was triple-clicked.
|
||||||
|
TripleClicked(WidgetInfo),
|
||||||
/// A widget gained keyboard focus (by tab key).
|
/// A widget gained keyboard focus (by tab key).
|
||||||
FocusGained(WidgetInfo),
|
FocusGained(WidgetInfo),
|
||||||
// Text selection was updated.
|
// Text selection was updated.
|
||||||
|
@ -304,6 +307,7 @@ impl std::fmt::Debug for OutputEvent {
|
||||||
match self {
|
match self {
|
||||||
Self::Clicked(wi) => write!(f, "Clicked({:?})", wi),
|
Self::Clicked(wi) => write!(f, "Clicked({:?})", wi),
|
||||||
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi),
|
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi),
|
||||||
|
Self::TripleClicked(wi) => write!(f, "TripleClicked({:?})", wi),
|
||||||
Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi),
|
Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi),
|
||||||
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi),
|
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi),
|
||||||
Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi),
|
Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi),
|
||||||
|
|
|
@ -349,7 +349,7 @@ impl InputState {
|
||||||
pub(crate) struct Click {
|
pub(crate) struct Click {
|
||||||
pub pos: Pos2,
|
pub pos: Pos2,
|
||||||
pub button: PointerButton,
|
pub button: PointerButton,
|
||||||
/// 1 or 2 (double-click)
|
/// 1 or 2 (double-click) or 3 (triple-click)
|
||||||
pub count: u32,
|
pub count: u32,
|
||||||
/// Allows you to check for e.g. shift-click
|
/// Allows you to check for e.g. shift-click
|
||||||
pub modifiers: Modifiers,
|
pub modifiers: Modifiers,
|
||||||
|
@ -359,6 +359,9 @@ impl Click {
|
||||||
pub fn is_double(&self) -> bool {
|
pub fn is_double(&self) -> bool {
|
||||||
self.count == 2
|
self.count == 2
|
||||||
}
|
}
|
||||||
|
pub fn is_triple(&self) -> bool {
|
||||||
|
self.count == 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
@ -429,6 +432,10 @@ pub struct PointerState {
|
||||||
/// Used to check for double-clicks.
|
/// Used to check for double-clicks.
|
||||||
last_click_time: f64,
|
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
|
/// All button events that occurred this frame
|
||||||
pub(crate) pointer_events: Vec<PointerEvent>,
|
pub(crate) pointer_events: Vec<PointerEvent>,
|
||||||
}
|
}
|
||||||
|
@ -447,6 +454,7 @@ impl Default for PointerState {
|
||||||
press_start_time: None,
|
press_start_time: None,
|
||||||
has_moved_too_much_for_a_click: false,
|
has_moved_too_much_for_a_click: false,
|
||||||
last_click_time: std::f64::NEG_INFINITY,
|
last_click_time: std::f64::NEG_INFINITY,
|
||||||
|
last_last_click_time: std::f64::NEG_INFINITY,
|
||||||
pointer_events: vec![],
|
pointer_events: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -508,8 +516,17 @@ impl PointerState {
|
||||||
let click = if clicked {
|
let click = if clicked {
|
||||||
let double_click =
|
let double_click =
|
||||||
(time - self.last_click_time) < MAX_DOUBLE_CLICK_DELAY;
|
(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;
|
self.last_click_time = time;
|
||||||
|
|
||||||
Some(Click {
|
Some(Click {
|
||||||
|
@ -797,6 +814,7 @@ impl PointerState {
|
||||||
press_start_time,
|
press_start_time,
|
||||||
has_moved_too_much_for_a_click,
|
has_moved_too_much_for_a_click,
|
||||||
last_click_time,
|
last_click_time,
|
||||||
|
last_last_click_time,
|
||||||
pointer_events,
|
pointer_events,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
@ -815,6 +833,7 @@ impl PointerState {
|
||||||
has_moved_too_much_for_a_click
|
has_moved_too_much_for_a_click
|
||||||
));
|
));
|
||||||
ui.label(format!("last_click_time: {:#?}", last_click_time));
|
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));
|
ui.label(format!("pointer_events: {:?}", pointer_events));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,9 @@ pub struct Response {
|
||||||
/// The thing was double-clicked.
|
/// The thing was double-clicked.
|
||||||
pub(crate) double_clicked: [bool; NUM_POINTER_BUTTONS],
|
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
|
/// The widgets is being dragged
|
||||||
pub(crate) dragged: bool,
|
pub(crate) dragged: bool,
|
||||||
|
|
||||||
|
@ -79,6 +82,7 @@ impl std::fmt::Debug for Response {
|
||||||
hovered,
|
hovered,
|
||||||
clicked,
|
clicked,
|
||||||
double_clicked,
|
double_clicked,
|
||||||
|
triple_clicked,
|
||||||
dragged,
|
dragged,
|
||||||
drag_released,
|
drag_released,
|
||||||
is_pointer_button_down_on,
|
is_pointer_button_down_on,
|
||||||
|
@ -94,6 +98,7 @@ impl std::fmt::Debug for Response {
|
||||||
.field("hovered", hovered)
|
.field("hovered", hovered)
|
||||||
.field("clicked", clicked)
|
.field("clicked", clicked)
|
||||||
.field("double_clicked", double_clicked)
|
.field("double_clicked", double_clicked)
|
||||||
|
.field("triple_clicked", triple_clicked)
|
||||||
.field("dragged", dragged)
|
.field("dragged", dragged)
|
||||||
.field("drag_released", drag_released)
|
.field("drag_released", drag_released)
|
||||||
.field("is_pointer_button_down_on", is_pointer_button_down_on)
|
.field("is_pointer_button_down_on", is_pointer_button_down_on)
|
||||||
|
@ -138,11 +143,21 @@ impl Response {
|
||||||
self.double_clicked[PointerButton::Primary as usize]
|
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.
|
/// Returns true if this widget was double-clicked this frame by the given button.
|
||||||
pub fn double_clicked_by(&self, button: PointerButton) -> bool {
|
pub fn double_clicked_by(&self, button: PointerButton) -> bool {
|
||||||
self.double_clicked[button as usize]
|
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.
|
/// `true` if there was a click *outside* this widget this frame.
|
||||||
pub fn clicked_elsewhere(&self) -> bool {
|
pub fn clicked_elsewhere(&self) -> bool {
|
||||||
// We do not use self.clicked(), because we want to catch all clicks within our frame,
|
// 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()))
|
Some(OutputEvent::Clicked(make_info()))
|
||||||
} else if self.double_clicked() {
|
} else if self.double_clicked() {
|
||||||
Some(OutputEvent::DoubleClicked(make_info()))
|
Some(OutputEvent::DoubleClicked(make_info()))
|
||||||
|
} else if self.triple_clicked() {
|
||||||
|
Some(OutputEvent::TripleClicked(make_info()))
|
||||||
} else if self.gained_focus() {
|
} else if self.gained_focus() {
|
||||||
Some(OutputEvent::FocusGained(make_info()))
|
Some(OutputEvent::FocusGained(make_info()))
|
||||||
} else if self.changed {
|
} else if self.changed {
|
||||||
|
@ -536,6 +553,11 @@ impl Response {
|
||||||
self.double_clicked[1] || other.double_clicked[1],
|
self.double_clicked[1] || other.double_clicked[1],
|
||||||
self.double_clicked[2] || other.double_clicked[2],
|
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,
|
dragged: self.dragged || other.dragged,
|
||||||
drag_released: self.drag_released || other.drag_released,
|
drag_released: self.drag_released || other.drag_released,
|
||||||
is_pointer_button_down_on: self.is_pointer_button_down_on
|
is_pointer_button_down_on: self.is_pointer_button_down_on
|
||||||
|
|
|
@ -430,7 +430,6 @@ impl<'t> TextEdit<'t> {
|
||||||
ui.output().mutable_text_under_cursor = true;
|
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)
|
// 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 singleline_offset = vec2(state.singleline_offset, 0.0);
|
||||||
let cursor_at_pointer =
|
let cursor_at_pointer =
|
||||||
|
@ -459,6 +458,14 @@ impl<'t> TextEdit<'t> {
|
||||||
primary: galley.from_ccursor(ccursor_range.primary),
|
primary: galley.from_ccursor(ccursor_range.primary),
|
||||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
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 {
|
} else if allow_drag_to_select {
|
||||||
if response.hovered() && ui.input().pointer.any_pressed() {
|
if response.hovered() && ui.input().pointer.any_pressed() {
|
||||||
ui.memory().request_focus(id);
|
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 {
|
fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
CCursor {
|
CCursor {
|
||||||
index: next_word_boundary_char_index(text.chars(), ccursor.index),
|
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 {
|
fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
let num_chars = text.chars().count();
|
let num_chars = text.chars().count();
|
||||||
CCursor {
|
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 {
|
fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
||||||
let mut it = it.skip(index);
|
let mut it = it.skip(index);
|
||||||
if let Some(_first) = it.next() {
|
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
|
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 {
|
fn is_word_char(c: char) -> bool {
|
||||||
c.is_ascii_alphanumeric() || c == '_'
|
c.is_ascii_alphanumeric() || c == '_'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_linebreak(c: char) -> bool {
|
||||||
|
c == '\r' || c == '\n'
|
||||||
|
}
|
||||||
|
|
||||||
/// Accepts and returns character offset (NOT byte offset!).
|
/// Accepts and returns character offset (NOT byte offset!).
|
||||||
fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
|
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
|
// We know that new lines, '\n', are a single byte char, but we have to
|
||||||
|
|
|
@ -332,7 +332,7 @@ impl super::View for InputTest {
|
||||||
});
|
});
|
||||||
|
|
||||||
let response = ui.add(
|
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()),
|
.sense(egui::Sense::click_and_drag()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -348,6 +348,9 @@ impl super::View for InputTest {
|
||||||
if response.double_clicked_by(button) {
|
if response.double_clicked_by(button) {
|
||||||
new_info += &format!("Double-clicked by {:?} button\n", 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) {
|
if response.dragged_by(button) {
|
||||||
new_info += &format!(
|
new_info += &format!(
|
||||||
"Dragged by {:?} button, delta: {:?}\n",
|
"Dragged by {:?} button, delta: {:?}\n",
|
||||||
|
|
Loading…
Reference in a new issue