use crate::ime::ImeState;
use super::undo::{EditOp, EditSnapshot};
pub(crate) fn selection_range(state: &ImeState) -> Option<(usize, usize)> {
let anchor = state.selection_anchor?;
let lo = state.caret.min(anchor);
let hi = state.caret.max(anchor);
(lo < hi).then_some((lo, hi))
}
pub(crate) fn apply_snapshot(state: &mut ImeState, snap: &EditSnapshot) {
state.text = snap.text.clone();
state.caret = snap.caret.min(state.text.len());
state.selection_anchor = snap.anchor.map(|a| a.min(state.text.len()));
state.preedit = None;
reset_blink(state);
}
pub(crate) fn record_edit(state: &mut ImeState, op: EditOp) {
let snap = EditSnapshot {
text: state.text.clone(),
caret: state.caret,
anchor: state.selection_anchor,
};
state.undo.record_edit(op, snap);
}
pub(crate) fn reset_blink(state: &mut ImeState) {
state.blink.visible = true;
state.blink.next = None;
}
pub(crate) fn delete_selection(state: &mut ImeState) -> bool {
let anchor = match state.selection_anchor {
Some(a) => a,
None => return false,
};
let lo = state.caret.min(anchor);
let hi = state.caret.max(anchor);
if lo < hi {
debug_assert!(state.text.is_char_boundary(lo));
debug_assert!(state.text.is_char_boundary(hi));
state.text.replace_range(lo..hi, "");
state.caret = lo;
state.selection_anchor = None;
true
} else {
state.selection_anchor = None;
false
}
}
#[derive(Clone, Copy)]
pub(crate) enum MotionDir {
Left,
Right,
}
pub(crate) fn apply_motion(
state: &mut ImeState,
dir: MotionDir,
shift: bool,
mover: impl FnOnce(&mut ImeState),
) {
if shift {
if state.selection_anchor.is_none() {
state.selection_anchor = Some(state.caret);
}
mover(state);
return;
}
if let Some(anchor) = state.selection_anchor {
let lo = state.caret.min(anchor);
let hi = state.caret.max(anchor);
state.selection_anchor = None;
if lo < hi {
state.caret = match dir {
MotionDir::Left => lo,
MotionDir::Right => hi,
};
return;
}
}
mover(state);
}
pub(crate) fn apply_motion_to(state: &mut ImeState, shift: bool, target: usize) {
if shift {
if state.selection_anchor.is_none() {
state.selection_anchor = Some(state.caret);
}
} else {
state.selection_anchor = None;
}
state.caret = target;
}
pub(crate) fn apply_visual_motion(
state: &mut ImeState,
line: &slate_text::ShapedLine,
move_right: bool,
shift: bool,
) -> bool {
if shift {
if state.selection_anchor.is_none() {
state.selection_anchor = Some(state.caret);
}
} else if let Some(anchor) = state.selection_anchor {
let lo = state.caret.min(anchor);
let hi = state.caret.max(anchor);
state.selection_anchor = None;
if lo < hi {
state.caret = if move_right { hi } else { lo };
state.caret_affinity = slate_text::Affinity::Downstream;
return true;
}
}
match slate_text::visual_caret_step(line, state.caret, state.caret_affinity, move_right) {
Some((byte, affinity)) => {
state.caret = byte;
state.caret_affinity = affinity;
true
}
None => false,
}
}
pub(crate) fn apply_visual_edge(
state: &mut ImeState,
line: &slate_text::ShapedLine,
to_end: bool,
shift: bool,
) -> bool {
match slate_text::visual_line_edge(line, to_end) {
Some((byte, affinity)) => {
apply_motion_to(state, shift, byte);
state.caret_affinity = affinity;
true
}
None => false,
}
}
#[cfg(test)]
mod selection_tests {
use super::*;
use crate::elements::text_edit::grapheme::{next_grapheme_boundary, prev_grapheme_boundary};
fn state_with(text: &str, caret: usize, anchor: Option<usize>) -> ImeState {
ImeState {
text: text.to_string(),
caret,
selection_anchor: anchor,
..Default::default()
}
}
#[test]
fn delete_selection_removes_range_and_collapses() {
let mut s = state_with("hello world", 5, Some(0));
assert!(delete_selection(&mut s));
assert_eq!(s.text, " world");
assert_eq!(s.caret, 0);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn delete_selection_caret_before_anchor() {
let mut s = state_with("abcdef", 1, Some(4));
assert!(delete_selection(&mut s));
assert_eq!(s.text, "aef");
assert_eq!(s.caret, 1);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn delete_selection_empty_clears_anchor_returns_false() {
let mut s = state_with("abc", 1, Some(1));
assert!(!delete_selection(&mut s));
assert_eq!(s.text, "abc");
assert_eq!(s.caret, 1);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn delete_selection_no_anchor_noop() {
let mut s = state_with("abc", 1, None);
assert!(!delete_selection(&mut s));
assert_eq!(s.text, "abc");
assert_eq!(s.caret, 1);
}
#[test]
fn shift_motion_anchors_at_pre_move_caret() {
let mut s = state_with("hello", 2, None);
apply_motion(&mut s, MotionDir::Right, true, |s| {
s.caret = next_grapheme_boundary(&s.text, s.caret);
});
assert_eq!(s.selection_anchor, Some(2));
assert_eq!(s.caret, 3);
}
#[test]
fn shift_motion_keeps_existing_anchor() {
let mut s = state_with("hello", 3, Some(1));
apply_motion(&mut s, MotionDir::Right, true, |s| {
s.caret = next_grapheme_boundary(&s.text, s.caret);
});
assert_eq!(s.selection_anchor, Some(1), "anchor must not move");
assert_eq!(s.caret, 4);
}
#[test]
fn plain_left_with_selection_collapses_to_left_edge() {
let mut s = state_with("abcdef", 4, Some(1));
apply_motion(&mut s, MotionDir::Left, false, |s| {
s.caret = prev_grapheme_boundary(&s.text, s.caret);
});
assert_eq!(s.caret, 1, "collapses to lo edge, no further move");
assert_eq!(s.selection_anchor, None);
}
#[test]
fn plain_right_with_selection_collapses_to_right_edge() {
let mut s = state_with("abcdef", 1, Some(4));
apply_motion(&mut s, MotionDir::Right, false, |s| {
s.caret = next_grapheme_boundary(&s.text, s.caret);
});
assert_eq!(s.caret, 4, "collapses to hi edge, no further move");
assert_eq!(s.selection_anchor, None);
}
#[test]
fn plain_motion_no_selection_moves_normally() {
let mut s = state_with("abc", 1, None);
apply_motion(&mut s, MotionDir::Right, false, |s| {
s.caret = next_grapheme_boundary(&s.text, s.caret);
});
assert_eq!(s.caret, 2);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn plain_motion_empty_selection_clears_anchor_and_moves() {
let mut s = state_with("abc", 1, Some(1));
apply_motion(&mut s, MotionDir::Right, false, |s| {
s.caret = next_grapheme_boundary(&s.text, s.caret);
});
assert_eq!(s.caret, 2);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn shift_home_then_shift_end_swaps_selection_around_anchor() {
let mut s = state_with("hello", 2, None);
apply_motion(&mut s, MotionDir::Left, true, |s| s.caret = 0);
assert_eq!(s.selection_anchor, Some(2));
assert_eq!(s.caret, 0);
apply_motion(&mut s, MotionDir::Right, true, |s| s.caret = s.text.len());
assert_eq!(s.selection_anchor, Some(2));
assert_eq!(s.caret, 5);
}
#[test]
fn selection_range_returns_lo_hi_when_non_empty() {
let s = state_with("abcdef", 4, Some(1));
assert_eq!(selection_range(&s), Some((1, 4)));
let s = state_with("abcdef", 1, Some(4));
assert_eq!(selection_range(&s), Some((1, 4)));
}
#[test]
fn selection_range_none_for_empty_or_unanchored() {
assert_eq!(selection_range(&state_with("abc", 1, None)), None);
assert_eq!(selection_range(&state_with("abc", 1, Some(1))), None);
}
#[test]
fn apply_motion_to_plain_jumps_and_clears_anchor() {
let mut s = state_with("hello", 1, Some(3));
apply_motion_to(&mut s, false, 4);
assert_eq!(s.caret, 4);
assert_eq!(s.selection_anchor, None);
}
#[test]
fn apply_motion_to_shift_anchors_and_extends() {
let mut s = state_with("hello", 2, None);
apply_motion_to(&mut s, true, 5);
assert_eq!(s.selection_anchor, Some(2), "anchors at pre-move caret");
assert_eq!(s.caret, 5);
apply_motion_to(&mut s, true, 0);
assert_eq!(s.selection_anchor, Some(2));
assert_eq!(s.caret, 0);
}
#[test]
fn grapheme_boundary_collapse_with_cjk() {
let mut s = state_with("こんにちは", 9, Some(3));
apply_motion(&mut s, MotionDir::Left, false, |s| {
s.caret = prev_grapheme_boundary(&s.text, s.caret);
});
assert_eq!(s.caret, 3, "collapse must land on a grapheme boundary");
assert!(s.text.is_char_boundary(s.caret));
}
}
#[cfg(test)]
mod visual_motion_tests {
use super::*;
use slate_text::{Affinity, Direction, FontHandle, FontId, RunSpan, ShapedGlyph, ShapedLine};
fn dglyph(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 mixed_line() -> ShapedLine {
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)],
}
}
fn state_at(caret: usize, affinity: Affinity) -> ImeState {
ImeState {
text: "abאב".to_string(),
caret,
caret_affinity: affinity,
..Default::default()
}
}
#[test]
fn visual_right_crosses_seam_and_threads_affinity() {
let line = mixed_line();
let mut s = state_at(1, Affinity::Downstream);
assert!(apply_visual_motion(&mut s, &line, true, false));
assert_eq!((s.caret, s.caret_affinity), (6, Affinity::Upstream));
assert!(apply_visual_motion(&mut s, &line, true, false));
assert_eq!((s.caret, s.caret_affinity), (4, Affinity::Downstream));
assert!(apply_visual_motion(&mut s, &line, true, false));
assert_eq!((s.caret, s.caret_affinity), (2, Affinity::Downstream));
}
#[test]
fn visual_motion_returns_false_at_visual_edge() {
let line = mixed_line();
let mut s = state_at(2, Affinity::Downstream);
assert!(!apply_visual_motion(&mut s, &line, true, false));
assert_eq!(s.caret, 2, "caret unchanged when caller must fall back");
let mut s = state_at(0, Affinity::Downstream);
assert!(!apply_visual_motion(&mut s, &line, false, false));
assert_eq!(s.caret, 0);
}
#[test]
fn shift_visual_motion_anchors_then_extends() {
let line = mixed_line();
let mut s = state_at(1, Affinity::Downstream);
apply_visual_motion(&mut s, &line, true, true);
assert_eq!(s.selection_anchor, Some(1), "anchor at pre-move caret");
assert_eq!(s.caret, 6);
apply_visual_motion(&mut s, &line, true, true);
assert_eq!(s.selection_anchor, Some(1));
assert_eq!(s.caret, 4);
}
#[test]
fn plain_visual_motion_over_selection_collapses_to_edge() {
let line = mixed_line();
let mut s = state_at(4, Affinity::Downstream);
s.selection_anchor = Some(1);
assert!(apply_visual_motion(&mut s, &line, true, false));
assert_eq!(s.caret, 4);
assert_eq!(s.selection_anchor, None);
assert_eq!(s.caret_affinity, Affinity::Downstream);
}
#[test]
fn visual_edge_home_end_hit_screen_extremes() {
let line = mixed_line();
let mut s = state_at(4, Affinity::Downstream);
assert!(apply_visual_edge(&mut s, &line, false, false));
assert_eq!((s.caret, s.caret_affinity), (0, Affinity::Downstream));
assert!(apply_visual_edge(&mut s, &line, true, false));
assert_eq!((s.caret, s.caret_affinity), (2, Affinity::Downstream));
}
#[test]
fn visual_ops_decline_on_runless_line() {
let line = ShapedLine {
glyphs: vec![dglyph(0, 5.0, Direction::Ltr)],
width_lpx: 5.0,
ascent_lpx: 10.0,
descent_lpx: -2.0,
y_offset_lpx: 0.0,
base_direction: Direction::Ltr,
runs: Vec::new(),
};
let mut s = state_at(0, Affinity::Downstream);
assert!(!apply_visual_motion(&mut s, &line, true, false));
assert!(!apply_visual_edge(&mut s, &line, true, false));
}
}