...
This commit is contained in:
22
build.sx
Normal file
22
build.sx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#import "modules/compiler.sx";
|
||||||
|
|
||||||
|
configure_build :: () {
|
||||||
|
opts := build_options();
|
||||||
|
inline if OS == {
|
||||||
|
case .wasm: {
|
||||||
|
output := if POINTER_SIZE == 4
|
||||||
|
then "sx-out/wasm32/index.html"
|
||||||
|
else "sx-out/wasm64/index.html";
|
||||||
|
opts.set_output_path(output);
|
||||||
|
opts.set_wasm_shell("shell.html");
|
||||||
|
opts.add_link_flag("-sUSE_SDL=3");
|
||||||
|
opts.add_link_flag("-sMAX_WEBGL_VERSION=2");
|
||||||
|
opts.add_link_flag("-sFULL_ES3=1");
|
||||||
|
opts.add_link_flag("--preload-file assets");
|
||||||
|
opts.add_link_flag("-sALLOW_MEMORY_GROWTH=1");
|
||||||
|
}
|
||||||
|
case .macos: {
|
||||||
|
opts.set_output_path("main");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 208 KiB |
76
main.sx
76
main.sx
@@ -1,40 +1,24 @@
|
|||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
#import "build.sx";
|
||||||
#import "modules/compiler.sx";
|
#import "modules/compiler.sx";
|
||||||
#import "modules/sdl3.sx";
|
#import "modules/sdl3.sx";
|
||||||
#import "modules/opengl.sx";
|
#import "modules/opengl.sx";
|
||||||
#import "modules/math";
|
#import "modules/math";
|
||||||
#import "modules/stb.sx";
|
#import "modules/stb.sx";
|
||||||
#import "modules/stb_truetype.sx";
|
#import "modules/stb_truetype.sx";
|
||||||
|
#import "modules/wasm.sx";
|
||||||
#import "ui";
|
#import "ui";
|
||||||
|
|
||||||
configure_build :: () {
|
|
||||||
opts := build_options();
|
|
||||||
inline if OS == {
|
|
||||||
case .wasm: {
|
|
||||||
opts.set_output_path(if POINTER_SIZE == 4 then "sx-out/wasm32/index.html" else "sx-out/wasm32/index.html");
|
|
||||||
opts.add_link_flag("-sUSE_SDL=3");
|
|
||||||
opts.add_link_flag("-sMAX_WEBGL_VERSION=2");
|
|
||||||
opts.add_link_flag("-sFULL_ES3=1");
|
|
||||||
opts.add_link_flag("--preload-file assets");
|
|
||||||
opts.add_link_flag("-sALLOW_MEMORY_GROWTH=1");
|
|
||||||
}
|
|
||||||
case .macos: {
|
|
||||||
opts.set_output_path("sx-out/macos/game");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#run configure_build();
|
#run configure_build();
|
||||||
|
|
||||||
libc :: #library "c";
|
|
||||||
emscripten_set_main_loop :: (func: *void, fps: s32, sim_infinite: s32) #foreign libc;
|
|
||||||
|
|
||||||
WIDTH :f32: 800;
|
|
||||||
HEIGHT :f32: 600;
|
|
||||||
|
|
||||||
// --- Frame state (globals for emscripten callback) ---
|
// --- Frame state (globals for emscripten callback) ---
|
||||||
g_window : *void = ---;
|
g_window : *void = ---;
|
||||||
g_pipeline : *UIPipeline = ---;
|
g_pipeline : *UIPipeline = ---;
|
||||||
g_running : bool = true;
|
g_running : bool = true;
|
||||||
|
g_width : s32 = 800; // logical window size
|
||||||
|
g_height : s32 = 600;
|
||||||
|
g_pixel_w : s32 = 800; // physical pixel size
|
||||||
|
g_pixel_h : s32 = 600;
|
||||||
|
|
||||||
load_texture :: (path: [:0]u8) -> u32 {
|
load_texture :: (path: [:0]u8) -> u32 {
|
||||||
w : s32 = 0;
|
w : s32 = 0;
|
||||||
@@ -141,13 +125,13 @@ run_ui_tests :: (pipeline: *UIPipeline) {
|
|||||||
// Render after tests and save snapshot to see scrolled state
|
// Render after tests and save snapshot to see scrolled state
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
pipeline.tick();
|
pipeline.tick();
|
||||||
save_snapshot("goldens/test_after_drag.png", xx WIDTH, xx HEIGHT);
|
save_snapshot("goldens/test_after_drag.png", g_pixel_w, g_pixel_h);
|
||||||
}
|
}
|
||||||
|
|
||||||
// One frame of the main loop — called repeatedly by emscripten or desktop while-loop
|
// One frame of the main loop — called repeatedly by emscripten or desktop while-loop
|
||||||
frame :: () {
|
frame :: () {
|
||||||
sdl_event : SDL_Event = .none;
|
sdl_event : SDL_Event = .none;
|
||||||
while SDL_PollEvent(sdl_event) {
|
while SDL_PollEvent(@sdl_event) {
|
||||||
print("SDL event: {}\n", sdl_event.tag);
|
print("SDL event: {}\n", sdl_event.tag);
|
||||||
|
|
||||||
if sdl_event == {
|
if sdl_event == {
|
||||||
@@ -155,17 +139,24 @@ frame :: () {
|
|||||||
case .key_up: (e) {
|
case .key_up: (e) {
|
||||||
if e.key == { case .escape: { g_running = false; } }
|
if e.key == { case .escape: { g_running = false; } }
|
||||||
}
|
}
|
||||||
|
case .window_resized: (data) {
|
||||||
|
g_width = data.data1;
|
||||||
|
g_height = data.data2;
|
||||||
|
SDL_GetWindowSizeInPixels(g_window, @g_pixel_w, @g_pixel_h);
|
||||||
|
g_pipeline.resize(xx g_width, xx g_height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ui_event := translate_sdl_event(@sdl_event);
|
ui_event := translate_sdl_event(@sdl_event);
|
||||||
if ui_event != .none {
|
if ui_event != .none {
|
||||||
print(" ui event dispatched\n");
|
print(" ui event dispatched\n");
|
||||||
g_pipeline.*.dispatch_event(@ui_event);
|
g_pipeline.dispatch_event(@ui_event);
|
||||||
} else {
|
} else {
|
||||||
print(" -> .none\n");
|
print(" -> .none\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
glViewport(0, 0, g_pixel_w, g_pixel_h);
|
||||||
glClearColor(0.12, 0.12, 0.15, 1.0);
|
glClearColor(0.12, 0.12, 0.15, 1.0);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
@@ -189,17 +180,42 @@ main :: () -> void {
|
|||||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||||
|
|
||||||
window := SDL_CreateWindow("SX UI Demo", xx WIDTH, xx HEIGHT, SDL_WINDOW_OPENGL);
|
// Auto-size: on desktop use 75% of usable display, on WASM SDL picks up canvas size
|
||||||
|
init_w : s32 = 800;
|
||||||
|
init_h : s32 = 600;
|
||||||
|
inline if OS == .wasm {
|
||||||
|
init_w = emscripten_run_script_int("window.innerWidth");
|
||||||
|
init_h = emscripten_run_script_int("window.innerHeight");
|
||||||
|
} else {
|
||||||
|
display_id := SDL_GetPrimaryDisplay();
|
||||||
|
bounds : SDL_Rect = ---;
|
||||||
|
if SDL_GetDisplayUsableBounds(display_id, @bounds) {
|
||||||
|
init_w = bounds.w * 3 / 4;
|
||||||
|
init_h = bounds.h * 3 / 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window := SDL_CreateWindow("SX UI Demo", init_w, init_h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
|
||||||
gl_ctx := SDL_GL_CreateContext(window);
|
gl_ctx := SDL_GL_CreateContext(window);
|
||||||
SDL_GL_MakeCurrent(window, gl_ctx);
|
SDL_GL_MakeCurrent(window, gl_ctx);
|
||||||
SDL_GL_SetSwapInterval(1);
|
SDL_GL_SetSwapInterval(1);
|
||||||
|
|
||||||
load_gl(xx SDL_GL_GetProcAddress);
|
load_gl(xx SDL_GL_GetProcAddress);
|
||||||
|
|
||||||
|
// Query actual window size (may differ from requested, especially on WASM)
|
||||||
|
SDL_GetWindowSize(window, @g_width, @g_height);
|
||||||
|
SDL_GetWindowSizeInPixels(window, @g_pixel_w, @g_pixel_h);
|
||||||
|
width_f : f32 = xx g_width;
|
||||||
|
height_f : f32 = xx g_height;
|
||||||
|
dpi_scale : f32 = if width_f > 0.0 then xx g_pixel_w / width_f else 1.0;
|
||||||
|
|
||||||
|
// Set viewport to physical pixel dimensions
|
||||||
|
glViewport(0, 0, g_pixel_w, g_pixel_h);
|
||||||
|
|
||||||
// --- Build UI ---
|
// --- Build UI ---
|
||||||
pipeline : UIPipeline = ---;
|
pipeline : UIPipeline = ---;
|
||||||
pipeline.init(WIDTH, HEIGHT);
|
pipeline.init(width_f, height_f);
|
||||||
pipeline.init_font("assets/fonts/default.ttf", 32.0);
|
pipeline.init_font("assets/fonts/default.ttf", 32.0, dpi_scale);
|
||||||
|
|
||||||
scroll_content := VStack.{ spacing = 10.0, alignment = .center } {
|
scroll_content := VStack.{ spacing = 10.0, alignment = .center } {
|
||||||
self.add(
|
self.add(
|
||||||
@@ -220,7 +236,7 @@ main :: () -> void {
|
|||||||
});
|
});
|
||||||
self.add(
|
self.add(
|
||||||
RectView.{ color = COLOR_DARK_GRAY, preferred_height = 60.0 }
|
RectView.{ color = COLOR_DARK_GRAY, preferred_height = 60.0 }
|
||||||
|> padding(EdgeInsets.symmetric(16.0, 8.0))
|
|> padding(.symmetric(16.0, 8.0))
|
||||||
|> background(COLOR_BLUE, 8.0)
|
|> background(COLOR_BLUE, 8.0)
|
||||||
);
|
);
|
||||||
self.add(RectView.{ color = COLOR_ORANGE, preferred_height = 120.0, corner_radius = 12.0 });
|
self.add(RectView.{ color = COLOR_ORANGE, preferred_height = 120.0, corner_radius = 12.0 });
|
||||||
@@ -243,6 +259,8 @@ main :: () -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
save_snapshot("goldens/last_frame.png", g_pixel_w, g_pixel_h);
|
||||||
|
|
||||||
SDL_GL_DestroyContext(gl_ctx);
|
SDL_GL_DestroyContext(gl_ctx);
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
|
|||||||
54
shell.html
Normal file
54
shell.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||||
|
<title>sx</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{width:100%;height:100%;overflow:hidden;background:#1e1e24}
|
||||||
|
canvas{display:block;width:100vw;height:100vh;outline:none}
|
||||||
|
#overlay{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#1e1e24;z-index:10;transition:opacity .4s}
|
||||||
|
#overlay.hidden{opacity:0;pointer-events:none}
|
||||||
|
#overlay .bar-track{width:min(280px,60vw);height:3px;background:#2a2a32;border-radius:2px;margin-top:18px}
|
||||||
|
#overlay .bar-fill{height:100%;width:0%;background:#7c7cff;border-radius:2px;transition:width .15s}
|
||||||
|
#overlay .status{color:#888;font:13px/1 -apple-system,system-ui,sans-serif;margin-top:10px;letter-spacing:.02em}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="overlay">
|
||||||
|
<div class="bar-track"><div class="bar-fill" id="bar"></div></div>
|
||||||
|
<div class="status" id="status">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
|
||||||
|
<script>
|
||||||
|
var c=document.getElementById('canvas');
|
||||||
|
var dpr=window.devicePixelRatio||1;
|
||||||
|
c.width=window.innerWidth*dpr;
|
||||||
|
c.height=window.innerHeight*dpr;
|
||||||
|
|
||||||
|
var bar=document.getElementById('bar');
|
||||||
|
var status=document.getElementById('status');
|
||||||
|
var overlay=document.getElementById('overlay');
|
||||||
|
var Module={
|
||||||
|
canvas:c,
|
||||||
|
print:function(){console.log(Array.prototype.slice.call(arguments).join(' '))},
|
||||||
|
printErr:function(){console.warn(Array.prototype.slice.call(arguments).join(' '))},
|
||||||
|
setStatus:function(t){
|
||||||
|
if(!t){overlay.classList.add('hidden');return}
|
||||||
|
var m=t.match(/\((\d+(?:\.\d+)?)\/(\d+)\)/);
|
||||||
|
if(m){bar.style.width=(parseInt(m[1])/parseInt(m[2])*100)+'%';status.textContent='Loading\u2026'}
|
||||||
|
else{status.textContent=t}
|
||||||
|
},
|
||||||
|
totalDependencies:0,
|
||||||
|
monitorRunDependencies:function(left){
|
||||||
|
this.totalDependencies=Math.max(this.totalDependencies,left);
|
||||||
|
this.setStatus(left?'Loading... ('+( this.totalDependencies-left)+'/'+this.totalDependencies+')':'');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Module.setStatus('Loading\u2026');
|
||||||
|
window.onerror=function(){Module.setStatus('Error \u2014 see console');};
|
||||||
|
</script>
|
||||||
|
{{{ SCRIPT }}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
92
ui/font.sx
92
ui/font.sx
@@ -1,95 +1,11 @@
|
|||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "ui/types.sx";
|
#import "ui/types.sx";
|
||||||
|
#import "ui/glyph_cache.sx";
|
||||||
|
|
||||||
FIRST_CHAR :s32: 32;
|
// Global glyph cache pointer for views (Label, Button) to access
|
||||||
NUM_CHARS :s32: 96;
|
g_font : *GlyphCache = xx 0;
|
||||||
ATLAS_W :s32: 512;
|
|
||||||
ATLAS_H :s32: 512;
|
|
||||||
|
|
||||||
// Matches stbtt_bakedchar memory layout
|
set_global_font :: (font: *GlyphCache) {
|
||||||
BakedChar :: struct {
|
|
||||||
x0, y0, x1, y1: u16;
|
|
||||||
xoff, yoff, xadvance: f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches stbtt_aligned_quad memory layout
|
|
||||||
AlignedQuad :: struct {
|
|
||||||
x0, y0, s0, t0: f32;
|
|
||||||
x1, y1, s1, t1: f32;
|
|
||||||
}
|
|
||||||
|
|
||||||
FontAtlas :: struct {
|
|
||||||
texture_id: u32;
|
|
||||||
font_size: f32;
|
|
||||||
char_data: [*]BakedChar;
|
|
||||||
bitmap: [*]u8;
|
|
||||||
ascent: f32;
|
|
||||||
descent: f32;
|
|
||||||
line_height: f32;
|
|
||||||
|
|
||||||
// Bake font glyphs into a bitmap. Call upload_texture() after GL is ready.
|
|
||||||
init :: (self: *FontAtlas, path: [:0]u8, size: f32) {
|
|
||||||
file_size : s32 = 0;
|
|
||||||
font_data := read_file_bytes(path, @file_size);
|
|
||||||
if xx font_data == 0 {
|
|
||||||
out("Failed to load font: ");
|
|
||||||
out(path);
|
|
||||||
out("\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.font_size = size;
|
|
||||||
|
|
||||||
// Allocate baked char data (96 entries for ASCII 32..127)
|
|
||||||
self.char_data = xx context.allocator.alloc(xx NUM_CHARS * size_of(BakedChar));
|
|
||||||
|
|
||||||
// Bake font bitmap (512x512 single-channel alpha)
|
|
||||||
bitmap_size : s64 = xx ATLAS_W * xx ATLAS_H;
|
|
||||||
self.bitmap = xx context.allocator.alloc(bitmap_size);
|
|
||||||
stbtt_BakeFontBitmap(font_data, 0, size, self.bitmap, ATLAS_W, ATLAS_H, FIRST_CHAR, NUM_CHARS, xx self.char_data);
|
|
||||||
|
|
||||||
// Get font vertical metrics
|
|
||||||
fontinfo : [256]u8 = ---;
|
|
||||||
stbtt_InitFont(xx @fontinfo, font_data, 0);
|
|
||||||
ascent_i : s32 = 0;
|
|
||||||
descent_i : s32 = 0;
|
|
||||||
linegap_i : s32 = 0;
|
|
||||||
stbtt_GetFontVMetrics(xx @fontinfo, @ascent_i, @descent_i, @linegap_i);
|
|
||||||
scale := stbtt_ScaleForPixelHeight(xx @fontinfo, size);
|
|
||||||
self.ascent = xx ascent_i * scale;
|
|
||||||
self.descent = xx descent_i * scale;
|
|
||||||
self.line_height = self.ascent - self.descent + xx linegap_i * scale;
|
|
||||||
|
|
||||||
font_data_ptr : *void = xx font_data;
|
|
||||||
free(font_data_ptr);
|
|
||||||
|
|
||||||
out("Font loaded: ");
|
|
||||||
out(path);
|
|
||||||
out("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
measure_text :: (self: *FontAtlas, text: string, font_size: f32) -> Size {
|
|
||||||
if self.char_data == null { return Size.zero(); }
|
|
||||||
scale := font_size / self.font_size;
|
|
||||||
xpos : f32 = 0.0;
|
|
||||||
ypos : f32 = 0.0;
|
|
||||||
q : AlignedQuad = ---;
|
|
||||||
i : s64 = 0;
|
|
||||||
while i < text.len {
|
|
||||||
ch : s32 = xx text[i];
|
|
||||||
if ch >= FIRST_CHAR and ch < FIRST_CHAR + NUM_CHARS {
|
|
||||||
stbtt_GetBakedQuad(xx self.char_data, ATLAS_W, ATLAS_H, ch - FIRST_CHAR, @xpos, @ypos, xx @q, 1);
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
Size.{ width = xpos * scale, height = self.line_height * scale };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global font atlas pointer for views (Label, Button) to access
|
|
||||||
g_font : *FontAtlas = xx 0;
|
|
||||||
|
|
||||||
set_global_font :: (font: *FontAtlas) {
|
|
||||||
g_font = font;
|
g_font = font;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
532
ui/glyph_cache.sx
Normal file
532
ui/glyph_cache.sx
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
#import "modules/std.sx";
|
||||||
|
#import "modules/opengl.sx";
|
||||||
|
#import "ui/types.sx";
|
||||||
|
|
||||||
|
// Cached glyph data with UV coordinates into the atlas texture
|
||||||
|
CachedGlyph :: struct {
|
||||||
|
uv_x: f32;
|
||||||
|
uv_y: f32;
|
||||||
|
uv_w: f32;
|
||||||
|
uv_h: f32;
|
||||||
|
width: f32;
|
||||||
|
height: f32;
|
||||||
|
offset_x: f32;
|
||||||
|
offset_y: f32;
|
||||||
|
advance: f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache entry: key + glyph data
|
||||||
|
GlyphEntry :: struct {
|
||||||
|
key: u32;
|
||||||
|
glyph: CachedGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quantize font size to half-point increments to limit cache entries.
|
||||||
|
// e.g., 13.0 -> 26, 13.5 -> 27, 14.0 -> 28
|
||||||
|
quantize_size :: (font_size: f32) -> u16 {
|
||||||
|
xx (font_size * 2.0 + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
dequantize_size :: (q: u16) -> f32 {
|
||||||
|
xx q / 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack (glyph_index, size_quantized) into a single u32 for fast comparison
|
||||||
|
make_glyph_key :: (glyph_index: u16, size_quantized: u16) -> u32 {
|
||||||
|
(xx glyph_index << 16) | xx size_quantized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shaped glyph — output of text shaping (positioned glyph with index)
|
||||||
|
ShapedGlyph :: struct {
|
||||||
|
glyph_index: u16;
|
||||||
|
x: f32; // horizontal position (logical units, cumulative)
|
||||||
|
y: f32; // vertical offset (logical units)
|
||||||
|
advance: f32; // advance width (logical units)
|
||||||
|
}
|
||||||
|
|
||||||
|
is_ascii :: (text: string) -> bool {
|
||||||
|
i : s64 = 0;
|
||||||
|
while i < text.len {
|
||||||
|
if text[i] >= 128 { return false; }
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// kbts constants (C enum values)
|
||||||
|
KBTS_DIRECTION_DONT_KNOW :u32: 0;
|
||||||
|
KBTS_LANGUAGE_DONT_KNOW :u32: 0;
|
||||||
|
KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX :u32: 0;
|
||||||
|
|
||||||
|
// SX structs matching kbts C struct layouts (64-bit).
|
||||||
|
// We define these in SX to access fields directly, casting from opaque C pointers.
|
||||||
|
|
||||||
|
KbtsGlyphIterator :: struct {
|
||||||
|
glyph_storage: *void;
|
||||||
|
current_glyph: *void;
|
||||||
|
last_advance_x: s32;
|
||||||
|
x: s32;
|
||||||
|
y: s32;
|
||||||
|
}
|
||||||
|
|
||||||
|
KbtsRun :: struct {
|
||||||
|
font: *void;
|
||||||
|
script: u32;
|
||||||
|
paragraph_direction: u32;
|
||||||
|
direction: u32;
|
||||||
|
flags: u32;
|
||||||
|
glyphs: KbtsGlyphIterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
KbtsGlyph :: struct {
|
||||||
|
prev: *void;
|
||||||
|
next: *void;
|
||||||
|
codepoint: u32;
|
||||||
|
id: u16;
|
||||||
|
uid: u16;
|
||||||
|
user_id_or_codepoint_index: s32;
|
||||||
|
offset_x: s32;
|
||||||
|
offset_y: s32;
|
||||||
|
advance_x: s32;
|
||||||
|
advance_y: s32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// kbts_font_info2 base (simplified — we only need the Size field for dispatch)
|
||||||
|
KBTS_FONT_INFO_STRING_ID_COUNT :s32: 7;
|
||||||
|
|
||||||
|
KbtsFontInfo2 :: struct {
|
||||||
|
size: u32;
|
||||||
|
strings: [7]*void; // char* array
|
||||||
|
string_lengths: [7]u16;
|
||||||
|
style_flags: u32;
|
||||||
|
weight: u32;
|
||||||
|
width: u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
KbtsFontInfo2_1 :: struct {
|
||||||
|
base: KbtsFontInfo2;
|
||||||
|
units_per_em: u16;
|
||||||
|
x_min: s16;
|
||||||
|
y_min: s16;
|
||||||
|
x_max: s16;
|
||||||
|
y_max: s16;
|
||||||
|
ascent: s16;
|
||||||
|
descent: s16;
|
||||||
|
line_gap: s16;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLYPH_ATLAS_W :s32: 1024;
|
||||||
|
GLYPH_ATLAS_H :s32: 1024;
|
||||||
|
FONTINFO_SIZE :s64: 256;
|
||||||
|
|
||||||
|
PackResult :: struct {
|
||||||
|
x, y: s32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic glyph cache with on-demand rasterization and texture atlas packing.
|
||||||
|
GlyphCache :: struct {
|
||||||
|
// Font data
|
||||||
|
font_info: *void; // heap-allocated stbtt_fontinfo (256 bytes)
|
||||||
|
font_data: *void; // raw TTF file bytes (kept alive for stbtt)
|
||||||
|
|
||||||
|
// Atlas texture (GPU)
|
||||||
|
texture_id: u32;
|
||||||
|
atlas_width: s32;
|
||||||
|
atlas_height: s32;
|
||||||
|
|
||||||
|
// Atlas bitmap (CPU-side for updates)
|
||||||
|
bitmap: [*]u8;
|
||||||
|
|
||||||
|
// Shelf packer state
|
||||||
|
shelf_y: s32;
|
||||||
|
shelf_height: s32;
|
||||||
|
cursor_x: s32;
|
||||||
|
padding: s32;
|
||||||
|
|
||||||
|
// Glyph lookup cache (flat list, linear scan)
|
||||||
|
entries: List(GlyphEntry);
|
||||||
|
|
||||||
|
// Dirty tracking for texture upload
|
||||||
|
dirty: bool;
|
||||||
|
|
||||||
|
// Font vertical metrics (at reference size 1.0 — scale by font_size)
|
||||||
|
ascent: f32;
|
||||||
|
descent: f32;
|
||||||
|
line_gap: f32;
|
||||||
|
|
||||||
|
// HiDPI: physical pixels per logical pixel (e.g. 2.0 on Retina)
|
||||||
|
dpi_scale: f32;
|
||||||
|
inv_dpi: f32;
|
||||||
|
|
||||||
|
// Text shaping (kb_text_shape)
|
||||||
|
shape_ctx: *void;
|
||||||
|
shape_font: *void;
|
||||||
|
units_per_em: u16;
|
||||||
|
font_data_size: s32;
|
||||||
|
shaped_buf: List(ShapedGlyph);
|
||||||
|
|
||||||
|
init :: (self: *GlyphCache, path: [:0]u8, default_size: f32) {
|
||||||
|
// Zero out the entire struct first (parent may be uninitialized with = ---)
|
||||||
|
memset(self, 0, size_of(GlyphCache));
|
||||||
|
|
||||||
|
// Load font file
|
||||||
|
file_size : s32 = 0;
|
||||||
|
font_data := read_file_bytes(path, @file_size);
|
||||||
|
if xx font_data == 0 {
|
||||||
|
out("Failed to load font: ");
|
||||||
|
out(path);
|
||||||
|
out("\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.font_data = xx font_data;
|
||||||
|
self.font_data_size = file_size;
|
||||||
|
|
||||||
|
// Init stbtt_fontinfo
|
||||||
|
self.font_info = context.allocator.alloc(FONTINFO_SIZE);
|
||||||
|
memset(self.font_info, 0, FONTINFO_SIZE);
|
||||||
|
stbtt_InitFont(self.font_info, font_data, 0);
|
||||||
|
|
||||||
|
// Get font vertical metrics (in unscaled font units)
|
||||||
|
ascent_i : s32 = 0;
|
||||||
|
descent_i : s32 = 0;
|
||||||
|
linegap_i : s32 = 0;
|
||||||
|
stbtt_GetFontVMetrics(self.font_info, @ascent_i, @descent_i, @linegap_i);
|
||||||
|
|
||||||
|
// Store unscaled metrics — we'll scale per font_size in measure_text
|
||||||
|
self.ascent = xx ascent_i;
|
||||||
|
self.descent = xx descent_i;
|
||||||
|
self.line_gap = xx linegap_i;
|
||||||
|
|
||||||
|
// Init text shaping context
|
||||||
|
self.shape_ctx = xx kbts_CreateShapeContext(xx 0, xx 0);
|
||||||
|
if xx self.shape_ctx != 0 {
|
||||||
|
self.shape_font = xx kbts_ShapePushFontFromMemory(xx self.shape_ctx, self.font_data, file_size, 0);
|
||||||
|
// Get font metrics (units_per_em) from kbts
|
||||||
|
kb_info : KbtsFontInfo2_1 = ---;
|
||||||
|
memset(@kb_info, 0, size_of(KbtsFontInfo2_1));
|
||||||
|
kb_info.base.size = xx size_of(KbtsFontInfo2_1);
|
||||||
|
kbts_GetFontInfo2(xx self.shape_font, xx @kb_info);
|
||||||
|
self.units_per_em = kb_info.units_per_em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate atlas bitmap
|
||||||
|
self.atlas_width = GLYPH_ATLAS_W;
|
||||||
|
self.atlas_height = GLYPH_ATLAS_H;
|
||||||
|
bitmap_size : s64 = xx self.atlas_width * xx self.atlas_height;
|
||||||
|
self.bitmap = xx context.allocator.alloc(bitmap_size);
|
||||||
|
memset(self.bitmap, 0, bitmap_size);
|
||||||
|
|
||||||
|
// Shelf packer init
|
||||||
|
self.shelf_y = 0;
|
||||||
|
self.shelf_height = 0;
|
||||||
|
self.cursor_x = 0;
|
||||||
|
self.padding = 1;
|
||||||
|
|
||||||
|
self.dirty = false;
|
||||||
|
self.dpi_scale = 1.0;
|
||||||
|
self.inv_dpi = 1.0;
|
||||||
|
|
||||||
|
// Create OpenGL texture
|
||||||
|
glGenTextures(1, @self.texture_id);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, self.texture_id);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, self.atlas_width, self.atlas_height, 0, GL_RED, GL_UNSIGNED_BYTE, self.bitmap);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
out("GlyphCache initialized: ");
|
||||||
|
out(path);
|
||||||
|
out("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up or rasterize a glyph, returning a pointer to its cached entry.
|
||||||
|
// Returns null for glyphs with no outline AND zero advance (shouldn't happen for valid chars).
|
||||||
|
get_or_rasterize :: (self: *GlyphCache, glyph_index: u16, font_size: f32) -> *CachedGlyph {
|
||||||
|
size_q := quantize_size(font_size);
|
||||||
|
key := make_glyph_key(glyph_index, size_q);
|
||||||
|
|
||||||
|
// Cache lookup (linear scan)
|
||||||
|
i : s64 = 0;
|
||||||
|
while i < self.entries.len {
|
||||||
|
if self.entries.items[i].key == key {
|
||||||
|
return @self.entries.items[i].glyph;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss — rasterize
|
||||||
|
actual_size := dequantize_size(size_q);
|
||||||
|
scale := stbtt_ScaleForPixelHeight(self.font_info, actual_size);
|
||||||
|
|
||||||
|
// Get glyph bounding box
|
||||||
|
x0 : s32 = 0;
|
||||||
|
y0 : s32 = 0;
|
||||||
|
x1 : s32 = 0;
|
||||||
|
y1 : s32 = 0;
|
||||||
|
stbtt_GetGlyphBitmapBox(self.font_info, xx glyph_index, scale, scale, @x0, @y0, @x1, @y1);
|
||||||
|
|
||||||
|
glyph_w := if x1 > x0 then x1 - x0 else 0;
|
||||||
|
glyph_h := if y1 > y0 then y1 - y0 else 0;
|
||||||
|
|
||||||
|
// Get horizontal metrics
|
||||||
|
advance_i : s32 = 0;
|
||||||
|
lsb_i : s32 = 0;
|
||||||
|
stbtt_GetGlyphHMetrics(self.font_info, xx glyph_index, @advance_i, @lsb_i);
|
||||||
|
advance : f32 = xx advance_i * scale;
|
||||||
|
|
||||||
|
// Zero-size glyph (e.g. space) — cache with advance only
|
||||||
|
if glyph_w == 0 or glyph_h == 0 {
|
||||||
|
entry := GlyphEntry.{
|
||||||
|
key = key,
|
||||||
|
glyph = CachedGlyph.{
|
||||||
|
uv_x = 0.0, uv_y = 0.0, uv_w = 0.0, uv_h = 0.0,
|
||||||
|
width = 0.0, height = 0.0,
|
||||||
|
offset_x = xx x0, offset_y = xx y0,
|
||||||
|
advance = advance
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.entries.append(entry);
|
||||||
|
return @self.entries.items[self.entries.len - 1].glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack into atlas
|
||||||
|
pack := self.try_pack(glyph_w, glyph_h);
|
||||||
|
if pack.x < 0 {
|
||||||
|
// Atlas full — grow and retry
|
||||||
|
self.grow();
|
||||||
|
return self.get_or_rasterize(glyph_index, font_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rasterize directly into atlas bitmap
|
||||||
|
dest_offset : s64 = xx pack.y * xx self.atlas_width + xx pack.x;
|
||||||
|
stbtt_MakeGlyphBitmap(
|
||||||
|
self.font_info,
|
||||||
|
@self.bitmap[dest_offset],
|
||||||
|
glyph_w, glyph_h,
|
||||||
|
self.atlas_width,
|
||||||
|
scale, scale,
|
||||||
|
xx glyph_index
|
||||||
|
);
|
||||||
|
self.dirty = true;
|
||||||
|
|
||||||
|
// Compute normalized UV coordinates
|
||||||
|
atlas_wf : f32 = xx self.atlas_width;
|
||||||
|
atlas_hf : f32 = xx self.atlas_height;
|
||||||
|
|
||||||
|
entry := GlyphEntry.{
|
||||||
|
key = key,
|
||||||
|
glyph = CachedGlyph.{
|
||||||
|
uv_x = xx pack.x / atlas_wf,
|
||||||
|
uv_y = xx pack.y / atlas_hf,
|
||||||
|
uv_w = xx glyph_w / atlas_wf,
|
||||||
|
uv_h = xx glyph_h / atlas_hf,
|
||||||
|
width = xx glyph_w,
|
||||||
|
height = xx glyph_h,
|
||||||
|
offset_x = xx x0,
|
||||||
|
offset_y = xx y0,
|
||||||
|
advance = advance
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.entries.append(entry);
|
||||||
|
return @self.entries.items[self.entries.len - 1].glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload dirty atlas to GPU
|
||||||
|
flush :: (self: *GlyphCache) {
|
||||||
|
if self.dirty == false { return; }
|
||||||
|
glBindTexture(GL_TEXTURE_2D, self.texture_id);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, self.atlas_width, self.atlas_height, GL_RED, GL_UNSIGNED_BYTE, self.bitmap);
|
||||||
|
self.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shelf-based rectangle packer.
|
||||||
|
// Returns PackResult with x >= 0 on success, x = -1 if no space.
|
||||||
|
try_pack :: (self: *GlyphCache, w: s32, h: s32) -> PackResult {
|
||||||
|
padded_w := w + self.padding;
|
||||||
|
padded_h := h + self.padding;
|
||||||
|
|
||||||
|
// Try fitting on the current shelf
|
||||||
|
eff_h := if self.shelf_height > padded_h then self.shelf_height else padded_h;
|
||||||
|
if self.cursor_x + padded_w <= self.atlas_width and self.shelf_y + eff_h <= self.atlas_height {
|
||||||
|
result := PackResult.{ x = self.cursor_x, y = self.shelf_y };
|
||||||
|
self.cursor_x += padded_w;
|
||||||
|
if padded_h > self.shelf_height {
|
||||||
|
self.shelf_height = padded_h;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new shelf
|
||||||
|
new_shelf_y := self.shelf_y + self.shelf_height;
|
||||||
|
if new_shelf_y + padded_h <= self.atlas_height and padded_w <= self.atlas_width {
|
||||||
|
self.shelf_y = new_shelf_y;
|
||||||
|
self.shelf_height = padded_h;
|
||||||
|
self.cursor_x = padded_w;
|
||||||
|
return PackResult.{ x = 0, y = new_shelf_y };
|
||||||
|
}
|
||||||
|
|
||||||
|
// No space
|
||||||
|
PackResult.{ x = 0 - 1, y = 0 - 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow the atlas by doubling dimensions
|
||||||
|
grow :: (self: *GlyphCache) {
|
||||||
|
new_w := self.atlas_width * 2;
|
||||||
|
new_h := self.atlas_height * 2;
|
||||||
|
new_size : s64 = xx new_w * xx new_h;
|
||||||
|
new_bitmap : [*]u8 = xx context.allocator.alloc(new_size);
|
||||||
|
memset(new_bitmap, 0, new_size);
|
||||||
|
|
||||||
|
// Copy old rows into new bitmap
|
||||||
|
y : s32 = 0;
|
||||||
|
while y < self.atlas_height {
|
||||||
|
old_off : s64 = xx y * xx self.atlas_width;
|
||||||
|
new_off : s64 = xx y * xx new_w;
|
||||||
|
memcpy(@new_bitmap[new_off], @self.bitmap[old_off], xx self.atlas_width);
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.allocator.dealloc(self.bitmap);
|
||||||
|
self.bitmap = new_bitmap;
|
||||||
|
self.atlas_width = new_w;
|
||||||
|
self.atlas_height = new_h;
|
||||||
|
|
||||||
|
// Recreate GL texture
|
||||||
|
glDeleteTextures(1, @self.texture_id);
|
||||||
|
glGenTextures(1, @self.texture_id);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, self.texture_id);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, new_w, new_h, 0, GL_RED, GL_UNSIGNED_BYTE, new_bitmap);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
// Recompute UV coordinates for all cached glyphs
|
||||||
|
atlas_wf : f32 = xx new_w;
|
||||||
|
atlas_hf : f32 = xx new_h;
|
||||||
|
i : s64 = 0;
|
||||||
|
while i < self.entries.len {
|
||||||
|
g := @self.entries.items[i].glyph;
|
||||||
|
if g.width > 0.0 {
|
||||||
|
g.uv_x = g.uv_x / 2.0;
|
||||||
|
g.uv_y = g.uv_y / 2.0;
|
||||||
|
g.uv_w = g.width / atlas_wf;
|
||||||
|
g.uv_h = g.height / atlas_hf;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dirty = false;
|
||||||
|
out("GlyphCache atlas grown\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
set_dpi_scale :: (self: *GlyphCache, scale: f32) {
|
||||||
|
self.dpi_scale = scale;
|
||||||
|
self.inv_dpi = 1.0 / scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the scale factor for a logical font size
|
||||||
|
scale_for_size :: (self: *GlyphCache, font_size: f32) -> f32 {
|
||||||
|
stbtt_ScaleForPixelHeight(self.font_info, font_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get scaled ascent for a logical font size
|
||||||
|
get_ascent :: (self: *GlyphCache, font_size: f32) -> f32 {
|
||||||
|
self.ascent * self.scale_for_size(font_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get scaled line height for a logical font size
|
||||||
|
get_line_height :: (self: *GlyphCache, font_size: f32) -> f32 {
|
||||||
|
s := self.scale_for_size(font_size);
|
||||||
|
(self.ascent - self.descent + self.line_gap) * s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape text into positioned glyphs.
|
||||||
|
// Uses ASCII fast-path (stbtt byte-by-byte) for pure ASCII,
|
||||||
|
// full kb_text_shape pipeline for Unicode/complex scripts.
|
||||||
|
// Results stored in self.shaped_buf (reused across calls).
|
||||||
|
shape_text :: (self: *GlyphCache, text: string, font_size: f32) {
|
||||||
|
self.shaped_buf.len = 0;
|
||||||
|
if text.len == 0 { return; }
|
||||||
|
|
||||||
|
if is_ascii(text) {
|
||||||
|
self.shape_ascii(text, font_size);
|
||||||
|
} else {
|
||||||
|
self.shape_with_kb(text, font_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shape_ascii :: (self: *GlyphCache, text: string, font_size: f32) {
|
||||||
|
scale := stbtt_ScaleForPixelHeight(self.font_info, font_size);
|
||||||
|
total : f32 = 0.0;
|
||||||
|
i : s64 = 0;
|
||||||
|
while i < text.len {
|
||||||
|
ch : s32 = xx text[i];
|
||||||
|
glyph_index : u16 = xx stbtt_FindGlyphIndex(self.font_info, ch);
|
||||||
|
|
||||||
|
advance_i : s32 = 0;
|
||||||
|
lsb_i : s32 = 0;
|
||||||
|
stbtt_GetGlyphHMetrics(self.font_info, xx glyph_index, @advance_i, @lsb_i);
|
||||||
|
adv : f32 = xx advance_i * scale;
|
||||||
|
|
||||||
|
self.shaped_buf.append(ShapedGlyph.{
|
||||||
|
glyph_index = glyph_index,
|
||||||
|
x = total,
|
||||||
|
y = 0.0,
|
||||||
|
advance = adv
|
||||||
|
});
|
||||||
|
total += adv;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shape_with_kb :: (self: *GlyphCache, text: string, font_size: f32) {
|
||||||
|
if xx self.shape_ctx == 0 {
|
||||||
|
self.shape_ascii(text, font_size);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scale : f32 = font_size / xx self.units_per_em;
|
||||||
|
total : f32 = 0.0;
|
||||||
|
|
||||||
|
kbts_ShapeBegin(xx self.shape_ctx, KBTS_DIRECTION_DONT_KNOW, KBTS_LANGUAGE_DONT_KNOW);
|
||||||
|
kbts_ShapeUtf8(xx self.shape_ctx, xx text.ptr, xx text.len, KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX);
|
||||||
|
kbts_ShapeEnd(xx self.shape_ctx);
|
||||||
|
|
||||||
|
run : KbtsRun = ---;
|
||||||
|
while kbts_ShapeRun(xx self.shape_ctx, xx @run) != 0 {
|
||||||
|
glyph_ptr : *KbtsGlyph = xx 0;
|
||||||
|
while kbts_GlyphIteratorNext(xx @run.glyphs, xx @glyph_ptr) != 0 {
|
||||||
|
if xx glyph_ptr == 0 { continue; }
|
||||||
|
gx := total + xx glyph_ptr.offset_x * scale;
|
||||||
|
gy : f32 = xx glyph_ptr.offset_y * scale;
|
||||||
|
adv : f32 = xx glyph_ptr.advance_x * scale;
|
||||||
|
|
||||||
|
self.shaped_buf.append(ShapedGlyph.{
|
||||||
|
glyph_index = glyph_ptr.id,
|
||||||
|
x = gx,
|
||||||
|
y = gy,
|
||||||
|
advance = adv
|
||||||
|
});
|
||||||
|
total += adv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure text at a logical font size using text shaping.
|
||||||
|
// Rasterizes at physical resolution (font_size * dpi_scale), returns logical dimensions.
|
||||||
|
measure_text :: (self: *GlyphCache, text: string, font_size: f32) -> Size {
|
||||||
|
self.shape_text(text, font_size);
|
||||||
|
width : f32 = 0.0;
|
||||||
|
i : s64 = 0;
|
||||||
|
while i < self.shaped_buf.len {
|
||||||
|
width += self.shaped_buf.items[i].advance;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
Size.{ width = width, height = self.get_line_height(font_size) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
UIPipeline :: struct {
|
UIPipeline :: struct {
|
||||||
renderer: UIRenderer;
|
renderer: UIRenderer;
|
||||||
render_tree: RenderTree;
|
render_tree: RenderTree;
|
||||||
font: FontAtlas;
|
font: GlyphCache;
|
||||||
screen_width: f32;
|
screen_width: f32;
|
||||||
screen_height: f32;
|
screen_height: f32;
|
||||||
root: ViewChild;
|
root: ViewChild;
|
||||||
@@ -23,9 +23,10 @@ UIPipeline :: struct {
|
|||||||
self.has_root = false;
|
self.has_root = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
init_font :: (self: *UIPipeline, path: [:0]u8, size: f32) {
|
init_font :: (self: *UIPipeline, path: [:0]u8, size: f32, dpi_scale: f32) {
|
||||||
self.font.init(path, size);
|
self.font.init(path, size);
|
||||||
upload_font_texture(@self.font);
|
self.font.set_dpi_scale(dpi_scale);
|
||||||
|
self.renderer.dpi_scale = dpi_scale;
|
||||||
set_global_font(@self.font);
|
set_global_font(@self.font);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,6 @@ UIPipeline :: struct {
|
|||||||
origin = Point.zero(),
|
origin = Point.zero(),
|
||||||
size = root_size
|
size = root_size
|
||||||
};
|
};
|
||||||
print("tick: computed_frame=({},{},{},{})\n", self.root.computed_frame.origin.x, self.root.computed_frame.origin.y, self.root.computed_frame.size.width, self.root.computed_frame.size.height);
|
|
||||||
self.root.view.layout(self.root.computed_frame);
|
self.root.view.layout(self.root.computed_frame);
|
||||||
|
|
||||||
// Render to tree
|
// Render to tree
|
||||||
|
|||||||
106
ui/renderer.sx
106
ui/renderer.sx
@@ -4,6 +4,8 @@
|
|||||||
#import "modules/math";
|
#import "modules/math";
|
||||||
#import "ui/types.sx";
|
#import "ui/types.sx";
|
||||||
#import "ui/render.sx";
|
#import "ui/render.sx";
|
||||||
|
#import "ui/glyph_cache.sx";
|
||||||
|
#import "ui/font.sx";
|
||||||
|
|
||||||
// Vertex: pos(2) + uv(2) + color(4) + params(4) = 12 floats
|
// Vertex: pos(2) + uv(2) + color(4) + params(4) = 12 floats
|
||||||
UI_VERTEX_FLOATS :s64: 12;
|
UI_VERTEX_FLOATS :s64: 12;
|
||||||
@@ -20,6 +22,7 @@ UIRenderer :: struct {
|
|||||||
vertex_count: s64;
|
vertex_count: s64;
|
||||||
screen_width: f32;
|
screen_width: f32;
|
||||||
screen_height: f32;
|
screen_height: f32;
|
||||||
|
dpi_scale: f32;
|
||||||
white_texture: u32;
|
white_texture: u32;
|
||||||
current_texture: u32;
|
current_texture: u32;
|
||||||
|
|
||||||
@@ -61,6 +64,8 @@ UIRenderer :: struct {
|
|||||||
|
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
|
self.dpi_scale = 1.0;
|
||||||
|
|
||||||
// 1x1 white texture for solid rects
|
// 1x1 white texture for solid rects
|
||||||
self.white_texture = create_white_texture();
|
self.white_texture = create_white_texture();
|
||||||
}
|
}
|
||||||
@@ -137,7 +142,7 @@ UIRenderer :: struct {
|
|||||||
self.push_quad(node.frame, node.fill_color, node.corner_radius, node.stroke_width);
|
self.push_quad(node.frame, node.fill_color, node.corner_radius, node.stroke_width);
|
||||||
}
|
}
|
||||||
case .text: {
|
case .text: {
|
||||||
if xx g_font != 0 and g_font.char_data != null {
|
if xx g_font != 0 {
|
||||||
self.render_text(node);
|
self.render_text(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,11 +154,12 @@ UIRenderer :: struct {
|
|||||||
case .clip_push: {
|
case .clip_push: {
|
||||||
self.flush();
|
self.flush();
|
||||||
glEnable(GL_SCISSOR_TEST);
|
glEnable(GL_SCISSOR_TEST);
|
||||||
|
dpi := self.dpi_scale;
|
||||||
glScissor(
|
glScissor(
|
||||||
xx node.frame.origin.x,
|
xx (node.frame.origin.x * dpi),
|
||||||
xx (self.screen_height - node.frame.origin.y - node.frame.size.height),
|
xx ((self.screen_height - node.frame.origin.y - node.frame.size.height) * dpi),
|
||||||
xx node.frame.size.width,
|
xx (node.frame.size.width * dpi),
|
||||||
xx node.frame.size.height
|
xx (node.frame.size.height * dpi)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case .clip_pop: {
|
case .clip_pop: {
|
||||||
@@ -195,8 +201,13 @@ UIRenderer :: struct {
|
|||||||
|
|
||||||
render_text :: (self: *UIRenderer, node: RenderNode) {
|
render_text :: (self: *UIRenderer, node: RenderNode) {
|
||||||
font := g_font;
|
font := g_font;
|
||||||
scale := node.font_size / font.font_size;
|
if xx font == 0 { return; }
|
||||||
|
|
||||||
|
// Shape text into positioned glyphs
|
||||||
|
font.shape_text(node.text, node.font_size);
|
||||||
|
|
||||||
|
// Flush any new glyphs to the atlas texture before rendering
|
||||||
|
font.flush();
|
||||||
self.bind_texture(font.texture_id);
|
self.bind_texture(font.texture_id);
|
||||||
|
|
||||||
r := node.text_color.rf();
|
r := node.text_color.rf();
|
||||||
@@ -204,56 +215,51 @@ UIRenderer :: struct {
|
|||||||
b := node.text_color.bf();
|
b := node.text_color.bf();
|
||||||
a := node.text_color.af();
|
a := node.text_color.af();
|
||||||
|
|
||||||
// stbtt_GetBakedQuad works at baked size; we scale output positions
|
ascent := font.get_ascent(node.font_size);
|
||||||
xpos : f32 = 0.0;
|
raster_size := node.font_size * font.dpi_scale;
|
||||||
ypos : f32 = 0.0;
|
inv_dpi := font.inv_dpi;
|
||||||
q : AlignedQuad = ---;
|
|
||||||
i : s64 = 0;
|
i : s64 = 0;
|
||||||
while i < node.text.len {
|
while i < font.shaped_buf.len {
|
||||||
ch : s32 = xx node.text[i];
|
shaped := font.shaped_buf.items[i];
|
||||||
if ch >= FIRST_CHAR and ch < FIRST_CHAR + NUM_CHARS {
|
cached := font.get_or_rasterize(shaped.glyph_index, raster_size);
|
||||||
stbtt_GetBakedQuad(xx font.char_data, ATLAS_W, ATLAS_H, ch - FIRST_CHAR, @xpos, @ypos, xx @q, 1);
|
|
||||||
|
|
||||||
// Scale and offset to frame position
|
if xx cached != 0 {
|
||||||
// ypos=0 means baseline is at y=0; glyphs go above (negative yoff)
|
if cached.width > 0.0 {
|
||||||
// Add ascent so top of text aligns with frame top
|
// Scale physical pixel dimensions back to logical units
|
||||||
gx0 := node.frame.origin.x + q.x0 * scale;
|
gx0 := node.frame.origin.x + shaped.x + cached.offset_x * inv_dpi;
|
||||||
gy0 := node.frame.origin.y + font.ascent * scale + q.y0 * scale;
|
gy0 := node.frame.origin.y + ascent + shaped.y + cached.offset_y * inv_dpi;
|
||||||
gx1 := node.frame.origin.x + q.x1 * scale;
|
gx1 := gx0 + cached.width * inv_dpi;
|
||||||
gy1 := node.frame.origin.y + font.ascent * scale + q.y1 * scale;
|
gy1 := gy0 + cached.height * inv_dpi;
|
||||||
|
|
||||||
if self.vertex_count + 6 > MAX_UI_VERTICES {
|
u0 := cached.uv_x;
|
||||||
self.flush();
|
v0 := cached.uv_y;
|
||||||
|
u1 := cached.uv_x + cached.uv_w;
|
||||||
|
v1 := cached.uv_y + cached.uv_h;
|
||||||
|
|
||||||
|
if self.vertex_count + 6 > MAX_UI_VERTICES {
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// corner_radius = -1.0 signals "text mode" to the fragment shader
|
||||||
|
neg1 : f32 = 0.0 - 1.0;
|
||||||
|
self.write_vertex(gx0, gy0, u0, v0, r, g, b, a, neg1, 0.0, 0.0, 0.0);
|
||||||
|
self.write_vertex(gx1, gy0, u1, v0, r, g, b, a, neg1, 0.0, 0.0, 0.0);
|
||||||
|
self.write_vertex(gx0, gy1, u0, v1, r, g, b, a, neg1, 0.0, 0.0, 0.0);
|
||||||
|
self.write_vertex(gx1, gy0, u1, v0, r, g, b, a, neg1, 0.0, 0.0, 0.0);
|
||||||
|
self.write_vertex(gx1, gy1, u1, v1, r, g, b, a, neg1, 0.0, 0.0, 0.0);
|
||||||
|
self.write_vertex(gx0, gy1, u0, v1, r, g, b, a, neg1, 0.0, 0.0, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// corner_radius = -1.0 signals "text mode" to the fragment shader
|
|
||||||
self.write_vertex(gx0, gy0, q.s0, q.t0, r, g, b, a, 0.0 - 1.0, 0.0, 0.0, 0.0);
|
|
||||||
self.write_vertex(gx1, gy0, q.s1, q.t0, r, g, b, a, 0.0 - 1.0, 0.0, 0.0, 0.0);
|
|
||||||
self.write_vertex(gx0, gy1, q.s0, q.t1, r, g, b, a, 0.0 - 1.0, 0.0, 0.0, 0.0);
|
|
||||||
self.write_vertex(gx1, gy0, q.s1, q.t0, r, g, b, a, 0.0 - 1.0, 0.0, 0.0, 0.0);
|
|
||||||
self.write_vertex(gx1, gy1, q.s1, q.t1, r, g, b, a, 0.0 - 1.0, 0.0, 0.0, 0.0);
|
|
||||||
self.write_vertex(gx0, gy1, q.s0, q.t1, r, g, b, a, 0.0 - 1.0, 0.0, 0.0, 0.0);
|
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush any glyphs rasterized during this text draw
|
||||||
|
font.flush();
|
||||||
self.bind_texture(self.white_texture);
|
self.bind_texture(self.white_texture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload font atlas bitmap as GL texture (called after GL init)
|
|
||||||
upload_font_texture :: (font: *FontAtlas) {
|
|
||||||
if font.bitmap == null { return; }
|
|
||||||
glGenTextures(1, @font.texture_id);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, font.texture_id);
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_R8, ATLAS_W, ATLAS_H, 0, GL_RED, GL_UNSIGNED_BYTE, font.bitmap);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
|
|
||||||
context.allocator.dealloc(font.bitmap);
|
|
||||||
font.bitmap = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
create_white_texture :: () -> u32 {
|
create_white_texture :: () -> u32 {
|
||||||
tex : u32 = 0;
|
tex : u32 = 0;
|
||||||
glGenTextures(1, @tex);
|
glGenTextures(1, @tex);
|
||||||
@@ -311,8 +317,10 @@ void main() {
|
|||||||
vec2 rectSize = vParams.zw;
|
vec2 rectSize = vParams.zw;
|
||||||
|
|
||||||
if (radius < 0.0) {
|
if (radius < 0.0) {
|
||||||
float textAlpha = texture(uTex, vUV).r;
|
float alpha = texture(uTex, vUV).r;
|
||||||
FragColor = vec4(vColor.rgb, vColor.a * textAlpha);
|
float ew = fwidth(alpha) * 0.7;
|
||||||
|
alpha = smoothstep(0.5 - ew, 0.5 + ew, alpha);
|
||||||
|
FragColor = vec4(vColor.rgb, vColor.a * pow(alpha, 0.9));
|
||||||
} else if (radius > 0.0 || border > 0.0) {
|
} else if (radius > 0.0 || border > 0.0) {
|
||||||
vec4 texColor = texture(uTex, vUV);
|
vec4 texColor = texture(uTex, vUV);
|
||||||
vec2 half_size = rectSize * 0.5;
|
vec2 half_size = rectSize * 0.5;
|
||||||
@@ -381,8 +389,10 @@ void main() {
|
|||||||
vec2 rectSize = vParams.zw;
|
vec2 rectSize = vParams.zw;
|
||||||
|
|
||||||
if (radius < 0.0) {
|
if (radius < 0.0) {
|
||||||
float textAlpha = texture(uTex, vUV).r;
|
float alpha = texture(uTex, vUV).r;
|
||||||
FragColor = vec4(vColor.rgb, vColor.a * textAlpha);
|
float ew = fwidth(alpha) * 0.7;
|
||||||
|
alpha = smoothstep(0.5 - ew, 0.5 + ew, alpha);
|
||||||
|
FragColor = vec4(vColor.rgb, vColor.a * pow(alpha, 0.9));
|
||||||
} else if (radius > 0.0 || border > 0.0) {
|
} else if (radius > 0.0 || border > 0.0) {
|
||||||
vec4 texColor = texture(uTex, vUV);
|
vec4 texColor = texture(uTex, vUV);
|
||||||
vec2 half_size = rectSize * 0.5;
|
vec2 half_size = rectSize * 0.5;
|
||||||
|
|||||||
15
vendors/kb_text_shape/kbts_api.h
vendored
Normal file
15
vendors/kb_text_shape/kbts_api.h
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Minimal API declarations for SX import.
|
||||||
|
// Only the functions/types we actually use — avoids parsing the full 30k-line header.
|
||||||
|
|
||||||
|
typedef struct kbts_shape_context kbts_shape_context;
|
||||||
|
typedef struct kbts_font kbts_font;
|
||||||
|
|
||||||
|
kbts_shape_context *kbts_CreateShapeContext(void *Allocator, void *AllocatorData);
|
||||||
|
void kbts_DestroyShapeContext(kbts_shape_context *Context);
|
||||||
|
kbts_font *kbts_ShapePushFontFromMemory(kbts_shape_context *Context, void *Memory, int Size, int FontIndex);
|
||||||
|
void kbts_GetFontInfo2(kbts_font *Font, void *Info);
|
||||||
|
void kbts_ShapeBegin(kbts_shape_context *Context, unsigned int ParagraphDirection, unsigned int Language);
|
||||||
|
void kbts_ShapeUtf8(kbts_shape_context *Context, const char *Utf8, int Length, unsigned int UserIdGenerationMode);
|
||||||
|
void kbts_ShapeEnd(kbts_shape_context *Context);
|
||||||
|
int kbts_ShapeRun(kbts_shape_context *Context, void *Run);
|
||||||
|
int kbts_GlyphIteratorNext(void *It, void **Glyph);
|
||||||
Reference in New Issue
Block a user