diff --git a/TODO b/TODO index aa82908..3ae26ea 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,6 @@ # TODOs, semi-random sorting + +[ ] Check every instance of foreach to see if I am using by-copy or by-reference correctly [x] Implement glyph draw command [x] Implement div.view and scrollbars [ ] Port font system from C to C3 (rewrite1) @@ -7,26 +9,33 @@ [ ] Use an arena allocator for cache [ ] Do not redraw if there was no update (no layout and no draw) [ ] Better handling of the active and focused widgets, try - to maintain focus until mouse release (fix scroll bars) +to maintain focus until mouse release (fix scroll bars) +[ ] Write a description for each file and the structs, interfaces provided ## Commands + [x] rect commads should have: - * border width - * border radius +_ 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 +[ ] Create and use ShortIds for atlases ## Fonts + [ ] Fix the missing alpha channel [x] Fix the alignment ## Raylib + [ ] Implement type (Rect, Color, Point) conversion functions between rl:: and ugui:: [x] Implement pixel radius rounding for border radius ## Widgets + [ ] Dynamic text box to implement an fps counter [ ] Button with label diff --git a/src/ugui_atlas.c3 b/src/ugui_atlas.c3 new file mode 100644 index 0000000..2c0c450 --- /dev/null +++ b/src/ugui_atlas.c3 @@ -0,0 +1,183 @@ +module ugui; + +import std::core::mem; +import std::math::random; + +random::SimpleRandom atlas_seed; + +fault UgAtlasError { + CANNOT_PLACE, + INVALID_FORMAT, + MISSING_ATLAS, +} + +enum AtlasFormat { + ATLAS_GRAYSCALE, + ATLAS_ALPHA, +} + +// black and white atlas +struct Atlas { + AtlasFormat format; + Id id; + ushort width, height; + char[] buffer; + + Point row; + ushort row_h; +} + +// FIXME: allocate atlases dynamically +fn void! Ctx.init_atlases(&ctx) +{ + atlas_seed.set_seed("ciao mamma"); + ctx.atlas = mem::new_array(Atlas, MAX_ATLAS); +} + +fn void Ctx.free_atlases(&ctx) +{ + foreach (a: ctx.atlas) { + a.free(); + } + mem::free(ctx.atlas); +} + +// FIXME: this could be slow +fn Atlas*! Ctx.get_atlas(&ctx, Id id) +{ + foreach (i, a: ctx.atlas) { + if (a.id == id) { + return &ctx.atlas[i]; + } + } + return UgAtlasError.MISSING_ATLAS?; +} + +// TODO: use the same allocator as all the other parts of the code +fn Atlas*! Ctx.request_new_atlas(&ctx, AtlasFormat format, Id *rid, ushort width, ushort height) +{ + usz idx; + bool free = false; + for (; idx < ctx.atlas.len; idx++) { + if (ctx.atlas[idx].id == 0) { + free = true; + break; + } + } + + if (free) { + *rid = ctx.unique_atlas_id(); + ctx.atlas[idx].new(format, *rid, width, height)!; + return &ctx.atlas[idx]; + } else { + // TODO: reallocate the atlas vector + return UgAtlasError.MISSING_ATLAS?; + } +} + +// FIXME: WTF +fn Id Ctx.unique_atlas_id(&ctx) { + Id id = atlas_seed.next_long(); + bool ok = true; + foreach (a: ctx.atlas) { + if (a.id == id) { + ok = false; + } + } + return ok ? id : ctx.unique_atlas_id(); +} + +fn void Ctx.discard_atlas(&ctx, Id id) +{ + foreach (&a: ctx.atlas) { + if (a.id == id) { + a.free(); + *a = Atlas{}; + } + } +} + +// Try to place a sprite into an atlas of the correct type and return it's id +// new_width and new_height are used in case a new atlas has to be requested, in that +// case they are used as width and height for the new atlas +// FIXME: maybe there has to be a default value +fn Id! Ctx.place_sprite(&ctx, Point* uv, AtlasFormat format, char[] pixels, ushort w, ushort h, ushort new_width, ushort new_height) +{ + // try to place the sprite into an existing atlas + foreach (&a: ctx.atlas) { + if (a.id != 0 && a.format == format) { + Point! ouv = a.place(pixels, w, h); + if (catch ouv) { + continue; + } + *uv = ouv; + return a.id; + } + } + + // fail... try to request a new atlas and place it there + Id id; + Atlas* atlas = ctx.request_new_atlas(format, &id, new_width, new_height)!; + *uv = atlas.place(pixels, w, h)!; + return id; +} + +fn void! Atlas.new(&atlas, AtlasFormat format, Id id, ushort width, ushort height) +{ + atlas.format = format; + atlas.id = id; + atlas.width = width; + atlas.height = height; + + usz bpp = 1; + switch (format) { + case ATLAS_GRAYSCALE: nextcase; + case ATLAS_ALPHA: + bpp = 1; + default: + return UgAtlasError.INVALID_FORMAT?; + } + usz size = ((usz)atlas.width*atlas.height) * bpp; + atlas.buffer = mem::new_array(char, size); +} + +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/ +// FIXME: untested with non-1bpp buffer depths +fn Point! Atlas.place(&atlas, char[] pixels, ushort w, ushort h) +{ + Point p; + + // TODO: simplify this and use rect_collision() and such + 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; +} diff --git a/src/ugui_data.c3 b/src/ugui_data.c3 index 03b80da..8893ea5 100644 --- a/src/ugui_data.c3 +++ b/src/ugui_data.c3 @@ -127,6 +127,7 @@ const uint STACK_STEP = 10; const uint MAX_ELEMS = 128; const uint MAX_CMDS = 256; const uint ROOT_ID = 1; +const uint MAX_ATLAS = 2; // command type enum CmdType { @@ -193,6 +194,8 @@ struct Ctx { // total size in pixels of the context ushort width, height; Style style; + + Atlas[] atlas; Font font; bool has_focus; diff --git a/src/ugui_font.c3 b/src/ugui_font.c3 index f188fbb..9ba8f62 100644 --- a/src/ugui_font.c3 +++ b/src/ugui_font.c3 @@ -34,7 +34,7 @@ struct Glyph { ushort u, v; ushort w, h; short adv, ox, oy; - short idx; // atlas index + Id atlas_id; } const uint FONT_CACHED = 512; @@ -47,19 +47,6 @@ 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; @@ -67,59 +54,14 @@ struct Font { 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! Font.load(&font, String path, uint height, float scale = 1) +fn void! Ctx.font_load(&cxt, String path, uint height, float scale = 1) { + Font* font = ctx.font; font.table.new_init(capacity: FONT_CACHED); - + font.size = height*scale; font.sft = schrift::Sft{ @@ -139,24 +81,14 @@ fn void! Font.load(&font, String path, uint height, float scale = 1) 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' - font.atlas = mem::new_array(AtlasBW, 1); - ushort size = (ushort)font.size*256; - font.atlas[0].new(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*! Ctx.get_glyph(&ctx, Codepoint code, bool* is_new = null) { + Font* font = &ctx.font; Glyph*! gp; gp = font.table.get_ref(code); - + if (catch excuse = gp) { if (excuse != SearchResult.MISSING) { return excuse?; @@ -171,7 +103,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 +131,12 @@ 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; + // TODO: allocate buffer based on FONT_CACHED and the size of a sample letter + // like the letter 'A' + ushort size = (ushort)font.size*256; + Point uv; + Id id = ctx.place_sprite(&uv, ATLAS_ALPHA, pixels, glyph.w, glyph.h, size, size)!; + glyph.atlas_id = id; glyph.u = uv.x; glyph.v = uv.y; @@ -212,10 +148,8 @@ fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null) return font.table.get_ref(code); } +// FIXME: maybe some atlases should be exclusive to fonts 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 index e39c82a..290a865 100644 --- a/src/ugui_text.c3 +++ b/src/ugui_text.c3 @@ -34,7 +34,7 @@ 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.get_glyph(cp, &n)!; line_len += gp.adv; if (n) { *update_atlas = true; } } else if (cp == '\n'){