rewrote string layout
This commit is contained in:
parent
5dfbad2399
commit
e3c0bac9ca
@ -1,7 +1,6 @@
|
|||||||
module ugui;
|
module ugui;
|
||||||
|
|
||||||
import schrift;
|
import schrift;
|
||||||
import grapheme;
|
|
||||||
import std::collections::map;
|
import std::collections::map;
|
||||||
import std::core::mem;
|
import std::core::mem;
|
||||||
import std::core::mem::allocator;
|
import std::core::mem::allocator;
|
||||||
@ -16,22 +15,6 @@ import std::ascii;
|
|||||||
// unicode code point, different type for a different hash
|
// unicode code point, different type for a different hash
|
||||||
alias Codepoint = uint;
|
alias Codepoint = uint;
|
||||||
|
|
||||||
<*
|
|
||||||
@require str.ptr != null: "string pointer must be non null"
|
|
||||||
@param [in] str
|
|
||||||
@param [&inout] off
|
|
||||||
*>
|
|
||||||
fn Codepoint str_to_codepoint(char[] str, usz* off)
|
|
||||||
{
|
|
||||||
Codepoint cp;
|
|
||||||
isz b = grapheme::decode_utf8(str, str.len, (uint*)&cp);
|
|
||||||
if (b == 0 || b > str.len) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
*off = b;
|
|
||||||
return cp;
|
|
||||||
}
|
|
||||||
|
|
||||||
//macro uint Codepoint.hash(self) => ((uint)self).hash();
|
//macro uint Codepoint.hash(self) => ((uint)self).hash();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------- //
|
||||||
@ -236,285 +219,3 @@ fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
|
|||||||
|
|
||||||
<* @param [&in] font *>
|
<* @param [&in] font *>
|
||||||
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);
|
fn int Font.line_height(&font) => (int)(font.ascender - font.descender + (float)0.5);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
// TEXT MEASUREMENT //
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
|
||||||
|
|
||||||
|
|
||||||
const uint TAB_SIZE = 4;
|
|
||||||
|
|
||||||
struct TextSize {
|
|
||||||
Size width, height;
|
|
||||||
int area;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measeure the size of a string.
|
|
||||||
// width.min: as if each word is broken up by a new line
|
|
||||||
// width.max: the width of the string left as-is
|
|
||||||
// height.min: the height of the string left as-is
|
|
||||||
// height.max: the height of the string with each word broken up by a new line
|
|
||||||
<*
|
|
||||||
@param [&in] ctx
|
|
||||||
@param [in] text
|
|
||||||
*>
|
|
||||||
fn TextSize? Ctx.measure_string(&ctx, String text)
|
|
||||||
{
|
|
||||||
if (text == "") return (TextSize){};
|
|
||||||
|
|
||||||
Font* font = &ctx.font;
|
|
||||||
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;
|
|
||||||
|
|
||||||
isz off;
|
|
||||||
usz x;
|
|
||||||
|
|
||||||
TextSize ts;
|
|
||||||
|
|
||||||
short word_width;
|
|
||||||
short words = 1;
|
|
||||||
Rect bounds; // unaltered text bounds;
|
|
||||||
Point origin;
|
|
||||||
|
|
||||||
Codepoint cp = str_to_codepoint(text[off..], &x);
|
|
||||||
for (; cp != 0; cp = str_to_codepoint(text[off..], &x)) {
|
|
||||||
off += x;
|
|
||||||
Glyph* gp = font.get_glyph(cp)!;
|
|
||||||
|
|
||||||
// update the text bounds
|
|
||||||
switch {
|
|
||||||
case cp == '\n':
|
|
||||||
origin.x = 0;
|
|
||||||
origin.y += line_height + line_gap;
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
bounds = containing_rect(bounds, b);
|
|
||||||
origin.x += gp.adv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the word width
|
|
||||||
switch {
|
|
||||||
case ascii::is_space((char)cp):
|
|
||||||
if (word_width > ts.width.min) ts.width.min = word_width;
|
|
||||||
word_width = 0;
|
|
||||||
words++;
|
|
||||||
default:
|
|
||||||
//word_width += gp.w + gp.ox;
|
|
||||||
if (off < text.len) {
|
|
||||||
word_width += gp.adv;
|
|
||||||
} else {
|
|
||||||
word_width += gp.w + gp.ox;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// end of string is also end of word
|
|
||||||
if (word_width > ts.width.min) ts.width.min = word_width;
|
|
||||||
|
|
||||||
ts.width.max = bounds.w;
|
|
||||||
ts.height.min = bounds.h;
|
|
||||||
ts.height.max = words * line_height + line_gap * (words-1);
|
|
||||||
ts.area = bounds.w * bounds.h;
|
|
||||||
|
|
||||||
return ts;
|
|
||||||
}
|
|
||||||
|
|
||||||
// layout a string inside a bounding box, following the given alignment (anchor).
|
|
||||||
// returns the position of the cursor, the returned height is the line height and the width is the last
|
|
||||||
// character's advance value
|
|
||||||
// TODO: implement a "reflow" flag to toggle reflow if a character goes out of bounds
|
|
||||||
// TODO: also return the total bounds of the laid out string
|
|
||||||
// TODO: Following improvements
|
|
||||||
// [ ] struct LineInfo which stores the line information, like offset, width and height, during
|
|
||||||
// the measurement stage each line is scanned and pushed to the list, then to layout
|
|
||||||
// simply pop each line. The LineInfo list can be stored on the stack with the tmem allocator
|
|
||||||
// [ ] if the alignment is TOP_LEFT we can simply skip measuring the string, opting to layout
|
|
||||||
// it directly
|
|
||||||
// [ ] implement a macro to fetch and layout each character, this can be used to reduce code
|
|
||||||
// repetition both here and in measure_string
|
|
||||||
// [ ] the cursor position can be determined by a separate function like get_cursor_position()
|
|
||||||
// this way the function can terminate early if the cursor position is found
|
|
||||||
// [ ] implement a function hit_test_string() to get the character position at point, this can
|
|
||||||
// be used to implement mouse interactions, like cursor movement and selection
|
|
||||||
<*
|
|
||||||
@param [&in] ctx
|
|
||||||
@param [in] text
|
|
||||||
*>
|
|
||||||
fn Rect? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, isz cursor = -1)
|
|
||||||
{
|
|
||||||
Font* font = &ctx.font;
|
|
||||||
short line_height = (short)font.line_height();
|
|
||||||
Rect cursor_rect = {.h = line_height};
|
|
||||||
|
|
||||||
if (bounds.w <= 0 || bounds.h <= 0) return cursor_rect;
|
|
||||||
ctx.push_scissor(bounds, z_index)!;
|
|
||||||
|
|
||||||
if (text == "") {
|
|
||||||
// when text is empty but we need to draw a cursor use a visually empty string
|
|
||||||
if (cursor >= 0) {
|
|
||||||
text = "\f";
|
|
||||||
} else {
|
|
||||||
return cursor_rect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Id texture_id = font.id;
|
|
||||||
short baseline = (short)font.ascender;
|
|
||||||
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;
|
|
||||||
short first_off;
|
|
||||||
for ITER: (usz x; (cp = str_to_codepoint(text[off..], &x)) != 0; off += x) {
|
|
||||||
Glyph* gp = font.get_glyph(cp)!;
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case cp == '\n':
|
|
||||||
off += x;
|
|
||||||
break ITER;
|
|
||||||
case cp == '\t':
|
|
||||||
o.x += tab_width;
|
|
||||||
case ascii::is_cntrl((char)cp):
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (off == line_start) first_off = gp.ox;
|
|
||||||
|
|
||||||
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) unreachable("something went wrong in measuring the line");
|
|
||||||
|
|
||||||
// with the line width calculate the right origin and layout the line
|
|
||||||
origin.x = bounds.x - first_off;
|
|
||||||
short next_line_x = bounds.x; // the x coordinate of the origin if the line_width is zero
|
|
||||||
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;
|
|
||||||
next_line_x += 0;
|
|
||||||
case TOP: nextcase;
|
|
||||||
case CENTER: nextcase;
|
|
||||||
case BOTTOM:
|
|
||||||
origin.x += (short)(bounds.w - line_width)/2+1;
|
|
||||||
next_line_x += bounds.w/2;
|
|
||||||
case TOP_RIGHT: nextcase;
|
|
||||||
case RIGHT: nextcase;
|
|
||||||
case BOTTOM_RIGHT:
|
|
||||||
origin.x += (short)(bounds.w - line_width);
|
|
||||||
next_line_x += bounds.w;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line_start <= cursor && cursor <= line_end) {
|
|
||||||
cursor_rect.x = origin.x;
|
|
||||||
cursor_rect.y = origin.y;
|
|
||||||
cursor_rect.w = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)!;
|
|
||||||
|
|
||||||
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)!;
|
|
||||||
origin.x += gp.adv;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line_start + off + x == cursor) {
|
|
||||||
cursor_rect.x = origin.x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// done with the line
|
|
||||||
line_start = line_end;
|
|
||||||
origin.y += line_height + line_gap;
|
|
||||||
|
|
||||||
if (cursor == text.len && text[^1] == '\n') {
|
|
||||||
cursor_rect.x = next_line_x;
|
|
||||||
cursor_rect.y = origin.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
} while(line_end < text.len);
|
|
||||||
|
|
||||||
|
|
||||||
ctx.reset_scissor(z_index)!;
|
|
||||||
|
|
||||||
return cursor_rect;
|
|
||||||
}
|
|
||||||
|
@ -184,7 +184,6 @@ macro Rect Rect.expand(Rect a, Rect b)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------- //
|
||||||
// POINT //
|
// POINT //
|
||||||
// ---------------------------------------------------------------------------------- //
|
// ---------------------------------------------------------------------------------- //
|
||||||
|
@ -1,4 +1,445 @@
|
|||||||
module ugui;
|
module ugui;
|
||||||
|
|
||||||
|
import std::collections::list;
|
||||||
|
import std::core::string;
|
||||||
|
|
||||||
|
struct LineInfo @local {
|
||||||
|
usz start, end;
|
||||||
|
short width, height;
|
||||||
|
short first_off; // first character offset
|
||||||
|
}
|
||||||
|
|
||||||
|
alias LineStack @local = list::List{LineInfo};
|
||||||
|
|
||||||
|
fn short Rect.y_off(Rect bounds, short height, Anchor anchor) @local
|
||||||
|
{
|
||||||
|
short off;
|
||||||
|
|
||||||
|
switch (anchor) {
|
||||||
|
case TOP_LEFT: nextcase;
|
||||||
|
case TOP: nextcase;
|
||||||
|
case TOP_RIGHT:
|
||||||
|
off = 0;
|
||||||
|
case LEFT: nextcase;
|
||||||
|
case CENTER: nextcase;
|
||||||
|
case RIGHT:
|
||||||
|
off = (short)(bounds.h - height)/2;
|
||||||
|
case BOTTOM_LEFT: nextcase;
|
||||||
|
case BOTTOM: nextcase;
|
||||||
|
case BOTTOM_RIGHT:
|
||||||
|
off = (short)(bounds.h - height);
|
||||||
|
}
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn short Rect.x_off(Rect bounds, short width, Anchor anchor) @local
|
||||||
|
{
|
||||||
|
short off;
|
||||||
|
|
||||||
|
switch (anchor) {
|
||||||
|
case TOP_LEFT: nextcase;
|
||||||
|
case LEFT: nextcase;
|
||||||
|
case BOTTOM_LEFT:
|
||||||
|
off = 0;
|
||||||
|
case TOP: nextcase;
|
||||||
|
case CENTER: nextcase;
|
||||||
|
case BOTTOM:
|
||||||
|
off = (short)(bounds.w - width)/2;
|
||||||
|
case TOP_RIGHT: nextcase;
|
||||||
|
case RIGHT: nextcase;
|
||||||
|
case BOTTOM_RIGHT:
|
||||||
|
off = (short)(bounds.w - width);
|
||||||
|
}
|
||||||
|
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------- //
|
||||||
|
// STRING LAYOUT //
|
||||||
|
// ---------------------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
|
||||||
|
<*
|
||||||
|
@param[&in] ctx
|
||||||
|
@param[in] text
|
||||||
|
*>
|
||||||
|
fn void? Ctx.layout_string(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
|
||||||
|
{
|
||||||
|
if (anchor == TOP_LEFT) {
|
||||||
|
return ctx.layout_string_topleft(text, bounds, z_index, hue, reflow);
|
||||||
|
} else {
|
||||||
|
return ctx.layout_string_aligned(text, bounds, anchor, z_index, hue, reflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout a string inside a bounding box, following the given alignment (anchor).
|
||||||
|
// TODO: Following improvements
|
||||||
|
// [ ] implement a macro to fetch and layout each character, this can be used to reduce code
|
||||||
|
// repetition both here and in measure_string
|
||||||
|
// [ ] the cursor position can be determined by a separate function like get_cursor_position()
|
||||||
|
// this way the function can terminate early if the cursor position is found
|
||||||
|
// [ ] implement a function hit_test_string() to get the character position at point, this can
|
||||||
|
// be used to implement mouse interactions, like cursor movement and selection
|
||||||
|
<*
|
||||||
|
@param [&in] ctx
|
||||||
|
@param [in] text
|
||||||
|
*>
|
||||||
|
fn void? Ctx.layout_string_aligned(&ctx, String text, Rect bounds, Anchor anchor, int z_index, Color hue, bool reflow = false)
|
||||||
|
{
|
||||||
|
if (text == "") return;
|
||||||
|
if (bounds.w <= 0 || bounds.h <= 0) return;
|
||||||
|
ctx.push_scissor(bounds, z_index)!;
|
||||||
|
|
||||||
|
Font* font = &ctx.font;
|
||||||
|
Id texture_id = font.id;
|
||||||
|
short line_height = (short)font.line_height();
|
||||||
|
short baseline = (short)font.ascender;
|
||||||
|
short line_gap = (short)font.linegap;
|
||||||
|
short space_width = font.get_glyph(' ').adv!;
|
||||||
|
short tab_width = space_width * TAB_SIZE;
|
||||||
|
|
||||||
|
Point origin = bounds.position();
|
||||||
|
|
||||||
|
LineStack lines;
|
||||||
|
lines.init(tmem, 1);
|
||||||
|
|
||||||
|
Rect str_bounds;
|
||||||
|
|
||||||
|
usz line_start;
|
||||||
|
LineInfo li;
|
||||||
|
Point o;
|
||||||
|
StringIterator ti = text.iterator();
|
||||||
|
for (Codepoint cp; ti.has_next();) {
|
||||||
|
// FIXME: what if the interface changes?
|
||||||
|
cp = ti.next()!;
|
||||||
|
usz off = ti.current;
|
||||||
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
|
bool push = false;
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cp == '\n':
|
||||||
|
push = true;
|
||||||
|
case cp == '\t':
|
||||||
|
o.x += tab_width;
|
||||||
|
case ascii::is_cntrl((char)cp):
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (off == line_start) li.first_off = gp.ox;
|
||||||
|
|
||||||
|
Rect b = {
|
||||||
|
.x = o.x + gp.ox,
|
||||||
|
.y = o.y + gp.oy + baseline,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reflow && b.x + b.w > bounds.w) {
|
||||||
|
li.width += gp.ox + gp.w;
|
||||||
|
li.height = line_height;
|
||||||
|
push = true;
|
||||||
|
} else {
|
||||||
|
o.x += gp.adv;
|
||||||
|
li.width += gp.adv;
|
||||||
|
li.height = line_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (push) {
|
||||||
|
li.start = line_start;
|
||||||
|
li.end = off;
|
||||||
|
lines.push(li);
|
||||||
|
str_bounds.w = max(str_bounds.w, li.width);
|
||||||
|
str_bounds.h += li.height;
|
||||||
|
|
||||||
|
o.x = 0;
|
||||||
|
o.y += line_height;
|
||||||
|
line_start = off;
|
||||||
|
|
||||||
|
li.height = 0;
|
||||||
|
li.width = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FIXME: crap
|
||||||
|
li.start = line_start;
|
||||||
|
li.end = ti.current;
|
||||||
|
lines.push(li);
|
||||||
|
str_bounds.w = max(str_bounds.w, li.width);
|
||||||
|
str_bounds.h += li.height;
|
||||||
|
|
||||||
|
// account for the line gap
|
||||||
|
str_bounds.h += (short)(lines.len() - 1)*line_gap;
|
||||||
|
|
||||||
|
o = bounds.position();
|
||||||
|
o.y += bounds.y_off(str_bounds.h, anchor);
|
||||||
|
foreach (idx, line : lines) {
|
||||||
|
o.x = bounds.x + bounds.x_off(line.width, anchor) - line.first_off;
|
||||||
|
|
||||||
|
StringIterator s = text[line.start:line.end-line.start].iterator();
|
||||||
|
for (Codepoint cp; s.has_next();) {
|
||||||
|
cp = s.next()!;
|
||||||
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cp == '\n':
|
||||||
|
break;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
Rect uv = {
|
||||||
|
.x = gp.u,
|
||||||
|
.y = gp.v,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
|
||||||
|
o.x += gp.adv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.y += line.height + line_gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.reset_scissor(z_index)!;
|
||||||
|
// ctx.dbg_rect(str_bounds.off(bounds.position()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout a string inside a bounding box, with TOP_LEFT alignment.
|
||||||
|
<*
|
||||||
|
@param[&in] ctx
|
||||||
|
@param[in] text
|
||||||
|
*>
|
||||||
|
fn void? Ctx.layout_string_topleft(&ctx, String text, Rect bounds, int z_index, Color hue, bool reflow = false)
|
||||||
|
{
|
||||||
|
if (text == "") return;
|
||||||
|
if (bounds.w <= 0 || bounds.h <= 0) return;
|
||||||
|
ctx.push_scissor(bounds, z_index)!;
|
||||||
|
|
||||||
|
Font* font = &ctx.font;
|
||||||
|
Id texture_id = font.id;
|
||||||
|
short line_height = (short)font.line_height();
|
||||||
|
short baseline = (short)font.ascender;
|
||||||
|
short line_gap = (short)font.linegap;
|
||||||
|
short space_width = font.get_glyph(' ').adv!;
|
||||||
|
short tab_width = space_width * TAB_SIZE;
|
||||||
|
|
||||||
|
Point o = bounds.position();
|
||||||
|
StringIterator it = text.iterator();
|
||||||
|
for (Codepoint cp; it.has_next();) {
|
||||||
|
cp = it.next()!;
|
||||||
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cp == '\n':
|
||||||
|
o.y += line_height;
|
||||||
|
o.x = bounds.x;
|
||||||
|
break;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
Rect uv = {
|
||||||
|
.x = gp.u,
|
||||||
|
.y = gp.v,
|
||||||
|
.w = gp.w,
|
||||||
|
.h = gp.h
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reflow && b.x + b.w > bounds.x + bounds.w) {
|
||||||
|
o.y += line_height + line_gap;
|
||||||
|
o.x = bounds.x;
|
||||||
|
b.x = o.x + gp.ox;
|
||||||
|
b.y = o.y + gp.oy + baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.push_sprite(b, uv, texture_id, z_index, hue)!;
|
||||||
|
o.x += gp.adv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.reset_scissor(z_index)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------- //
|
||||||
|
// CURSOR AND MOUSE //
|
||||||
|
// ---------------------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
|
||||||
|
fn Rect? Ctx.get_cursor_position(&ctx, String text, Rect bounds, Anchor anchor, usz cursor, bool reflow = false)
|
||||||
|
{
|
||||||
|
if (anchor != TOP_LEFT) {
|
||||||
|
unreachable("TODO: anchor has to be TOP_LEFT");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bounds.w <= 0 || bounds.h <= 0) return {};
|
||||||
|
if (text == "") text = "\f";
|
||||||
|
|
||||||
|
Font* font = &ctx.font;
|
||||||
|
Id texture_id = font.id;
|
||||||
|
short line_height = (short)font.line_height();
|
||||||
|
short baseline = (short)font.ascender;
|
||||||
|
short line_gap = (short)font.linegap;
|
||||||
|
short space_width = font.get_glyph(' ').adv!;
|
||||||
|
short tab_width = space_width * TAB_SIZE;
|
||||||
|
|
||||||
|
Rect cursor_rect;
|
||||||
|
cursor_rect.x = bounds.x;
|
||||||
|
cursor_rect.y = bounds.y;
|
||||||
|
cursor_rect.h = line_height;
|
||||||
|
|
||||||
|
if (cursor == 0) return cursor_rect;
|
||||||
|
|
||||||
|
Point o = bounds.position();
|
||||||
|
StringIterator it = text.iterator();
|
||||||
|
usz off;
|
||||||
|
for (Codepoint cp; it.has_next();) {
|
||||||
|
cp = it.next()!;
|
||||||
|
off = it.current;
|
||||||
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cp == '\n':
|
||||||
|
o.y += line_height;
|
||||||
|
o.x = bounds.x;
|
||||||
|
break;
|
||||||
|
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 (reflow && b.x + b.w > bounds.x + bounds.w) {
|
||||||
|
o.y += line_height + line_gap;
|
||||||
|
o.x = bounds.x;
|
||||||
|
b.x = o.x + gp.ox;
|
||||||
|
b.y = o.y + gp.oy + baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
o.x += gp.adv;
|
||||||
|
}
|
||||||
|
if (off == cursor) {
|
||||||
|
cursor_rect.x = o.x;
|
||||||
|
cursor_rect.y = o.y;
|
||||||
|
return cursor_rect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------- //
|
||||||
|
// TEXT MEASUREMENT //
|
||||||
|
// ---------------------------------------------------------------------------------- //
|
||||||
|
|
||||||
|
|
||||||
|
const uint TAB_SIZE = 4;
|
||||||
|
|
||||||
|
struct TextSize {
|
||||||
|
Size width, height;
|
||||||
|
int area;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measeure the size of a string.
|
||||||
|
// width.min: as if each word is broken up by a new line
|
||||||
|
// width.max: the width of the string left as-is
|
||||||
|
// height.min: the height of the string left as-is
|
||||||
|
// height.max: the height of the string with each word broken up by a new line
|
||||||
|
<*
|
||||||
|
@param [&in] ctx
|
||||||
|
@param [in] text
|
||||||
|
*>
|
||||||
|
fn TextSize? Ctx.measure_string(&ctx, String text)
|
||||||
|
{
|
||||||
|
if (text == "") return (TextSize){};
|
||||||
|
|
||||||
|
Font* font = &ctx.font;
|
||||||
|
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;
|
||||||
|
|
||||||
|
isz off;
|
||||||
|
usz x;
|
||||||
|
|
||||||
|
TextSize ts;
|
||||||
|
|
||||||
|
short word_width;
|
||||||
|
short words = 1;
|
||||||
|
Rect bounds; // unaltered text bounds;
|
||||||
|
Point origin;
|
||||||
|
|
||||||
|
StringIterator it = text.iterator();
|
||||||
|
for (Codepoint cp; it.has_next();) {
|
||||||
|
cp = it.next()!;
|
||||||
|
Glyph* gp = font.get_glyph(cp)!;
|
||||||
|
|
||||||
|
// update the text bounds
|
||||||
|
switch {
|
||||||
|
case cp == '\n':
|
||||||
|
origin.x = 0;
|
||||||
|
origin.y += line_height + line_gap;
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
bounds = containing_rect(bounds, b);
|
||||||
|
origin.x += gp.adv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the word width
|
||||||
|
switch {
|
||||||
|
case ascii::is_space((char)cp):
|
||||||
|
if (word_width > ts.width.min) ts.width.min = word_width;
|
||||||
|
word_width = 0;
|
||||||
|
words++;
|
||||||
|
default:
|
||||||
|
//word_width += gp.w + gp.ox;
|
||||||
|
if (off < text.len) {
|
||||||
|
word_width += gp.adv;
|
||||||
|
} else {
|
||||||
|
word_width += gp.w + gp.ox;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end of string is also end of word
|
||||||
|
if (word_width > ts.width.min) ts.width.min = word_width;
|
||||||
|
|
||||||
|
ts.width.max = bounds.w;
|
||||||
|
ts.height.min = bounds.h;
|
||||||
|
ts.height.max = words * line_height + line_gap * (words-1);
|
||||||
|
ts.area = bounds.w * bounds.h;
|
||||||
|
|
||||||
|
return ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,6 +74,7 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te)
|
|||||||
after -= how_many;
|
after -= how_many;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// FIXME: this still doesn't work
|
||||||
if (mod.up) {
|
if (mod.up) {
|
||||||
// back up to previous line
|
// back up to previous line
|
||||||
if (te.cursor > 0) {
|
if (te.cursor > 0) {
|
||||||
@ -97,6 +98,10 @@ fn bool Ctx.text_edit(&ctx, TextEdit* te)
|
|||||||
after = te.chars - te.cursor;
|
after = te.chars - te.cursor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: mod.home
|
||||||
|
// TODO: mod.end
|
||||||
|
// TODO: selection with shift+arrows
|
||||||
}
|
}
|
||||||
|
|
||||||
return free == 0;
|
return free == 0;
|
||||||
|
@ -39,9 +39,9 @@ fn void? Ctx.text_id(&ctx, Id id, String text)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, Anchor text_alignment = TOP_LEFT, ...)
|
macro Ctx.text_box(&ctx, Size w, Size h, TextEdit* te, Anchor text_alignment = TOP_LEFT, bool reflow = true, ...)
|
||||||
=> ctx.text_box_id(@compute_id($vasplat), w, h, te, text_alignment);
|
=> ctx.text_box_id(@compute_id($vasplat), w, h, te, text_alignment, reflow);
|
||||||
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor text_alignment)
|
fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor text_alignment, bool reflow)
|
||||||
{
|
{
|
||||||
id = ctx.gen_id(id)!;
|
id = ctx.gen_id(id)!;
|
||||||
|
|
||||||
@ -76,16 +76,13 @@ fn ElemEvents? Ctx.text_box_id(&ctx, Id id, Size w, Size h, TextEdit* te, Anchor
|
|||||||
Rect bg_bounds = elem.bounds.pad(style.margin);
|
Rect bg_bounds = elem.bounds.pad(style.margin);
|
||||||
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
|
Rect text_bounds = elem.bounds.pad(elem.layout.content_offset);
|
||||||
ctx.push_rect(bg_bounds, parent.z_index, style)!;
|
ctx.push_rect(bg_bounds, parent.z_index, style)!;
|
||||||
Rect cur;
|
ctx.layout_string(elem.text.te.to_string(), text_bounds, text_alignment, parent.z_index, style.fg, reflow)!;
|
||||||
cur = ctx.layout_string(elem.text.te.to_string(), text_bounds, text_alignment, parent.z_index, style.fg, elem.text.te.cursor)!;
|
|
||||||
|
|
||||||
// draw the cursor if the element has focus
|
// draw the cursor if the element has focus
|
||||||
cur.w = 2;
|
|
||||||
cur.x -= 2;
|
|
||||||
if (elem.events.has_focus) {
|
if (elem.events.has_focus) {
|
||||||
ctx.push_scissor(text_bounds, parent.z_index)!;
|
Rect cur = ctx.get_cursor_position(elem.text.te.to_string(), text_bounds, text_alignment, elem.text.te.cursor, reflow)!;
|
||||||
|
cur.w = 2;
|
||||||
ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!;
|
ctx.push_rect(cur, parent.z_index, &&(Style){.bg = style.fg})!;
|
||||||
ctx.reset_scissor(parent.z_index)!;
|
|
||||||
}
|
}
|
||||||
return elem.events;
|
return elem.events;
|
||||||
}
|
}
|
@ -245,6 +245,7 @@ $endswitch
|
|||||||
|
|
||||||
// wait for the next event, timeout after 100ms
|
// wait for the next event, timeout after 100ms
|
||||||
int timeout = LIMIT_FPS ? (int)(100.0-sleep_clock.mark().to_ms()-0.5) : 0;
|
int timeout = LIMIT_FPS ? (int)(100.0-sleep_clock.mark().to_ms()-0.5) : 0;
|
||||||
|
if (ui.skip_frame) timeout = 0;
|
||||||
sdl::wait_event_timeout(&e, timeout);
|
sdl::wait_event_timeout(&e, timeout);
|
||||||
|
|
||||||
fps = 1.0 / fps_clock.mark().to_sec();
|
fps = 1.0 / fps_clock.mark().to_sec();
|
||||||
@ -459,7 +460,7 @@ fn void calculator(ugui::Ctx* ui, TextEdit* te)
|
|||||||
ui.slider_ver(ugui::@exact(20), ugui::@exact(100), &f)!!;
|
ui.slider_ver(ugui::@exact(20), ugui::@exact(100), &f)!!;
|
||||||
}!!;
|
}!!;
|
||||||
ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER) {
|
ui.@div(ugui::@grow(), ugui::@fit(), anchor: CENTER) {
|
||||||
ui.text_box(ugui::@grow(), ugui::@exact(100), te, RIGHT)!!;
|
ui.text_box(ugui::@grow(), ugui::@exact(100), te, TOP_LEFT)!!;
|
||||||
}!!;
|
}!!;
|
||||||
}!!; }!!;
|
}!!; }!!;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user