Skip to content

Commit

Permalink
Add IME support to the editor
Browse files Browse the repository at this point in the history
Remove workaround for #186

Use `unchecked` name

Fix doc comment

Restore debug code

Remove comment from Masonry

Co-Authored-By: Tom Churchman <[email protected]>
  • Loading branch information
DJMcNab and tomcur committed Dec 4, 2024
1 parent 5d748f6 commit 23a768b
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 21 deletions.
4 changes: 3 additions & 1 deletion examples/vello_editor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ impl ApplicationHandler<accesskit_winit::Event> 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();

Expand Down Expand Up @@ -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(())
}

Expand Down
95 changes: 87 additions & 8 deletions examples/vello_editor/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down Expand Up @@ -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(),
Expand All @@ -65,7 +69,7 @@ impl Editor {
&mut self.editor
}

pub fn text(&self) -> &str {
pub fn text(&self) -> [&str; 2] {
self.editor.text()
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -257,7 +261,7 @@ impl Editor {
}
WindowEvent::Touch(Touch {
phase, location, ..
}) => {
}) if !self.editor.is_composing() => {
use winit::event::TouchPhase::*;
match phase {
Started => {
Expand Down Expand Up @@ -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 {
Expand All @@ -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));
}
}
_ => {}
}
}
Expand Down Expand Up @@ -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();
Expand All @@ -371,7 +421,7 @@ impl Editor {
.collect::<Vec<_>>();
scene
.draw_glyphs(font)
.brush(Color::WHITE)
.brush(style.brush)
.hint(true)
.transform(transform)
.glyph_transform(glyph_xform)
Expand All @@ -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()
Expand Down
61 changes: 49 additions & 12 deletions parley/src/layout/editor.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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.
Expand All @@ -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<f32>) {
// 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;
}

Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 23a768b

Please sign in to comment.