better font atlas implementation
This commit is contained in:
parent
5a89e9ec7d
commit
6d8300f9d9
@ -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;
|
||||
}
|
||||
|
85
src/ugui_atlas.c3
Normal file
85
src/ugui_atlas.c3
Normal file
@ -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;
|
||||
}
|
@ -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)!;
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
Atlas atlas;
|
||||
bool should_update; // should send update_atlas command, resets at frame_end()
|
||||
}
|
||||
|
||||
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! 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);
|
||||
}
|
@ -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 = {
|
||||
|
@ -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)!;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user