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:
parent
ddd52f47c5
commit
c7638ca7f5
11 changed files with 810 additions and 533 deletions
|
@ -9,9 +9,13 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
||||||
|
|
||||||
### Added ⭐
|
### Added ⭐
|
||||||
* Add context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)).
|
* Add context menus: See `Ui::menu_button` and `Response::context_menu` ([#543](https://github.com/emilk/egui/pull/543)).
|
||||||
|
* You can now read and write the cursor of a `TextEdit` ([#848](https://github.com/emilk/egui/pull/848)).
|
||||||
|
|
||||||
### Changed 🔧
|
### Changed 🔧
|
||||||
* Unified the four `Memory` data buckts (`data`, `data_temp`, `id_data` and `id_data_temp`) into a single `Memory::data`, with a new interface ([#836](https://github.com/emilk/egui/pull/836)).
|
* Unifiy the four `Memory` data buckets (`data`, `data_temp`, `id_data` and `id_data_temp`) into a single `Memory::data`, with a new interface ([#836](https://github.com/emilk/egui/pull/836)).
|
||||||
|
|
||||||
|
### Contributors 🙏
|
||||||
|
* [mankinskin](https://github.com/mankinskin) ([#543](https://github.com/emilk/egui/pull/543))
|
||||||
|
|
||||||
|
|
||||||
## 0.15.0 - 2021-10-24 - Syntax highlighting and hscroll
|
## 0.15.0 - 2021-10-24 - Syntax highlighting and hscroll
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub(crate) mod window;
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
area::Area,
|
area::Area,
|
||||||
collapsing_header::*,
|
collapsing_header::{CollapsingHeader, CollapsingResponse},
|
||||||
combo_box::*,
|
combo_box::*,
|
||||||
frame::Frame,
|
frame::Frame,
|
||||||
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
||||||
|
|
|
@ -269,6 +269,12 @@ impl Modifiers {
|
||||||
pub fn shift_only(&self) -> bool {
|
pub fn shift_only(&self) -> bool {
|
||||||
self.shift && !(self.alt || self.command)
|
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.
|
/// Keyboard keys.
|
||||||
|
|
|
@ -17,7 +17,7 @@ mod progress_bar;
|
||||||
mod selected_label;
|
mod selected_label;
|
||||||
mod separator;
|
mod separator;
|
||||||
mod slider;
|
mod slider;
|
||||||
pub(crate) mod text_edit;
|
pub mod text_edit;
|
||||||
|
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
pub use drag_value::DragValue;
|
pub use drag_value::DragValue;
|
||||||
|
@ -28,7 +28,7 @@ pub use progress_bar::ProgressBar;
|
||||||
pub use selected_label::SelectableLabel;
|
pub use selected_label::SelectableLabel;
|
||||||
pub use separator::Separator;
|
pub use separator::Separator;
|
||||||
pub use slider::*;
|
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.
|
/// Show a button to reset a value to its default.
|
||||||
|
|
File diff suppressed because it is too large
Load diff
130
egui/src/widgets/text_edit/cursor_range.rs
Normal file
130
egui/src/widgets/text_edit/cursor_range.rs
Normal 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,
|
||||||
|
}
|
10
egui/src/widgets/text_edit/mod.rs
Normal file
10
egui/src/widgets/text_edit/mod.rs
Normal 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,
|
||||||
|
};
|
16
egui/src/widgets/text_edit/output.rs
Normal file
16
egui/src/widgets/text_edit/output.rs
Normal 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>,
|
||||||
|
}
|
85
egui/src/widgets/text_edit/state.rs
Normal file
85
egui/src/widgets/text_edit/state.rs
Normal 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),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
124
egui/src/widgets/text_edit/text_buffer.rs
Normal file
124
egui/src/widgets/text_edit/text_buffer.rs
Normal 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()
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use egui::*;
|
use egui::{text_edit::CCursorRange, *};
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
|
@ -58,6 +58,8 @@ impl EasyMarkEditor {
|
||||||
ui.checkbox(&mut self.show_rendered, "Show rendered");
|
ui.checkbox(&mut self.show_rendered, "Show rendered");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.label("Use ctrl/cmd + key to toggle: B: *strong* C: `code` I: /italics/ L: $lowered$ R: ^raised^ S: ~strikethrough~ U: _underline_");
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
if self.show_rendered {
|
if self.show_rendered {
|
||||||
|
@ -84,7 +86,7 @@ impl EasyMarkEditor {
|
||||||
code, highlighter, ..
|
code, highlighter, ..
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
if self.highlight_editor {
|
let response = if self.highlight_editor {
|
||||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
||||||
let mut layout_job = highlighter.highlight(ui.visuals(), easymark);
|
let mut layout_job = highlighter.highlight(ui.visuals(), easymark);
|
||||||
layout_job.wrap_width = wrap_width;
|
layout_job.wrap_width = wrap_width;
|
||||||
|
@ -96,12 +98,103 @@ impl EasyMarkEditor {
|
||||||
.desired_width(f32::INFINITY)
|
.desired_width(f32::INFINITY)
|
||||||
.text_style(egui::TextStyle::Monospace) // for cursor height
|
.text_style(egui::TextStyle::Monospace) // for cursor height
|
||||||
.layouter(&mut layouter),
|
.layouter(&mut layouter),
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY));
|
ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
|
||||||
|
if let Some(mut ccursor_range) = state.ccursor_range() {
|
||||||
|
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||||
|
if any_change {
|
||||||
|
state.set_ccursor_range(Some(ccursor_range));
|
||||||
|
state.store(ui.ctx(), response.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// let cursor = TextEdit::cursor(response.id);
|
}
|
||||||
// TODO: cmd-i, cmd-b, etc for italics, bold, ....
|
}
|
||||||
|
|
||||||
|
fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool {
|
||||||
|
let mut any_change = false;
|
||||||
|
for event in &ui.input().events {
|
||||||
|
if let Event::Key {
|
||||||
|
key,
|
||||||
|
pressed: true,
|
||||||
|
modifiers,
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
if modifiers.command_only() {
|
||||||
|
match &key {
|
||||||
|
// toggle *bold*
|
||||||
|
Key::B => {
|
||||||
|
toggle_surrounding(code, ccursor_range, "*");
|
||||||
|
any_change = true;
|
||||||
|
}
|
||||||
|
// toggle `code`
|
||||||
|
Key::C => {
|
||||||
|
toggle_surrounding(code, ccursor_range, "`");
|
||||||
|
any_change = true;
|
||||||
|
}
|
||||||
|
// toggle /italics/
|
||||||
|
Key::I => {
|
||||||
|
toggle_surrounding(code, ccursor_range, "/");
|
||||||
|
any_change = true;
|
||||||
|
}
|
||||||
|
// toggle $lowered$
|
||||||
|
Key::L => {
|
||||||
|
toggle_surrounding(code, ccursor_range, "$");
|
||||||
|
any_change = true;
|
||||||
|
}
|
||||||
|
// toggle ^raised^
|
||||||
|
Key::R => {
|
||||||
|
toggle_surrounding(code, ccursor_range, "^");
|
||||||
|
any_change = true;
|
||||||
|
}
|
||||||
|
// toggle ~strikethrough~
|
||||||
|
Key::S => {
|
||||||
|
toggle_surrounding(code, ccursor_range, "~");
|
||||||
|
any_change = true;
|
||||||
|
}
|
||||||
|
// toggle _underline_
|
||||||
|
Key::U => {
|
||||||
|
toggle_surrounding(code, ccursor_range, "_");
|
||||||
|
any_change = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
any_change
|
||||||
|
}
|
||||||
|
|
||||||
|
/// E.g. toggle *strong* with `toggle(&mut text, &mut cursor, "*")`
|
||||||
|
fn toggle_surrounding(
|
||||||
|
code: &mut dyn TextBuffer,
|
||||||
|
ccursor_range: &mut CCursorRange,
|
||||||
|
surrounding: &str,
|
||||||
|
) {
|
||||||
|
let [primary, secondary] = ccursor_range.sorted();
|
||||||
|
|
||||||
|
let surrounding_ccount = surrounding.chars().count();
|
||||||
|
|
||||||
|
let prefix_crange = primary.index.saturating_sub(surrounding_ccount)..primary.index;
|
||||||
|
let suffix_crange = secondary.index..secondary.index.saturating_add(surrounding_ccount);
|
||||||
|
let already_surrounded = code.char_range(prefix_crange.clone()) == surrounding
|
||||||
|
&& code.char_range(suffix_crange.clone()) == surrounding;
|
||||||
|
|
||||||
|
if already_surrounded {
|
||||||
|
code.delete_char_range(suffix_crange);
|
||||||
|
code.delete_char_range(prefix_crange);
|
||||||
|
ccursor_range.primary.index -= surrounding_ccount;
|
||||||
|
ccursor_range.secondary.index -= surrounding_ccount;
|
||||||
|
} else {
|
||||||
|
code.insert_text(surrounding, secondary.index);
|
||||||
|
let advance = code.insert_text(surrounding, primary.index);
|
||||||
|
|
||||||
|
ccursor_range.primary.index += advance;
|
||||||
|
ccursor_range.secondary.index += advance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue