use crate::core::runtime::{FocusId, LayoutNode, NodeContent, find_focus, first_text};
use crate::geometry::Rect;
use crate::platform::ImeState;
use crate::render::{Color, Font, Scene};
fn entry_leaf(tree: &LayoutNode, focused: FocusId) -> Option<&LayoutNode> {
let node = find_focus(tree, focused)?;
let entry = node
.semantics
.as_ref()
.and_then(|s| s.role)
.is_none_or(|r| {
matches!(
r,
crate::core::a11y::Role::TextField | crate::core::a11y::Role::SpinButton
)
});
if node.action.is_some() || !entry {
return None;
}
let leaf = first_text(node)?;
leaf.caret.is_some().then_some(leaf)
}
fn position(leaf: &LayoutNode, font: Option<&Font>, i: usize) -> Option<(f64, f64, f64)> {
let NodeContent::Text { text, size, .. } = &leaf.content else {
return None;
};
let line_h = font.map(|f| f.line_height(*size)).unwrap_or(*size);
let mut i = i.min(text.len());
while !text.is_char_boundary(i) {
i -= 1;
}
let ls = text[..i].rfind('\n').map_or(0, |n| n + 1);
let line = text[..ls].bytes().filter(|&b| b == b'\n').count();
let w = font.map_or(0.0, |f| f.measure(&text[ls..i], *size).width);
let b = leaf.bounds;
let x = (b.min_x() + w).min(b.max_x().max(b.min_x()));
Some((x, b.min_y() + line as f64 * line_h, line_h))
}
pub fn paint_preedit(
tree: &LayoutNode,
focused: FocusId,
scene: &mut Scene,
font: Option<&Font>,
color: Color,
) {
let Some(leaf) = entry_leaf(tree, focused) else {
return;
};
let (Some((s, e)), NodeContent::Text { text, .. }) = (leaf.preedit, &leaf.content) else {
return;
};
let e = e.min(text.len());
let mut start = s.min(e);
while start < e {
let end = text[start..e].find('\n').map_or(e, |n| start + n);
if let (Some((x0, y, h)), Some((x1, _, _))) =
(position(leaf, font, start), position(leaf, font, end))
&& x1 > x0
{
scene.fill_rect(Rect::from_xywh(x0, y + h - 2.0, x1 - x0, 1.5), color);
}
start = end + 1;
}
}
pub fn ime_state(
tree: &LayoutNode,
focused: Option<FocusId>,
font: Option<&Font>,
) -> Option<ImeState> {
let leaf = entry_leaf(tree, focused?)?;
let NodeContent::Text { text, .. } = &leaf.content else {
return None;
};
let caret = leaf.caret.unwrap_or(text.len()).min(text.len());
let at = leaf.preedit.map_or(caret, |(s, _)| s);
let (x, y, h) = position(leaf, font, at)?;
let (surrounding, cursor, anchor) = match leaf.preedit {
Some((s, e)) if e <= text.len() && s <= e => {
let mut t = text[..s].to_string();
t.push_str(&text[e..]);
(t, s, s)
}
_ => {
let anchor = match leaf.selection {
Some((a, b)) if b == caret => a,
Some((a, b)) if a == caret => b,
_ => caret,
};
(text.clone(), caret, anchor)
}
};
Some(ImeState {
cursor_area: Rect::from_xywh(x, y, 2.0, h),
surrounding,
cursor,
anchor,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Cx, Element, layout};
use crate::style::Theme;
use crate::widgets::{EditBuffer, text_editor};
fn tree_for(buffer: &EditBuffer) -> (LayoutNode, FocusId) {
let theme = Theme::light();
let mut cx: Cx<EditBuffer> = Cx::new(&theme);
let el: Element = text_editor(&mut cx, &theme, buffer, |s: &mut EditBuffer| s);
let tree = layout(&el, Rect::from_xywh(0.0, 0.0, 400.0, 100.0), None);
let id = tree_focus(&tree).expect("the field is focusable");
(tree, id)
}
fn tree_focus(node: &LayoutNode) -> Option<FocusId> {
node.focus
.or_else(|| node.children.iter().find_map(tree_focus))
}
#[test]
fn ime_state_reports_text_without_the_preedit() {
let mut b = EditBuffer::from_text("abc");
b.place_caret(1);
b.set_preedit("xy", Some((1, 1)));
let (tree, id) = tree_for(&b);
let leaf = entry_leaf(&tree, id).unwrap();
assert_eq!(leaf.preedit, Some((1, 3)));
let state = ime_state(&tree, Some(id), None).unwrap();
assert_eq!(state.surrounding, "abc");
assert_eq!((state.cursor, state.anchor), (1, 1));
assert!(state.cursor_area.min_x() >= leaf.bounds.min_x());
assert_eq!(ime_state(&tree, None, None), None);
}
#[test]
fn ime_state_reports_the_selection_anchor() {
let mut b = EditBuffer::from_text("hello");
b.place_caret(1);
b.extend_to(4);
let (tree, id) = tree_for(&b);
let state = ime_state(&tree, Some(id), None).unwrap();
assert_eq!((state.cursor, state.anchor), (4, 1));
}
}