Custom draw

tuneup
This commit is contained in:
Zachary Levy
2026-04-19 21:11:26 -07:00
parent 0953462b3b
commit 90fba74243
5 changed files with 180 additions and 41 deletions

View File

@@ -1,7 +1,6 @@
package draw
import "core:c"
import "core:hash"
import "core:log"
import "core:strings"
import sdl "vendor:sdl3"
@@ -14,11 +13,21 @@ Font_Key :: struct {
size: u16,
}
Cache_Source :: enum u8 {
Custom,
Clay,
}
Cache_Key :: struct {
id: u32,
source: Cache_Source,
}
Text_Cache :: struct {
engine: ^sdl_ttf.TextEngine,
font_bytes: [dynamic][]u8,
sdl_fonts: map[Font_Key]^sdl_ttf.Font,
cache: map[u32]^sdl_ttf.Text,
cache: map[Cache_Key]^sdl_ttf.Text,
}
// Internal for fetching SDL TTF font pointer for rendering
@@ -75,27 +84,20 @@ Text :: struct {
}
// ---------------------------------------------------------------------------------------------------------------------
// ----- Text cache hashing ------------
// ----- Text cache lookup -------------
// ---------------------------------------------------------------------------------------------------------------------
// Hash a string to a u32 cache key using the same Jenkins one-at-a-time algorithm as Clay.
// This means Clay element IDs and user-chosen string IDs share the same keyspace — same
// string produces the same cache key regardless of whether it came from Clay or user code.
text_cache_hash :: #force_inline proc(text_string: string) -> u32 {
return hash.jenkins(transmute([]u8)text_string) + 1 // +1 reserves 0 as "no entry" (matches Clay convention)
}
// Shared cache lookup/create/update logic used by both the `text` proc and the Clay render path.
// Returns the cached (or newly created) TTF_Text pointer.
@(private)
cache_get_or_update :: proc(cache_id: u32, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text {
existing, found := GLOB.text_cache.cache[cache_id]
cache_get_or_update :: proc(key: Cache_Key, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text {
existing, found := GLOB.text_cache.cache[key]
if !found {
sdl_text := sdl_ttf.CreateText(GLOB.text_cache.engine, font, c_str, 0)
if sdl_text == nil {
log.panicf("Failed to create SDL text: %s", sdl.GetError())
}
GLOB.text_cache.cache[cache_id] = sdl_text
GLOB.text_cache.cache[key] = sdl_text
return sdl_text
} else {
if !sdl_ttf.SetTextString(existing, c_str, 0) {
@@ -114,11 +116,12 @@ cache_get_or_update :: proc(cache_id: u32, c_str: cstring, font: ^sdl_ttf.Font)
// When `id` is nil (the default), the text is created and destroyed each frame — simple and
// leak-free, appropriate for HUDs and moderate UI (up to ~50 text elements per frame).
//
// When `id` is set, the TTF_Text object is cached across frames using a hash of the provided
// string (same algorithm as Clay's element IDs). This avoids per-frame HarfBuzz shaping and
// allocation, which matters for text-heavy apps (editors, terminals, chat). The user is
// responsible for choosing unique ID strings per logical text element and calling
// `clear_text_cache` or `clear_text_cache_entry` when cached entries are no longer needed.
// When `id` is set, the TTF_Text object is cached across frames keyed by the provided u32.
// This avoids per-frame HarfBuzz shaping and allocation, which matters for text-heavy apps
// (editors, terminals, chat). The user is responsible for choosing unique IDs per logical text
// element and calling `clear_text_cache` or `clear_text_cache_entry` when cached entries are
// no longer needed. Custom text IDs occupy a separate namespace from Clay text IDs, so
// collisions between the two are impossible.
//
// `origin` is in pixels from the text block's top-left corner (raylib convention).
// The point whose local coords equal `origin` lands at `pos` in world space.
@@ -132,7 +135,7 @@ text :: proc(
color: Color = BLACK,
origin: [2]f32 = {0, 0},
rotation: f32 = 0,
id: Maybe(string) = nil,
id: Maybe(u32) = nil,
temp_allocator := context.temp_allocator,
) {
c_str := strings.clone_to_cstring(text_string, temp_allocator)
@@ -140,9 +143,9 @@ text :: proc(
sdl_text: ^sdl_ttf.Text
cached := false
if id_string, ok := id.?; ok {
if cache_id, ok := id.?; ok {
cached = true
sdl_text = cache_get_or_update(text_cache_hash(id_string), c_str, get_font(font_id, font_size))
sdl_text = cache_get_or_update(Cache_Key{cache_id, .Custom}, c_str, get_font(font_id, font_size))
} else {
sdl_text = sdl_ttf.CreateText(GLOB.text_cache.engine, get_font(font_id, font_size), c_str, 0)
if sdl_text == nil {
@@ -236,8 +239,8 @@ bottom_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u
// ----- Cache management --------------
// ---------------------------------------------------------------------------------------------------------------------
// Destroy all cached text objects. Call on scene transitions, view changes, or periodically
// in apps that produce many distinct cached text entries over time.
// Destroy all cached text objects (both custom and Clay entries). Call on scene transitions,
// view changes, or periodically in apps that produce many distinct cached text entries over time.
// After calling this, subsequent text draws with an `id` will re-create their cache entries.
clear_text_cache :: proc() {
for _, sdl_text in GLOB.text_cache.cache {
@@ -246,12 +249,12 @@ clear_text_cache :: proc() {
clear(&GLOB.text_cache.cache)
}
// Destroy a specific cached text entry by its string id (same string used when drawing).
// Uses the same hash as Clay's element IDs, so this also works for clearing Clay text entries
// by passing the same string used in clay.ID("...").
// Destroy a specific cached custom text entry by its u32 id (the same value passed to the
// `text` proc's `id` parameter). This only affects custom text entries — Clay text entries
// are managed internally and are not addressable by the user.
// No-op if the id is not in the cache.
clear_text_cache_entry :: proc(id: string) {
key := text_cache_hash(id)
clear_text_cache_entry :: proc(id: u32) {
key := Cache_Key{id, .Custom}
sdl_text, ok := GLOB.text_cache.cache[key]
if ok {
sdl_ttf.DestroyText(sdl_text)
@@ -287,7 +290,7 @@ init_text_cache :: proc(
text_cache = Text_Cache {
engine = engine,
cache = make(map[u32]^sdl_ttf.Text, allocator = allocator),
cache = make(map[Cache_Key]^sdl_ttf.Text, allocator = allocator),
}
log.debug("Done initializing text cache")