use slate_text::MultilineLayout;
use crate::elements::text_edit::grapheme::insert_text_at;
use crate::elements::text_edit::ops::{
apply_motion_to, apply_visual_edge, delete_selection, record_edit, reset_blink,
};
use crate::elements::text_edit::undo::EditOp;
use crate::ime::ImeState;
fn finish_motion(state: &mut ImeState) {
reset_blink(state);
state.undo.mark_motion();
}
pub(crate) fn move_vertical(
state: &mut ImeState,
layout: &MultilineLayout,
down: bool,
shift: bool,
) {
if layout.lines.is_empty() {
return;
}
let (line_idx, x, _) = layout.caret_position_with_affinity(state.caret, state.caret_affinity);
let sticky = state.desired_x.unwrap_or(x);
state.desired_x = Some(sticky);
state.caret_affinity = slate_text::Affinity::Downstream;
let last = layout.lines.len() - 1;
let target_line = if down {
if line_idx >= last {
let end = layout.line_caret_end(&state.text, last);
apply_motion_to(state, shift, end);
finish_motion(state);
return;
}
line_idx + 1
} else {
if line_idx == 0 {
let start = layout.lines[0].byte_start;
apply_motion_to(state, shift, start);
finish_motion(state);
return;
}
line_idx - 1
};
let target_byte = layout.byte_at_line_x(&state.text, target_line, sticky);
apply_motion_to(state, shift, target_byte);
finish_motion(state);
}
pub(crate) fn move_line_edge(
state: &mut ImeState,
layout: &MultilineLayout,
to_end: bool,
shift: bool,
) {
if layout.lines.is_empty() {
return;
}
let line_idx = layout.line_for_byte(state.caret);
let handled = match layout.lines.get(line_idx) {
Some(vline) if !vline.line.runs.is_empty() => {
apply_visual_edge(state, &vline.line, to_end, shift)
}
_ => false,
};
if !handled {
let target = if to_end {
layout.line_caret_end(&state.text, line_idx)
} else {
layout.lines[line_idx].byte_start
};
apply_motion_to(state, shift, target);
state.caret_affinity = slate_text::Affinity::Downstream;
}
state.desired_x = None;
finish_motion(state);
}
pub(crate) fn visual_cross_line(
state: &mut ImeState,
layout: &MultilineLayout,
move_right: bool,
shift: bool,
) {
let idx = layout.line_for_byte(state.caret);
let target_idx = if move_right {
if idx + 1 >= layout.lines.len() {
return; }
idx + 1
} else {
if idx == 0 {
return; }
idx - 1
};
let Some(vline) = layout.lines.get(target_idx) else {
return;
};
let enter_to_end = !move_right;
let (target, affinity) = if !vline.line.runs.is_empty() {
slate_text::visual_line_edge(&vline.line, enter_to_end)
.unwrap_or((vline.byte_start, slate_text::Affinity::Downstream))
} else if enter_to_end {
(
layout.line_caret_end(&state.text, target_idx),
slate_text::Affinity::Downstream,
)
} else {
(vline.byte_start, slate_text::Affinity::Downstream)
};
apply_motion_to(state, shift, target);
state.caret_affinity = affinity;
}
pub(crate) fn insert_newline(state: &mut ImeState) -> String {
delete_selection(state);
let caret = state.caret;
state.caret = insert_text_at(&mut state.text, caret, "\n");
record_edit(state, EditOp::Discrete);
reset_blink(state);
state.desired_x = None;
state.caret_affinity = slate_text::Affinity::Downstream;
state.text.clone()
}
#[cfg(test)]
mod tests {
use super::*;
use slate_text::{Affinity, Direction, FontId, RunSpan, ShapedGlyph, ShapedLine};
use slate_text::{MultilineLayout, VisualLine};
fn glyph(cluster: u32, adv: f32) -> ShapedGlyph {
dglyph(cluster, adv, Direction::Ltr)
}
fn dglyph(cluster: u32, adv: f32, dir: Direction) -> ShapedGlyph {
ShapedGlyph {
glyph_id: 1,
font_id: FontId::PRIMARY,
font_handle: slate_text::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 vline(glyphs: Vec<ShapedGlyph>, byte_start: usize, byte_end: usize, y: f32) -> VisualLine {
let width: f32 = glyphs.iter().map(|g| g.x_advance_lpx).sum();
VisualLine {
line: ShapedLine {
glyphs,
width_lpx: width,
ascent_lpx: 10.0,
descent_lpx: -2.0,
y_offset_lpx: y,
base_direction: Direction::Ltr,
runs: Vec::new(),
},
byte_start,
byte_end,
}
}
fn layout_abcd() -> MultilineLayout {
MultilineLayout {
lines: vec![
vline(vec![glyph(0, 5.0), glyph(1, 6.0)], 0, 3, 0.0),
vline(vec![glyph(3, 7.0), glyph(4, 8.0)], 3, 5, 12.0),
],
total_height_lpx: 24.0,
line_height_lpx: 12.0,
}
}
fn state_with(text: &str, caret: usize) -> ImeState {
ImeState {
text: text.to_string(),
caret,
..Default::default()
}
}
fn layout_mixed_then_ltr() -> MultilineLayout {
let mixed = VisualLine {
line: ShapedLine {
glyphs: vec![
dglyph(0, 5.0, Direction::Ltr),
dglyph(1, 6.0, Direction::Ltr),
dglyph(4, 7.0, Direction::Rtl),
dglyph(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)],
},
byte_start: 0,
byte_end: 7,
};
MultilineLayout {
lines: vec![mixed, vline(vec![glyph(7, 5.0), glyph(8, 6.0)], 7, 9, 12.0)],
total_height_lpx: 24.0,
line_height_lpx: 12.0,
}
}
#[test]
fn vertical_down_preserves_column_and_seeds_desired_x() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 2);
move_vertical(&mut s, &layout, true, false);
assert_eq!(s.desired_x, Some(11.0));
assert_eq!(s.caret, 5);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn vertical_sticky_survives_second_move_and_resets_after_horizontal() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 1);
move_vertical(&mut s, &layout, true, false);
let after_down = s.caret;
assert_eq!(s.desired_x, Some(5.0));
move_vertical(&mut s, &layout, false, false);
assert_eq!(s.caret, 1, "↑ restores the seeded column");
assert_ne!(after_down, 1);
move_line_edge(&mut s, &layout, false, false);
assert_eq!(s.desired_x, None);
}
#[test]
fn vertical_clamps_at_edges() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 1);
move_vertical(&mut s, &layout, false, false);
assert_eq!(s.caret, 0);
let mut s = state_with("ab\ncd", 4);
move_vertical(&mut s, &layout, true, false);
assert_eq!(s.caret, 5);
}
#[test]
fn shift_vertical_extends_selection() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 2);
move_vertical(&mut s, &layout, true, true);
assert_eq!(s.selection_anchor, Some(2), "anchor at pre-move caret");
assert_eq!(s.caret, 5);
}
#[test]
fn line_edge_is_visual_line_relative() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 4);
move_line_edge(&mut s, &layout, false, false);
assert_eq!(s.caret, 3);
move_line_edge(&mut s, &layout, true, false);
assert_eq!(s.caret, 5);
let mut s = state_with("ab\ncd", 0);
move_line_edge(&mut s, &layout, true, false);
assert_eq!(s.caret, 2);
}
#[test]
fn cmd_right_single_line_jumps_to_visual_end() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 0);
move_line_edge(&mut s, &layout, true, false);
assert_eq!(s.caret, 2);
}
#[test]
fn cmd_left_single_line_jumps_to_visual_start() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 1);
move_line_edge(&mut s, &layout, false, false);
assert_eq!(s.caret, 0);
}
#[test]
fn cmd_right_wrapped_line_clamps_no_cross() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 1);
move_line_edge(&mut s, &layout, true, false);
assert_eq!(s.caret, 2, "first Cmd+→ lands at line0 end");
move_line_edge(&mut s, &layout, true, false);
assert_eq!(s.caret, 2, "second Cmd+→ at edge clamps — no cross");
}
#[test]
fn cmd_left_wrapped_line_clamps_no_cross() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 4);
move_line_edge(&mut s, &layout, false, false);
assert_eq!(s.caret, 3);
move_line_edge(&mut s, &layout, false, false);
assert_eq!(s.caret, 3, "repeated Cmd+← at start clamps");
}
#[test]
fn cmd_right_mixed_bidi_lands_at_rightmost_visual_column() {
let layout = layout_mixed_then_ltr();
let mut s = state_with("abאב\ncd", 1);
move_line_edge(&mut s, &layout, true, false);
assert_eq!(s.caret, 2);
}
#[test]
fn cmd_left_mixed_bidi_lands_at_leftmost_visual_column() {
let layout = layout_mixed_then_ltr();
let mut s = state_with("abאב\ncd", 4);
move_line_edge(&mut s, &layout, false, false);
assert_eq!(s.caret, 0);
}
#[test]
fn shift_cmd_arrow_extends_selection() {
let layout = layout_abcd();
let mut s = state_with("ab\ncd", 1);
move_line_edge(&mut s, &layout, true, true);
assert_eq!(s.selection_anchor, Some(1), "anchor at pre-move caret");
assert_eq!(s.caret, 2);
}
#[test]
fn cmd_arrow_no_layout_lines_is_noop() {
let empty = MultilineLayout {
lines: Vec::new(),
total_height_lpx: 0.0,
line_height_lpx: 0.0,
};
let mut s = state_with("ab", 1);
move_line_edge(&mut s, &empty, true, false);
assert_eq!(s.caret, 1);
}
#[test]
fn enter_inserts_newline_at_caret() {
let mut s = state_with("ab", 1);
let text = insert_newline(&mut s);
assert_eq!(text, "a\nb");
assert_eq!(s.caret, 2, "caret advances past the inserted '\\n'");
assert_eq!(s.desired_x, None);
}
#[test]
fn enter_replaces_selection() {
let mut s = state_with("abcd", 3);
s.selection_anchor = Some(1); let text = insert_newline(&mut s);
assert_eq!(text, "a\nd");
assert_eq!(s.caret, 2);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn visual_cross_line_right_enters_next_line_visual_left() {
let layout = layout_mixed_then_ltr();
let mut s = state_with("abאב\ncd", 2);
s.caret_affinity = Affinity::Downstream;
visual_cross_line(&mut s, &layout, true, false);
assert_eq!(s.caret, 7);
assert_eq!(s.caret_affinity, Affinity::Downstream);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn visual_cross_line_left_enters_prev_line_visual_right() {
let layout = layout_mixed_then_ltr();
let mut s = state_with("abאב\ncd", 7);
visual_cross_line(&mut s, &layout, false, false);
assert_eq!(s.caret, 2, "enters mixed line at its visual-right edge");
}
#[test]
fn visual_cross_line_clamps_at_document_edges() {
let layout = layout_mixed_then_ltr();
let mut s = state_with("abאב\ncd", 9);
visual_cross_line(&mut s, &layout, true, false);
assert_eq!(s.caret, 9);
let mut s = state_with("abאב\ncd", 2);
visual_cross_line(&mut s, &layout, false, false);
assert_eq!(s.caret, 2);
}
#[test]
fn visual_cross_line_shift_extends_from_existing_anchor() {
let layout = layout_mixed_then_ltr();
let mut s = state_with("abאב\ncd", 2);
s.selection_anchor = Some(2); visual_cross_line(&mut s, &layout, true, true);
assert_eq!(s.selection_anchor, Some(2), "anchor preserved");
assert_eq!(s.caret, 7);
}
}