Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ This release has an [MSRV] of 1.88.

- `FallbackKey` now implements `From<Script>` for convenience. ([#594][] by [@xStrom][])

#### Parley

- `PlainEditorDriver` now exposes UTF-8 document-range editing helpers for host-side text input and composition handling, plus delete-to-edge helpers for semantic edit-command integration.
- `PlainEditorDriver::commit_composition` to explicitly commit an active composition.
- `SplitString::to_utf8_range` and `TextIndexEncoding` to convert encoded offsets over visible editor text without allocating.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should have the PR number and author attribution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


### Changed

#### Parley

- Breaking change: removed legacy `PlainEditorDriver` composition mutation methods `set_compose`, `set_compose_byte_range`, `clear_compose`, and `finish_compose` in favor of the document-space editing API.
- Breaking change: `PlainEditor::raw_compose` has been replaced by `PlainEditor::composition`, which exposes composing text and its document offset instead of raw-buffer byte ranges.
- Breaking change: `PlainEditor::ime_cursor_area` has been renamed to `PlainEditor::text_input_area`.

## [0.8.0] - 2026-03-27

This release has an [MSRV] of 1.88.
Expand Down
6 changes: 2 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions examples/vello_editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ publish = false
vello = "0.8.0"
anyhow = "1.0.102"
pollster = "0.4.0"
ui-events-winit = "0.3"
ui-events = "0.3"
ui-events = { git = "https://github.com/endoli/ui-events.git", rev = "5e840f0e39b412573aaec8debadd4be81c17d823", package = "ui-events", default-features = false, features = [
"kurbo",
] }
ui-events-winit = { git = "https://github.com/endoli/ui-events.git", rev = "5e840f0e39b412573aaec8debadd4be81c17d823", package = "ui-events-winit", default-features = false }
winit = "0.30.13"
parley = { workspace = true, default-features = true, features = ["accesskit"] }
peniko = { workspace = true }
Expand Down
15 changes: 9 additions & 6 deletions examples/vello_editor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ struct SimpleVelloApp<'s> {
/// The last generation of the editor layout that we drew.
last_drawn_generation: text::Generation,

/// The IME cursor area we last sent to the platform.
last_sent_ime_cursor_area: parley::BoundingBox,
/// The text input area we last sent to the platform.
last_sent_text_input_area: parley::BoundingBox,

/// The event loop proxy required by the AccessKit winit adapter.
event_loop_proxy: EventLoopProxy<accesskit_winit::Event>,
Expand Down Expand Up @@ -251,6 +251,9 @@ impl ApplicationHandler<accesskit_winit::Event> for SimpleVelloApp<'_> {
WindowEventTranslation::Pointer(p) => {
self.editor.handle_pointer_event(&p);
}
WindowEventTranslation::Text(t) => {
self.editor.handle_text_input_event(&t);
}
}
} else {
self.editor.handle_event(event.clone());
Expand All @@ -259,9 +262,9 @@ impl ApplicationHandler<accesskit_winit::Event> for SimpleVelloApp<'_> {

if self.last_drawn_generation != self.editor.generation() {
render_state.window.request_redraw();
let area = self.editor.editor().ime_cursor_area();
if self.last_sent_ime_cursor_area != area {
self.last_sent_ime_cursor_area = area;
let area = self.editor.editor().text_input_area();
if self.last_sent_text_input_area != area {
self.last_sent_text_input_area = area;
// Note: on X11 `set_ime_cursor_area` may cause the exclusion area to be obscured
// until https://github.com/rust-windowing/winit/pull/3966 is in the Winit release
// used by this example.
Expand Down Expand Up @@ -414,7 +417,7 @@ fn main() -> Result<()> {
scene: Scene::new(),
editor: text::Editor::new(text::LOREM),
last_drawn_generation: text::Generation::default(),
last_sent_ime_cursor_area: parley::BoundingBox::new(f64::NAN, f64::NAN, f64::NAN, f64::NAN),
last_sent_text_input_area: parley::BoundingBox::new(f64::NAN, f64::NAN, f64::NAN, f64::NAN),
event_loop_proxy: event_loop.create_proxy(),
event_reducer: WindowEventReducer::default(),
};
Expand Down
102 changes: 86 additions & 16 deletions examples/vello_editor/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@ use web_time::Instant;

use accesskit::{Node, TextDecoration, TextDecorationStyle, TreeUpdate};
use core::default::Default;
use parley::editing::SplitString;
use parley::editing::{SplitString, TextIndexEncoding};
use parley::layout::{PositionedLayoutItem, Style};
use parley::{GenericFamily, StyleProperty};
use std::ops::Range;
use std::time::Duration;
use ui_events::pointer::PointerButton;
use ui_events::{
keyboard::{Key, KeyboardEvent, NamedKey},
pointer::{PointerButtonEvent, PointerEvent, PointerInfo, PointerState, PointerType},
text::{TextInputAction, TextInputEvent, TextRange, TextRangeEncoding, TextTargetRange},
};
use vello::{
Scene,
kurbo::{Affine, Line, Stroke},
peniko::color::{AlphaColor, Srgb, palette},
peniko::{Brush, Fill},
};
use winit::event::{Ime, WindowEvent};
use winit::event::WindowEvent;

pub use parley::editing::Generation;
use parley::{FontContext, LayoutContext, PlainEditor, PlainEditorDriver};
Expand Down Expand Up @@ -334,26 +336,72 @@ impl Editor {
}
}

pub fn handle_event(&mut self, event: WindowEvent) {
/// Handle normalized text input events surfaced by `ui-events-winit`.
///
/// On the current `winit` backend this example mostly exercises the
/// preedit/commit IME path. Richer platform-originated text events such as
/// selection updates, composing-region updates, or non-UTF-8 surrounding
/// deletes are still handled here for parity with Parley's host-facing API,
/// but they are not expected to originate from `winit` today.
pub fn handle_text_input_event(&mut self, event: &TextInputEvent) {
self.cursor_reset();
match event {
WindowEvent::Resized(size) => {
self.editor
.set_width(Some(size.width as f32 - 2_f32 * INSET));
TextInputEvent::Insert(insert) => {
let replacement = self.document_range_to_byte_range(insert.replacement_range);
let mut drv = self.driver();
let applied = drv.insert_or_replace(&insert.text, replacement, None);
debug_assert!(applied, "invalid insert range from text input event");
}
WindowEvent::Ime(Ime::Disabled) => {
self.driver().clear_compose();
TextInputEvent::DeleteBackward => self.driver().backdelete(),
TextInputEvent::DeleteForward => self.driver().delete(),
TextInputEvent::DeleteSurrounding(delete)
if delete.encoding == TextRangeEncoding::Utf8Bytes =>
{
let before = usize::try_from(delete.before_length).ok();
let after = usize::try_from(delete.after_length).ok();
if let (Some(before), Some(after)) = (before, after) {
let applied = self.driver().delete_surrounding(before, after);
debug_assert!(
applied,
"invalid surrounding delete range from text input event",
);
}
}
WindowEvent::Ime(Ime::Commit(text)) => {
self.driver().insert_or_replace_selection(&text);
TextInputEvent::SetSelection(range) => {
if let Some(range) = self.document_range_to_byte_range(Some(*range)) {
let applied = self.driver().set_document_selection(range);
debug_assert!(applied, "invalid document selection from text input event");
}
}
WindowEvent::Ime(Ime::Preedit(text, cursor)) => {
if text.is_empty() {
self.driver().clear_compose();
} else {
self.driver().set_compose(&text, cursor);
TextInputEvent::SetComposingRegion(range) => {
if let Some(range) = self.document_range_to_byte_range(Some(*range)) {
let applied = self.driver().set_composing_region(range);
debug_assert!(applied, "invalid composing region from text input event");
}
}
_ => {}
TextInputEvent::CompositionUpdate(state) => {
let replacement = self.document_range_to_byte_range(state.replacement_range);
let mut drv = self.driver();
let applied = drv.update_composition(
&state.text,
replacement,
Self::text_range_to_byte_range(state.selection),
);
debug_assert!(applied, "invalid composition update from text input event");
}
TextInputEvent::CompositionEnd => {
let _ = self.driver().clear_composition();
}
TextInputEvent::Action(TextInputAction::Newline) => self.driver().insert_newline(),
TextInputEvent::Action(_) => {}
TextInputEvent::DeleteSurrounding(_) => {}
}
}

pub fn handle_event(&mut self, event: WindowEvent) {
if let WindowEvent::Resized(size) = event {
self.editor
.set_width(Some(size.width as f32 - 2_f32 * INSET));
}
}

Expand All @@ -370,6 +418,28 @@ impl Editor {
self.editor.generation()
}

fn document_range_to_byte_range(&self, range: Option<TextTargetRange>) -> Option<Range<usize>> {
range.and_then(|range| {
self.text().to_utf8_range(
range.range.start,
range.range.end,
match range.encoding {
TextRangeEncoding::Utf8Bytes => TextIndexEncoding::Utf8Bytes,
TextRangeEncoding::Utf16CodeUnits => TextIndexEncoding::Utf16CodeUnits,
TextRangeEncoding::UnicodeCodePoints => TextIndexEncoding::UnicodeCodePoints,
},
)
})
}

fn text_range_to_byte_range(range: Option<TextRange>) -> Option<Range<usize>> {
range.and_then(|range| {
let start = usize::try_from(range.start).ok()?;
let end = usize::try_from(range.end).ok()?;
Some(start..end)
})
}

/// Draw into scene.
///
/// Returns drawn `Generation`.
Expand Down
Loading
Loading