tiny.gui.paint.text
Defined in paint.
API (68)
Actions
Public operations.
Atlas.CaretLine.advanceForByteOffsetAtlas.CaretLine.hitTestAdvanceAtlas.advanceForByteOffsetAtlas.cacheStatusAtlas.caretLineAtlas.deinitAtlas.hitTestAdvanceAtlas.initFromBitmapGlyphsAtlas.initFromOwnedBytesAtlas.metricsAtlas.shapeAtlasCacheStorage.Capacity.deriveAtlasCacheStorage.activateAtlasCacheStorage.admitShapePayloadAtlasCacheStorage.deinitAtlasCacheStorage.initAtlasCacheStorage.statusAtlasScratch.Capacity.deriveAtlasScratch.beginAtlasScratch.deinitAtlasScratch.endAtlasScratch.initAtlasScratch.statusAtlasSet.forStyleAtlasSet.forTextMetrics.heightOwnedAtlasImage.deinitappendFrameCommandsappendFrameRegionCommandsdrawGlyphRunframeResolversmeasuremeasureFrameTextmeasureSetpackAtlasImageAlloctextAdvanceForByteOffsettextCaretGeometrytextHitTestAdvancetextHitTestPointtextHitTestWidgetPointtextHitTestWidgetPointClamped
Types and contracts
Public types and contracts.
AtlasAtlas.BackendAtlas.BitmapAtlas.CaretLineAtlas.OutlineAtlasCacheStorageAtlasCacheStorage.CapacityAtlasCacheStorage.DeriveErrorAtlasCacheStorage.EpochStatusAtlasCacheStorage.ExhaustionAtlasCacheStorage.LimitsAtlasCacheStorage.StatusAtlasScratchAtlasScratch.CapacityAtlasScratch.LimitsAtlasScratch.StatusAtlasSetAtlasSet.EntryBitmapGlyphBitmapMetricsMetricsOwnedAtlasImageTextCaretAffinityTextCaretGeometry
Values and defaults
Public values and defaults.
Source
Source: lib/gui/src/paint/root.zig:13
zig
pub const text = @import("text.zig");Source: lib/gui/src/paint/text.zig
zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const filigree = @import("filigree");const command = @import("command.zig");const gui = @import("../root.zig");const cpu = @import("cpu/root.zig");const fallback_mod = @import("fallback.zig");const synth = @import("synth.zig");const Allocator = std.mem.Allocator;const Color = gui.model.UiColor;const Image = command.Image;const Region = cpu.Region;const Size = gui.layout.Size;const UiFrame = gui.model.UiFrame;const UiNode = gui.model.UiNode;const UiText = gui.model.UiText;const UiTextRun = gui.model.UiTextRun;const UiTextStyle = gui.model.UiTextStyle;const UiTextSelection = gui.model.UiTextSelection;const WidgetFrame = gui.model.WidgetFrame;const Rect = gui.layout.Rect;pub const OwnedAtlasImage = struct { image: Image, pixels: []u32, pub fn deinit(self: *OwnedAtlasImage, allocator: Allocator) void { allocator.free(self.pixels); self.* = undefined; }};pub const BitmapGlyph = struct { codepoint: u21, rows: []const u8,};pub const BitmapMetrics = struct { width: u16, height: u16, stride: u16,};pub const AtlasScratch = struct { capacity: Capacity, bytes: []align(storage_alignment) u8, fixed: std.heap.FixedBufferAllocator, active: bool = false, epoch_peak_bytes: usize = 0, last_epoch_peak_bytes: usize = 0, high_water_bytes: usize = 0, epochs: usize = 0, exhaustions: usize = 0, pub const storage_alignment: usize = 64; pub const Limits = struct { bytes: usize, }; pub const Capacity = struct { bytes: usize, total_bytes: usize, pub fn derive(limits: Limits) Capacity { return .{ .bytes = limits.bytes, .total_bytes = limits.bytes, }; } }; pub const Status = struct { capacity: Capacity, last_epoch_peak_bytes: usize, high_water_bytes: usize, epochs: usize, exhaustions: usize, }; pub fn init(allocator: Allocator, limits: Limits) Allocator.Error!AtlasScratch { const capacity = Capacity.derive(limits); const bytes = try allocator.alignedAlloc(u8, .fromByteUnits(storage_alignment), capacity.bytes); return .{ .capacity = capacity, .bytes = bytes, .fixed = std.heap.FixedBufferAllocator.init(bytes), }; } pub fn deinit(self: *AtlasScratch, allocator: Allocator) void { std.debug.assert(!self.active); allocator.free(self.bytes); self.* = undefined; } pub fn begin(self: *AtlasScratch) Allocator { std.debug.assert(!self.active); self.fixed = std.heap.FixedBufferAllocator.init(self.bytes); self.epoch_peak_bytes = 0; self.active = true; return .{ .ptr = self, .vtable = &.{ .alloc = alloc, .resize = resize, .remap = remap, .free = free, }, }; } pub fn end(self: *AtlasScratch) void { std.debug.assert(self.active); self.last_epoch_peak_bytes = self.epoch_peak_bytes; self.high_water_bytes = @max(self.high_water_bytes, self.epoch_peak_bytes); self.fixed = std.heap.FixedBufferAllocator.init(self.bytes); self.active = false; self.epochs +|= 1; } pub fn status(self: *const AtlasScratch) Status { std.debug.assert(!self.active); return .{ .capacity = self.capacity, .last_epoch_peak_bytes = self.last_epoch_peak_bytes, .high_water_bytes = self.high_water_bytes, .epochs = self.epochs, .exhaustions = self.exhaustions, }; } fn observe(self: *AtlasScratch) void { self.epoch_peak_bytes = @max(self.epoch_peak_bytes, self.fixed.end_index); } fn alloc(context: *anyopaque, len: usize, alignment: std.mem.Alignment, return_address: usize) ?[*]u8 { const self: *AtlasScratch = @ptrCast(@alignCast(context)); const result = std.heap.FixedBufferAllocator.alloc(&self.fixed, len, alignment, return_address) orelse { self.exhaustions +|= 1; return null; }; self.observe(); return result; } fn resize(context: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, return_address: usize) bool { const self: *AtlasScratch = @ptrCast(@alignCast(context)); const resized = std.heap.FixedBufferAllocator.resize(&self.fixed, memory, alignment, new_len, return_address); if (resized) self.observe(); return resized; } fn remap(context: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, return_address: usize) ?[*]u8 { const self: *AtlasScratch = @ptrCast(@alignCast(context)); const result = std.heap.FixedBufferAllocator.remap(&self.fixed, memory, alignment, new_len, return_address); if (result != null) self.observe(); return result; } fn free(context: *anyopaque, memory: []u8, alignment: std.mem.Alignment, return_address: usize) void { const self: *AtlasScratch = @ptrCast(@alignCast(context)); std.heap.FixedBufferAllocator.free(&self.fixed, memory, alignment, return_address); }};pub const Atlas = struct { allocator: Allocator, pixel_size: i32, atlas: filigree.GlyphAtlas, image: OwnedAtlasImage, glyph_index: std.AutoHashMapUnmanaged(u32, usize) = .empty, cache: AtlasCacheStorage, line_break_workspace: filigree.LineBreakWorkspace = .{}, composite_workspace: CompositeWorkspace = .{}, backend: Backend, pub const Backend = union(enum) { outline: Outline, bitmap: Bitmap, }; pub const Outline = struct { font_bytes: []u8, font: filigree.Font, context: filigree.Context, output: filigree.Output, }; pub const Bitmap = struct { advance: i32, height: i32, map: std.AutoHashMapUnmanaged(u21, u32) = .empty, shaped: std.ArrayListUnmanaged(filigree.ShapedGlyph) = .empty, clusters: std.ArrayListUnmanaged(filigree.Cluster) = .empty, }; pub const CaretLine = struct { content: []const u8, run: filigree.GlyphRun, pub inline fn advanceForByteOffset(self: CaretLine, byte_offset: usize) f32 { return @call(.always_inline, filigree.caret.advanceForByteOffset, .{ self.run, byte_offset, self.content.len, }); } pub fn hitTestAdvance(self: CaretLine, advance: f32) filigree.caret.LineHit { return filigree.caret.hitTestAdvance(self.run, advance, self.content); } }; pub fn initFromOwnedBytes( allocator: Allocator, scratch: *AtlasScratch, bytes: []u8, pixel_size: i32, cache_limits: AtlasCacheStorage.Limits, output_limits: filigree.Output.Limits, ) !Atlas { return initOutlineFromOwnedBytes(allocator, scratch, bytes, pixel_size, cache_limits, output_limits); } fn initOutlineFromOwnedBytes( allocator: Allocator, scratch: *AtlasScratch, bytes: []u8, pixel_size: i32, cache_limits: AtlasCacheStorage.Limits, output_limits: filigree.Output.Limits, ) !Atlas { errdefer allocator.free(bytes); const scratch_allocator = scratch.begin(); defer scratch.end(); var font = filigree.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.InvalidFont; errdefer font.deinit(); font.setScale(@floatFromInt(pixel_size), 72); const glyph_count: u32 = font.face.num_glyphs; const first_glyph_id: u32 = 1; if (glyph_count <= first_glyph_id) return error.FontHasNoPrintableAscii; const raster_glyph_count = glyph_count - first_glyph_id; var glyph_ids: std.ArrayListUnmanaged(i32) = .empty; defer glyph_ids.deinit(scratch_allocator); var codepoints: std.ArrayListUnmanaged(i32) = .empty; defer codepoints.deinit(scratch_allocator); try glyph_ids.ensureTotalCapacity(scratch_allocator, raster_glyph_count); try codepoints.ensureTotalCapacity(scratch_allocator, raster_glyph_count); var glyph_id: u32 = first_glyph_id; while (glyph_id < glyph_count) : (glyph_id += 1) { glyph_ids.appendAssumeCapacity(std.math.cast(i32, glyph_id) orelse return error.GlyphIdTooLarge); codepoints.appendAssumeCapacity(0); } var atlas = try filigree.glyphAtlasAlloc(allocator, scratch_allocator, bytes, pixel_size, glyph_ids.items, codepoints.items, 1); errdefer atlas.deinit(allocator); var image = try packAtlasImageAlloc(allocator, atlas); errdefer image.deinit(allocator); var glyph_index = try glyphIndexAlloc(allocator, atlas); errdefer glyph_index.deinit(allocator); var cache = try AtlasCacheStorage.init(allocator, cache_limits); errdefer cache.deinit(allocator); cache.activate(); return .{ .allocator = allocator, .pixel_size = pixel_size, .atlas = atlas, .image = image, .glyph_index = glyph_index, .cache = cache, .backend = .{ .outline = .{ .font_bytes = bytes, .font = font, .context = filigree.Context.init(allocator, .{}), .output = try filigree.Output.init(allocator, output_limits), } }, }; } pub fn initFromBitmapGlyphs( allocator: Allocator, glyphs: []const BitmapGlyph, cell: BitmapMetrics, pixel_size: i32, cache_limits: AtlasCacheStorage.Limits, ) !Atlas { if (glyphs.len == 0 or cell.width == 0 or cell.height == 0) return error.FontHasNoPrintableAscii; const cell_width: usize = cell.width; const cell_height: usize = cell.height; const padded = cell_width + 1; const atlas_width = std.math.mul(usize, padded, glyphs.len) catch return error.InvalidAtlas; const pixel_total = try pixelCount( std.math.cast(u32, atlas_width) orelse return error.InvalidAtlas, std.math.cast(u32, cell_height) orelse return error.InvalidAtlas, ); const rgba = try allocator.alloc(u8, pixel_total * 4); errdefer allocator.free(rgba); @memset(rgba, 0); const atlas_glyphs = try allocator.alloc(filigree.GlyphAtlasGlyph, glyphs.len); errdefer allocator.free(atlas_glyphs); const recs = try allocator.alloc(filigree.GlyphAtlasRectangle, glyphs.len); errdefer allocator.free(recs); var map: std.AutoHashMapUnmanaged(u21, u32) = .empty; errdefer map.deinit(allocator); for (glyphs, 0..) |glyph, index| { if (glyph.rows.len < @as(usize, cell.stride) * cell_height) return error.InvalidAtlas; const origin_x = index * padded; for (0..cell_height) |y| { const row = glyph.rows[y * cell.stride ..][0..cell.stride]; for (0..cell_width) |x| { const bit = (row[x / 8] >> @intCast(7 - (x % 8))) & 1; if (bit == 0) continue; const base = ((y * atlas_width) + origin_x + x) * 4; rgba[base] = 255; rgba[base + 1] = 255; rgba[base + 2] = 255; rgba[base + 3] = 255; } } atlas_glyphs[index] = .{ .codepoint = std.math.cast(i32, glyph.codepoint) orelse return error.CodepointTooLarge, .glyph_id = glyph.codepoint, .width = @intCast(cell_width), .height = @intCast(cell_height), .offset_x = 0, .offset_y = 0, .advance_x = @intCast(cell_width), }; recs[index] = .{ .x = @floatFromInt(origin_x), .y = 0, .width = @floatFromInt(cell_width), .height = @floatFromInt(cell_height), }; try map.put(allocator, glyph.codepoint, glyph.codepoint); } const atlas = filigree.GlyphAtlas{ .rgba = rgba, .width = @intCast(atlas_width), .height = @intCast(cell_height), .glyphs = atlas_glyphs, .recs = recs, .base_size = @intCast(cell_height), .glyph_padding = 1, }; var image = try packAtlasImageAlloc(allocator, atlas); errdefer image.deinit(allocator); var glyph_index = try glyphIndexAlloc(allocator, atlas); errdefer glyph_index.deinit(allocator); var cache = try AtlasCacheStorage.init(allocator, cache_limits); errdefer cache.deinit(allocator); cache.activate(); return .{ .allocator = allocator, .pixel_size = pixel_size, .atlas = atlas, .image = image, .glyph_index = glyph_index, .cache = cache, .backend = .{ .bitmap = .{ .advance = @as(i32, @intCast(cell_width)) * 64, .height = @intCast(cell_height), .map = map, } }, }; } pub fn deinit(self: *Atlas) void { self.cache.deinit(self.allocator); self.line_break_workspace.deinit(self.allocator); self.composite_workspace.deinit(self.allocator); self.glyph_index.deinit(self.allocator); self.image.deinit(self.allocator); self.atlas.deinit(self.allocator); switch (self.backend) { .outline => |*outline| { outline.output.deinit(self.allocator); outline.context.deinit(); outline.font.deinit(); self.allocator.free(outline.font_bytes); }, .bitmap => |*bitmap| { bitmap.map.deinit(self.allocator); bitmap.shaped.deinit(self.allocator); bitmap.clusters.deinit(self.allocator); }, } self.* = undefined; } pub fn cacheStatus(self: *const Atlas) AtlasCacheStorage.Status { return self.cache.status(); } pub fn shape(self: *Atlas, content: []const u8) !filigree.GlyphRun { if (self.cache.lookupShape(content)) |cached| return cached; const fresh = try self.shapeUncached(content); return self.cache.storeShape(content, fresh) catch fresh; } pub fn caretLine(self: *Atlas, content: []const u8) !CaretLine { return .{ .content = content, .run = try self.shape(content) }; } pub fn advanceForByteOffset(self: *Atlas, content: []const u8, byte_offset: usize) !f32 { const line = try self.caretLine(content); return line.advanceForByteOffset(byte_offset); } pub fn hitTestAdvance(self: *Atlas, content: []const u8, advance: f32) !filigree.caret.LineHit { const line = try self.caretLine(content); return line.hitTestAdvance(advance); } fn shapeUncached(self: *Atlas, content: []const u8) !filigree.GlyphRun { switch (self.backend) { .outline => |*outline| { try outline.context.shapeRun(.{ .font = &outline.font, .text = .{ .utf8 = content }, }, &outline.output); return outline.output.run(); }, .bitmap => |*bitmap| return shapeBitmap(self.allocator, bitmap, content), } } fn outlineFont(self: *Atlas) ?*const filigree.Font { return switch (self.backend) { .outline => |*outline| &outline.font, .bitmap => null, }; } fn coversUtf8(self: *Atlas, content: []const u8) !bool { const selected = self.outlineFont() orelse return true; var index: usize = 0; while (index < content.len) { const sequence_len = std.unicode.utf8ByteSequenceLength(content[index]) catch return error.InvalidUtf8; if (index + sequence_len > content.len) return error.InvalidUtf8; const codepoint = std.unicode.utf8Decode(content[index .. index + sequence_len]) catch return error.InvalidUtf8; if (selected.face.glyphId(codepoint) == 0) return false; index += sequence_len; } return true; } pub fn metrics(self: *const Atlas) Metrics { switch (self.backend) { .outline => |*outline| { const face = outline.font.face; const upem: f32 = @floatFromInt(if (face.units_per_em == 0) 1000 else face.units_per_em); const to_pixels = @as(f32, @floatFromInt(self.pixel_size)) / upem; return .{ .ascent = @as(f32, @floatFromInt(face.ascender)) * to_pixels, .descent = @as(f32, @floatFromInt(-@as(i32, face.descender))) * to_pixels, .line_gap = @as(f32, @floatFromInt(face.line_gap)) * to_pixels, }; }, .bitmap => |*bitmap| return .{ .ascent = @floatFromInt(bitmap.height), .descent = 0, .line_gap = 0, }, } }};pub const Metrics = struct { ascent: f32, descent: f32, line_gap: f32, pub fn height(self: Metrics) f32 { return self.ascent + self.descent; }};const CompositeGlyphSource = struct { segment_index: u32,};const CompositeMetricSpan = struct { byte_start: u32, byte_end: u32, atlas: *const Atlas, image_index: u32, scale_ratio: f32, point_size: f32, ascent: f32, descent: f32,};const CompositeWorkspace = struct { glyphs: std.ArrayListUnmanaged(filigree.ShapedGlyph) = .empty, clusters: std.ArrayListUnmanaged(filigree.Cluster) = .empty, ligature_carets: std.ArrayListUnmanaged(filigree.LigatureCaret) = .empty, glyph_sources: std.ArrayListUnmanaged(CompositeGlyphSource) = .empty, spans: std.ArrayListUnmanaged(CompositeMetricSpan) = .empty, fn clear(self: *CompositeWorkspace) void { self.glyphs.clearRetainingCapacity(); self.clusters.clearRetainingCapacity(); self.ligature_carets.clearRetainingCapacity(); self.glyph_sources.clearRetainingCapacity(); self.spans.clearRetainingCapacity(); } fn deinit(self: *CompositeWorkspace, allocator: Allocator) void { self.spans.deinit(allocator); self.glyph_sources.deinit(allocator); self.ligature_carets.deinit(allocator); self.clusters.deinit(allocator); self.glyphs.deinit(allocator); self.* = .{}; }};const MeasureKey = struct { content: []const u8, styles: []const UiTextStyle, runs: []const UiTextRun, atlas_entries: []const AtlasSet.Entry, fallback_entries: []const AtlasSet.Entry, fallback_identity: usize, font_asset_id: u64, point_size_bits: u64, line_height_bits: u64, wrap_width_bits: u64, device_scale_bits: u32, fn fromText(text: UiText, atlases: ?*const AtlasSet) MeasureKey { const include_metric_plan = atlases != null; return .{ .content = text.content, .styles = if (include_metric_plan) text.styles else &.{}, .runs = if (include_metric_plan) text.runs else &.{}, .atlas_entries = if (atlases) |set| set.entries else &.{}, .fallback_entries = if (atlases) |set| set.fallback_entries else &.{}, .fallback_identity = if (atlases) |set| if (set.fallback) |value| @intFromPtr(value) else 0 else 0, .font_asset_id = if (include_metric_plan) text.font_asset_id else 0, .point_size_bits = floatBits(text.point_size), .line_height_bits = floatBits(text.line_height), .wrap_width_bits = floatBits(text.wrap_width), .device_scale_bits = if (atlases) |set| @bitCast(set.device_scale) else 0, }; }};const MeasureContext = struct { pub fn hash(_: MeasureContext, key: MeasureKey) u64 { var hasher = std.hash.Wyhash.init(0); hasher.update(key.content); std.hash.autoHash(&hasher, key.font_asset_id); hasher.update(std.mem.asBytes(&key.point_size_bits)); hasher.update(std.mem.asBytes(&key.line_height_bits)); hasher.update(std.mem.asBytes(&key.wrap_width_bits)); hasher.update(std.mem.asBytes(&key.device_scale_bits)); for (key.styles) |style| { std.hash.autoHash(&hasher, style.font_asset_id); std.hash.autoHash(&hasher, floatBits(style.point_size)); } for (key.runs) |run| hashTextRun(&hasher, run); for (key.atlas_entries) |entry| { std.hash.autoHash(&hasher, entry.face); std.hash.autoHash(&hasher, entry.image_index); std.hash.autoHash(&hasher, @intFromPtr(entry.atlas)); } for (key.fallback_entries) |entry| { std.hash.autoHash(&hasher, entry.face); std.hash.autoHash(&hasher, entry.image_index); std.hash.autoHash(&hasher, @intFromPtr(entry.atlas)); } std.hash.autoHash(&hasher, key.fallback_identity); return hasher.final(); } pub fn eql(_: MeasureContext, left: MeasureKey, right: MeasureKey) bool { if (left.font_asset_id != right.font_asset_id or left.point_size_bits != right.point_size_bits or left.line_height_bits != right.line_height_bits or left.wrap_width_bits != right.wrap_width_bits or left.device_scale_bits != right.device_scale_bits or left.fallback_identity != right.fallback_identity) { return false; } if (!std.mem.eql(u8, left.content, right.content)) return false; if (left.styles.len != right.styles.len or left.runs.len != right.runs.len or left.atlas_entries.len != right.atlas_entries.len or left.fallback_entries.len != right.fallback_entries.len) { return false; } for (left.styles, right.styles) |left_style, right_style| { if (!gui.model.textStylesEqual(left_style, right_style)) return false; } for (left.runs, right.runs) |left_run, right_run| { if (!textRunsEqual(left_run, right_run)) return false; } for (left.atlas_entries, right.atlas_entries) |left_entry, right_entry| { if (left_entry.face != right_entry.face or left_entry.image_index != right_entry.image_index or left_entry.atlas != right_entry.atlas) { return false; } } for (left.fallback_entries, right.fallback_entries) |left_entry, right_entry| { if (left_entry.face != right_entry.face or left_entry.image_index != right_entry.image_index or left_entry.atlas != right_entry.atlas) { return false; } } return true; }};fn hashTextRun(hasher: anytype, run: UiTextRun) void { std.hash.autoHash(hasher, run.byte_start); std.hash.autoHash(hasher, run.byte_end); std.hash.autoHash(hasher, run.style_slot); hashOptionalTextColor(hasher, run.foreground); hashOptionalTextColor(hasher, run.background); std.hash.autoHash(hasher, run.underline); std.hash.autoHash(hasher, run.strikethrough);}fn hashOptionalTextColor(hasher: anytype, color: ?Color) void { std.hash.autoHash(hasher, color != null); if (color) |value| { std.hash.autoHash(hasher, value.r); std.hash.autoHash(hasher, value.g); std.hash.autoHash(hasher, value.b); std.hash.autoHash(hasher, value.a); }}fn textRunsEqual(left: UiTextRun, right: UiTextRun) bool { return left.byte_start == right.byte_start and left.byte_end == right.byte_end and left.style_slot == right.style_slot and std.meta.eql(left.foreground, right.foreground) and std.meta.eql(left.background, right.background) and left.underline == right.underline and left.strikethrough == right.strikethrough;}const ShapedRun = struct { glyphs: []const filigree.ShapedGlyph, clusters: []const filigree.Cluster, ligature_carets: []const filigree.LigatureCaret, total_x_advance: i32, total_y_advance: i32, direction: filigree.Direction, writing_mode: filigree.WritingMode, output_order: filigree.OutputOrder, fn run(self: ShapedRun) filigree.GlyphRun { return .{ .glyphs = self.glyphs, .clusters = self.clusters, .ligature_carets = self.ligature_carets, .total_x_advance = self.total_x_advance, .total_y_advance = self.total_y_advance, .direction = self.direction, .writing_mode = self.writing_mode, .output_order = self.output_order, }; }};const MeasureCacheEntry = struct { hash: u64, key: MeasureKey, value: Size,};const ShapeCacheEntry = struct { hash: u64, content: []const u8, shaped: ShapedRun,};const CacheEpochUsage = struct { entries: usize = 0, payload_bytes: usize = 0, high_water_entries: usize = 0, high_water_payload_bytes: usize = 0, high_water_physical_payload_bytes: usize = 0, rollovers: u64 = 0, disabled_bypasses: u64 = 0, oversize_bypasses: u64 = 0, fn inserted(self: *CacheEpochUsage, raw_bytes: usize, physical_bytes: usize) void { self.entries += 1; self.payload_bytes += raw_bytes; self.high_water_entries = @max(self.high_water_entries, self.entries); self.high_water_payload_bytes = @max(self.high_water_payload_bytes, self.payload_bytes); self.high_water_physical_payload_bytes = @max( self.high_water_physical_payload_bytes, physical_bytes, ); } fn replaced(self: *CacheEpochUsage) void { self.entries = 0; self.payload_bytes = 0; self.rollovers +|= 1; }};const CachePayload = struct { bytes: []u8, cursor: usize = 0, fn reset(self: *CachePayload) void { self.cursor = 0; } fn dupe(self: *CachePayload, comptime T: type, source: []const T) []T { const start = cacheAligned(self.cursor, @alignOf(T)) catch unreachable; const byte_count = cacheMultiplied(source.len, @sizeOf(T)) catch unreachable; const end = cacheAdded(start, byte_count) catch unreachable; std.debug.assert(end <= self.bytes.len); const region: []align(@alignOf(T)) u8 = @alignCast(self.bytes[start..end]); const owned = std.mem.bytesAsSlice(T, region); @memcpy(owned, source); self.cursor = end; return owned; }};pub const AtlasCacheStorage = struct { phase: alloc_phase.capacity.Phase, capacity: Capacity, bytes: []align(storage_alignment) u8, measure_slots: []u32, measure_entries: []MeasureCacheEntry, measure_payload: CachePayload, measure_usage: CacheEpochUsage = .{}, shape_slots: []u32, shape_entries: []ShapeCacheEntry, shape_payload: CachePayload, shape_usage: CacheEpochUsage = .{}, pub const storage_alignment: usize = @max( @alignOf(MeasureCacheEntry), @max( @alignOf(ShapeCacheEntry), @max(measure_payload_alignment, shape_payload_alignment), ), ); pub const Limits = struct { measure_entries: usize, measure_payload_bytes: usize, shape_entries: usize, shape_payload_bytes: usize, }; pub const DeriveError = error{ InvalidMeasureLimits, InvalidShapeLimits, EntryLimitTooLarge, CapacityOverflow, }; pub const Exhaustion = error{ CacheDisabled, EntryTooLarge, }; pub const Capacity = struct { limits: Limits, measure_index_slots: usize, measure_index_offset: usize, measure_entries_offset: usize, measure_payload_offset: usize, measure_payload_storage_bytes: usize, shape_index_slots: usize, shape_index_offset: usize, shape_entries_offset: usize, shape_payload_offset: usize, shape_payload_storage_bytes: usize, storage_bytes: usize, pub fn derive(limits: Limits) DeriveError!Capacity { try cacheLimitsValid(limits); const measure_index_slots = try cacheIndexSlots(limits.measure_entries); const shape_index_slots = try cacheIndexSlots(limits.shape_entries); const measure_padding = try cacheMultiplied( limits.measure_entries, measure_padding_per_entry, ); const shape_padding = try cacheMultiplied( limits.shape_entries, shape_padding_per_entry, ); const measure_payload_bytes = try cacheAdded( limits.measure_payload_bytes, measure_padding, ); const shape_payload_bytes = try cacheAdded( limits.shape_payload_bytes, shape_padding, ); const measure_index = try cachePlaced(u32, 0, measure_index_slots); const measure_entries = try cachePlaced( MeasureCacheEntry, measure_index.end, limits.measure_entries, ); const measure_payload = try cachePlacedBytes( measure_entries.end, measure_payload_alignment, measure_payload_bytes, ); const shape_index = try cachePlaced(u32, measure_payload.end, shape_index_slots); const shape_entries = try cachePlaced( ShapeCacheEntry, shape_index.end, limits.shape_entries, ); const shape_payload = try cachePlacedBytes( shape_entries.end, shape_payload_alignment, shape_payload_bytes, ); return .{ .limits = limits, .measure_index_slots = measure_index_slots, .measure_index_offset = measure_index.start, .measure_entries_offset = measure_entries.start, .measure_payload_offset = measure_payload.start, .measure_payload_storage_bytes = measure_payload.bytes, .shape_index_slots = shape_index_slots, .shape_index_offset = shape_index.start, .shape_entries_offset = shape_entries.start, .shape_payload_offset = shape_payload.start, .shape_payload_storage_bytes = shape_payload.bytes, .storage_bytes = shape_payload.end, }; } }; pub const EpochStatus = struct { entry_capacity: usize, payload_capacity_bytes: usize, physical_payload_capacity_bytes: usize, entries: usize, payload_bytes: usize, physical_payload_bytes: usize, high_water_entries: usize, high_water_payload_bytes: usize, high_water_physical_payload_bytes: usize, rollovers: u64, disabled_bypasses: u64, oversize_bypasses: u64, }; pub const Status = struct { phase: alloc_phase.capacity.Phase, capacity: Capacity, storage_bytes: usize, measure: EpochStatus, shape: EpochStatus, }; pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "gui.text_atlas_cache_storage", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "measure_cache_fixed_index_and_dense_entries", .lifetime = .steady, .detail = "measure cache fixed index and dense entries", }, .{ .id = "measure_cache_aligned_payload_bytes", .lifetime = .steady, .detail = "measure cache aligned payload bytes", }, .{ .id = "shape_cache_fixed_index_and_dense_entries", .lifetime = .steady, .detail = "shape cache fixed index and dense entries", }, .{ .id = "shape_cache_aligned_payload_bytes", .lifetime = .steady, .detail = "shape cache aligned payload bytes", }, }, .excluded = &.{ "outline shaping context, output, and foreign backend storage", "bitmap shaping, line-break, and composite workspaces", "font bytes, glyph atlas, image, glyph index, and bitmap map", "caller-owned text, returned data, and Renderer storage", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "measure_entries", "measure_entries"), alloc_phase.capacity.bindInput(Limits, "shape_entries", "shape_entries"), alloc_phase.capacity.bindInput(Limits, "measure_payload_bytes", "measure_payload_bytes"), alloc_phase.capacity.bindInput(Limits, "shape_payload_bytes", "shape_payload_bytes"), }, .type_selectors = &.{ alloc_phase.capacity.bindType(u32, "u32"), alloc_phase.capacity.bindType(MeasureCacheEntry, "measureentry"), alloc_phase.capacity.bindType(ShapeCacheEntry, "shapeentry"), }, .nodes = &.{ .{ .input = 0 }, .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 2 } } }, .{ .next_power_of_two = 1 }, .{ .input = 1 }, .{ .scale = .{ .node = 3, .coefficient = .{ .literal = 2 } } }, .{ .next_power_of_two = 4 }, .{ .scale = .{ .node = 2, .coefficient = .{ .size_of_concrete_type = 0 } } }, .{ .alignment = .{ .node = 6, .alignment = .{ .literal = 16 } } }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 1 } } }, .{ .alignment = .{ .node = 8, .alignment = .{ .literal = 16 } } }, .{ .input = 2 }, .{ .alignment = .{ .node = 10, .alignment = .{ .literal = 16 } } }, .{ .scale = .{ .node = 5, .coefficient = .{ .size_of_concrete_type = 0 } } }, .{ .alignment = .{ .node = 12, .alignment = .{ .literal = 16 } } }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 2 } } }, .{ .alignment = .{ .node = 14, .alignment = .{ .literal = 16 } } }, .{ .input = 3 }, .{ .alignment = .{ .node = 16, .alignment = .{ .literal = 16 } } }, .{ .add = .{ .left = 7, .right = 9 } }, .{ .add = .{ .left = 18, .right = 11 } }, .{ .add = .{ .left = 19, .right = 13 } }, .{ .add = .{ .left = 20, .right = 15 } }, .{ .add = .{ .left = 21, .right = 17 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 22, }}, }, .overload = .{ .kind = .replace, .detail = "aggregate entry or payload exhaustion replaces one epoch; disabled and individually oversized records bypass before mutation", }, .risks = .{ .transitive = .{ .status = .excluded, .detail = "uncached Atlas shaping and measurement can allocate in backend and workspace owners outside this cache-only claim", }, .foreign = .{ .status = .excluded, .detail = "cache lookup, copying, and replacement cross no foreign boundary; outline shaping remains excluded", }, }, .obligations = &.{ .{ .key = "gui_text_atlas_cache_capacity", .role = .capacity_model }, .{ .key = "gui_text_atlas_cache_acquisition", .role = .custom }, .{ .key = "gui_text_atlas_cache_oom", .role = .custom }, .{ .key = "gui_text_atlas_cache_boundaries", .role = .overload }, .{ .key = "gui_text_atlas_cache_sealed_transitive_risk", .role = .transitive_risk }, .{ .key = "gui_text_atlas_cache_sealed_foreign_risk", .role = .foreign_risk }, .{ .key = "gui_text_atlas_cache_collisions", .role = .custom }, .{ .key = "gui_text_atlas_cache_stability", .role = .custom }, .{ .key = "gui_text_atlas_cache_differential", .role = .overload }, .{ .key = "gui_text_atlas_cache_root", .role = .custom }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, }, }; pub fn init(allocator: Allocator, limits: Limits) (Allocator.Error || DeriveError)!AtlasCacheStorage { const capacity = try Capacity.derive(limits); const bytes = try allocator.alignedAlloc( u8, .fromByteUnits(storage_alignment), capacity.storage_bytes, ); return .{ .phase = .initialization, .capacity = capacity, .bytes = bytes, .measure_slots = cacheTypedSlice( u32, bytes, capacity.measure_index_offset, capacity.measure_index_slots, ), .measure_entries = cacheTypedSlice( MeasureCacheEntry, bytes, capacity.measure_entries_offset, limits.measure_entries, ), .measure_payload = .{ .bytes = bytes[capacity.measure_payload_offset..][0..capacity.measure_payload_storage_bytes] }, .shape_slots = cacheTypedSlice( u32, bytes, capacity.shape_index_offset, capacity.shape_index_slots, ), .shape_entries = cacheTypedSlice( ShapeCacheEntry, bytes, capacity.shape_entries_offset, limits.shape_entries, ), .shape_payload = .{ .bytes = bytes[capacity.shape_payload_offset..][0..capacity.shape_payload_storage_bytes] }, }; } pub fn activate(self: *AtlasCacheStorage) void { self.assertStorage(); std.debug.assert(self.phase == .initialization); @memset(self.measure_slots, 0); @memset(self.shape_slots, 0); self.phase = .steady; self.assertStorage(); } pub fn deinit(self: *AtlasCacheStorage, allocator: Allocator) void { self.assertStorage(); std.debug.assert(self.phase != .teardown); self.phase = .teardown; allocator.free(self.bytes); self.bytes = &.{}; self.measure_slots = &.{}; self.measure_entries = &.{}; self.measure_payload = .{ .bytes = &.{} }; self.shape_slots = &.{}; self.shape_entries = &.{}; self.shape_payload = .{ .bytes = &.{} }; } pub fn status(self: *const AtlasCacheStorage) Status { self.assertStorage(); return .{ .phase = self.phase, .capacity = self.capacity, .storage_bytes = self.capacity.storage_bytes, .measure = cacheEpochStatus( self.capacity.limits.measure_entries, self.capacity.limits.measure_payload_bytes, self.capacity.measure_payload_storage_bytes, self.measure_payload.cursor, self.measure_usage, ), .shape = cacheEpochStatus( self.capacity.limits.shape_entries, self.capacity.limits.shape_payload_bytes, self.capacity.shape_payload_storage_bytes, self.shape_payload.cursor, self.shape_usage, ), }; } fn lookupMeasure(self: *const AtlasCacheStorage, key: MeasureKey) ?Size { self.assertStorage(); std.debug.assert(self.phase == .steady); if (self.measure_slots.len == 0) return null; const hash = MeasureContext.hash(.{}, key); var slot = cacheStartSlot(hash, self.measure_slots.len); var probes: usize = 0; while (probes < self.measure_slots.len) : (probes += 1) { const encoded = self.measure_slots[slot]; if (encoded == 0) return null; const entry = self.measure_entries[encoded - 1]; if (entry.hash == hash and MeasureContext.eql(.{}, entry.key, key)) { return entry.value; } slot = cacheNextSlot(slot, self.measure_slots.len); } unreachable; } fn lookupShape(self: *const AtlasCacheStorage, content: []const u8) ?filigree.GlyphRun { self.assertStorage(); std.debug.assert(self.phase == .steady); if (self.shape_slots.len == 0) return null; const hash = std.hash_map.hashString(content); var slot = cacheStartSlot(hash, self.shape_slots.len); var probes: usize = 0; while (probes < self.shape_slots.len) : (probes += 1) { const encoded = self.shape_slots[slot]; if (encoded == 0) return null; const entry = self.shape_entries[encoded - 1]; if (entry.hash == hash and std.mem.eql(u8, entry.content, content)) { return entry.shaped.run(); } slot = cacheNextSlot(slot, self.shape_slots.len); } unreachable; } fn storeMeasure( self: *AtlasCacheStorage, key: MeasureKey, value: Size, ) Exhaustion!Size { self.assertStorage(); defer self.assertStorage(); std.debug.assert(self.phase == .steady); std.debug.assert(self.lookupMeasure(key) == null); const payload_bytes = measurePayloadBytes(key) orelse { self.measure_usage.oversize_bypasses +|= 1; return error.EntryTooLarge; }; try self.prepareMeasure(payload_bytes); const hash = MeasureContext.hash(.{}, key); const slot = cacheEmptySlot(self.measure_slots, hash); const entry_index = self.measure_usage.entries; var owned_key = key; owned_key.content = self.measure_payload.dupe(u8, key.content); owned_key.styles = self.measure_payload.dupe(UiTextStyle, key.styles); owned_key.runs = self.measure_payload.dupe(UiTextRun, key.runs); owned_key.atlas_entries = self.measure_payload.dupe(AtlasSet.Entry, key.atlas_entries); owned_key.fallback_entries = self.measure_payload.dupe(AtlasSet.Entry, key.fallback_entries); self.measure_entries[entry_index] = .{ .hash = hash, .key = owned_key, .value = value }; self.measure_slots[slot] = @intCast(entry_index + 1); self.measure_usage.inserted(payload_bytes, self.measure_payload.cursor); return value; } fn storeShape( self: *AtlasCacheStorage, content: []const u8, fresh: filigree.GlyphRun, ) Exhaustion!filigree.GlyphRun { self.assertStorage(); defer self.assertStorage(); std.debug.assert(self.phase == .steady); std.debug.assert(self.lookupShape(content) == null); const payload_bytes = shapePayloadBytes(content, fresh) orelse { self.shape_usage.oversize_bypasses +|= 1; return error.EntryTooLarge; }; try self.prepareShape(payload_bytes); const hash = std.hash_map.hashString(content); const slot = cacheEmptySlot(self.shape_slots, hash); const entry_index = self.shape_usage.entries; const owned_content = self.shape_payload.dupe(u8, content); const shaped = ShapedRun{ .glyphs = self.shape_payload.dupe(filigree.ShapedGlyph, fresh.glyphs), .clusters = self.shape_payload.dupe(filigree.Cluster, fresh.clusters), .ligature_carets = self.shape_payload.dupe( filigree.LigatureCaret, fresh.ligature_carets, ), .total_x_advance = fresh.total_x_advance, .total_y_advance = fresh.total_y_advance, .direction = fresh.direction, .writing_mode = fresh.writing_mode, .output_order = fresh.output_order, }; self.shape_entries[entry_index] = .{ .hash = hash, .content = owned_content, .shaped = shaped, }; self.shape_slots[slot] = @intCast(entry_index + 1); self.shape_usage.inserted(payload_bytes, self.shape_payload.cursor); return shaped.run(); } pub fn admitShapePayload( self: *const AtlasCacheStorage, payload_bytes: usize, ) Exhaustion!void { self.assertStorage(); std.debug.assert(self.phase == .steady); const limits = self.capacity.limits; if (limits.shape_entries == 0) return error.CacheDisabled; if (payload_bytes > limits.shape_payload_bytes) return error.EntryTooLarge; } fn prepareMeasure(self: *AtlasCacheStorage, payload_bytes: usize) Exhaustion!void { const limits = self.capacity.limits; if (limits.measure_entries == 0) { self.measure_usage.disabled_bypasses +|= 1; return error.CacheDisabled; } if (payload_bytes > limits.measure_payload_bytes) { self.measure_usage.oversize_bypasses +|= 1; return error.EntryTooLarge; } std.debug.assert(self.measure_usage.payload_bytes <= limits.measure_payload_bytes); const payload_full = payload_bytes > limits.measure_payload_bytes - self.measure_usage.payload_bytes; if (self.measure_usage.entries == limits.measure_entries or payload_full) { self.replaceMeasure(); } } fn prepareShape(self: *AtlasCacheStorage, payload_bytes: usize) Exhaustion!void { const limits = self.capacity.limits; self.admitShapePayload(payload_bytes) catch |err| { switch (err) { error.CacheDisabled => self.shape_usage.disabled_bypasses +|= 1, error.EntryTooLarge => self.shape_usage.oversize_bypasses +|= 1, } return err; }; std.debug.assert(self.shape_usage.payload_bytes <= limits.shape_payload_bytes); const payload_full = payload_bytes > limits.shape_payload_bytes - self.shape_usage.payload_bytes; if (self.shape_usage.entries == limits.shape_entries or payload_full) { self.replaceShape(); } } fn replaceMeasure(self: *AtlasCacheStorage) void { self.assertStorage(); defer self.assertStorage(); @memset(self.measure_slots, 0); self.measure_payload.reset(); self.measure_usage.replaced(); } fn replaceShape(self: *AtlasCacheStorage) void { self.assertStorage(); defer self.assertStorage(); @memset(self.shape_slots, 0); self.shape_payload.reset(); self.shape_usage.replaced(); } fn assertStorage(self: *const AtlasCacheStorage) void { const limits = self.capacity.limits; std.debug.assert(self.bytes.len == self.capacity.storage_bytes); std.debug.assert(self.measure_slots.len == self.capacity.measure_index_slots); std.debug.assert(self.measure_entries.len == limits.measure_entries); std.debug.assert( self.measure_payload.bytes.len == self.capacity.measure_payload_storage_bytes, ); std.debug.assert(self.shape_slots.len == self.capacity.shape_index_slots); std.debug.assert(self.shape_entries.len == limits.shape_entries); std.debug.assert( self.shape_payload.bytes.len == self.capacity.shape_payload_storage_bytes, ); const base = @intFromPtr(self.bytes.ptr); assertCacheAddress(base, self.capacity.measure_index_offset, self.measure_slots); assertCacheAddress(base, self.capacity.measure_entries_offset, self.measure_entries); assertCacheAddress(base, self.capacity.measure_payload_offset, self.measure_payload.bytes); assertCacheAddress(base, self.capacity.shape_index_offset, self.shape_slots); assertCacheAddress(base, self.capacity.shape_entries_offset, self.shape_entries); assertCacheAddress(base, self.capacity.shape_payload_offset, self.shape_payload.bytes); std.debug.assert(self.measure_usage.entries <= limits.measure_entries); std.debug.assert(self.measure_usage.payload_bytes <= limits.measure_payload_bytes); std.debug.assert(self.measure_payload.cursor <= self.measure_payload.bytes.len); std.debug.assert(self.shape_usage.entries <= limits.shape_entries); std.debug.assert(self.shape_usage.payload_bytes <= limits.shape_payload_bytes); std.debug.assert(self.shape_payload.cursor <= self.shape_payload.bytes.len); }};const measure_payload_alignment: usize = @max( @alignOf(UiTextStyle), @max(@alignOf(UiTextRun), @alignOf(AtlasSet.Entry)),);const shape_payload_alignment: usize = @max( @alignOf(filigree.ShapedGlyph), @max(@alignOf(filigree.Cluster), @alignOf(filigree.LigatureCaret)),);const measure_padding_per_entry: usize = @alignOf(UiTextStyle) - 1 + @alignOf(UiTextRun) - 1 + @alignOf(AtlasSet.Entry) - 1;const shape_padding_per_entry: usize = @alignOf(filigree.ShapedGlyph) - 1 + @alignOf(filigree.Cluster) - 1 + @alignOf(filigree.LigatureCaret) - 1;const CacheRegion = struct { start: usize, bytes: usize, end: usize,};fn cacheLimitsValid(limits: AtlasCacheStorage.Limits) AtlasCacheStorage.DeriveError!void { const measure_disabled = limits.measure_entries == 0 and limits.measure_payload_bytes == 0; const measure_enabled = limits.measure_entries != 0 and limits.measure_payload_bytes != 0; if (!measure_disabled and !measure_enabled) return error.InvalidMeasureLimits; const shape_disabled = limits.shape_entries == 0 and limits.shape_payload_bytes == 0; const shape_enabled = limits.shape_entries != 0 and limits.shape_payload_bytes != 0; if (!shape_disabled and !shape_enabled) return error.InvalidShapeLimits; if (limits.measure_entries > std.math.maxInt(u32)) return error.EntryLimitTooLarge; if (limits.shape_entries > std.math.maxInt(u32)) return error.EntryLimitTooLarge;}fn cacheIndexSlots(entries: usize) AtlasCacheStorage.DeriveError!usize { if (entries == 0) return 0; const doubled = try cacheMultiplied(entries, 2); return std.math.ceilPowerOfTwo(usize, doubled) catch error.CapacityOverflow;}fn cacheAdded(left: usize, right: usize) AtlasCacheStorage.DeriveError!usize { return std.math.add(usize, left, right) catch error.CapacityOverflow;}fn cacheMultiplied(left: usize, right: usize) AtlasCacheStorage.DeriveError!usize { return std.math.mul(usize, left, right) catch error.CapacityOverflow;}fn cacheAligned(offset: usize, alignment: usize) AtlasCacheStorage.DeriveError!usize { const mask = alignment - 1; return (try cacheAdded(offset, mask)) & ~mask;}fn cachePlaced(comptime T: type, offset: usize, count: usize) AtlasCacheStorage.DeriveError!CacheRegion { return cachePlacedBytes(offset, @alignOf(T), try cacheMultiplied(count, @sizeOf(T)));}fn cachePlacedBytes( offset: usize, alignment: usize, byte_count: usize,) AtlasCacheStorage.DeriveError!CacheRegion { const start = try cacheAligned(offset, alignment); return .{ .start = start, .bytes = byte_count, .end = try cacheAdded(start, byte_count) };}fn cacheTypedSlice( comptime T: type, bytes: []align(AtlasCacheStorage.storage_alignment) u8, offset: usize, count: usize,) []T { if (count == 0) return &.{}; const byte_count = count * @sizeOf(T); const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]); return std.mem.bytesAsSlice(T, region);}fn assertCacheAddress(base: usize, offset: usize, region: anytype) void { if (region.len == 0) return; std.debug.assert(@intFromPtr(region.ptr) == base + offset);}fn cacheStartSlot(hash: u64, slot_count: usize) usize { std.debug.assert(std.math.isPowerOfTwo(slot_count)); const truncated: usize = @truncate(hash); return truncated & (slot_count - 1);}fn cacheNextSlot(slot: usize, slot_count: usize) usize { std.debug.assert(std.math.isPowerOfTwo(slot_count)); return (slot + 1) & (slot_count - 1);}fn cacheEmptySlot(slots: []const u32, hash: u64) usize { var slot = cacheStartSlot(hash, slots.len); var probes: usize = 0; while (probes < slots.len) : (probes += 1) { if (slots[slot] == 0) return slot; slot = cacheNextSlot(slot, slots.len); } unreachable;}fn cacheEpochStatus( entry_capacity: usize, payload_capacity_bytes: usize, physical_payload_capacity_bytes: usize, physical_payload_bytes: usize, usage: CacheEpochUsage,) AtlasCacheStorage.EpochStatus { return .{ .entry_capacity = entry_capacity, .payload_capacity_bytes = payload_capacity_bytes, .physical_payload_capacity_bytes = physical_payload_capacity_bytes, .entries = usage.entries, .payload_bytes = usage.payload_bytes, .physical_payload_bytes = physical_payload_bytes, .high_water_entries = usage.high_water_entries, .high_water_payload_bytes = usage.high_water_payload_bytes, .high_water_physical_payload_bytes = usage.high_water_physical_payload_bytes, .rollovers = usage.rollovers, .disabled_bypasses = usage.disabled_bypasses, .oversize_bypasses = usage.oversize_bypasses, };}const CacheModelRegion = struct { start: u128, bytes: u128, end: u128,};const CacheModelLayout = struct { measure_slots: u128, measure_index: CacheModelRegion, measure_entries: CacheModelRegion, measure_payload: CacheModelRegion, shape_slots: u128, shape_index: CacheModelRegion, shape_entries: CacheModelRegion, shape_payload: CacheModelRegion, fn capacity( self: CacheModelLayout, limits: AtlasCacheStorage.Limits, ) AtlasCacheStorage.DeriveError!AtlasCacheStorage.Capacity { const values = [_]u128{ self.measure_slots, self.measure_index.start, self.measure_entries.start, self.measure_payload.start, self.measure_payload.bytes, self.shape_slots, self.shape_index.start, self.shape_entries.start, self.shape_payload.start, self.shape_payload.bytes, self.shape_payload.end, }; for (values) |value| { if (value > std.math.maxInt(usize)) return error.CapacityOverflow; } return .{ .limits = limits, .measure_index_slots = @intCast(self.measure_slots), .measure_index_offset = @intCast(self.measure_index.start), .measure_entries_offset = @intCast(self.measure_entries.start), .measure_payload_offset = @intCast(self.measure_payload.start), .measure_payload_storage_bytes = @intCast(self.measure_payload.bytes), .shape_index_slots = @intCast(self.shape_slots), .shape_index_offset = @intCast(self.shape_index.start), .shape_entries_offset = @intCast(self.shape_entries.start), .shape_payload_offset = @intCast(self.shape_payload.start), .shape_payload_storage_bytes = @intCast(self.shape_payload.bytes), .storage_bytes = @intCast(self.shape_payload.end), }; }};fn modelAtlasCacheLimitsValid( limits: AtlasCacheStorage.Limits,) AtlasCacheStorage.DeriveError!void { const measure_disabled = limits.measure_entries == 0 and limits.measure_payload_bytes == 0; const measure_enabled = limits.measure_entries != 0 and limits.measure_payload_bytes != 0; if (!measure_disabled and !measure_enabled) return error.InvalidMeasureLimits; const shape_disabled = limits.shape_entries == 0 and limits.shape_payload_bytes == 0; const shape_enabled = limits.shape_entries != 0 and limits.shape_payload_bytes != 0; if (!shape_disabled and !shape_enabled) return error.InvalidShapeLimits; if (limits.measure_entries > std.math.maxInt(u32)) return error.EntryLimitTooLarge; if (limits.shape_entries > std.math.maxInt(u32)) return error.EntryLimitTooLarge;}fn modelAtlasCacheCapacity( limits: AtlasCacheStorage.Limits,) AtlasCacheStorage.DeriveError!AtlasCacheStorage.Capacity { try modelAtlasCacheLimitsValid(limits); const measure_slots = modelCacheIndexSlots(limits.measure_entries); const shape_slots = modelCacheIndexSlots(limits.shape_entries); const measure_index = modelCachePlaced(0, @alignOf(u32), measure_slots, @sizeOf(u32)); const measure_entries = modelCachePlaced( measure_index.end, @alignOf(MeasureCacheEntry), limits.measure_entries, @sizeOf(MeasureCacheEntry), ); const measure_payload_bytes = @as(u128, limits.measure_payload_bytes) + @as(u128, limits.measure_entries) * measure_padding_per_entry; const measure_payload = modelCachePlaced( measure_entries.end, measure_payload_alignment, measure_payload_bytes, 1, ); const shape_index = modelCachePlaced( measure_payload.end, @alignOf(u32), shape_slots, @sizeOf(u32), ); const shape_entries = modelCachePlaced( shape_index.end, @alignOf(ShapeCacheEntry), limits.shape_entries, @sizeOf(ShapeCacheEntry), ); const shape_payload_bytes = @as(u128, limits.shape_payload_bytes) + @as(u128, limits.shape_entries) * shape_padding_per_entry; const shape_payload = modelCachePlaced( shape_entries.end, shape_payload_alignment, shape_payload_bytes, 1, ); return (CacheModelLayout{ .measure_slots = measure_slots, .measure_index = measure_index, .measure_entries = measure_entries, .measure_payload = measure_payload, .shape_slots = shape_slots, .shape_index = shape_index, .shape_entries = shape_entries, .shape_payload = shape_payload, }).capacity(limits);}fn modelCacheIndexSlots(entries: usize) u128 { if (entries == 0) return 0; const minimum = @as(u128, entries) * 2; var slots: u128 = 1; var steps: usize = 0; while (steps < 64 and slots < minimum) : (steps += 1) slots *= 2; std.debug.assert(slots >= minimum); return slots;}fn modelCachePlaced(offset: u128, alignment: usize, count: u128, size: usize) CacheModelRegion { const mask = @as(u128, alignment - 1); const start = (offset + mask) & ~mask; const bytes = count * size; return .{ .start = start, .bytes = bytes, .end = start + bytes };}comptime { alloc_phase.capacity.requireAllocatorRejectingOwnerShape(AtlasCacheStorage);}fn shapePayloadBytes(content: []const u8, run: filigree.GlyphRun) ?usize { var bytes = content.len; inline for (.{ .{ filigree.ShapedGlyph, run.glyphs.len }, .{ filigree.Cluster, run.clusters.len }, .{ filigree.LigatureCaret, run.ligature_carets.len }, }) |slice| { const slice_bytes = std.math.mul(usize, @sizeOf(slice[0]), slice[1]) catch return null; bytes = std.math.add(usize, bytes, slice_bytes) catch return null; } return bytes;}fn floatBits(value: f64) u64 { return @bitCast(value);}pub const AtlasSet = struct { entries: []const Entry = &.{}, fallback_entries: []const Entry = &.{}, fallback: ?*fallback_mod.Engine = null, device_scale: f32 = 1, pub const Entry = struct { face: u64, image_index: u32, atlas: *Atlas, }; pub fn forText(self: *const AtlasSet, text: UiText) ?Entry { return self.forStyle(gui.model.resolvedTextStyle(text, 0)); } pub fn forStyle(self: *const AtlasSet, style: UiTextStyle) ?Entry { if (self.entries.len == 0) return null; const point: f32 = @floatCast(style.point_size); const scaled_point = point * @max(self.device_scale, 1); var best: ?Entry = null; var best_face = false; var best_cost: f32 = std.math.floatMax(f32); for (self.entries) |entry| { const face_match = entry.face == style.font_asset_id; if (best_face and !face_match) continue; const cost = sizeCost(entry.atlas.pixel_size, scaled_point); if (face_match and !best_face) { best = entry; best_face = true; best_cost = cost; continue; } if (cost < best_cost) { best = entry; best_cost = cost; } } return best; } fn fallbackFor(self: *const AtlasSet, primary: Entry) ?Entry { if (self.fallback == null or primary.atlas.outlineFont() == null) return null; var best: ?Entry = null; var best_cost: u64 = std.math.maxInt(u64); for (self.fallback_entries) |entry| { if (entry.atlas.outlineFont() == null) continue; const cost = @abs(@as(i64, entry.atlas.pixel_size) - primary.atlas.pixel_size); if (cost < best_cost) { best = entry; best_cost = cost; } } return best; } fn requiresFallback(self: *const AtlasSet, primary: Entry, content: []const u8) !bool { if (self.fallbackFor(primary) == null) return false; return !(try primary.atlas.coversUtf8(content)); } fn faceSegments(self: *const AtlasSet, primary: Entry, content: []const u8) !?FaceSegments { const fallback_entry = self.fallbackFor(primary) orelse return null; if (try primary.atlas.coversUtf8(content)) return null; const primary_font = primary.atlas.outlineFont().?; const fallback_font = fallback_entry.atlas.outlineFont().?; return .{ .fallback_entry = fallback_entry, .items = try self.fallback.?.segments(primary_font, fallback_font, content), }; } fn sizeCost(pixel_size: i32, point: f32) f32 { const size: f32 = @floatFromInt(pixel_size); if (size >= point) return size - point; return (point - size) * 4; }};const FaceSegments = struct { fallback_entry: AtlasSet.Entry, items: []const fallback_mod.FaceSegment,};fn requestedPointSize(text: UiText) f32 { if (std.math.isFinite(text.point_size) and text.point_size > 0) return @floatCast(text.point_size); return 16;}pub fn frameResolvers(atlases: *const AtlasSet) gui.frame.FrameResolvers { return .{ .context = @constCast(atlases), .text_size = measureFrameText, };}pub fn measureFrameText(context: ?*anyopaque, node: *const UiNode) anyerror!?Size { const text = node.text orelse return null; if (text.content.len == 0) return null; const atlases: *const AtlasSet = @ptrCast(@alignCast(context orelse return null)); return try measureSet(atlases, text);}pub fn measure(atlas: *Atlas, text: UiText) !Size { try gui.model.validateText(text); if (hasMixedTextMetrics(text)) return error.MixedTextRequiresAtlasSet; return measureCached(atlas, text, .single);}pub fn measureSet(atlases: *const AtlasSet, text: UiText) !Size { try gui.model.validateText(text); const entry = atlases.forText(text) orelse return error.MissingTextAtlas; const composite = hasMixedTextMetrics(text) or try atlases.requiresFallback(entry, text.content); return measureCached( entry.atlas, text, if (composite) .{ .composite = atlases } else .single, );}pub fn textAdvanceForByteOffset( atlases: *const AtlasSet, text: UiText, byte_offset: usize,) !f32 { const line = try textCaretPlan(atlases, text); return filigree.caret.advanceForByteOffset( line.plan.run, byte_offset, text.content.len, ) * line.scale;}pub fn textHitTestAdvance( atlases: *const AtlasSet, text: UiText, advance: f32,) !filigree.caret.LineHit { const line = try textCaretPlan(atlases, text); var hit = filigree.caret.hitTestAdvance( line.plan.run, advance / line.scale, text.content, ); hit.advance *= line.scale; return hit;}pub const TextCaretAffinity = enum { upstream, downstream,};pub const TextCaretGeometry = struct { byte_offset: usize, affinity: TextCaretAffinity, line_index: usize, x: f32, y: f32, height: f32,};pub fn textCaretGeometry( atlases: *const AtlasSet, text: UiText, box: Size, byte_offset: usize, affinity: TextCaretAffinity,) !TextCaretGeometry { const layout = try TextLayout.init(atlases, text, .{ .width = box.width, .height = box.height, }, 1); const target = @min(byte_offset, text.content.len); var iterator = layout.iterator(); while (try iterator.next()) |line| { if (target < line.hard_line_start) continue; if (!line.visual.containsCaret(target - line.hard_line_start, affinity)) continue; return line.caret(target, affinity); } return error.MissingTextCaret;}pub fn textHitTestPoint( atlases: *const AtlasSet, text: UiText, box: Size, point: gui.model.UiPoint,) !TextCaretGeometry { const layout = try TextLayout.init(atlases, text, .{ .width = box.width, .height = box.height, }, 1); var iterator = layout.iterator(); var candidate: ?TextCaretGeometry = null; while (try iterator.next()) |line| { const hit = line.hit(point.x); if (point.y < line.y + line.height) return hit; candidate = hit; } return candidate orelse error.MissingTextCaret;}pub fn textHitTestWidgetPoint( atlases: *const AtlasSet, widget: WidgetFrame, point: gui.model.UiPoint,) !?TextCaretGeometry { if (!gui.model.pointInRect(widget.rect, point.x, point.y)) return null; if (!gui.model.pointInRect(widget.visible_rect, point.x, point.y)) return null; return try textHitTestWidgetLocalPoint(atlases, widget, point);}pub fn textHitTestWidgetPointClamped( atlases: *const AtlasSet, widget: WidgetFrame, point: gui.model.UiPoint,) !?TextCaretGeometry { if (!std.math.isFinite(point.x) or !std.math.isFinite(point.y)) return null; const left = @max(widget.rect.x, widget.visible_rect.x); const top = @max(widget.rect.y, widget.visible_rect.y); const right = @min( widget.rect.x + widget.rect.width, widget.visible_rect.x + widget.visible_rect.width, ); const bottom = @min( widget.rect.y + widget.rect.height, widget.visible_rect.y + widget.visible_rect.height, ); if (right <= left or bottom <= top) return null; return try textHitTestWidgetLocalPoint(atlases, widget, .{ .x = std.math.clamp(point.x, left, right), .y = std.math.clamp(point.y, top, bottom), });}fn textHitTestWidgetLocalPoint( atlases: *const AtlasSet, widget: WidgetFrame, point: gui.model.UiPoint,) !?TextCaretGeometry { const text = widget.text orelse return null; return try textHitTestPoint( atlases, text, .{ .width = widget.rect.width, .height = widget.rect.height }, .{ .x = point.x - widget.rect.x, .y = point.y - widget.rect.y, }, );}const TextCaretPlan = struct { plan: LinePlan, scale: f32,};fn textCaretPlan(atlases: *const AtlasSet, text: UiText) !TextCaretPlan { try gui.model.validateText(text); if (std.mem.indexOfScalar(u8, text.content, '\n') != null) return error.MultilineText; const entry = atlases.forText(text) orelse return error.MissingTextAtlas; const scale = textScale(entry.atlas, text); const composite = hasMixedTextMetrics(text) or try atlases.requiresFallback(entry, text.content); const plan = if (composite) try LinePlan.initComposite( entry.atlas, atlases, text, text.content, 0, 0, ) else try LinePlan.init(entry.atlas, text.content, 0); return .{ .plan = plan, .scale = scale };}const MeasureMode = union(enum) { single, composite: *const AtlasSet,};fn measureCached(atlas: *Atlas, text: UiText, mode: MeasureMode) !Size { const metric_atlases: ?*const AtlasSet = switch (mode) { .single => null, .composite => |atlases| atlases, }; const key = MeasureKey.fromText(text, metric_atlases); if (atlas.cache.lookupMeasure(key)) |cached| return cached; const measured = switch (mode) { .single => try measureUncached(atlas, text), .composite => |atlases| try measureCompositeUncached(atlas, atlases, text), }; return atlas.cache.storeMeasure(key, measured) catch measured;}fn measurePayloadBytes(key: MeasureKey) ?usize { var bytes = key.content.len; bytes = std.math.add(usize, bytes, std.math.mul(usize, key.styles.len, @sizeOf(UiTextStyle)) catch return null) catch return null; bytes = std.math.add(usize, bytes, std.math.mul(usize, key.runs.len, @sizeOf(UiTextRun)) catch return null) catch return null; bytes = std.math.add(usize, bytes, std.math.mul(usize, key.atlas_entries.len, @sizeOf(AtlasSet.Entry)) catch return null) catch return null; bytes = std.math.add(usize, bytes, std.math.mul(usize, key.fallback_entries.len, @sizeOf(AtlasSet.Entry)) catch return null) catch return null; return bytes;}fn hasMixedTextMetrics(text: UiText) bool { const base = gui.model.resolvedTextStyle(text, 0); for (text.runs) |run| { if (!gui.model.textStylesEqual(base, gui.model.resolvedTextStyle(text, run.style_slot))) return true; } return false;}const VisualLine = struct { glyph_start: usize, glyph_end: usize, byte_start: usize, byte_end: usize, advance_start: f32, advance: f32, is_last: bool, fn glyphs(self: VisualLine, run: filigree.GlyphRun) []const filigree.ShapedGlyph { return run.glyphs[self.glyph_start..self.glyph_end]; } fn advanceForByteOffset(self: VisualLine, run: filigree.GlyphRun, byte_offset: usize, text_len: usize) f32 { const absolute = filigree.caret.advanceForByteOffset(run, byte_offset, text_len); return std.math.clamp(absolute - self.advance_start, @as(f32, 0), self.advance); } fn containsCaret( self: VisualLine, byte_offset: usize, affinity: TextCaretAffinity, ) bool { if (byte_offset < self.byte_start) return false; if (byte_offset < self.byte_end) return true; if (byte_offset != self.byte_end) return false; return self.is_last or affinity == .upstream; }};const LinePlan = struct { content: []const u8, run: filigree.GlyphRun, glyph_ends: []const u32, wrapped: bool, glyph_sources: []const CompositeGlyphSource = &.{}, metric_spans: []const CompositeMetricSpan = &.{}, base_metric: ?CompositeMetricSpan = null, fn init(atlas: *Atlas, content: []const u8, wrap_width: f32) !LinePlan { const run = try atlas.shape(content); if (wrap_width <= 0) { return .{ .content = content, .run = run, .glyph_ends = &.{}, .wrapped = false, }; } const glyph_ends = try filigree.breakLinesInto( atlas.allocator, run.glyphs, .{ .utf8 = content }, wrap_width, &atlas.line_break_workspace, ); std.debug.assert(glyph_ends.len > 0); std.debug.assert(glyph_ends.len <= run.glyphs.len + 1); return .{ .content = content, .run = run, .glyph_ends = glyph_ends, .wrapped = true, }; } fn initComposite( base_atlas: *Atlas, atlases: *const AtlasSet, text: UiText, content: []const u8, content_start: usize, wrap_width: f32, ) !LinePlan { const content_end = std.math.add(usize, content_start, content.len) catch return error.SourceTooLong; if (content_end > text.content.len) return error.SourceTooLong; if (content.len > std.math.maxInt(u32)) return error.SourceTooLong; const base_style = gui.model.resolvedTextStyle(text, 0); const base_entry = atlases.forStyle(base_style) orelse return error.MissingTextAtlas; const base_scale = textStyleScale(base_entry.atlas, base_style); if (!(base_scale > 0)) return error.InvalidTextScale; const base_metric = compositeMetricSpan( base_entry, base_style, base_scale, 0, @intCast(content.len), ); const workspace = &base_atlas.composite_workspace; workspace.clear(); errdefer workspace.clear(); var cursor = MetricSegmentCursor{ .text = text, .position = content_start, .end = content_end, }; while (cursor.next()) |segment| { const entry = atlases.forStyle(segment.style) orelse return error.MissingTextAtlas; const local_start = segment.byte_start - content_start; const local_end = segment.byte_end - content_start; if (local_start > std.math.maxInt(u32) or local_end > std.math.maxInt(u32)) { return error.SourceTooLong; } try appendMetricSegment( workspace, base_atlas.allocator, atlases, entry, segment.style, base_scale, content[local_start..local_end], local_start, ); } var total_x: i32 = 0; var total_y: i32 = 0; for (workspace.glyphs.items) |glyph| { total_x = std.math.add(i32, total_x, glyph.x_advance) catch return error.TextGeometryOverflow; total_y = std.math.add(i32, total_y, glyph.y_advance) catch return error.TextGeometryOverflow; } const run = filigree.GlyphRun{ .glyphs = workspace.glyphs.items, .clusters = workspace.clusters.items, .ligature_carets = workspace.ligature_carets.items, .total_x_advance = total_x, .total_y_advance = total_y, .direction = .ltr, .writing_mode = .horizontal, .output_order = .visual, }; const glyph_ends = if (wrap_width > 0) try filigree.breakLinesInto( base_atlas.allocator, run.glyphs, .{ .utf8 = content }, wrap_width, &base_atlas.line_break_workspace, ) else &.{}; if (wrap_width > 0) { std.debug.assert(glyph_ends.len > 0); std.debug.assert(glyph_ends.len <= run.glyphs.len + 1); } return .{ .content = content, .run = run, .glyph_ends = glyph_ends, .wrapped = wrap_width > 0, .glyph_sources = workspace.glyph_sources.items, .metric_spans = workspace.spans.items, .base_metric = base_metric, }; } fn iterator(_: *const LinePlan) Iterator { return .{}; } fn compositeLineBox(self: *const LinePlan, visual: VisualLine, multiplier: f32) CompositeLineBox { var ascent: f32 = 0; var descent: f32 = 0; var requested: f32 = 0; for (self.metric_spans) |span| { if (span.byte_end <= visual.byte_start or span.byte_start >= visual.byte_end) continue; ascent = @max(ascent, span.ascent); descent = @max(descent, span.descent); requested = @max(requested, span.point_size * multiplier); } if (ascent == 0 and descent == 0) { const base = self.base_metric.?; ascent = base.ascent; descent = base.descent; requested = base.point_size * multiplier; } const font_height = ascent + descent; const height = @max(requested, font_height); return .{ .ascent = ascent, .descent = descent, .height = height, .half_leading = @max(0, height - font_height) / 2, }; } const Iterator = struct { break_index: usize = 0, glyph_start: usize = 0, advance_start: f32 = 0, fn next(self: *Iterator, plan: *const LinePlan) ?VisualLine { const line_count = if (plan.wrapped) plan.glyph_ends.len else 1; if (self.break_index >= line_count) return null; const raw_end = if (plan.wrapped) plan.glyph_ends[self.break_index] else plan.run.glyphs.len; const glyph_end = @min(@as(usize, @intCast(raw_end)), plan.run.glyphs.len); std.debug.assert(glyph_end >= self.glyph_start); const advance = glyphSliceAdvance(plan.run.glyphs[self.glyph_start..glyph_end]); const byte_start = if (self.glyph_start == 0) 0 else sourceOffset(plan.run.glyphs[self.glyph_start].source_start, plan.content.len); const byte_end = if (glyph_end >= plan.run.glyphs.len) plan.content.len else sourceOffset(plan.run.glyphs[glyph_end].source_start, plan.content.len); const visual = VisualLine{ .glyph_start = self.glyph_start, .glyph_end = glyph_end, .byte_start = byte_start, .byte_end = @max(byte_start, byte_end), .advance_start = self.advance_start, .advance = advance, .is_last = self.break_index + 1 == line_count, }; self.glyph_start = glyph_end; self.advance_start += advance; self.break_index += 1; return visual; } };};const CompositeLineBox = struct { ascent: f32, descent: f32, height: f32, half_leading: f32,};const TextLayout = struct { atlases: *const AtlasSet, text: UiText, rect: Rect, base_atlas: *Atlas, scale: f32, single_line_height: f32, single_half_leading: f32, line_height_multiplier: f32, plan_wrap_width: f32, block_y: f32, composite: bool, fn init( atlases: *const AtlasSet, text: UiText, rect: Rect, unit_scale: f32, ) !TextLayout { try gui.model.validateText(text); if (!std.math.isFinite(unit_scale) or unit_scale <= 0) { return error.InvalidTextScale; } const entry = atlases.forText(text) orelse return error.MissingTextAtlas; const base_atlas = entry.atlas; const scale = textScale(base_atlas, text) * unit_scale; if (!std.math.isFinite(scale) or scale <= 0) return error.InvalidTextScale; const line_height = textLineHeight(base_atlas, text) * unit_scale; const half_leading = @max( 0, line_height - base_atlas.metrics().height() * scale, ) / 2; const wrap_width = textWrapWidth(text) * unit_scale; const measured_height = if (text.vertical_align == .start) 0 else (try measureSet(atlases, text)).height * unit_scale; const composite = hasMixedTextMetrics(text) or try atlases.requiresFallback(entry, text.content); return .{ .atlases = atlases, .text = text, .rect = rect, .base_atlas = base_atlas, .scale = scale, .single_line_height = line_height, .single_half_leading = half_leading, .line_height_multiplier = textLineHeightMultiplier(text), .plan_wrap_width = if (wrap_width > 0) wrap_width / scale else 0, .block_y = if (text.vertical_align == .start) rect.y else alignedStart( rect.y, rect.height, measured_height, text.vertical_align, ), .composite = composite, }; } fn iterator(self: *const TextLayout) Iterator { return .{ .layout = self, .line_y = self.block_y, }; } const Iterator = struct { layout: *const TextLayout, next_hard_line_start: usize = 0, hard_lines_done: bool = false, has_plan: bool = false, current_hard_line_start: usize = 0, current_hard_line: []const u8 = &.{}, current_plan: LinePlan = undefined, visual_iterator: LinePlan.Iterator = .{}, line_index: usize = 0, line_y: f32, fn next(self: *Iterator) !?TextLayoutLine { while (true) { if (self.has_plan) { if (self.visual_iterator.next(&self.current_plan)) |visual| { const box = if (self.layout.composite) self.current_plan.compositeLineBox( visual, self.layout.line_height_multiplier, ) else CompositeLineBox{ .ascent = self.layout.base_atlas.metrics().ascent, .descent = self.layout.base_atlas.metrics().descent, .height = self.layout.single_line_height / self.layout.scale, .half_leading = self.layout.single_half_leading / self.layout.scale, }; const line_height = if (self.layout.composite) box.height * self.layout.scale else self.layout.single_line_height; const line_width = visual.advance * self.layout.scale; const line = TextLayoutLine{ .plan = &self.current_plan, .visual = visual, .hard_line = self.current_hard_line, .hard_line_start = self.current_hard_line_start, .line_index = self.line_index, .origin_x = alignedStart( self.layout.rect.x, self.layout.rect.width, line_width, self.layout.text.horizontal_align, ), .origin_y = self.line_y + box.half_leading * self.layout.scale, .y = self.line_y, .height = line_height, .scale = self.layout.scale, .ascent = box.ascent, .descent = box.descent, }; self.line_y += line_height; self.line_index += 1; return line; } self.has_plan = false; } if (!try self.loadHardLine()) return null; } } fn loadHardLine(self: *Iterator) !bool { if (self.hard_lines_done) return false; const content = self.layout.text.content; const start = self.next_hard_line_start; const relative_end = std.mem.indexOfScalar( u8, content[start..], '\n', ); const end = if (relative_end) |offset| start + offset else content.len; if (end < content.len) { self.next_hard_line_start = end + 1; } else { self.hard_lines_done = true; } const hard_line = content[start..end]; self.current_hard_line_start = start; self.current_hard_line = hard_line; self.current_plan = if (self.layout.composite) try LinePlan.initComposite( self.layout.base_atlas, self.layout.atlases, self.layout.text, hard_line, start, self.layout.plan_wrap_width, ) else try LinePlan.init( self.layout.base_atlas, hard_line, self.layout.plan_wrap_width, ); self.visual_iterator = .{}; self.has_plan = true; return true; } };};const TextLayoutLine = struct { plan: *const LinePlan, visual: VisualLine, hard_line: []const u8, hard_line_start: usize, line_index: usize, origin_x: f32, origin_y: f32, y: f32, height: f32, scale: f32, ascent: f32, descent: f32, fn caret( self: TextLayoutLine, byte_offset: usize, affinity: TextCaretAffinity, ) TextCaretGeometry { const local_offset = byte_offset - self.hard_line_start; return .{ .byte_offset = byte_offset, .affinity = affinity, .line_index = self.line_index, .x = self.origin_x + self.visual.advanceForByteOffset( self.plan.run, local_offset, self.plan.content.len, ) * self.scale, .y = self.y, .height = self.height, }; } fn hit(self: TextLayoutLine, x: f32) TextCaretGeometry { const absolute_advance = self.visual.advance_start + (x - self.origin_x) / self.scale; const raw = filigree.caret.hitTestAdvance( self.plan.run, absolute_advance, self.plan.content, ); const local_offset = std.math.clamp( raw.byte_offset, self.visual.byte_start, self.visual.byte_end, ); const affinity: TextCaretAffinity = if (!self.visual.is_last and local_offset == self.visual.byte_end) .upstream else .downstream; return self.caret(self.hard_line_start + local_offset, affinity); }};const MetricSegment = struct { byte_start: usize, byte_end: usize, style: UiTextStyle,};const MetricSegmentCursor = struct { text: UiText, position: usize, end: usize, fn next(self: *MetricSegmentCursor) ?MetricSegment { if (self.position >= self.end) return null; const byte_start = self.position; const style = textStyleAt(self.text, byte_start); var byte_end = nextTextMetricBoundary(self.text, byte_start, self.end); while (byte_end < self.end and gui.model.textStylesEqual(style, textStyleAt(self.text, byte_end))) { byte_end = nextTextMetricBoundary(self.text, byte_end, self.end); } std.debug.assert(byte_end > byte_start); self.position = byte_end; return .{ .byte_start = byte_start, .byte_end = byte_end, .style = style, }; }};fn textStyleAt(text: UiText, byte_offset: usize) UiTextStyle { const run = findTextRun(text.runs, byte_offset) orelse return gui.model.resolvedTextStyle(text, 0); return gui.model.resolvedTextStyle(text, run.style_slot);}fn nextTextMetricBoundary(text: UiText, byte_offset: usize, end: usize) usize { const index = firstRunEndingAfter(text.runs, byte_offset); if (index >= text.runs.len) return end; const run = text.runs[index]; if (byte_offset < run.byte_start) return @min(end, run.byte_start); return @min(end, run.byte_end);}fn textStyleScale(atlas: *const Atlas, style: UiTextStyle) f32 { if (atlas.pixel_size <= 0) return 1; return @as(f32, @floatCast(style.point_size)) / @as(f32, @floatFromInt(atlas.pixel_size));}fn appendMetricSegment( workspace: *CompositeWorkspace, allocator: Allocator, atlases: *const AtlasSet, primary: AtlasSet.Entry, style: UiTextStyle, base_scale: f32, content: []const u8, content_start: usize,) !void { const segmented = try atlases.faceSegments(primary, content) orelse { return appendStyledFaceSegment( workspace, allocator, primary, style, base_scale, content, content_start, ); }; for (segmented.items) |segment| { const start: usize = @intCast(segment.source.start); const end: usize = @intCast(segment.source.end); if (start > end or end > content.len) return error.SourceTooLong; const entry = try entryForFaceSegment( primary, segmented.fallback_entry, segment.candidate_index, segment.missing_everywhere, ); try appendLookupOrderedFaceSegment( workspace, allocator, primary, entry, style, base_scale, content[start..end], try std.math.add(usize, content_start, start), ); }}fn entryForFaceSegment( primary: AtlasSet.Entry, fallback: AtlasSet.Entry, candidate_index: usize, missing_everywhere: bool,) !AtlasSet.Entry { if (candidate_index == 1) return fallback; if (candidate_index != 0) return error.InvalidFallbackCandidate; return if (missing_everywhere) fallback else primary;}fn appendLookupOrderedFaceSegment( workspace: *CompositeWorkspace, allocator: Allocator, primary: AtlasSet.Entry, selected: AtlasSet.Entry, style: UiTextStyle, base_scale: f32, content: []const u8, content_start: usize,) !void { if (selected.atlas == primary.atlas or content.len == 0) { return appendStyledFaceSegment( workspace, allocator, selected, style, base_scale, content, content_start, ); } const append = OrderedFaceAppend{ .workspace = workspace, .allocator = allocator, .primary = primary, .selected = selected, .style = style, .base_scale = base_scale, .content = content, .content_start = content_start, }; var iterator = try filigree.unicode.SourceIterator.init(.{ .utf8 = content }, 0); var grapheme: filigree.unicode.GraphemeState = .{}; var cluster_start: usize = 0; var cluster_first: u21 = 0; var cluster_count: usize = 0; var runs = OrderedRunState{}; while (try iterator.next()) |scalar| { if (grapheme.consume(scalar.codepoint)) { cluster_count += 1; continue; } if (cluster_count > 0) { try runs.accept(append, cluster_start, cluster_count == 1 and synth.covered(cluster_first)); } cluster_start = @intCast(scalar.source.start); cluster_first = scalar.codepoint; cluster_count = 1; } std.debug.assert(cluster_count > 0); try runs.accept(append, cluster_start, cluster_count == 1 and synth.covered(cluster_first)); return append.run(runs.start, content.len, runs.synthetic.?);}const OrderedFaceAppend = struct { workspace: *CompositeWorkspace, allocator: Allocator, primary: AtlasSet.Entry, selected: AtlasSet.Entry, style: UiTextStyle, base_scale: f32, content: []const u8, content_start: usize, fn run(self: OrderedFaceAppend, start: usize, end: usize, synthetic: bool) !void { std.debug.assert(start < end); std.debug.assert(end <= self.content.len); return appendStyledFaceSegment( self.workspace, self.allocator, if (synthetic) self.primary else self.selected, self.style, self.base_scale, self.content[start..end], std.math.add(usize, self.content_start, start) catch return error.SourceTooLong, ); }};const OrderedRunState = struct { start: usize = 0, synthetic: ?bool = null, fn accept(self: *OrderedRunState, append: OrderedFaceAppend, cluster_start: usize, synthetic: bool) !void { if (self.synthetic) |current| { if (current != synthetic) { try append.run(self.start, cluster_start, current); self.start = cluster_start; } } self.synthetic = synthetic; }};fn appendStyledFaceSegment( workspace: *CompositeWorkspace, allocator: Allocator, entry: AtlasSet.Entry, style: UiTextStyle, base_scale: f32, content: []const u8, content_start: usize,) !void { if (content_start > std.math.maxInt(u32) or content.len > std.math.maxInt(u32) - content_start) { return error.SourceTooLong; } const scale_ratio = textStyleScale(entry.atlas, style) / base_scale; if (!std.math.isFinite(scale_ratio) or scale_ratio <= 0) return error.InvalidTextScale; const span_index = workspace.spans.items.len; if (span_index > std.math.maxInt(u32)) return error.SourceTooLong; const content_end = content_start + content.len; try workspace.spans.append(allocator, compositeMetricSpan( entry, style, base_scale, @intCast(content_start), @intCast(content_end), )); try appendCompositeSegment( workspace, allocator, try entry.atlas.shape(content), @intCast(content_start), @intCast(span_index), scale_ratio, );}fn compositeMetricSpan( entry: AtlasSet.Entry, style: UiTextStyle, base_scale: f32, byte_start: u32, byte_end: u32,) CompositeMetricSpan { const scale_ratio = textStyleScale(entry.atlas, style) / base_scale; const metrics = entry.atlas.metrics(); return .{ .byte_start = byte_start, .byte_end = byte_end, .atlas = entry.atlas, .image_index = entry.image_index, .scale_ratio = scale_ratio, .point_size = @as(f32, @floatCast(style.point_size)) / base_scale, .ascent = metrics.ascent * scale_ratio, .descent = metrics.descent * scale_ratio, };}fn appendCompositeSegment( workspace: *CompositeWorkspace, allocator: Allocator, run: filigree.GlyphRun, source_base: u32, segment_index: u32, scale_ratio: f32,) !void { const glyph_base = workspace.glyphs.items.len; const cluster_base = workspace.clusters.items.len; const caret_base = workspace.ligature_carets.items.len; try workspace.glyphs.ensureUnusedCapacity(allocator, run.glyphs.len); try workspace.clusters.ensureUnusedCapacity(allocator, run.clusters.len); try workspace.ligature_carets.ensureUnusedCapacity(allocator, run.ligature_carets.len); try workspace.glyph_sources.ensureUnusedCapacity(allocator, run.glyphs.len); for (run.ligature_carets) |source| { var caret = source; caret.x_offset = try scaleCompositeValue(caret.x_offset, scale_ratio); workspace.ligature_carets.appendAssumeCapacity(caret); } for (run.clusters) |source| { var cluster = source; cluster.source.start = try addCompositeIndex(cluster.source.start, source_base); cluster.source.end = try addCompositeIndex(cluster.source.end, source_base); cluster.glyphs.start = try addCompositeIndex(cluster.glyphs.start, glyph_base); cluster.glyphs.end = try addCompositeIndex(cluster.glyphs.end, glyph_base); workspace.clusters.appendAssumeCapacity(cluster); } for (run.glyphs) |source| { var glyph = source; glyph.cluster = try addCompositeIndex(glyph.cluster, source_base); glyph.cluster_index = try addCompositeIndex(glyph.cluster_index, cluster_base); glyph.source_start = try addCompositeIndex(glyph.source_start, source_base); glyph.source_end = try addCompositeIndex(glyph.source_end, source_base); glyph.x_advance = try scaleCompositeValue(glyph.x_advance, scale_ratio); glyph.y_advance = try scaleCompositeValue(glyph.y_advance, scale_ratio); glyph.x_offset = try scaleCompositeValue(glyph.x_offset, scale_ratio); glyph.y_offset = try scaleCompositeValue(glyph.y_offset, scale_ratio); if (glyph.attachment) |*attachment| { attachment.target_glyph_index = try addCompositeIndex( attachment.target_glyph_index, glyph_base, ); } if (glyph.ligature_caret_count > 0) { glyph.ligature_caret_start = try addCompositeIndex( glyph.ligature_caret_start, caret_base, ); } workspace.glyphs.appendAssumeCapacity(glyph); workspace.glyph_sources.appendAssumeCapacity(.{ .segment_index = segment_index }); }}fn addCompositeIndex(value: u32, base: anytype) !u32 { const wide = std.math.add(u64, value, @intCast(base)) catch return error.SourceTooLong; if (wide > std.math.maxInt(u32)) return error.SourceTooLong; return @intCast(wide);}fn scaleCompositeValue(value: i32, ratio: f32) !i32 { const scaled = @as(f64, @floatFromInt(value)) * @as(f64, ratio); if (!std.math.isFinite(scaled) or scaled < @as(f64, @floatFromInt(std.math.minInt(i32))) or scaled > @as(f64, @floatFromInt(std.math.maxInt(i32)))) { return error.TextGeometryOverflow; } return @intFromFloat(@round(scaled));}fn measureUncached(atlas: *Atlas, text: UiText) !Size { const scale = textScale(atlas, text); const line_height = textLineHeight(atlas, text); const wrap_width = textWrapWidth(text); var line_count: usize = 0; var max_width: f32 = 0; var lines = std.mem.splitScalar(u8, text.content, '\n'); while (lines.next()) |line| { const plan = try LinePlan.init(atlas, line, wrap_width / scale); var iterator = plan.iterator(); while (iterator.next(&plan)) |visual| { max_width = @max(max_width, visual.advance * scale); line_count += 1; } } if (line_count == 0) line_count = 1; return .{ .width = if (wrap_width > 0) @min(max_width, wrap_width) else max_width, .height = line_height * @as(f32, @floatFromInt(line_count)), };}fn measureCompositeUncached(base_atlas: *Atlas, atlases: *const AtlasSet, text: UiText) !Size { const base_scale = textScale(base_atlas, text); const wrap_width = textWrapWidth(text); const multiplier = textLineHeightMultiplier(text); var max_width: f32 = 0; var total_height: f32 = 0; var line_start: usize = 0; var lines = std.mem.splitScalar(u8, text.content, '\n'); while (lines.next()) |line| { defer line_start = nextLineStart(text.content.len, line_start, line.len); const plan = try LinePlan.initComposite( base_atlas, atlases, text, line, line_start, if (wrap_width > 0) wrap_width / base_scale else 0, ); var iterator = plan.iterator(); while (iterator.next(&plan)) |visual| { const box = plan.compositeLineBox(visual, multiplier); max_width = @max(max_width, visual.advance * base_scale); total_height += box.height * base_scale; } } if (total_height == 0) { const base = compositeMetricSpan( atlases.forText(text) orelse return error.MissingTextAtlas, gui.model.resolvedTextStyle(text, 0), base_scale, 0, 0, ); total_height = @max( base.point_size * multiplier, base.ascent + base.descent, ) * base_scale; } return .{ .width = if (wrap_width > 0) @min(max_width, wrap_width) else max_width, .height = total_height, };}fn shapeBitmap(allocator: Allocator, bitmap: *Atlas.Bitmap, content: []const u8) !filigree.GlyphRun { bitmap.shaped.clearRetainingCapacity(); bitmap.clusters.clearRetainingCapacity(); var total: i32 = 0; var index: usize = 0; while (index < content.len) { const sequence_len = std.unicode.utf8ByteSequenceLength(content[index]) catch { index += 1; continue; }; if (index + sequence_len > content.len) break; const cluster: u32 = @intCast(index); const codepoint = std.unicode.utf8Decode(content[index .. index + sequence_len]) catch { index += sequence_len; continue; }; index += sequence_len; const glyph_start: u32 = @intCast(bitmap.shaped.items.len); const cluster_index: u32 = @intCast(bitmap.clusters.items.len); if (codepoint < 0x20) { try bitmap.clusters.append(allocator, .{ .source = .{ .start = cluster, .end = @intCast(index) }, .glyphs = .{ .start = glyph_start, .end = glyph_start }, }); continue; } const glyph_id = bitmap.map.get(codepoint) orelse bitmap.map.get('?') orelse std.math.maxInt(u32); try bitmap.shaped.append(allocator, .{ .glyph_id = glyph_id, .cluster = cluster, .cluster_index = cluster_index, .source_start = cluster, .source_end = @intCast(index), .x_advance = bitmap.advance, .y_advance = 0, .x_offset = 0, .y_offset = 0, }); try bitmap.clusters.append(allocator, .{ .source = .{ .start = cluster, .end = @intCast(index) }, .glyphs = .{ .start = glyph_start, .end = glyph_start + 1 }, }); total = addClampedI32(total, bitmap.advance); } return .{ .glyphs = bitmap.shaped.items, .clusters = bitmap.clusters.items, .ligature_carets = &.{}, .total_x_advance = total, .total_y_advance = 0, .direction = .ltr, .writing_mode = .horizontal, .output_order = .visual, };}pub fn packAtlasImageAlloc(allocator: Allocator, atlas: filigree.GlyphAtlas) !OwnedAtlasImage { if (atlas.width <= 0 or atlas.height <= 0) return error.InvalidAtlas; const width = std.math.cast(u32, atlas.width) orelse return error.InvalidAtlas; const height = std.math.cast(u32, atlas.height) orelse return error.InvalidAtlas; const pixel_count = try pixelCount(width, height); const byte_count = std.math.mul(usize, pixel_count, 4) catch return error.InvalidAtlas; if (atlas.rgba.len < byte_count) return error.BufferTooSmall; const pixels = try allocator.alloc(u32, pixel_count); errdefer allocator.free(pixels); for (pixels, 0..) |*pixel, index| { const base = index * 4; pixel.* = packRgba(.{ .r = atlas.rgba[base], .g = atlas.rgba[base + 1], .b = atlas.rgba[base + 2], .a = atlas.rgba[base + 3], }); } return .{ .image = .{ .width = width, .height = height, .pixels = pixels }, .pixels = pixels, };}fn glyphIndexAlloc(allocator: Allocator, atlas: filigree.GlyphAtlas) !std.AutoHashMapUnmanaged(u32, usize) { var index: std.AutoHashMapUnmanaged(u32, usize) = .empty; errdefer index.deinit(allocator); try index.ensureTotalCapacity(allocator, @intCast(atlas.glyphs.len)); for (atlas.glyphs, 0..) |glyph, position| { index.putAssumeCapacity(glyph.glyph_id, position); } return index;}pub fn drawGlyphRun( writer: anytype, atlas_image_index: u32, atlas: filigree.GlyphAtlas, run: filigree.GlyphRun, origin_x: f32, origin_y: f32, color: Color,) void { var pen_x: i32 = 0; var pen_y: i32 = 0; for (run.glyphs) |glyph| { const atlas_index = findAtlasGlyph(atlas, glyph.glyph_id) orelse { pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); continue; }; const atlas_glyph = atlas.glyphs[atlas_index]; const source = atlas.recs[atlas_index]; if (atlas_glyph.width <= 0 or atlas_glyph.height <= 0) { pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); continue; } const rect = Rect{ .x = origin_x + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_x, glyph.x_offset)) + atlas_glyph.offset_x)), .y = origin_y + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_y, glyph.y_offset)) + atlas_glyph.offset_y)), .width = @floatFromInt(atlas_glyph.width), .height = @floatFromInt(atlas_glyph.height), }; writer.drawGlyph(rect, atlas_image_index, .{ .x = source.x, .y = source.y, .width = source.width, .height = source.height, }, color); pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); }}pub fn appendFrameCommands(commands: *command.CommandBuffer, frame: UiFrame, atlases: *const AtlasSet, device_scale: f32, layer: u8) !void { const start = commands.items().len; errdefer commands.rollback(start); for (frame.widgets) |widget| { if (widget.layer != layer) continue; try appendWidgetCommands(commands, widget, atlases, device_scale); }}pub fn appendFrameRegionCommands(commands: *command.CommandBuffer, frame: UiFrame, atlases: *const AtlasSet, region: Region, device_scale: f32, layer: u8) !void { const start = commands.items().len; errdefer commands.rollback(start); const rect = regionRect(region); for (frame.widgets) |widget| { if (widget.layer != layer) continue; if (!intersects(scaleRect(widget.visible_rect, device_scale), rect)) continue; try appendWidgetCommands(commands, widget, atlases, device_scale); }}fn appendWidgetCommands(commands: *command.CommandBuffer, widget: WidgetFrame, atlases: *const AtlasSet, device_scale: f32) !void { const text = widget.text orelse return; try gui.model.validateText(text); const entry = atlases.forText(text) orelse return; if (hasMixedTextMetrics(text) or try atlases.requiresFallback(entry, text.content)) { return appendCompositeWidgetCommands(commands, widget, atlases, device_scale); } return appendSingleWidgetCommands(commands, widget, atlases, device_scale);}fn appendSingleWidgetCommands(commands: *command.CommandBuffer, widget: WidgetFrame, atlases: *const AtlasSet, device_scale: f32) !void { const text = widget.text.?; const has_markers = widget.text_selection.cursor_visible or widget.text_selection.selection_active; if (text.content.len == 0 and !has_markers) return; const entry = atlases.forText(text) orelse return; const atlas = entry.atlas; const foreground = widget.paint.foreground orelse Color{ .r = 28, .g = 34, .b = 31, .a = 255 }; const rect = scaleRect(widget.rect, device_scale); const layout = try TextLayout.init(atlases, text, rect, device_scale); const font_metrics = atlas.metrics(); var writer = GlyphCommandWriter{ .commands = commands, .clip = scaleRect(widget.visible_rect, device_scale), }; var pending_caret: ?TextMarker = null; var glyph_runs = TextRunCursor{ .runs = text.runs }; var background_runs = TextRunRangeCursor{ .runs = text.runs }; var decoration_runs = TextRunRangeCursor{ .runs = text.runs }; const caret_run = findTextRun(text.runs, widget.text_selection.cursor_byte_offset); const caret_foreground = if (caret_run) |run| run.foreground orelse foreground else foreground; const caret_background = if (caret_run) |run| run.background else null; var iterator = layout.iterator(); while (try iterator.next()) |line| { if (text.runs.len > 0) { appendRunBackgroundCommands( &writer, &background_runs, line.plan, line.visual, line.hard_line_start, line.origin_x, line.y, line.height, line.scale, ); } const cursor = if (has_markers) blk: { appendSelectionCommand( &writer, widget.text_selection, line.plan, line.visual, line.hard_line_start, line.origin_x, line.y, line.height, line.scale, foreground, ); if (caretMarker( atlas, widget.text_selection, line.plan, line.visual, line.hard_line_start, line.origin_x, line.y, line.height, line.scale, device_scale, caret_foreground, )) |marker| { if (widget.text_selection.cursor_block) { writer.fillRect(marker.rect, marker.color); } else { pending_caret = marker; } } break :blk blockCursorHighlight( widget, line.plan, line.visual, line.hard_line_start, caret_foreground, caret_background, ); } else null; var line_run = line.plan.run; line_run.glyphs = line.visual.glyphs(line.plan.run); if (text.runs.len == 0) { drawGlyphRunScaled( &writer, entry.image_index, atlas, line_run, line.hard_line, line.y, line.height, line.origin_x, line.origin_y, line.scale, foreground, cursor, ); } else { drawStyledGlyphRunScaled( &writer, entry.image_index, atlas, line_run, line.hard_line, line.hard_line_start, line.y, line.height, line.origin_x, line.origin_y, line.scale, foreground, cursor, &glyph_runs, ); appendRunDecorationCommands( &writer, &decoration_runs, line.plan, line.visual, line.hard_line_start, line.origin_x, line.y, line.origin_y, line.height, line.scale, device_scale, font_metrics, foreground, ); } } if (pending_caret) |caret| writer.fillRect(caret.rect, caret.color); if (writer.failure) |failure| return failure;}fn appendCompositeWidgetCommands( commands: *command.CommandBuffer, widget: WidgetFrame, atlases: *const AtlasSet, device_scale: f32,) !void { const text = widget.text.?; const has_markers = widget.text_selection.cursor_visible or widget.text_selection.selection_active; if (text.content.len == 0 and !has_markers) return; const entry = atlases.forText(text) orelse return; const base_atlas = entry.atlas; const foreground = widget.paint.foreground orelse Color{ .r = 28, .g = 34, .b = 31, .a = 255 }; const rect = scaleRect(widget.rect, device_scale); const layout = try TextLayout.init(atlases, text, rect, device_scale); var writer = GlyphCommandWriter{ .commands = commands, .clip = scaleRect(widget.visible_rect, device_scale), }; var pending_caret: ?TextMarker = null; var glyph_runs = TextRunCursor{ .runs = text.runs }; var background_runs = TextRunRangeCursor{ .runs = text.runs }; var decoration_runs = TextRunRangeCursor{ .runs = text.runs }; const caret_run = findTextRun(text.runs, widget.text_selection.cursor_byte_offset); const caret_foreground = if (caret_run) |run| run.foreground orelse foreground else foreground; const caret_background = if (caret_run) |run| run.background else null; var iterator = layout.iterator(); while (try iterator.next()) |line| { appendRunBackgroundCommands( &writer, &background_runs, line.plan, line.visual, line.hard_line_start, line.origin_x, line.y, line.height, line.scale, ); const cursor = if (has_markers) blk: { appendSelectionCommand( &writer, widget.text_selection, line.plan, line.visual, line.hard_line_start, line.origin_x, line.y, line.height, line.scale, foreground, ); if (caretMarker( base_atlas, widget.text_selection, line.plan, line.visual, line.hard_line_start, line.origin_x, line.y, line.height, line.scale, device_scale, caret_foreground, )) |marker| { if (widget.text_selection.cursor_block) { writer.fillRect(marker.rect, marker.color); } else { pending_caret = marker; } } break :blk blockCursorHighlight( widget, line.plan, line.visual, line.hard_line_start, caret_foreground, caret_background, ); } else null; drawCompositeStyledGlyphRunScaled( &writer, line.plan, line.visual, line.hard_line, line.hard_line_start, line.y, line.height, line.origin_x, line.origin_y, line.scale, line.ascent, foreground, cursor, &glyph_runs, ); appendRunDecorationCommands( &writer, &decoration_runs, line.plan, line.visual, line.hard_line_start, line.origin_x, line.y, line.origin_y, line.height, line.scale, device_scale, .{ .ascent = line.ascent, .descent = line.descent, .line_gap = 0 }, foreground, ); } if (pending_caret) |caret| writer.fillRect(caret.rect, caret.color); if (writer.failure) |failure| return failure;}fn scaleRect(rect: Rect, scale: f32) Rect { return .{ .x = rect.x * scale, .y = rect.y * scale, .width = rect.width * scale, .height = rect.height * scale, };}fn regionRect(region: Region) Rect { return .{ .x = @floatFromInt(region.x), .y = @floatFromInt(region.y), .width = @floatFromInt(region.width), .height = @floatFromInt(region.height), };}fn intersects(left: Rect, right: Rect) bool { return left.x < right.x + right.width and right.x < left.x + left.width and left.y < right.y + right.height and right.y < left.y + left.height;}const GlyphCommandWriter = struct { commands: *command.CommandBuffer, clip: Rect, failure: ?Allocator.Error = null, fn drawGlyph(self: *GlyphCommandWriter, rect: Rect, image_index: u32, source: Rect, color: Color) void { if (self.failure != null) return; self.commands.append(.{ .kind = .glyph, .rect = rect, .clip = self.clip, .source = source, .color = color, .image_index = image_index, }) catch |failure| { self.failure = failure; }; } pub fn fillRect(self: *GlyphCommandWriter, rect: Rect, color: Color) void { if (self.failure != null) return; self.commands.append(.{ .kind = .fill, .rect = rect, .clip = self.clip, .color = color, }) catch |failure| { self.failure = failure; }; }};const TextRunCursor = struct { runs: []const UiTextRun, index: usize = 0, last_offset: usize = 0, started: bool = false, fn find(self: *TextRunCursor, byte_offset: usize) ?UiTextRun { if (!self.started or byte_offset < self.last_offset) { self.index = firstRunEndingAfter(self.runs, byte_offset); self.started = true; } else { while (self.index < self.runs.len and self.runs[self.index].byte_end <= byte_offset) { self.index += 1; } } self.last_offset = byte_offset; if (self.index >= self.runs.len) return null; const run = self.runs[self.index]; if (byte_offset < run.byte_start) return null; return run; }};const TextRunRangeCursor = struct { runs: []const UiTextRun, index: usize = 0, fn firstOverlap(self: *TextRunRangeCursor, byte_start: usize) usize { while (self.index < self.runs.len and self.runs[self.index].byte_end <= byte_start) { self.index += 1; } return self.index; }};fn firstRunEndingAfter(runs: []const UiTextRun, byte_offset: usize) usize { var low: usize = 0; var high = runs.len; while (low < high) { const middle = low + (high - low) / 2; if (runs[middle].byte_end <= byte_offset) { low = middle + 1; } else { high = middle; } } return low;}fn findTextRun(runs: []const UiTextRun, byte_offset: usize) ?UiTextRun { const index = firstRunEndingAfter(runs, byte_offset); if (index >= runs.len or byte_offset < runs[index].byte_start) return null; return runs[index];}fn appendRunBackgroundCommands( writer: *GlyphCommandWriter, cursor: *TextRunRangeCursor, plan: *const LinePlan, visual: VisualLine, line_start: usize, origin_x: f32, line_y: f32, line_height: f32, scale: f32,) void { const visual_start = line_start + visual.byte_start; const visual_end = line_start + visual.byte_end; if (visual_end <= visual_start) return; var index = cursor.firstOverlap(visual_start); while (index < cursor.runs.len and cursor.runs[index].byte_start < visual_end) : (index += 1) { const run = cursor.runs[index]; const color = run.background orelse continue; const rect = textRunRect( plan, visual, line_start, @max(run.byte_start, visual_start), @min(run.byte_end, visual_end), origin_x, line_y, line_height, scale, ) orelse continue; writer.fillRect(rect, color); }}fn appendRunDecorationCommands( writer: *GlyphCommandWriter, cursor: *TextRunRangeCursor, plan: *const LinePlan, visual: VisualLine, line_start: usize, origin_x: f32, line_y: f32, origin_y: f32, line_height: f32, scale: f32, device_scale: f32, metrics: Metrics, foreground: Color,) void { const visual_start = line_start + visual.byte_start; const visual_end = line_start + visual.byte_end; if (visual_end <= visual_start) return; const thickness = @max(device_scale, @min(line_height * 0.08, @max(device_scale, scale))); const bottom = line_y + line_height - thickness; const baseline = origin_y + metrics.ascent * scale; const underline_y = @min(bottom, baseline + @max(device_scale, scale * 0.5)); const strike_y = std.math.clamp(origin_y + metrics.ascent * scale * 0.55, line_y, bottom); var index = cursor.firstOverlap(visual_start); while (index < cursor.runs.len and cursor.runs[index].byte_start < visual_end) : (index += 1) { const run = cursor.runs[index]; if (!run.underline and !run.strikethrough) continue; const range = textRunRect( plan, visual, line_start, @max(run.byte_start, visual_start), @min(run.byte_end, visual_end), origin_x, 0, thickness, scale, ) orelse continue; const color = run.foreground orelse foreground; if (run.underline) { writer.fillRect(.{ .x = range.x, .y = underline_y, .width = range.width, .height = thickness, }, color); } if (run.strikethrough) { writer.fillRect(.{ .x = range.x, .y = strike_y, .width = range.width, .height = thickness, }, color); } }}fn textRunRect( plan: *const LinePlan, visual: VisualLine, line_start: usize, byte_start: usize, byte_end: usize, origin_x: f32, y: f32, height: f32, scale: f32,) ?Rect { if (byte_end <= byte_start) return null; const start_x = origin_x + visual.advanceForByteOffset( plan.run, byte_start - line_start, plan.content.len, ) * scale; const end_x = origin_x + visual.advanceForByteOffset( plan.run, byte_end - line_start, plan.content.len, ) * scale; const left = @min(start_x, end_x); const right = @max(start_x, end_x); if (right <= left) return null; return .{ .x = left, .y = y, .width = right - left, .height = height, };}const TextMarker = struct { rect: Rect, color: Color,};fn appendSelectionCommand( writer: *GlyphCommandWriter, selection: UiTextSelection, plan: *const LinePlan, visual: VisualLine, line_start: usize, origin_x: f32, line_y: f32, line_height: f32, scale: f32, foreground: Color,) void { if (!selection.selection_active) return; const selected = sortedSelection(selection); const visual_start = line_start + visual.byte_start; const visual_end = line_start + visual.byte_end; const start = @max(selected.start, visual_start); const end = @min(selected.end, visual_end); if (end <= start) return; const start_x = origin_x + visual.advanceForByteOffset(plan.run, start - line_start, plan.content.len) * scale; const end_x = origin_x + visual.advanceForByteOffset(plan.run, end - line_start, plan.content.len) * scale; writer.fillRect(.{ .x = start_x, .y = line_y, .width = end_x - start_x, .height = line_height, }, .{ .r = foreground.r, .g = foreground.g, .b = foreground.b, .a = 64 });}fn caretMarker( atlas: *Atlas, selection: UiTextSelection, plan: *const LinePlan, visual: VisualLine, line_start: usize, origin_x: f32, line_y: f32, line_height: f32, scale: f32, device_scale: f32, foreground: Color,) ?TextMarker { if (!selection.cursor_visible) return null; if (selection.cursor_byte_offset < line_start) return null; const cursor_offset = selection.cursor_byte_offset - line_start; if (!visual.containsCaret(cursor_offset, .downstream)) return null; const x = origin_x + visual.advanceForByteOffset(plan.run, cursor_offset, plan.content.len) * scale; if (selection.cursor_block) { return .{ .rect = .{ .x = x, .y = line_y, .width = caretBlockWidth(atlas, plan, visual, cursor_offset, scale, device_scale), .height = @max(1, line_height), }, .color = .{ .r = foreground.r, .g = foreground.g, .b = foreground.b, .a = 176 }, }; } const inset = @min(line_height * 0.12, @max(device_scale, scale)); return .{ .rect = .{ .x = x, .y = line_y + inset, .width = @max(device_scale, scale), .height = @max(1, line_height - inset * 2), }, .color = foreground, };}fn blockCursorHighlight( widget: WidgetFrame, plan: *const LinePlan, visual: VisualLine, line_start: usize, foreground: Color, run_background: ?Color,) ?CursorHighlight { const selection = widget.text_selection; if (!selection.cursor_visible or !selection.cursor_block) return null; if (selection.cursor_byte_offset < line_start) return null; const start = selection.cursor_byte_offset - line_start; if (!visual.containsCaret(start, .downstream) or start >= visual.byte_end) return null; const end = @min(nextCaretByteOffset(plan.run, plan.content, start), visual.byte_end); if (end <= start) return null; return .{ .lead_advance = visual.advanceForByteOffset(plan.run, start, plan.content.len), .trail_advance = visual.advanceForByteOffset(plan.run, end, plan.content.len), .color = run_background orelse widget.paint.background orelse contrastColor(foreground), };}fn contrastColor(foreground: Color) Color { const luminance = (@as(u32, foreground.r) * 299 + @as(u32, foreground.g) * 587 + @as(u32, foreground.b) * 114) / 1000; if (luminance > 128) return .{ .r = 24, .g = 27, .b = 26, .a = 255 }; return .{ .r = 245, .g = 246, .b = 244, .a = 255 };}fn caretBlockWidth(atlas: *Atlas, plan: *const LinePlan, visual: VisualLine, cursor_offset: usize, scale: f32, device_scale: f32) f32 { if (cursor_offset < visual.byte_end) { const end = @min(nextCaretByteOffset(plan.run, plan.content, cursor_offset), visual.byte_end); const lead = visual.advanceForByteOffset(plan.run, cursor_offset, plan.content.len) * scale; const trail = visual.advanceForByteOffset(plan.run, end, plan.content.len) * scale; if (trail > lead) return trail - lead; } const run = atlas.shape("M") catch return @max(device_scale, scale); return @max(1, runAdvance(run) * scale);}const TextRange = struct { start: usize, end: usize,};fn sortedSelection(selection: UiTextSelection) TextRange { const anchor = selection.selection_anchor_byte_offset; const focus = selection.selection_focus_byte_offset; return if (anchor <= focus) .{ .start = anchor, .end = focus } else .{ .start = focus, .end = anchor };}fn findAtlasGlyph(atlas: filigree.GlyphAtlas, glyph_id: u32) ?usize { for (atlas.glyphs, 0..) |glyph, index| { if (glyph.glyph_id == glyph_id) return index; } return null;}fn synthCodepoint(line: []const u8, glyph: filigree.ShapedGlyph) ?u21 { if (glyph.source_codepoint_count != 1) return null; const start: usize = glyph.source_start; const end: usize = glyph.source_end; if (start >= end or end > line.len) return null; const sequence = line[start..end]; const length = std.unicode.utf8ByteSequenceLength(sequence[0]) catch return null; if (length > sequence.len) return null; const codepoint = std.unicode.utf8Decode(sequence[0..length]) catch return null; return if (synth.covered(codepoint)) codepoint else null;}fn drawSyntheticOrNotdef( writer: anytype, line: []const u8, glyph: filigree.ShapedGlyph, pen_x: i32, line_y: f32, line_height: f32, origin_x: f32, scale: f32, color: Color,) void { std.debug.assert(glyph.glyph_id == 0); const cell = Rect{ .x = origin_x + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_x, glyph.x_offset)))) * scale, .y = line_y, .width = @as(f32, @floatFromInt(glyph.x_advance)) / 64 * scale, .height = line_height, }; if (synthCodepoint(line, glyph)) |codepoint| { const drawn = synth.draw(writer, codepoint, cell, color); std.debug.assert(drawn); return; } synth.drawNotdef(writer, cell, color);}const CursorHighlight = struct { lead_advance: f32, trail_advance: f32, color: Color,};fn glyphRunColor(pen_x: i32, cursor: ?CursorHighlight, color: Color) Color { const highlight = cursor orelse return color; const pen = @as(f32, @floatFromInt(pen_x)) / 64; const epsilon = 0.01; if (pen >= highlight.lead_advance - epsilon and pen < highlight.trail_advance - epsilon) return highlight.color; return color;}fn drawCompositeStyledGlyphRunScaled( writer: anytype, plan: *const LinePlan, visual: VisualLine, line: []const u8, line_start: usize, line_y: f32, line_height: f32, origin_x: f32, origin_y: f32, scale: f32, line_ascent: f32, color: Color, cursor: ?CursorHighlight, run_cursor: *TextRunCursor,) void { var pen_x: i32 = 0; var pen_y: i32 = 0; for (plan.run.glyphs[visual.glyph_start..visual.glyph_end], visual.glyph_start..) |glyph, glyph_index| { const byte_offset = line_start + sourceOffset(glyph.source_start, line.len); const text_run = run_cursor.find(byte_offset); const styled_color = if (text_run) |value| value.foreground orelse color else color; const glyph_color = glyphRunColor(pen_x, cursor, styled_color); if (glyph.glyph_id == 0) { drawSyntheticOrNotdef( writer, line, glyph, pen_x, line_y, line_height, origin_x, scale, glyph_color, ); pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); continue; } const source_index = plan.glyph_sources[glyph_index].segment_index; const span = plan.metric_spans[source_index]; const atlas = span.atlas; const atlas_index = atlas.glyph_index.get(glyph.glyph_id) orelse { pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); continue; }; const atlas_glyph = atlas.atlas.glyphs[atlas_index]; const source = atlas.atlas.recs[atlas_index]; if (atlas_glyph.width <= 0 or atlas_glyph.height <= 0) { pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); continue; } const style_origin_y = origin_y + (line_ascent - span.ascent) * scale; const rect = Rect{ .x = origin_x + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_x, glyph.x_offset)))) * scale + @as(f32, @floatFromInt(atlas_glyph.offset_x)) * span.scale_ratio * scale, .y = style_origin_y + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_y, glyph.y_offset)))) * scale + @as(f32, @floatFromInt(atlas_glyph.offset_y)) * span.scale_ratio * scale, .width = @as(f32, @floatFromInt(atlas_glyph.width)) * span.scale_ratio * scale, .height = @as(f32, @floatFromInt(atlas_glyph.height)) * span.scale_ratio * scale, }; writer.drawGlyph(rect, span.image_index, .{ .x = source.x, .y = source.y, .width = source.width, .height = source.height, }, glyph_color); pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); }}fn drawGlyphRunScaled( writer: anytype, atlas_image_index: u32, atlas: *const Atlas, run: filigree.GlyphRun, line: []const u8, line_y: f32, line_height: f32, origin_x: f32, origin_y: f32, scale: f32, color: Color, cursor: ?CursorHighlight,) void { drawGlyphRunScaledMode( false, writer, atlas_image_index, atlas, run, line, 0, line_y, line_height, origin_x, origin_y, scale, color, cursor, null, );}fn drawStyledGlyphRunScaled( writer: anytype, atlas_image_index: u32, atlas: *const Atlas, run: filigree.GlyphRun, line: []const u8, line_start: usize, line_y: f32, line_height: f32, origin_x: f32, origin_y: f32, scale: f32, color: Color, cursor: ?CursorHighlight, run_cursor: *TextRunCursor,) void { drawGlyphRunScaledMode( true, writer, atlas_image_index, atlas, run, line, line_start, line_y, line_height, origin_x, origin_y, scale, color, cursor, run_cursor, );}fn drawGlyphRunScaledMode( comptime styled: bool, writer: anytype, atlas_image_index: u32, atlas: *const Atlas, run: filigree.GlyphRun, line: []const u8, line_start: usize, line_y: f32, line_height: f32, origin_x: f32, origin_y: f32, scale: f32, color: Color, cursor: ?CursorHighlight, run_cursor: ?*TextRunCursor,) void { var pen_x: i32 = 0; var pen_y: i32 = 0; for (run.glyphs) |glyph| { const styled_color = if (styled) blk: { const byte_offset = line_start + sourceOffset(glyph.source_start, line.len); const text_run = run_cursor.?.find(byte_offset); break :blk if (text_run) |value| value.foreground orelse color else color; } else color; const glyph_color = glyphRunColor(pen_x, cursor, styled_color); if (glyph.glyph_id == 0) { drawSyntheticOrNotdef( writer, line, glyph, pen_x, line_y, line_height, origin_x, scale, glyph_color, ); pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); continue; } const atlas_index = atlas.glyph_index.get(glyph.glyph_id) orelse { pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); continue; }; const atlas_glyph = atlas.atlas.glyphs[atlas_index]; const source = atlas.atlas.recs[atlas_index]; if (atlas_glyph.width <= 0 or atlas_glyph.height <= 0) { pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); continue; } const rect = Rect{ .x = origin_x + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_x, glyph.x_offset)) + atlas_glyph.offset_x)) * scale, .y = origin_y + @as(f32, @floatFromInt(round26Dot6(addClampedI32(pen_y, glyph.y_offset)) + atlas_glyph.offset_y)) * scale, .width = @as(f32, @floatFromInt(atlas_glyph.width)) * scale, .height = @as(f32, @floatFromInt(atlas_glyph.height)) * scale, }; writer.drawGlyph(rect, atlas_image_index, .{ .x = source.x, .y = source.y, .width = source.width, .height = source.height, }, glyph_color); pen_x = addClampedI32(pen_x, glyph.x_advance); pen_y = addClampedI32(pen_y, glyph.y_advance); }}fn runAdvance(run: filigree.GlyphRun) f32 { return @as(f32, @floatFromInt(run.total_x_advance)) / 64;}fn glyphSliceAdvance(glyphs: []const filigree.ShapedGlyph) f32 { var advance: f32 = 0; for (glyphs) |glyph| advance += @as(f32, @floatFromInt(glyph.x_advance)) / 64; return advance;}fn nextCaretByteOffset(run: filigree.GlyphRun, text: []const u8, byte_offset: usize) usize { const target = @min(byte_offset, text.len); if (target >= text.len) return text.len; const map = run.clusterMap(); for (run.clusters, 0..) |cluster, cluster_index| { const start = sourceOffset(cluster.source.start, text.len); const end = sourceOffset(cluster.source.end, text.len); if (end <= target) continue; if (target < start) return start; const stop_count = map.clusterCaretStopCount(cluster_index) orelse 0; if (stop_count > 2) { var stop_index: usize = 1; while (stop_index < stop_count) : (stop_index += 1) { const stop = map.clusterCaretStop(cluster_index, stop_index) orelse continue; const offset = sourceOffset(stop, text.len); if (offset > target) return offset; } return end; } return nextGraphemeBoundary(text, start, end, target); } return text.len;}fn nextGraphemeBoundary(text: []const u8, start: usize, end: usize, target: usize) usize { if (end <= start) return end; const source_start = std.math.cast(u32, start) orelse return end; var iterator = filigree.unicode.SourceIterator.init(.{ .utf8 = text[start..end] }, source_start) catch return end; var state: filigree.unicode.GraphemeState = .{}; while (iterator.next() catch return end) |scalar| { const boundary = !state.consume(scalar.codepoint); const offset = sourceOffset(scalar.source.start, text.len); if (boundary and offset > target) return offset; } return end;}fn sourceOffset(offset: u32, text_len: usize) usize { return @min(@as(usize, @intCast(offset)), text_len);}fn nextLineStart(content_len: usize, line_start: usize, line_len: usize) usize { const line_end = @min(content_len, line_start + line_len); return if (line_end < content_len) line_end + 1 else line_end;}fn textPointSize(atlas: *const Atlas, text: UiText) f32 { if (std.math.isFinite(text.point_size) and text.point_size > 0) return @floatCast(text.point_size); if (atlas.pixel_size > 0) return @floatFromInt(atlas.pixel_size); return 16;}fn textScale(atlas: *const Atlas, text: UiText) f32 { if (atlas.pixel_size <= 0) return 1; return textPointSize(atlas, text) / @as(f32, @floatFromInt(atlas.pixel_size));}fn textWrapWidth(text: UiText) f32 { if (std.math.isFinite(text.wrap_width) and text.wrap_width > 0) return @floatCast(text.wrap_width); return 0;}fn textLineHeight(atlas: *const Atlas, text: UiText) f32 { const multiplier = textLineHeightMultiplier(text); const requested = textPointSize(atlas, text) * multiplier; return @max(requested, atlas.metrics().height() * textScale(atlas, text));}fn textLineHeightMultiplier(text: UiText) f32 { return if (std.math.isFinite(text.line_height) and text.line_height > 0) @floatCast(text.line_height) else 1.2;}fn alignedStart(start: f32, container: f32, content: f32, alignment: gui.model.UiTextAlign) f32 { return switch (alignment) { .start => start, .center => start + @max(container - content, 0) / 2, .end => start + @max(container - content, 0), };}fn pixelCount(width: u32, height: u32) !usize { return std.math.mul(usize, width, height) catch error.InvalidAtlas;}fn packRgba(color: Color) u32 { return @as(u32, color.r) | (@as(u32, color.g) << 8) | (@as(u32, color.b) << 16) | (@as(u32, color.a) << 24);}fn round26Dot6(value: i32) i32 { if (value >= 0) return @divTrunc(value + 32, 64); return @divTrunc(value - 32, 64);}fn addClampedI32(left: i32, right: i32) i32 { const value = @as(i64, left) + @as(i64, right); if (value > std.math.maxInt(i32)) return std.math.maxInt(i32); if (value < std.math.minInt(i32)) return std.math.minInt(i32); return @intCast(value);}const TestRecorder = struct { glyph_count: usize = 0, last_rect: Rect = .{}, last_source: Rect = .{}, last_color: Color = .{}, last_image_index: u32 = 0, fn drawGlyph(self: *TestRecorder, rect: Rect, image_index: u32, source: Rect, color: Color) void { self.glyph_count += 1; self.last_rect = rect; self.last_source = source; self.last_color = color; self.last_image_index = image_index; }};const test_output_limits: filigree.Output.Limits = .{ .max_glyphs = 4096, .max_ligature_carets = 4096,};const test_atlas_cache_limits: AtlasCacheStorage.Limits = .{ .measure_entries = 512, .measure_payload_bytes = 64 * 1024, .shape_entries = 1024, .shape_payload_bytes = 512 * 1024,};fn initShapeOnlyAtlas(allocator: Allocator, bytes: []u8, pixel_size: i32) !Atlas { errdefer allocator.free(bytes); var font = filigree.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.InvalidFont; errdefer font.deinit(); font.setScale(@floatFromInt(pixel_size), 72); var cache = try AtlasCacheStorage.init(allocator, test_atlas_cache_limits); errdefer cache.deinit(allocator); cache.activate(); return .{ .allocator = allocator, .pixel_size = pixel_size, .atlas = .{}, .image = .{ .image = .{ .width = 0, .height = 0, .pixels = &.{} }, .pixels = &.{}, }, .cache = cache, .backend = .{ .outline = .{ .font_bytes = bytes, .font = font, .context = filigree.Context.init(allocator, .{}), .output = try filigree.Output.init(allocator, test_output_limits), } }, };}test "packAtlasImageAlloc converts filigree atlas rgba to packed image" { const allocator = std.testing.allocator; var rgba = [_]u8{ 255, 255, 255, 128, 10, 20, 30, 40, }; const atlas = filigree.GlyphAtlas{ .rgba = rgba[0..], .width = 2, .height = 1, }; var owned = try packAtlasImageAlloc(allocator, atlas); defer owned.deinit(allocator); try std.testing.expectEqual(@as(u32, 2), owned.image.width); try std.testing.expectEqual(@as(u32, 1), owned.image.height); try std.testing.expectEqual(@as(u32, 0x80ff_ffff), owned.pixels[0]); try std.testing.expectEqual(@as(u32, 0x281e_140a), owned.pixels[1]);}test "drawGlyphRun emits atlas-backed glyph commands" { var atlas_glyphs = [_]filigree.GlyphAtlasGlyph{.{ .glyph_id = 7, .width = 3, .height = 4, .offset_x = 1, .offset_y = 2, }}; var atlas_recs = [_]filigree.GlyphAtlasRectangle{.{ .x = 5, .y = 6, .width = 3, .height = 4, }}; const atlas = filigree.GlyphAtlas{ .width = 8, .height = 8, .glyphs = atlas_glyphs[0..], .recs = atlas_recs[0..], }; const shaped = [_]filigree.ShapedGlyph{.{ .glyph_id = 7, .cluster = 0, .x_advance = 8 * 64, .y_advance = 0, .x_offset = 64, .y_offset = 0, }}; const run = filigree.GlyphRun{ .glyphs = shaped[0..], .clusters = &.{}, .ligature_carets = &.{}, .total_x_advance = 8 * 64, .total_y_advance = 0, .direction = .ltr, .writing_mode = .horizontal, .output_order = .visual, }; var recorder = TestRecorder{}; drawGlyphRun(&recorder, 4, atlas, run, 10, 20, .{ .r = 1, .g = 2, .b = 3, .a = 200 }); try std.testing.expectEqual(@as(usize, 1), recorder.glyph_count); try std.testing.expectEqual(@as(u32, 4), recorder.last_image_index); try std.testing.expectEqual(@as(f32, 12), recorder.last_rect.x); try std.testing.expectEqual(@as(f32, 22), recorder.last_rect.y); try std.testing.expectEqual(@as(f32, 3), recorder.last_rect.width); try std.testing.expectEqual(@as(f32, 5), recorder.last_source.x); try std.testing.expectEqual(@as(u8, 200), recorder.last_color.a);}test "drawGlyphRun consumes filigree shaped atlas output" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var font = filigree.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.InvalidFont; defer font.deinit(); font.setPixelHeightScale(20); var context = filigree.Context.init(allocator, .{}); defer context.deinit(); var output = try filigree.Output.init(allocator, test_output_limits); defer output.deinit(allocator); try context.shapeRun(.{ .font = &font, .text = .{ .utf8 = "A" }, }, &output); const run = output.run(); try std.testing.expect(run.glyphs.len > 0); var glyph_ids = try allocator.alloc(i32, run.glyphs.len); defer allocator.free(glyph_ids); var codepoints = try allocator.alloc(i32, run.glyphs.len); defer allocator.free(codepoints); for (run.glyphs, 0..) |glyph, index| { glyph_ids[index] = std.math.cast(i32, glyph.glyph_id) orelse return error.GlyphIdTooLarge; codepoints[index] = 'A'; } const atlas = try filigree.glyphAtlasAlloc(allocator, allocator, bytes, 20, glyph_ids, codepoints, 2); defer atlas.deinit(allocator); var owned = try packAtlasImageAlloc(allocator, atlas); defer owned.deinit(allocator); var recorder = TestRecorder{}; drawGlyphRun(&recorder, 0, atlas, run, 0, 0, .{ .r = 20, .g = 30, .b = 40, .a = 255 }); try std.testing.expectEqual(run.glyphs.len, recorder.glyph_count); try std.testing.expect(owned.image.width > 0); try std.testing.expect(owned.image.height > 0); try std.testing.expect(owned.pixels.len > 0);}test "bitmap atlas rasterizes cells and shapes ascii with fallback" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0x81, 0xFF } }, .{ .codepoint = '?', .rows = &.{ 0xFF, 0x00 } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); try std.testing.expectEqual(@as(i32, 18), atlas.atlas.width); try std.testing.expectEqual(@as(i32, 2), atlas.atlas.height); try std.testing.expectEqual(@as(u8, 255), atlas.atlas.rgba[3]); try std.testing.expectEqual(@as(u8, 0), atlas.atlas.rgba[7]); try std.testing.expect(atlas.image.image.width == 18); const run = try atlas.shape("AZ\n"); try std.testing.expectEqual(@as(usize, 2), run.glyphs.len); try std.testing.expectEqual(@as(u32, 'A'), run.glyphs[0].glyph_id); try std.testing.expectEqual(@as(u32, '?'), run.glyphs[1].glyph_id); try std.testing.expectEqual(@as(i32, 2 * 8 * 64), run.total_x_advance); var recorder = TestRecorder{}; drawGlyphRun(&recorder, 0, atlas.atlas, run, 100, 200, .{ .r = 9, .g = 8, .b = 7, .a = 255 }); try std.testing.expectEqual(@as(usize, 2), recorder.glyph_count); try std.testing.expectEqual(@as(f32, 108), recorder.last_rect.x); try std.testing.expectEqual(@as(f32, 200), recorder.last_rect.y); try std.testing.expectEqual(@as(f32, 9), recorder.last_source.x);}test "bitmap atlas measures scaled multiline text" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0x81, 0xFF } }, .{ .codepoint = '?', .rows = &.{ 0xFF, 0x00 } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const measured = try measure(&atlas, .{ .content = "AZ\nA", .point_size = 20, .line_height = 1.5, }); try std.testing.expectApproxEqAbs(@as(f32, 20), measured.width, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 60), measured.height, 0.001);}test "bitmap atlas measures and paints Unicode wrapped text with one line plan" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const wrapped = UiText{ .content = "AB CD", .point_size = 16, .line_height = 1, .wrap_width = 24, }; const measured = try measure(&atlas, wrapped); try std.testing.expectApproxEqAbs(@as(f32, 24), measured.width, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 32), measured.height, 0.001); const child = [_]UiNode{.{ .widget_id = 2, .kind = .label, .text = wrapped, .paint = .{ .foreground = .{ .r = 255, .g = 255, .b = 255, .a = 255 } }, .size = .{ .width = 24, .height = 32 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 24, .height = 32 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); try std.testing.expectEqual(@as(usize, 5), commands.items().len); try std.testing.expectApproxEqAbs(@as(f32, 16), commands.items()[3].rect.y - commands.items()[0].rect.y, 0.001);}test "multiline text geometry distinguishes soft affinity and hard empty lines" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'E', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'F', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas, }}; const atlases = AtlasSet{ .entries = &entries }; const text = UiText{ .content = "AB CD\n\nEF", .point_size = 16, .line_height = 1, .wrap_width = 24, .horizontal_align = .center, .vertical_align = .center, }; const box = Size{ .width = 64, .height = 80 }; const upstream = try textCaretGeometry( &atlases, text, box, 3, .upstream, ); try std.testing.expectEqual(@as(usize, 0), upstream.line_index); try std.testing.expectApproxEqAbs(@as(f32, 44), upstream.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 8), upstream.y, 0.001); const downstream = try textCaretGeometry( &atlases, text, box, 3, .downstream, ); try std.testing.expectEqual(@as(usize, 1), downstream.line_index); try std.testing.expectApproxEqAbs(@as(f32, 24), downstream.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 24), downstream.y, 0.001); const hard_end = try textCaretGeometry( &atlases, text, box, 5, .downstream, ); try std.testing.expectEqual(@as(usize, 1), hard_end.line_index); try std.testing.expectApproxEqAbs(@as(f32, 40), hard_end.x, 0.001); const empty = try textCaretGeometry( &atlases, text, box, 6, .downstream, ); try std.testing.expectEqual(@as(usize, 2), empty.line_index); try std.testing.expectApproxEqAbs(@as(f32, 32), empty.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 40), empty.y, 0.001); const upstream_hit = try textHitTestPoint( &atlases, text, box, .{ .x = 60, .y = 12 }, ); try std.testing.expectEqual(@as(usize, 3), upstream_hit.byte_offset); try std.testing.expectEqual(TextCaretAffinity.upstream, upstream_hit.affinity); const downstream_hit = try textHitTestPoint( &atlases, text, box, .{ .x = 0, .y = 24 }, ); try std.testing.expectEqual(@as(usize, 3), downstream_hit.byte_offset); try std.testing.expectEqual(TextCaretAffinity.downstream, downstream_hit.affinity); const bottom = try textHitTestPoint( &atlases, text, box, .{ .x = 100, .y = 200 }, ); try std.testing.expectEqual(text.content.len, bottom.byte_offset); try std.testing.expectEqual(@as(usize, 3), bottom.line_index);}test "widget text hit query honors translated clip and clamps to cluster boundaries" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 0x00E9, .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas, }}; const atlases = AtlasSet{ .entries = &entries }; const widget = WidgetFrame{ .root_id = 1, .widget_id = 2, .kind = .label, .rect = .{ .x = 30, .y = -8, .width = 24, .height = 32 }, .visible_rect = .{ .x = 34, .y = 0, .width = 16, .height = 16 }, .paint = .{}, .scroll = .{}, .constraints = .{}, .content_size = .{ .width = 24, .height = 32 }, .focusable = false, .has_text = true, .text = .{ .content = "AéB\nAB", .point_size = 16, .line_height = 1, .wrap_width = 24, }, }; try std.testing.expect((try textHitTestWidgetPoint( &atlases, widget, .{ .x = 33, .y = 4 }, )) == null); const visible = (try textHitTestWidgetPoint( &atlases, widget, .{ .x = 42, .y = 4 }, )).?; const direct = try textHitTestPoint( &atlases, widget.text.?, .{ .width = widget.rect.width, .height = widget.rect.height }, .{ .x = 12, .y = 12 }, ); try std.testing.expectEqual(direct.byte_offset, visible.byte_offset); try std.testing.expectEqual(direct.affinity, visible.affinity); const leading = (try textHitTestWidgetPointClamped( &atlases, widget, .{ .x = -100, .y = -100 }, )).?; const trailing = (try textHitTestWidgetPointClamped( &atlases, widget, .{ .x = 100, .y = 100 }, )).?; try std.testing.expect(leading.byte_offset <= trailing.byte_offset); try std.testing.expect( leading.byte_offset == 0 or leading.byte_offset == widget.text.?.content.len or widget.text.?.content[leading.byte_offset] & 0b1100_0000 != 0b1000_0000, ); try std.testing.expect( trailing.byte_offset == 0 or trailing.byte_offset == widget.text.?.content.len or widget.text.?.content[trailing.byte_offset] & 0b1100_0000 != 0b1000_0000, ); try std.testing.expect((try textHitTestWidgetPointClamped( &atlases, .{ .root_id = widget.root_id, .widget_id = widget.widget_id, .kind = widget.kind, .rect = widget.rect, .visible_rect = .{}, .paint = widget.paint, .scroll = widget.scroll, .constraints = widget.constraints, .content_size = widget.content_size, .focusable = false, .text = widget.text, }, .{}, )) == null);}test "multiline text geometry follows mixed metric line boxes and end alignment" { const allocator = std.testing.allocator; const base_glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, }; const large_glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F } }, }; var base_atlas = try Atlas.initFromBitmapGlyphs( allocator, &base_glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer base_atlas.deinit(); var large_atlas = try Atlas.initFromBitmapGlyphs( allocator, &large_glyphs, .{ .width = 12, .height = 4, .stride = 2 }, 24, test_atlas_cache_limits, ); defer large_atlas.deinit(); const styles = [_]UiTextStyle{.{ .font_asset_id = 2, .point_size = 24, }}; const runs = [_]UiTextRun{.{ .byte_start = 2, .byte_end = 3, .style_slot = 1, }}; const text = UiText{ .content = "A\nBC", .runs = &runs, .styles = &styles, .font_asset_id = 1, .point_size = 16, .line_height = 1, .horizontal_align = .end, .vertical_align = .end, }; const entries = [_]AtlasSet.Entry{ .{ .face = 1, .image_index = 0, .atlas = &base_atlas }, .{ .face = 2, .image_index = 1, .atlas = &large_atlas }, }; const atlases = AtlasSet{ .entries = &entries }; const box = Size{ .width = 48, .height = 60 }; const first = try textCaretGeometry( &atlases, text, box, 1, .downstream, ); try std.testing.expectEqual(@as(usize, 0), first.line_index); try std.testing.expectApproxEqAbs(@as(f32, 48), first.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 20), first.y, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 16), first.height, 0.001); const mixed = try textCaretGeometry( &atlases, text, box, 3, .downstream, ); try std.testing.expectEqual(@as(usize, 1), mixed.line_index); try std.testing.expectApproxEqAbs(@as(f32, 40), mixed.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 36), mixed.y, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 24), mixed.height, 0.001);}test "warmed multiline text geometry needs no backing allocation" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const allocator = failing.allocator(); const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas, }}; const atlases = AtlasSet{ .entries = &entries }; const text = UiText{ .content = "AB AB\n\nAB AB", .point_size = 16, .line_height = 1, .wrap_width = 24, .vertical_align = .center, }; const box = Size{ .width = 48, .height = 96 }; for (0..text.content.len + 1) |byte_offset| { _ = try textCaretGeometry( &atlases, text, box, byte_offset, .downstream, ); } _ = try textHitTestPoint(&atlases, text, box, .{ .x = 12, .y = 32 }); failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; for (0..8) |_| { for (0..text.content.len + 1) |byte_offset| { _ = try textCaretGeometry( &atlases, text, box, byte_offset, .downstream, ); } _ = try textHitTestPoint(&atlases, text, box, .{ .x = 12, .y = 32 }); } try std.testing.expect(!failing.has_induced_failure);}test "multiline query geometry matches painted caret at device scales" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'E', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'F', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const text = UiText{ .content = "AB CD\nEF", .point_size = 16, .line_height = 1, .wrap_width = 24, .horizontal_align = .center, .vertical_align = .center, }; const child = [_]UiNode{.{ .widget_id = 2, .kind = .text_input, .text = text, .text_selection = .{ .cursor_visible = true, .cursor_byte_offset = 3, }, .paint = .{ .foreground = .{ .r = 240, .g = 240, .b = 240, .a = 255 }, }, .size = .{ .width = 64, .height = 80 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 80, .height = 96 }, .root = .{ .widget_id = 1, .children = &child }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); const widget = frame.widgets[1]; const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas, }}; const atlases = AtlasSet{ .entries = &entries }; const geometry = try textCaretGeometry( &atlases, text, .{ .width = widget.rect.width, .height = widget.rect.height }, 3, .downstream, ); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); for ([_]f32{ 1, 2 }) |device_scale| { commands.reset(); try appendFrameCommands( &commands, frame, &atlases, device_scale, 0, ); const caret = commands.items()[commands.items().len - 1]; try std.testing.expectEqual(command.Kind.fill, caret.kind); try std.testing.expectApproxEqAbs( (widget.rect.x + geometry.x) * device_scale, caret.rect.x, 0.001, ); try std.testing.expect( caret.rect.y >= (widget.rect.y + geometry.y) * device_scale, ); try std.testing.expect( caret.rect.y + caret.rect.height <= (widget.rect.y + geometry.y + geometry.height) * device_scale, ); }}test "shape cache preserves cluster and ligature caret geometry" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithGsubLigatureAndGdefCarets(allocator); var atlas = try initShapeOnlyAtlas(allocator, bytes, 20); defer atlas.deinit(); const text = "fi"; const fresh = try atlas.shapeUncached(text); const expected_glyphs = try allocator.dupe(filigree.ShapedGlyph, fresh.glyphs); defer allocator.free(expected_glyphs); const expected_clusters = try allocator.dupe(filigree.Cluster, fresh.clusters); defer allocator.free(expected_clusters); const expected_carets = try allocator.dupe(filigree.LigatureCaret, fresh.ligature_carets); defer allocator.free(expected_carets); const expected = filigree.GlyphRun{ .glyphs = expected_glyphs, .clusters = expected_clusters, .ligature_carets = expected_carets, .total_x_advance = fresh.total_x_advance, .total_y_advance = fresh.total_y_advance, .direction = fresh.direction, .writing_mode = fresh.writing_mode, .output_order = fresh.output_order, }; const cached = try atlas.shape(text); try expectGlyphRunEqual(expected, cached); try std.testing.expect(cached.clusters.len > 0); try std.testing.expect(cached.ligature_carets.len > 0); try std.testing.expectEqual( shapePayloadBytes(text, cached).?, atlas.cacheStatus().shape.payload_bytes, ); try std.testing.expectEqual( filigree.caret.advanceForByteOffset(expected, 1, text.len), try atlas.advanceForByteOffset(text, 1), ); try std.testing.expectEqual( filigree.caret.hitTestAdvance(expected, 4.6, text), try atlas.hitTestAdvance(text, 4.6), ); _ = try atlas.shape("A"); const retained = try atlas.shape(text); try expectGlyphRunEqual(expected, retained); try std.testing.expectEqual(cached.glyphs.ptr, retained.glyphs.ptr); try std.testing.expectEqual(cached.clusters.ptr, retained.clusters.ptr); try std.testing.expectEqual(cached.ligature_carets.ptr, retained.ligature_carets.ptr);}test "style boundaries inside ligatures keep one glyph and use its cluster start" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithGsubLigatureAndGdefCarets(allocator); var atlas = try initShapeOnlyAtlas(allocator, bytes, 20); defer atlas.deinit(); const content = "fi"; const run = try atlas.shape(content); try std.testing.expectEqual(@as(usize, 1), run.glyphs.len); try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start); const glyph_start = sourceOffset(run.glyphs[0].source_start, content.len); const trailing_style = [_]UiTextRun{.{ .byte_start = 1, .byte_end = 2, .foreground = .{ .r = 220, .g = 30, .b = 20 }, }}; var trailing_cursor = TextRunCursor{ .runs = &trailing_style }; try std.testing.expect(trailing_cursor.find(glyph_start) == null); const leading_style = [_]UiTextRun{.{ .byte_start = 0, .byte_end = 1, .foreground = .{ .r = 20, .g = 80, .b = 220 }, }}; var leading_cursor = TextRunCursor{ .runs = &leading_style }; try std.testing.expectEqual( leading_style[0], leading_cursor.find(glyph_start).?, ); try std.testing.expectEqual( try measure(&atlas, .{ .content = content }), try measure(&atlas, .{ .content = content, .runs = &trailing_style }), );}test "cached combining and ZWJ caret queries match fresh shaping" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); var atlas = try initShapeOnlyAtlas(allocator, bytes, 20); defer atlas.deinit(); const cases = [_][]const u8{ "e\u{0301}", "\u{1f469}\u{200d}\u{1f4bb}", }; for (cases) |text| { const fresh = try atlas.shapeUncached(text); const expected_glyphs = try allocator.dupe(filigree.ShapedGlyph, fresh.glyphs); defer allocator.free(expected_glyphs); const expected_clusters = try allocator.dupe(filigree.Cluster, fresh.clusters); defer allocator.free(expected_clusters); const expected_carets = try allocator.dupe(filigree.LigatureCaret, fresh.ligature_carets); defer allocator.free(expected_carets); const expected = filigree.GlyphRun{ .glyphs = expected_glyphs, .clusters = expected_clusters, .ligature_carets = expected_carets, .total_x_advance = fresh.total_x_advance, .total_y_advance = fresh.total_y_advance, .direction = fresh.direction, .writing_mode = fresh.writing_mode, .output_order = fresh.output_order, }; try std.testing.expect(expected.clusters.len > 0); for (0..text.len + 1) |byte_offset| { try std.testing.expectEqual( filigree.caret.advanceForByteOffset(expected, byte_offset, text.len), try atlas.advanceForByteOffset(text, byte_offset), ); } for (0..33) |raw_advance| { const advance: f32 = @floatFromInt(raw_advance); try std.testing.expectEqual( filigree.caret.hitTestAdvance(expected, advance, text), try atlas.hitTestAdvance(text, advance), ); } }}test "wrapped markers preserve glyph geometry and partition selection by visual line" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'E', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'F', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const text = UiText{ .content = "AB CD EF", .point_size = 16, .line_height = 1, .wrap_width = 24, }; const nodes = [_]UiNode{ .{ .widget_id = 2, .kind = .text_input, .text = text, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 24, .height = 48 }, }, .{ .widget_id = 2, .kind = .text_input, .text = text, .text_selection = .{ .cursor_visible = true, .cursor_byte_offset = 7, .selection_active = true, .selection_anchor_byte_offset = 1, .selection_focus_byte_offset = 7, }, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 24, .height = 48 }, }, .{ .widget_id = 2, .kind = .text_input, .text = text, .text_selection = .{ .cursor_visible = true, .cursor_byte_offset = 7, }, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 24, .height = 48 }, }, }; const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = &entries }; var frames: [3]gui.frame.Workspace = .{ gui.frame.Workspace.init(allocator), gui.frame.Workspace.init(allocator), gui.frame.Workspace.init(allocator), }; defer for (&frames) |*frame| frame.deinit(); var buffers: [3]command.CommandBuffer = .{ command.CommandBuffer.init(allocator), command.CommandBuffer.init(allocator), command.CommandBuffer.init(allocator), }; defer for (&buffers) |*buffer| buffer.deinit(); for (0..nodes.len) |index| { const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 24, .height = 48 }, .root = .{ .widget_id = 1, .children = nodes[index .. index + 1] }, }; const frame = try frames[index].buildSurface(&surface, .{}); try appendFrameCommands(&buffers[index], frame, &atlases, 1, 0); } var unfocused_glyphs: [16]command.Command = undefined; var unfocused_count: usize = 0; for (buffers[0].items()) |item| { if (item.kind != .glyph) continue; unfocused_glyphs[unfocused_count] = item; unfocused_count += 1; } var focused_count: usize = 0; var selection_y: [3]f32 = undefined; var selection_count: usize = 0; for (buffers[1].items()) |item| { if (item.kind == .glyph) { const expected = unfocused_glyphs[focused_count]; try std.testing.expectEqual(expected.kind, item.kind); try std.testing.expectEqual(expected.rect, item.rect); try std.testing.expectEqual(expected.clip, item.clip); try std.testing.expectEqual(expected.source, item.source); try std.testing.expectEqual(expected.color, item.color); try std.testing.expectEqual(expected.image_index, item.image_index); focused_count += 1; } if (item.kind == .fill and item.color.a == 64) { selection_y[selection_count] = item.rect.y; selection_count += 1; } } try std.testing.expectEqual(@as(usize, 8), unfocused_count); try std.testing.expectEqual(unfocused_count, focused_count); try std.testing.expectEqual(@as(usize, 3), selection_count); try std.testing.expectApproxEqAbs(@as(f32, 16), selection_y[1] - selection_y[0], 0.001); try std.testing.expectApproxEqAbs(@as(f32, 16), selection_y[2] - selection_y[1], 0.001); const fragment_id = command.FragmentId{ .root_id = 1, .element_id = 2, .namespace = command.fragment_namespace_widget, .part = command.fragment_part_text, }; try buffers[0].commitFragment(fragment_id, 0); try buffers[2].commitFragment(fragment_id, 0); var retained = gui.paint.RetainedCommands.init(allocator); defer retained.deinit(); try retained.retain(&buffers[0]); const damage = try retained.diff( &buffers[2], &.{Region.full(320, 200)}, 320, 200, ); const narrowed = switch (damage) { .semantic => return error.ExpectedNarrowedDamage, .narrowed => |value| value, }; try std.testing.expectEqual(@as(usize, 1), narrowed.slice().len); const bounds = narrowed.bounding().?; try std.testing.expect(bounds.width <= 3); try std.testing.expect(bounds.height <= 20);}test "bitmap wrapped measurement reuses warmed line workspace" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const allocator = failing.allocator(); const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const wrapped = UiText{ .content = "AB CD", .point_size = 16, .line_height = 1, .wrap_width = 24, }; _ = try measureUncached(&atlas, wrapped); failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; const repeated = try measureUncached(&atlas, wrapped); try std.testing.expectApproxEqAbs(@as(f32, 32), repeated.height, 0.001); try std.testing.expect(!failing.has_induced_failure);}test "warmed visual line and caret queries need no backing allocation" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const allocator = failing.allocator(); const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = ' ', .rows = &.{ 0x00, 0x00 } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const text = "AB AB AB AB AB AB"; _ = try LinePlan.init(&atlas, text, 24); for (0..text.len + 1) |byte_offset| { _ = try atlas.advanceForByteOffset(text, byte_offset); } failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; for (0..8) |_| { const plan = try LinePlan.init(&atlas, text, 24); var iterator = plan.iterator(); while (iterator.next(&plan)) |visual| { _ = visual.advanceForByteOffset(plan.run, visual.byte_start, text.len); _ = visual.advanceForByteOffset(plan.run, visual.byte_end, text.len); } for (0..text.len + 1) |byte_offset| { const advance = try atlas.advanceForByteOffset(text, byte_offset); _ = try atlas.hitTestAdvance(text, advance); } } try std.testing.expect(!failing.has_induced_failure);}test "atlas metrics derive from face and bitmap cell geometry" { const allocator = std.testing.allocator; var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 }); defer scratch.deinit(allocator); const bytes = try filigree.fixtures.createWithOutlines(allocator); var outline_atlas = try Atlas.initFromOwnedBytes( allocator, &scratch, bytes, 20, test_atlas_cache_limits, test_output_limits, ); defer outline_atlas.deinit(); const outline_metrics = outline_atlas.metrics(); const face = outline_atlas.backend.outline.font.face; const upem: f32 = @floatFromInt(face.units_per_em); try std.testing.expectApproxEqAbs( @as(f32, @floatFromInt(face.ascender)) * 20.0 / upem, outline_metrics.ascent, 0.001, ); try std.testing.expectApproxEqAbs( @as(f32, @floatFromInt(-@as(i32, face.descender))) * 20.0 / upem, outline_metrics.descent, 0.001, ); try std.testing.expect(outline_metrics.height() > 0); const glyphs = [_]BitmapGlyph{.{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }}; var bitmap_atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer bitmap_atlas.deinit(); const bitmap_metrics = bitmap_atlas.metrics(); try std.testing.expectApproxEqAbs(@as(f32, 2), bitmap_metrics.ascent, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 0), bitmap_metrics.descent, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 2), bitmap_metrics.height(), 0.001);}test "fallback face segments preserve primary runs and paint final notdef" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const allocator = failing.allocator(); var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 }); defer scratch.deinit(allocator); const primary_bytes = try filigree.fixtures.createWithOutlines(allocator); var primary = try Atlas.initFromOwnedBytes( allocator, &scratch, primary_bytes, 18, test_atlas_cache_limits, test_output_limits, ); defer primary.deinit(); const fallback_bytes = try filigree.fixtures.createFallbackWithOutlines(allocator); var fallback_atlas = try Atlas.initFromOwnedBytes( allocator, &scratch, fallback_bytes, 18, test_atlas_cache_limits, test_output_limits, ); defer fallback_atlas.deinit(); var fallback = try fallback_mod.Engine.init(allocator, .{ .max_source_units = 32, .cache_entries = 8, .cache_payload_bytes = 1024, }, test_output_limits); defer fallback.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 1, .image_index = 3, .atlas = &primary, }}; const fallback_entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 9, .atlas = &fallback_atlas, }}; const atlases = AtlasSet{ .entries = &entries, .fallback_entries = &fallback_entries, .fallback = &fallback, }; const content = "A\u{3b2}B\u{3bb}"; const text = UiText{ .content = content, .font_asset_id = 1, .point_size = 18, .line_height = 1, }; const plan = try LinePlan.initComposite(&primary, &atlases, text, content, 0, 0); try std.testing.expectEqual(@as(usize, 4), plan.run.glyphs.len); try std.testing.expectEqual(@as(usize, 4), plan.metric_spans.len); try std.testing.expectEqual(@as(u32, 3), plan.metric_spans[plan.glyph_sources[0].segment_index].image_index); try std.testing.expectEqual(@as(u32, 9), plan.metric_spans[plan.glyph_sources[1].segment_index].image_index); try std.testing.expectEqual(@as(u32, 3), plan.metric_spans[plan.glyph_sources[2].segment_index].image_index); try std.testing.expectEqual(@as(u32, 9), plan.metric_spans[plan.glyph_sources[3].segment_index].image_index); try std.testing.expectEqual(@as(u32, 0), plan.run.glyphs[3].glyph_id); try std.testing.expect(!fallback_atlas.glyph_index.contains(0)); const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 96, .height = 24 }, .root = .{ .widget_id = 1, .children = &.{.{ .widget_id = 2, .kind = .label, .text = text, .size = .{ .width = 96, .height = 24 }, }}, }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{ .resolvers = frameResolvers(&atlases), }); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); try appendFrameCommands(&commands, frame, &atlases, 1, 0); var glyph_images: [3]u32 = undefined; var glyph_count: usize = 0; var fill_count: usize = 0; for (commands.items()) |item| { switch (item.kind) { .glyph => { glyph_images[glyph_count] = item.image_index; glyph_count += 1; }, .fill => fill_count += 1, else => {}, } } try std.testing.expectEqual(@as(usize, 3), glyph_count); try std.testing.expectEqualSlices(u32, &.{ 3, 9, 3 }, &glyph_images); try std.testing.expectEqual(@as(usize, 4), fill_count); const primary_plan = try textCaretPlan(&atlases, .{ .content = "AB", .font_asset_id = 1, .point_size = 18, .line_height = 1, }); try std.testing.expectEqual(@as(usize, 0), primary_plan.plan.glyph_sources.len); try expectGlyphRunEqual(try primary.shape("AB"), primary_plan.plan.run); const primary_font = primary.outlineFont().?; const fallback_font = fallback_atlas.outlineFont().?; _ = try fallback.segments(primary_font, fallback_font, content); failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; for (0..1024) |_| { _ = try fallback.segments(primary_font, fallback_font, content); } try std.testing.expect(!failing.has_induced_failure);}fn appendComposerPromptCommands( atlases: *const AtlasSet, workspace: *gui.frame.Workspace, commands: *command.CommandBuffer,) !void { const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 256, .height = 24 }, .root = .{ .widget_id = 1, .children = &.{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "\u{276f} review styled text", .font_asset_id = 1, .point_size = 18, .line_height = 1, }, .size = .{ .width = 256, .height = 24 }, }}, }, }; const frame = try workspace.buildSurface(&surface, .{ .resolvers = frameResolvers(atlases), }); try appendFrameCommands(commands, frame, atlases, 1, 0);}test "composer prompt glyph renders exactly with and without fallback face" { const allocator = std.testing.allocator; var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 }); defer scratch.deinit(allocator); const primary_bytes = try filigree.fixtures.createWithOutlines(allocator); var primary = try Atlas.initFromOwnedBytes( allocator, &scratch, primary_bytes, 18, test_atlas_cache_limits, test_output_limits, ); defer primary.deinit(); const fallback_bytes = try filigree.fixtures.createFallbackWithOutlines(allocator); var fallback_atlas = try Atlas.initFromOwnedBytes( allocator, &scratch, fallback_bytes, 18, test_atlas_cache_limits, test_output_limits, ); defer fallback_atlas.deinit(); var fallback = try fallback_mod.Engine.init(allocator, .{ .max_source_units = 64, .cache_entries = 4, .cache_payload_bytes = 1024, }, test_output_limits); defer fallback.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 1, .image_index = 3, .atlas = &primary }}; const fallback_entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 9, .atlas = &fallback_atlas }}; const atlas_sets = [_]AtlasSet{ .{ .entries = &entries }, .{ .entries = &entries, .fallback_entries = &fallback_entries, .fallback = &fallback }, }; var workspaces = [_]gui.frame.Workspace{ gui.frame.Workspace.init(allocator), gui.frame.Workspace.init(allocator) }; defer for (&workspaces) |*workspace| workspace.deinit(); var buffers = [_]command.CommandBuffer{ command.CommandBuffer.init(allocator), command.CommandBuffer.init(allocator) }; defer for (&buffers) |*buffer| buffer.deinit(); for (&atlas_sets, 0..) |*atlases, index| { try appendComposerPromptCommands(atlases, &workspaces[index], &buffers[index]); } try std.testing.expectEqual(buffers[0].items().len, buffers[1].items().len); for (buffers[0].items(), buffers[1].items()) |without_fallback, with_fallback| { try std.testing.expect(std.meta.eql(without_fallback, with_fallback)); }}test "outline atlas scratch admits its measured maximum and rejects one byte less" { const allocator = std.testing.allocator; var survey = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 }); defer survey.deinit(allocator); const survey_bytes = try filigree.fixtures.createWithOutlines(allocator); var surveyed = try Atlas.initFromOwnedBytes( allocator, &survey, survey_bytes, 20, test_atlas_cache_limits, test_output_limits, ); surveyed.deinit(); const measured = survey.status().high_water_bytes; try std.testing.expect(measured > 0); var exact = try AtlasScratch.init(allocator, .{ .bytes = measured }); defer exact.deinit(allocator); const exact_bytes = try filigree.fixtures.createWithOutlines(allocator); var admitted = try Atlas.initFromOwnedBytes( allocator, &exact, exact_bytes, 20, test_atlas_cache_limits, test_output_limits, ); admitted.deinit(); try std.testing.expectEqual(measured, exact.status().high_water_bytes); try std.testing.expectEqual(@as(usize, 0), exact.status().exhaustions); var short = try AtlasScratch.init(allocator, .{ .bytes = measured - 1 }); defer short.deinit(allocator); const short_bytes = try filigree.fixtures.createWithOutlines(allocator); try std.testing.expectError( error.OutOfMemory, Atlas.initFromOwnedBytes( allocator, &short, short_bytes, 20, test_atlas_cache_limits, test_output_limits, ), ); try std.testing.expectEqual(@as(usize, 1), short.status().epochs); try std.testing.expect(short.status().exhaustions > 0);}test "appendFrameCommands keeps glyph runs inside their line box" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'g', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const child = [_]gui.model.UiNode{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "Ag\ngA", .point_size = 16, .line_height = 1.5 }, .paint = .{ .foreground = .{ .r = 255, .g = 255, .b = 255, .a = 255 } }, .size = .{ .width = 64, .height = 48 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 80, .height = 64 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); var block_y: f32 = 0; for (frame.widgets) |widget| { if (widget.text != null) block_y = widget.rect.y; } const line_height: f32 = 24; const half_leading: f32 = (line_height - 2) / 2; var glyph_index: usize = 0; for (commands.items()) |item| { if (item.kind != .glyph) continue; const line_index: f32 = if (glyph_index < 2) 0 else 1; const line_top = block_y + line_index * line_height; try std.testing.expectApproxEqAbs(line_top + half_leading, item.rect.y, 0.001); try std.testing.expect(item.rect.y >= line_top); try std.testing.expect(item.rect.y + item.rect.height <= line_top + line_height); glyph_index += 1; } try std.testing.expectEqual(@as(usize, 4), glyph_index);}test "appendFrameCommands paints the glyph under a block cursor in the background color" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'm', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'e', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const background = gui.model.UiColor{ .r = 30, .g = 33, .b = 39, .a = 255 }; const child = [_]gui.model.UiNode{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "me", .point_size = 16, .line_height = 1.5 }, .text_selection = .{ .cursor_visible = true, .cursor_block = true, .cursor_byte_offset = 0 }, .paint = .{ .foreground = .{ .r = 220, .g = 223, .b = 228, .a = 255 }, .background = background, }, .size = .{ .width = 64, .height = 24 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 80, .height = 32 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); var saw_fill = false; var glyph_colors: [2]Color = undefined; var glyph_count: usize = 0; for (commands.items()) |item| { switch (item.kind) { .fill => saw_fill = true, .glyph => { glyph_colors[glyph_count] = item.color; glyph_count += 1; }, else => {}, } } try std.testing.expect(saw_fill); try std.testing.expectEqual(@as(usize, 2), glyph_count); try std.testing.expectEqual(background.r, glyph_colors[0].r); try std.testing.expectEqual(background.g, glyph_colors[0].g); try std.testing.expectEqual(background.b, glyph_colors[0].b); try std.testing.expectEqual(@as(u8, 220), glyph_colors[1].r);}test "appendFrameCommands scales glyph commands to text point size" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0x81, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const child = [_]gui.model.UiNode{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "A", .point_size = 32, .line_height = 1.0 }, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 64, .height = 40 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 80, .height = 48 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); try std.testing.expect(commands.items().len > 0); try std.testing.expectApproxEqAbs(@as(f32, 16), commands.items()[0].rect.width, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 4), commands.items()[0].rect.height, 0.001);}test "appendFrameCommands scales glyph geometry by device scale and keeps atlas sources" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0x81, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const child = [_]gui.model.UiNode{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "A", .point_size = 16, .line_height = 1.0 }, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 64, .height = 40 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 80, .height = 48 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; var logical = command.CommandBuffer.init(allocator); defer logical.deinit(); try appendFrameCommands(&logical, frame, &atlases, 1, 0); var device = command.CommandBuffer.init(allocator); defer device.deinit(); try appendFrameCommands(&device, frame, &atlases, 2, 0); try std.testing.expectEqual(logical.items().len, device.items().len); for (logical.items(), device.items()) |one, two| { try std.testing.expectEqual(one.kind, two.kind); try std.testing.expectApproxEqAbs(one.rect.x * 2, two.rect.x, 0.001); try std.testing.expectApproxEqAbs(one.rect.y * 2, two.rect.y, 0.001); try std.testing.expectApproxEqAbs(one.rect.width * 2, two.rect.width, 0.001); try std.testing.expectApproxEqAbs(one.rect.height * 2, two.rect.height, 0.001); try std.testing.expectApproxEqAbs(one.clip.x * 2, two.clip.x, 0.001); try std.testing.expectEqual(one.source.x, two.source.x); try std.testing.expectEqual(one.source.width, two.source.width); }}test "appendFrameCommands draws text selection fills before glyph commands" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const child = [_]gui.model.UiNode{.{ .widget_id = 2, .kind = .text_input, .text = .{ .content = "ABCD", .point_size = 16, .line_height = 1.0 }, .text_selection = .{ .cursor_visible = true, .cursor_byte_offset = 2, .selection_active = true, .selection_anchor_byte_offset = 1, .selection_focus_byte_offset = 3, }, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 64, .height = 20 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 80, .height = 32 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); try std.testing.expectEqual(@as(usize, 6), commands.items().len); try std.testing.expectEqual(command.Kind.fill, commands.items()[0].kind); try std.testing.expectEqual(command.Kind.glyph, commands.items()[1].kind); try std.testing.expectEqual(command.Kind.fill, commands.items()[5].kind); try std.testing.expectApproxEqAbs(@as(f32, 8), commands.items()[0].rect.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 16), commands.items()[0].rect.width, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 16), commands.items()[5].rect.x, 0.001); try std.testing.expect(commands.items()[0].rect.width > commands.items()[5].rect.width);}test "styled text runs preserve layout caret and selection geometry" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const runs = [_]UiTextRun{ .{ .byte_start = 1, .byte_end = 2, .foreground = .{ .r = 220, .g = 30, .b = 20, .a = 255 }, .background = .{ .r = 15, .g = 35, .b = 90, .a = 180 }, .underline = true, }, .{ .byte_start = 2, .byte_end = 4, .foreground = .{ .r = 20, .g = 190, .b = 70, .a = 255 }, .strikethrough = true, }, }; const plain = UiText{ .content = "ABCD", .point_size = 16, .line_height = 1 }; const styled = UiText{ .content = plain.content, .runs = &runs, .point_size = plain.point_size, .line_height = plain.line_height, .wrap_width = plain.wrap_width, }; const selection = UiTextSelection{ .cursor_visible = true, .cursor_byte_offset = 3, .selection_active = true, .selection_anchor_byte_offset = 1, .selection_focus_byte_offset = 4, }; try std.testing.expectEqual(try measure(&atlas, plain), try measure(&atlas, styled)); try std.testing.expectEqual(@as(usize, 1), atlas.cacheStatus().measure.entries); const nodes = [_]UiNode{ .{ .widget_id = 2, .kind = .label, .text = plain, .text_selection = selection, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 64, .height = 16 }, }, .{ .widget_id = 2, .kind = .label, .text = styled, .text_selection = selection, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 64, .height = 16 }, }, }; const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = &entries }; var frames = [_]gui.frame.Workspace{ gui.frame.Workspace.init(allocator), gui.frame.Workspace.init(allocator), }; defer for (&frames) |*frame| frame.deinit(); var buffers = [_]command.CommandBuffer{ command.CommandBuffer.init(allocator), command.CommandBuffer.init(allocator), }; defer for (&buffers) |*buffer| buffer.deinit(); var widget_geometry: [2]WidgetFrame = undefined; for (0..nodes.len) |index| { const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 64, .height = 16 }, .root = .{ .widget_id = 1, .children = nodes[index .. index + 1] }, }; const frame = try frames[index].buildSurface(&surface, .{}); widget_geometry[index] = frame.widgets[1]; try appendFrameCommands(&buffers[index], frame, &atlases, 1, 0); } try std.testing.expectEqual(widget_geometry[0].rect, widget_geometry[1].rect); try std.testing.expectEqual(widget_geometry[0].visible_rect, widget_geometry[1].visible_rect); try std.testing.expectEqual(widget_geometry[0].constraints, widget_geometry[1].constraints); try std.testing.expectEqual(widget_geometry[0].content_size, widget_geometry[1].content_size); var plain_glyphs: [4]command.Command = undefined; var styled_glyphs: [4]command.Command = undefined; var plain_count: usize = 0; var styled_count: usize = 0; for (buffers[0].items()) |item| { if (item.kind != .glyph) continue; plain_glyphs[plain_count] = item; plain_count += 1; } for (buffers[1].items()) |item| { if (item.kind != .glyph) continue; styled_glyphs[styled_count] = item; styled_count += 1; } try std.testing.expectEqual(@as(usize, 4), plain_count); try std.testing.expectEqual(plain_count, styled_count); for (plain_glyphs, styled_glyphs) |plain_glyph, styled_glyph| { try std.testing.expectEqual(plain_glyph.rect, styled_glyph.rect); try std.testing.expectEqual(plain_glyph.source, styled_glyph.source); try std.testing.expectEqual(plain_glyph.clip, styled_glyph.clip); } try std.testing.expectEqual(@as(u8, 12), styled_glyphs[0].color.r); try std.testing.expectEqual(@as(u8, 220), styled_glyphs[1].color.r); try std.testing.expectEqual(@as(u8, 20), styled_glyphs[2].color.r); try std.testing.expectEqual(@as(u8, 20), styled_glyphs[3].color.r); const plain_commands = buffers[0].items(); const styled_commands = buffers[1].items(); try std.testing.expectEqual(@as(usize, 6), plain_commands.len); try std.testing.expectEqual(@as(usize, 9), styled_commands.len); try std.testing.expectEqual(command.Kind.fill, styled_commands[0].kind); try std.testing.expectEqual(@as(u8, 15), styled_commands[0].color.r); try std.testing.expectApproxEqAbs(@as(f32, 8), styled_commands[0].rect.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 8), styled_commands[0].rect.width, 0.001); try std.testing.expectEqual(command.Kind.fill, styled_commands[6].kind); try std.testing.expectEqual(@as(u8, 220), styled_commands[6].color.r); try std.testing.expectEqual(command.Kind.fill, styled_commands[7].kind); try std.testing.expectEqual(@as(u8, 20), styled_commands[7].color.r); try std.testing.expectEqual(plain_commands[0].rect, styled_commands[1].rect); try std.testing.expectEqual(plain_commands[plain_commands.len - 1].rect, styled_commands[styled_commands.len - 1].rect);}test "resolved equal metric slots preserve measure and command bytes" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const plain = UiText{ .content = "ABC", .point_size = 16, .line_height = 1, .wrap_width = 16, }; const styles = [_]UiTextStyle{.{ .font_asset_id = plain.font_asset_id, .point_size = plain.point_size, }}; const runs = [_]UiTextRun{.{ .byte_start = 1, .byte_end = 2, .style_slot = 1, }}; const slotted = UiText{ .content = plain.content, .styles = &styles, .runs = &runs, .point_size = plain.point_size, .line_height = plain.line_height, .wrap_width = plain.wrap_width, }; try std.testing.expectEqual(try measure(&atlas, plain), try measure(&atlas, slotted)); try std.testing.expectEqual(@as(usize, 1), atlas.cacheStatus().measure.entries); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 4, .atlas = &atlas, }}; const atlases = AtlasSet{ .entries = &entries }; var workspaces = [_]gui.frame.Workspace{ gui.frame.Workspace.init(allocator), gui.frame.Workspace.init(allocator), }; defer for (&workspaces) |*workspace| workspace.deinit(); var buffers = [_]command.CommandBuffer{ command.CommandBuffer.init(allocator), command.CommandBuffer.init(allocator), }; defer for (&buffers) |*buffer| buffer.deinit(); const texts = [_]UiText{ plain, slotted }; for (texts, 0..) |text, index| { const tree = gui.model.UiSurfaceTree{ .available_size = .{ .width = 64, .height = 24 }, .root = .{ .widget_id = 1, .children = &.{.{ .widget_id = 2, .kind = .text_input, .text = text, .text_selection = .{ .cursor_visible = true, .cursor_byte_offset = 2, .selection_active = true, .selection_anchor_byte_offset = 0, .selection_focus_byte_offset = 3, }, .size = .{ .width = 64, .height = 48 }, }}, }, }; const frame = try workspaces[index].buildSurface(&tree, .{ .resolvers = frameResolvers(&atlases), }); try appendFrameCommands(&buffers[index], frame, &atlases, 1, 0); } try std.testing.expectEqual(buffers[0].items().len, buffers[1].items().len); for (buffers[0].items(), buffers[1].items()) |plain_command, slotted_command| { try std.testing.expect(std.meta.eql(plain_command, slotted_command)); }}test "mixed metric measurement keys distinguish alternate atlas sets" { const allocator = std.testing.allocator; const base_glyphs = [_]BitmapGlyph{.{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF }, }}; const narrow_glyphs = [_]BitmapGlyph{.{ .codepoint = 'B', .rows = &.{ 0xFF, 0xF0, 0xFF, 0xF0 }, }}; const wide_glyphs = [_]BitmapGlyph{.{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xF0 }, }}; var base_atlas = try Atlas.initFromBitmapGlyphs( allocator, &base_glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer base_atlas.deinit(); var narrow_atlas = try Atlas.initFromBitmapGlyphs( allocator, &narrow_glyphs, .{ .width = 12, .height = 2, .stride = 2 }, 16, test_atlas_cache_limits, ); defer narrow_atlas.deinit(); var wide_atlas = try Atlas.initFromBitmapGlyphs( allocator, &wide_glyphs, .{ .width = 20, .height = 2, .stride = 3 }, 16, test_atlas_cache_limits, ); defer wide_atlas.deinit(); const styles = [_]UiTextStyle{.{ .font_asset_id = 2, .point_size = 16, }}; const runs = [_]UiTextRun{.{ .byte_start = 1, .byte_end = 2, .style_slot = 1, }}; const text = UiText{ .content = "AB", .runs = &runs, .styles = &styles, .font_asset_id = 1, .point_size = 16, .line_height = 1, }; const narrow_entries = [_]AtlasSet.Entry{ .{ .face = 1, .image_index = 0, .atlas = &base_atlas }, .{ .face = 2, .image_index = 1, .atlas = &narrow_atlas }, }; const wide_entries = [_]AtlasSet.Entry{ .{ .face = 1, .image_index = 0, .atlas = &base_atlas }, .{ .face = 2, .image_index = 2, .atlas = &wide_atlas }, }; const narrow = AtlasSet{ .entries = &narrow_entries }; const wide = AtlasSet{ .entries = &wide_entries }; try std.testing.expectEqual(@as(f32, 20), (try measureSet(&narrow, text)).width); try std.testing.expectEqual(@as(f32, 28), (try measureSet(&wide, text)).width); try std.testing.expectEqual(@as(usize, 2), base_atlas.cacheStatus().measure.entries);}test "mixed metric runs share a baseline and retain per glyph atlas identity" { const allocator = std.testing.allocator; const base_glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, }; const large_glyphs = [_]BitmapGlyph{ .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0, } }, }; var base_atlas = try Atlas.initFromBitmapGlyphs( allocator, &base_glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer base_atlas.deinit(); var large_atlas = try Atlas.initFromBitmapGlyphs( allocator, &large_glyphs, .{ .width = 12, .height = 4, .stride = 2 }, 24, test_atlas_cache_limits, ); defer large_atlas.deinit(); const styles = [_]UiTextStyle{.{ .font_asset_id = 2, .point_size = 24, }}; const runs = [_]UiTextRun{.{ .byte_start = 1, .byte_end = 2, .style_slot = 1, .background = .{ .r = 12, .g = 24, .b = 48, .a = 180 }, .underline = true, }}; const text = UiText{ .content = "ABC", .runs = &runs, .styles = &styles, .font_asset_id = 1, .point_size = 16, .line_height = 1, }; const entries = [_]AtlasSet.Entry{ .{ .face = 1, .image_index = 3, .atlas = &base_atlas }, .{ .face = 2, .image_index = 9, .atlas = &large_atlas }, }; const atlases = AtlasSet{ .entries = &entries }; try std.testing.expectEqual(Size{ .width = 28, .height = 24 }, try measureSet(&atlases, text)); const plan = try LinePlan.initComposite( &base_atlas, &atlases, text, text.content, 0, 0, ); try std.testing.expectEqual(@as(usize, 3), plan.run.glyphs.len); try std.testing.expectEqual(@as(i32, 8 * 64), plan.run.glyphs[0].x_advance); try std.testing.expectEqual(@as(i32, 12 * 64), plan.run.glyphs[1].x_advance); try std.testing.expectEqual(@as(i32, 8 * 64), plan.run.glyphs[2].x_advance); for (0..text.content.len + 1) |byte_offset| { const advance = try textAdvanceForByteOffset(&atlases, text, byte_offset); const hit = try textHitTestAdvance(&atlases, text, advance); try std.testing.expectEqual(byte_offset, hit.byte_offset); try std.testing.expectApproxEqAbs(advance, hit.advance, 0.001); } _ = try base_atlas.shape("CC"); try std.testing.expectEqual(@as(u32, 'A'), plan.run.glyphs[0].glyph_id); try std.testing.expectEqual(@as(u32, 'B'), plan.run.glyphs[1].glyph_id); const tree = gui.model.UiSurfaceTree{ .available_size = .{ .width = 64, .height = 24 }, .root = .{ .widget_id = 1, .children = &.{.{ .widget_id = 2, .kind = .label, .text = text, .text_selection = .{ .selection_active = true, .selection_anchor_byte_offset = 0, .selection_focus_byte_offset = 3, }, .size = .{ .width = 64, .height = 24 }, }}, }, }; var workspace = gui.frame.Workspace.init(allocator); defer workspace.deinit(); const frame = try workspace.buildSurface(&tree, .{ .resolvers = frameResolvers(&atlases), }); var one = command.CommandBuffer.init(allocator); defer one.deinit(); var two = command.CommandBuffer.init(allocator); defer two.deinit(); try appendFrameCommands(&one, frame, &atlases, 1, 0); try appendFrameCommands(&two, frame, &atlases, 2, 0); var one_glyphs: [3]command.Command = undefined; var two_glyphs: [3]command.Command = undefined; var one_count: usize = 0; var two_count: usize = 0; for (one.items()) |item| { if (item.kind != .glyph) continue; one_glyphs[one_count] = item; one_count += 1; } for (two.items()) |item| { if (item.kind != .glyph) continue; two_glyphs[two_count] = item; two_count += 1; } try std.testing.expectEqual(@as(usize, 3), one_count); try std.testing.expectEqual(one_count, two_count); try std.testing.expectEqual(@as(u32, 3), one_glyphs[0].image_index); try std.testing.expectEqual(@as(u32, 9), one_glyphs[1].image_index); try std.testing.expectEqual(@as(u32, 3), one_glyphs[2].image_index); try std.testing.expectApproxEqAbs( one_glyphs[0].rect.y + one_glyphs[0].rect.height, one_glyphs[1].rect.y + one_glyphs[1].rect.height, 0.001, ); try std.testing.expectApproxEqAbs( one_glyphs[1].rect.y + one_glyphs[1].rect.height, one_glyphs[2].rect.y + one_glyphs[2].rect.height, 0.001, ); for (one_glyphs, two_glyphs) |logical, scaled| { try std.testing.expectEqual(logical.image_index, scaled.image_index); try std.testing.expectApproxEqAbs(logical.rect.x * 2, scaled.rect.x, 0.001); try std.testing.expectApproxEqAbs(logical.rect.y * 2, scaled.rect.y, 0.001); try std.testing.expectApproxEqAbs(logical.rect.width * 2, scaled.rect.width, 0.001); try std.testing.expectApproxEqAbs(logical.rect.height * 2, scaled.rect.height, 0.001); }}test "styled text runs rasterize fixed colors gaps decorations and wrapped partitions" { const allocator = std.testing.allocator; const ink = @as([8]u8, @splat(0x80)); const blank = @as([8]u8, @splat(0)); const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &ink }, .{ .codepoint = 'B', .rows = &ink }, .{ .codepoint = ' ', .rows = &blank }, .{ .codepoint = 'C', .rows = &ink }, .{ .codepoint = 'D', .rows = &ink }, .{ .codepoint = 'E', .rows = &ink }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 4, .height = 8, .stride = 1 }, 8, test_atlas_cache_limits, ); defer atlas.deinit(); const base = Color{ .r = 20, .g = 30, .b = 40, .a = 255 }; const clear = Color{ .r = 2, .g = 3, .b = 4, .a = 255 }; const red = Color{ .r = 220, .g = 30, .b = 20, .a = 255 }; const blue = Color{ .r = 10, .g = 40, .b = 100, .a = 255 }; const green = Color{ .r = 20, .g = 200, .b = 60, .a = 255 }; const runs = [_]UiTextRun{ .{ .byte_start = 1, .byte_end = 4, .foreground = red, .background = blue, .underline = true, }, .{ .byte_start = 4, .byte_end = 5, .foreground = green, .strikethrough = true, }, .{ .byte_start = 5, .byte_end = 6, .foreground = .{ .r = 255, .b = 255, .a = 0 }, .background = .{ .a = 0 }, }, }; const children = [_]UiNode{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "AB CDE", .runs = &runs, .point_size = 8, .line_height = 1, .wrap_width = 12, }, .paint = .{ .foreground = base }, .size = .{ .width = 12, .height = 16 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 12, .height = 16 }, .root = .{ .widget_id = 1, .children = &children }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas, }}; const atlases = AtlasSet{ .entries = &entries }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); var pixels = @as([(12 * 16)]u32, @splat(0)); const images = [_]command.Image{atlas.image.image}; try cpu.renderCommandsPackedWithImages( commands.items(), .{ .width = 12, .height = 16, .pixels = &pixels }, clear, .{ .images = &images }, ); try std.testing.expectEqual(cpu.packRgba(base), pixels[0 + 2 * 12]); try std.testing.expectEqual(cpu.packRgba(clear), pixels[2 + 2 * 12]); try std.testing.expectEqual(cpu.packRgba(red), pixels[4 + 2 * 12]); try std.testing.expectEqual(cpu.packRgba(blue), pixels[6 + 2 * 12]); try std.testing.expectEqual(cpu.packRgba(blue), pixels[10 + 2 * 12]); try std.testing.expectEqual(cpu.packRgba(red), pixels[6 + 7 * 12]); try std.testing.expectEqual(cpu.packRgba(red), pixels[0 + 10 * 12]); try std.testing.expectEqual(cpu.packRgba(blue), pixels[2 + 10 * 12]); try std.testing.expectEqual(cpu.packRgba(red), pixels[2 + 15 * 12]); try std.testing.expectEqual(cpu.packRgba(green), pixels[4 + 10 * 12]); try std.testing.expectEqual( cpu.packRgba(.{ .r = 11, .g = 102, .b = 32, .a = 255 }), pixels[6 + 12 * 12], ); try std.testing.expectEqual(cpu.packRgba(clear), pixels[8 + 10 * 12]); try std.testing.expectEqual(cpu.packRgba(clear), pixels[10 + 10 * 12]);}test "retained text run color changes damage only the styled glyph" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'C', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'D', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const red = [_]UiTextRun{.{ .byte_start = 1, .byte_end = 2, .foreground = .{ .r = 220, .g = 30, .b = 20 }, }}; const blue = [_]UiTextRun{.{ .byte_start = 1, .byte_end = 2, .foreground = .{ .r = 20, .g = 80, .b = 220 }, }}; const nodes = [_]UiNode{ .{ .widget_id = 2, .kind = .label, .text = .{ .content = "ABCD", .runs = &red, .point_size = 16, .line_height = 1 }, .size = .{ .width = 32, .height = 16 }, }, .{ .widget_id = 2, .kind = .label, .text = .{ .content = "ABCD", .runs = &blue, .point_size = 16, .line_height = 1 }, .size = .{ .width = 32, .height = 16 }, }, }; const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = &entries }; var frames = [_]gui.frame.Workspace{ gui.frame.Workspace.init(allocator), gui.frame.Workspace.init(allocator), }; defer for (&frames) |*frame| frame.deinit(); var buffers = [_]command.CommandBuffer{ command.CommandBuffer.init(allocator), command.CommandBuffer.init(allocator), }; defer for (&buffers) |*buffer| buffer.deinit(); const fragment_id = command.FragmentId{ .root_id = 1, .element_id = 2, .namespace = command.fragment_namespace_widget, .part = command.fragment_part_text, }; for (0..nodes.len) |index| { const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 32, .height = 16 }, .root = .{ .widget_id = 1, .children = nodes[index .. index + 1] }, }; const frame = try frames[index].buildSurface(&surface, .{}); try appendFrameCommands(&buffers[index], frame, &atlases, 1, 0); try buffers[index].commitFragment(fragment_id, 0); } var retained = gui.paint.RetainedCommands.init(allocator); defer retained.deinit(); try retained.retain(&buffers[0]); const damage = try retained.diff( &buffers[1], &.{Region.full(32, 16)}, 32, 16, ); const narrowed = switch (damage) { .semantic => return error.ExpectedNarrowedDamage, .narrowed => |value| value, }; try std.testing.expect(narrowed.slice().len > 0); try std.testing.expect(narrowed.slice().len <= 2); const bounds = narrowed.bounding().?; try std.testing.expect(bounds.x >= 7); try std.testing.expect(bounds.x <= 8); try std.testing.expect(bounds.width <= 10); try std.testing.expect(bounds.height <= 4);}test "warmed identical styled text measure emission and retained diff need no allocation or damage" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const allocator = failing.allocator(); const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const runs = [_]UiTextRun{.{ .byte_start = 0, .byte_end = 1, .foreground = .{ .r = 220, .g = 30, .b = 20 }, .background = .{ .r = 10, .g = 20, .b = 40, .a = 160 }, .underline = true, }}; const child = [_]UiNode{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "AB", .runs = &runs, .point_size = 16, .line_height = 1 }, .size = .{ .width = 16, .height = 16 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 16, .height = 16 }, .root = .{ .widget_id = 1, .children = &child }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = &entries }; const measured = try measure(&atlas, child[0].text.?); const fragment_id = command.FragmentId{ .root_id = 1, .element_id = 2, .namespace = command.fragment_namespace_widget, .part = command.fragment_part_text, }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); try commands.commitFragment(fragment_id, 0); var retained = gui.paint.RetainedCommands.init(allocator); defer retained.deinit(); try retained.retain(&commands); try appendFrameCommands(&commands, frame, &atlases, 1, 0); try commands.commitFragment(fragment_id, 0); try std.testing.expect((try retained.diff(&commands, &.{}, 16, 16)) == .semantic); try retained.retain(&commands); failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; for (0..8) |_| { try std.testing.expectEqual(measured, try measure(&atlas, child[0].text.?)); try appendFrameCommands(&commands, frame, &atlases, 1, 0); try std.testing.expectEqual(@as(usize, 4), commands.items().len); try commands.commitFragment(fragment_id, 0); try std.testing.expect((try retained.diff(&commands, &.{}, 16, 16)) == .semantic); try retained.retain(&commands); } try std.testing.expect(!failing.has_induced_failure);}test "warmed mixed metric measure planning emission and retained diff need no allocation" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const allocator = failing.allocator(); const base_glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xFF } }, }; const large_glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0 } }, .{ .codepoint = 'B', .rows = &.{ 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0 } }, }; var base_atlas = try Atlas.initFromBitmapGlyphs( allocator, &base_glyphs, .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer base_atlas.deinit(); var large_atlas = try Atlas.initFromBitmapGlyphs( allocator, &large_glyphs, .{ .width = 12, .height = 3, .stride = 2 }, 24, test_atlas_cache_limits, ); defer large_atlas.deinit(); const styles = [_]UiTextStyle{.{ .font_asset_id = 2, .point_size = 24, }}; const runs = [_]UiTextRun{.{ .byte_start = 1, .byte_end = 2, .style_slot = 1, .foreground = .{ .r = 220, .g = 30, .b = 20 }, .background = .{ .r = 10, .g = 20, .b = 40, .a = 160 }, .underline = true, }}; const text = UiText{ .content = "AB", .runs = &runs, .styles = &styles, .font_asset_id = 1, .point_size = 16, .line_height = 1, }; const child = [_]UiNode{.{ .widget_id = 2, .kind = .text_input, .text = text, .size = .{ .width = 24, .height = 24 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 24, .height = 24 }, .root = .{ .widget_id = 1, .children = &child }, }; const entries = [_]AtlasSet.Entry{ .{ .face = 1, .image_index = 1, .atlas = &base_atlas }, .{ .face = 2, .image_index = 2, .atlas = &large_atlas }, }; const atlases = AtlasSet{ .entries = &entries }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{ .resolvers = frameResolvers(&atlases), }); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const measured = try measureSet(&atlases, text); _ = try LinePlan.initComposite(&base_atlas, &atlases, text, text.content, 0, 0); const fragment_id = command.FragmentId{ .root_id = 1, .element_id = 2, .namespace = command.fragment_namespace_widget, .part = command.fragment_part_text, }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); try commands.commitFragment(fragment_id, 0); var retained = gui.paint.RetainedCommands.init(allocator); defer retained.deinit(); try retained.retain(&commands); try appendFrameCommands(&commands, frame, &atlases, 1, 0); try commands.commitFragment(fragment_id, 0); try std.testing.expect((try retained.diff(&commands, &.{}, 24, 24)) == .semantic); try retained.retain(&commands); failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; for (0..8) |_| { try std.testing.expectEqual(measured, try measureSet(&atlases, text)); _ = try LinePlan.initComposite(&base_atlas, &atlases, text, text.content, 0, 0); try appendFrameCommands(&commands, frame, &atlases, 1, 0); try commands.commitFragment(fragment_id, 0); try std.testing.expect((try retained.diff(&commands, &.{}, 24, 24)) == .semantic); try retained.retain(&commands); } try std.testing.expect(!failing.has_induced_failure);}test "measure and paint reject malformed text runs" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{.{ .codepoint = 'A', .rows = &.{0xFF} }}; var atlas = try Atlas.initFromBitmapGlyphs( allocator, &glyphs, .{ .width = 8, .height = 1, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const runs = [_]UiTextRun{.{ .byte_start = 0, .byte_end = 2 }}; const text = UiText{ .content = "A", .runs = &runs }; try std.testing.expectError(error.InvalidTextRun, measure(&atlas, text)); const child = [_]UiNode{.{ .widget_id = 2, .kind = .label, .text = text, .size = .{ .width = 16, .height = 16 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 16, .height = 16 }, .root = .{ .widget_id = 1, .children = &child }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = &entries }; try std.testing.expectError(error.InvalidTextRun, appendFrameCommands(&commands, frame, &atlases, 1, 0)); try std.testing.expectEqual(@as(usize, 0), commands.items().len);}test "appendFrameCommands draws cursor for empty text input" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const child = [_]gui.model.UiNode{.{ .widget_id = 2, .kind = .text_input, .text = .{ .content = "", .point_size = 16, .line_height = 1.0 }, .text_selection = .{ .cursor_visible = true, .cursor_byte_offset = 0, }, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 64, .height = 20 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 80, .height = 32 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); try std.testing.expectEqual(@as(usize, 1), commands.items().len); try std.testing.expectEqual(command.Kind.fill, commands.items()[0].kind); try std.testing.expectApproxEqAbs(@as(f32, 0), commands.items()[0].rect.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 1), commands.items()[0].rect.width, 0.001); try std.testing.expect(commands.items()[0].rect.height > 0);}test "shape cache reuses stored runs and survives eviction" { const allocator = std.testing.allocator; const limits = AtlasCacheStorage.Limits{ .measure_entries = 0, .measure_payload_bytes = 0, .shape_entries = 4, .shape_payload_bytes = 64 * 1024, }; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'a', .rows = &.{ 0xFF, 0xFF } }, .{ .codepoint = 'b', .rows = &.{ 0x0F, 0xF0 } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, limits, ); defer atlas.deinit(); const first = try atlas.shape("ab"); try std.testing.expectEqual(@as(usize, 2), first.glyphs.len); const second = try atlas.shape("ab"); try std.testing.expectEqual(first.glyphs.ptr, second.glyphs.ptr); try std.testing.expectEqual(first.total_x_advance, second.total_x_advance); var name_buffer: [32]u8 = undefined; for (0..limits.shape_entries) |index| { const name = try std.fmt.bufPrint(name_buffer[0..], "evict {d}", .{index}); _ = try atlas.shape(name); } const evicted = try atlas.shape("ab"); try std.testing.expectEqual(@as(usize, 2), evicted.glyphs.len); try std.testing.expectEqual(first.total_x_advance, evicted.total_x_advance); try std.testing.expectEqual(@as(u32, 'a'), evicted.glyphs[0].glyph_id); try std.testing.expectEqual(@as(u32, 'b'), evicted.glyphs[1].glyph_id); try std.testing.expectEqual(@as(u64, 1), atlas.cacheStatus().shape.rollovers);}test "Atlas cache capacity matches an independent aligned byte model" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_capacity"), null, null, null, null, null, null, ); } const cases = [_]AtlasCacheStorage.Limits{ .{ .measure_entries = 0, .measure_payload_bytes = 0, .shape_entries = 0, .shape_payload_bytes = 0, }, .{ .measure_entries = 1, .measure_payload_bytes = 1, .shape_entries = 1, .shape_payload_bytes = 1, }, test_atlas_cache_limits, .{ .measure_entries = std.math.maxInt(u32), .measure_payload_bytes = 1, .shape_entries = 1, .shape_payload_bytes = 1, }, }; for (cases) |limits| { try std.testing.expectEqual( try modelAtlasCacheCapacity(limits), try AtlasCacheStorage.Capacity.derive(limits), ); } try expectAtlasCacheCapacityErrors();}test "Atlas cache storage acquires one exact aligned region" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_acquisition"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const capacity = try AtlasCacheStorage.Capacity.derive(test_atlas_cache_limits); var storage = try AtlasCacheStorage.init(failing.allocator(), test_atlas_cache_limits); defer storage.deinit(failing.allocator()); try std.testing.expectEqual(@as(usize, 1), failing.alloc_index); try std.testing.expectEqual(capacity.storage_bytes, failing.allocated_bytes); try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.status().phase); try expectAtlasCacheRegionAddresses(&storage); storage.activate(); try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, storage.status().phase);}test "Atlas cache storage retries after every allocation failure" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_oom"), null, null, null, null, null, null, ); } try std.testing.checkAllAllocationFailures( std.testing.allocator, checkAtlasCacheInitFailures, .{}, );}test "Atlas cache storage preserves exact max and max plus one overload behavior" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_boundaries"), null, null, null, null, null, null, ); } try expectShapeCacheBoundaries(); try expectMeasureCacheBoundaries(); try expectDisabledAtlasCaches();}test "Activated Atlas cache operations make no backing allocation from cold" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_sealed_transitive_risk"), null, null, null, null, null, null, ); } comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_sealed_foreign_risk"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const limits = AtlasCacheStorage.Limits{ .measure_entries = 2, .measure_payload_bytes = 16, .shape_entries = 2, .shape_payload_bytes = 16, }; var storage = try AtlasCacheStorage.init(failing.allocator(), limits); defer storage.deinit(failing.allocator()); storage.activate(); const allocations = failing.allocations; const deallocations = failing.deallocations; const allocated_bytes = failing.allocated_bytes; const freed_bytes = failing.freed_bytes; const resize_index = failing.resize_index; failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; try exerciseColdAtlasCache(&storage); try std.testing.expectEqual(allocations, failing.allocations); try std.testing.expectEqual(deallocations, failing.deallocations); try std.testing.expectEqual(allocated_bytes, failing.allocated_bytes); try std.testing.expectEqual(freed_bytes, failing.freed_bytes); try std.testing.expectEqual(resize_index, failing.resize_index);}test "Atlas cache bounded probes preserve colliding shape keys" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_collisions"), null, null, null, null, null, null, ); } const limits = AtlasCacheStorage.Limits{ .measure_entries = 0, .measure_payload_bytes = 0, .shape_entries = 2, .shape_payload_bytes = 32, }; var storage = try AtlasCacheStorage.init(std.testing.allocator, limits); defer storage.deinit(std.testing.allocator); storage.activate(); const keys = try collidingShapeKeys(storage.capacity.shape_index_slots); _ = try storage.storeShape(keys.first, testShapedRun(11)); _ = try storage.storeShape(keys.second, testShapedRun(22)); try std.testing.expectEqual(@as(i32, 11), storage.lookupShape(keys.first).?.total_x_advance); try std.testing.expectEqual(@as(i32, 22), storage.lookupShape(keys.second).?.total_x_advance);}test "Atlas cache storage keeps region pointers and capacity stable" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_stability"), null, null, null, null, null, null, ); } var storage = try AtlasCacheStorage.init(std.testing.allocator, .{ .measure_entries = 2, .measure_payload_bytes = 16, .shape_entries = 2, .shape_payload_bytes = 16, }); defer storage.deinit(std.testing.allocator); const capacity = storage.capacity; const pointers = atlasCachePointers(storage); storage.activate(); var iteration: usize = 0; while (iteration < 32) : (iteration += 1) { const key = if (iteration % 2 == 0) "aa" else "bb"; _ = storage.storeShape(key, testShapedRun(@intCast(iteration))) catch {}; storage.replaceShape(); } try std.testing.expectEqual(capacity, storage.capacity); try std.testing.expectEqual(pointers, atlasCachePointers(storage));}test "Atlas cache shape operations match a whole epoch reference model" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(AtlasCacheStorage, "gui_text_atlas_cache_differential"), null, null, null, null, null, null, ); } try expectAtlasCacheDifferential();}test "Atlas cache bypasses oversized entries without changing either epoch" { const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'a', .rows = &.{ 0xFF, 0xFF } }, }; const limits = AtlasCacheStorage.Limits{ .measure_entries = 2, .measure_payload_bytes = 128, .shape_entries = 2, .shape_payload_bytes = 128, }; var atlas = try Atlas.initFromBitmapGlyphs( std.testing.allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, limits, ); defer atlas.deinit(); const retained_run = try atlas.shape("a"); const retained_measure = try measure(&atlas, cacheEpochText("a")); const before = atlas.cacheStatus(); const content: [129]u8 = @splat('x'); const first_run = try atlas.shape(&content); const second_run = try atlas.shape(&content); try expectGlyphRunEqual(first_run, second_run); const first_measure = try measure(&atlas, cacheEpochText(&content)); const second_measure = try measure(&atlas, cacheEpochText(&content)); try std.testing.expectEqual(first_measure, second_measure); const after = atlas.cacheStatus(); try std.testing.expectEqual(before.shape.entries, after.shape.entries); try std.testing.expectEqual(before.shape.payload_bytes, after.shape.payload_bytes); try std.testing.expectEqual(before.shape.rollovers, after.shape.rollovers); try std.testing.expectEqual(before.shape.oversize_bypasses + 4, after.shape.oversize_bypasses); try std.testing.expectEqual(before.measure.entries, after.measure.entries); try std.testing.expectEqual(before.measure.payload_bytes, after.measure.payload_bytes); try std.testing.expectEqual(before.measure.rollovers, after.measure.rollovers); try std.testing.expectEqual(before.measure.oversize_bypasses + 2, after.measure.oversize_bypasses); const retained_again = try atlas.shape("a"); try std.testing.expectEqual(retained_run.glyphs.ptr, retained_again.glyphs.ptr); try std.testing.expectEqual(retained_measure, try measure(&atlas, cacheEpochText("a")));}fn expectAtlasCacheCapacityErrors() !void { try std.testing.expectError(error.InvalidMeasureLimits, AtlasCacheStorage.Capacity.derive(.{ .measure_entries = 1, .measure_payload_bytes = 0, .shape_entries = 0, .shape_payload_bytes = 0, })); try std.testing.expectError(error.InvalidShapeLimits, AtlasCacheStorage.Capacity.derive(.{ .measure_entries = 0, .measure_payload_bytes = 0, .shape_entries = 0, .shape_payload_bytes = 1, })); try std.testing.expectError(error.CapacityOverflow, AtlasCacheStorage.Capacity.derive(.{ .measure_entries = 1, .measure_payload_bytes = std.math.maxInt(usize), .shape_entries = 0, .shape_payload_bytes = 0, })); if (@bitSizeOf(usize) > @bitSizeOf(u32)) { try std.testing.expectError(error.EntryLimitTooLarge, AtlasCacheStorage.Capacity.derive(.{ .measure_entries = @as(usize, std.math.maxInt(u32)) + 1, .measure_payload_bytes = 1, .shape_entries = 0, .shape_payload_bytes = 0, })); }}fn expectAtlasCacheRegionAddresses(storage: *const AtlasCacheStorage) !void { const base = @intFromPtr(storage.bytes.ptr); const capacity = storage.capacity; try std.testing.expectEqual( base + capacity.measure_index_offset, @intFromPtr(storage.measure_slots.ptr), ); try std.testing.expectEqual( base + capacity.measure_entries_offset, @intFromPtr(storage.measure_entries.ptr), ); try std.testing.expectEqual( base + capacity.measure_payload_offset, @intFromPtr(storage.measure_payload.bytes.ptr), ); try std.testing.expectEqual( base + capacity.shape_index_offset, @intFromPtr(storage.shape_slots.ptr), ); try std.testing.expectEqual( base + capacity.shape_entries_offset, @intFromPtr(storage.shape_entries.ptr), ); try std.testing.expectEqual( base + capacity.shape_payload_offset, @intFromPtr(storage.shape_payload.bytes.ptr), );}fn checkAtlasCacheInitFailures(allocator: Allocator) !void { var storage = try AtlasCacheStorage.init(allocator, .{ .measure_entries = 3, .measure_payload_bytes = 31, .shape_entries = 5, .shape_payload_bytes = 47, }); storage.deinit(allocator);}fn expectShapeCacheBoundaries() !void { var storage = try AtlasCacheStorage.init(std.testing.allocator, .{ .measure_entries = 0, .measure_payload_bytes = 0, .shape_entries = 2, .shape_payload_bytes = 64, }); defer storage.deinit(std.testing.allocator); storage.activate(); _ = try storage.storeShape("a", testShapedRun(1)); _ = try storage.storeShape("b", testShapedRun(2)); try std.testing.expectEqual(@as(usize, 2), storage.status().shape.entries); _ = try storage.storeShape("c", testShapedRun(3)); try std.testing.expectEqual(@as(usize, 1), storage.status().shape.entries); try std.testing.expectEqual(@as(u64, 1), storage.status().shape.rollovers); try std.testing.expect(storage.lookupShape("a") == null); try std.testing.expectEqual(@as(i32, 3), storage.lookupShape("c").?.total_x_advance); var payload = try shapePayloadBoundaryStorage(); defer payload.deinit(std.testing.allocator); try payload.admitShapePayload(3); try std.testing.expectError(error.EntryTooLarge, payload.admitShapePayload(4)); _ = try payload.storeShape("abc", testShapedRun(4)); const before = payload.status(); try std.testing.expectError( error.EntryTooLarge, payload.storeShape("abcd", testShapedRun(5)), ); const after = payload.status(); try expectEpochUnchangedExceptOversize(before.shape, after.shape);}fn shapePayloadBoundaryStorage() !AtlasCacheStorage { var storage = try AtlasCacheStorage.init(std.testing.allocator, .{ .measure_entries = 0, .measure_payload_bytes = 0, .shape_entries = 2, .shape_payload_bytes = 3, }); storage.activate(); return storage;}fn expectMeasureCacheBoundaries() !void { var storage = try AtlasCacheStorage.init(std.testing.allocator, .{ .measure_entries = 2, .measure_payload_bytes = 3, .shape_entries = 0, .shape_payload_bytes = 0, }); defer storage.deinit(std.testing.allocator); storage.activate(); _ = try storage.storeMeasure(testMeasureKey("a"), .{ .width = 1 }); _ = try storage.storeMeasure(testMeasureKey("b"), .{ .width = 2 }); try std.testing.expectEqual(@as(usize, 2), storage.status().measure.entries); _ = try storage.storeMeasure(testMeasureKey("c"), .{ .width = 3 }); try std.testing.expectEqual(@as(usize, 1), storage.status().measure.entries); try std.testing.expectEqual(@as(u64, 1), storage.status().measure.rollovers); try std.testing.expect(storage.lookupMeasure(testMeasureKey("a")) == null); const before = storage.status(); try std.testing.expectError( error.EntryTooLarge, storage.storeMeasure(testMeasureKey("abcd"), .{ .width = 4 }), ); try expectEpochUnchangedExceptOversize(before.measure, storage.status().measure);}fn expectEpochUnchangedExceptOversize( before: AtlasCacheStorage.EpochStatus, after: AtlasCacheStorage.EpochStatus,) !void { try std.testing.expectEqual(before.entries, after.entries); try std.testing.expectEqual(before.payload_bytes, after.payload_bytes); try std.testing.expectEqual(before.physical_payload_bytes, after.physical_payload_bytes); try std.testing.expectEqual(before.rollovers, after.rollovers); try std.testing.expectEqual(before.disabled_bypasses, after.disabled_bypasses); try std.testing.expectEqual(before.oversize_bypasses + 1, after.oversize_bypasses);}fn expectDisabledAtlasCaches() !void { var storage = try AtlasCacheStorage.init(std.testing.allocator, .{ .measure_entries = 0, .measure_payload_bytes = 0, .shape_entries = 0, .shape_payload_bytes = 0, }); defer storage.deinit(std.testing.allocator); storage.activate(); try std.testing.expectError(error.CacheDisabled, storage.admitShapePayload(0)); try std.testing.expectError( error.CacheDisabled, storage.storeShape("a", testShapedRun(1)), ); try std.testing.expectError( error.CacheDisabled, storage.storeMeasure(testMeasureKey("a"), .{}), ); const status = storage.status(); try std.testing.expectEqual(@as(usize, 0), status.storage_bytes); try std.testing.expectEqual(@as(u64, 1), status.shape.disabled_bypasses); try std.testing.expectEqual(@as(u64, 1), status.measure.disabled_bypasses);}fn exerciseColdAtlasCache(storage: *AtlasCacheStorage) !void { _ = try storage.storeShape("a", testShapedRun(1)); _ = try storage.storeShape("b", testShapedRun(2)); _ = try storage.storeShape("c", testShapedRun(3)); try std.testing.expectEqual(@as(i32, 3), storage.lookupShape("c").?.total_x_advance); _ = try storage.storeMeasure(testMeasureKey("a"), .{ .width = 1 }); _ = try storage.storeMeasure(testMeasureKey("b"), .{ .width = 2 }); _ = try storage.storeMeasure(testMeasureKey("c"), .{ .width = 3 }); try std.testing.expectEqual(@as(f32, 3), storage.lookupMeasure(testMeasureKey("c")).?.width); try std.testing.expectEqual(@as(u64, 1), storage.status().shape.rollovers); try std.testing.expectEqual(@as(u64, 1), storage.status().measure.rollovers);}const CollidingShapeKeys = struct { first: []const u8, second: []const u8,};fn collidingShapeKeys(slot_count: usize) !CollidingShapeKeys { const candidates = [_][]const u8{ "a", "b", "c", "d", "e", "f", "g", "h" }; for (candidates, 0..) |first, first_index| { for (candidates[first_index + 1 ..]) |second| { const first_slot = cacheStartSlot(std.hash_map.hashString(first), slot_count); const second_slot = cacheStartSlot(std.hash_map.hashString(second), slot_count); if (first_slot == second_slot) return .{ .first = first, .second = second }; } } return error.NoCollision;}const AtlasCachePointers = struct { bytes: usize, measure_slots: usize, measure_entries: usize, measure_payload: usize, shape_slots: usize, shape_entries: usize, shape_payload: usize,};fn atlasCachePointers(storage: AtlasCacheStorage) AtlasCachePointers { return .{ .bytes = @intFromPtr(storage.bytes.ptr), .measure_slots = @intFromPtr(storage.measure_slots.ptr), .measure_entries = @intFromPtr(storage.measure_entries.ptr), .measure_payload = @intFromPtr(storage.measure_payload.bytes.ptr), .shape_slots = @intFromPtr(storage.shape_slots.ptr), .shape_entries = @intFromPtr(storage.shape_entries.ptr), .shape_payload = @intFromPtr(storage.shape_payload.bytes.ptr), };}fn testShapedRun(advance: i32) filigree.GlyphRun { return .{ .glyphs = &.{}, .clusters = &.{}, .ligature_carets = &.{}, .total_x_advance = advance, .total_y_advance = 0, .direction = .ltr, .writing_mode = .horizontal, .output_order = .visual, };}fn testMeasureKey(content: []const u8) MeasureKey { return .{ .content = content, .styles = &.{}, .runs = &.{}, .atlas_entries = &.{}, .fallback_entries = &.{}, .fallback_identity = 0, .font_asset_id = 0, .point_size_bits = 0, .line_height_bits = 0, .wrap_width_bits = 0, .device_scale_bits = 0, };}const ShapeCacheReference = struct { limits: AtlasCacheStorage.Limits, keys: [3]?[]const u8 = @splat(null), values: [3]i32 = @splat(0), entries: usize = 0, payload_bytes: usize = 0, rollovers: u64 = 0, oversize_bypasses: u64 = 0, fn lookup(self: ShapeCacheReference, content: []const u8) ?i32 { for (self.keys[0..self.entries], self.values[0..self.entries]) |key, value| { if (std.mem.eql(u8, key.?, content)) return value; } return null; } fn store(self: *ShapeCacheReference, content: []const u8, value: i32) bool { if (content.len > self.limits.shape_payload_bytes) { self.oversize_bypasses +|= 1; return false; } const payload_full = content.len > self.limits.shape_payload_bytes - self.payload_bytes; if (self.entries == self.limits.shape_entries or payload_full) { self.entries = 0; self.payload_bytes = 0; self.rollovers +|= 1; } self.keys[self.entries] = content; self.values[self.entries] = value; self.entries += 1; self.payload_bytes += content.len; return true; }};fn expectAtlasCacheDifferential() !void { const limits = AtlasCacheStorage.Limits{ .measure_entries = 0, .measure_payload_bytes = 0, .shape_entries = 3, .shape_payload_bytes = 8, }; var storage = try AtlasCacheStorage.init(std.testing.allocator, limits); defer storage.deinit(std.testing.allocator); storage.activate(); var reference = ShapeCacheReference{ .limits = limits }; const keys = [_][]const u8{ "a", "bb", "ccc", "dddd", "012345678" }; var state: u32 = 0x9E37_79B9; var step: usize = 0; while (step < 512) : (step += 1) { state = state *% 1_664_525 +% 1_013_904_223; const content = keys[state % keys.len]; try compareAtlasCacheLookup(&storage, reference, content); if (reference.lookup(content) == null) { const value: i32 = @intCast(step); const admitted = reference.store(content, value); if (admitted) { _ = try storage.storeShape(content, testShapedRun(value)); } else { try std.testing.expectError( error.EntryTooLarge, storage.storeShape(content, testShapedRun(value)), ); } } try compareAtlasCacheStatus(storage.status().shape, reference); }}fn compareAtlasCacheLookup( storage: *const AtlasCacheStorage, reference: ShapeCacheReference, content: []const u8,) !void { const expected = reference.lookup(content); const actual = storage.lookupShape(content); try std.testing.expectEqual(expected != null, actual != null); if (expected) |value| try std.testing.expectEqual(value, actual.?.total_x_advance);}fn compareAtlasCacheStatus( actual: AtlasCacheStorage.EpochStatus, expected: ShapeCacheReference,) !void { try std.testing.expectEqual(expected.entries, actual.entries); try std.testing.expectEqual(expected.payload_bytes, actual.payload_bytes); try std.testing.expectEqual(expected.rollovers, actual.rollovers); try std.testing.expectEqual(expected.oversize_bypasses, actual.oversize_bypasses);}fn cacheEpochText(content: []const u8) UiText { return .{ .content = content, .point_size = 16, .line_height = 1 };}fn expectGlyphRunEqual(expected: filigree.GlyphRun, actual: filigree.GlyphRun) !void { try std.testing.expectEqualSlices(filigree.ShapedGlyph, expected.glyphs, actual.glyphs); try std.testing.expectEqualSlices(filigree.Cluster, expected.clusters, actual.clusters); try std.testing.expectEqualSlices(filigree.LigatureCaret, expected.ligature_carets, actual.ligature_carets); try std.testing.expectEqual(expected.total_x_advance, actual.total_x_advance); try std.testing.expectEqual(expected.total_y_advance, actual.total_y_advance); try std.testing.expectEqual(expected.direction, actual.direction); try std.testing.expectEqual(expected.writing_mode, actual.writing_mode); try std.testing.expectEqual(expected.output_order, actual.output_order);}test "bitmap atlas skips glyphs missing from the map without question mark" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{ .{ .codepoint = 'x', .rows = &.{ 0xFF, 0xFF } }, }; var atlas = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer atlas.deinit(); const run = try atlas.shape("xy"); try std.testing.expectEqual(@as(usize, 2), run.glyphs.len); try std.testing.expectEqual(@as(u32, 'x'), run.glyphs[0].glyph_id); try std.testing.expectEqual(std.math.maxInt(u32), run.glyphs[1].glyph_id); var recorder = TestRecorder{}; drawGlyphRun(&recorder, 0, atlas.atlas, run, 0, 0, .{ .a = 255 }); try std.testing.expectEqual(@as(usize, 1), recorder.glyph_count);}test "Atlas shapes fixture bytes and appends frame glyph commands" { const allocator = std.testing.allocator; var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 }); defer scratch.deinit(allocator); const bytes = try filigree.fixtures.createWithOutlines(allocator); var atlas = try Atlas.initFromOwnedBytes( allocator, &scratch, bytes, 18, test_atlas_cache_limits, test_output_limits, ); defer atlas.deinit(); const child = [_]gui.model.UiNode{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "AB" }, .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } }, .size = .{ .width = 32, .height = 18 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 48, .height = 24 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); try std.testing.expect(commands.items().len > 0); try std.testing.expectEqual(command.Kind.glyph, commands.items()[0].kind); try std.testing.expectEqual(@as(u32, 0), commands.items()[0].image_index); try std.testing.expectEqual(@as(u8, 255), commands.items()[0].color.a); try std.testing.expect(atlas.image.image.width > 0);}test "missing terminal glyphs synthesize fills instead of blank advances" { const allocator = std.testing.allocator; var scratch = try AtlasScratch.init(allocator, .{ .bytes = 1024 * 1024 }); defer scratch.deinit(allocator); const bytes = try filigree.fixtures.createWithOutlines(allocator); var atlas = try Atlas.initFromOwnedBytes( allocator, &scratch, bytes, 18, test_atlas_cache_limits, test_output_limits, ); defer atlas.deinit(); const child = [_]gui.model.UiNode{.{ .widget_id = 2, .kind = .label, .text = .{ .content = "\u{258F}\u{280B}\u{276F}" }, .paint = .{ .foreground = .{ .r = 251, .g = 73, .b = 52, .a = 255 } }, .size = .{ .width = 48, .height = 24 }, }}; const surface = gui.model.UiSurfaceTree{ .available_size = .{ .width = 64, .height = 32 }, .root = .{ .widget_id = 1, .children = child[0..], }, }; var frame_workspace = gui.frame.Workspace.init(allocator); defer frame_workspace.deinit(); const frame = try frame_workspace.buildSurface(&surface, .{}); var commands = command.CommandBuffer.init(allocator); defer commands.deinit(); const entries = [_]AtlasSet.Entry{.{ .face = 0, .image_index = 0, .atlas = &atlas }}; const atlases = AtlasSet{ .entries = entries[0..] }; try appendFrameCommands(&commands, frame, &atlases, 1, 0); var fills: usize = 0; for (commands.items()) |item| { if (item.kind == .fill) fills += 1; } try std.testing.expect(fills >= 5); for (commands.items()) |item| { try std.testing.expectEqual(@as(u8, 251), item.color.r); }}test "atlas set picks matching face and nearest downscale size" { const allocator = std.testing.allocator; const glyphs = [_]BitmapGlyph{.{ .codepoint = 'A', .rows = &.{ 0xFF, 0xFF } }}; var small = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 13, test_atlas_cache_limits, ); defer small.deinit(); var body = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 16, test_atlas_cache_limits, ); defer body.deinit(); var mono = try Atlas.initFromBitmapGlyphs( allocator, glyphs[0..], .{ .width = 8, .height = 2, .stride = 1 }, 15, test_atlas_cache_limits, ); defer mono.deinit(); const entries = [_]AtlasSet.Entry{ .{ .face = 0, .image_index = 0, .atlas = &small }, .{ .face = 0, .image_index = 1, .atlas = &body }, .{ .face = 2, .image_index = 2, .atlas = &mono }, }; const atlases = AtlasSet{ .entries = entries[0..] }; const body_pick = atlases.forText(.{ .content = "A", .point_size = 16 }).?; try std.testing.expectEqual(@as(u32, 1), body_pick.image_index); const caption_pick = atlases.forText(.{ .content = "A", .point_size = 13 }).?; try std.testing.expectEqual(@as(u32, 0), caption_pick.image_index); const near_pick = atlases.forText(.{ .content = "A", .point_size = 14 }).?; try std.testing.expectEqual(@as(u32, 1), near_pick.image_index); const mono_pick = atlases.forText(.{ .content = "A", .point_size = 15, .font_asset_id = 2 }).?; try std.testing.expectEqual(@as(u32, 2), mono_pick.image_index); const missing_face_pick = atlases.forText(.{ .content = "A", .point_size = 16, .font_asset_id = 9 }).?; try std.testing.expectEqual(@as(u32, 1), missing_face_pick.image_index); const empty = AtlasSet{}; try std.testing.expect(empty.forText(.{ .content = "A" }) == null);}Complete caller list for paint.TextAtlas.initFromBitmapGlyphs
33 direct callers.
lib.gui.src.paint.text.test_Atlas_cache_bypasses_oversized_entries_without_changing_either_epoch[function] — test source atlib/gui/src/paint/text.zig:6694in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_draws_cursor_for_empty_text_input[function] — test source atlib/gui/src/paint/text.zig:6371in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_draws_text_selection_fills_before_glyph_commands[function] — test source atlib/gui/src/paint/text.zig:5482in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_keeps_glyph_runs_inside_their_line_box[function] — test source atlib/gui/src/paint/text.zig:5265in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_paints_the_glyph_under_a_block_cursor_in_the_background_color[function] — test source atlib/gui/src/paint/text.zig:5322in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_scales_glyph_commands_to_text_point_size[function] — test source atlib/gui/src/paint/text.zig:5386in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_scales_glyph_geometry_by_device_scale_and_keeps_atlas_sources[function] — test source atlib/gui/src/paint/text.zig:5428in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_atlas_metrics_derive_from_face_and_bitmap_cell_geometry[function] — test source atlib/gui/src/paint/text.zig:4971in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_atlas_set_picks_matching_face_and_nearest_downscale_size[function] — test source atlib/gui/src/paint/text.zig:7212in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_bitmap_atlas_measures_and_paints_Unicode_wrapped_text_with_one_line_plan[function] — test source atlib/gui/src/paint/text.zig:4139in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_bitmap_atlas_measures_scaled_multiline_text[function] — test source atlib/gui/src/paint/text.zig:4114in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_bitmap_atlas_rasterizes_cells_and_shapes_ascii_with_fallback[function] — test source atlib/gui/src/paint/text.zig:4079in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_bitmap_atlas_skips_glyphs_missing_from_the_map_without_question_mark[function] — test source atlib/gui/src/paint/text.zig:7095in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_bitmap_wrapped_measurement_reuses_warmed_line_workspace[function] — test source atlib/gui/src/paint/text.zig:4898in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_measure_and_paint_reject_malformed_text_runs[function] — test source atlib/gui/src/paint/text.zig:6336in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_mixed_metric_measurement_keys_distinguish_alternate_atlas_sets[function] — test source atlib/gui/src/paint/text.zig:5763in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_mixed_metric_runs_share_a_baseline_and_retain_per_glyph_atlas_identity[function] — test source atlib/gui/src/paint/text.zig:5833in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_multiline_query_geometry_matches_painted_caret_at_device_scales[function] — test source atlib/gui/src/paint/text.zig:4538in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_multiline_text_geometry_distinguishes_soft_affinity_and_hard_empty_lines[function] — test source atlib/gui/src/paint/text.zig:4195in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_multiline_text_geometry_follows_mixed_metric_line_boxes_and_end_alignment[function] — test source atlib/gui/src/paint/text.zig:4401in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_resolved_equal_metric_slots_preserve_measure_and_command_bytes[function] — test source atlib/gui/src/paint/text.zig:5675in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_retained_text_run_color_changes_damage_only_the_styled_glyph[function] — test source atlib/gui/src/paint/text.zig:6078in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_shape_cache_reuses_stored_runs_and_survives_eviction[function] — test source atlib/gui/src/paint/text.zig:6419in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_styled_text_runs_preserve_layout_caret_and_selection_geometry[function] — test source atlib/gui/src/paint/text.zig:5539in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_styled_text_runs_rasterize_fixed_colors_gaps_decorations_and_wrapped_partitions[function] — test source atlib/gui/src/paint/text.zig:5975in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_warmed_identical_styled_text_measure_emission_and_retained_diff_need_no_allocation_or_damage[function] — test source atlib/gui/src/paint/text.zig:6167in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_warmed_mixed_metric_measure_planning_emission_and_retained_diff_need_no_allocation[function] — test source atlib/gui/src/paint/text.zig:6236in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_warmed_multiline_text_geometry_needs_no_backing_allocation[function] — test source atlib/gui/src/paint/text.zig:4480in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_warmed_visual_line_and_caret_queries_need_no_backing_allocation[function] — test source atlib/gui/src/paint/text.zig:4932in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_widget_text_hit_query_honors_translated_clip_and_clamps_to_cluster_boundaries[function] — test source atlib/gui/src/paint/text.zig:4301in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_wrapped_markers_preserve_glyph_geometry_and_partition_selection_by_visual_line[function] — test source atlib/gui/src/paint/text.zig:4756in nearest public ownertiny.gui.paint.textlib.gui.src.properties.text.SelectionPartitionProperty.property[function] — private source atlib/gui/src/properties/text.zig:81in nearest public ownerlib.gui.src.properties.textlib.gui.src.properties.text.bitmapAtlas[function] — private source atlib/gui/src/properties/text.zig:35in nearest public ownerlib.gui.src.properties.text
Complete caller list for paint.TextAtlasCacheStorage.activate
12 direct callers.
tiny.gui.paint.TextAtlas.initFromBitmapGlyphs[function] atlib/gui/src/paint/text.zig:284lib.gui.src.paint.text.Atlas.initOutlineFromOwnedBytes[function] — private source atlib/gui/src/paint/text.zig:228in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectAtlasCacheDifferential[function] — private source atlib/gui/src/paint/text.zig:7025in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectDisabledAtlasCaches[function] — private source atlib/gui/src/paint/text.zig:6884in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectMeasureCacheBoundaries[function] — private source atlib/gui/src/paint/text.zig:6848in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectShapeCacheBoundaries[function] — private source atlib/gui/src/paint/text.zig:6805in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.initShapeOnlyAtlas[function] — private source atlib/gui/src/paint/text.zig:3939in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.shapePayloadBoundaryStorage[function] — private source atlib/gui/src/paint/text.zig:6837in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Activated_Atlas_cache_operations_make_no_backing_allocation_from_cold[function] — test source atlib/gui/src/paint/text.zig:6566in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_bounded_probes_preserve_colliding_shape_keys[function] — test source atlib/gui/src/paint/text.zig:6616in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_storage_acquires_one_exact_aligned_region[function] — test source atlib/gui/src/paint/text.zig:6502in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_storage_keeps_region_pointers_and_capacity_stable[function] — test source atlib/gui/src/paint/text.zig:6645in nearest public ownertiny.gui.paint.text
Complete caller list for paint.TextAtlasCacheStorage.deinit
13 direct callers.
tiny.gui.paint.TextAtlas.deinit[method] atlib/gui/src/paint/text.zig:376tiny.gui.paint.TextAtlas.initFromBitmapGlyphs[function] atlib/gui/src/paint/text.zig:284lib.gui.src.paint.text.Atlas.initOutlineFromOwnedBytes[function] — private source atlib/gui/src/paint/text.zig:228in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.checkAtlasCacheInitFailures[function] — private source atlib/gui/src/paint/text.zig:6795in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectAtlasCacheDifferential[function] — private source atlib/gui/src/paint/text.zig:7025in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectDisabledAtlasCaches[function] — private source atlib/gui/src/paint/text.zig:6884in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectMeasureCacheBoundaries[function] — private source atlib/gui/src/paint/text.zig:6848in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectShapeCacheBoundaries[function] — private source atlib/gui/src/paint/text.zig:6805in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.initShapeOnlyAtlas[function] — private source atlib/gui/src/paint/text.zig:3939in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Activated_Atlas_cache_operations_make_no_backing_allocation_from_cold[function] — test source atlib/gui/src/paint/text.zig:6566in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_bounded_probes_preserve_colliding_shape_keys[function] — test source atlib/gui/src/paint/text.zig:6616in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_storage_acquires_one_exact_aligned_region[function] — test source atlib/gui/src/paint/text.zig:6502in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_storage_keeps_region_pointers_and_capacity_stable[function] — test source atlib/gui/src/paint/text.zig:6645in nearest public ownertiny.gui.paint.text
Complete caller list for paint.TextAtlasCacheStorage.init
13 direct callers.
tiny.gui.paint.TextAtlas.initFromBitmapGlyphs[function] atlib/gui/src/paint/text.zig:284lib.gui.src.paint.text.Atlas.initOutlineFromOwnedBytes[function] — private source atlib/gui/src/paint/text.zig:228in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.checkAtlasCacheInitFailures[function] — private source atlib/gui/src/paint/text.zig:6795in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectAtlasCacheDifferential[function] — private source atlib/gui/src/paint/text.zig:7025in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectDisabledAtlasCaches[function] — private source atlib/gui/src/paint/text.zig:6884in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectMeasureCacheBoundaries[function] — private source atlib/gui/src/paint/text.zig:6848in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.expectShapeCacheBoundaries[function] — private source atlib/gui/src/paint/text.zig:6805in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.initShapeOnlyAtlas[function] — private source atlib/gui/src/paint/text.zig:3939in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.shapePayloadBoundaryStorage[function] — private source atlib/gui/src/paint/text.zig:6837in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Activated_Atlas_cache_operations_make_no_backing_allocation_from_cold[function] — test source atlib/gui/src/paint/text.zig:6566in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_bounded_probes_preserve_colliding_shape_keys[function] — test source atlib/gui/src/paint/text.zig:6616in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_storage_acquires_one_exact_aligned_region[function] — test source atlib/gui/src/paint/text.zig:6502in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_cache_storage_keeps_region_pointers_and_capacity_stable[function] — test source atlib/gui/src/paint/text.zig:6645in nearest public ownertiny.gui.paint.text
Complete caller list for paint.text.appendFrameCommands
21 direct callers.
lib.gui.src.paint.text.appendComposerPromptCommands[function] — private source atlib/gui/src/paint/text.zig:5138in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_Atlas_shapes_fixture_bytes_and_appends_frame_glyph_commands[function] — test source atlib/gui/src/paint/text.zig:7119in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_draws_cursor_for_empty_text_input[function] — test source atlib/gui/src/paint/text.zig:6371in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_draws_text_selection_fills_before_glyph_commands[function] — test source atlib/gui/src/paint/text.zig:5482in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_keeps_glyph_runs_inside_their_line_box[function] — test source atlib/gui/src/paint/text.zig:5265in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_paints_the_glyph_under_a_block_cursor_in_the_background_color[function] — test source atlib/gui/src/paint/text.zig:5322in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_scales_glyph_commands_to_text_point_size[function] — test source atlib/gui/src/paint/text.zig:5386in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_appendFrameCommands_scales_glyph_geometry_by_device_scale_and_keeps_atlas_sources[function] — test source atlib/gui/src/paint/text.zig:5428in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_bitmap_atlas_measures_and_paints_Unicode_wrapped_text_with_one_line_plan[function] — test source atlib/gui/src/paint/text.zig:4139in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_fallback_face_segments_preserve_primary_runs_and_paint_final_notdef[function] — test source atlib/gui/src/paint/text.zig:5017in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_measure_and_paint_reject_malformed_text_runs[function] — test source atlib/gui/src/paint/text.zig:6336in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_missing_terminal_glyphs_synthesize_fills_instead_of_blank_advances[function] — test source atlib/gui/src/paint/text.zig:7164in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_mixed_metric_runs_share_a_baseline_and_retain_per_glyph_atlas_identity[function] — test source atlib/gui/src/paint/text.zig:5833in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_multiline_query_geometry_matches_painted_caret_at_device_scales[function] — test source atlib/gui/src/paint/text.zig:4538in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_resolved_equal_metric_slots_preserve_measure_and_command_bytes[function] — test source atlib/gui/src/paint/text.zig:5675in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_retained_text_run_color_changes_damage_only_the_styled_glyph[function] — test source atlib/gui/src/paint/text.zig:6078in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_styled_text_runs_preserve_layout_caret_and_selection_geometry[function] — test source atlib/gui/src/paint/text.zig:5539in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_styled_text_runs_rasterize_fixed_colors_gaps_decorations_and_wrapped_partitions[function] — test source atlib/gui/src/paint/text.zig:5975in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_warmed_identical_styled_text_measure_emission_and_retained_diff_need_no_allocation_or_damage[function] — test source atlib/gui/src/paint/text.zig:6167in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_warmed_mixed_metric_measure_planning_emission_and_retained_diff_need_no_allocation[function] — test source atlib/gui/src/paint/text.zig:6236in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_wrapped_markers_preserve_glyph_geometry_and_partition_selection_by_visual_line[function] — test source atlib/gui/src/paint/text.zig:4756in nearest public ownertiny.gui.paint.text
Complete caller list for paint.text.measure
8 direct callers.
lib.gui.src.paint.text.test_Atlas_cache_bypasses_oversized_entries_without_changing_either_epoch[function] — test source atlib/gui/src/paint/text.zig:6694in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_bitmap_atlas_measures_and_paints_Unicode_wrapped_text_with_one_line_plan[function] — test source atlib/gui/src/paint/text.zig:4139in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_bitmap_atlas_measures_scaled_multiline_text[function] — test source atlib/gui/src/paint/text.zig:4114in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_measure_and_paint_reject_malformed_text_runs[function] — test source atlib/gui/src/paint/text.zig:6336in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_resolved_equal_metric_slots_preserve_measure_and_command_bytes[function] — test source atlib/gui/src/paint/text.zig:5675in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_style_boundaries_inside_ligatures_keep_one_glyph_and_use_its_cluster_start[function] — test source atlib/gui/src/paint/text.zig:4677in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_styled_text_runs_preserve_layout_caret_and_selection_geometry[function] — test source atlib/gui/src/paint/text.zig:5539in nearest public ownertiny.gui.paint.textlib.gui.src.paint.text.test_warmed_identical_styled_text_measure_emission_and_retained_diff_need_no_allocation_or_damage[function] — test source atlib/gui/src/paint/text.zig:6167in nearest public ownertiny.gui.paint.text
Audit
| Definitions | 69 |
|---|---|
| Public names | 104 |
| Members | 114 |
| Version | 26.7.0 |
| Revision | daab053ee433 |