also includes - small layout fix for grow elements - ElemEvents now includes has_focus flag
450 lines
11 KiB
Plaintext
450 lines
11 KiB
Plaintext
module ugui;
|
|
|
|
import schrift;
|
|
import grapheme;
|
|
import std::collections::map;
|
|
import std::core::mem;
|
|
import std::core::mem::allocator;
|
|
import std::io;
|
|
import std::ascii;
|
|
|
|
|
|
// ---------------------------------------------------------------------------------- //
|
|
// CODEPOINT //
|
|
// ---------------------------------------------------------------------------------- //
|
|
|
|
// unicode code point, different type for a different hash
|
|
alias Codepoint = uint;
|
|
|
|
<*
|
|
@require off != null
|
|
@require str.ptr != null
|
|
*>
|
|
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();
|
|
|
|
// ---------------------------------------------------------------------------------- //
|
|
// FONT ATLAS //
|
|
// ---------------------------------------------------------------------------------- //
|
|
|
|
/* width and height of a glyph contain the kering advance
|
|
* (u,v)
|
|
* +-------------*---+ -
|
|
* | ^ | | ^
|
|
* | |oy | | |
|
|
* | v | | |
|
|
* | .ii. | | |
|
|
* | @@@@@@. | | |
|
|
* | V@Mio@@o | | |
|
|
* | :i. V@V | | h
|
|
* | :oM@@M | | |
|
|
* | :@@@MM@M | | |
|
|
* | @@o o@M | | |
|
|
* |<->:@@. M@M | | |
|
|
* |ox @@@o@@@@ | | |
|
|
* | :M@@V:@@.| | v
|
|
* +-------------*---+ -
|
|
* |<---- w ---->|
|
|
* |<------ adv ---->|
|
|
*/
|
|
struct Glyph {
|
|
Codepoint code;
|
|
ushort u, v;
|
|
ushort w, h;
|
|
short adv, ox, oy;
|
|
}
|
|
|
|
const uint FONT_CACHED = 255;
|
|
alias GlyphTable = map::HashMap{Codepoint, Glyph};
|
|
|
|
faultdef TTF_LOAD_FAILED, MISSING_GLYPH, BAD_GLYPH_METRICS, RENDER_ERROR;
|
|
|
|
struct Font {
|
|
schrift::Sft sft;
|
|
String path;
|
|
Id id; // font id, same as atlas id
|
|
GlyphTable table;
|
|
|
|
float size;
|
|
float ascender, descender, linegap; // Line Metrics
|
|
Atlas atlas;
|
|
bool should_update; // should send update_atlas command, resets at frame_end()
|
|
}
|
|
|
|
fn void? Font.load(&font, String name, ZString path, uint height, float scale)
|
|
{
|
|
font.table.init(allocator::mem, capacity: FONT_CACHED);
|
|
font.id = name.hash();
|
|
|
|
font.size = height*scale;
|
|
|
|
font.sft = {
|
|
.xScale = (double)font.size,
|
|
.yScale = (double)font.size,
|
|
.flags = schrift::SFT_DOWNWARD_Y,
|
|
};
|
|
|
|
font.sft.font = schrift::loadfile(path);
|
|
if (font.sft.font == null) {
|
|
font.table.free();
|
|
return TTF_LOAD_FAILED?;
|
|
}
|
|
|
|
schrift::SftLMetrics lmetrics;
|
|
schrift::lmetrics(&font.sft, &lmetrics);
|
|
font.ascender = (float)lmetrics.ascender;
|
|
font.descender = (float)lmetrics.descender;
|
|
font.linegap = (float)lmetrics.lineGap;
|
|
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
|
|
|
|
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
|
|
// like the letter 'A'
|
|
ushort size = (ushort)font.size*(ushort)($$sqrt((float)FONT_CACHED));
|
|
font.atlas.new(font.id, ATLAS_GRAYSCALE, size, size)!;
|
|
|
|
// preallocate the ASCII range
|
|
for (char c = ' '; c < '~'; c++) {
|
|
font.get_glyph((Codepoint)c)!;
|
|
}
|
|
}
|
|
|
|
fn Glyph*? Font.get_glyph(&font, Codepoint code)
|
|
{
|
|
Glyph*? gp;
|
|
gp = font.table.get_ref(code);
|
|
|
|
if (catch excuse = gp) {
|
|
if (excuse != NOT_FOUND) {
|
|
return excuse?;
|
|
}
|
|
} else {
|
|
return gp;
|
|
}
|
|
|
|
// missing glyph, render and place into an atlas
|
|
Glyph glyph;
|
|
|
|
schrift::SftGlyph gid;
|
|
schrift::SftGMetrics gmtx;
|
|
|
|
if (schrift::lookup(&font.sft, (SftUChar)code, &gid) < 0) {
|
|
return MISSING_GLYPH?;
|
|
}
|
|
|
|
if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) {
|
|
return BAD_GLYPH_METRICS?;
|
|
}
|
|
|
|
schrift::SftImage img = {
|
|
.width = gmtx.minWidth,
|
|
.height = gmtx.minHeight,
|
|
};
|
|
char[] pixels = mem::new_array(char, (usz)img.width * img.height);
|
|
img.pixels = pixels;
|
|
if (schrift::render(&font.sft, gid, img) < 0) {
|
|
return RENDER_ERROR?;
|
|
}
|
|
|
|
glyph.code = code;
|
|
glyph.w = (ushort)img.width;
|
|
glyph.h = (ushort)img.height;
|
|
glyph.ox = (short)gmtx.leftSideBearing;
|
|
glyph.oy = (short)gmtx.yOffset;
|
|
glyph.adv = (short)gmtx.advanceWidth;
|
|
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
|
|
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
|
|
|
|
Point uv = font.atlas.place(pixels, glyph.w, glyph.h, (ushort)img.width)!;
|
|
glyph.u = uv.x;
|
|
glyph.v = uv.y;
|
|
|
|
mem::free(pixels);
|
|
|
|
font.table.set(code, glyph);
|
|
|
|
font.should_update = true;
|
|
return font.table.get_ref(code);
|
|
}
|
|
|
|
fn void Font.free(&font)
|
|
{
|
|
font.atlas.free();
|
|
font.table.free();
|
|
schrift::freefont(font.sft.font);
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------------- //
|
|
// FONT LOAD AND QUERY //
|
|
// ---------------------------------------------------------------------------------- //
|
|
|
|
fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
|
|
{
|
|
return ctx.font.load(name, path, height, scale);
|
|
}
|
|
|
|
// TODO: check if the font is present in the context
|
|
fn Id Ctx.get_font_id(&ctx, String label)
|
|
{
|
|
return (Id)label.hash();
|
|
}
|
|
|
|
fn Atlas*? Ctx.get_font_atlas(&ctx, String name)
|
|
{
|
|
// TODO: use the font name, for now there is only one font
|
|
if (name.hash() != ctx.font.id) {
|
|
return WRONG_ID?;
|
|
}
|
|
|
|
return &ctx.font.atlas;
|
|
}
|
|
|
|
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
|
|
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
|
|
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 (text == "" || bounds.w <= 0 || bounds.h <= 0) return cursor_rect;
|
|
ctx.push_scissor(bounds, z_index)!;
|
|
|
|
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;
|
|
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
|
|
origin.x = bounds.x;
|
|
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);
|
|
}
|
|
|
|
cursor_rect.x = origin.x;
|
|
cursor_rect.y = origin.y;
|
|
|
|
// 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()})!;
|
|
|
|
if (line_start + off < cursor) {
|
|
cursor_rect.x = origin.x + gp.adv;
|
|
cursor_rect.y = origin.y;
|
|
cursor_rect.w = gp.adv;
|
|
}
|
|
|
|
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)!;
|
|
|
|
return cursor_rect;
|
|
}
|