use std::ops::Range;
use std::rc::Rc;
use std::time::Instant;
use slate_platform::PhysicalRect;
use crate::elements::text_edit::undo::UndoStack;
#[derive(Clone, Debug, Default)]
pub struct Preedit {
pub text: String,
pub cursor_byte_offset: usize,
pub selection: Option<Range<usize>>,
}
#[derive(Clone, Debug)]
pub struct BlinkState {
pub visible: bool,
pub next: Option<Instant>,
}
impl Default for BlinkState {
fn default() -> Self {
Self {
visible: true,
next: None,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ImeState {
pub text: String,
pub caret: usize,
pub caret_affinity: slate_text::Affinity,
pub selection_anchor: Option<usize>,
pub preedit: Option<Preedit>,
pub caret_client_rect: Option<PhysicalRect>,
pub last_shaped: Option<Rc<slate_text::ShapedLine>>,
pub paint_origin_x: f32,
pub paint_origin_y: f32,
pub dragging: bool,
pub blink: BlinkState,
pub undo: UndoStack,
pub undo_seeded: bool,
pub desired_x: Option<f32>,
pub last_layout: Option<Rc<slate_text::MultilineLayout>>,
pub last_click_time: Option<Instant>,
pub last_click_pos: (f32, f32),
pub click_count: u8,
}
impl ImeState {
pub fn seed_undo_baseline(&mut self) {
if !self.undo_seeded {
self.undo = UndoStack::with_baseline(self.text.clone(), self.caret);
self.undo_seeded = true;
}
}
pub fn answer_ime_text(&self, range: Range<usize>) -> Option<String> {
self.text.get(range).map(|s| s.to_string())
}
pub fn answer_selected_range(&self) -> Option<Range<usize>> {
match self.selection_anchor {
Some(anchor) => {
let (lo, hi) = if self.caret <= anchor {
(self.caret, anchor)
} else {
(anchor, self.caret)
};
Some(lo..hi)
}
None => Some(self.caret..self.caret),
}
}
pub fn answer_marked_range(&self) -> Option<Range<usize>> {
let preedit = self.preedit.as_ref()?;
let start = self.caret;
let end = start + preedit.text.len();
Some(start..end)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn answer_ime_text_returns_substring() {
let s = ImeState {
text: "hello world".to_string(),
..Default::default()
};
assert_eq!(s.answer_ime_text(0..5), Some("hello".to_string()));
assert_eq!(s.answer_ime_text(6..11), Some("world".to_string()));
assert_eq!(s.answer_ime_text(0..100), None);
}
#[test]
fn answer_selected_range_returns_caret_empty_range() {
let s = ImeState {
text: "abc".to_string(),
caret: 2,
..Default::default()
};
assert_eq!(s.answer_selected_range(), Some(2..2));
}
#[test]
fn answer_marked_range_none_when_no_preedit() {
let s = ImeState::default();
assert_eq!(s.answer_marked_range(), None);
}
#[test]
fn selection_anchor_defaults_to_none() {
let s = ImeState::default();
assert!(s.selection_anchor.is_none());
}
#[test]
fn answer_selected_range_with_anchor_forward() {
let s = ImeState {
text: "abcdef".to_string(),
caret: 5,
selection_anchor: Some(2),
..Default::default()
};
assert_eq!(s.answer_selected_range(), Some(2..5));
}
#[test]
fn answer_selected_range_with_anchor_reverse() {
let s = ImeState {
text: "abcdef".to_string(),
caret: 1,
selection_anchor: Some(4),
..Default::default()
};
assert_eq!(s.answer_selected_range(), Some(1..4));
}
#[test]
fn answer_selected_range_collapsed_anchor() {
let s = ImeState {
text: "abc".to_string(),
caret: 2,
selection_anchor: Some(2),
..Default::default()
};
assert_eq!(s.answer_selected_range(), Some(2..2));
}
#[test]
fn seed_undo_baseline_is_one_time() {
use crate::elements::text_edit::undo::{EditOp, EditSnapshot};
let mut state = ImeState {
text: "init".to_string(),
caret: 4,
..Default::default()
};
state.seed_undo_baseline();
assert!(state.undo_seeded);
state.undo.record_edit(
EditOp::Insert,
EditSnapshot {
text: "initX".to_string(),
caret: 5,
anchor: None,
},
);
state.seed_undo_baseline();
let restored = state.undo.undo();
assert_eq!(
restored.map(|s| s.text),
Some("init".to_string()),
"second seed must not reset a stack with recorded edits"
);
}
#[test]
fn answer_marked_range_with_preedit() {
let s = ImeState {
text: "abc".to_string(),
caret: 3,
preedit: Some(Preedit {
text: "xy".to_string(),
cursor_byte_offset: 2,
selection: None,
}),
..Default::default()
};
assert_eq!(s.answer_marked_range(), Some(3..5));
}
}