use std::sync::Arc;
use slate_reactive::Signal;
use slate_text::byte_at_pixel_x;
use crate::event::{
self, ElementImeCommitHandler, ElementImePreeditHandler, ElementKeyHandler,
ElementTextInputHandler, EventCtx, ImeCommitEvent, ImePreeditEvent, Key, KeyEvent, MouseEvent,
MouseHandler, NamedKey, TextInputEvent,
};
use crate::ime::{ImeState, Preedit};
use crate::elements::text_edit::grapheme::{
insert_text_at, next_grapheme_boundary, prev_grapheme_boundary,
};
use crate::elements::text_edit::ops::{
MotionDir, apply_motion, apply_visual_edge, apply_visual_motion, delete_selection, record_edit,
reset_blink,
};
use crate::elements::text_edit::shortcuts;
use crate::elements::text_edit::undo::EditOp;
pub(super) fn build_key_down_handler(value: Signal<String>) -> ElementKeyHandler {
Arc::new(move |ev: &KeyEvent, cx: &mut EventCtx| {
let id = match cx.element_id() {
Some(i) => i,
None => return,
};
let state_rc = match cx.ime_state(id) {
Some(s) => s,
None => return,
};
if shortcuts::handle_command_shortcut(ev, cx, &state_rc, &value, false) {
return;
}
{
let state = state_rc.borrow();
if state.preedit.is_some() {
return;
}
}
let shift = ev.modifiers.shift;
let new_text: Option<String> = match &ev.key {
Key::Named(NamedKey::Backspace) => {
let mut state = state_rc.borrow_mut();
debug_assert!(
state.text.is_char_boundary(state.caret),
"TextField caret not on char boundary"
);
state.caret_affinity = slate_text::Affinity::Downstream;
if state.selection_anchor.is_some_and(|a| a != state.caret) {
delete_selection(&mut state);
record_edit(&mut state, EditOp::Discrete);
reset_blink(&mut state);
cx.stop_propagation();
Some(state.text.clone())
} else {
state.selection_anchor = None;
let old_caret = state.caret;
let new_caret = prev_grapheme_boundary(&state.text, old_caret);
if new_caret < old_caret {
state.text.replace_range(new_caret..old_caret, "");
state.caret = new_caret;
record_edit(&mut state, EditOp::Backspace);
reset_blink(&mut state);
cx.stop_propagation();
Some(state.text.clone())
} else {
None
}
}
}
Key::Named(NamedKey::ArrowLeft) | Key::Named(NamedKey::ArrowRight) => {
let move_right = matches!(ev.key, Key::Named(NamedKey::ArrowRight));
if event::is_line_edge_modifier(&ev.modifiers) {
let shaped = state_rc.borrow().last_shaped.clone();
{
let mut state = state_rc.borrow_mut();
apply_line_edge(&mut state, shaped.as_deref(), move_right, shift);
reset_blink(&mut state);
state.undo.mark_motion();
}
cx.stop_propagation();
return;
}
let shaped = state_rc.borrow().last_shaped.clone();
{
let mut state = state_rc.borrow_mut();
let run_bearing = shaped.as_ref().is_some_and(|s| !s.runs.is_empty());
if run_bearing {
apply_visual_motion(
&mut state,
shaped.as_ref().unwrap(),
move_right,
shift,
);
} else {
state.caret_affinity = slate_text::Affinity::Downstream;
let dir = if move_right {
MotionDir::Right
} else {
MotionDir::Left
};
apply_motion(&mut state, dir, shift, |s| {
s.caret = if move_right {
next_grapheme_boundary(&s.text, s.caret)
} else {
prev_grapheme_boundary(&s.text, s.caret)
};
});
}
reset_blink(&mut state);
state.undo.mark_motion();
}
cx.stop_propagation();
None
}
Key::Named(NamedKey::Home) | Key::Named(NamedKey::End) => {
let to_end = matches!(ev.key, Key::Named(NamedKey::End));
let shaped = state_rc.borrow().last_shaped.clone();
{
let mut state = state_rc.borrow_mut();
apply_line_edge(&mut state, shaped.as_deref(), to_end, shift);
reset_blink(&mut state);
state.undo.mark_motion();
}
cx.stop_propagation();
None
}
_ => None,
};
if let Some(t) = new_text {
value.set(t);
}
})
}
fn apply_line_edge(
state: &mut ImeState,
shaped: Option<&slate_text::ShapedLine>,
to_end: bool,
shift: bool,
) {
let handled = match shaped {
Some(s) if !s.runs.is_empty() => apply_visual_edge(state, s, to_end, shift),
_ => false,
};
if !handled {
state.caret_affinity = slate_text::Affinity::Downstream;
let (dir, target) = if to_end {
(MotionDir::Right, state.text.len())
} else {
(MotionDir::Left, 0)
};
apply_motion(state, dir, shift, |s| s.caret = target);
}
}
pub(super) fn build_text_input_handler(value: Signal<String>) -> ElementTextInputHandler {
Arc::new(move |ev: &TextInputEvent, cx: &mut EventCtx| {
let id = match cx.element_id() {
Some(i) => i,
None => return,
};
let state_rc = match cx.ime_state(id) {
Some(s) => s,
None => return,
};
{
let state = state_rc.borrow();
if state.preedit.is_some() {
return;
}
}
let new_text = {
let mut state = state_rc.borrow_mut();
debug_assert!(
state.text.is_char_boundary(state.caret),
"TextField caret not on char boundary before text insert"
);
state.caret_affinity = slate_text::Affinity::Downstream;
let had_selection = state.selection_anchor.is_some_and(|a| a != state.caret);
delete_selection(&mut state);
let old_caret = state.caret;
let new_caret = insert_text_at(&mut state.text, old_caret, &ev.text);
state.caret = new_caret;
let op = if had_selection {
EditOp::Discrete
} else {
EditOp::Insert
};
record_edit(&mut state, op);
reset_blink(&mut state);
state.text.clone()
};
cx.stop_propagation();
value.set(new_text);
})
}
pub(super) fn build_mouse_down_handler() -> MouseHandler {
Arc::new(move |ev: &MouseEvent, cx: &mut EventCtx| {
let id = match cx.element_id() {
Some(i) => i,
None => return,
};
let state_rc = match cx.ime_state(id) {
Some(s) => s,
None => return,
};
{
let mut state = state_rc.borrow_mut();
if state.preedit.is_some() {
return;
}
let shaped = match state.last_shaped.clone() {
Some(s) => s,
None => return,
};
let local_x = ev.position.0 - state.paint_origin_x;
let byte = byte_at_pixel_x(&shaped, &state.text, local_x);
debug_assert!(
state.text.is_char_boundary(byte),
"byte_at_pixel_x must return a char boundary"
);
state.caret = byte;
state.caret_affinity = slate_text::Affinity::Downstream;
state.selection_anchor = Some(byte);
state.dragging = true;
reset_blink(&mut state);
state.undo.mark_motion();
}
cx.stop_propagation();
})
}
pub(super) fn build_mouse_move_handler() -> MouseHandler {
Arc::new(move |ev: &MouseEvent, cx: &mut EventCtx| {
let id = match cx.element_id() {
Some(i) => i,
None => return,
};
let state_rc = match cx.ime_state(id) {
Some(s) => s,
None => return,
};
let mut state = state_rc.borrow_mut();
if !state.dragging || state.preedit.is_some() {
return;
}
let shaped = match state.last_shaped.clone() {
Some(s) => s,
None => return,
};
let local_x = ev.position.0 - state.paint_origin_x;
let byte = byte_at_pixel_x(&shaped, &state.text, local_x);
debug_assert!(
state.text.is_char_boundary(byte),
"byte_at_pixel_x must return a char boundary"
);
state.caret = byte;
state.caret_affinity = slate_text::Affinity::Downstream;
reset_blink(&mut state);
drop(state);
cx.stop_propagation();
})
}
pub(super) fn build_mouse_up_handler() -> MouseHandler {
Arc::new(move |_ev: &MouseEvent, cx: &mut EventCtx| {
let id = match cx.element_id() {
Some(i) => i,
None => return,
};
let state_rc = match cx.ime_state(id) {
Some(s) => s,
None => return,
};
{
let mut state = state_rc.borrow_mut();
state.dragging = false;
if state.selection_anchor == Some(state.caret) {
state.selection_anchor = None;
}
}
cx.stop_propagation();
})
}
pub(super) fn build_ime_preedit_handler() -> ElementImePreeditHandler {
Arc::new(move |ev: &ImePreeditEvent, cx: &mut EventCtx| {
let id = match cx.element_id() {
Some(i) => i,
None => return,
};
let state_rc = match cx.ime_state(id) {
Some(s) => s,
None => return,
};
{
let mut state = state_rc.borrow_mut();
if ev.text.is_empty() {
state.preedit = None;
} else {
state.preedit = Some(Preedit {
text: ev.text.clone(),
cursor_byte_offset: ev.cursor_byte_offset,
selection: ev.selection.clone(),
});
}
}
cx.stop_propagation();
})
}
pub(super) fn build_ime_commit_handler(value: Signal<String>) -> ElementImeCommitHandler {
Arc::new(move |ev: &ImeCommitEvent, cx: &mut EventCtx| {
let id = match cx.element_id() {
Some(i) => i,
None => return,
};
let state_rc = match cx.ime_state(id) {
Some(s) => s,
None => return,
};
let new_text: Option<String> = {
let mut state = state_rc.borrow_mut();
if ev.text.is_empty() {
state.preedit = None;
None
} else {
debug_assert!(
state.text.is_char_boundary(state.caret),
"TextField caret not on char boundary before ime commit"
);
state.caret_affinity = slate_text::Affinity::Downstream;
delete_selection(&mut state);
let old_caret = state.caret;
let new_caret = insert_text_at(&mut state.text, old_caret, &ev.text);
state.caret = new_caret;
state.preedit = None;
record_edit(&mut state, EditOp::Discrete);
reset_blink(&mut state);
Some(state.text.clone())
}
};
cx.stop_propagation();
if let Some(t) = new_text {
value.set(t);
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ime::ImeState;
use slate_text::{Affinity, Direction, FontHandle, FontId, RunSpan, ShapedGlyph, ShapedLine};
use std::cell::RefCell;
use std::rc::Rc;
fn glyph_ltr(cluster: u32, adv: f32) -> ShapedGlyph {
ShapedGlyph {
glyph_id: 1,
font_id: FontId::PRIMARY,
font_handle: FontHandle::default(),
x_advance_lpx: adv,
position_lpx: [0.0, 0.0],
cluster,
direction: Direction::Ltr,
}
}
fn glyph(cluster: u32, adv: f32, dir: Direction) -> ShapedGlyph {
ShapedGlyph {
glyph_id: 1,
font_id: FontId::PRIMARY,
font_handle: FontHandle::default(),
x_advance_lpx: adv,
position_lpx: [0.0, 0.0],
cluster,
direction: dir,
}
}
fn run(range: std::ops::Range<usize>, dir: Direction) -> RunSpan {
RunSpan {
level: if dir == Direction::Rtl { 1 } else { 0 },
byte_range: range,
direction: dir,
}
}
fn pure_ltr_line() -> ShapedLine {
ShapedLine {
glyphs: vec![
glyph_ltr(0, 5.0),
glyph_ltr(1, 5.0),
glyph_ltr(2, 5.0),
glyph_ltr(3, 5.0),
glyph_ltr(4, 5.0),
],
width_lpx: 25.0,
ascent_lpx: 10.0,
descent_lpx: -2.0,
y_offset_lpx: 0.0,
base_direction: Direction::Ltr,
runs: Vec::new(),
}
}
fn mixed_line() -> ShapedLine {
ShapedLine {
glyphs: vec![
glyph(0, 5.0, Direction::Ltr),
glyph(1, 6.0, Direction::Ltr),
glyph(4, 7.0, Direction::Rtl),
glyph(2, 8.0, Direction::Rtl),
],
width_lpx: 26.0,
ascent_lpx: 10.0,
descent_lpx: -2.0,
y_offset_lpx: 0.0,
base_direction: Direction::Rtl,
runs: vec![run(0..2, Direction::Ltr), run(2..6, Direction::Rtl)],
}
}
fn state_with(text: &str, caret: usize) -> ImeState {
ImeState {
text: text.to_string(),
caret,
..Default::default()
}
}
#[test]
fn line_edge_pure_ltr_left_jumps_to_byte_0() {
let line = pure_ltr_line();
let mut s = state_with("hello", 3);
apply_line_edge(&mut s, Some(&line), false, false);
assert_eq!(s.caret, 0);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn line_edge_pure_ltr_right_jumps_to_text_len() {
let line = pure_ltr_line();
let mut s = state_with("hello", 3);
apply_line_edge(&mut s, Some(&line), true, false);
assert_eq!(s.caret, "hello".len());
}
#[test]
fn line_edge_run_bearing_right_lands_at_visual_rightmost() {
let line = mixed_line();
let mut s = state_with("abאב", 1);
apply_line_edge(&mut s, Some(&line), true, false);
assert_eq!(s.caret, 2);
assert_eq!(s.caret_affinity, Affinity::Downstream);
}
#[test]
fn line_edge_run_bearing_left_lands_at_visual_leftmost() {
let line = mixed_line();
let mut s = state_with("abאב", 4);
apply_line_edge(&mut s, Some(&line), false, false);
assert_eq!(s.caret, 0);
}
#[test]
fn line_edge_shift_extends_selection_from_pre_move_caret() {
let line = pure_ltr_line();
let mut s = state_with("hello", 2);
apply_line_edge(&mut s, Some(&line), true, true);
assert_eq!(s.selection_anchor, Some(2));
assert_eq!(s.caret, "hello".len());
}
#[test]
fn line_edge_repeated_at_edge_is_no_op() {
let line = pure_ltr_line();
let mut s = state_with("hello", 5);
apply_line_edge(&mut s, Some(&line), true, false);
assert_eq!(s.caret, 5);
apply_line_edge(&mut s, Some(&line), true, false);
assert_eq!(s.caret, 5);
}
#[test]
fn line_edge_no_shaped_falls_back_to_logical() {
let mut s = state_with("hello", 2);
apply_line_edge(&mut s, None, true, false);
assert_eq!(s.caret, "hello".len());
apply_line_edge(&mut s, None, false, false);
assert_eq!(s.caret, 0);
}
#[test]
fn cmd_arrow_during_preedit_short_circuits() {
let mut s = state_with("abc", 1);
s.preedit = Some(crate::ime::Preedit {
text: "x".into(),
cursor_byte_offset: 1,
selection: None,
});
let rc = Rc::new(RefCell::new(s));
assert!(rc.borrow().preedit.is_some());
}
}