use teksilo_core::event::{EventResponse, Key, WidgetEvent};
use teksilo_core::widget::EventContext;
use teksilo_platform::clipboard::ClipboardHandle;
use teksilo_text::CursorAffinity;
use teksilo_text::text_document::{MoveMode, MoveOperation, SelectionType};
use crate::common::text_nav::{CaretStep, caret_step, deletes_word};
use super::state::{SharedState, TextInputState, sync_cursor_signals};
pub(crate) fn report_ime_cursor_area(state: &SharedState, ctx: &mut EventContext) {
let area = {
let st = state.borrow();
if !st.has_focus || !st.engine.has_full_layout() {
return;
}
let caret = st
.engine
.caret_rect(st.cursor.position(), CursorAffinity::Downstream);
teksilo_canvas::Rect::new(
st.viewport_origin.x + caret[0] - st.scroll_x,
st.viewport_origin.y + caret[1],
caret[2].max(1.0),
caret[3],
)
};
{
let mut st = state.borrow_mut();
if st.last_ime_area == Some(area) {
return;
}
st.last_ime_area = Some(area);
}
ctx.set_ime_cursor_area(area);
}
pub(crate) fn handle_key(
state: &SharedState,
event: &WidgetEvent,
ctx: &mut EventContext,
) -> EventResponse {
if let WidgetEvent::ImeComposition { text, .. } = event {
return handle_ime_composition(state, ctx, text);
}
if let WidgetEvent::ImeCommit { text } = event {
clear_ime_preedit(state);
return push_pending_chars(state, ctx, text);
}
let WidgetEvent::KeyDown {
key,
modifiers,
text,
..
} = event
else {
return EventResponse::Ignored;
};
let shift = modifiers.shift();
let ctrl = modifiers.command();
let mode = if shift {
MoveMode::KeepAnchor
} else {
MoveMode::MoveAnchor
};
let read_only = state.borrow().read_only;
let handled = {
let mut st = state.borrow_mut();
match key {
Key::ArrowLeft => {
let op = match caret_step(*modifiers) {
CaretStep::Character => MoveOperation::Left,
CaretStep::Word => MoveOperation::WordLeft,
CaretStep::LineEdge => MoveOperation::Start,
};
st.cursor.move_position(op, mode, 1);
true
}
Key::ArrowRight => {
let op = match caret_step(*modifiers) {
CaretStep::Character => MoveOperation::Right,
CaretStep::Word => MoveOperation::WordRight,
CaretStep::LineEdge => MoveOperation::End,
};
st.cursor.move_position(op, mode, 1);
true
}
Key::Home => {
st.cursor.move_position(MoveOperation::Start, mode, 1);
true
}
Key::End => {
st.cursor.move_position(MoveOperation::End, mode, 1);
true
}
Key::A if ctrl => {
st.cursor.select(SelectionType::Document);
true
}
Key::C if ctrl => {
clipboard_copy(&mut st, ctx);
true
}
Key::X if ctrl && !read_only => {
clipboard_cut(&mut st, ctx);
true
}
Key::V if ctrl && !read_only => {
clipboard_paste(&mut st, ctx);
true
}
Key::Backspace if !read_only => {
if deletes_word(*modifiers) {
if !st.cursor.has_selection() {
st.cursor
.move_position(MoveOperation::WordLeft, MoveMode::KeepAnchor, 1);
}
let _ = st.cursor.remove_selected_text();
} else if st.cursor.has_selection() {
let _ = st.cursor.remove_selected_text();
} else {
let _ = st.cursor.delete_previous_char();
}
true
}
Key::Delete if !read_only => {
if deletes_word(*modifiers) {
if !st.cursor.has_selection() {
st.cursor
.move_position(MoveOperation::WordRight, MoveMode::KeepAnchor, 1);
}
let _ = st.cursor.remove_selected_text();
} else if st.cursor.has_selection() {
let _ = st.cursor.remove_selected_text();
} else {
let _ = st.cursor.delete_char();
}
true
}
Key::Z if ctrl && !shift && !read_only => {
let _ = st.document.undo();
true
}
Key::Y if ctrl && !read_only => {
let _ = st.document.redo();
true
}
Key::Z if ctrl && shift && !read_only => {
let _ = st.document.redo();
true
}
Key::Enter => {
if let Some(ref on_submit) = st.on_submit {
let submit = on_submit.clone();
drop(st);
submit(ctx);
return EventResponse::Handled;
}
true
}
Key::Tab => {
return EventResponse::Ignored;
}
_ => {
if ctrl {
false } else if let Some(t) = text.as_deref() {
if !read_only {
let clean: String = t
.chars()
.filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
.filter(|c| st.char_filter_admits(*c))
.collect();
if !clean.is_empty() {
if let Some(max) = st.max_length {
let current_len = st
.document
.to_plain_text()
.unwrap_or_default()
.chars()
.count();
let sel_len = if st.cursor.has_selection() {
st.cursor
.selected_text()
.ok()
.unwrap_or_default()
.chars()
.count()
} else {
0
};
let remaining =
max.saturating_sub(current_len.saturating_sub(sel_len));
if remaining == 0 {
return EventResponse::Handled;
}
let truncated: String = clean.chars().take(remaining).collect();
st.pending_chars.push_str(&truncated);
} else {
st.pending_chars.push_str(&clean);
}
true
} else if t.chars().any(|c| !c.is_control()) {
return EventResponse::Handled;
} else {
false
}
} else {
false
}
} else {
false
}
}
}
};
if handled {
sync_cursor_signals(state);
report_ime_cursor_area(state, ctx);
ctx.request_frame();
EventResponse::Handled
} else {
EventResponse::Ignored
}
}
fn handle_ime_composition(
state: &SharedState,
ctx: &mut EventContext,
text: &str,
) -> EventResponse {
if state.borrow().read_only {
return EventResponse::Handled;
}
{
let st = state.borrow();
let empty = text
.chars()
.all(|c| c.is_control() || c == '\n' || c == '\r');
if empty && st.ime_preedit_range.is_none() {
return EventResponse::Ignored;
}
}
{
let mut st = state.borrow_mut();
st.cursor.begin_edit_block();
if let Some(range) = st.ime_preedit_range.take() {
let doc_end = st.document.character_count();
let start = range.start.min(doc_end);
let end = range.end.min(doc_end);
if start < end {
st.cursor.set_position(start, MoveMode::MoveAnchor);
st.cursor.set_position(end, MoveMode::KeepAnchor);
let _ = st.cursor.remove_selected_text();
}
}
let clean: String = text
.chars()
.filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
.filter(|c| st.char_filter_admits(*c))
.collect();
let clean = if let Some(max) = st.max_length {
let current_len = st.document.character_count();
let sel_len = if st.cursor.has_selection() {
st.cursor
.selected_text()
.ok()
.unwrap_or_default()
.chars()
.count()
} else {
0
};
let remaining = max.saturating_sub(current_len.saturating_sub(sel_len));
clean.chars().take(remaining).collect::<String>()
} else {
clean
};
if !clean.is_empty() {
let start = st.cursor.position();
let _ = st.cursor.insert_text(&clean);
let end = st.cursor.position();
st.ime_preedit = Some(clean);
st.ime_preedit_range = Some(start..end);
} else {
st.ime_preedit = None;
}
st.cursor.end_edit_block();
st.pending_text_changed = true;
}
sync_cursor_signals(state);
report_ime_cursor_area(state, ctx);
ctx.request_frame();
EventResponse::Handled
}
pub(crate) fn clear_ime_preedit(state: &SharedState) {
let mut st = state.borrow_mut();
if let Some(range) = st.ime_preedit_range.take() {
let doc_end = st.document.character_count();
let start = range.start.min(doc_end);
let end = range.end.min(doc_end);
if start < end {
st.cursor.set_position(start, MoveMode::MoveAnchor);
st.cursor.set_position(end, MoveMode::KeepAnchor);
let _ = st.cursor.remove_selected_text();
}
}
st.ime_preedit = None;
}
fn push_pending_chars(state: &SharedState, ctx: &mut EventContext, text: &str) -> EventResponse {
let read_only = state.borrow().read_only;
if read_only {
return EventResponse::Handled;
}
let clean: String = {
let st = state.borrow();
text.chars()
.filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
.filter(|c| st.char_filter_admits(*c))
.collect()
};
if clean.is_empty() {
return EventResponse::Handled;
}
{
let mut st = state.borrow_mut();
if let Some(max) = st.max_length {
let current_len = st
.document
.to_plain_text()
.unwrap_or_default()
.chars()
.count();
let sel_len = if st.cursor.has_selection() {
st.cursor
.selected_text()
.ok()
.unwrap_or_default()
.chars()
.count()
} else {
0
};
let remaining = max.saturating_sub(current_len.saturating_sub(sel_len));
if remaining == 0 {
return EventResponse::Handled;
}
let truncated: String = clean.chars().take(remaining).collect();
st.pending_chars.push_str(&truncated);
} else {
st.pending_chars.push_str(&clean);
}
}
sync_cursor_signals(state);
report_ime_cursor_area(state, ctx);
ctx.request_frame();
EventResponse::Handled
}
pub(crate) fn clipboard_copy(state: &mut TextInputState, ctx: &EventContext) {
if !state.copy_allowed() {
return;
}
if !state.cursor.has_selection() {
return;
}
let plain = state.cursor.selected_text().ok().unwrap_or_default();
if let Some(cb) = ctx.app_state::<ClipboardHandle>() {
let _ = cb.set_text(&plain);
}
}
pub(crate) fn clipboard_cut(state: &mut TextInputState, ctx: &EventContext) {
if !state.copy_allowed() {
return;
}
if !state.cursor.has_selection() {
return;
}
clipboard_copy(state, ctx);
let _ = state.cursor.remove_selected_text();
state.pending_text_changed = true;
}
pub(crate) fn clipboard_paste(state: &mut TextInputState, ctx: &EventContext) {
let Some(cb) = ctx.app_state::<ClipboardHandle>() else {
return;
};
let Ok(system) = cb.get_text() else {
return;
};
if system.is_empty() {
return;
}
let clean: String = system
.chars()
.filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
.filter(|c| state.char_filter_admits(*c))
.collect();
if clean.is_empty() {
return;
}
if let Some(max) = state.max_length {
let current_len = state
.document
.to_plain_text()
.unwrap_or_default()
.chars()
.count();
let sel_len = if state.cursor.has_selection() {
state
.cursor
.selected_text()
.ok()
.unwrap_or_default()
.chars()
.count()
} else {
0
};
let remaining = max.saturating_sub(current_len.saturating_sub(sel_len));
if remaining == 0 {
return;
}
let truncated: String = clean.chars().take(remaining).collect();
let _ = state.cursor.insert_text(&truncated);
} else {
let _ = state.cursor.insert_text(&clean);
}
state.cursor.clear_selection();
state.pending_text_changed = true;
}