changed text (glyph) placement
This commit is contained in:
parent
c1c1247af4
commit
b48c413d2d
13
TODO
13
TODO
@ -48,7 +48,9 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
|
||||
[x] Flexbox
|
||||
[ ] Center elements to the row/column
|
||||
[ ] 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
|
||||
[ ] Correct whitespace handling in text (\t \r etc)
|
||||
[ ] Consider a multi-pass recursive approach to layout (like https://github.com/nicbarker/clay)
|
||||
instead of the curren multi-frame approach.
|
||||
|
||||
@ -59,6 +61,8 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
[ ] Touch input
|
||||
[x] Do not set input event to true if the movement was zero (like no mouse movement)
|
||||
[ ] Use input event flags, for example to consume the input event
|
||||
[ ] Fix bug in text box: when spamming keys you can get multiple characters in the text input field
|
||||
of the context, this causes a bug where only the first char is actually used
|
||||
|
||||
## Commands
|
||||
|
||||
@ -67,7 +71,7 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
- border radius
|
||||
[x] add a command to update an atlas
|
||||
[ ] New window command, useful for popups
|
||||
[ ] Text command returns the text bounds, this way we can avoid the pattern
|
||||
[x] Text command returns the text bounds, this way we can avoid the pattern
|
||||
draw_text(a, pos) -> off = compute_bounds(a) -> draw_text(b, pos+off) -> ...
|
||||
|
||||
## Atlas
|
||||
@ -91,11 +95,6 @@ to maintain focus until mouse release (fix scroll bars)
|
||||
[x] Checkbox
|
||||
[ ] Selectable text box
|
||||
|
||||
## Main / exaple
|
||||
|
||||
[ ] Create maps from ids to textures and images instead of hardcoding them
|
||||
|
||||
|
||||
## API
|
||||
|
||||
[ ] Introduce a Layout structure that specifies the positioning of elements inside
|
||||
|
@ -44,17 +44,20 @@ fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool ac
|
||||
|
||||
Elem *parent = ctx.get_parent()!;
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_BUTTON)!;
|
||||
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
Rect text_size = ctx.get_text_bounds(label)!;
|
||||
Rect btn_size = text_size.add({0,0,10,10});
|
||||
Style* style_norm = ctx.styles.get_style(@str_hash("button"));
|
||||
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
|
||||
|
||||
// TODO: cache text_size just like text_unbounded() does
|
||||
Rect text_size = ctx.get_text_bounds(label)!;
|
||||
Rect btn_size = text_size.grow({10,10});
|
||||
|
||||
// 2. Layout
|
||||
elem.bounds = ctx.position_element(parent, btn_size, style_norm);
|
||||
if (elem.bounds.is_null()) { return {}; }
|
||||
|
||||
Style* style_active = ctx.styles.get_style(@str_hash("button-active"));
|
||||
|
||||
text_size.x = elem.bounds.x;
|
||||
text_size.y = elem.bounds.y;
|
||||
text_size = text_size.center_to(elem.bounds);
|
||||
|
||||
elem.events = ctx.get_elem_events(elem);
|
||||
|
||||
@ -62,11 +65,6 @@ fn ElemEvents? Ctx.button_label_id(&ctx, Id id, String label, Rect size, bool ac
|
||||
Style* style = is_active ? style_active : style_norm;
|
||||
|
||||
// Draw the button
|
||||
text_size.x = elem.bounds.x;
|
||||
text_size.y = elem.bounds.y;
|
||||
Point off = ctx.center_text(text_size, elem.bounds);
|
||||
text_size.x += off.x;
|
||||
text_size.y += off.y;
|
||||
ctx.push_rect(elem.bounds, parent.div.z_index, style)!;
|
||||
ctx.push_string(text_size, label, parent.div.z_index, style.fg)!;
|
||||
|
||||
|
@ -136,56 +136,30 @@ fn void? Ctx.push_sprite(&ctx, Rect bounds, Rect texture, Id texture_id, int z_i
|
||||
ctx.push_cmd(&cmd, z_index)!;
|
||||
}
|
||||
|
||||
fn void? Ctx.push_string(&ctx, Rect bounds, char[] text, int z_index, Color hue)
|
||||
// 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;
|
||||
return {};
|
||||
}
|
||||
|
||||
ctx.push_scissor(bounds, z_index)!;
|
||||
|
||||
short baseline = (short)ctx.font.ascender;
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
Id texture_id = ctx.font.id; // or ctx.font.atlas.id
|
||||
Point orig = {
|
||||
.x = bounds.x,
|
||||
.y = bounds.y,
|
||||
};
|
||||
Rect text_bounds = {bounds.x, bounds.y, 0, 0};
|
||||
|
||||
short line_len;
|
||||
Codepoint cp;
|
||||
usz off, x;
|
||||
while (off < text.len && (cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||
off += x;
|
||||
Glyph* gp;
|
||||
if (!ascii::is_cntrl((char)cp)) {
|
||||
gp = ctx.font.get_glyph(cp)!;
|
||||
Rect gb = {
|
||||
.x = orig.x + line_len + gp.ox,
|
||||
.y = orig.y + gp.oy + baseline,
|
||||
.w = gp.w,
|
||||
.h = gp.h,
|
||||
};
|
||||
Rect gt = {
|
||||
.x = gp.u,
|
||||
.y = gp.v,
|
||||
.w = gp.w,
|
||||
.h = gp.h,
|
||||
};
|
||||
// push the sprite only if it collides with the bounds
|
||||
if (!cull_rect(gb, bounds)) ctx.push_sprite(gb, gt, texture_id, z_index, hue)!;
|
||||
line_len += gp.adv;
|
||||
} else if (cp == '\n'){
|
||||
orig.y += line_height + line_gap;
|
||||
line_len = 0;
|
||||
} else {
|
||||
continue;
|
||||
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)!;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: we never get here if an error was thrown before
|
||||
|
||||
ctx.push_scissor({}, z_index)!;
|
||||
|
||||
return ti;
|
||||
}
|
||||
|
||||
fn void? Ctx.push_update_atlas(&ctx, Atlas* atlas)
|
||||
|
@ -167,6 +167,7 @@ fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale
|
||||
|
||||
<*
|
||||
@require off != null
|
||||
@require str.ptr != null
|
||||
*>
|
||||
fn Codepoint str_to_codepoint(char[] str, usz* off)
|
||||
{
|
||||
@ -179,72 +180,119 @@ fn Codepoint str_to_codepoint(char[] str, usz* off)
|
||||
return cp;
|
||||
}
|
||||
|
||||
const uint TAB_SIZE = 4;
|
||||
|
||||
// TODO: change the name
|
||||
// TODO: reorder to make smaller
|
||||
struct TextInfo {
|
||||
Font* font;
|
||||
Rect bounds;
|
||||
String text;
|
||||
usz off;
|
||||
|
||||
// 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)
|
||||
{
|
||||
Rect text_bounds;
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
text_bounds.h = line_height;
|
||||
Glyph* gp;
|
||||
|
||||
// TODO: account for unicode codepoints
|
||||
short line_len;
|
||||
Codepoint cp;
|
||||
usz off, x;
|
||||
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||
off += x;
|
||||
bool n;
|
||||
if (!ascii::is_cntrl((char)cp)) {
|
||||
gp = ctx.font.get_glyph(cp)!;
|
||||
line_len += gp.adv;
|
||||
} else if (cp == '\n'){
|
||||
text_bounds.h += line_height + line_gap;
|
||||
line_len = 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if (line_len > text_bounds.w) {
|
||||
text_bounds.w = line_len;
|
||||
}
|
||||
}
|
||||
|
||||
return text_bounds;
|
||||
}
|
||||
|
||||
fn Point? Ctx.get_cursor_position(&ctx, String text)
|
||||
{
|
||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||
short line_gap = (short)ctx.font.linegap;
|
||||
Glyph* gp;
|
||||
|
||||
// TODO: account for unicode codepoints
|
||||
Point line;
|
||||
Codepoint cp;
|
||||
usz off, x;
|
||||
while ((cp = str_to_codepoint(text[off..], &x)) != 0) {
|
||||
off += x;
|
||||
bool n;
|
||||
if (!ascii::is_cntrl((char)cp)) {
|
||||
gp = ctx.font.get_glyph(cp)!;
|
||||
line.x += gp.adv;
|
||||
} else if (cp == '\n'){
|
||||
line.y += line_height + line_gap;
|
||||
line.x = 0;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
|
||||
fn Point Ctx.center_text(&ctx, Rect text_bounds, Rect bounds)
|
||||
{
|
||||
short dw = bounds.w - text_bounds.w;
|
||||
short dh = bounds.h - text_bounds.h;
|
||||
|
||||
return {.x = dw/2, .y = dh/2};
|
||||
TextInfo ti;
|
||||
ti.init(&ctx.font, text);
|
||||
while (ti.place_glyph(false)!);
|
||||
return ti.text_bounds;
|
||||
}
|
||||
|
||||
// TODO: check if the font is present in the context
|
||||
|
@ -10,6 +10,9 @@ struct Rect {
|
||||
short x, y, w, h;
|
||||
}
|
||||
|
||||
// TODO: find another name
|
||||
const Rect RECT_MAX = {0, 0, short.max, short.max};
|
||||
|
||||
// return true if rect a contains b
|
||||
macro bool Rect.contains(Rect a, Rect b)
|
||||
{
|
||||
@ -33,6 +36,21 @@ macro bool Rect.collides(Rect a, Rect b)
|
||||
return !(a.x > b.x+b.w || a.x+a.w < b.x || a.y > b.y+b.h || a.y+a.h < b.y);
|
||||
}
|
||||
|
||||
// return a rect that contains both rects, a bounding box of both
|
||||
macro Rect containing_rect(Rect a, Rect b)
|
||||
{
|
||||
short min_x = (short)min(a.x, b.x);
|
||||
short min_y = (short)min(a.y, b.y);
|
||||
short max_x = (short)max(a.x + a.w, b.x + b.w);
|
||||
short max_y = (short)max(a.y + a.h, b.y + b.h);
|
||||
return {
|
||||
.x = min_x,
|
||||
.y = min_y,
|
||||
.w = (short)(max_x - min_x),
|
||||
.h = (short)(max_y - min_y)
|
||||
};
|
||||
}
|
||||
|
||||
// check for empty rect
|
||||
macro bool Rect.is_null(Rect r) => r.x == 0 && r.y == 0 && r.x == 0 && r.w == 0;
|
||||
|
||||
@ -136,6 +154,12 @@ macro Point Rect.bottom_right(Rect r)
|
||||
};
|
||||
}
|
||||
|
||||
macro Rect Rect.center_to(Rect a, Rect b)
|
||||
{
|
||||
Point off = {.x = (b.w - a.w)/2, .y = (b.h - a.h)/2};
|
||||
return a.off(off);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------- //
|
||||
// POINT //
|
||||
|
@ -3,8 +3,10 @@ module ugui;
|
||||
import std::io;
|
||||
|
||||
struct ElemText {
|
||||
char[] str;
|
||||
String str;
|
||||
usz cursor; // cursor offset
|
||||
Id hash;
|
||||
Rect bounds;
|
||||
}
|
||||
|
||||
macro Ctx.text_unbounded(&ctx, String text, ...)
|
||||
@ -17,12 +19,15 @@ fn void? Ctx.text_unbounded_id(&ctx, Id id, String text)
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("text"));
|
||||
|
||||
Id text_hash = text.hash();
|
||||
if (elem.flags.is_new || elem.text.hash != text_hash) {
|
||||
elem.text.bounds = ctx.get_text_bounds(text)!;
|
||||
}
|
||||
elem.text.str = text;
|
||||
elem.text.hash = text_hash;
|
||||
|
||||
// if the element is new or the parent was updated then redo layout
|
||||
Rect text_size = ctx.get_text_bounds(text)!;
|
||||
// 2. Layout
|
||||
elem.bounds = ctx.position_element(parent, text_size, style);
|
||||
elem.bounds = ctx.position_element(parent, elem.text.bounds, style);
|
||||
if (elem.bounds.is_null()) { return; }
|
||||
|
||||
ctx.push_string(elem.bounds, text, parent.div.z_index, style.fg)!;
|
||||
@ -38,7 +43,7 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_le
|
||||
Elem *elem = ctx.get_elem(id, ETYPE_TEXT)!;
|
||||
Style* style = ctx.styles.get_style(@str_hash("text-box"));
|
||||
|
||||
elem.text.str = text;
|
||||
elem.text.str = (String)text;
|
||||
|
||||
// layout the text box
|
||||
elem.bounds = ctx.position_element(parent, size, style);
|
||||
@ -64,27 +69,11 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Rect size, char[] text, usz* text_le
|
||||
|
||||
// draw the box
|
||||
short line_height = (short)ctx.font.line_height();
|
||||
Rect text_box = elem.bounds.sub({0,0,0,line_height});
|
||||
Rect input_box = {
|
||||
.x = elem.bounds.x,
|
||||
.y = elem.bounds.y + elem.bounds.h - line_height,
|
||||
.w = elem.bounds.w,
|
||||
.h = line_height,
|
||||
};
|
||||
Rect cursor;
|
||||
Point b = ctx.get_cursor_position((String)text[:elem.text.cursor])!;
|
||||
cursor = {
|
||||
.x = b.x,
|
||||
.y = b.y,
|
||||
.w = 3,
|
||||
.h = line_height,
|
||||
};
|
||||
cursor = cursor.off(elem.bounds.position());
|
||||
|
||||
Rect text_box = elem.bounds;
|
||||
ctx.push_rect(text_box, parent.div.z_index, style)!;
|
||||
ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg)!;
|
||||
ctx.push_rect(input_box, parent.div.z_index, style)!;
|
||||
ctx.push_rect(cursor, parent.div.z_index, style)!;
|
||||
ctx.push_string(text_box, text[:*text_len], parent.div.z_index, style.fg, true)!;
|
||||
|
||||
// TODO: draw cursor
|
||||
|
||||
return elem.events;
|
||||
}
|
||||
|
@ -279,7 +279,7 @@ fn int main(String[] args)
|
||||
ui.layout_set_column()!!;
|
||||
ui.text_unbounded(string::tformat("frame %d, fps = %.2f", frame, fps))!!;
|
||||
ui.text_unbounded(string::tformat("ui avg: %s\ndraw avg: %s\nTOT: %s", uts.avg, dts.avg, uts.avg+dts.avg))!!;
|
||||
ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[..]))!!;
|
||||
ui.text_unbounded(string::tformat("%s %s", mod.lctrl, (String)ui.input.keyboard.text[:ui.input.keyboard.text_len]))!!;
|
||||
};
|
||||
ui.div_end()!!;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user