parent
dbe70eb4f4
commit
2356d165fe
@ -1,7 +1,21 @@ |
|||||||
# TODOs, semi-random sorting |
# TODOs, semi-random sorting |
||||||
[ ] Implement glyph draw command |
[x] Implement glyph draw command |
||||||
[ ] Implement div.view and scrollbars |
[x] Implement div.view and scrollbars |
||||||
[ ] Port font system from C to C3 (rewrite1) |
[ ] Port font system from C to C3 (rewrite1) |
||||||
[ ] Update ARCHITECTURE.md |
[ ] Update ARCHITECTURE.md |
||||||
[ ] Write a README.md |
[ ] Write a README.md |
||||||
[ ] Use an arena allocator for cache |
[ ] 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 |
||||||
|
@ -1,18 +1,217 @@ |
|||||||
module ugui; |
module ugui; |
||||||
|
|
||||||
import schrift; |
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(<Codepoint, Glyph>) @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; |
schrift::Sft sft; |
||||||
String path; |
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); |
||||||
} |
} |
||||||
|
@ -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)!; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue