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:
parent
6a8ee29a4e
commit
267485976b
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue