ugui/lib/ugui.c3l/src/ugui_font.c3

315 lines
6.8 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;
// unicode code point, different type for a different hash
alias Codepoint = uint;
//macro uint Codepoint.hash(self) => ((uint)self).hash();
/* 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::heap(), 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);
}
fn void? Ctx.load_font(&ctx, String name, ZString path, uint height, float scale = 1.0)
{
return ctx.font.load(name, path, height, scale);
}
<*
@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;
}
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)
{
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
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);