diff --git a/src/main.c3 b/src/main.c3 index d9bef14..2a9b439 100644 --- a/src/main.c3 +++ b/src/main.c3 @@ -27,7 +27,7 @@ fn int main(String[] args) { ugui::Ctx ui; ui.init()!!; - ui.font.load("/usr/share/fonts/TTF/HackNerdFontMono-Regular.ttf", 16)!!; + ui.load_font("font1", "/usr/share/fonts/TTF/HackNerdFontMono-Regular.ttf", 16)!!; short width = 800; short height = 450; @@ -195,7 +195,6 @@ fn int main(String[] args) rl::close_window(); - ui.font.free(); ui.free(); return 0; } diff --git a/src/ugui_atlas.c3 b/src/ugui_atlas.c3 new file mode 100644 index 0000000..0f04c08 --- /dev/null +++ b/src/ugui_atlas.c3 @@ -0,0 +1,85 @@ +module ugui; + +import std::io; + +fault UgAtlasError { + CANNOT_PLACE, + INVALID_TYPE, +} + +enum AtlasType { + ATLAS_GRAYSCALE, +} + +// black and white atlas +struct Atlas { + AtlasType type; + Id id; + + ushort width, height; + char[] buffer; + + Point row; + ushort row_h; +} + +// bytes per pixel +macro usz AtlasType.bpp(type) +{ + switch (type) { + case ATLAS_GRAYSCALE: return 1; + } +} + +fn void! Atlas.new(&atlas, Id id, AtlasType type, ushort width, ushort height) +{ + atlas.id = id; + atlas.type = type; + atlas.width = width; + atlas.height = height; + + atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height*type.bpp()); +} + +fn void Atlas.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! Atlas.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++) { + char[] buf = atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x) ..]; + char[] pix = pixels[y*w + x ..]; + usz bpp = atlas.type.bpp(); + + buf[0..bpp-1] = pix[0..bpp-1]; + } + } + + atlas.row.x += w; + if (h > atlas.row_h) { + atlas.row_h = h; + } + + return p; +} diff --git a/src/ugui_cmd.c3 b/src/ugui_cmd.c3 index 13dfb3e..e021ad1 100644 --- a/src/ugui_cmd.c3 +++ b/src/ugui_cmd.c3 @@ -2,6 +2,45 @@ module ugui; import std::ascii; +// command type +enum CmdType { + CMD_RECT, + CMD_UPDATE_ATLAS, + CMD_SPRITE, +} + +// command to draw a rect +struct CmdRect { + Rect rect; + ushort radius; + Color color; +} + +// FIXME: For now only support black and white atlas, so PIXELFORMAT_UNCOMPRESSED_GRAYSCALE +struct CmdUpdateAtlas { + Id id; + char* raw_buffer; + short width, height, bpp; +} + +// 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; + } +} + // FIXME: is this really the best solution? // "rect" is the bounding box of the element, which includes the border and the padding (so not just the content) fn void! Ctx.push_rect(&ctx, Rect rect, Color color, bool do_border = false, bool do_padding = false, bool do_radius = false) @@ -97,3 +136,18 @@ fn void! Ctx.push_string(&ctx, Rect bounds, String text) } } } + +fn void! Ctx.push_update_atlas(&ctx, Atlas* atlas) +{ + Cmd up = { + .type = CMD_UPDATE_ATLAS, + .update_atlas = { + .id = atlas.id, + .raw_buffer = atlas.buffer, + .width = atlas.width, + .height = atlas.height, + .bpp = (ushort)atlas.type.bpp(), + }, + }; + ctx.cmd_queue.enqueue(&up)!; +} \ No newline at end of file diff --git a/src/ugui_data.c3 b/src/ugui_data.c3 index 03b80da..1ec0539 100644 --- a/src/ugui_data.c3 +++ b/src/ugui_data.c3 @@ -128,45 +128,6 @@ const uint MAX_ELEMS = 128; const uint MAX_CMDS = 256; const uint ROOT_ID = 1; -// command type -enum CmdType { - CMD_RECT, - CMD_UPDATE_ATLAS, - CMD_SPRITE, -} - -// command to draw a rect -// TODO: implement radius -struct CmdRect { - Rect rect; - ushort radius; - 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; - } -} - enum Layout { ROW, COLUMN, diff --git a/src/ugui_font.c3 b/src/ugui_font.c3 index f188fbb..68d1be8 100644 --- a/src/ugui_font.c3 +++ b/src/ugui_font.c3 @@ -34,7 +34,6 @@ struct Glyph { ushort u, v; ushort w, h; short adv, ox, oy; - short idx; // atlas index } const uint FONT_CACHED = 512; @@ -47,79 +46,23 @@ fault UgFontError { 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; + Id id; // font id, same as atlas id 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); + Atlas atlas; + bool should_update; // should send update_atlas command, resets at frame_end() } -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! Font.load(&font, String path, uint height, float scale = 1) +fn void! Font.load(&font, String name, String path, uint height, float scale) { font.table.new_init(capacity: FONT_CACHED); - + font.id = name.hash(); + font.size = height*scale; font.sft = schrift::Sft{ @@ -142,27 +85,25 @@ fn void! Font.load(&font, String path, uint height, float scale = 1) // 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*256; - font.atlas[0].new(size, size)!; - + 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, bool* is_new = null) +fn Glyph*! Font.get_glyph(&font, Codepoint code) { 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; } @@ -171,7 +112,7 @@ fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null) schrift::SftGlyph gid; schrift::SftGMetrics gmtx; - + if (schrift::lookup(&font.sft, code, &gid) < 0) { return UgFontError.MISSING_GLYPH?; } @@ -199,8 +140,7 @@ fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null) //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[0].place(pixels, glyph.w, glyph.h)!; - glyph.idx = 0; + Point uv = font.atlas.place(pixels, glyph.w, glyph.h)!; glyph.u = uv.x; glyph.v = uv.y; @@ -208,14 +148,17 @@ fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null) font.table.set(code, glyph); - if (is_new) { *is_new = true; } + font.should_update = true; return font.table.get_ref(code); } fn void Font.free(&font) { - foreach (atlas: font.atlas) { - atlas.free(); - } + font.atlas.free(); schrift::freefont(font.sft.font); } + +fn void! Ctx.load_font(&ctx, String name, String path, uint height, float scale = 1.0) +{ + return ctx.font.load(name, path, height, scale); +} \ No newline at end of file diff --git a/src/ugui_impl.c3 b/src/ugui_impl.c3 index e49c8d4..541dfb7 100644 --- a/src/ugui_impl.c3 +++ b/src/ugui_impl.c3 @@ -67,6 +67,7 @@ fn void Ctx.free(&ctx) (void)ctx.tree.free(); (void)ctx.cache.free(); (void)ctx.cmd_queue.free(); + (void)ctx.font.free(); } fn void! Ctx.frame_begin(&ctx) @@ -127,8 +128,14 @@ fn void! Ctx.frame_end(&ctx) ctx.input.events = (InputEvents)0; ctx.input.events.force_update = f; - // draw mouse position + // send atlas updates + if (ctx.font.should_update) { + ctx.push_update_atlas(&ctx.font.atlas)!; + ctx.font.should_update = false; + } + $if 1: + // draw mouse position Cmd cmd = { .type = CMD_RECT, .rect.rect = { diff --git a/src/ugui_text.c3 b/src/ugui_text.c3 index e39c82a..4b25ad8 100644 --- a/src/ugui_text.c3 +++ b/src/ugui_text.c3 @@ -18,7 +18,7 @@ fn Codepoint str_to_codepoint(char[] str, usz* off) return cp; } -fn Rect! Ctx.get_text_bounds(&ctx, String text, bool* update_atlas) +fn Rect! Ctx.get_text_bounds(&ctx, String text) { Rect text_bounds; short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; @@ -34,9 +34,8 @@ fn Rect! Ctx.get_text_bounds(&ctx, String text, bool* update_atlas) off += x; bool n; if (!ascii::is_cntrl((char)cp)) { - gp = ctx.font.get_glyph(cp, &n)!; + gp = ctx.font.get_glyph(cp)!; line_len += gp.adv; - if (n) { *update_atlas = true; } } else if (cp == '\n'){ text_bounds.h += line_height + line_gap; line_len = 0; @@ -67,10 +66,9 @@ fn void! Ctx.text_unbounded(&ctx, String label, String text) short baseline = (short)ctx.font.ascender; short line_height = (short)ctx.font.ascender - (short)ctx.font.descender; short line_gap = (short)ctx.font.linegap; - 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 = ctx.get_text_bounds(text, &update_atlas)!; + Rect text_size = ctx.get_text_bounds(text)!; // 2. Layout c_elem.bounds = ctx.position_element(parent, text_size, true); @@ -79,25 +77,5 @@ fn void! Ctx.text_unbounded(&ctx, String label, String text) 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)!; - } - - Cmd bounds = { - .type = CMD_RECT, - .rect.rect = c_elem.bounds, - .rect.color = uint_to_rgba(0x000000ff), - }; - ctx.cmd_queue.enqueue(&bounds)!; - ctx.push_string(c_elem.bounds, text)!; }