lib/gui/src/paint/text.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const alloc_phase = @import("alloc_phase");
   3 const filigree = @import("filigree");
   4 
   5 const command = @import("command.zig");
   6 const gui = @import("../root.zig");
   7 const cpu = @import("cpu/root.zig");
   8 const fallback_mod = @import("fallback.zig");
   9 const synth = @import("synth.zig");
  10 
  11 const Allocator = std.mem.Allocator;
  12 const Color = gui.model.UiColor;
  13 const Image = command.Image;
  14 const Region = cpu.Region;
  15 const Size = gui.layout.Size;
  16 const UiFrame = gui.model.UiFrame;
  17 const UiNode = gui.model.UiNode;
  18 const UiText = gui.model.UiText;
  19 const UiTextRun = gui.model.UiTextRun;
  20 const UiTextStyle = gui.model.UiTextStyle;
  21 const UiTextSelection = gui.model.UiTextSelection;
  22 const WidgetFrame = gui.model.WidgetFrame;
  23 const Rect = gui.layout.Rect;
  24 
  25 pub const OwnedAtlasImage = struct {
  26     image: Image,
  27     pixels: []u32,
  28 
  29     pub fn deinit(self: *OwnedAtlasImage, allocator: Allocator) void {
  30         allocator.free(self.pixels);
  31         self.* = undefined;
  32     }
  33 };
  34 
  35 pub const BitmapGlyph = struct {
  36     codepoint: u21,
  37     rows: []const u8,
  38 };
  39 
  40 pub const BitmapMetrics = struct {
  41     width: u16,
  42     height: u16,
  43     stride: u16,
  44 };
  45 
  46 pub const AtlasScratch = struct {
  47     capacity: Capacity,
  48     bytes: []align(storage_alignment) u8,
  49     fixed: std.heap.FixedBufferAllocator,
  50     active: bool = false,
  51     epoch_peak_bytes: usize = 0,
  52     last_epoch_peak_bytes: usize = 0,
  53     high_water_bytes: usize = 0,
  54     epochs: usize = 0,
  55     exhaustions: usize = 0,
  56 
  57     pub const storage_alignment: usize = 64;
  58 
  59     pub const Limits = struct {
  60         bytes: usize,
  61     };
  62 
  63     pub const Capacity = struct {
  64         bytes: usize,
  65         total_bytes: usize,
  66 
  67         pub fn derive(limits: Limits) Capacity {
  68             return .{
  69                 .bytes = limits.bytes,
  70                 .total_bytes = limits.bytes,
  71             };
  72         }
  73     };
  74 
  75     pub const Status = struct {
  76         capacity: Capacity,
  77         last_epoch_peak_bytes: usize,
  78         high_water_bytes: usize,
  79         epochs: usize,
  80         exhaustions: usize,
  81     };
  82 
  83     pub fn init(allocator: Allocator, limits: Limits) Allocator.Error!AtlasScratch {
  84         const capacity = Capacity.derive(limits);
  85         const bytes = try allocator.alignedAlloc(u8, .fromByteUnits(storage_alignment), capacity.bytes);
  86         return .{
  87             .capacity = capacity,
  88             .bytes = bytes,
  89             .fixed = std.heap.FixedBufferAllocator.init(bytes),
  90         };
  91     }
  92 
  93     pub fn deinit(self: *AtlasScratch, allocator: Allocator) void {
  94         std.debug.assert(!self.active);
  95         allocator.free(self.bytes);
  96         self.* = undefined;
  97     }
  98 
  99     pub fn begin(self: *AtlasScratch) Allocator {
 100         std.debug.assert(!self.active);
 101         self.fixed = std.heap.FixedBufferAllocator.init(self.bytes);
 102         self.epoch_peak_bytes = 0;
 103         self.active = true;
 104         return .{
 105             .ptr = self,
 106             .vtable = &.{
 107                 .alloc = alloc,
 108                 .resize = resize,
 109                 .remap = remap,
 110                 .free = free,
 111             },
 112         };
 113     }
 114 
 115     pub fn end(self: *AtlasScratch) void {
 116         std.debug.assert(self.active);
 117         self.last_epoch_peak_bytes = self.epoch_peak_bytes;
 118         self.high_water_bytes = @max(self.high_water_bytes, self.epoch_peak_bytes);
 119         self.fixed = std.heap.FixedBufferAllocator.init(self.bytes);
 120         self.active = false;
 121         self.epochs +|= 1;
 122     }
 123 
 124     pub fn status(self: *const AtlasScratch) Status {
 125         std.debug.assert(!self.active);
 126         return .{
 127             .capacity = self.capacity,
 128             .last_epoch_peak_bytes = self.last_epoch_peak_bytes,
 129             .high_water_bytes = self.high_water_bytes,
 130             .epochs = self.epochs,
 131             .exhaustions = self.exhaustions,
 132         };
 133     }
 134 
 135     fn observe(self: *AtlasScratch) void {
 136         self.epoch_peak_bytes = @max(self.epoch_peak_bytes, self.fixed.end_index);
 137     }
 138 
 139     fn alloc(context: *anyopaque, len: usize, alignment: std.mem.Alignment, return_address: usize) ?[*]u8 {
 140         const self: *AtlasScratch = @ptrCast(@alignCast(context));
 141         const result = std.heap.FixedBufferAllocator.alloc(&self.fixed, len, alignment, return_address) orelse {
 142             self.exhaustions +|= 1;
 143             return null;
 144         };
 145         self.observe();
 146         return result;
 147     }
 148 
 149     fn resize(context: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, return_address: usize) bool {
 150         const self: *AtlasScratch = @ptrCast(@alignCast(context));
 151         const resized = std.heap.FixedBufferAllocator.resize(&self.fixed, memory, alignment, new_len, return_address);
 152         if (resized) self.observe();
 153         return resized;
 154     }
 155 
 156     fn remap(context: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, return_address: usize) ?[*]u8 {
 157         const self: *AtlasScratch = @ptrCast(@alignCast(context));
 158         const result = std.heap.FixedBufferAllocator.remap(&self.fixed, memory, alignment, new_len, return_address);
 159         if (result != null) self.observe();
 160         return result;
 161     }
 162 
 163     fn free(context: *anyopaque, memory: []u8, alignment: std.mem.Alignment, return_address: usize) void {
 164         const self: *AtlasScratch = @ptrCast(@alignCast(context));
 165         std.heap.FixedBufferAllocator.free(&self.fixed, memory, alignment, return_address);
 166     }
 167 };
 168 
 169 pub const Atlas = struct {
 170     allocator: Allocator,
 171     pixel_size: i32,
 172     atlas: filigree.GlyphAtlas,
 173     image: OwnedAtlasImage,
 174     glyph_index: std.AutoHashMapUnmanaged(u32, usize) = .empty,
 175     cache: AtlasCacheStorage,
 176     line_break_workspace: filigree.LineBreakWorkspace = .{},
 177     composite_workspace: CompositeWorkspace = .{},
 178     backend: Backend,
 179 
 180     pub const Backend = union(enum) {
 181         outline: Outline,
 182         bitmap: Bitmap,
 183     };
 184 
 185     pub const Outline = struct {
 186         font_bytes: []u8,
 187         font: filigree.Font,
 188         context: filigree.Context,
 189         output: filigree.Output,
 190     };
 191 
 192     pub const Bitmap = struct {
 193         advance: i32,
 194         height: i32,
 195         map: std.AutoHashMapUnmanaged(u21, u32) = .empty,
 196         shaped: std.ArrayListUnmanaged(filigree.ShapedGlyph) = .empty,
 197         clusters: std.ArrayListUnmanaged(filigree.Cluster) = .empty,
 198     };
 199 
 200     pub const CaretLine = struct {
 201         content: []const u8,
 202         run: filigree.GlyphRun,
 203 
 204         pub inline fn advanceForByteOffset(self: CaretLine, byte_offset: usize) f32 {
 205             return @call(.always_inline, filigree.caret.advanceForByteOffset, .{
 206                 self.run,
 207                 byte_offset,
 208                 self.content.len,
 209             });
 210         }
 211 
 212         pub fn hitTestAdvance(self: CaretLine, advance: f32) filigree.caret.LineHit {
 213             return filigree.caret.hitTestAdvance(self.run, advance, self.content);
 214         }
 215     };
 216 
 217     pub fn initFromOwnedBytes(
 218         allocator: Allocator,
 219         scratch: *AtlasScratch,
 220         bytes: []u8,
 221         pixel_size: i32,
 222         cache_limits: AtlasCacheStorage.Limits,
 223         output_limits: filigree.Output.Limits,
 224     ) !Atlas {
 225         return initOutlineFromOwnedBytes(allocator, scratch, bytes, pixel_size, cache_limits, output_limits);
 226     }
 227 
 228     fn initOutlineFromOwnedBytes(
 229         allocator: Allocator,
 230         scratch: *AtlasScratch,
 231         bytes: []u8,
 232         pixel_size: i32,
 233         cache_limits: AtlasCacheStorage.Limits,
 234         output_limits: filigree.Output.Limits,
 235     ) !Atlas {
 236         errdefer allocator.free(bytes);
 237         const scratch_allocator = scratch.begin();
 238         defer scratch.end();
 239         var font = filigree.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.InvalidFont;
 240         errdefer font.deinit();
 241         font.setScale(@floatFromInt(pixel_size), 72);
 242 
 243         const glyph_count: u32 = font.face.num_glyphs;
 244         const first_glyph_id: u32 = 1;
 245         if (glyph_count <= first_glyph_id) return error.FontHasNoPrintableAscii;
 246         const raster_glyph_count = glyph_count - first_glyph_id;
 247         var glyph_ids: std.ArrayListUnmanaged(i32) = .empty;
 248         defer glyph_ids.deinit(scratch_allocator);
 249         var codepoints: std.ArrayListUnmanaged(i32) = .empty;
 250         defer codepoints.deinit(scratch_allocator);
 251         try glyph_ids.ensureTotalCapacity(scratch_allocator, raster_glyph_count);
 252         try codepoints.ensureTotalCapacity(scratch_allocator, raster_glyph_count);
 253         var glyph_id: u32 = first_glyph_id;
 254         while (glyph_id < glyph_count) : (glyph_id += 1) {
 255             glyph_ids.appendAssumeCapacity(std.math.cast(i32, glyph_id) orelse return error.GlyphIdTooLarge);
 256             codepoints.appendAssumeCapacity(0);
 257         }
 258 
 259         var atlas = try filigree.glyphAtlasAlloc(allocator, scratch_allocator, bytes, pixel_size, glyph_ids.items, codepoints.items, 1);
 260         errdefer atlas.deinit(allocator);
 261         var image = try packAtlasImageAlloc(allocator, atlas);
 262         errdefer image.deinit(allocator);
 263         var glyph_index = try glyphIndexAlloc(allocator, atlas);
 264         errdefer glyph_index.deinit(allocator);
 265         var cache = try AtlasCacheStorage.init(allocator, cache_limits);
 266         errdefer cache.deinit(allocator);
 267         cache.activate();
 268         return .{
 269             .allocator = allocator,
 270             .pixel_size = pixel_size,
 271             .atlas = atlas,
 272             .image = image,
 273             .glyph_index = glyph_index,
 274             .cache = cache,
 275             .backend = .{ .outline = .{
 276                 .font_bytes = bytes,
 277                 .font = font,
 278                 .context = filigree.Context.init(allocator, .{}),
 279                 .output = try filigree.Output.init(allocator, output_limits),
 280             } },
 281         };
 282     }
 283 
 284     pub fn initFromBitmapGlyphs(
 285         allocator: Allocator,
 286         glyphs: []const BitmapGlyph,
 287         cell: BitmapMetrics,
 288         pixel_size: i32,
 289         cache_limits: AtlasCacheStorage.Limits,
 290     ) !Atlas {
 291         if (glyphs.len == 0 or cell.width == 0 or cell.height == 0) return error.FontHasNoPrintableAscii;
 292         const cell_width: usize = cell.width;
 293         const cell_height: usize = cell.height;
 294         const padded = cell_width + 1;
 295         const atlas_width = std.math.mul(usize, padded, glyphs.len) catch return error.InvalidAtlas;
 296         const pixel_total = try pixelCount(
 297             std.math.cast(u32, atlas_width) orelse return error.InvalidAtlas,
 298             std.math.cast(u32, cell_height) orelse return error.InvalidAtlas,
 299         );
 300 
 301         const rgba = try allocator.alloc(u8, pixel_total * 4);
 302         errdefer allocator.free(rgba);
 303         @memset(rgba, 0);
 304         const atlas_glyphs = try allocator.alloc(filigree.GlyphAtlasGlyph, glyphs.len);
 305         errdefer allocator.free(atlas_glyphs);
 306         const recs = try allocator.alloc(filigree.GlyphAtlasRectangle, glyphs.len);
 307         errdefer allocator.free(recs);
 308 
 309         var map: std.AutoHashMapUnmanaged(u21, u32) = .empty;
 310         errdefer map.deinit(allocator);
 311 
 312         for (glyphs, 0..) |glyph, index| {
 313             if (glyph.rows.len < @as(usize, cell.stride) * cell_height) return error.InvalidAtlas;
 314             const origin_x = index * padded;
 315             for (0..cell_height) |y| {
 316                 const row = glyph.rows[y * cell.stride ..][0..cell.stride];
 317                 for (0..cell_width) |x| {
 318                     const bit = (row[x / 8] >> @intCast(7 - (x % 8))) & 1;
 319                     if (bit == 0) continue;
 320                     const base = ((y * atlas_width) + origin_x + x) * 4;
 321                     rgba[base] = 255;
 322                     rgba[base + 1] = 255;
 323                     rgba[base + 2] = 255;
 324                     rgba[base + 3] = 255;
 325                 }
 326             }
 327             atlas_glyphs[index] = .{
 328                 .codepoint = std.math.cast(i32, glyph.codepoint) orelse return error.CodepointTooLarge,
 329                 .glyph_id = glyph.codepoint,
 330                 .width = @intCast(cell_width),
 331                 .height = @intCast(cell_height),
 332                 .offset_x = 0,
 333                 .offset_y = 0,
 334                 .advance_x = @intCast(cell_width),
 335             };
 336             recs[index] = .{
 337                 .x = @floatFromInt(origin_x),
 338                 .y = 0,
 339                 .width = @floatFromInt(cell_width),
 340                 .height = @floatFromInt(cell_height),
 341             };
 342             try map.put(allocator, glyph.codepoint, glyph.codepoint);
 343         }
 344 
 345         const atlas = filigree.GlyphAtlas{
 346             .rgba = rgba,
 347             .width = @intCast(atlas_width),
 348             .height = @intCast(cell_height),
 349             .glyphs = atlas_glyphs,
 350             .recs = recs,
 351             .base_size = @intCast(cell_height),
 352             .glyph_padding = 1,
 353         };
 354         var image = try packAtlasImageAlloc(allocator, atlas);
 355         errdefer image.deinit(allocator);
 356         var glyph_index = try glyphIndexAlloc(allocator, atlas);
 357         errdefer glyph_index.deinit(allocator);
 358         var cache = try AtlasCacheStorage.init(allocator, cache_limits);
 359         errdefer cache.deinit(allocator);
 360         cache.activate();
 361         return .{
 362             .allocator = allocator,
 363             .pixel_size = pixel_size,
 364             .atlas = atlas,
 365             .image = image,
 366             .glyph_index = glyph_index,
 367             .cache = cache,
 368             .backend = .{ .bitmap = .{
 369                 .advance = @as(i32, @intCast(cell_width)) * 64,
 370                 .height = @intCast(cell_height),
 371                 .map = map,
 372             } },
 373         };
 374     }
 375 
 376     pub fn deinit(self: *Atlas) void {
 377         self.cache.deinit(self.allocator);
 378         self.line_break_workspace.deinit(self.allocator);
 379         self.composite_workspace.deinit(self.allocator);
 380         self.glyph_index.deinit(self.allocator);
 381         self.image.deinit(self.allocator);
 382         self.atlas.deinit(self.allocator);
 383         switch (self.backend) {
 384             .outline => |*outline| {
 385                 outline.output.deinit(self.allocator);
 386                 outline.context.deinit();
 387                 outline.font.deinit();
 388                 self.allocator.free(outline.font_bytes);
 389             },
 390             .bitmap => |*bitmap| {
 391                 bitmap.map.deinit(self.allocator);
 392                 bitmap.shaped.deinit(self.allocator);
 393                 bitmap.clusters.deinit(self.allocator);
 394             },
 395         }
 396         self.* = undefined;
 397     }
 398 
 399     pub fn cacheStatus(self: *const Atlas) AtlasCacheStorage.Status {
 400         return self.cache.status();
 401     }
 402 
 403     pub fn shape(self: *Atlas, content: []const u8) !filigree.GlyphRun {
 404         if (self.cache.lookupShape(content)) |cached| return cached;
 405         const fresh = try self.shapeUncached(content);
 406         return self.cache.storeShape(content, fresh) catch fresh;
 407     }
 408 
 409     pub fn caretLine(self: *Atlas, content: []const u8) !CaretLine {
 410         return .{ .content = content, .run = try self.shape(content) };
 411     }
 412 
 413     pub fn advanceForByteOffset(self: *Atlas, content: []const u8, byte_offset: usize) !f32 {
 414         const line = try self.caretLine(content);
 415         return line.advanceForByteOffset(byte_offset);
 416     }
 417 
 418     pub fn hitTestAdvance(self: *Atlas, content: []const u8, advance: f32) !filigree.caret.LineHit {
 419         const line = try self.caretLine(content);
 420         return line.hitTestAdvance(advance);
 421     }
 422 
 423     fn shapeUncached(self: *Atlas, content: []const u8) !filigree.GlyphRun {
 424         switch (self.backend) {
 425             .outline => |*outline| {
 426                 try outline.context.shapeRun(.{
 427                     .font = &outline.font,
 428                     .text = .{ .utf8 = content },
 429                 }, &outline.output);
 430                 return outline.output.run();
 431             },
 432             .bitmap => |*bitmap| return shapeBitmap(self.allocator, bitmap, content),
 433         }
 434     }
 435 
 436     fn outlineFont(self: *Atlas) ?*const filigree.Font {
 437         return switch (self.backend) {
 438             .outline => |*outline| &outline.font,
 439             .bitmap => null,
 440         };
 441     }
 442 
 443     fn coversUtf8(self: *Atlas, content: []const u8) !bool {
 444         const selected = self.outlineFont() orelse return true;
 445         var index: usize = 0;
 446         while (index < content.len) {
 447             const sequence_len = std.unicode.utf8ByteSequenceLength(content[index]) catch return error.InvalidUtf8;
 448             if (index + sequence_len > content.len) return error.InvalidUtf8;
 449             const codepoint = std.unicode.utf8Decode(content[index .. index + sequence_len]) catch return error.InvalidUtf8;
 450             if (selected.face.glyphId(codepoint) == 0) return false;
 451             index += sequence_len;
 452         }
 453         return true;
 454     }
 455 
 456     pub fn metrics(self: *const Atlas) Metrics {
 457         switch (self.backend) {
 458             .outline => |*outline| {
 459                 const face = outline.font.face;
 460                 const upem: f32 = @floatFromInt(if (face.units_per_em == 0) 1000 else face.units_per_em);
 461                 const to_pixels = @as(f32, @floatFromInt(self.pixel_size)) / upem;
 462                 return .{
 463                     .ascent = @as(f32, @floatFromInt(face.ascender)) * to_pixels,
 464                     .descent = @as(f32, @floatFromInt(-@as(i32, face.descender))) * to_pixels,
 465                     .line_gap = @as(f32, @floatFromInt(face.line_gap)) * to_pixels,
 466                 };
 467             },
 468             .bitmap => |*bitmap| return .{
 469                 .ascent = @floatFromInt(bitmap.height),
 470                 .descent = 0,
 471                 .line_gap = 0,
 472             },
 473         }
 474     }
 475 };
 476 
 477 pub const Metrics = struct {
 478     ascent: f32,
 479     descent: f32,
 480     line_gap: f32,
 481 
 482     pub fn height(self: Metrics) f32 {
 483         return self.ascent + self.descent;
 484     }
 485 };
 486 
 487 const CompositeGlyphSource = struct {
 488     segment_index: u32,
 489 };
 490 
 491 const CompositeMetricSpan = struct {
 492     byte_start: u32,
 493     byte_end: u32,
 494     atlas: *const Atlas,
 495     image_index: u32,
 496     scale_ratio: f32,
 497     point_size: f32,
 498     ascent: f32,
 499     descent: f32,
 500 };
 501 
 502 const CompositeWorkspace = struct {
 503     glyphs: std.ArrayListUnmanaged(filigree.ShapedGlyph) = .empty,
 504     clusters: std.ArrayListUnmanaged(filigree.Cluster) = .empty,
 505     ligature_carets: std.ArrayListUnmanaged(filigree.LigatureCaret) = .empty,
 506     glyph_sources: std.ArrayListUnmanaged(CompositeGlyphSource) = .empty,
 507     spans: std.ArrayListUnmanaged(CompositeMetricSpan) = .empty,
 508 
 509     fn clear(self: *CompositeWorkspace) void {
 510         self.glyphs.clearRetainingCapacity();
 511         self.clusters.clearRetainingCapacity();
 512         self.ligature_carets.clearRetainingCapacity();
 513         self.glyph_sources.clearRetainingCapacity();
 514         self.spans.clearRetainingCapacity();
 515     }
 516 
 517     fn deinit(self: *CompositeWorkspace, allocator: Allocator) void {
 518         self.spans.deinit(allocator);
 519         self.glyph_sources.deinit(allocator);
 520         self.ligature_carets.deinit(allocator);
 521         self.clusters.deinit(allocator);
 522         self.glyphs.deinit(allocator);
 523         self.* = .{};
 524     }
 525 };
 526 
 527 const MeasureKey = struct {
 528     content: []const u8,
 529     styles: []const UiTextStyle,
 530     runs: []const UiTextRun,
 531     atlas_entries: []const AtlasSet.Entry,
 532     fallback_entries: []const AtlasSet.Entry,
 533     fallback_identity: usize,
 534     font_asset_id: u64,
 535     point_size_bits: u64,
 536     line_height_bits: u64,
 537     wrap_width_bits: u64,
 538     device_scale_bits: u32,
 539 
 540     fn fromText(text: UiText, atlases: ?*const AtlasSet) MeasureKey {
 541         const include_metric_plan = atlases != null;
 542         return .{
 543             .content = text.content,
 544             .styles = if (include_metric_plan) text.styles else &.{},
 545             .runs = if (include_metric_plan) text.runs else &.{},
 546             .atlas_entries = if (atlases) |set| set.entries else &.{},
 547             .fallback_entries = if (atlases) |set| set.fallback_entries else &.{},
 548             .fallback_identity = if (atlases) |set| if (set.fallback) |value| @intFromPtr(value) else 0 else 0,
 549             .font_asset_id = if (include_metric_plan) text.font_asset_id else 0,
 550             .point_size_bits = floatBits(text.point_size),
 551             .line_height_bits = floatBits(text.line_height),
 552             .wrap_width_bits = floatBits(text.wrap_width),
 553             .device_scale_bits = if (atlases) |set| @bitCast(set.device_scale) else 0,
 554         };
 555     }
 556 };
 557 
 558 const MeasureContext = struct {
 559     pub fn hash(_: MeasureContext, key: MeasureKey) u64 {
 560         var hasher = std.hash.Wyhash.init(0);
 561         hasher.update(key.content);
 562         std.hash.autoHash(&hasher, key.font_asset_id);
 563         hasher.update(std.mem.asBytes(&key.point_size_bits));
 564         hasher.update(std.mem.asBytes(&key.line_height_bits));
 565         hasher.update(std.mem.asBytes(&key.wrap_width_bits));
 566         hasher.update(std.mem.asBytes(&key.device_scale_bits));
 567         for (key.styles) |style| {
 568             std.hash.autoHash(&hasher, style.font_asset_id);
 569             std.hash.autoHash(&hasher, floatBits(style.point_size));
 570         }
 571         for (key.runs) |run| hashTextRun(&hasher, run);
 572         for (key.atlas_entries) |entry| {
 573             std.hash.autoHash(&hasher, entry.face);
 574             std.hash.autoHash(&hasher, entry.image_index);
 575             std.hash.autoHash(&hasher, @intFromPtr(entry.atlas));
 576         }
 577         for (key.fallback_entries) |entry| {
 578             std.hash.autoHash(&hasher, entry.face);
 579             std.hash.autoHash(&hasher, entry.image_index);
 580             std.hash.autoHash(&hasher, @intFromPtr(entry.atlas));
 581         }
 582         std.hash.autoHash(&hasher, key.fallback_identity);
 583         return hasher.final();
 584     }
 585 
 586     pub fn eql(_: MeasureContext, left: MeasureKey, right: MeasureKey) bool {
 587         if (left.font_asset_id != right.font_asset_id or
 588             left.point_size_bits != right.point_size_bits or
 589             left.line_height_bits != right.line_height_bits or
 590             left.wrap_width_bits != right.wrap_width_bits or
 591             left.device_scale_bits != right.device_scale_bits or
 592             left.fallback_identity != right.fallback_identity)
 593         {
 594             return false;
 595         }
 596         if (!std.mem.eql(u8, left.content, right.content)) return false;
 597         if (left.styles.len != right.styles.len or
 598             left.runs.len != right.runs.len or
 599             left.atlas_entries.len != right.atlas_entries.len or
 600             left.fallback_entries.len != right.fallback_entries.len)
 601         {
 602             return false;
 603         }
 604         for (left.styles, right.styles) |left_style, right_style| {
 605             if (!gui.model.textStylesEqual(left_style, right_style)) return false;
 606         }
 607         for (left.runs, right.runs) |left_run, right_run| {
 608             if (!textRunsEqual(left_run, right_run)) return false;
 609         }
 610         for (left.atlas_entries, right.atlas_entries) |left_entry, right_entry| {
 611             if (left_entry.face != right_entry.face or
 612                 left_entry.image_index != right_entry.image_index or
 613                 left_entry.atlas != right_entry.atlas)
 614             {
 615                 return false;
 616             }
 617         }
 618         for (left.fallback_entries, right.fallback_entries) |left_entry, right_entry| {
 619             if (left_entry.face != right_entry.face or
 620                 left_entry.image_index != right_entry.image_index or
 621                 left_entry.atlas != right_entry.atlas)
 622             {
 623                 return false;
 624             }
 625         }
 626         return true;
 627     }
 628 };
 629 
 630 fn hashTextRun(hasher: anytype, run: UiTextRun) void {
 631     std.hash.autoHash(hasher, run.byte_start);
 632     std.hash.autoHash(hasher, run.byte_end);
 633     std.hash.autoHash(hasher, run.style_slot);
 634     hashOptionalTextColor(hasher, run.foreground);
 635     hashOptionalTextColor(hasher, run.background);
 636     std.hash.autoHash(hasher, run.underline);
 637     std.hash.autoHash(hasher, run.strikethrough);
 638 }
 639 
 640 fn hashOptionalTextColor(hasher: anytype, color: ?Color) void {
 641     std.hash.autoHash(hasher, color != null);
 642     if (color) |value| {
 643         std.hash.autoHash(hasher, value.r);
 644         std.hash.autoHash(hasher, value.g);
 645         std.hash.autoHash(hasher, value.b);
 646         std.hash.autoHash(hasher, value.a);
 647     }
 648 }
 649 
 650 fn textRunsEqual(left: UiTextRun, right: UiTextRun) bool {
 651     return left.byte_start == right.byte_start and
 652         left.byte_end == right.byte_end and
 653         left.style_slot == right.style_slot and
 654         std.meta.eql(left.foreground, right.foreground) and
 655         std.meta.eql(left.background, right.background) and
 656         left.underline == right.underline and
 657         left.strikethrough == right.strikethrough;
 658 }
 659 
 660 const ShapedRun = struct {
 661     glyphs: []const filigree.ShapedGlyph,
 662     clusters: []const filigree.Cluster,
 663     ligature_carets: []const filigree.LigatureCaret,
 664     total_x_advance: i32,
 665     total_y_advance: i32,
 666     direction: filigree.Direction,
 667     writing_mode: filigree.WritingMode,
 668     output_order: filigree.OutputOrder,
 669 
 670     fn run(self: ShapedRun) filigree.GlyphRun {
 671         return .{
 672             .glyphs = self.glyphs,
 673             .clusters = self.clusters,
 674             .ligature_carets = self.ligature_carets,
 675             .total_x_advance = self.total_x_advance,
 676             .total_y_advance = self.total_y_advance,
 677             .direction = self.direction,
 678             .writing_mode = self.writing_mode,
 679             .output_order = self.output_order,
 680         };
 681     }
 682 };
 683 
 684 const MeasureCacheEntry = struct {
 685     hash: u64,
 686     key: MeasureKey,
 687     value: Size,
 688 };
 689 
 690 const ShapeCacheEntry = struct {
 691     hash: u64,
 692     content: []const u8,
 693     shaped: ShapedRun,
 694 };
 695 
 696 const CacheEpochUsage = struct {
 697     entries: usize = 0,
 698     payload_bytes: usize = 0,
 699     high_water_entries: usize = 0,
 700     high_water_payload_bytes: usize = 0,
 701     high_water_physical_payload_bytes: usize = 0,
 702     rollovers: u64 = 0,
 703     disabled_bypasses: u64 = 0,
 704     oversize_bypasses: u64 = 0,
 705 
 706     fn inserted(self: *CacheEpochUsage, raw_bytes: usize, physical_bytes: usize) void {
 707         self.entries += 1;
 708         self.payload_bytes += raw_bytes;
 709         self.high_water_entries = @max(self.high_water_entries, self.entries);
 710         self.high_water_payload_bytes = @max(self.high_water_payload_bytes, self.payload_bytes);
 711         self.high_water_physical_payload_bytes = @max(
 712             self.high_water_physical_payload_bytes,
 713             physical_bytes,
 714         );
 715     }
 716 
 717     fn replaced(self: *CacheEpochUsage) void {
 718         self.entries = 0;
 719         self.payload_bytes = 0;
 720         self.rollovers +|= 1;
 721     }
 722 };
 723 
 724 const CachePayload = struct {
 725     bytes: []u8,
 726     cursor: usize = 0,
 727 
 728     fn reset(self: *CachePayload) void {
 729         self.cursor = 0;
 730     }
 731 
 732     fn dupe(self: *CachePayload, comptime T: type, source: []const T) []T {
 733         const start = cacheAligned(self.cursor, @alignOf(T)) catch unreachable;
 734         const byte_count = cacheMultiplied(source.len, @sizeOf(T)) catch unreachable;
 735         const end = cacheAdded(start, byte_count) catch unreachable;
 736         std.debug.assert(end <= self.bytes.len);
 737         const region: []align(@alignOf(T)) u8 = @alignCast(self.bytes[start..end]);
 738         const owned = std.mem.bytesAsSlice(T, region);
 739         @memcpy(owned, source);
 740         self.cursor = end;
 741         return owned;
 742     }
 743 };
 744 
 745 pub const AtlasCacheStorage = struct {
 746     phase: alloc_phase.capacity.Phase,
 747     capacity: Capacity,
 748     bytes: []align(storage_alignment) u8,
 749     measure_slots: []u32,
 750     measure_entries: []MeasureCacheEntry,
 751     measure_payload: CachePayload,
 752     measure_usage: CacheEpochUsage = .{},
 753     shape_slots: []u32,
 754     shape_entries: []ShapeCacheEntry,
 755     shape_payload: CachePayload,
 756     shape_usage: CacheEpochUsage = .{},
 757 
 758     pub const storage_alignment: usize = @max(
 759         @alignOf(MeasureCacheEntry),
 760         @max(
 761             @alignOf(ShapeCacheEntry),
 762             @max(measure_payload_alignment, shape_payload_alignment),
 763         ),
 764     );
 765 
 766     pub const Limits = struct {
 767         measure_entries: usize,
 768         measure_payload_bytes: usize,
 769         shape_entries: usize,
 770         shape_payload_bytes: usize,
 771     };
 772 
 773     pub const DeriveError = error{
 774         InvalidMeasureLimits,
 775         InvalidShapeLimits,
 776         EntryLimitTooLarge,
 777         CapacityOverflow,
 778     };
 779 
 780     pub const Exhaustion = error{
 781         CacheDisabled,
 782         EntryTooLarge,
 783     };
 784 
 785     pub const Capacity = struct {
 786         limits: Limits,
 787         measure_index_slots: usize,
 788         measure_index_offset: usize,
 789         measure_entries_offset: usize,
 790         measure_payload_offset: usize,
 791         measure_payload_storage_bytes: usize,
 792         shape_index_slots: usize,
 793         shape_index_offset: usize,
 794         shape_entries_offset: usize,
 795         shape_payload_offset: usize,
 796         shape_payload_storage_bytes: usize,
 797         storage_bytes: usize,
 798 
 799         pub fn derive(limits: Limits) DeriveError!Capacity {
 800             try cacheLimitsValid(limits);
 801             const measure_index_slots = try cacheIndexSlots(limits.measure_entries);
 802             const shape_index_slots = try cacheIndexSlots(limits.shape_entries);
 803             const measure_padding = try cacheMultiplied(
 804                 limits.measure_entries,
 805                 measure_padding_per_entry,
 806             );
 807             const shape_padding = try cacheMultiplied(
 808                 limits.shape_entries,
 809                 shape_padding_per_entry,
 810             );
 811             const measure_payload_bytes = try cacheAdded(
 812                 limits.measure_payload_bytes,
 813                 measure_padding,
 814             );
 815             const shape_payload_bytes = try cacheAdded(
 816                 limits.shape_payload_bytes,
 817                 shape_padding,
 818             );
 819             const measure_index = try cachePlaced(u32, 0, measure_index_slots);
 820             const measure_entries = try cachePlaced(
 821                 MeasureCacheEntry,
 822                 measure_index.end,
 823                 limits.measure_entries,
 824             );
 825             const measure_payload = try cachePlacedBytes(
 826                 measure_entries.end,
 827                 measure_payload_alignment,
 828                 measure_payload_bytes,
 829             );
 830             const shape_index = try cachePlaced(u32, measure_payload.end, shape_index_slots);
 831             const shape_entries = try cachePlaced(
 832                 ShapeCacheEntry,
 833                 shape_index.end,
 834                 limits.shape_entries,
 835             );
 836             const shape_payload = try cachePlacedBytes(
 837                 shape_entries.end,
 838                 shape_payload_alignment,
 839                 shape_payload_bytes,
 840             );
 841             return .{
 842                 .limits = limits,
 843                 .measure_index_slots = measure_index_slots,
 844                 .measure_index_offset = measure_index.start,
 845                 .measure_entries_offset = measure_entries.start,
 846                 .measure_payload_offset = measure_payload.start,
 847                 .measure_payload_storage_bytes = measure_payload.bytes,
 848                 .shape_index_slots = shape_index_slots,
 849                 .shape_index_offset = shape_index.start,
 850                 .shape_entries_offset = shape_entries.start,
 851                 .shape_payload_offset = shape_payload.start,
 852                 .shape_payload_storage_bytes = shape_payload.bytes,
 853                 .storage_bytes = shape_payload.end,
 854             };
 855         }
 856     };
 857 
 858     pub const EpochStatus = struct {
 859         entry_capacity: usize,
 860         payload_capacity_bytes: usize,
 861         physical_payload_capacity_bytes: usize,
 862         entries: usize,
 863         payload_bytes: usize,
 864         physical_payload_bytes: usize,
 865         high_water_entries: usize,
 866         high_water_payload_bytes: usize,
 867         high_water_physical_payload_bytes: usize,
 868         rollovers: u64,
 869         disabled_bypasses: u64,
 870         oversize_bypasses: u64,
 871     };
 872 
 873     pub const Status = struct {
 874         phase: alloc_phase.capacity.Phase,
 875         capacity: Capacity,
 876         storage_bytes: usize,
 877         measure: EpochStatus,
 878         shape: EpochStatus,
 879     };
 880 
 881     pub const claim: alloc_phase.capacity.Declaration = .{
 882         .source = .{
 883             .id = "gui.text_atlas_cache_storage",
 884             .kind = .phase_static,
 885             .limit_source = .caller,
 886             .storage = .{
 887                 .covered = &.{
 888                     .{
 889                         .id = "measure_cache_fixed_index_and_dense_entries",
 890                         .lifetime = .steady,
 891                         .detail = "measure cache fixed index and dense entries",
 892                     },
 893                     .{
 894                         .id = "measure_cache_aligned_payload_bytes",
 895                         .lifetime = .steady,
 896                         .detail = "measure cache aligned payload bytes",
 897                     },
 898                     .{
 899                         .id = "shape_cache_fixed_index_and_dense_entries",
 900                         .lifetime = .steady,
 901                         .detail = "shape cache fixed index and dense entries",
 902                     },
 903                     .{
 904                         .id = "shape_cache_aligned_payload_bytes",
 905                         .lifetime = .steady,
 906                         .detail = "shape cache aligned payload bytes",
 907                     },
 908                 },
 909                 .excluded = &.{
 910                     "outline shaping context, output, and foreign backend storage",
 911                     "bitmap shaping, line-break, and composite workspaces",
 912                     "font bytes, glyph atlas, image, glyph index, and bitmap map",
 913                     "caller-owned text, returned data, and Renderer storage",
 914                 },
 915             },
 916             .capacity = .{
 917                 .inputs = &.{
 918                     alloc_phase.capacity.bindInput(Limits, "measure_entries", "measure_entries"),
 919                     alloc_phase.capacity.bindInput(Limits, "shape_entries", "shape_entries"),
 920                     alloc_phase.capacity.bindInput(Limits, "measure_payload_bytes", "measure_payload_bytes"),
 921                     alloc_phase.capacity.bindInput(Limits, "shape_payload_bytes", "shape_payload_bytes"),
 922                 },
 923                 .type_selectors = &.{
 924                     alloc_phase.capacity.bindType(u32, "u32"),
 925                     alloc_phase.capacity.bindType(MeasureCacheEntry, "measureentry"),
 926                     alloc_phase.capacity.bindType(ShapeCacheEntry, "shapeentry"),
 927                 },
 928                 .nodes = &.{
 929                     .{ .input = 0 },
 930                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } },
 931                     .{ .next_power_of_two = 1 },
 932                     .{ .input = 1 },
 933                     .{ .scale = .{ .node = 3, .coefficient = .{ .literal = 2 } } },
 934                     .{ .next_power_of_two = 4 },
 935                     .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 0 } } },
 936                     .{ .alignment = .{ .node = 6, .alignment = .{ .literal = 16 } } },
 937                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 1 } } },
 938                     .{ .alignment = .{ .node = 8, .alignment = .{ .literal = 16 } } },
 939                     .{ .input = 2 },
 940                     .{ .alignment = .{ .node = 10, .alignment = .{ .literal = 16 } } },
 941                     .{ .scale = .{ .node = 5, .coefficient = .{ .size_of_concrete_type = 0 } } },
 942                     .{ .alignment = .{ .node = 12, .alignment = .{ .literal = 16 } } },
 943                     .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 2 } } },
 944                     .{ .alignment = .{ .node = 14, .alignment = .{ .literal = 16 } } },
 945                     .{ .input = 3 },
 946                     .{ .alignment = .{ .node = 16, .alignment = .{ .literal = 16 } } },
 947                     .{ .add = .{ .left = 7, .right = 9 } },
 948                     .{ .add = .{ .left = 18, .right = 11 } },
 949                     .{ .add = .{ .left = 19, .right = 13 } },
 950                     .{ .add = .{ .left = 20, .right = 15 } },
 951                     .{ .add = .{ .left = 21, .right = 17 } },
 952                 },
 953                 .assertions = &.{.{
 954                     .scope = .closure_total,
 955                     .measure = .retained,
 956                     .relation = .exact,
 957                     .expression = 22,
 958                 }},
 959             },
 960             .overload = .{
 961                 .kind = .replace,
 962                 .detail = "aggregate entry or payload exhaustion replaces one epoch; disabled and individually oversized records bypass before mutation",
 963             },
 964             .risks = .{
 965                 .transitive = .{
 966                     .status = .excluded,
 967                     .detail = "uncached Atlas shaping and measurement can allocate in backend and workspace owners outside this cache-only claim",
 968                 },
 969                 .foreign = .{
 970                     .status = .excluded,
 971                     .detail = "cache lookup, copying, and replacement cross no foreign boundary; outline shaping remains excluded",
 972                 },
 973             },
 974             .obligations = &.{
 975                 .{ .key = "gui_text_atlas_cache_capacity", .role = .capacity_model },
 976                 .{ .key = "gui_text_atlas_cache_acquisition", .role = .custom },
 977                 .{ .key = "gui_text_atlas_cache_oom", .role = .custom },
 978                 .{ .key = "gui_text_atlas_cache_boundaries", .role = .overload },
 979                 .{ .key = "gui_text_atlas_cache_sealed_transitive_risk", .role = .transitive_risk },
 980                 .{ .key = "gui_text_atlas_cache_sealed_foreign_risk", .role = .foreign_risk },
 981                 .{ .key = "gui_text_atlas_cache_collisions", .role = .custom },
 982                 .{ .key = "gui_text_atlas_cache_stability", .role = .custom },
 983                 .{ .key = "gui_text_atlas_cache_differential", .role = .overload },
 984                 .{ .key = "gui_text_atlas_cache_root", .role = .custom },
 985             },
 986         },
 987         .bindings = .{
 988             .owner = @This(),
 989             .seal = .{
 990                 .family = alloc_phase.capacity.selector(@This().activate),
 991                 .premise = .{
 992                     .class = .checked_semantic_fact,
 993                     .authority = .checker,
 994                 },
 995             },
 996             .teardown = .{
 997                 .family = alloc_phase.capacity.selector(@This().deinit),
 998                 .premise = .{
 999                     .class = .checked_semantic_fact,
1000                     .authority = .checker,
1001                 },
1002             },
1003         },
1004     };
1005 
1006     pub fn init(allocator: Allocator, limits: Limits) (Allocator.Error || DeriveError)!AtlasCacheStorage {
1007         const capacity = try Capacity.derive(limits);
1008         const bytes = try allocator.alignedAlloc(
1009             u8,
1010             .fromByteUnits(storage_alignment),
1011             capacity.storage_bytes,
1012         );
1013         return .{
1014             .phase = .initialization,
1015             .capacity = capacity,
1016             .bytes = bytes,
1017             .measure_slots = cacheTypedSlice(
1018                 u32,
1019                 bytes,
1020                 capacity.measure_index_offset,
1021                 capacity.measure_index_slots,
1022             ),
1023             .measure_entries = cacheTypedSlice(
1024                 MeasureCacheEntry,
1025                 bytes,
1026                 capacity.measure_entries_offset,
1027                 limits.measure_entries,
1028             ),
1029             .measure_payload = .{ .bytes = bytes[capacity.measure_payload_offset..][0..capacity.measure_payload_storage_bytes] },
1030             .shape_slots = cacheTypedSlice(
1031                 u32,
1032                 bytes,
1033                 capacity.shape_index_offset,
1034                 capacity.shape_index_slots,
1035             ),
1036             .shape_entries = cacheTypedSlice(
1037                 ShapeCacheEntry,
1038                 bytes,
1039                 capacity.shape_entries_offset,
1040                 limits.shape_entries,
1041             ),
1042             .shape_payload = .{ .bytes = bytes[capacity.shape_payload_offset..][0..capacity.shape_payload_storage_bytes] },
1043         };
1044     }
1045 
1046     pub fn activate(self: *AtlasCacheStorage) void {
1047         self.assertStorage();
1048         std.debug.assert(self.phase == .initialization);
1049         @memset(self.measure_slots, 0);
1050         @memset(self.shape_slots, 0);
1051         self.phase = .steady;
1052         self.assertStorage();
1053     }
1054 
1055     pub fn deinit(self: *AtlasCacheStorage, allocator: Allocator) void {
1056         self.assertStorage();
1057         std.debug.assert(self.phase != .teardown);
1058         self.phase = .teardown;
1059         allocator.free(self.bytes);
1060         self.bytes = &.{};
1061         self.measure_slots = &.{};
1062         self.measure_entries = &.{};
1063         self.measure_payload = .{ .bytes = &.{} };
1064         self.shape_slots = &.{};
1065         self.shape_entries = &.{};
1066         self.shape_payload = .{ .bytes = &.{} };
1067     }
1068 
1069     pub fn status(self: *const AtlasCacheStorage) Status {
1070         self.assertStorage();
1071         return .{
1072             .phase = self.phase,
1073             .capacity = self.capacity,
1074             .storage_bytes = self.capacity.storage_bytes,
1075             .measure = cacheEpochStatus(
1076                 self.capacity.limits.measure_entries,
1077                 self.capacity.limits.measure_payload_bytes,
1078                 self.capacity.measure_payload_storage_bytes,
1079                 self.measure_payload.cursor,
1080                 self.measure_usage,
1081             ),
1082             .shape = cacheEpochStatus(
1083                 self.capacity.limits.shape_entries,
1084                 self.capacity.limits.shape_payload_bytes,
1085                 self.capacity.shape_payload_storage_bytes,
1086                 self.shape_payload.cursor,
1087                 self.shape_usage,
1088             ),
1089         };
1090     }
1091 
1092     fn lookupMeasure(self: *const AtlasCacheStorage, key: MeasureKey) ?Size {
1093         self.assertStorage();
1094         std.debug.assert(self.phase == .steady);
1095         if (self.measure_slots.len == 0) return null;
1096         const hash = MeasureContext.hash(.{}, key);
1097         var slot = cacheStartSlot(hash, self.measure_slots.len);
1098         var probes: usize = 0;
1099         while (probes < self.measure_slots.len) : (probes += 1) {
1100             const encoded = self.measure_slots[slot];
1101             if (encoded == 0) return null;
1102             const entry = self.measure_entries[encoded - 1];
1103             if (entry.hash == hash and MeasureContext.eql(.{}, entry.key, key)) {
1104                 return entry.value;
1105             }
1106             slot = cacheNextSlot(slot, self.measure_slots.len);
1107         }
1108         unreachable;
1109     }
1110 
1111     fn lookupShape(self: *const AtlasCacheStorage, content: []const u8) ?filigree.GlyphRun {
1112         self.assertStorage();
1113         std.debug.assert(self.phase == .steady);
1114         if (self.shape_slots.len == 0) return null;
1115         const hash = std.hash_map.hashString(content);
1116         var slot = cacheStartSlot(hash, self.shape_slots.len);
1117         var probes: usize = 0;
1118         while (probes < self.shape_slots.len) : (probes += 1) {
1119             const encoded = self.shape_slots[slot];
1120             if (encoded == 0) return null;
1121             const entry = self.shape_entries[encoded - 1];
1122             if (entry.hash == hash and std.mem.eql(u8, entry.content, content)) {
1123                 return entry.shaped.run();
1124             }
1125             slot = cacheNextSlot(slot, self.shape_slots.len);
1126         }
1127         unreachable;
1128     }
1129 
1130     fn storeMeasure(
1131         self: *AtlasCacheStorage,
1132         key: MeasureKey,
1133         value: Size,
1134     ) Exhaustion!Size {
1135         self.assertStorage();
1136         defer self.assertStorage();
1137         std.debug.assert(self.phase == .steady);
1138         std.debug.assert(self.lookupMeasure(key) == null);
1139         const payload_bytes = measurePayloadBytes(key) orelse {
1140             self.measure_usage.oversize_bypasses +|= 1;
1141             return error.EntryTooLarge;
1142         };
1143         try self.prepareMeasure(payload_bytes);
1144         const hash = MeasureContext.hash(.{}, key);
1145         const slot = cacheEmptySlot(self.measure_slots, hash);
1146         const entry_index = self.measure_usage.entries;
1147         var owned_key = key;
1148         owned_key.content = self.measure_payload.dupe(u8, key.content);
1149         owned_key.styles = self.measure_payload.dupe(UiTextStyle, key.styles);
1150         owned_key.runs = self.measure_payload.dupe(UiTextRun, key.runs);
1151         owned_key.atlas_entries = self.measure_payload.dupe(AtlasSet.Entry, key.atlas_entries);
1152         owned_key.fallback_entries = self.measure_payload.dupe(AtlasSet.Entry, key.fallback_entries);
1153         self.measure_entries[entry_index] = .{ .hash = hash, .key = owned_key, .value = value };
1154         self.measure_slots[slot] = @intCast(entry_index + 1);
1155         self.measure_usage.inserted(payload_bytes, self.measure_payload.cursor);
1156         return value;
1157     }
1158 
1159     fn storeShape(
1160         self: *AtlasCacheStorage,
1161         content: []const u8,
1162         fresh: filigree.GlyphRun,
1163     ) Exhaustion!filigree.GlyphRun {
1164         self.assertStorage();
1165         defer self.assertStorage();
1166         std.debug.assert(self.phase == .steady);
1167         std.debug.assert(self.lookupShape(content) == null);
1168         const payload_bytes = shapePayloadBytes(content, fresh) orelse {
1169             self.shape_usage.oversize_bypasses +|= 1;
1170             return error.EntryTooLarge;
1171         };
1172         try self.prepareShape(payload_bytes);
1173         const hash = std.hash_map.hashString(content);
1174         const slot = cacheEmptySlot(self.shape_slots, hash);
1175         const entry_index = self.shape_usage.entries;
1176         const owned_content = self.shape_payload.dupe(u8, content);
1177         const shaped = ShapedRun{
1178             .glyphs = self.shape_payload.dupe(filigree.ShapedGlyph, fresh.glyphs),
1179             .clusters = self.shape_payload.dupe(filigree.Cluster, fresh.clusters),
1180             .ligature_carets = self.shape_payload.dupe(
1181                 filigree.LigatureCaret,
1182                 fresh.ligature_carets,
1183             ),
1184             .total_x_advance = fresh.total_x_advance,
1185             .total_y_advance = fresh.total_y_advance,
1186             .direction = fresh.direction,
1187             .writing_mode = fresh.writing_mode,
1188             .output_order = fresh.output_order,
1189         };
1190         self.shape_entries[entry_index] = .{
1191             .hash = hash,
1192             .content = owned_content,
1193             .shaped = shaped,
1194         };
1195         self.shape_slots[slot] = @intCast(entry_index + 1);
1196         self.shape_usage.inserted(payload_bytes, self.shape_payload.cursor);
1197         return shaped.run();
1198     }
1199 
1200     pub fn admitShapePayload(
1201         self: *const AtlasCacheStorage,
1202         payload_bytes: usize,
1203     ) Exhaustion!void {
1204         self.assertStorage();
1205         std.debug.assert(self.phase == .steady);
1206         const limits = self.capacity.limits;
1207         if (limits.shape_entries == 0) return error.CacheDisabled;
1208         if (payload_bytes > limits.shape_payload_bytes) return error.EntryTooLarge;
1209     }
1210 
1211     fn prepareMeasure(self: *AtlasCacheStorage, payload_bytes: usize) Exhaustion!void {
1212         const limits = self.capacity.limits;
1213         if (limits.measure_entries == 0) {
1214             self.measure_usage.disabled_bypasses +|= 1;
1215             return error.CacheDisabled;
1216         }
1217         if (payload_bytes > limits.measure_payload_bytes) {
1218             self.measure_usage.oversize_bypasses +|= 1;
1219             return error.EntryTooLarge;
1220         }
1221         std.debug.assert(self.measure_usage.payload_bytes <= limits.measure_payload_bytes);
1222         const payload_full = payload_bytes >
1223             limits.measure_payload_bytes - self.measure_usage.payload_bytes;
1224         if (self.measure_usage.entries == limits.measure_entries or payload_full) {
1225             self.replaceMeasure();
1226         }
1227     }
1228 
1229     fn prepareShape(self: *AtlasCacheStorage, payload_bytes: usize) Exhaustion!void {
1230         const limits = self.capacity.limits;
1231         self.admitShapePayload(payload_bytes) catch |err| {
1232             switch (err) {
1233                 error.CacheDisabled => self.shape_usage.disabled_bypasses +|= 1,
1234                 error.EntryTooLarge => self.shape_usage.oversize_bypasses +|= 1,
1235             }
1236             return err;
1237         };
1238         std.debug.assert(self.shape_usage.payload_bytes <= limits.shape_payload_bytes);
1239         const payload_full = payload_bytes >
1240             limits.shape_payload_bytes - self.shape_usage.payload_bytes;
1241         if (self.shape_usage.entries == limits.shape_entries or payload_full) {
1242             self.replaceShape();
1243         }
1244     }
1245 
1246     fn replaceMeasure(self: *AtlasCacheStorage) void {
1247         self.assertStorage();
1248         defer self.assertStorage();
1249         @memset(self.measure_slots, 0);
1250         self.measure_payload.reset();
1251         self.measure_usage.replaced();
1252     }
1253 
1254     fn replaceShape(self: *AtlasCacheStorage) void {
1255         self.assertStorage();
1256         defer self.assertStorage();
1257         @memset(self.shape_slots, 0);
1258         self.shape_payload.reset();
1259         self.shape_usage.replaced();
1260     }
1261 
1262     fn assertStorage(self: *const AtlasCacheStorage) void {
1263         const limits = self.capacity.limits;
1264         std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
1265         std.debug.assert(self.measure_slots.len == self.capacity.measure_index_slots);
1266         std.debug.assert(self.measure_entries.len == limits.measure_entries);
1267         std.debug.assert(
1268             self.measure_payload.bytes.len == self.capacity.measure_payload_storage_bytes,
1269         );
1270         std.debug.assert(self.shape_slots.len == self.capacity.shape_index_slots);
1271         std.debug.assert(self.shape_entries.len == limits.shape_entries);
1272         std.debug.assert(
1273             self.shape_payload.bytes.len == self.capacity.shape_payload_storage_bytes,
1274         );
1275         const base = @intFromPtr(self.bytes.ptr);
1276         assertCacheAddress(base, self.capacity.measure_index_offset, self.measure_slots);
1277         assertCacheAddress(base, self.capacity.measure_entries_offset, self.measure_entries);
1278         assertCacheAddress(base, self.capacity.measure_payload_offset, self.measure_payload.bytes);
1279         assertCacheAddress(base, self.capacity.shape_index_offset, self.shape_slots);
1280         assertCacheAddress(base, self.capacity.shape_entries_offset, self.shape_entries);
1281         assertCacheAddress(base, self.capacity.shape_payload_offset, self.shape_payload.bytes);
1282         std.debug.assert(self.measure_usage.entries <= limits.measure_entries);
1283         std.debug.assert(self.measure_usage.payload_bytes <= limits.measure_payload_bytes);
1284         std.debug.assert(self.measure_payload.cursor <= self.measure_payload.bytes.len);
1285         std.debug.assert(self.shape_usage.entries <= limits.shape_entries);
1286         std.debug.assert(self.shape_usage.payload_bytes <= limits.shape_payload_bytes);
1287         std.debug.assert(self.shape_payload.cursor <= self.shape_payload.bytes.len);
1288     }
1289 };
1290 
1291 const measure_payload_alignment: usize = @max(
1292     @alignOf(UiTextStyle),
1293     @max(@alignOf(UiTextRun), @alignOf(AtlasSet.Entry)),
1294 );
1295 const shape_payload_alignment: usize = @max(
1296     @alignOf(filigree.ShapedGlyph),
1297     @max(@alignOf(filigree.Cluster), @alignOf(filigree.LigatureCaret)),
1298 );
1299 const measure_padding_per_entry: usize = @alignOf(UiTextStyle) - 1 +
1300     @alignOf(UiTextRun) - 1 + @alignOf(AtlasSet.Entry) - 1;
1301 const shape_padding_per_entry: usize = @alignOf(filigree.ShapedGlyph) - 1 +
1302     @alignOf(filigree.Cluster) - 1 + @alignOf(filigree.LigatureCaret) - 1;
1303 
1304 const CacheRegion = struct {
1305     start: usize,
1306     bytes: usize,
1307     end: usize,
1308 };
1309 
1310 fn cacheLimitsValid(limits: AtlasCacheStorage.Limits) AtlasCacheStorage.DeriveError!void {
1311     const measure_disabled = limits.measure_entries == 0 and limits.measure_payload_bytes == 0;
1312     const measure_enabled = limits.measure_entries != 0 and limits.measure_payload_bytes != 0;
1313     if (!measure_disabled and !measure_enabled) return error.InvalidMeasureLimits;
1314     const shape_disabled = limits.shape_entries == 0 and limits.shape_payload_bytes == 0;
1315     const shape_enabled = limits.shape_entries != 0 and limits.shape_payload_bytes != 0;
1316     if (!shape_disabled and !shape_enabled) return error.InvalidShapeLimits;
1317     if (limits.measure_entries > std.math.maxInt(u32)) return error.EntryLimitTooLarge;
1318     if (limits.shape_entries > std.math.maxInt(u32)) return error.EntryLimitTooLarge;
1319 }
1320 
1321 fn cacheIndexSlots(entries: usize) AtlasCacheStorage.DeriveError!usize {
1322     if (entries == 0) return 0;
1323     const doubled = try cacheMultiplied(entries, 2);
1324     return std.math.ceilPowerOfTwo(usize, doubled) catch error.CapacityOverflow;
1325 }
1326 
1327 fn cacheAdded(left: usize, right: usize) AtlasCacheStorage.DeriveError!usize {
1328     return std.math.add(usize, left, right) catch error.CapacityOverflow;
1329 }
1330 
1331 fn cacheMultiplied(left: usize, right: usize) AtlasCacheStorage.DeriveError!usize {
1332     return std.math.mul(usize, left, right) catch error.CapacityOverflow;
1333 }
1334 
1335 fn cacheAligned(offset: usize, alignment: usize) AtlasCacheStorage.DeriveError!usize {
1336     const mask = alignment - 1;
1337     return (try cacheAdded(offset, mask)) & ~mask;
1338 }
1339 
1340 fn cachePlaced(comptime T: type, offset: usize, count: usize) AtlasCacheStorage.DeriveError!CacheRegion {
1341     return cachePlacedBytes(offset, @alignOf(T), try cacheMultiplied(count, @sizeOf(T)));
1342 }
1343 
1344 fn cachePlacedBytes(
1345     offset: usize,
1346     alignment: usize,
1347     byte_count: usize,
1348 ) AtlasCacheStorage.DeriveError!CacheRegion {
1349     const start = try cacheAligned(offset, alignment);
1350     return .{ .start = start, .bytes = byte_count, .end = try cacheAdded(start, byte_count) };
1351 }
1352 
1353 fn cacheTypedSlice(
1354     comptime T: type,
1355     bytes: []align(AtlasCacheStorage.storage_alignment) u8,
1356     offset: usize,
1357     count: usize,
1358 ) []T {
1359     if (count == 0) return &.{};
1360     const byte_count = count * @sizeOf(T);
1361     const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
1362     return std.mem.bytesAsSlice(T, region);
1363 }
1364 
1365 fn assertCacheAddress(base: usize, offset: usize, region: anytype) void {
1366     if (region.len == 0) return;
1367     std.debug.assert(@intFromPtr(region.ptr) == base + offset);
1368 }
1369 
1370 fn cacheStartSlot(hash: u64, slot_count: usize) usize {
1371     std.debug.assert(std.math.isPowerOfTwo(slot_count));
1372     const truncated: usize = @truncate(hash);
1373     return truncated & (slot_count - 1);
1374 }
1375 
1376 fn cacheNextSlot(slot: usize, slot_count: usize) usize {
1377     std.debug.assert(std.math.isPowerOfTwo(slot_count));
1378     return (slot + 1) & (slot_count - 1);
1379 }
1380 
1381 fn cacheEmptySlot(slots: []const u32, hash: u64) usize {
1382     var slot = cacheStartSlot(hash, slots.len);
1383     var probes: usize = 0;
1384     while (probes < slots.len) : (probes += 1) {
1385         if (slots[slot] == 0) return slot;
1386         slot = cacheNextSlot(slot, slots.len);
1387     }
1388     unreachable;
1389 }
1390 
1391 fn cacheEpochStatus(
1392     entry_capacity: usize,
1393     payload_capacity_bytes: usize,
1394     physical_payload_capacity_bytes: usize,
1395     physical_payload_bytes: usize,
1396     usage: CacheEpochUsage,
1397 ) AtlasCacheStorage.EpochStatus {
1398     return .{
1399         .entry_capacity = entry_capacity,
1400         .payload_capacity_bytes = payload_capacity_bytes,
1401         .physical_payload_capacity_bytes = physical_payload_capacity_bytes,
1402         .entries = usage.entries,
1403         .payload_bytes = usage.payload_bytes,
1404         .physical_payload_bytes = physical_payload_bytes,
1405         .high_water_entries = usage.high_water_entries,
1406         .high_water_payload_bytes = usage.high_water_payload_bytes,
1407         .high_water_physical_payload_bytes = usage.high_water_physical_payload_bytes,
1408         .rollovers = usage.rollovers,
1409         .disabled_bypasses = usage.disabled_bypasses,
1410         .oversize_bypasses = usage.oversize_bypasses,
1411     };
1412 }
1413 
1414 const CacheModelRegion = struct {
1415     start: u128,
1416     bytes: u128,
1417     end: u128,
1418 };
1419 
1420 const CacheModelLayout = struct {
1421     measure_slots: u128,
1422     measure_index: CacheModelRegion,
1423     measure_entries: CacheModelRegion,
1424     measure_payload: CacheModelRegion,
1425     shape_slots: u128,
1426     shape_index: CacheModelRegion,
1427     shape_entries: CacheModelRegion,
1428     shape_payload: CacheModelRegion,
1429 
1430     fn capacity(
1431         self: CacheModelLayout,
1432         limits: AtlasCacheStorage.Limits,
1433     ) AtlasCacheStorage.DeriveError!AtlasCacheStorage.Capacity {
1434         const values = [_]u128{
1435             self.measure_slots,
1436             self.measure_index.start,
1437             self.measure_entries.start,
1438             self.measure_payload.start,
1439             self.measure_payload.bytes,
1440             self.shape_slots,
1441             self.shape_index.start,
1442             self.shape_entries.start,
1443             self.shape_payload.start,
1444             self.shape_payload.bytes,
1445             self.shape_payload.end,
1446         };
1447         for (values) |value| {
1448             if (value > std.math.maxInt(usize)) return error.CapacityOverflow;
1449         }
1450         return .{
1451             .limits = limits,
1452             .measure_index_slots = @intCast(self.measure_slots),
1453             .measure_index_offset = @intCast(self.measure_index.start),
1454             .measure_entries_offset = @intCast(self.measure_entries.start),
1455             .measure_payload_offset = @intCast(self.measure_payload.start),
1456             .measure_payload_storage_bytes = @intCast(self.measure_payload.bytes),
1457             .shape_index_slots = @intCast(self.shape_slots),
1458             .shape_index_offset = @intCast(self.shape_index.start),
1459             .shape_entries_offset = @intCast(self.shape_entries.start),
1460             .shape_payload_offset = @intCast(self.shape_payload.start),
1461             .shape_payload_storage_bytes = @intCast(self.shape_payload.bytes),
1462             .storage_bytes = @intCast(self.shape_payload.end),
1463         };
1464     }
1465 };
1466 
1467 fn modelAtlasCacheLimitsValid(
1468     limits: AtlasCacheStorage.Limits,
1469 ) AtlasCacheStorage.DeriveError!void {
1470     const measure_disabled = limits.measure_entries == 0 and limits.measure_payload_bytes == 0;
1471     const measure_enabled = limits.measure_entries != 0 and limits.measure_payload_bytes != 0;
1472     if (!measure_disabled and !measure_enabled) return error.InvalidMeasureLimits;
1473     const shape_disabled = limits.shape_entries == 0 and limits.shape_payload_bytes == 0;
1474     const shape_enabled = limits.shape_entries != 0 and limits.shape_payload_bytes != 0;
1475     if (!shape_disabled and !shape_enabled) return error.InvalidShapeLimits;
1476     if (limits.measure_entries > std.math.maxInt(u32)) return error.EntryLimitTooLarge;
1477     if (limits.shape_entries > std.math.maxInt(u32)) return error.EntryLimitTooLarge;
1478 }
1479 
1480 fn modelAtlasCacheCapacity(
1481     limits: AtlasCacheStorage.Limits,
1482 ) AtlasCacheStorage.DeriveError!AtlasCacheStorage.Capacity {
1483     try modelAtlasCacheLimitsValid(limits);
1484     const measure_slots = modelCacheIndexSlots(limits.measure_entries);
1485     const shape_slots = modelCacheIndexSlots(limits.shape_entries);
1486     const measure_index = modelCachePlaced(0, @alignOf(u32), measure_slots, @sizeOf(u32));
1487     const measure_entries = modelCachePlaced(
1488         measure_index.end,
1489         @alignOf(MeasureCacheEntry),
1490         limits.measure_entries,
1491         @sizeOf(MeasureCacheEntry),
1492     );
1493     const measure_payload_bytes = @as(u128, limits.measure_payload_bytes) +
1494         @as(u128, limits.measure_entries) * measure_padding_per_entry;
1495     const measure_payload = modelCachePlaced(
1496         measure_entries.end,
1497         measure_payload_alignment,
1498         measure_payload_bytes,
1499         1,
1500     );
1501     const shape_index = modelCachePlaced(
1502         measure_payload.end,
1503         @alignOf(u32),
1504         shape_slots,
1505         @sizeOf(u32),
1506     );
1507     const shape_entries = modelCachePlaced(
1508         shape_index.end,
1509         @alignOf(ShapeCacheEntry),
1510         limits.shape_entries,
1511         @sizeOf(ShapeCacheEntry),
1512     );
1513     const shape_payload_bytes = @as(u128, limits.shape_payload_bytes) +
1514         @as(u128, limits.shape_entries) * shape_padding_per_entry;
1515     const shape_payload = modelCachePlaced(
1516         shape_entries.end,
1517         shape_payload_alignment,
1518         shape_payload_bytes,
1519         1,
1520     );
1521     return (CacheModelLayout{
1522         .measure_slots = measure_slots,
1523         .measure_index = measure_index,
1524         .measure_entries = measure_entries,
1525         .measure_payload = measure_payload,
1526         .shape_slots = shape_slots,
1527         .shape_index = shape_index,
1528         .shape_entries = shape_entries,
1529         .shape_payload = shape_payload,
1530     }).capacity(limits);
1531 }
1532 
1533 fn modelCacheIndexSlots(entries: usize) u128 {
1534     if (entries == 0) return 0;
1535     const minimum = @as(u128, entries) * 2;
1536     var slots: u128 = 1;
1537     var steps: usize = 0;
1538     while (steps < 64 and slots < minimum) : (steps += 1) slots *= 2;
1539     std.debug.assert(slots >= minimum);
1540     return slots;
1541 }
1542 
1543 fn modelCachePlaced(offset: u128, alignment: usize, count: u128, size: usize) CacheModelRegion {
1544     const mask = @as(u128, alignment - 1);
1545     const start = (offset + mask) & ~mask;
1546     const bytes = count * size;
1547     return .{ .start = start, .bytes = bytes, .end = start + bytes };
1548 }
1549 
1550 comptime {
1551     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(AtlasCacheStorage);
1552 }
1553 
1554 fn shapePayloadBytes(content: []const u8, run: filigree.GlyphRun) ?usize {
1555     var bytes = content.len;
1556     inline for (.{
1557         .{ filigree.ShapedGlyph, run.glyphs.len },
1558         .{ filigree.Cluster, run.clusters.len },
1559         .{ filigree.LigatureCaret, run.ligature_carets.len },
1560     }) |slice| {
1561         const slice_bytes = std.math.mul(usize, @sizeOf(slice[0]), slice[1]) catch return null;
1562         bytes = std.math.add(usize, bytes, slice_bytes) catch return null;
1563     }
1564     return bytes;
1565 }
1566 
1567 fn floatBits(value: f64) u64 {
1568     return @bitCast(value);
1569 }
1570 
1571 pub const AtlasSet = struct {
1572     entries: []const Entry = &.{},
1573     fallback_entries: []const Entry = &.{},
1574     fallback: ?*fallback_mod.Engine = null,
1575     device_scale: f32 = 1,
1576 
1577     pub const Entry = struct {
1578         face: u64,
1579         image_index: u32,
1580         atlas: *Atlas,
1581     };
1582 
1583     pub fn forText(self: *const AtlasSet, text: UiText) ?Entry {
1584         return self.forStyle(gui.model.resolvedTextStyle(text, 0));
1585     }
1586 
1587     pub fn forStyle(self: *const AtlasSet, style: UiTextStyle) ?Entry {
1588         if (self.entries.len == 0) return null;
1589         const point: f32 = @floatCast(style.point_size);
1590         const scaled_point = point * @max(self.device_scale, 1);
1591         var best: ?Entry = null;
1592         var best_face = false;
1593         var best_cost: f32 = std.math.floatMax(f32);
1594         for (self.entries) |entry| {
1595             const face_match = entry.face == style.font_asset_id;
1596             if (best_face and !face_match) continue;
1597             const cost = sizeCost(entry.atlas.pixel_size, scaled_point);
1598             if (face_match and !best_face) {
1599                 best = entry;
1600                 best_face = true;
1601                 best_cost = cost;
1602                 continue;
1603             }
1604             if (cost < best_cost) {
1605                 best = entry;
1606                 best_cost = cost;
1607             }
1608         }
1609         return best;
1610     }
1611 
1612     fn fallbackFor(self: *const AtlasSet, primary: Entry) ?Entry {
1613         if (self.fallback == null or primary.atlas.outlineFont() == null) return null;
1614         var best: ?Entry = null;
1615         var best_cost: u64 = std.math.maxInt(u64);
1616         for (self.fallback_entries) |entry| {
1617             if (entry.atlas.outlineFont() == null) continue;
1618             const cost = @abs(@as(i64, entry.atlas.pixel_size) - primary.atlas.pixel_size);
1619             if (cost < best_cost) {
1620                 best = entry;
1621                 best_cost = cost;
1622             }
1623         }
1624         return best;
1625     }
1626 
1627     fn requiresFallback(self: *const AtlasSet, primary: Entry, content: []const u8) !bool {
1628         if (self.fallbackFor(primary) == null) return false;
1629         return !(try primary.atlas.coversUtf8(content));
1630     }
1631 
1632     fn faceSegments(self: *const AtlasSet, primary: Entry, content: []const u8) !?FaceSegments {
1633         const fallback_entry = self.fallbackFor(primary) orelse return null;
1634         if (try primary.atlas.coversUtf8(content)) return null;
1635         const primary_font = primary.atlas.outlineFont().?;
1636         const fallback_font = fallback_entry.atlas.outlineFont().?;
1637         return .{
1638             .fallback_entry = fallback_entry,
1639             .items = try self.fallback.?.segments(primary_font, fallback_font, content),
1640         };
1641     }
1642 
1643     fn sizeCost(pixel_size: i32, point: f32) f32 {
1644         const size: f32 = @floatFromInt(pixel_size);
1645         if (size >= point) return size - point;
1646         return (point - size) * 4;
1647     }
1648 };
1649 
1650 const FaceSegments = struct {
1651     fallback_entry: AtlasSet.Entry,
1652     items: []const fallback_mod.FaceSegment,
1653 };
1654 
1655 fn requestedPointSize(text: UiText) f32 {
1656     if (std.math.isFinite(text.point_size) and text.point_size > 0) return @floatCast(text.point_size);
1657     return 16;
1658 }
1659 
1660 pub fn frameResolvers(atlases: *const AtlasSet) gui.frame.FrameResolvers {
1661     return .{
1662         .context = @constCast(atlases),
1663         .text_size = measureFrameText,
1664     };
1665 }
1666 
1667 pub fn measureFrameText(context: ?*anyopaque, node: *const UiNode) anyerror!?Size {
1668     const text = node.text orelse return null;
1669     if (text.content.len == 0) return null;
1670     const atlases: *const AtlasSet = @ptrCast(@alignCast(context orelse return null));
1671     return try measureSet(atlases, text);
1672 }
1673 
1674 pub fn measure(atlas: *Atlas, text: UiText) !Size {
1675     try gui.model.validateText(text);
1676     if (hasMixedTextMetrics(text)) return error.MixedTextRequiresAtlasSet;
1677     return measureCached(atlas, text, .single);
1678 }
1679 
1680 pub fn measureSet(atlases: *const AtlasSet, text: UiText) !Size {
1681     try gui.model.validateText(text);
1682     const entry = atlases.forText(text) orelse return error.MissingTextAtlas;
1683     const composite = hasMixedTextMetrics(text) or try atlases.requiresFallback(entry, text.content);
1684     return measureCached(
1685         entry.atlas,
1686         text,
1687         if (composite) .{ .composite = atlases } else .single,
1688     );
1689 }
1690 
1691 pub fn textAdvanceForByteOffset(
1692     atlases: *const AtlasSet,
1693     text: UiText,
1694     byte_offset: usize,
1695 ) !f32 {
1696     const line = try textCaretPlan(atlases, text);
1697     return filigree.caret.advanceForByteOffset(
1698         line.plan.run,
1699         byte_offset,
1700         text.content.len,
1701     ) * line.scale;
1702 }
1703 
1704 pub fn textHitTestAdvance(
1705     atlases: *const AtlasSet,
1706     text: UiText,
1707     advance: f32,
1708 ) !filigree.caret.LineHit {
1709     const line = try textCaretPlan(atlases, text);
1710     var hit = filigree.caret.hitTestAdvance(
1711         line.plan.run,
1712         advance / line.scale,
1713         text.content,
1714     );
1715     hit.advance *= line.scale;
1716     return hit;
1717 }
1718 
1719 pub const TextCaretAffinity = enum {
1720     upstream,
1721     downstream,
1722 };
1723 
1724 pub const TextCaretGeometry = struct {
1725     byte_offset: usize,
1726     affinity: TextCaretAffinity,
1727     line_index: usize,
1728     x: f32,
1729     y: f32,
1730     height: f32,
1731 };
1732 
1733 pub fn textCaretGeometry(
1734     atlases: *const AtlasSet,
1735     text: UiText,
1736     box: Size,
1737     byte_offset: usize,
1738     affinity: TextCaretAffinity,
1739 ) !TextCaretGeometry {
1740     const layout = try TextLayout.init(atlases, text, .{
1741         .width = box.width,
1742         .height = box.height,
1743     }, 1);
1744     const target = @min(byte_offset, text.content.len);
1745     var iterator = layout.iterator();
1746     while (try iterator.next()) |line| {
1747         if (target < line.hard_line_start) continue;
1748         if (!line.visual.containsCaret(target - line.hard_line_start, affinity)) continue;
1749         return line.caret(target, affinity);
1750     }
1751     return error.MissingTextCaret;
1752 }
1753 
1754 pub fn textHitTestPoint(
1755     atlases: *const AtlasSet,
1756     text: UiText,
1757     box: Size,
1758     point: gui.model.UiPoint,
1759 ) !TextCaretGeometry {
1760     const layout = try TextLayout.init(atlases, text, .{
1761         .width = box.width,
1762         .height = box.height,
1763     }, 1);
1764     var iterator = layout.iterator();
1765     var candidate: ?TextCaretGeometry = null;
1766     while (try iterator.next()) |line| {
1767         const hit = line.hit(point.x);
1768         if (point.y < line.y + line.height) return hit;
1769         candidate = hit;
1770     }
1771     return candidate orelse error.MissingTextCaret;
1772 }
1773 
1774 pub fn textHitTestWidgetPoint(
1775     atlases: *const AtlasSet,
1776     widget: WidgetFrame,
1777     point: gui.model.UiPoint,
1778 ) !?TextCaretGeometry {
1779     if (!gui.model.pointInRect(widget.rect, point.x, point.y)) return null;
1780     if (!gui.model.pointInRect(widget.visible_rect, point.x, point.y)) return null;
1781     return try textHitTestWidgetLocalPoint(atlases, widget, point);
1782 }
1783 
1784 pub fn textHitTestWidgetPointClamped(
1785     atlases: *const AtlasSet,
1786     widget: WidgetFrame,
1787     point: gui.model.UiPoint,
1788 ) !?TextCaretGeometry {
1789     if (!std.math.isFinite(point.x) or !std.math.isFinite(point.y)) return null;
1790     const left = @max(widget.rect.x, widget.visible_rect.x);
1791     const top = @max(widget.rect.y, widget.visible_rect.y);
1792     const right = @min(
1793         widget.rect.x + widget.rect.width,
1794         widget.visible_rect.x + widget.visible_rect.width,
1795     );
1796     const bottom = @min(
1797         widget.rect.y + widget.rect.height,
1798         widget.visible_rect.y + widget.visible_rect.height,
1799     );
1800     if (right <= left or bottom <= top) return null;
1801     return try textHitTestWidgetLocalPoint(atlases, widget, .{
1802         .x = std.math.clamp(point.x, left, right),
1803         .y = std.math.clamp(point.y, top, bottom),
1804     });
1805 }
1806 
1807 fn textHitTestWidgetLocalPoint(
1808     atlases: *const AtlasSet,
1809     widget: WidgetFrame,
1810     point: gui.model.UiPoint,
1811 ) !?TextCaretGeometry {
1812     const text = widget.text orelse return null;
1813     return try textHitTestPoint(
1814         atlases,
1815         text,
1816         .{ .width = widget.rect.width, .height = widget.rect.height },
1817         .{
1818             .x = point.x - widget.rect.x,
1819             .y = point.y - widget.rect.y,
1820         },
1821     );
1822 }
1823 
1824 const TextCaretPlan = struct {
1825     plan: LinePlan,
1826     scale: f32,
1827 };
1828 
1829 fn textCaretPlan(atlases: *const AtlasSet, text: UiText) !TextCaretPlan {
1830     try gui.model.validateText(text);
1831     if (std.mem.indexOfScalar(u8, text.content, '\n') != null) return error.MultilineText;
1832     const entry = atlases.forText(text) orelse return error.MissingTextAtlas;
1833     const scale = textScale(entry.atlas, text);
1834     const composite = hasMixedTextMetrics(text) or try atlases.requiresFallback(entry, text.content);
1835     const plan = if (composite)
1836         try LinePlan.initComposite(
1837             entry.atlas,
1838             atlases,
1839             text,
1840             text.content,
1841             0,
1842             0,
1843         )
1844     else
1845         try LinePlan.init(entry.atlas, text.content, 0);
1846     return .{ .plan = plan, .scale = scale };
1847 }
1848 
1849 const MeasureMode = union(enum) {
1850     single,
1851     composite: *const AtlasSet,
1852 };
1853 
1854 fn measureCached(atlas: *Atlas, text: UiText, mode: MeasureMode) !Size {
1855     const metric_atlases: ?*const AtlasSet = switch (mode) {
1856         .single => null,
1857         .composite => |atlases| atlases,
1858     };
1859     const key = MeasureKey.fromText(text, metric_atlases);
1860     if (atlas.cache.lookupMeasure(key)) |cached| return cached;
1861     const measured = switch (mode) {
1862         .single => try measureUncached(atlas, text),
1863         .composite => |atlases| try measureCompositeUncached(atlas, atlases, text),
1864     };
1865     return atlas.cache.storeMeasure(key, measured) catch measured;
1866 }
1867 
1868 fn measurePayloadBytes(key: MeasureKey) ?usize {
1869     var bytes = key.content.len;
1870     bytes = std.math.add(usize, bytes, std.math.mul(usize, key.styles.len, @sizeOf(UiTextStyle)) catch return null) catch return null;
1871     bytes = std.math.add(usize, bytes, std.math.mul(usize, key.runs.len, @sizeOf(UiTextRun)) catch return null) catch return null;
1872     bytes = std.math.add(usize, bytes, std.math.mul(usize, key.atlas_entries.len, @sizeOf(AtlasSet.Entry)) catch return null) catch return null;
1873     bytes = std.math.add(usize, bytes, std.math.mul(usize, key.fallback_entries.len, @sizeOf(AtlasSet.Entry)) catch return null) catch return null;
1874     return bytes;
1875 }
1876 
1877 fn hasMixedTextMetrics(text: UiText) bool {
1878     const base = gui.model.resolvedTextStyle(text, 0);
1879     for (text.runs) |run| {
1880         if (!gui.model.textStylesEqual(base, gui.model.resolvedTextStyle(text, run.style_slot))) return true;
1881     }
1882     return false;
1883 }
1884 
1885 const VisualLine = struct {
1886     glyph_start: usize,
1887     glyph_end: usize,
1888     byte_start: usize,
1889     byte_end: usize,
1890     advance_start: f32,
1891     advance: f32,
1892     is_last: bool,
1893 
1894     fn glyphs(self: VisualLine, run: filigree.GlyphRun) []const filigree.ShapedGlyph {
1895         return run.glyphs[self.glyph_start..self.glyph_end];
1896     }
1897 
1898     fn advanceForByteOffset(self: VisualLine, run: filigree.GlyphRun, byte_offset: usize, text_len: usize) f32 {
1899         const absolute = filigree.caret.advanceForByteOffset(run, byte_offset, text_len);
1900         return std.math.clamp(absolute - self.advance_start, @as(f32, 0), self.advance);
1901     }
1902 
1903     fn containsCaret(
1904         self: VisualLine,
1905         byte_offset: usize,
1906         affinity: TextCaretAffinity,
1907     ) bool {
1908         if (byte_offset < self.byte_start) return false;
1909         if (byte_offset < self.byte_end) return true;
1910         if (byte_offset != self.byte_end) return false;
1911         return self.is_last or affinity == .upstream;
1912     }
1913 };
1914 
1915 const LinePlan = struct {
1916     content: []const u8,
1917     run: filigree.GlyphRun,
1918     glyph_ends: []const u32,
1919     wrapped: bool,
1920     glyph_sources: []const CompositeGlyphSource = &.{},
1921     metric_spans: []const CompositeMetricSpan = &.{},
1922     base_metric: ?CompositeMetricSpan = null,
1923 
1924     fn init(atlas: *Atlas, content: []const u8, wrap_width: f32) !LinePlan {
1925         const run = try atlas.shape(content);
1926         if (wrap_width <= 0) {
1927             return .{
1928                 .content = content,
1929                 .run = run,
1930                 .glyph_ends = &.{},
1931                 .wrapped = false,
1932             };
1933         }
1934         const glyph_ends = try filigree.breakLinesInto(
1935             atlas.allocator,
1936             run.glyphs,
1937             .{ .utf8 = content },
1938             wrap_width,
1939             &atlas.line_break_workspace,
1940         );
1941         std.debug.assert(glyph_ends.len > 0);
1942         std.debug.assert(glyph_ends.len <= run.glyphs.len + 1);
1943         return .{
1944             .content = content,
1945             .run = run,
1946             .glyph_ends = glyph_ends,
1947             .wrapped = true,
1948         };
1949     }
1950 
1951     fn initComposite(
1952         base_atlas: *Atlas,
1953         atlases: *const AtlasSet,
1954         text: UiText,
1955         content: []const u8,
1956         content_start: usize,
1957         wrap_width: f32,
1958     ) !LinePlan {
1959         const content_end = std.math.add(usize, content_start, content.len) catch
1960             return error.SourceTooLong;
1961         if (content_end > text.content.len) return error.SourceTooLong;
1962         if (content.len > std.math.maxInt(u32)) return error.SourceTooLong;
1963         const base_style = gui.model.resolvedTextStyle(text, 0);
1964         const base_entry = atlases.forStyle(base_style) orelse return error.MissingTextAtlas;
1965         const base_scale = textStyleScale(base_entry.atlas, base_style);
1966         if (!(base_scale > 0)) return error.InvalidTextScale;
1967         const base_metric = compositeMetricSpan(
1968             base_entry,
1969             base_style,
1970             base_scale,
1971             0,
1972             @intCast(content.len),
1973         );
1974         const workspace = &base_atlas.composite_workspace;
1975         workspace.clear();
1976         errdefer workspace.clear();
1977         var cursor = MetricSegmentCursor{
1978             .text = text,
1979             .position = content_start,
1980             .end = content_end,
1981         };
1982         while (cursor.next()) |segment| {
1983             const entry = atlases.forStyle(segment.style) orelse return error.MissingTextAtlas;
1984             const local_start = segment.byte_start - content_start;
1985             const local_end = segment.byte_end - content_start;
1986             if (local_start > std.math.maxInt(u32) or local_end > std.math.maxInt(u32)) {
1987                 return error.SourceTooLong;
1988             }
1989             try appendMetricSegment(
1990                 workspace,
1991                 base_atlas.allocator,
1992                 atlases,
1993                 entry,
1994                 segment.style,
1995                 base_scale,
1996                 content[local_start..local_end],
1997                 local_start,
1998             );
1999         }
2000         var total_x: i32 = 0;
2001         var total_y: i32 = 0;
2002         for (workspace.glyphs.items) |glyph| {
2003             total_x = std.math.add(i32, total_x, glyph.x_advance) catch
2004                 return error.TextGeometryOverflow;
2005             total_y = std.math.add(i32, total_y, glyph.y_advance) catch
2006                 return error.TextGeometryOverflow;
2007         }
2008         const run = filigree.GlyphRun{
2009             .glyphs = workspace.glyphs.items,
2010             .clusters = workspace.clusters.items,
2011             .ligature_carets = workspace.ligature_carets.items,
2012             .total_x_advance = total_x,
2013             .total_y_advance = total_y,
2014             .direction = .ltr,
2015             .writing_mode = .horizontal,
2016             .output_order = .visual,
2017         };
2018         const glyph_ends = if (wrap_width > 0) try filigree.breakLinesInto(
2019             base_atlas.allocator,
2020             run.glyphs,
2021             .{ .utf8 = content },
2022             wrap_width,
2023             &base_atlas.line_break_workspace,
2024         ) else &.{};
2025         if (wrap_width > 0) {
2026             std.debug.assert(glyph_ends.len > 0);
2027             std.debug.assert(glyph_ends.len <= run.glyphs.len + 1);
2028         }
2029         return .{
2030             .content = content,
2031             .run = run,
2032             .glyph_ends = glyph_ends,
2033             .wrapped = wrap_width > 0,
2034             .glyph_sources = workspace.glyph_sources.items,
2035             .metric_spans = workspace.spans.items,
2036             .base_metric = base_metric,
2037         };
2038     }
2039 
2040     fn iterator(_: *const LinePlan) Iterator {
2041         return .{};
2042     }
2043 
2044     fn compositeLineBox(self: *const LinePlan, visual: VisualLine, multiplier: f32) CompositeLineBox {
2045         var ascent: f32 = 0;
2046         var descent: f32 = 0;
2047         var requested: f32 = 0;
2048         for (self.metric_spans) |span| {
2049             if (span.byte_end <= visual.byte_start or span.byte_start >= visual.byte_end) continue;
2050             ascent = @max(ascent, span.ascent);
2051             descent = @max(descent, span.descent);
2052             requested = @max(requested, span.point_size * multiplier);
2053         }
2054         if (ascent == 0 and descent == 0) {
2055             const base = self.base_metric.?;
2056             ascent = base.ascent;
2057             descent = base.descent;
2058             requested = base.point_size * multiplier;
2059         }
2060         const font_height = ascent + descent;
2061         const height = @max(requested, font_height);
2062         return .{
2063             .ascent = ascent,
2064             .descent = descent,
2065             .height = height,
2066             .half_leading = @max(0, height - font_height) / 2,
2067         };
2068     }
2069 
2070     const Iterator = struct {
2071         break_index: usize = 0,
2072         glyph_start: usize = 0,
2073         advance_start: f32 = 0,
2074 
2075         fn next(self: *Iterator, plan: *const LinePlan) ?VisualLine {
2076             const line_count = if (plan.wrapped) plan.glyph_ends.len else 1;
2077             if (self.break_index >= line_count) return null;
2078             const raw_end = if (plan.wrapped)
2079                 plan.glyph_ends[self.break_index]
2080             else
2081                 plan.run.glyphs.len;
2082             const glyph_end = @min(@as(usize, @intCast(raw_end)), plan.run.glyphs.len);
2083             std.debug.assert(glyph_end >= self.glyph_start);
2084             const advance = glyphSliceAdvance(plan.run.glyphs[self.glyph_start..glyph_end]);
2085             const byte_start = if (self.glyph_start == 0)
2086                 0
2087             else
2088                 sourceOffset(plan.run.glyphs[self.glyph_start].source_start, plan.content.len);
2089             const byte_end = if (glyph_end >= plan.run.glyphs.len)
2090                 plan.content.len
2091             else
2092                 sourceOffset(plan.run.glyphs[glyph_end].source_start, plan.content.len);
2093             const visual = VisualLine{
2094                 .glyph_start = self.glyph_start,
2095                 .glyph_end = glyph_end,
2096                 .byte_start = byte_start,
2097                 .byte_end = @max(byte_start, byte_end),
2098                 .advance_start = self.advance_start,
2099                 .advance = advance,
2100                 .is_last = self.break_index + 1 == line_count,
2101             };
2102             self.glyph_start = glyph_end;
2103             self.advance_start += advance;
2104             self.break_index += 1;
2105             return visual;
2106         }
2107     };
2108 };
2109 
2110 const CompositeLineBox = struct {
2111     ascent: f32,
2112     descent: f32,
2113     height: f32,
2114     half_leading: f32,
2115 };
2116 
2117 const TextLayout = struct {
2118     atlases: *const AtlasSet,
2119     text: UiText,
2120     rect: Rect,
2121     base_atlas: *Atlas,
2122     scale: f32,
2123     single_line_height: f32,
2124     single_half_leading: f32,
2125     line_height_multiplier: f32,
2126     plan_wrap_width: f32,
2127     block_y: f32,
2128     composite: bool,
2129 
2130     fn init(
2131         atlases: *const AtlasSet,
2132         text: UiText,
2133         rect: Rect,
2134         unit_scale: f32,
2135     ) !TextLayout {
2136         try gui.model.validateText(text);
2137         if (!std.math.isFinite(unit_scale) or unit_scale <= 0) {
2138             return error.InvalidTextScale;
2139         }
2140         const entry = atlases.forText(text) orelse return error.MissingTextAtlas;
2141         const base_atlas = entry.atlas;
2142         const scale = textScale(base_atlas, text) * unit_scale;
2143         if (!std.math.isFinite(scale) or scale <= 0) return error.InvalidTextScale;
2144         const line_height = textLineHeight(base_atlas, text) * unit_scale;
2145         const half_leading = @max(
2146             0,
2147             line_height - base_atlas.metrics().height() * scale,
2148         ) / 2;
2149         const wrap_width = textWrapWidth(text) * unit_scale;
2150         const measured_height = if (text.vertical_align == .start)
2151             0
2152         else
2153             (try measureSet(atlases, text)).height * unit_scale;
2154         const composite = hasMixedTextMetrics(text) or try atlases.requiresFallback(entry, text.content);
2155         return .{
2156             .atlases = atlases,
2157             .text = text,
2158             .rect = rect,
2159             .base_atlas = base_atlas,
2160             .scale = scale,
2161             .single_line_height = line_height,
2162             .single_half_leading = half_leading,
2163             .line_height_multiplier = textLineHeightMultiplier(text),
2164             .plan_wrap_width = if (wrap_width > 0) wrap_width / scale else 0,
2165             .block_y = if (text.vertical_align == .start)
2166                 rect.y
2167             else
2168                 alignedStart(
2169                     rect.y,
2170                     rect.height,
2171                     measured_height,
2172                     text.vertical_align,
2173                 ),
2174             .composite = composite,
2175         };
2176     }
2177 
2178     fn iterator(self: *const TextLayout) Iterator {
2179         return .{
2180             .layout = self,
2181             .line_y = self.block_y,
2182         };
2183     }
2184 
2185     const Iterator = struct {
2186         layout: *const TextLayout,
2187         next_hard_line_start: usize = 0,
2188         hard_lines_done: bool = false,
2189         has_plan: bool = false,
2190         current_hard_line_start: usize = 0,
2191         current_hard_line: []const u8 = &.{},
2192         current_plan: LinePlan = undefined,
2193         visual_iterator: LinePlan.Iterator = .{},
2194         line_index: usize = 0,
2195         line_y: f32,
2196 
2197         fn next(self: *Iterator) !?TextLayoutLine {
2198             while (true) {
2199                 if (self.has_plan) {
2200                     if (self.visual_iterator.next(&self.current_plan)) |visual| {
2201                         const box = if (self.layout.composite)
2202                             self.current_plan.compositeLineBox(
2203                                 visual,
2204                                 self.layout.line_height_multiplier,
2205                             )
2206                         else
2207                             CompositeLineBox{
2208                                 .ascent = self.layout.base_atlas.metrics().ascent,
2209                                 .descent = self.layout.base_atlas.metrics().descent,
2210                                 .height = self.layout.single_line_height / self.layout.scale,
2211                                 .half_leading = self.layout.single_half_leading / self.layout.scale,
2212                             };
2213                         const line_height = if (self.layout.composite)
2214                             box.height * self.layout.scale
2215                         else
2216                             self.layout.single_line_height;
2217                         const line_width = visual.advance * self.layout.scale;
2218                         const line = TextLayoutLine{
2219                             .plan = &self.current_plan,
2220                             .visual = visual,
2221                             .hard_line = self.current_hard_line,
2222                             .hard_line_start = self.current_hard_line_start,
2223                             .line_index = self.line_index,
2224                             .origin_x = alignedStart(
2225                                 self.layout.rect.x,
2226                                 self.layout.rect.width,
2227                                 line_width,
2228                                 self.layout.text.horizontal_align,
2229                             ),
2230                             .origin_y = self.line_y + box.half_leading * self.layout.scale,
2231                             .y = self.line_y,
2232                             .height = line_height,
2233                             .scale = self.layout.scale,
2234                             .ascent = box.ascent,
2235                             .descent = box.descent,
2236                         };
2237                         self.line_y += line_height;
2238                         self.line_index += 1;
2239                         return line;
2240                     }
2241                     self.has_plan = false;
2242                 }
2243                 if (!try self.loadHardLine()) return null;
2244             }
2245         }
2246 
2247         fn loadHardLine(self: *Iterator) !bool {
2248             if (self.hard_lines_done) return false;
2249             const content = self.layout.text.content;
2250             const start = self.next_hard_line_start;
2251             const relative_end = std.mem.indexOfScalar(
2252                 u8,
2253                 content[start..],
2254                 '\n',
2255             );
2256             const end = if (relative_end) |offset| start + offset else content.len;
2257             if (end < content.len) {
2258                 self.next_hard_line_start = end + 1;
2259             } else {
2260                 self.hard_lines_done = true;
2261             }
2262             const hard_line = content[start..end];
2263             self.current_hard_line_start = start;
2264             self.current_hard_line = hard_line;
2265             self.current_plan = if (self.layout.composite)
2266                 try LinePlan.initComposite(
2267                     self.layout.base_atlas,
2268                     self.layout.atlases,
2269                     self.layout.text,
2270                     hard_line,
2271                     start,
2272                     self.layout.plan_wrap_width,
2273                 )
2274             else
2275                 try LinePlan.init(
2276                     self.layout.base_atlas,
2277                     hard_line,
2278                     self.layout.plan_wrap_width,
2279                 );
2280             self.visual_iterator = .{};
2281             self.has_plan = true;
2282             return true;
2283         }
2284     };
2285 };
2286 
2287 const TextLayoutLine = struct {
2288     plan: *const LinePlan,
2289     visual: VisualLine,
2290     hard_line: []const u8,
2291     hard_line_start: usize,
2292     line_index: usize,
2293     origin_x: f32,
2294     origin_y: f32,
2295     y: f32,
2296     height: f32,
2297     scale: f32,
2298     ascent: f32,
2299     descent: f32,
2300 
2301     fn caret(
2302         self: TextLayoutLine,
2303         byte_offset: usize,
2304         affinity: TextCaretAffinity,
2305     ) TextCaretGeometry {
2306         const local_offset = byte_offset - self.hard_line_start;
2307         return .{
2308             .byte_offset = byte_offset,
2309             .affinity = affinity,
2310             .line_index = self.line_index,
2311             .x = self.origin_x + self.visual.advanceForByteOffset(
2312                 self.plan.run,
2313                 local_offset,
2314                 self.plan.content.len,
2315             ) * self.scale,
2316             .y = self.y,
2317             .height = self.height,
2318         };
2319     }
2320 
2321     fn hit(self: TextLayoutLine, x: f32) TextCaretGeometry {
2322         const absolute_advance = self.visual.advance_start +
2323             (x - self.origin_x) / self.scale;
2324         const raw = filigree.caret.hitTestAdvance(
2325             self.plan.run,
2326             absolute_advance,
2327             self.plan.content,
2328         );
2329         const local_offset = std.math.clamp(
2330             raw.byte_offset,
2331             self.visual.byte_start,
2332             self.visual.byte_end,
2333         );
2334         const affinity: TextCaretAffinity = if (!self.visual.is_last and
2335             local_offset == self.visual.byte_end)
2336             .upstream
2337         else
2338             .downstream;
2339         return self.caret(self.hard_line_start + local_offset, affinity);
2340     }
2341 };
2342 
2343 const MetricSegment = struct {
2344     byte_start: usize,
2345     byte_end: usize,
2346     style: UiTextStyle,
2347 };
2348 
2349 const MetricSegmentCursor = struct {
2350     text: UiText,
2351     position: usize,
2352     end: usize,
2353 
2354     fn next(self: *MetricSegmentCursor) ?MetricSegment {
2355         if (self.position >= self.end) return null;
2356         const byte_start = self.position;
2357         const style = textStyleAt(self.text, byte_start);
2358         var byte_end = nextTextMetricBoundary(self.text, byte_start, self.end);
2359         while (byte_end < self.end and
2360             gui.model.textStylesEqual(style, textStyleAt(self.text, byte_end)))
2361         {
2362             byte_end = nextTextMetricBoundary(self.text, byte_end, self.end);
2363         }
2364         std.debug.assert(byte_end > byte_start);
2365         self.position = byte_end;
2366         return .{
2367             .byte_start = byte_start,
2368             .byte_end = byte_end,
2369             .style = style,
2370         };
2371     }
2372 };
2373 
2374 fn textStyleAt(text: UiText, byte_offset: usize) UiTextStyle {
2375     const run = findTextRun(text.runs, byte_offset) orelse
2376         return gui.model.resolvedTextStyle(text, 0);
2377     return gui.model.resolvedTextStyle(text, run.style_slot);
2378 }
2379 
2380 fn nextTextMetricBoundary(text: UiText, byte_offset: usize, end: usize) usize {
2381     const index = firstRunEndingAfter(text.runs, byte_offset);
2382     if (index >= text.runs.len) return end;
2383     const run = text.runs[index];
2384     if (byte_offset < run.byte_start) return @min(end, run.byte_start);
2385     return @min(end, run.byte_end);
2386 }
2387 
2388 fn textStyleScale(atlas: *const Atlas, style: UiTextStyle) f32 {
2389     if (atlas.pixel_size <= 0) return 1;
2390     return @as(f32, @floatCast(style.point_size)) /
2391         @as(f32, @floatFromInt(atlas.pixel_size));
2392 }
2393 
2394 fn appendMetricSegment(
2395     workspace: *CompositeWorkspace,
2396     allocator: Allocator,
2397     atlases: *const AtlasSet,
2398     primary: AtlasSet.Entry,
2399     style: UiTextStyle,
2400     base_scale: f32,
2401     content: []const u8,
2402     content_start: usize,
2403 ) !void {
2404     const segmented = try atlases.faceSegments(primary, content) orelse {
2405         return appendStyledFaceSegment(
2406             workspace,
2407             allocator,
2408             primary,
2409             style,
2410             base_scale,
2411             content,
2412             content_start,
2413         );
2414     };
2415     for (segmented.items) |segment| {
2416         const start: usize = @intCast(segment.source.start);
2417         const end: usize = @intCast(segment.source.end);
2418         if (start > end or end > content.len) return error.SourceTooLong;
2419         const entry = try entryForFaceSegment(
2420             primary,
2421             segmented.fallback_entry,
2422             segment.candidate_index,
2423             segment.missing_everywhere,
2424         );
2425         try appendLookupOrderedFaceSegment(
2426             workspace,
2427             allocator,
2428             primary,
2429             entry,
2430             style,
2431             base_scale,
2432             content[start..end],
2433             try std.math.add(usize, content_start, start),
2434         );
2435     }
2436 }
2437 
2438 fn entryForFaceSegment(
2439     primary: AtlasSet.Entry,
2440     fallback: AtlasSet.Entry,
2441     candidate_index: usize,
2442     missing_everywhere: bool,
2443 ) !AtlasSet.Entry {
2444     if (candidate_index == 1) return fallback;
2445     if (candidate_index != 0) return error.InvalidFallbackCandidate;
2446     return if (missing_everywhere) fallback else primary;
2447 }
2448 
2449 fn appendLookupOrderedFaceSegment(
2450     workspace: *CompositeWorkspace,
2451     allocator: Allocator,
2452     primary: AtlasSet.Entry,
2453     selected: AtlasSet.Entry,
2454     style: UiTextStyle,
2455     base_scale: f32,
2456     content: []const u8,
2457     content_start: usize,
2458 ) !void {
2459     if (selected.atlas == primary.atlas or content.len == 0) {
2460         return appendStyledFaceSegment(
2461             workspace,
2462             allocator,
2463             selected,
2464             style,
2465             base_scale,
2466             content,
2467             content_start,
2468         );
2469     }
2470     const append = OrderedFaceAppend{
2471         .workspace = workspace,
2472         .allocator = allocator,
2473         .primary = primary,
2474         .selected = selected,
2475         .style = style,
2476         .base_scale = base_scale,
2477         .content = content,
2478         .content_start = content_start,
2479     };
2480     var iterator = try filigree.unicode.SourceIterator.init(.{ .utf8 = content }, 0);
2481     var grapheme: filigree.unicode.GraphemeState = .{};
2482     var cluster_start: usize = 0;
2483     var cluster_first: u21 = 0;
2484     var cluster_count: usize = 0;
2485     var runs = OrderedRunState{};
2486     while (try iterator.next()) |scalar| {
2487         if (grapheme.consume(scalar.codepoint)) {
2488             cluster_count += 1;
2489             continue;
2490         }
2491         if (cluster_count > 0) {
2492             try runs.accept(append, cluster_start, cluster_count == 1 and synth.covered(cluster_first));
2493         }
2494         cluster_start = @intCast(scalar.source.start);
2495         cluster_first = scalar.codepoint;
2496         cluster_count = 1;
2497     }
2498     std.debug.assert(cluster_count > 0);
2499     try runs.accept(append, cluster_start, cluster_count == 1 and synth.covered(cluster_first));
2500     return append.run(runs.start, content.len, runs.synthetic.?);
2501 }
2502 
2503 const OrderedFaceAppend = struct {
2504     workspace: *CompositeWorkspace,
2505     allocator: Allocator,
2506     primary: AtlasSet.Entry,
2507     selected: AtlasSet.Entry,
2508     style: UiTextStyle,
2509     base_scale: f32,
2510     content: []const u8,
2511     content_start: usize,
2512 
2513     fn run(self: OrderedFaceAppend, start: usize, end: usize, synthetic: bool) !void {
2514         std.debug.assert(start < end);
2515         std.debug.assert(end <= self.content.len);
2516         return appendStyledFaceSegment(
2517             self.workspace,
2518             self.allocator,
2519             if (synthetic) self.primary else self.selected,
2520             self.style,
2521             self.base_scale,
2522             self.content[start..end],
2523             std.math.add(usize, self.content_start, start) catch return error.SourceTooLong,
2524         );
2525     }
2526 };
2527 
2528 const OrderedRunState = struct {
2529     start: usize = 0,
2530     synthetic: ?bool = null,
2531 
2532     fn accept(self: *OrderedRunState, append: OrderedFaceAppend, cluster_start: usize, synthetic: bool) !void {
2533         if (self.synthetic) |current| {
2534             if (current != synthetic) {
2535                 try append.run(self.start, cluster_start, current);
2536                 self.start = cluster_start;
2537             }
2538         }
2539         self.synthetic = synthetic;
2540     }
2541 };
2542 
2543 fn appendStyledFaceSegment(
2544     workspace: *CompositeWorkspace,
2545     allocator: Allocator,
2546     entry: AtlasSet.Entry,
2547     style: UiTextStyle,
2548     base_scale: f32,
2549     content: []const u8,
2550     content_start: usize,
2551 ) !void {
2552     if (content_start > std.math.maxInt(u32) or content.len > std.math.maxInt(u32) - content_start) {
2553         return error.SourceTooLong;
2554     }
2555     const scale_ratio = textStyleScale(entry.atlas, style) / base_scale;
2556     if (!std.math.isFinite(scale_ratio) or scale_ratio <= 0) return error.InvalidTextScale;
2557     const span_index = workspace.spans.items.len;
2558     if (span_index > std.math.maxInt(u32)) return error.SourceTooLong;
2559     const content_end = content_start + content.len;
2560     try workspace.spans.append(allocator, compositeMetricSpan(
2561         entry,
2562         style,
2563         base_scale,
2564         @intCast(content_start),
2565         @intCast(content_end),
2566     ));
2567     try appendCompositeSegment(
2568         workspace,
2569         allocator,
2570         try entry.atlas.shape(content),
2571         @intCast(content_start),
2572         @intCast(span_index),
2573         scale_ratio,
2574     );
2575 }
2576 
2577 fn compositeMetricSpan(
2578     entry: AtlasSet.Entry,
2579     style: UiTextStyle,
2580     base_scale: f32,
2581     byte_start: u32,
2582     byte_end: u32,
2583 ) CompositeMetricSpan {
2584     const scale_ratio = textStyleScale(entry.atlas, style) / base_scale;
2585     const metrics = entry.atlas.metrics();
2586     return .{
2587         .byte_start = byte_start,
2588         .byte_end = byte_end,
2589         .atlas = entry.atlas,
2590         .image_index = entry.image_index,
2591         .scale_ratio = scale_ratio,
2592         .point_size = @as(f32, @floatCast(style.point_size)) / base_scale,
2593         .ascent = metrics.ascent * scale_ratio,
2594         .descent = metrics.descent * scale_ratio,
2595     };
2596 }
2597 
2598 fn appendCompositeSegment(
2599     workspace: *CompositeWorkspace,
2600     allocator: Allocator,
2601     run: filigree.GlyphRun,
2602     source_base: u32,
2603     segment_index: u32,
2604     scale_ratio: f32,
2605 ) !void {
2606     const glyph_base = workspace.glyphs.items.len;
2607     const cluster_base = workspace.clusters.items.len;
2608     const caret_base = workspace.ligature_carets.items.len;
2609     try workspace.glyphs.ensureUnusedCapacity(allocator, run.glyphs.len);
2610     try workspace.clusters.ensureUnusedCapacity(allocator, run.clusters.len);
2611     try workspace.ligature_carets.ensureUnusedCapacity(allocator, run.ligature_carets.len);
2612     try workspace.glyph_sources.ensureUnusedCapacity(allocator, run.glyphs.len);
2613     for (run.ligature_carets) |source| {
2614         var caret = source;
2615         caret.x_offset = try scaleCompositeValue(caret.x_offset, scale_ratio);
2616         workspace.ligature_carets.appendAssumeCapacity(caret);
2617     }
2618     for (run.clusters) |source| {
2619         var cluster = source;
2620         cluster.source.start = try addCompositeIndex(cluster.source.start, source_base);
2621         cluster.source.end = try addCompositeIndex(cluster.source.end, source_base);
2622         cluster.glyphs.start = try addCompositeIndex(cluster.glyphs.start, glyph_base);
2623         cluster.glyphs.end = try addCompositeIndex(cluster.glyphs.end, glyph_base);
2624         workspace.clusters.appendAssumeCapacity(cluster);
2625     }
2626     for (run.glyphs) |source| {
2627         var glyph = source;
2628         glyph.cluster = try addCompositeIndex(glyph.cluster, source_base);
2629         glyph.cluster_index = try addCompositeIndex(glyph.cluster_index, cluster_base);
2630         glyph.source_start = try addCompositeIndex(glyph.source_start, source_base);
2631         glyph.source_end = try addCompositeIndex(glyph.source_end, source_base);
2632         glyph.x_advance = try scaleCompositeValue(glyph.x_advance, scale_ratio);
2633         glyph.y_advance = try scaleCompositeValue(glyph.y_advance, scale_ratio);
2634         glyph.x_offset = try scaleCompositeValue(glyph.x_offset, scale_ratio);
2635         glyph.y_offset = try scaleCompositeValue(glyph.y_offset, scale_ratio);
2636         if (glyph.attachment) |*attachment| {
2637             attachment.target_glyph_index = try addCompositeIndex(
2638                 attachment.target_glyph_index,
2639                 glyph_base,
2640             );
2641         }
2642         if (glyph.ligature_caret_count > 0) {
2643             glyph.ligature_caret_start = try addCompositeIndex(
2644                 glyph.ligature_caret_start,
2645                 caret_base,
2646             );
2647         }
2648         workspace.glyphs.appendAssumeCapacity(glyph);
2649         workspace.glyph_sources.appendAssumeCapacity(.{ .segment_index = segment_index });
2650     }
2651 }
2652 
2653 fn addCompositeIndex(value: u32, base: anytype) !u32 {
2654     const wide = std.math.add(u64, value, @intCast(base)) catch
2655         return error.SourceTooLong;
2656     if (wide > std.math.maxInt(u32)) return error.SourceTooLong;
2657     return @intCast(wide);
2658 }
2659 
2660 fn scaleCompositeValue(value: i32, ratio: f32) !i32 {
2661     const scaled = @as(f64, @floatFromInt(value)) * @as(f64, ratio);
2662     if (!std.math.isFinite(scaled) or
2663         scaled < @as(f64, @floatFromInt(std.math.minInt(i32))) or
2664         scaled > @as(f64, @floatFromInt(std.math.maxInt(i32))))
2665     {
2666         return error.TextGeometryOverflow;
2667     }
2668     return @intFromFloat(@round(scaled));
2669 }
2670 
2671 fn measureUncached(atlas: *Atlas, text: UiText) !Size {
2672     const scale = textScale(atlas, text);
2673     const line_height = textLineHeight(atlas, text);
2674     const wrap_width = textWrapWidth(text);
2675     var line_count: usize = 0;
2676     var max_width: f32 = 0;
2677     var lines = std.mem.splitScalar(u8, text.content, '\n');
2678     while (lines.next()) |line| {
2679         const plan = try LinePlan.init(atlas, line, wrap_width / scale);
2680         var iterator = plan.iterator();
2681         while (iterator.next(&plan)) |visual| {
2682             max_width = @max(max_width, visual.advance * scale);
2683             line_count += 1;
2684         }
2685     }
2686     if (line_count == 0) line_count = 1;
2687     return .{
2688         .width = if (wrap_width > 0) @min(max_width, wrap_width) else max_width,
2689         .height = line_height * @as(f32, @floatFromInt(line_count)),
2690     };
2691 }
2692 
2693 fn measureCompositeUncached(base_atlas: *Atlas, atlases: *const AtlasSet, text: UiText) !Size {
2694     const base_scale = textScale(base_atlas, text);
2695     const wrap_width = textWrapWidth(text);
2696     const multiplier = textLineHeightMultiplier(text);
2697     var max_width: f32 = 0;
2698     var total_height: f32 = 0;
2699     var line_start: usize = 0;
2700     var lines = std.mem.splitScalar(u8, text.content, '\n');
2701     while (lines.next()) |line| {
2702         defer line_start = nextLineStart(text.content.len, line_start, line.len);
2703         const plan = try LinePlan.initComposite(
2704             base_atlas,
2705             atlases,
2706             text,
2707             line,
2708             line_start,
2709             if (wrap_width > 0) wrap_width / base_scale else 0,
2710         );
2711         var iterator = plan.iterator();
2712         while (iterator.next(&plan)) |visual| {
2713             const box = plan.compositeLineBox(visual, multiplier);
2714             max_width = @max(max_width, visual.advance * base_scale);
2715             total_height += box.height * base_scale;
2716         }
2717     }
2718     if (total_height == 0) {
2719         const base = compositeMetricSpan(
2720             atlases.forText(text) orelse return error.MissingTextAtlas,
2721             gui.model.resolvedTextStyle(text, 0),
2722             base_scale,
2723             0,
2724             0,
2725         );
2726         total_height = @max(
2727             base.point_size * multiplier,
2728             base.ascent + base.descent,
2729         ) * base_scale;
2730     }
2731     return .{
2732         .width = if (wrap_width > 0) @min(max_width, wrap_width) else max_width,
2733         .height = total_height,
2734     };
2735 }
2736 
2737 fn shapeBitmap(allocator: Allocator, bitmap: *Atlas.Bitmap, content: []const u8) !filigree.GlyphRun {
2738     bitmap.shaped.clearRetainingCapacity();
2739     bitmap.clusters.clearRetainingCapacity();
2740     var total: i32 = 0;
2741     var index: usize = 0;
2742     while (index < content.len) {
2743         const sequence_len = std.unicode.utf8ByteSequenceLength(content[index]) catch {
2744             index += 1;
2745             continue;
2746         };
2747         if (index + sequence_len > content.len) break;
2748         const cluster: u32 = @intCast(index);
2749         const codepoint = std.unicode.utf8Decode(content[index .. index + sequence_len]) catch {
2750             index += sequence_len;
2751             continue;
2752         };
2753         index += sequence_len;
2754         const glyph_start: u32 = @intCast(bitmap.shaped.items.len);
2755         const cluster_index: u32 = @intCast(bitmap.clusters.items.len);
2756         if (codepoint < 0x20) {
2757             try bitmap.clusters.append(allocator, .{
2758                 .source = .{ .start = cluster, .end = @intCast(index) },
2759                 .glyphs = .{ .start = glyph_start, .end = glyph_start },
2760             });
2761             continue;
2762         }
2763         const glyph_id = bitmap.map.get(codepoint) orelse
2764             bitmap.map.get('?') orelse
2765             std.math.maxInt(u32);
2766         try bitmap.shaped.append(allocator, .{
2767             .glyph_id = glyph_id,
2768             .cluster = cluster,
2769             .cluster_index = cluster_index,
2770             .source_start = cluster,
2771             .source_end = @intCast(index),
2772             .x_advance = bitmap.advance,
2773             .y_advance = 0,
2774             .x_offset = 0,
2775             .y_offset = 0,
2776         });
2777         try bitmap.clusters.append(allocator, .{
2778             .source = .{ .start = cluster, .end = @intCast(index) },
2779             .glyphs = .{ .start = glyph_start, .end = glyph_start + 1 },
2780         });
2781         total = addClampedI32(total, bitmap.advance);
2782     }
2783     return .{
2784         .glyphs = bitmap.shaped.items,
2785         .clusters = bitmap.clusters.items,
2786         .ligature_carets = &.{},
2787         .total_x_advance = total,
2788         .total_y_advance = 0,
2789         .direction = .ltr,
2790         .writing_mode = .horizontal,
2791         .output_order = .visual,
2792     };
2793 }
2794 
2795 pub fn packAtlasImageAlloc(allocator: Allocator, atlas: filigree.GlyphAtlas) !OwnedAtlasImage {
2796     if (atlas.width <= 0 or atlas.height <= 0) return error.InvalidAtlas;
2797     const width = std.math.cast(u32, atlas.width) orelse return error.InvalidAtlas;
2798     const height = std.math.cast(u32, atlas.height) orelse return error.InvalidAtlas;
2799     const pixel_count = try pixelCount(width, height);
2800     const byte_count = std.math.mul(usize, pixel_count, 4) catch return error.InvalidAtlas;
2801     if (atlas.rgba.len < byte_count) return error.BufferTooSmall;
2802 
2803     const pixels = try allocator.alloc(u32, pixel_count);
2804     errdefer allocator.free(pixels);
2805     for (pixels, 0..) |*pixel, index| {
2806         const base = index * 4;
2807         pixel.* = packRgba(.{
2808             .r = atlas.rgba[base],
2809             .g = atlas.rgba[base + 1],
2810             .b = atlas.rgba[base + 2],
2811             .a = atlas.rgba[base + 3],
2812         });
2813     }
2814     return .{
2815         .image = .{ .width = width, .height = height, .pixels = pixels },
2816         .pixels = pixels,
2817     };
2818 }
2819 
2820 fn glyphIndexAlloc(allocator: Allocator, atlas: filigree.GlyphAtlas) !std.AutoHashMapUnmanaged(u32, usize) {
2821     var index: std.AutoHashMapUnmanaged(u32, usize) = .empty;
2822     errdefer index.deinit(allocator);
2823     try index.ensureTotalCapacity(allocator, @intCast(atlas.glyphs.len));
2824     for (atlas.glyphs, 0..) |glyph, position| {
2825         index.putAssumeCapacity(glyph.glyph_id, position);
2826     }
2827     return index;
2828 }
2829 
2830 pub fn drawGlyphRun(
2831     writer: anytype,
2832     atlas_image_index: u32,
2833     atlas: filigree.GlyphAtlas,
2834     run: filigree.GlyphRun,
2835     origin_x: f32,
2836     origin_y: f32,
2837     color: Color,
2838 ) void {
2839     var pen_x: i32 = 0;
2840     var pen_y: i32 = 0;
2841     for (run.glyphs) |glyph| {
2842         const atlas_index = findAtlasGlyph(atlas, glyph.glyph_id) orelse {
2843             pen_x = addClampedI32(pen_x, glyph.x_advance);
2844             pen_y = addClampedI32(pen_y, glyph.y_advance);
2845             continue;
2846         };
2847         const atlas_glyph = atlas.glyphs[atlas_index];
2848         const source = atlas.recs[atlas_index];
2849         if (atlas_glyph.width <= 0 or atlas_glyph.height <= 0) {
2850             pen_x = addClampedI32(pen_x, glyph.x_advance);
2851             pen_y = addClampedI32(pen_y, glyph.y_advance);
2852             continue;
2853         }
2854         const rect = Rect{
2855             .x = origin_x + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_x, glyph.x_offset)) + atlas_glyph.offset_x)),
2856             .y = origin_y + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_y, glyph.y_offset)) + atlas_glyph.offset_y)),
2857             .width = @floatFromInt(atlas_glyph.width),
2858             .height = @floatFromInt(atlas_glyph.height),
2859         };
2860         writer.drawGlyph(rect, atlas_image_index, .{
2861             .x = source.x,
2862             .y = source.y,
2863             .width = source.width,
2864             .height = source.height,
2865         }, color);
2866         pen_x = addClampedI32(pen_x, glyph.x_advance);
2867         pen_y = addClampedI32(pen_y, glyph.y_advance);
2868     }
2869 }
2870 
2871 pub fn appendFrameCommands(commands: *command.CommandBuffer, frame: UiFrame, atlases: *const AtlasSet, device_scale: f32, layer: u8) !void {
2872     const start = commands.items().len;
2873     errdefer commands.rollback(start);
2874     for (frame.widgets) |widget| {
2875         if (widget.layer != layer) continue;
2876         try appendWidgetCommands(commands, widget, atlases, device_scale);
2877     }
2878 }
2879 
2880 pub fn appendFrameRegionCommands(commands: *command.CommandBuffer, frame: UiFrame, atlases: *const AtlasSet, region: Region, device_scale: f32, layer: u8) !void {
2881     const start = commands.items().len;
2882     errdefer commands.rollback(start);
2883     const rect = regionRect(region);
2884     for (frame.widgets) |widget| {
2885         if (widget.layer != layer) continue;
2886         if (!intersects(scaleRect(widget.visible_rect, device_scale), rect)) continue;
2887         try appendWidgetCommands(commands, widget, atlases, device_scale);
2888     }
2889 }
2890 
2891 fn appendWidgetCommands(commands: *command.CommandBuffer, widget: WidgetFrame, atlases: *const AtlasSet, device_scale: f32) !void {
2892     const text = widget.text orelse return;
2893     try gui.model.validateText(text);
2894     const entry = atlases.forText(text) orelse return;
2895     if (hasMixedTextMetrics(text) or try atlases.requiresFallback(entry, text.content)) {
2896         return appendCompositeWidgetCommands(commands, widget, atlases, device_scale);
2897     }
2898     return appendSingleWidgetCommands(commands, widget, atlases, device_scale);
2899 }
2900 
2901 fn appendSingleWidgetCommands(commands: *command.CommandBuffer, widget: WidgetFrame, atlases: *const AtlasSet, device_scale: f32) !void {
2902     const text = widget.text.?;
2903     const has_markers = widget.text_selection.cursor_visible or widget.text_selection.selection_active;
2904     if (text.content.len == 0 and !has_markers) return;
2905     const entry = atlases.forText(text) orelse return;
2906     const atlas = entry.atlas;
2907     const foreground = widget.paint.foreground orelse Color{ .r = 28, .g = 34, .b = 31, .a = 255 };
2908     const rect = scaleRect(widget.rect, device_scale);
2909     const layout = try TextLayout.init(atlases, text, rect, device_scale);
2910     const font_metrics = atlas.metrics();
2911     var writer = GlyphCommandWriter{
2912         .commands = commands,
2913         .clip = scaleRect(widget.visible_rect, device_scale),
2914     };
2915     var pending_caret: ?TextMarker = null;
2916     var glyph_runs = TextRunCursor{ .runs = text.runs };
2917     var background_runs = TextRunRangeCursor{ .runs = text.runs };
2918     var decoration_runs = TextRunRangeCursor{ .runs = text.runs };
2919     const caret_run = findTextRun(text.runs, widget.text_selection.cursor_byte_offset);
2920     const caret_foreground = if (caret_run) |run| run.foreground orelse foreground else foreground;
2921     const caret_background = if (caret_run) |run| run.background else null;
2922     var iterator = layout.iterator();
2923     while (try iterator.next()) |line| {
2924         if (text.runs.len > 0) {
2925             appendRunBackgroundCommands(
2926                 &writer,
2927                 &background_runs,
2928                 line.plan,
2929                 line.visual,
2930                 line.hard_line_start,
2931                 line.origin_x,
2932                 line.y,
2933                 line.height,
2934                 line.scale,
2935             );
2936         }
2937         const cursor = if (has_markers) blk: {
2938             appendSelectionCommand(
2939                 &writer,
2940                 widget.text_selection,
2941                 line.plan,
2942                 line.visual,
2943                 line.hard_line_start,
2944                 line.origin_x,
2945                 line.y,
2946                 line.height,
2947                 line.scale,
2948                 foreground,
2949             );
2950             if (caretMarker(
2951                 atlas,
2952                 widget.text_selection,
2953                 line.plan,
2954                 line.visual,
2955                 line.hard_line_start,
2956                 line.origin_x,
2957                 line.y,
2958                 line.height,
2959                 line.scale,
2960                 device_scale,
2961                 caret_foreground,
2962             )) |marker| {
2963                 if (widget.text_selection.cursor_block) {
2964                     writer.fillRect(marker.rect, marker.color);
2965                 } else {
2966                     pending_caret = marker;
2967                 }
2968             }
2969             break :blk blockCursorHighlight(
2970                 widget,
2971                 line.plan,
2972                 line.visual,
2973                 line.hard_line_start,
2974                 caret_foreground,
2975                 caret_background,
2976             );
2977         } else null;
2978         var line_run = line.plan.run;
2979         line_run.glyphs = line.visual.glyphs(line.plan.run);
2980         if (text.runs.len == 0) {
2981             drawGlyphRunScaled(
2982                 &writer,
2983                 entry.image_index,
2984                 atlas,
2985                 line_run,
2986                 line.hard_line,
2987                 line.y,
2988                 line.height,
2989                 line.origin_x,
2990                 line.origin_y,
2991                 line.scale,
2992                 foreground,
2993                 cursor,
2994             );
2995         } else {
2996             drawStyledGlyphRunScaled(
2997                 &writer,
2998                 entry.image_index,
2999                 atlas,
3000                 line_run,
3001                 line.hard_line,
3002                 line.hard_line_start,
3003                 line.y,
3004                 line.height,
3005                 line.origin_x,
3006                 line.origin_y,
3007                 line.scale,
3008                 foreground,
3009                 cursor,
3010                 &glyph_runs,
3011             );
3012             appendRunDecorationCommands(
3013                 &writer,
3014                 &decoration_runs,
3015                 line.plan,
3016                 line.visual,
3017                 line.hard_line_start,
3018                 line.origin_x,
3019                 line.y,
3020                 line.origin_y,
3021                 line.height,
3022                 line.scale,
3023                 device_scale,
3024                 font_metrics,
3025                 foreground,
3026             );
3027         }
3028     }
3029     if (pending_caret) |caret| writer.fillRect(caret.rect, caret.color);
3030     if (writer.failure) |failure| return failure;
3031 }
3032 
3033 fn appendCompositeWidgetCommands(
3034     commands: *command.CommandBuffer,
3035     widget: WidgetFrame,
3036     atlases: *const AtlasSet,
3037     device_scale: f32,
3038 ) !void {
3039     const text = widget.text.?;
3040     const has_markers = widget.text_selection.cursor_visible or
3041         widget.text_selection.selection_active;
3042     if (text.content.len == 0 and !has_markers) return;
3043     const entry = atlases.forText(text) orelse return;
3044     const base_atlas = entry.atlas;
3045     const foreground = widget.paint.foreground orelse
3046         Color{ .r = 28, .g = 34, .b = 31, .a = 255 };
3047     const rect = scaleRect(widget.rect, device_scale);
3048     const layout = try TextLayout.init(atlases, text, rect, device_scale);
3049     var writer = GlyphCommandWriter{
3050         .commands = commands,
3051         .clip = scaleRect(widget.visible_rect, device_scale),
3052     };
3053     var pending_caret: ?TextMarker = null;
3054     var glyph_runs = TextRunCursor{ .runs = text.runs };
3055     var background_runs = TextRunRangeCursor{ .runs = text.runs };
3056     var decoration_runs = TextRunRangeCursor{ .runs = text.runs };
3057     const caret_run = findTextRun(text.runs, widget.text_selection.cursor_byte_offset);
3058     const caret_foreground = if (caret_run) |run|
3059         run.foreground orelse foreground
3060     else
3061         foreground;
3062     const caret_background = if (caret_run) |run| run.background else null;
3063     var iterator = layout.iterator();
3064     while (try iterator.next()) |line| {
3065         appendRunBackgroundCommands(
3066             &writer,
3067             &background_runs,
3068             line.plan,
3069             line.visual,
3070             line.hard_line_start,
3071             line.origin_x,
3072             line.y,
3073             line.height,
3074             line.scale,
3075         );
3076         const cursor = if (has_markers) blk: {
3077             appendSelectionCommand(
3078                 &writer,
3079                 widget.text_selection,
3080                 line.plan,
3081                 line.visual,
3082                 line.hard_line_start,
3083                 line.origin_x,
3084                 line.y,
3085                 line.height,
3086                 line.scale,
3087                 foreground,
3088             );
3089             if (caretMarker(
3090                 base_atlas,
3091                 widget.text_selection,
3092                 line.plan,
3093                 line.visual,
3094                 line.hard_line_start,
3095                 line.origin_x,
3096                 line.y,
3097                 line.height,
3098                 line.scale,
3099                 device_scale,
3100                 caret_foreground,
3101             )) |marker| {
3102                 if (widget.text_selection.cursor_block) {
3103                     writer.fillRect(marker.rect, marker.color);
3104                 } else {
3105                     pending_caret = marker;
3106                 }
3107             }
3108             break :blk blockCursorHighlight(
3109                 widget,
3110                 line.plan,
3111                 line.visual,
3112                 line.hard_line_start,
3113                 caret_foreground,
3114                 caret_background,
3115             );
3116         } else null;
3117         drawCompositeStyledGlyphRunScaled(
3118             &writer,
3119             line.plan,
3120             line.visual,
3121             line.hard_line,
3122             line.hard_line_start,
3123             line.y,
3124             line.height,
3125             line.origin_x,
3126             line.origin_y,
3127             line.scale,
3128             line.ascent,
3129             foreground,
3130             cursor,
3131             &glyph_runs,
3132         );
3133         appendRunDecorationCommands(
3134             &writer,
3135             &decoration_runs,
3136             line.plan,
3137             line.visual,
3138             line.hard_line_start,
3139             line.origin_x,
3140             line.y,
3141             line.origin_y,
3142             line.height,
3143             line.scale,
3144             device_scale,
3145             .{ .ascent = line.ascent, .descent = line.descent, .line_gap = 0 },
3146             foreground,
3147         );
3148     }
3149     if (pending_caret) |caret| writer.fillRect(caret.rect, caret.color);
3150     if (writer.failure) |failure| return failure;
3151 }
3152 
3153 fn scaleRect(rect: Rect, scale: f32) Rect {
3154     return .{
3155         .x = rect.x * scale,
3156         .y = rect.y * scale,
3157         .width = rect.width * scale,
3158         .height = rect.height * scale,
3159     };
3160 }
3161 
3162 fn regionRect(region: Region) Rect {
3163     return .{
3164         .x = @floatFromInt(region.x),
3165         .y = @floatFromInt(region.y),
3166         .width = @floatFromInt(region.width),
3167         .height = @floatFromInt(region.height),
3168     };
3169 }
3170 
3171 fn intersects(left: Rect, right: Rect) bool {
3172     return left.x < right.x + right.width and
3173         right.x < left.x + left.width and
3174         left.y < right.y + right.height and
3175         right.y < left.y + left.height;
3176 }
3177 
3178 const GlyphCommandWriter = struct {
3179     commands: *command.CommandBuffer,
3180     clip: Rect,
3181     failure: ?Allocator.Error = null,
3182 
3183     fn drawGlyph(self: *GlyphCommandWriter, rect: Rect, image_index: u32, source: Rect, color: Color) void {
3184         if (self.failure != null) return;
3185         self.commands.append(.{
3186             .kind = .glyph,
3187             .rect = rect,
3188             .clip = self.clip,
3189             .source = source,
3190             .color = color,
3191             .image_index = image_index,
3192         }) catch |failure| {
3193             self.failure = failure;
3194         };
3195     }
3196 
3197     pub fn fillRect(self: *GlyphCommandWriter, rect: Rect, color: Color) void {
3198         if (self.failure != null) return;
3199         self.commands.append(.{
3200             .kind = .fill,
3201             .rect = rect,
3202             .clip = self.clip,
3203             .color = color,
3204         }) catch |failure| {
3205             self.failure = failure;
3206         };
3207     }
3208 };
3209 
3210 const TextRunCursor = struct {
3211     runs: []const UiTextRun,
3212     index: usize = 0,
3213     last_offset: usize = 0,
3214     started: bool = false,
3215 
3216     fn find(self: *TextRunCursor, byte_offset: usize) ?UiTextRun {
3217         if (!self.started or byte_offset < self.last_offset) {
3218             self.index = firstRunEndingAfter(self.runs, byte_offset);
3219             self.started = true;
3220         } else {
3221             while (self.index < self.runs.len and self.runs[self.index].byte_end <= byte_offset) {
3222                 self.index += 1;
3223             }
3224         }
3225         self.last_offset = byte_offset;
3226         if (self.index >= self.runs.len) return null;
3227         const run = self.runs[self.index];
3228         if (byte_offset < run.byte_start) return null;
3229         return run;
3230     }
3231 };
3232 
3233 const TextRunRangeCursor = struct {
3234     runs: []const UiTextRun,
3235     index: usize = 0,
3236 
3237     fn firstOverlap(self: *TextRunRangeCursor, byte_start: usize) usize {
3238         while (self.index < self.runs.len and self.runs[self.index].byte_end <= byte_start) {
3239             self.index += 1;
3240         }
3241         return self.index;
3242     }
3243 };
3244 
3245 fn firstRunEndingAfter(runs: []const UiTextRun, byte_offset: usize) usize {
3246     var low: usize = 0;
3247     var high = runs.len;
3248     while (low < high) {
3249         const middle = low + (high - low) / 2;
3250         if (runs[middle].byte_end <= byte_offset) {
3251             low = middle + 1;
3252         } else {
3253             high = middle;
3254         }
3255     }
3256     return low;
3257 }
3258 
3259 fn findTextRun(runs: []const UiTextRun, byte_offset: usize) ?UiTextRun {
3260     const index = firstRunEndingAfter(runs, byte_offset);
3261     if (index >= runs.len or byte_offset < runs[index].byte_start) return null;
3262     return runs[index];
3263 }
3264 
3265 fn appendRunBackgroundCommands(
3266     writer: *GlyphCommandWriter,
3267     cursor: *TextRunRangeCursor,
3268     plan: *const LinePlan,
3269     visual: VisualLine,
3270     line_start: usize,
3271     origin_x: f32,
3272     line_y: f32,
3273     line_height: f32,
3274     scale: f32,
3275 ) void {
3276     const visual_start = line_start + visual.byte_start;
3277     const visual_end = line_start + visual.byte_end;
3278     if (visual_end <= visual_start) return;
3279     var index = cursor.firstOverlap(visual_start);
3280     while (index < cursor.runs.len and cursor.runs[index].byte_start < visual_end) : (index += 1) {
3281         const run = cursor.runs[index];
3282         const color = run.background orelse continue;
3283         const rect = textRunRect(
3284             plan,
3285             visual,
3286             line_start,
3287             @max(run.byte_start, visual_start),
3288             @min(run.byte_end, visual_end),
3289             origin_x,
3290             line_y,
3291             line_height,
3292             scale,
3293         ) orelse continue;
3294         writer.fillRect(rect, color);
3295     }
3296 }
3297 
3298 fn appendRunDecorationCommands(
3299     writer: *GlyphCommandWriter,
3300     cursor: *TextRunRangeCursor,
3301     plan: *const LinePlan,
3302     visual: VisualLine,
3303     line_start: usize,
3304     origin_x: f32,
3305     line_y: f32,
3306     origin_y: f32,
3307     line_height: f32,
3308     scale: f32,
3309     device_scale: f32,
3310     metrics: Metrics,
3311     foreground: Color,
3312 ) void {
3313     const visual_start = line_start + visual.byte_start;
3314     const visual_end = line_start + visual.byte_end;
3315     if (visual_end <= visual_start) return;
3316     const thickness = @max(device_scale, @min(line_height * 0.08, @max(device_scale, scale)));
3317     const bottom = line_y + line_height - thickness;
3318     const baseline = origin_y + metrics.ascent * scale;
3319     const underline_y = @min(bottom, baseline + @max(device_scale, scale * 0.5));
3320     const strike_y = std.math.clamp(origin_y + metrics.ascent * scale * 0.55, line_y, bottom);
3321     var index = cursor.firstOverlap(visual_start);
3322     while (index < cursor.runs.len and cursor.runs[index].byte_start < visual_end) : (index += 1) {
3323         const run = cursor.runs[index];
3324         if (!run.underline and !run.strikethrough) continue;
3325         const range = textRunRect(
3326             plan,
3327             visual,
3328             line_start,
3329             @max(run.byte_start, visual_start),
3330             @min(run.byte_end, visual_end),
3331             origin_x,
3332             0,
3333             thickness,
3334             scale,
3335         ) orelse continue;
3336         const color = run.foreground orelse foreground;
3337         if (run.underline) {
3338             writer.fillRect(.{
3339                 .x = range.x,
3340                 .y = underline_y,
3341                 .width = range.width,
3342                 .height = thickness,
3343             }, color);
3344         }
3345         if (run.strikethrough) {
3346             writer.fillRect(.{
3347                 .x = range.x,
3348                 .y = strike_y,
3349                 .width = range.width,
3350                 .height = thickness,
3351             }, color);
3352         }
3353     }
3354 }
3355 
3356 fn textRunRect(
3357     plan: *const LinePlan,
3358     visual: VisualLine,
3359     line_start: usize,
3360     byte_start: usize,
3361     byte_end: usize,
3362     origin_x: f32,
3363     y: f32,
3364     height: f32,
3365     scale: f32,
3366 ) ?Rect {
3367     if (byte_end <= byte_start) return null;
3368     const start_x = origin_x + visual.advanceForByteOffset(
3369         plan.run,
3370         byte_start - line_start,
3371         plan.content.len,
3372     ) * scale;
3373     const end_x = origin_x + visual.advanceForByteOffset(
3374         plan.run,
3375         byte_end - line_start,
3376         plan.content.len,
3377     ) * scale;
3378     const left = @min(start_x, end_x);
3379     const right = @max(start_x, end_x);
3380     if (right <= left) return null;
3381     return .{
3382         .x = left,
3383         .y = y,
3384         .width = right - left,
3385         .height = height,
3386     };
3387 }
3388 
3389 const TextMarker = struct {
3390     rect: Rect,
3391     color: Color,
3392 };
3393 
3394 fn appendSelectionCommand(
3395     writer: *GlyphCommandWriter,
3396     selection: UiTextSelection,
3397     plan: *const LinePlan,
3398     visual: VisualLine,
3399     line_start: usize,
3400     origin_x: f32,
3401     line_y: f32,
3402     line_height: f32,
3403     scale: f32,
3404     foreground: Color,
3405 ) void {
3406     if (!selection.selection_active) return;
3407     const selected = sortedSelection(selection);
3408     const visual_start = line_start + visual.byte_start;
3409     const visual_end = line_start + visual.byte_end;
3410     const start = @max(selected.start, visual_start);
3411     const end = @min(selected.end, visual_end);
3412     if (end <= start) return;
3413     const start_x = origin_x + visual.advanceForByteOffset(plan.run, start - line_start, plan.content.len) * scale;
3414     const end_x = origin_x + visual.advanceForByteOffset(plan.run, end - line_start, plan.content.len) * scale;
3415     writer.fillRect(.{
3416         .x = start_x,
3417         .y = line_y,
3418         .width = end_x - start_x,
3419         .height = line_height,
3420     }, .{ .r = foreground.r, .g = foreground.g, .b = foreground.b, .a = 64 });
3421 }
3422 
3423 fn caretMarker(
3424     atlas: *Atlas,
3425     selection: UiTextSelection,
3426     plan: *const LinePlan,
3427     visual: VisualLine,
3428     line_start: usize,
3429     origin_x: f32,
3430     line_y: f32,
3431     line_height: f32,
3432     scale: f32,
3433     device_scale: f32,
3434     foreground: Color,
3435 ) ?TextMarker {
3436     if (!selection.cursor_visible) return null;
3437     if (selection.cursor_byte_offset < line_start) return null;
3438     const cursor_offset = selection.cursor_byte_offset - line_start;
3439     if (!visual.containsCaret(cursor_offset, .downstream)) return null;
3440     const x = origin_x + visual.advanceForByteOffset(plan.run, cursor_offset, plan.content.len) * scale;
3441     if (selection.cursor_block) {
3442         return .{
3443             .rect = .{
3444                 .x = x,
3445                 .y = line_y,
3446                 .width = caretBlockWidth(atlas, plan, visual, cursor_offset, scale, device_scale),
3447                 .height = @max(1, line_height),
3448             },
3449             .color = .{ .r = foreground.r, .g = foreground.g, .b = foreground.b, .a = 176 },
3450         };
3451     }
3452     const inset = @min(line_height * 0.12, @max(device_scale, scale));
3453     return .{
3454         .rect = .{
3455             .x = x,
3456             .y = line_y + inset,
3457             .width = @max(device_scale, scale),
3458             .height = @max(1, line_height - inset * 2),
3459         },
3460         .color = foreground,
3461     };
3462 }
3463 
3464 fn blockCursorHighlight(
3465     widget: WidgetFrame,
3466     plan: *const LinePlan,
3467     visual: VisualLine,
3468     line_start: usize,
3469     foreground: Color,
3470     run_background: ?Color,
3471 ) ?CursorHighlight {
3472     const selection = widget.text_selection;
3473     if (!selection.cursor_visible or !selection.cursor_block) return null;
3474     if (selection.cursor_byte_offset < line_start) return null;
3475     const start = selection.cursor_byte_offset - line_start;
3476     if (!visual.containsCaret(start, .downstream) or start >= visual.byte_end) return null;
3477     const end = @min(nextCaretByteOffset(plan.run, plan.content, start), visual.byte_end);
3478     if (end <= start) return null;
3479     return .{
3480         .lead_advance = visual.advanceForByteOffset(plan.run, start, plan.content.len),
3481         .trail_advance = visual.advanceForByteOffset(plan.run, end, plan.content.len),
3482         .color = run_background orelse widget.paint.background orelse contrastColor(foreground),
3483     };
3484 }
3485 
3486 fn contrastColor(foreground: Color) Color {
3487     const luminance = (@as(u32, foreground.r) * 299 + @as(u32, foreground.g) * 587 + @as(u32, foreground.b) * 114) / 1000;
3488     if (luminance > 128) return .{ .r = 24, .g = 27, .b = 26, .a = 255 };
3489     return .{ .r = 245, .g = 246, .b = 244, .a = 255 };
3490 }
3491 
3492 fn caretBlockWidth(atlas: *Atlas, plan: *const LinePlan, visual: VisualLine, cursor_offset: usize, scale: f32, device_scale: f32) f32 {
3493     if (cursor_offset < visual.byte_end) {
3494         const end = @min(nextCaretByteOffset(plan.run, plan.content, cursor_offset), visual.byte_end);
3495         const lead = visual.advanceForByteOffset(plan.run, cursor_offset, plan.content.len) * scale;
3496         const trail = visual.advanceForByteOffset(plan.run, end, plan.content.len) * scale;
3497         if (trail > lead) return trail - lead;
3498     }
3499     const run = atlas.shape("M") catch return @max(device_scale, scale);
3500     return @max(1, runAdvance(run) * scale);
3501 }
3502 
3503 const TextRange = struct {
3504     start: usize,
3505     end: usize,
3506 };
3507 
3508 fn sortedSelection(selection: UiTextSelection) TextRange {
3509     const anchor = selection.selection_anchor_byte_offset;
3510     const focus = selection.selection_focus_byte_offset;
3511     return if (anchor <= focus)
3512         .{ .start = anchor, .end = focus }
3513     else
3514         .{ .start = focus, .end = anchor };
3515 }
3516 
3517 fn findAtlasGlyph(atlas: filigree.GlyphAtlas, glyph_id: u32) ?usize {
3518     for (atlas.glyphs, 0..) |glyph, index| {
3519         if (glyph.glyph_id == glyph_id) return index;
3520     }
3521     return null;
3522 }
3523 
3524 fn synthCodepoint(line: []const u8, glyph: filigree.ShapedGlyph) ?u21 {
3525     if (glyph.source_codepoint_count != 1) return null;
3526     const start: usize = glyph.source_start;
3527     const end: usize = glyph.source_end;
3528     if (start >= end or end > line.len) return null;
3529     const sequence = line[start..end];
3530     const length = std.unicode.utf8ByteSequenceLength(sequence[0]) catch return null;
3531     if (length > sequence.len) return null;
3532     const codepoint = std.unicode.utf8Decode(sequence[0..length]) catch return null;
3533     return if (synth.covered(codepoint)) codepoint else null;
3534 }
3535 
3536 fn drawSyntheticOrNotdef(
3537     writer: anytype,
3538     line: []const u8,
3539     glyph: filigree.ShapedGlyph,
3540     pen_x: i32,
3541     line_y: f32,
3542     line_height: f32,
3543     origin_x: f32,
3544     scale: f32,
3545     color: Color,
3546 ) void {
3547     std.debug.assert(glyph.glyph_id == 0);
3548     const cell = Rect{
3549         .x = origin_x +
3550             @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_x, glyph.x_offset)))) * scale,
3551         .y = line_y,
3552         .width = @as(f32, @floatFromInt(glyph.x_advance)) / 64 * scale,
3553         .height = line_height,
3554     };
3555     if (synthCodepoint(line, glyph)) |codepoint| {
3556         const drawn = synth.draw(writer, codepoint, cell, color);
3557         std.debug.assert(drawn);
3558         return;
3559     }
3560     synth.drawNotdef(writer, cell, color);
3561 }
3562 
3563 const CursorHighlight = struct {
3564     lead_advance: f32,
3565     trail_advance: f32,
3566     color: Color,
3567 };
3568 
3569 fn glyphRunColor(pen_x: i32, cursor: ?CursorHighlight, color: Color) Color {
3570     const highlight = cursor orelse return color;
3571     const pen = @as(f32, @floatFromInt(pen_x)) / 64;
3572     const epsilon = 0.01;
3573     if (pen >= highlight.lead_advance - epsilon and pen < highlight.trail_advance - epsilon) return highlight.color;
3574     return color;
3575 }
3576 
3577 fn drawCompositeStyledGlyphRunScaled(
3578     writer: anytype,
3579     plan: *const LinePlan,
3580     visual: VisualLine,
3581     line: []const u8,
3582     line_start: usize,
3583     line_y: f32,
3584     line_height: f32,
3585     origin_x: f32,
3586     origin_y: f32,
3587     scale: f32,
3588     line_ascent: f32,
3589     color: Color,
3590     cursor: ?CursorHighlight,
3591     run_cursor: *TextRunCursor,
3592 ) void {
3593     var pen_x: i32 = 0;
3594     var pen_y: i32 = 0;
3595     for (plan.run.glyphs[visual.glyph_start..visual.glyph_end], visual.glyph_start..) |glyph, glyph_index| {
3596         const byte_offset = line_start + sourceOffset(glyph.source_start, line.len);
3597         const text_run = run_cursor.find(byte_offset);
3598         const styled_color = if (text_run) |value|
3599             value.foreground orelse color
3600         else
3601             color;
3602         const glyph_color = glyphRunColor(pen_x, cursor, styled_color);
3603         if (glyph.glyph_id == 0) {
3604             drawSyntheticOrNotdef(
3605                 writer,
3606                 line,
3607                 glyph,
3608                 pen_x,
3609                 line_y,
3610                 line_height,
3611                 origin_x,
3612                 scale,
3613                 glyph_color,
3614             );
3615             pen_x = addClampedI32(pen_x, glyph.x_advance);
3616             pen_y = addClampedI32(pen_y, glyph.y_advance);
3617             continue;
3618         }
3619         const source_index = plan.glyph_sources[glyph_index].segment_index;
3620         const span = plan.metric_spans[source_index];
3621         const atlas = span.atlas;
3622         const atlas_index = atlas.glyph_index.get(glyph.glyph_id) orelse {
3623             pen_x = addClampedI32(pen_x, glyph.x_advance);
3624             pen_y = addClampedI32(pen_y, glyph.y_advance);
3625             continue;
3626         };
3627         const atlas_glyph = atlas.atlas.glyphs[atlas_index];
3628         const source = atlas.atlas.recs[atlas_index];
3629         if (atlas_glyph.width <= 0 or atlas_glyph.height <= 0) {
3630             pen_x = addClampedI32(pen_x, glyph.x_advance);
3631             pen_y = addClampedI32(pen_y, glyph.y_advance);
3632             continue;
3633         }
3634         const style_origin_y = origin_y + (line_ascent - span.ascent) * scale;
3635         const rect = Rect{
3636             .x = origin_x +
3637                 @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_x, glyph.x_offset)))) * scale +
3638                 @as(f32, @floatFromInt(atlas_glyph.offset_x)) * span.scale_ratio * scale,
3639             .y = style_origin_y +
3640                 @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_y, glyph.y_offset)))) * scale +
3641                 @as(f32, @floatFromInt(atlas_glyph.offset_y)) * span.scale_ratio * scale,
3642             .width = @as(f32, @floatFromInt(atlas_glyph.width)) * span.scale_ratio * scale,
3643             .height = @as(f32, @floatFromInt(atlas_glyph.height)) * span.scale_ratio * scale,
3644         };
3645         writer.drawGlyph(rect, span.image_index, .{
3646             .x = source.x,
3647             .y = source.y,
3648             .width = source.width,
3649             .height = source.height,
3650         }, glyph_color);
3651         pen_x = addClampedI32(pen_x, glyph.x_advance);
3652         pen_y = addClampedI32(pen_y, glyph.y_advance);
3653     }
3654 }
3655 
3656 fn drawGlyphRunScaled(
3657     writer: anytype,
3658     atlas_image_index: u32,
3659     atlas: *const Atlas,
3660     run: filigree.GlyphRun,
3661     line: []const u8,
3662     line_y: f32,
3663     line_height: f32,
3664     origin_x: f32,
3665     origin_y: f32,
3666     scale: f32,
3667     color: Color,
3668     cursor: ?CursorHighlight,
3669 ) void {
3670     drawGlyphRunScaledMode(
3671         false,
3672         writer,
3673         atlas_image_index,
3674         atlas,
3675         run,
3676         line,
3677         0,
3678         line_y,
3679         line_height,
3680         origin_x,
3681         origin_y,
3682         scale,
3683         color,
3684         cursor,
3685         null,
3686     );
3687 }
3688 
3689 fn drawStyledGlyphRunScaled(
3690     writer: anytype,
3691     atlas_image_index: u32,
3692     atlas: *const Atlas,
3693     run: filigree.GlyphRun,
3694     line: []const u8,
3695     line_start: usize,
3696     line_y: f32,
3697     line_height: f32,
3698     origin_x: f32,
3699     origin_y: f32,
3700     scale: f32,
3701     color: Color,
3702     cursor: ?CursorHighlight,
3703     run_cursor: *TextRunCursor,
3704 ) void {
3705     drawGlyphRunScaledMode(
3706         true,
3707         writer,
3708         atlas_image_index,
3709         atlas,
3710         run,
3711         line,
3712         line_start,
3713         line_y,
3714         line_height,
3715         origin_x,
3716         origin_y,
3717         scale,
3718         color,
3719         cursor,
3720         run_cursor,
3721     );
3722 }
3723 
3724 fn drawGlyphRunScaledMode(
3725     comptime styled: bool,
3726     writer: anytype,
3727     atlas_image_index: u32,
3728     atlas: *const Atlas,
3729     run: filigree.GlyphRun,
3730     line: []const u8,
3731     line_start: usize,
3732     line_y: f32,
3733     line_height: f32,
3734     origin_x: f32,
3735     origin_y: f32,
3736     scale: f32,
3737     color: Color,
3738     cursor: ?CursorHighlight,
3739     run_cursor: ?*TextRunCursor,
3740 ) void {
3741     var pen_x: i32 = 0;
3742     var pen_y: i32 = 0;
3743     for (run.glyphs) |glyph| {
3744         const styled_color = if (styled) blk: {
3745             const byte_offset = line_start + sourceOffset(glyph.source_start, line.len);
3746             const text_run = run_cursor.?.find(byte_offset);
3747             break :blk if (text_run) |value| value.foreground orelse color else color;
3748         } else color;
3749         const glyph_color = glyphRunColor(pen_x, cursor, styled_color);
3750         if (glyph.glyph_id == 0) {
3751             drawSyntheticOrNotdef(
3752                 writer,
3753                 line,
3754                 glyph,
3755                 pen_x,
3756                 line_y,
3757                 line_height,
3758                 origin_x,
3759                 scale,
3760                 glyph_color,
3761             );
3762             pen_x = addClampedI32(pen_x, glyph.x_advance);
3763             pen_y = addClampedI32(pen_y, glyph.y_advance);
3764             continue;
3765         }
3766         const atlas_index = atlas.glyph_index.get(glyph.glyph_id) orelse {
3767             pen_x = addClampedI32(pen_x, glyph.x_advance);
3768             pen_y = addClampedI32(pen_y, glyph.y_advance);
3769             continue;
3770         };
3771         const atlas_glyph = atlas.atlas.glyphs[atlas_index];
3772         const source = atlas.atlas.recs[atlas_index];
3773         if (atlas_glyph.width <= 0 or atlas_glyph.height <= 0) {
3774             pen_x = addClampedI32(pen_x, glyph.x_advance);
3775             pen_y = addClampedI32(pen_y, glyph.y_advance);
3776             continue;
3777         }
3778         const rect = Rect{
3779             .x = origin_x + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_x, glyph.x_offset)) + atlas_glyph.offset_x)) * scale,
3780             .y = origin_y + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_y, glyph.y_offset)) + atlas_glyph.offset_y)) * scale,
3781             .width = @as(f32, @floatFromInt(atlas_glyph.width)) * scale,
3782             .height = @as(f32, @floatFromInt(atlas_glyph.height)) * scale,
3783         };
3784         writer.drawGlyph(rect, atlas_image_index, .{
3785             .x = source.x,
3786             .y = source.y,
3787             .width = source.width,
3788             .height = source.height,
3789         }, glyph_color);
3790         pen_x = addClampedI32(pen_x, glyph.x_advance);
3791         pen_y = addClampedI32(pen_y, glyph.y_advance);
3792     }
3793 }
3794 
3795 fn runAdvance(run: filigree.GlyphRun) f32 {
3796     return @as(f32, @floatFromInt(run.total_x_advance)) / 64;
3797 }
3798 
3799 fn glyphSliceAdvance(glyphs: []const filigree.ShapedGlyph) f32 {
3800     var advance: f32 = 0;
3801     for (glyphs) |glyph| advance += @as(f32, @floatFromInt(glyph.x_advance)) / 64;
3802     return advance;
3803 }
3804 
3805 fn nextCaretByteOffset(run: filigree.GlyphRun, text: []const u8, byte_offset: usize) usize {
3806     const target = @min(byte_offset, text.len);
3807     if (target >= text.len) return text.len;
3808     const map = run.clusterMap();
3809     for (run.clusters, 0..) |cluster, cluster_index| {
3810         const start = sourceOffset(cluster.source.start, text.len);
3811         const end = sourceOffset(cluster.source.end, text.len);
3812         if (end <= target) continue;
3813         if (target < start) return start;
3814         const stop_count = map.clusterCaretStopCount(cluster_index) orelse 0;
3815         if (stop_count > 2) {
3816             var stop_index: usize = 1;
3817             while (stop_index < stop_count) : (stop_index += 1) {
3818                 const stop = map.clusterCaretStop(cluster_index, stop_index) orelse continue;
3819                 const offset = sourceOffset(stop, text.len);
3820                 if (offset > target) return offset;
3821             }
3822             return end;
3823         }
3824         return nextGraphemeBoundary(text, start, end, target);
3825     }
3826     return text.len;
3827 }
3828 
3829 fn nextGraphemeBoundary(text: []const u8, start: usize, end: usize, target: usize) usize {
3830     if (end <= start) return end;
3831     const source_start = std.math.cast(u32, start) orelse return end;
3832     var iterator = filigree.unicode.SourceIterator.init(.{ .utf8 = text[start..end] }, source_start) catch return end;
3833     var state: filigree.unicode.GraphemeState = .{};
3834     while (iterator.next() catch return end) |scalar| {
3835         const boundary = !state.consume(scalar.codepoint);
3836         const offset = sourceOffset(scalar.source.start, text.len);
3837         if (boundary and offset > target) return offset;
3838     }
3839     return end;
3840 }
3841 
3842 fn sourceOffset(offset: u32, text_len: usize) usize {
3843     return @min(@as(usize, @intCast(offset)), text_len);
3844 }
3845 
3846 fn nextLineStart(content_len: usize, line_start: usize, line_len: usize) usize {
3847     const line_end = @min(content_len, line_start + line_len);
3848     return if (line_end < content_len) line_end + 1 else line_end;
3849 }
3850 
3851 fn textPointSize(atlas: *const Atlas, text: UiText) f32 {
3852     if (std.math.isFinite(text.point_size) and text.point_size > 0) return @floatCast(text.point_size);
3853     if (atlas.pixel_size > 0) return @floatFromInt(atlas.pixel_size);
3854     return 16;
3855 }
3856 
3857 fn textScale(atlas: *const Atlas, text: UiText) f32 {
3858     if (atlas.pixel_size <= 0) return 1;
3859     return textPointSize(atlas, text) / @as(f32, @floatFromInt(atlas.pixel_size));
3860 }
3861 
3862 fn textWrapWidth(text: UiText) f32 {
3863     if (std.math.isFinite(text.wrap_width) and text.wrap_width > 0) return @floatCast(text.wrap_width);
3864     return 0;
3865 }
3866 
3867 fn textLineHeight(atlas: *const Atlas, text: UiText) f32 {
3868     const multiplier = textLineHeightMultiplier(text);
3869     const requested = textPointSize(atlas, text) * multiplier;
3870     return @max(requested, atlas.metrics().height() * textScale(atlas, text));
3871 }
3872 
3873 fn textLineHeightMultiplier(text: UiText) f32 {
3874     return if (std.math.isFinite(text.line_height) and text.line_height > 0)
3875         @floatCast(text.line_height)
3876     else
3877         1.2;
3878 }
3879 
3880 fn alignedStart(start: f32, container: f32, content: f32, alignment: gui.model.UiTextAlign) f32 {
3881     return switch (alignment) {
3882         .start => start,
3883         .center => start + @max(container - content, 0) / 2,
3884         .end => start + @max(container - content, 0),
3885     };
3886 }
3887 
3888 fn pixelCount(width: u32, height: u32) !usize {
3889     return std.math.mul(usize, width, height) catch error.InvalidAtlas;
3890 }
3891 
3892 fn packRgba(color: Color) u32 {
3893     return @as(u32, color.r) |
3894         (@as(u32, color.g) << 8) |
3895         (@as(u32, color.b) << 16) |
3896         (@as(u32, color.a) << 24);
3897 }
3898 
3899 fn round26Dot6(value: i32) i32 {
3900     if (value >= 0) return @divTrunc(value + 32, 64);
3901     return @divTrunc(value - 32, 64);
3902 }
3903 
3904 fn addClampedI32(left: i32, right: i32) i32 {
3905     const value = @as(i64, left) + @as(i64, right);
3906     if (value > std.math.maxInt(i32)) return std.math.maxInt(i32);
3907     if (value < std.math.minInt(i32)) return std.math.minInt(i32);
3908     return @intCast(value);
3909 }
3910 
3911 const TestRecorder = struct {
3912     glyph_count: usize = 0,
3913     last_rect: Rect = .{},
3914     last_source: Rect = .{},
3915     last_color: Color = .{},
3916     last_image_index: u32 = 0,
3917 
3918     fn drawGlyph(self: *TestRecorder, rect: Rect, image_index: u32, source: Rect, color: Color) void {
3919         self.glyph_count += 1;
3920         self.last_rect = rect;
3921         self.last_source = source;
3922         self.last_color = color;
3923         self.last_image_index = image_index;
3924     }
3925 };
3926 
3927 const test_output_limits: filigree.Output.Limits = .{
3928     .max_glyphs = 4096,
3929     .max_ligature_carets = 4096,
3930 };
3931 
3932 const test_atlas_cache_limits: AtlasCacheStorage.Limits = .{
3933     .measure_entries = 512,
3934     .measure_payload_bytes = 64 * 1024,
3935     .shape_entries = 1024,
3936     .shape_payload_bytes = 512 * 1024,
3937 };
3938 
3939 fn initShapeOnlyAtlas(allocator: Allocator, bytes: []u8, pixel_size: i32) !Atlas {
3940     errdefer allocator.free(bytes);
3941     var font = filigree.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.InvalidFont;
3942     errdefer font.deinit();
3943     font.setScale(@floatFromInt(pixel_size), 72);
3944     var cache = try AtlasCacheStorage.init(allocator, test_atlas_cache_limits);
3945     errdefer cache.deinit(allocator);
3946     cache.activate();
3947     return .{
3948         .allocator = allocator,
3949         .pixel_size = pixel_size,
3950         .atlas = .{},
3951         .image = .{
3952             .image = .{ .width = 0, .height = 0, .pixels = &.{} },
3953             .pixels = &.{},
3954         },
3955         .cache = cache,
3956         .backend = .{ .outline = .{
3957             .font_bytes = bytes,
3958             .font = font,
3959             .context = filigree.Context.init(allocator, .{}),
3960             .output = try filigree.Output.init(allocator, test_output_limits),
3961         } },
3962     };
3963 }
3964 
3965 test "packAtlasImageAlloc converts filigree atlas rgba to packed image" {
3966     const allocator = std.testing.allocator;
3967     var rgba = [_]u8{
3968         255, 255, 255, 128,
3969         10,  20,  30,  40,
3970     };
3971     const atlas = filigree.GlyphAtlas{
3972         .rgba = rgba[0..],
3973         .width = 2,
3974         .height = 1,
3975     };
3976 
3977     var owned = try packAtlasImageAlloc(allocator, atlas);
3978     defer owned.deinit(allocator);
3979 
3980     try std.testing.expectEqual(@as(u32, 2), owned.image.width);
3981     try std.testing.expectEqual(@as(u32, 1), owned.image.height);
3982     try std.testing.expectEqual(@as(u32, 0x80ff_ffff), owned.pixels[0]);
3983     try std.testing.expectEqual(@as(u32, 0x281e_140a), owned.pixels[1]);
3984 }
3985 
3986 test "drawGlyphRun emits atlas-backed glyph commands" {
3987     var atlas_glyphs = [_]filigree.GlyphAtlasGlyph{.{
3988         .glyph_id = 7,
3989         .width = 3,
3990         .height = 4,
3991         .offset_x = 1,
3992         .offset_y = 2,
3993     }};
3994     var atlas_recs = [_]filigree.GlyphAtlasRectangle{.{
3995         .x = 5,
3996         .y = 6,
3997         .width = 3,
3998         .height = 4,
3999     }};
4000     const atlas = filigree.GlyphAtlas{
4001         .width = 8,
4002         .height = 8,
4003         .glyphs = atlas_glyphs[0..],
4004         .recs = atlas_recs[0..],
4005     };
4006     const shaped = [_]filigree.ShapedGlyph{.{
4007         .glyph_id = 7,
4008         .cluster = 0,
4009         .x_advance = 8 * 64,
4010         .y_advance = 0,
4011         .x_offset = 64,
4012         .y_offset = 0,
4013     }};
4014     const run = filigree.GlyphRun{
4015         .glyphs = shaped[0..],
4016         .clusters = &.{},
4017         .ligature_carets = &.{},
4018         .total_x_advance = 8 * 64,
4019         .total_y_advance = 0,
4020         .direction = .ltr,
4021         .writing_mode = .horizontal,
4022         .output_order = .visual,
4023     };
4024     var recorder = TestRecorder{};
4025 
4026     drawGlyphRun(&recorder, 4, atlas, run, 10, 20, .{ .r = 1, .g = 2, .b = 3, .a = 200 });
4027 
4028     try std.testing.expectEqual(@as(usize, 1), recorder.glyph_count);
4029     try std.testing.expectEqual(@as(u32, 4), recorder.last_image_index);
4030     try std.testing.expectEqual(@as(f32, 12), recorder.last_rect.x);
4031     try std.testing.expectEqual(@as(f32, 22), recorder.last_rect.y);
4032     try std.testing.expectEqual(@as(f32, 3), recorder.last_rect.width);
4033     try std.testing.expectEqual(@as(f32, 5), recorder.last_source.x);
4034     try std.testing.expectEqual(@as(u8, 200), recorder.last_color.a);
4035 }
4036 
4037 test "drawGlyphRun consumes filigree shaped atlas output" {
4038     const allocator = std.testing.allocator;
4039     const bytes = try filigree.fixtures.createWithOutlines(allocator);
4040     defer allocator.free(bytes);
4041     var font = filigree.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.InvalidFont;
4042     defer font.deinit();
4043     font.setPixelHeightScale(20);
4044 
4045     var context = filigree.Context.init(allocator, .{});
4046     defer context.deinit();
4047     var output = try filigree.Output.init(allocator, test_output_limits);
4048     defer output.deinit(allocator);
4049     try context.shapeRun(.{
4050         .font = &font,
4051         .text = .{ .utf8 = "A" },
4052     }, &output);
4053     const run = output.run();
4054     try std.testing.expect(run.glyphs.len > 0);
4055 
4056     var glyph_ids = try allocator.alloc(i32, run.glyphs.len);
4057     defer allocator.free(glyph_ids);
4058     var codepoints = try allocator.alloc(i32, run.glyphs.len);
4059     defer allocator.free(codepoints);
4060     for (run.glyphs, 0..) |glyph, index| {
4061         glyph_ids[index] = std.math.cast(i32, glyph.glyph_id) orelse return error.GlyphIdTooLarge;
4062         codepoints[index] = 'A';
4063     }
4064 
4065     const atlas = try filigree.glyphAtlasAlloc(allocator, allocator, bytes, 20, glyph_ids, codepoints, 2);
4066     defer atlas.deinit(allocator);
4067     var owned = try packAtlasImageAlloc(allocator, atlas);
4068     defer owned.deinit(allocator);
4069     var recorder = TestRecorder{};
4070 
4071     drawGlyphRun(&recorder, 0, atlas, run, 0, 0, .{ .r = 20, .g = 30, .b = 40, .a = 255 });
4072 
4073     try std.testing.expectEqual(run.glyphs.len, recorder.glyph_count);
4074     try std.testing.expect(owned.image.width > 0);
4075     try std.testing.expect(owned.image.height > 0);
4076     try std.testing.expect(owned.pixels.len > 0);
4077 }
4078 
4079 test "bitmap atlas rasterizes cells and shapes ascii with fallback" {
4080     const allocator = std.testing.allocator;
4081     const glyphs = [_]BitmapGlyph{
4082         .{ .codepoint = 'A', .rows = &.{ 0x81, 0xFF } },
4083         .{ .codepoint = '?', .rows = &.{ 0xFF, 0x00 } },
4084     };
4085     var atlas = try Atlas.initFromBitmapGlyphs(
4086         allocator,
4087         glyphs[0..],
4088         .{ .width = 8, .height = 2, .stride = 1 },
4089         16,
4090         test_atlas_cache_limits,
4091     );
4092     defer atlas.deinit();
4093 
4094     try std.testing.expectEqual(@as(i32, 18), atlas.atlas.width);
4095     try std.testing.expectEqual(@as(i32, 2), atlas.atlas.height);
4096     try std.testing.expectEqual(@as(u8, 255), atlas.atlas.rgba[3]);
4097     try std.testing.expectEqual(@as(u8, 0), atlas.atlas.rgba[7]);
4098     try std.testing.expect(atlas.image.image.width == 18);
4099 
4100     const run = try atlas.shape("AZ\n");
4101     try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
4102     try std.testing.expectEqual(@as(u32, 'A'), run.glyphs[0].glyph_id);
4103     try std.testing.expectEqual(@as(u32, '?'), run.glyphs[1].glyph_id);
4104     try std.testing.expectEqual(@as(i32, 2 * 8 * 64), run.total_x_advance);
4105 
4106     var recorder = TestRecorder{};
4107     drawGlyphRun(&recorder, 0, atlas.atlas, run, 100, 200, .{ .r = 9, .g = 8, .b = 7, .a = 255 });
4108     try std.testing.expectEqual(@as(usize, 2), recorder.glyph_count);
4109     try std.testing.expectEqual(@as(f32, 108), recorder.last_rect.x);
4110     try std.testing.expectEqual(@as(f32, 200), recorder.last_rect.y);
4111     try std.testing.expectEqual(@as(f32, 9), recorder.last_source.x);
4112 }
4113 
4114 test "bitmap atlas measures scaled multiline text" {
4115     const allocator = std.testing.allocator;
4116     const glyphs = [_]BitmapGlyph{
4117         .{ .codepoint = 'A', .rows = &.{ 0x81, 0xFF } },
4118         .{ .codepoint = '?', .rows = &.{ 0xFF, 0x00 } },
4119     };
4120     var atlas = try Atlas.initFromBitmapGlyphs(
4121         allocator,
4122         glyphs[0..],
4123         .{ .width = 8, .height = 2, .stride = 1 },
4124         16,
4125         test_atlas_cache_limits,
4126     );
4127     defer atlas.deinit();
4128 
4129     const measured = try measure(&atlas, .{
4130         .content = "AZ\nA",
4131         .point_size = 20,
4132         .line_height = 1.5,
4133     });
4134 
4135     try std.testing.expectApproxEqAbs(@as(f32, 20), measured.width, 0.001);
4136     try std.testing.expectApproxEqAbs(@as(f32, 60), measured.height, 0.001);
4137 }
4138 
4139 test "bitmap atlas measures and paints Unicode wrapped text with one line plan" {
4140     const allocator = std.testing.allocator;
4141     const glyphs = [_]BitmapGlyph{
4142         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4143         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4144         .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } },
4145         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
4146         .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } },
4147     };
4148     var atlas = try Atlas.initFromBitmapGlyphs(
4149         allocator,
4150         glyphs[0..],
4151         .{ .width = 8, .height = 2, .stride = 1 },
4152         16,
4153         test_atlas_cache_limits,
4154     );
4155     defer atlas.deinit();
4156     const wrapped = UiText{
4157         .content = "AB CD",
4158         .point_size = 16,
4159         .line_height = 1,
4160         .wrap_width = 24,
4161     };
4162 
4163     const measured = try measure(&atlas, wrapped);
4164     try std.testing.expectApproxEqAbs(@as(f32, 24), measured.width, 0.001);
4165     try std.testing.expectApproxEqAbs(@as(f32, 32), measured.height, 0.001);
4166 
4167     const child = [_]UiNode{.{
4168         .widget_id = 2,
4169         .kind = .label,
4170         .text = wrapped,
4171         .paint = .{ .foreground = .{ .r = 255, .g = 255, .b = 255, .a = 255 } },
4172         .size = .{ .width = 24, .height = 32 },
4173     }};
4174     const surface = gui.model.UiSurfaceTree{
4175         .available_size = .{ .width = 24, .height = 32 },
4176         .root = .{
4177             .widget_id = 1,
4178             .children = child[0..],
4179         },
4180     };
4181     var frame_workspace = gui.frame.Workspace.init(allocator);
4182     defer frame_workspace.deinit();
4183     const frame = try frame_workspace.buildSurface(&surface, .{});
4184     var commands = command.CommandBuffer.init(allocator);
4185     defer commands.deinit();
4186     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
4187     const atlases = AtlasSet{ .entries = entries[0..] };
4188 
4189     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
4190 
4191     try std.testing.expectEqual(@as(usize, 5), commands.items().len);
4192     try std.testing.expectApproxEqAbs(@as(f32, 16), commands.items()[3].rect.y - commands.items()[0].rect.y, 0.001);
4193 }
4194 
4195 test "multiline text geometry distinguishes soft affinity and hard empty lines" {
4196     const allocator = std.testing.allocator;
4197     const glyphs = [_]BitmapGlyph{
4198         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4199         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4200         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
4201         .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } },
4202         .{ .codepoint = 'E', .rows = &.{ 0xFF, 0xFF } },
4203         .{ .codepoint = 'F', .rows = &.{ 0xFF, 0xFF } },
4204         .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } },
4205     };
4206     var atlas = try Atlas.initFromBitmapGlyphs(
4207         allocator,
4208         &glyphs,
4209         .{ .width = 8, .height = 2, .stride = 1 },
4210         16,
4211         test_atlas_cache_limits,
4212     );
4213     defer atlas.deinit();
4214     const entries = [_]AtlasSet.Entry{.{
4215         .face = 0,
4216         .image_index = 0,
4217         .atlas = &atlas,
4218     }};
4219     const atlases = AtlasSet{ .entries = &entries };
4220     const text = UiText{
4221         .content = "AB CD\n\nEF",
4222         .point_size = 16,
4223         .line_height = 1,
4224         .wrap_width = 24,
4225         .horizontal_align = .center,
4226         .vertical_align = .center,
4227     };
4228     const box = Size{ .width = 64, .height = 80 };
4229 
4230     const upstream = try textCaretGeometry(
4231         &atlases,
4232         text,
4233         box,
4234         3,
4235         .upstream,
4236     );
4237     try std.testing.expectEqual(@as(usize, 0), upstream.line_index);
4238     try std.testing.expectApproxEqAbs(@as(f32, 44), upstream.x, 0.001);
4239     try std.testing.expectApproxEqAbs(@as(f32, 8), upstream.y, 0.001);
4240 
4241     const downstream = try textCaretGeometry(
4242         &atlases,
4243         text,
4244         box,
4245         3,
4246         .downstream,
4247     );
4248     try std.testing.expectEqual(@as(usize, 1), downstream.line_index);
4249     try std.testing.expectApproxEqAbs(@as(f32, 24), downstream.x, 0.001);
4250     try std.testing.expectApproxEqAbs(@as(f32, 24), downstream.y, 0.001);
4251 
4252     const hard_end = try textCaretGeometry(
4253         &atlases,
4254         text,
4255         box,
4256         5,
4257         .downstream,
4258     );
4259     try std.testing.expectEqual(@as(usize, 1), hard_end.line_index);
4260     try std.testing.expectApproxEqAbs(@as(f32, 40), hard_end.x, 0.001);
4261 
4262     const empty = try textCaretGeometry(
4263         &atlases,
4264         text,
4265         box,
4266         6,
4267         .downstream,
4268     );
4269     try std.testing.expectEqual(@as(usize, 2), empty.line_index);
4270     try std.testing.expectApproxEqAbs(@as(f32, 32), empty.x, 0.001);
4271     try std.testing.expectApproxEqAbs(@as(f32, 40), empty.y, 0.001);
4272 
4273     const upstream_hit = try textHitTestPoint(
4274         &atlases,
4275         text,
4276         box,
4277         .{ .x = 60, .y = 12 },
4278     );
4279     try std.testing.expectEqual(@as(usize, 3), upstream_hit.byte_offset);
4280     try std.testing.expectEqual(TextCaretAffinity.upstream, upstream_hit.affinity);
4281 
4282     const downstream_hit = try textHitTestPoint(
4283         &atlases,
4284         text,
4285         box,
4286         .{ .x = 0, .y = 24 },
4287     );
4288     try std.testing.expectEqual(@as(usize, 3), downstream_hit.byte_offset);
4289     try std.testing.expectEqual(TextCaretAffinity.downstream, downstream_hit.affinity);
4290 
4291     const bottom = try textHitTestPoint(
4292         &atlases,
4293         text,
4294         box,
4295         .{ .x = 100, .y = 200 },
4296     );
4297     try std.testing.expectEqual(text.content.len, bottom.byte_offset);
4298     try std.testing.expectEqual(@as(usize, 3), bottom.line_index);
4299 }
4300 
4301 test "widget text hit query honors translated clip and clamps to cluster boundaries" {
4302     const allocator = std.testing.allocator;
4303     const glyphs = [_]BitmapGlyph{
4304         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4305         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4306         .{ .codepoint = 0x00E9, .rows = &.{ 0xFF, 0xFF } },
4307     };
4308     var atlas = try Atlas.initFromBitmapGlyphs(
4309         allocator,
4310         &glyphs,
4311         .{ .width = 8, .height = 2, .stride = 1 },
4312         16,
4313         test_atlas_cache_limits,
4314     );
4315     defer atlas.deinit();
4316     const entries = [_]AtlasSet.Entry{.{
4317         .face = 0,
4318         .image_index = 0,
4319         .atlas = &atlas,
4320     }};
4321     const atlases = AtlasSet{ .entries = &entries };
4322     const widget = WidgetFrame{
4323         .root_id = 1,
4324         .widget_id = 2,
4325         .kind = .label,
4326         .rect = .{ .x = 30, .y = -8, .width = 24, .height = 32 },
4327         .visible_rect = .{ .x = 34, .y = 0, .width = 16, .height = 16 },
4328         .paint = .{},
4329         .scroll = .{},
4330         .constraints = .{},
4331         .content_size = .{ .width = 24, .height = 32 },
4332         .focusable = false,
4333         .has_text = true,
4334         .text = .{
4335             .content = "AéB\nAB",
4336             .point_size = 16,
4337             .line_height = 1,
4338             .wrap_width = 24,
4339         },
4340     };
4341 
4342     try std.testing.expect((try textHitTestWidgetPoint(
4343         &atlases,
4344         widget,
4345         .{ .x = 33, .y = 4 },
4346     )) == null);
4347     const visible = (try textHitTestWidgetPoint(
4348         &atlases,
4349         widget,
4350         .{ .x = 42, .y = 4 },
4351     )).?;
4352     const direct = try textHitTestPoint(
4353         &atlases,
4354         widget.text.?,
4355         .{ .width = widget.rect.width, .height = widget.rect.height },
4356         .{ .x = 12, .y = 12 },
4357     );
4358     try std.testing.expectEqual(direct.byte_offset, visible.byte_offset);
4359     try std.testing.expectEqual(direct.affinity, visible.affinity);
4360 
4361     const leading = (try textHitTestWidgetPointClamped(
4362         &atlases,
4363         widget,
4364         .{ .x = -100, .y = -100 },
4365     )).?;
4366     const trailing = (try textHitTestWidgetPointClamped(
4367         &atlases,
4368         widget,
4369         .{ .x = 100, .y = 100 },
4370     )).?;
4371     try std.testing.expect(leading.byte_offset <= trailing.byte_offset);
4372     try std.testing.expect(
4373         leading.byte_offset == 0 or
4374             leading.byte_offset == widget.text.?.content.len or
4375             widget.text.?.content[leading.byte_offset] & 0b1100_0000 != 0b1000_0000,
4376     );
4377     try std.testing.expect(
4378         trailing.byte_offset == 0 or
4379             trailing.byte_offset == widget.text.?.content.len or
4380             widget.text.?.content[trailing.byte_offset] & 0b1100_0000 != 0b1000_0000,
4381     );
4382     try std.testing.expect((try textHitTestWidgetPointClamped(
4383         &atlases,
4384         .{
4385             .root_id = widget.root_id,
4386             .widget_id = widget.widget_id,
4387             .kind = widget.kind,
4388             .rect = widget.rect,
4389             .visible_rect = .{},
4390             .paint = widget.paint,
4391             .scroll = widget.scroll,
4392             .constraints = widget.constraints,
4393             .content_size = widget.content_size,
4394             .focusable = false,
4395             .text = widget.text,
4396         },
4397         .{},
4398     )) == null);
4399 }
4400 
4401 test "multiline text geometry follows mixed metric line boxes and end alignment" {
4402     const allocator = std.testing.allocator;
4403     const base_glyphs = [_]BitmapGlyph{
4404         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4405         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4406         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
4407     };
4408     const large_glyphs = [_]BitmapGlyph{
4409         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F } },
4410         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F } },
4411         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F } },
4412     };
4413     var base_atlas = try Atlas.initFromBitmapGlyphs(
4414         allocator,
4415         &base_glyphs,
4416         .{ .width = 8, .height = 2, .stride = 1 },
4417         16,
4418         test_atlas_cache_limits,
4419     );
4420     defer base_atlas.deinit();
4421     var large_atlas = try Atlas.initFromBitmapGlyphs(
4422         allocator,
4423         &large_glyphs,
4424         .{ .width = 12, .height = 4, .stride = 2 },
4425         24,
4426         test_atlas_cache_limits,
4427     );
4428     defer large_atlas.deinit();
4429     const styles = [_]UiTextStyle{.{
4430         .font_asset_id = 2,
4431         .point_size = 24,
4432     }};
4433     const runs = [_]UiTextRun{.{
4434         .byte_start = 2,
4435         .byte_end = 3,
4436         .style_slot = 1,
4437     }};
4438     const text = UiText{
4439         .content = "A\nBC",
4440         .runs = &runs,
4441         .styles = &styles,
4442         .font_asset_id = 1,
4443         .point_size = 16,
4444         .line_height = 1,
4445         .horizontal_align = .end,
4446         .vertical_align = .end,
4447     };
4448     const entries = [_]AtlasSet.Entry{
4449         .{ .face = 1, .image_index = 0, .atlas = &base_atlas },
4450         .{ .face = 2, .image_index = 1, .atlas = &large_atlas },
4451     };
4452     const atlases = AtlasSet{ .entries = &entries };
4453     const box = Size{ .width = 48, .height = 60 };
4454 
4455     const first = try textCaretGeometry(
4456         &atlases,
4457         text,
4458         box,
4459         1,
4460         .downstream,
4461     );
4462     try std.testing.expectEqual(@as(usize, 0), first.line_index);
4463     try std.testing.expectApproxEqAbs(@as(f32, 48), first.x, 0.001);
4464     try std.testing.expectApproxEqAbs(@as(f32, 20), first.y, 0.001);
4465     try std.testing.expectApproxEqAbs(@as(f32, 16), first.height, 0.001);
4466 
4467     const mixed = try textCaretGeometry(
4468         &atlases,
4469         text,
4470         box,
4471         3,
4472         .downstream,
4473     );
4474     try std.testing.expectEqual(@as(usize, 1), mixed.line_index);
4475     try std.testing.expectApproxEqAbs(@as(f32, 40), mixed.x, 0.001);
4476     try std.testing.expectApproxEqAbs(@as(f32, 36), mixed.y, 0.001);
4477     try std.testing.expectApproxEqAbs(@as(f32, 24), mixed.height, 0.001);
4478 }
4479 
4480 test "warmed multiline text geometry needs no backing allocation" {
4481     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
4482     const allocator = failing.allocator();
4483     const glyphs = [_]BitmapGlyph{
4484         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4485         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4486         .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } },
4487     };
4488     var atlas = try Atlas.initFromBitmapGlyphs(
4489         allocator,
4490         &glyphs,
4491         .{ .width = 8, .height = 2, .stride = 1 },
4492         16,
4493         test_atlas_cache_limits,
4494     );
4495     defer atlas.deinit();
4496     const entries = [_]AtlasSet.Entry{.{
4497         .face = 0,
4498         .image_index = 0,
4499         .atlas = &atlas,
4500     }};
4501     const atlases = AtlasSet{ .entries = &entries };
4502     const text = UiText{
4503         .content = "AB AB\n\nAB AB",
4504         .point_size = 16,
4505         .line_height = 1,
4506         .wrap_width = 24,
4507         .vertical_align = .center,
4508     };
4509     const box = Size{ .width = 48, .height = 96 };
4510     for (0..text.content.len + 1) |byte_offset| {
4511         _ = try textCaretGeometry(
4512             &atlases,
4513             text,
4514             box,
4515             byte_offset,
4516             .downstream,
4517         );
4518     }
4519     _ = try textHitTestPoint(&atlases, text, box, .{ .x = 12, .y = 32 });
4520 
4521     failing.fail_index = failing.alloc_index;
4522     failing.resize_fail_index = failing.resize_index;
4523     for (0..8) |_| {
4524         for (0..text.content.len + 1) |byte_offset| {
4525             _ = try textCaretGeometry(
4526                 &atlases,
4527                 text,
4528                 box,
4529                 byte_offset,
4530                 .downstream,
4531             );
4532         }
4533         _ = try textHitTestPoint(&atlases, text, box, .{ .x = 12, .y = 32 });
4534     }
4535     try std.testing.expect(!failing.has_induced_failure);
4536 }
4537 
4538 test "multiline query geometry matches painted caret at device scales" {
4539     const allocator = std.testing.allocator;
4540     const glyphs = [_]BitmapGlyph{
4541         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4542         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4543         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
4544         .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } },
4545         .{ .codepoint = 'E', .rows = &.{ 0xFF, 0xFF } },
4546         .{ .codepoint = 'F', .rows = &.{ 0xFF, 0xFF } },
4547         .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } },
4548     };
4549     var atlas = try Atlas.initFromBitmapGlyphs(
4550         allocator,
4551         &glyphs,
4552         .{ .width = 8, .height = 2, .stride = 1 },
4553         16,
4554         test_atlas_cache_limits,
4555     );
4556     defer atlas.deinit();
4557     const text = UiText{
4558         .content = "AB CD\nEF",
4559         .point_size = 16,
4560         .line_height = 1,
4561         .wrap_width = 24,
4562         .horizontal_align = .center,
4563         .vertical_align = .center,
4564     };
4565     const child = [_]UiNode{.{
4566         .widget_id = 2,
4567         .kind = .text_input,
4568         .text = text,
4569         .text_selection = .{
4570             .cursor_visible = true,
4571             .cursor_byte_offset = 3,
4572         },
4573         .paint = .{
4574             .foreground = .{ .r = 240, .g = 240, .b = 240, .a = 255 },
4575         },
4576         .size = .{ .width = 64, .height = 80 },
4577     }};
4578     const surface = gui.model.UiSurfaceTree{
4579         .available_size = .{ .width = 80, .height = 96 },
4580         .root = .{ .widget_id = 1, .children = &child },
4581     };
4582     var frame_workspace = gui.frame.Workspace.init(allocator);
4583     defer frame_workspace.deinit();
4584     const frame = try frame_workspace.buildSurface(&surface, .{});
4585     const widget = frame.widgets[1];
4586     const entries = [_]AtlasSet.Entry{.{
4587         .face = 0,
4588         .image_index = 0,
4589         .atlas = &atlas,
4590     }};
4591     const atlases = AtlasSet{ .entries = &entries };
4592     const geometry = try textCaretGeometry(
4593         &atlases,
4594         text,
4595         .{ .width = widget.rect.width, .height = widget.rect.height },
4596         3,
4597         .downstream,
4598     );
4599     var commands = command.CommandBuffer.init(allocator);
4600     defer commands.deinit();
4601     for ([_]f32{ 1, 2 }) |device_scale| {
4602         commands.reset();
4603         try appendFrameCommands(
4604             &commands,
4605             frame,
4606             &atlases,
4607             device_scale,
4608             0,
4609         );
4610         const caret = commands.items()[commands.items().len - 1];
4611         try std.testing.expectEqual(command.Kind.fill, caret.kind);
4612         try std.testing.expectApproxEqAbs(
4613             (widget.rect.x + geometry.x) * device_scale,
4614             caret.rect.x,
4615             0.001,
4616         );
4617         try std.testing.expect(
4618             caret.rect.y >= (widget.rect.y + geometry.y) * device_scale,
4619         );
4620         try std.testing.expect(
4621             caret.rect.y + caret.rect.height <=
4622                 (widget.rect.y + geometry.y + geometry.height) * device_scale,
4623         );
4624     }
4625 }
4626 
4627 test "shape cache preserves cluster and ligature caret geometry" {
4628     const allocator = std.testing.allocator;
4629     const bytes = try filigree.fixtures.createWithGsubLigatureAndGdefCarets(allocator);
4630     var atlas = try initShapeOnlyAtlas(allocator, bytes, 20);
4631     defer atlas.deinit();
4632     const text = "fi";
4633 
4634     const fresh = try atlas.shapeUncached(text);
4635     const expected_glyphs = try allocator.dupe(filigree.ShapedGlyph, fresh.glyphs);
4636     defer allocator.free(expected_glyphs);
4637     const expected_clusters = try allocator.dupe(filigree.Cluster, fresh.clusters);
4638     defer allocator.free(expected_clusters);
4639     const expected_carets = try allocator.dupe(filigree.LigatureCaret, fresh.ligature_carets);
4640     defer allocator.free(expected_carets);
4641     const expected = filigree.GlyphRun{
4642         .glyphs = expected_glyphs,
4643         .clusters = expected_clusters,
4644         .ligature_carets = expected_carets,
4645         .total_x_advance = fresh.total_x_advance,
4646         .total_y_advance = fresh.total_y_advance,
4647         .direction = fresh.direction,
4648         .writing_mode = fresh.writing_mode,
4649         .output_order = fresh.output_order,
4650     };
4651 
4652     const cached = try atlas.shape(text);
4653     try expectGlyphRunEqual(expected, cached);
4654     try std.testing.expect(cached.clusters.len > 0);
4655     try std.testing.expect(cached.ligature_carets.len > 0);
4656     try std.testing.expectEqual(
4657         shapePayloadBytes(text, cached).?,
4658         atlas.cacheStatus().shape.payload_bytes,
4659     );
4660     try std.testing.expectEqual(
4661         filigree.caret.advanceForByteOffset(expected, 1, text.len),
4662         try atlas.advanceForByteOffset(text, 1),
4663     );
4664     try std.testing.expectEqual(
4665         filigree.caret.hitTestAdvance(expected, 4.6, text),
4666         try atlas.hitTestAdvance(text, 4.6),
4667     );
4668 
4669     _ = try atlas.shape("A");
4670     const retained = try atlas.shape(text);
4671     try expectGlyphRunEqual(expected, retained);
4672     try std.testing.expectEqual(cached.glyphs.ptr, retained.glyphs.ptr);
4673     try std.testing.expectEqual(cached.clusters.ptr, retained.clusters.ptr);
4674     try std.testing.expectEqual(cached.ligature_carets.ptr, retained.ligature_carets.ptr);
4675 }
4676 
4677 test "style boundaries inside ligatures keep one glyph and use its cluster start" {
4678     const allocator = std.testing.allocator;
4679     const bytes = try filigree.fixtures.createWithGsubLigatureAndGdefCarets(allocator);
4680     var atlas = try initShapeOnlyAtlas(allocator, bytes, 20);
4681     defer atlas.deinit();
4682     const content = "fi";
4683     const run = try atlas.shape(content);
4684     try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
4685     try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
4686     const glyph_start = sourceOffset(run.glyphs[0].source_start, content.len);
4687     const trailing_style = [_]UiTextRun{.{
4688         .byte_start = 1,
4689         .byte_end = 2,
4690         .foreground = .{ .r = 220, .g = 30, .b = 20 },
4691     }};
4692     var trailing_cursor = TextRunCursor{ .runs = &trailing_style };
4693     try std.testing.expect(trailing_cursor.find(glyph_start) == null);
4694     const leading_style = [_]UiTextRun{.{
4695         .byte_start = 0,
4696         .byte_end = 1,
4697         .foreground = .{ .r = 20, .g = 80, .b = 220 },
4698     }};
4699     var leading_cursor = TextRunCursor{ .runs = &leading_style };
4700     try std.testing.expectEqual(
4701         leading_style[0],
4702         leading_cursor.find(glyph_start).?,
4703     );
4704     try std.testing.expectEqual(
4705         try measure(&atlas, .{ .content = content }),
4706         try measure(&atlas, .{ .content = content, .runs = &trailing_style }),
4707     );
4708 }
4709 
4710 test "cached combining and ZWJ caret queries match fresh shaping" {
4711     const allocator = std.testing.allocator;
4712     const bytes = try filigree.fixtures.createWithOutlines(allocator);
4713     var atlas = try initShapeOnlyAtlas(allocator, bytes, 20);
4714     defer atlas.deinit();
4715     const cases = [_][]const u8{
4716         "e\u{0301}",
4717         "\u{1f469}\u{200d}\u{1f4bb}",
4718     };
4719 
4720     for (cases) |text| {
4721         const fresh = try atlas.shapeUncached(text);
4722         const expected_glyphs = try allocator.dupe(filigree.ShapedGlyph, fresh.glyphs);
4723         defer allocator.free(expected_glyphs);
4724         const expected_clusters = try allocator.dupe(filigree.Cluster, fresh.clusters);
4725         defer allocator.free(expected_clusters);
4726         const expected_carets = try allocator.dupe(filigree.LigatureCaret, fresh.ligature_carets);
4727         defer allocator.free(expected_carets);
4728         const expected = filigree.GlyphRun{
4729             .glyphs = expected_glyphs,
4730             .clusters = expected_clusters,
4731             .ligature_carets = expected_carets,
4732             .total_x_advance = fresh.total_x_advance,
4733             .total_y_advance = fresh.total_y_advance,
4734             .direction = fresh.direction,
4735             .writing_mode = fresh.writing_mode,
4736             .output_order = fresh.output_order,
4737         };
4738         try std.testing.expect(expected.clusters.len > 0);
4739 
4740         for (0..text.len + 1) |byte_offset| {
4741             try std.testing.expectEqual(
4742                 filigree.caret.advanceForByteOffset(expected, byte_offset, text.len),
4743                 try atlas.advanceForByteOffset(text, byte_offset),
4744             );
4745         }
4746         for (0..33) |raw_advance| {
4747             const advance: f32 = @floatFromInt(raw_advance);
4748             try std.testing.expectEqual(
4749                 filigree.caret.hitTestAdvance(expected, advance, text),
4750                 try atlas.hitTestAdvance(text, advance),
4751             );
4752         }
4753     }
4754 }
4755 
4756 test "wrapped markers preserve glyph geometry and partition selection by visual line" {
4757     const allocator = std.testing.allocator;
4758     const glyphs = [_]BitmapGlyph{
4759         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4760         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4761         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
4762         .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } },
4763         .{ .codepoint = 'E', .rows = &.{ 0xFF, 0xFF } },
4764         .{ .codepoint = 'F', .rows = &.{ 0xFF, 0xFF } },
4765         .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } },
4766     };
4767     var atlas = try Atlas.initFromBitmapGlyphs(
4768         allocator,
4769         &glyphs,
4770         .{ .width = 8, .height = 2, .stride = 1 },
4771         16,
4772         test_atlas_cache_limits,
4773     );
4774     defer atlas.deinit();
4775     const text = UiText{
4776         .content = "AB CD EF",
4777         .point_size = 16,
4778         .line_height = 1,
4779         .wrap_width = 24,
4780     };
4781     const nodes = [_]UiNode{
4782         .{
4783             .widget_id = 2,
4784             .kind = .text_input,
4785             .text = text,
4786             .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
4787             .size = .{ .width = 24, .height = 48 },
4788         },
4789         .{
4790             .widget_id = 2,
4791             .kind = .text_input,
4792             .text = text,
4793             .text_selection = .{
4794                 .cursor_visible = true,
4795                 .cursor_byte_offset = 7,
4796                 .selection_active = true,
4797                 .selection_anchor_byte_offset = 1,
4798                 .selection_focus_byte_offset = 7,
4799             },
4800             .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
4801             .size = .{ .width = 24, .height = 48 },
4802         },
4803         .{
4804             .widget_id = 2,
4805             .kind = .text_input,
4806             .text = text,
4807             .text_selection = .{
4808                 .cursor_visible = true,
4809                 .cursor_byte_offset = 7,
4810             },
4811             .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
4812             .size = .{ .width = 24, .height = 48 },
4813         },
4814     };
4815     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
4816     const atlases = AtlasSet{ .entries = &entries };
4817     var frames: [3]gui.frame.Workspace = .{
4818         gui.frame.Workspace.init(allocator),
4819         gui.frame.Workspace.init(allocator),
4820         gui.frame.Workspace.init(allocator),
4821     };
4822     defer for (&frames) |*frame| frame.deinit();
4823     var buffers: [3]command.CommandBuffer = .{
4824         command.CommandBuffer.init(allocator),
4825         command.CommandBuffer.init(allocator),
4826         command.CommandBuffer.init(allocator),
4827     };
4828     defer for (&buffers) |*buffer| buffer.deinit();
4829 
4830     for (0..nodes.len) |index| {
4831         const surface = gui.model.UiSurfaceTree{
4832             .available_size = .{ .width = 24, .height = 48 },
4833             .root = .{ .widget_id = 1, .children = nodes[index .. index + 1] },
4834         };
4835         const frame = try frames[index].buildSurface(&surface, .{});
4836         try appendFrameCommands(&buffers[index], frame, &atlases, 1, 0);
4837     }
4838 
4839     var unfocused_glyphs: [16]command.Command = undefined;
4840     var unfocused_count: usize = 0;
4841     for (buffers[0].items()) |item| {
4842         if (item.kind != .glyph) continue;
4843         unfocused_glyphs[unfocused_count] = item;
4844         unfocused_count += 1;
4845     }
4846     var focused_count: usize = 0;
4847     var selection_y: [3]f32 = undefined;
4848     var selection_count: usize = 0;
4849     for (buffers[1].items()) |item| {
4850         if (item.kind == .glyph) {
4851             const expected = unfocused_glyphs[focused_count];
4852             try std.testing.expectEqual(expected.kind, item.kind);
4853             try std.testing.expectEqual(expected.rect, item.rect);
4854             try std.testing.expectEqual(expected.clip, item.clip);
4855             try std.testing.expectEqual(expected.source, item.source);
4856             try std.testing.expectEqual(expected.color, item.color);
4857             try std.testing.expectEqual(expected.image_index, item.image_index);
4858             focused_count += 1;
4859         }
4860         if (item.kind == .fill and item.color.a == 64) {
4861             selection_y[selection_count] = item.rect.y;
4862             selection_count += 1;
4863         }
4864     }
4865     try std.testing.expectEqual(@as(usize, 8), unfocused_count);
4866     try std.testing.expectEqual(unfocused_count, focused_count);
4867     try std.testing.expectEqual(@as(usize, 3), selection_count);
4868     try std.testing.expectApproxEqAbs(@as(f32, 16), selection_y[1] - selection_y[0], 0.001);
4869     try std.testing.expectApproxEqAbs(@as(f32, 16), selection_y[2] - selection_y[1], 0.001);
4870 
4871     const fragment_id = command.FragmentId{
4872         .root_id = 1,
4873         .element_id = 2,
4874         .namespace = command.fragment_namespace_widget,
4875         .part = command.fragment_part_text,
4876     };
4877     try buffers[0].commitFragment(fragment_id, 0);
4878     try buffers[2].commitFragment(fragment_id, 0);
4879     var retained = gui.paint.RetainedCommands.init(allocator);
4880     defer retained.deinit();
4881     try retained.retain(&buffers[0]);
4882     const damage = try retained.diff(
4883         &buffers[2],
4884         &.{Region.full(320, 200)},
4885         320,
4886         200,
4887     );
4888     const narrowed = switch (damage) {
4889         .semantic => return error.ExpectedNarrowedDamage,
4890         .narrowed => |value| value,
4891     };
4892     try std.testing.expectEqual(@as(usize, 1), narrowed.slice().len);
4893     const bounds = narrowed.bounding().?;
4894     try std.testing.expect(bounds.width <= 3);
4895     try std.testing.expect(bounds.height <= 20);
4896 }
4897 
4898 test "bitmap wrapped measurement reuses warmed line workspace" {
4899     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
4900     const allocator = failing.allocator();
4901     const glyphs = [_]BitmapGlyph{
4902         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4903         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4904         .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } },
4905         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
4906         .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } },
4907     };
4908     var atlas = try Atlas.initFromBitmapGlyphs(
4909         allocator,
4910         glyphs[0..],
4911         .{ .width = 8, .height = 2, .stride = 1 },
4912         16,
4913         test_atlas_cache_limits,
4914     );
4915     defer atlas.deinit();
4916     const wrapped = UiText{
4917         .content = "AB CD",
4918         .point_size = 16,
4919         .line_height = 1,
4920         .wrap_width = 24,
4921     };
4922     _ = try measureUncached(&atlas, wrapped);
4923 
4924     failing.fail_index = failing.alloc_index;
4925     failing.resize_fail_index = failing.resize_index;
4926     const repeated = try measureUncached(&atlas, wrapped);
4927 
4928     try std.testing.expectApproxEqAbs(@as(f32, 32), repeated.height, 0.001);
4929     try std.testing.expect(!failing.has_induced_failure);
4930 }
4931 
4932 test "warmed visual line and caret queries need no backing allocation" {
4933     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
4934     const allocator = failing.allocator();
4935     const glyphs = [_]BitmapGlyph{
4936         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
4937         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
4938         .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } },
4939     };
4940     var atlas = try Atlas.initFromBitmapGlyphs(
4941         allocator,
4942         &glyphs,
4943         .{ .width = 8, .height = 2, .stride = 1 },
4944         16,
4945         test_atlas_cache_limits,
4946     );
4947     defer atlas.deinit();
4948     const text = "AB AB AB AB AB AB";
4949     _ = try LinePlan.init(&atlas, text, 24);
4950     for (0..text.len + 1) |byte_offset| {
4951         _ = try atlas.advanceForByteOffset(text, byte_offset);
4952     }
4953 
4954     failing.fail_index = failing.alloc_index;
4955     failing.resize_fail_index = failing.resize_index;
4956     for (0..8) |_| {
4957         const plan = try LinePlan.init(&atlas, text, 24);
4958         var iterator = plan.iterator();
4959         while (iterator.next(&plan)) |visual| {
4960             _ = visual.advanceForByteOffset(plan.run, visual.byte_start, text.len);
4961             _ = visual.advanceForByteOffset(plan.run, visual.byte_end, text.len);
4962         }
4963         for (0..text.len + 1) |byte_offset| {
4964             const advance = try atlas.advanceForByteOffset(text, byte_offset);
4965             _ = try atlas.hitTestAdvance(text, advance);
4966         }
4967     }
4968     try std.testing.expect(!failing.has_induced_failure);
4969 }
4970 
4971 test "atlas metrics derive from face and bitmap cell geometry" {
4972     const allocator = std.testing.allocator;
4973     var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 });
4974     defer scratch.deinit(allocator);
4975     const bytes = try filigree.fixtures.createWithOutlines(allocator);
4976     var outline_atlas = try Atlas.initFromOwnedBytes(
4977         allocator,
4978         &scratch,
4979         bytes,
4980         20,
4981         test_atlas_cache_limits,
4982         test_output_limits,
4983     );
4984     defer outline_atlas.deinit();
4985 
4986     const outline_metrics = outline_atlas.metrics();
4987     const face = outline_atlas.backend.outline.font.face;
4988     const upem: f32 = @floatFromInt(face.units_per_em);
4989     try std.testing.expectApproxEqAbs(
4990         @as(f32, @floatFromInt(face.ascender)) * 20.0 / upem,
4991         outline_metrics.ascent,
4992         0.001,
4993     );
4994     try std.testing.expectApproxEqAbs(
4995         @as(f32, @floatFromInt(-@as(i32, face.descender))) * 20.0 / upem,
4996         outline_metrics.descent,
4997         0.001,
4998     );
4999     try std.testing.expect(outline_metrics.height() > 0);
5000 
5001     const glyphs = [_]BitmapGlyph{.{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }};
5002     var bitmap_atlas = try Atlas.initFromBitmapGlyphs(
5003         allocator,
5004         glyphs[0..],
5005         .{ .width = 8, .height = 2, .stride = 1 },
5006         16,
5007         test_atlas_cache_limits,
5008     );
5009     defer bitmap_atlas.deinit();
5010 
5011     const bitmap_metrics = bitmap_atlas.metrics();
5012     try std.testing.expectApproxEqAbs(@as(f32, 2), bitmap_metrics.ascent, 0.001);
5013     try std.testing.expectApproxEqAbs(@as(f32, 0), bitmap_metrics.descent, 0.001);
5014     try std.testing.expectApproxEqAbs(@as(f32, 2), bitmap_metrics.height(), 0.001);
5015 }
5016 
5017 test "fallback face segments preserve primary runs and paint final notdef" {
5018     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
5019     const allocator = failing.allocator();
5020     var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 });
5021     defer scratch.deinit(allocator);
5022     const primary_bytes = try filigree.fixtures.createWithOutlines(allocator);
5023     var primary = try Atlas.initFromOwnedBytes(
5024         allocator,
5025         &scratch,
5026         primary_bytes,
5027         18,
5028         test_atlas_cache_limits,
5029         test_output_limits,
5030     );
5031     defer primary.deinit();
5032     const fallback_bytes = try filigree.fixtures.createFallbackWithOutlines(allocator);
5033     var fallback_atlas = try Atlas.initFromOwnedBytes(
5034         allocator,
5035         &scratch,
5036         fallback_bytes,
5037         18,
5038         test_atlas_cache_limits,
5039         test_output_limits,
5040     );
5041     defer fallback_atlas.deinit();
5042     var fallback = try fallback_mod.Engine.init(allocator, .{
5043         .max_source_units = 32,
5044         .cache_entries = 8,
5045         .cache_payload_bytes = 1024,
5046     }, test_output_limits);
5047     defer fallback.deinit();
5048 
5049     const entries = [_]AtlasSet.Entry{.{
5050         .face = 1,
5051         .image_index = 3,
5052         .atlas = &primary,
5053     }};
5054     const fallback_entries = [_]AtlasSet.Entry{.{
5055         .face = 0,
5056         .image_index = 9,
5057         .atlas = &fallback_atlas,
5058     }};
5059     const atlases = AtlasSet{
5060         .entries = &entries,
5061         .fallback_entries = &fallback_entries,
5062         .fallback = &fallback,
5063     };
5064     const content = "A\u{3b2}B\u{3bb}";
5065     const text = UiText{
5066         .content = content,
5067         .font_asset_id = 1,
5068         .point_size = 18,
5069         .line_height = 1,
5070     };
5071     const plan = try LinePlan.initComposite(&primary, &atlases, text, content, 0, 0);
5072     try std.testing.expectEqual(@as(usize, 4), plan.run.glyphs.len);
5073     try std.testing.expectEqual(@as(usize, 4), plan.metric_spans.len);
5074     try std.testing.expectEqual(@as(u32, 3), plan.metric_spans[plan.glyph_sources[0].segment_index].image_index);
5075     try std.testing.expectEqual(@as(u32, 9), plan.metric_spans[plan.glyph_sources[1].segment_index].image_index);
5076     try std.testing.expectEqual(@as(u32, 3), plan.metric_spans[plan.glyph_sources[2].segment_index].image_index);
5077     try std.testing.expectEqual(@as(u32, 9), plan.metric_spans[plan.glyph_sources[3].segment_index].image_index);
5078     try std.testing.expectEqual(@as(u32, 0), plan.run.glyphs[3].glyph_id);
5079     try std.testing.expect(!fallback_atlas.glyph_index.contains(0));
5080 
5081     const surface = gui.model.UiSurfaceTree{
5082         .available_size = .{ .width = 96, .height = 24 },
5083         .root = .{
5084             .widget_id = 1,
5085             .children = &.{.{
5086                 .widget_id = 2,
5087                 .kind = .label,
5088                 .text = text,
5089                 .size = .{ .width = 96, .height = 24 },
5090             }},
5091         },
5092     };
5093     var frame_workspace = gui.frame.Workspace.init(allocator);
5094     defer frame_workspace.deinit();
5095     const frame = try frame_workspace.buildSurface(&surface, .{
5096         .resolvers = frameResolvers(&atlases),
5097     });
5098     var commands = command.CommandBuffer.init(allocator);
5099     defer commands.deinit();
5100     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
5101     var glyph_images: [3]u32 = undefined;
5102     var glyph_count: usize = 0;
5103     var fill_count: usize = 0;
5104     for (commands.items()) |item| {
5105         switch (item.kind) {
5106             .glyph => {
5107                 glyph_images[glyph_count] = item.image_index;
5108                 glyph_count += 1;
5109             },
5110             .fill => fill_count += 1,
5111             else => {},
5112         }
5113     }
5114     try std.testing.expectEqual(@as(usize, 3), glyph_count);
5115     try std.testing.expectEqualSlices(u32, &.{ 3, 9, 3 }, &glyph_images);
5116     try std.testing.expectEqual(@as(usize, 4), fill_count);
5117 
5118     const primary_plan = try textCaretPlan(&atlases, .{
5119         .content = "AB",
5120         .font_asset_id = 1,
5121         .point_size = 18,
5122         .line_height = 1,
5123     });
5124     try std.testing.expectEqual(@as(usize, 0), primary_plan.plan.glyph_sources.len);
5125     try expectGlyphRunEqual(try primary.shape("AB"), primary_plan.plan.run);
5126 
5127     const primary_font = primary.outlineFont().?;
5128     const fallback_font = fallback_atlas.outlineFont().?;
5129     _ = try fallback.segments(primary_font, fallback_font, content);
5130     failing.fail_index = failing.alloc_index;
5131     failing.resize_fail_index = failing.resize_index;
5132     for (0..1024) |_| {
5133         _ = try fallback.segments(primary_font, fallback_font, content);
5134     }
5135     try std.testing.expect(!failing.has_induced_failure);
5136 }
5137 
5138 fn appendComposerPromptCommands(
5139     atlases: *const AtlasSet,
5140     workspace: *gui.frame.Workspace,
5141     commands: *command.CommandBuffer,
5142 ) !void {
5143     const surface = gui.model.UiSurfaceTree{
5144         .available_size = .{ .width = 256, .height = 24 },
5145         .root = .{
5146             .widget_id = 1,
5147             .children = &.{.{
5148                 .widget_id = 2,
5149                 .kind = .label,
5150                 .text = .{
5151                     .content = "\u{276f} review styled text",
5152                     .font_asset_id = 1,
5153                     .point_size = 18,
5154                     .line_height = 1,
5155                 },
5156                 .size = .{ .width = 256, .height = 24 },
5157             }},
5158         },
5159     };
5160     const frame = try workspace.buildSurface(&surface, .{
5161         .resolvers = frameResolvers(atlases),
5162     });
5163     try appendFrameCommands(commands, frame, atlases, 1, 0);
5164 }
5165 
5166 test "composer prompt glyph renders exactly with and without fallback face" {
5167     const allocator = std.testing.allocator;
5168     var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 });
5169     defer scratch.deinit(allocator);
5170     const primary_bytes = try filigree.fixtures.createWithOutlines(allocator);
5171     var primary = try Atlas.initFromOwnedBytes(
5172         allocator,
5173         &scratch,
5174         primary_bytes,
5175         18,
5176         test_atlas_cache_limits,
5177         test_output_limits,
5178     );
5179     defer primary.deinit();
5180     const fallback_bytes = try filigree.fixtures.createFallbackWithOutlines(allocator);
5181     var fallback_atlas = try Atlas.initFromOwnedBytes(
5182         allocator,
5183         &scratch,
5184         fallback_bytes,
5185         18,
5186         test_atlas_cache_limits,
5187         test_output_limits,
5188     );
5189     defer fallback_atlas.deinit();
5190     var fallback = try fallback_mod.Engine.init(allocator, .{
5191         .max_source_units = 64,
5192         .cache_entries = 4,
5193         .cache_payload_bytes = 1024,
5194     }, test_output_limits);
5195     defer fallback.deinit();
5196     const entries = [_]AtlasSet.Entry{.{ .face = 1, .image_index = 3, .atlas = &primary }};
5197     const fallback_entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 9, .atlas = &fallback_atlas }};
5198     const atlas_sets = [_]AtlasSet{
5199         .{ .entries = &entries },
5200         .{ .entries = &entries, .fallback_entries = &fallback_entries, .fallback = &fallback },
5201     };
5202     var workspaces = [_]gui.frame.Workspace{ gui.frame.Workspace.init(allocator), gui.frame.Workspace.init(allocator) };
5203     defer for (&workspaces) |*workspace| workspace.deinit();
5204     var buffers = [_]command.CommandBuffer{ command.CommandBuffer.init(allocator), command.CommandBuffer.init(allocator) };
5205     defer for (&buffers) |*buffer| buffer.deinit();
5206     for (&atlas_sets, 0..) |*atlases, index| {
5207         try appendComposerPromptCommands(atlases, &workspaces[index], &buffers[index]);
5208     }
5209     try std.testing.expectEqual(buffers[0].items().len, buffers[1].items().len);
5210     for (buffers[0].items(), buffers[1].items()) |without_fallback, with_fallback| {
5211         try std.testing.expect(std.meta.eql(without_fallback, with_fallback));
5212     }
5213 }
5214 
5215 test "outline atlas scratch admits its measured maximum and rejects one byte less" {
5216     const allocator = std.testing.allocator;
5217     var survey = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 });
5218     defer survey.deinit(allocator);
5219     const survey_bytes = try filigree.fixtures.createWithOutlines(allocator);
5220     var surveyed = try Atlas.initFromOwnedBytes(
5221         allocator,
5222         &survey,
5223         survey_bytes,
5224         20,
5225         test_atlas_cache_limits,
5226         test_output_limits,
5227     );
5228     surveyed.deinit();
5229     const measured = survey.status().high_water_bytes;
5230     try std.testing.expect(measured > 0);
5231 
5232     var exact = try AtlasScratch.init(allocator, .{ .bytes = measured });
5233     defer exact.deinit(allocator);
5234     const exact_bytes = try filigree.fixtures.createWithOutlines(allocator);
5235     var admitted = try Atlas.initFromOwnedBytes(
5236         allocator,
5237         &exact,
5238         exact_bytes,
5239         20,
5240         test_atlas_cache_limits,
5241         test_output_limits,
5242     );
5243     admitted.deinit();
5244     try std.testing.expectEqual(measured, exact.status().high_water_bytes);
5245     try std.testing.expectEqual(@as(usize, 0), exact.status().exhaustions);
5246 
5247     var short = try AtlasScratch.init(allocator, .{ .bytes = measured - 1 });
5248     defer short.deinit(allocator);
5249     const short_bytes = try filigree.fixtures.createWithOutlines(allocator);
5250     try std.testing.expectError(
5251         error.OutOfMemory,
5252         Atlas.initFromOwnedBytes(
5253             allocator,
5254             &short,
5255             short_bytes,
5256             20,
5257             test_atlas_cache_limits,
5258             test_output_limits,
5259         ),
5260     );
5261     try std.testing.expectEqual(@as(usize, 1), short.status().epochs);
5262     try std.testing.expect(short.status().exhaustions > 0);
5263 }
5264 
5265 test "appendFrameCommands keeps glyph runs inside their line box" {
5266     const allocator = std.testing.allocator;
5267     const glyphs = [_]BitmapGlyph{
5268         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
5269         .{ .codepoint = 'g', .rows = &.{ 0xFF, 0xFF } },
5270     };
5271     var atlas = try Atlas.initFromBitmapGlyphs(
5272         allocator,
5273         glyphs[0..],
5274         .{ .width = 8, .height = 2, .stride = 1 },
5275         16,
5276         test_atlas_cache_limits,
5277     );
5278     defer atlas.deinit();
5279     const child = [_]gui.model.UiNode{.{
5280         .widget_id = 2,
5281         .kind = .label,
5282         .text = .{ .content = "Ag\ngA", .point_size = 16, .line_height = 1.5 },
5283         .paint = .{ .foreground = .{ .r = 255, .g = 255, .b = 255, .a = 255 } },
5284         .size = .{ .width = 64, .height = 48 },
5285     }};
5286     const surface = gui.model.UiSurfaceTree{
5287         .available_size = .{ .width = 80, .height = 64 },
5288         .root = .{
5289             .widget_id = 1,
5290             .children = child[0..],
5291         },
5292     };
5293     var frame_workspace = gui.frame.Workspace.init(allocator);
5294     defer frame_workspace.deinit();
5295     const frame = try frame_workspace.buildSurface(&surface, .{});
5296     var commands = command.CommandBuffer.init(allocator);
5297     defer commands.deinit();
5298 
5299     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
5300     const atlases = AtlasSet{ .entries = entries[0..] };
5301     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
5302 
5303     var block_y: f32 = 0;
5304     for (frame.widgets) |widget| {
5305         if (widget.text != null) block_y = widget.rect.y;
5306     }
5307     const line_height: f32 = 24;
5308     const half_leading: f32 = (line_height - 2) / 2;
5309     var glyph_index: usize = 0;
5310     for (commands.items()) |item| {
5311         if (item.kind != .glyph) continue;
5312         const line_index: f32 = if (glyph_index < 2) 0 else 1;
5313         const line_top = block_y + line_index * line_height;
5314         try std.testing.expectApproxEqAbs(line_top + half_leading, item.rect.y, 0.001);
5315         try std.testing.expect(item.rect.y >= line_top);
5316         try std.testing.expect(item.rect.y + item.rect.height <= line_top + line_height);
5317         glyph_index += 1;
5318     }
5319     try std.testing.expectEqual(@as(usize, 4), glyph_index);
5320 }
5321 
5322 test "appendFrameCommands paints the glyph under a block cursor in the background color" {
5323     const allocator = std.testing.allocator;
5324     const glyphs = [_]BitmapGlyph{
5325         .{ .codepoint = 'm', .rows = &.{ 0xFF, 0xFF } },
5326         .{ .codepoint = 'e', .rows = &.{ 0xFF, 0xFF } },
5327     };
5328     var atlas = try Atlas.initFromBitmapGlyphs(
5329         allocator,
5330         glyphs[0..],
5331         .{ .width = 8, .height = 2, .stride = 1 },
5332         16,
5333         test_atlas_cache_limits,
5334     );
5335     defer atlas.deinit();
5336     const background = gui.model.UiColor{ .r = 30, .g = 33, .b = 39, .a = 255 };
5337     const child = [_]gui.model.UiNode{.{
5338         .widget_id = 2,
5339         .kind = .label,
5340         .text = .{ .content = "me", .point_size = 16, .line_height = 1.5 },
5341         .text_selection = .{ .cursor_visible = true, .cursor_block = true, .cursor_byte_offset = 0 },
5342         .paint = .{
5343             .foreground = .{ .r = 220, .g = 223, .b = 228, .a = 255 },
5344             .background = background,
5345         },
5346         .size = .{ .width = 64, .height = 24 },
5347     }};
5348     const surface = gui.model.UiSurfaceTree{
5349         .available_size = .{ .width = 80, .height = 32 },
5350         .root = .{
5351             .widget_id = 1,
5352             .children = child[0..],
5353         },
5354     };
5355     var frame_workspace = gui.frame.Workspace.init(allocator);
5356     defer frame_workspace.deinit();
5357     const frame = try frame_workspace.buildSurface(&surface, .{});
5358     var commands = command.CommandBuffer.init(allocator);
5359     defer commands.deinit();
5360 
5361     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
5362     const atlases = AtlasSet{ .entries = entries[0..] };
5363     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
5364 
5365     var saw_fill = false;
5366     var glyph_colors: [2]Color = undefined;
5367     var glyph_count: usize = 0;
5368     for (commands.items()) |item| {
5369         switch (item.kind) {
5370             .fill => saw_fill = true,
5371             .glyph => {
5372                 glyph_colors[glyph_count] = item.color;
5373                 glyph_count += 1;
5374             },
5375             else => {},
5376         }
5377     }
5378     try std.testing.expect(saw_fill);
5379     try std.testing.expectEqual(@as(usize, 2), glyph_count);
5380     try std.testing.expectEqual(background.r, glyph_colors[0].r);
5381     try std.testing.expectEqual(background.g, glyph_colors[0].g);
5382     try std.testing.expectEqual(background.b, glyph_colors[0].b);
5383     try std.testing.expectEqual(@as(u8, 220), glyph_colors[1].r);
5384 }
5385 
5386 test "appendFrameCommands scales glyph commands to text point size" {
5387     const allocator = std.testing.allocator;
5388     const glyphs = [_]BitmapGlyph{
5389         .{ .codepoint = 'A', .rows = &.{ 0x81, 0xFF } },
5390     };
5391     var atlas = try Atlas.initFromBitmapGlyphs(
5392         allocator,
5393         glyphs[0..],
5394         .{ .width = 8, .height = 2, .stride = 1 },
5395         16,
5396         test_atlas_cache_limits,
5397     );
5398     defer atlas.deinit();
5399     const child = [_]gui.model.UiNode{.{
5400         .widget_id = 2,
5401         .kind = .label,
5402         .text = .{ .content = "A", .point_size = 32, .line_height = 1.0 },
5403         .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
5404         .size = .{ .width = 64, .height = 40 },
5405     }};
5406     const surface = gui.model.UiSurfaceTree{
5407         .available_size = .{ .width = 80, .height = 48 },
5408         .root = .{
5409             .widget_id = 1,
5410             .children = child[0..],
5411         },
5412     };
5413     var frame_workspace = gui.frame.Workspace.init(allocator);
5414     defer frame_workspace.deinit();
5415     const frame = try frame_workspace.buildSurface(&surface, .{});
5416     var commands = command.CommandBuffer.init(allocator);
5417     defer commands.deinit();
5418 
5419     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
5420     const atlases = AtlasSet{ .entries = entries[0..] };
5421     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
5422 
5423     try std.testing.expect(commands.items().len > 0);
5424     try std.testing.expectApproxEqAbs(@as(f32, 16), commands.items()[0].rect.width, 0.001);
5425     try std.testing.expectApproxEqAbs(@as(f32, 4), commands.items()[0].rect.height, 0.001);
5426 }
5427 
5428 test "appendFrameCommands scales glyph geometry by device scale and keeps atlas sources" {
5429     const allocator = std.testing.allocator;
5430     const glyphs = [_]BitmapGlyph{
5431         .{ .codepoint = 'A', .rows = &.{ 0x81, 0xFF } },
5432     };
5433     var atlas = try Atlas.initFromBitmapGlyphs(
5434         allocator,
5435         glyphs[0..],
5436         .{ .width = 8, .height = 2, .stride = 1 },
5437         16,
5438         test_atlas_cache_limits,
5439     );
5440     defer atlas.deinit();
5441     const child = [_]gui.model.UiNode{.{
5442         .widget_id = 2,
5443         .kind = .label,
5444         .text = .{ .content = "A", .point_size = 16, .line_height = 1.0 },
5445         .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
5446         .size = .{ .width = 64, .height = 40 },
5447     }};
5448     const surface = gui.model.UiSurfaceTree{
5449         .available_size = .{ .width = 80, .height = 48 },
5450         .root = .{
5451             .widget_id = 1,
5452             .children = child[0..],
5453         },
5454     };
5455     var frame_workspace = gui.frame.Workspace.init(allocator);
5456     defer frame_workspace.deinit();
5457     const frame = try frame_workspace.buildSurface(&surface, .{});
5458 
5459     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
5460     const atlases = AtlasSet{ .entries = entries[0..] };
5461 
5462     var logical = command.CommandBuffer.init(allocator);
5463     defer logical.deinit();
5464     try appendFrameCommands(&logical, frame, &atlases, 1, 0);
5465     var device = command.CommandBuffer.init(allocator);
5466     defer device.deinit();
5467     try appendFrameCommands(&device, frame, &atlases, 2, 0);
5468 
5469     try std.testing.expectEqual(logical.items().len, device.items().len);
5470     for (logical.items(), device.items()) |one, two| {
5471         try std.testing.expectEqual(one.kind, two.kind);
5472         try std.testing.expectApproxEqAbs(one.rect.x * 2, two.rect.x, 0.001);
5473         try std.testing.expectApproxEqAbs(one.rect.y * 2, two.rect.y, 0.001);
5474         try std.testing.expectApproxEqAbs(one.rect.width * 2, two.rect.width, 0.001);
5475         try std.testing.expectApproxEqAbs(one.rect.height * 2, two.rect.height, 0.001);
5476         try std.testing.expectApproxEqAbs(one.clip.x * 2, two.clip.x, 0.001);
5477         try std.testing.expectEqual(one.source.x, two.source.x);
5478         try std.testing.expectEqual(one.source.width, two.source.width);
5479     }
5480 }
5481 
5482 test "appendFrameCommands draws text selection fills before glyph commands" {
5483     const allocator = std.testing.allocator;
5484     const glyphs = [_]BitmapGlyph{
5485         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
5486         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
5487         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
5488         .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } },
5489     };
5490     var atlas = try Atlas.initFromBitmapGlyphs(
5491         allocator,
5492         glyphs[0..],
5493         .{ .width = 8, .height = 2, .stride = 1 },
5494         16,
5495         test_atlas_cache_limits,
5496     );
5497     defer atlas.deinit();
5498     const child = [_]gui.model.UiNode{.{
5499         .widget_id = 2,
5500         .kind = .text_input,
5501         .text = .{ .content = "ABCD", .point_size = 16, .line_height = 1.0 },
5502         .text_selection = .{
5503             .cursor_visible = true,
5504             .cursor_byte_offset = 2,
5505             .selection_active = true,
5506             .selection_anchor_byte_offset = 1,
5507             .selection_focus_byte_offset = 3,
5508         },
5509         .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
5510         .size = .{ .width = 64, .height = 20 },
5511     }};
5512     const surface = gui.model.UiSurfaceTree{
5513         .available_size = .{ .width = 80, .height = 32 },
5514         .root = .{
5515             .widget_id = 1,
5516             .children = child[0..],
5517         },
5518     };
5519     var frame_workspace = gui.frame.Workspace.init(allocator);
5520     defer frame_workspace.deinit();
5521     const frame = try frame_workspace.buildSurface(&surface, .{});
5522     var commands = command.CommandBuffer.init(allocator);
5523     defer commands.deinit();
5524 
5525     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
5526     const atlases = AtlasSet{ .entries = entries[0..] };
5527     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
5528 
5529     try std.testing.expectEqual(@as(usize, 6), commands.items().len);
5530     try std.testing.expectEqual(command.Kind.fill, commands.items()[0].kind);
5531     try std.testing.expectEqual(command.Kind.glyph, commands.items()[1].kind);
5532     try std.testing.expectEqual(command.Kind.fill, commands.items()[5].kind);
5533     try std.testing.expectApproxEqAbs(@as(f32, 8), commands.items()[0].rect.x, 0.001);
5534     try std.testing.expectApproxEqAbs(@as(f32, 16), commands.items()[0].rect.width, 0.001);
5535     try std.testing.expectApproxEqAbs(@as(f32, 16), commands.items()[5].rect.x, 0.001);
5536     try std.testing.expect(commands.items()[0].rect.width > commands.items()[5].rect.width);
5537 }
5538 
5539 test "styled text runs preserve layout caret and selection geometry" {
5540     const allocator = std.testing.allocator;
5541     const glyphs = [_]BitmapGlyph{
5542         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
5543         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
5544         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
5545         .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } },
5546     };
5547     var atlas = try Atlas.initFromBitmapGlyphs(
5548         allocator,
5549         &glyphs,
5550         .{ .width = 8, .height = 2, .stride = 1 },
5551         16,
5552         test_atlas_cache_limits,
5553     );
5554     defer atlas.deinit();
5555     const runs = [_]UiTextRun{
5556         .{
5557             .byte_start = 1,
5558             .byte_end = 2,
5559             .foreground = .{ .r = 220, .g = 30, .b = 20, .a = 255 },
5560             .background = .{ .r = 15, .g = 35, .b = 90, .a = 180 },
5561             .underline = true,
5562         },
5563         .{
5564             .byte_start = 2,
5565             .byte_end = 4,
5566             .foreground = .{ .r = 20, .g = 190, .b = 70, .a = 255 },
5567             .strikethrough = true,
5568         },
5569     };
5570     const plain = UiText{ .content = "ABCD", .point_size = 16, .line_height = 1 };
5571     const styled = UiText{
5572         .content = plain.content,
5573         .runs = &runs,
5574         .point_size = plain.point_size,
5575         .line_height = plain.line_height,
5576         .wrap_width = plain.wrap_width,
5577     };
5578     const selection = UiTextSelection{
5579         .cursor_visible = true,
5580         .cursor_byte_offset = 3,
5581         .selection_active = true,
5582         .selection_anchor_byte_offset = 1,
5583         .selection_focus_byte_offset = 4,
5584     };
5585     try std.testing.expectEqual(try measure(&atlas, plain), try measure(&atlas, styled));
5586     try std.testing.expectEqual(@as(usize, 1), atlas.cacheStatus().measure.entries);
5587 
5588     const nodes = [_]UiNode{
5589         .{
5590             .widget_id = 2,
5591             .kind = .label,
5592             .text = plain,
5593             .text_selection = selection,
5594             .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
5595             .size = .{ .width = 64, .height = 16 },
5596         },
5597         .{
5598             .widget_id = 2,
5599             .kind = .label,
5600             .text = styled,
5601             .text_selection = selection,
5602             .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
5603             .size = .{ .width = 64, .height = 16 },
5604         },
5605     };
5606     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
5607     const atlases = AtlasSet{ .entries = &entries };
5608     var frames = [_]gui.frame.Workspace{
5609         gui.frame.Workspace.init(allocator),
5610         gui.frame.Workspace.init(allocator),
5611     };
5612     defer for (&frames) |*frame| frame.deinit();
5613     var buffers = [_]command.CommandBuffer{
5614         command.CommandBuffer.init(allocator),
5615         command.CommandBuffer.init(allocator),
5616     };
5617     defer for (&buffers) |*buffer| buffer.deinit();
5618     var widget_geometry: [2]WidgetFrame = undefined;
5619     for (0..nodes.len) |index| {
5620         const surface = gui.model.UiSurfaceTree{
5621             .available_size = .{ .width = 64, .height = 16 },
5622             .root = .{ .widget_id = 1, .children = nodes[index .. index + 1] },
5623         };
5624         const frame = try frames[index].buildSurface(&surface, .{});
5625         widget_geometry[index] = frame.widgets[1];
5626         try appendFrameCommands(&buffers[index], frame, &atlases, 1, 0);
5627     }
5628     try std.testing.expectEqual(widget_geometry[0].rect, widget_geometry[1].rect);
5629     try std.testing.expectEqual(widget_geometry[0].visible_rect, widget_geometry[1].visible_rect);
5630     try std.testing.expectEqual(widget_geometry[0].constraints, widget_geometry[1].constraints);
5631     try std.testing.expectEqual(widget_geometry[0].content_size, widget_geometry[1].content_size);
5632 
5633     var plain_glyphs: [4]command.Command = undefined;
5634     var styled_glyphs: [4]command.Command = undefined;
5635     var plain_count: usize = 0;
5636     var styled_count: usize = 0;
5637     for (buffers[0].items()) |item| {
5638         if (item.kind != .glyph) continue;
5639         plain_glyphs[plain_count] = item;
5640         plain_count += 1;
5641     }
5642     for (buffers[1].items()) |item| {
5643         if (item.kind != .glyph) continue;
5644         styled_glyphs[styled_count] = item;
5645         styled_count += 1;
5646     }
5647     try std.testing.expectEqual(@as(usize, 4), plain_count);
5648     try std.testing.expectEqual(plain_count, styled_count);
5649     for (plain_glyphs, styled_glyphs) |plain_glyph, styled_glyph| {
5650         try std.testing.expectEqual(plain_glyph.rect, styled_glyph.rect);
5651         try std.testing.expectEqual(plain_glyph.source, styled_glyph.source);
5652         try std.testing.expectEqual(plain_glyph.clip, styled_glyph.clip);
5653     }
5654     try std.testing.expectEqual(@as(u8, 12), styled_glyphs[0].color.r);
5655     try std.testing.expectEqual(@as(u8, 220), styled_glyphs[1].color.r);
5656     try std.testing.expectEqual(@as(u8, 20), styled_glyphs[2].color.r);
5657     try std.testing.expectEqual(@as(u8, 20), styled_glyphs[3].color.r);
5658 
5659     const plain_commands = buffers[0].items();
5660     const styled_commands = buffers[1].items();
5661     try std.testing.expectEqual(@as(usize, 6), plain_commands.len);
5662     try std.testing.expectEqual(@as(usize, 9), styled_commands.len);
5663     try std.testing.expectEqual(command.Kind.fill, styled_commands[0].kind);
5664     try std.testing.expectEqual(@as(u8, 15), styled_commands[0].color.r);
5665     try std.testing.expectApproxEqAbs(@as(f32, 8), styled_commands[0].rect.x, 0.001);
5666     try std.testing.expectApproxEqAbs(@as(f32, 8), styled_commands[0].rect.width, 0.001);
5667     try std.testing.expectEqual(command.Kind.fill, styled_commands[6].kind);
5668     try std.testing.expectEqual(@as(u8, 220), styled_commands[6].color.r);
5669     try std.testing.expectEqual(command.Kind.fill, styled_commands[7].kind);
5670     try std.testing.expectEqual(@as(u8, 20), styled_commands[7].color.r);
5671     try std.testing.expectEqual(plain_commands[0].rect, styled_commands[1].rect);
5672     try std.testing.expectEqual(plain_commands[plain_commands.len - 1].rect, styled_commands[styled_commands.len - 1].rect);
5673 }
5674 
5675 test "resolved equal metric slots preserve measure and command bytes" {
5676     const allocator = std.testing.allocator;
5677     const glyphs = [_]BitmapGlyph{
5678         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
5679         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
5680         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
5681     };
5682     var atlas = try Atlas.initFromBitmapGlyphs(
5683         allocator,
5684         &glyphs,
5685         .{ .width = 8, .height = 2, .stride = 1 },
5686         16,
5687         test_atlas_cache_limits,
5688     );
5689     defer atlas.deinit();
5690     const plain = UiText{
5691         .content = "ABC",
5692         .point_size = 16,
5693         .line_height = 1,
5694         .wrap_width = 16,
5695     };
5696     const styles = [_]UiTextStyle{.{
5697         .font_asset_id = plain.font_asset_id,
5698         .point_size = plain.point_size,
5699     }};
5700     const runs = [_]UiTextRun{.{
5701         .byte_start = 1,
5702         .byte_end = 2,
5703         .style_slot = 1,
5704     }};
5705     const slotted = UiText{
5706         .content = plain.content,
5707         .styles = &styles,
5708         .runs = &runs,
5709         .point_size = plain.point_size,
5710         .line_height = plain.line_height,
5711         .wrap_width = plain.wrap_width,
5712     };
5713     try std.testing.expectEqual(try measure(&atlas, plain), try measure(&atlas, slotted));
5714     try std.testing.expectEqual(@as(usize, 1), atlas.cacheStatus().measure.entries);
5715     const entries = [_]AtlasSet.Entry{.{
5716         .face = 0,
5717         .image_index = 4,
5718         .atlas = &atlas,
5719     }};
5720     const atlases = AtlasSet{ .entries = &entries };
5721     var workspaces = [_]gui.frame.Workspace{
5722         gui.frame.Workspace.init(allocator),
5723         gui.frame.Workspace.init(allocator),
5724     };
5725     defer for (&workspaces) |*workspace| workspace.deinit();
5726     var buffers = [_]command.CommandBuffer{
5727         command.CommandBuffer.init(allocator),
5728         command.CommandBuffer.init(allocator),
5729     };
5730     defer for (&buffers) |*buffer| buffer.deinit();
5731     const texts = [_]UiText{ plain, slotted };
5732     for (texts, 0..) |text, index| {
5733         const tree = gui.model.UiSurfaceTree{
5734             .available_size = .{ .width = 64, .height = 24 },
5735             .root = .{
5736                 .widget_id = 1,
5737                 .children = &.{.{
5738                     .widget_id = 2,
5739                     .kind = .text_input,
5740                     .text = text,
5741                     .text_selection = .{
5742                         .cursor_visible = true,
5743                         .cursor_byte_offset = 2,
5744                         .selection_active = true,
5745                         .selection_anchor_byte_offset = 0,
5746                         .selection_focus_byte_offset = 3,
5747                     },
5748                     .size = .{ .width = 64, .height = 48 },
5749                 }},
5750             },
5751         };
5752         const frame = try workspaces[index].buildSurface(&tree, .{
5753             .resolvers = frameResolvers(&atlases),
5754         });
5755         try appendFrameCommands(&buffers[index], frame, &atlases, 1, 0);
5756     }
5757     try std.testing.expectEqual(buffers[0].items().len, buffers[1].items().len);
5758     for (buffers[0].items(), buffers[1].items()) |plain_command, slotted_command| {
5759         try std.testing.expect(std.meta.eql(plain_command, slotted_command));
5760     }
5761 }
5762 
5763 test "mixed metric measurement keys distinguish alternate atlas sets" {
5764     const allocator = std.testing.allocator;
5765     const base_glyphs = [_]BitmapGlyph{.{
5766         .codepoint = 'A',
5767         .rows = &.{ 0xFF, 0xFF },
5768     }};
5769     const narrow_glyphs = [_]BitmapGlyph{.{
5770         .codepoint = 'B',
5771         .rows = &.{ 0xFF, 0xF0, 0xFF, 0xF0 },
5772     }};
5773     const wide_glyphs = [_]BitmapGlyph{.{
5774         .codepoint = 'B',
5775         .rows = &.{ 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xF0 },
5776     }};
5777     var base_atlas = try Atlas.initFromBitmapGlyphs(
5778         allocator,
5779         &base_glyphs,
5780         .{ .width = 8, .height = 2, .stride = 1 },
5781         16,
5782         test_atlas_cache_limits,
5783     );
5784     defer base_atlas.deinit();
5785     var narrow_atlas = try Atlas.initFromBitmapGlyphs(
5786         allocator,
5787         &narrow_glyphs,
5788         .{ .width = 12, .height = 2, .stride = 2 },
5789         16,
5790         test_atlas_cache_limits,
5791     );
5792     defer narrow_atlas.deinit();
5793     var wide_atlas = try Atlas.initFromBitmapGlyphs(
5794         allocator,
5795         &wide_glyphs,
5796         .{ .width = 20, .height = 2, .stride = 3 },
5797         16,
5798         test_atlas_cache_limits,
5799     );
5800     defer wide_atlas.deinit();
5801     const styles = [_]UiTextStyle{.{
5802         .font_asset_id = 2,
5803         .point_size = 16,
5804     }};
5805     const runs = [_]UiTextRun{.{
5806         .byte_start = 1,
5807         .byte_end = 2,
5808         .style_slot = 1,
5809     }};
5810     const text = UiText{
5811         .content = "AB",
5812         .runs = &runs,
5813         .styles = &styles,
5814         .font_asset_id = 1,
5815         .point_size = 16,
5816         .line_height = 1,
5817     };
5818     const narrow_entries = [_]AtlasSet.Entry{
5819         .{ .face = 1, .image_index = 0, .atlas = &base_atlas },
5820         .{ .face = 2, .image_index = 1, .atlas = &narrow_atlas },
5821     };
5822     const wide_entries = [_]AtlasSet.Entry{
5823         .{ .face = 1, .image_index = 0, .atlas = &base_atlas },
5824         .{ .face = 2, .image_index = 2, .atlas = &wide_atlas },
5825     };
5826     const narrow = AtlasSet{ .entries = &narrow_entries };
5827     const wide = AtlasSet{ .entries = &wide_entries };
5828     try std.testing.expectEqual(@as(f32, 20), (try measureSet(&narrow, text)).width);
5829     try std.testing.expectEqual(@as(f32, 28), (try measureSet(&wide, text)).width);
5830     try std.testing.expectEqual(@as(usize, 2), base_atlas.cacheStatus().measure.entries);
5831 }
5832 
5833 test "mixed metric runs share a baseline and retain per glyph atlas identity" {
5834     const allocator = std.testing.allocator;
5835     const base_glyphs = [_]BitmapGlyph{
5836         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
5837         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
5838     };
5839     const large_glyphs = [_]BitmapGlyph{
5840         .{ .codepoint = 'B', .rows = &.{
5841             0xFF, 0xF0,
5842             0xFF, 0xF0,
5843             0xFF, 0xF0,
5844             0xFF, 0xF0,
5845         } },
5846     };
5847     var base_atlas = try Atlas.initFromBitmapGlyphs(
5848         allocator,
5849         &base_glyphs,
5850         .{ .width = 8, .height = 2, .stride = 1 },
5851         16,
5852         test_atlas_cache_limits,
5853     );
5854     defer base_atlas.deinit();
5855     var large_atlas = try Atlas.initFromBitmapGlyphs(
5856         allocator,
5857         &large_glyphs,
5858         .{ .width = 12, .height = 4, .stride = 2 },
5859         24,
5860         test_atlas_cache_limits,
5861     );
5862     defer large_atlas.deinit();
5863     const styles = [_]UiTextStyle{.{
5864         .font_asset_id = 2,
5865         .point_size = 24,
5866     }};
5867     const runs = [_]UiTextRun{.{
5868         .byte_start = 1,
5869         .byte_end = 2,
5870         .style_slot = 1,
5871         .background = .{ .r = 12, .g = 24, .b = 48, .a = 180 },
5872         .underline = true,
5873     }};
5874     const text = UiText{
5875         .content = "ABC",
5876         .runs = &runs,
5877         .styles = &styles,
5878         .font_asset_id = 1,
5879         .point_size = 16,
5880         .line_height = 1,
5881     };
5882     const entries = [_]AtlasSet.Entry{
5883         .{ .face = 1, .image_index = 3, .atlas = &base_atlas },
5884         .{ .face = 2, .image_index = 9, .atlas = &large_atlas },
5885     };
5886     const atlases = AtlasSet{ .entries = &entries };
5887     try std.testing.expectEqual(Size{ .width = 28, .height = 24 }, try measureSet(&atlases, text));
5888     const plan = try LinePlan.initComposite(
5889         &base_atlas,
5890         &atlases,
5891         text,
5892         text.content,
5893         0,
5894         0,
5895     );
5896     try std.testing.expectEqual(@as(usize, 3), plan.run.glyphs.len);
5897     try std.testing.expectEqual(@as(i32, 8 * 64), plan.run.glyphs[0].x_advance);
5898     try std.testing.expectEqual(@as(i32, 12 * 64), plan.run.glyphs[1].x_advance);
5899     try std.testing.expectEqual(@as(i32, 8 * 64), plan.run.glyphs[2].x_advance);
5900     for (0..text.content.len + 1) |byte_offset| {
5901         const advance = try textAdvanceForByteOffset(&atlases, text, byte_offset);
5902         const hit = try textHitTestAdvance(&atlases, text, advance);
5903         try std.testing.expectEqual(byte_offset, hit.byte_offset);
5904         try std.testing.expectApproxEqAbs(advance, hit.advance, 0.001);
5905     }
5906     _ = try base_atlas.shape("CC");
5907     try std.testing.expectEqual(@as(u32, 'A'), plan.run.glyphs[0].glyph_id);
5908     try std.testing.expectEqual(@as(u32, 'B'), plan.run.glyphs[1].glyph_id);
5909     const tree = gui.model.UiSurfaceTree{
5910         .available_size = .{ .width = 64, .height = 24 },
5911         .root = .{
5912             .widget_id = 1,
5913             .children = &.{.{
5914                 .widget_id = 2,
5915                 .kind = .label,
5916                 .text = text,
5917                 .text_selection = .{
5918                     .selection_active = true,
5919                     .selection_anchor_byte_offset = 0,
5920                     .selection_focus_byte_offset = 3,
5921                 },
5922                 .size = .{ .width = 64, .height = 24 },
5923             }},
5924         },
5925     };
5926     var workspace = gui.frame.Workspace.init(allocator);
5927     defer workspace.deinit();
5928     const frame = try workspace.buildSurface(&tree, .{
5929         .resolvers = frameResolvers(&atlases),
5930     });
5931     var one = command.CommandBuffer.init(allocator);
5932     defer one.deinit();
5933     var two = command.CommandBuffer.init(allocator);
5934     defer two.deinit();
5935     try appendFrameCommands(&one, frame, &atlases, 1, 0);
5936     try appendFrameCommands(&two, frame, &atlases, 2, 0);
5937     var one_glyphs: [3]command.Command = undefined;
5938     var two_glyphs: [3]command.Command = undefined;
5939     var one_count: usize = 0;
5940     var two_count: usize = 0;
5941     for (one.items()) |item| {
5942         if (item.kind != .glyph) continue;
5943         one_glyphs[one_count] = item;
5944         one_count += 1;
5945     }
5946     for (two.items()) |item| {
5947         if (item.kind != .glyph) continue;
5948         two_glyphs[two_count] = item;
5949         two_count += 1;
5950     }
5951     try std.testing.expectEqual(@as(usize, 3), one_count);
5952     try std.testing.expectEqual(one_count, two_count);
5953     try std.testing.expectEqual(@as(u32, 3), one_glyphs[0].image_index);
5954     try std.testing.expectEqual(@as(u32, 9), one_glyphs[1].image_index);
5955     try std.testing.expectEqual(@as(u32, 3), one_glyphs[2].image_index);
5956     try std.testing.expectApproxEqAbs(
5957         one_glyphs[0].rect.y + one_glyphs[0].rect.height,
5958         one_glyphs[1].rect.y + one_glyphs[1].rect.height,
5959         0.001,
5960     );
5961     try std.testing.expectApproxEqAbs(
5962         one_glyphs[1].rect.y + one_glyphs[1].rect.height,
5963         one_glyphs[2].rect.y + one_glyphs[2].rect.height,
5964         0.001,
5965     );
5966     for (one_glyphs, two_glyphs) |logical, scaled| {
5967         try std.testing.expectEqual(logical.image_index, scaled.image_index);
5968         try std.testing.expectApproxEqAbs(logical.rect.x * 2, scaled.rect.x, 0.001);
5969         try std.testing.expectApproxEqAbs(logical.rect.y * 2, scaled.rect.y, 0.001);
5970         try std.testing.expectApproxEqAbs(logical.rect.width * 2, scaled.rect.width, 0.001);
5971         try std.testing.expectApproxEqAbs(logical.rect.height * 2, scaled.rect.height, 0.001);
5972     }
5973 }
5974 
5975 test "styled text runs rasterize fixed colors gaps decorations and wrapped partitions" {
5976     const allocator = std.testing.allocator;
5977     const ink = @as([8]u8, @splat(0x80));
5978     const blank = @as([8]u8, @splat(0));
5979     const glyphs = [_]BitmapGlyph{
5980         .{ .codepoint = 'A', .rows = &ink },
5981         .{ .codepoint = 'B', .rows = &ink },
5982         .{ .codepoint = ' ', .rows = &blank },
5983         .{ .codepoint = 'C', .rows = &ink },
5984         .{ .codepoint = 'D', .rows = &ink },
5985         .{ .codepoint = 'E', .rows = &ink },
5986     };
5987     var atlas = try Atlas.initFromBitmapGlyphs(
5988         allocator,
5989         &glyphs,
5990         .{ .width = 4, .height = 8, .stride = 1 },
5991         8,
5992         test_atlas_cache_limits,
5993     );
5994     defer atlas.deinit();
5995     const base = Color{ .r = 20, .g = 30, .b = 40, .a = 255 };
5996     const clear = Color{ .r = 2, .g = 3, .b = 4, .a = 255 };
5997     const red = Color{ .r = 220, .g = 30, .b = 20, .a = 255 };
5998     const blue = Color{ .r = 10, .g = 40, .b = 100, .a = 255 };
5999     const green = Color{ .r = 20, .g = 200, .b = 60, .a = 255 };
6000     const runs = [_]UiTextRun{
6001         .{
6002             .byte_start = 1,
6003             .byte_end = 4,
6004             .foreground = red,
6005             .background = blue,
6006             .underline = true,
6007         },
6008         .{
6009             .byte_start = 4,
6010             .byte_end = 5,
6011             .foreground = green,
6012             .strikethrough = true,
6013         },
6014         .{
6015             .byte_start = 5,
6016             .byte_end = 6,
6017             .foreground = .{ .r = 255, .b = 255, .a = 0 },
6018             .background = .{ .a = 0 },
6019         },
6020     };
6021     const children = [_]UiNode{.{
6022         .widget_id = 2,
6023         .kind = .label,
6024         .text = .{
6025             .content = "AB CDE",
6026             .runs = &runs,
6027             .point_size = 8,
6028             .line_height = 1,
6029             .wrap_width = 12,
6030         },
6031         .paint = .{ .foreground = base },
6032         .size = .{ .width = 12, .height = 16 },
6033     }};
6034     const surface = gui.model.UiSurfaceTree{
6035         .available_size = .{ .width = 12, .height = 16 },
6036         .root = .{ .widget_id = 1, .children = &children },
6037     };
6038     var frame_workspace = gui.frame.Workspace.init(allocator);
6039     defer frame_workspace.deinit();
6040     const frame = try frame_workspace.buildSurface(&surface, .{});
6041     var commands = command.CommandBuffer.init(allocator);
6042     defer commands.deinit();
6043     const entries = [_]AtlasSet.Entry{.{
6044         .face = 0,
6045         .image_index = 0,
6046         .atlas = &atlas,
6047     }};
6048     const atlases = AtlasSet{ .entries = &entries };
6049     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
6050 
6051     var pixels = @as([(12 * 16)]u32, @splat(0));
6052     const images = [_]command.Image{atlas.image.image};
6053     try cpu.renderCommandsPackedWithImages(
6054         commands.items(),
6055         .{ .width = 12, .height = 16, .pixels = &pixels },
6056         clear,
6057         .{ .images = &images },
6058     );
6059 
6060     try std.testing.expectEqual(cpu.packRgba(base), pixels[0 + 2 * 12]);
6061     try std.testing.expectEqual(cpu.packRgba(clear), pixels[2 + 2 * 12]);
6062     try std.testing.expectEqual(cpu.packRgba(red), pixels[4 + 2 * 12]);
6063     try std.testing.expectEqual(cpu.packRgba(blue), pixels[6 + 2 * 12]);
6064     try std.testing.expectEqual(cpu.packRgba(blue), pixels[10 + 2 * 12]);
6065     try std.testing.expectEqual(cpu.packRgba(red), pixels[6 + 7 * 12]);
6066     try std.testing.expectEqual(cpu.packRgba(red), pixels[0 + 10 * 12]);
6067     try std.testing.expectEqual(cpu.packRgba(blue), pixels[2 + 10 * 12]);
6068     try std.testing.expectEqual(cpu.packRgba(red), pixels[2 + 15 * 12]);
6069     try std.testing.expectEqual(cpu.packRgba(green), pixels[4 + 10 * 12]);
6070     try std.testing.expectEqual(
6071         cpu.packRgba(.{ .r = 11, .g = 102, .b = 32, .a = 255 }),
6072         pixels[6 + 12 * 12],
6073     );
6074     try std.testing.expectEqual(cpu.packRgba(clear), pixels[8 + 10 * 12]);
6075     try std.testing.expectEqual(cpu.packRgba(clear), pixels[10 + 10 * 12]);
6076 }
6077 
6078 test "retained text run color changes damage only the styled glyph" {
6079     const allocator = std.testing.allocator;
6080     const glyphs = [_]BitmapGlyph{
6081         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
6082         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
6083         .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } },
6084         .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } },
6085     };
6086     var atlas = try Atlas.initFromBitmapGlyphs(
6087         allocator,
6088         &glyphs,
6089         .{ .width = 8, .height = 2, .stride = 1 },
6090         16,
6091         test_atlas_cache_limits,
6092     );
6093     defer atlas.deinit();
6094     const red = [_]UiTextRun{.{
6095         .byte_start = 1,
6096         .byte_end = 2,
6097         .foreground = .{ .r = 220, .g = 30, .b = 20 },
6098     }};
6099     const blue = [_]UiTextRun{.{
6100         .byte_start = 1,
6101         .byte_end = 2,
6102         .foreground = .{ .r = 20, .g = 80, .b = 220 },
6103     }};
6104     const nodes = [_]UiNode{
6105         .{
6106             .widget_id = 2,
6107             .kind = .label,
6108             .text = .{ .content = "ABCD", .runs = &red, .point_size = 16, .line_height = 1 },
6109             .size = .{ .width = 32, .height = 16 },
6110         },
6111         .{
6112             .widget_id = 2,
6113             .kind = .label,
6114             .text = .{ .content = "ABCD", .runs = &blue, .point_size = 16, .line_height = 1 },
6115             .size = .{ .width = 32, .height = 16 },
6116         },
6117     };
6118     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
6119     const atlases = AtlasSet{ .entries = &entries };
6120     var frames = [_]gui.frame.Workspace{
6121         gui.frame.Workspace.init(allocator),
6122         gui.frame.Workspace.init(allocator),
6123     };
6124     defer for (&frames) |*frame| frame.deinit();
6125     var buffers = [_]command.CommandBuffer{
6126         command.CommandBuffer.init(allocator),
6127         command.CommandBuffer.init(allocator),
6128     };
6129     defer for (&buffers) |*buffer| buffer.deinit();
6130     const fragment_id = command.FragmentId{
6131         .root_id = 1,
6132         .element_id = 2,
6133         .namespace = command.fragment_namespace_widget,
6134         .part = command.fragment_part_text,
6135     };
6136     for (0..nodes.len) |index| {
6137         const surface = gui.model.UiSurfaceTree{
6138             .available_size = .{ .width = 32, .height = 16 },
6139             .root = .{ .widget_id = 1, .children = nodes[index .. index + 1] },
6140         };
6141         const frame = try frames[index].buildSurface(&surface, .{});
6142         try appendFrameCommands(&buffers[index], frame, &atlases, 1, 0);
6143         try buffers[index].commitFragment(fragment_id, 0);
6144     }
6145     var retained = gui.paint.RetainedCommands.init(allocator);
6146     defer retained.deinit();
6147     try retained.retain(&buffers[0]);
6148     const damage = try retained.diff(
6149         &buffers[1],
6150         &.{Region.full(32, 16)},
6151         32,
6152         16,
6153     );
6154     const narrowed = switch (damage) {
6155         .semantic => return error.ExpectedNarrowedDamage,
6156         .narrowed => |value| value,
6157     };
6158     try std.testing.expect(narrowed.slice().len > 0);
6159     try std.testing.expect(narrowed.slice().len <= 2);
6160     const bounds = narrowed.bounding().?;
6161     try std.testing.expect(bounds.x >= 7);
6162     try std.testing.expect(bounds.x <= 8);
6163     try std.testing.expect(bounds.width <= 10);
6164     try std.testing.expect(bounds.height <= 4);
6165 }
6166 
6167 test "warmed identical styled text measure emission and retained diff need no allocation or damage" {
6168     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
6169     const allocator = failing.allocator();
6170     const glyphs = [_]BitmapGlyph{
6171         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
6172         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
6173     };
6174     var atlas = try Atlas.initFromBitmapGlyphs(
6175         allocator,
6176         &glyphs,
6177         .{ .width = 8, .height = 2, .stride = 1 },
6178         16,
6179         test_atlas_cache_limits,
6180     );
6181     defer atlas.deinit();
6182     const runs = [_]UiTextRun{.{
6183         .byte_start = 0,
6184         .byte_end = 1,
6185         .foreground = .{ .r = 220, .g = 30, .b = 20 },
6186         .background = .{ .r = 10, .g = 20, .b = 40, .a = 160 },
6187         .underline = true,
6188     }};
6189     const child = [_]UiNode{.{
6190         .widget_id = 2,
6191         .kind = .label,
6192         .text = .{ .content = "AB", .runs = &runs, .point_size = 16, .line_height = 1 },
6193         .size = .{ .width = 16, .height = 16 },
6194     }};
6195     const surface = gui.model.UiSurfaceTree{
6196         .available_size = .{ .width = 16, .height = 16 },
6197         .root = .{ .widget_id = 1, .children = &child },
6198     };
6199     var frame_workspace = gui.frame.Workspace.init(allocator);
6200     defer frame_workspace.deinit();
6201     const frame = try frame_workspace.buildSurface(&surface, .{});
6202     var commands = command.CommandBuffer.init(allocator);
6203     defer commands.deinit();
6204     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
6205     const atlases = AtlasSet{ .entries = &entries };
6206     const measured = try measure(&atlas, child[0].text.?);
6207     const fragment_id = command.FragmentId{
6208         .root_id = 1,
6209         .element_id = 2,
6210         .namespace = command.fragment_namespace_widget,
6211         .part = command.fragment_part_text,
6212     };
6213     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
6214     try commands.commitFragment(fragment_id, 0);
6215     var retained = gui.paint.RetainedCommands.init(allocator);
6216     defer retained.deinit();
6217     try retained.retain(&commands);
6218     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
6219     try commands.commitFragment(fragment_id, 0);
6220     try std.testing.expect((try retained.diff(&commands, &.{}, 16, 16)) == .semantic);
6221     try retained.retain(&commands);
6222 
6223     failing.fail_index = failing.alloc_index;
6224     failing.resize_fail_index = failing.resize_index;
6225     for (0..8) |_| {
6226         try std.testing.expectEqual(measured, try measure(&atlas, child[0].text.?));
6227         try appendFrameCommands(&commands, frame, &atlases, 1, 0);
6228         try std.testing.expectEqual(@as(usize, 4), commands.items().len);
6229         try commands.commitFragment(fragment_id, 0);
6230         try std.testing.expect((try retained.diff(&commands, &.{}, 16, 16)) == .semantic);
6231         try retained.retain(&commands);
6232     }
6233     try std.testing.expect(!failing.has_induced_failure);
6234 }
6235 
6236 test "warmed mixed metric measure planning emission and retained diff need no allocation" {
6237     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
6238     const allocator = failing.allocator();
6239     const base_glyphs = [_]BitmapGlyph{
6240         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
6241         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } },
6242     };
6243     const large_glyphs = [_]BitmapGlyph{
6244         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0 } },
6245         .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0 } },
6246     };
6247     var base_atlas = try Atlas.initFromBitmapGlyphs(
6248         allocator,
6249         &base_glyphs,
6250         .{ .width = 8, .height = 2, .stride = 1 },
6251         16,
6252         test_atlas_cache_limits,
6253     );
6254     defer base_atlas.deinit();
6255     var large_atlas = try Atlas.initFromBitmapGlyphs(
6256         allocator,
6257         &large_glyphs,
6258         .{ .width = 12, .height = 3, .stride = 2 },
6259         24,
6260         test_atlas_cache_limits,
6261     );
6262     defer large_atlas.deinit();
6263     const styles = [_]UiTextStyle{.{
6264         .font_asset_id = 2,
6265         .point_size = 24,
6266     }};
6267     const runs = [_]UiTextRun{.{
6268         .byte_start = 1,
6269         .byte_end = 2,
6270         .style_slot = 1,
6271         .foreground = .{ .r = 220, .g = 30, .b = 20 },
6272         .background = .{ .r = 10, .g = 20, .b = 40, .a = 160 },
6273         .underline = true,
6274     }};
6275     const text = UiText{
6276         .content = "AB",
6277         .runs = &runs,
6278         .styles = &styles,
6279         .font_asset_id = 1,
6280         .point_size = 16,
6281         .line_height = 1,
6282     };
6283     const child = [_]UiNode{.{
6284         .widget_id = 2,
6285         .kind = .text_input,
6286         .text = text,
6287         .size = .{ .width = 24, .height = 24 },
6288     }};
6289     const surface = gui.model.UiSurfaceTree{
6290         .available_size = .{ .width = 24, .height = 24 },
6291         .root = .{ .widget_id = 1, .children = &child },
6292     };
6293     const entries = [_]AtlasSet.Entry{
6294         .{ .face = 1, .image_index = 1, .atlas = &base_atlas },
6295         .{ .face = 2, .image_index = 2, .atlas = &large_atlas },
6296     };
6297     const atlases = AtlasSet{ .entries = &entries };
6298     var frame_workspace = gui.frame.Workspace.init(allocator);
6299     defer frame_workspace.deinit();
6300     const frame = try frame_workspace.buildSurface(&surface, .{
6301         .resolvers = frameResolvers(&atlases),
6302     });
6303     var commands = command.CommandBuffer.init(allocator);
6304     defer commands.deinit();
6305     const measured = try measureSet(&atlases, text);
6306     _ = try LinePlan.initComposite(&base_atlas, &atlases, text, text.content, 0, 0);
6307     const fragment_id = command.FragmentId{
6308         .root_id = 1,
6309         .element_id = 2,
6310         .namespace = command.fragment_namespace_widget,
6311         .part = command.fragment_part_text,
6312     };
6313     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
6314     try commands.commitFragment(fragment_id, 0);
6315     var retained = gui.paint.RetainedCommands.init(allocator);
6316     defer retained.deinit();
6317     try retained.retain(&commands);
6318     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
6319     try commands.commitFragment(fragment_id, 0);
6320     try std.testing.expect((try retained.diff(&commands, &.{}, 24, 24)) == .semantic);
6321     try retained.retain(&commands);
6322 
6323     failing.fail_index = failing.alloc_index;
6324     failing.resize_fail_index = failing.resize_index;
6325     for (0..8) |_| {
6326         try std.testing.expectEqual(measured, try measureSet(&atlases, text));
6327         _ = try LinePlan.initComposite(&base_atlas, &atlases, text, text.content, 0, 0);
6328         try appendFrameCommands(&commands, frame, &atlases, 1, 0);
6329         try commands.commitFragment(fragment_id, 0);
6330         try std.testing.expect((try retained.diff(&commands, &.{}, 24, 24)) == .semantic);
6331         try retained.retain(&commands);
6332     }
6333     try std.testing.expect(!failing.has_induced_failure);
6334 }
6335 
6336 test "measure and paint reject malformed text runs" {
6337     const allocator = std.testing.allocator;
6338     const glyphs = [_]BitmapGlyph{.{ .codepoint = 'A', .rows = &.{0xFF} }};
6339     var atlas = try Atlas.initFromBitmapGlyphs(
6340         allocator,
6341         &glyphs,
6342         .{ .width = 8, .height = 1, .stride = 1 },
6343         16,
6344         test_atlas_cache_limits,
6345     );
6346     defer atlas.deinit();
6347     const runs = [_]UiTextRun{.{ .byte_start = 0, .byte_end = 2 }};
6348     const text = UiText{ .content = "A", .runs = &runs };
6349     try std.testing.expectError(error.InvalidTextRun, measure(&atlas, text));
6350     const child = [_]UiNode{.{
6351         .widget_id = 2,
6352         .kind = .label,
6353         .text = text,
6354         .size = .{ .width = 16, .height = 16 },
6355     }};
6356     const surface = gui.model.UiSurfaceTree{
6357         .available_size = .{ .width = 16, .height = 16 },
6358         .root = .{ .widget_id = 1, .children = &child },
6359     };
6360     var frame_workspace = gui.frame.Workspace.init(allocator);
6361     defer frame_workspace.deinit();
6362     const frame = try frame_workspace.buildSurface(&surface, .{});
6363     var commands = command.CommandBuffer.init(allocator);
6364     defer commands.deinit();
6365     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
6366     const atlases = AtlasSet{ .entries = &entries };
6367     try std.testing.expectError(error.InvalidTextRun, appendFrameCommands(&commands, frame, &atlases, 1, 0));
6368     try std.testing.expectEqual(@as(usize, 0), commands.items().len);
6369 }
6370 
6371 test "appendFrameCommands draws cursor for empty text input" {
6372     const allocator = std.testing.allocator;
6373     const glyphs = [_]BitmapGlyph{
6374         .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } },
6375     };
6376     var atlas = try Atlas.initFromBitmapGlyphs(
6377         allocator,
6378         glyphs[0..],
6379         .{ .width = 8, .height = 2, .stride = 1 },
6380         16,
6381         test_atlas_cache_limits,
6382     );
6383     defer atlas.deinit();
6384     const child = [_]gui.model.UiNode{.{
6385         .widget_id = 2,
6386         .kind = .text_input,
6387         .text = .{ .content = "", .point_size = 16, .line_height = 1.0 },
6388         .text_selection = .{
6389             .cursor_visible = true,
6390             .cursor_byte_offset = 0,
6391         },
6392         .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
6393         .size = .{ .width = 64, .height = 20 },
6394     }};
6395     const surface = gui.model.UiSurfaceTree{
6396         .available_size = .{ .width = 80, .height = 32 },
6397         .root = .{
6398             .widget_id = 1,
6399             .children = child[0..],
6400         },
6401     };
6402     var frame_workspace = gui.frame.Workspace.init(allocator);
6403     defer frame_workspace.deinit();
6404     const frame = try frame_workspace.buildSurface(&surface, .{});
6405     var commands = command.CommandBuffer.init(allocator);
6406     defer commands.deinit();
6407 
6408     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
6409     const atlases = AtlasSet{ .entries = entries[0..] };
6410     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
6411 
6412     try std.testing.expectEqual(@as(usize, 1), commands.items().len);
6413     try std.testing.expectEqual(command.Kind.fill, commands.items()[0].kind);
6414     try std.testing.expectApproxEqAbs(@as(f32, 0), commands.items()[0].rect.x, 0.001);
6415     try std.testing.expectApproxEqAbs(@as(f32, 1), commands.items()[0].rect.width, 0.001);
6416     try std.testing.expect(commands.items()[0].rect.height > 0);
6417 }
6418 
6419 test "shape cache reuses stored runs and survives eviction" {
6420     const allocator = std.testing.allocator;
6421     const limits = AtlasCacheStorage.Limits{
6422         .measure_entries = 0,
6423         .measure_payload_bytes = 0,
6424         .shape_entries = 4,
6425         .shape_payload_bytes = 64 * 1024,
6426     };
6427     const glyphs = [_]BitmapGlyph{
6428         .{ .codepoint = 'a', .rows = &.{ 0xFF, 0xFF } },
6429         .{ .codepoint = 'b', .rows = &.{ 0x0F, 0xF0 } },
6430     };
6431     var atlas = try Atlas.initFromBitmapGlyphs(
6432         allocator,
6433         glyphs[0..],
6434         .{ .width = 8, .height = 2, .stride = 1 },
6435         16,
6436         limits,
6437     );
6438     defer atlas.deinit();
6439 
6440     const first = try atlas.shape("ab");
6441     try std.testing.expectEqual(@as(usize, 2), first.glyphs.len);
6442     const second = try atlas.shape("ab");
6443     try std.testing.expectEqual(first.glyphs.ptr, second.glyphs.ptr);
6444     try std.testing.expectEqual(first.total_x_advance, second.total_x_advance);
6445 
6446     var name_buffer: [32]u8 = undefined;
6447     for (0..limits.shape_entries) |index| {
6448         const name = try std.fmt.bufPrint(name_buffer[0..], "evict {d}", .{index});
6449         _ = try atlas.shape(name);
6450     }
6451     const evicted = try atlas.shape("ab");
6452     try std.testing.expectEqual(@as(usize, 2), evicted.glyphs.len);
6453     try std.testing.expectEqual(first.total_x_advance, evicted.total_x_advance);
6454     try std.testing.expectEqual(@as(u32, 'a'), evicted.glyphs[0].glyph_id);
6455     try std.testing.expectEqual(@as(u32, 'b'), evicted.glyphs[1].glyph_id);
6456     try std.testing.expectEqual(@as(u64, 1), atlas.cacheStatus().shape.rollovers);
6457 }
6458 
6459 test "Atlas cache capacity matches an independent aligned byte model" {
6460     comptime {
6461         @stardustClaim(
6462             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_capacity"),
6463             null,
6464             null,
6465             null,
6466             null,
6467             null,
6468             null,
6469         );
6470     }
6471 
6472     const cases = [_]AtlasCacheStorage.Limits{
6473         .{
6474             .measure_entries = 0,
6475             .measure_payload_bytes = 0,
6476             .shape_entries = 0,
6477             .shape_payload_bytes = 0,
6478         },
6479         .{
6480             .measure_entries = 1,
6481             .measure_payload_bytes = 1,
6482             .shape_entries = 1,
6483             .shape_payload_bytes = 1,
6484         },
6485         test_atlas_cache_limits,
6486         .{
6487             .measure_entries = std.math.maxInt(u32),
6488             .measure_payload_bytes = 1,
6489             .shape_entries = 1,
6490             .shape_payload_bytes = 1,
6491         },
6492     };
6493     for (cases) |limits| {
6494         try std.testing.expectEqual(
6495             try modelAtlasCacheCapacity(limits),
6496             try AtlasCacheStorage.Capacity.derive(limits),
6497         );
6498     }
6499     try expectAtlasCacheCapacityErrors();
6500 }
6501 
6502 test "Atlas cache storage acquires one exact aligned region" {
6503     comptime {
6504         @stardustClaim(
6505             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_acquisition"),
6506             null,
6507             null,
6508             null,
6509             null,
6510             null,
6511             null,
6512         );
6513     }
6514 
6515     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
6516     const capacity = try AtlasCacheStorage.Capacity.derive(test_atlas_cache_limits);
6517     var storage = try AtlasCacheStorage.init(failing.allocator(), test_atlas_cache_limits);
6518     defer storage.deinit(failing.allocator());
6519 
6520     try std.testing.expectEqual(@as(usize, 1), failing.alloc_index);
6521     try std.testing.expectEqual(capacity.storage_bytes, failing.allocated_bytes);
6522     try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase);
6523     try expectAtlasCacheRegionAddresses(&storage);
6524     storage.activate();
6525     try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, storage.status().phase);
6526 }
6527 
6528 test "Atlas cache storage retries after every allocation failure" {
6529     comptime {
6530         @stardustClaim(
6531             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_oom"),
6532             null,
6533             null,
6534             null,
6535             null,
6536             null,
6537             null,
6538         );
6539     }
6540 
6541     try std.testing.checkAllAllocationFailures(
6542         std.testing.allocator,
6543         checkAtlasCacheInitFailures,
6544         .{},
6545     );
6546 }
6547 
6548 test "Atlas cache storage preserves exact max and max plus one overload behavior" {
6549     comptime {
6550         @stardustClaim(
6551             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_boundaries"),
6552             null,
6553             null,
6554             null,
6555             null,
6556             null,
6557             null,
6558         );
6559     }
6560 
6561     try expectShapeCacheBoundaries();
6562     try expectMeasureCacheBoundaries();
6563     try expectDisabledAtlasCaches();
6564 }
6565 
6566 test "Activated Atlas cache operations make no backing allocation from cold" {
6567     comptime {
6568         @stardustClaim(
6569             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_sealed_transitive_risk"),
6570             null,
6571             null,
6572             null,
6573             null,
6574             null,
6575             null,
6576         );
6577     }
6578     comptime {
6579         @stardustClaim(
6580             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_sealed_foreign_risk"),
6581             null,
6582             null,
6583             null,
6584             null,
6585             null,
6586             null,
6587         );
6588     }
6589 
6590     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
6591     const limits = AtlasCacheStorage.Limits{
6592         .measure_entries = 2,
6593         .measure_payload_bytes = 16,
6594         .shape_entries = 2,
6595         .shape_payload_bytes = 16,
6596     };
6597     var storage = try AtlasCacheStorage.init(failing.allocator(), limits);
6598     defer storage.deinit(failing.allocator());
6599     storage.activate();
6600     const allocations = failing.allocations;
6601     const deallocations = failing.deallocations;
6602     const allocated_bytes = failing.allocated_bytes;
6603     const freed_bytes = failing.freed_bytes;
6604     const resize_index = failing.resize_index;
6605     failing.fail_index = failing.alloc_index;
6606     failing.resize_fail_index = failing.resize_index;
6607 
6608     try exerciseColdAtlasCache(&storage);
6609     try std.testing.expectEqual(allocations, failing.allocations);
6610     try std.testing.expectEqual(deallocations, failing.deallocations);
6611     try std.testing.expectEqual(allocated_bytes, failing.allocated_bytes);
6612     try std.testing.expectEqual(freed_bytes, failing.freed_bytes);
6613     try std.testing.expectEqual(resize_index, failing.resize_index);
6614 }
6615 
6616 test "Atlas cache bounded probes preserve colliding shape keys" {
6617     comptime {
6618         @stardustClaim(
6619             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_collisions"),
6620             null,
6621             null,
6622             null,
6623             null,
6624             null,
6625             null,
6626         );
6627     }
6628 
6629     const limits = AtlasCacheStorage.Limits{
6630         .measure_entries = 0,
6631         .measure_payload_bytes = 0,
6632         .shape_entries = 2,
6633         .shape_payload_bytes = 32,
6634     };
6635     var storage = try AtlasCacheStorage.init(std.testing.allocator, limits);
6636     defer storage.deinit(std.testing.allocator);
6637     storage.activate();
6638     const keys = try collidingShapeKeys(storage.capacity.shape_index_slots);
6639     _ = try storage.storeShape(keys.first, testShapedRun(11));
6640     _ = try storage.storeShape(keys.second, testShapedRun(22));
6641     try std.testing.expectEqual(@as(i32, 11), storage.lookupShape(keys.first).?.total_x_advance);
6642     try std.testing.expectEqual(@as(i32, 22), storage.lookupShape(keys.second).?.total_x_advance);
6643 }
6644 
6645 test "Atlas cache storage keeps region pointers and capacity stable" {
6646     comptime {
6647         @stardustClaim(
6648             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_stability"),
6649             null,
6650             null,
6651             null,
6652             null,
6653             null,
6654             null,
6655         );
6656     }
6657 
6658     var storage = try AtlasCacheStorage.init(std.testing.allocator, .{
6659         .measure_entries = 2,
6660         .measure_payload_bytes = 16,
6661         .shape_entries = 2,
6662         .shape_payload_bytes = 16,
6663     });
6664     defer storage.deinit(std.testing.allocator);
6665     const capacity = storage.capacity;
6666     const pointers = atlasCachePointers(storage);
6667     storage.activate();
6668     var iteration: usize = 0;
6669     while (iteration < 32) : (iteration += 1) {
6670         const key = if (iteration % 2 == 0) "aa" else "bb";
6671         _ = storage.storeShape(key, testShapedRun(@intCast(iteration))) catch {};
6672         storage.replaceShape();
6673     }
6674     try std.testing.expectEqual(capacity, storage.capacity);
6675     try std.testing.expectEqual(pointers, atlasCachePointers(storage));
6676 }
6677 
6678 test "Atlas cache shape operations match a whole epoch reference model" {
6679     comptime {
6680         @stardustClaim(
6681             @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_differential"),
6682             null,
6683             null,
6684             null,
6685             null,
6686             null,
6687             null,
6688         );
6689     }
6690 
6691     try expectAtlasCacheDifferential();
6692 }
6693 
6694 test "Atlas cache bypasses oversized entries without changing either epoch" {
6695     const glyphs = [_]BitmapGlyph{
6696         .{ .codepoint = 'a', .rows = &.{ 0xFF, 0xFF } },
6697     };
6698     const limits = AtlasCacheStorage.Limits{
6699         .measure_entries = 2,
6700         .measure_payload_bytes = 128,
6701         .shape_entries = 2,
6702         .shape_payload_bytes = 128,
6703     };
6704     var atlas = try Atlas.initFromBitmapGlyphs(
6705         std.testing.allocator,
6706         glyphs[0..],
6707         .{ .width = 8, .height = 2, .stride = 1 },
6708         16,
6709         limits,
6710     );
6711     defer atlas.deinit();
6712     const retained_run = try atlas.shape("a");
6713     const retained_measure = try measure(&atlas, cacheEpochText("a"));
6714     const before = atlas.cacheStatus();
6715     const content: [129]u8 = @splat('x');
6716 
6717     const first_run = try atlas.shape(&content);
6718     const second_run = try atlas.shape(&content);
6719     try expectGlyphRunEqual(first_run, second_run);
6720     const first_measure = try measure(&atlas, cacheEpochText(&content));
6721     const second_measure = try measure(&atlas, cacheEpochText(&content));
6722     try std.testing.expectEqual(first_measure, second_measure);
6723     const after = atlas.cacheStatus();
6724     try std.testing.expectEqual(before.shape.entries, after.shape.entries);
6725     try std.testing.expectEqual(before.shape.payload_bytes, after.shape.payload_bytes);
6726     try std.testing.expectEqual(before.shape.rollovers, after.shape.rollovers);
6727     try std.testing.expectEqual(before.shape.oversize_bypasses + 4, after.shape.oversize_bypasses);
6728     try std.testing.expectEqual(before.measure.entries, after.measure.entries);
6729     try std.testing.expectEqual(before.measure.payload_bytes, after.measure.payload_bytes);
6730     try std.testing.expectEqual(before.measure.rollovers, after.measure.rollovers);
6731     try std.testing.expectEqual(before.measure.oversize_bypasses + 2, after.measure.oversize_bypasses);
6732     const retained_again = try atlas.shape("a");
6733     try std.testing.expectEqual(retained_run.glyphs.ptr, retained_again.glyphs.ptr);
6734     try std.testing.expectEqual(retained_measure, try measure(&atlas, cacheEpochText("a")));
6735 }
6736 
6737 fn expectAtlasCacheCapacityErrors() !void {
6738     try std.testing.expectError(error.InvalidMeasureLimits, AtlasCacheStorage.Capacity.derive(.{
6739         .measure_entries = 1,
6740         .measure_payload_bytes = 0,
6741         .shape_entries = 0,
6742         .shape_payload_bytes = 0,
6743     }));
6744     try std.testing.expectError(error.InvalidShapeLimits, AtlasCacheStorage.Capacity.derive(.{
6745         .measure_entries = 0,
6746         .measure_payload_bytes = 0,
6747         .shape_entries = 0,
6748         .shape_payload_bytes = 1,
6749     }));
6750     try std.testing.expectError(error.CapacityOverflow, AtlasCacheStorage.Capacity.derive(.{
6751         .measure_entries = 1,
6752         .measure_payload_bytes = std.math.maxInt(usize),
6753         .shape_entries = 0,
6754         .shape_payload_bytes = 0,
6755     }));
6756     if (@bitSizeOf(usize) > @bitSizeOf(u32)) {
6757         try std.testing.expectError(error.EntryLimitTooLarge, AtlasCacheStorage.Capacity.derive(.{
6758             .measure_entries = @as(usize, std.math.maxInt(u32)) + 1,
6759             .measure_payload_bytes = 1,
6760             .shape_entries = 0,
6761             .shape_payload_bytes = 0,
6762         }));
6763     }
6764 }
6765 
6766 fn expectAtlasCacheRegionAddresses(storage: *const AtlasCacheStorage) !void {
6767     const base = @intFromPtr(storage.bytes.ptr);
6768     const capacity = storage.capacity;
6769     try std.testing.expectEqual(
6770         base + capacity.measure_index_offset,
6771         @intFromPtr(storage.measure_slots.ptr),
6772     );
6773     try std.testing.expectEqual(
6774         base + capacity.measure_entries_offset,
6775         @intFromPtr(storage.measure_entries.ptr),
6776     );
6777     try std.testing.expectEqual(
6778         base + capacity.measure_payload_offset,
6779         @intFromPtr(storage.measure_payload.bytes.ptr),
6780     );
6781     try std.testing.expectEqual(
6782         base + capacity.shape_index_offset,
6783         @intFromPtr(storage.shape_slots.ptr),
6784     );
6785     try std.testing.expectEqual(
6786         base + capacity.shape_entries_offset,
6787         @intFromPtr(storage.shape_entries.ptr),
6788     );
6789     try std.testing.expectEqual(
6790         base + capacity.shape_payload_offset,
6791         @intFromPtr(storage.shape_payload.bytes.ptr),
6792     );
6793 }
6794 
6795 fn checkAtlasCacheInitFailures(allocator: Allocator) !void {
6796     var storage = try AtlasCacheStorage.init(allocator, .{
6797         .measure_entries = 3,
6798         .measure_payload_bytes = 31,
6799         .shape_entries = 5,
6800         .shape_payload_bytes = 47,
6801     });
6802     storage.deinit(allocator);
6803 }
6804 
6805 fn expectShapeCacheBoundaries() !void {
6806     var storage = try AtlasCacheStorage.init(std.testing.allocator, .{
6807         .measure_entries = 0,
6808         .measure_payload_bytes = 0,
6809         .shape_entries = 2,
6810         .shape_payload_bytes = 64,
6811     });
6812     defer storage.deinit(std.testing.allocator);
6813     storage.activate();
6814     _ = try storage.storeShape("a", testShapedRun(1));
6815     _ = try storage.storeShape("b", testShapedRun(2));
6816     try std.testing.expectEqual(@as(usize, 2), storage.status().shape.entries);
6817     _ = try storage.storeShape("c", testShapedRun(3));
6818     try std.testing.expectEqual(@as(usize, 1), storage.status().shape.entries);
6819     try std.testing.expectEqual(@as(u64, 1), storage.status().shape.rollovers);
6820     try std.testing.expect(storage.lookupShape("a") == null);
6821     try std.testing.expectEqual(@as(i32, 3), storage.lookupShape("c").?.total_x_advance);
6822 
6823     var payload = try shapePayloadBoundaryStorage();
6824     defer payload.deinit(std.testing.allocator);
6825     try payload.admitShapePayload(3);
6826     try std.testing.expectError(error.EntryTooLarge, payload.admitShapePayload(4));
6827     _ = try payload.storeShape("abc", testShapedRun(4));
6828     const before = payload.status();
6829     try std.testing.expectError(
6830         error.EntryTooLarge,
6831         payload.storeShape("abcd", testShapedRun(5)),
6832     );
6833     const after = payload.status();
6834     try expectEpochUnchangedExceptOversize(before.shape, after.shape);
6835 }
6836 
6837 fn shapePayloadBoundaryStorage() !AtlasCacheStorage {
6838     var storage = try AtlasCacheStorage.init(std.testing.allocator, .{
6839         .measure_entries = 0,
6840         .measure_payload_bytes = 0,
6841         .shape_entries = 2,
6842         .shape_payload_bytes = 3,
6843     });
6844     storage.activate();
6845     return storage;
6846 }
6847 
6848 fn expectMeasureCacheBoundaries() !void {
6849     var storage = try AtlasCacheStorage.init(std.testing.allocator, .{
6850         .measure_entries = 2,
6851         .measure_payload_bytes = 3,
6852         .shape_entries = 0,
6853         .shape_payload_bytes = 0,
6854     });
6855     defer storage.deinit(std.testing.allocator);
6856     storage.activate();
6857     _ = try storage.storeMeasure(testMeasureKey("a"), .{ .width = 1 });
6858     _ = try storage.storeMeasure(testMeasureKey("b"), .{ .width = 2 });
6859     try std.testing.expectEqual(@as(usize, 2), storage.status().measure.entries);
6860     _ = try storage.storeMeasure(testMeasureKey("c"), .{ .width = 3 });
6861     try std.testing.expectEqual(@as(usize, 1), storage.status().measure.entries);
6862     try std.testing.expectEqual(@as(u64, 1), storage.status().measure.rollovers);
6863     try std.testing.expect(storage.lookupMeasure(testMeasureKey("a")) == null);
6864     const before = storage.status();
6865     try std.testing.expectError(
6866         error.EntryTooLarge,
6867         storage.storeMeasure(testMeasureKey("abcd"), .{ .width = 4 }),
6868     );
6869     try expectEpochUnchangedExceptOversize(before.measure, storage.status().measure);
6870 }
6871 
6872 fn expectEpochUnchangedExceptOversize(
6873     before: AtlasCacheStorage.EpochStatus,
6874     after: AtlasCacheStorage.EpochStatus,
6875 ) !void {
6876     try std.testing.expectEqual(before.entries, after.entries);
6877     try std.testing.expectEqual(before.payload_bytes, after.payload_bytes);
6878     try std.testing.expectEqual(before.physical_payload_bytes, after.physical_payload_bytes);
6879     try std.testing.expectEqual(before.rollovers, after.rollovers);
6880     try std.testing.expectEqual(before.disabled_bypasses, after.disabled_bypasses);
6881     try std.testing.expectEqual(before.oversize_bypasses + 1, after.oversize_bypasses);
6882 }
6883 
6884 fn expectDisabledAtlasCaches() !void {
6885     var storage = try AtlasCacheStorage.init(std.testing.allocator, .{
6886         .measure_entries = 0,
6887         .measure_payload_bytes = 0,
6888         .shape_entries = 0,
6889         .shape_payload_bytes = 0,
6890     });
6891     defer storage.deinit(std.testing.allocator);
6892     storage.activate();
6893     try std.testing.expectError(error.CacheDisabled, storage.admitShapePayload(0));
6894     try std.testing.expectError(
6895         error.CacheDisabled,
6896         storage.storeShape("a", testShapedRun(1)),
6897     );
6898     try std.testing.expectError(
6899         error.CacheDisabled,
6900         storage.storeMeasure(testMeasureKey("a"), .{}),
6901     );
6902     const status = storage.status();
6903     try std.testing.expectEqual(@as(usize, 0), status.storage_bytes);
6904     try std.testing.expectEqual(@as(u64, 1), status.shape.disabled_bypasses);
6905     try std.testing.expectEqual(@as(u64, 1), status.measure.disabled_bypasses);
6906 }
6907 
6908 fn exerciseColdAtlasCache(storage: *AtlasCacheStorage) !void {
6909     _ = try storage.storeShape("a", testShapedRun(1));
6910     _ = try storage.storeShape("b", testShapedRun(2));
6911     _ = try storage.storeShape("c", testShapedRun(3));
6912     try std.testing.expectEqual(@as(i32, 3), storage.lookupShape("c").?.total_x_advance);
6913     _ = try storage.storeMeasure(testMeasureKey("a"), .{ .width = 1 });
6914     _ = try storage.storeMeasure(testMeasureKey("b"), .{ .width = 2 });
6915     _ = try storage.storeMeasure(testMeasureKey("c"), .{ .width = 3 });
6916     try std.testing.expectEqual(@as(f32, 3), storage.lookupMeasure(testMeasureKey("c")).?.width);
6917     try std.testing.expectEqual(@as(u64, 1), storage.status().shape.rollovers);
6918     try std.testing.expectEqual(@as(u64, 1), storage.status().measure.rollovers);
6919 }
6920 
6921 const CollidingShapeKeys = struct {
6922     first: []const u8,
6923     second: []const u8,
6924 };
6925 
6926 fn collidingShapeKeys(slot_count: usize) !CollidingShapeKeys {
6927     const candidates = [_][]const u8{ "a", "b", "c", "d", "e", "f", "g", "h" };
6928     for (candidates, 0..) |first, first_index| {
6929         for (candidates[first_index + 1 ..]) |second| {
6930             const first_slot = cacheStartSlot(std.hash_map.hashString(first), slot_count);
6931             const second_slot = cacheStartSlot(std.hash_map.hashString(second), slot_count);
6932             if (first_slot == second_slot) return .{ .first = first, .second = second };
6933         }
6934     }
6935     return error.NoCollision;
6936 }
6937 
6938 const AtlasCachePointers = struct {
6939     bytes: usize,
6940     measure_slots: usize,
6941     measure_entries: usize,
6942     measure_payload: usize,
6943     shape_slots: usize,
6944     shape_entries: usize,
6945     shape_payload: usize,
6946 };
6947 
6948 fn atlasCachePointers(storage: AtlasCacheStorage) AtlasCachePointers {
6949     return .{
6950         .bytes = @intFromPtr(storage.bytes.ptr),
6951         .measure_slots = @intFromPtr(storage.measure_slots.ptr),
6952         .measure_entries = @intFromPtr(storage.measure_entries.ptr),
6953         .measure_payload = @intFromPtr(storage.measure_payload.bytes.ptr),
6954         .shape_slots = @intFromPtr(storage.shape_slots.ptr),
6955         .shape_entries = @intFromPtr(storage.shape_entries.ptr),
6956         .shape_payload = @intFromPtr(storage.shape_payload.bytes.ptr),
6957     };
6958 }
6959 
6960 fn testShapedRun(advance: i32) filigree.GlyphRun {
6961     return .{
6962         .glyphs = &.{},
6963         .clusters = &.{},
6964         .ligature_carets = &.{},
6965         .total_x_advance = advance,
6966         .total_y_advance = 0,
6967         .direction = .ltr,
6968         .writing_mode = .horizontal,
6969         .output_order = .visual,
6970     };
6971 }
6972 
6973 fn testMeasureKey(content: []const u8) MeasureKey {
6974     return .{
6975         .content = content,
6976         .styles = &.{},
6977         .runs = &.{},
6978         .atlas_entries = &.{},
6979         .fallback_entries = &.{},
6980         .fallback_identity = 0,
6981         .font_asset_id = 0,
6982         .point_size_bits = 0,
6983         .line_height_bits = 0,
6984         .wrap_width_bits = 0,
6985         .device_scale_bits = 0,
6986     };
6987 }
6988 
6989 const ShapeCacheReference = struct {
6990     limits: AtlasCacheStorage.Limits,
6991     keys: [3]?[]const u8 = @splat(null),
6992     values: [3]i32 = @splat(0),
6993     entries: usize = 0,
6994     payload_bytes: usize = 0,
6995     rollovers: u64 = 0,
6996     oversize_bypasses: u64 = 0,
6997 
6998     fn lookup(self: ShapeCacheReference, content: []const u8) ?i32 {
6999         for (self.keys[0..self.entries], self.values[0..self.entries]) |key, value| {
7000             if (std.mem.eql(u8, key.?, content)) return value;
7001         }
7002         return null;
7003     }
7004 
7005     fn store(self: *ShapeCacheReference, content: []const u8, value: i32) bool {
7006         if (content.len > self.limits.shape_payload_bytes) {
7007             self.oversize_bypasses +|= 1;
7008             return false;
7009         }
7010         const payload_full = content.len >
7011             self.limits.shape_payload_bytes - self.payload_bytes;
7012         if (self.entries == self.limits.shape_entries or payload_full) {
7013             self.entries = 0;
7014             self.payload_bytes = 0;
7015             self.rollovers +|= 1;
7016         }
7017         self.keys[self.entries] = content;
7018         self.values[self.entries] = value;
7019         self.entries += 1;
7020         self.payload_bytes += content.len;
7021         return true;
7022     }
7023 };
7024 
7025 fn expectAtlasCacheDifferential() !void {
7026     const limits = AtlasCacheStorage.Limits{
7027         .measure_entries = 0,
7028         .measure_payload_bytes = 0,
7029         .shape_entries = 3,
7030         .shape_payload_bytes = 8,
7031     };
7032     var storage = try AtlasCacheStorage.init(std.testing.allocator, limits);
7033     defer storage.deinit(std.testing.allocator);
7034     storage.activate();
7035     var reference = ShapeCacheReference{ .limits = limits };
7036     const keys = [_][]const u8{ "a", "bb", "ccc", "dddd", "012345678" };
7037     var state: u32 = 0x9E37_79B9;
7038     var step: usize = 0;
7039     while (step < 512) : (step += 1) {
7040         state = state *% 1_664_525 +% 1_013_904_223;
7041         const content = keys[state % keys.len];
7042         try compareAtlasCacheLookup(&storage, reference, content);
7043         if (reference.lookup(content) == null) {
7044             const value: i32 = @intCast(step);
7045             const admitted = reference.store(content, value);
7046             if (admitted) {
7047                 _ = try storage.storeShape(content, testShapedRun(value));
7048             } else {
7049                 try std.testing.expectError(
7050                     error.EntryTooLarge,
7051                     storage.storeShape(content, testShapedRun(value)),
7052                 );
7053             }
7054         }
7055         try compareAtlasCacheStatus(storage.status().shape, reference);
7056     }
7057 }
7058 
7059 fn compareAtlasCacheLookup(
7060     storage: *const AtlasCacheStorage,
7061     reference: ShapeCacheReference,
7062     content: []const u8,
7063 ) !void {
7064     const expected = reference.lookup(content);
7065     const actual = storage.lookupShape(content);
7066     try std.testing.expectEqual(expected != null, actual != null);
7067     if (expected) |value| try std.testing.expectEqual(value, actual.?.total_x_advance);
7068 }
7069 
7070 fn compareAtlasCacheStatus(
7071     actual: AtlasCacheStorage.EpochStatus,
7072     expected: ShapeCacheReference,
7073 ) !void {
7074     try std.testing.expectEqual(expected.entries, actual.entries);
7075     try std.testing.expectEqual(expected.payload_bytes, actual.payload_bytes);
7076     try std.testing.expectEqual(expected.rollovers, actual.rollovers);
7077     try std.testing.expectEqual(expected.oversize_bypasses, actual.oversize_bypasses);
7078 }
7079 
7080 fn cacheEpochText(content: []const u8) UiText {
7081     return .{ .content = content, .point_size = 16, .line_height = 1 };
7082 }
7083 
7084 fn expectGlyphRunEqual(expected: filigree.GlyphRun, actual: filigree.GlyphRun) !void {
7085     try std.testing.expectEqualSlices(filigree.ShapedGlyph, expected.glyphs, actual.glyphs);
7086     try std.testing.expectEqualSlices(filigree.Cluster, expected.clusters, actual.clusters);
7087     try std.testing.expectEqualSlices(filigree.LigatureCaret, expected.ligature_carets, actual.ligature_carets);
7088     try std.testing.expectEqual(expected.total_x_advance, actual.total_x_advance);
7089     try std.testing.expectEqual(expected.total_y_advance, actual.total_y_advance);
7090     try std.testing.expectEqual(expected.direction, actual.direction);
7091     try std.testing.expectEqual(expected.writing_mode, actual.writing_mode);
7092     try std.testing.expectEqual(expected.output_order, actual.output_order);
7093 }
7094 
7095 test "bitmap atlas skips glyphs missing from the map without question mark" {
7096     const allocator = std.testing.allocator;
7097     const glyphs = [_]BitmapGlyph{
7098         .{ .codepoint = 'x', .rows = &.{ 0xFF, 0xFF } },
7099     };
7100     var atlas = try Atlas.initFromBitmapGlyphs(
7101         allocator,
7102         glyphs[0..],
7103         .{ .width = 8, .height = 2, .stride = 1 },
7104         16,
7105         test_atlas_cache_limits,
7106     );
7107     defer atlas.deinit();
7108 
7109     const run = try atlas.shape("xy");
7110     try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
7111     try std.testing.expectEqual(@as(u32, 'x'), run.glyphs[0].glyph_id);
7112     try std.testing.expectEqual(std.math.maxInt(u32), run.glyphs[1].glyph_id);
7113 
7114     var recorder = TestRecorder{};
7115     drawGlyphRun(&recorder, 0, atlas.atlas, run, 0, 0, .{ .a = 255 });
7116     try std.testing.expectEqual(@as(usize, 1), recorder.glyph_count);
7117 }
7118 
7119 test "Atlas shapes fixture bytes and appends frame glyph commands" {
7120     const allocator = std.testing.allocator;
7121     var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 });
7122     defer scratch.deinit(allocator);
7123     const bytes = try filigree.fixtures.createWithOutlines(allocator);
7124     var atlas = try Atlas.initFromOwnedBytes(
7125         allocator,
7126         &scratch,
7127         bytes,
7128         18,
7129         test_atlas_cache_limits,
7130         test_output_limits,
7131     );
7132     defer atlas.deinit();
7133     const child = [_]gui.model.UiNode{.{
7134         .widget_id = 2,
7135         .kind = .label,
7136         .text = .{ .content = "AB" },
7137         .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
7138         .size = .{ .width = 32, .height = 18 },
7139     }};
7140     const surface = gui.model.UiSurfaceTree{
7141         .available_size = .{ .width = 48, .height = 24 },
7142         .root = .{
7143             .widget_id = 1,
7144             .children = child[0..],
7145         },
7146     };
7147     var frame_workspace = gui.frame.Workspace.init(allocator);
7148     defer frame_workspace.deinit();
7149     const frame = try frame_workspace.buildSurface(&surface, .{});
7150     var commands = command.CommandBuffer.init(allocator);
7151     defer commands.deinit();
7152 
7153     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
7154     const atlases = AtlasSet{ .entries = entries[0..] };
7155     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
7156 
7157     try std.testing.expect(commands.items().len > 0);
7158     try std.testing.expectEqual(command.Kind.glyph, commands.items()[0].kind);
7159     try std.testing.expectEqual(@as(u32, 0), commands.items()[0].image_index);
7160     try std.testing.expectEqual(@as(u8, 255), commands.items()[0].color.a);
7161     try std.testing.expect(atlas.image.image.width > 0);
7162 }
7163 
7164 test "missing terminal glyphs synthesize fills instead of blank advances" {
7165     const allocator = std.testing.allocator;
7166     var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 });
7167     defer scratch.deinit(allocator);
7168     const bytes = try filigree.fixtures.createWithOutlines(allocator);
7169     var atlas = try Atlas.initFromOwnedBytes(
7170         allocator,
7171         &scratch,
7172         bytes,
7173         18,
7174         test_atlas_cache_limits,
7175         test_output_limits,
7176     );
7177     defer atlas.deinit();
7178     const child = [_]gui.model.UiNode{.{
7179         .widget_id = 2,
7180         .kind = .label,
7181         .text = .{ .content = "\u{258F}\u{280B}\u{276F}" },
7182         .paint = .{ .foreground = .{ .r = 251, .g = 73, .b = 52, .a = 255 } },
7183         .size = .{ .width = 48, .height = 24 },
7184     }};
7185     const surface = gui.model.UiSurfaceTree{
7186         .available_size = .{ .width = 64, .height = 32 },
7187         .root = .{
7188             .widget_id = 1,
7189             .children = child[0..],
7190         },
7191     };
7192     var frame_workspace = gui.frame.Workspace.init(allocator);
7193     defer frame_workspace.deinit();
7194     const frame = try frame_workspace.buildSurface(&surface, .{});
7195     var commands = command.CommandBuffer.init(allocator);
7196     defer commands.deinit();
7197 
7198     const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }};
7199     const atlases = AtlasSet{ .entries = entries[0..] };
7200     try appendFrameCommands(&commands, frame, &atlases, 1, 0);
7201 
7202     var fills: usize = 0;
7203     for (commands.items()) |item| {
7204         if (item.kind == .fill) fills += 1;
7205     }
7206     try std.testing.expect(fills >= 5);
7207     for (commands.items()) |item| {
7208         try std.testing.expectEqual(@as(u8, 251), item.color.r);
7209     }
7210 }
7211 
7212 test "atlas set picks matching face and nearest downscale size" {
7213     const allocator = std.testing.allocator;
7214     const glyphs = [_]BitmapGlyph{.{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }};
7215     var small = try Atlas.initFromBitmapGlyphs(
7216         allocator,
7217         glyphs[0..],
7218         .{ .width = 8, .height = 2, .stride = 1 },
7219         13,
7220         test_atlas_cache_limits,
7221     );
7222     defer small.deinit();
7223     var body = try Atlas.initFromBitmapGlyphs(
7224         allocator,
7225         glyphs[0..],
7226         .{ .width = 8, .height = 2, .stride = 1 },
7227         16,
7228         test_atlas_cache_limits,
7229     );
7230     defer body.deinit();
7231     var mono = try Atlas.initFromBitmapGlyphs(
7232         allocator,
7233         glyphs[0..],
7234         .{ .width = 8, .height = 2, .stride = 1 },
7235         15,
7236         test_atlas_cache_limits,
7237     );
7238     defer mono.deinit();
7239 
7240     const entries = [_]AtlasSet.Entry{
7241         .{ .face = 0, .image_index = 0, .atlas = &small },
7242         .{ .face = 0, .image_index = 1, .atlas = &body },
7243         .{ .face = 2, .image_index = 2, .atlas = &mono },
7244     };
7245     const atlases = AtlasSet{ .entries = entries[0..] };
7246 
7247     const body_pick = atlases.forText(.{ .content = "A", .point_size = 16 }).?;
7248     try std.testing.expectEqual(@as(u32, 1), body_pick.image_index);
7249 
7250     const caption_pick = atlases.forText(.{ .content = "A", .point_size = 13 }).?;
7251     try std.testing.expectEqual(@as(u32, 0), caption_pick.image_index);
7252 
7253     const near_pick = atlases.forText(.{ .content = "A", .point_size = 14 }).?;
7254     try std.testing.expectEqual(@as(u32, 1), near_pick.image_index);
7255 
7256     const mono_pick = atlases.forText(.{ .content = "A", .point_size = 15, .font_asset_id = 2 }).?;
7257     try std.testing.expectEqual(@as(u32, 2), mono_pick.image_index);
7258 
7259     const missing_face_pick = atlases.forText(.{ .content = "A", .point_size = 16, .font_asset_id = 9 }).?;
7260     try std.testing.expectEqual(@as(u32, 1), missing_face_pick.image_index);
7261 
7262     const empty = AtlasSet{};
7263     try std.testing.expect(empty.forText(.{ .content = "A" }) == null);
7264 }