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,