use std::ops::{Deref, DerefMut}; use crate::prelude::*; use cosmic_text::{Affinity, Attrs, Cursor, FontSystem, LayoutRun, Motion}; use unicode_segmentation::UnicodeSegmentation; use winit::{ event::KeyEvent, keyboard::{Key, NamedKey}, }; pub struct TextEdit { view: TextView, selection: TextSelection, history: Vec<(String, TextSelection)>, double_hit: Option, pub single_line: bool, } impl TextEdit { pub fn new(view: TextView, single_line: bool) -> Self { Self { view, selection: Default::default(), history: Default::default(), double_hit: None, single_line, } } pub fn select_content(&self, start: Cursor, end: Cursor) -> String { let (start, end) = sort_cursors(start, end); let mut iter = self.buf.lines.iter().skip(start.line); let first = iter.next().unwrap(); if start.line == end.line { first.text()[start.index..end.index].to_string() } else { let mut str = first.text()[start.index..].to_string(); for _ in (start.line + 1)..end.line { str = str + "\n" + iter.next().unwrap().text(); } let last = iter.next().unwrap(); str = str + "\n" + &last.text()[..end.index]; str } } } impl Widget for TextEdit { fn draw(&mut self, painter: &mut Painter) { let base = painter.layer; painter.child_layer(); self.view.draw(painter); painter.layer = base; let region = self.region(); let size = vec2(1, self.attrs.line_height); match self.selection { TextSelection::None => (), TextSelection::Pos(cursor) => { if let Some(offset) = cursor_pos(cursor, &self.buf) { painter.primitive_within( RectPrimitive::color(Color::WHITE), size.align(Align::TOP_LEFT).offset(offset).within(®ion), ); } } TextSelection::Span { start, end } => { let (start, end) = sort_cursors(start, end); for (l, x, width) in iter_layout_lines(start, end, &self.buf) { let top_left = vec2(x, self.attrs.line_height * l as f32); painter.primitive_within( RectPrimitive::color(Color::SKY), size.with_x(width) .align(Align::TOP_LEFT) .offset(top_left) .within(®ion), ); } if let Some(end_offset) = cursor_pos(end, &self.buf) { painter.primitive_within( RectPrimitive::color(Color::WHITE), size.align(Align::TOP_LEFT) .offset(end_offset) .within(®ion), ); } } } } fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len { self.view.desired_width(ctx) } fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len { self.view.desired_height(ctx) } } /// provides top left + width fn iter_layout_lines( start: Cursor, end: Cursor, buf: &TextBuffer, ) -> impl Iterator { gen move { let mut iter = buf.layout_runs().enumerate(); for (i, line) in iter.by_ref() { if line.line_i == start.line && let Some(start_x) = index_x(&line, start.index) { if start.line == end.line && let Some(end_x) = index_x(&line, end.index) { yield (i, start_x, end_x - start_x); return; } yield (i, start_x, line.line_w - start_x); break; } } for (i, line) in iter { if line.line_i > end.line { return; } if line.line_i == end.line && let Some(end_x) = index_x(&line, end.index) { yield (i, 0.0, end_x); return; } yield (i, 0.0, line.line_w); } } } /// copied & modified from fn found in Editor in cosmic_text /// returns x pos of a (non layout) index within an layout run fn index_x(run: &LayoutRun, index: usize) -> Option { for glyph in run.glyphs.iter() { if index == glyph.start { return Some(glyph.x); } else if index > glyph.start && index < glyph.end { // Guess x offset based on characters let mut before = 0; let mut total = 0; let cluster = &run.text[glyph.start..glyph.end]; for (i, _) in cluster.grapheme_indices(true) { if glyph.start + i < index { before += 1; } total += 1; } let offset = glyph.w * (before as f32) / (total as f32); return Some(glyph.x + offset); } } None } /// returns top of line segment where cursor should visually select fn cursor_pos(cursor: Cursor, buf: &TextBuffer) -> Option { let mut prev = None; for run in buf .layout_runs() .skip_while(|r| r.line_i < cursor.line) .take_while(|r| r.line_i == cursor.line) { prev = Some(vec2(run.line_w, run.line_top)); if let Some(pos) = index_x(&run, cursor.index) { return Some(vec2(pos, run.line_top)); } } prev } pub struct TextEditCtx<'a> { pub text: &'a mut TextEdit, pub font_system: &'a mut FontSystem, } impl<'a> TextEditCtx<'a> { pub fn take(&mut self) -> String { let text = self .text .buf .lines .drain(..) .map(|l| l.into_text()) .collect::>() .join("\n"); self.text .buf .set_text(self.font_system, "", &Attrs::new(), SHAPING, None); self.text.selection.clear(); text } pub fn set(&mut self, text: &str) { let text = self.string(text); self.text .buf .set_text(self.font_system, &text, &Attrs::new(), SHAPING, None); self.text.selection.clear(); } pub fn motion(&mut self, motion: Motion, select: bool) { if let TextSelection::Pos(cursor) = self.text.selection && let Some(new) = self.buf_motion(cursor, motion) { if select { self.text.selection = TextSelection::Span { start: cursor, end: new, }; } else { self.text.selection = TextSelection::Pos(new); } } else if let TextSelection::Span { start, end } = self.text.selection { if select { if let Some(cursor) = self.buf_motion(end, motion) { self.text.selection = TextSelection::Span { start, end: cursor }; } } else { let (start, end) = sort_cursors(start, end); let sel = &mut self.text.selection; match motion { Motion::Left | Motion::LeftWord => *sel = TextSelection::Pos(start), Motion::Right | Motion::RightWord => *sel = TextSelection::Pos(end), _ => { if let Some(cursor) = self.buf_motion(end, motion) { self.text.selection = TextSelection::Pos(cursor); } } } } } } pub fn replace(&mut self, len: usize, text: &str) { let text = self.string(text); for _ in 0..len { self.delete(false); } self.insert_inner(&text, false); } fn string(&self, text: &str) -> String { if self.text.single_line { text.replace('\n', "") } else { text.to_string() } } pub fn insert(&mut self, text: &str) { let text = self.string(text); let mut lines = text.split('\n'); let Some(first) = lines.next() else { return; }; self.insert_inner(first, true); for line in lines { self.newline(); self.insert_inner(line, true); } } pub fn clear_span(&mut self) -> bool { if let TextSelection::Span { start, end } = self.text.selection { self.delete_between(start, end); let (start, _) = sort_cursors(start, end); self.text.selection = TextSelection::Pos(start); true } else { false } } pub fn delete_between(&mut self, start: Cursor, end: Cursor) { let lines = &mut self.text.view.buf.lines; let (start, end) = sort_cursors(start, end); if start.line == end.line { let line = &mut lines[start.line]; let text = line.text(); let text = text[..start.index].to_string() + &text[end.index..]; edit_line(line, text); } else { // start let start_text = lines[start.line].text()[..start.index].to_string(); let end_text = &lines[end.line].text()[end.index..]; let text = start_text + end_text; edit_line(&mut lines[start.line], text); } // between let range = (start.line + 1)..=end.line; if !range.is_empty() { lines.splice(range, None); } } fn insert_inner(&mut self, text: &str, mov: bool) { self.clear_span(); if let TextSelection::Pos(cursor) = &mut self.text.selection { let line = &mut self.text.view.buf.lines[cursor.line]; let mut line_text = line.text().to_string(); line_text.insert_str(cursor.index, text); edit_line(line, line_text); if mov { for _ in 0..text.chars().count() { self.motion(Motion::Right, false); } } } } pub fn newline(&mut self) { if self.text.single_line { return; } self.clear_span(); if let TextSelection::Pos(cursor) = &mut self.text.selection { let lines = &mut self.text.view.buf.lines; let line = &mut lines[cursor.line]; let new = line.split_off(cursor.index); cursor.line += 1; lines.insert(cursor.line, new); cursor.index = 0; } } pub fn backspace(&mut self, word: bool) { if !self.clear_span() && let TextSelection::Pos(cursor) = &mut self.text.selection && (cursor.index != 0 || cursor.line != 0) { self.motion(if word { Motion::LeftWord } else { Motion::Left }, false); self.delete(word); } } pub fn delete(&mut self, word: bool) { if !self.clear_span() && let TextSelection::Pos(cursor) = &mut self.text.selection { if word { let start = *cursor; if let Some(end) = self.buf_motion(start, Motion::RightWord) { self.delete_between(start, end); } } else { let lines = &mut self.text.view.buf.lines; let line = &mut lines[cursor.line]; if cursor.index == line.text().len() { if cursor.line == lines.len() - 1 { return; } let add = lines.remove(cursor.line + 1).into_text(); let line = &mut lines[cursor.line]; let mut cur = line.text().to_string(); cur.push_str(&add); edit_line(line, cur); } else { let mut text = line.text().to_string(); text.remove(cursor.index); edit_line(line, text); } } } } fn buf_motion(&mut self, cursor: Cursor, motion: Motion) -> Option { self.text .buf .cursor_motion(self.font_system, cursor, None, motion) .map(|r| r.0) } pub fn select_word_at(&mut self, cursor: Cursor) { if let (Some(start), Some(end)) = ( self.buf_motion(cursor, Motion::LeftWord), self.buf_motion(cursor, Motion::RightWord), ) { self.text.selection = TextSelection::Span { start, end }; } } pub fn select_line_at(&mut self, cursor: Cursor) { let end = self.text.buf.lines[cursor.line].text().len(); self.text.selection = TextSelection::Span { start: Cursor::new(cursor.line, 0), end: Cursor::new(cursor.line, end), } } pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { let pos = pos - self.text.region().top_left().to_abs(size); let hit = self.text.buf.hit(pos.x, pos.y); let sel = &mut self.text.selection; match sel { TextSelection::None => { if !drag && let Some(hit) = hit { *sel = TextSelection::Pos(hit) } } TextSelection::Pos(pos) => match (hit, drag) { (None, false) => *sel = TextSelection::None, (None, true) => (), (Some(hit), false) => { if recent && hit == *pos { self.text.double_hit = Some(hit); return self.select_word_at(hit); } else { *pos = hit } } (Some(end), true) => *sel = TextSelection::Span { start: *pos, end }, }, TextSelection::Span { start, end } => match (hit, drag) { (None, false) => *sel = TextSelection::None, (None, true) => *sel = TextSelection::Pos(*start), (Some(hit), false) => { if recent && let Some(double) = self.text.double_hit && double == hit { return self.select_line_at(hit); } else { *sel = TextSelection::Pos(hit) } } (Some(hit), true) => *end = hit, }, } if let TextSelection::Span { start, end } = sel && start == end { *sel = TextSelection::Pos(*start); } } pub fn deselect(&mut self) { self.text.selection = TextSelection::None; } pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { let old = (self.text.content(), self.text.selection); let mut undo = false; let res = self.apply_event_inner(event, modifiers, &mut undo); if undo && let Some((old, selection)) = self.text.history.pop() { self.set(&old); self.text.selection = selection; } else if self.text.content() != 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 => { if modifiers.control { self.motion(Motion::RightWord, modifiers.shift) } else { self.motion(Motion::Right, modifiers.shift) } } NamedKey::ArrowLeft => { if modifiers.control { self.motion(Motion::LeftWord, modifiers.shift) } else { self.motion(Motion::Left, modifiers.shift) } } NamedKey::ArrowUp => self.motion(Motion::Up, modifiers.shift), NamedKey::ArrowDown => self.motion(Motion::Down, 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 TextSelection::Span { start, end } = self.text.selection { let content = self.text.select_content(start, end); return TextInputResult::Copy(content); } } "x" => { if let TextSelection::Span { start, end } = self.text.selection { let content = self.text.select_content(start, end); self.clear_span(); return TextInputResult::Copy(content); } } "a" => { if !self.text.buf.lines[0].text().is_empty() || self.text.buf.lines.len() > 1 { let lines = &self.text.buf.lines; let last_line = lines.len() - 1; let last_idx = lines[last_line].text().len(); self.text.selection = TextSelection::Span { start: Cursor::new(0, 0), end: Cursor::new(last_line, last_idx), }; } } "z" => { *undo = true; } _ => self.insert(text), } } else { self.insert(text); } } _ => return TextInputResult::Unused, } TextInputResult::Used } } #[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, } #[derive(Debug, Default, Clone, Copy)] pub enum TextSelection { #[default] None, Pos(Cursor), Span { start: Cursor, end: Cursor, }, } impl TextSelection { pub fn clear(&mut self) { match self { TextSelection::None => (), TextSelection::Pos(cursor) => { cursor.line = 0; cursor.index = 0; cursor.affinity = Affinity::default(); } TextSelection::Span { start: _, end: _ } => { *self = TextSelection::None; } } } } 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 } }