diff --git a/TODO b/TODO index 3b2f9cb..837d2a6 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,21 @@ # TODOs, semi-random sorting -[ ] Implement glyph draw command -[ ] Implement div.view and scrollbars +[x] Implement glyph draw command +[x] Implement div.view and scrollbars [ ] Port font system from C to C3 (rewrite1) [ ] Update ARCHITECTURE.md [ ] Write a README.md [ ] Use an arena allocator for cache + +## Commands +[ ] rect commads should have: + * border width + * border radius +[x] add a command to update an atlas + +## Atlases +[ ] Add an interface to create, destroy, update and get atlases based on their ids +[ ] Implement multiple font atlases + +## Fonts +[ ] Fix the missing alpha channel +[ ] Fix the allignment diff --git a/lib/libschrift.c3l/libschrift.c3 b/lib/libschrift.c3l/libschrift.c3 index 1b0f24b..9cf8f31 100644 --- a/lib/libschrift.c3l/libschrift.c3 +++ b/lib/libschrift.c3l/libschrift.c3 @@ -4,6 +4,8 @@ def SftFont = void*; def SftUChar = uint; def SftGlyph = uint; +const int SFT_DOWNWARD_Y = 0x01; + struct Sft { SftFont font; @@ -45,12 +47,12 @@ struct SftImage extern fn char* sft_version() @extern("sft_version"); -extern fn SftFont sft_loadmem(void* mem, usz size) @extern("sft_loadmem"); -extern fn SftFont sft_loadfile(char* filename) @extern("sft_loadfile"); -extern fn void sft_freefont(SftFont font) @extern("sft_freefont"); +extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem"); +extern fn SftFont loadfile(char* filename) @extern("sft_loadfile"); +extern fn void freefont(SftFont font) @extern("sft_freefont"); -extern fn int sft_lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics"); -extern fn int sft_lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup"); -extern fn int sft_gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics"); -extern fn int sft_kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning"); -extern fn int sft_render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render"); +extern fn int lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics"); +extern fn int lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup"); +extern fn int gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics"); +extern fn int kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning"); +extern fn int render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render"); diff --git a/src/main.c3 b/src/main.c3 index 864b38a..dc833e0 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -27,6 +27,7 @@ fn int main(String[] args) { ugui::Ctx ui; ui.init()!!; + ui.font.load("/usr/share/fonts/TTF/FreeSans.ttf", 16)!!; short width = 800; short height = 450; @@ -92,6 +93,8 @@ fn int main(String[] args) ugui::Elem* e = ui.get_elem_by_label("slider")!!; io::printfn("slider: %f", e.slider.value); } + + ui.text_unbounded("text1", "Ciao Mamma")!!; |}; ui.div_end()!!; @@ -125,6 +128,8 @@ fn int main(String[] args) // ClearBackground(BLACK); rl::Color c; + static rl::Image font_atlas; + static rl::Texture2D font_texture; for (Cmd* cmd; (cmd = ui.cmd_queue.dequeue() ?? null) != null;) { switch (cmd.type) { case ugui::CmdType.CMD_RECT: @@ -141,8 +146,34 @@ fn int main(String[] args) cmd.rect.rect.h, c ); + case ugui::CmdType.CMD_UPDATE_ATLAS: + rl::unload_image(font_atlas); + font_atlas.data = cmd.update_atlas.raw_buffer; + font_atlas.width = cmd.update_atlas.width; + font_atlas.height = cmd.update_atlas.height; + font_atlas.mipmaps = 1; + //font_atlas.format = rl::PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE; + font_atlas.format = 1; + if (rl::is_texture_ready(font_texture)) { + rl::unload_texture(font_texture); + } + font_texture = rl::load_texture_from_image(font_atlas); + //rl::draw_texture(font_texture, 0, 0, rl::WHITE); + case ugui::CmdType.CMD_SPRITE: + rl::Rectangle source = { + .x = cmd.sprite.texture_rect.x, + .y = cmd.sprite.texture_rect.y, + .width = cmd.sprite.texture_rect.w, + .height = cmd.sprite.texture_rect.h, + }; + rl::Vector2 position = { + .x = cmd.sprite.rect.x, + .y = (float)cmd.sprite.rect.y - cmd.sprite.rect.h, + }; + + rl::draw_texture_rec(font_texture, source, position, rl::WHITE); default: - io::printfn("Unknown cmd type: %d", cmd.type); + io::printfn("Unknown cmd type: %s", cmd.type); } } draw_times.push(clock.mark()); @@ -154,6 +185,7 @@ fn int main(String[] args) rl::close_window(); + ui.font.free(); ui.free(); return 0; } diff --git a/src/ugui_data.c3 b/src/ugui_data.c3 index c7e0895..33f0395 100644 --- a/src/ugui_data.c3 +++ b/src/ugui_data.c3 @@ -28,6 +28,7 @@ enum ElemType { ETYPE_DIV, ETYPE_BUTTON, ETYPE_SLIDER, + ETYPE_TEXT, } bitstruct ElemFlags : uint { @@ -75,6 +76,10 @@ struct Slider { Rect handle; } +struct Text { + char* str; +} + // element structure struct Elem { Id id; @@ -85,6 +90,7 @@ struct Elem { union { Div div; Slider slider; + Text text; } } @@ -126,6 +132,8 @@ const uint ROOT_ID = 1; // command type enum CmdType { CMD_RECT, + CMD_UPDATE_ATLAS, + CMD_SPRITE, } // command to draw a rect @@ -134,11 +142,27 @@ struct CmdRect { Color color; } +// FIXME: For now only support black and white atlas, so PIXELFORMAT_UNCOMPRESSED_GRAYSCALE +struct CmdUpdateAtlas { + char* raw_buffer; + short width, height; +} + +// TODO: +// 1. Add atlases as a data type +// 2. Each atlas has an id +struct CmdSprite { + Rect rect; + Rect texture_rect; +} + // command structure struct Cmd { CmdType type; union { CmdRect rect; + CmdUpdateAtlas update_atlas; + CmdSprite sprite; } } diff --git a/src/ugui_font.c3 b/src/ugui_font.c3 index 10ab764..350f7ca 100644 --- a/src/ugui_font.c3 +++ b/src/ugui_font.c3 @@ -1,18 +1,217 @@ module ugui; import schrift; +import std::collections::map; +import std::core::mem; -struct Font @private { +// unicode code point, different type for a different hash +def Codepoint = uint; + + +/* width and height of a glyph contain the kering advance + * (u,v) + * +-------------*---+ - + * | ^ | | ^ + * | |oy | | | + * | v | | | + * | .ii. | | | + * | @@@@@@. |<->| | + * | V@Mio@@o |adv| |h + * | :i. V@V | | | + * | :oM@@M | | | + * | :@@@MM@M | | | + * | @@o o@M | | | + * |<->:@@. M@M | | | + * |ox @@@o@@@@ | | | + * | :M@@V:@@.| | v + * +-------------*---+ - + * |<------------->| + * w + */ +struct Glyph { + Codepoint code; + ushort u, v; + ushort w, h; + short adv, ox, oy; + short idx; // atlas index +} + +const uint FONT_CACHED = 512; +def GlyphTable = map::HashMap() @private; + +fault UgFontError { + TTF_LOAD_FAILED, + MISSING_GLYPH, + BAD_GLYPH_METRICS, + RENDER_ERROR, +} + +fault UgAtlasError { + CANNOT_PLACE, +} + +// black and white atlas +struct AtlasBW { + ushort width, height; + char[] buffer; + + Point row; + ushort row_h; +} + +struct Font { schrift::Sft sft; String path; + GlyphTable table; + + float size; + float ascender, descender, linegap; // Line Metrics + AtlasBW[] atlas; +} + +fn void! AtlasBW.new(&atlas, ushort width, ushort height) +{ + atlas.width = width; + atlas.height = height; + atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height); +} + +fn void AtlasBW.free(&atlas) +{ + free(atlas.buffer); +} + +// place a rect inside the atlas +// uses a row first algorithm +// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/ +fn Point! AtlasBW.place(&atlas, char[] pixels, ushort w, ushort h) +{ + Point p; + + if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) { + p = atlas.row; + } else { + atlas.row.x = 0; + atlas.row.y = atlas.row.y + atlas.row_h; + atlas.row_h = 0; + if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) { + p = atlas.row; + } else { + return UgAtlasError.CANNOT_PLACE?; + } + } + + for (usz y = 0; y < h; y++) { + for (usz x = 0; x < w; x++) { + atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x)] = pixels[y*w + x]; + } + } + + atlas.row.x += w; + if (h > atlas.row_h) { + atlas.row_h = h; + } + + return p; } -fn void! Ctx.font_load(&ctx, String path) +fn void! Font.load(&font, String path, uint height, float scale = 1) { + font.table.new_init(capacity: FONT_CACHED); + + font.size = height*scale; + + font.sft = schrift::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) { + return UgFontError.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; + + // TODO: allocate buffer based on FONT_CACHED and the size of a sample letter + // like the letter 'A' + font.atlas = mem::new_array(AtlasBW, 1); + ushort size = (ushort)font.size*512; + font.atlas[0].new(size, size)!; + + // preallocate the ASCII range +// for (char c = ' '; c < '~'; c++) { +// font.get_glyph((Codepoint)c)!; +// } } -fn void Ctx.font_free(&ctx) +fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null) { + Glyph*! gp; + gp = font.table.get_ref(code); + + if (catch excuse = gp) { + if (excuse != SearchResult.MISSING) { + return excuse?; + } + } else { + if (is_new) { *is_new = false; } + return gp; + } + + // missing glyph, render and place into an atlas + Glyph glyph; + schrift::SftGlyph gid; + schrift::SftGMetrics gmtx; + + if (schrift::lookup(&font.sft, code, &gid) < 0) { + return UgFontError.MISSING_GLYPH?; + } + + if (schrift::gmetrics(&font.sft, gid, &gmtx) < 0) { + return UgFontError.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 UgFontError.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; + + Point uv = font.atlas[0].place(pixels, glyph.w, glyph.h)!; + glyph.idx = 0; + glyph.u = uv.x; + glyph.v = uv.y; + + mem::free(pixels); + + font.table.set(code, glyph); + + if (is_new) { *is_new = true; } + return font.table.get_ref(code); +} + +fn void Font.free(&font) +{ + foreach (atlas: font.atlas) { + atlas.free(); + } + schrift::freefont(font.sft.font); } diff --git a/src/ugui_text.c3 b/src/ugui_text.c3 new file mode 100644 index 0000000..1e6cb29 --- /dev/null +++ b/src/ugui_text.c3 @@ -0,0 +1,81 @@ +module ugui; + +import std::io; + +fn void! Ctx.text_unbounded(&ctx, String label, String text) +{ + Id id = label.hash(); + + Elem *parent = ctx.get_parent()!; + Elem *c_elem = ctx.get_elem(id)!; + // add it to the tree + ctx.tree.add(id, ctx.active_div)!; + + // 1. Fill the element fields + // this resets the flags + c_elem.type = ETYPE_TEXT; + + bool update_atlas; + // if the element is new or the parent was updated then redo layout + if (c_elem.flags.is_new || parent.flags.updated) { + Rect text_size; + Glyph* gp; + + // FIXME: newlines are not counted + foreach (c: text) { + Codepoint cp = (Codepoint)c; + bool n; + gp = ctx.font.get_glyph(cp, &n)!; + text_size.w += gp.w + gp.ox + gp.adv; + text_size.h += gp.h + gp.oy; + if (n) { update_atlas = true; } + } + + // 2. Layout + c_elem.bounds = ctx.position_element(parent, text_size, true); + + // 3. Fill the button specific fields + c_elem.text.str = text; + } + + if (update_atlas) { + // FIXME: atlas here is hardcoded, look at the todo in ugui_data + Cmd up = { + .type = CMD_UPDATE_ATLAS, + .update_atlas = { + .raw_buffer = ctx.font.atlas[0].buffer, + .width = ctx.font.atlas[0].width, + .height = ctx.font.atlas[0].height, + }, + }; + ctx.cmd_queue.enqueue(&up)!; + } + + Point orig = { + .x = c_elem.bounds.x, + .y = c_elem.bounds.y, + }; + foreach (c: text) { + Glyph* gp; + Codepoint cp = (Codepoint)c; + gp = ctx.font.get_glyph(cp)!; + + Cmd cmd = { + .type = CMD_SPRITE, + .sprite.rect = { + .x = orig.x + gp.ox, + .y = orig.y + gp.oy, + .w = gp.w, + .h = gp.h, + }, + .sprite.texture_rect = { + .x = gp.u, + .y = gp.v, + .w = gp.w, + .h = gp.h, + }, + }; + orig.x += gp.w + gp.ox; + ctx.cmd_queue.enqueue(&cmd)!; + } +}