diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index 8d9aef98..66e0451a 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -16,6 +16,7 @@ use winit::window::Window; // #[path = "text2.rs"] mod text; +use parley::layout::editor::PlainEditorOp; // Simple struct to hold the state of the renderer pub struct ActiveRenderState<'s> { @@ -47,7 +48,7 @@ struct SimpleVelloApp<'s> { scene: Scene, // Our text state object - editor: text::Editor, + editor: text::Editor<'s>, } impl ApplicationHandler for SimpleVelloApp<'_> { @@ -70,7 +71,12 @@ impl ApplicationHandler for SimpleVelloApp<'_> { wgpu::PresentMode::AutoVsync, ); let surface = pollster::block_on(surface_future).expect("Error creating surface"); - self.editor.update_layout(size.width as _, 1.0); + + self.editor.transact([ + PlainEditorOp::SetScale(1.0), + PlainEditorOp::SetWidth(size.width as f32 - 2f32 * text::INSET), + PlainEditorOp::SetText(text::LOREM.into()), + ]); // Create a vello Renderer for the surface (using its device id) self.renderers @@ -107,7 +113,7 @@ impl ApplicationHandler for SimpleVelloApp<'_> { _ => return, }; - self.editor.handle_event(&event); + self.editor.handle_event(event.clone()); render_state.window.request_redraw(); // render_state // .window @@ -122,7 +128,17 @@ impl ApplicationHandler for SimpleVelloApp<'_> { self.context .resize_surface(&mut render_state.surface, size.width, size.height); render_state.window.request_redraw(); - self.editor.update_layout(size.width as _, 1.0); + self.editor.transact([ + PlainEditorOp::SetScale(1.0), + PlainEditorOp::SetWidth(size.width as f32 - 2f32 * text::INSET), + PlainEditorOp::SetDefaultStyle(Arc::new([ + parley::style::StyleProperty::FontSize(32.0), + parley::style::StyleProperty::LineHeight(1.2), + parley::style::StyleProperty::FontStack(parley::style::FontStack::Source( + "system-ui", + )), + ])), + ]); } // This is where all the rendering happens @@ -189,13 +205,12 @@ fn main() -> Result<()> { editor: text::Editor::default(), }; - app.editor.set_text(text::LOREM); - // Create and run a winit event loop let event_loop = EventLoop::new()?; event_loop .run_app(&mut app) .expect("Couldn't run event loop"); + print!("{}", app.editor.text()); Ok(()) } diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index b8ca3838..3e474358 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -1,11 +1,7 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -#[cfg(not(target_os = "android"))] -use clipboard_rs::{Clipboard, ClipboardContext}; -use parley::layout::cursor::{Selection, VisualMode}; -use parley::layout::Affinity; -use parley::{layout::PositionedLayoutItem, FontContext}; +use parley::layout::PositionedLayoutItem; use peniko::{kurbo::Affine, Color, Fill}; use std::time::Instant; use vello::Scene; @@ -14,126 +10,48 @@ use winit::{ keyboard::{KeyCode, PhysicalKey}, }; -type LayoutContext = parley::LayoutContext; -type Layout = parley::Layout; +extern crate alloc; +use alloc::{sync::Arc, vec}; -const INSET: f32 = 32.0; +use core::{default::Default, iter::IntoIterator}; -#[allow(dead_code)] -#[derive(Copy, Clone, Debug)] -pub enum ActiveText<'a> { - FocusedCluster(Affinity, &'a str), - Selection(&'a str), -} +use parley::{FontContext, LayoutContext, PlainEditor, PlainEditorOp}; + +pub const INSET: f32 = 32.0; #[derive(Default)] -pub struct Editor { +pub struct Editor<'a> { font_cx: FontContext, - layout_cx: LayoutContext, - buffer: String, - layout: Layout, - selection: Selection, - cursor_mode: VisualMode, + layout_cx: LayoutContext, + editor: PlainEditor<'a, Color>, last_click_time: Option, click_count: u32, pointer_down: bool, cursor_pos: (f32, f32), modifiers: Option, - width: f32, } -impl Editor { - pub fn set_text(&mut self, text: &str) { - self.buffer.clear(); - self.buffer.push_str(text); - } - - pub fn update_layout(&mut self, width: f32, scale: f32) { - let mut builder = self - .layout_cx - .ranged_builder(&mut self.font_cx, &self.buffer, scale); - builder.push_default(&parley::style::StyleProperty::FontSize(32.0)); - builder.push_default(&parley::style::StyleProperty::LineHeight(1.2)); - builder.push_default(&parley::style::StyleProperty::FontStack( - parley::style::FontStack::Source("system-ui"), - )); - builder.build_into(&mut self.layout, &self.buffer); - self.layout.break_all_lines(Some(width - INSET * 2.0)); - self.layout - .align(Some(width - INSET * 2.0), parley::layout::Alignment::Start); - self.width = width; - } - - #[allow(unused)] - pub fn active_text(&self) -> ActiveText { - if self.selection.is_collapsed() { - let range = self - .selection - .focus() - .cluster_path() - .cluster(&self.layout) - .map(|c| c.text_range()) - .unwrap_or_default(); - ActiveText::FocusedCluster(self.selection.focus().affinity(), &self.buffer[range]) - } else { - ActiveText::Selection(&self.buffer[self.selection.text_range()]) - } - } - - #[cfg(not(target_os = "android"))] - fn handle_clipboard(&mut self, code: KeyCode) { - match code { - KeyCode::KeyC => { - if !self.selection.is_collapsed() { - let text = &self.buffer[self.selection.text_range()]; - let cb = ClipboardContext::new().unwrap(); - cb.set_text(text.to_owned()).ok(); - } - } - KeyCode::KeyX => { - if !self.selection.is_collapsed() { - let text = &self.buffer[self.selection.text_range()]; - let cb = ClipboardContext::new().unwrap(); - cb.set_text(text.to_owned()).ok(); - if let Some(start) = self.delete_current_selection() { - self.update_layout(self.width, 1.0); - let (start, affinity) = if start > 0 { - (start - 1, Affinity::Upstream) - } else { - (start, Affinity::Downstream) - }; - self.selection = Selection::from_index(&self.layout, start, affinity); - } - } - } - KeyCode::KeyV => { - let cb = ClipboardContext::new().unwrap(); - let text = cb.get_text().unwrap_or_default(); - let start = self - .delete_current_selection() - .unwrap_or_else(|| self.selection.focus().text_range().start); - self.buffer.insert_str(start, &text); - self.update_layout(self.width, 1.0); - self.selection = - Selection::from_index(&self.layout, start + text.len(), Affinity::default()); - } - _ => {} - } +impl<'a> Editor<'a> { + pub fn transact(&mut self, t: impl IntoIterator>) { + self.editor + .transact(&mut self.font_cx, &mut self.layout_cx, t); } - #[cfg(target_os = "android")] - fn handle_clipboard(&mut self, _code: KeyCode) { - // TODO: support clipboard on Android + pub fn text(&self) -> Arc { + self.editor.text() } - pub fn handle_event(&mut self, event: &WindowEvent) { + pub fn handle_event(&mut self, event: WindowEvent) { match event { WindowEvent::Resized(size) => { - self.update_layout(size.width as f32, 1.0); - self.selection = self.selection.refresh(&self.layout); + self.editor.transact( + &mut self.font_cx, + &mut self.layout_cx, + [PlainEditorOp::SetWidth(size.width as f32 - 2f32 * INSET)], + ); } WindowEvent::ModifiersChanged(modifiers) => { - self.modifiers = Some(*modifiers); + self.modifiers = Some(modifiers); } WindowEvent::KeyboardInput { event, .. } => { if !event.state.is_pressed() { @@ -150,123 +68,131 @@ impl Editor { ) }) .unwrap_or_default(); + #[cfg(target_os = "macos")] let action_mod = cmd; #[cfg(not(target_os = "macos"))] let action_mod = ctrl; if let PhysicalKey::Code(code) = event.physical_key { - match code { - KeyCode::KeyC if action_mod => { - self.handle_clipboard(code); - } - KeyCode::KeyX if action_mod => { - self.handle_clipboard(code); - } - KeyCode::KeyV if action_mod => { - self.handle_clipboard(code); - } - KeyCode::ArrowLeft => { - self.selection = if ctrl { - self.selection.previous_word(&self.layout, shift) - } else { - self.selection.previous_visual( - &self.layout, - self.cursor_mode, - shift, - ) - }; - } - KeyCode::ArrowRight => { - self.selection = if ctrl { - self.selection.next_word(&self.layout, shift) + self.editor.transact( + &mut self.font_cx, + &mut self.layout_cx, + match code { + KeyCode::KeyA if action_mod => vec![PlainEditorOp::SelectAll], + #[cfg(not(target_os = "android"))] + KeyCode::KeyC | KeyCode::KeyX | KeyCode::KeyX if action_mod => { + use clipboard_rs::{Clipboard, ClipboardContext}; + use parley::layout::editor::ActiveText; + + match code { + KeyCode::KeyC => { + if let ActiveText::Selection(text) = + self.editor.active_text() + { + let cb = ClipboardContext::new().unwrap(); + cb.set_text(text.to_owned()).ok(); + } + vec![] + } + KeyCode::KeyX => { + if let ActiveText::Selection(text) = + self.editor.active_text() + { + let cb = ClipboardContext::new().unwrap(); + cb.set_text(text.to_owned()).ok(); + vec![PlainEditorOp::DeleteSelection] + } else { + vec![] + } + } + KeyCode::KeyV => { + let cb = ClipboardContext::new().unwrap(); + let text = cb.get_text().unwrap_or_default(); + vec![PlainEditorOp::InsertOrReplaceSelection(text.into())] + } + _ => vec![], + } + } + KeyCode::ArrowLeft => vec![if ctrl { + if shift { + PlainEditorOp::SelectWordLeft + } else { + PlainEditorOp::MoveWordLeft + } + } else if shift { + PlainEditorOp::SelectLeft } else { - self.selection - .next_visual(&self.layout, self.cursor_mode, shift) - }; - } - KeyCode::ArrowUp => { - self.selection = self.selection.previous_line(&self.layout, shift); - } - KeyCode::ArrowDown => { - self.selection = self.selection.next_line(&self.layout, shift); - } - KeyCode::Home => { - if ctrl { - self.selection = - self.selection.move_lines(&self.layout, isize::MIN, shift); + PlainEditorOp::MoveLeft + }], + KeyCode::ArrowRight => vec![if ctrl { + if shift { + PlainEditorOp::SelectWordRight + } else { + PlainEditorOp::MoveWordRight + } + } else if shift { + PlainEditorOp::SelectRight } else { - self.selection = self.selection.line_start(&self.layout, shift); - } - } - KeyCode::End => { - if ctrl { - self.selection = - self.selection.move_lines(&self.layout, isize::MAX, shift); + PlainEditorOp::MoveRight + }], + KeyCode::ArrowUp => vec![if shift { + PlainEditorOp::SelectUp } else { - self.selection = self.selection.line_end(&self.layout, shift); - } - } - KeyCode::Delete => { - let start = if self.selection.is_collapsed() { - let range = self.selection.focus().text_range(); - let start = range.start; - self.buffer.replace_range(range, ""); - Some(start) + PlainEditorOp::MoveUp + }], + KeyCode::ArrowDown => vec![if shift { + PlainEditorOp::SelectDown } else { - self.delete_current_selection() - }; - if let Some(start) = start { - self.update_layout(self.width, 1.0); - self.selection = - Selection::from_index(&self.layout, start, Affinity::default()); - } - } - KeyCode::Backspace => { - let start = if self.selection.is_collapsed() { - let end = self.selection.focus().text_range().start; - if let Some((start, _)) = - self.buffer[..end].char_indices().next_back() - { - self.buffer.replace_range(start..end, ""); - Some(start) + PlainEditorOp::MoveDown + }], + KeyCode::Home => vec![if ctrl { + if shift { + PlainEditorOp::SelectToTextStart } else { - None + PlainEditorOp::MoveToTextStart } + } else if shift { + PlainEditorOp::SelectToLineStart } else { - self.delete_current_selection() - }; - if let Some(start) = start { - self.update_layout(self.width, 1.0); - let (start, affinity) = if start > 0 { - (start - 1, Affinity::Upstream) + PlainEditorOp::MoveToLineStart + }], + KeyCode::End => vec![if ctrl { + if shift { + PlainEditorOp::SelectToTextEnd } else { - (start, Affinity::Downstream) - }; - self.selection = - Selection::from_index(&self.layout, start, affinity); - } - } - _ => { - if let Some(text) = &event.text { - let start = self - .delete_current_selection() - .unwrap_or_else(|| self.selection.focus().text_range().start); - self.buffer.insert_str(start, text); - self.update_layout(self.width, 1.0); - self.selection = Selection::from_index( - &self.layout, - start + text.len() - 1, - Affinity::Upstream, - ); - } - } - } + PlainEditorOp::MoveToTextEnd + } + } else if shift { + PlainEditorOp::SelectToLineEnd + } else { + PlainEditorOp::MoveToLineEnd + }], + KeyCode::Delete => vec![if action_mod { + PlainEditorOp::DeleteWord + } else { + PlainEditorOp::Delete + }], + KeyCode::Backspace => vec![if action_mod { + PlainEditorOp::BackdeleteWord + } else { + PlainEditorOp::Backdelete + }], + _ => event + .text + .map(|text| { + vec![PlainEditorOp::InsertOrReplaceSelection( + text.as_str().into(), + )] + }) + .unwrap_or(vec![]), + }, + ); } // println!("Active text: {:?}", self.active_text()); } WindowEvent::MouseInput { state, button, .. } => { - if *button == winit::event::MouseButton::Left { + if button == winit::event::MouseButton::Left { self.pointer_down = state.is_pressed(); if self.pointer_down { let now = Instant::now(); @@ -280,33 +206,25 @@ impl Editor { self.click_count = 1; } self.last_click_time = Some(now); - match self.click_count { - 2 => { - self.selection = Selection::word_from_point( - &self.layout, + self.editor.transact( + &mut self.font_cx, + &mut self.layout_cx, + match self.click_count { + 2 => [PlainEditorOp::SelectWordAtPoint( self.cursor_pos.0, self.cursor_pos.1, - ); - } - 3 => { - let focus = *Selection::from_point( - &self.layout, + )], + 3 => [PlainEditorOp::SelectLineAtPoint( self.cursor_pos.0, self.cursor_pos.1, - ) - .line_start(&self.layout, true) - .focus(); - self.selection = - Selection::from(focus).line_end(&self.layout, true); - } - _ => { - self.selection = Selection::from_point( - &self.layout, + )], + _ => [PlainEditorOp::MoveToPoint( self.cursor_pos.0, self.cursor_pos.1, - ); - } - } + )], + }, + ); + // println!("Active text: {:?}", self.active_text()); } } @@ -316,10 +234,13 @@ impl Editor { self.cursor_pos = (position.x as f32 - INSET, position.y as f32 - INSET); // macOS seems to generate a spurious move after selecting word? if self.pointer_down && prev_pos != self.cursor_pos { - self.selection = self.selection.extend_to_point( - &self.layout, - self.cursor_pos.0, - self.cursor_pos.1, + self.editor.transact( + &mut self.font_cx, + &mut self.layout_cx, + [PlainEditorOp::ExtendSelectionToPoint( + self.cursor_pos.0, + self.cursor_pos.1, + )], ); // println!("Active text: {:?}", self.active_text()); } @@ -328,29 +249,18 @@ impl Editor { } } - fn delete_current_selection(&mut self) -> Option { - if !self.selection.is_collapsed() { - let range = self.selection.text_range(); - let start = range.start; - self.buffer.replace_range(range, ""); - Some(start) - } else { - None - } - } - pub fn draw(&self, scene: &mut Scene) { let transform = Affine::translate((INSET as f64, INSET as f64)); - self.selection.geometry_with(&self.layout, |rect| { + for rect in self.editor.selection_geometry().iter() { scene.fill(Fill::NonZero, transform, Color::STEEL_BLUE, None, &rect); - }); - if let Some(cursor) = self.selection.focus().strong_geometry(&self.layout, 1.5) { + } + if let Some(cursor) = self.editor.selection_strong_geometry(1.5) { scene.fill(Fill::NonZero, transform, Color::WHITE, None, &cursor); }; - if let Some(cursor) = self.selection.focus().weak_geometry(&self.layout, 1.5) { + if let Some(cursor) = self.editor.selection_weak_geometry(1.5) { scene.fill(Fill::NonZero, transform, Color::LIGHT_GRAY, None, &cursor); }; - for line in self.layout.lines() { + for line in self.editor.lines() { for item in line.items() { let PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue; diff --git a/parley/src/layout/editor.rs b/parley/src/layout/editor.rs new file mode 100644 index 00000000..d3fd8a6b --- /dev/null +++ b/parley/src/layout/editor.rs @@ -0,0 +1,450 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use core::{cmp::PartialEq, default::Default, fmt::Debug, iter::IntoIterator}; + +use crate::{ + layout::{ + cursor::{Selection, VisualMode}, + Affinity, Alignment, Layout, Line, + }, + style::{Brush, StyleProperty}, + FontContext, LayoutContext, Rect, +}; +use alloc::{sync::Arc, vec::Vec}; + +#[derive(Copy, Clone, Debug)] +pub enum ActiveText<'a> { + /// The selection is empty and the cursor is a caret; this is the text of the cluster it is on + FocusedCluster(Affinity, &'a str), + /// The selection contains this text + Selection(&'a str), +} + +/// Basic plain text editor with a single default style. +pub struct PlainEditor<'a, T> +where + T: Brush + Clone + Debug + PartialEq + Default, +{ + default_style: Arc<[StyleProperty<'a, T>]>, + buffer: String, + layout: Layout, + selection: Selection, + cursor_mode: VisualMode, + width: f32, + scale: f32, +} + +// TODO: When MSRV >= 1.80 we can remove this. Default was not implemented for Arc<[T]> where T: !Default until 1.80 +impl<'a, T> Default for PlainEditor<'a, T> +where + T: Brush + Clone + Debug + PartialEq + Default, +{ + fn default() -> Self { + Self { + default_style: Arc::new([]), + buffer: Default::default(), + layout: Default::default(), + selection: Default::default(), + cursor_mode: Default::default(), + width: Default::default(), + scale: Default::default(), + } + } +} + +/// Operations on a `PlainEditor` for `PlainEditor::transact` +#[non_exhaustive] +pub enum PlainEditorOp<'a, T> +where + T: Brush + Clone + Debug + PartialEq + Default, +{ + /// Replace the whole text buffer + SetText(Arc), + /// Set the width of the layout + SetWidth(f32), + /// Set the scale for the layout + SetScale(f32), + /// Set the default style for the layout + SetDefaultStyle(Arc<[StyleProperty<'a, T>]>), + /// Insert at cursor, or replace selection + InsertOrReplaceSelection(Arc), + /// Delete the selection + DeleteSelection, + /// Delete the selection or the next cluster (typical ‘delete’ behavior) + Delete, + /// Delete the selection or up to the next word boundary (typical ‘ctrl + delete’ behavior) + DeleteWord, + /// Delete the selection or the previous cluster (typical ‘backspace’ behavior) + Backdelete, + /// Delete the selection or back to the previous word boundary (typical ‘ctrl + backspace’ behavior) + BackdeleteWord, + /// Move the cursor to the cluster boundary nearest this point in the layout + MoveToPoint(f32, f32), + /// Move the cursor to the start of the buffer + MoveToTextStart, + /// Move the cursor to the start of the physical line + MoveToLineStart, + /// Move the cursor to the end of the buffer + MoveToTextEnd, + /// Move the cursor to the end of the physical line + MoveToLineEnd, + /// Move up to the closest physical cluster boundary on the previous line, preserving the horizontal position for repeated movements + MoveUp, + /// Move down to the closest physical cluster boundary on the next line, preserving the horizontal position for repeated movements + MoveDown, + /// Move to the next cluster left in visual order + MoveLeft, + /// Move to the next cluster right in visual order + MoveRight, + /// Move to the next word boundary left + MoveWordLeft, + /// Move to the next word boundary right + MoveWordRight, + /// Select the whole buffer + SelectAll, + /// Move the selection focus point to the start of the buffer + SelectToTextStart, + /// Move the selection focus point to the start of the physical line + SelectToLineStart, + /// Move the selection focus point to the end of the buffer + SelectToTextEnd, + /// Move the selection focus point to the end of the physical line + SelectToLineEnd, + /// Move the selection focus point up to the nearest cluster boundary on the previous line, preserving the horizontal position for repeated movements + SelectUp, + /// Move the selection focus point down to the nearest cluster boundary on the next line, preserving the horizontal position for repeated movements + SelectDown, + /// Move the selection focus point to the next cluster left in visual order + SelectLeft, + /// Move the selection focus point to the next cluster right in visual order + SelectRight, + /// Move the selection focus point to the next word boundary left + SelectWordLeft, + /// Move the selection focus point to the next word boundary right + SelectWordRight, + /// Select the word at the point + SelectWordAtPoint(f32, f32), + /// Select the physical line at the point + SelectLineAtPoint(f32, f32), + /// Move the selection focus point to the cluster boundary closest to point + ExtendSelectionToPoint(f32, f32), +} + +impl<'a, T> PlainEditor<'a, T> +where + T: Brush + Clone + Debug + PartialEq + Default, +{ + /// Run a series of `PlainEditorOp`s, updating the layout if necessary + pub fn transact( + &mut self, + font_cx: &mut FontContext, + layout_cx: &mut LayoutContext, + t: impl IntoIterator>, + ) { + let mut layout_after = false; + + for op in t.into_iter() { + match op { + PlainEditorOp::SetText(is) => { + self.buffer.clear(); + self.buffer.push_str(&is); + layout_after = true; + } + PlainEditorOp::SetWidth(width) => { + self.width = width; + layout_after = true; + } + PlainEditorOp::SetScale(scale) => { + self.scale = scale; + layout_after = true; + } + PlainEditorOp::SetDefaultStyle(style) => { + self.default_style = style.clone(); + layout_after = true; + } + PlainEditorOp::DeleteSelection => { + self.replace_selection(font_cx, layout_cx, ""); + } + PlainEditorOp::Delete => { + if self.selection.is_collapsed() { + let range = self.selection.focus().text_range(); + if !range.is_empty() { + let start = range.start; + self.buffer.replace_range(range, ""); + self.update_layout(font_cx, layout_cx); + self.selection = if start == self.buffer.len() { + Selection::from_index( + &self.layout, + start.saturating_sub(1), + Affinity::Upstream, + ) + } else { + Selection::from_index( + &self.layout, + start.min(self.buffer.len()), + Affinity::Downstream, + ) + }; + } + } else { + self.replace_selection(font_cx, layout_cx, ""); + } + } + PlainEditorOp::DeleteWord => { + let start = self.selection.insertion_index(); + if self.selection.is_collapsed() { + let end = self + .selection + .focus() + .next_word(&self.layout) + .text_range() + .end; + + self.buffer.replace_range(start..end, ""); + self.update_layout(font_cx, layout_cx); + let (start, affinity) = if start > 0 { + (start - 1, Affinity::Upstream) + } else { + (start, Affinity::Downstream) + }; + self.selection = Selection::from_index(&self.layout, start, affinity); + } else { + self.replace_selection(font_cx, layout_cx, ""); + } + } + PlainEditorOp::Backdelete => { + let end = self.selection.focus().text_range().start; + if self.selection.is_collapsed() { + if let Some(start) = self + .selection + .focus() + .cluster_path() + .cluster(&self.layout) + .map(|x| { + if self.selection.focus().affinity() == Affinity::Upstream { + Some(x) + } else { + x.previous_logical() + } + }) + .and_then(|c| c.map(|x| x.text_range().start)) + { + self.buffer.replace_range(start..end, ""); + self.update_layout(font_cx, layout_cx); + let (start, affinity) = if start > 0 { + (start - 1, Affinity::Upstream) + } else { + (start, Affinity::Downstream) + }; + self.selection = Selection::from_index(&self.layout, start, affinity); + } + } else { + self.replace_selection(font_cx, layout_cx, ""); + } + } + PlainEditorOp::BackdeleteWord => { + let end = self.selection.focus().text_range().start; + if self.selection.is_collapsed() { + let start = self + .selection + .focus() + .previous_word(&self.layout) + .text_range() + .start; + + self.buffer.replace_range(start..end, ""); + self.update_layout(font_cx, layout_cx); + let (start, affinity) = if start > 0 { + (start - 1, Affinity::Upstream) + } else { + (start, Affinity::Downstream) + }; + self.selection = Selection::from_index(&self.layout, start, affinity); + } else { + self.replace_selection(font_cx, layout_cx, ""); + } + } + PlainEditorOp::InsertOrReplaceSelection(s) => { + self.replace_selection(font_cx, layout_cx, &s); + } + PlainEditorOp::MoveToPoint(x, y) => { + self.selection = Selection::from_point(&self.layout, x, y); + } + PlainEditorOp::MoveToTextStart => { + self.selection = self.selection.move_lines(&self.layout, isize::MIN, false); + } + PlainEditorOp::MoveToLineStart => { + self.selection = self.selection.line_start(&self.layout, false); + } + PlainEditorOp::MoveToTextEnd => { + self.selection = self.selection.move_lines(&self.layout, isize::MAX, false); + } + PlainEditorOp::MoveToLineEnd => { + self.selection = self.selection.line_end(&self.layout, false); + } + PlainEditorOp::MoveUp => { + self.selection = self.selection.previous_line(&self.layout, false); + } + PlainEditorOp::MoveDown => { + self.selection = self.selection.next_line(&self.layout, false); + } + PlainEditorOp::MoveLeft => { + self.selection = + self.selection + .previous_visual(&self.layout, self.cursor_mode, false); + } + PlainEditorOp::MoveRight => { + self.selection = + self.selection + .next_visual(&self.layout, self.cursor_mode, false); + } + PlainEditorOp::MoveWordLeft => { + self.selection = self.selection.previous_word(&self.layout, false); + } + PlainEditorOp::MoveWordRight => { + self.selection = self.selection.next_word(&self.layout, false); + } + PlainEditorOp::SelectAll => { + self.selection = + Selection::from_index(&self.layout, 0usize, Affinity::default()) + .move_lines(&self.layout, isize::MAX, true); + } + PlainEditorOp::SelectToTextStart => { + self.selection = self.selection.move_lines(&self.layout, isize::MIN, true); + } + PlainEditorOp::SelectToLineStart => { + self.selection = self.selection.line_start(&self.layout, true); + } + PlainEditorOp::SelectToTextEnd => { + self.selection = self.selection.move_lines(&self.layout, isize::MAX, true); + } + PlainEditorOp::SelectToLineEnd => { + self.selection = self.selection.line_end(&self.layout, true); + } + PlainEditorOp::SelectUp => { + self.selection = self.selection.previous_line(&self.layout, true); + } + PlainEditorOp::SelectDown => { + self.selection = self.selection.next_line(&self.layout, true); + } + PlainEditorOp::SelectLeft => { + self.selection = + self.selection + .previous_visual(&self.layout, self.cursor_mode, true); + } + PlainEditorOp::SelectRight => { + self.selection = + self.selection + .next_visual(&self.layout, self.cursor_mode, true); + } + PlainEditorOp::SelectWordLeft => { + self.selection = self.selection.previous_word(&self.layout, true); + } + PlainEditorOp::SelectWordRight => { + self.selection = self.selection.next_word(&self.layout, true); + } + PlainEditorOp::SelectWordAtPoint(x, y) => { + self.selection = Selection::word_from_point(&self.layout, x, y); + } + PlainEditorOp::SelectLineAtPoint(x, y) => { + let focus = *Selection::from_point(&self.layout, x, y) + .line_start(&self.layout, true) + .focus(); + self.selection = Selection::from(focus).line_end(&self.layout, true); + } + PlainEditorOp::ExtendSelectionToPoint(x, y) => { + // FIXME: This is usually the wrong way to handle selection extension for mouse moves, but not a regression. + self.selection = self.selection.extend_to_point(&self.layout, x, y); + } + } + } + + if layout_after { + self.update_layout(font_cx, layout_cx); + } + } + + 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_start = start.saturating_add(s.len()); + self.selection = if new_start == self.buffer.len() { + Selection::from_index( + &self.layout, + new_start.saturating_sub(1), + Affinity::Upstream, + ) + } else { + Selection::from_index( + &self.layout, + new_start.min(self.buffer.len()), + Affinity::Downstream, + ) + }; + } + + /// Get either the contents of the current selection, or the text of the cluster at the caret + pub fn active_text(&self) -> ActiveText { + if self.selection.is_collapsed() { + let range = self + .selection + .focus() + .cluster_path() + .cluster(&self.layout) + .map(|c| c.text_range()) + .unwrap_or_default(); + ActiveText::FocusedCluster(self.selection.focus().affinity(), &self.buffer[range]) + } else { + ActiveText::Selection(&self.buffer[self.selection.text_range()]) + } + } + + /// Get rectangles representing the selected portions of text + pub fn selection_geometry(&self) -> Vec { + self.selection.geometry(&self.layout) + } + + /// Get a rectangle representing the current caret cursor position + pub fn selection_strong_geometry(&self, size: f32) -> Option { + self.selection.focus().strong_geometry(&self.layout, size) + } + + pub fn selection_weak_geometry(&self, size: f32) -> Option { + self.selection.focus().weak_geometry(&self.layout, size) + } + + /// Get the lines from the `Layout` + pub fn lines(&self) -> impl Iterator> + '_ + Clone { + self.layout.lines() + } + + /// Get a copy of the text content of the buffer + pub fn text(&self) -> Arc { + self.buffer.clone().into() + } + + /// 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() { + builder.push_default(prop); + } + builder.build_into(&mut self.layout, &self.buffer); + self.layout.break_all_lines(Some(self.width)); + self.layout.align(Some(self.width), Alignment::Start); + self.selection = self.selection.refresh(&self.layout); + } +} diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 0f3fc7d0..f3547996 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -12,6 +12,11 @@ pub(crate) mod data; pub mod cursor; +// TODO: make editor `no_std` capable. +// `std` required only because of `RangedEditor::build_into`. +#[cfg(feature = "std")] +pub mod editor; + use self::alignment::align; use super::style::Brush; diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 40f1585d..d5f59190 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -97,6 +97,7 @@ mod util; pub mod layout; pub mod style; +pub use peniko::kurbo::Rect; pub use peniko::Font; pub use builder::{RangedBuilder, TreeBuilder}; @@ -106,5 +107,8 @@ pub use inline_box::InlineBox; #[doc(inline)] pub use layout::Layout; +#[cfg(feature = "std")] +pub use layout::editor::{PlainEditor, PlainEditorOp}; + pub use layout::*; pub use style::*;