Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cursor rework #170

Merged
merged 20 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions examples/vello_editor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use winit::window::Window;
mod access_ids;
use access_ids::{TEXT_INPUT_ID, WINDOW_ID};

// #[path = "text2.rs"]
mod text;
use parley::{GenericFamily, StyleProperty};

Expand Down Expand Up @@ -304,7 +303,7 @@ impl ApplicationHandler<accesskit_winit::Event> for SimpleVelloApp<'_> {
base_color: Color::rgb8(30, 30, 30), // Background color
width,
height,
antialiasing_method: AaConfig::Msaa16,
antialiasing_method: AaConfig::Area,
},
)
.expect("failed to render to surface");
Expand Down
18 changes: 5 additions & 13 deletions examples/vello_editor/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,15 @@ impl Editor {
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
Key::Character(c) if action_mod && matches!(c.as_str(), "c" | "x" | "v") => {
use clipboard_rs::{Clipboard, ClipboardContext};
use parley::layout::editor::ActiveText;

match c.to_lowercase().as_str() {
"c" => {
if let ActiveText::Selection(text) = self.editor.active_text() {
if let Some(text) = self.editor.selected_text() {
let cb = ClipboardContext::new().unwrap();
cb.set_text(text.to_owned()).ok();
}
}
"x" => {
if let ActiveText::Selection(text) = self.editor.active_text() {
if let Some(text) = self.editor.selected_text() {
let cb = ClipboardContext::new().unwrap();
cb.set_text(text.to_owned()).ok();
self.transact(|txn| txn.delete_selection());
Expand Down Expand Up @@ -230,8 +228,6 @@ impl Editor {
}
_ => (),
}

// println!("Active text: {:?}", self.active_text());
}
WindowEvent::Touch(Touch {
phase, location, ..
Expand Down Expand Up @@ -262,6 +258,7 @@ impl Editor {
WindowEvent::MouseInput { state, button, .. } => {
if button == winit::event::MouseButton::Left {
self.pointer_down = state.is_pressed();
self.cursor_reset();
if self.pointer_down {
let now = Instant::now();
if let Some(last) = self.last_click_time.take() {
Expand All @@ -281,8 +278,6 @@ impl Editor {
3 => txn.select_line_at_point(cursor_pos.0, cursor_pos.1),
_ => txn.move_to_point(cursor_pos.0, cursor_pos.1),
});

// println!("Active text: {:?}", self.active_text());
}
}
}
Expand All @@ -291,9 +286,9 @@ impl Editor {
self.cursor_pos = (position.x as f32 - INSET, position.y as f32 - INSET);
// macOS seems to generate a spurious move after selecting word?
if self.pointer_down && prev_pos != self.cursor_pos {
self.cursor_reset();
let cursor_pos = self.cursor_pos;
self.transact(|txn| txn.extend_selection_to_point(cursor_pos.0, cursor_pos.1));
// println!("Active text: {:?}", self.active_text());
}
}
_ => {}
Expand Down Expand Up @@ -324,12 +319,9 @@ impl Editor {
scene.fill(Fill::NonZero, transform, Color::STEEL_BLUE, None, &rect);
}
if self.cursor_visible {
if let Some(cursor) = self.editor.selection_strong_geometry(1.5) {
if let Some(cursor) = self.editor.cursor_geometry(1.5) {
scene.fill(Fill::NonZero, transform, Color::WHITE, None, &cursor);
};
if let Some(cursor) = self.editor.selection_weak_geometry(1.5) {
scene.fill(Fill::NonZero, transform, Color::LIGHT_GRAY, None, &cursor);
};
}
for line in self.editor.lines() {
for item in line.items() {
Expand Down
137 changes: 71 additions & 66 deletions parley/src/layout/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT

use super::*;
use swash::text::cluster::Whitespace;

/// Defines the visual side of the cluster for hit testing.
///
/// See [`Cluster::from_point`].
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ClusterSide {
/// Cluster was hit on the left half.
Left,
/// Cluster was hit on the right half.
Right,
}

impl<'a, B: Brush> Cluster<'a, B> {
/// Returns the cluster for the given layout and byte index.
pub fn from_index(layout: &'a Layout<B>, byte_index: usize) -> Option<Self> {
pub fn from_byte_index(layout: &'a Layout<B>, byte_index: usize) -> Option<Self> {
let mut path = ClusterPath::default();
if let Some((line_index, line)) = layout.line_for_byte_index(byte_index) {
path.line_index = line_index as u32;
Expand All @@ -22,11 +34,11 @@ impl<'a, B: Brush> Cluster<'a, B> {
}
}
}
path.cluster(layout)
None
}

/// Returns the cluster and affinity for the given layout and point.
pub fn from_point(layout: &'a Layout<B>, x: f32, y: f32) -> Option<(Self, Affinity)> {
/// Returns the cluster and side for the given layout and point.
pub fn from_point(layout: &'a Layout<B>, x: f32, y: f32) -> Option<(Self, ClusterSide)> {
let mut path = ClusterPath::default();
if let Some((line_index, line)) = layout.line_for_offset(y) {
path.line_index = line_index as u32;
Expand All @@ -52,13 +64,20 @@ impl<'a, B: Brush> Cluster<'a, B> {
if x > offset && !is_last_cluster {
continue;
}
let affinity =
Affinity::new(cluster.is_rtl(), x <= edge + cluster_advance * 0.5);
return Some((path.cluster(layout)?, affinity));
let side = if x <= edge + cluster_advance * 0.5 {
ClusterSide::Left
} else {
ClusterSide::Right
};
return Some((path.cluster(layout)?, side));
}
}
}
Some((path.cluster(layout)?, Affinity::default()))
if y <= 0.0 {
Some((path.cluster(layout)?, ClusterSide::Left))
} else {
None
}
}

/// Returns the line that contains the cluster.
Expand Down Expand Up @@ -109,19 +128,28 @@ impl<'a, B: Brush> Cluster<'a, B> {

/// Returns `true` if the cluster is a soft line break.
pub fn is_soft_line_break(&self) -> bool {
self.data.info.boundary() == Boundary::Line
self.is_end_of_line()
&& matches!(
self.line().data.break_reason,
BreakReason::Regular | BreakReason::Emergency
)
}

/// Returns `true` if the cluster is a hard line break.
pub fn is_hard_line_break(&self) -> bool {
self.data.info.boundary() == Boundary::Mandatory
self.data.info.whitespace() == Whitespace::Newline
}

/// Returns `true` if the cluster is a space or no-break space.
pub fn is_space_or_nbsp(&self) -> bool {
self.data.info.whitespace().is_space_or_nbsp()
}

/// Returns `true` if the cluster is an emoji sequence.
pub fn is_emoji(&self) -> bool {
self.data.info.is_emoji()
}

/// Returns an iterator over the glyphs in the cluster.
pub fn glyphs(&self) -> impl Iterator<Item = Glyph> + 'a + Clone {
if self.data.glyph_len == 0xFF {
Expand Down Expand Up @@ -162,33 +190,6 @@ impl<'a, B: Brush> Cluster<'a, B> {
}
}

/// If this cluster, combined with the given affinity, sits on a
/// directional boundary, returns the cluster that represents the alternate
/// insertion position.
///
/// For example, if this cluster is a left-to-right cluster, then this
/// will return the cluster that represents the position where a
/// right-to-left character would be inserted, and vice versa.
pub fn bidi_link(&self, affinity: Affinity) -> Option<Self> {
let run_end = self.run.len().checked_sub(1)?;
let visual_index = self.run.logical_to_visual(self.path.logical_index())?;
let is_rtl = self.is_rtl();
let is_leading = affinity.is_visually_leading(is_rtl);
let at_start = visual_index == 0 && is_leading;
let at_end = visual_index == run_end && !is_leading;
let other = if (at_start && !is_rtl) || (at_end && is_rtl) {
self.previous_logical()?
} else if (at_end && !is_rtl) || (at_start && is_rtl) {
self.next_logical()?
} else {
return None;
};
if other.is_rtl() == is_rtl {
return None;
}
Some(other)
}

/// Returns the cluster that follows this one in logical order.
pub fn next_logical(&self) -> Option<Self> {
if self.path.logical_index() + 1 < self.run.cluster_range().len() {
Expand All @@ -205,7 +206,7 @@ impl<'a, B: Brush> Cluster<'a, B> {
return None;
}
// We have to search for the cluster containing our end index
Self::from_index(self.run.layout, index)
Self::from_byte_index(self.run.layout, index)
}
}

Expand All @@ -220,7 +221,7 @@ impl<'a, B: Brush> Cluster<'a, B> {
}
.cluster(self.run.layout)
} else {
Self::from_index(self.run.layout, self.text_range().start.checked_sub(1)?)
Self::from_byte_index(self.run.layout, self.text_range().start.checked_sub(1)?)
}
}

Expand Down Expand Up @@ -299,7 +300,7 @@ impl<'a, B: Brush> Cluster<'a, B> {
}

/// Returns the next cluster that is marked as a word boundary.
pub fn next_word(&self) -> Option<Self> {
pub fn next_logical_word(&self) -> Option<Self> {
let mut cluster = self.clone();
while let Some(next) = cluster.next_logical() {
if next.is_word_boundary() {
Expand All @@ -310,8 +311,20 @@ impl<'a, B: Brush> Cluster<'a, B> {
None
}

/// Returns the next cluster that is marked as a word boundary.
pub fn next_visual_word(&self) -> Option<Self> {
let mut cluster = self.clone();
while let Some(next) = cluster.next_visual() {
if next.is_word_boundary() {
return Some(next);
}
cluster = next;
}
None
}

/// Returns the previous cluster that is marked as a word boundary.
pub fn previous_word(&self) -> Option<Self> {
pub fn previous_logical_word(&self) -> Option<Self> {
let mut cluster = self.clone();
while let Some(prev) = cluster.previous_logical() {
if prev.is_word_boundary() {
Expand All @@ -322,13 +335,25 @@ impl<'a, B: Brush> Cluster<'a, B> {
None
}

/// Returns the previous cluster that is marked as a word boundary.
pub fn previous_visual_word(&self) -> Option<Self> {
let mut cluster = self.clone();
while let Some(prev) = cluster.previous_visual() {
if prev.is_word_boundary() {
return Some(prev);
}
cluster = prev;
}
None
}

/// Returns the visual offset of this cluster along direction of text flow.
///
/// This cost of this function is roughly linear in the number of clusters
/// on the containing line.
pub fn visual_offset(&self) -> Option<f32> {
let line = self.path.line(self.run.layout)?;
let mut offset = 0.0;
let mut offset = line.metrics().offset;
for run_index in 0..=self.path.run_index() {
let run = line.run(run_index)?;
if run_index != self.path.run_index() {
Expand All @@ -351,42 +376,22 @@ impl<'a, B: Brush> Cluster<'a, B> {
/// Determines how a cursor attaches to a cluster.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum Affinity {
/// Left side for LTR clusters and right side for RTL clusters.
/// Cursor is attached to the character that is logically following in the
/// text stream.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This is so much better.

#[default]
Downstream = 0,
/// Right side for LTR clusters and left side for RTL clusters.
/// Cursor is attached to the character that is logically preceding in the
/// text stream.
Upstream = 1,
}

impl Affinity {
pub(crate) fn new(is_rtl: bool, is_leading: bool) -> Self {
match (is_rtl, is_leading) {
// trailing edge of RTL and leading edge of LTR
(true, false) | (false, true) => Affinity::Downstream,
// leading edge of RTL and trailing edge of LTR
(true, true) | (false, false) => Affinity::Upstream,
}
}

pub fn invert(&self) -> Self {
match self {
Self::Downstream => Self::Upstream,
Self::Upstream => Self::Downstream,
}
}

/// Returns true if the cursor should be placed on the leading edge.
pub fn is_visually_leading(&self, is_rtl: bool) -> bool {
match (*self, is_rtl) {
(Self::Upstream, true) | (Self::Downstream, false) => true,
(Self::Upstream, false) | (Self::Downstream, true) => false,
}
}

/// Returns true if the cursor should be placed on the trailing edge.
pub fn is_visually_trailing(&self, is_rtl: bool) -> bool {
!self.is_visually_leading(is_rtl)
}
}

/// Index based path to a cluster.
Expand Down
Loading