Files
plotter/include/plot/render/text.hpp
T

174 lines
8.8 KiB
C++

#pragma once
#include <cmath>
#include <plot/core/types.hpp>
#include <plot/math/vec2.hpp>
namespace plot::render {
/// One glyph's atlas rectangle and metrics, in font pixels.
///
/// **The rectangle is normalised, v grows downward.** `(u0, v0)` is the glyph's
/// top-left atlas texel and `(u1, v1)` its bottom-right, each divided by the
/// atlas' size -- so the atlas is stored with its top row first, exactly the row
/// order `atlas_pixels()` hands over, and a larger `v` is a row further down.
/// Text is laid out with the same handedness: a glyph's own +y runs from `v0`
/// toward `v1`, which on screen is downward when the glyph is unrotated. A
/// backend that rasterised its atlas bottom row first would draw every glyph
/// mirrored unless it divided `v` the other way, so the convention is stated
/// once, here, and every consumer -- the text shader's UVs, the vertex shader's
/// second cell axis, `TextBatch` -- uses it and nothing else.
///
/// `(xoff, yoff)` is the bearing from the pen to the glyph's top-left corner
/// (top-left origin, font pixels) and `advance` is how far the pen moves along
/// the baseline. Both are in font pixels, so a caller scaling a run multiplies
/// them by the same factor it draws the atlas with.
///
/// **The rect is the atlas cell**, not the ink box: every glyph of a backend
/// occupies one cell of one size, so `u1 - u0` and `v1 - v0` are the same pair for
/// every codepoint -- the pair `TextBackend::cell_uv()` reports. The ink sits
/// inside the cell at the bearing. See `cell_uv()` for why the pipeline needs
/// that, and `glyph_is_cell()` for the check that enforces it.
struct Glyph {
f32 u0 = 0.0f;
f32 v0 = 0.0f;
f32 u1 = 0.0f;
f32 v1 = 0.0f;
f32 xoff = 0.0f;
f32 yoff = 0.0f;
f32 advance = 0.0f;
};
/// A byte sequence that is not valid UTF-8, as reported by `next_codepoint`. It
/// is deliberately not a Unicode scalar value, so it can only map to whatever a
/// backend draws for "no glyph" rather than colliding with a real codepoint.
inline constexpr u32 kInvalidCodepoint = 0xFFFFFFFFu;
/// Tolerance for comparing two normalised atlas sizes. A backend derives a rect
/// size as `u1 - u0`, a subtraction of two rounded fractions, so a *correct* cell
/// comes back a few 1e-8 out; the smallest rect that is genuinely a different
/// size differs by one atlas texel, which is 1e-4 or coarser on any atlas a GPU
/// will hold. 1e-6 separates "the same cell, rounded twice" from "a different
/// rect" with orders of magnitude to spare on both sides.
inline constexpr f32 kAtlasCellTolerance = 1e-6f;
/// True when `glyph`'s rect is the atlas cell `cell_uv` -- the invariant an atlas
/// is required to keep (see `TextBackend::cell_uv`).
///
/// It is a function rather than an assertion inside one caller because it is the
/// check the staging path asserts for every glyph it stages, and a test has to be
/// able to drive it over a backend that breaks it without aborting the suite.
inline bool glyph_is_cell(const Glyph& glyph, Vec2 cell_uv) noexcept {
const auto near = [](f32 a, f32 b) { return std::fabs(a - b) <= kAtlasCellTolerance; };
return near(glyph.u1 - glyph.u0, cell_uv.x) && near(glyph.v1 - glyph.v0, cell_uv.y);
}
/// Decodes one UTF-8 sequence from `p` and advances `p` past it -- one byte when
/// the sequence is malformed, so a damaged label still terminates.
///
/// It belongs to this interface rather than to a font because it is what keeps a
/// backend's `measure()` and the glyphs the pipeline stages for the same string
/// in step: both walk the string with this decoder, so neither can disagree with
/// the other about where a codepoint starts.
u32 next_codepoint(const char*& p) noexcept;
/// The font a `Plot` measures with and the renderer draws with: metrics, plus the
/// coverage atlas the text pipeline samples.
///
/// Text is passed as UTF-8, not as a byte-per-character string: `plot::format_si`
/// emits U+00B5, U+00B0 and U+03A9 for micro, degree and ohm, and an
/// implementation that ignores that prints mojibake.
///
/// The atlas is the backend's own: it owns the pixels, the glyph table that
/// describes them and the `revision()` that says when they changed. The renderer
/// uploads whatever the backend exposes and re-uploads when the revision moves,
/// so a backend can be swapped -- or re-baked at another size -- without the
/// renderer knowing anything about fonts. `measure()`/`height()`/`ascent()` are
/// in the atlas' font pixels: the pipeline scales them, never re-rasterises.
///
/// **One atlas, one cell size**: the atlas is a *fixed-cell grid* and
/// `cell_uv()` is where that size is declared -- see it for the full contract
/// and for what a proportional face has to do to satisfy it. The staging path
/// asserts `glyph_is_cell()` for every glyph it stages in a debug build.
class TextBackend {
public:
TextBackend() noexcept = default;
TextBackend(const TextBackend&) = default;
TextBackend& operator=(const TextBackend&) = default;
virtual ~TextBackend() = default;
/// Width of `utf8` in the same units as `height()` (font pixels for a font
/// atlas). Zero for the empty string.
virtual f32 measure(const char* utf8) const = 0;
/// Line height: the cell a single line of text occupies.
virtual f32 height() const = 0;
/// Distance from the top of the cell to the baseline, so a caller can align
/// text bottoms instead of cell bottoms.
virtual f32 ascent() const = 0;
/// The face's em size in font pixels: the size the atlas was baked at, in the
/// same units as `height()` and `ascent()`.
///
/// It exists so a run can be drawn at the size a caller asked for whatever the
/// backend was baked at: the renderer scales by `requested_px / em_px()`, so a
/// backend re-baked at another size -- or swapped for a different face -- keeps
/// drawing `Style::axis_font_px`, and the renderer never has to know which font
/// it holds. A backend with no meaningful em (one whose "font pixels" are an
/// arbitrary unit) returns something proportional, `height()` being the
/// natural choice.
virtual f32 em_px() const = 0;
/// The glyph for `codepoint`, or null when this backend has none -- the caller
/// then draws nothing for it (a backend that wants a visible missing-glyph box
/// publishes one as the glyph for the codepoints it cannot render). The
/// returned pointer stays valid for the backend's lifetime, so callers may hold
/// it across a frame.
///
/// Its rect is the atlas **cell**, not the ink box: see `cell_uv()`.
virtual const Glyph* glyph(u32 codepoint) const = 0;
/// The atlas size of one cell, as a normalised rect size: `(u1 - u0, v1 - v0)`
/// of every glyph the atlas holds -- the size `glyph()` reports each of them
/// with. `(0, 0)` is legal and means "no text".
///
/// **An atlas is a fixed-cell grid, and this is where that is declared.** Each
/// glyph is drawn in a cell of this one size, because the pipeline stages a
/// glyph's rect *origin* per instance and carries the rect's *size* once for
/// the whole draw: six floats per glyph instead of eight, no per-glyph size in
/// the vertex shader, and rotation for two of those floats. The staging path
/// asserts `glyph_is_cell()` -- every staged glyph's rect against this value --
/// in a debug build, so a backend that breaks the contract fails loudly rather
/// than drawing every glyph at whichever size it happened to be told.
///
/// **For implementers:** a proportional face therefore draws its glyphs *into*
/// a uniform cell -- the ink placed inside it at `xoff`/`yoff`, the pen
/// advanced by `advance` -- rather than trimming each atlas rect to its ink,
/// and spends the atlas area instead of the correctness. The cell has to be
/// wide enough for the widest advance and tall enough for the tallest ink, and
/// this must agree with the rects `glyph()` reports; a backend whose rects are
/// ink boxes (a naive TrueType packing, which trims each bitmap to its ink)
/// violates it.
virtual Vec2 cell_uv() const = 0;
/// 8-bit single-channel coverage, row-major, `atlas_width() * atlas_height()`
/// bytes, top row first. Uploaded as GL_R8. Null is legal and means "no text".
/// The bytes are a grid of `cell_uv()`-sized cells.
virtual const u8* atlas_pixels() const = 0;
/// Atlas size in texels. Zero width or height means "no text", like a null
/// `atlas_pixels()`.
virtual u32 atlas_width() const = 0;
virtual u32 atlas_height() const = 0;
/// Bumped whenever `atlas_pixels()` content changes, so the renderer knows to
/// re-upload instead of trusting its cache. Must not change otherwise -- a
/// revision that moves on its own would re-upload the same bytes every frame --
/// and any change counts, so a backend may re-bake at a new size and keep
/// counting up.
virtual u32 revision() const = 0;
};
} // namespace plot::render