Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 19 additions & 2 deletions packages/blitz-dom/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3362,12 +3362,29 @@ mod ime_focus_tests {
assert!(shell.areas.lock().unwrap().is_empty());

doc.resolve(0.0);
assert_eq!(*shell.areas.lock().unwrap(), vec![(0.0, 0.0, 100.0, 20.0)]);
// The area is the caret rect: at the start of the (empty) input, caret-width wide,
// vertically centred within the 20px content box.
let (x, y, w, h) = shell.areas.lock().unwrap()[0];
assert_eq!(x, 0.0);
assert!(w > 0.0 && w < 5.0, "caret width {w}");
assert!(h > 0.0 && h <= 20.0, "caret height {h}");
assert!(y >= 0.0 && y + h <= 20.0, "caret y {y} height {h}");
assert_eq!(shell.areas.lock().unwrap().len(), 1);

// Unchanged layout does not re-report the area
// Unchanged caret does not re-report the area
doc.resolve(0.0);
assert_eq!(shell.areas.lock().unwrap().len(), 1);

// Typing moves the caret, so the area is re-reported further right
doc.with_text_input(input, |mut driver| {
driver.insert_or_replace_selection("abc")
});
doc.resolve(0.0);
let areas = shell.areas.lock().unwrap();
assert_eq!(areas.len(), 2);
assert!(areas[1].0 > x, "caret should move right: {:?}", areas[1]);
drop(areas);

doc.clear_focus();
assert_eq!(*shell.enabled.lock().unwrap(), vec![true, false]);
}
Expand Down
43 changes: 35 additions & 8 deletions packages/blitz-dom/src/node/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,18 +680,45 @@ impl Node {
self.element_data().is_some_and(|el| el.is_text_input())
}

/// The node's content box as `(x, y, width, height)` in CSS pixels, for use as the IME
/// cursor area. Returns `None` if the node is not a text input or has not been laid out
/// yet (e.g. it was focussed before the first layout).
/// The IME cursor area as `(x, y, width, height)` in CSS pixels relative to the viewport:
/// the caret rectangle of the text input (so IME candidate windows appear next to the
/// caret), falling back to the input's content box if the caret geometry is unavailable.
/// Returns `None` if the node is not a text input or has not been laid out yet (e.g. it
/// was focussed before the first layout).
pub fn ime_cursor_area(&self) -> Option<(f32, f32, f32, f32)> {
self.element_data()?.text_input_data()?;
let input_data = self.element_data()?.text_input_data()?;
let layout = self.final_layout();
let pos = self.absolute_position(0.0, 0.0);
let content_x = pos.x + layout.content_box_x();
let content_y = pos.y + layout.content_box_y();

let caret = input_data
.editor
.try_layout()
.map(|text_layout| text_layout.scale())
.zip(input_data.editor.cursor_geometry(1.5));
let Some((scale, caret)) = caret else {
return Some((
content_x,
content_y,
layout.content_box_width(),
layout.content_box_height(),
));
};

// Caret geometry is in scaled (device) pixels relative to the text content; convert
// to CSS pixels and apply the same centering/scroll offsets used when painting.
let (scroll_x, scroll_y) = if input_data.is_multiline {
(0.0, input_data.scroll_offset)
} else {
(input_data.scroll_offset, 0.0)
};
let y_offset = self.text_input_v_centering_offset(scale as f64) as f32;
Some((
pos.x + layout.content_box_x(),
pos.y + layout.content_box_y(),
layout.content_box_width(),
layout.content_box_height(),
content_x + caret.x0 as f32 / scale - scroll_x,
content_y + y_offset + caret.y0 as f32 / scale - scroll_y,
(caret.x1 - caret.x0) as f32 / scale,
(caret.y1 - caret.y0) as f32 / scale,
))
}

Expand Down
25 changes: 18 additions & 7 deletions packages/blitz-shell/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub use crate::net::DataUriNetProvider;
))]
use blitz_traits::shell::FileDialogFilter;
use blitz_traits::shell::ShellProvider;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use winit::cursor::{Cursor, CursorIcon};
use winit::dpi::{LogicalPosition, LogicalSize};
pub use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy};
Expand Down Expand Up @@ -87,10 +87,17 @@ pub fn current_android_app() -> android_activity::AndroidApp {
pub struct BlitzShellProvider {
window: Arc<dyn Window>,
proxy: BlitzShellProxy,
/// The most recently reported IME cursor area (logical position and size). Winit requires
/// an initial cursor area when enabling the IME with the `cursor_area` capability.
ime_cursor_area: Mutex<(LogicalPosition<f32>, LogicalSize<f32>)>,
}
impl BlitzShellProvider {
pub fn new(window: Arc<dyn Window>, proxy: BlitzShellProxy) -> Self {
Self { window, proxy }
Self {
window,
proxy,
ime_cursor_area: Mutex::new(Default::default()),
}
}
}

Expand All @@ -115,19 +122,23 @@ impl ShellProvider for BlitzShellProvider {
}
fn set_ime_enabled(&self, is_enabled: bool) {
if is_enabled {
let (position, size) = *self.ime_cursor_area.lock().unwrap();
let request_data =
ImeRequestData::default().with_cursor_area(position.into(), size.into());
let _ = self.window.request_ime_update(ImeRequest::Enable(
ImeEnableRequest::new(ImeCapabilities::new(), ImeRequestData::default()).unwrap(),
ImeEnableRequest::new(ImeCapabilities::new().with_cursor_area(), request_data)
.unwrap(),
));
} else {
let _ = self.window.request_ime_update(ImeRequest::Disable);
}
}
fn set_ime_cursor_area(&self, x: f32, y: f32, width: f32, height: f32) {
let position = LogicalPosition::new(x, y);
let size = LogicalSize::new(width, height);
*self.ime_cursor_area.lock().unwrap() = (position, size);
let _ = self.window.request_ime_update(ImeRequest::Update(
ImeRequestData::default().with_cursor_area(
LogicalPosition::new(x, y).into(),
LogicalSize::new(width, height).into(),
),
ImeRequestData::default().with_cursor_area(position.into(), size.into()),
));
}

Expand Down
Loading