From 61df088cc710567f2c6577eb48683364d070f469 Mon Sep 17 00:00:00 2001 From: shadow cat Date: Sun, 28 Sep 2025 01:32:10 -0400 Subject: [PATCH] initial text wrapping impl (resizing will break) --- src/core/text.rs | 18 +++++++----- src/core/text_edit.rs | 68 +++++++++++++++++++++++++++++-------------- src/layout/painter.rs | 30 +++++++++++++++++-- src/layout/pos.rs | 21 +++++++++---- src/layout/text.rs | 25 +++++++++------- src/layout/vec2.rs | 7 +++-- src/lib.rs | 1 + src/testing/mod.rs | 11 +++---- 8 files changed, 126 insertions(+), 55 deletions(-) diff --git a/src/core/text.rs b/src/core/text.rs index bbe46e0..bf18fc5 100644 --- a/src/core/text.rs +++ b/src/core/text.rs @@ -44,11 +44,8 @@ impl Text { 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), - ); + fn update_buf(&mut self, font_system: &mut FontSystem, width: Option) { + self.attrs.apply(font_system, &mut self.buf, width); if self.content.changed { self.content.changed = false; self.buf.set_text( @@ -63,8 +60,13 @@ impl Text { impl Widget for Text { fn draw(&mut self, painter: &mut Painter) { + let width = if self.attrs.wrap { + Some(painter.px_size().x) + } else { + None + }; let font_system = &mut painter.text_data().font_system; - self.update_buf(font_system); + self.update_buf(font_system, width); let (handle, offset) = painter.render_text(&mut self.buf, &self.attrs); let dims = handle.size(); self.size = offset.size(&handle); @@ -75,7 +77,7 @@ impl Widget for Text { } fn desired_size(&mut self, ctx: &mut SizeCtx) -> UiVec2 { - self.update_buf(&mut ctx.text.font_system); + self.update_buf(&mut ctx.text.font_system, None); let (handle, offset) = ctx.draw_text(&mut self.buf, &self.attrs); UiVec2::abs(offset.size(&handle)) } @@ -137,7 +139,7 @@ impl FnOnce<(&mut Ui,)> for TextBuilder { }; text.content.changed = false; self.attrs - .apply(&mut args.0.text.font_system, &mut text.buf); + .apply(&mut args.0.text.font_system, &mut text.buf, None); text } } diff --git a/src/core/text_edit.rs b/src/core/text_edit.rs index 5acfeac..f200713 100644 --- a/src/core/text_edit.rs +++ b/src/core/text_edit.rs @@ -23,8 +23,13 @@ impl TextEdit { impl Widget for TextEdit { fn draw(&mut self, painter: &mut Painter) { + let width = if self.attrs.wrap { + Some(painter.px_size().x) + } else { + None + }; let font_system = &mut painter.text_data().font_system; - self.attrs.apply(font_system, &mut self.buf); + self.attrs.apply(font_system, &mut self.buf, width); self.buf.shape_until_scroll(font_system, false); let (handle, tex_offset) = painter.render_text(&mut self.buf, &self.attrs); let dims = handle.size(); @@ -36,10 +41,9 @@ impl Widget for TextEdit { painter.texture_within(&handle, tex_region); if let Some(cursor) = &self.cursor - && let Some(pos) = cursor_pos(cursor, &self.buf) + && let Some(offset) = 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) @@ -53,35 +57,51 @@ impl Widget for TextEdit { } fn desired_size(&mut self, ctx: &mut SizeCtx) -> UiVec2 { + let width = if self.attrs.wrap { + Some(ctx.px_size().x) + } else { + None + }; + self.attrs + .apply(&mut ctx.text.font_system, &mut self.buf, width); + self.buf + .shape_until_scroll(&mut ctx.text.font_system, false); 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; +fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<(f32, f32)> { + 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((run.line_w, run.line_top)); + for glyph in run.glyphs.iter() { + if cursor.index == glyph.start { + return Some((glyph.x, run.line_top)); + } 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; + 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; } - total += 1; - } - let offset = glyph.w * (before as f32) / (total as f32); - return Some(glyph.x + offset); + let offset = glyph.w * (before as f32) / (total as f32); + return Some((glyph.x + offset, run.line_top)); + } } } - Some(run.line_w) + prev } pub struct TextEditBuilder { @@ -113,6 +133,10 @@ impl TextEditBuilder { self.align = align; self } + pub fn wrap(mut self, wrap: bool) -> Self { + self.attrs.wrap = wrap; + self + } } pub struct TextEditCtx<'a> { @@ -305,7 +329,7 @@ impl FnOnce<(&mut Ui,)> for TextEditBuilder { Shaping::Advanced, ); self.attrs - .apply(&mut args.0.text.font_system, &mut text.buf); + .apply(&mut args.0.text.font_system, &mut text.buf, None); text } } diff --git a/src/layout/painter.rs b/src/layout/painter.rs index ff413c1..595999e 100644 --- a/src/layout/painter.rs +++ b/src/layout/painter.rs @@ -14,6 +14,9 @@ pub struct Painter<'a, 'c> { primitives: Vec, children: Vec, sized_children: HashMap, + /// whether this widget depends on region's final pixel size or not + /// TODO: decide if point (pt) should be used here instead of px + px_dependent: bool, pub layer: usize, id: Id, } @@ -75,8 +78,10 @@ impl<'a> PainterCtx<'a> { text: self.text, textures: self.textures, widgets: self.widgets, + region: UiRegion::full(), + screen_size: self.screen_size, }; - let desired = ctx.size_inner(id); + let desired = ctx.size_inner(id, active.region); if size != desired { self.redraw(rid); if self.drawing.contains(&id) { @@ -152,6 +157,7 @@ impl<'a> PainterCtx<'a> { ctx: self, children: Vec::new(), sized_children: Default::default(), + px_dependent: false, }; // draw widgets @@ -305,9 +311,16 @@ impl<'a, 'c> Painter<'a, 'c> { textures: self.ctx.textures, widgets: self.ctx.widgets, checked: &mut self.sized_children, + screen_size: self.ctx.screen_size, + region: self.region, } } + pub fn px_size(&mut self) -> Vec2 { + self.px_dependent = true; + self.region.in_size(self.ctx.screen_size) + } + pub fn text_data(&mut self) -> &mut TextData { self.ctx.text } @@ -326,11 +339,16 @@ pub struct SizeCtx<'a> { pub textures: &'a mut Textures, widgets: &'a Widgets, checked: &'a mut HashMap, + region: UiRegion, + screen_size: Vec2, } impl SizeCtx<'_> { - fn size_inner(&mut self, id: Id) -> UiVec2 { + fn size_inner(&mut self, id: Id, region: UiRegion) -> UiVec2 { + let self_region = self.region; + self.region = region; let size = self.widgets.get_dyn_dynamic(id).desired_size(self); + self.region = self_region; self.checked.insert(id, size); size } @@ -339,7 +357,13 @@ impl SizeCtx<'_> { // if let Some(size) = self.checked.get(&id.id) { // return Some(*size); // } - self.size_inner(id.id) + self.size_inner(id.id, self.region) + } + pub fn size_within(&mut self, id: &WidgetId, region: UiRegion) -> UiVec2 { + self.size_inner(id.id, region.within(&self.region)) + } + pub fn px_size(&self) -> Vec2 { + self.region.in_size(self.screen_size) } pub fn draw_text( &mut self, diff --git a/src/layout/pos.rs b/src/layout/pos.rs index 5507ec1..e6d6507 100644 --- a/src/layout/pos.rs +++ b/src/layout/pos.rs @@ -1,5 +1,7 @@ +use std::marker::Destruct; + use crate::{ - layout::{Align, Axis, Vec2}, + layout::{Align, Axis, UiNum, Vec2}, util::{LerpUtil, impl_op}, }; @@ -25,16 +27,16 @@ impl UiVec2 { } } - pub const fn abs(abs: Vec2) -> Self { + pub const fn abs(abs: impl const Into) -> Self { Self { rel: Vec2::ZERO, - abs, + abs: abs.into(), } } - pub const fn rel(rel: Vec2) -> Self { + pub const fn rel(rel: impl const Into) -> Self { Self { - rel, + rel: rel.into(), abs: Vec2::ZERO, } } @@ -133,6 +135,15 @@ impl const From for UiVec2 { } } +impl const From<(T, U)> for UiVec2 +where + (T, U): const Destruct, +{ + fn from(abs: (T, U)) -> Self { + Self::abs(abs) + } +} + #[derive(Clone, Copy, Debug, Default)] pub struct UiScalar { pub rel: f32, diff --git a/src/layout/text.rs b/src/layout/text.rs index 4a2603c..f1c96c3 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -26,11 +26,17 @@ pub struct TextAttrs { pub font_size: f32, pub line_height: f32, pub family: Family<'static>, + pub wrap: bool, } 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)); + pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option) { + buf.set_metrics_and_size( + font_system, + Metrics::new(self.font_size, self.line_height), + width, + None, + ); let attrs = Attrs::new().family(self.family); let list = AttrsList::new(&attrs); for line in &mut buf.lines { @@ -49,6 +55,7 @@ impl Default for TextAttrs { font_size: size, line_height: size * 1.2, family: Family::SansSerif, + wrap: false, } } } @@ -70,6 +77,7 @@ impl TextData { cosmic_text::Color::rgba(c.r, c.g, c.b, c.a) }; let mut max_width = 0.0f32; + let mut height = 0.0; for run in buffer.layout_runs() { for glyph in run.glyphs.iter() { let physical_glyph = glyph.physical((0., 0.), 1.0); @@ -95,22 +103,19 @@ impl TextData { ); } max_width = max_width.max(run.line_w); + height += run.line_height; } - let width = (max_x - min_x + 1) as u32; - let height = (max_y - min_y + 1) as u32; - let mut image = RgbaImage::new(width, height); + let img_width = (max_x - min_x + 1) as u32; + let img_height = (max_y - min_y + 1) as u32; + let mut image = RgbaImage::new(img_width, img_height); for ((x, y), color) in pixels { let x = (x - min_x) as u32; let y = (y - min_y) as u32; image.put_pixel(x, y, color); } - let lines = buffer.lines.len(); let offset = TextOffset { top_left: Vec2::new(min_x as f32, min_y as f32), - bot_right: Vec2::new( - max_width - max_x as f32, - attrs.line_height * lines as f32 - max_y as f32, - ), + bot_right: Vec2::new(max_width - max_x as f32, height - max_y as f32), }; (textures.add(image), offset) } diff --git a/src/layout/vec2.rs b/src/layout/vec2.rs index 8cc6995..344571e 100644 --- a/src/layout/vec2.rs +++ b/src/layout/vec2.rs @@ -2,7 +2,7 @@ use crate::{ layout::UiNum, util::{DivOr, impl_op}, }; -use std::ops::*; +use std::{marker::Destruct, ops::*}; #[repr(C)] #[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)] @@ -79,7 +79,10 @@ impl Neg for Vec2 { } } -impl From<(T, U)> for Vec2 { +impl const From<(T, U)> for Vec2 +where + (T, U): const Destruct, +{ fn from((x, y): (T, U)) -> Self { Self { x: x.to_f32(), diff --git a/src/lib.rs b/src/lib.rs index 5cab667..ae4731f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ #![feature(unboxed_closures)] #![feature(fn_traits)] #![feature(const_cmp)] +#![feature(const_destruct)] pub mod core; pub mod layout; diff --git a/src/testing/mod.rs b/src/testing/mod.rs index 748c3a9..0db449e 100644 --- a/src/testing/mod.rs +++ b/src/testing/mod.rs @@ -139,13 +139,14 @@ impl Client { }) .id_on(Submit, move |id, client: &mut Client, _| { let content = client.ui.text(id).take(); - let text = text_edit(content).size(30).text_align(Align::Left).id_on( - CursorSense::click(), - |id, client: &mut Client, ctx| { + let text = text_edit(content) + .size(30) + .text_align(Align::Left) + .wrap(true) + .id_on(CursorSense::click(), |id, client: &mut Client, ctx| { client.ui.text(id).select(ctx.cursor, ctx.size); client.focus = Some(id.clone()); - }, - ); + }); let msg_box = (rect(Color::WHITE.darker(0.5)), text) .stack() .size(StackSize::Child(1))