initial text wrapping impl (resizing will break)

This commit is contained in:
2025-09-28 01:32:10 -04:00
parent b2950566af
commit 61df088cc7
8 changed files with 126 additions and 55 deletions

View File

@@ -44,11 +44,8 @@ impl Text {
UiRegion::from_size_align(self.size, self.align) UiRegion::from_size_align(self.size, self.align)
} }
fn update_buf(&mut self, font_system: &mut FontSystem) { fn update_buf(&mut self, font_system: &mut FontSystem, width: Option<f32>) {
self.buf.set_metrics( self.attrs.apply(font_system, &mut self.buf, width);
font_system,
Metrics::new(self.attrs.font_size, self.attrs.line_height),
);
if self.content.changed { if self.content.changed {
self.content.changed = false; self.content.changed = false;
self.buf.set_text( self.buf.set_text(
@@ -63,8 +60,13 @@ impl Text {
impl Widget for Text { impl Widget for Text {
fn draw(&mut self, painter: &mut Painter) { 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; 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 (handle, offset) = painter.render_text(&mut self.buf, &self.attrs);
let dims = handle.size(); let dims = handle.size();
self.size = offset.size(&handle); self.size = offset.size(&handle);
@@ -75,7 +77,7 @@ impl Widget for Text {
} }
fn desired_size(&mut self, ctx: &mut SizeCtx) -> UiVec2 { 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); let (handle, offset) = ctx.draw_text(&mut self.buf, &self.attrs);
UiVec2::abs(offset.size(&handle)) UiVec2::abs(offset.size(&handle))
} }
@@ -137,7 +139,7 @@ impl FnOnce<(&mut Ui,)> for TextBuilder {
}; };
text.content.changed = false; text.content.changed = false;
self.attrs self.attrs
.apply(&mut args.0.text.font_system, &mut text.buf); .apply(&mut args.0.text.font_system, &mut text.buf, None);
text text
} }
} }

View File

@@ -23,8 +23,13 @@ impl TextEdit {
impl Widget for TextEdit { impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) { 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; 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); self.buf.shape_until_scroll(font_system, false);
let (handle, tex_offset) = painter.render_text(&mut self.buf, &self.attrs); let (handle, tex_offset) = painter.render_text(&mut self.buf, &self.attrs);
let dims = handle.size(); let dims = handle.size();
@@ -36,10 +41,9 @@ impl Widget for TextEdit {
painter.texture_within(&handle, tex_region); painter.texture_within(&handle, tex_region);
if let Some(cursor) = &self.cursor 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 size = vec2(1, self.attrs.line_height);
let offset = vec2(pos, cursor.line as f32 * self.attrs.line_height);
painter.primitive_within( painter.primitive_within(
RectPrimitive::color(Color::WHITE), RectPrimitive::color(Color::WHITE),
UiRegion::from_size_align(size, Align::TopLeft) UiRegion::from_size_align(size, Align::TopLeft)
@@ -53,35 +57,51 @@ impl Widget for TextEdit {
} }
fn desired_size(&mut self, ctx: &mut SizeCtx) -> UiVec2 { 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); let (handle, offset) = ctx.draw_text(&mut self.buf, &self.attrs);
UiVec2::abs(offset.size(&handle)) UiVec2::abs(offset.size(&handle))
} }
} }
/// copied & modified from fn found in Editor in cosmic_text /// copied & modified from fn found in Editor in cosmic_text
fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<f32> { fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<(f32, f32)> {
let run = buf.layout_runs().find(|r| r.line_i == cursor.line)?; let mut prev = None;
for glyph in run.glyphs.iter() { for run in buf
if cursor.index == glyph.start { .layout_runs()
return Some(glyph.x); .skip_while(|r| r.line_i < cursor.line)
} else if cursor.index > glyph.start && cursor.index < glyph.end { .take_while(|r| r.line_i == cursor.line)
// Guess x offset based on characters {
let mut before = 0; prev = Some((run.line_w, run.line_top));
let mut total = 0; 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]; let cluster = &run.text[glyph.start..glyph.end];
for (i, _) in cluster.grapheme_indices(true) { for (i, _) in cluster.grapheme_indices(true) {
if glyph.start + i < cursor.index { if glyph.start + i < cursor.index {
before += 1; before += 1;
}
total += 1;
} }
total += 1;
}
let offset = glyph.w * (before as f32) / (total as f32); let offset = glyph.w * (before as f32) / (total as f32);
return Some(glyph.x + offset); return Some((glyph.x + offset, run.line_top));
}
} }
} }
Some(run.line_w) prev
} }
pub struct TextEditBuilder { pub struct TextEditBuilder {
@@ -113,6 +133,10 @@ impl TextEditBuilder {
self.align = align; self.align = align;
self self
} }
pub fn wrap(mut self, wrap: bool) -> Self {
self.attrs.wrap = wrap;
self
}
} }
pub struct TextEditCtx<'a> { pub struct TextEditCtx<'a> {
@@ -305,7 +329,7 @@ impl FnOnce<(&mut Ui,)> for TextEditBuilder {
Shaping::Advanced, Shaping::Advanced,
); );
self.attrs self.attrs
.apply(&mut args.0.text.font_system, &mut text.buf); .apply(&mut args.0.text.font_system, &mut text.buf, None);
text text
} }
} }

View File

@@ -14,6 +14,9 @@ pub struct Painter<'a, 'c> {
primitives: Vec<PrimitiveHandle>, primitives: Vec<PrimitiveHandle>,
children: Vec<Id>, children: Vec<Id>,
sized_children: HashMap<Id, UiVec2>, sized_children: HashMap<Id, UiVec2>,
/// 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, pub layer: usize,
id: Id, id: Id,
} }
@@ -75,8 +78,10 @@ impl<'a> PainterCtx<'a> {
text: self.text, text: self.text,
textures: self.textures, textures: self.textures,
widgets: self.widgets, 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 { if size != desired {
self.redraw(rid); self.redraw(rid);
if self.drawing.contains(&id) { if self.drawing.contains(&id) {
@@ -152,6 +157,7 @@ impl<'a> PainterCtx<'a> {
ctx: self, ctx: self,
children: Vec::new(), children: Vec::new(),
sized_children: Default::default(), sized_children: Default::default(),
px_dependent: false,
}; };
// draw widgets // draw widgets
@@ -305,9 +311,16 @@ impl<'a, 'c> Painter<'a, 'c> {
textures: self.ctx.textures, textures: self.ctx.textures,
widgets: self.ctx.widgets, widgets: self.ctx.widgets,
checked: &mut self.sized_children, 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 { pub fn text_data(&mut self) -> &mut TextData {
self.ctx.text self.ctx.text
} }
@@ -326,11 +339,16 @@ pub struct SizeCtx<'a> {
pub textures: &'a mut Textures, pub textures: &'a mut Textures,
widgets: &'a Widgets, widgets: &'a Widgets,
checked: &'a mut HashMap<Id, UiVec2>, checked: &'a mut HashMap<Id, UiVec2>,
region: UiRegion,
screen_size: Vec2,
} }
impl SizeCtx<'_> { 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); let size = self.widgets.get_dyn_dynamic(id).desired_size(self);
self.region = self_region;
self.checked.insert(id, size); self.checked.insert(id, size);
size size
} }
@@ -339,7 +357,13 @@ impl SizeCtx<'_> {
// if let Some(size) = self.checked.get(&id.id) { // if let Some(size) = self.checked.get(&id.id) {
// return Some(*size); // return Some(*size);
// } // }
self.size_inner(id.id) self.size_inner(id.id, self.region)
}
pub fn size_within<W>(&mut self, id: &WidgetId<W>, 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( pub fn draw_text(
&mut self, &mut self,

View File

@@ -1,5 +1,7 @@
use std::marker::Destruct;
use crate::{ use crate::{
layout::{Align, Axis, Vec2}, layout::{Align, Axis, UiNum, Vec2},
util::{LerpUtil, impl_op}, 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<Vec2>) -> Self {
Self { Self {
rel: Vec2::ZERO, rel: Vec2::ZERO,
abs, abs: abs.into(),
} }
} }
pub const fn rel(rel: Vec2) -> Self { pub const fn rel(rel: impl const Into<Vec2>) -> Self {
Self { Self {
rel, rel: rel.into(),
abs: Vec2::ZERO, abs: Vec2::ZERO,
} }
} }
@@ -133,6 +135,15 @@ impl const From<Vec2> for UiVec2 {
} }
} }
impl<T: const UiNum, U: const UiNum> 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)] #[derive(Clone, Copy, Debug, Default)]
pub struct UiScalar { pub struct UiScalar {
pub rel: f32, pub rel: f32,

View File

@@ -26,11 +26,17 @@ pub struct TextAttrs {
pub font_size: f32, pub font_size: f32,
pub line_height: f32, pub line_height: f32,
pub family: Family<'static>, pub family: Family<'static>,
pub wrap: bool,
} }
impl TextAttrs { impl TextAttrs {
pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer) { pub fn apply(&self, font_system: &mut FontSystem, buf: &mut Buffer, width: Option<f32>) {
buf.set_metrics(font_system, Metrics::new(self.font_size, self.line_height)); 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 attrs = Attrs::new().family(self.family);
let list = AttrsList::new(&attrs); let list = AttrsList::new(&attrs);
for line in &mut buf.lines { for line in &mut buf.lines {
@@ -49,6 +55,7 @@ impl Default for TextAttrs {
font_size: size, font_size: size,
line_height: size * 1.2, line_height: size * 1.2,
family: Family::SansSerif, family: Family::SansSerif,
wrap: false,
} }
} }
} }
@@ -70,6 +77,7 @@ impl TextData {
cosmic_text::Color::rgba(c.r, c.g, c.b, c.a) cosmic_text::Color::rgba(c.r, c.g, c.b, c.a)
}; };
let mut max_width = 0.0f32; let mut max_width = 0.0f32;
let mut height = 0.0;
for run in buffer.layout_runs() { for run in buffer.layout_runs() {
for glyph in run.glyphs.iter() { for glyph in run.glyphs.iter() {
let physical_glyph = glyph.physical((0., 0.), 1.0); let physical_glyph = glyph.physical((0., 0.), 1.0);
@@ -95,22 +103,19 @@ impl TextData {
); );
} }
max_width = max_width.max(run.line_w); max_width = max_width.max(run.line_w);
height += run.line_height;
} }
let width = (max_x - min_x + 1) as u32; let img_width = (max_x - min_x + 1) as u32;
let height = (max_y - min_y + 1) as u32; let img_height = (max_y - min_y + 1) as u32;
let mut image = RgbaImage::new(width, height); let mut image = RgbaImage::new(img_width, img_height);
for ((x, y), color) in pixels { for ((x, y), color) in pixels {
let x = (x - min_x) as u32; let x = (x - min_x) as u32;
let y = (y - min_y) as u32; let y = (y - min_y) as u32;
image.put_pixel(x, y, color); image.put_pixel(x, y, color);
} }
let lines = buffer.lines.len();
let offset = TextOffset { let offset = TextOffset {
top_left: Vec2::new(min_x as f32, min_y as f32), top_left: Vec2::new(min_x as f32, min_y as f32),
bot_right: Vec2::new( bot_right: Vec2::new(max_width - max_x as f32, height - max_y as f32),
max_width - max_x as f32,
attrs.line_height * lines as f32 - max_y as f32,
),
}; };
(textures.add(image), offset) (textures.add(image), offset)
} }

View File

@@ -2,7 +2,7 @@ use crate::{
layout::UiNum, layout::UiNum,
util::{DivOr, impl_op}, util::{DivOr, impl_op},
}; };
use std::ops::*; use std::{marker::Destruct, ops::*};
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Clone, Copy, PartialEq, Default, bytemuck::Pod, bytemuck::Zeroable)]
@@ -79,7 +79,10 @@ impl Neg for Vec2 {
} }
} }
impl<T: UiNum, U: UiNum> From<(T, U)> for Vec2 { impl<T: const UiNum, U: const UiNum> const From<(T, U)> for Vec2
where
(T, U): const Destruct,
{
fn from((x, y): (T, U)) -> Self { fn from((x, y): (T, U)) -> Self {
Self { Self {
x: x.to_f32(), x: x.to_f32(),

View File

@@ -6,6 +6,7 @@
#![feature(unboxed_closures)] #![feature(unboxed_closures)]
#![feature(fn_traits)] #![feature(fn_traits)]
#![feature(const_cmp)] #![feature(const_cmp)]
#![feature(const_destruct)]
pub mod core; pub mod core;
pub mod layout; pub mod layout;

View File

@@ -139,13 +139,14 @@ impl Client {
}) })
.id_on(Submit, move |id, client: &mut Client, _| { .id_on(Submit, move |id, client: &mut Client, _| {
let content = client.ui.text(id).take(); let content = client.ui.text(id).take();
let text = text_edit(content).size(30).text_align(Align::Left).id_on( let text = text_edit(content)
CursorSense::click(), .size(30)
|id, client: &mut Client, ctx| { .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.ui.text(id).select(ctx.cursor, ctx.size);
client.focus = Some(id.clone()); client.focus = Some(id.clone());
}, });
);
let msg_box = (rect(Color::WHITE.darker(0.5)), text) let msg_box = (rect(Color::WHITE.darker(0.5)), text)
.stack() .stack()
.size(StackSize::Child(1)) .size(StackSize::Child(1))