diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b9ca8c4..504f0ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ This release has an [MSRV] of 1.75. - `Generation` on `PlainEditor` to help implement lazy drawing. ([#143] by [@xorgy]) - Support for preedit for input methods in `PlainEditor` ([#192][], [#198][] by [@tomcur][]) +- `PlainEditor` method to get a cursor area for use by the platform's input method ([#224][] by [@tomcur][]) ### Changed @@ -113,6 +114,7 @@ This release has an [MSRV] of 1.70. [#198]: https://github.com/linebender/parley/pull/198 [#211]: https://github.com/linebender/parley/pull/211 [#223]: https://github.com/linebender/parley/pull/223 +[#224]: https://github.com/linebender/parley/pull/224 [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/main.rs b/examples/vello_editor/src/main.rs index 60e6c448..6ff17797 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -8,16 +8,17 @@ #![allow(clippy::shadow_unrelated)] #![allow(clippy::unseparated_literal_suffix)] -use accesskit::{Node, Rect, Role, Tree, TreeUpdate}; +use accesskit::{Node, Role, Tree, TreeUpdate}; use anyhow::Result; use std::num::NonZeroUsize; use std::sync::Arc; +use vello::kurbo; use vello::peniko::Color; use vello::util::{RenderContext, RenderSurface}; use vello::wgpu; use vello::{AaConfig, Renderer, RendererOptions, Scene}; use winit::application::ApplicationHandler; -use winit::dpi::LogicalSize; +use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize}; use winit::event::{StartCause, WindowEvent}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy}; use winit::window::Window; @@ -55,7 +56,7 @@ impl ActiveRenderState<'_> { } let mut node = Node::new(Role::TextInput); let size = self.window.inner_size(); - node.set_bounds(Rect { + node.set_bounds(accesskit::Rect { x0: 0.0, y0: 0.0, x1: size.width as _, @@ -94,6 +95,9 @@ 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: kurbo::Rect, + /// The event loop proxy required by the AccessKit winit adapter. event_loop_proxy: EventLoopProxy, } @@ -215,6 +219,20 @@ impl ApplicationHandler for SimpleVelloApp<'_> { self.editor.handle_event(event.clone()); 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; + // 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. + render_state.window.set_ime_cursor_area( + PhysicalPosition::new( + area.x0 + text::INSET as f64, + area.y0 + text::INSET as f64, + ), + PhysicalSize::new(area.width(), area.height()), + ); + } } // render_state // .window @@ -340,6 +358,7 @@ fn main() -> Result<()> { scene: Scene::new(), editor: text::Editor::new(text::LOREM), last_drawn_generation: Default::default(), + last_sent_ime_cursor_area: kurbo::Rect::new(f64::NAN, f64::NAN, f64::NAN, f64::NAN), event_loop_proxy: event_loop.create_proxy(), }; diff --git a/parley/src/layout/editor.rs b/parley/src/layout/editor.rs index 45fbbe86..403dc836 100644 --- a/parley/src/layout/editor.rs +++ b/parley/src/layout/editor.rs @@ -8,6 +8,7 @@ use crate::{ cursor::{Cursor, Selection}, Affinity, Alignment, Layout, }, + resolve::ResolvedStyle, style::Brush, FontContext, LayoutContext, Rect, StyleProperty, StyleSet, }; @@ -770,6 +771,61 @@ where .then(|| self.selection.focus().geometry(&self.layout, size)) } + /// Get a rectangle bounding the text the user is currently editing. + /// + /// This is useful for suggesting an exclusion area to the platform for, e.g., IME candidate + /// box placement. This bounds the area of the preedit text if present, otherwise it bounds the + /// selection on the focused line. + pub fn ime_cursor_area(&self) -> Rect { + let (area, focus) = if let Some(preedit_range) = &self.compose { + let selection = Selection::new( + self.cursor_at(preedit_range.start), + self.cursor_at(preedit_range.end), + ); + + // Bound the entire preedit text. + let mut area = None; + selection.geometry_with(&self.layout, |rect| { + let area = area.get_or_insert(rect); + *area = area.union(rect); + }); + + ( + area.unwrap_or_else(|| selection.focus().geometry(&self.layout, 0.)), + selection.focus(), + ) + } else { + // Bound the selected parts of the focused line only. + let focus = self.selection.focus().geometry(&self.layout, 0.); + let mut area = focus; + self.selection.geometry_with(&self.layout, |rect| { + if rect.y0 == focus.y0 { + area = area.union(rect); + } + }); + + (area, self.selection.focus()) + }; + + // Ensure some context is captured even for tiny or collapsed selections by including a + // region surrounding the selection. Doing this unconditionally, the IME candidate box + // usually does not need to jump around when composing starts or the preedit is added to. + let [upstream, downstream] = focus.logical_clusters(&self.layout); + let font_size = downstream + .or(upstream) + .map(|cluster| cluster.run().font_size()) + .unwrap_or(ResolvedStyle::::default().font_size); + // Using 0.6 as an estimate of the average advance + let inflate = 3. * 0.6 * font_size as f64; + let editor_width = self.width.map(f64::from).unwrap_or(f64::INFINITY); + Rect { + x0: (area.x0 - inflate).max(0.), + x1: (area.x1 + inflate).min(editor_width), + y0: area.y0, + y1: area.y1, + } + } + /// Borrow the text content of the buffer. /// /// The return value is a `SplitString` because it