use crate::prelude::*; use cosmic_text::{Affinity, Attrs, Cursor, Family, FontSystem, Metrics, Motion, Shaping}; use unicode_segmentation::UnicodeSegmentation; use winit::{ event::KeyEvent, keyboard::{Key, NamedKey}, }; pub struct TextEdit { pub attrs: TextAttrs, /// inner alignment of text region (within where its drawn) pub align: Align, buf: TextBuffer, cursor: Option, size: Vec2, } impl TextEdit { pub fn region(&self) -> UiRegion { UiRegion::from_size_align(self.size, self.align) } } impl Widget for TextEdit { fn draw(&mut self, painter: &mut Painter) { let font_system = &mut painter.text_data().font_system; self.attrs.apply(font_system, &mut self.buf); self.buf.shape_until_scroll(font_system, false); let (handle, tex_offset) = painter.render_text(&mut self.buf, &self.attrs); let dims = handle.size(); self.size = tex_offset.size(&handle); let region = self.region(); let mut tex_region = region; tex_region.top_left.abs += tex_offset.top_left; tex_region.bot_right.abs = tex_region.top_left.abs + dims; painter.texture_within(&handle, tex_region); if let Some(cursor) = &self.cursor && let Some(pos) = cursor_pos(cursor, &self.buf) { let size = vec2(1, self.attrs.line_height); let offset = vec2(pos, cursor.line as f32 * self.attrs.line_height); painter.primitive_within( RectPrimitive::color(Color::WHITE), UiRegion::from_size_align(size, Align::TopLeft) .shifted(offset) .within(®ion), ); } else { // keep number of primitives constant so shifting isn't needed painter.primitive(RectPrimitive::color(Color::NONE)); } } fn desired_size(&mut self, ctx: &mut SizeCtx) -> UiVec2 { let (handle, offset) = ctx.draw_text(&mut self.buf, &self.attrs); UiVec2::abs(offset.size(&handle)) } } /// copied & modified from fn found in Editor in cosmic_text fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option { let run = buf.layout_runs().find(|r| r.line_i == cursor.line)?; for glyph in run.glyphs.iter() { if cursor.index == glyph.start { return Some(glyph.x); } else if cursor.index > glyph.start && cursor.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 < cursor.index { before += 1; } total += 1; } let offset = glyph.w * (before as f32) / (total as f32); return Some(glyph.x + offset); } } Some(run.line_w) } pub struct TextEditBuilder { pub content: String, pub attrs: TextAttrs, /// inner alignment of text region (within where its drawn) pub align: Align, } impl TextEditBuilder { pub fn size(mut self, size: impl UiNum) -> Self { self.attrs.font_size = size.to_f32(); self.attrs.line_height = self.attrs.font_size * 1.1; self } pub fn color(mut self, color: UiColor) -> Self { self.attrs.color = color; self } pub fn family(mut self, family: Family<'static>) -> Self { self.attrs.family = family; self } pub fn line_height(mut self, height: f32) -> Self { self.attrs.line_height = height; self } pub fn text_align(mut self, align: Align) -> Self { self.align = align; self } } 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::Advanced); if let Some(cursor) = &mut self.text.cursor { cursor.line = 0; cursor.index = 0; cursor.affinity = Affinity::default(); } text } pub fn motion(&mut self, motion: Motion) { if let Some(cursor) = self.text.cursor && let Some((cursor, _)) = self.text .buf .cursor_motion(self.font_system, cursor, None, motion) { self.text.cursor = Some(cursor); } } pub fn insert(&mut self, text: &str) { let mut lines = text.split('\n'); let Some(first) = lines.next() else { return; }; self.insert_inner(first); for line in lines { self.newline(); self.insert_inner(line); } } fn insert_inner(&mut self, text: &str) { if let Some(cursor) = &mut self.text.cursor { let line = &mut self.text.buf.lines[cursor.line]; let mut line_text = line.text().to_string(); line_text.insert_str(cursor.index, text); line.set_text(line_text, line.ending(), line.attrs_list().clone()); for _ in 0..text.len() { self.motion(Motion::Right); } } } pub fn newline(&mut self) { if let Some(cursor) = &mut self.text.cursor { let line = &mut self.text.buf.lines[cursor.line]; let new = line.split_off(cursor.index); cursor.line += 1; self.text.buf.lines.insert(cursor.line, new); cursor.index = 0; } } pub fn backspace(&mut self) { if let Some(cursor) = &mut self.text.cursor && (cursor.index != 0 || cursor.line != 0) { self.motion(Motion::Left); self.delete(); } } pub fn delete(&mut self) { if let Some(cursor) = &mut self.text.cursor { let line = &mut self.text.buf.lines[cursor.line]; if cursor.index == line.text().len() { if cursor.line == self.text.buf.lines.len() - 1 { return; } let add = self.text.buf.lines.remove(cursor.line + 1).into_text(); let line = &mut self.text.buf.lines[cursor.line]; let mut cur = line.text().to_string(); cur.push_str(&add); line.set_text(cur, line.ending(), line.attrs_list().clone()); } else { let mut text = line.text().to_string(); text.remove(cursor.index); line.set_text(text, line.ending(), line.attrs_list().clone()); } } } pub fn select(&mut self, pos: Vec2, size: Vec2) { let pos = pos - self.text.region().top_left.to_size(size); self.text.cursor = self.text.buf.hit(pos.x, pos.y); } pub fn deselect(&mut self) { self.text.cursor = None; } pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult { match &event.logical_key { Key::Named(named) => match named { NamedKey::Backspace => self.backspace(), NamedKey::Delete => self.delete(), NamedKey::Space => self.insert(" "), NamedKey::Enter => { if modifiers.shift { self.newline(); } else { return TextInputResult::Submit; } } NamedKey::ArrowRight => self.motion(Motion::Right), NamedKey::ArrowLeft => self.motion(Motion::Left), NamedKey::ArrowUp => self.motion(Motion::Up), NamedKey::ArrowDown => self.motion(Motion::Down), NamedKey::Escape => { self.deselect(); return TextInputResult::Unfocus; } _ => return TextInputResult::Unused, }, Key::Character(text) => 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, } impl TextInputResult { pub fn unfocus(&self) -> bool { matches!(self, TextInputResult::Unfocus) } } impl FnOnce<(&mut Ui,)> for TextEditBuilder { type Output = TextEdit; extern "rust-call" fn call_once(self, args: (&mut Ui,)) -> Self::Output { let mut text = TextEdit { buf: TextBuffer::new_empty(Metrics::new(self.attrs.font_size, self.attrs.line_height)), attrs: self.attrs, align: self.align, cursor: None, size: Vec2::ZERO, }; text.buf.set_text( &mut args.0.text.font_system, &self.content, &Attrs::new(), Shaping::Advanced, ); self.attrs .apply(&mut args.0.text.font_system, &mut text.buf); text } } pub fn text_edit(content: impl Into) -> TextEditBuilder { TextEditBuilder { content: content.into(), attrs: TextAttrs::default(), align: Align::Center, } }