use crate::prelude::*; use iris_core::{TextData, UiColor}; use parley::{Affinity, Layout, Selection}; use std::ops::{Deref, DerefMut}; use winit::{ event::KeyEvent, keyboard::{Key, NamedKey}, }; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Motion { Left, Right, LeftWord, RightWord, Up, Down, LineStart, LineEnd, } pub struct TextEdit { view: TextView, /// `None` represents unfocused, which Parley's `Selection` cannot express. selection: Option, history: Vec<(String, Option)>, double_hit: Option, pub mode: EditMode, } #[derive(Clone, Copy, PartialEq)] pub enum EditMode { SingleLine, MultiLine, } impl TextEdit { pub fn new(view: TextView, mode: EditMode) -> Self { Self { view, selection: None, history: Default::default(), double_hit: None, mode, } } pub fn selected_text(&self) -> Option { let sel = self.selection?; if sel.is_collapsed() { return None; } Some(self.buf.text()[sel.text_range()].to_string()) } } impl Widget for TextEdit { fn draw(&mut self, painter: &mut Painter) -> Size { let base = painter.layer; painter.child_layer(); let (_, size) = self.view.draw(painter); painter.layer = base; let region = self.region(); let Some(selection) = self.selection else { return size; }; let layout = self.view.buf.layout(); // parley reports selection as boxes in layout space, so bidi and // wrapped lines come out right without this code knowing about either. for (rect, _) in selection.geometry(layout) { let rect_size = vec2(rect.width() as f32, rect.height() as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32); painter.primitive_within( RectPrimitive::color(Color::SKY), rect_size .align(Align::TOP_LEFT) .offset(top_left) .within(®ion), ); } let caret = selection.focus().geometry(layout, CARET_WIDTH); let caret_size = vec2(caret.width() as f32, caret.height() as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32); painter.primitive_within( RectPrimitive::color(Color::WHITE), caret_size .align(Align::TOP_LEFT) .offset(top_left) .within(®ion), ); size } } const CARET_WIDTH: f32 = 1.0; pub struct TextEditCtx<'a> { pub text: &'a mut TextEdit, pub data: &'a mut TextData, } impl<'a> TextEditCtx<'a> { fn layout(&mut self) -> &Layout { let attrs = self.text.view.attrs.clone(); let width = self.text.view.wrap_width(); self.text.view.buf.shape(self.data, &attrs, width); self.text.view.buf.layout() } fn clamp_selection_to_layout(&mut self) { if let Some(sel) = self.text.selection { let layout = self.layout(); self.text.selection = Some(sel.refresh(layout)); } } pub fn take(&mut self) -> String { let text = std::mem::take(self.text.view.buf.edit()); self.text.selection = None; text } pub fn set(&mut self, text: &str) { let text = self.string(text); self.text.view.buf.set_text(text); self.text.selection = None; } pub fn motion(&mut self, motion: Motion, select: bool) { let Some(sel) = self.text.selection else { return; }; let layout = self.layout(); let sel = apply_motion(sel, layout, motion, select); self.text.selection = Some(sel); } /// Replace the `len` characters before the caret. This is the IME's /// preedit path: it re-sends the whole composition each time. pub fn replace(&mut self, len: usize, text: &str) { let text = self.string(text); for _ in 0..len { self.backspace(false); } self.insert_str(&text); } fn string(&self, text: &str) -> String { if self.text.mode == EditMode::SingleLine { text.replace('\n', "") } else { text.to_string() } } pub fn insert(&mut self, text: &str) { let text = self.string(text); self.insert_str(&text); } fn insert_str(&mut self, text: &str) { if text.is_empty() { return; } self.clear_span(); let at = match self.text.selection { Some(sel) => sel.focus().index(), None => return, }; let at = at.min(self.text.view.buf.text().len()); self.text.view.buf.edit().insert_str(at, text); self.set_caret(at + text.len()); } pub fn clear_span(&mut self) -> bool { let Some(sel) = self.text.selection else { return false; }; if sel.is_collapsed() { return false; } let range = sel.text_range(); self.text.view.buf.edit().replace_range(range.clone(), ""); self.set_caret(range.start); true } fn set_caret(&mut self, index: usize) { let index = index.min(self.text.view.buf.text().len()); let layout = self.layout(); self.text.selection = Some(Selection::from_byte_index( layout, index, Affinity::default(), )); } pub fn newline(&mut self) { if self.text.mode == EditMode::MultiLine { self.insert_str("\n"); } } pub fn backspace(&mut self, word: bool) { if self.clear_span() { return; } let Some(sel) = self.text.selection else { return; }; let end = sel.focus().index(); if end == 0 { return; } let layout = self.layout(); let start = if word { sel.focus().previous_logical_word(layout).index() } else { let Some(cluster) = sel.focus().logical_clusters(layout)[0] else { return; }; let range = cluster.text_range(); if cluster.is_hard_line_break() || cluster.is_emoji() { range.start } else { self.text.view.buf.text()[..range.end] .char_indices() .next_back() .map_or(range.start, |(start, _)| start) } }; self.delete_range(start, end); } pub fn delete(&mut self, word: bool) { if self.clear_span() { return; } let Some(sel) = self.text.selection else { return; }; let start = sel.focus().index(); if start >= self.text.view.buf.text().len() { return; } let layout = self.layout(); let end = if word { sel.focus().next_logical_word(layout).index() } else { let clusters = sel.focus().logical_clusters(layout); let Some(cluster) = clusters[1].as_ref() else { return; }; cluster.text_range().end }; self.delete_range(start, end); } fn delete_range(&mut self, start: usize, end: usize) { self.text.view.buf.edit().replace_range(start..end, ""); self.set_caret(start); } pub fn select_all(&mut self) { let len = self.text.view.buf.text().len(); if len == 0 { return; } let layout = self.layout(); let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default()); let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default()); self.text.selection = Some(Selection::new(anchor, focus)); } pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { let pos = pos - self.text.region().top_left().to_px(size); let prev_sel = self.text.selection; let prev_hit = self.text.double_hit; let layout = self.layout(); let (selection, double_hit) = if drag { let Some(selection) = prev_sel else { return; }; (selection.extend_to_point(layout, pos.x, pos.y), prev_hit) } else { let hit = Selection::from_point(layout, pos.x, pos.y); let index = hit.focus().index(); // Successive clicks at one index select the word, then the line. if recent && prev_hit == Some(index) { (Selection::line_from_point(layout, pos.x, pos.y), None) } else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) { ( Selection::word_from_point(layout, pos.x, pos.y), Some(index), ) } else { (hit, None) } }; self.text.selection = Some(selection); self.text.double_hit = double_hit; } pub fn deselect(&mut self) { self.text.selection = None; self.text.double_hit = None; } pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { let old = (self.text.view.buf.text().to_string(), self.text.selection); let mut undo = false; let res = self.apply_event_inner(event, modifiers, &mut undo); if undo { if let Some((old, selection)) = self.text.history.pop() { self.set(&old); self.text.selection = selection; self.clamp_selection_to_layout(); } } else if self.text.view.buf.text() != old.0 { self.text.history.push(old); } res } fn apply_event_inner( &mut self, event: &KeyEvent, modifiers: &Modifiers, undo: &mut bool, ) -> TextInputResult { match &event.logical_key { Key::Named(named) => match named { NamedKey::Backspace => self.backspace(modifiers.control), NamedKey::Delete => self.delete(modifiers.control), NamedKey::Space => self.insert(" "), NamedKey::Enter => { if modifiers.shift { self.newline(); } else { return TextInputResult::Submit; } } NamedKey::ArrowRight => { let motion = if modifiers.control { Motion::RightWord } else { Motion::Right }; self.motion(motion, modifiers.shift); } NamedKey::ArrowLeft => { let motion = if modifiers.control { Motion::LeftWord } else { Motion::Left }; self.motion(motion, modifiers.shift); } NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift), NamedKey::ArrowDown => self.motion(Motion::Down, modifiers.shift), NamedKey::Home => self.motion(Motion::LineStart, modifiers.shift), NamedKey::End => self.motion(Motion::LineEnd, modifiers.shift), NamedKey::Escape => { self.deselect(); return TextInputResult::Unfocus; } _ => return TextInputResult::Unused, }, Key::Character(text) => { if modifiers.control { match text.as_str() { "v" => return TextInputResult::Paste, "c" => { if let Some(content) = self.text.selected_text() { return TextInputResult::Copy(content); } } "x" => { if let Some(content) = self.text.selected_text() { self.clear_span(); return TextInputResult::Copy(content); } } "a" => self.select_all(), "z" => *undo = true, _ => self.insert(text), } } else { self.insert(text); } } _ => return TextInputResult::Unused, } TextInputResult::Used } } fn apply_motion( sel: Selection, layout: &Layout, motion: Motion, extend: bool, ) -> Selection { match motion { Motion::Left => sel.previous_visual(layout, extend), Motion::Right => sel.next_visual(layout, extend), Motion::LeftWord => sel.previous_visual_word(layout, extend), Motion::RightWord => sel.next_visual_word(layout, extend), Motion::Up => sel.previous_line(layout, extend), Motion::Down => sel.next_line(layout, extend), Motion::LineStart => sel.line_start(layout, extend), Motion::LineEnd => sel.line_end(layout, extend), } } #[derive(Default)] pub struct Modifiers { pub shift: bool, pub control: bool, } impl Modifiers { pub fn clear(&mut self) { self.shift = false; self.control = false; } } pub enum TextInputResult { Used, Unused, Unfocus, Submit, Copy(String), Paste, } impl TextInputResult { pub fn unfocus(&self) -> bool { matches!(self, TextInputResult::Unfocus) } } impl Deref for TextEdit { type Target = TextView; fn deref(&self) -> &Self::Target { &self.view } } impl DerefMut for TextEdit { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.view } } pub trait TextEditable { fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a>; } impl> TextEditable for I { fn edit<'a>(&self, ui: &'a mut impl UiRsc) -> TextEditCtx<'a> { let ui = ui.ui_mut(); TextEditCtx { text: ui.widgets.get_mut(self).unwrap(), data: &mut ui.text, } } }