Simplify the text cursor API (#5785)

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* Closes N/A, but this is part of
https://github.com/emilk/egui/issues/3378
* [x] I have followed the instructions in the PR template

Other text layout libraries in Rust--namely, Parley and Cosmic
Text--have one canonical text cursor type (Parley's is a byte index,
Cosmic Text's also stores the line index). To prepare for migrating egui
to one of those libraries, it should also have only one text cursor
type. I also think simplifying the API is a good idea in and of
itself--having three different cursor types that you have to convert
between (and a `Cursor` struct which contains all three at once) is
confusing.

After a bit of experimentation, I found that the best cursor type to
coalesce around is `CCursor`. In the few places where we need a
paragraph index or row/column position, we can calculate them as
necessary.

I've removed `CursorRange` and `PCursorRange` (the latter appears to
have never been used), merging the functionality with `CCursorRange`. To
preserve the cursor position when navigating row-by-row, `CCursorRange`
now stores the previous horizontal position of the cursor.

I've also removed `PCursor`, and renamed `RowCursor` to `LayoutCursor`
(since it includes not only the row but the column). I have not renamed
either `CCursorRange` or `CCursor` as those names are used in a lot of
places, and I don't want to clutter this PR with a bunch of renames.
I'll leave it for a later PR.

Finally, I've removed the deprecated methods from `TextEditState`--it
made the refactoring easier, and it should be pretty easy to migrate to
the equivalent `TextCursorState` methods.

I'm not sure how many breaking changes people will actually encounter. A
lot of these APIs were technically public, but I don't think many were
useful. The `TextBuffer` trait now takes `&CCursorRange` instead of
`&CursorRange` in a couple of methods, and I renamed
`CCursorRange::sorted` to `CCursorRange::sorted_cursors` to match
`CursorRange`.

I did encounter a couple of apparent minor bugs when testing out text
cursor behavior, but I checked them against the current version of egui
and they're all pre-existing.
This commit is contained in:
valadaptive 2025-03-20 05:49:38 -04:00 committed by GitHub
parent 6a8ee29a4e
commit 267485976b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 394 additions and 730 deletions

View File

@ -471,7 +471,7 @@ pub use epaint::{
};
pub mod text {
pub use crate::text_selection::{CCursorRange, CursorRange};
pub use crate::text_selection::CCursorRange;
pub use epaint::text::{
cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob,
LayoutSection, TextFormat, TextWrapping, TAB_SIZE,

View File

@ -2,13 +2,13 @@ use emath::TSTransform;
use crate::{Context, Galley, Id};
use super::{text_cursor_state::is_word_char, CursorRange};
use super::{text_cursor_state::is_word_char, CCursorRange};
/// Update accesskit with the current text state.
pub fn update_accesskit_for_text_widget(
ctx: &Context,
widget_id: Id,
cursor_range: Option<CursorRange>,
cursor_range: Option<CCursorRange>,
role: accesskit::Role,
global_from_galley: TSTransform,
galley: &Galley,
@ -17,8 +17,8 @@ pub fn update_accesskit_for_text_widget(
let parent_id = widget_id;
if let Some(cursor_range) = &cursor_range {
let anchor = &cursor_range.secondary.rcursor;
let focus = &cursor_range.primary.rcursor;
let anchor = galley.layout_from_cursor(cursor_range.secondary);
let focus = galley.layout_from_cursor(cursor_range.primary);
builder.set_text_selection(accesskit::TextSelection {
anchor: accesskit::TextPosition {
node: parent_id.with(anchor.row).accesskit_id(),

View File

@ -1,41 +1,45 @@
use epaint::{
text::cursor::{CCursor, Cursor, PCursor},
Galley,
};
use epaint::{text::cursor::CCursor, Galley};
use crate::{os::OperatingSystem, Event, Id, Key, Modifiers};
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).
///
/// 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 CursorRange {
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: Cursor,
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: Cursor,
pub secondary: CCursor,
/// Saved horizontal position of the cursor.
pub h_pos: Option<f32>,
}
impl CursorRange {
impl CCursorRange {
/// The empty range.
#[inline]
pub fn one(cursor: Cursor) -> Self {
pub fn one(ccursor: CCursor) -> Self {
Self {
primary: cursor,
secondary: cursor,
primary: ccursor,
secondary: ccursor,
h_pos: None,
}
}
#[inline]
pub fn two(min: Cursor, max: Cursor) -> Self {
pub fn two(min: impl Into<CCursor>, max: impl Into<CCursor>) -> Self {
Self {
primary: max,
secondary: min,
primary: max.into(),
secondary: min.into(),
h_pos: None,
}
}
@ -44,39 +48,31 @@ impl CursorRange {
Self::two(galley.begin(), galley.end())
}
pub fn as_ccursor_range(&self) -> CCursorRange {
CCursorRange {
primary: self.primary.ccursor,
secondary: self.secondary.ccursor,
}
}
/// The range of selected character indices.
pub fn as_sorted_char_range(&self) -> std::ops::Range<usize> {
let [start, end] = self.sorted_cursors();
std::ops::Range {
start: start.ccursor.index,
end: end.ccursor.index,
start: start.index,
end: end.index,
}
}
/// True if the selected range contains no characters.
#[inline]
pub fn is_empty(&self) -> bool {
self.primary.ccursor == self.secondary.ccursor
self.primary == self.secondary
}
/// Is `self` a super-set of the other range?
pub fn contains(&self, other: &Self) -> bool {
pub fn contains(&self, other: Self) -> bool {
let [self_min, self_max] = self.sorted_cursors();
let [other_min, other_max] = other.sorted_cursors();
self_min.ccursor.index <= other_min.ccursor.index
&& other_max.ccursor.index <= self_max.ccursor.index
self_min.index <= other_min.index && other_max.index <= self_max.index
}
/// If there is a selection, None is returned.
/// If the two ends are the same, that is returned.
pub fn single(&self) -> Option<Cursor> {
pub fn single(&self) -> Option<CCursor> {
if self.is_empty() {
Some(self.primary)
} else {
@ -84,25 +80,16 @@ impl CursorRange {
}
}
#[inline]
pub fn is_sorted(&self) -> bool {
let p = self.primary.ccursor;
let s = self.secondary.ccursor;
let p = self.primary;
let s = self.secondary;
(p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
}
pub fn sorted(self) -> Self {
if self.is_sorted() {
self
} else {
Self {
primary: self.secondary,
secondary: self.primary,
}
}
}
/// Returns the two ends ordered.
pub fn sorted_cursors(&self) -> [Cursor; 2] {
/// returns the two ends ordered
#[inline]
pub fn sorted_cursors(&self) -> [CCursor; 2] {
if self.is_sorted() {
[self.primary, self.secondary]
} else {
@ -110,9 +97,15 @@ impl CursorRange {
}
}
#[inline]
#[deprecated = "Use `self.sorted_cursors` instead."]
pub fn sorted(&self) -> [CCursor; 2] {
self.sorted_cursors()
}
pub fn slice_str<'s>(&self, text: &'s str) -> &'s str {
let [min, max] = self.sorted_cursors();
slice_char_range(text, min.ccursor.index..max.ccursor.index)
slice_char_range(text, min.index..max.index)
}
/// Check for key presses that are moving the cursor.
@ -146,7 +139,14 @@ impl CursorRange {
| Key::ArrowDown
| Key::Home
| Key::End => {
move_single_cursor(os, &mut self.primary, galley, key, modifiers);
move_single_cursor(
os,
&mut self.primary,
&mut self.h_pos,
galley,
key,
modifiers,
);
if !modifiers.shift {
self.secondary = self.primary;
}
@ -156,7 +156,14 @@ impl CursorRange {
Key::P | Key::N | Key::B | Key::F | Key::A | Key::E
if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift =>
{
move_single_cursor(os, &mut self.primary, galley, key, modifiers);
move_single_cursor(
os,
&mut self.primary,
&mut self.h_pos,
galley,
key,
modifiers,
);
self.secondary = self.primary;
true
}
@ -196,8 +203,9 @@ impl CursorRange {
ccursor_from_accesskit_text_position(_widget_id, galley, &selection.anchor);
if let (Some(primary), Some(secondary)) = (primary, secondary) {
*self = Self {
primary: galley.from_ccursor(primary),
secondary: galley.from_ccursor(secondary),
primary,
secondary,
h_pos: None,
};
return true;
}
@ -210,71 +218,6 @@ impl CursorRange {
}
}
/// 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.
#[inline]
pub fn one(ccursor: CCursor) -> Self {
Self {
primary: ccursor,
secondary: ccursor,
}
}
#[inline]
pub fn two(min: impl Into<CCursor>, max: impl Into<CCursor>) -> Self {
Self {
primary: max.into(),
secondary: min.into(),
}
}
#[inline]
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
#[inline]
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,
}
// ----------------------------------------------------------------------------
#[cfg(feature = "accesskit")]
@ -304,78 +247,83 @@ fn ccursor_from_accesskit_text_position(
/// Move a text cursor based on keyboard
fn move_single_cursor(
os: OperatingSystem,
cursor: &mut Cursor,
cursor: &mut CCursor,
h_pos: &mut Option<f32>,
galley: &Galley,
key: Key,
modifiers: &Modifiers,
) {
if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift {
match key {
Key::A => *cursor = galley.cursor_begin_of_row(cursor),
Key::E => *cursor = galley.cursor_end_of_row(cursor),
Key::P => *cursor = galley.cursor_up_one_row(cursor),
Key::N => *cursor = galley.cursor_down_one_row(cursor),
Key::B => *cursor = galley.cursor_left_one_character(cursor),
Key::F => *cursor = galley.cursor_right_one_character(cursor),
_ => (),
}
return;
}
match key {
Key::ArrowLeft => {
if modifiers.alt || modifiers.ctrl {
// alt on mac, ctrl on windows
*cursor = galley.from_ccursor(ccursor_previous_word(galley, cursor.ccursor));
} else if modifiers.mac_cmd {
*cursor = galley.cursor_begin_of_row(cursor);
} else {
*cursor = galley.cursor_left_one_character(cursor);
let (new_cursor, new_h_pos) =
if os == OperatingSystem::Mac && modifiers.ctrl && !modifiers.shift {
match key {
Key::A => (galley.cursor_begin_of_row(cursor), None),
Key::E => (galley.cursor_end_of_row(cursor), None),
Key::P => galley.cursor_up_one_row(cursor, *h_pos),
Key::N => galley.cursor_down_one_row(cursor, *h_pos),
Key::B => (galley.cursor_left_one_character(cursor), None),
Key::F => (galley.cursor_right_one_character(cursor), None),
_ => return,
}
}
Key::ArrowRight => {
if modifiers.alt || modifiers.ctrl {
// alt on mac, ctrl on windows
*cursor = galley.from_ccursor(ccursor_next_word(galley, cursor.ccursor));
} else if modifiers.mac_cmd {
*cursor = galley.cursor_end_of_row(cursor);
} else {
*cursor = galley.cursor_right_one_character(cursor);
}
}
Key::ArrowUp => {
if modifiers.command {
// mac and windows behavior
*cursor = galley.begin();
} else {
*cursor = galley.cursor_up_one_row(cursor);
}
}
Key::ArrowDown => {
if modifiers.command {
// mac and windows behavior
*cursor = galley.end();
} else {
*cursor = galley.cursor_down_one_row(cursor);
}
}
} else {
match key {
Key::ArrowLeft => {
if modifiers.alt || modifiers.ctrl {
// alt on mac, ctrl on windows
(ccursor_previous_word(galley, *cursor), None)
} else if modifiers.mac_cmd {
(galley.cursor_begin_of_row(cursor), None)
} else {
(galley.cursor_left_one_character(cursor), None)
}
}
Key::ArrowRight => {
if modifiers.alt || modifiers.ctrl {
// alt on mac, ctrl on windows
(ccursor_next_word(galley, *cursor), None)
} else if modifiers.mac_cmd {
(galley.cursor_end_of_row(cursor), None)
} else {
(galley.cursor_right_one_character(cursor), None)
}
}
Key::ArrowUp => {
if modifiers.command {
// mac and windows behavior
(galley.begin(), None)
} else {
galley.cursor_up_one_row(cursor, *h_pos)
}
}
Key::ArrowDown => {
if modifiers.command {
// mac and windows behavior
(galley.end(), None)
} else {
galley.cursor_down_one_row(cursor, *h_pos)
}
}
Key::Home => {
if modifiers.ctrl {
// windows behavior
*cursor = galley.begin();
} else {
*cursor = galley.cursor_begin_of_row(cursor);
}
}
Key::End => {
if modifiers.ctrl {
// windows behavior
*cursor = galley.end();
} else {
*cursor = galley.cursor_end_of_row(cursor);
}
}
Key::Home => {
if modifiers.ctrl {
// windows behavior
(galley.begin(), None)
} else {
(galley.cursor_begin_of_row(cursor), None)
}
}
Key::End => {
if modifiers.ctrl {
// windows behavior
(galley.end(), None)
} else {
(galley.cursor_end_of_row(cursor), None)
}
}
_ => unreachable!(),
}
_ => unreachable!(),
}
};
*cursor = new_cursor;
*h_pos = new_h_pos;
}

View File

@ -10,7 +10,7 @@ use crate::{
use super::{
text_cursor_state::cursor_rect,
visuals::{paint_text_selection, RowVertexIndices},
CursorRange, TextCursorState,
TextCursorState,
};
/// Turn on to help debug this
@ -44,7 +44,7 @@ impl WidgetTextCursor {
}
fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
galley.pos_from_ccursor(ccursor).center()
galley.pos_from_cursor(ccursor).center()
}
impl std::fmt::Debug for WidgetTextCursor {
@ -235,7 +235,7 @@ impl LabelSelectionState {
self.selection = None;
}
fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CursorRange) {
fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CCursorRange) {
let new_text = selected_text(galley, cursor_range);
if new_text.is_empty() {
return;
@ -433,7 +433,11 @@ impl LabelSelectionState {
match (primary, secondary) {
(Some(primary), Some(secondary)) => {
// This is the only selected label.
TextCursorState::from(CCursorRange { primary, secondary })
TextCursorState::from(CCursorRange {
primary,
secondary,
h_pos: None,
})
}
(Some(primary), None) => {
@ -442,12 +446,16 @@ impl LabelSelectionState {
// Secondary was before primary.
// Select everything up to the cursor.
// We assume normal left-to-right and top-down layout order here.
galley.begin().ccursor
galley.begin()
} else {
// Select everything from the cursor onward:
galley.end().ccursor
galley.end()
};
TextCursorState::from(CCursorRange { primary, secondary })
TextCursorState::from(CCursorRange {
primary,
secondary,
h_pos: None,
})
}
(None, Some(secondary)) => {
@ -456,12 +464,16 @@ impl LabelSelectionState {
// Primary was before secondary.
// Select everything up to the cursor.
// We assume normal left-to-right and top-down layout order here.
galley.begin().ccursor
galley.begin()
} else {
// Select everything from the cursor onward:
galley.end().ccursor
galley.end()
};
TextCursorState::from(CCursorRange { primary, secondary })
TextCursorState::from(CCursorRange {
primary,
secondary,
h_pos: None,
})
}
(None, None) => {
@ -515,7 +527,7 @@ impl LabelSelectionState {
let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley);
let old_range = cursor_state.range(galley);
let old_range = cursor_state.char_range();
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
if response.contains_pointer() {
@ -529,7 +541,7 @@ impl LabelSelectionState {
}
}
if let Some(mut cursor_range) = cursor_state.range(galley) {
if let Some(mut cursor_range) = cursor_state.char_range() {
let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
@ -543,11 +555,11 @@ impl LabelSelectionState {
self.copy_text(galley_rect, galley, &cursor_range);
}
cursor_state.set_range(Some(cursor_range));
cursor_state.set_char_range(Some(cursor_range));
}
// Look for changes due to keyboard and/or mouse interaction:
let new_range = cursor_state.range(galley);
let new_range = cursor_state.char_range();
let selection_changed = old_range != new_range;
if let (true, Some(range)) = (selection_changed, new_range) {
@ -617,7 +629,7 @@ impl LabelSelectionState {
}
}
let cursor_range = cursor_state.range(galley);
let cursor_range = cursor_state.char_range();
let mut new_vertex_indices = vec![];
@ -657,7 +669,7 @@ fn process_selection_key_events(
ctx: &Context,
galley: &Galley,
widget_id: Id,
cursor_range: &mut CursorRange,
cursor_range: &mut CCursorRange,
) -> bool {
let os = ctx.os();
@ -674,10 +686,10 @@ fn process_selection_key_events(
changed
}
fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String {
fn selected_text(galley: &Galley, cursor_range: &CCursorRange) -> String {
// This logic means we can select everything in an elided label (including the `…`)
// and still copy the entire un-elided text!
let everything_is_selected = cursor_range.contains(&CursorRange::select_all(galley));
let everything_is_selected = cursor_range.contains(CCursorRange::select_all(galley));
let copy_everything = cursor_range.is_empty() || everything_is_selected;

View File

@ -8,6 +8,6 @@ mod label_text_selection;
pub mod text_cursor_state;
pub mod visuals;
pub use cursor_range::{CCursorRange, CursorRange, PCursorRange};
pub use cursor_range::CCursorRange;
pub use label_text_selection::LabelSelectionState;
pub use text_cursor_state::TextCursorState;

View File

@ -1,13 +1,10 @@
//! Text cursor changes/interaction, without modifying the text.
use epaint::text::{
cursor::{CCursor, Cursor},
Galley,
};
use epaint::text::{cursor::CCursor, Galley};
use crate::{epaint, NumExt, Rect, Response, Ui};
use super::{CCursorRange, CursorRange};
use super::CCursorRange;
/// The state of a text cursor selection.
///
@ -16,29 +13,12 @@ use super::{CCursorRange, CursorRange};
#[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 From<CursorRange> for TextCursorState {
fn from(cursor_range: CursorRange) -> Self {
Self {
cursor_range: Some(cursor_range),
ccursor_range: Some(CCursorRange {
primary: cursor_range.primary.ccursor,
secondary: cursor_range.secondary.ccursor,
}),
}
}
}
impl From<CCursorRange> for TextCursorState {
fn from(ccursor_range: CCursorRange) -> Self {
Self {
cursor_range: None,
ccursor_range: Some(ccursor_range),
}
}
@ -46,50 +26,18 @@ impl From<CCursorRange> for TextCursorState {
impl TextCursorState {
pub fn is_empty(&self) -> bool {
self.cursor_range.is_none() && self.ccursor_range.is_none()
self.ccursor_range.is_none()
}
/// 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(&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),
})
})
self.ccursor_range
}
/// 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 {
@ -100,7 +48,7 @@ impl TextCursorState {
&mut self,
ui: &Ui,
response: &Response,
cursor_at_pointer: Cursor,
cursor_at_pointer: CCursor,
galley: &Galley,
is_being_dragged: bool,
) -> bool {
@ -108,39 +56,33 @@ impl TextCursorState {
if response.double_clicked() {
// Select word:
let ccursor_range = select_word_at(text, cursor_at_pointer.ccursor);
self.set_range(Some(CursorRange {
primary: galley.from_ccursor(ccursor_range.primary),
secondary: galley.from_ccursor(ccursor_range.secondary),
}));
let ccursor_range = select_word_at(text, cursor_at_pointer);
self.set_char_range(Some(ccursor_range));
true
} else if response.triple_clicked() {
// Select line:
let ccursor_range = select_line_at(text, cursor_at_pointer.ccursor);
self.set_range(Some(CursorRange {
primary: galley.from_ccursor(ccursor_range.primary),
secondary: galley.from_ccursor(ccursor_range.secondary),
}));
let ccursor_range = select_line_at(text, cursor_at_pointer);
self.set_char_range(Some(ccursor_range));
true
} else if response.sense.senses_drag() {
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
// The start of a drag (or a click).
if ui.input(|i| i.modifiers.shift) {
if let Some(mut cursor_range) = self.range(galley) {
if let Some(mut cursor_range) = self.char_range() {
cursor_range.primary = cursor_at_pointer;
self.set_range(Some(cursor_range));
self.set_char_range(Some(cursor_range));
} else {
self.set_range(Some(CursorRange::one(cursor_at_pointer)));
self.set_char_range(Some(CCursorRange::one(cursor_at_pointer)));
}
} else {
self.set_range(Some(CursorRange::one(cursor_at_pointer)));
self.set_char_range(Some(CCursorRange::one(cursor_at_pointer)));
}
true
} else if is_being_dragged {
// Drag to select text:
if let Some(mut cursor_range) = self.range(galley) {
if let Some(mut cursor_range) = self.char_range() {
cursor_range.primary = cursor_at_pointer;
self.set_range(Some(cursor_range));
self.set_char_range(Some(cursor_range));
}
true
} else {
@ -336,8 +278,8 @@ pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
}
/// The thin rectangle of one end of the selection, e.g. the primary cursor, in local galley coordinates.
pub fn cursor_rect(galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect {
let mut cursor_pos = galley.pos_from_cursor(cursor);
pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
let mut cursor_pos = galley.pos_from_cursor(*cursor);
// Handle completely empty galleys
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);

View File

@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::{pos2, vec2, Galley, Painter, Rect, Ui, Visuals};
use super::CursorRange;
use super::CCursorRange;
#[derive(Clone, Debug)]
pub struct RowVertexIndices {
@ -14,7 +14,7 @@ pub struct RowVertexIndices {
pub fn paint_text_selection(
galley: &mut Arc<Galley>,
visuals: &Visuals,
cursor_range: &CursorRange,
cursor_range: &CCursorRange,
mut new_vertex_indices: Option<&mut Vec<RowVertexIndices>>,
) {
if cursor_range.is_empty() {
@ -27,8 +27,8 @@ pub fn paint_text_selection(
let color = visuals.selection.bg_fill;
let [min, max] = cursor_range.sorted_cursors();
let min = min.rcursor;
let max = max.rcursor;
let min = galley.layout_from_cursor(min);
let max = galley.layout_from_cursor(max);
for ri in min.row..=max.row {
let row = &mut galley.rows[ri];

View File

@ -11,9 +11,7 @@ use crate::{
os::OperatingSystem,
output::OutputEvent,
response, text_selection,
text_selection::{
text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange, CursorRange,
},
text_selection::{text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange},
vec2, Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id,
ImeEvent, Key, KeyboardShortcut, Margin, Modifiers, NumExt, Response, Sense, Shape, TextBuffer,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState,
@ -614,14 +612,14 @@ impl TextEdit<'_> {
}
let mut cursor_range = None;
let prev_cursor_range = state.cursor.range(&galley);
let prev_cursor_range = state.cursor.char_range();
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CursorRange::one(galley.end())
CCursorRange::one(galley.end())
} else {
CursorRange::default()
CCursorRange::default()
};
let (changed, new_cursor_range) = events(
@ -655,7 +653,7 @@ impl TextEdit<'_> {
// Visual clipping for singleline text editor with text larger than width
if clip_text && align_offset == 0.0 {
let cursor_pos = match (cursor_range, ui.memory(|mem| mem.has_focus(id))) {
(Some(cursor_range), true) => galley.pos_from_cursor(&cursor_range.primary).min.x,
(Some(cursor_range), true) => galley.pos_from_cursor(cursor_range.primary).min.x,
_ => 0.0,
};
@ -683,7 +681,7 @@ impl TextEdit<'_> {
let selection_changed = if let (Some(cursor_range), Some(prev_cursor_range)) =
(cursor_range, prev_cursor_range)
{
prev_cursor_range.as_ccursor_range() != cursor_range.as_ccursor_range()
prev_cursor_range != cursor_range
} else {
false
};
@ -717,7 +715,7 @@ impl TextEdit<'_> {
let has_focus = ui.memory(|mem| mem.has_focus(id));
if has_focus {
if let Some(cursor_range) = state.cursor.range(&galley) {
if let Some(cursor_range) = state.cursor.char_range() {
// Add text selection rectangles to the galley:
paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
}
@ -739,7 +737,7 @@ impl TextEdit<'_> {
painter.galley(galley_pos, galley.clone(), text_color);
if has_focus {
if let Some(cursor_range) = state.cursor.range(&galley) {
if let Some(cursor_range) = state.cursor.char_range() {
let primary_cursor_rect =
cursor_rect(&galley, &cursor_range.primary, row_height)
.translate(galley_pos.to_vec2());
@ -809,8 +807,7 @@ impl TextEdit<'_> {
});
} else if selection_changed {
let cursor_range = cursor_range.unwrap();
let char_range =
cursor_range.primary.ccursor.index..=cursor_range.secondary.ccursor.index;
let char_range = cursor_range.primary.index..=cursor_range.secondary.index;
let info = WidgetInfo::text_selection_changed(
ui.is_enabled(),
char_range,
@ -887,20 +884,20 @@ fn events(
wrap_width: f32,
multiline: bool,
password: bool,
default_cursor_range: CursorRange,
default_cursor_range: CCursorRange,
char_limit: usize,
event_filter: EventFilter,
return_key: Option<KeyboardShortcut>,
) -> (bool, CursorRange) {
) -> (bool, CCursorRange) {
let os = ui.ctx().os();
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
let mut cursor_range = state.cursor.char_range().unwrap_or(default_cursor_range);
// We feed state to the undoer both before and after handling input
// so that the undoer creates automatic saves even when there are no events for a while.
state.undoer.lock().feed_state(
ui.input(|i| i.time),
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
&(cursor_range, text.as_str().to_owned()),
);
let copy_if_not_password = |ui: &Ui, text: String| {
@ -1010,7 +1007,7 @@ fn events(
if let Some((redo_ccursor_range, redo_txt)) = state
.undoer
.lock()
.redo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned()))
.redo(&(cursor_range, text.as_str().to_owned()))
{
text.replace_with(redo_txt);
Some(*redo_ccursor_range)
@ -1028,7 +1025,7 @@ fn events(
if let Some((undo_ccursor_range, undo_txt)) = state
.undoer
.lock()
.undo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned()))
.undo(&(cursor_range, text.as_str().to_owned()))
{
text.replace_with(undo_txt);
Some(*undo_ccursor_range)
@ -1072,14 +1069,14 @@ fn events(
state.ime_enabled = false;
if !prediction.is_empty()
&& cursor_range.secondary.ccursor.index
== state.ime_cursor_range.secondary.ccursor.index
&& cursor_range.secondary.index
== state.ime_cursor_range.secondary.index
{
let mut ccursor = text.delete_selected(&cursor_range);
text.insert_text_at(&mut ccursor, prediction, char_limit);
Some(CCursorRange::one(ccursor))
} else {
let ccursor = cursor_range.primary.ccursor;
let ccursor = cursor_range.primary;
Some(CCursorRange::one(ccursor))
}
}
@ -1100,18 +1097,15 @@ fn events(
*galley = layouter(ui, text.as_str(), wrap_width);
// Set cursor_range using new galley:
cursor_range = CursorRange {
primary: galley.from_ccursor(new_ccursor_range.primary),
secondary: galley.from_ccursor(new_ccursor_range.secondary),
};
cursor_range = new_ccursor_range;
}
}
state.cursor.set_range(Some(cursor_range));
state.cursor.set_char_range(Some(cursor_range));
state.undoer.lock().feed_state(
ui.input(|i| i.time),
&(cursor_range.as_ccursor_range(), text.as_str().to_owned()),
&(cursor_range, text.as_str().to_owned()),
);
(any_change, cursor_range)
@ -1143,7 +1137,7 @@ fn remove_ime_incompatible_events(events: &mut Vec<Event>) {
/// Returns `Some(new_cursor)` if we did mutate `text`.
fn check_for_mutating_key_press(
os: OperatingSystem,
cursor_range: &CursorRange,
cursor_range: &CCursorRange,
text: &mut dyn TextBuffer,
galley: &Galley,
modifiers: &Modifiers,
@ -1156,9 +1150,9 @@ fn check_for_mutating_key_press(
} else if let Some(cursor) = cursor_range.single() {
if modifiers.alt || modifiers.ctrl {
// alt on mac, ctrl on windows
text.delete_previous_word(cursor.ccursor)
text.delete_previous_word(cursor)
} else {
text.delete_previous_char(cursor.ccursor)
text.delete_previous_char(cursor)
}
} else {
text.delete_selected(cursor_range)
@ -1172,9 +1166,9 @@ fn check_for_mutating_key_press(
} else if let Some(cursor) = cursor_range.single() {
if modifiers.alt || modifiers.ctrl {
// alt on mac, ctrl on windows
text.delete_next_word(cursor.ccursor)
text.delete_next_word(cursor)
} else {
text.delete_next_char(cursor.ccursor)
text.delete_next_char(cursor)
}
} else {
text.delete_selected(cursor_range)
@ -1187,7 +1181,7 @@ fn check_for_mutating_key_press(
}
Key::H if modifiers.ctrl => {
let ccursor = text.delete_previous_char(cursor_range.primary.ccursor);
let ccursor = text.delete_previous_char(cursor_range.primary);
Some(CCursorRange::one(ccursor))
}
@ -1203,7 +1197,7 @@ fn check_for_mutating_key_press(
Key::W if modifiers.ctrl => {
let ccursor = if let Some(cursor) = cursor_range.single() {
text.delete_previous_word(cursor.ccursor)
text.delete_previous_word(cursor)
} else {
text.delete_selected(cursor_range)
};

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::text::CursorRange;
use crate::text::CCursorRange;
/// The output from a [`TextEdit`](crate::TextEdit).
pub struct TextEditOutput {
@ -20,7 +20,7 @@ pub struct TextEditOutput {
pub state: super::TextEditState,
/// Where the text cursor is.
pub cursor_range: Option<CursorRange>,
pub cursor_range: Option<CCursorRange>,
}
impl TextEditOutput {

View File

@ -3,8 +3,8 @@ use std::sync::Arc;
use crate::mutex::Mutex;
use crate::{
text_selection::{CCursorRange, CursorRange, TextCursorState},
Context, Galley, Id,
text_selection::{CCursorRange, TextCursorState},
Context, Id,
};
pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>;
@ -47,7 +47,7 @@ pub struct TextEditState {
// cursor range for IME candidate.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) ime_cursor_range: CursorRange,
pub(crate) ime_cursor_range: CCursorRange,
// Visual offset when editing singleline text bigger than the width.
#[cfg_attr(feature = "serde", serde(skip))]
@ -68,23 +68,6 @@ impl TextEditState {
ctx.data_mut(|d| d.insert_persisted(id, self));
}
/// The currently selected range of characters.
#[deprecated = "Use `self.cursor.char_range` instead"]
pub fn ccursor_range(&self) -> Option<CCursorRange> {
self.cursor.char_range()
}
/// Sets the currently selected range of characters.
#[deprecated = "Use `self.cursor.set_char_range` instead"]
pub fn set_ccursor_range(&mut self, ccursor_range: Option<CCursorRange>) {
self.cursor.set_char_range(ccursor_range);
}
#[deprecated = "Use `self.cursor.set_range` instead"]
pub fn set_cursor_range(&mut self, cursor_range: Option<CursorRange>) {
self.cursor.set_range(cursor_range);
}
pub fn undoer(&self) -> TextEditUndoer {
self.undoer.lock().clone()
}
@ -97,9 +80,4 @@ impl TextEditState {
pub fn clear_undoer(&mut self) {
self.set_undoer(TextEditUndoer::default());
}
#[deprecated = "Use `self.cursor.range` instead"]
pub fn cursor_range(&self, galley: &Galley) -> Option<CursorRange> {
self.cursor.range(galley)
}
}

View File

@ -1,19 +1,16 @@
use std::{borrow::Cow, ops::Range};
use epaint::{
text::{
cursor::{CCursor, PCursor},
TAB_SIZE,
},
text::{cursor::CCursor, TAB_SIZE},
Galley,
};
use crate::text_selection::{
text_cursor_state::{
use crate::{
text::CCursorRange,
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
@ -111,9 +108,9 @@ pub trait TextBuffer {
}
}
fn delete_selected(&mut self, cursor_range: &CursorRange) -> CCursor {
fn delete_selected(&mut self, cursor_range: &CCursorRange) -> CCursor {
let [min, max] = cursor_range.sorted_cursors();
self.delete_selected_ccursor_range([min.ccursor, max.ccursor])
self.delete_selected_ccursor_range([min, max])
}
fn delete_selected_ccursor_range(&mut self, [min, max]: [CCursor; 2]) -> CCursor {
@ -151,36 +148,28 @@ pub trait TextBuffer {
fn delete_paragraph_before_cursor(
&mut self,
galley: &Galley,
cursor_range: &CursorRange,
cursor_range: &CCursorRange,
) -> 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)
let min = galley.cursor_begin_of_paragraph(&min);
if min == max {
self.delete_previous_char(min)
} else {
self.delete_selected(&CursorRange::two(min, max))
self.delete_selected(&CCursorRange::two(min, max))
}
}
fn delete_paragraph_after_cursor(
&mut self,
galley: &Galley,
cursor_range: &CursorRange,
cursor_range: &CCursorRange,
) -> 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)
let max = galley.cursor_end_of_paragraph(&max);
if min == max {
self.delete_next_char(min)
} else {
self.delete_selected(&CursorRange::two(min, max))
self.delete_selected(&CCursorRange::two(min, max))
}
}
}

View File

@ -146,7 +146,7 @@ fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRang
if ui.input_mut(|i| i.consume_shortcut(&SHORTCUT_INDENT)) {
// This is a placeholder till we can indent the active line
any_change = true;
let [primary, _secondary] = ccursor_range.sorted();
let [primary, _secondary] = ccursor_range.sorted_cursors();
let advance = code.insert_text(" ", primary.index);
ccursor_range.primary.index += advance;
@ -177,7 +177,7 @@ fn toggle_surrounding(
ccursor_range: &mut CCursorRange,
surrounding: &str,
) {
let [primary, secondary] = ccursor_range.sorted();
let [primary, secondary] = ccursor_range.sorted_cursors();
let surrounding_ccount = surrounding.chars().count();

View File

@ -26,13 +26,6 @@ impl CCursor {
}
}
impl From<Cursor> for CCursor {
#[inline]
fn from(c: Cursor) -> Self {
c.ccursor
}
}
/// Two `CCursor`s are considered equal if they refer to the same character boundary,
/// even if one prefers the start of the next row.
impl PartialEq for CCursor {
@ -76,10 +69,12 @@ impl std::ops::SubAssign<usize> for CCursor {
}
}
/// Row Cursor
/// Row/column cursor.
///
/// This refers to rows and columns in layout terms--text wrapping creates multiple rows.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct RCursor {
pub struct LayoutCursor {
/// 0 is first row, and so on.
/// Note that a single paragraph can span multiple rows.
/// (a paragraph is text separated by `\n`).
@ -90,48 +85,3 @@ pub struct RCursor {
/// When moving up/down it may again be within the next row.
pub column: usize,
}
/// Paragraph Cursor
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct PCursor {
/// 0 is first paragraph, and so on.
/// Note that a single paragraph can span multiple rows.
/// (a paragraph is text separated by `\n`).
pub paragraph: usize,
/// Character based (NOT bytes).
/// It is fine if this points to something beyond the end of the current paragraph.
/// When moving up/down it may again be within the next paragraph.
pub offset: usize,
/// If this cursors sits right at the border of a wrapped row break (NOT paragraph break)
/// do we prefer the next row?
/// This is *almost* always what you want, *except* for when
/// explicitly clicking the end of a row or pressing the end key.
pub prefer_next_row: bool,
}
/// Two `PCursor`s are considered equal if they refer to the same character boundary,
/// even if one prefers the start of the next row.
impl PartialEq for PCursor {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.paragraph == other.paragraph && self.offset == other.offset
}
}
/// All different types of cursors together.
///
/// They all point to the same place, but in their own different ways.
/// pcursor/rcursor can also point to after the end of the paragraph/row.
/// Does not implement `PartialEq` because you must think which cursor should be equivalent.
///
/// The default cursor is the zero-cursor, to the first character.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Cursor {
pub ccursor: CCursor,
pub rcursor: RCursor,
pub pcursor: PCursor,
}

View File

@ -5,7 +5,7 @@ use std::ops::Range;
use std::sync::Arc;
use super::{
cursor::{CCursor, Cursor, PCursor, RCursor},
cursor::{CCursor, LayoutCursor},
font::UvRect,
};
use crate::{Color32, FontId, Mesh, Stroke};
@ -766,53 +766,18 @@ impl Galley {
}
/// Returns a 0-width Rect.
pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect {
self.pos_from_pcursor(cursor.pcursor) // pcursor is what TextEdit stores
fn pos_from_layout_cursor(&self, layout_cursor: &LayoutCursor) -> Rect {
let Some(row) = self.rows.get(layout_cursor.row) else {
return self.end_pos();
};
let x = row.x_offset(layout_cursor.column);
Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
}
/// Returns a 0-width Rect.
pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect {
let mut it = PCursor::default();
for row in &self.rows {
if it.paragraph == pcursor.paragraph {
// Right paragraph, but is it the right row in the paragraph?
if it.offset <= pcursor.offset
&& (pcursor.offset <= it.offset + row.char_count_excluding_newline()
|| row.ends_with_newline)
{
let column = pcursor.offset - it.offset;
let select_next_row_instead = pcursor.prefer_next_row
&& !row.ends_with_newline
&& column >= row.char_count_excluding_newline();
if !select_next_row_instead {
let x = row.x_offset(column);
return Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()));
}
}
}
if row.ends_with_newline {
it.paragraph += 1;
it.offset = 0;
} else {
it.offset += row.char_count_including_newline();
}
}
self.end_pos()
}
/// Returns a 0-width Rect.
pub fn pos_from_ccursor(&self, ccursor: CCursor) -> Rect {
self.pos_from_cursor(&self.from_ccursor(ccursor))
}
/// Returns a 0-width Rect.
pub fn pos_from_rcursor(&self, rcursor: RCursor) -> Rect {
self.pos_from_cursor(&self.from_rcursor(rcursor))
pub fn pos_from_cursor(&self, cursor: CCursor) -> Rect {
self.pos_from_layout_cursor(&self.layout_from_cursor(cursor))
}
/// Cursor at the given position within the galley.
@ -822,7 +787,7 @@ impl Galley {
/// and a cursor below the galley is considered
/// same as a cursor at the end.
/// This allows implementing text-selection by dragging above/below the galley.
pub fn cursor_from_pos(&self, pos: Vec2) -> Cursor {
pub fn cursor_from_pos(&self, pos: Vec2) -> CCursor {
if let Some(first_row) = self.rows.first() {
if pos.y < first_row.min_y() {
return self.begin();
@ -835,32 +800,20 @@ impl Galley {
}
let mut best_y_dist = f32::INFINITY;
let mut cursor = Cursor::default();
let mut cursor = CCursor::default();
let mut ccursor_index = 0;
let mut pcursor_it = PCursor::default();
for (row_nr, row) in self.rows.iter().enumerate() {
for row in &self.rows {
let is_pos_within_row = row.min_y() <= pos.y && pos.y <= row.max_y();
let y_dist = (row.min_y() - pos.y).abs().min((row.max_y() - pos.y).abs());
if is_pos_within_row || y_dist < best_y_dist {
best_y_dist = y_dist;
let column = row.char_at(pos.x);
let prefer_next_row = column < row.char_count_excluding_newline();
cursor = Cursor {
ccursor: CCursor {
index: ccursor_index + column,
prefer_next_row,
},
rcursor: RCursor {
row: row_nr,
column,
},
pcursor: PCursor {
paragraph: pcursor_it.paragraph,
offset: pcursor_it.offset + column,
prefer_next_row,
},
cursor = CCursor {
index: ccursor_index + column,
prefer_next_row,
};
if is_pos_within_row {
@ -868,12 +821,6 @@ impl Galley {
}
}
ccursor_index += row.char_count_including_newline();
if row.ends_with_newline {
pcursor_it.paragraph += 1;
pcursor_it.offset = 0;
} else {
pcursor_it.offset += row.char_count_including_newline();
}
}
cursor
@ -884,15 +831,15 @@ impl Galley {
impl Galley {
/// Cursor to the first character.
///
/// This is the same as [`Cursor::default`].
/// This is the same as [`CCursor::default`].
#[inline]
#[allow(clippy::unused_self)]
pub fn begin(&self) -> Cursor {
Cursor::default()
pub fn begin(&self) -> CCursor {
CCursor::default()
}
/// Cursor to one-past last character.
pub fn end(&self) -> Cursor {
pub fn end(&self) -> CCursor {
if self.rows.is_empty() {
return Default::default();
}
@ -900,31 +847,47 @@ impl Galley {
index: 0,
prefer_next_row: true,
};
let mut pcursor = PCursor {
paragraph: 0,
offset: 0,
prefer_next_row: true,
};
for row in &self.rows {
let row_char_count = row.char_count_including_newline();
ccursor.index += row_char_count;
if row.ends_with_newline {
pcursor.paragraph += 1;
pcursor.offset = 0;
} else {
pcursor.offset += row_char_count;
}
}
Cursor {
ccursor,
rcursor: self.end_rcursor(),
pcursor,
}
ccursor
}
}
/// ## Cursor conversions
impl Galley {
// The returned cursor is clamped.
pub fn layout_from_cursor(&self, cursor: CCursor) -> LayoutCursor {
let prefer_next_row = cursor.prefer_next_row;
let mut ccursor_it = CCursor {
index: 0,
prefer_next_row,
};
for (row_nr, row) in self.rows.iter().enumerate() {
let row_char_count = row.char_count_excluding_newline();
if ccursor_it.index <= cursor.index && cursor.index <= ccursor_it.index + row_char_count
{
let column = cursor.index - ccursor_it.index;
let select_next_row_instead = prefer_next_row
&& !row.ends_with_newline
&& column >= row.char_count_excluding_newline();
if !select_next_row_instead {
return LayoutCursor {
row: row_nr,
column,
};
}
}
ccursor_it.index += row.char_count_including_newline();
}
debug_assert!(ccursor_it == self.end());
pub fn end_rcursor(&self) -> RCursor {
if let Some(last_row) = self.rows.last() {
RCursor {
LayoutCursor {
row: self.rows.len() - 1,
column: last_row.char_count_including_newline(),
}
@ -932,268 +895,156 @@ impl Galley {
Default::default()
}
}
}
/// ## Cursor conversions
impl Galley {
// The returned cursor is clamped.
pub fn from_ccursor(&self, ccursor: CCursor) -> Cursor {
let prefer_next_row = ccursor.prefer_next_row;
let mut ccursor_it = CCursor {
index: 0,
prefer_next_row,
};
let mut pcursor_it = PCursor {
paragraph: 0,
offset: 0,
prefer_next_row,
};
for (row_nr, row) in self.rows.iter().enumerate() {
let row_char_count = row.char_count_excluding_newline();
if ccursor_it.index <= ccursor.index
&& ccursor.index <= ccursor_it.index + row_char_count
{
let column = ccursor.index - ccursor_it.index;
let select_next_row_instead = prefer_next_row
&& !row.ends_with_newline
&& column >= row.char_count_excluding_newline();
if !select_next_row_instead {
pcursor_it.offset += column;
return Cursor {
ccursor,
rcursor: RCursor {
row: row_nr,
column,
},
pcursor: pcursor_it,
};
}
}
ccursor_it.index += row.char_count_including_newline();
if row.ends_with_newline {
pcursor_it.paragraph += 1;
pcursor_it.offset = 0;
} else {
pcursor_it.offset += row.char_count_including_newline();
}
}
debug_assert!(ccursor_it == self.end().ccursor);
Cursor {
ccursor: ccursor_it, // clamp
rcursor: self.end_rcursor(),
pcursor: pcursor_it,
}
}
pub fn from_rcursor(&self, rcursor: RCursor) -> Cursor {
if rcursor.row >= self.rows.len() {
fn cursor_from_layout(&self, layout_cursor: LayoutCursor) -> CCursor {
if layout_cursor.row >= self.rows.len() {
return self.end();
}
let prefer_next_row =
rcursor.column < self.rows[rcursor.row].char_count_excluding_newline();
let mut ccursor_it = CCursor {
layout_cursor.column < self.rows[layout_cursor.row].char_count_excluding_newline();
let mut cursor_it = CCursor {
index: 0,
prefer_next_row,
};
let mut pcursor_it = PCursor {
paragraph: 0,
offset: 0,
prefer_next_row,
};
for (row_nr, row) in self.rows.iter().enumerate() {
if row_nr == rcursor.row {
ccursor_it.index += rcursor.column.at_most(row.char_count_excluding_newline());
if row_nr == layout_cursor.row {
cursor_it.index += layout_cursor
.column
.at_most(row.char_count_excluding_newline());
if row.ends_with_newline {
// Allow offset to go beyond the end of the paragraph
pcursor_it.offset += rcursor.column;
} else {
pcursor_it.offset += rcursor.column.at_most(row.char_count_excluding_newline());
}
return Cursor {
ccursor: ccursor_it,
rcursor,
pcursor: pcursor_it,
};
}
ccursor_it.index += row.char_count_including_newline();
if row.ends_with_newline {
pcursor_it.paragraph += 1;
pcursor_it.offset = 0;
} else {
pcursor_it.offset += row.char_count_including_newline();
return cursor_it;
}
cursor_it.index += row.char_count_including_newline();
}
Cursor {
ccursor: ccursor_it,
rcursor: self.end_rcursor(),
pcursor: pcursor_it,
}
}
// TODO(emilk): return identical cursor, or clamp?
pub fn from_pcursor(&self, pcursor: PCursor) -> Cursor {
let prefer_next_row = pcursor.prefer_next_row;
let mut ccursor_it = CCursor {
index: 0,
prefer_next_row,
};
let mut pcursor_it = PCursor {
paragraph: 0,
offset: 0,
prefer_next_row,
};
for (row_nr, row) in self.rows.iter().enumerate() {
if pcursor_it.paragraph == pcursor.paragraph {
// Right paragraph, but is it the right row in the paragraph?
if pcursor_it.offset <= pcursor.offset
&& (pcursor.offset <= pcursor_it.offset + row.char_count_excluding_newline()
|| row.ends_with_newline)
{
let column = pcursor.offset - pcursor_it.offset;
let select_next_row_instead = pcursor.prefer_next_row
&& !row.ends_with_newline
&& column >= row.char_count_excluding_newline();
if !select_next_row_instead {
ccursor_it.index += column.at_most(row.char_count_excluding_newline());
return Cursor {
ccursor: ccursor_it,
rcursor: RCursor {
row: row_nr,
column,
},
pcursor,
};
}
}
}
ccursor_it.index += row.char_count_including_newline();
if row.ends_with_newline {
pcursor_it.paragraph += 1;
pcursor_it.offset = 0;
} else {
pcursor_it.offset += row.char_count_including_newline();
}
}
Cursor {
ccursor: ccursor_it,
rcursor: self.end_rcursor(),
pcursor,
}
cursor_it
}
}
/// ## Cursor positions
impl Galley {
pub fn cursor_left_one_character(&self, cursor: &Cursor) -> Cursor {
if cursor.ccursor.index == 0 {
#[allow(clippy::unused_self)]
pub fn cursor_left_one_character(&self, cursor: &CCursor) -> CCursor {
if cursor.index == 0 {
Default::default()
} else {
let ccursor = CCursor {
index: cursor.ccursor.index,
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end.
};
self.from_ccursor(ccursor - 1)
CCursor {
index: cursor.index - 1,
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the beginning of a row than at the end.
}
}
}
pub fn cursor_right_one_character(&self, cursor: &Cursor) -> Cursor {
let ccursor = CCursor {
index: cursor.ccursor.index,
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the begging of a row than at the end.
};
self.from_ccursor(ccursor + 1)
pub fn cursor_right_one_character(&self, cursor: &CCursor) -> CCursor {
CCursor {
index: (cursor.index + 1).min(self.end().index),
prefer_next_row: true, // default to this when navigating. It is more often useful to put cursor at the beginning of a row than at the end.
}
}
pub fn cursor_up_one_row(&self, cursor: &Cursor) -> Cursor {
if cursor.rcursor.row == 0 {
Cursor::default()
pub fn cursor_up_one_row(
&self,
cursor: &CCursor,
h_pos: Option<f32>,
) -> (CCursor, Option<f32>) {
let layout_cursor = self.layout_from_cursor(*cursor);
let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
if layout_cursor.row == 0 {
(CCursor::default(), None)
} else {
let new_row = cursor.rcursor.row - 1;
let new_row = layout_cursor.row - 1;
let cursor_is_beyond_end_of_current_row = cursor.rcursor.column
>= self.rows[cursor.rcursor.row].char_count_excluding_newline();
let new_rcursor = if cursor_is_beyond_end_of_current_row {
// keep same column
RCursor {
row: new_row,
column: cursor.rcursor.column,
}
} else {
let new_layout_cursor = {
// keep same X coord
let x = self.pos_from_cursor(cursor).center().x;
let column = if x > self.rows[new_row].rect.right() {
// beyond the end of this row - keep same column
cursor.rcursor.column
} else {
self.rows[new_row].char_at(x)
};
RCursor {
let column = self.rows[new_row].char_at(h_pos);
LayoutCursor {
row: new_row,
column,
}
};
self.from_rcursor(new_rcursor)
(self.cursor_from_layout(new_layout_cursor), Some(h_pos))
}
}
pub fn cursor_down_one_row(&self, cursor: &Cursor) -> Cursor {
if cursor.rcursor.row + 1 < self.rows.len() {
let new_row = cursor.rcursor.row + 1;
pub fn cursor_down_one_row(
&self,
cursor: &CCursor,
h_pos: Option<f32>,
) -> (CCursor, Option<f32>) {
let layout_cursor = self.layout_from_cursor(*cursor);
let h_pos = h_pos.unwrap_or_else(|| self.pos_from_layout_cursor(&layout_cursor).center().x);
if layout_cursor.row + 1 < self.rows.len() {
let new_row = layout_cursor.row + 1;
let cursor_is_beyond_end_of_current_row = cursor.rcursor.column
>= self.rows[cursor.rcursor.row].char_count_excluding_newline();
let new_rcursor = if cursor_is_beyond_end_of_current_row {
// keep same column
RCursor {
row: new_row,
column: cursor.rcursor.column,
}
} else {
let new_layout_cursor = {
// keep same X coord
let x = self.pos_from_cursor(cursor).center().x;
let column = if x > self.rows[new_row].rect.right() {
// beyond the end of the next row - keep same column
cursor.rcursor.column
} else {
self.rows[new_row].char_at(x)
};
RCursor {
let column = self.rows[new_row].char_at(h_pos);
LayoutCursor {
row: new_row,
column,
}
};
self.from_rcursor(new_rcursor)
(self.cursor_from_layout(new_layout_cursor), Some(h_pos))
} else {
self.end()
(self.end(), None)
}
}
pub fn cursor_begin_of_row(&self, cursor: &Cursor) -> Cursor {
self.from_rcursor(RCursor {
row: cursor.rcursor.row,
pub fn cursor_begin_of_row(&self, cursor: &CCursor) -> CCursor {
let layout_cursor = self.layout_from_cursor(*cursor);
self.cursor_from_layout(LayoutCursor {
row: layout_cursor.row,
column: 0,
})
}
pub fn cursor_end_of_row(&self, cursor: &Cursor) -> Cursor {
self.from_rcursor(RCursor {
row: cursor.rcursor.row,
column: self.rows[cursor.rcursor.row].char_count_excluding_newline(),
pub fn cursor_end_of_row(&self, cursor: &CCursor) -> CCursor {
let layout_cursor = self.layout_from_cursor(*cursor);
self.cursor_from_layout(LayoutCursor {
row: layout_cursor.row,
column: self.rows[layout_cursor.row].char_count_excluding_newline(),
})
}
pub fn cursor_begin_of_paragraph(&self, cursor: &CCursor) -> CCursor {
let mut layout_cursor = self.layout_from_cursor(*cursor);
layout_cursor.column = 0;
loop {
let prev_row = layout_cursor
.row
.checked_sub(1)
.and_then(|row| self.rows.get(row));
let Some(prev_row) = prev_row else {
// This is the first row
break;
};
if prev_row.ends_with_newline {
break;
}
layout_cursor.row -= 1;
}
self.cursor_from_layout(layout_cursor)
}
pub fn cursor_end_of_paragraph(&self, cursor: &CCursor) -> CCursor {
let mut layout_cursor = self.layout_from_cursor(*cursor);
loop {
let row = &self.rows[layout_cursor.row];
if row.ends_with_newline || layout_cursor.row == self.rows.len() - 1 {
layout_cursor.column = row.char_count_excluding_newline();
break;
}
layout_cursor.row += 1;
}
self.cursor_from_layout(layout_cursor)
}
}