// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Text to glyphs across a fallback list, without shaping: a stand-in until //! there is a shaper. //! //! The text is split wherever the face that draws it changes, as an //! itemizer would split it, and each piece is laid out by //! `font_renderer.simple_layout` after the one before. The same caveats //! apply: no `GSUB`, no `GPOS`, no right-to-left. const std = @import("std"); const Allocator = std.mem.Allocator; const renderer = @import("font_renderer"); const Face = renderer.Face; const FontList = @import("FontList.zig"); const Utf8Iterator = renderer.simple_layout.Utf8Iterator; pub const Layout = struct { /// One per stretch of text drawn from one face, in order. Every run's /// glyphs are placed relative to the same origin, so all of them are /// drawn at the one position. runs: []renderer.Run, /// The pen's position after the last glyph. end_x: f32, pub fn deinit(l: *Layout, gpa: Allocator) void { for (l.runs) |r| gpa.free(r.glyphs); gpa.free(l.runs); l.* = undefined; } }; pub const Error = Allocator.Error || error{ /// No font in the list can be loaded. NoFont, }; /// Lay `text` out at `size` pixels per em. Each character is drawn from /// the first face in `list` that has it, or from the primary face, as /// `.notdef`, if none does. pub fn layout(gpa: Allocator, list: *FontList, size: f32, text: []const u8) Error!Layout { const fallback = try list.primary() orelse return error.NoFont; var runs: std.ArrayList(renderer.Run) = .empty; errdefer { for (runs.items) |r| gpa.free(r.glyphs); runs.deinit(gpa); } var pen: f32 = 0; var start: usize = 0; var current: ?*Face = null; var it: Utf8Iterator = .{ .bytes = text }; while (true) { const at = it.i; const cp = it.next(); const f: ?*Face = if (cp) |c| (try list.faceFor(c)) orelse fallback else null; if (current) |cur| if (f != cur) { var placed = try renderer.simple_layout.layout(gpa, cur, size, text[start..at]); for (placed.glyphs) |*g| g.x += pen; pen += placed.end_x; runs.append(gpa, .{ .face = cur, .size = size, .glyphs = placed.glyphs }) catch |err| { placed.deinit(gpa); return err; }; start = at; }; current = f; if (cp == null) break; } return .{ .runs = try runs.toOwnedSlice(gpa), .end_x = pen }; }