Add support for buffers other than a String in TextEdit (#399)

* Initial design for `TextBuffer` trait, to allow `TextEdit` to edit types other than `String`.

* Moved `insert_text` implementation into `TextBuffer`.
This allows the user to implement text inserting depedent on their type instead of using a `String` and converting back to `S`, which may be a lossless convertion.

* Moved part of `delete_selected_ccursor_range` implementation into `TextBuffer::delete_range`.

* `TextBuffer::insert_text` not returns how many characters were inserted into the buffer.
This allows implementations to "saturate" the buffer, only allowing for a limited length of characters to be inserted.

* Now using `byte_index_from_char_index` instead of custom implementation.

* `decrease_identation` impl now modified the string in-place.
Removed `From<String>` bound for `TextBuffer`.

* Added changes to changelog.

* Moved updated changelog to .

* Updated documentation on `TextBuffer`.

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* Renamed `TextBuffer::delete_text_range` to `delete_char_range`.

Co-authored-by: Filipe Rodrigues <filipejacintorodrigues1@gmail.com>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Zenithsiz 2021-05-20 20:00:50 +01:00 committed by GitHub
parent d292b831a1
commit 57981d49ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 120 additions and 93 deletions

View file

@ -10,7 +10,8 @@ NOTE: [`eframe`](eframe/CHANGELOG.md), [`egui_web`](egui_web/CHANGELOG.md) and [
### Added ⭐ ### Added ⭐
* Add support for [cint](https://crates.io/crates/cint) under `cint` feature. * 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 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 ## 0.12.0 - 2021-05-10 - Multitouch, user memory, window pivots, and improved plots

View file

@ -953,14 +953,20 @@ impl Ui {
/// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`). /// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`).
/// ///
/// See also [`TextEdit`]. /// See also [`TextEdit`].
pub fn text_edit_singleline(&mut self, text: &mut String) -> Response { pub fn text_edit_singleline<S: widgets::text_edit::TextBuffer>(
&mut self,
text: &mut S,
) -> Response {
TextEdit::singleline(text).ui(self) TextEdit::singleline(text).ui(self)
} }
/// A `TextEdit` for multiple lines. Pressing enter key will create a new line. /// A `TextEdit` for multiple lines. Pressing enter key will create a new line.
/// ///
/// See also [`TextEdit`]. /// See also [`TextEdit`].
pub fn text_edit_multiline(&mut self, text: &mut String) -> Response { pub fn text_edit_multiline<S: widgets::text_edit::TextBuffer>(
&mut self,
text: &mut S,
) -> Response {
TextEdit::multiline(text).ui(self) TextEdit::multiline(text).ui(self)
} }
@ -969,7 +975,7 @@ impl Ui {
/// This will be multiline, monospace, and will insert tabs instead of moving focus. /// This will be multiline, monospace, and will insert tabs instead of moving focus.
/// ///
/// See also [`TextEdit::code_editor`]. /// See also [`TextEdit::code_editor`].
pub fn code_editor(&mut self, text: &mut String) -> Response { pub fn code_editor<S: widgets::text_edit::TextBuffer>(&mut self, text: &mut S) -> Response {
self.add(TextEdit::multiline(text).code_editor()) self.add(TextEdit::multiline(text).code_editor())
} }

View file

@ -1,14 +1,15 @@
use crate::{util::undoer::Undoer, *}; use crate::{util::undoer::Undoer, *};
use epaint::{text::cursor::*, *}; use epaint::{text::cursor::*, *};
use std::ops::Range;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))] #[cfg_attr(feature = "persistence", serde(default))]
pub(crate) struct State { pub(crate) struct State<S: TextBuffer> {
cursorp: Option<CursorPair>, cursorp: Option<CursorPair>,
#[cfg_attr(feature = "persistence", serde(skip))] #[cfg_attr(feature = "persistence", serde(skip))]
undoer: Undoer<(CCursorPair, String)>, undoer: Undoer<(CCursorPair, S)>,
// If IME candidate window is shown on this text edit. // If IME candidate window is shown on this text edit.
#[cfg_attr(feature = "persistence", serde(skip))] #[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<str> + Into<String> + 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<usize>);
}
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<usize>) {
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. /// A text region that the user can edit the contents of.
/// ///
/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`]. /// 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);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug)] #[derive(Debug)]
pub struct TextEdit<'t> { pub struct TextEdit<'t, S: TextBuffer = String> {
text: &'t mut String, text: &'t mut S,
hint_text: String, hint_text: String,
id: Option<Id>, id: Option<Id>,
id_source: Option<Id>, id_source: Option<Id>,
@ -151,23 +198,23 @@ pub struct TextEdit<'t> {
desired_height_rows: usize, desired_height_rows: usize,
lock_focus: bool, lock_focus: bool,
} }
impl<'t> TextEdit<'t> { impl<'t, S: TextBuffer> TextEdit<'t, S> {
pub fn cursor(ui: &Ui, id: Id) -> Option<CursorPair> { pub fn cursor(ui: &Ui, id: Id) -> Option<CursorPair> {
ui.memory() ui.memory()
.id_data .id_data
.get::<State>(&id) .get::<State<S>>(&id)
.and_then(|state| state.cursorp) .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"] #[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) Self::multiline(text)
} }
/// No newlines (`\n`) allowed. Pressing enter key will result in the `TextEdit` losing focus (`response.lost_focus`). /// 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 { TextEdit {
text, text,
hint_text: Default::default(), 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. /// 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 { TextEdit {
text, text,
hint_text: Default::default(), 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 { fn ui(self, ui: &mut Ui) -> Response {
let frame = self.frame; let frame = self.frame;
let where_to_put_background = ui.painter().add(Shape::Noop); 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 { fn content_ui(self, ui: &mut Ui) -> Response {
let TextEdit { let TextEdit {
text, 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_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
let desired_height = (desired_height_rows.at_least(1) as f32) * line_spacing; 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 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::<State>(id).clone(); let mut state = ui.memory().id_data.get_or_default::<State<S>>(id).clone();
let sense = if enabled { let sense = if enabled {
Sense::click_and_drag() Sense::click_and_drag()
@ -416,7 +463,7 @@ impl<'t> TextEdit<'t> {
if response.double_clicked() { if response.double_clicked() {
// Select word: // Select word:
let center = cursor_at_pointer; 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 { state.cursorp = Some(CursorPair {
primary: galley.from_ccursor(ccursorp.primary), primary: galley.from_ccursor(ccursorp.primary),
secondary: galley.from_ccursor(ccursorp.secondary), secondary: galley.from_ccursor(ccursorp.secondary),
@ -478,18 +525,24 @@ impl<'t> TextEdit<'t> {
let did_mutate_text = match event { let did_mutate_text = match event {
Event::Copy => { Event::Copy => {
if cursorp.is_empty() { if cursorp.is_empty() {
copy_if_not_password(ui, text.clone()); copy_if_not_password(ui, text.as_ref().to_owned());
} else { } 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 None
} }
Event::Cut => { Event::Cut => {
if cursorp.is_empty() { 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()) Some(CCursorPair::default())
} else { } 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))) Some(CCursorPair::one(delete_selected(text, &cursorp)))
} }
} }
@ -603,7 +656,7 @@ impl<'t> TextEdit<'t> {
response.mark_changed(); response.mark_changed();
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync. // 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: // Set cursorp using new galley:
cursorp = CursorPair { cursorp = CursorPair {
@ -642,7 +695,7 @@ impl<'t> TextEdit<'t> {
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()); .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
ui.painter().galley(response.rect.min, galley, 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 { let galley = if multiline {
ui.fonts() ui.fonts()
.layout_multiline(text_style, hint_text, available_width) .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() s.len()
} }
fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) { fn insert_text<S: TextBuffer>(ccursor: &mut CCursor, text: &mut S, text_to_insert: &str) {
let mut char_it = text.chars(); ccursor.index += text.insert_text(text_to_insert, ccursor.index);
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 delete_selected(text: &mut String, cursorp: &CursorPair) -> CCursor { fn delete_selected<S: TextBuffer>(text: &mut S, cursorp: &CursorPair) -> CCursor {
let [min, max] = cursorp.sorted(); let [min, max] = cursorp.sorted();
delete_selected_ccursor_range(text, [min.ccursor, max.ccursor]) delete_selected_ccursor_range(text, [min.ccursor, max.ccursor])
} }
fn delete_selected_ccursor_range(text: &mut String, [min, max]: [CCursor; 2]) -> CCursor { fn delete_selected_ccursor_range<S: TextBuffer>(text: &mut S, [min, max]: [CCursor; 2]) -> CCursor {
let [min, max] = [min.index, max.index]; text.delete_char_range(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;
}
CCursor { CCursor {
index: min, index: min.index,
prefer_next_row: true, prefer_next_row: true,
} }
} }
fn delete_previous_char(text: &mut String, ccursor: CCursor) -> CCursor { fn delete_previous_char<S: TextBuffer>(text: &mut S, ccursor: CCursor) -> CCursor {
if ccursor.index > 0 { if ccursor.index > 0 {
let max_ccursor = ccursor; let max_ccursor = ccursor;
let min_ccursor = max_ccursor - 1; 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<S: TextBuffer>(text: &mut S, ccursor: CCursor) -> CCursor {
delete_selected_ccursor_range(text, [ccursor, ccursor + 1]) delete_selected_ccursor_range(text, [ccursor, ccursor + 1])
} }
fn delete_previous_word(text: &mut String, max_ccursor: CCursor) -> CCursor { fn delete_previous_word<S: TextBuffer>(text: &mut S, max_ccursor: CCursor) -> CCursor {
let min_ccursor = ccursor_previous_word(text, max_ccursor); let min_ccursor = ccursor_previous_word(text.as_ref(), max_ccursor);
delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) delete_selected_ccursor_range(text, [min_ccursor, max_ccursor])
} }
fn delete_next_word(text: &mut String, min_ccursor: CCursor) -> CCursor { fn delete_next_word<S: TextBuffer>(text: &mut S, min_ccursor: CCursor) -> CCursor {
let max_ccursor = ccursor_next_word(text, min_ccursor); let max_ccursor = ccursor_next_word(text.as_ref(), min_ccursor);
delete_selected_ccursor_range(text, [min_ccursor, max_ccursor]) delete_selected_ccursor_range(text, [min_ccursor, max_ccursor])
} }
fn delete_paragraph_before_cursor( fn delete_paragraph_before_cursor<S: TextBuffer>(
text: &mut String, text: &mut S,
galley: &Galley, galley: &Galley,
cursorp: &CursorPair, cursorp: &CursorPair,
) -> CCursor { ) -> CCursor {
@ -821,8 +855,8 @@ fn delete_paragraph_before_cursor(
} }
} }
fn delete_paragraph_after_cursor( fn delete_paragraph_after_cursor<S: TextBuffer>(
text: &mut String, text: &mut S,
galley: &Galley, galley: &Galley,
cursorp: &CursorPair, cursorp: &CursorPair,
) -> CCursor { ) -> CCursor {
@ -842,9 +876,9 @@ fn delete_paragraph_after_cursor(
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// Returns `Some(new_cursor)` if we did mutate `text`. /// Returns `Some(new_cursor)` if we did mutate `text`.
fn on_key_press( fn on_key_press<S: TextBuffer>(
cursorp: &mut CursorPair, cursorp: &mut CursorPair,
text: &mut String, text: &mut S,
galley: &Galley, galley: &Galley,
key: Key, key: Key,
modifiers: &Modifiers, 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) { fn decrease_identation<S: TextBuffer>(ccursor: &mut CCursor, text: &mut S) {
let mut new_text = String::with_capacity(text.len()); 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)
let mut char_it = text.chars().peekable(); } else if text.as_ref()[line_start.index..]
for _ in 0..line_start.index { .chars()
let c = char_it.next().unwrap(); .take(text::TAB_SIZE)
new_text.push(c); .all(|c| c == ' ')
} {
Some(text::TAB_SIZE)
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 { } else {
break; None
} };
}
new_text.extend(char_it);
*text = new_text;
if let Some(len) = remove_len {
text.delete_char_range(line_start.index..(line_start.index + len));
if *ccursor != line_start { if *ccursor != line_start {
*ccursor -= chars_removed; *ccursor -= len;
}
} }
} }