1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 05:40:03 -04:00

Add read/write of TextEdit cursor state (#848)

* Rename `CursorPair` to `CursorRange`
* Easymark editor: add keyboard shortcuts to toggle bold, italics etc
* Split up TextEdit into separate files
* Add TextEdit::show that returns a rich TextEditOutput object with response, galley and cursor
* Rename text_edit::State to TextEditState
This commit is contained in:
Emil Ernerfeldt
2021-10-27 16:30:14 +02:00
committed by GitHub
parent ddd52f47c5
commit c7638ca7f5
11 changed files with 810 additions and 533 deletions

View File

@@ -14,7 +14,7 @@ pub(crate) mod window;
pub use {
area::Area,
collapsing_header::*,
collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*,
frame::Frame,
panel::{CentralPanel, SidePanel, TopBottomPanel},

View File

@@ -269,6 +269,12 @@ impl Modifiers {
pub fn shift_only(&self) -> bool {
self.shift && !(self.alt || self.command)
}
/// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed.
#[inline(always)]
pub fn command_only(&self) -> bool {
!self.alt && !self.shift && self.command
}
}
/// Keyboard keys.

View File

@@ -17,7 +17,7 @@ mod progress_bar;
mod selected_label;
mod separator;
mod slider;
pub(crate) mod text_edit;
pub mod text_edit;
pub use button::*;
pub use drag_value::DragValue;
@@ -28,7 +28,7 @@ pub use progress_bar::ProgressBar;
pub use selected_label::SelectableLabel;
pub use separator::Separator;
pub use slider::*;
pub use text_edit::*;
pub use text_edit::{TextBuffer, TextEdit};
// ----------------------------------------------------------------------------
@@ -80,6 +80,11 @@ where
}
}
/// Helper so that you can do `TextEdit::State::read…`
pub trait WidgetWithState {
type State;
}
// ----------------------------------------------------------------------------
/// Show a button to reset a value to its default.

View File

@@ -0,0 +1,130 @@
use epaint::text::cursor::*;
/// A selected text range (could be a range of length zero).
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct CursorRange {
/// When selecting with a mouse, this is where the mouse was released.
/// When moving with e.g. shift+arrows, this is what moves.
/// Note that the two ends can come in any order, and also be equal (no selection).
pub primary: Cursor,
/// When selecting with a mouse, this is where the mouse was first pressed.
/// This part of the cursor does not move when shift is down.
pub secondary: Cursor,
}
impl CursorRange {
/// The empty range.
pub fn one(cursor: Cursor) -> Self {
Self {
primary: cursor,
secondary: cursor,
}
}
pub fn two(min: Cursor, max: Cursor) -> Self {
Self {
primary: max,
secondary: min,
}
}
pub fn as_ccursor_range(&self) -> CCursorRange {
CCursorRange {
primary: self.primary.ccursor,
secondary: self.secondary.ccursor,
}
}
/// True if the selected range contains no characters.
pub fn is_empty(&self) -> bool {
self.primary.ccursor == self.secondary.ccursor
}
/// If there is a selection, None is returned.
/// If the two ends is the same, that is returned.
pub fn single(&self) -> Option<Cursor> {
if self.is_empty() {
Some(self.primary)
} else {
None
}
}
pub fn is_sorted(&self) -> bool {
let p = self.primary.ccursor;
let s = self.secondary.ccursor;
(p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
}
/// returns the two ends ordered
pub fn sorted(&self) -> [Cursor; 2] {
if self.is_sorted() {
[self.primary, self.secondary]
} else {
[self.secondary, self.primary]
}
}
}
/// A selected text range (could be a range of length zero).
///
/// The selection is based on character count (NOT byte count!).
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct CCursorRange {
/// When selecting with a mouse, this is where the mouse was released.
/// When moving with e.g. shift+arrows, this is what moves.
/// Note that the two ends can come in any order, and also be equal (no selection).
pub primary: CCursor,
/// When selecting with a mouse, this is where the mouse was first pressed.
/// This part of the cursor does not move when shift is down.
pub secondary: CCursor,
}
impl CCursorRange {
/// The empty range.
pub fn one(ccursor: CCursor) -> Self {
Self {
primary: ccursor,
secondary: ccursor,
}
}
pub fn two(min: CCursor, max: CCursor) -> Self {
Self {
primary: max,
secondary: min,
}
}
pub fn is_sorted(&self) -> bool {
let p = self.primary;
let s = self.secondary;
(p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
}
/// returns the two ends ordered
pub fn sorted(&self) -> [CCursor; 2] {
if self.is_sorted() {
[self.primary, self.secondary]
} else {
[self.secondary, self.primary]
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PCursorRange {
/// When selecting with a mouse, this is where the mouse was released.
/// When moving with e.g. shift+arrows, this is what moves.
/// Note that the two ends can come in any order, and also be equal (no selection).
pub primary: PCursor,
/// When selecting with a mouse, this is where the mouse was first pressed.
/// This part of the cursor does not move when shift is down.
pub secondary: PCursor,
}

View File

@@ -0,0 +1,10 @@
mod builder;
mod cursor_range;
mod output;
mod state;
mod text_buffer;
pub use {
builder::TextEdit, cursor_range::*, output::TextEditOutput, state::TextEditState,
text_buffer::TextBuffer,
};

View File

@@ -0,0 +1,16 @@
use std::sync::Arc;
/// The output from a `TextEdit`.
pub struct TextEditOutput {
/// The interaction response.
pub response: crate::Response,
/// How the text was displayed.
pub galley: Arc<crate::Galley>,
/// The state we stored after the run/
pub state: super::TextEditState,
/// Where the text cursor is.
pub cursor_range: Option<super::CursorRange>,
}

View File

@@ -0,0 +1,85 @@
use std::sync::Arc;
use crate::mutex::Mutex;
use crate::*;
use super::{CCursorRange, CursorRange};
type Undoer = crate::util::undoer::Undoer<(CCursorRange, String)>;
/// The text edit state stored between frames.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct TextEditState {
cursor_range: Option<CursorRange>,
/// This is what is easiest to work with when editing text,
/// so users are more likely to read/write this.
ccursor_range: Option<CCursorRange>,
/// Wrapped in Arc for cheaper clones.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) undoer: Arc<Mutex<Undoer>>,
// If IME candidate window is shown on this text edit.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) has_ime: bool,
// Visual offset when editing singleline text bigger than the width.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) singleline_offset: f32,
}
impl TextEditState {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.memory().data.get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.memory().data.insert_persisted(id, self);
}
/// The the currently selected range of characters.
pub fn ccursor_range(&self) -> Option<CCursorRange> {
self.ccursor_range.or_else(|| {
self.cursor_range
.map(|cursor_range| cursor_range.as_ccursor_range())
})
}
/// Sets the currently selected range of characters.
pub fn set_ccursor_range(&mut self, ccursor_range: Option<CCursorRange>) {
self.cursor_range = None;
self.ccursor_range = ccursor_range;
}
pub fn set_cursor_range(&mut self, cursor_range: Option<CursorRange>) {
self.cursor_range = cursor_range;
self.ccursor_range = None;
}
pub fn cursor_range(&mut self, galley: &Galley) -> Option<CursorRange> {
self.cursor_range
.map(|cursor_range| {
// We only use the PCursor (paragraph number, and character offset within that paragraph).
// This is so that if we resize the `TextEdit` region, and text wrapping changes,
// we keep the same byte character offset from the beginning of the text,
// even though the number of rows changes
// (each paragraph can be several rows, due to word wrapping).
// The column (character offset) should be able to extend beyond the last word so that we can
// go down and still end up on the same column when we return.
CursorRange {
primary: galley.from_pcursor(cursor_range.primary.pcursor),
secondary: galley.from_pcursor(cursor_range.secondary.pcursor),
}
})
.or_else(|| {
self.ccursor_range.map(|ccursor_range| CursorRange {
primary: galley.from_ccursor(ccursor_range.primary),
secondary: galley.from_ccursor(ccursor_range.secondary),
})
})
}
}

View File

@@ -0,0 +1,124 @@
use std::ops::Range;
/// 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> {
/// Can this text be edited?
fn is_mutable(&self) -> bool;
/// Returns this buffer as a `str`.
///
/// This is an utility method, as it simply relies on the `AsRef<str>`
/// implementation.
fn as_str(&self) -> &str {
self.as_ref()
}
/// Reads the given character range.
fn char_range(&self, char_range: Range<usize>) -> &str {
assert!(char_range.start <= char_range.end);
let start_byte = self.byte_index_from_char_index(char_range.start);
let end_byte = self.byte_index_from_char_index(char_range.end);
&self.as_str()[start_byte..end_byte]
}
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
/// `char_index` is a *character index*, not a byte index.
///
/// # Return
/// Returns how many *characters* were successfully inserted
fn insert_text(&mut self, text: &str, char_index: usize) -> usize;
/// Deletes a range of text `char_range` from this buffer.
///
/// # Notes
/// `char_range` is a *character range*, not a byte range.
fn delete_char_range(&mut self, char_range: Range<usize>);
/// Clears all characters in this buffer
fn clear(&mut self) {
self.delete_char_range(0..self.as_ref().len());
}
/// Replaces all contents of this string with `text`
fn replace(&mut self, text: &str) {
self.clear();
self.insert_text(text, 0);
}
/// Clears all characters in this buffer and returns a string of the contents.
fn take(&mut self) -> String {
let s = self.as_ref().to_owned();
self.clear();
s
}
}
impl TextBuffer for String {
fn is_mutable(&self) -> bool {
true
}
fn insert_text(&mut self, text: &str, char_index: usize) -> usize {
// Get the byte index from the character index
let byte_idx = self.byte_index_from_char_index(char_index);
// Then insert the string
self.insert_str(byte_idx, text);
text.chars().count()
}
fn delete_char_range(&mut self, char_range: Range<usize>) {
assert!(char_range.start <= char_range.end);
// Get both byte indices
let byte_start = self.byte_index_from_char_index(char_range.start);
let byte_end = self.byte_index_from_char_index(char_range.end);
// Then drain all characters within this range
self.drain(byte_start..byte_end);
}
fn clear(&mut self) {
self.clear();
}
fn replace(&mut self, text: &str) {
*self = text.to_owned();
}
fn take(&mut self) -> String {
std::mem::take(self)
}
}
/// Immutable view of a `&str`!
impl<'a> TextBuffer for &'a str {
fn is_mutable(&self) -> bool {
false
}
fn insert_text(&mut self, _text: &str, _ch_idx: usize) -> usize {
0
}
fn delete_char_range(&mut self, _ch_range: Range<usize>) {}
}
fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
for (ci, (bi, _)) in s.char_indices().enumerate() {
if ci == char_index {
return bi;
}
}
s.len()
}