use super::*;
use kas::event::components::TextInput;
use kas::event::{FocusSource, ImeSurroundingText, Scroll};
use kas::geom::Vec2;
use kas::prelude::*;
use kas::text::{CursorRange, NotReady, SelectionHelper};
use kas::theme::{Text, TextClass};
use kas::util::UndoStack;
use std::borrow::Cow;
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
#[autoimpl(Debug)]
pub struct Editor {
pub(super) id: Id,
pub(super) pos: Coord,
pub(super) editable: bool,
pub(super) text: Text<String>,
pub(super) selection: SelectionHelper,
pub(super) edit_x_coord: Option<f32>,
last_edit: Option<EditOp>,
undo_stack: UndoStack<(String, CursorRange)>,
pub(super) has_key_focus: bool,
pub(super) current: CurrentAction,
error_state: bool,
error_message: Option<Cow<'static, str>>,
pub(super) input_handler: TextInput,
}
impl Editor {
#[inline]
pub(super) fn new() -> Self {
Editor {
id: Id::default(),
pos: Coord::ZERO,
editable: true,
text: Text::new(String::new(), TextClass::Editor, false),
selection: Default::default(),
edit_x_coord: None,
last_edit: Some(EditOp::Initial),
undo_stack: UndoStack::new(),
has_key_focus: false,
current: CurrentAction::None,
error_state: false,
error_message: None,
input_handler: Default::default(),
}
}
#[inline]
pub(super) fn from<S: ToString>(text: S) -> Self {
let text = text.to_string();
let len = text.len();
Editor {
text: Text::new(text, TextClass::Editor, false),
selection: SelectionHelper::from(len),
..Editor::new()
}
}
pub(super) fn cancel_selection_and_ime(&mut self, cx: &mut EventState) {
if self.current == CurrentAction::Selection {
self.input_handler.stop_selecting();
self.current = CurrentAction::None;
} else if self.current.is_ime_enabled() {
self.clear_ime();
cx.cancel_ime_focus(&self.id);
}
}
pub(super) fn clear_ime(&mut self) {
if self.current.is_ime_enabled() {
let action = std::mem::replace(&mut self.current, CurrentAction::None);
if let CurrentAction::ImePreedit { edit_range } = action {
self.selection.set_cursor(edit_range.start.cast());
self.text.replace_range(edit_range.cast(), "");
}
}
}
pub(super) fn ime_surrounding_text(&self) -> Option<ImeSurroundingText> {
const MAX_TEXT_BYTES: usize = ImeSurroundingText::MAX_TEXT_BYTES;
let sel_range = self.selection.range();
let edit_range = match self.current.clone() {
CurrentAction::ImePreedit { edit_range } => Some(edit_range.cast()),
_ => None,
};
let mut range = edit_range.clone().unwrap_or(sel_range);
let initial_range = range.clone();
let edit_len = edit_range.clone().map(|r| r.len()).unwrap_or(0);
if let Ok(Some((_, line_range))) = self.text.find_line(range.start) {
range.start = line_range.start;
}
if let Ok(Some((_, line_range))) = self.text.find_line(range.end) {
range.end = line_range.end;
}
if range.len() - edit_len > MAX_TEXT_BYTES {
range.end = range.end.min(initial_range.end + MAX_TEXT_BYTES / 2);
while !self.text.as_str().is_char_boundary(range.end) {
range.end -= 1;
}
if range.len() - edit_len > MAX_TEXT_BYTES {
range.start = range.start.max(initial_range.start - MAX_TEXT_BYTES / 2);
while !self.text.as_str().is_char_boundary(range.start) {
range.start += 1;
}
}
}
let start = range.start;
let mut text = String::with_capacity(range.len() - edit_len);
if let Some(er) = edit_range {
text.push_str(&self.text.as_str()[range.start..er.start]);
text.push_str(&self.text.as_str()[er.end..range.end]);
} else {
text = self.text.as_str()[range].to_string();
}
let cursor = self.selection.edit_index().saturating_sub(start);
let sel_index = self.selection.sel_index().saturating_sub(start);
ImeSurroundingText::new(text, cursor, sel_index)
.inspect_err(|err| {
log::warn!("Editor::ime_surrounding_text failed: {err:?}")
})
.ok()
}
pub(super) fn set_ime_cursor_area(&self, cx: &mut EventState) {
if let Ok(text) = self.text.display() {
let range = match self.current.clone() {
CurrentAction::ImeStart => self.selection.range(),
CurrentAction::ImePreedit { edit_range } => edit_range.cast(),
_ => return,
};
let (m1, m2);
if range.is_empty() {
let mut iter = text.text_glyph_pos(range.start);
m1 = iter.next();
m2 = iter.next();
} else {
m1 = text.text_glyph_pos(range.start).next_back();
m2 = text.text_glyph_pos(range.end).next();
}
let rect = if let Some((c1, c2)) = m1.zip(m2) {
let left = c1.pos.0.min(c2.pos.0);
let right = c1.pos.0.max(c2.pos.0);
let top = (c1.pos.1 - c1.ascent).min(c2.pos.1 - c2.ascent);
let bottom = (c1.pos.1 - c1.descent).max(c2.pos.1 - c2.ascent);
let p1 = Vec2(left, top).cast_floor();
let p2 = Vec2(right, bottom).cast_ceil();
Rect::from_coords(p1, p2)
} else if let Some(c) = m1.or(m2) {
let p1 = Vec2(c.pos.0, c.pos.1 - c.ascent).cast_floor();
let p2 = Vec2(c.pos.0, c.pos.1 - c.descent).cast_ceil();
Rect::from_coords(p1, p2)
} else {
return;
};
cx.set_ime_cursor_area(&self.id, rect + Offset::conv(self.pos));
}
}
pub(super) fn clear_error(&mut self) {
self.error_state = false;
self.error_message = None;
}
pub(super) fn tooltip(&self) -> Option<&str> {
self.error_message.as_deref()
}
pub(super) fn save_undo_state(&mut self, edit: Option<EditOp>) {
if let Some(op) = edit
&& op.try_merge(&mut self.last_edit)
{
return;
}
self.last_edit = edit;
self.undo_stack
.try_push((self.clone_string(), self.cursor_range()));
}
pub(super) fn prepare_and_scroll(&mut self, cx: &mut EventCx, force_set_offset: bool) {
let bb = self.text.bounding_box();
if self.text.prepare() {
self.text.ensure_no_left_overhang();
cx.redraw();
}
let mut set_offset = force_set_offset;
if bb != self.text.bounding_box() {
cx.resize();
set_offset = true;
}
if set_offset {
self.set_view_offset_from_cursor(cx);
}
}
pub(super) fn received_text(&mut self, cx: &mut EventCx, text: &str) -> IsUsed {
if !self.editable {
return Unused;
}
self.cancel_selection_and_ime(cx);
let index = self.selection.edit_index();
let selection = self.selection.range();
let have_sel = selection.start < selection.end;
if have_sel {
self.text.replace_range(selection.clone(), text);
self.selection.set_cursor(selection.start + text.len());
} else {
self.text.insert_str(index, text);
self.selection.set_cursor(index + text.len());
}
self.edit_x_coord = None;
self.prepare_and_scroll(cx, false);
Used
}
pub(super) fn request_key_focus(&self, cx: &mut EventCx, source: FocusSource) {
if !self.has_key_focus && !self.current.is_ime_enabled() {
cx.request_key_focus(self.id(), source);
}
}
pub(super) fn trim_paste(&self, text: &str) -> Range<usize> {
let mut end = text.len();
if !self.multi_line() {
for (i, c) in text.char_indices() {
if c < '\u{20}' || ('\u{7f}'..='\u{9f}').contains(&c) {
end = i;
break;
}
}
}
0..end
}
pub(super) fn cmd_action(
&mut self,
cx: &mut EventCx,
cmd: Command,
) -> Result<CmdAction, NotReady> {
let editable = self.editable;
let mut shift = cx.modifiers().shift_key();
let mut buf = [0u8; 4];
let cursor = self.selection.edit_index();
let len = self.text.str_len();
let multi_line = self.multi_line();
let selection = self.selection.range();
let have_sel = selection.end > selection.start;
let string;
enum Action<'a> {
None,
Deselect,
Activate,
Insert(&'a str, EditOp),
Delete(Range<usize>, EditOp),
Move(usize, Option<f32>),
UndoRedo(bool),
}
let action = match cmd {
Command::Escape | Command::Deselect if !selection.is_empty() => Action::Deselect,
Command::Activate => Action::Activate,
Command::Enter if shift || !multi_line => Action::Activate,
Command::Enter if editable && multi_line => {
Action::Insert('\n'.encode_utf8(&mut buf), EditOp::KeyInput)
}
Command::Left | Command::Home if !shift && have_sel => {
Action::Move(selection.start, None)
}
Command::Left if cursor > 0 => GraphemeCursor::new(cursor, len, true)
.prev_boundary(self.text.text(), 0)
.unwrap()
.map(|index| Action::Move(index, None))
.unwrap_or(Action::None),
Command::Right | Command::End if !shift && have_sel => {
Action::Move(selection.end, None)
}
Command::Right if cursor < len => GraphemeCursor::new(cursor, len, true)
.next_boundary(self.text.text(), 0)
.unwrap()
.map(|index| Action::Move(index, None))
.unwrap_or(Action::None),
Command::WordLeft if cursor > 0 => {
let mut iter = self.text.text()[0..cursor].split_word_bound_indices();
let mut p = iter.next_back().map(|(index, _)| index).unwrap_or(0);
while self.text.text()[p..]
.chars()
.next()
.map(|c| c.is_whitespace())
.unwrap_or(false)
{
if let Some((index, _)) = iter.next_back() {
p = index;
} else {
break;
}
}
Action::Move(p, None)
}
Command::WordRight if cursor < len => {
let mut iter = self.text.text()[cursor..]
.split_word_bound_indices()
.skip(1);
let mut p = iter.next().map(|(index, _)| cursor + index).unwrap_or(len);
while self.text.text()[p..]
.chars()
.next()
.map(|c| c.is_whitespace())
.unwrap_or(false)
{
if let Some((index, _)) = iter.next() {
p = cursor + index;
} else {
break;
}
}
Action::Move(p, None)
}
Command::Left | Command::Right | Command::WordLeft | Command::WordRight => Action::None,
Command::Up | Command::Down if multi_line => {
let x = match self.edit_x_coord {
Some(x) => x,
None => self
.text
.text_glyph_pos(cursor)?
.next_back()
.map(|r| r.pos.0)
.unwrap_or(0.0),
};
let mut line = self.text.find_line(cursor)?.map(|r| r.0).unwrap_or(0);
line = match cmd {
Command::Up => line.wrapping_sub(1),
Command::Down => line.wrapping_add(1),
_ => unreachable!(),
};
const HALF: usize = usize::MAX / 2;
let nearest_end = match line {
0..=HALF => len,
_ => 0,
};
self.text
.line_index_nearest(line, x)?
.map(|index| Action::Move(index, Some(x)))
.unwrap_or(Action::Move(nearest_end, None))
}
Command::Home if cursor > 0 => {
let index = self.text.find_line(cursor)?.map(|r| r.1.start).unwrap_or(0);
Action::Move(index, None)
}
Command::End if cursor < len => {
let index = self.text.find_line(cursor)?.map(|r| r.1.end).unwrap_or(len);
Action::Move(index, None)
}
Command::DocHome if cursor > 0 => Action::Move(0, None),
Command::DocEnd if cursor < len => Action::Move(len, None),
Command::Home | Command::End | Command::DocHome | Command::DocEnd => Action::None,
Command::PageUp | Command::PageDown if multi_line => {
let mut v = self
.text
.text_glyph_pos(cursor)?
.next_back()
.map(|r| r.pos.into())
.unwrap_or(Vec2::ZERO);
if let Some(x) = self.edit_x_coord {
v.0 = x;
}
const FACTOR: f32 = 2.0 / 3.0;
let mut h_dist = f32::conv(self.text.rect().size.1) * FACTOR;
if cmd == Command::PageUp {
h_dist *= -1.0;
}
v.1 += h_dist;
Action::Move(self.text.text_index_nearest(v)?, Some(v.0))
}
Command::Delete | Command::DelBack if editable && have_sel => {
Action::Delete(selection.clone(), EditOp::Delete)
}
Command::Delete if editable => GraphemeCursor::new(cursor, len, true)
.next_boundary(self.text.text(), 0)
.unwrap()
.map(|next| Action::Delete(cursor..next, EditOp::Delete))
.unwrap_or(Action::None),
Command::DelBack if editable => GraphemeCursor::new(cursor, len, true)
.prev_boundary(self.text.text(), 0)
.unwrap()
.map(|prev| Action::Delete(prev..cursor, EditOp::Delete))
.unwrap_or(Action::None),
Command::DelWord if editable => {
let next = self.text.text()[cursor..]
.split_word_bound_indices()
.nth(1)
.map(|(index, _)| cursor + index)
.unwrap_or(len);
Action::Delete(cursor..next, EditOp::Delete)
}
Command::DelWordBack if editable => {
let prev = self.text.text()[0..cursor]
.split_word_bound_indices()
.next_back()
.map(|(index, _)| index)
.unwrap_or(0);
Action::Delete(prev..cursor, EditOp::Delete)
}
Command::SelectAll => {
self.selection.set_sel_index(0);
shift = true; Action::Move(len, None)
}
Command::Cut if editable && have_sel => {
cx.set_clipboard((self.text.text()[selection.clone()]).into());
Action::Delete(selection.clone(), EditOp::Clipboard)
}
Command::Copy if have_sel => {
cx.set_clipboard((self.text.text()[selection.clone()]).into());
Action::None
}
Command::Paste if editable => {
if let Some(content) = cx.get_clipboard() {
let range = self.trim_paste(&content);
string = content;
Action::Insert(&string[range], EditOp::Clipboard)
} else {
Action::None
}
}
Command::Undo | Command::Redo if editable => Action::UndoRedo(cmd == Command::Redo),
_ => return Ok(CmdAction::Unused),
};
if !matches!(action, Action::None | Action::Deselect) {
self.request_key_focus(cx, FocusSource::Synthetic);
}
if !matches!(action, Action::None) {
self.cancel_selection_and_ime(cx);
}
let edit_op = match action {
Action::None => return Ok(CmdAction::Used),
Action::Deselect | Action::Move(_, _) => Some(EditOp::Cursor),
Action::Activate | Action::UndoRedo(_) => None,
Action::Insert(_, edit) | Action::Delete(_, edit) => Some(edit),
};
self.save_undo_state(edit_op);
Ok(match action {
Action::None => unreachable!(),
Action::Deselect => {
self.selection.set_empty();
cx.redraw();
CmdAction::Cursor
}
Action::Activate => CmdAction::Activate,
Action::Insert(s, _) => {
let mut index = cursor;
let range = if have_sel {
index = selection.start;
selection.clone()
} else {
index..index
};
self.text.replace_range(range, s);
self.selection.set_cursor(index + s.len());
self.edit_x_coord = None;
CmdAction::Edit
}
Action::Delete(sel, _) => {
self.text.replace_range(sel.clone(), "");
self.selection.set_cursor(sel.start);
self.edit_x_coord = None;
CmdAction::Edit
}
Action::Move(index, x_coord) => {
self.selection.set_edit_index(index);
if !shift {
self.selection.set_empty();
} else {
self.set_primary(cx);
}
self.edit_x_coord = x_coord;
cx.redraw();
CmdAction::Cursor
}
Action::UndoRedo(redo) => {
if let Some((text, cursor)) = self.undo_stack.undo_or_redo(redo) {
if self.text.set_str(text) {
self.edit_x_coord = None;
}
self.selection = (*cursor).into();
CmdAction::Edit
} else {
CmdAction::Used
}
}
})
}
pub(super) fn set_cursor_from_coord(&mut self, cx: &mut EventCx, coord: Coord) {
let rel_pos = (coord - self.pos).cast();
if let Ok(index) = self.text.text_index_nearest(rel_pos) {
if index != self.selection.edit_index() {
self.selection.set_edit_index(index);
self.set_view_offset_from_cursor(cx);
self.edit_x_coord = None;
cx.redraw();
}
}
}
pub(super) fn set_primary(&self, cx: &mut EventCx) {
if self.has_key_focus && !self.selection.is_empty() && cx.has_primary() {
let range = self.selection.range();
cx.set_primary(String::from(&self.text.as_str()[range]));
}
}
pub(super) fn set_view_offset_from_cursor(&mut self, cx: &mut EventCx) {
let cursor = self.selection.edit_index();
if let Some(marker) = self
.text
.text_glyph_pos(cursor)
.ok()
.and_then(|mut m| m.next_back())
{
let y0 = (marker.pos.1 - marker.ascent).cast_floor();
let pos = self.pos + Offset(marker.pos.0.cast_nearest(), y0);
let size = Size(0, i32::conv_ceil(marker.pos.1 - marker.descent) - y0);
cx.set_scroll(Scroll::Rect(Rect { pos, size }));
}
}
}
impl Editor {
#[inline]
pub fn id_ref(&self) -> &Id {
&self.id
}
#[inline]
pub fn id(&self) -> Id {
self.id.clone()
}
#[inline]
pub fn as_str(&self) -> &str {
self.text.as_str()
}
#[inline]
pub fn clone_string(&self) -> String {
self.text.clone_string()
}
#[inline]
pub fn pre_commit(&mut self) {
self.save_undo_state(Some(EditOp::Synthetic));
}
#[inline]
pub fn clear(&mut self, cx: &mut EventState) {
self.last_edit = Some(EditOp::Initial);
self.undo_stack.clear();
self.set_string(cx, String::new());
}
#[inline]
pub fn set_str(&mut self, cx: &mut EventState, text: &str) -> bool {
if self.text.as_str() != text {
self.set_string(cx, text.to_string());
true
} else {
false
}
}
pub fn set_string(&mut self, cx: &mut EventState, string: String) -> bool {
self.cancel_selection_and_ime(cx);
if !self.text.set_string(string) {
return false;
}
let len = self.text.str_len();
self.selection.set_max_len(len);
self.edit_x_coord = None;
self.clear_error();
self.text.prepare()
}
#[inline]
pub fn replace_selected_text(&mut self, cx: &mut EventState, text: &str) -> bool {
self.cancel_selection_and_ime(cx);
let index = self.selection.edit_index();
let selection = self.selection.range();
let have_sel = selection.start < selection.end;
if have_sel {
self.text.replace_range(selection.clone(), text);
self.selection.set_cursor(selection.start + text.len());
} else {
self.text.insert_str(index, text);
self.selection.set_cursor(index + text.len());
}
self.edit_x_coord = None;
self.clear_error();
self.text.prepare()
}
#[inline]
pub fn cursor_range(&self) -> CursorRange {
*self.selection
}
#[inline]
pub fn set_cursor_range(&mut self, range: impl Into<CursorRange>) {
self.edit_x_coord = None;
self.selection = range.into().into();
}
#[inline]
pub fn is_editable(&self) -> bool {
self.editable
}
#[inline]
pub fn set_editable(&mut self, editable: bool) {
self.editable = editable;
}
#[inline]
pub fn multi_line(&self) -> bool {
self.text.wrap()
}
#[inline]
pub fn class(&self) -> TextClass {
self.text.class()
}
#[inline]
pub fn has_input_focus(&self) -> bool {
self.has_key_focus || self.current.is_ime_enabled()
}
#[inline]
pub fn has_error(&self) -> bool {
self.error_state
}
pub fn set_error(&mut self, cx: &mut EventState, message: Option<Cow<'static, str>>) {
self.error_state = true;
self.error_message = message;
cx.redraw(&self.id);
}
}