diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index a6445bea..13f33040 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -349,8 +349,8 @@ fn main() -> Result<()> { event_loop .run_app(&mut app) .expect("Couldn't run event loop"); - let [text1, text2] = app.editor.text(); - print!("{text1}{text2}"); + let text = app.editor.text(); + print!("{text}"); Ok(()) } diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 9651c06a..c3202933 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -3,7 +3,7 @@ use accesskit::{Node, TreeUpdate}; use core::default::Default; -use parley::{layout::PositionedLayoutItem, GenericFamily, StyleProperty}; +use parley::{editor::SplitString, layout::PositionedLayoutItem, GenericFamily, StyleProperty}; use peniko::{kurbo::Affine, Color, Fill}; use std::time::{Duration, Instant}; use vello::{ @@ -60,16 +60,15 @@ impl Editor { } } - pub fn drive(&mut self, callback: impl FnOnce(&mut PlainEditorDriver<'_, Color>)) { - self.editor - .drive(&mut self.font_cx, &mut self.layout_cx, callback); + fn driver(&mut self) -> PlainEditorDriver<'_, Color> { + self.editor.driver(&mut self.font_cx, &mut self.layout_cx) } pub fn editor(&mut self) -> &mut PlainEditor { &mut self.editor } - pub fn text(&self) -> [&str; 2] { + pub fn text(&self) -> SplitString<'_> { self.editor.text() } @@ -117,6 +116,7 @@ impl Editor { return; } self.cursor_reset(); + let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx); #[allow(unused)] let (shift, action_mod) = self .modifiers @@ -138,36 +138,34 @@ impl Editor { use clipboard_rs::{Clipboard, ClipboardContext}; match c.to_lowercase().as_str() { "c" => { - if let Some(text) = self.editor.selected_text() { + if let Some(text) = drv.editor.selected_text() { let cb = ClipboardContext::new().unwrap(); cb.set_text(text.to_owned()).ok(); } } "x" => { - if let Some(text) = self.editor.selected_text() { + if let Some(text) = drv.editor.selected_text() { let cb = ClipboardContext::new().unwrap(); cb.set_text(text.to_owned()).ok(); - self.drive(|drv| drv.delete_selection()); + drv.delete_selection(); } } "v" => { let cb = ClipboardContext::new().unwrap(); let text = cb.get_text().unwrap_or_default(); - self.drive(|drv| drv.insert_or_replace_selection(&text)); + drv.insert_or_replace_selection(&text); } _ => (), } } Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => { - self.drive(|drv| { - if shift { - drv.collapse_selection(); - } else { - drv.select_all(); - } - }); + if shift { + drv.collapse_selection(); + } else { + drv.select_all(); + }; } - Key::Named(NamedKey::ArrowLeft) => self.drive(|drv| { + Key::Named(NamedKey::ArrowLeft) => { if action_mod { if shift { drv.select_word_left(); @@ -179,8 +177,8 @@ impl Editor { } else { drv.move_left(); } - }), - Key::Named(NamedKey::ArrowRight) => self.drive(|drv| { + } + Key::Named(NamedKey::ArrowRight) => { if action_mod { if shift { drv.select_word_right(); @@ -192,22 +190,22 @@ impl Editor { } else { drv.move_right(); } - }), - Key::Named(NamedKey::ArrowUp) => self.drive(|drv| { + } + Key::Named(NamedKey::ArrowUp) => { if shift { drv.select_up(); } else { drv.move_up(); } - }), - Key::Named(NamedKey::ArrowDown) => self.drive(|drv| { + } + Key::Named(NamedKey::ArrowDown) => { if shift { drv.select_down(); } else { drv.move_down(); } - }), - Key::Named(NamedKey::Home) => self.drive(|drv| { + } + Key::Named(NamedKey::Home) => { if action_mod { if shift { drv.select_to_text_start(); @@ -219,8 +217,11 @@ impl Editor { } else { drv.move_to_line_start(); } - }), - Key::Named(NamedKey::End) => self.drive(|drv| { + } + Key::Named(NamedKey::End) => { + let this = &mut *self; + let mut drv = this.driver(); + if action_mod { if shift { drv.select_to_text_end(); @@ -232,29 +233,29 @@ impl Editor { } else { drv.move_to_line_end(); } - }), - Key::Named(NamedKey::Delete) => self.drive(|drv| { + } + Key::Named(NamedKey::Delete) => { if action_mod { drv.delete_word(); } else { drv.delete(); } - }), - Key::Named(NamedKey::Backspace) => self.drive(|drv| { + } + Key::Named(NamedKey::Backspace) => { if action_mod { drv.backdelete_word(); } else { drv.backdelete(); } - }), + } Key::Named(NamedKey::Enter) => { - self.drive(|drv| drv.insert_or_replace_selection("\n")); + drv.insert_or_replace_selection("\n"); } Key::Named(NamedKey::Space) => { - self.drive(|drv| drv.insert_or_replace_selection(" ")); + drv.insert_or_replace_selection(" "); } Key::Character(s) => { - self.drive(|drv| drv.insert_or_replace_selection(&s)); + drv.insert_or_replace_selection(&s); } _ => (), } @@ -262,25 +263,22 @@ impl Editor { WindowEvent::Touch(Touch { phase, location, .. }) if !self.editor.is_composing() => { + let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx); use winit::event::TouchPhase::*; match phase { Started => { // TODO: start a timer to convert to a SelectWordAtPoint - self.drive(|drv| { - drv.move_to_point(location.x as f32 - INSET, location.y as f32 - INSET); - }); + drv.move_to_point(location.x as f32 - INSET, location.y as f32 - INSET); } Cancelled => { - self.drive(|drv| drv.collapse_selection()); + drv.collapse_selection(); } Moved => { // TODO: cancel SelectWordAtPoint timer - self.drive(|drv| { - drv.extend_selection_to_point( - location.x as f32 - INSET, - location.y as f32 - INSET, - ); - }); + drv.extend_selection_to_point( + location.x as f32 - INSET, + location.y as f32 - INSET, + ); } Ended => (), } @@ -303,11 +301,12 @@ impl Editor { self.last_click_time = Some(now); let click_count = self.click_count; let cursor_pos = self.cursor_pos; - self.drive(|drv| match click_count { + let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx); + match click_count { 2 => drv.select_word_at_point(cursor_pos.0, cursor_pos.1), 3 => drv.select_line_at_point(cursor_pos.0, cursor_pos.1), _ => drv.move_to_point(cursor_pos.0, cursor_pos.1), - }); + }; } } } @@ -318,20 +317,21 @@ impl Editor { if self.pointer_down && prev_pos != self.cursor_pos && !self.editor.is_composing() { self.cursor_reset(); let cursor_pos = self.cursor_pos; - self.drive(|drv| drv.extend_selection_to_point(cursor_pos.0, cursor_pos.1)); + self.driver() + .extend_selection_to_point(cursor_pos.0, cursor_pos.1); } } WindowEvent::Ime(Ime::Disabled) => { - self.drive(|drv| drv.clear_compose()); + self.driver().clear_compose(); } WindowEvent::Ime(Ime::Commit(text)) => { - self.drive(|drv| drv.insert_or_replace_selection(&text)); + self.driver().insert_or_replace_selection(&text); } WindowEvent::Ime(Ime::Preedit(text, cursor)) => { if text.is_empty() { - self.drive(|drv| drv.clear_compose()); + self.driver().clear_compose(); } else { - self.drive(|drv| drv.set_compose(&text, cursor)); + self.driver().set_compose(&text, cursor); } } _ => {} @@ -341,9 +341,7 @@ impl Editor { pub fn handle_accesskit_action_request(&mut self, req: &accesskit::ActionRequest) { if req.action == accesskit::Action::SetTextSelection { if let Some(accesskit::ActionData::SetTextSelection(selection)) = &req.data { - self.drive(|drv| { - drv.select_from_accesskit(selection); - }); + self.driver().select_from_accesskit(selection); } } } @@ -475,10 +473,8 @@ impl Editor { } pub fn accessibility(&mut self, update: &mut TreeUpdate, node: &mut Node) { - self.editor - .drive(&mut self.font_cx, &mut self.layout_cx, |drv| { - drv.accessibility(update, node, next_node_id, INSET.into(), INSET.into()); - }); + let mut drv = self.editor.driver(&mut self.font_cx, &mut self.layout_cx); + drv.accessibility(update, node, next_node_id, INSET.into(), INSET.into()); } } diff --git a/parley/src/layout/editor.rs b/parley/src/layout/editor.rs index 38c478e4..bf445a97 100644 --- a/parley/src/layout/editor.rs +++ b/parley/src/layout/editor.rs @@ -12,7 +12,12 @@ use crate::{ FontContext, LayoutContext, Rect, StyleProperty, StyleSet, }; use alloc::{borrow::ToOwned, string::String, vec::Vec}; -use core::{cmp::PartialEq, default::Default, fmt::Debug, ops::Range}; +use core::{ + cmp::PartialEq, + default::Default, + fmt::{Debug, Display}, + ops::Range, +}; #[cfg(feature = "accesskit")] use crate::layout::LayoutAccessibility; @@ -36,6 +41,55 @@ impl Generation { } } +/// A string which is potentially discontiguous in memory. +/// +/// This is returned by [`PlainEditor::text`], as the IME preedit +/// area needs to be efficiently excluded from its return value. +#[derive(Debug, Clone, Copy)] +pub struct SplitString<'source>([&'source str; 2]); + +impl<'source> SplitString<'source> { + /// Get the characters of this string. + pub fn chars(self) -> impl Iterator + 'source { + self.into_iter().flat_map(str::chars) + } +} + +impl PartialEq<&'_ str> for SplitString<'_> { + fn eq(&self, other: &&'_ str) -> bool { + let [a, b] = self.0; + let mid = a.len(); + // When our MSRV is 1.80 or above, use split_at_checked instead. + // is_char_boundary checks bounds + let (a_1, b_1) = if other.is_char_boundary(mid) { + other.split_at(mid) + } else { + return false; + }; + + a_1 == a && b_1 == b + } +} +// We intentionally choose not to: +// impl PartialEq for SplitString<'_> {} +// for simplicity, as the impl wouldn't be useful and is non-trivial + +impl Display for SplitString<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let [a, b] = self.0; + write!(f, "{a}{b}") + } +} + +/// Iterate through the source strings. +impl<'source> IntoIterator for SplitString<'source> { + type Item = &'source str; + type IntoIter = <[&'source str; 2] as IntoIterator>::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + /// Basic plain text editor with a single style applied to the entire text. /// /// Internally, this is a wrapper around a string buffer and its corresponding [`Layout`], @@ -117,7 +171,7 @@ where // --- MARK: Forced relayout --- /// Insert at cursor, or replace selection. pub fn insert_or_replace_selection(&mut self, s: &str) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor .replace_selection(self.font_cx, self.layout_cx, s); @@ -125,14 +179,14 @@ where /// Delete the selection. pub fn delete_selection(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.insert_or_replace_selection(""); } /// Delete the selection or the next cluster (typical ‘delete’ behavior). pub fn delete(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); if self.editor.selection.is_collapsed() { // Upstream cluster range @@ -155,13 +209,13 @@ where /// Delete the selection or up to the next word boundary (typical ‘ctrl + delete’ behavior). pub fn delete_word(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); if self.editor.selection.is_collapsed() { let focus = self.editor.selection.focus(); let start = focus.index(); let end = focus.next_logical_word(&self.editor.layout).index(); - if self.editor.text().get(start..end).is_some() { + if self.editor.buffer.get(start..end).is_some() { self.editor.buffer.replace_range(start..end, ""); self.update_layout(); self.editor.set_selection( @@ -176,7 +230,7 @@ where /// Delete the selection or the previous cluster (typical ‘backspace’ behavior). pub fn backdelete(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); if self.editor.selection.is_collapsed() { // Upstream cluster @@ -218,13 +272,13 @@ where /// Delete the selection or back to the previous word boundary (typical ‘ctrl + backspace’ behavior). pub fn backdelete_word(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); if self.editor.selection.is_collapsed() { let focus = self.editor.selection.focus(); let end = focus.index(); let start = focus.previous_logical_word(&self.editor.layout).index(); - if self.editor.text().get(start..end).is_some() { + if self.editor.buffer.get(start..end).is_some() { self.editor.buffer.replace_range(start..end, ""); self.update_layout(); self.editor.set_selection( @@ -304,7 +358,7 @@ where // --- MARK: Cursor Movement --- /// Move the cursor to the cluster boundary nearest this point in the layout. pub fn move_to_point(&mut self, x: f32, y: f32) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.refresh_layout(); self.editor @@ -315,7 +369,7 @@ where /// /// No-op if index is not a char boundary. pub fn move_to_byte(&mut self, index: usize) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); if self.editor.buffer.is_char_boundary(index) { self.refresh_layout(); @@ -326,7 +380,7 @@ where /// Move the cursor to the start of the buffer. pub fn move_to_text_start(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, @@ -337,7 +391,7 @@ where /// Move the cursor to the start of the physical line. pub fn move_to_line_start(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor .set_selection(self.editor.selection.line_start(&self.editor.layout, false)); @@ -345,7 +399,7 @@ where /// Move the cursor to the end of the buffer. pub fn move_to_text_end(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, @@ -356,7 +410,7 @@ where /// Move the cursor to the end of the physical line. pub fn move_to_line_end(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor .set_selection(self.editor.selection.line_end(&self.editor.layout, false)); @@ -364,7 +418,7 @@ where /// Move up to the closest physical cluster boundary on the previous line, preserving the horizontal position for repeated movements. pub fn move_up(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -375,7 +429,7 @@ where /// Move down to the closest physical cluster boundary on the next line, preserving the horizontal position for repeated movements. pub fn move_down(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor .set_selection(self.editor.selection.next_line(&self.editor.layout, false)); @@ -383,7 +437,7 @@ where /// Move to the next cluster left in visual order. pub fn move_left(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -394,7 +448,7 @@ where /// Move to the next cluster right in visual order. pub fn move_right(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -405,7 +459,7 @@ where /// Move to the next word boundary left. pub fn move_word_left(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -416,7 +470,7 @@ where /// Move to the next word boundary right. pub fn move_word_right(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -427,7 +481,7 @@ where /// Select the whole buffer. pub fn select_all(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( Selection::from_byte_index(&self.editor.layout, 0_usize, Affinity::default()) @@ -437,14 +491,14 @@ where /// Collapse selection into caret. pub fn collapse_selection(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection(self.editor.selection.collapse()); } /// Move the selection focus point to the start of the buffer. pub fn select_to_text_start(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, @@ -455,7 +509,7 @@ where /// Move the selection focus point to the start of the physical line. pub fn select_to_line_start(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor .set_selection(self.editor.selection.line_start(&self.editor.layout, true)); @@ -463,7 +517,7 @@ where /// Move the selection focus point to the end of the buffer. pub fn select_to_text_end(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, @@ -474,7 +528,7 @@ where /// Move the selection focus point to the end of the physical line. pub fn select_to_line_end(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor .set_selection(self.editor.selection.line_end(&self.editor.layout, true)); @@ -482,7 +536,7 @@ where /// Move the selection focus point up to the nearest cluster boundary on the previous line, preserving the horizontal position for repeated movements. pub fn select_up(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -493,7 +547,7 @@ where /// Move the selection focus point down to the nearest cluster boundary on the next line, preserving the horizontal position for repeated movements. pub fn select_down(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor .set_selection(self.editor.selection.next_line(&self.editor.layout, true)); @@ -501,7 +555,7 @@ where /// Move the selection focus point to the next cluster left in visual order. pub fn select_left(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -512,7 +566,7 @@ where /// Move the selection focus point to the next cluster right in visual order. pub fn select_right(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor .set_selection(self.editor.selection.next_visual(&self.editor.layout, true)); @@ -520,7 +574,7 @@ where /// Move the selection focus point to the next word boundary left. pub fn select_word_left(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -531,7 +585,7 @@ where /// Move the selection focus point to the next word boundary right. pub fn select_word_right(&mut self) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.editor.set_selection( self.editor @@ -542,7 +596,7 @@ where /// Select the word at the point. pub fn select_word_at_point(&mut self, x: f32, y: f32) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.refresh_layout(); self.editor @@ -551,7 +605,7 @@ where /// Select the physical line at the point. pub fn select_line_at_point(&mut self, x: f32, y: f32) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.refresh_layout(); let line = Selection::line_from_point(&self.editor.layout, x, y); @@ -560,7 +614,7 @@ where /// Move the selection focus point to the cluster boundary closest to point. pub fn extend_selection_to_point(&mut self, x: f32, y: f32) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.refresh_layout(); // FIXME: This is usually the wrong way to handle selection extension for mouse moves, but not a regression. @@ -575,7 +629,7 @@ where /// /// No-op if index is not a char boundary. pub fn extend_selection_to_byte(&mut self, index: usize) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); if self.editor.buffer.is_char_boundary(index) { self.refresh_layout(); @@ -588,7 +642,7 @@ where /// /// No-op if either index is not a char boundary. pub fn select_byte_range(&mut self, start: usize, end: usize) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); if self.editor.buffer.is_char_boundary(start) && self.editor.buffer.is_char_boundary(end) { self.refresh_layout(); @@ -602,7 +656,7 @@ where #[cfg(feature = "accesskit")] /// Select inside the editor based on the selection provided by accesskit. pub fn select_from_accesskit(&mut self, selection: &accesskit::TextSelection) { - debug_assert!(!self.editor.is_composing()); + assert!(!self.editor.is_composing()); self.refresh_layout(); if let Some(selection) = Selection::from_access_selection( @@ -614,6 +668,7 @@ where } } + /// --- MARK: Rendering --- #[cfg(feature = "accesskit")] /// Perform an accessibility update. pub fn accessibility( @@ -630,6 +685,10 @@ where Some(()) } + /// Get the up-to-date layout for this driver. + pub fn layout(&mut self) -> &Layout { + self.editor.layout(self.font_cx, self.layout_cx) + } // --- MARK: Internal helpers--- /// Update the layout if needed. fn refresh_layout(&mut self) { @@ -646,21 +705,6 @@ impl PlainEditor where T: Brush + Clone + Debug + PartialEq + Default, { - // --- MARK: PlainEditor API --- - /// Run a series of [`PlainEditorDriver`] methods. - /// - /// This is a utility shorthand around [`driver`](Self::driver); it - /// does not have any different behaviour. - pub fn drive( - &mut self, - font_cx: &mut FontContext, - layout_cx: &mut LayoutContext, - callback: impl FnOnce(&mut PlainEditorDriver<'_, T>) -> R, - ) -> R { - let mut drv = self.driver(font_cx, layout_cx); - callback(&mut drv) - } - /// Run a series of [`PlainEditorDriver`] methods. /// /// This type is only used to simplify methods which require both @@ -702,13 +746,13 @@ where /// Borrow the text content of the buffer. /// - /// The return values concatenated is the full text content. - /// This split is used when composing. - pub fn text(&self) -> [&str; 2] { + /// The return value is a `SplitString` because it + /// excludes the IME preedit region. + pub fn text(&self) -> SplitString<'_> { if let Some(compose) = &self.compose { - [&self.buffer[..compose.start], &self.buffer[compose.end..]] + SplitString([&self.buffer[..compose.start], &self.buffer[compose.end..]]) } else { - [&self.buffer, ""] + SplitString([&self.buffer, ""]) } } @@ -722,7 +766,7 @@ where /// Replace the whole text buffer. pub fn set_text(&mut self, is: &str) { - debug_assert!(!self.is_composing()); + assert!(!self.is_composing()); self.buffer.clear(); self.buffer.push_str(is);