use cranpose_foundation::text::{
InputTransformation, TextFieldBuffer, TextFieldLineLimits, TextFieldState, TextRange,
floor_grapheme_boundary,
};
use crate::{
key_event::{KeyCode, KeyEvent},
text::{
AnnotatedString, ResolvedTextDirection, TextLayoutOptions, TextStyle,
get_offset_for_position, measure_text, resolve_text_direction, wrapped_line_ranges,
},
text_selection::{LineAffinity, caret_visual_line},
word_boundaries::{find_word_end, find_word_start},
};
pub(crate) struct KeyInput<'a> {
pub line_limits: TextFieldLineLimits,
pub transformation: Option<&'a InputTransformation>,
pub style: &'a TextStyle,
pub node_id: Option<cranpose_core::NodeId>,
pub wrap_width: Option<f32>,
}
pub(crate) fn handle_key_event_impl(
state: &TextFieldState,
event: &KeyEvent,
input: &KeyInput<'_>,
) -> bool {
let transformation = input.transformation;
match event.key_code {
KeyCode::Enter => {
if input.line_limits.is_single_line() {
false
} else {
state.edit_as_user(transformation, |buffer| buffer.insert("\n"));
state.set_desired_x(None);
true
}
}
_ if !event.text.is_empty() && !event.modifiers.command_or_ctrl() => {
state.edit_as_user(transformation, |buffer| buffer.insert(&event.text));
state.set_desired_x(None);
true
}
KeyCode::Backspace => {
state.edit_as_user(transformation, TextFieldBuffer::delete_before_cursor);
state.set_desired_x(None);
true
}
KeyCode::Delete => {
state.edit_as_user(transformation, TextFieldBuffer::delete_after_cursor);
state.set_desired_x(None);
true
}
KeyCode::ArrowLeft | KeyCode::ArrowRight => {
state.set_desired_x(None);
let direction = input.style.paragraph_style.text_direction;
state.edit(|buffer| {
let rtl = paragraph_is_rtl(buffer.text(), buffer.selection().end, direction);
let back = (event.key_code == KeyCode::ArrowLeft) != rtl;
let by_word = event.modifiers.command_or_ctrl() && !event.modifiers.shift;
match (back, by_word, event.modifiers.shift) {
(true, true, _) => {
let target = find_word_start(buffer.text(), buffer.selection().start);
buffer.place_cursor_before_char(target);
}
(false, true, _) => {
let target = find_word_end(buffer.text(), buffer.selection().end);
buffer.place_cursor_before_char(target);
}
(true, false, true) => buffer.extend_selection_left(),
(false, false, true) => buffer.extend_selection_right(),
(true, false, false) => buffer.move_cursor_left(),
(false, false, false) => buffer.move_cursor_right(),
}
});
true
}
KeyCode::ArrowUp => {
move_line(state, event.modifiers.shift, LineStep::Up, input);
true
}
KeyCode::ArrowDown => {
move_line(state, event.modifiers.shift, LineStep::Down, input);
true
}
KeyCode::Home | KeyCode::End => {
state.set_desired_x(None);
let to_start = event.key_code == KeyCode::Home;
if event.modifiers.command_or_ctrl() {
state.edit(if to_start {
TextFieldBuffer::place_cursor_at_start
} else {
TextFieldBuffer::place_cursor_at_end
});
} else {
state.edit(|buffer| {
let line =
LaidOutLines::of(buffer.text(), input).line_at(buffer.selection().end);
buffer.place_cursor_before_char(if to_start { line.start } else { line.end });
});
}
true
}
KeyCode::A if event.modifiers.command_or_ctrl() => {
state.edit(TextFieldBuffer::select_all);
true
}
KeyCode::Z if event.modifiers.command_or_ctrl() && !event.modifiers.shift => {
state.undo();
true
}
KeyCode::Z if event.modifiers.command_or_ctrl() && event.modifiers.shift => {
state.redo();
true
}
KeyCode::Y if event.modifiers.command_or_ctrl() => {
state.redo();
true
}
_ => false,
}
}
fn paragraph_is_rtl(text: &str, offset: usize, direction: crate::text::TextDirection) -> bool {
let offset = text.floor_char_boundary(offset);
let start = text[..offset].rfind('\n').map_or(0, |i| i + 1);
let end = text[offset..].find('\n').map_or(text.len(), |i| offset + i);
resolve_text_direction(&text[start..end], Some(direction)) == ResolvedTextDirection::Rtl
}
struct LaidOutLines<'a> {
text: &'a str,
style: &'a TextStyle,
ranges: Vec<std::ops::Range<usize>>,
}
impl<'a> LaidOutLines<'a> {
fn of(text: &'a str, input: &KeyInput<'a>) -> Self {
let ranges = wrapped_line_ranges(
input.node_id,
&AnnotatedString::from(text),
input.style,
TextLayoutOptions::default(),
input.wrap_width,
);
Self {
text,
style: input.style,
ranges,
}
}
fn index_at(&self, offset: usize) -> usize {
caret_visual_line(&self.ranges, offset, LineAffinity::Upstream).0
}
fn line_at(&self, offset: usize) -> std::ops::Range<usize> {
self.ranges
.get(self.index_at(offset))
.cloned()
.unwrap_or(0..self.text.len())
}
fn x_at(&self, offset: usize) -> f32 {
let start = self.line_at(offset).start.min(offset);
measure_text(
&AnnotatedString::from(&self.text[start..offset]),
self.style,
)
.width
}
fn offset_at(&self, index: usize, x: f32) -> Option<usize> {
let range = self.ranges.get(index)?;
let line = &self.text[range.clone()];
let within = get_offset_for_position(&AnnotatedString::from(line), self.style, x, 0.0);
Some(floor_grapheme_boundary(
self.text,
range.start + within.min(line.len()),
))
}
}
#[derive(Clone, Copy)]
enum LineStep {
Up,
Down,
}
fn move_line(state: &TextFieldState, extend_selection: bool, step: LineStep, input: &KeyInput<'_>) {
state.edit(|buffer| {
let lines = LaidOutLines::of(buffer.text(), input);
let selection = buffer.selection();
let cursor = selection.end.min(buffer.len());
let x = state.desired_x().unwrap_or_else(|| {
let x = lines.x_at(cursor);
state.set_desired_x(Some(x));
x
});
let line = lines.index_at(cursor);
let target_line = match step {
LineStep::Up => line.checked_sub(1),
LineStep::Down => Some(line + 1),
};
let target = target_line
.and_then(|index| lines.offset_at(index, x))
.unwrap_or_else(|| {
state.set_desired_x(None);
match step {
LineStep::Up => 0,
LineStep::Down => buffer.len(),
}
});
if extend_selection {
buffer.select(TextRange::new(selection.start, target));
} else {
buffer.place_cursor_before_char(target);
}
});
}