From 0aa565561854443b11a439f4c8533b740cf3b3c2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 19 Jan 2024 14:58:03 +0100 Subject: [PATCH] Add helpful members to `trait TextBuffer` --- crates/egui/src/lib.rs | 2 +- crates/egui/src/widgets/text_edit/builder.rs | 185 +++--------------- crates/egui/src/widgets/text_edit/output.rs | 2 +- .../egui/src/widgets/text_edit/text_buffer.rs | 147 +++++++++++++- .../src/easy_mark/easy_mark_editor.rs | 2 +- 5 files changed, 168 insertions(+), 170 deletions(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index ba8318de346..86330546113 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -399,7 +399,7 @@ pub use epaint::{ }; pub mod text { - pub use crate::text_selection::CCursorRange; + pub use crate::text_selection::{CCursorRange, CursorRange}; pub use epaint::text::{ cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat, TextWrapping, TAB_SIZE, diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index a904e403050..eae8a6e4159 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -8,16 +8,11 @@ use crate::{ text_selection::{ text_cursor_state::cursor_rect, visuals::{paint_cursor, paint_text_selection}, - CursorRange, + CCursorRange, CursorRange, }, *, }; -use self::text_selection::{ - text_cursor_state::{ccursor_next_word, ccursor_previous_word, find_line_start}, - CCursorRange, -}; - use super::{TextEditOutput, TextEditState}; /// A text region that the user can edit the contents of. @@ -823,14 +818,14 @@ fn events( Some(CCursorRange::default()) } else { copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned()); - Some(CCursorRange::one(delete_selected(text, &cursor_range))) + Some(CCursorRange::one(text.delete_selected(&cursor_range))) } } Event::Paste(text_to_insert) => { if !text_to_insert.is_empty() { - let mut ccursor = delete_selected(text, &cursor_range); + let mut ccursor = text.delete_selected(&cursor_range); - insert_text(&mut ccursor, text, text_to_insert, char_limit); + text.insert_text_at(&mut ccursor, text_to_insert, char_limit); Some(CCursorRange::one(ccursor)) } else { @@ -840,9 +835,9 @@ fn events( Event::Text(text_to_insert) => { // Newlines are handled by `Key::Enter`. if !text_to_insert.is_empty() && text_to_insert != "\n" && text_to_insert != "\r" { - let mut ccursor = delete_selected(text, &cursor_range); + let mut ccursor = text.delete_selected(&cursor_range); - insert_text(&mut ccursor, text, text_to_insert, char_limit); + text.insert_text_at(&mut ccursor, text_to_insert, char_limit); Some(CCursorRange::one(ccursor)) } else { @@ -855,12 +850,12 @@ fn events( modifiers, .. } if multiline => { - let mut ccursor = delete_selected(text, &cursor_range); + let mut ccursor = text.delete_selected(&cursor_range); if modifiers.shift { // TODO(emilk): support removing indentation over a selection? - decrease_indentation(&mut ccursor, text); + text.decrease_indentation(&mut ccursor); } else { - insert_text(&mut ccursor, text, "\t", char_limit); + text.insert_text_at(&mut ccursor, "\t", char_limit); } Some(CCursorRange::one(ccursor)) } @@ -870,8 +865,8 @@ fn events( .. } => { if multiline { - let mut ccursor = delete_selected(text, &cursor_range); - insert_text(&mut ccursor, text, "\n", char_limit); + let mut ccursor = text.delete_selected(&cursor_range); + text.insert_text_at(&mut ccursor, "\n", char_limit); // TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket Some(CCursorRange::one(ccursor)) } else { @@ -933,10 +928,10 @@ fn events( // empty prediction can be produced when user press backspace // or escape during ime. We should clear current text. if text_mark != "\n" && text_mark != "\r" && state.has_ime { - let mut ccursor = delete_selected(text, &cursor_range); + let mut ccursor = text.delete_selected(&cursor_range); let start_cursor = ccursor; if !text_mark.is_empty() { - insert_text(&mut ccursor, text, text_mark, char_limit); + text.insert_text_at(&mut ccursor, text_mark, char_limit); } Some(CCursorRange::two(start_cursor, ccursor)) } else { @@ -948,9 +943,9 @@ fn events( // CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first, so do not check `state.has_ime = true` in the following statement. if prediction != "\n" && prediction != "\r" { state.has_ime = false; - let mut ccursor = delete_selected(text, &cursor_range); + let mut ccursor = text.delete_selected(&cursor_range); if !prediction.is_empty() { - insert_text(&mut ccursor, text, prediction, char_limit); + text.insert_text_at(&mut ccursor, prediction, char_limit); } Some(CCursorRange::one(ccursor)) } else { @@ -987,105 +982,6 @@ fn events( // ---------------------------------------------------------------------------- -fn insert_text( - ccursor: &mut CCursor, - text: &mut dyn TextBuffer, - text_to_insert: &str, - char_limit: usize, -) { - if char_limit < usize::MAX { - let mut new_string = text_to_insert; - // Avoid subtract with overflow panic - let cutoff = char_limit.saturating_sub(text.as_str().chars().count()); - - new_string = match new_string.char_indices().nth(cutoff) { - None => new_string, - Some((idx, _)) => &new_string[..idx], - }; - - ccursor.index += text.insert_text(new_string, ccursor.index); - } else { - ccursor.index += text.insert_text(text_to_insert, ccursor.index); - } -} - -// ---------------------------------------------------------------------------- - -fn delete_selected(text: &mut dyn TextBuffer, cursor_range: &CursorRange) -> CCursor { - let [min, max] = cursor_range.sorted_cursors(); - delete_selected_ccursor_range(text, [min.ccursor, max.ccursor]) -} - -fn delete_selected_ccursor_range(text: &mut dyn TextBuffer, [min, max]: [CCursor; 2]) -> CCursor { - text.delete_char_range(min.index..max.index); - CCursor { - index: min.index, - prefer_next_row: true, - } -} - -fn delete_previous_char(text: &mut dyn TextBuffer, 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]) - } else { - ccursor - } -} - -fn delete_next_char(text: &mut dyn TextBuffer, ccursor: CCursor) -> CCursor { - delete_selected_ccursor_range(text, [ccursor, ccursor + 1]) -} - -fn delete_previous_word(text: &mut dyn TextBuffer, max_ccursor: CCursor) -> CCursor { - let min_ccursor = ccursor_previous_word(text.as_str(), max_ccursor); - delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) -} - -fn delete_next_word(text: &mut dyn TextBuffer, min_ccursor: CCursor) -> CCursor { - let max_ccursor = ccursor_next_word(text.as_str(), min_ccursor); - delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) -} - -fn delete_paragraph_before_cursor( - text: &mut dyn TextBuffer, - galley: &Galley, - cursor_range: &CursorRange, -) -> CCursor { - let [min, max] = cursor_range.sorted_cursors(); - let min = galley.from_pcursor(PCursor { - paragraph: min.pcursor.paragraph, - offset: 0, - prefer_next_row: true, - }); - if min.ccursor == max.ccursor { - delete_previous_char(text, min.ccursor) - } else { - delete_selected(text, &CursorRange::two(min, max)) - } -} - -fn delete_paragraph_after_cursor( - text: &mut dyn TextBuffer, - galley: &Galley, - cursor_range: &CursorRange, -) -> CCursor { - let [min, max] = cursor_range.sorted_cursors(); - let max = galley.from_pcursor(PCursor { - paragraph: max.pcursor.paragraph, - offset: usize::MAX, // end of paragraph - prefer_next_row: false, - }); - if min.ccursor == max.ccursor { - delete_next_char(text, min.ccursor) - } else { - delete_selected(text, &CursorRange::two(min, max)) - } -} - -// ---------------------------------------------------------------------------- - /// Returns `Some(new_cursor)` if we did mutate `text`. fn check_for_mutating_key_press( os: OperatingSystem, @@ -1098,32 +994,32 @@ fn check_for_mutating_key_press( match key { Key::Backspace => { let ccursor = if modifiers.mac_cmd { - delete_paragraph_before_cursor(text, galley, cursor_range) + text.delete_paragraph_before_cursor(galley, cursor_range) } else if let Some(cursor) = cursor_range.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - delete_previous_word(text, cursor.ccursor) + text.delete_previous_word(cursor.ccursor) } else { - delete_previous_char(text, cursor.ccursor) + text.delete_previous_char(cursor.ccursor) } } else { - delete_selected(text, cursor_range) + text.delete_selected(cursor_range) }; Some(CCursorRange::one(ccursor)) } Key::Delete if !modifiers.shift || os != OperatingSystem::Windows => { let ccursor = if modifiers.mac_cmd { - delete_paragraph_after_cursor(text, galley, cursor_range) + text.delete_paragraph_after_cursor(galley, cursor_range) } else if let Some(cursor) = cursor_range.single() { if modifiers.alt || modifiers.ctrl { // alt on mac, ctrl on windows - delete_next_word(text, cursor.ccursor) + text.delete_next_word(cursor.ccursor) } else { - delete_next_char(text, cursor.ccursor) + text.delete_next_char(cursor.ccursor) } } else { - delete_selected(text, cursor_range) + text.delete_selected(cursor_range) }; let ccursor = CCursor { prefer_next_row: true, @@ -1133,25 +1029,25 @@ fn check_for_mutating_key_press( } Key::H if modifiers.ctrl => { - let ccursor = delete_previous_char(text, cursor_range.primary.ccursor); + let ccursor = text.delete_previous_char(cursor_range.primary.ccursor); Some(CCursorRange::one(ccursor)) } Key::K if modifiers.ctrl => { - let ccursor = delete_paragraph_after_cursor(text, galley, cursor_range); + let ccursor = text.delete_paragraph_after_cursor(galley, cursor_range); Some(CCursorRange::one(ccursor)) } Key::U if modifiers.ctrl => { - let ccursor = delete_paragraph_before_cursor(text, galley, cursor_range); + let ccursor = text.delete_paragraph_before_cursor(galley, cursor_range); Some(CCursorRange::one(ccursor)) } Key::W if modifiers.ctrl => { let ccursor = if let Some(cursor) = cursor_range.single() { - delete_previous_word(text, cursor.ccursor) + text.delete_previous_word(cursor.ccursor) } else { - delete_selected(text, cursor_range) + text.delete_selected(cursor_range) }; Some(CCursorRange::one(ccursor)) } @@ -1159,28 +1055,3 @@ fn check_for_mutating_key_press( _ => None, } } - -// ---------------------------------------------------------------------------- - -fn decrease_indentation(ccursor: &mut CCursor, text: &mut dyn TextBuffer) { - let line_start = find_line_start(text.as_str(), *ccursor); - - let remove_len = if text.as_str()[line_start.index..].starts_with('\t') { - Some(1) - } else if text.as_str()[line_start.index..] - .chars() - .take(text::TAB_SIZE) - .all(|c| c == ' ') - { - Some(text::TAB_SIZE) - } else { - None - }; - - if let Some(len) = remove_len { - text.delete_char_range(line_start.index..(line_start.index + len)); - if *ccursor != line_start { - *ccursor -= len; - } - } -} diff --git a/crates/egui/src/widgets/text_edit/output.rs b/crates/egui/src/widgets/text_edit/output.rs index f56b5471e93..d02c1d1c59a 100644 --- a/crates/egui/src/widgets/text_edit/output.rs +++ b/crates/egui/src/widgets/text_edit/output.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::text_selection::CursorRange; +use crate::text::CursorRange; /// The output from a [`TextEdit`](crate::TextEdit). pub struct TextEditOutput { diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index a84727419f3..f28878a1dd3 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -1,6 +1,20 @@ use std::{borrow::Cow, ops::Range}; -use crate::text_selection::text_cursor_state::{byte_index_from_char_index, slice_char_range}; +use epaint::{ + text::{ + cursor::{CCursor, PCursor}, + TAB_SIZE, + }, + Galley, +}; + +use crate::text_selection::{ + text_cursor_state::{ + byte_index_from_char_index, ccursor_next_word, ccursor_previous_word, find_line_start, + slice_char_range, + }, + CursorRange, +}; /// Trait constraining what types [`crate::TextEdit`] may use as /// an underlying buffer. @@ -13,15 +27,6 @@ pub trait TextBuffer { /// Returns this buffer as a `str`. fn as_str(&self) -> &str; - /// Reads the given character range. - fn char_range(&self, char_range: Range) -> &str { - slice_char_range(self.as_str(), char_range) - } - - fn byte_index_from_char_index(&self, char_index: usize) -> usize { - byte_index_from_char_index(self.as_str(), char_index) - } - /// Inserts text `text` into this buffer at character index `char_index`. /// /// # Notes @@ -37,6 +42,15 @@ pub trait TextBuffer { /// `char_range` is a *character range*, not a byte range. fn delete_char_range(&mut self, char_range: Range); + /// Reads the given character range. + fn char_range(&self, char_range: Range) -> &str { + slice_char_range(self.as_str(), char_range) + } + + fn byte_index_from_char_index(&self, char_index: usize) -> usize { + byte_index_from_char_index(self.as_str(), char_index) + } + /// Clears all characters in this buffer fn clear(&mut self) { self.delete_char_range(0..self.as_str().len()); @@ -54,6 +68,119 @@ pub trait TextBuffer { self.clear(); s } + + fn insert_text_at(&mut self, ccursor: &mut CCursor, text_to_insert: &str, char_limit: usize) { + if char_limit < usize::MAX { + let mut new_string = text_to_insert; + // Avoid subtract with overflow panic + let cutoff = char_limit.saturating_sub(self.as_str().chars().count()); + + new_string = match new_string.char_indices().nth(cutoff) { + None => new_string, + Some((idx, _)) => &new_string[..idx], + }; + + ccursor.index += self.insert_text(new_string, ccursor.index); + } else { + ccursor.index += self.insert_text(text_to_insert, ccursor.index); + } + } + + fn decrease_indentation(&mut self, ccursor: &mut CCursor) { + let line_start = find_line_start(self.as_str(), *ccursor); + + let remove_len = if self.as_str()[line_start.index..].starts_with('\t') { + Some(1) + } else if self.as_str()[line_start.index..] + .chars() + .take(TAB_SIZE) + .all(|c| c == ' ') + { + Some(TAB_SIZE) + } else { + None + }; + + if let Some(len) = remove_len { + self.delete_char_range(line_start.index..(line_start.index + len)); + if *ccursor != line_start { + *ccursor -= len; + } + } + } + + fn delete_selected(&mut self, cursor_range: &CursorRange) -> CCursor { + let [min, max] = cursor_range.sorted_cursors(); + self.delete_selected_ccursor_range([min.ccursor, max.ccursor]) + } + + fn delete_selected_ccursor_range(&mut self, [min, max]: [CCursor; 2]) -> CCursor { + self.delete_char_range(min.index..max.index); + CCursor { + index: min.index, + prefer_next_row: true, + } + } + + fn delete_previous_char(&mut self, ccursor: CCursor) -> CCursor { + if ccursor.index > 0 { + let max_ccursor = ccursor; + let min_ccursor = max_ccursor - 1; + self.delete_selected_ccursor_range([min_ccursor, max_ccursor]) + } else { + ccursor + } + } + + fn delete_next_char(&mut self, ccursor: CCursor) -> CCursor { + self.delete_selected_ccursor_range([ccursor, ccursor + 1]) + } + + fn delete_previous_word(&mut self, max_ccursor: CCursor) -> CCursor { + let min_ccursor = ccursor_previous_word(self.as_str(), max_ccursor); + self.delete_selected_ccursor_range([min_ccursor, max_ccursor]) + } + + fn delete_next_word(&mut self, min_ccursor: CCursor) -> CCursor { + let max_ccursor = ccursor_next_word(self.as_str(), min_ccursor); + self.delete_selected_ccursor_range([min_ccursor, max_ccursor]) + } + + fn delete_paragraph_before_cursor( + &mut self, + galley: &Galley, + cursor_range: &CursorRange, + ) -> CCursor { + let [min, max] = cursor_range.sorted_cursors(); + let min = galley.from_pcursor(PCursor { + paragraph: min.pcursor.paragraph, + offset: 0, + prefer_next_row: true, + }); + if min.ccursor == max.ccursor { + self.delete_previous_char(min.ccursor) + } else { + self.delete_selected(&CursorRange::two(min, max)) + } + } + + fn delete_paragraph_after_cursor( + &mut self, + galley: &Galley, + cursor_range: &CursorRange, + ) -> CCursor { + let [min, max] = cursor_range.sorted_cursors(); + let max = galley.from_pcursor(PCursor { + paragraph: max.pcursor.paragraph, + offset: usize::MAX, // end of paragraph + prefer_next_row: false, + }); + if min.ccursor == max.ccursor { + self.delete_next_char(min.ccursor) + } else { + self.delete_selected(&CursorRange::two(min, max)) + } + } } impl TextBuffer for String { diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 95fb5b7d5ad..0971abd45b2 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -1,4 +1,4 @@ -use egui::{text_edit::CCursorRange, *}; +use egui::{text::CCursorRange, *}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))]