From 0fd88e52dab6a20572c2ac90ad314210ecd6b69e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 May 2021 14:38:08 -0500 Subject: [PATCH 01/18] Expose getter for currently focused widget. --- egui/src/memory.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/egui/src/memory.rs b/egui/src/memory.rs index 0a082738c11..06545e77125 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -314,6 +314,11 @@ impl Memory { self.interaction.focus.id == Some(id) } + /// Which widget has keyboard focus? + pub fn focus(&self) -> Option { + self.interaction.focus.id + } + pub(crate) fn lock_focus(&mut self, id: Id, lock_focus: bool) { if self.had_focus_last_frame(id) && self.has_focus(id) { self.interaction.focus.is_focus_locked = lock_focus; From 50c8310de5ab2e7f72a5d80c0bbd3db1f876073b Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 May 2021 15:49:08 -0500 Subject: [PATCH 02/18] Remove a level of indirection, exposing the widget event on the top level. --- egui/src/data/output.rs | 14 +++----------- egui/src/response.rs | 4 ++-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 7cc8f3eed5c..c1b79ec3541 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -40,7 +40,7 @@ impl Output { // only describe last event: if let Some(event) = self.events.iter().rev().next() { match event { - OutputEvent::WidgetEvent(WidgetEvent::Focus, widget_info) => { + OutputEvent::FocusGained(widget_info) => { return widget_info.description(); } } @@ -205,25 +205,17 @@ impl Default for CursorIcon { #[derive(Clone, PartialEq)] pub enum OutputEvent { /// A widget gained keyboard focus (by tab key). - WidgetEvent(WidgetEvent, WidgetInfo), + FocusGained(WidgetInfo), } impl std::fmt::Debug for OutputEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::WidgetEvent(we, wi) => write!(f, "{:?}: {:?}", we, wi), + Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi), } } } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum WidgetEvent { - /// Keyboard focused moved onto the widget. - Focus, - // /// Started hovering a new widget. - // Hover, // TODO: cursor hovered events -} - /// Describes a widget such as a [`crate::Button`] or a [`crate::TextEdit`]. #[derive(Clone, PartialEq)] pub struct WidgetInfo { diff --git a/egui/src/response.rs b/egui/src/response.rs index 71b2ddd511b..2cc5e0615fe 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -429,9 +429,9 @@ impl Response { /// Call after interacting and potential calls to [`Self::mark_changed`]. pub fn widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) { if self.gained_focus() { - use crate::output::{OutputEvent, WidgetEvent}; + use crate::output::OutputEvent; let widget_info = make_info(); - let event = OutputEvent::WidgetEvent(WidgetEvent::Focus, widget_info); + let event = OutputEvent::FocusGained(widget_info); self.ctx.output().events.push(event); } } From b3ced6106b9274e5f501dc74f64a4bd0fad31e86 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 May 2021 16:00:08 -0500 Subject: [PATCH 03/18] Align widget descriptions more closely with common screen reader conventions. Note that this work isn't complete--I'll correct more cases as I add more widgets and become familiar with their structures. --- egui/src/data/output.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index c1b79ec3541..6acf9f532d7 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -327,7 +327,7 @@ impl WidgetInfo { } = self; // TODO: localization - let widget_name = match typ { + let widget_type = match typ { WidgetType::Hyperlink => "link", WidgetType::TextEdit => "text edit", WidgetType::Button => "button", @@ -343,20 +343,19 @@ impl WidgetInfo { WidgetType::Label | WidgetType::Other => "", }; - let mut description = widget_name.to_owned(); + let mut description = widget_type.to_owned(); if let Some(selected) = selected { if *typ == WidgetType::Checkbox { - description += " "; - description += if *selected { "checked" } else { "unchecked" }; + let state = if *selected { "checked" } else { "unchecked" }; + description = format!("{} {}", state, description); } else { description += if *selected { "selected" } else { "" }; }; } if let Some(label) = label { - description += " "; - description += label; + description = format!("{}: {}", label, description); } if let Some(edit_text) = edit_text { From 8269cca95d607baff0926106e3072e6e35dace51 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 12 May 2021 08:35:36 -0500 Subject: [PATCH 04/18] Add support for click and double-click events. --- egui/src/data/output.rs | 12 ++++++++++++ egui/src/response.rs | 15 +++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 6acf9f532d7..f76e5529a5f 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -40,6 +40,12 @@ impl Output { // only describe last event: if let Some(event) = self.events.iter().rev().next() { match event { + OutputEvent::Clicked(widget_info) => { + return widget_info.description(); + } + OutputEvent::DoubleClicked(widget_info) => { + return widget_info.description(); + } OutputEvent::FocusGained(widget_info) => { return widget_info.description(); } @@ -204,6 +210,10 @@ impl Default for CursorIcon { /// In particular, these events may be useful for accessability, i.e. for screen readers. #[derive(Clone, PartialEq)] pub enum OutputEvent { + // A widget was clicked. + Clicked(WidgetInfo), + // A widget was double-clicked. + DoubleClicked(WidgetInfo), /// A widget gained keyboard focus (by tab key). FocusGained(WidgetInfo), } @@ -211,6 +221,8 @@ pub enum OutputEvent { impl std::fmt::Debug for OutputEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::Clicked(wi) => write!(f, "Clicked({:?})", wi), + Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi), Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi), } } diff --git a/egui/src/response.rs b/egui/src/response.rs index 2cc5e0615fe..91d491e2c7c 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -428,10 +428,17 @@ impl Response { /// /// Call after interacting and potential calls to [`Self::mark_changed`]. pub fn widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) { - if self.gained_focus() { - use crate::output::OutputEvent; - let widget_info = make_info(); - let event = OutputEvent::FocusGained(widget_info); + use crate::output::OutputEvent; + let event = if self.clicked() { + Some(OutputEvent::Clicked(make_info())) + } else if self.double_clicked() { + Some(OutputEvent::DoubleClicked(make_info())) + } else if self.gained_focus() { + Some(OutputEvent::FocusGained(make_info())) + } else { + None + }; + if let Some(event) = event { self.ctx.output().events.push(event); } } From 97aa56f46548696b014169d11dfad0bc4667e42a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 12 May 2021 13:17:58 -0500 Subject: [PATCH 05/18] Add `ValueChanged` events, with initial support for text. --- egui/src/data/output.rs | 54 +++++++++++------ egui/src/response.rs | 2 + egui/src/widgets/text_edit.rs | 109 +++++++++++++++++++++------------- 3 files changed, 106 insertions(+), 59 deletions(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index f76e5529a5f..51682755c03 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -40,13 +40,10 @@ impl Output { // only describe last event: if let Some(event) = self.events.iter().rev().next() { match event { - OutputEvent::Clicked(widget_info) => { - return widget_info.description(); - } - OutputEvent::DoubleClicked(widget_info) => { - return widget_info.description(); - } - OutputEvent::FocusGained(widget_info) => { + OutputEvent::Clicked(widget_info) + | OutputEvent::DoubleClicked(widget_info) + | OutputEvent::FocusGained(widget_info) + | OutputEvent::ValueChanged(widget_info) => { return widget_info.description(); } } @@ -216,6 +213,8 @@ pub enum OutputEvent { DoubleClicked(WidgetInfo), /// A widget gained keyboard focus (by tab key). FocusGained(WidgetInfo), + // A widget's value changed. + ValueChanged(WidgetInfo), } impl std::fmt::Debug for OutputEvent { @@ -224,6 +223,7 @@ impl std::fmt::Debug for OutputEvent { Self::Clicked(wi) => write!(f, "Clicked({:?})", wi), Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi), Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi), + Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi), } } } @@ -236,7 +236,9 @@ pub struct WidgetInfo { /// The text on labels, buttons, checkboxes etc. pub label: Option, /// The contents of some editable text (for `TextEdit` fields). - pub edit_text: Option, + pub text_value: Option, + // The previous text value. + prev_text_value: Option, /// The current value of checkboxes and radio buttons. pub selected: Option, /// The current value of sliders etc. @@ -248,7 +250,8 @@ impl std::fmt::Debug for WidgetInfo { let Self { typ, label, - edit_text, + text_value, + prev_text_value, selected, value, } = self; @@ -260,8 +263,11 @@ impl std::fmt::Debug for WidgetInfo { if let Some(label) = label { s.field("label", label); } - if let Some(edit_text) = edit_text { - s.field("edit_text", edit_text); + if let Some(text_value) = text_value { + s.field("text_value", text_value); + } + if let Some(prev_text_value) = prev_text_value { + s.field("prev_text_value", prev_text_value); } if let Some(selected) = selected { s.field("selected", selected); @@ -279,7 +285,8 @@ impl WidgetInfo { Self { typ, label: None, - edit_text: None, + text_value: None, + prev_text_value: None, selected: None, value: None, } @@ -321,9 +328,10 @@ impl WidgetInfo { } #[allow(clippy::needless_pass_by_value)] - pub fn text_edit(edit_text: impl ToString) -> Self { + pub fn text_edit(text_value: impl ToString, prev_text_value: impl ToString) -> Self { Self { - edit_text: Some(edit_text.to_string()), + text_value: Some(text_value.to_string()), + prev_text_value: Some(prev_text_value.to_string()), ..Self::new(WidgetType::TextEdit) } } @@ -333,7 +341,8 @@ impl WidgetInfo { let Self { typ, label, - edit_text, + text_value, + prev_text_value: _, selected, value, } = self; @@ -370,9 +379,18 @@ impl WidgetInfo { description = format!("{}: {}", label, description); } - if let Some(edit_text) = edit_text { - description += " "; - description += edit_text; + if typ == &WidgetType::TextEdit { + let text; + if let Some(text_value) = text_value { + if text_value.is_empty() { + text = "blank".into(); + } else { + text = text_value.to_string(); + } + } else { + text = "blank".into(); + } + description = format!("{}: {}", text, description); } if let Some(value) = value { diff --git a/egui/src/response.rs b/egui/src/response.rs index 91d491e2c7c..0c6ebf6ea8c 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -435,6 +435,8 @@ impl Response { Some(OutputEvent::DoubleClicked(make_info())) } else if self.gained_focus() { Some(OutputEvent::FocusGained(make_info())) + } else if self.changed { + Some(OutputEvent::ValueChanged(make_info())) } else { None }; diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index fe81162dd65..c5b35cac51e 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -138,6 +138,7 @@ impl CCursorPair { #[derive(Debug)] pub struct TextEdit<'t> { text: &'t mut String, + prev_text: String, hint_text: String, id: Option, id_source: Option, @@ -170,6 +171,7 @@ impl<'t> TextEdit<'t> { pub fn singleline(text: &'t mut String) -> Self { TextEdit { text, + prev_text: Default::default(), hint_text: Default::default(), id: None, id_source: None, @@ -189,6 +191,7 @@ impl<'t> TextEdit<'t> { pub fn multiline(text: &'t mut String) -> Self { TextEdit { text, + prev_text: Default::default(), hint_text: Default::default(), id: None, id_source: None, @@ -331,6 +334,7 @@ impl<'t> Widget for TextEdit<'t> { impl<'t> TextEdit<'t> { fn content_ui(self, ui: &mut Ui) -> Response { let TextEdit { + mut prev_text, text, hint_text, id, @@ -490,7 +494,11 @@ impl<'t> TextEdit<'t> { Some(CCursorPair::default()) } else { copy_if_not_password(ui, selected_str(text, &cursorp).to_owned()); - Some(CCursorPair::one(delete_selected(text, &cursorp))) + Some(CCursorPair::one(delete_selected( + text, + &mut prev_text, + &cursorp, + ))) } } Event::Text(text_to_insert) => { @@ -499,9 +507,9 @@ impl<'t> TextEdit<'t> { && text_to_insert != "\n" && text_to_insert != "\r" { - let mut ccursor = delete_selected(text, &cursorp); + let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); - insert_text(&mut ccursor, text, text_to_insert); + insert_text(&mut ccursor, text, &mut prev_text, text_to_insert); Some(CCursorPair::one(ccursor)) } else { None @@ -513,12 +521,12 @@ impl<'t> TextEdit<'t> { modifiers, } => { if multiline && ui.memory().has_lock_focus(id) { - let mut ccursor = delete_selected(text, &cursorp); + let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); if modifiers.shift { // TODO: support removing indentation over a selection? decrease_identation(&mut ccursor, text); } else { - insert_text(&mut ccursor, text, "\t"); + insert_text(&mut ccursor, text, &mut prev_text, "\t"); } Some(CCursorPair::one(ccursor)) } else { @@ -531,8 +539,8 @@ impl<'t> TextEdit<'t> { .. } => { if multiline { - let mut ccursor = delete_selected(text, &cursorp); - insert_text(&mut ccursor, text, "\n"); + let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); + insert_text(&mut ccursor, text, &mut prev_text, "\n"); Some(CCursorPair::one(ccursor)) } else { ui.memory().surrender_focus(id); // End input with enter @@ -548,6 +556,7 @@ impl<'t> TextEdit<'t> { if let Some((undo_ccursorp, undo_txt)) = state.undoer.undo(&(cursorp.as_ccursorp(), text.clone())) { + prev_text = text.clone(); *text = undo_txt.clone(); Some(*undo_ccursorp) } else { @@ -559,7 +568,7 @@ impl<'t> TextEdit<'t> { key, pressed: true, modifiers, - } => on_key_press(&mut cursorp, text, &galley, *key, modifiers), + } => on_key_press(&mut cursorp, text, &mut prev_text, &galley, *key, modifiers), Event::CompositionStart => { state.has_ime = true; @@ -572,9 +581,9 @@ impl<'t> TextEdit<'t> { && text_mark != "\r" && state.has_ime { - let mut ccursor = delete_selected(text, &cursorp); + let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); let start_cursor = ccursor; - insert_text(&mut ccursor, text, text_mark); + insert_text(&mut ccursor, text, &mut prev_text, text_mark); Some(CCursorPair::two(start_cursor, ccursor)) } else { None @@ -588,8 +597,8 @@ impl<'t> TextEdit<'t> { && state.has_ime { state.has_ime = false; - let mut ccursor = delete_selected(text, &cursorp); - insert_text(&mut ccursor, text, prediction); + let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); + insert_text(&mut ccursor, text, &mut prev_text, prediction); Some(CCursorPair::one(ccursor)) } else { None @@ -656,7 +665,7 @@ impl<'t> TextEdit<'t> { ui.memory().id_data.insert(id, state); - response.widget_info(|| WidgetInfo::text_edit(&*text)); + response.widget_info(|| WidgetInfo::text_edit(&*text, &*prev_text)); response } } @@ -741,7 +750,13 @@ fn byte_index_from_char_index(s: &str, char_index: usize) -> usize { s.len() } -fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) { +fn insert_text( + ccursor: &mut CCursor, + text: &mut String, + prev_text: &mut String, + text_to_insert: &str, +) { + *prev_text = text.clone(); let mut char_it = text.chars(); let mut new_text = String::with_capacity(text.len() + text_to_insert.len()); for _ in 0..ccursor.index { @@ -756,15 +771,20 @@ fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) { // ---------------------------------------------------------------------------- -fn delete_selected(text: &mut String, cursorp: &CursorPair) -> CCursor { +fn delete_selected(text: &mut String, prev_text: &mut String, cursorp: &CursorPair) -> CCursor { let [min, max] = cursorp.sorted(); - delete_selected_ccursor_range(text, [min.ccursor, max.ccursor]) + delete_selected_ccursor_range(text, prev_text, [min.ccursor, max.ccursor]) } -fn delete_selected_ccursor_range(text: &mut String, [min, max]: [CCursor; 2]) -> CCursor { +fn delete_selected_ccursor_range( + text: &mut String, + prev_text: &mut String, + [min, max]: [CCursor; 2], +) -> CCursor { let [min, max] = [min.index, max.index]; assert!(min <= max); if min < max { + *prev_text = text.clone(); let mut char_it = text.chars(); let mut new_text = String::with_capacity(text.len()); for _ in 0..min { @@ -779,32 +799,37 @@ fn delete_selected_ccursor_range(text: &mut String, [min, max]: [CCursor; 2]) -> } } -fn delete_previous_char(text: &mut String, ccursor: CCursor) -> CCursor { +fn delete_previous_char(text: &mut String, prev_text: &mut String, ccursor: CCursor) -> CCursor { if ccursor.index > 0 { let max_ccursor = ccursor; let min_ccursor = max_ccursor - 1; - delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) + delete_selected_ccursor_range(text, prev_text, [min_ccursor, max_ccursor]) } else { ccursor } } -fn delete_next_char(text: &mut String, ccursor: CCursor) -> CCursor { - delete_selected_ccursor_range(text, [ccursor, ccursor + 1]) +fn delete_next_char(text: &mut String, prev_text: &mut String, ccursor: CCursor) -> CCursor { + delete_selected_ccursor_range(text, prev_text, [ccursor, ccursor + 1]) } -fn delete_previous_word(text: &mut String, max_ccursor: CCursor) -> CCursor { +fn delete_previous_word( + text: &mut String, + prev_text: &mut String, + max_ccursor: CCursor, +) -> CCursor { let min_ccursor = ccursor_previous_word(text, max_ccursor); - delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) + delete_selected_ccursor_range(text, prev_text, [min_ccursor, max_ccursor]) } -fn delete_next_word(text: &mut String, min_ccursor: CCursor) -> CCursor { +fn delete_next_word(text: &mut String, prev_text: &mut String, min_ccursor: CCursor) -> CCursor { let max_ccursor = ccursor_next_word(text, min_ccursor); - delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) + delete_selected_ccursor_range(text, prev_text, [min_ccursor, max_ccursor]) } fn delete_paragraph_before_cursor( text: &mut String, + prev_text: &mut String, galley: &Galley, cursorp: &CursorPair, ) -> CCursor { @@ -815,14 +840,15 @@ fn delete_paragraph_before_cursor( prefer_next_row: true, }); if min.ccursor == max.ccursor { - delete_previous_char(text, min.ccursor) + delete_previous_char(text, prev_text, min.ccursor) } else { - delete_selected(text, &CursorPair::two(min, max)) + delete_selected(text, prev_text, &CursorPair::two(min, max)) } } fn delete_paragraph_after_cursor( text: &mut String, + prev_text: &mut String, galley: &Galley, cursorp: &CursorPair, ) -> CCursor { @@ -833,9 +859,9 @@ fn delete_paragraph_after_cursor( prefer_next_row: false, }); if min.ccursor == max.ccursor { - delete_next_char(text, min.ccursor) + delete_next_char(text, prev_text, min.ccursor) } else { - delete_selected(text, &CursorPair::two(min, max)) + delete_selected(text, prev_text, &CursorPair::two(min, max)) } } @@ -845,6 +871,7 @@ fn delete_paragraph_after_cursor( fn on_key_press( cursorp: &mut CursorPair, text: &mut String, + prev_text: &mut String, galley: &Galley, key: Key, modifiers: &Modifiers, @@ -852,31 +879,31 @@ fn on_key_press( match key { Key::Backspace => { let ccursor = if modifiers.mac_cmd { - delete_paragraph_before_cursor(text, galley, cursorp) + delete_paragraph_before_cursor(text, prev_text, galley, cursorp) } else if let Some(cursor) = cursorp.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - delete_previous_word(text, cursor.ccursor) + delete_previous_word(text, prev_text, cursor.ccursor) } else { - delete_previous_char(text, cursor.ccursor) + delete_previous_char(text, prev_text, cursor.ccursor) } } else { - delete_selected(text, cursorp) + delete_selected(text, prev_text, cursorp) }; Some(CCursorPair::one(ccursor)) } Key::Delete => { let ccursor = if modifiers.mac_cmd { - delete_paragraph_after_cursor(text, galley, cursorp) + delete_paragraph_after_cursor(text, prev_text, galley, cursorp) } else if let Some(cursor) = cursorp.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - delete_next_word(text, cursor.ccursor) + delete_next_word(text, prev_text, cursor.ccursor) } else { - delete_next_char(text, cursor.ccursor) + delete_next_char(text, prev_text, cursor.ccursor) } } else { - delete_selected(text, cursorp) + delete_selected(text, prev_text, cursorp) }; let ccursor = CCursor { prefer_next_row: true, @@ -892,20 +919,20 @@ fn on_key_press( } Key::K if modifiers.ctrl => { - let ccursor = delete_paragraph_after_cursor(text, galley, cursorp); + let ccursor = delete_paragraph_after_cursor(text, prev_text, galley, cursorp); Some(CCursorPair::one(ccursor)) } Key::U if modifiers.ctrl => { - let ccursor = delete_paragraph_before_cursor(text, galley, cursorp); + let ccursor = delete_paragraph_before_cursor(text, prev_text, galley, cursorp); Some(CCursorPair::one(ccursor)) } Key::W if modifiers.ctrl => { let ccursor = if let Some(cursor) = cursorp.single() { - delete_previous_word(text, cursor.ccursor) + delete_previous_word(text, prev_text, cursor.ccursor) } else { - delete_selected(text, cursorp) + delete_selected(text, prev_text, cursorp) }; Some(CCursorPair::one(ccursor)) } From 2433506c925f05bd4d4fb3cfffe9b268f8221ddd Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 18 May 2021 13:26:20 -0500 Subject: [PATCH 06/18] Add support for reporting cursor selection changes. --- egui/src/context.rs | 3 ++- egui/src/data/output.rs | 36 ++++++++++++++++++++++++++++++++++- egui/src/response.rs | 12 ++++++++++++ egui/src/widgets/text_edit.rs | 13 ++++++++++++- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/egui/src/context.rs b/egui/src/context.rs index 8b20492bd2c..f011415e313 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -193,7 +193,8 @@ impl CtxRef { drag_released: false, is_pointer_button_down_on: false, interact_pointer_pos: None, - changed: false, // must be set by the widget itself + changed: false, // must be set by the widget itself + has_widget_info: false, // must be set by the widget itself }; if !enabled || !sense.focusable || !layer_id.allow_interaction() { diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 51682755c03..c779fb070e9 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -43,6 +43,7 @@ impl Output { OutputEvent::Clicked(widget_info) | OutputEvent::DoubleClicked(widget_info) | OutputEvent::FocusGained(widget_info) + | OutputEvent::TextSelectionChanged(widget_info) | OutputEvent::ValueChanged(widget_info) => { return widget_info.description(); } @@ -213,6 +214,8 @@ pub enum OutputEvent { DoubleClicked(WidgetInfo), /// A widget gained keyboard focus (by tab key). FocusGained(WidgetInfo), + // Text selection was updated. + TextSelectionChanged(WidgetInfo), // A widget's value changed. ValueChanged(WidgetInfo), } @@ -223,6 +226,7 @@ impl std::fmt::Debug for OutputEvent { Self::Clicked(wi) => write!(f, "Clicked({:?})", wi), Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi), Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi), + Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi), Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi), } } @@ -238,11 +242,15 @@ pub struct WidgetInfo { /// The contents of some editable text (for `TextEdit` fields). pub text_value: Option, // The previous text value. - prev_text_value: Option, + pub prev_text_value: Option, /// The current value of checkboxes and radio buttons. pub selected: Option, /// The current value of sliders etc. pub value: Option, + // Location of primary cursor. + pub primary_cursor: Option, + // Location of secondary cursor. + pub secondary_cursor: Option, } impl std::fmt::Debug for WidgetInfo { @@ -254,6 +262,8 @@ impl std::fmt::Debug for WidgetInfo { prev_text_value, selected, value, + primary_cursor, + secondary_cursor, } = self; let mut s = f.debug_struct("WidgetInfo"); @@ -275,6 +285,12 @@ impl std::fmt::Debug for WidgetInfo { if let Some(value) = value { s.field("value", value); } + if let Some(primary_cursor) = primary_cursor { + s.field("primary_cursor", primary_cursor); + } + if let Some(secondary_cursor) = secondary_cursor { + s.field("secondary_cursor", secondary_cursor); + } s.finish() } @@ -289,6 +305,8 @@ impl WidgetInfo { prev_text_value: None, selected: None, value: None, + primary_cursor: None, + secondary_cursor: None, } } @@ -336,6 +354,20 @@ impl WidgetInfo { } } + #[allow(clippy::needless_pass_by_value)] + pub fn text_selection_changed( + primary_cursor: usize, + secondary_cursor: usize, + text_value: impl ToString, + ) -> Self { + Self { + primary_cursor: Some(primary_cursor), + secondary_cursor: Some(secondary_cursor), + text_value: Some(text_value.to_string()), + ..Self::new(WidgetType::TextEdit) + } + } + /// This can be used by a text-to-speech system to describe the widget. pub fn description(&self) -> String { let Self { @@ -345,6 +377,8 @@ impl WidgetInfo { prev_text_value: _, selected, value, + primary_cursor: _, + secondary_cursor: _, } = self; // TODO: localization diff --git a/egui/src/response.rs b/egui/src/response.rs index 0c6ebf6ea8c..7f1edd997d1 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -65,6 +65,8 @@ pub struct Response { /// e.g. the slider was dragged, text was entered in a `TextEdit` etc. /// Always `false` for something like a `Button`. pub(crate) changed: bool, + /// Has a `WidgetInfo` but isn't covered by some other case (E.g. `changed`, `clicked`.) + pub(crate) has_widget_info: bool, } impl std::fmt::Debug for Response { @@ -84,6 +86,7 @@ impl std::fmt::Debug for Response { is_pointer_button_down_on, interact_pointer_pos, changed, + has_widget_info, } = self; f.debug_struct("Response") .field("layer_id", layer_id) @@ -99,6 +102,7 @@ impl std::fmt::Debug for Response { .field("is_pointer_button_down_on", is_pointer_button_down_on) .field("interact_pointer_pos", interact_pointer_pos) .field("changed", changed) + .field("has_widget_info", has_widget_info) .finish() } } @@ -437,6 +441,13 @@ impl Response { Some(OutputEvent::FocusGained(make_info())) } else if self.changed { Some(OutputEvent::ValueChanged(make_info())) + } else if self.has_widget_info { + let info = make_info(); + if info.primary_cursor.is_some() && info.secondary_cursor.is_some() { + Some(OutputEvent::TextSelectionChanged(info)) + } else { + None + } } else { None }; @@ -479,6 +490,7 @@ impl Response { || other.is_pointer_button_down_on, interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), changed: self.changed || other.changed, + has_widget_info: self.has_widget_info || other.has_widget_info, } } } diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index c5b35cac51e..4d0485cb643 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -665,7 +665,18 @@ impl<'t> TextEdit<'t> { ui.memory().id_data.insert(id, state); - response.widget_info(|| WidgetInfo::text_edit(&*text, &*prev_text)); + if response.changed { + response.widget_info(|| WidgetInfo::text_edit(&*text, &*prev_text)); + } else if let Some(text_cursor) = text_cursor { + response.has_widget_info = true; + response.widget_info(|| { + WidgetInfo::text_selection_changed( + text_cursor.primary.ccursor.index, + text_cursor.secondary.ccursor.index, + &*text, + ) + }); + } response } } From 73271a6265743eacf95a2b6fa2b7dc4d06f29770 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 18 May 2021 14:52:53 -0500 Subject: [PATCH 07/18] Track enabled/disabled status. --- egui/src/data/output.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index c779fb070e9..590a72f2b44 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -237,6 +237,8 @@ impl std::fmt::Debug for OutputEvent { pub struct WidgetInfo { /// The type of widget this is. pub typ: WidgetType, + // Whether the widget is enabled. + pub enabled: bool, /// The text on labels, buttons, checkboxes etc. pub label: Option, /// The contents of some editable text (for `TextEdit` fields). @@ -257,6 +259,7 @@ impl std::fmt::Debug for WidgetInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { typ, + enabled, label, text_value, prev_text_value, @@ -269,6 +272,7 @@ impl std::fmt::Debug for WidgetInfo { let mut s = f.debug_struct("WidgetInfo"); s.field("typ", typ); + s.field("enabled", enabled); if let Some(label) = label { s.field("label", label); @@ -300,6 +304,7 @@ impl WidgetInfo { pub fn new(typ: WidgetType) -> Self { Self { typ, + enabled: true, label: None, text_value: None, prev_text_value: None, @@ -372,6 +377,7 @@ impl WidgetInfo { pub fn description(&self) -> String { let Self { typ, + enabled, label, text_value, prev_text_value: _, @@ -432,6 +438,9 @@ impl WidgetInfo { description += &value.to_string(); } + if !enabled { + description += ": disabled"; + } description.trim().to_owned() } } From fa18727933d37c96d87cbe689562f0fec9304837 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 19 May 2021 06:18:05 -0500 Subject: [PATCH 08/18] Move `prev_text` off of the widget struct. --- egui/src/widgets/text_edit.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 4d0485cb643..cc74ea79c87 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -138,7 +138,6 @@ impl CCursorPair { #[derive(Debug)] pub struct TextEdit<'t> { text: &'t mut String, - prev_text: String, hint_text: String, id: Option, id_source: Option, @@ -171,7 +170,6 @@ impl<'t> TextEdit<'t> { pub fn singleline(text: &'t mut String) -> Self { TextEdit { text, - prev_text: Default::default(), hint_text: Default::default(), id: None, id_source: None, @@ -191,7 +189,6 @@ impl<'t> TextEdit<'t> { pub fn multiline(text: &'t mut String) -> Self { TextEdit { text, - prev_text: Default::default(), hint_text: Default::default(), id: None, id_source: None, @@ -334,7 +331,6 @@ impl<'t> Widget for TextEdit<'t> { impl<'t> TextEdit<'t> { fn content_ui(self, ui: &mut Ui) -> Response { let TextEdit { - mut prev_text, text, hint_text, id, @@ -350,6 +346,7 @@ impl<'t> TextEdit<'t> { lock_focus, } = self; + let mut prev_text = text.clone(); let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); let line_spacing = ui.fonts().row_height(text_style); let available_width = ui.available_width(); @@ -556,7 +553,6 @@ impl<'t> TextEdit<'t> { if let Some((undo_ccursorp, undo_txt)) = state.undoer.undo(&(cursorp.as_ccursorp(), text.clone())) { - prev_text = text.clone(); *text = undo_txt.clone(); Some(*undo_ccursorp) } else { From 131525536bfa45211df78db29f3ba9162762eaf7 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 19 May 2021 08:42:33 -0500 Subject: [PATCH 09/18] Get rid of `has_widget_info` and push events directly where it makes sense. --- egui/src/context.rs | 3 +-- egui/src/response.rs | 12 ------------ egui/src/widgets/text_edit.rs | 21 +++++++++++---------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/egui/src/context.rs b/egui/src/context.rs index f011415e313..8b20492bd2c 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -193,8 +193,7 @@ impl CtxRef { drag_released: false, is_pointer_button_down_on: false, interact_pointer_pos: None, - changed: false, // must be set by the widget itself - has_widget_info: false, // must be set by the widget itself + changed: false, // must be set by the widget itself }; if !enabled || !sense.focusable || !layer_id.allow_interaction() { diff --git a/egui/src/response.rs b/egui/src/response.rs index 7f1edd997d1..0c6ebf6ea8c 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -65,8 +65,6 @@ pub struct Response { /// e.g. the slider was dragged, text was entered in a `TextEdit` etc. /// Always `false` for something like a `Button`. pub(crate) changed: bool, - /// Has a `WidgetInfo` but isn't covered by some other case (E.g. `changed`, `clicked`.) - pub(crate) has_widget_info: bool, } impl std::fmt::Debug for Response { @@ -86,7 +84,6 @@ impl std::fmt::Debug for Response { is_pointer_button_down_on, interact_pointer_pos, changed, - has_widget_info, } = self; f.debug_struct("Response") .field("layer_id", layer_id) @@ -102,7 +99,6 @@ impl std::fmt::Debug for Response { .field("is_pointer_button_down_on", is_pointer_button_down_on) .field("interact_pointer_pos", interact_pointer_pos) .field("changed", changed) - .field("has_widget_info", has_widget_info) .finish() } } @@ -441,13 +437,6 @@ impl Response { Some(OutputEvent::FocusGained(make_info())) } else if self.changed { Some(OutputEvent::ValueChanged(make_info())) - } else if self.has_widget_info { - let info = make_info(); - if info.primary_cursor.is_some() && info.secondary_cursor.is_some() { - Some(OutputEvent::TextSelectionChanged(info)) - } else { - None - } } else { None }; @@ -490,7 +479,6 @@ impl Response { || other.is_pointer_button_down_on, interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), changed: self.changed || other.changed, - has_widget_info: self.has_widget_info || other.has_widget_info, } } } diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index cc74ea79c87..d1824f27860 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -1,4 +1,4 @@ -use crate::{util::undoer::Undoer, *}; +use crate::{output::OutputEvent, util::undoer::Undoer, *}; use epaint::{text::cursor::*, *}; #[derive(Clone, Debug, Default)] @@ -505,7 +505,6 @@ impl<'t> TextEdit<'t> { && text_to_insert != "\r" { let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); - insert_text(&mut ccursor, text, &mut prev_text, text_to_insert); Some(CCursorPair::one(ccursor)) } else { @@ -664,14 +663,16 @@ impl<'t> TextEdit<'t> { if response.changed { response.widget_info(|| WidgetInfo::text_edit(&*text, &*prev_text)); } else if let Some(text_cursor) = text_cursor { - response.has_widget_info = true; - response.widget_info(|| { - WidgetInfo::text_selection_changed( - text_cursor.primary.ccursor.index, - text_cursor.secondary.ccursor.index, - &*text, - ) - }); + let info = WidgetInfo::text_selection_changed( + text_cursor.primary.ccursor.index, + text_cursor.secondary.ccursor.index, + &*text, + ); + response + .ctx + .output() + .events + .push(OutputEvent::TextSelectionChanged(info)); } response } From ff9059fe68b65c05fe0d60995bd839c8389ddddc Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 19 May 2021 09:03:49 -0500 Subject: [PATCH 10/18] Fix typo. --- egui/src/data/output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 590a72f2b44..b23f6f00191 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -24,7 +24,7 @@ pub struct Output { /// Events that may be useful to e.g. a screen reader. pub events: Vec, - /// Position of text widgts' cursor + /// Position of text widget's cursor pub text_cursor: Option, } From 875d3a650d4c7cbe90b7bfcfbd0c317c5986e8f8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 19 May 2021 09:39:04 -0500 Subject: [PATCH 11/18] s/text_value/current_text_value/ --- egui/src/data/output.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index b23f6f00191..7bbf5a761da 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -242,7 +242,7 @@ pub struct WidgetInfo { /// The text on labels, buttons, checkboxes etc. pub label: Option, /// The contents of some editable text (for `TextEdit` fields). - pub text_value: Option, + pub current_text_value: Option, // The previous text value. pub prev_text_value: Option, /// The current value of checkboxes and radio buttons. @@ -261,7 +261,7 @@ impl std::fmt::Debug for WidgetInfo { typ, enabled, label, - text_value, + current_text_value: text_value, prev_text_value, selected, value, @@ -306,7 +306,7 @@ impl WidgetInfo { typ, enabled: true, label: None, - text_value: None, + current_text_value: None, prev_text_value: None, selected: None, value: None, @@ -353,7 +353,7 @@ impl WidgetInfo { #[allow(clippy::needless_pass_by_value)] pub fn text_edit(text_value: impl ToString, prev_text_value: impl ToString) -> Self { Self { - text_value: Some(text_value.to_string()), + current_text_value: Some(text_value.to_string()), prev_text_value: Some(prev_text_value.to_string()), ..Self::new(WidgetType::TextEdit) } @@ -368,7 +368,7 @@ impl WidgetInfo { Self { primary_cursor: Some(primary_cursor), secondary_cursor: Some(secondary_cursor), - text_value: Some(text_value.to_string()), + current_text_value: Some(text_value.to_string()), ..Self::new(WidgetType::TextEdit) } } @@ -379,7 +379,7 @@ impl WidgetInfo { typ, enabled, label, - text_value, + current_text_value: text_value, prev_text_value: _, selected, value, From 68499cebf65f7f4ae7573d509f8d4134b07da250 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 19 May 2021 10:47:18 -0500 Subject: [PATCH 12/18] Use a `RangeInclusive` for text selection. --- egui/src/data/output.rs | 32 +++++++++++--------------------- egui/src/widgets/text_edit.rs | 8 +++----- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 7bbf5a761da..47b5e372332 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -249,10 +249,8 @@ pub struct WidgetInfo { pub selected: Option, /// The current value of sliders etc. pub value: Option, - // Location of primary cursor. - pub primary_cursor: Option, - // Location of secondary cursor. - pub secondary_cursor: Option, + // Selected range of characters in [`Self::current_text_value`]. + pub text_selection: Option>, } impl std::fmt::Debug for WidgetInfo { @@ -265,8 +263,7 @@ impl std::fmt::Debug for WidgetInfo { prev_text_value, selected, value, - primary_cursor, - secondary_cursor, + text_selection, } = self; let mut s = f.debug_struct("WidgetInfo"); @@ -289,11 +286,8 @@ impl std::fmt::Debug for WidgetInfo { if let Some(value) = value { s.field("value", value); } - if let Some(primary_cursor) = primary_cursor { - s.field("primary_cursor", primary_cursor); - } - if let Some(secondary_cursor) = secondary_cursor { - s.field("secondary_cursor", secondary_cursor); + if let Some(text_selection) = text_selection { + s.field("text_selection", text_selection); } s.finish() @@ -310,8 +304,7 @@ impl WidgetInfo { prev_text_value: None, selected: None, value: None, - primary_cursor: None, - secondary_cursor: None, + text_selection: None, } } @@ -361,14 +354,12 @@ impl WidgetInfo { #[allow(clippy::needless_pass_by_value)] pub fn text_selection_changed( - primary_cursor: usize, - secondary_cursor: usize, - text_value: impl ToString, + text_selection: std::ops::RangeInclusive, + current_text_value: impl ToString, ) -> Self { Self { - primary_cursor: Some(primary_cursor), - secondary_cursor: Some(secondary_cursor), - current_text_value: Some(text_value.to_string()), + text_selection: Some(text_selection), + current_text_value: Some(current_text_value.to_string()), ..Self::new(WidgetType::TextEdit) } } @@ -383,8 +374,7 @@ impl WidgetInfo { prev_text_value: _, selected, value, - primary_cursor: _, - secondary_cursor: _, + text_selection: _, } = self; // TODO: localization diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index d1824f27860..15299f8bf04 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -663,11 +663,9 @@ impl<'t> TextEdit<'t> { if response.changed { response.widget_info(|| WidgetInfo::text_edit(&*text, &*prev_text)); } else if let Some(text_cursor) = text_cursor { - let info = WidgetInfo::text_selection_changed( - text_cursor.primary.ccursor.index, - text_cursor.secondary.ccursor.index, - &*text, - ); + let char_range = + text_cursor.primary.ccursor.index..=text_cursor.secondary.ccursor.index; + let info = WidgetInfo::text_selection_changed(char_range, &*text); response .ctx .output() From 4fa1e8212961a77268e56bebcc143e11dce0d29e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 19 May 2021 10:53:15 -0500 Subject: [PATCH 13/18] Invert parameters. --- egui/src/data/output.rs | 2 +- egui/src/widgets/text_edit.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 47b5e372332..a3c5fc887f9 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -344,7 +344,7 @@ impl WidgetInfo { } #[allow(clippy::needless_pass_by_value)] - pub fn text_edit(text_value: impl ToString, prev_text_value: impl ToString) -> Self { + pub fn text_edit(prev_text_value: impl ToString, text_value: impl ToString) -> Self { Self { current_text_value: Some(text_value.to_string()), prev_text_value: Some(prev_text_value.to_string()), diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 15299f8bf04..0ebe92c4ba1 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -661,7 +661,7 @@ impl<'t> TextEdit<'t> { ui.memory().id_data.insert(id, state); if response.changed { - response.widget_info(|| WidgetInfo::text_edit(&*text, &*prev_text)); + response.widget_info(|| WidgetInfo::text_edit(&*prev_text, &*text)); } else if let Some(text_cursor) = text_cursor { let char_range = text_cursor.primary.ccursor.index..=text_cursor.secondary.ccursor.index; From 9fe465cc4a95c499d852772e651dbbf0fba3b32a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 20 May 2021 12:26:34 -0500 Subject: [PATCH 14/18] Various fixes. * Only dispatch `SelectionChanged` if the selection actually changes. * Fix missing focus events. --- egui/src/widgets/text_edit.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 0ebe92c4ba1..b3cf262c73c 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -449,6 +449,7 @@ impl<'t> TextEdit<'t> { } let mut text_cursor = None; + let prev_text_cursor = state.cursorp; if ui.memory().has_focus(id) && enabled { ui.memory().lock_focus(id, lock_focus); @@ -660,9 +661,19 @@ impl<'t> TextEdit<'t> { ui.memory().id_data.insert(id, state); + let selection_changed = if let (Some(text_cursor), Some(prev_text_cursor)) = + (text_cursor, prev_text_cursor) + { + text_cursor.primary.ccursor.index != prev_text_cursor.primary.ccursor.index + || text_cursor.secondary.ccursor.index != prev_text_cursor.secondary.ccursor.index + } else { + false + }; + if response.changed { response.widget_info(|| WidgetInfo::text_edit(&*prev_text, &*text)); - } else if let Some(text_cursor) = text_cursor { + } else if selection_changed { + let text_cursor = text_cursor.unwrap(); let char_range = text_cursor.primary.ccursor.index..=text_cursor.secondary.ccursor.index; let info = WidgetInfo::text_selection_changed(char_range, &*text); @@ -671,6 +682,8 @@ impl<'t> TextEdit<'t> { .output() .events .push(OutputEvent::TextSelectionChanged(info)); + } else { + response.widget_info(|| WidgetInfo::text_edit(&*prev_text, &*text)); } response } From 265190a89226faaf04e3a7a0d13bfad538086a28 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 20 May 2021 12:34:45 -0500 Subject: [PATCH 15/18] If values for `current_text` and `prev_text` are unchanged, filter out the previous value. --- egui/src/data/output.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index a3c5fc887f9..0f1fb15340f 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -345,9 +345,16 @@ impl WidgetInfo { #[allow(clippy::needless_pass_by_value)] pub fn text_edit(prev_text_value: impl ToString, text_value: impl ToString) -> Self { + let text_value = text_value.to_string(); + let prev_text_value = prev_text_value.to_string(); + let prev_text_value = if text_value == prev_text_value { + None + } else { + Some(prev_text_value) + }; Self { current_text_value: Some(text_value.to_string()), - prev_text_value: Some(prev_text_value.to_string()), + prev_text_value, ..Self::new(WidgetType::TextEdit) } } From a22d2cc66503cc2a2487385e20710aba739d65f7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 26 May 2021 23:16:49 +0200 Subject: [PATCH 16/18] No need to pass in `&mut prev_text` everywhere --- egui/src/widgets/text_edit.rs | 104 ++++++++++++++-------------------- 1 file changed, 41 insertions(+), 63 deletions(-) diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index b3cf262c73c..67d1c7655bb 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -346,7 +346,7 @@ impl<'t> TextEdit<'t> { lock_focus, } = self; - let mut prev_text = text.clone(); + let prev_text = text.clone(); let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); let line_spacing = ui.fonts().row_height(text_style); let available_width = ui.available_width(); @@ -492,11 +492,7 @@ impl<'t> TextEdit<'t> { Some(CCursorPair::default()) } else { copy_if_not_password(ui, selected_str(text, &cursorp).to_owned()); - Some(CCursorPair::one(delete_selected( - text, - &mut prev_text, - &cursorp, - ))) + Some(CCursorPair::one(delete_selected(text, &cursorp))) } } Event::Text(text_to_insert) => { @@ -505,8 +501,8 @@ impl<'t> TextEdit<'t> { && text_to_insert != "\n" && text_to_insert != "\r" { - let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); - insert_text(&mut ccursor, text, &mut prev_text, text_to_insert); + let mut ccursor = delete_selected(text, &cursorp); + insert_text(&mut ccursor, text, text_to_insert); Some(CCursorPair::one(ccursor)) } else { None @@ -518,12 +514,12 @@ impl<'t> TextEdit<'t> { modifiers, } => { if multiline && ui.memory().has_lock_focus(id) { - let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); + let mut ccursor = delete_selected(text, &cursorp); if modifiers.shift { // TODO: support removing indentation over a selection? decrease_identation(&mut ccursor, text); } else { - insert_text(&mut ccursor, text, &mut prev_text, "\t"); + insert_text(&mut ccursor, text, "\t"); } Some(CCursorPair::one(ccursor)) } else { @@ -536,8 +532,8 @@ impl<'t> TextEdit<'t> { .. } => { if multiline { - let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); - insert_text(&mut ccursor, text, &mut prev_text, "\n"); + let mut ccursor = delete_selected(text, &cursorp); + insert_text(&mut ccursor, text, "\n"); Some(CCursorPair::one(ccursor)) } else { ui.memory().surrender_focus(id); // End input with enter @@ -564,7 +560,7 @@ impl<'t> TextEdit<'t> { key, pressed: true, modifiers, - } => on_key_press(&mut cursorp, text, &mut prev_text, &galley, *key, modifiers), + } => on_key_press(&mut cursorp, text, &galley, *key, modifiers), Event::CompositionStart => { state.has_ime = true; @@ -577,9 +573,9 @@ impl<'t> TextEdit<'t> { && text_mark != "\r" && state.has_ime { - let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); + let mut ccursor = delete_selected(text, &cursorp); let start_cursor = ccursor; - insert_text(&mut ccursor, text, &mut prev_text, text_mark); + insert_text(&mut ccursor, text, text_mark); Some(CCursorPair::two(start_cursor, ccursor)) } else { None @@ -593,8 +589,8 @@ impl<'t> TextEdit<'t> { && state.has_ime { state.has_ime = false; - let mut ccursor = delete_selected(text, &mut prev_text, &cursorp); - insert_text(&mut ccursor, text, &mut prev_text, prediction); + let mut ccursor = delete_selected(text, &cursorp); + insert_text(&mut ccursor, text, prediction); Some(CCursorPair::one(ccursor)) } else { None @@ -769,13 +765,7 @@ fn byte_index_from_char_index(s: &str, char_index: usize) -> usize { s.len() } -fn insert_text( - ccursor: &mut CCursor, - text: &mut String, - prev_text: &mut String, - text_to_insert: &str, -) { - *prev_text = text.clone(); +fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) { let mut char_it = text.chars(); let mut new_text = String::with_capacity(text.len() + text_to_insert.len()); for _ in 0..ccursor.index { @@ -790,20 +780,15 @@ fn insert_text( // ---------------------------------------------------------------------------- -fn delete_selected(text: &mut String, prev_text: &mut String, cursorp: &CursorPair) -> CCursor { +fn delete_selected(text: &mut String, cursorp: &CursorPair) -> CCursor { let [min, max] = cursorp.sorted(); - delete_selected_ccursor_range(text, prev_text, [min.ccursor, max.ccursor]) + delete_selected_ccursor_range(text, [min.ccursor, max.ccursor]) } -fn delete_selected_ccursor_range( - text: &mut String, - prev_text: &mut String, - [min, max]: [CCursor; 2], -) -> CCursor { +fn delete_selected_ccursor_range(text: &mut String, [min, max]: [CCursor; 2]) -> CCursor { let [min, max] = [min.index, max.index]; assert!(min <= max); if min < max { - *prev_text = text.clone(); let mut char_it = text.chars(); let mut new_text = String::with_capacity(text.len()); for _ in 0..min { @@ -818,37 +803,32 @@ fn delete_selected_ccursor_range( } } -fn delete_previous_char(text: &mut String, prev_text: &mut String, ccursor: CCursor) -> CCursor { +fn delete_previous_char(text: &mut String, ccursor: CCursor) -> CCursor { if ccursor.index > 0 { let max_ccursor = ccursor; let min_ccursor = max_ccursor - 1; - delete_selected_ccursor_range(text, prev_text, [min_ccursor, max_ccursor]) + delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) } else { ccursor } } -fn delete_next_char(text: &mut String, prev_text: &mut String, ccursor: CCursor) -> CCursor { - delete_selected_ccursor_range(text, prev_text, [ccursor, ccursor + 1]) +fn delete_next_char(text: &mut String, ccursor: CCursor) -> CCursor { + delete_selected_ccursor_range(text, [ccursor, ccursor + 1]) } -fn delete_previous_word( - text: &mut String, - prev_text: &mut String, - max_ccursor: CCursor, -) -> CCursor { +fn delete_previous_word(text: &mut String, max_ccursor: CCursor) -> CCursor { let min_ccursor = ccursor_previous_word(text, max_ccursor); - delete_selected_ccursor_range(text, prev_text, [min_ccursor, max_ccursor]) + delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) } -fn delete_next_word(text: &mut String, prev_text: &mut String, min_ccursor: CCursor) -> CCursor { +fn delete_next_word(text: &mut String, min_ccursor: CCursor) -> CCursor { let max_ccursor = ccursor_next_word(text, min_ccursor); - delete_selected_ccursor_range(text, prev_text, [min_ccursor, max_ccursor]) + delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) } fn delete_paragraph_before_cursor( text: &mut String, - prev_text: &mut String, galley: &Galley, cursorp: &CursorPair, ) -> CCursor { @@ -859,15 +839,14 @@ fn delete_paragraph_before_cursor( prefer_next_row: true, }); if min.ccursor == max.ccursor { - delete_previous_char(text, prev_text, min.ccursor) + delete_previous_char(text, min.ccursor) } else { - delete_selected(text, prev_text, &CursorPair::two(min, max)) + delete_selected(text, &CursorPair::two(min, max)) } } fn delete_paragraph_after_cursor( text: &mut String, - prev_text: &mut String, galley: &Galley, cursorp: &CursorPair, ) -> CCursor { @@ -878,9 +857,9 @@ fn delete_paragraph_after_cursor( prefer_next_row: false, }); if min.ccursor == max.ccursor { - delete_next_char(text, prev_text, min.ccursor) + delete_next_char(text, min.ccursor) } else { - delete_selected(text, prev_text, &CursorPair::two(min, max)) + delete_selected(text, &CursorPair::two(min, max)) } } @@ -890,7 +869,6 @@ fn delete_paragraph_after_cursor( fn on_key_press( cursorp: &mut CursorPair, text: &mut String, - prev_text: &mut String, galley: &Galley, key: Key, modifiers: &Modifiers, @@ -898,31 +876,31 @@ fn on_key_press( match key { Key::Backspace => { let ccursor = if modifiers.mac_cmd { - delete_paragraph_before_cursor(text, prev_text, galley, cursorp) + delete_paragraph_before_cursor(text, galley, cursorp) } else if let Some(cursor) = cursorp.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - delete_previous_word(text, prev_text, cursor.ccursor) + delete_previous_word(text, cursor.ccursor) } else { - delete_previous_char(text, prev_text, cursor.ccursor) + delete_previous_char(text, cursor.ccursor) } } else { - delete_selected(text, prev_text, cursorp) + delete_selected(text, cursorp) }; Some(CCursorPair::one(ccursor)) } Key::Delete => { let ccursor = if modifiers.mac_cmd { - delete_paragraph_after_cursor(text, prev_text, galley, cursorp) + delete_paragraph_after_cursor(text, galley, cursorp) } else if let Some(cursor) = cursorp.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - delete_next_word(text, prev_text, cursor.ccursor) + delete_next_word(text, cursor.ccursor) } else { - delete_next_char(text, prev_text, cursor.ccursor) + delete_next_char(text, cursor.ccursor) } } else { - delete_selected(text, prev_text, cursorp) + delete_selected(text, cursorp) }; let ccursor = CCursor { prefer_next_row: true, @@ -938,20 +916,20 @@ fn on_key_press( } Key::K if modifiers.ctrl => { - let ccursor = delete_paragraph_after_cursor(text, prev_text, galley, cursorp); + let ccursor = delete_paragraph_after_cursor(text, galley, cursorp); Some(CCursorPair::one(ccursor)) } Key::U if modifiers.ctrl => { - let ccursor = delete_paragraph_before_cursor(text, prev_text, galley, cursorp); + let ccursor = delete_paragraph_before_cursor(text, galley, cursorp); Some(CCursorPair::one(ccursor)) } Key::W if modifiers.ctrl => { let ccursor = if let Some(cursor) = cursorp.single() { - delete_previous_word(text, prev_text, cursor.ccursor) + delete_previous_word(text, cursor.ccursor) } else { - delete_selected(text, prev_text, cursorp) + delete_selected(text, cursorp) }; Some(CCursorPair::one(ccursor)) } From 4d5400ed4f90ff6683401ba5217e844aee68fa02 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Jun 2021 13:37:26 -0500 Subject: [PATCH 17/18] Appease Clippy. --- egui/src/data/output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 0729efbac81..0f263a03466 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -353,7 +353,7 @@ impl WidgetInfo { Some(prev_text_value) }; Self { - current_text_value: Some(text_value.to_string()), + current_text_value: Some(text_value), prev_text_value, ..Self::new(WidgetType::TextEdit) } From a88a0a922742f0279e77f8d7ac7572cfdb0438ba Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 11 Jun 2021 11:49:58 -0500 Subject: [PATCH 18/18] Mask password fields in generated events. --- egui/src/widgets/text_edit.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index bb39dafcc90..90a59a50c06 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -721,18 +721,36 @@ impl<'t, S: TextBuffer> TextEdit<'t, S> { false }; + let masked = if self.password { + let prev_text_len = prev_text.to_string().len(); + let text_len = text.to_string().len(); + Some(("*".repeat(prev_text_len), "*".repeat(text_len))) + } else { + None + }; + if response.changed { - response.widget_info(|| WidgetInfo::text_edit(&prev_text, &text)); + if let Some((prev_text, text)) = masked { + response.widget_info(|| WidgetInfo::text_edit(&prev_text, &text)); + } else { + response.widget_info(|| WidgetInfo::text_edit(&prev_text, &text)); + } } else if selection_changed { let text_cursor = text_cursor.unwrap(); let char_range = text_cursor.primary.ccursor.index..=text_cursor.secondary.ccursor.index; - let info = WidgetInfo::text_selection_changed(char_range, &*text); + let info = if let Some((_, text)) = masked { + WidgetInfo::text_selection_changed(char_range, text) + } else { + WidgetInfo::text_selection_changed(char_range, &*text) + }; response .ctx .output() .events .push(OutputEvent::TextSelectionChanged(info)); + } else if let Some((prev_text, text)) = masked { + response.widget_info(|| WidgetInfo::text_edit(&prev_text, &text)); } else { response.widget_info(|| WidgetInfo::text_edit(&prev_text, &text)); }