From fadb70ac0de789ff82b2012da87bc929992e6347 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:30:51 +0000 Subject: [PATCH 1/5] Minimise the need to batch operations in `PlainEditor` Co-Authored-By: Tom Churchman --- examples/vello_editor/src/main.rs | 21 +- examples/vello_editor/src/text.rs | 41 ++- parley/Cargo.toml | 4 +- parley/src/layout/editor.rs | 412 ++++++++++++++++++++++-------- parley/src/style/mod.rs | 2 + parley/src/style/styleset.rs | 74 ++++++ 6 files changed, 422 insertions(+), 132 deletions(-) create mode 100644 parley/src/style/styleset.rs diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index 0422e769..feadc1db 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -28,7 +28,6 @@ mod access_ids; use access_ids::{TEXT_INPUT_ID, WINDOW_ID}; mod text; -use parley::{GenericFamily, StyleProperty}; const WINDOW_TITLE: &str = "Vello Text Editor"; @@ -117,12 +116,6 @@ impl ApplicationHandler for SimpleVelloApp<'_> { let size = window.inner_size(); - self.editor.transact(|txn| { - txn.set_scale(1.0); - txn.set_width(Some(size.width as f32 - 2f32 * text::INSET)); - txn.set_text(text::LOREM); - }); - // Create a vello Surface let surface_future = { let surface = self @@ -236,15 +229,9 @@ impl ApplicationHandler for SimpleVelloApp<'_> { WindowEvent::Resized(size) => { self.context .resize_surface(&mut render_state.surface, size.width, size.height); - self.editor.transact(|txn| { - txn.set_scale(1.0); - txn.set_width(Some(size.width as f32 - 2f32 * text::INSET)); - txn.set_default_style(Arc::new([ - StyleProperty::FontSize(32.0), - StyleProperty::LineHeight(1.2), - GenericFamily::SystemUi.into(), - ])); - }); + let editor = self.editor.editor(); + editor.set_scale(1.0); + editor.set_width(Some(size.width as f32 - 2f32 * text::INSET)); render_state.window.request_redraw(); } @@ -352,7 +339,7 @@ fn main() -> Result<()> { renderers: vec![], state: RenderState::Suspended(None), scene: Scene::new(), - editor: text::Editor::default(), + editor: text::Editor::new(text::LOREM), last_drawn_generation: Default::default(), event_loop_proxy: event_loop.create_proxy(), }; diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 2211c7e9..405009d1 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; +use parley::{layout::PositionedLayoutItem, GenericFamily, StyleProperty}; use peniko::{kurbo::Affine, Color, Fill}; use std::time::{Duration, Instant}; use vello::Scene; @@ -19,7 +19,6 @@ use crate::access_ids::next_node_id; pub const INSET: f32 = 32.0; -#[derive(Default)] pub struct Editor { font_cx: FontContext, layout_cx: LayoutContext, @@ -35,11 +34,37 @@ pub struct Editor { } impl Editor { + pub fn new(text: &str) -> Self { + let mut editor = PlainEditor::new(32.0); + editor.set_text(text); + editor.set_scale(1.0); + let styles = editor.edit_styles(); + styles.insert(StyleProperty::LineHeight(1.2)); + styles.insert(GenericFamily::SystemUi.into()); + Self { + font_cx: Default::default(), + layout_cx: Default::default(), + editor, + last_click_time: Default::default(), + click_count: Default::default(), + pointer_down: Default::default(), + cursor_pos: Default::default(), + cursor_visible: Default::default(), + modifiers: Default::default(), + start_time: Default::default(), + blink_period: Default::default(), + } + } + pub fn transact(&mut self, callback: impl FnOnce(&mut PlainEditorTxn<'_, Color>)) { self.editor .transact(&mut self.font_cx, &mut self.layout_cx, callback); } + pub fn editor(&mut self) -> &mut PlainEditor { + &mut self.editor + } + pub fn text(&self) -> &str { self.editor.text() } @@ -77,7 +102,8 @@ impl Editor { pub fn handle_event(&mut self, event: WindowEvent) { match event { WindowEvent::Resized(size) => { - self.transact(|txn| txn.set_width(Some(size.width as f32 - 2f32 * INSET))); + self.editor + .set_width(Some(size.width as f32 - 2f32 * INSET)); } WindowEvent::ModifiersChanged(modifiers) => { self.modifiers = Some(modifiers); @@ -313,7 +339,7 @@ impl Editor { /// Draw into scene. /// /// Returns drawn `Generation`. - pub fn draw(&self, scene: &mut Scene) -> Generation { + pub fn draw(&mut self, scene: &mut Scene) -> Generation { let transform = Affine::translate((INSET as f64, INSET as f64)); for rect in self.editor.selection_geometry().iter() { scene.fill(Fill::NonZero, transform, Color::STEEL_BLUE, None, &rect); @@ -323,7 +349,8 @@ impl Editor { scene.fill(Fill::NonZero, transform, Color::WHITE, None, &cursor); }; } - for line in self.editor.lines() { + let layout = self.editor.layout(&mut self.font_cx, &mut self.layout_cx); + for line in layout.lines() { for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue; @@ -370,7 +397,9 @@ impl Editor { pub fn accessibility(&mut self, update: &mut TreeUpdate, node: &mut Node) { self.editor - .accessibility(update, node, next_node_id, INSET.into(), INSET.into()); + .transact(&mut self.font_cx, &mut self.layout_cx, |txn| { + txn.accessibility(update, node, next_node_id, INSET.into(), INSET.into()); + }); } } diff --git a/parley/Cargo.toml b/parley/Cargo.toml index a6bc492c..7d89a993 100644 --- a/parley/Cargo.toml +++ b/parley/Cargo.toml @@ -22,7 +22,7 @@ std = ["fontique/std", "skrifa/std", "peniko/std"] libm = ["fontique/libm", "skrifa/libm", "peniko/libm", "dep:core_maths"] # Enables support for system font backends system = ["std", "fontique/system"] -accesskit = ["dep:accesskit", "dep:hashbrown"] +accesskit = ["dep:accesskit"] [dependencies] swash = { workspace = true } @@ -31,7 +31,7 @@ peniko = { workspace = true } fontique = { workspace = true } core_maths = { version = "0.1.0", optional = true } accesskit = { workspace = true, optional = true } -hashbrown = { workspace = true, optional = true } +hashbrown = { workspace = true } [dev-dependencies] tiny-skia = "0.11.4" diff --git a/parley/src/layout/editor.rs b/parley/src/layout/editor.rs index 05eb0138..2eaea045 100644 --- a/parley/src/layout/editor.rs +++ b/parley/src/layout/editor.rs @@ -1,21 +1,23 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use core::{cmp::PartialEq, default::Default, fmt::Debug}; +//! Import of Parley's `PlainEditor` as the version in Parley is insufficient for our needs. -#[cfg(feature = "accesskit")] -use crate::layout::LayoutAccessibility; use crate::{ layout::{ cursor::{Cursor, Selection}, - Affinity, Alignment, Layout, Line, + Affinity, Alignment, Layout, }, - style::{Brush, StyleProperty}, - FontContext, LayoutContext, Rect, + style::Brush, + FontContext, LayoutContext, Rect, StyleProperty, StyleSet, }; +use alloc::{borrow::ToOwned, string::String, vec::Vec}; +use core::{cmp::PartialEq, default::Default, fmt::Debug, ops::Range}; + +#[cfg(feature = "accesskit")] +use crate::layout::LayoutAccessibility; #[cfg(feature = "accesskit")] use accesskit::{Node, NodeId, TreeUpdate}; -use alloc::{borrow::ToOwned, string::String, sync::Arc, vec::Vec}; /// Opaque representation of a generation. /// @@ -40,12 +42,15 @@ pub struct PlainEditor where T: Brush + Clone + Debug + PartialEq + Default, { - default_style: Arc<[StyleProperty<'static, T>]>, + default_style: StyleSet, buffer: String, layout: Layout, #[cfg(feature = "accesskit")] layout_access: LayoutAccessibility, selection: Selection, + /// Byte offsets of IME composing preedit text in the text buffer. + /// `None` if the IME is not currently composing. + compose: Option>, width: Option, scale: f32, // Simple tracking of when the layout needs to be updated @@ -54,25 +59,31 @@ where // Not all operations on `PlainEditor` need to operate on a // clean layout, and not all operations trigger a layout. layout_dirty: bool, + // TODO: We could avoid redoing the full text layout if linebreaking or + // alignment were unchanged + // linebreak_dirty: bool, + // alignment_dirty: bool, + alignment: Alignment, generation: Generation, } -// TODO: When MSRV >= 1.80 we can remove this. Default was not implemented for Arc<[T]> where T: !Default until 1.80 -impl Default for PlainEditor +impl PlainEditor where - T: Brush + Clone + Debug + PartialEq + Default, + T: Brush, { - fn default() -> Self { + pub fn new(font_size: f32) -> Self { Self { - default_style: Arc::new([]), + default_style: StyleSet::new(font_size), buffer: Default::default(), layout: Default::default(), #[cfg(feature = "accesskit")] layout_access: Default::default(), selection: Default::default(), - width: Default::default(), + compose: None, + width: None, scale: 1.0, - layout_dirty: Default::default(), + layout_dirty: true, + alignment: Alignment::Start, // We don't use the `default` value to start with, as our consumers // will choose to use that as their initial value, but will probably need // to redraw if they haven't already. @@ -87,53 +98,35 @@ pub struct PlainEditorTxn<'a, T> where T: Brush + Clone + Debug + PartialEq + Default, { - editor: &'a mut PlainEditor, - font_cx: &'a mut FontContext, - layout_cx: &'a mut LayoutContext, + pub editor: &'a mut PlainEditor, + pub font_cx: &'a mut FontContext, + pub layout_cx: &'a mut LayoutContext, } impl PlainEditorTxn<'_, T> where T: Brush + Clone + Debug + PartialEq + Default, { - /// Replace the whole text buffer. - pub fn set_text(&mut self, is: &str) { - self.editor.buffer.clear(); - self.editor.buffer.push_str(is); - self.editor.layout_dirty = true; - } - - /// Set the width of the layout. - pub fn set_width(&mut self, width: Option) { - self.editor.width = width; - self.editor.layout_dirty = true; - } - - /// Set the scale for the layout. - pub fn set_scale(&mut self, scale: f32) { - self.editor.scale = scale; - self.editor.layout_dirty = true; - } - - /// Set the default style for the layout. - pub fn set_default_style(&mut self, style: Arc<[StyleProperty<'static, T>]>) { - self.editor.default_style = style; - self.editor.layout_dirty = true; - } - + // --- 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()); + self.editor .replace_selection(self.font_cx, self.layout_cx, s); } /// Delete the selection. pub fn delete_selection(&mut self) { + debug_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()); + if self.editor.selection.is_collapsed() { // Upstream cluster range if let Some(range) = self @@ -155,6 +148,8 @@ 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()); + if self.editor.selection.is_collapsed() { let focus = self.editor.selection.focus(); let start = focus.index(); @@ -174,6 +169,8 @@ where /// Delete the selection or the previous cluster (typical ‘backspace’ behavior). pub fn backdelete(&mut self) { + debug_assert!(!self.editor.is_composing()); + if self.editor.selection.is_collapsed() { // Upstream cluster if let Some(cluster) = self @@ -214,6 +211,8 @@ 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()); + if self.editor.selection.is_collapsed() { let focus = self.editor.selection.focus(); let end = focus.index(); @@ -231,8 +230,75 @@ where } } + // --- MARK: IME --- + /// Set the IME preedit composing text. + /// + /// This starts composing. Composing is reset by calling [`PlainEditorTxn::clear_compose`]. + /// While composing, it is a logic error to call anything other than + /// [`PlainEditorTxn::set_compose`] or [`PlainEditorTxn::clear_compose`]. + /// + /// The preedit text replaces the current selection if this call starts composing. + /// + /// The selection is updated based on `cursor`, which contains the byte offsets relative to the + /// start of the preedit text. If `cursor` is `None`, the selection is collapsed to a caret in + /// front of the preedit text. + pub fn set_compose(&mut self, text: &str, cursor: Option<(usize, usize)>) { + debug_assert!(!text.is_empty()); + debug_assert!(cursor.map(|cursor| cursor.1 <= text.len()).unwrap_or(true)); + + let start = if let Some(preedit_range) = self.editor.compose.clone() { + self.editor + .buffer + .replace_range(preedit_range.clone(), text); + preedit_range.start + } else { + if self.editor.selection.is_collapsed() { + self.editor + .buffer + .insert_str(self.editor.selection.text_range().start, text); + } else { + self.editor + .buffer + .replace_range(self.editor.selection.text_range(), text); + } + self.editor.selection.text_range().start + }; + self.editor.compose = Some(start..start + text.len()); + self.update_layout(); + + if let Some(cursor) = cursor { + // Select the location indicated by the IME. + self.editor.set_selection(Selection::new( + self.editor.cursor_at(start + cursor.0), + self.editor.cursor_at(start + cursor.1), + )); + } else { + // IME indicates nothing is to be selected: collapse the selection to a + // caret just in front of the preedit. + self.editor + .set_selection(self.editor.cursor_at(start).into()); + } + } + + /// Stop IME composing. + /// + /// This removes the IME preedit text. + pub fn clear_compose(&mut self) { + if let Some(preedit_range) = self.editor.compose.clone() { + self.editor.buffer.replace_range(preedit_range.clone(), ""); + self.editor.compose = None; + self.update_layout(); + + self.editor + .set_selection(self.editor.cursor_at(preedit_range.start).into()); + } + } + + // --- 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()); + self.refresh_layout(); self.editor .set_selection(Selection::from_point(&self.editor.layout, x, y)); @@ -242,6 +308,8 @@ 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()); + if self.editor.buffer.is_char_boundary(index) { self.refresh_layout(); self.editor @@ -251,6 +319,8 @@ where /// Move the cursor to the start of the buffer. pub fn move_to_text_start(&mut self) { + debug_assert!(!self.editor.is_composing()); + self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, isize::MIN, @@ -260,12 +330,16 @@ 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()); + self.editor .set_selection(self.editor.selection.line_start(&self.editor.layout, false)); } /// Move the cursor to the end of the buffer. pub fn move_to_text_end(&mut self) { + debug_assert!(!self.editor.is_composing()); + self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, isize::MAX, @@ -275,12 +349,16 @@ 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()); + self.editor .set_selection(self.editor.selection.line_end(&self.editor.layout, false)); } /// 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()); + self.editor.set_selection( self.editor .selection @@ -290,12 +368,16 @@ 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()); + self.editor .set_selection(self.editor.selection.next_line(&self.editor.layout, false)); } /// Move to the next cluster left in visual order. pub fn move_left(&mut self) { + debug_assert!(!self.editor.is_composing()); + self.editor.set_selection( self.editor .selection @@ -305,6 +387,8 @@ where /// Move to the next cluster right in visual order. pub fn move_right(&mut self) { + debug_assert!(!self.editor.is_composing()); + self.editor.set_selection( self.editor .selection @@ -314,6 +398,8 @@ where /// Move to the next word boundary left. pub fn move_word_left(&mut self) { + debug_assert!(!self.editor.is_composing()); + self.editor.set_selection( self.editor .selection @@ -323,6 +409,8 @@ where /// Move to the next word boundary right. pub fn move_word_right(&mut self) { + debug_assert!(!self.editor.is_composing()); + self.editor.set_selection( self.editor .selection @@ -332,19 +420,25 @@ where /// Select the whole buffer. pub fn select_all(&mut self) { + debug_assert!(!self.editor.is_composing()); + self.editor.set_selection( - Selection::from_byte_index(&self.editor.layout, 0usize, Affinity::default()) + Selection::from_byte_index(&self.editor.layout, 0_usize, Affinity::default()) .move_lines(&self.editor.layout, isize::MAX, true), ); } /// Collapse selection into caret. pub fn collapse_selection(&mut self) { + debug_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()); + self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, isize::MIN, @@ -354,12 +448,16 @@ 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()); + self.editor .set_selection(self.editor.selection.line_start(&self.editor.layout, true)); } /// 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()); + self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, isize::MAX, @@ -369,12 +467,16 @@ 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()); + self.editor .set_selection(self.editor.selection.line_end(&self.editor.layout, true)); } /// 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()); + self.editor.set_selection( self.editor .selection @@ -384,12 +486,16 @@ 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()); + self.editor .set_selection(self.editor.selection.next_line(&self.editor.layout, true)); } /// 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()); + self.editor.set_selection( self.editor .selection @@ -399,12 +505,16 @@ 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()); + self.editor .set_selection(self.editor.selection.next_visual(&self.editor.layout, true)); } /// Move the selection focus point to the next word boundary left. pub fn select_word_left(&mut self) { + debug_assert!(!self.editor.is_composing()); + self.editor.set_selection( self.editor .selection @@ -414,6 +524,8 @@ 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()); + self.editor.set_selection( self.editor .selection @@ -423,6 +535,8 @@ 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()); + self.refresh_layout(); self.editor .set_selection(Selection::word_from_point(&self.editor.layout, x, y)); @@ -430,6 +544,8 @@ 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()); + self.refresh_layout(); let line = Selection::line_from_point(&self.editor.layout, x, y); self.editor.set_selection(line); @@ -437,6 +553,8 @@ 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()); + self.refresh_layout(); // FIXME: This is usually the wrong way to handle selection extension for mouse moves, but not a regression. self.editor.set_selection( @@ -450,13 +568,12 @@ 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()); + if self.editor.buffer.is_char_boundary(index) { self.refresh_layout(); - self.editor.set_selection( - self.editor - .selection - .maybe_extend(self.editor.cursor_at(index), true), - ); + self.editor + .set_selection(self.editor.selection.extend(self.editor.cursor_at(index))); } } @@ -464,17 +581,21 @@ 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()); + if self.editor.buffer.is_char_boundary(start) && self.editor.buffer.is_char_boundary(end) { self.refresh_layout(); - self.editor.set_selection( - Selection::from(self.editor.cursor_at(start)) - .maybe_extend(self.editor.cursor_at(end), true), - ); + self.editor.set_selection(Selection::new( + self.editor.cursor_at(start), + self.editor.cursor_at(end), + )); } } #[cfg(feature = "accesskit")] pub fn select_from_accesskit(&mut self, selection: &accesskit::TextSelection) { + debug_assert!(!self.editor.is_composing()); + self.refresh_layout(); if let Some(selection) = Selection::from_access_selection( selection, @@ -485,6 +606,22 @@ where } } + #[cfg(feature = "accesskit")] + pub fn accessibility( + &mut self, + update: &mut TreeUpdate, + node: &mut Node, + next_node_id: impl FnMut() -> NodeId, + x_offset: f64, + y_offset: f64, + ) -> Option<()> { + self.refresh_layout(); + self.editor + .accessibility_raw(update, node, next_node_id, x_offset, y_offset); + Some(()) + } + + // --- MARK: Internal helpers --- fn update_layout(&mut self) { self.editor.update_layout(self.font_cx, self.layout_cx); } @@ -498,25 +635,39 @@ impl PlainEditor where T: Brush + Clone + Debug + PartialEq + Default, { - /// Run a series of [`PlainEditorTxn`] methods, updating the layout - /// if necessary. - pub fn transact( + /// Run a series of [`PlainEditorTxn`] methods. + /// + /// This is a utility shorthand around [`transaction`](Self::transaction); + pub fn transact( &mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext, - callback: impl FnOnce(&mut PlainEditorTxn<'_, T>), - ) { - let mut txn = PlainEditorTxn { + callback: impl FnOnce(&mut PlainEditorTxn<'_, T>) -> R, + ) -> R { + let mut txn = self.transaction(font_cx, layout_cx); + callback(&mut txn) + } + + /// Run a series of [`PlainEditorTxn`] methods, updating the layout + /// if necessary. + /// + /// This is a utility shorthand to simplify methods which require the editor + /// and the provided contexts. + pub fn transaction<'txn>( + &'txn mut self, + font_cx: &'txn mut FontContext, + layout_cx: &'txn mut LayoutContext, + ) -> PlainEditorTxn<'txn, T> { + PlainEditorTxn { editor: self, font_cx, layout_cx, - }; - callback(&mut txn); - txn.refresh_layout(); + } } /// Make a cursor at a given byte index fn cursor_at(&self, index: usize) -> Cursor { + // TODO: Do we need to be non-dirty? // FIXME: `Selection` should make this easier if index >= self.buffer.len() { Cursor::from_byte_index(&self.layout, self.buffer.len(), Affinity::Upstream) @@ -555,37 +706,7 @@ where { self.generation.nudge(); } - // Keeping this commented debug code in for now because it's quite - // useful when diagnosing selection problems: - //---------------------------------------------------------------------- - // #[cfg(feature = "std")] - // { - // let focus = new_sel.focus(); - // let cluster = focus.logical_clusters(&self.layout); - // let dbg = ( - // cluster[0].as_ref().map(|c| &self.buffer[c.text_range()]), - // focus.index(), - // focus.affinity(), - // cluster[1].as_ref().map(|c| &self.buffer[c.text_range()]), - // ); - // print!("{dbg:?}"); - // let cluster = focus.visual_clusters(&self.layout); - // let dbg = ( - // cluster[0].as_ref().map(|c| &self.buffer[c.text_range()]), - // cluster[0] - // .as_ref() - // .map(|c| if c.is_word_boundary() { " W" } else { "" }) - // .unwrap_or_default(), - // focus.index(), - // focus.affinity(), - // cluster[1].as_ref().map(|c| &self.buffer[c.text_range()]), - // cluster[1] - // .as_ref() - // .map(|c| if c.is_word_boundary() { " W" } else { "" }) - // .unwrap_or_default(), - // ); - // println!(" | visual: {dbg:?}"); - // } + self.selection = new_sel; } @@ -609,16 +730,6 @@ where Some(self.selection.focus().geometry(&self.layout, size)) } - /// Returns the underlying `Layout`. - pub fn layout(&self) -> &Layout { - &self.layout - } - - /// Get the lines from the `Layout`. - pub fn lines(&self) -> impl Iterator> + '_ + Clone { - self.layout.lines() - } - /// Borrow the text content of the buffer. pub fn text(&self) -> &str { &self.buffer @@ -632,6 +743,64 @@ where self.generation } + /// Get the full read-only details from the layout + pub fn layout( + &mut self, + font_cx: &mut FontContext, + layout_cx: &mut LayoutContext, + ) -> &Layout { + self.refresh_layout(font_cx, layout_cx); + &self.layout + } + + /// Get the full read-only details from the layout, if valid. + pub fn get_layout(&self) -> Option<&Layout> { + if self.layout_dirty { + None + } else { + Some(&self.layout) + } + } + + /// Get the (potentially invalid) details from the layout. + pub fn layout_raw(&self) -> &Layout { + &self.layout + } + + /// Replace the whole text buffer. + pub fn set_text(&mut self, is: &str) { + self.buffer.clear(); + self.buffer.push_str(is); + self.layout_dirty = true; + } + + /// Set the width of the layout. + // TODO: If this is infinite, is the width used for alignnment the min width? + pub fn set_width(&mut self, width: Option) { + // Don't allow empty widths: + // https://github.com/linebender/parley/issues/186 + self.width = width.map(|width| if width > 10. { width } else { 10. }); + self.layout_dirty = true; + } + + /// Set the alignment of the layout. + pub fn set_alignment(&mut self, alignment: Alignment) { + self.alignment = alignment; + self.layout_dirty = true; + } + + /// Set the scale for the layout. + pub fn set_scale(&mut self, scale: f32) { + self.scale = scale; + self.layout_dirty = true; + } + + /// Set the default style for the layout. + pub fn edit_styles(&mut self) -> &mut StyleSet { + self.layout_dirty = true; + &mut self.default_style + } + /// Update the layout if it is dirty. fn refresh_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext) { if self.layout_dirty { @@ -642,19 +811,48 @@ where /// Update the layout. fn update_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext) { let mut builder = layout_cx.ranged_builder(font_cx, &self.buffer, self.scale); - for prop in self.default_style.iter() { + for prop in self.default_style.inner().values() { builder.push_default(prop.to_owned()); } - builder.build_into(&mut self.layout, &self.buffer); + if let Some(ref preedit_range) = self.compose { + builder.push(StyleProperty::Underline(true), preedit_range.clone()); + } + self.layout = builder.build(&self.buffer); self.layout.break_all_lines(self.width); - self.layout.align(self.width, Alignment::Start); + self.layout.align(self.width, self.alignment); self.selection = self.selection.refresh(&self.layout); self.layout_dirty = false; self.generation.nudge(); } + /// Whether the editor is currently in IME composing mode. + pub fn is_composing(&self) -> bool { + self.compose.is_some() + } + #[cfg(feature = "accesskit")] - pub fn accessibility( + #[inline] + /// Perform an accessibility update if the layout is valid. + /// + /// Returns `None` if the layout is not up-to-date. + pub fn try_accessibility( + &mut self, + update: &mut TreeUpdate, + node: &mut Node, + next_node_id: impl FnMut() -> NodeId, + x_offset: f64, + y_offset: f64, + ) -> Option<()> { + if self.layout_dirty { + return None; + } + self.accessibility_raw(update, node, next_node_id, x_offset, y_offset); + Some(()) + } + + #[cfg(feature = "accesskit")] + /// Perform an accessibility update, even if the layout isn't valid. + pub fn accessibility_raw( &mut self, update: &mut TreeUpdate, node: &mut Node, diff --git a/parley/src/style/mod.rs b/parley/src/style/mod.rs index c999f3c7..c99382aa 100644 --- a/parley/src/style/mod.rs +++ b/parley/src/style/mod.rs @@ -5,6 +5,7 @@ mod brush; mod font; +mod styleset; use alloc::borrow::Cow; @@ -13,6 +14,7 @@ pub use font::{ FontFamily, FontFeature, FontSettings, FontStack, FontStretch, FontStyle, FontVariation, FontWeight, GenericFamily, }; +pub use styleset::StyleSet; #[derive(Debug, Clone, Copy)] pub enum WhiteSpaceCollapse { diff --git a/parley/src/style/styleset.rs b/parley/src/style/styleset.rs new file mode 100644 index 00000000..eead2f42 --- /dev/null +++ b/parley/src/style/styleset.rs @@ -0,0 +1,74 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use core::mem::Discriminant; +use hashbrown::HashMap; + +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 +/// mutable API for styles applied to all text managed by it. +/// These styles do not have a corresponding range, and are generally unsuited for rich text. +#[derive(Clone, Debug)] +pub struct StyleSet( + HashMap>, StyleProperty>, +); + +impl StyleSet { + /// Create a new collection of styles. + /// + /// The font size will be `font_size`, and can be overwritten at runtime by + /// [inserting](Self::insert) a new [`FontSize`](crate::StyleProperty::FontSize). + pub fn new(font_size: f32) -> Self { + let mut this = Self(Default::default()); + this.insert(StyleProperty::FontSize(font_size)); + this + } + + /// Add `style` to this collection, returning any overwritten value. + /// + /// Note: Adding a [font stack](crate::StyleProperty::FontStack) to this collection is not + /// additive, and instead overwrites any previously added font stack. + pub fn insert(&mut self, style: StyleProperty) -> Option> { + let discriminant = core::mem::discriminant(&style); + self.0.insert(discriminant, style) + } + + /// [Retain](std::vec::Vec::retain) only the styles for which `f` returns true. + /// + /// Styles which are removed return to their default values. + /// + /// Removing the [font size](crate::StyleProperty::FontSize) is not recommended, as an unspecified + /// fallback font size will be used. + pub fn retain(&mut self, mut f: impl FnMut(&StyleProperty) -> bool) { + self.0.retain(|_, v| f(v)); + } + + /// Remove the style with the discriminant `property`. + /// + /// Styles which are removed return to their default values. + /// + /// To get the discriminant requires constructing a valid `StyleProperty` for the + /// the desired property and passing it to [`core::mem::discriminant`]. + /// Getting this discriminant is usually possible in a `const` context. + /// + /// Removing the [font size](crate::StyleProperty::FontSize) is not recommended, as an unspecified + /// fallback font size will be used. + pub fn remove( + &mut self, + property: Discriminant>, + ) -> Option> { + self.0.remove(&property) + } + + /// Read the raw underlying storage of this. + /// + /// Write access is not provided due to the invariant that keys + /// are the discriminant of their corresponding value. + pub fn inner(&self) -> &HashMap>, StyleProperty> { + &self.0 + } +} From 5d748f6f7376ce49da30253e88c68f2d911e18b1 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:46:17 +0000 Subject: [PATCH 2/5] Rename to `PlainEditorDriver` and fixup docs Add to the changelog Add missing composing `assert` --- CHANGELOG.md | 7 + examples/vello_editor/src/text.rs | 114 ++++++------ parley/src/layout/editor.rs | 290 +++++++++++++++++------------- parley/src/lib.rs | 2 +- parley/src/style/styleset.rs | 2 + 5 files changed, 229 insertions(+), 186 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 659ccdcd..a1ef5ffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,11 @@ This release has an [MSRV] of 1.75. - `Generation` on `PlainEditor` to help implement lazy drawing. ([#143] by [@xorgy]) +### Changed + +### Parley + +- Breaking change: `PlainEditor`'s semantics are no longer transactional ([#192][] by [@DJMcNab][]) ## [0.2.0] - 2024-10-10 @@ -76,6 +81,7 @@ This release has an [MSRV] of 1.70. [MSRV]: README.md#minimum-supported-rust-version-msrv [@dfrg]: https://github.com/dfrg +[@DJMcNab]: https://github.com/DJMcNab [@nicoburns]: https://github.com/nicoburns [@waywardmonkeys]: https://github.com/waywardmonkeys [@xorgy]: https://github.com/xorgy @@ -93,6 +99,7 @@ This release has an [MSRV] of 1.70. [#126]: https://github.com/linebender/parley/pull/126 [#129]: https://github.com/linebender/parley/pull/129 [#143]: https://github.com/linebender/parley/pull/143 +[#192]: https://github.com/linebender/parley/pull/192 [Unreleased]: https://github.com/linebender/parley/compare/v0.2.0...HEAD [0.2.0]: https://github.com/linebender/parley/releases/tag/v0.2.0 diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 405009d1..e4ae819b 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -13,7 +13,7 @@ use winit::{ }; pub use parley::layout::editor::Generation; -use parley::{FontContext, LayoutContext, PlainEditor, PlainEditorTxn}; +use parley::{FontContext, LayoutContext, PlainEditor, PlainEditorDriver}; use crate::access_ids::next_node_id; @@ -56,9 +56,9 @@ impl Editor { } } - pub fn transact(&mut self, callback: impl FnOnce(&mut PlainEditorTxn<'_, Color>)) { + pub fn drive(&mut self, callback: impl FnOnce(&mut PlainEditorDriver<'_, Color>)) { self.editor - .transact(&mut self.font_cx, &mut self.layout_cx, callback); + .drive(&mut self.font_cx, &mut self.layout_cx, callback); } pub fn editor(&mut self) -> &mut PlainEditor { @@ -143,114 +143,114 @@ impl Editor { 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()); + self.drive(|drv| drv.delete_selection()); } } "v" => { let cb = ClipboardContext::new().unwrap(); let text = cb.get_text().unwrap_or_default(); - self.transact(|txn| txn.insert_or_replace_selection(&text)); + self.drive(|drv| drv.insert_or_replace_selection(&text)); } _ => (), } } Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => { - self.transact(|txn| { + self.drive(|drv| { if shift { - txn.collapse_selection(); + drv.collapse_selection(); } else { - txn.select_all(); + drv.select_all(); } }); } - Key::Named(NamedKey::ArrowLeft) => self.transact(|txn| { + Key::Named(NamedKey::ArrowLeft) => self.drive(|drv| { if action_mod { if shift { - txn.select_word_left(); + drv.select_word_left(); } else { - txn.move_word_left(); + drv.move_word_left(); } } else if shift { - txn.select_left(); + drv.select_left(); } else { - txn.move_left(); + drv.move_left(); } }), - Key::Named(NamedKey::ArrowRight) => self.transact(|txn| { + Key::Named(NamedKey::ArrowRight) => self.drive(|drv| { if action_mod { if shift { - txn.select_word_right(); + drv.select_word_right(); } else { - txn.move_word_right(); + drv.move_word_right(); } } else if shift { - txn.select_right(); + drv.select_right(); } else { - txn.move_right(); + drv.move_right(); } }), - Key::Named(NamedKey::ArrowUp) => self.transact(|txn| { + Key::Named(NamedKey::ArrowUp) => self.drive(|drv| { if shift { - txn.select_up(); + drv.select_up(); } else { - txn.move_up(); + drv.move_up(); } }), - Key::Named(NamedKey::ArrowDown) => self.transact(|txn| { + Key::Named(NamedKey::ArrowDown) => self.drive(|drv| { if shift { - txn.select_down(); + drv.select_down(); } else { - txn.move_down(); + drv.move_down(); } }), - Key::Named(NamedKey::Home) => self.transact(|txn| { + Key::Named(NamedKey::Home) => self.drive(|drv| { if action_mod { if shift { - txn.select_to_text_start(); + drv.select_to_text_start(); } else { - txn.move_to_text_start(); + drv.move_to_text_start(); } } else if shift { - txn.select_to_line_start(); + drv.select_to_line_start(); } else { - txn.move_to_line_start(); + drv.move_to_line_start(); } }), - Key::Named(NamedKey::End) => self.transact(|txn| { + Key::Named(NamedKey::End) => self.drive(|drv| { if action_mod { if shift { - txn.select_to_text_end(); + drv.select_to_text_end(); } else { - txn.move_to_text_end(); + drv.move_to_text_end(); } } else if shift { - txn.select_to_line_end(); + drv.select_to_line_end(); } else { - txn.move_to_line_end(); + drv.move_to_line_end(); } }), - Key::Named(NamedKey::Delete) => self.transact(|txn| { + Key::Named(NamedKey::Delete) => self.drive(|drv| { if action_mod { - txn.delete_word(); + drv.delete_word(); } else { - txn.delete(); + drv.delete(); } }), - Key::Named(NamedKey::Backspace) => self.transact(|txn| { + Key::Named(NamedKey::Backspace) => self.drive(|drv| { if action_mod { - txn.backdelete_word(); + drv.backdelete_word(); } else { - txn.backdelete(); + drv.backdelete(); } }), Key::Named(NamedKey::Enter) => { - self.transact(|txn| txn.insert_or_replace_selection("\n")); + self.drive(|drv| drv.insert_or_replace_selection("\n")); } Key::Named(NamedKey::Space) => { - self.transact(|txn| txn.insert_or_replace_selection(" ")); + self.drive(|drv| drv.insert_or_replace_selection(" ")); } Key::Character(s) => { - self.transact(|txn| txn.insert_or_replace_selection(&s)); + self.drive(|drv| drv.insert_or_replace_selection(&s)); } _ => (), } @@ -262,17 +262,17 @@ impl Editor { match phase { Started => { // TODO: start a timer to convert to a SelectWordAtPoint - self.transact(|txn| { - txn.move_to_point(location.x as f32 - INSET, location.y as f32 - INSET); + self.drive(|drv| { + drv.move_to_point(location.x as f32 - INSET, location.y as f32 - INSET); }); } Cancelled => { - self.transact(|txn| txn.collapse_selection()); + self.drive(|drv| drv.collapse_selection()); } Moved => { // TODO: cancel SelectWordAtPoint timer - self.transact(|txn| { - txn.extend_selection_to_point( + self.drive(|drv| { + drv.extend_selection_to_point( location.x as f32 - INSET, location.y as f32 - INSET, ); @@ -299,10 +299,10 @@ impl Editor { self.last_click_time = Some(now); let click_count = self.click_count; let cursor_pos = self.cursor_pos; - self.transact(|txn| match click_count { - 2 => txn.select_word_at_point(cursor_pos.0, cursor_pos.1), - 3 => txn.select_line_at_point(cursor_pos.0, cursor_pos.1), - _ => txn.move_to_point(cursor_pos.0, cursor_pos.1), + self.drive(|drv| 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), }); } } @@ -314,7 +314,7 @@ impl Editor { 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)); + self.drive(|drv| drv.extend_selection_to_point(cursor_pos.0, cursor_pos.1)); } } _ => {} @@ -324,8 +324,8 @@ 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.transact(|txn| { - txn.select_from_accesskit(selection); + self.drive(|drv| { + drv.select_from_accesskit(selection); }); } } @@ -397,8 +397,8 @@ impl Editor { pub fn accessibility(&mut self, update: &mut TreeUpdate, node: &mut Node) { self.editor - .transact(&mut self.font_cx, &mut self.layout_cx, |txn| { - txn.accessibility(update, node, next_node_id, INSET.into(), INSET.into()); + .drive(&mut self.font_cx, &mut self.layout_cx, |drv| { + 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 2eaea045..41236300 100644 --- a/parley/src/layout/editor.rs +++ b/parley/src/layout/editor.rs @@ -36,15 +36,19 @@ impl Generation { } } -/// Basic plain text editor with a single default style. +/// 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`], +/// which is kept up-to-date as needed. +/// This layout is invalidated by a number. #[derive(Clone)] pub struct PlainEditor where T: Brush + Clone + Debug + PartialEq + Default, { - default_style: StyleSet, - buffer: String, layout: Layout, + buffer: String, + default_style: StyleSet, #[cfg(feature = "accesskit")] layout_access: LayoutAccessibility, selection: Selection, @@ -59,8 +63,8 @@ where // Not all operations on `PlainEditor` need to operate on a // clean layout, and not all operations trigger a layout. layout_dirty: bool, - // TODO: We could avoid redoing the full text layout if linebreaking or - // alignment were unchanged + // TODO: We could avoid redoing the full text layout if only + // linebreaking or alignment were changed. // linebreak_dirty: bool, // alignment_dirty: bool, alignment: Alignment, @@ -71,6 +75,7 @@ impl PlainEditor where T: Brush, { + /// Create a new editor, with default font size `font_size`. pub fn new(font_size: f32) -> Self { Self { default_style: StyleSet::new(font_size), @@ -92,9 +97,11 @@ where } } -/// The argument passed to the callback of [`PlainEditor::transact`], -/// on which the caller performs operations. -pub struct PlainEditorTxn<'a, T> +/// A short-lived wrapper around [`PlainEditor`]. +/// +/// This can perform operations which require the editor's layout to +/// be up-to-date by refreshing it as necessary. +pub struct PlainEditorDriver<'a, T> where T: Brush + Clone + Debug + PartialEq + Default, { @@ -103,7 +110,7 @@ where pub layout_cx: &'a mut LayoutContext, } -impl PlainEditorTxn<'_, T> +impl PlainEditorDriver<'_, T> where T: Brush + Clone + Debug + PartialEq + Default, { @@ -233,9 +240,9 @@ where // --- MARK: IME --- /// Set the IME preedit composing text. /// - /// This starts composing. Composing is reset by calling [`PlainEditorTxn::clear_compose`]. + /// This starts composing. Composing is reset by calling [`clear_compose`](Self::clear_compose). /// While composing, it is a logic error to call anything other than - /// [`PlainEditorTxn::set_compose`] or [`PlainEditorTxn::clear_compose`]. + /// [`Self::set_compose`] or [`Self::clear_compose`]. /// /// The preedit text replaces the current selection if this call starts composing. /// @@ -577,7 +584,7 @@ where } } - /// Select a range of byte indices + /// Select a range of byte indices. /// /// No-op if either index is not a char boundary. pub fn select_byte_range(&mut self, start: usize, end: usize) { @@ -593,6 +600,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()); @@ -607,6 +615,7 @@ where } #[cfg(feature = "accesskit")] + /// Perform an accessibility update. pub fn accessibility( &mut self, update: &mut TreeUpdate, @@ -621,95 +630,53 @@ where Some(()) } - // --- MARK: Internal helpers --- - fn update_layout(&mut self) { - self.editor.update_layout(self.font_cx, self.layout_cx); - } - + // --- MARK: Internal helpers--- + /// Update the layout if needed. fn refresh_layout(&mut self) { self.editor.refresh_layout(self.font_cx, self.layout_cx); } + + /// Update the layout unconditionally. + fn update_layout(&mut self) { + self.editor.update_layout(self.font_cx, self.layout_cx); + } } impl PlainEditor where T: Brush + Clone + Debug + PartialEq + Default, { - /// Run a series of [`PlainEditorTxn`] methods. + // --- MARK: PlainEditor API --- + /// Run a series of [`PlainEditorDriver`] methods. /// - /// This is a utility shorthand around [`transaction`](Self::transaction); - pub fn transact( + /// 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 PlainEditorTxn<'_, T>) -> R, + callback: impl FnOnce(&mut PlainEditorDriver<'_, T>) -> R, ) -> R { - let mut txn = self.transaction(font_cx, layout_cx); - callback(&mut txn) + let mut drv = self.driver(font_cx, layout_cx); + callback(&mut drv) } - /// Run a series of [`PlainEditorTxn`] methods, updating the layout - /// if necessary. + /// Run a series of [`PlainEditorDriver`] methods. /// - /// This is a utility shorthand to simplify methods which require the editor - /// and the provided contexts. - pub fn transaction<'txn>( - &'txn mut self, - font_cx: &'txn mut FontContext, - layout_cx: &'txn mut LayoutContext, - ) -> PlainEditorTxn<'txn, T> { - PlainEditorTxn { + /// This type is only used to simplify methods which require both + /// the editor and the provided contexts. + pub fn driver<'drv>( + &'drv mut self, + font_cx: &'drv mut FontContext, + layout_cx: &'drv mut LayoutContext, + ) -> PlainEditorDriver<'drv, T> { + PlainEditorDriver { editor: self, font_cx, layout_cx, } } - /// Make a cursor at a given byte index - fn cursor_at(&self, index: usize) -> Cursor { - // TODO: Do we need to be non-dirty? - // FIXME: `Selection` should make this easier - if index >= self.buffer.len() { - Cursor::from_byte_index(&self.layout, self.buffer.len(), Affinity::Upstream) - } else { - Cursor::from_byte_index(&self.layout, index, Affinity::Downstream) - } - } - - fn replace_selection( - &mut self, - font_cx: &mut FontContext, - layout_cx: &mut LayoutContext, - s: &str, - ) { - let range = self.selection.text_range(); - let start = range.start; - if self.selection.is_collapsed() { - self.buffer.insert_str(start, s); - } else { - self.buffer.replace_range(range, s); - } - - self.update_layout(font_cx, layout_cx); - let new_index = start.saturating_add(s.len()); - let affinity = if s.ends_with("\n") { - Affinity::Downstream - } else { - Affinity::Upstream - }; - self.set_selection(Cursor::from_byte_index(&self.layout, new_index, affinity).into()); - } - - /// Update the selection, and nudge the `Generation` if something other than `h_pos` changed. - fn set_selection(&mut self, new_sel: Selection) { - if new_sel.focus() != self.selection.focus() || new_sel.anchor() != self.selection.anchor() - { - self.generation.nudge(); - } - - self.selection = new_sel; - } - /// If the current selection is not collapsed, returns the text content of /// that selection. pub fn selected_text(&self) -> Option<&str> { @@ -743,32 +710,10 @@ where self.generation } - /// Get the full read-only details from the layout - pub fn layout( - &mut self, - font_cx: &mut FontContext, - layout_cx: &mut LayoutContext, - ) -> &Layout { - self.refresh_layout(font_cx, layout_cx); - &self.layout - } - - /// Get the full read-only details from the layout, if valid. - pub fn get_layout(&self) -> Option<&Layout> { - if self.layout_dirty { - None - } else { - Some(&self.layout) - } - } - - /// Get the (potentially invalid) details from the layout. - pub fn layout_raw(&self) -> &Layout { - &self.layout - } - /// Replace the whole text buffer. pub fn set_text(&mut self, is: &str) { + debug_assert!(!self.is_composing()); + self.buffer.clear(); self.buffer.push_str(is); self.layout_dirty = true; @@ -795,39 +740,45 @@ where self.layout_dirty = true; } - /// Set the default style for the layout. + /// Modify the styles provided for this editor. pub fn edit_styles(&mut self) -> &mut StyleSet { self.layout_dirty = true; &mut self.default_style } - /// Update the layout if it is dirty. - fn refresh_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext) { - if self.layout_dirty { - self.update_layout(font_cx, layout_cx); - } + /// Whether the editor is currently in IME composing mode. + pub fn is_composing(&self) -> bool { + self.compose.is_some() } - /// Update the layout. - fn update_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext) { - let mut builder = layout_cx.ranged_builder(font_cx, &self.buffer, self.scale); - for prop in self.default_style.inner().values() { - builder.push_default(prop.to_owned()); - } - if let Some(ref preedit_range) = self.compose { - builder.push(StyleProperty::Underline(true), preedit_range.clone()); - } - self.layout = builder.build(&self.buffer); - self.layout.break_all_lines(self.width); - self.layout.align(self.width, self.alignment); - self.selection = self.selection.refresh(&self.layout); - self.layout_dirty = false; - self.generation.nudge(); + /// Get the full read-only details from the layout, which will be updated if necessary. + /// + /// If the required contexts are not available, then [`refresh_layout`](Self::refresh_layout) can + /// be called in a scope when they are available, and [`try_layout`](Self::try_layout) can + /// be used instead. + pub fn layout( + &mut self, + font_cx: &mut FontContext, + layout_cx: &mut LayoutContext, + ) -> &Layout { + self.refresh_layout(font_cx, layout_cx); + &self.layout } - /// Whether the editor is currently in IME composing mode. - pub fn is_composing(&self) -> bool { - self.compose.is_some() + // --- MARK: Raw APIs --- + /// Get the full read-only details from the layout, if valid. + /// + /// Returns `None` if the layout is not up-to-date. + /// You can call [`refresh_layout`](Self::refresh_layout) before using this method, + /// to ensure that the layout is up-to-date. + /// + /// The [`layout`](Self::layout) method should generally be preferred. + pub fn try_layout(&self) -> Option<&Layout> { + if self.layout_dirty { + None + } else { + Some(&self.layout) + } } #[cfg(feature = "accesskit")] @@ -835,6 +786,10 @@ where /// Perform an accessibility update if the layout is valid. /// /// Returns `None` if the layout is not up-to-date. + /// You can call [`refresh_layout`](Self::refresh_layout) before using this method, + /// to ensure that the layout is up-to-date. + /// The [`accessibility`](PlainEditorDriver::accessibility) method on the driver type + /// should be preferred if the contexts are available, which will do this automatically. pub fn try_accessibility( &mut self, update: &mut TreeUpdate, @@ -850,9 +805,88 @@ where Some(()) } + /// Update the layout if it is dirty. + /// + /// This should only be used alongside [`try_layout`](Self::try_layout) + /// or [`try_accessibility`](Self::try_accessibility), if those will be + /// called in a scope where the contexts are not available. + pub fn refresh_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext) { + if self.layout_dirty { + self.update_layout(font_cx, layout_cx); + } + } + + // --- MARK: Internal Helpers --- + /// Make a cursor at a given byte index. + fn cursor_at(&self, index: usize) -> Cursor { + // TODO: Do we need to be non-dirty? + // FIXME: `Selection` should make this easier + if index >= self.buffer.len() { + Cursor::from_byte_index(&self.layout, self.buffer.len(), Affinity::Upstream) + } else { + Cursor::from_byte_index(&self.layout, index, Affinity::Downstream) + } + } + + fn replace_selection( + &mut self, + font_cx: &mut FontContext, + layout_cx: &mut LayoutContext, + s: &str, + ) { + let range = self.selection.text_range(); + let start = range.start; + if self.selection.is_collapsed() { + self.buffer.insert_str(start, s); + } else { + self.buffer.replace_range(range, s); + } + + self.update_layout(font_cx, layout_cx); + let new_index = start.saturating_add(s.len()); + let affinity = if s.ends_with("\n") { + Affinity::Downstream + } else { + Affinity::Upstream + }; + self.set_selection(Cursor::from_byte_index(&self.layout, new_index, affinity).into()); + } + + /// Update the selection, and nudge the `Generation` if something other than `h_pos` changed. + fn set_selection(&mut self, new_sel: Selection) { + if new_sel.focus() != self.selection.focus() || new_sel.anchor() != self.selection.anchor() + { + self.generation.nudge(); + } + + self.selection = new_sel; + } + /// Update the layout. + fn update_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext) { + let mut builder = layout_cx.ranged_builder(font_cx, &self.buffer, self.scale); + for prop in self.default_style.inner().values() { + builder.push_default(prop.to_owned()); + } + if let Some(ref preedit_range) = self.compose { + builder.push(StyleProperty::Underline(true), preedit_range.clone()); + } + self.layout = builder.build(&self.buffer); + self.layout.break_all_lines(self.width); + self.layout.align(self.width, self.alignment); + self.selection = self.selection.refresh(&self.layout); + self.layout_dirty = false; + self.generation.nudge(); + } + #[cfg(feature = "accesskit")] - /// Perform an accessibility update, even if the layout isn't valid. - pub fn accessibility_raw( + /// Perform an accessibility update, assuming that the layout is valid. + /// + /// The wrapper [`accessibility`](PlainEditorDriver::accessibility) on the driver type should + /// be preferred. + /// + /// You should always call [`refresh_layout`](Self::refresh_layout) before using this method, + /// with no other modifying method calls in between. + fn accessibility_raw( &mut self, update: &mut TreeUpdate, node: &mut Node, diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 81c654d0..774aecd7 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -131,7 +131,7 @@ pub use inline_box::InlineBox; #[doc(inline)] pub use layout::Layout; -pub use layout::editor::{PlainEditor, PlainEditorTxn}; +pub use layout::editor::{PlainEditor, PlainEditorDriver}; pub use layout::*; pub use style::*; diff --git a/parley/src/style/styleset.rs b/parley/src/style/styleset.rs index eead2f42..f3dfd7ee 100644 --- a/parley/src/style/styleset.rs +++ b/parley/src/style/styleset.rs @@ -11,6 +11,8 @@ type StyleProperty = crate::StyleProperty<'static, Brush>; /// /// This is used by [`PlainEditor`](crate::editor::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). +/// /// These styles do not have a corresponding range, and are generally unsuited for rich text. #[derive(Clone, Debug)] pub struct StyleSet( From 23a768bc36cfcfe6a1eefa5b42e331c10fd1a04e Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:49:16 +0000 Subject: [PATCH 3/5] Add IME support to the editor Remove workaround for #186 Use `unchecked` name Fix doc comment Restore debug code Remove comment from Masonry Co-Authored-By: Tom Churchman --- examples/vello_editor/src/main.rs | 4 +- examples/vello_editor/src/text.rs | 95 ++++++++++++++++++++++++++++--- parley/src/layout/editor.rs | 61 ++++++++++++++++---- 3 files changed, 139 insertions(+), 21 deletions(-) diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index feadc1db..a6445bea 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -113,6 +113,7 @@ impl ApplicationHandler for SimpleVelloApp<'_> { let access_adapter = accesskit_winit::Adapter::with_event_loop_proxy(&window, self.event_loop_proxy.clone()); window.set_visible(true); + window.set_ime_allowed(true); let size = window.inner_size(); @@ -348,7 +349,8 @@ fn main() -> Result<()> { event_loop .run_app(&mut app) .expect("Couldn't run event loop"); - print!("{}", app.editor.text()); + let [text1, text2] = app.editor.text(); + print!("{text1}{text2}"); Ok(()) } diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index e4ae819b..9651c06a 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -6,9 +6,12 @@ use core::default::Default; use parley::{layout::PositionedLayoutItem, GenericFamily, StyleProperty}; use peniko::{kurbo::Affine, Color, Fill}; use std::time::{Duration, Instant}; -use vello::Scene; +use vello::{ + kurbo::{Line, Stroke}, + Scene, +}; use winit::{ - event::{Modifiers, Touch, WindowEvent}, + event::{Ime, Modifiers, Touch, WindowEvent}, keyboard::{Key, NamedKey}, }; @@ -41,6 +44,7 @@ impl Editor { let styles = editor.edit_styles(); styles.insert(StyleProperty::LineHeight(1.2)); styles.insert(GenericFamily::SystemUi.into()); + styles.insert(StyleProperty::Brush(Color::WHITE)); Self { font_cx: Default::default(), layout_cx: Default::default(), @@ -65,7 +69,7 @@ impl Editor { &mut self.editor } - pub fn text(&self) -> &str { + pub fn text(&self) -> [&str; 2] { self.editor.text() } @@ -108,7 +112,7 @@ impl Editor { WindowEvent::ModifiersChanged(modifiers) => { self.modifiers = Some(modifiers); } - WindowEvent::KeyboardInput { event, .. } => { + WindowEvent::KeyboardInput { event, .. } if !self.editor.is_composing() => { if !event.state.is_pressed() { return; } @@ -257,7 +261,7 @@ impl Editor { } WindowEvent::Touch(Touch { phase, location, .. - }) => { + }) if !self.editor.is_composing() => { use winit::event::TouchPhase::*; match phase { Started => { @@ -285,7 +289,7 @@ impl Editor { if button == winit::event::MouseButton::Left { self.pointer_down = state.is_pressed(); self.cursor_reset(); - if self.pointer_down { + if self.pointer_down && !self.editor.is_composing() { let now = Instant::now(); if let Some(last) = self.last_click_time.take() { if now.duration_since(last).as_secs_f64() < 0.25 { @@ -311,12 +315,25 @@ impl Editor { let prev_pos = self.cursor_pos; 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 { + 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)); } } + WindowEvent::Ime(Ime::Disabled) => { + self.drive(|drv| drv.clear_compose()); + } + WindowEvent::Ime(Ime::Commit(text)) => { + self.drive(|drv| drv.insert_or_replace_selection(&text)); + } + WindowEvent::Ime(Ime::Preedit(text, cursor)) => { + if text.is_empty() { + self.drive(|drv| drv.clear_compose()); + } else { + self.drive(|drv| drv.set_compose(&text, cursor)); + } + } _ => {} } } @@ -355,6 +372,39 @@ impl Editor { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue; }; + let style = glyph_run.style(); + // We draw underlines under the text, then the strikethrough on top, following: + // https://drafts.csswg.org/css-text-decor/#painting-order + if let Some(underline) = &style.underline { + let underline_brush = &style.brush; + let run_metrics = glyph_run.run().metrics(); + let offset = match underline.offset { + Some(offset) => offset, + None => run_metrics.underline_offset, + }; + let width = match underline.size { + Some(size) => size, + None => run_metrics.underline_size, + }; + // The `offset` is the distance from the baseline to the top of the underline + // so we move the line down by half the width + // Remember that we are using a y-down coordinate system + // If there's a custom width, because this is an underline, we want the custom + // width to go down from the default expectation + let y = glyph_run.baseline() - offset + width / 2.; + + let line = Line::new( + (glyph_run.offset() as f64, y as f64), + ((glyph_run.offset() + glyph_run.advance()) as f64, y as f64), + ); + scene.stroke( + &Stroke::new(width.into()), + transform, + underline_brush, + None, + &line, + ); + } let mut x = glyph_run.offset(); let y = glyph_run.baseline(); let run = glyph_run.run(); @@ -371,7 +421,7 @@ impl Editor { .collect::>(); scene .draw_glyphs(font) - .brush(Color::WHITE) + .brush(style.brush) .hint(true) .transform(transform) .glyph_transform(glyph_xform) @@ -390,6 +440,35 @@ impl Editor { } }), ); + if let Some(strikethrough) = &style.strikethrough { + let strikethrough_brush = &style.brush; + let run_metrics = glyph_run.run().metrics(); + let offset = match strikethrough.offset { + Some(offset) => offset, + None => run_metrics.strikethrough_offset, + }; + let width = match strikethrough.size { + Some(size) => size, + None => run_metrics.strikethrough_size, + }; + // The `offset` is the distance from the baseline to the *top* of the strikethrough + // so we calculate the middle y-position of the strikethrough based on the font's + // standard strikethrough width. + // Remember that we are using a y-down coordinate system + let y = glyph_run.baseline() - offset + run_metrics.strikethrough_size / 2.; + + let line = Line::new( + (glyph_run.offset() as f64, y as f64), + ((glyph_run.offset() + glyph_run.advance()) as f64, y as f64), + ); + scene.stroke( + &Stroke::new(width.into()), + transform, + strikethrough_brush, + None, + &line, + ); + } } } self.editor.generation() diff --git a/parley/src/layout/editor.rs b/parley/src/layout/editor.rs index 41236300..38c478e4 100644 --- a/parley/src/layout/editor.rs +++ b/parley/src/layout/editor.rs @@ -1,7 +1,7 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -//! Import of Parley's `PlainEditor` as the version in Parley is insufficient for our needs. +//! A simple plain text editor and related types. use crate::{ layout::{ @@ -196,7 +196,7 @@ where // Otherwise, delete the previous character let Some((start, _)) = self .editor - .text() + .buffer .get(..end) .and_then(|str| str.char_indices().next_back()) else { @@ -626,7 +626,7 @@ where ) -> Option<()> { self.refresh_layout(); self.editor - .accessibility_raw(update, node, next_node_id, x_offset, y_offset); + .accessibility_unchecked(update, node, next_node_id, x_offset, y_offset); Some(()) } @@ -680,8 +680,11 @@ where /// If the current selection is not collapsed, returns the text content of /// that selection. pub fn selected_text(&self) -> Option<&str> { + if self.is_composing() { + return None; + } if !self.selection.is_collapsed() { - self.text().get(self.selection.text_range()) + self.buffer.get(self.selection.text_range()) } else { None } @@ -698,8 +701,15 @@ where } /// Borrow the text content of the buffer. - pub fn text(&self) -> &str { - &self.buffer + /// + /// The return values concatenated is the full text content. + /// This split is used when composing. + pub fn text(&self) -> [&str; 2] { + if let Some(compose) = &self.compose { + [&self.buffer[..compose.start], &self.buffer[compose.end..]] + } else { + [&self.buffer, ""] + } } /// Get the current `Generation` of the layout, to decide whether to draw. @@ -720,11 +730,8 @@ where } /// Set the width of the layout. - // TODO: If this is infinite, is the width used for alignnment the min width? pub fn set_width(&mut self, width: Option) { - // Don't allow empty widths: - // https://github.com/linebender/parley/issues/186 - self.width = width.map(|width| if width > 10. { width } else { 10. }); + self.width = width; self.layout_dirty = true; } @@ -801,7 +808,7 @@ where if self.layout_dirty { return None; } - self.accessibility_raw(update, node, next_node_id, x_offset, y_offset); + self.accessibility_unchecked(update, node, next_node_id, x_offset, y_offset); Some(()) } @@ -859,6 +866,36 @@ where self.generation.nudge(); } + // This debug code is quite useful when diagnosing selection problems. + #[cfg(feature = "std")] + #[allow(clippy::print_stderr)] // reason = "unreachable debug code" + if false { + let focus = new_sel.focus(); + let cluster = focus.logical_clusters(&self.layout); + let dbg = ( + cluster[0].as_ref().map(|c| &self.buffer[c.text_range()]), + focus.index(), + focus.affinity(), + cluster[1].as_ref().map(|c| &self.buffer[c.text_range()]), + ); + eprint!("{dbg:?}"); + let cluster = focus.visual_clusters(&self.layout); + let dbg = ( + cluster[0].as_ref().map(|c| &self.buffer[c.text_range()]), + cluster[0] + .as_ref() + .map(|c| if c.is_word_boundary() { " W" } else { "" }) + .unwrap_or_default(), + focus.index(), + focus.affinity(), + cluster[1].as_ref().map(|c| &self.buffer[c.text_range()]), + cluster[1] + .as_ref() + .map(|c| if c.is_word_boundary() { " W" } else { "" }) + .unwrap_or_default(), + ); + eprintln!(" | visual: {dbg:?}"); + } self.selection = new_sel; } /// Update the layout. @@ -886,7 +923,7 @@ where /// /// You should always call [`refresh_layout`](Self::refresh_layout) before using this method, /// with no other modifying method calls in between. - fn accessibility_raw( + fn accessibility_unchecked( &mut self, update: &mut TreeUpdate, node: &mut Node, From 1a783fe42129b688b75e4aec606f2e101189090c Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:57:18 +0000 Subject: [PATCH 4/5] Address Review Comments Migrate to a `SplitString` abstraction Add a layout helper method Replace composing checks with full asserts --- examples/vello_editor/src/main.rs | 4 +- examples/vello_editor/src/text.rs | 114 ++++++++++----------- parley/src/layout/editor.rs | 164 +++++++++++++++++++----------- 3 files changed, 161 insertions(+), 121 deletions(-) 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); From 399794c302e0615c396469ee6918f48143fc8c22 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:10:36 +0000 Subject: [PATCH 5/5] Fixup with the tests --- parley/src/layout/editor.rs | 23 ++++++++++++++++++++++- parley/src/tests/test_editor.rs | 32 +++++++++++++++----------------- parley/src/tests/utils/env.rs | 31 +++++++++++++++---------------- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/parley/src/layout/editor.rs b/parley/src/layout/editor.rs index bf445a97..afac08db 100644 --- a/parley/src/layout/editor.rs +++ b/parley/src/layout/editor.rs @@ -382,6 +382,7 @@ where pub fn move_to_text_start(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, isize::MIN, @@ -393,6 +394,7 @@ where pub fn move_to_line_start(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor .set_selection(self.editor.selection.line_start(&self.editor.layout, false)); } @@ -401,6 +403,7 @@ where pub fn move_to_text_end(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, isize::MAX, @@ -412,6 +415,7 @@ where pub fn move_to_line_end(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor .set_selection(self.editor.selection.line_end(&self.editor.layout, false)); } @@ -420,6 +424,7 @@ where pub fn move_up(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -431,6 +436,7 @@ where pub fn move_down(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor .set_selection(self.editor.selection.next_line(&self.editor.layout, false)); } @@ -439,6 +445,7 @@ where pub fn move_left(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -450,6 +457,7 @@ where pub fn move_right(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -461,6 +469,7 @@ where pub fn move_word_left(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -472,6 +481,7 @@ where pub fn move_word_right(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -483,6 +493,7 @@ where pub fn select_all(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( Selection::from_byte_index(&self.editor.layout, 0_usize, Affinity::default()) .move_lines(&self.editor.layout, isize::MAX, true), @@ -500,6 +511,7 @@ where pub fn select_to_text_start(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, isize::MIN, @@ -511,6 +523,7 @@ where pub fn select_to_line_start(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor .set_selection(self.editor.selection.line_start(&self.editor.layout, true)); } @@ -519,6 +532,7 @@ where pub fn select_to_text_end(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection(self.editor.selection.move_lines( &self.editor.layout, isize::MAX, @@ -530,6 +544,7 @@ where pub fn select_to_line_end(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor .set_selection(self.editor.selection.line_end(&self.editor.layout, true)); } @@ -538,6 +553,7 @@ where pub fn select_up(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -549,6 +565,7 @@ where pub fn select_down(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor .set_selection(self.editor.selection.next_line(&self.editor.layout, true)); } @@ -557,6 +574,7 @@ where pub fn select_left(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -568,6 +586,7 @@ where pub fn select_right(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor .set_selection(self.editor.selection.next_visual(&self.editor.layout, true)); } @@ -576,6 +595,7 @@ where pub fn select_word_left(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -587,6 +607,7 @@ where pub fn select_word_right(&mut self) { assert!(!self.editor.is_composing()); + self.refresh_layout(); self.editor.set_selection( self.editor .selection @@ -691,7 +712,7 @@ where } // --- MARK: Internal helpers--- /// Update the layout if needed. - fn refresh_layout(&mut self) { + pub fn refresh_layout(&mut self) { self.editor.refresh_layout(self.font_cx, self.layout_cx); } diff --git a/parley/src/tests/test_editor.rs b/parley/src/tests/test_editor.rs index b4ce97dd..0c1e5d80 100644 --- a/parley/src/tests/test_editor.rs +++ b/parley/src/tests/test_editor.rs @@ -7,27 +7,25 @@ use crate::testenv; fn editor_simple_move() { let mut env = testenv!(); let mut editor = env.editor("Hi, all!\nNext"); - env.check_editor_snapshot(&editor); - env.transact(&mut editor, |e| { - e.move_right(); - e.move_right(); - e.move_right(); - }); - env.check_editor_snapshot(&editor); - env.transact(&mut editor, |e| e.move_down()); - env.check_editor_snapshot(&editor); - env.transact(&mut editor, |e| e.move_left()); - env.check_editor_snapshot(&editor); - env.transact(&mut editor, |e| e.move_up()); - env.check_editor_snapshot(&editor); + env.check_editor_snapshot(&mut editor); + let mut drv = env.driver(&mut editor); + drv.move_right(); + drv.move_right(); + drv.move_right(); + + env.check_editor_snapshot(&mut editor); + env.driver(&mut editor).move_down(); + env.check_editor_snapshot(&mut editor); + env.driver(&mut editor).move_left(); + env.check_editor_snapshot(&mut editor); + env.driver(&mut editor).move_up(); + env.check_editor_snapshot(&mut editor); } #[test] fn editor_select_all() { let mut env = testenv!(); let mut editor = env.editor("Hi, all!\nNext"); - env.transact(&mut editor, |e| { - e.select_all(); - }); - env.check_editor_snapshot(&editor); + env.driver(&mut editor).select_all(); + env.check_editor_snapshot(&mut editor); } diff --git a/parley/src/tests/utils/env.rs b/parley/src/tests/utils/env.rs index 587b7525..18cd6eb6 100644 --- a/parley/src/tests/utils/env.rs +++ b/parley/src/tests/utils/env.rs @@ -3,13 +3,12 @@ use crate::tests::utils::renderer::{render_layout, RenderingConfig}; use crate::{ - FontContext, FontFamily, FontStack, Layout, LayoutContext, PlainEditor, PlainEditorTxn, + FontContext, FontFamily, FontStack, Layout, LayoutContext, PlainEditor, PlainEditorDriver, RangedBuilder, Rect, StyleProperty, }; use fontique::{Collection, CollectionOptions}; use peniko::Color; use std::path::{Path, PathBuf}; -use std::sync::Arc; use tiny_skia::Pixmap; // Creates a new instance of TestEnv and put current function name in constructor @@ -60,6 +59,7 @@ pub(crate) struct TestEnv { rendering_config: RenderingConfig, cursor_size: f32, tolerance: f32, + // TODO: Add core::panic::Location for case. errors: Vec<(PathBuf, String)>, next_test_case_name: String, } @@ -156,21 +156,19 @@ impl TestEnv { builder } - pub(crate) fn transact( - &mut self, - editor: &mut PlainEditor, - callback: impl FnOnce(&mut PlainEditorTxn<'_, Color>), - ) { - editor.transact(&mut self.font_cx, &mut self.layout_cx, callback); + pub(crate) fn driver<'a>( + &'a mut self, + editor: &'a mut PlainEditor, + ) -> PlainEditorDriver<'a, Color> { + editor.driver(&mut self.font_cx, &mut self.layout_cx) } pub(crate) fn editor(&mut self, text: &str) -> PlainEditor { - let default_style = Arc::new(self.default_style()); - let mut editor = PlainEditor::default(); - self.transact(&mut editor, |editor| { - editor.set_default_style(default_style); - editor.set_text(text); - }); + let mut editor = PlainEditor::new(16.); + for style in self.default_style() { + editor.edit_styles().insert(style); + } + editor.set_text(text); editor } @@ -229,9 +227,10 @@ impl TestEnv { self } - pub(crate) fn check_editor_snapshot(&mut self, editor: &PlainEditor) { + pub(crate) fn check_editor_snapshot(&mut self, editor: &mut PlainEditor) { + editor.refresh_layout(&mut self.font_cx, &mut self.layout_cx); self.render_and_check_snapshot( - editor.layout(), + editor.try_layout().unwrap(), editor.cursor_geometry(self.cursor_size), &editor.selection_geometry(), );