somewhat functional text rendering

font_atlas
Alessandro Mauri 2 weeks ago
parent dbe70eb4f4
commit 2356d165fe
  1. 18
      TODO
  2. 18
      lib/libschrift.c3l/libschrift.c3
  3. 34
      src/main.c3
  4. 24
      src/ugui_data.c3
  5. 205
      src/ugui_font.c3
  6. 81
      src/ugui_text.c3

18
TODO

@ -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

@ -4,6 +4,8 @@ def SftFont = void*;
def SftUChar = uint; def SftUChar = uint;
def SftGlyph = uint; def SftGlyph = uint;
const int SFT_DOWNWARD_Y = 0x01;
struct Sft struct Sft
{ {
SftFont font; SftFont font;
@ -45,12 +47,12 @@ struct SftImage
extern fn char* sft_version() @extern("sft_version"); extern fn char* sft_version() @extern("sft_version");
extern fn SftFont sft_loadmem(void* mem, usz size) @extern("sft_loadmem"); extern fn SftFont loadmem(void* mem, usz size) @extern("sft_loadmem");
extern fn SftFont sft_loadfile(char* filename) @extern("sft_loadfile"); extern fn SftFont loadfile(char* filename) @extern("sft_loadfile");
extern fn void sft_freefont(SftFont font) @extern("sft_freefont"); extern fn void freefont(SftFont font) @extern("sft_freefont");
extern fn int sft_lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics"); extern fn int lmetrics(Sft* sft, SftLMetrics* metrics) @extern("sft_lmetrics");
extern fn int sft_lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup"); extern fn int lookup(Sft* sft, SftUChar codepoint, SftGlyph* glyph) @extern("sft_lookup");
extern fn int sft_gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics"); extern fn int gmetrics(Sft* sft, SftGlyph glyph, SftGMetrics* metrics) @extern("sft_gmetrics");
extern fn int sft_kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning"); extern fn int kerning(Sft* sft, SftGlyph leftGlyph, SftGlyph rightGlyph, SftKerning* kerning) @extern("sft_kerning");
extern fn int sft_render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render"); extern fn int render(Sft* sft, SftGlyph glyph, SftImage image) @extern("sft_render");

@ -27,6 +27,7 @@ fn int main(String[] args)
{ {
ugui::Ctx ui; ugui::Ctx ui;
ui.init()!!; ui.init()!!;
ui.font.load("/usr/share/fonts/TTF/FreeSans.ttf", 16)!!;
short width = 800; short width = 800;
short height = 450; short height = 450;
@ -92,6 +93,8 @@ fn int main(String[] args)
ugui::Elem* e = ui.get_elem_by_label("slider")!!; ugui::Elem* e = ui.get_elem_by_label("slider")!!;
io::printfn("slider: %f", e.slider.value); io::printfn("slider: %f", e.slider.value);
} }
ui.text_unbounded("text1", "Ciao Mamma")!!;
|}; |};
ui.div_end()!!; ui.div_end()!!;
@ -125,6 +128,8 @@ fn int main(String[] args)
// ClearBackground(BLACK); // ClearBackground(BLACK);
rl::Color c; rl::Color c;
static rl::Image font_atlas;
static rl::Texture2D font_texture;
for (Cmd* cmd; (cmd = ui.cmd_queue.dequeue() ?? null) != null;) { for (Cmd* cmd; (cmd = ui.cmd_queue.dequeue() ?? null) != null;) {
switch (cmd.type) { switch (cmd.type) {
case ugui::CmdType.CMD_RECT: case ugui::CmdType.CMD_RECT:
@ -141,8 +146,34 @@ fn int main(String[] args)
cmd.rect.rect.h, cmd.rect.rect.h,
c c
); );
case ugui::CmdType.CMD_UPDATE_ATLAS:
rl::unload_image(font_atlas);
font_atlas.data = cmd.update_atlas.raw_buffer;
font_atlas.width = cmd.update_atlas.width;
font_atlas.height = cmd.update_atlas.height;
font_atlas.mipmaps = 1;
//font_atlas.format = rl::PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE;
font_atlas.format = 1;
if (rl::is_texture_ready(font_texture)) {
rl::unload_texture(font_texture);
}
font_texture = rl::load_texture_from_image(font_atlas);
//rl::draw_texture(font_texture, 0, 0, rl::WHITE);
case ugui::CmdType.CMD_SPRITE:
rl::Rectangle source = {
.x = cmd.sprite.texture_rect.x,
.y = cmd.sprite.texture_rect.y,
.width = cmd.sprite.texture_rect.w,
.height = cmd.sprite.texture_rect.h,
};
rl::Vector2 position = {
.x = cmd.sprite.rect.x,
.y = (float)cmd.sprite.rect.y - cmd.sprite.rect.h,
};
rl::draw_texture_rec(font_texture, source, position, rl::WHITE);
default: default:
io::printfn("Unknown cmd type: %d", cmd.type); io::printfn("Unknown cmd type: %s", cmd.type);
} }
} }
draw_times.push(clock.mark()); draw_times.push(clock.mark());
@ -154,6 +185,7 @@ fn int main(String[] args)
rl::close_window(); rl::close_window();
ui.font.free();
ui.free(); ui.free();
return 0; return 0;
} }

@ -28,6 +28,7 @@ enum ElemType {
ETYPE_DIV, ETYPE_DIV,
ETYPE_BUTTON, ETYPE_BUTTON,
ETYPE_SLIDER, ETYPE_SLIDER,
ETYPE_TEXT,
} }
bitstruct ElemFlags : uint { bitstruct ElemFlags : uint {
@ -75,6 +76,10 @@ struct Slider {
Rect handle; Rect handle;
} }
struct Text {
char* str;
}
// element structure // element structure
struct Elem { struct Elem {
Id id; Id id;
@ -85,6 +90,7 @@ struct Elem {
union { union {
Div div; Div div;
Slider slider; Slider slider;
Text text;
} }
} }
@ -126,6 +132,8 @@ const uint ROOT_ID = 1;
// command type // command type
enum CmdType { enum CmdType {
CMD_RECT, CMD_RECT,
CMD_UPDATE_ATLAS,
CMD_SPRITE,
} }
// command to draw a rect // command to draw a rect
@ -134,11 +142,27 @@ struct CmdRect {
Color color; 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 // command structure
struct Cmd { struct Cmd {
CmdType type; CmdType type;
union { union {
CmdRect rect; CmdRect rect;
CmdUpdateAtlas update_atlas;
CmdSprite sprite;
} }
} }

@ -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…
Cancel
Save