diff --git a/src/core/mod.rs b/src/core/mod.rs index b0f9fc2..ed3066b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -7,6 +7,7 @@ mod sized; mod span; mod stack; mod text; +mod text_edit; mod trait_fns; pub use align::*; @@ -18,4 +19,5 @@ pub use sized::*; pub use span::*; pub use stack::*; pub use text::*; +pub use text_edit::*; pub use trait_fns::*; diff --git a/src/core/text.rs b/src/core/text.rs index af5f417..2708ad1 100644 --- a/src/core/text.rs +++ b/src/core/text.rs @@ -1,4 +1,4 @@ -use cosmic_text::{Family, Metrics}; +use cosmic_text::{Attrs, Family, FontSystem, Metrics, Shaping}; use crate::prelude::*; @@ -7,8 +7,7 @@ pub struct Text { pub attrs: TextAttrs, /// inner alignment of text region (within where its drawn) pub align: Align, - buf: TextBuffer, - cursor: Cursor, + pub(super) buf: TextBuffer, size: Vec2, } @@ -37,129 +36,33 @@ impl Text { buf: TextBuffer::new_empty(Metrics::new(attrs.font_size, attrs.line_height)), attrs, align: Align::Center, - cursor: Cursor::None, size: Vec2::ZERO, } } - pub fn select(&mut self, pos: Vec2, size: Vec2) { - let pos = pos - self.region().top_left.to_size(size); - let Some(cursor) = self.buf.hit(pos.x, pos.y) else { - return; - }; - self.cursor = Cursor::Select { - line: cursor.line as isize, - col: cursor.index as isize, - }; - } - - pub fn deselect(&mut self) { - self.cursor = Cursor::None; - } - - pub fn insert(&mut self, text: &str) { - let i = self.update_cursor(); - self.content.insert_str(i, text); - - match &mut self.cursor { - Cursor::None => (), - Cursor::Select { col, .. } => { - *col += 1; - } - } - self.update_cursor(); - } - - pub fn backspace(&mut self) { - if let Some(i) = self - .update_cursor() - .checked_sub(1) - .map(|i| self.content.floor_char_boundary(i)) - { - self.content.remove(i); - match &mut self.cursor { - Cursor::None => (), - Cursor::Select { col, .. } => { - *col -= 1; - } - } - } - } - - pub fn delete(&mut self) { - let i = self.update_cursor(); - if i != self.content.len() { - self.content.remove(i); - } - } - - pub fn move_cursor(&mut self, dir: Dir) { - self.update_cursor(); - if let Cursor::Select { line, col } = &mut self.cursor { - match dir { - Dir::LEFT => *col -= 1, - Dir::RIGHT => *col += 1, - Dir::UP => *line -= 1, - Dir::DOWN => *line += 1, - } - } - } - - pub fn update_cursor(&mut self) -> usize { - match &mut self.cursor { - Cursor::None => 0, - Cursor::Select { line, col } => { - if *col < 0 { - *line -= 1; - } - if *line < 0 { - *line = 0; - *col = 0; - } - let mut idx = self.content.len(); - let mut l = 0; - let mut c = 0; - let mut cur_len = 0; - for (i, ch) in self.content.char_indices() { - if ch == '\n' { - l += 1; - c = 0; - } else { - if l == *line { - cur_len = c + 1; - if c == *col { - idx = i; - } - } - c += 1; - } - } - if *col < 0 { - *col = cur_len; - } - if *col > cur_len { - *col = 0; - *line += 1; - } - if *line > l { - *line = l; - *col = cur_len; - } - idx - } - } - } - pub fn region(&self) -> UiRegion { UiRegion::from_size_align(self.size, self.align) } + + fn update_buf(&mut self, font_system: &mut FontSystem) { + self.buf.set_metrics( + font_system, + Metrics::new(self.attrs.font_size, self.attrs.line_height), + ); + self.buf.set_text( + font_system, + &self.content, + &Attrs::new().family(self.attrs.family), + Shaping::Advanced, + ); + } } impl Widget for Text { fn draw(&mut self, painter: &mut Painter) { - self.update_cursor(); - let (handle, offset) = - painter.render_text(&mut self.buf, &self.content, &self.attrs, &self.cursor); + let font_system = &mut painter.text_data().font_system; + self.update_buf(font_system); + let (handle, offset) = painter.render_text(&mut self.buf, &self.attrs, &VisualCursor::None); let dims = handle.size(); self.size = offset.size(&handle); let mut region = self.region(); @@ -169,8 +72,8 @@ impl Widget for Text { } fn get_size(&mut self, ctx: &mut SizeCtx) -> Vec2 { - let (handle, offset) = - ctx.draw_text(&mut self.buf, &self.content, &self.attrs, &self.cursor); + self.update_buf(&mut ctx.text.font_system); + let (handle, offset) = ctx.draw_text(&mut self.buf, &self.attrs, &VisualCursor::None); offset.size(&handle) } } diff --git a/src/core/text_edit.rs b/src/core/text_edit.rs new file mode 100644 index 0000000..a81f81e --- /dev/null +++ b/src/core/text_edit.rs @@ -0,0 +1,203 @@ +use crate::prelude::*; +use cosmic_text::{Attrs, Cursor, Family, FontSystem, Metrics, Motion, Shaping}; + +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.buf.shape_until_scroll(font_system, false); + self.attrs.apply(font_system, &mut self.buf); + let (handle, offset) = painter.render_text( + &mut self.buf, + &self.attrs, + &match self.cursor { + None => VisualCursor::None, + Some(cursor) => VisualCursor::Select { + line: cursor.line as isize, + col: cursor.index as isize, + }, + }, + ); + let dims = handle.size(); + self.size = offset.size(&handle); + let mut region = self.region(); + region.top_left.offset += offset.top_left; + region.bot_right.offset = region.top_left.offset + dims; + painter.draw_texture_within(&handle, region); + } + + fn get_size(&mut self, ctx: &mut SizeCtx) -> Vec2 { + let (handle, offset) = ctx.draw_text(&mut self.buf, &self.attrs, &VisualCursor::None); + offset.size(&handle) + } +} + +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 font_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 struct TextEditCtx<'a> { + pub text: &'a mut TextEdit, + pub font_system: &'a mut FontSystem, +} + +impl<'a> TextEditCtx<'a> { + 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) { + 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 { + if cursor.index == 0 { + if cursor.line == 0 { + return; + } + let add = self.text.buf.lines.remove(cursor.line).into_text(); + cursor.line -= 1; + let line = &mut self.text.buf.lines[cursor.line]; + let mut cur = line.text().to_string(); + cursor.index = cur.len(); + cur.push_str(&add); + line.set_text(cur, line.ending(), line.attrs_list().clone()); + } else { + let line = &mut self.text.buf.lines[cursor.line]; + let mut text = line.text().to_string(); + let idx = text.floor_char_boundary(cursor.index - 1); + text.remove(idx); + line.set_text(text, line.ending(), line.attrs_list().clone()); + cursor.index = idx; + } + } + } + + 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()); + } + } + if let Some(cursor) = self.text.cursor { + let line = &mut self.text.buf.lines[cursor.line]; + 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; + } +} + +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, + } +} diff --git a/src/layout/painter.rs b/src/layout/painter.rs index b6ba534..86fa3e5 100644 --- a/src/layout/painter.rs +++ b/src/layout/painter.rs @@ -2,8 +2,8 @@ use std::ops::Range; use crate::{ layout::{ - Active, Cursor, TextAttrs, TextBuffer, TextData, TextOffset, TextureHandle, Textures, - UiRegion, Vec2, WidgetId, Widgets, + Active, TextAttrs, TextBuffer, TextData, TextOffset, TextureHandle, Textures, UiRegion, + Vec2, VisualCursor, WidgetId, Widgets, }, render::{Primitive, PrimitiveHandle, Primitives}, util::{HashSet, Id}, @@ -277,13 +277,10 @@ impl<'a, 'c> Painter<'a, 'c> { pub fn render_text( &mut self, buffer: &mut TextBuffer, - content: &str, attrs: &TextAttrs, - cursor: &Cursor, + cursor: &VisualCursor, ) -> (TextureHandle, TextOffset) { - self.ctx - .text - .draw(buffer, content, attrs, cursor, self.ctx.textures) + self.ctx.text.draw(buffer, attrs, cursor, self.ctx.textures) } pub fn region(&self) -> UiRegion { @@ -307,6 +304,10 @@ impl<'a, 'c> Painter<'a, 'c> { checked: &mut self.sized_children, } } + + pub fn text_data(&mut self) -> &mut TextData { + self.ctx.text + } } pub struct SizeCtx<'a> { @@ -325,12 +326,10 @@ impl SizeCtx<'_> { pub fn draw_text( &mut self, buffer: &mut TextBuffer, - content: &str, attrs: &TextAttrs, - cursor: &Cursor, + cursor: &VisualCursor, ) -> (TextureHandle, TextOffset) { - self.text - .draw(buffer, content, attrs, cursor, self.textures) + self.text.draw(buffer, attrs, cursor, self.textures) } } diff --git a/src/layout/text.rs b/src/layout/text.rs index 300ac98..0ad59ce 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -1,4 +1,4 @@ -use cosmic_text::{Attrs, Buffer, Family, FontSystem, Metrics, Shaping, SwashCache}; +use cosmic_text::{Attrs, AttrsList, Buffer, Family, FontSystem, Metrics, SwashCache}; use image::{Rgba, RgbaImage}; use crate::{ @@ -7,8 +7,8 @@ use crate::{ }; pub struct TextData { - font_system: FontSystem, - swash_cache: SwashCache, + pub font_system: FontSystem, + pub swash_cache: SwashCache, } impl Default for TextData { @@ -28,8 +28,19 @@ pub struct TextAttrs { pub family: Family<'static>, } +impl TextAttrs { + pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer) { + buf.set_metrics(font_system, Metrics::new(self.font_size, self.line_height)); + let attrs = Attrs::new().family(self.family); + let list = AttrsList::new(&attrs); + for line in &mut buf.lines { + line.set_attrs_list(list.clone()); + } + } +} + #[derive(Default, Debug, Copy, Clone)] -pub enum Cursor { +pub enum VisualCursor { #[default] None, Select { @@ -56,21 +67,10 @@ impl TextData { pub fn draw( &mut self, buffer: &mut TextBuffer, - content: &str, attrs: &TextAttrs, - cursor: &Cursor, + cursor: &VisualCursor, textures: &mut Textures, ) -> (TextureHandle, TextOffset) { - buffer.set_metrics( - &mut self.font_system, - Metrics::new(attrs.font_size, attrs.line_height), - ); - buffer.set_text( - &mut self.font_system, - content, - &Attrs::new().family(attrs.family), - Shaping::Advanced, - ); let mut pixels = HashMap::new(); let mut min_x = 0; let mut min_y = 0; @@ -80,7 +80,7 @@ impl TextData { let mut max_width = 0.0f32; let mut cursor_x = 0; for (run_i, run) in buffer.layout_runs().enumerate() { - if let Cursor::Select { line, .. } = cursor + if let VisualCursor::Select { line, .. } = cursor && *line == run_i as isize { cursor_x = run.line_w as i32; @@ -93,7 +93,7 @@ impl TextData { None => cosmic_text::Color::rgba(c.r, c.g, c.b, c.a), }; - if let Cursor::Select { col: idx, line } = cursor + if let VisualCursor::Select { col: idx, line } = cursor && *line == run_i as isize && *idx == i as isize { @@ -117,7 +117,7 @@ impl TextData { } max_width = max_width.max(run.line_w); } - if let &Cursor::Select { line, .. } = cursor { + if let &VisualCursor::Select { line, .. } = cursor { let y = (attrs.line_height * (line + 1) as f32) as i32 - 1; max_y = max_y.max(y); max_x = max_x.max(cursor_x); @@ -130,7 +130,7 @@ impl TextData { let y = (y - min_y) as u32; image.put_pixel(x, y, color); } - if let &Cursor::Select { line, .. } = cursor { + if let &VisualCursor::Select { line, .. } = cursor { let x = (cursor_x - min_x) as u32; for y in 0..attrs.line_height as u32 { // no clue if this is good or bad for non integer values @@ -139,10 +139,7 @@ impl TextData { image.put_pixel(x, y, Rgba(c.as_arr())); } } - let mut lines = buffer.lines.len(); - if content.ends_with('\n') { - lines += 1; - } + let lines = buffer.lines.len(); let offset = TextOffset { top_left: Vec2::new(min_x as f32, min_y as f32), bot_right: Vec2::new( diff --git a/src/layout/ui.rs b/src/layout/ui.rs index 8aba521..0e98970 100644 --- a/src/layout/ui.rs +++ b/src/layout/ui.rs @@ -1,6 +1,7 @@ use image::DynamicImage; use crate::{ + core::{TextEdit, TextEditCtx}, layout::{ ActiveSensors, PainterCtx, Sensor, SensorMap, StaticWidgetId, TextData, TextureHandle, Textures, Vec2, Widget, WidgetId, WidgetInstance, WidgetLike, @@ -24,7 +25,7 @@ pub struct Ui { // TODO: make these non pub(crate) pub(crate) primitives: Primitives, pub(crate) textures: Textures, - text: TextData, + pub(crate) text: TextData, full_redraw: bool, pub(super) active: Active, @@ -188,6 +189,14 @@ impl Ui { .push(f); self.widgets.data_mut(&id.id).unwrap().sensor = true; } + + pub fn text(&mut self, id: &WidgetId) -> TextEditCtx<'_> { + self.updates.push(id.id.duplicate()); + TextEditCtx { + text: self.widgets.get_mut(id).unwrap(), + font_system: &mut self.text.font_system, + } + } } impl Index<&WidgetId> for Ui { diff --git a/src/lib.rs b/src/lib.rs index 39f8826..d9852ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ #![feature(map_try_insert)] #![feature(trait_alias)] #![feature(round_char_boundary)] +#![feature(unboxed_closures)] +#![feature(fn_traits)] pub mod core; pub mod layout; diff --git a/src/testing/mod.rs b/src/testing/mod.rs index 74517c5..417fe77 100644 --- a/src/testing/mod.rs +++ b/src/testing/mod.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use app::App; -use cosmic_text::Family; +use cosmic_text::{Family, Motion}; use render::Renderer; use senses::*; use ui::prelude::*; @@ -27,7 +27,7 @@ pub struct Client { input: Input, ui: Ui, info: WidgetId, - selected: Option>, + selected: Option>, } impl Client { @@ -127,10 +127,10 @@ impl Client { let texts = Span::empty(Dir::DOWN).add(&mut ui); let text_edit_scroll = ( texts, - text("add") + text_edit("add") .font_size(30) .id_on(PRESS_START, |id, client: &mut Client, ctx| { - client.ui[id].select(ctx.cursor, ctx.size); + client.ui.text(id).select(ctx.cursor, ctx.size); client.selected = Some(id.clone()); }) .pad(30), @@ -208,17 +208,17 @@ impl Client { if let Some(sel) = &self.selected && event.state.is_pressed() { - let w = &mut self.ui[sel]; + let w = &mut self.ui.text(sel); match &event.logical_key { Key::Named(named) => match named { NamedKey::Backspace => w.backspace(), NamedKey::Delete => w.delete(), NamedKey::Space => w.insert(" "), - NamedKey::Enter => w.insert("\n"), - NamedKey::ArrowRight => w.move_cursor(Dir::RIGHT), - NamedKey::ArrowLeft => w.move_cursor(Dir::LEFT), - NamedKey::ArrowUp => w.move_cursor(Dir::UP), - NamedKey::ArrowDown => w.move_cursor(Dir::DOWN), + NamedKey::Enter => w.newline(), + NamedKey::ArrowRight => w.motion(Motion::Right), + NamedKey::ArrowLeft => w.motion(Motion::Left), + NamedKey::ArrowUp => w.motion(Motion::Up), + NamedKey::ArrowDown => w.motion(Motion::Down), NamedKey::Escape => { w.deselect(); self.selected = None;