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 `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 🔧
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue