feat(text): optional vendored stb_truetype backend for runtime external fonts

This commit is contained in:
Martino Ferrari committed 2026-09-28 16:07:27 +02:00
1 parent daff60e3f0
commit 14d664a246
12 files changed
+6511 -19

No files matched your search

+30 -10
View File
@@ -113,7 +113,8 @@ f32 walk_text_clipped(Vec2 pen, const char* utf8, f32 budget_px, const render::T
void warn_replaced_text_backend(const render::TextBackend* previous) noexcept {
std::fprintf(stderr,
"plot::Renderer::draw: replacing the Plot's TextBackend (%p) with the renderer's "
"own embedded font; a plot drawn by a Renderer measures the labels it draws.\n",
"own font; a plot drawn by a Renderer measures the labels it draws -- install the "
"face with Renderer::set_text_backend() instead.\n",
static_cast<const void*>(previous));
}
#endif
@@ -185,6 +186,23 @@ u32 Renderer::AxisFont::revision() const { return font_ != nullptr ? font_->revi
// ------------------------------------------------------------------ init
void Renderer::set_text_backend(const render::TextBackend* font) noexcept {
const render::TextBackend* next = font != nullptr ? font : &default_font_;
if (next == font_) {
return; // the same face: no upload, and nothing changed
}
// A replacement is a different *source* of atlas bytes, which `revision()`
// cannot express on its own: two faces may legitimately number their first bake
// the same. So the cache is dropped rather than trusted. Assigning a fresh
// atlas releases the texture the last frame sampled and forgets the revision
// that was uploaded, which makes the next `begin_frame_text()` upload whatever
// the new backend exposes, at whatever size it says. That is one upload on the
// frame a host deliberately swapped the font -- and it is the direction that
// cannot draw the old face's glyphs under the new face's rects.
atlas_ = render::TextAtlas{};
font_ = next;
}
bool Renderer::init(Status* st) noexcept {
if (!gl::available()) {
return fail(st, ErrorCode::Unsupported,
@@ -202,7 +220,7 @@ bool Renderer::init(Status* st) noexcept {
const render::UploadMode mode =
persistent ? render::UploadMode::PersistentMapped : render::UploadMode::Orphan;
font_backend_.reset(default_font_, style_.device(style_.axis_font_px));
font_backend_.reset(*font_, style_.device(style_.axis_font_px));
// The atlas is the backend's: `sync()` uploads whatever it exposes, at whatever
// size it says, and restores the host's unpack alignment around the upload
@@ -292,7 +310,7 @@ bool Renderer::begin_frame_text() noexcept {
// The plot reserves its label strips from TextBackend metrics, so it must
// measure with the size the text pipeline draws with, and re-read the style
// each frame so a changed axis_font_px takes effect on the next one.
font_backend_.reset(default_font_, style_.device(style_.axis_font_px));
font_backend_.reset(*font_, style_.device(style_.axis_font_px));
// The cell every glyph staged this frame is drawn in, declared from the backend
// rather than left to whichever glyph is staged first: the instances carry rect
// origins, and the draw carries this size (see TextBackend::cell_uv).
@@ -310,10 +328,12 @@ bool Renderer::begin_frame_text() noexcept {
void Renderer::prepare_plot(Plot& plot) {
#ifndef NDEBUG
// The renderer owns the text of the frames it draws: every label goes through
// the text pipeline in `default_font_`, so the plot is handed this backend's metrics
// whatever a host installed. A host backend is therefore replaced on the first
// draw -- said out loud in a debug build, since its disappearance is otherwise
// silent -- and a Plot nobody draws keeps it (see Plot::set_text_backend()).
// the text pipeline in `font_` -- the embedded face, or whatever a host put
// there with Renderer::set_text_backend() -- so the plot is handed this
// backend's metrics whatever it was given. A backend installed on the *Plot* is
// therefore replaced on the first draw -- said out loud in a debug build, since
// its disappearance is otherwise silent -- and a Plot nobody draws keeps it (see
// Plot::set_text_backend()).
if (const render::TextBackend* previous = plot.text_backend();
previous != nullptr && previous != &font_backend_) {
warn_replaced_text_backend(previous);
@@ -573,17 +593,17 @@ void Renderer::stage_xy(const f32* xy, u32 n, bool log_x, bool log_y) {
Vec2 Renderer::stage_text(Vec2 pos_px, const char* utf8) {
// Text is staged in viewport pixels: the text program has no offset uniform.
return glyphs_.add_text(utf8, to_viewport(pos_px), font_backend_.scale(), default_font_);
return glyphs_.add_text(utf8, to_viewport(pos_px), font_backend_.scale(), *font_);
}
f32 Renderer::clipped_text_width(const char* utf8, f32 budget_px, Vec2 axis) const {
return walk_text_clipped(Vec2{}, utf8, budget_px, default_font_, font_backend_.scale(), axis,
return walk_text_clipped(Vec2{}, utf8, budget_px, *font_, font_backend_.scale(), axis,
[](Vec2, const render::Glyph&) {});
}
f32 Renderer::stage_text_clipped(Vec2 pos_px, const char* utf8, f32 budget_px, Vec2 axis) {
const f32 scale = font_backend_.scale();
return walk_text_clipped(to_viewport(pos_px), utf8, budget_px, default_font_, scale, axis,
return walk_text_clipped(to_viewport(pos_px), utf8, budget_px, *font_, scale, axis,
[this, scale, axis](Vec2 pen, const render::Glyph& glyph) {
glyphs_.add_glyph(pen, glyph, scale, axis);
});