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);