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;
|
ugui::Ctx ui;
|
||||||
ui.init()!!;
|
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 width = 800;
|
||||||
short height = 450;
|
short height = 450;
|
||||||
@ -195,7 +195,6 @@ fn int main(String[] args)
|
|||||||
|
|
||||||
rl::close_window();
|
rl::close_window();
|
||||||
|
|
||||||
ui.font.free();
|
|
||||||
ui.free();
|
ui.free();
|
||||||
return 0;
|
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;
|
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?
|
// 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)
|
// "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)
|
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 MAX_CMDS = 256;
|
||||||
const uint ROOT_ID = 1;
|
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 {
|
enum Layout {
|
||||||
ROW,
|
ROW,
|
||||||
COLUMN,
|
COLUMN,
|
||||||
|
@ -34,7 +34,6 @@ struct Glyph {
|
|||||||
ushort u, v;
|
ushort u, v;
|
||||||
ushort w, h;
|
ushort w, h;
|
||||||
short adv, ox, oy;
|
short adv, ox, oy;
|
||||||
short idx; // atlas index
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint FONT_CACHED = 512;
|
const uint FONT_CACHED = 512;
|
||||||
@ -47,78 +46,22 @@ fault UgFontError {
|
|||||||
RENDER_ERROR,
|
RENDER_ERROR,
|
||||||
}
|
}
|
||||||
|
|
||||||
fault UgAtlasError {
|
|
||||||
CANNOT_PLACE,
|
|
||||||
}
|
|
||||||
|
|
||||||
// black and white atlas
|
|
||||||
struct AtlasBW {
|
|
||||||
ushort width, height;
|
|
||||||
char[] buffer;
|
|
||||||
|
|
||||||
Point row;
|
|
||||||
ushort row_h;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Font {
|
struct Font {
|
||||||
schrift::Sft sft;
|
schrift::Sft sft;
|
||||||
String path;
|
String path;
|
||||||
|
Id id; // font id, same as atlas id
|
||||||
GlyphTable table;
|
GlyphTable table;
|
||||||
|
|
||||||
float size;
|
float size;
|
||||||
float ascender, descender, linegap; // Line Metrics
|
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)
|
fn void! Font.load(&font, String name, String path, uint height, float scale)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
font.table.new_init(capacity: FONT_CACHED);
|
font.table.new_init(capacity: FONT_CACHED);
|
||||||
|
font.id = name.hash();
|
||||||
|
|
||||||
font.size = height*scale;
|
font.size = height*scale;
|
||||||
|
|
||||||
@ -142,9 +85,8 @@ 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
|
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
|
||||||
// like the letter 'A'
|
// like the letter 'A'
|
||||||
font.atlas = mem::new_array(AtlasBW, 1);
|
|
||||||
ushort size = (ushort)font.size*256;
|
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
|
// preallocate the ASCII range
|
||||||
// for (char c = ' '; c < '~'; c++) {
|
// for (char c = ' '; c < '~'; c++) {
|
||||||
@ -152,7 +94,7 @@ fn void! Font.load(&font, String path, uint height, float scale = 1)
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null)
|
fn Glyph*! Font.get_glyph(&font, Codepoint code)
|
||||||
{
|
{
|
||||||
Glyph*! gp;
|
Glyph*! gp;
|
||||||
gp = font.table.get_ref(code);
|
gp = font.table.get_ref(code);
|
||||||
@ -162,7 +104,6 @@ fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null)
|
|||||||
return excuse?;
|
return excuse?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (is_new) { *is_new = false; }
|
|
||||||
return gp;
|
return gp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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",
|
//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);
|
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
|
||||||
|
|
||||||
Point uv = font.atlas[0].place(pixels, glyph.w, glyph.h)!;
|
Point uv = font.atlas.place(pixels, glyph.w, glyph.h)!;
|
||||||
glyph.idx = 0;
|
|
||||||
glyph.u = uv.x;
|
glyph.u = uv.x;
|
||||||
glyph.v = uv.y;
|
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);
|
font.table.set(code, glyph);
|
||||||
|
|
||||||
if (is_new) { *is_new = true; }
|
font.should_update = true;
|
||||||
return font.table.get_ref(code);
|
return font.table.get_ref(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn void Font.free(&font)
|
fn void Font.free(&font)
|
||||||
{
|
{
|
||||||
foreach (atlas: font.atlas) {
|
font.atlas.free();
|
||||||
atlas.free();
|
|
||||||
}
|
|
||||||
schrift::freefont(font.sft.font);
|
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.tree.free();
|
||||||
(void)ctx.cache.free();
|
(void)ctx.cache.free();
|
||||||
(void)ctx.cmd_queue.free();
|
(void)ctx.cmd_queue.free();
|
||||||
|
(void)ctx.font.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn void! Ctx.frame_begin(&ctx)
|
fn void! Ctx.frame_begin(&ctx)
|
||||||
@ -127,8 +128,14 @@ fn void! Ctx.frame_end(&ctx)
|
|||||||
ctx.input.events = (InputEvents)0;
|
ctx.input.events = (InputEvents)0;
|
||||||
ctx.input.events.force_update = f;
|
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:
|
$if 1:
|
||||||
|
// draw mouse position
|
||||||
Cmd cmd = {
|
Cmd cmd = {
|
||||||
.type = CMD_RECT,
|
.type = CMD_RECT,
|
||||||
.rect.rect = {
|
.rect.rect = {
|
||||||
|
@ -18,7 +18,7 @@ fn Codepoint str_to_codepoint(char[] str, usz* off)
|
|||||||
return cp;
|
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;
|
Rect text_bounds;
|
||||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
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;
|
off += x;
|
||||||
bool n;
|
bool n;
|
||||||
if (!ascii::is_cntrl((char)cp)) {
|
if (!ascii::is_cntrl((char)cp)) {
|
||||||
gp = ctx.font.get_glyph(cp, &n)!;
|
gp = ctx.font.get_glyph(cp)!;
|
||||||
line_len += gp.adv;
|
line_len += gp.adv;
|
||||||
if (n) { *update_atlas = true; }
|
|
||||||
} else if (cp == '\n'){
|
} else if (cp == '\n'){
|
||||||
text_bounds.h += line_height + line_gap;
|
text_bounds.h += line_height + line_gap;
|
||||||
line_len = 0;
|
line_len = 0;
|
||||||
@ -67,10 +66,9 @@ fn void! Ctx.text_unbounded(&ctx, String label, String text)
|
|||||||
short baseline = (short)ctx.font.ascender;
|
short baseline = (short)ctx.font.ascender;
|
||||||
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
short line_height = (short)ctx.font.ascender - (short)ctx.font.descender;
|
||||||
short line_gap = (short)ctx.font.linegap;
|
short line_gap = (short)ctx.font.linegap;
|
||||||
bool update_atlas;
|
|
||||||
// if the element is new or the parent was updated then redo layout
|
// if the element is new or the parent was updated then redo layout
|
||||||
if (c_elem.flags.is_new || parent.flags.updated) {
|
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
|
// 2. Layout
|
||||||
c_elem.bounds = ctx.position_element(parent, text_size, true);
|
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;
|
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)!;
|
ctx.push_string(c_elem.bounds, text)!;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user