draw text correctly

This commit is contained in:
Alessandro Mauri 2025-09-09 19:10:04 +02:00
parent db63b2c6b1
commit c3a6390404
6 changed files with 156 additions and 162 deletions

12
TODO
View File

@ -44,19 +44,19 @@ to maintain focus until mouse release (fix scroll bars)
[x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the [x] Fix how padding is applied in push_rect. In CSS padding is applied between the border and the
content, the background color is applied starting from the border. Right now push_rect() offsets content, the background color is applied starting from the border. Right now push_rect() offsets
the background rect by both border and padding the background rect by both border and padding
[ ] Investigate why the debug pointer (cyan rectangle) disappears... [x] Investigate why the debug pointer (cyan rectangle) disappears...
## Layout ## Layout
[x] Flexbox [x] Flexbox
[ ] Center elements to the row/column [x] Center elements to the row/column
[x] Text wrapping / reflow [x] Text wrapping / reflow
[x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct [x] Implement a better and unified way to place a glyph and get the cursor position, maybe with a struct
[ ] Correct whitespace handling in text (\t \r etc) [ ] Correct whitespace handling in text (\t \r etc)
[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay) [x] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
instead of the curren multi-frame approach. instead of the curren multi-frame approach.
[ ] Implement column/row sizing (min, max) [x] Implement column/row sizing (min, max)
[ ] Implement a way to size the element as the current row/column size [x] Implement a way to size the element as the current row/column size
* +-------------+ * +-------------+
* | | * | |
* +-------------+ * +-------------+
@ -68,7 +68,7 @@ to maintain focus until mouse release (fix scroll bars)
See the calculator example for why it is useful See the calculator example for why it is useful
[ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing [ ] Find a way to concile pixel measurements to the mm ones used in css, for example in min/max sizing
of elements of elements
[ ] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly) [x] Center elements to div (center children_bounds to the center of the div bounds and shift the origin accordingly)
[x] Use containing_rect() in position_element() to skip some computing and semplify the function [x] Use containing_rect() in position_element() to skip some computing and semplify the function
[x] Rename position_element() to layout_element() [x] Rename position_element() to layout_element()
[x] Make functions to mark rows/columns as full, to fix the calculator demo [x] Make functions to mark rows/columns as full, to fix the calculator demo

View File

@ -40,7 +40,6 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon)
.x = icon_size.w + inner_pad, // text sizing is handled differently .x = icon_size.w + inner_pad, // text sizing is handled differently
.y = icon_size.h + inner_pad, .y = icon_size.h + inner_pad,
}; };
//elem.layout.w = @fit(min_size);
elem.layout.w = @fit(min_size); elem.layout.w = @fit(min_size);
elem.layout.h = @fit(min_size); elem.layout.h = @fit(min_size);
elem.layout.children.w = @exact(content_size.x); elem.layout.children.w = @exact(content_size.x);
@ -82,7 +81,7 @@ fn ElemEvents? Ctx.button_id(&ctx, Id id, String label, String icon)
ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!; ctx.push_sprite(icon_bounds, sprite.uv(), ctx.sprite_atlas.id, parent.div.z_index, type: sprite.type)!;
} }
if (label != "") { if (label != "") {
ctx.push_string(text_bounds, label, parent.div.z_index, style.fg, true)!; ctx.layout_string(label, text_bounds, CENTER, parent.div.z_index, style.fg)!;
} }
return elem.events; return elem.events;
} }

View File

@ -163,32 +163,6 @@ fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_i
ctx.push_cmd(&cmd, z_index)!; ctx.push_cmd(&cmd, z_index)!;
} }
// TODO: do not return the WHOLE TextInfo but instead something smaller
fn TextInfo? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue, bool reflow = false)
{
if (text.len == 0) {
return {};
}
ctx.push_scissor(bounds, z_index)!;
Id texture_id = ctx.font.id; // or ctx.font.atlas.id
Rect text_bounds = {bounds.x, bounds.y, 0, 0};
TextInfo ti;
ti.init(&ctx.font, (String)text, bounds);
while (ti.place_glyph(reflow)!) {
if (!cull_rect(ti.glyph_bounds, bounds)) {
ctx.push_sprite(ti.glyph_bounds, ti.glyph_uv, texture_id, z_index, hue)!;
}
}
ctx.reset_scissor(z_index)!;
return ti;
}
fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas) fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
{ {
Cmd up = { Cmd up = {

View File

@ -218,121 +218,6 @@ fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)
const uint TAB_SIZE = 4; const uint TAB_SIZE = 4;
// TODO: change the name
// TODO: reorder to make smaller
struct TextInfo {
Font* font;
Rect bounds;
String text;
usz off;
Size width, height;
// current glyph info
Point origin;
Rect glyph_bounds;
Rect glyph_uv;
// cursor info
usz cursor_idx;
Point cursor_pos;
// current text bounds
Rect text_bounds;
}
<*
@require f != null
*>
fn void TextInfo.init(&self, Font* f, String s, Rect bounds = RECT_MAX)
{
*self = {};
self.font = f;
self.text = s;
self.bounds = bounds;
self.origin = bounds.position();
self.text_bounds = { .x = bounds.x, .y = bounds.y };
}
fn void TextInfo.reset(&self)
{
TextInfo old = *self;
self.init(old.font, old.text, old.bounds);
}
fn bool? TextInfo.place_glyph(&self, bool reflow)
{
if (self.off >= self.text.len) {
return false;
}
if (!self.origin.in_rect(self.bounds)) {
return false;
}
short baseline = (short)self.font.ascender;
short line_height = (short)self.font.ascender - (short)self.font.descender;
short line_gap = (short)self.font.linegap;
Codepoint cp;
Glyph* gp;
usz x;
cp = str_to_codepoint(self.text[self.off..], &x);
if (cp == 0) return false;
self.off += x;
if (ascii::is_cntrl((char)cp) == false) {
gp = self.font.get_glyph(cp)!;
self.glyph_uv = {
.x = gp.u,
.y = gp.v,
.w = gp.w,
.h = gp.h,
};
self.glyph_bounds = {
.x = self.origin.x + gp.ox,
.y = self.origin.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h,
};
// try to wrap the text if the charcater goes outside of the bounds
if (reflow && !self.bounds.contains(self.glyph_bounds)) {
self.origin.y += line_height + line_gap;
self.glyph_bounds.y += line_height + line_gap;
self.origin.x = self.bounds.x;
self.glyph_bounds.x = self.bounds.x;
}
// handle tab
if (cp == '\t') {
self.origin.x += gp.adv*TAB_SIZE;
} else {
self.origin.x += gp.adv;
}
} else if (cp == '\n'){
self.origin.y += line_height + line_gap;
self.origin.x = self.bounds.x;
}
self.text_bounds = containing_rect(self.text_bounds, self.glyph_bounds);
if (self.off == self.cursor_idx) {
self.cursor_pos = self.origin;
}
return true;
}
fn Rect? Ctx.get_text_bounds(&ctx, String text)
{
TextInfo ti;
ti.init(&ctx.font, text);
while (ti.place_glyph(false)!);
return ti.text_bounds;
}
struct TextSize { struct TextSize {
Size width, height; Size width, height;
int area; int area;
@ -345,6 +230,8 @@ struct TextSize {
// height.max: the height of the string with each word broken up by a new line // height.max: the height of the string with each word broken up by a new line
fn TextSize? Ctx.measure_string(&ctx, String text) fn TextSize? Ctx.measure_string(&ctx, String text)
{ {
if (text == "") return (TextSize){};
Font* font = &ctx.font; Font* font = &ctx.font;
short baseline = (short)font.ascender; short baseline = (short)font.ascender;
short line_height = (short)font.line_height(); short line_height = (short)font.line_height();
@ -412,3 +299,134 @@ fn TextSize? Ctx.measure_string(&ctx, String text)
return ts; return ts;
} }
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue)
{
if (text.len == 0 || bounds.w <= 0 || bounds.h <= 0) return;
ctx.push_scissor(bounds, z_index)!;
Font* font = &ctx.font;
Id texture_id = font.id;
short baseline = (short)font.ascender;
short line_height = (short)font.line_height();
short line_gap = (short)font.linegap;
short space_width = font.get_glyph(' ').adv!;
short tab_width = space_width * TAB_SIZE;
Point origin = bounds.position();
// measure string height inside the bounds to center it vertically
// FIXME: this is fast but it just gives back the height in LINES, most lines have only short
// characters which would result in a lower height
int string_height = line_height;
foreach (c: text) {
string_height += (line_height + line_gap) * (int)(c == '\n');
}
switch (anchor) {
case TOP_LEFT: nextcase;
case TOP: nextcase;
case TOP_RIGHT:
origin.y += 0;
case LEFT: nextcase;
case CENTER: nextcase;
case RIGHT:
origin.y += (short)(bounds.h - string_height)/2;
case BOTTOM_LEFT: nextcase;
case BOTTOM: nextcase;
case BOTTOM_RIGHT:
origin.y += (short)(bounds.h - string_height);
}
// measure the line until it exits the bounds or the string ends
usz line_start, line_end;
do {
int line_width;
Point o = {.x = bounds.x, .y = bounds.y};
Codepoint cp;
isz off = line_start;
for ITER: (usz x; (cp = str_to_codepoint(text[off..], &x)) != 0; off += x) {
Glyph* gp = font.get_glyph(cp)!;
switch {
case cp == '\n':
break ITER;
case cp == '\t':
o.x += tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Rect b = {
.x = o.x + gp.ox,
.y = o.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h,
};
if (b.x + b.w > bounds.x + bounds.w) {
break ITER;
}
o.x += gp.adv;
line_width += gp.adv;
}
}
line_end = off;
if (line_end == line_start) break;
// with the line width calculate the right origin and layout the line
switch (anchor) {
case TOP_LEFT: nextcase;
case LEFT: nextcase;
case BOTTOM_LEFT:
// TODO: we didn't need to measure the line width with this alignment
origin.x += 0;
case TOP: nextcase;
case CENTER: nextcase;
case BOTTOM:
origin.x += (short)(bounds.w - line_width)/2+1;
case TOP_RIGHT: nextcase;
case RIGHT: nextcase;
case BOTTOM_RIGHT:
origin.x += (short)(bounds.w - line_width);
}
// see the fixme when measuring the height
//ctx.push_rect({.x = origin.x,.y=origin.y,.w=(short)line_width,.h=(short)string_height}, z_index, &&(Style){.bg=0xff000042u.@to_rgba()})!;
String line = text[line_start:line_end-line_start];
off = 0;
for (usz x; (cp = str_to_codepoint(line[off..], &x)) != 0 && off < line.len; off += x) {
Glyph* gp = font.get_glyph(cp)!;
// update the text bounds
switch {
case cp == '\t':
origin.x += tab_width;
case ascii::is_cntrl((char)cp):
break;
default:
Rect b = {
.x = origin.x + gp.ox,
.y = origin.y + gp.oy + baseline,
.w = gp.w,
.h = gp.h
};
Rect uv = {
.x = gp.u,
.y = gp.v,
.w = gp.w,
.h = gp.h
};
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
//ctx.push_rect(b, z_index, &&(Style){.bg=0x0000ff66u.@to_rgba()})!;
origin.x += gp.adv;
}
}
// done with the line
line_start = line_end;
origin.y += line_height + line_gap;
} while(line_end < text.len);
ctx.reset_scissor(z_index)!;
}

View File

@ -3,16 +3,14 @@ module ugui;
import std::io; import std::io;
struct ElemText { struct ElemText {
String str;
usz cursor; // cursor offset usz cursor; // cursor offset
Id hash; Id hash;
Rect bounds; TextSize size;
} }
/* macro Ctx.text(&ctx, String text, ...)
macro Ctx.text_unbounded(&ctx, String text, ...) => ctx.text_id(@compute_id($vasplat), text);
=> ctx.text_unbounded_id(@compute_id($vasplat), text); fn void? Ctx.text_id(&ctx, Id id, String text)
fn void? Ctx.text_unbounded_id(&ctx, Id id, String text)
{ {
id = ctx.gen_id(id)!; id = ctx.gen_id(id)!;
@ -22,18 +20,22 @@ fn void? Ctx.text_unbounded_id(&ctx, Id id, String text)
Id text_hash = text.hash(); Id text_hash = text.hash();
if (elem.flags.is_new || elem.text.hash != text_hash) { if (elem.flags.is_new || elem.text.hash != text_hash) {
elem.text.bounds = ctx.get_text_bounds(text)!; elem.text.size = ctx.measure_string(text)!;
} }
elem.text.str = text;
elem.text.hash = text_hash; elem.text.hash = text_hash;
// 2. Layout elem.layout.w = @fit(style.size);
elem.bounds = ctx.layout_element(parent, elem.text.bounds, style); elem.layout.h = @fit(style.size);
if (elem.bounds.is_null()) { return; } elem.layout.text = elem.text.size;
elem.layout.content_offset = style.margin + style.border + style.padding;
ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!; update_parent_grow(elem, parent);
update_parent_size(elem, parent);
ctx.layout_string(text, elem.bounds.pad(elem.layout.content_offset), TOP_LEFT, parent.div.z_index, style.fg)!;
} }
/*
macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...) macro Ctx.text_box(&ctx, Rect size, char[] text, usz* text_len, ...)
=> ctx.text_box_id(@compute_id($vasplat), size, text, text_len); => ctx.text_box_id(@compute_id($vasplat), size, text, text_len);
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len) fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_len)

View File

@ -343,6 +343,7 @@ fn void calculator(ugui::Ctx* ui)
case "/": nextcase; case "/": nextcase;
case "(": nextcase; case "(": nextcase;
case ")": nextcase; case ")": nextcase;
case ".": nextcase;
case "0": nextcase; case "0": nextcase;
case "1": nextcase; case "1": nextcase;
case "2": nextcase; case "2": nextcase;
@ -367,7 +368,7 @@ fn void calculator(ugui::Ctx* ui)
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT) { ui.@div(ugui::@fit(), ugui::@fit(), COLUMN, TOP_LEFT) {
ui.@div(ugui::@grow(), ugui::@exact(100), ROW, RIGHT) { ui.@div(ugui::@grow(), ugui::@exact(100), ROW, RIGHT) {
ui.button("TODO")!!; ui.text((String)buffer[:len])!!;
}!!; }!!;
ui.@div(ugui::@fit(), ugui::@fit(), ROW, TOP_LEFT) { ui.@div(ugui::@fit(), ugui::@fit(), ROW, TOP_LEFT) {
ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) { ui.@div(ugui::@fit(), ugui::@fit(), COLUMN) {