// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The fonts grebe draws with, and lines of text drawn in them: one for //! the commit list and everything else of the interface, and a monospace //! one for diffs. const std = @import("std"); const Allocator = std.mem.Allocator; const aerie = @import("aerie"); const Canvas = aerie.gpu.Canvas; const text = aerie.text; const Shaper = text.Shaper; const Text = @This(); gpa: Allocator, fonts: *text.Fonts, families: [2]text.Family, shaper: text.Shaper, glyphs: text.Glyphs, /// Scratch for a line being drawn. shaped: std.ArrayList(text.Shaper.Glyph) = .empty, /// Pixels the families are drawn at, before scaling. base_px: [2]f32, scale: f32 = 1, pub const Face = enum(u1) { ui, mono }; /// A family, by its fontconfig name, at a size in pixels before scaling. pub const Spec = struct { family: []const u8, px: f32, }; pub fn init(gpa: Allocator, fonts: *text.Fonts, specs: [2]Spec) !Text { var ui: text.Family = try .init(gpa, fonts, specs[0].family, specs[0].px); errdefer ui.deinit(); var mono: text.Family = try .init(gpa, fonts, specs[1].family, specs[1].px); errdefer mono.deinit(); var shaper: text.Shaper = try .init(gpa, .{}); errdefer shaper.deinit(); return .{ .gpa = gpa, .fonts = fonts, .families = .{ ui, mono }, .shaper = shaper, .glyphs = try .init(gpa), .base_px = .{ specs[0].px, specs[1].px }, }; } /// Draws in `specs` from now on: every glyph is rasterized again. On /// failure, keeps the families it had. pub fn setFamilies(t: *Text, specs: [2]Spec) !void { var ui: text.Family = try .init(t.gpa, t.fonts, specs[0].family, specs[0].px * t.scale); errdefer ui.deinit(); var mono: text.Family = try .init(t.gpa, t.fonts, specs[1].family, specs[1].px * t.scale); errdefer mono.deinit(); for (&t.families) |*f| f.deinit(); t.families = .{ ui, mono }; t.base_px = .{ specs[0].px, specs[1].px }; t.glyphs.clear(); } pub fn deinit(t: *Text) void { t.shaped.deinit(t.gpa); t.glyphs.deinit(); t.shaper.deinit(); for (&t.families) |*f| f.deinit(); t.* = undefined; } pub fn family(t: *Text, face: Face) *text.Family { return &t.families[@backingInt(face)]; } /// Draws at `scale` from now on: every glyph is rasterized again. pub fn setScale(t: *Text, scale: f32) !void { t.scale = scale; for (&t.families, t.base_px) |*f, px| try f.setSize(px * scale); t.glyphs.clear(); } /// The height of a line in `face`. pub fn lineHeight(t: *Text, face: Face) f32 { return @floatFromInt(t.family(face).metrics.cell_height); } /// Where the baseline goes for a line of `face` centered in `height`, /// down from its top. pub fn baselineIn(t: *Text, face: Face, height: f32) f32 { const m = t.family(face).metrics; return @round((height - @as(f32, @floatFromInt(m.cell_height))) / 2) + @as(f32, @floatFromInt(m.baseline)); } pub fn measure(t: *Text, face: Face, s: []const u8) !f32 { const f = t.family(face); return t.shaper.measure(f, f.size_px, .regular, s); } /// The byte of `s` nearest `x` pixels from its start, on a code point's /// boundary: where a click in it puts a caret. pub fn offsetAt(t: *Text, face: Face, s: []const u8, x: f32) !usize { const f = t.family(face); t.shaped.clearRetainingCapacity(); const w = try t.shaper.shape(f, f.size_px, .regular, s, &t.shaped); return Shaper.offsetAt(t.shaped.items, w, s.len, x); } /// How far into `s`, in pixels, its byte `offset` is. pub fn xOf(t: *Text, face: Face, s: []const u8, offset: usize) !f32 { const f = t.family(face); t.shaped.clearRetainingCapacity(); const w = try t.shaper.shape(f, f.size_px, .regular, s, &t.shaped); return Shaper.xOf(t.shaped.items, w, offset); } /// Draws `s` with its origin at `x` on the baseline `baseline`. Returns /// its width. pub fn draw(t: *Text, canvas: *Canvas, face: Face, s: []const u8, x: f32, baseline: f32, color: [4]u8) !f32 { const f = t.family(face); t.shaped.clearRetainingCapacity(); const w = try t.shaper.shape(f, f.size_px, .regular, s, &t.shaped); try canvas.text(&t.glyphs, t.shaped.items, f.size_px, x, baseline, color); return w; }