Refactor: move text selection logic to own module (#3843)
This is a follow-up to https://github.com/emilk/egui/issues/3804 and a pre-requisite for https://github.com/emilk/egui/issues/3816
This commit is contained in:
parent
3936418f0b
commit
f034f6db9f
|
|
@ -364,6 +364,7 @@ pub(crate) mod placer;
|
|||
mod response;
|
||||
mod sense;
|
||||
pub mod style;
|
||||
pub mod text_selection;
|
||||
mod ui;
|
||||
pub mod util;
|
||||
pub mod viewport;
|
||||
|
|
@ -398,7 +399,7 @@ pub use epaint::{
|
|||
};
|
||||
|
||||
pub mod text {
|
||||
pub use crate::text_edit::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,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{Context, Galley, Id, Pos2};
|
||||
|
||||
use super::{cursor_interaction::is_word_char, CursorRange};
|
||||
use super::{text_cursor_state::is_word_char, CursorRange};
|
||||
|
||||
/// Update accesskit with the current text state.
|
||||
pub fn update_accesskit_for_text_widget(
|
||||
|
|
@ -2,7 +2,7 @@ use epaint::{text::cursor::*, Galley};
|
|||
|
||||
use crate::{os::OperatingSystem, Event, Id, Key, Modifiers};
|
||||
|
||||
use super::cursor_interaction::{ccursor_next_word, ccursor_previous_word, slice_char_range};
|
||||
use super::text_cursor_state::{ccursor_next_word, ccursor_previous_word, slice_char_range};
|
||||
|
||||
/// A selected text range (could be a range of length zero).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
use epaint::{Galley, Pos2};
|
||||
|
||||
use crate::{Context, CursorIcon, Event, Id, Response, Ui};
|
||||
|
||||
use super::{
|
||||
text_cursor_state::cursor_rect, visuals::paint_text_selection, CursorRange, TextCursorState,
|
||||
};
|
||||
|
||||
/// Handle text selection state for a label or similar widget.
|
||||
///
|
||||
/// Make sure the widget senses clicks and drags.
|
||||
///
|
||||
/// This should be called after painting the text, because this will also
|
||||
/// paint the text cursor/selection on top.
|
||||
pub fn label_text_selection(ui: &Ui, response: &Response, galley_pos: Pos2, galley: &Galley) {
|
||||
let mut cursor_state = LabelSelectionState::load(ui.ctx(), response.id);
|
||||
let original_cursor = cursor_state.range(galley);
|
||||
|
||||
if response.hovered {
|
||||
ui.ctx().set_cursor_icon(CursorIcon::Text);
|
||||
} else if !cursor_state.is_empty() && ui.input(|i| i.pointer.any_pressed()) {
|
||||
// We clicked somewhere else - deselect this label.
|
||||
cursor_state = Default::default();
|
||||
LabelSelectionState::store(ui.ctx(), response.id, cursor_state);
|
||||
}
|
||||
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - galley_pos);
|
||||
cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley);
|
||||
}
|
||||
|
||||
if let Some(mut cursor_range) = cursor_state.range(galley) {
|
||||
process_selection_key_events(ui.ctx(), galley, response.id, &mut cursor_range);
|
||||
cursor_state.set_range(Some(cursor_range));
|
||||
}
|
||||
|
||||
let cursor_range = cursor_state.range(galley);
|
||||
|
||||
if let Some(cursor_range) = cursor_range {
|
||||
// We paint the cursor on top of the text, in case
|
||||
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
|
||||
paint_text_selection(
|
||||
ui.painter(),
|
||||
ui.visuals(),
|
||||
galley_pos,
|
||||
galley,
|
||||
&cursor_range,
|
||||
);
|
||||
|
||||
let selection_changed = original_cursor != Some(cursor_range);
|
||||
|
||||
let is_fully_visible = ui.clip_rect().contains_rect(response.rect); // TODO: remove this HACK workaround for https://github.com/emilk/egui/issues/1531
|
||||
|
||||
if selection_changed && !is_fully_visible {
|
||||
// Scroll to keep primary cursor in view:
|
||||
let row_height = estimate_row_height(galley);
|
||||
let primary_cursor_rect =
|
||||
cursor_rect(galley_pos, galley, &cursor_range.primary, row_height);
|
||||
ui.scroll_to_rect(primary_cursor_rect, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
super::accesskit_text::update_accesskit_for_text_widget(
|
||||
ui.ctx(),
|
||||
response.id,
|
||||
cursor_range,
|
||||
accesskit::Role::StaticText,
|
||||
galley_pos,
|
||||
galley,
|
||||
);
|
||||
|
||||
if !cursor_state.is_empty() {
|
||||
LabelSelectionState::store(ui.ctx(), response.id, cursor_state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles text selection in labels (NOT in [`crate::TextEdit`])s.
|
||||
///
|
||||
/// One state for all labels, because we only support text selection in one label at a time.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct LabelSelectionState {
|
||||
/// Id of the (only) label with a selection, if any
|
||||
id: Option<Id>,
|
||||
|
||||
/// The current selection, if any.
|
||||
selection: TextCursorState,
|
||||
}
|
||||
|
||||
impl LabelSelectionState {
|
||||
/// Load the range of text of text that is selected for the given widget.
|
||||
fn load(ctx: &Context, id: Id) -> TextCursorState {
|
||||
ctx.data(|data| data.get_temp::<Self>(Id::NULL))
|
||||
.and_then(|state| (state.id == Some(id)).then_some(state.selection))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Load the range of text of text that is selected for the given widget.
|
||||
fn store(ctx: &Context, id: Id, selection: TextCursorState) {
|
||||
ctx.data_mut(|data| {
|
||||
data.insert_temp(
|
||||
Id::NULL,
|
||||
Self {
|
||||
id: Some(id),
|
||||
selection,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn process_selection_key_events(
|
||||
ctx: &Context,
|
||||
galley: &Galley,
|
||||
widget_id: Id,
|
||||
cursor_range: &mut CursorRange,
|
||||
) {
|
||||
let mut copy_text = None;
|
||||
|
||||
ctx.input(|i| {
|
||||
// NOTE: we have a lock on ui/ctx here,
|
||||
// so be careful to not call into `ui` or `ctx` again.
|
||||
|
||||
for event in &i.events {
|
||||
match event {
|
||||
Event::Copy | Event::Cut => {
|
||||
// This logic means we can select everything in an ellided label (including the `…`)
|
||||
// and still copy the entire un-ellided text!
|
||||
let everything_is_selected =
|
||||
cursor_range.contains(&CursorRange::select_all(galley));
|
||||
|
||||
let copy_everything = cursor_range.is_empty() || everything_is_selected;
|
||||
|
||||
if copy_everything {
|
||||
copy_text = Some(galley.text().to_owned());
|
||||
} else {
|
||||
copy_text = Some(cursor_range.slice_str(galley).to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
event => {
|
||||
cursor_range.on_event(ctx.os(), event, galley, widget_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(copy_text) = copy_text {
|
||||
ctx.copy_text(copy_text);
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_row_height(galley: &Galley) -> f32 {
|
||||
if let Some(row) = galley.rows.first() {
|
||||
row.rect.height()
|
||||
} else {
|
||||
galley.size().y
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
//! Helpers regarding text selection for labels and text edit.
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub mod accesskit_text;
|
||||
|
||||
mod cursor_range;
|
||||
mod label_text_selection;
|
||||
pub mod text_cursor_state;
|
||||
pub mod visuals;
|
||||
|
||||
pub use cursor_range::{CCursorRange, CursorRange, PCursorRange};
|
||||
pub use label_text_selection::label_text_selection;
|
||||
pub use text_cursor_state::TextCursorState;
|
||||
|
|
@ -1,12 +1,73 @@
|
|||
//! Text cursor changes/interaction, without modifying the text.
|
||||
|
||||
use epaint::text::{cursor::*, Galley};
|
||||
use text_edit::state::TextCursorState;
|
||||
|
||||
use crate::*;
|
||||
|
||||
use super::{CCursorRange, CursorRange};
|
||||
|
||||
/// The state of a text cursor selection.
|
||||
///
|
||||
/// Used for [`crate::TextEdit`] and [`crate::Label`].
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct TextCursorState {
|
||||
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>,
|
||||
}
|
||||
|
||||
impl TextCursorState {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.cursor_range.is_none() && self.ccursor_range.is_none()
|
||||
}
|
||||
|
||||
/// The the currently selected range of characters.
|
||||
pub fn char_range(&self) -> Option<CCursorRange> {
|
||||
self.ccursor_range.or_else(|| {
|
||||
self.cursor_range
|
||||
.map(|cursor_range| cursor_range.as_ccursor_range())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn 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),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the currently selected range of characters.
|
||||
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
|
||||
self.cursor_range = None;
|
||||
self.ccursor_range = ccursor_range;
|
||||
}
|
||||
|
||||
pub fn set_range(&mut self, cursor_range: Option<CursorRange>) {
|
||||
self.cursor_range = cursor_range;
|
||||
self.ccursor_range = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl TextCursorState {
|
||||
/// Handle clicking and/or dragging text.
|
||||
///
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
use crate::*;
|
||||
|
||||
use super::CursorRange;
|
||||
|
||||
pub fn paint_text_selection(
|
||||
painter: &Painter,
|
||||
visuals: &Visuals,
|
||||
galley_pos: Pos2,
|
||||
galley: &Galley,
|
||||
cursor_range: &CursorRange,
|
||||
) {
|
||||
if cursor_range.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// We paint the cursor selection on top of the text, so make it transparent:
|
||||
let color = visuals.selection.bg_fill.linear_multiply(0.5);
|
||||
let [min, max] = cursor_range.sorted_cursors();
|
||||
let min = min.rcursor;
|
||||
let max = max.rcursor;
|
||||
|
||||
for ri in min.row..=max.row {
|
||||
let row = &galley.rows[ri];
|
||||
let left = if ri == min.row {
|
||||
row.x_offset(min.column)
|
||||
} else {
|
||||
row.rect.left()
|
||||
};
|
||||
let right = if ri == max.row {
|
||||
row.x_offset(max.column)
|
||||
} else {
|
||||
let newline_size = if row.ends_with_newline {
|
||||
row.height() / 2.0 // visualize that we select the newline
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
row.rect.right() + newline_size
|
||||
};
|
||||
let rect = Rect::from_min_max(
|
||||
galley_pos + vec2(left, row.min_y()),
|
||||
galley_pos + vec2(right, row.max_y()),
|
||||
);
|
||||
painter.rect_filled(rect, 0.0, color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint one end of the selection, e.g. the primary cursor.
|
||||
pub fn paint_cursor(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
|
||||
let stroke = visuals.text_cursor;
|
||||
|
||||
let top = cursor_rect.center_top();
|
||||
let bottom = cursor_rect.center_bottom();
|
||||
|
||||
painter.line_segment([top, bottom], (stroke.width, stroke.color));
|
||||
|
||||
if false {
|
||||
// Roof/floor:
|
||||
let extrusion = 3.0;
|
||||
let width = 1.0;
|
||||
painter.line_segment(
|
||||
[top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)],
|
||||
(width, stroke.color),
|
||||
);
|
||||
painter.line_segment(
|
||||
[bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)],
|
||||
(width, stroke.color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ impl Widget for Link {
|
|||
|
||||
let selectable = ui.style().interaction.selectable_labels;
|
||||
if selectable {
|
||||
crate::widgets::label::text_selection(ui, &response, galley_pos, &galley);
|
||||
crate::text_selection::label_text_selection(ui, &response, galley_pos, &galley);
|
||||
}
|
||||
|
||||
if response.hovered() {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
text_edit::{
|
||||
cursor_interaction::cursor_rect, paint_cursor_selection, CursorRange, TextCursorState,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use crate::*;
|
||||
|
||||
/// Static text.
|
||||
///
|
||||
|
|
@ -262,160 +257,10 @@ impl Widget for Label {
|
|||
|
||||
let selectable = selectable.unwrap_or_else(|| ui.style().interaction.selectable_labels);
|
||||
if selectable {
|
||||
text_selection(ui, &response, galley_pos, &galley);
|
||||
crate::text_selection::label_text_selection(ui, &response, galley_pos, &galley);
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle text selection state for a label or similar widget.
|
||||
///
|
||||
/// Make sure the widget senses to clicks and drags.
|
||||
///
|
||||
/// This should be called after painting the text, because this will also
|
||||
/// paint the text cursor/selection on top.
|
||||
pub fn text_selection(ui: &Ui, response: &Response, galley_pos: Pos2, galley: &Galley) {
|
||||
let mut cursor_state = LabelSelectionState::load(ui.ctx(), response.id);
|
||||
let original_cursor = cursor_state.range(galley);
|
||||
|
||||
if response.hovered {
|
||||
ui.ctx().set_cursor_icon(CursorIcon::Text);
|
||||
} else if !cursor_state.is_empty() && ui.input(|i| i.pointer.any_pressed()) {
|
||||
// We clicked somewhere else - deselect this label.
|
||||
cursor_state = Default::default();
|
||||
LabelSelectionState::store(ui.ctx(), response.id, cursor_state);
|
||||
}
|
||||
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - galley_pos);
|
||||
cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley);
|
||||
}
|
||||
|
||||
if let Some(mut cursor_range) = cursor_state.range(galley) {
|
||||
process_selection_key_events(ui, galley, response.id, &mut cursor_range);
|
||||
cursor_state.set_range(Some(cursor_range));
|
||||
}
|
||||
|
||||
let cursor_range = cursor_state.range(galley);
|
||||
|
||||
if let Some(cursor_range) = cursor_range {
|
||||
// We paint the cursor on top of the text, in case
|
||||
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
|
||||
paint_cursor_selection(
|
||||
ui.visuals(),
|
||||
ui.painter(),
|
||||
galley_pos,
|
||||
galley,
|
||||
&cursor_range,
|
||||
);
|
||||
|
||||
let selection_changed = original_cursor != Some(cursor_range);
|
||||
|
||||
let is_fully_visible = ui.clip_rect().contains_rect(response.rect); // TODO: remove this HACK workaround for https://github.com/emilk/egui/issues/1531
|
||||
|
||||
if selection_changed && !is_fully_visible {
|
||||
// Scroll to keep primary cursor in view:
|
||||
let row_height = estimate_row_height(galley);
|
||||
let primary_cursor_rect =
|
||||
cursor_rect(galley_pos, galley, &cursor_range.primary, row_height);
|
||||
ui.scroll_to_rect(primary_cursor_rect, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
text_edit::accesskit_text::update_accesskit_for_text_widget(
|
||||
ui.ctx(),
|
||||
response.id,
|
||||
cursor_range,
|
||||
accesskit::Role::StaticText,
|
||||
galley_pos,
|
||||
galley,
|
||||
);
|
||||
|
||||
if !cursor_state.is_empty() {
|
||||
LabelSelectionState::store(ui.ctx(), response.id, cursor_state);
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_row_height(galley: &Galley) -> f32 {
|
||||
if let Some(row) = galley.rows.first() {
|
||||
row.rect.height()
|
||||
} else {
|
||||
galley.size().y
|
||||
}
|
||||
}
|
||||
|
||||
fn process_selection_key_events(
|
||||
ui: &Ui,
|
||||
galley: &Galley,
|
||||
widget_id: Id,
|
||||
cursor_range: &mut CursorRange,
|
||||
) {
|
||||
let mut copy_text = None;
|
||||
|
||||
ui.input(|i| {
|
||||
// NOTE: we have a lock on ui/ctx here,
|
||||
// so be careful to not call into `ui` or `ctx` again.
|
||||
|
||||
for event in &i.events {
|
||||
match event {
|
||||
Event::Copy | Event::Cut => {
|
||||
// This logic means we can select everything in an ellided label (including the `…`)
|
||||
// and still copy the entire un-ellided text!
|
||||
let everything_is_selected =
|
||||
cursor_range.contains(&CursorRange::select_all(galley));
|
||||
|
||||
let copy_everything = cursor_range.is_empty() || everything_is_selected;
|
||||
|
||||
if copy_everything {
|
||||
copy_text = Some(galley.text().to_owned());
|
||||
} else {
|
||||
copy_text = Some(cursor_range.slice_str(galley).to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
event => {
|
||||
cursor_range.on_event(ui.ctx().os(), event, galley, widget_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(copy_text) = copy_text {
|
||||
ui.ctx().copy_text(copy_text);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// One state for all labels, because we only support text selection in one label at a time.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct LabelSelectionState {
|
||||
/// Id of the (only) label with a selection, if any
|
||||
id: Option<Id>,
|
||||
|
||||
/// The current selection, if any.
|
||||
selection: TextCursorState,
|
||||
}
|
||||
|
||||
impl LabelSelectionState {
|
||||
fn load(ctx: &Context, id: Id) -> TextCursorState {
|
||||
ctx.data(|data| data.get_temp::<Self>(Id::NULL))
|
||||
.and_then(|state| (state.id == Some(id)).then_some(state.selection))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn store(ctx: &Context, id: Id, selection: TextCursorState) {
|
||||
ctx.data_mut(|data| {
|
||||
data.insert_temp(
|
||||
Id::NULL,
|
||||
Self {
|
||||
id: Some(id),
|
||||
selection,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ use std::sync::Arc;
|
|||
use epaint::text::{cursor::*, Galley, LayoutJob};
|
||||
|
||||
use crate::{
|
||||
os::OperatingSystem, output::OutputEvent, text_edit::cursor_interaction::cursor_rect, *,
|
||||
os::OperatingSystem,
|
||||
output::OutputEvent,
|
||||
text_selection::{
|
||||
text_cursor_state::cursor_rect,
|
||||
visuals::{paint_cursor, paint_text_selection},
|
||||
CCursorRange, CursorRange,
|
||||
},
|
||||
*,
|
||||
};
|
||||
|
||||
use super::{
|
||||
cursor_interaction::{ccursor_next_word, ccursor_previous_word, find_line_start},
|
||||
CCursorRange, CursorRange, TextEditOutput, TextEditState,
|
||||
};
|
||||
use super::{TextEditOutput, TextEditState};
|
||||
|
||||
/// A text region that the user can edit the contents of.
|
||||
///
|
||||
|
|
@ -651,9 +655,9 @@ impl<'t> TextEdit<'t> {
|
|||
if let Some(cursor_range) = state.cursor.range(&galley) {
|
||||
// We paint the cursor on top of the text, in case
|
||||
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
|
||||
paint_cursor_selection(
|
||||
ui.visuals(),
|
||||
paint_text_selection(
|
||||
&painter,
|
||||
ui.visuals(),
|
||||
galley_pos,
|
||||
&galley,
|
||||
&cursor_range,
|
||||
|
|
@ -722,7 +726,7 @@ impl<'t> TextEdit<'t> {
|
|||
accesskit::Role::TextInput
|
||||
};
|
||||
|
||||
super::accesskit_text::update_accesskit_for_text_widget(
|
||||
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
|
||||
ui.ctx(),
|
||||
id,
|
||||
cursor_range,
|
||||
|
|
@ -814,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 {
|
||||
|
|
@ -831,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 {
|
||||
|
|
@ -846,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))
|
||||
}
|
||||
|
|
@ -861,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 {
|
||||
|
|
@ -924,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 {
|
||||
|
|
@ -939,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 {
|
||||
|
|
@ -978,173 +982,6 @@ fn events(
|
|||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub fn paint_cursor_selection(
|
||||
visuals: &Visuals,
|
||||
painter: &Painter,
|
||||
galley_pos: Pos2,
|
||||
galley: &Galley,
|
||||
cursor_range: &CursorRange,
|
||||
) {
|
||||
if cursor_range.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// We paint the cursor selection on top of the text, so make it transparent:
|
||||
let color = visuals.selection.bg_fill.linear_multiply(0.5);
|
||||
let [min, max] = cursor_range.sorted_cursors();
|
||||
let min = min.rcursor;
|
||||
let max = max.rcursor;
|
||||
|
||||
for ri in min.row..=max.row {
|
||||
let row = &galley.rows[ri];
|
||||
let left = if ri == min.row {
|
||||
row.x_offset(min.column)
|
||||
} else {
|
||||
row.rect.left()
|
||||
};
|
||||
let right = if ri == max.row {
|
||||
row.x_offset(max.column)
|
||||
} else {
|
||||
let newline_size = if row.ends_with_newline {
|
||||
row.height() / 2.0 // visualize that we select the newline
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
row.rect.right() + newline_size
|
||||
};
|
||||
let rect = Rect::from_min_max(
|
||||
galley_pos + vec2(left, row.min_y()),
|
||||
galley_pos + vec2(right, row.max_y()),
|
||||
);
|
||||
painter.rect_filled(rect, 0.0, color);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint one end of the selection, e.g. the primary cursor.
|
||||
fn paint_cursor(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
|
||||
let stroke = visuals.text_cursor;
|
||||
|
||||
let top = cursor_rect.center_top();
|
||||
let bottom = cursor_rect.center_bottom();
|
||||
|
||||
painter.line_segment([top, bottom], (stroke.width, stroke.color));
|
||||
|
||||
if false {
|
||||
// Roof/floor:
|
||||
let extrusion = 3.0;
|
||||
let width = 1.0;
|
||||
painter.line_segment(
|
||||
[top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)],
|
||||
(width, stroke.color),
|
||||
);
|
||||
painter.line_segment(
|
||||
[bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)],
|
||||
(width, stroke.color),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
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,
|
||||
|
|
@ -1157,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,
|
||||
|
|
@ -1192,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))
|
||||
}
|
||||
|
|
@ -1218,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,9 @@
|
|||
mod builder;
|
||||
pub mod cursor_interaction;
|
||||
mod cursor_range;
|
||||
mod output;
|
||||
mod state;
|
||||
mod text_buffer;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub mod accesskit_text;
|
||||
|
||||
pub use {
|
||||
builder::{paint_cursor_selection, TextEdit},
|
||||
cursor_range::*,
|
||||
output::TextEditOutput,
|
||||
state::TextCursorState,
|
||||
state::TextEditState,
|
||||
text_buffer::TextBuffer,
|
||||
crate::text_selection::TextCursorState, builder::TextEdit, output::TextEditOutput,
|
||||
state::TextEditState, text_buffer::TextBuffer,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::text::CursorRange;
|
||||
|
||||
/// The output from a [`TextEdit`](crate::TextEdit).
|
||||
pub struct TextEditOutput {
|
||||
/// The interaction response.
|
||||
|
|
@ -18,7 +20,7 @@ pub struct TextEditOutput {
|
|||
pub state: super::TextEditState,
|
||||
|
||||
/// Where the text cursor is.
|
||||
pub cursor_range: Option<super::CursorRange>,
|
||||
pub cursor_range: Option<CursorRange>,
|
||||
}
|
||||
|
||||
impl TextEditOutput {
|
||||
|
|
|
|||
|
|
@ -4,82 +4,19 @@ use crate::mutex::Mutex;
|
|||
|
||||
use crate::*;
|
||||
|
||||
use super::{CCursorRange, CursorRange};
|
||||
use self::text_selection::{CCursorRange, CursorRange, TextCursorState};
|
||||
|
||||
pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>;
|
||||
|
||||
/// The state of a text cursor selection.
|
||||
///
|
||||
/// Used for [`crate::TextEdit`] and [`crate::Label`].
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct TextCursorState {
|
||||
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>,
|
||||
}
|
||||
|
||||
impl TextCursorState {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.cursor_range.is_none() && self.ccursor_range.is_none()
|
||||
}
|
||||
|
||||
/// The the currently selected range of characters.
|
||||
pub fn char_range(&self) -> Option<CCursorRange> {
|
||||
self.ccursor_range.or_else(|| {
|
||||
self.cursor_range
|
||||
.map(|cursor_range| cursor_range.as_ccursor_range())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn 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),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the currently selected range of characters.
|
||||
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
|
||||
self.cursor_range = None;
|
||||
self.ccursor_range = ccursor_range;
|
||||
}
|
||||
|
||||
pub fn set_range(&mut self, cursor_range: Option<CursorRange>) {
|
||||
self.cursor_range = cursor_range;
|
||||
self.ccursor_range = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The text edit state stored between frames.
|
||||
///
|
||||
/// Attention: You also need to `store` the updated state.
|
||||
/// ```
|
||||
/// # use egui::text::CCursor;
|
||||
/// # use egui::text_edit::{CCursorRange, TextEditOutput};
|
||||
/// # use egui::TextEdit;
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # let mut text = String::new();
|
||||
/// let mut output = TextEdit::singleline(&mut text).show(ui);
|
||||
/// use egui::text::{CCursor, CCursorRange};
|
||||
///
|
||||
/// let mut output = egui::TextEdit::singleline(&mut text).show(ui);
|
||||
///
|
||||
/// // Create a new selection range
|
||||
/// let min = CCursor::new(0);
|
||||
|
|
@ -87,7 +24,7 @@ impl TextCursorState {
|
|||
/// let new_range = CCursorRange::two(min, max);
|
||||
///
|
||||
/// // Update the state
|
||||
/// output.state.set_ccursor_range(Some(new_range));
|
||||
/// output.state.cursor.set_char_range(Some(new_range));
|
||||
/// // Store the updated state
|
||||
/// output.state.store(ui.ctx(), output.response.id);
|
||||
/// # });
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use super::cursor_interaction::{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<usize>) -> &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<usize>);
|
||||
|
||||
/// Reads the given character range.
|
||||
fn char_range(&self, char_range: Range<usize>) -> &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 {
|
||||
|
|
|
|||
|
|
@ -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))]
|
||||
|
|
|
|||
Loading…
Reference in New Issue