// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Positioned glyphs: what a shaper produces and what the renderer draws. //! //! The renderer never looks at text. Turning text into glyphs — choosing //! fonts, applying `GSUB` and `GPOS`, reordering, bidi — is a shaper's job, //! and what it hands over is a `Run`: one face at one size, and the glyphs //! to draw from it with where each one goes. const std = @import("std"); const Allocator = std.mem.Allocator; const Face = @import("Face.zig"); /// One glyph and where its origin goes, in pixels relative to the run's /// origin, y down. Fractional positions are kept: the renderer places /// glyphs to a quarter of a pixel. pub const Glyph = struct { id: u16, x: f32, y: f32, }; /// Glyphs from one face at one size. pub const Run = struct { face: *const Face, /// Pixels per em. size: f32, glyphs: []const Glyph, }; /// How a shaper positions one glyph, in font units, y up: the offset of the /// glyph from the pen, and how far the pen then moves. These are HarfBuzz's /// `hb_glyph_position_t`, at a font scale of the face's units per em. pub const Position = struct { x_advance: f32 = 0, y_advance: f32 = 0, x_offset: f32 = 0, y_offset: f32 = 0, }; /// Glyphs placed from a shaper's advances and offsets, by walking a pen /// along them from the origin. Also gives where the pen ended up, which is /// where a following run starts. The glyphs are owned by the caller. pub const Placed = struct { glyphs: []Glyph, /// The pen's position after the last glyph, in pixels, y down. end_x: f32, end_y: f32, pub fn deinit(p: *Placed, gpa: Allocator) void { gpa.free(p.glyphs); p.* = undefined; } }; pub fn place( gpa: Allocator, face: *const Face, size: f32, ids: []const u16, positions: []const Position, ) Allocator.Error!Placed { std.debug.assert(ids.len == positions.len); const s = face.scale(size); const glyphs = try gpa.alloc(Glyph, ids.len); var pen_x: f32 = 0; var pen_y: f32 = 0; for (glyphs, ids, positions) |*g, id, p| { g.* = .{ .id = id, .x = (pen_x + p.x_offset) * s, .y = -(pen_y + p.y_offset) * s }; pen_x += p.x_advance; pen_y += p.y_advance; } return .{ .glyphs = glyphs, .end_x = pen_x * s, .end_y = -pen_y * s }; } test "place walks the pen and flips y" { var f: @import("font").Font = undefined; f.fvar = null; f.head.units_per_em = 1000; var face = try Face.init(std.testing.allocator, &f, .{}); defer face.deinit(std.testing.allocator); var p = try place(std.testing.allocator, &face, 10, &.{ 3, 4, 5 }, &.{ .{ .x_advance = 500 }, .{ .x_advance = 600, .y_offset = 100 }, .{ .x_advance = 0, .x_offset = -250, .y_advance = 50 }, }); defer p.deinit(std.testing.allocator); try std.testing.expectEqualSlices(Glyph, &.{ .{ .id = 3, .x = 0, .y = 0 }, .{ .id = 4, .x = 5, .y = -1 }, .{ .id = 5, .x = 8.5, .y = 0 }, }, p.glyphs); try std.testing.expectEqual(@as(f32, 11), p.end_x); try std.testing.expectEqual(@as(f32, -0.5), p.end_y); }