diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd48cc9..bfab560a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [ ### Added ⭐ * Add support for [cint](https://crates.io/crates/cint) under `cint` feature. * Add features `extra_asserts` and `extra_debug_asserts` to enable additional checks. -* Add an option to overwrite frame of SidePanel and TopPanel +* Add an option to overwrite frame of SidePanel and TopPanel. +* `TextEdit` now supports edits on a generic buffer using `TextBuffer`. ## 0.12.0 - 2021-05-10 - Multitouch, user memory, window pivots, and improved plots diff --git a/egui/src/ui.rs b/egui/src/ui.rs index ab8fab2c..e7e34868 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -953,14 +953,20 @@ impl Ui { /// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`). /// /// See also [`TextEdit`]. - pub fn text_edit_singleline(&mut self, text: &mut String) -> Response { + pub fn text_edit_singleline( + &mut self, + text: &mut S, + ) -> Response { TextEdit::singleline(text).ui(self) } /// A `TextEdit` for multiple lines. Pressing enter key will create a new line. /// /// See also [`TextEdit`]. - pub fn text_edit_multiline(&mut self, text: &mut String) -> Response { + pub fn text_edit_multiline( + &mut self, + text: &mut S, + ) -> Response { TextEdit::multiline(text).ui(self) } @@ -969,7 +975,7 @@ impl Ui { /// This will be multiline, monospace, and will insert tabs instead of moving focus. /// /// See also [`TextEdit::code_editor`]. - pub fn code_editor(&mut self, text: &mut String) -> Response { + pub fn code_editor(&mut self, text: &mut S) -> Response { self.add(TextEdit::multiline(text).code_editor()) } diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index fe81162d..8772e39c 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -1,14 +1,15 @@ use crate::{util::undoer::Undoer, *}; use epaint::{text::cursor::*, *}; +use std::ops::Range; #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "persistence", serde(default))] -pub(crate) struct State { +pub(crate) struct State { cursorp: Option, #[cfg_attr(feature = "persistence", serde(skip))] - undoer: Undoer<(CCursorPair, String)>, + undoer: Undoer<(CCursorPair, S)>, // If IME candidate window is shown on this text edit. #[cfg_attr(feature = "persistence", serde(skip))] @@ -108,6 +109,52 @@ impl CCursorPair { } } +/// Trait constraining what types [`TextEdit`] may use as +/// an underlying buffer. +/// +/// Most likely you will use a `String` which implements `TextBuffer`. +pub trait TextBuffer: + AsRef + Into + PartialEq + Clone + Default + Send + Sync + 'static + std::fmt::Display +{ + /// Inserts text `text` into this buffer at character index `ch_idx`. + /// + /// # Notes + /// `ch_idx` is a *character index*, not a byte index. + /// + /// # Return + /// Returns how many *characters* were successfully inserted + fn insert_text(&mut self, text: &str, ch_idx: usize) -> usize; + + /// Deletes a range of text `ch_range` from this buffer. + /// + /// # Notes + /// `ch_range` is a *character range*, not a byte range. + fn delete_char_range(&mut self, ch_range: Range); +} + +impl TextBuffer for String { + fn insert_text(&mut self, text: &str, ch_idx: usize) -> usize { + // Get the byte index from the character index + let byte_idx = self::byte_index_from_char_index(self, ch_idx); + + // Then insert the string + self.insert_str(byte_idx, text); + + text.chars().count() + } + + fn delete_char_range(&mut self, ch_range: Range) { + assert!(ch_range.start <= ch_range.end); + + // Get both byte indices + let byte_start = self::byte_index_from_char_index(self, ch_range.start); + let byte_end = self::byte_index_from_char_index(self, ch_range.end); + + // Then drain all characters within this range + self.drain(byte_start..byte_end); + } +} + /// A text region that the user can edit the contents of. /// /// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`]. @@ -136,8 +183,8 @@ impl CCursorPair { /// #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[derive(Debug)] -pub struct TextEdit<'t> { - text: &'t mut String, +pub struct TextEdit<'t, S: TextBuffer = String> { + text: &'t mut S, hint_text: String, id: Option, id_source: Option, @@ -151,23 +198,23 @@ pub struct TextEdit<'t> { desired_height_rows: usize, lock_focus: bool, } -impl<'t> TextEdit<'t> { +impl<'t, S: TextBuffer> TextEdit<'t, S> { pub fn cursor(ui: &Ui, id: Id) -> Option { ui.memory() .id_data - .get::(&id) + .get::>(&id) .and_then(|state| state.cursorp) } } -impl<'t> TextEdit<'t> { +impl<'t, S: TextBuffer> TextEdit<'t, S> { #[deprecated = "Use `TextEdit::singleline` or `TextEdit::multiline` (or the helper `ui.text_edit_singleline`, `ui.text_edit_multiline`) instead"] - pub fn new(text: &'t mut String) -> Self { + pub fn new(text: &'t mut S) -> Self { Self::multiline(text) } /// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`). - pub fn singleline(text: &'t mut String) -> Self { + pub fn singleline(text: &'t mut S) -> Self { TextEdit { text, hint_text: Default::default(), @@ -186,7 +233,7 @@ impl<'t> TextEdit<'t> { } /// A `TextEdit` for multiple lines. Pressing enter key will create a new line. - pub fn multiline(text: &'t mut String) -> Self { + pub fn multiline(text: &'t mut S) -> Self { TextEdit { text, hint_text: Default::default(), @@ -289,7 +336,7 @@ impl<'t> TextEdit<'t> { } } -impl<'t> Widget for TextEdit<'t> { +impl<'t, S: TextBuffer> Widget for TextEdit<'t, S> { fn ui(self, ui: &mut Ui) -> Response { let frame = self.frame; let where_to_put_background = ui.painter().add(Shape::Noop); @@ -328,7 +375,7 @@ impl<'t> Widget for TextEdit<'t> { } } -impl<'t> TextEdit<'t> { +impl<'t, S: TextBuffer> TextEdit<'t, S> { fn content_ui(self, ui: &mut Ui) -> Response { let TextEdit { text, @@ -372,7 +419,7 @@ impl<'t> TextEdit<'t> { } }; - let mut galley = make_galley(ui, text); + let mut galley = make_galley(ui, text.as_ref()); let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width); let desired_height = (desired_height_rows.at_least(1) as f32) * line_spacing; @@ -389,7 +436,7 @@ impl<'t> TextEdit<'t> { auto_id // Since we are only storing the cursor a persistent Id is not super important } }); - let mut state = ui.memory().id_data.get_or_default::(id).clone(); + let mut state = ui.memory().id_data.get_or_default::>(id).clone(); let sense = if enabled { Sense::click_and_drag() @@ -416,7 +463,7 @@ impl<'t> TextEdit<'t> { if response.double_clicked() { // Select word: let center = cursor_at_pointer; - let ccursorp = select_word_at(text, center.ccursor); + let ccursorp = select_word_at(text.as_ref(), center.ccursor); state.cursorp = Some(CursorPair { primary: galley.from_ccursor(ccursorp.primary), secondary: galley.from_ccursor(ccursorp.secondary), @@ -478,18 +525,24 @@ impl<'t> TextEdit<'t> { let did_mutate_text = match event { Event::Copy => { if cursorp.is_empty() { - copy_if_not_password(ui, text.clone()); + copy_if_not_password(ui, text.as_ref().to_owned()); } else { - copy_if_not_password(ui, selected_str(text, &cursorp).to_owned()); + copy_if_not_password( + ui, + selected_str(text.as_ref(), &cursorp).to_owned(), + ); } None } Event::Cut => { if cursorp.is_empty() { - copy_if_not_password(ui, std::mem::take(text)); + copy_if_not_password(ui, std::mem::take(text).into()); Some(CCursorPair::default()) } else { - copy_if_not_password(ui, selected_str(text, &cursorp).to_owned()); + copy_if_not_password( + ui, + selected_str(text.as_ref(), &cursorp).to_owned(), + ); Some(CCursorPair::one(delete_selected(text, &cursorp))) } } @@ -603,7 +656,7 @@ impl<'t> TextEdit<'t> { response.mark_changed(); // Layout again to avoid frame delay, and to keep `text` and `galley` in sync. - galley = make_galley(ui, text); + galley = make_galley(ui, text.as_ref()); // Set cursorp using new galley: cursorp = CursorPair { @@ -642,7 +695,7 @@ impl<'t> TextEdit<'t> { .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()); ui.painter().galley(response.rect.min, galley, text_color); - if text.is_empty() && !hint_text.is_empty() { + if text.as_ref().is_empty() && !hint_text.is_empty() { let galley = if multiline { ui.fonts() .layout_multiline(text_style, hint_text, available_width) @@ -741,45 +794,26 @@ 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) { - let mut char_it = text.chars(); - let mut new_text = String::with_capacity(text.len() + text_to_insert.len()); - for _ in 0..ccursor.index { - let c = char_it.next().unwrap(); - new_text.push(c); - } - ccursor.index += text_to_insert.chars().count(); - new_text += text_to_insert; - new_text.extend(char_it); - *text = new_text; +fn insert_text(ccursor: &mut CCursor, text: &mut S, text_to_insert: &str) { + ccursor.index += text.insert_text(text_to_insert, ccursor.index); } // ---------------------------------------------------------------------------- -fn delete_selected(text: &mut String, cursorp: &CursorPair) -> CCursor { +fn delete_selected(text: &mut S, cursorp: &CursorPair) -> CCursor { let [min, max] = cursorp.sorted(); delete_selected_ccursor_range(text, [min.ccursor, max.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 { - let mut char_it = text.chars(); - let mut new_text = String::with_capacity(text.len()); - for _ in 0..min { - new_text.push(char_it.next().unwrap()) - } - new_text.extend(char_it.skip(max - min)); - *text = new_text; - } +fn delete_selected_ccursor_range(text: &mut S, [min, max]: [CCursor; 2]) -> CCursor { + text.delete_char_range(min.index..max.index); CCursor { - index: min, + index: min.index, prefer_next_row: true, } } -fn delete_previous_char(text: &mut String, ccursor: CCursor) -> CCursor { +fn delete_previous_char(text: &mut S, ccursor: CCursor) -> CCursor { if ccursor.index > 0 { let max_ccursor = ccursor; let min_ccursor = max_ccursor - 1; @@ -789,22 +823,22 @@ fn delete_previous_char(text: &mut String, ccursor: CCursor) -> CCursor { } } -fn delete_next_char(text: &mut String, ccursor: CCursor) -> CCursor { +fn delete_next_char(text: &mut S, ccursor: CCursor) -> CCursor { delete_selected_ccursor_range(text, [ccursor, ccursor + 1]) } -fn delete_previous_word(text: &mut String, max_ccursor: CCursor) -> CCursor { - let min_ccursor = ccursor_previous_word(text, max_ccursor); +fn delete_previous_word(text: &mut S, max_ccursor: CCursor) -> CCursor { + let min_ccursor = ccursor_previous_word(text.as_ref(), max_ccursor); delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) } -fn delete_next_word(text: &mut String, min_ccursor: CCursor) -> CCursor { - let max_ccursor = ccursor_next_word(text, min_ccursor); +fn delete_next_word(text: &mut S, min_ccursor: CCursor) -> CCursor { + let max_ccursor = ccursor_next_word(text.as_ref(), min_ccursor); delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) } -fn delete_paragraph_before_cursor( - text: &mut String, +fn delete_paragraph_before_cursor( + text: &mut S, galley: &Galley, cursorp: &CursorPair, ) -> CCursor { @@ -821,8 +855,8 @@ fn delete_paragraph_before_cursor( } } -fn delete_paragraph_after_cursor( - text: &mut String, +fn delete_paragraph_after_cursor( + text: &mut S, galley: &Galley, cursorp: &CursorPair, ) -> CCursor { @@ -842,9 +876,9 @@ fn delete_paragraph_after_cursor( // ---------------------------------------------------------------------------- /// Returns `Some(new_cursor)` if we did mutate `text`. -fn on_key_press( +fn on_key_press( cursorp: &mut CursorPair, - text: &mut String, + text: &mut S, galley: &Galley, key: Key, modifiers: &Modifiers, @@ -1078,39 +1112,25 @@ fn find_line_start(text: &str, current_index: CCursor) -> CCursor { } } -fn decrease_identation(ccursor: &mut CCursor, text: &mut String) { - let mut new_text = String::with_capacity(text.len()); +fn decrease_identation(ccursor: &mut CCursor, text: &mut S) { + let line_start = find_line_start(text.as_ref(), *ccursor); - let line_start = find_line_start(text, *ccursor); + let remove_len = if text.as_ref()[line_start.index..].starts_with('\t') { + Some(1) + } else if text.as_ref()[line_start.index..] + .chars() + .take(text::TAB_SIZE) + .all(|c| c == ' ') + { + Some(text::TAB_SIZE) + } else { + None + }; - let mut char_it = text.chars().peekable(); - for _ in 0..line_start.index { - let c = char_it.next().unwrap(); - new_text.push(c); - } - - let mut chars_removed = 0; - while let Some(&c) = char_it.peek() { - if c == '\t' { - char_it.next(); - chars_removed += 1; - break; - } else if c == ' ' { - char_it.next(); - chars_removed += 1; - if chars_removed == text::TAB_SIZE { - break; - } - } else { - break; + if let Some(len) = remove_len { + text.delete_char_range(line_start.index..(line_start.index + len)); + if *ccursor != line_start { + *ccursor -= len; } } - - new_text.extend(char_it); - - *text = new_text; - - if *ccursor != line_start { - *ccursor -= chars_removed; - } }