diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 3149285d0..9edf626f2 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -8,7 +8,9 @@ use web_time::Instant; use accesskit::{Node, TreeUpdate}; use core::default::Default; -use parley::{GenericFamily, StyleProperty, editor::SplitString, layout::PositionedLayoutItem}; +use parley::editing::SplitString; +use parley::layout::PositionedLayoutItem; +use parley::{GenericFamily, StyleProperty}; use std::time::Duration; use ui_events::pointer::PointerButton; use ui_events::{ @@ -23,7 +25,7 @@ use vello::{ }; use winit::event::{Ime, WindowEvent}; -pub use parley::layout::editor::Generation; +pub use parley::editing::Generation; use parley::{FontContext, LayoutContext, PlainEditor, PlainEditorDriver}; use crate::access_ids::next_node_id; diff --git a/parley/src/editing/cursor.rs b/parley/src/editing/cursor.rs new file mode 100644 index 000000000..f8a422f3c --- /dev/null +++ b/parley/src/editing/cursor.rs @@ -0,0 +1,464 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::BoundingBox; +#[cfg(feature = "accesskit")] +use crate::layout::LayoutAccessibility; +use crate::layout::{Affinity, BreakReason, Cluster, ClusterSide, Layout, Line}; +use crate::style::Brush; +#[cfg(feature = "accesskit")] +use accesskit::TextPosition; +#[cfg(feature = "accesskit")] +use swash::text::cluster::Whitespace; + +/// Defines a position with a text layout. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct Cursor { + pub(crate) index: usize, + pub(crate) affinity: Affinity, +} + +impl Cursor { + /// Creates a new cursor from the given byte index and affinity. + pub fn from_byte_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + if let Some(cluster) = Cluster::from_byte_index(layout, index) { + let index = cluster.text_range().start; + Self { + index, + affinity: if index != 0 { + affinity + } else { + // There is no Upstream cluster of the 0 position so we force Downstream affinity. + Affinity::Downstream + }, + } + } else { + Self { + index: layout.data.text_len, + affinity: Affinity::Upstream, + } + } + } + + /// Creates a new cursor from the given coordinates. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + let (index, affinity) = if let Some((cluster, side)) = Cluster::from_point(layout, x, y) { + let is_leading = side == ClusterSide::Left; + if cluster.is_rtl() { + if is_leading { + (cluster.text_range().end, Affinity::Upstream) + } else { + (cluster.text_range().start, Affinity::Downstream) + } + } else { + // We never want to position the cursor _after_ a hard + // line since that cursor appears visually at the start + // of the next line + if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) { + (cluster.text_range().start, Affinity::Downstream) + } else { + (cluster.text_range().end, Affinity::Upstream) + } + } + } else { + (layout.data.text_len, Affinity::Downstream) + }; + Self { index, affinity } + } + + #[cfg(feature = "accesskit")] + pub fn from_access_position( + pos: &TextPosition, + layout: &Layout, + layout_access: &LayoutAccessibility, + ) -> Option { + let (line_index, run_index) = *layout_access.run_paths_by_access_id.get(&pos.node)?; + let line = layout.get(line_index)?; + let run = line.item(run_index)?.run()?; + let index = run + .get(pos.character_index) + .map(|cluster| cluster.text_range().start) + .unwrap_or(layout.data.text_len); + Some(Self::from_byte_index(layout, index, Affinity::Downstream)) + } + + pub(crate) fn from_cluster( + layout: &Layout, + cluster: Cluster<'_, B>, + moving_right: bool, + ) -> Self { + Self::from_byte_index( + layout, + cluster.text_range().start, + affinity_for_dir(cluster.is_rtl(), moving_right), + ) + } + + /// Returns the logical text index of the cursor. + pub fn index(&self) -> usize { + self.index + } + + /// Returns the affinity of the cursor. + /// + /// This defines the direction from which the cursor entered its current + /// position and affects the visual location of the rendered cursor. + pub fn affinity(&self) -> Affinity { + self.affinity + } + + /// Returns a new cursor that is guaranteed to be within the bounds of the + /// given layout. + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + Self::from_byte_index(layout, self.index, self.affinity) + } + + /// Returns a new cursor that is positioned at the previous cluster boundary + /// in visual order. + #[must_use] + pub fn previous_visual(&self, layout: &Layout) -> Self { + let [left, right] = self.visual_clusters(layout); + if let (Some(left), Some(right)) = (&left, &right) { + if left.is_soft_line_break() { + if left.is_rtl() && self.affinity == Affinity::Upstream { + let index = if right.is_rtl() { + left.text_range().start + } else { + left.text_range().end + }; + return Self::from_byte_index(layout, index, Affinity::Downstream); + } else if !left.is_rtl() && self.affinity == Affinity::Downstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Upstream); + } + } + } + if let Some(left) = left { + let index = if left.is_rtl() { + left.text_range().end + } else { + left.text_range().start + }; + return Self::from_byte_index(layout, index, affinity_for_dir(left.is_rtl(), false)); + } + *self + } + + /// Returns a new cursor that is positioned at the next cluster boundary + /// in visual order. + #[must_use] + pub fn next_visual(&self, layout: &Layout) -> Self { + let [left, right] = self.visual_clusters(layout); + if let (Some(left), Some(right)) = (&left, &right) { + if left.is_soft_line_break() { + if left.is_rtl() && self.affinity == Affinity::Downstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Upstream); + } else if !left.is_rtl() && self.affinity == Affinity::Upstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Downstream); + } + } + let index = if right.is_rtl() { + right.text_range().start + } else { + right.text_range().end + }; + return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); + } + if let Some(right) = right { + let index = if right.is_rtl() { + right.text_range().start + } else { + right.text_range().end + }; + return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); + } + *self + } + + /// Returns a new cursor that is positioned at the next word boundary + /// in visual order. + #[must_use] + pub fn next_visual_word(&self, layout: &Layout) -> Self { + let mut cur = *self; + loop { + let next = cur.next_visual(layout); + if next == cur { + break; + } + cur = next; + let [Some(left), Some(right)] = cur.visual_clusters(layout) else { + break; + }; + if left.is_rtl() { + if left.is_word_boundary() && !left.is_space_or_nbsp() { + break; + } + } else if right.is_word_boundary() && !left.is_space_or_nbsp() { + break; + } + } + cur + } + + /// Returns a new cursor that is positioned at the previous word boundary + /// in visual order. + #[must_use] + pub fn previous_visual_word(&self, layout: &Layout) -> Self { + let mut cur = *self; + loop { + let next = cur.previous_visual(layout); + if next == cur { + break; + } + cur = next; + let [Some(left), Some(right)] = cur.visual_clusters(layout) else { + break; + }; + if left.is_rtl() { + if left.is_word_boundary() + && (left.is_space_or_nbsp() + || (right.is_word_boundary() && !right.is_space_or_nbsp())) + { + break; + } + } else if right.is_word_boundary() && !right.is_space_or_nbsp() { + break; + } + } + cur + } + + /// Returns a new cursor that is positioned at the next word boundary + /// in logical order. + #[must_use] + pub fn next_logical_word(&self, layout: &Layout) -> Self { + let [left, right] = self.logical_clusters(layout); + if let Some(cluster) = right.or(left) { + let start = cluster.clone(); + let cluster = cluster.next_logical_word().unwrap_or(cluster); + if cluster.path == start.path { + return Self::from_byte_index(layout, usize::MAX, Affinity::Downstream); + } + return Self::from_cluster(layout, cluster, true); + } + *self + } + + /// Returns a new cursor that is positioned at the previous word boundary + /// in logical order. + #[must_use] + pub fn previous_logical_word(&self, layout: &Layout) -> Self { + let [left, right] = self.logical_clusters(layout); + if let Some(cluster) = left.or(right) { + let cluster = cluster.previous_logical_word().unwrap_or(cluster); + return Self::from_cluster(layout, cluster, true); + } + *self + } + + /// Returns a rectangle that represents the visual geometry of the cursor + /// in layout space. + /// + /// The `width` parameter defines the width of the resulting rectangle. + pub fn geometry(&self, layout: &Layout, width: f32) -> BoundingBox { + match self.visual_clusters(layout) { + [Some(left), Some(right)] => { + if left.is_end_of_line() { + if left.is_soft_line_break() { + let (cluster, at_end) = if left.is_rtl() + && self.affinity == Affinity::Downstream + || !left.is_rtl() && self.affinity == Affinity::Upstream + { + (left, true) + } else { + (right, false) + }; + cursor_rect(&cluster, at_end, width) + } else { + cursor_rect(&right, false, width) + } + } else { + cursor_rect(&left, true, width) + } + } + [Some(left), None] if left.is_hard_line_break() => last_line_cursor_rect(layout, width), + [Some(left), _] => cursor_rect(&left, true, width), + [_, Some(right)] => cursor_rect(&right, false, width), + _ => last_line_cursor_rect(layout, width), + } + } + + /// Returns the pair of clusters that logically bound the cursor + /// position. + /// + /// The order in the array is upstream followed by downstream. + pub fn logical_clusters<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> [Option>; 2] { + let upstream = self + .index + .checked_sub(1) + .and_then(|index| Cluster::from_byte_index(layout, index)); + let downstream = Cluster::from_byte_index(layout, self.index); + [upstream, downstream] + } + + /// Returns the pair of clusters that visually bound the cursor + /// position. + /// + /// The order in the array is left followed by right. + pub fn visual_clusters<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> [Option>; 2] { + if self.affinity == Affinity::Upstream { + if let Some(cluster) = self.upstream_cluster(layout) { + if cluster.is_rtl() { + [cluster.previous_visual(), Some(cluster)] + } else { + [Some(cluster.clone()), cluster.next_visual()] + } + } else if let Some(cluster) = self.downstream_cluster(layout) { + if cluster.is_rtl() { + [None, Some(cluster)] + } else { + [Some(cluster), None] + } + } else { + [None, None] + } + } else if let Some(cluster) = self.downstream_cluster(layout) { + if cluster.is_rtl() { + [Some(cluster.clone()), cluster.next_visual()] + } else { + [cluster.previous_visual(), Some(cluster)] + } + } else if let Some(cluster) = self.upstream_cluster(layout) { + if cluster.is_rtl() { + [None, Some(cluster)] + } else { + [Some(cluster), None] + } + } else { + [None, None] + } + } + + pub(crate) fn line(self, layout: &Layout) -> Option<(usize, Line<'_, B>)> { + let geometry = self.geometry(layout, 0.0); + layout.line_for_offset(geometry.y0 as f32) + } + + pub(crate) fn upstream_cluster(self, layout: &Layout) -> Option> { + self.index + .checked_sub(1) + .and_then(|index| Cluster::from_byte_index(layout, index)) + } + + pub(crate) fn downstream_cluster(self, layout: &Layout) -> Option> { + Cluster::from_byte_index(layout, self.index) + } + + #[cfg(feature = "accesskit")] + pub fn to_access_position( + &self, + layout: &Layout, + layout_access: &LayoutAccessibility, + ) -> Option { + if layout.data.text_len == 0 { + // If the text is empty, just return the first node with a + // character index of 0. + return Some(TextPosition { + node: *layout_access.access_ids_by_run_path.get(&(0, 0))?, + character_index: 0, + }); + } + // Prefer the downstream cluster except at the end of the text + // where we'll choose the upstream cluster and add 1 to the + // character index. + let (offset, path) = self + .downstream_cluster(layout) + .map(|cluster| (0, cluster.path)) + .or_else(|| { + self.upstream_cluster(layout) + .map(|cluster| (1, cluster.path)) + })?; + // If we're at the end of the layout and the layout ends with a newline + // then make sure we use the "phantom" run at the end so that + // AccessKit has correct visual geometry for the cursor. + let (run_path, character_index) = if self.index == layout.data.text_len + && layout + .data + .clusters + .last() + .map(|cluster| cluster.info.whitespace() == Whitespace::Newline) + .unwrap_or_default() + { + ((path.line_index() + 1, 0), 0) + } else { + ( + (path.line_index(), path.run_index()), + path.logical_index() + offset, + ) + }; + let id = layout_access.access_ids_by_run_path.get(&run_path)?; + Some(TextPosition { + node: *id, + character_index, + }) + } +} + +// --- + +fn affinity_for_dir(is_rtl: bool, moving_right: bool) -> Affinity { + match (is_rtl, moving_right) { + (true, true) | (false, false) => Affinity::Downstream, + _ => Affinity::Upstream, + } +} + +fn cursor_rect(cluster: &Cluster<'_, B>, at_end: bool, size: f32) -> BoundingBox { + let mut line_x = cluster.visual_offset().unwrap_or_default(); + if at_end { + line_x += cluster.advance(); + } + let line = cluster.line(); + let metrics = line.metrics(); + BoundingBox::new( + line_x as f64, + metrics.min_coord as f64, + (line_x + size) as f64, + metrics.max_coord as f64, + ) +} + +fn last_line_cursor_rect(layout: &Layout, size: f32) -> BoundingBox { + if let Some(line) = layout.get(layout.len().saturating_sub(1)) { + let metrics = line.metrics(); + BoundingBox::new( + 0.0, + metrics.min_coord as f64, + size as f64, + metrics.max_coord as f64, + ) + } else { + BoundingBox::default() + } +} diff --git a/parley/src/layout/editor.rs b/parley/src/editing/editor.rs similarity index 99% rename from parley/src/layout/editor.rs rename to parley/src/editing/editor.rs index 46906a24f..d8c494d21 100644 --- a/parley/src/layout/editor.rs +++ b/parley/src/editing/editor.rs @@ -3,14 +3,6 @@ //! A simple plain text editor and related types. -use crate::{ - BoundingBox, FontContext, LayoutContext, StyleProperty, StyleSet, - layout::{ - Affinity, Alignment, AlignmentOptions, Layout, - cursor::{Cursor, Selection}, - }, - style::Brush, -}; use alloc::{borrow::ToOwned, string::String, vec::Vec}; use core::{ cmp::PartialEq, @@ -20,6 +12,11 @@ use core::{ ops::Range, }; +use crate::editing::{Cursor, Selection}; +use crate::layout::{Affinity, Alignment, AlignmentOptions, Layout}; +use crate::style::Brush; +use crate::{BoundingBox, FontContext, LayoutContext, StyleProperty, StyleSet}; + #[cfg(feature = "accesskit")] use crate::layout::LayoutAccessibility; #[cfg(feature = "accesskit")] diff --git a/parley/src/editing/mod.rs b/parley/src/editing/mod.rs new file mode 100644 index 000000000..95a5959d4 --- /dev/null +++ b/parley/src/editing/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +mod cursor; +mod editor; +mod selection; + +pub use self::cursor::*; +pub use self::editor::*; +pub use self::selection::*; diff --git a/parley/src/layout/cursor.rs b/parley/src/editing/selection.rs similarity index 60% rename from parley/src/layout/cursor.rs rename to parley/src/editing/selection.rs index 0a07cae48..b9ac2f282 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/editing/selection.rs @@ -1,432 +1,15 @@ -// Copyright 2021 the Parley Authors +// Copyright 2025 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -//! Text selection support. - -#[cfg(feature = "accesskit")] -use super::LayoutAccessibility; -use super::{Affinity, BreakReason, Brush, Cluster, ClusterSide, Layout, Line, LineItem}; use crate::BoundingBox; +use crate::editing::Cursor; #[cfg(feature = "accesskit")] -use accesskit::TextPosition; +use crate::layout::LayoutAccessibility; +use crate::layout::{Affinity, BreakReason, Cluster, Layout, LineItem}; +use crate::style::Brush; + use alloc::vec::Vec; use core::ops::Range; -#[cfg(feature = "accesskit")] -use swash::text::cluster::Whitespace; - -/// Defines a position with a text layout. -#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct Cursor { - index: usize, - affinity: Affinity, -} - -impl Cursor { - /// Creates a new cursor from the given byte index and affinity. - pub fn from_byte_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { - if let Some(cluster) = Cluster::from_byte_index(layout, index) { - let index = cluster.text_range().start; - Self { - index, - affinity: if index != 0 { - affinity - } else { - // There is no Upstream cluster of the 0 position so we force Downstream affinity. - Affinity::Downstream - }, - } - } else { - Self { - index: layout.data.text_len, - affinity: Affinity::Upstream, - } - } - } - - /// Creates a new cursor from the given coordinates. - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - let (index, affinity) = if let Some((cluster, side)) = Cluster::from_point(layout, x, y) { - let is_leading = side == ClusterSide::Left; - if cluster.is_rtl() { - if is_leading { - (cluster.text_range().end, Affinity::Upstream) - } else { - (cluster.text_range().start, Affinity::Downstream) - } - } else { - // We never want to position the cursor _after_ a hard - // line since that cursor appears visually at the start - // of the next line - if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) { - (cluster.text_range().start, Affinity::Downstream) - } else { - (cluster.text_range().end, Affinity::Upstream) - } - } - } else { - (layout.data.text_len, Affinity::Downstream) - }; - Self { index, affinity } - } - - #[cfg(feature = "accesskit")] - pub fn from_access_position( - pos: &TextPosition, - layout: &Layout, - layout_access: &LayoutAccessibility, - ) -> Option { - let (line_index, run_index) = *layout_access.run_paths_by_access_id.get(&pos.node)?; - let line = layout.get(line_index)?; - let run = line.item(run_index)?.run()?; - let index = run - .get(pos.character_index) - .map(|cluster| cluster.text_range().start) - .unwrap_or(layout.data.text_len); - Some(Self::from_byte_index(layout, index, Affinity::Downstream)) - } - - fn from_cluster( - layout: &Layout, - cluster: Cluster<'_, B>, - moving_right: bool, - ) -> Self { - Self::from_byte_index( - layout, - cluster.text_range().start, - affinity_for_dir(cluster.is_rtl(), moving_right), - ) - } - - /// Returns the logical text index of the cursor. - pub fn index(&self) -> usize { - self.index - } - - /// Returns the affinity of the cursor. - /// - /// This defines the direction from which the cursor entered its current - /// position and affects the visual location of the rendered cursor. - pub fn affinity(&self) -> Affinity { - self.affinity - } - - /// Returns a new cursor that is guaranteed to be within the bounds of the - /// given layout. - #[must_use] - pub fn refresh(&self, layout: &Layout) -> Self { - Self::from_byte_index(layout, self.index, self.affinity) - } - - /// Returns a new cursor that is positioned at the previous cluster boundary - /// in visual order. - #[must_use] - pub fn previous_visual(&self, layout: &Layout) -> Self { - let [left, right] = self.visual_clusters(layout); - if let (Some(left), Some(right)) = (&left, &right) { - if left.is_soft_line_break() { - if left.is_rtl() && self.affinity == Affinity::Upstream { - let index = if right.is_rtl() { - left.text_range().start - } else { - left.text_range().end - }; - return Self::from_byte_index(layout, index, Affinity::Downstream); - } else if !left.is_rtl() && self.affinity == Affinity::Downstream { - let index = if right.is_rtl() { - right.text_range().end - } else { - right.text_range().start - }; - return Self::from_byte_index(layout, index, Affinity::Upstream); - } - } - } - if let Some(left) = left { - let index = if left.is_rtl() { - left.text_range().end - } else { - left.text_range().start - }; - return Self::from_byte_index(layout, index, affinity_for_dir(left.is_rtl(), false)); - } - *self - } - - /// Returns a new cursor that is positioned at the next cluster boundary - /// in visual order. - #[must_use] - pub fn next_visual(&self, layout: &Layout) -> Self { - let [left, right] = self.visual_clusters(layout); - if let (Some(left), Some(right)) = (&left, &right) { - if left.is_soft_line_break() { - if left.is_rtl() && self.affinity == Affinity::Downstream { - let index = if right.is_rtl() { - right.text_range().end - } else { - right.text_range().start - }; - return Self::from_byte_index(layout, index, Affinity::Upstream); - } else if !left.is_rtl() && self.affinity == Affinity::Upstream { - let index = if right.is_rtl() { - right.text_range().end - } else { - right.text_range().start - }; - return Self::from_byte_index(layout, index, Affinity::Downstream); - } - } - let index = if right.is_rtl() { - right.text_range().start - } else { - right.text_range().end - }; - return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); - } - if let Some(right) = right { - let index = if right.is_rtl() { - right.text_range().start - } else { - right.text_range().end - }; - return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); - } - *self - } - - /// Returns a new cursor that is positioned at the next word boundary - /// in visual order. - #[must_use] - pub fn next_visual_word(&self, layout: &Layout) -> Self { - let mut cur = *self; - loop { - let next = cur.next_visual(layout); - if next == cur { - break; - } - cur = next; - let [Some(left), Some(right)] = cur.visual_clusters(layout) else { - break; - }; - if left.is_rtl() { - if left.is_word_boundary() && !left.is_space_or_nbsp() { - break; - } - } else if right.is_word_boundary() && !left.is_space_or_nbsp() { - break; - } - } - cur - } - - /// Returns a new cursor that is positioned at the previous word boundary - /// in visual order. - #[must_use] - pub fn previous_visual_word(&self, layout: &Layout) -> Self { - let mut cur = *self; - loop { - let next = cur.previous_visual(layout); - if next == cur { - break; - } - cur = next; - let [Some(left), Some(right)] = cur.visual_clusters(layout) else { - break; - }; - if left.is_rtl() { - if left.is_word_boundary() - && (left.is_space_or_nbsp() - || (right.is_word_boundary() && !right.is_space_or_nbsp())) - { - break; - } - } else if right.is_word_boundary() && !right.is_space_or_nbsp() { - break; - } - } - cur - } - - /// Returns a new cursor that is positioned at the next word boundary - /// in logical order. - #[must_use] - pub fn next_logical_word(&self, layout: &Layout) -> Self { - let [left, right] = self.logical_clusters(layout); - if let Some(cluster) = right.or(left) { - let start = cluster.clone(); - let cluster = cluster.next_logical_word().unwrap_or(cluster); - if cluster.path == start.path { - return Self::from_byte_index(layout, usize::MAX, Affinity::Downstream); - } - return Self::from_cluster(layout, cluster, true); - } - *self - } - - /// Returns a new cursor that is positioned at the previous word boundary - /// in logical order. - #[must_use] - pub fn previous_logical_word(&self, layout: &Layout) -> Self { - let [left, right] = self.logical_clusters(layout); - if let Some(cluster) = left.or(right) { - let cluster = cluster.previous_logical_word().unwrap_or(cluster); - return Self::from_cluster(layout, cluster, true); - } - *self - } - - /// Returns a rectangle that represents the visual geometry of the cursor - /// in layout space. - /// - /// The `width` parameter defines the width of the resulting rectangle. - pub fn geometry(&self, layout: &Layout, width: f32) -> BoundingBox { - match self.visual_clusters(layout) { - [Some(left), Some(right)] => { - if left.is_end_of_line() { - if left.is_soft_line_break() { - let (cluster, at_end) = if left.is_rtl() - && self.affinity == Affinity::Downstream - || !left.is_rtl() && self.affinity == Affinity::Upstream - { - (left, true) - } else { - (right, false) - }; - cursor_rect(&cluster, at_end, width) - } else { - cursor_rect(&right, false, width) - } - } else { - cursor_rect(&left, true, width) - } - } - [Some(left), None] if left.is_hard_line_break() => last_line_cursor_rect(layout, width), - [Some(left), _] => cursor_rect(&left, true, width), - [_, Some(right)] => cursor_rect(&right, false, width), - _ => last_line_cursor_rect(layout, width), - } - } - - /// Returns the pair of clusters that logically bound the cursor - /// position. - /// - /// The order in the array is upstream followed by downstream. - pub fn logical_clusters<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> [Option>; 2] { - let upstream = self - .index - .checked_sub(1) - .and_then(|index| Cluster::from_byte_index(layout, index)); - let downstream = Cluster::from_byte_index(layout, self.index); - [upstream, downstream] - } - - /// Returns the pair of clusters that visually bound the cursor - /// position. - /// - /// The order in the array is left followed by right. - pub fn visual_clusters<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> [Option>; 2] { - if self.affinity == Affinity::Upstream { - if let Some(cluster) = self.upstream_cluster(layout) { - if cluster.is_rtl() { - [cluster.previous_visual(), Some(cluster)] - } else { - [Some(cluster.clone()), cluster.next_visual()] - } - } else if let Some(cluster) = self.downstream_cluster(layout) { - if cluster.is_rtl() { - [None, Some(cluster)] - } else { - [Some(cluster), None] - } - } else { - [None, None] - } - } else if let Some(cluster) = self.downstream_cluster(layout) { - if cluster.is_rtl() { - [Some(cluster.clone()), cluster.next_visual()] - } else { - [cluster.previous_visual(), Some(cluster)] - } - } else if let Some(cluster) = self.upstream_cluster(layout) { - if cluster.is_rtl() { - [None, Some(cluster)] - } else { - [Some(cluster), None] - } - } else { - [None, None] - } - } - - fn line(self, layout: &Layout) -> Option<(usize, Line<'_, B>)> { - let geometry = self.geometry(layout, 0.0); - layout.line_for_offset(geometry.y0 as f32) - } - - fn upstream_cluster(self, layout: &Layout) -> Option> { - self.index - .checked_sub(1) - .and_then(|index| Cluster::from_byte_index(layout, index)) - } - - fn downstream_cluster(self, layout: &Layout) -> Option> { - Cluster::from_byte_index(layout, self.index) - } - - #[cfg(feature = "accesskit")] - pub fn to_access_position( - &self, - layout: &Layout, - layout_access: &LayoutAccessibility, - ) -> Option { - if layout.data.text_len == 0 { - // If the text is empty, just return the first node with a - // character index of 0. - return Some(TextPosition { - node: *layout_access.access_ids_by_run_path.get(&(0, 0))?, - character_index: 0, - }); - } - // Prefer the downstream cluster except at the end of the text - // where we'll choose the upstream cluster and add 1 to the - // character index. - let (offset, path) = self - .downstream_cluster(layout) - .map(|cluster| (0, cluster.path)) - .or_else(|| { - self.upstream_cluster(layout) - .map(|cluster| (1, cluster.path)) - })?; - // If we're at the end of the layout and the layout ends with a newline - // then make sure we use the "phantom" run at the end so that - // AccessKit has correct visual geometry for the cursor. - let (run_path, character_index) = if self.index == layout.data.text_len - && layout - .data - .clusters - .last() - .map(|cluster| cluster.info.whitespace() == Whitespace::Newline) - .unwrap_or_default() - { - ((path.line_index() + 1, 0), 0) - } else { - ( - (path.line_index(), path.run_index()), - path.logical_index() + offset, - ) - }; - let id = layout_access.access_ids_by_run_path.get(&run_path)?; - Some(TextPosition { - node: *id, - character_index, - }) - } -} /// Defines a range within a text layout. #[derive(Copy, Clone, Default, Debug)] @@ -437,6 +20,14 @@ pub struct Selection { h_pos: Option, } +#[derive(Copy, Clone, Default, Debug)] +enum AnchorBase { + #[default] + Cluster, + Word(Cursor, Cursor), + Line(Cursor, Cursor), +} + impl Selection { /// Creates a new selection from the given anchor and focus cursors. pub fn new(anchor: Cursor, focus: Cursor) -> Self { @@ -1051,50 +642,6 @@ impl From for Selection { } } -#[derive(Copy, Clone, Default, Debug)] -enum AnchorBase { - #[default] - Cluster, - Word(Cursor, Cursor), - Line(Cursor, Cursor), -} - -fn cursor_rect(cluster: &Cluster<'_, B>, at_end: bool, size: f32) -> BoundingBox { - let mut line_x = cluster.visual_offset().unwrap_or_default(); - if at_end { - line_x += cluster.advance(); - } - let line = cluster.line(); - let metrics = line.metrics(); - BoundingBox::new( - line_x as f64, - metrics.min_coord as f64, - (line_x + size) as f64, - metrics.max_coord as f64, - ) -} - -fn last_line_cursor_rect(layout: &Layout, size: f32) -> BoundingBox { - if let Some(line) = layout.get(layout.len().saturating_sub(1)) { - let metrics = line.metrics(); - BoundingBox::new( - 0.0, - metrics.min_coord as f64, - size as f64, - metrics.max_coord as f64, - ) - } else { - BoundingBox::default() - } -} - -fn affinity_for_dir(is_rtl: bool, moving_right: bool) -> Affinity { - match (is_rtl, moving_right) { - (true, true) | (false, false) => Affinity::Downstream, - _ => Affinity::Upstream, - } -} - /// Given the anchor base and the target selection, returns the anchor and focus of the resulting selection extension. fn extend_selection(target_selection: Selection, anchor_base: [Cursor; 2]) -> [Cursor; 2] { let extending_to_the_right = target_selection.anchor.index >= anchor_base[0].index; diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 1622f20c9..3b772e884 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -10,12 +10,9 @@ mod run; pub(crate) mod data; -pub mod cursor; -pub mod editor; - use self::alignment::align; -use super::style::Brush; +use crate::style::Brush; use crate::{FontData, InlineBox, OverflowWrap}; #[cfg(feature = "accesskit")] use accesskit::{Node, NodeId, Role, TextDirection, TreeUpdate}; @@ -31,13 +28,16 @@ use swash::text::cluster::Boundary; pub use alignment::AlignmentOptions; pub use cluster::{Affinity, ClusterPath, ClusterSide}; -pub use cursor::{Cursor, Selection}; pub use data::BreakReason; pub(crate) use line::LineItem; pub use line::greedy::BreakLines; pub use line::{GlyphRun, LineMetrics, PositionedInlineBox, PositionedLayoutItem}; pub use run::RunMetrics; +// TODO - Deprecation not yet active to ease internal code migration. +#[deprecated(since = "TBD", note = "Access from the `editing` module instead.")] +pub use crate::editing::{Cursor, Selection}; + /// Alignment of a layout. #[derive(Copy, Clone, Default, PartialEq, Eq, Debug)] #[repr(u8)] @@ -253,9 +253,9 @@ pub struct Run<'a, B: Brush> { /// Atomic unit of text. #[derive(Copy, Clone)] pub struct Cluster<'a, B: Brush> { - path: ClusterPath, - run: Run<'a, B>, - data: &'a ClusterData, + pub(crate) path: ClusterPath, + pub(crate) run: Run<'a, B>, + pub(crate) data: &'a ClusterData, } /// Glyph with an offset and advance. diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 411fabb86..fb9ba03f4 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -114,6 +114,7 @@ mod shape; mod swash_convert; mod util; +pub mod editing; pub mod layout; pub mod style; @@ -130,8 +131,7 @@ pub use inline_box::InlineBox; #[doc(inline)] pub use layout::Layout; -pub use layout::editor::{PlainEditor, PlainEditorDriver}; - +pub use editing::*; pub use layout::*; pub use style::*; diff --git a/parley/src/style/styleset.rs b/parley/src/style/styleset.rs index f9d0c149d..bc89fd7b6 100644 --- a/parley/src/style/styleset.rs +++ b/parley/src/style/styleset.rs @@ -9,9 +9,9 @@ type StyleProperty = crate::StyleProperty<'static, Brush>; /// A long-lived collection of [`StyleProperties`](super::StyleProperty), containing at /// most one of each property. /// -/// This is used by [`PlainEditor`](crate::editor::PlainEditor) to provide a reasonably ergonomic +/// This is used by [`PlainEditor`](crate::editing::PlainEditor) to provide a reasonably ergonomic /// mutable API for styles applied to all text managed by it. -/// This can be accessed using [`PlainEditor::edit_styles`](crate::editor::PlainEditor::edit_styles). +/// This can be accessed using [`PlainEditor::edit_styles`](crate::editing::PlainEditor::edit_styles). /// /// These styles do not have a corresponding range, and are generally unsuited for rich text. #[derive(Clone, Debug)]