tiny.gui.frame
Defined in tiny.gui.
API (36)
Actions
Public operations.
Session.appendSession.beginSession.deinitSession.initSession.publishSession.stagedWidgetsSurfaceFacts.addWorkspace.buildSurfaceWorkspace.deinitWorkspace.initappendSurfaceFramechildClipclampScrollcontentSizeintersectRectsmakeTextRefmaxScrollXmaxScrollYnormalizeTextSelectionoffsetRectpointInRectscrollForVisibleTargetsnapRectsurveyFrameRoot: Validates solved spans and derives exact shell-layout and frame demand.surveySurface
Types and contracts
Public types and contracts.
FrameResolversFrameStorageOffsetSessionSolvedBinding: Semantic and ancestry data paired with one externally solved rectangle.SolvedSubtree: A caller-owned preorder subtree solved in viewport-local coordinates.SurfaceFactsSurfaceFrameOptionsSurfaceFrameRootWorkspace
Values and defaults
Public values and defaults.
Source
Source: lib/gui/src/frame.zig
zig
const std = @import("std");const alloc_phase = @import("alloc_phase");const flex_layout = @import("arrange");const model = @import("model.zig");const widget_query = @import("widget.zig");const Allocator = std.mem.Allocator;const RootFrame = model.RootFrame;const UiNode = model.UiNode;const UiSurfaceTree = model.UiSurfaceTree;const UiScroll = model.UiScroll;const UiPoint = model.UiPoint;const UiTextSelection = model.UiTextSelection;const UiFrame = model.UiFrame;const WidgetFrame = model.WidgetFrame;const nonNegativeFinite = model.nonNegativeFinite;pub const max_depth = flex_layout.max_depth;/// Semantic and ancestry data paired with one externally solved rectangle.pub const SolvedBinding = struct { node: *const UiNode, /// Preorder span including this binding and all of its descendants. subtree_size: usize,};/// A caller-owned preorder subtree solved in viewport-local coordinates./// All four spans have equal nonzero length. Clips are already effective./// Offsets translate rectangles only, leaving viewport-local clips anchored.pub const SolvedSubtree = struct { viewport_widget_id: u64, bindings: []const SolvedBinding, rectangles: []const flex_layout.Rect, clips: []const flex_layout.Rect, offsets: []const Offset,};pub const SurfaceFrameRoot = struct { surface: *const UiSurfaceTree, root_id: u64 = 1, revision: u64 = 1, hovered_widget: ?u64 = null, hovered_point: ?UiPoint = null, captured_widget: ?u64 = null, focused_widget: ?u64 = null, solved_subtrees: []const SolvedSubtree = &.{},};pub const SurfaceFrameOptions = struct { root_id: u64 = 1, revision: u64 = 1, hovered_widget: ?u64 = null, hovered_point: ?UiPoint = null, captured_widget: ?u64 = null, focused_widget: ?u64 = null, resolvers: FrameResolvers = .{}, solved_subtrees: []const SolvedSubtree = &.{},};pub const FrameResolvers = struct { context: ?*anyopaque = null, scroll: ?*const fn (?*anyopaque, u64, *const UiNode, flex_layout.Size, flex_layout.Rect) UiScroll = null, text_selection: ?*const fn (?*anyopaque, u64, *const UiNode) UiTextSelection = null, text_size: ?*const fn (?*anyopaque, *const UiNode) anyerror!?flex_layout.Size = null,};pub const SurfaceFacts = struct { nodes: usize = 0, layout_nodes: usize = 0, children: usize = 0, pub fn add(self: SurfaceFacts, other: SurfaceFacts) SurfaceFacts { return .{ .nodes = self.nodes + other.nodes, .layout_nodes = self.layout_nodes + other.layout_nodes, .children = self.children + other.children, }; }};pub fn surveySurface(surface: *const UiSurfaceTree) error{SurfaceTooDeep}!SurfaceFacts { var facts = SurfaceFacts{}; try surveyNode(&surface.root, &facts, 0); return facts;}fn surveyNode(node: *const UiNode, facts: *SurfaceFacts, depth: usize) error{SurfaceTooDeep}!void { if (depth >= max_depth) return error.SurfaceTooDeep; facts.nodes += 1; facts.layout_nodes += 1; facts.children += node.children.len; for (node.children) |*child| { try surveyNode(child, facts, depth + 1); }}/// Validates solved spans and derives exact shell-layout and frame demand.pub fn surveyFrameRoot(root: SurfaceFrameRoot) !SurfaceFacts { var facts = try surveySurface(root.surface); try validateSolvedSubtrees(root.surface, root.solved_subtrees); for (root.solved_subtrees) |subtree| { facts.nodes = std.math.add(usize, facts.nodes, subtree.bindings.len) catch return error.SurfaceTooLarge; } return facts;}fn validateSolvedSubtrees(surface: *const UiSurfaceTree, subtrees: []const SolvedSubtree) !void { for (subtrees, 0..) |subtree, subtree_index| { for (subtrees[0..subtree_index]) |earlier| { if (earlier.viewport_widget_id == subtree.viewport_widget_id) { return error.DuplicateSolvedViewport; } } const viewport_depth = try opaqueViewportDepth(&surface.root, subtree.viewport_widget_id, 0); const subtree_depth = try validateSolvedSubtree(subtree); if (viewport_depth + 1 + subtree_depth >= max_depth) return error.SurfaceTooDeep; }}fn opaqueViewportDepth(node: *const UiNode, widget_id: u64, depth: usize) !usize { var match_depth: ?usize = null; try findViewport(node, widget_id, depth, &match_depth); return match_depth orelse error.SolvedViewportNotFound;}fn findViewport(node: *const UiNode, widget_id: u64, depth: usize, match_depth: *?usize) !void { if (node.widget_id == widget_id) { if (match_depth.* != null) return error.AmbiguousSolvedViewport; if (node.children.len != 0) return error.SolvedViewportNotOpaque; match_depth.* = depth; } for (node.children) |*child| try findViewport(child, widget_id, depth + 1, match_depth);}fn validateSolvedSubtree(subtree: SolvedSubtree) !usize { const count = subtree.bindings.len; if (count == 0 or subtree.rectangles.len != count or subtree.clips.len != count or subtree.offsets.len != count) { return error.InvalidSolvedSpans; } if (subtree.bindings[0].subtree_size != count) return error.InvalidSolvedBindings; var ancestor_ends: [max_depth]usize = undefined; var ancestor_count: usize = 0; var maximum_depth: usize = 0; for (subtree.bindings, 0..) |binding, index| { while (ancestor_count > 0 and index == ancestor_ends[ancestor_count - 1]) { ancestor_count -= 1; } if (index != 0 and ancestor_count == 0) return error.InvalidSolvedBindings; const subtree_end = std.math.add(usize, index, binding.subtree_size) catch return error.InvalidSolvedBindings; const parent_end = if (ancestor_count == 0) count else ancestor_ends[ancestor_count - 1]; if (binding.subtree_size == 0 or subtree_end > parent_end) return error.InvalidSolvedBindings; maximum_depth = @max(maximum_depth, ancestor_count); if (binding.subtree_size > 1) { if (ancestor_count >= max_depth) return error.SurfaceTooDeep; ancestor_ends[ancestor_count] = subtree_end; ancestor_count += 1; } if (!validSolvedRect(subtree.rectangles[index]) or !validSolvedRect(subtree.clips[index]) or !validSolvedOffset(subtree.offsets[index])) { return error.InvalidSolvedGeometry; } } while (ancestor_count > 0 and count == ancestor_ends[ancestor_count - 1]) { ancestor_count -= 1; } if (ancestor_count != 0) return error.InvalidSolvedBindings; return maximum_depth;}fn validSolvedRect(rect: flex_layout.Rect) bool { return std.math.isFinite(rect.x) and std.math.isFinite(rect.y) and std.math.isFinite(rect.width) and std.math.isFinite(rect.height) and rect.width >= 0 and rect.height >= 0;}fn validSolvedOffset(offset: Offset) bool { return std.math.isFinite(offset.x) and std.math.isFinite(offset.y);}pub const FrameStorage = struct { pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "gui.frame_storage", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "root_frame_slots_at_the_admitted_root_quota", .lifetime = .steady, .detail = "root frame slots at the admitted root quota", }, .{ .id = "widget_frame_slots_at_the_surveyed_shell_and_solved_node_count", .lifetime = .steady, .detail = "widget frame slots at the surveyed shell and solved node count", }, .{ .id = "flex_node_spans_at_the_surveyed_child_count", .lifetime = .steady, .detail = "flex node spans at the surveyed child count", }, .{ .id = "layout_node_state_spans_at_the_surveyed_shell_node_count", .lifetime = .steady, .detail = "layout node-state spans at the surveyed shell node count", }, .{ .id = "layout_child_index_result_metric_and_line_spans", .lifetime = .steady, .detail = "layout child-index, result, metric, and line spans", }, }, .excluded = &.{ "the caller-owned UiSurfaceTree, solved spans, and strings", "resolver callbacks and any storage they touch", "sdfii semantics byte ownership and root-id ordering scratch", "paint command, atlas, and executor storage", "epoch replacement allocation and release outside one admitted demand epoch", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "roots", "roots"), alloc_phase.capacity.bindInput(Limits, "nodes", "nodes"), alloc_phase.capacity.bindInput(Limits, "children", "children"), alloc_phase.capacity.bindInput(Limits, "layout_nodes", "layout_nodes"), }, .type_selectors = &.{ alloc_phase.capacity.bindType(RootFrame, "root"), alloc_phase.capacity.bindType(WidgetFrame, "widget"), alloc_phase.capacity.bindType(flex_layout.NodeState, "state"), alloc_phase.capacity.bindType(flex_layout.Node, "node"), alloc_phase.capacity.bindType(flex_layout.LayoutResult, "result"), alloc_phase.capacity.bindType(flex_layout.ChildIndex, "index"), alloc_phase.capacity.bindType(flex_layout.ChildMetrics, "metric"), alloc_phase.capacity.bindType(flex_layout.Line, "line"), }, .nodes = &.{ .{ .input = 0 }, .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } }, .{ .input = 1 }, .{ .constant = 1 }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 1 } } }, .{ .product = .{ .left = 2, .right = 4 } }, .{ .input = 3 }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 2 } } }, .{ .product = .{ .left = 6, .right = 7 } }, .{ .input = 2 }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 3 } } }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 4 } } }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 5 } } }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 6 } } }, .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 7 } } }, .{ .add = .{ .left = 10, .right = 11 } }, .{ .add = .{ .left = 15, .right = 12 } }, .{ .add = .{ .left = 16, .right = 13 } }, .{ .add = .{ .left = 17, .right = 14 } }, .{ .product = .{ .left = 9, .right = 18 } }, .{ .add = .{ .left = 1, .right = 5 } }, .{ .add = .{ .left = 20, .right = 8 } }, .{ .add = .{ .left = 21, .right = 19 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 22, }}, }, .overload = .{ .kind = .reject_before_seal, .detail = "surveyFrameRoot rejects invalid or over-deep shell and solved trees before acquisition; demand past the admitted epoch re-derives storage at the new high water before building; carve past admitted capacity is a capacity-model bug caught by assertion", }, .risks = .{ .transitive = .{ .status = .open, .detail = "the build crosses layout, widget query, and model helpers without a machine-checked call-graph closure certificate", }, .foreign = .{ .status = .open, .detail = "resolver callbacks (text measurement, scroll, selection) run host code outside the claim during builds", }, }, .obligations = &.{ .{ .key = "gui_frame_capacity", .role = .capacity_model }, .{ .key = "gui_frame_acquisition", .role = .custom }, .{ .key = "gui_frame_survey", .role = .overload }, .{ .key = "gui_frame_highwater", .role = .overload }, .{ .key = "gui_frame_steady_transitive_risk", .role = .transitive_risk }, .{ .key = "gui_frame_steady_foreign_risk", .role = .foreign_risk }, .{ .key = "gui_frame_oom", .role = .custom }, .{ .key = "gui_frame_session", .role = .custom }, .{ .key = "gui_frame_session_steady", .role = .transitive_risk }, }, }, .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 const Limits = struct { nodes: usize, layout_nodes: usize, children: usize, roots: usize, }; pub const Capacity = struct { roots: usize, widgets: usize, flex_nodes: usize, layout_results: usize, layout_metrics: usize, layout_lines: usize, layout_states: usize, layout_child_indices: usize, total_bytes: usize, pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity { if (limits.roots == 0) return error.CapacityOverflow; if (limits.nodes == 0) return error.CapacityOverflow; if (limits.layout_nodes == 0 or limits.layout_nodes > limits.nodes) return error.CapacityOverflow; const root_bytes = std.math.mul( usize, limits.roots, @sizeOf(RootFrame), ) catch return error.CapacityOverflow; const widget_bytes = std.math.mul( usize, limits.nodes, @sizeOf(WidgetFrame), ) catch return error.CapacityOverflow; const state_bytes = std.math.mul( usize, limits.layout_nodes, @sizeOf(flex_layout.NodeState), ) catch return error.CapacityOverflow; const per_child = @sizeOf(flex_layout.Node) + @sizeOf(flex_layout.LayoutResult) + @sizeOf(flex_layout.ChildIndex) + @sizeOf(flex_layout.ChildMetrics) + @sizeOf(flex_layout.Line); const child_bytes = std.math.mul( usize, limits.children, per_child, ) catch return error.CapacityOverflow; const frame_bytes = std.math.add( usize, root_bytes, widget_bytes, ) catch return error.CapacityOverflow; const owned_node_bytes = std.math.add( usize, frame_bytes, state_bytes, ) catch return error.CapacityOverflow; const total_bytes = std.math.add( usize, owned_node_bytes, child_bytes, ) catch return error.CapacityOverflow; return .{ .roots = limits.roots, .widgets = limits.nodes, .flex_nodes = limits.children, .layout_results = limits.children, .layout_metrics = limits.children, .layout_lines = limits.children, .layout_states = limits.layout_nodes, .layout_child_indices = limits.children, .total_bytes = total_bytes, }; } }; phase: alloc_phase.capacity.Phase, capacity: Capacity, limits: Limits, roots: []RootFrame, roots_used: usize, widgets: []WidgetFrame, widgets_used: usize, flex_nodes: []flex_layout.Node, flex_used: usize, layout_scratch: flex_layout.Scratch, pub fn init(allocator: Allocator, limits: Limits) !FrameStorage { const capacity = try Capacity.derive(limits); const roots = try allocator.alloc(RootFrame, capacity.roots); errdefer allocator.free(roots); const widgets = try allocator.alloc(WidgetFrame, capacity.widgets); errdefer allocator.free(widgets); const flex_nodes = try allocator.alloc(flex_layout.Node, capacity.flex_nodes); errdefer allocator.free(flex_nodes); const results = try allocator.alloc(flex_layout.LayoutResult, capacity.layout_results); errdefer allocator.free(results); const metrics = try allocator.alloc(flex_layout.ChildMetrics, capacity.layout_metrics); errdefer allocator.free(metrics); const lines = try allocator.alloc(flex_layout.Line, capacity.layout_lines); errdefer allocator.free(lines); const states = try allocator.alloc(flex_layout.NodeState, capacity.layout_states); errdefer allocator.free(states); const child_indices = try allocator.alloc( flex_layout.ChildIndex, capacity.layout_child_indices, ); return .{ .phase = .initialization, .capacity = capacity, .limits = limits, .roots = roots, .roots_used = 0, .widgets = widgets, .widgets_used = 0, .flex_nodes = flex_nodes, .flex_used = 0, .layout_scratch = .{ .results = results, .metrics = metrics, .lines = lines, .states = states, .child_indices = child_indices, }, }; } pub fn activate(self: *FrameStorage) void { std.debug.assert(self.phase == .initialization); self.assertStorage(); self.phase = .steady; } pub fn deinit(self: *FrameStorage, allocator: Allocator) void { std.debug.assert(self.phase != .teardown); self.assertStorage(); self.phase = .teardown; allocator.free(self.roots); allocator.free(self.widgets); allocator.free(self.flex_nodes); allocator.free(self.layout_scratch.results); allocator.free(self.layout_scratch.metrics); allocator.free(self.layout_scratch.lines); allocator.free(self.layout_scratch.states); allocator.free(self.layout_scratch.child_indices); } pub fn admits(self: *const FrameStorage, facts: SurfaceFacts, roots: usize) bool { std.debug.assert(self.phase == .steady); if (facts.nodes > self.capacity.widgets) return false; if (facts.layout_nodes > self.capacity.layout_states) return false; if (facts.children > self.capacity.flex_nodes) return false; return roots <= self.capacity.roots; } pub fn reset(self: *FrameStorage) void { std.debug.assert(self.phase == .steady); self.roots_used = 0; self.widgets_used = 0; self.flex_used = 0; self.layout_scratch.reset(); } fn carveFlexNodes(self: *FrameStorage, count: usize) []flex_layout.Node { std.debug.assert(self.flex_used + count <= self.flex_nodes.len); const span = self.flex_nodes[self.flex_used..][0..count]; self.flex_used += count; return span; } fn assertStorage(self: *const FrameStorage) void { const expected = Capacity.derive(self.limits) catch unreachable; std.debug.assert(std.meta.eql(expected, self.capacity)); std.debug.assert(self.roots.len == self.capacity.roots); std.debug.assert(self.widgets.len == self.capacity.widgets); std.debug.assert(self.flex_nodes.len == self.capacity.flex_nodes); std.debug.assert(self.layout_scratch.results.len == self.capacity.layout_results); std.debug.assert(self.layout_scratch.metrics.len == self.capacity.layout_metrics); std.debug.assert(self.layout_scratch.lines.len == self.capacity.layout_lines); std.debug.assert(self.layout_scratch.states.len == self.capacity.layout_states); std.debug.assert( self.layout_scratch.child_indices.len == self.capacity.layout_child_indices, ); }};comptime { alloc_phase.capacity.requireAllocatorExactOwnerShape(FrameStorage);}fn modelFrameCapacity(limits: FrameStorage.Limits) error{CapacityOverflow}!FrameStorage.Capacity { if (limits.roots == 0) return error.CapacityOverflow; if (limits.nodes == 0) return error.CapacityOverflow; if (limits.layout_nodes == 0 or limits.layout_nodes > limits.nodes) return error.CapacityOverflow; const per_child: u128 = @sizeOf(flex_layout.Node) + @sizeOf(flex_layout.LayoutResult) + @sizeOf(flex_layout.ChildIndex) + @sizeOf(flex_layout.ChildMetrics) + @sizeOf(flex_layout.Line); const total = @as(u128, limits.roots) * @sizeOf(RootFrame) + @as(u128, limits.nodes) * @sizeOf(WidgetFrame) + @as(u128, limits.layout_nodes) * @sizeOf(flex_layout.NodeState) + @as(u128, limits.children) * per_child; if (total > std.math.maxInt(usize)) return error.CapacityOverflow; return .{ .roots = limits.roots, .widgets = limits.nodes, .flex_nodes = limits.children, .layout_results = limits.children, .layout_metrics = limits.children, .layout_lines = limits.children, .layout_states = limits.layout_nodes, .layout_child_indices = limits.children, .total_bytes = @intCast(total), };}comptime { const worst = FrameStorage.Limits{ .nodes = std.math.maxInt(u32), .layout_nodes = std.math.maxInt(u32), .children = std.math.maxInt(u32), .roots = std.math.maxInt(u16), }; _ = modelFrameCapacity(worst) catch @compileError("frame capacity exceeds the target word size");}pub const Workspace = struct { allocator: Allocator, storage: ?FrameStorage = null, pub fn init(allocator: Allocator) Workspace { return .{ .allocator = allocator }; } pub fn deinit(self: *Workspace) void { if (self.storage) |*storage| storage.deinit(self.allocator); self.* = undefined; } pub fn buildSurface(self: *Workspace, surface: *const UiSurfaceTree, options: SurfaceFrameOptions) !UiFrame { const root = SurfaceFrameRoot{ .surface = surface, .root_id = options.root_id, .revision = options.revision, .hovered_widget = options.hovered_widget, .hovered_point = options.hovered_point, .captured_widget = options.captured_widget, .focused_widget = options.focused_widget, .solved_subtrees = options.solved_subtrees, }; const facts = try surveyFrameRoot(root); const storage = try self.ensureStorage(facts, 1); storage.reset(); appendSurfaceFrame( storage, root, options.resolvers, ) catch |err| { storage.reset(); return err; }; return .{ .revision = options.revision, .root_count = storage.roots_used, .roots = storage.roots[0..storage.roots_used], .widget_count = storage.widgets_used, .widgets = storage.widgets[0..storage.widgets_used], }; } fn ensureStorage(self: *Workspace, facts: SurfaceFacts, roots: usize) !*FrameStorage { if (self.storage) |*storage| { if (storage.admits(facts, roots)) return storage; } const grown = FrameStorage.Limits{ .nodes = @max(facts.nodes, if (self.storage) |s| s.capacity.widgets else 0), .layout_nodes = @max(facts.layout_nodes, if (self.storage) |s| s.capacity.layout_states else 0), .children = @max(facts.children, if (self.storage) |s| s.capacity.flex_nodes else 0), .roots = @max(roots, if (self.storage) |s| s.capacity.roots else 0), }; var next = try FrameStorage.init(self.allocator, grown); next.activate(); if (self.storage) |*storage| storage.deinit(self.allocator); self.storage = next; return &self.storage.?; }};pub const Session = struct { allocator: Allocator, front: ?FrameStorage = null, back: ?FrameStorage = null, pub fn init(allocator: Allocator) Session { return .{ .allocator = allocator }; } pub fn deinit(self: *Session) void { if (self.front) |*storage| storage.deinit(self.allocator); if (self.back) |*storage| storage.deinit(self.allocator); self.* = undefined; } pub fn begin(self: *Session, facts: SurfaceFacts, roots: usize) !*FrameStorage { const admitted_roots = @max(roots, 1); if (self.back) |*storage| { if (storage.admits(facts, admitted_roots)) { storage.reset(); return storage; } } const grown = FrameStorage.Limits{ .nodes = @max(facts.nodes, if (self.back) |s| s.capacity.widgets else 0), .layout_nodes = @max(facts.layout_nodes, if (self.back) |s| s.capacity.layout_states else 0), .children = @max(facts.children, if (self.back) |s| s.capacity.flex_nodes else 0), .roots = @max(admitted_roots, if (self.back) |s| s.capacity.roots else 0), }; var next = try FrameStorage.init(self.allocator, grown); next.activate(); if (self.back) |*storage| storage.deinit(self.allocator); self.back = next; return &self.back.?; } pub fn append(self: *Session, root: SurfaceFrameRoot, resolvers: FrameResolvers) !void { std.debug.assert(self.back != null); try appendSurfaceFrame(&self.back.?, root, resolvers); } pub fn stagedWidgets(self: *Session) []WidgetFrame { std.debug.assert(self.back != null); const storage = &self.back.?; return storage.widgets[0..storage.widgets_used]; } pub fn publish(self: *Session, revision: u64) UiFrame { std.debug.assert(self.back != null); std.mem.swap(?FrameStorage, &self.front, &self.back); const storage = &self.front.?; return .{ .revision = revision, .root_count = storage.roots_used, .roots = storage.roots[0..storage.roots_used], .widget_count = storage.widgets_used, .widgets = storage.widgets[0..storage.widgets_used], }; }};pub fn appendSurfaceFrame( storage: *FrameStorage, root: SurfaceFrameRoot, resolvers: FrameResolvers,) !void { std.debug.assert(storage.phase == .steady); const facts = try surveyFrameRoot(root); if (storage.roots_used >= storage.roots.len or facts.nodes > storage.widgets.len - storage.widgets_used or facts.children > storage.flex_nodes.len - storage.flex_used or facts.layout_nodes > storage.layout_scratch.states.len or facts.children > storage.layout_scratch.results.len or facts.children > storage.layout_scratch.metrics.len or facts.children > storage.layout_scratch.lines.len or facts.children > storage.layout_scratch.child_indices.len) { return error.FrameStorageCapacity; } const widget_start = storage.widgets_used; const flex_start = storage.flex_used; const root_start = storage.roots_used; const scratch_marks = storage.layout_scratch; errdefer { storage.widgets_used = widget_start; storage.flex_used = flex_start; storage.roots_used = root_start; storage.layout_scratch.results_used = scratch_marks.results_used; storage.layout_scratch.metrics_used = scratch_marks.metrics_used; storage.layout_scratch.lines_used = scratch_marks.lines_used; } const flex_root = try buildFlexNodeWithResolvers(storage, &root.surface.root, resolvers, 0); const layout = flex_layout.computeLayout(&storage.layout_scratch, flex_root, root.surface.available_size); std.debug.assert(storage.roots_used < storage.roots.len); const root_index = storage.roots_used; storage.roots[root_index] = .{ .root_id = root.root_id, .surface_revision = root.revision, .rect = snapRect(layout.rect), .widget_start = widget_start, .widget_count = 0, }; storage.roots_used += 1; appendWidgetFrames( storage, root, resolvers, &root.surface.root, &layout, .{}, null, 0, 0, ); if (root.hovered_widget == null) markHoveredPoint(storage.widgets[widget_start..storage.widgets_used], root.root_id, root.hovered_point); storage.roots[root_index].widget_count = storage.widgets_used - widget_start;}fn markHoveredPoint(widgets: []WidgetFrame, root_id: u64, hovered_point: ?UiPoint) void { const point = hovered_point orelse return; const hovered = widget_query.hitTestActivatable(widgets, root_id, point.x, point.y) orelse widget_query.hitTest(widgets, root_id, point.x, point.y) orelse return; const index = widget_query.findIndexById(widgets, root_id, hovered.widget_id) orelse return; widgets[index].hovered = true;}fn buildFlexNodeWithResolvers( storage: *FrameStorage, node: *const UiNode, resolvers: FrameResolvers, depth: usize,) !flex_layout.Node { std.debug.assert(depth < max_depth); const children = storage.carveFlexNodes(node.children.len); for (node.children, 0..) |*child, index| { children[index] = try buildFlexNodeWithResolvers(storage, child, resolvers, depth + 1); } const intrinsic_size = if (resolvers.text_size) |resolver| blk: { const measured = try resolver(resolvers.context, node); break :blk measured orelse flex_layout.Size{}; } else flex_layout.Size{}; return .{ .id = std.math.cast(usize, node.widget_id) orelse 0, .style = node.style, .size = node.size, .intrinsic_size = intrinsic_size, .clip_x = node.scroll.overflow_x != .visible, .clip_y = node.scroll.overflow_y != .visible, .children = children, };}fn appendWidgetFrames( storage: *FrameStorage, root: SurfaceFrameRoot, resolvers: FrameResolvers, node: *const UiNode, layout: *const flex_layout.LayoutResult, offset: Offset, active_clip: ?flex_layout.Rect, parent_layer: u8, depth: usize,) void { std.debug.assert(depth < max_depth); const layer = @max(parent_layer, node.layer); const frame_rect = snapRect(offsetRect(layout.rect, offset.x, offset.y)); const visible_rect = if (active_clip) |clip| intersectRects(frame_rect, clip) else frame_rect; const content_size_value = contentSize(layout); const scroll = resolveScroll(resolvers, root.root_id, node, content_size_value, frame_rect); const text_selection = resolveTextSelection(resolvers, root.root_id, node); const frame_text = node.text; const has_text = if (frame_text) |text| text.content.len != 0 else false; const text_ref = if (has_text) makeTextRef(root.root_id, node.widget_id) else 0; const widget_index = storage.widgets_used; std.debug.assert(widget_index < storage.widgets.len); storage.widgets[widget_index] = .{ .root_id = root.root_id, .widget_id = node.widget_id, .kind = node.kind, .rect = frame_rect, .visible_rect = visible_rect, .paint = node.paint, .scroll = scroll, .constraints = node.style.constraints, .content_size = content_size_value, .focusable = (node.focusable or node.kind == .button or node.kind == .text_input) and !widget_query.isDisabledNode(node), .layer = layer, .hovered = root.hovered_widget == node.widget_id, .captured = root.captured_widget == node.widget_id, .focused = root.focused_widget == node.widget_id, .has_text = has_text, .action = node.action, .role = node.role, .state_flags = node.state_flags, .text_ref = text_ref, .text = frame_text, .text_selection = text_selection, }; storage.widgets_used += 1; const child_offset = Offset{ .x = offset.x - scroll.scroll_x, .y = offset.y - scroll.scroll_y, }; const next_clip = childClip(active_clip, frame_rect, scroll); if (solvedSubtreeForViewport(root.solved_subtrees, node.widget_id)) |subtree| { _ = appendSolvedNode( storage, root, resolvers, subtree, 0, frame_rect, visible_rect, layer, ); } for (node.children, 0..) |*child_node, index| { appendWidgetFrames( storage, root, resolvers, child_node, &layout.children[index], child_offset, next_clip, layer, depth + 1, ); } storage.widgets[widget_index].subtree_size = storage.widgets_used - widget_index;}fn solvedSubtreeForViewport(subtrees: []const SolvedSubtree, widget_id: u64) ?*const SolvedSubtree { for (subtrees) |*subtree| { if (subtree.viewport_widget_id == widget_id) return subtree; } return null;}fn appendSolvedNode( storage: *FrameStorage, root: SurfaceFrameRoot, resolvers: FrameResolvers, subtree: *const SolvedSubtree, binding_index: usize, viewport_rect: flex_layout.Rect, viewport_clip: flex_layout.Rect, parent_layer: u8,) usize { const binding = subtree.bindings[binding_index]; const node = binding.node; const layer = @max(parent_layer, node.layer); const local_rect = subtree.rectangles[binding_index]; const node_offset = subtree.offsets[binding_index]; const frame_rect = snapRect(offsetRect( local_rect, viewport_rect.x + node_offset.x, viewport_rect.y + node_offset.y, )); const solved_clip = offsetRect( subtree.clips[binding_index], viewport_rect.x, viewport_rect.y, ); const visible_rect = intersectRects(frame_rect, intersectRects(viewport_clip, solved_clip)); const content_size_value = solvedContentSize(subtree, binding_index); const scroll = resolveScroll(resolvers, root.root_id, node, content_size_value, frame_rect); const text_selection = resolveTextSelection(resolvers, root.root_id, node); const frame_text = node.text; const has_text = if (frame_text) |value| value.content.len != 0 else false; const text_ref = if (has_text) makeTextRef(root.root_id, node.widget_id) else 0; const widget_index = storage.widgets_used; std.debug.assert(widget_index < storage.widgets.len); storage.widgets[widget_index] = .{ .root_id = root.root_id, .widget_id = node.widget_id, .kind = node.kind, .rect = frame_rect, .visible_rect = visible_rect, .paint = node.paint, .scroll = scroll, .constraints = node.style.constraints, .content_size = content_size_value, .focusable = (node.focusable or node.kind == .button or node.kind == .text_input) and !widget_query.isDisabledNode(node), .layer = layer, .hovered = root.hovered_widget == node.widget_id, .captured = root.captured_widget == node.widget_id, .focused = root.focused_widget == node.widget_id, .has_text = has_text, .action = node.action, .role = node.role, .state_flags = node.state_flags, .text_ref = text_ref, .text = frame_text, .text_selection = text_selection, .subtree_size = binding.subtree_size, }; storage.widgets_used += 1; var child_index = binding_index + 1; const subtree_end = binding_index + binding.subtree_size; while (child_index < subtree_end) { child_index = appendSolvedNode( storage, root, resolvers, subtree, child_index, viewport_rect, viewport_clip, layer, ); } return subtree_end;}fn solvedContentSize(subtree: *const SolvedSubtree, binding_index: usize) flex_layout.Size { const rect = subtree.rectangles[binding_index]; var right = rect.x + rect.width; var bottom = rect.y + rect.height; const subtree_end = binding_index + subtree.bindings[binding_index].subtree_size; for (subtree.rectangles[binding_index + 1 .. subtree_end]) |descendant| { right = @max(right, descendant.x + descendant.width); bottom = @max(bottom, descendant.y + descendant.height); } return .{ .width = @max(right - rect.x, rect.width), .height = @max(bottom - rect.y, rect.height), };}fn resolveScroll( resolvers: FrameResolvers, root_id: u64, node: *const UiNode, node_content_size: flex_layout.Size, rect: flex_layout.Rect,) UiScroll { if (resolvers.scroll) |resolver| return resolver(resolvers.context, root_id, node, node_content_size, rect); return clampScroll(node.scroll, node_content_size, rect);}fn resolveTextSelection( resolvers: FrameResolvers, root_id: u64, node: *const UiNode,) UiTextSelection { const selection = if (resolvers.text_selection) |resolver| resolver(resolvers.context, root_id, node) else node.text_selection; const text_len = if (node.text) |text| text.content.len else 0; return normalizeTextSelection(selection, text_len);}pub const pointInRect = model.pointInRect;pub fn offsetRect(rect: flex_layout.Rect, x: f32, y: f32) flex_layout.Rect { return .{ .x = rect.x + x, .y = rect.y + y, .width = rect.width, .height = rect.height, };}pub fn snapRect(rect: flex_layout.Rect) flex_layout.Rect { const left = @round(rect.x); const top = @round(rect.y); const right = @round(rect.x + rect.width); const bottom = @round(rect.y + rect.height); return .{ .x = left, .y = top, .width = right - left, .height = bottom - top, };}pub fn contentSize(layout: *const flex_layout.LayoutResult) flex_layout.Size { var right = layout.rect.x + layout.rect.width; var bottom = layout.rect.y + layout.rect.height; for (layout.children) |child| { right = @max(right, child.rect.x + child.rect.width); bottom = @max(bottom, child.rect.y + child.rect.height); } return .{ .width = @max(right - layout.rect.x, layout.rect.width), .height = @max(bottom - layout.rect.y, layout.rect.height), };}pub const Offset = struct { x: f32 = 0, y: f32 = 0,};pub fn childClip(current: ?flex_layout.Rect, rect: flex_layout.Rect, scroll: UiScroll) ?flex_layout.Rect { if (scroll.overflow_x == .visible and scroll.overflow_y == .visible) return current; const next = flex_layout.Rect{ .x = if (scroll.overflow_x == .clip) rect.x else -clip_extent, .y = if (scroll.overflow_y == .clip) rect.y else -clip_extent, .width = if (scroll.overflow_x == .clip) rect.width else clip_extent * 2, .height = if (scroll.overflow_y == .clip) rect.height else clip_extent * 2, }; return if (current) |clip| intersectRects(clip, next) else next;}pub fn intersectRects(a: flex_layout.Rect, b: flex_layout.Rect) flex_layout.Rect { const left = @max(a.x, b.x); const top = @max(a.y, b.y); const right = @min(a.x + a.width, b.x + b.width); const bottom = @min(a.y + a.height, b.y + b.height); return .{ .x = left, .y = top, .width = @max(right - left, 0), .height = @max(bottom - top, 0), };}pub fn clampScroll(scroll: UiScroll, content_size: flex_layout.Size, rect: flex_layout.Rect) UiScroll { var result = scroll; result.scroll_x = nonNegativeFinite(result.scroll_x); result.scroll_y = nonNegativeFinite(result.scroll_y); if (result.overflow_x == .clip) result.scroll_x = std.math.clamp(result.scroll_x, 0, maxScrollX(content_size, rect)); if (result.overflow_y == .clip) result.scroll_y = std.math.clamp(result.scroll_y, 0, maxScrollY(content_size, rect)); return result;}pub fn scrollForVisibleTarget(widget: *const WidgetFrame, current: UiScroll, target_rect: flex_layout.Rect) ?UiScroll { var next = current; const viewport = widget.visible_rect; if (current.overflow_x == .clip and viewport.width > 0) { next.scroll_x = scrollOffsetForVisibleRange(current.scroll_x, viewport.x, viewport.width, target_rect.x, target_rect.width); } if (current.overflow_y == .clip and viewport.height > 0) { next.scroll_y = scrollOffsetForVisibleRange(current.scroll_y, viewport.y, viewport.height, target_rect.y, target_rect.height); } next = clampScroll(next, widget.content_size, widget.rect); if (next.scroll_x == current.scroll_x and next.scroll_y == current.scroll_y) return null; return next;}fn scrollOffsetForVisibleRange(current_scroll: f32, viewport_start: f32, viewport_size: f32, target_start: f32, target_size: f32) f32 { const viewport_end = viewport_start + viewport_size; const target_end = target_start + target_size; if (target_start <= viewport_start and viewport_end <= target_end) return current_scroll; if (viewport_start <= target_start and target_end <= viewport_end) return current_scroll; if (target_start < viewport_start) return current_scroll - (viewport_start - target_start); if (target_end > viewport_end) return current_scroll + (target_end - viewport_end); return current_scroll;}pub fn maxScrollX(content_size: flex_layout.Size, rect: flex_layout.Rect) f32 { return @max(content_size.width - rect.width, 0);}pub fn maxScrollY(content_size: flex_layout.Size, rect: flex_layout.Rect) f32 { return @max(content_size.height - rect.height, 0);}pub fn makeTextRef(root_id: u64, widget_id: u64) u64 { var pair = [2]u64{ root_id, widget_id }; const hashed = std.hash.Wyhash.hash(0, std.mem.asBytes(&pair)) & std.math.maxInt(i64); return if (hashed == 0) 1 else hashed;}pub fn normalizeTextSelection(selection: UiTextSelection, text_len: usize) UiTextSelection { var result = selection; result.cursor_byte_offset = @min(result.cursor_byte_offset, text_len); result.selection_anchor_byte_offset = @min(result.selection_anchor_byte_offset, text_len); result.selection_focus_byte_offset = @min(result.selection_focus_byte_offset, text_len); if (result.selection_anchor_byte_offset == result.selection_focus_byte_offset) result.selection_active = false; return result;}const clip_extent: f32 = 1_000_000;test "layers propagate from a subtree root to its descendants" { const allocator = std.testing.allocator; const overlay_children = [_]UiNode{ .{ .widget_id = 4, .size = .{ .width = 40, .height = 10 }, .text = .{ .content = "modal" }, }, }; const children = [_]UiNode{ .{ .widget_id = 2, .size = .{ .width = 80, .height = 24 }, .text = .{ .content = "base" }, }, .{ .widget_id = 3, .layer = 1, .style = .{ .position = .absolute, .inset = .{ .left = 10, .top = 5 } }, .size = .{ .width = 60, .height = 20 }, .children = overlay_children[0..], }, }; const surface = UiSurfaceTree{ .available_size = .{ .width = 200, .height = 40 }, .root = .{ .widget_id = 1, .children = children[0..], }, }; var workspace = Workspace.init(allocator); defer workspace.deinit(); const frame = try workspace.buildSurface(&surface, .{ .root_id = 42, .revision = 1 }); const base = widget_query.findById(frame.widgets, 42, 2) orelse return error.MissingBaseWidget; try std.testing.expectEqual(@as(u8, 0), base.layer); const panel = widget_query.findById(frame.widgets, 42, 3) orelse return error.MissingPanelWidget; try std.testing.expectEqual(@as(u8, 1), panel.layer); const descendant = widget_query.findById(frame.widgets, 42, 4) orelse return error.MissingDescendantWidget; try std.testing.expectEqual(@as(u8, 1), descendant.layer);}test "surface frame facts preserve roles actions and hit geometry" { const allocator = std.testing.allocator; const children = [_]UiNode{ .{ .widget_id = 2, .kind = .button, .size = .{ .width = 80, .height = 24 }, .text = .{ .content = "Run" }, .action = "demo.run", .role = "demo.command", }, .{ .widget_id = 3, .kind = .button, .size = .{ .width = 80, .height = 24 }, .text = .{ .content = "Stop" }, .action = "demo.stop", .role = "demo.command", .state_flags = model.ui_state_disabled, }, }; const surface = UiSurfaceTree{ .available_size = .{ .width = 200, .height = 40 }, .root = .{ .widget_id = 1, .style = .{ .flex_direction = .row, .gap = 8, }, .children = children[0..], }, }; var workspace = Workspace.init(allocator); defer workspace.deinit(); const frame = try workspace.buildSurface(&surface, .{ .root_id = 42, .revision = 7, .hovered_widget = 2, .focused_widget = 2, }); try std.testing.expectEqual(@as(u64, 7), frame.revision); try std.testing.expectEqual(@as(usize, 1), frame.root_count); try std.testing.expectEqual(@as(usize, 3), frame.widget_count); const run = widget_query.findById(frame.widgets, 42, 2) orelse return error.MissingRunWidget; try std.testing.expect(run.focusable); try std.testing.expect(run.hovered); try std.testing.expect(run.focused); try std.testing.expect(run.has_text); try std.testing.expect(run.text_ref != 0); try std.testing.expectEqualStrings("demo.run", run.action); const hit = widget_query.hitTest(frame.widgets, 42, run.rect.x + 1, run.rect.y + 1) orelse return error.MissingHit; try std.testing.expectEqual(@as(u64, 2), hit.widget_id); const stop = widget_query.findById(frame.widgets, 42, 3) orelse return error.MissingStopWidget; try std.testing.expect(!stop.focusable); const disabled_hit = widget_query.hitTest(frame.widgets, 42, stop.rect.x + 1, stop.rect.y + 1); if (disabled_hit) |hit_widget| try std.testing.expect(hit_widget.widget_id != 3);}test "surface frame appends an externally solved subtree beneath an opaque viewport" { const viewport_children = [_]UiNode{.{ .widget_id = 2, .style = .{ .position = .absolute, .inset = .{ .left = 20, .top = 10 }, }, .size = .{ .width = 100, .height = 60 }, }}; const surface = UiSurfaceTree{ .available_size = .{ .width = 160, .height = 100 }, .root = .{ .widget_id = 1, .children = &viewport_children }, }; const external_nodes = [_]UiNode{ .{ .widget_id = 10, .role = "document", .layer = 1 }, .{ .widget_id = 11, .kind = .button, .action = "open", .role = "command" }, .{ .widget_id = 12, .kind = .label, .text = .{ .content = "status" }, .layer = 2 }, }; const bindings = [_]SolvedBinding{ .{ .node = &external_nodes[0], .subtree_size = 3 }, .{ .node = &external_nodes[1], .subtree_size = 1 }, .{ .node = &external_nodes[2], .subtree_size = 1 }, }; const rectangles = [_]flex_layout.Rect{ .{ .width = 100, .height = 60 }, .{ .x = 5, .y = 8, .width = 30, .height = 10 }, .{ .x = 5, .y = 30, .width = 80, .height = 20 }, }; const clips = [_]flex_layout.Rect{ .{ .width = 100, .height = 60 }, .{ .width = 20, .height = 15 }, .{ .width = 100, .height = 50 }, }; const offsets = [_]Offset{ .{}, .{ .x = 2, .y = 3 }, .{ .y = -4 }, }; const solved = SolvedSubtree{ .viewport_widget_id = 2, .bindings = &bindings, .rectangles = &rectangles, .clips = &clips, .offsets = &offsets, }; const root = SurfaceFrameRoot{ .surface = &surface, .root_id = 7, .revision = 9, .solved_subtrees = &.{solved}, }; const facts = try surveyFrameRoot(root); try std.testing.expectEqual(@as(usize, 5), facts.nodes); try std.testing.expectEqual(@as(usize, 2), facts.layout_nodes); try std.testing.expectEqual(@as(usize, 1), facts.children); var workspace = Workspace.init(std.testing.allocator); defer workspace.deinit(); const frame = try workspace.buildSurface(&surface, .{ .root_id = root.root_id, .revision = root.revision, .solved_subtrees = root.solved_subtrees, }); try std.testing.expectEqual(@as(usize, 5), frame.widget_count); try std.testing.expectEqual(@as(usize, 5), frame.widgets[0].subtree_size); try std.testing.expectEqual(@as(usize, 4), frame.widgets[1].subtree_size); try std.testing.expectEqual(@as(u64, 10), frame.widgets[2].widget_id); try std.testing.expectEqual(@as(usize, 3), frame.widgets[2].subtree_size); const command = widget_query.findById(frame.widgets, 7, 11) orelse return error.MissingSolvedCommand; try std.testing.expectEqualStrings("open", command.action); try std.testing.expectEqual(@as(u8, 1), command.layer); try std.testing.expectEqual(@as(f32, 27), command.rect.x); try std.testing.expectEqual(@as(f32, 21), command.rect.y); try std.testing.expectEqual(@as(f32, 13), command.visible_rect.width); try std.testing.expectEqual(@as(f32, 4), command.visible_rect.height); const status = widget_query.findById(frame.widgets, 7, 12) orelse return error.MissingSolvedStatus; try std.testing.expectEqual(@as(u8, 2), status.layer); try std.testing.expectEqual(@as(f32, 36), status.rect.y); try std.testing.expect(status.has_text); try std.testing.expectEqual(@as(usize, 2), workspace.storage.?.layout_scratch.states.len);}test "surface frame rejects invalid solved spans before replacing the accepted frame" { const viewport_children = [_]UiNode{.{ .widget_id = 2, .size = .{ .width = 16, .height = 16 }, }}; const surface = UiSurfaceTree{ .available_size = .{ .width = 32, .height = 32 }, .root = .{ .widget_id = 1, .children = &viewport_children }, }; const external = UiNode{ .widget_id = 10 }; const bindings = [_]SolvedBinding{.{ .node = &external, .subtree_size = 1 }}; const rectangles = [_]flex_layout.Rect{.{ .width = 8, .height = 8 }}; const clips = [_]flex_layout.Rect{.{ .width = 8, .height = 8 }}; const offsets = [_]Offset{.{}}; const invalid = SolvedSubtree{ .viewport_widget_id = 2, .bindings = &bindings, .rectangles = &rectangles, .clips = &clips, .offsets = &.{}, }; const valid = SolvedSubtree{ .viewport_widget_id = 2, .bindings = &bindings, .rectangles = &rectangles, .clips = &clips, .offsets = &offsets, }; const invalid_bindings = [_]SolvedBinding{.{ .node = &external, .subtree_size = 2 }}; const invalid_geometry = [_]flex_layout.Rect{.{ .x = std.math.nan(f32), .width = 8, .height = 8, }}; try std.testing.expectError( error.InvalidSolvedBindings, surveyFrameRoot(.{ .surface = &surface, .solved_subtrees = &.{.{ .viewport_widget_id = 2, .bindings = &invalid_bindings, .rectangles = &rectangles, .clips = &clips, .offsets = &offsets, }}, }), ); try std.testing.expectError( error.InvalidSolvedGeometry, surveyFrameRoot(.{ .surface = &surface, .solved_subtrees = &.{.{ .viewport_widget_id = 2, .bindings = &bindings, .rectangles = &invalid_geometry, .clips = &clips, .offsets = &offsets, }}, }), ); try std.testing.expectError( error.SolvedViewportNotFound, surveyFrameRoot(.{ .surface = &surface, .solved_subtrees = &.{.{ .viewport_widget_id = 3, .bindings = &bindings, .rectangles = &rectangles, .clips = &clips, .offsets = &offsets, }}, }), ); try std.testing.expectError( error.DuplicateSolvedViewport, surveyFrameRoot(.{ .surface = &surface, .solved_subtrees = &.{ valid, valid }, }), ); var workspace = Workspace.init(std.testing.allocator); defer workspace.deinit(); const accepted = try workspace.buildSurface(&surface, .{ .revision = 1 }); const accepted_widgets = accepted.widgets.ptr; try std.testing.expectError( error.InvalidSolvedSpans, workspace.buildSurface(&surface, .{ .revision = 2, .solved_subtrees = &.{invalid}, }), ); try std.testing.expectEqual(accepted_widgets, accepted.widgets.ptr); try std.testing.expectEqual(@as(u64, 1), accepted.revision); try std.testing.expectEqual(@as(u64, 2), accepted.widgets[1].widget_id);}test "surface frame reuses admitted solved-subtree storage" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const viewport_children = [_]UiNode{.{ .widget_id = 2, .size = .{ .width = 16, .height = 16 }, }}; const surface = UiSurfaceTree{ .available_size = .{ .width = 32, .height = 32 }, .root = .{ .widget_id = 1, .children = &viewport_children }, }; const external_nodes = [_]UiNode{ .{ .widget_id = 10 }, .{ .widget_id = 11, .kind = .label, .text = .{ .content = "ready" } }, }; const bindings = [_]SolvedBinding{ .{ .node = &external_nodes[0], .subtree_size = 2 }, .{ .node = &external_nodes[1], .subtree_size = 1 }, }; const rectangles = [_]flex_layout.Rect{ .{ .width = 16, .height = 16 }, .{ .width = 12, .height = 8 }, }; const clips = [_]flex_layout.Rect{ .{ .width = 16, .height = 16 }, .{ .width = 16, .height = 16 }, }; const offsets = [_]Offset{ .{}, .{} }; const solved = SolvedSubtree{ .viewport_widget_id = 2, .bindings = &bindings, .rectangles = &rectangles, .clips = &clips, .offsets = &offsets, }; var workspace = Workspace.init(failing.allocator()); defer workspace.deinit(); for (0..2) |_| { const frame = try workspace.buildSurface(&surface, .{ .solved_subtrees = &.{solved} }); try std.testing.expectEqual(@as(usize, 4), frame.widget_count); } failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; for (0..8) |_| { const frame = try workspace.buildSurface(&surface, .{ .solved_subtrees = &.{solved} }); try std.testing.expectEqual(@as(usize, 4), frame.widget_count); } try std.testing.expect(!failing.has_induced_failure);}test "surface frame applies text intrinsic measurement to flex layout" { const allocator = std.testing.allocator; const children = [_]UiNode{ .{ .widget_id = 2, .kind = .label, .text = .{ .content = "abc" }, .size = .{ .height = 12 }, }, .{ .widget_id = 3, .kind = .label, .text = .{ .content = "z" }, .size = .{ .height = 12 }, }, }; const surface = UiSurfaceTree{ .available_size = .{ .width = 100, .height = 20 }, .root = .{ .widget_id = 1, .style = .{ .flex_direction = .row, .gap = 5, .align_items = .flex_start }, .children = children[0..], }, }; var workspace = Workspace.init(allocator); defer workspace.deinit(); const frame = try workspace.buildSurface(&surface, .{ .resolvers = .{ .text_size = measureTextForFrameTest }, }); const abc = widget_query.findById(frame.widgets, 1, 2) orelse return error.MissingAbc; const z = widget_query.findById(frame.widgets, 1, 3) orelse return error.MissingZ; try std.testing.expectEqual(@as(f32, 21), abc.rect.width); try std.testing.expectEqual(@as(f32, 26), z.rect.x); try std.testing.expectEqual(@as(f32, 7), z.rect.width);}fn measureTextForFrameTest(_: ?*anyopaque, node: *const UiNode) anyerror!?flex_layout.Size { const text = node.text orelse return null; return .{ .width = @as(f32, @floatFromInt(text.content.len)) * 7, .height = 11, };}test "frame rects snap to whole pixels and adjacent children share edges" { const allocator = std.testing.allocator; const children = [_]UiNode{ .{ .widget_id = 2, .size = .{ .width = 10.5, .height = 15 }, }, .{ .widget_id = 3, .size = .{ .width = 10.5, .height = 15 }, }, }; const surface = UiSurfaceTree{ .available_size = .{ .width = 40, .height = 20 }, .root = .{ .widget_id = 1, .style = .{ .flex_direction = .row, .align_items = .center }, .children = children[0..], }, }; var workspace = Workspace.init(allocator); defer workspace.deinit(); const frame = try workspace.buildSurface(&surface, .{}); const first = widget_query.findById(frame.widgets, 1, 2) orelse return error.MissingFirst; const second = widget_query.findById(frame.widgets, 1, 3) orelse return error.MissingSecond; for ([_]flex_layout.Rect{ first.rect, second.rect }) |rect| { try std.testing.expectEqual(rect.x, @round(rect.x)); try std.testing.expectEqual(rect.y, @round(rect.y)); try std.testing.expectEqual(rect.width, @round(rect.width)); try std.testing.expectEqual(rect.height, @round(rect.height)); } try std.testing.expectEqual(@as(f32, 3), first.rect.y); try std.testing.expectEqual(@as(f32, 15), first.rect.height); try std.testing.expectEqual(@as(f32, 11), first.rect.width); try std.testing.expectEqual(first.rect.x + first.rect.width, second.rect.x); try std.testing.expectEqual(@as(f32, 10), second.rect.width);}test "surface frame facts derive hovered widget from point" { const allocator = std.testing.allocator; const children = [_]UiNode{ .{ .widget_id = 2, .kind = .button, .size = .{ .width = 80, .height = 24 }, .text = .{ .content = "Run" }, .action = "demo.run", }, .{ .widget_id = 3, .kind = .button, .size = .{ .width = 80, .height = 24 }, .text = .{ .content = "Stop" }, .action = "demo.stop", }, }; const surface = UiSurfaceTree{ .available_size = .{ .width = 200, .height = 40 }, .root = .{ .widget_id = 1, .style = .{ .flex_direction = .row, .gap = 8, }, .children = children[0..], }, }; var workspace = Workspace.init(allocator); defer workspace.deinit(); const frame = try workspace.buildSurface(&surface, .{ .root_id = 43, .hovered_point = .{ .x = 92, .y = 12 }, }); const run = widget_query.findById(frame.widgets, 43, 2) orelse return error.MissingRunWidget; const stop = widget_query.findById(frame.widgets, 43, 3) orelse return error.MissingStopWidget; try std.testing.expect(!run.hovered); try std.testing.expect(stop.hovered);}test "Workspace retains frame storage at the high-water mark" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_highwater"), null, null, null, null, null, null, ); } var children: [16]UiNode = undefined; for (&children, 0..) |*child, index| { child.* = .{ .widget_id = index + 2, .size = .{ .width = 4, .height = 4 }, }; } var surface = UiSurfaceTree{ .available_size = .{ .width = 64, .height = 16 }, .root = .{ .widget_id = 1, .style = .{ .flex_direction = .row }, .children = children[0..], }, }; var workspace = Workspace.init(std.testing.allocator); defer workspace.deinit(); const large = try workspace.buildSurface(&surface, .{}); const root_storage = large.roots.ptr; const widget_storage = large.widgets.ptr; surface.root.children = children[0..2]; const small = try workspace.buildSurface(&surface, .{}); try std.testing.expectEqual(@as(usize, 3), small.widgets.len); try std.testing.expectEqual(root_storage, small.roots.ptr); try std.testing.expectEqual(widget_storage, small.widgets.ptr); surface.root.children = children[0..]; const regrown = try workspace.buildSurface(&surface, .{}); try std.testing.expectEqual(@as(usize, 17), regrown.widgets.len); try std.testing.expectEqual(root_storage, regrown.roots.ptr); try std.testing.expectEqual(widget_storage, regrown.widgets.ptr);}test "Workspace warmed builds need no backing allocation" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_steady_transitive_risk"), null, null, null, null, null, null, ); } comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_steady_foreign_risk"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const child = [_]UiNode{.{ .widget_id = 2, .size = .{ .width = 8, .height = 8 }, }}; const surface = UiSurfaceTree{ .available_size = .{ .width = 16, .height = 16 }, .root = .{ .widget_id = 1, .children = child[0..] }, }; var workspace = Workspace.init(failing.allocator()); defer workspace.deinit(); for (0..3) |_| _ = try workspace.buildSurface(&surface, .{}); failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; for (0..8) |_| { const frame = try workspace.buildSurface(&surface, .{}); try std.testing.expectEqual(@as(usize, 2), frame.widgets.len); } try std.testing.expect(!failing.has_induced_failure);}test "Workspace remains reusable after allocation failure" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_oom"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); const child = [_]UiNode{.{ .widget_id = 2, .size = .{ .width = 8, .height = 8 }, }}; const surface = UiSurfaceTree{ .available_size = .{ .width = 16, .height = 16 }, .root = .{ .widget_id = 1, .children = child[0..] }, }; var workspace = Workspace.init(failing.allocator()); defer workspace.deinit(); try std.testing.expectError(error.OutOfMemory, workspace.buildSurface(&surface, .{})); try std.testing.expect(workspace.storage == null); failing.fail_index = std.math.maxInt(usize); const frame = try workspace.buildSurface(&surface, .{}); try std.testing.expectEqual(@as(usize, 2), frame.widgets.len);}test "FrameStorage capacity matches an independent typed-byte model" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_capacity"), null, null, null, null, null, null, ); } const cases = [_]FrameStorage.Limits{ .{ .nodes = 1, .layout_nodes = 1, .children = 0, .roots = 1 }, .{ .nodes = 17, .layout_nodes = 9, .children = 8, .roots = 1 }, .{ .nodes = 4_096, .layout_nodes = 2_048, .children = 2_047, .roots = 8 }, .{ .nodes = std.math.maxInt(u32), .layout_nodes = std.math.maxInt(u32), .children = std.math.maxInt(u32), .roots = 64 }, }; for (cases) |limits| { const derived = try FrameStorage.Capacity.derive(limits); const modeled = try modelFrameCapacity(limits); try std.testing.expectEqual(modeled, derived); } try std.testing.expectError( error.CapacityOverflow, FrameStorage.Capacity.derive(.{ .nodes = 0, .layout_nodes = 0, .children = 0, .roots = 1 }), ); try std.testing.expectError( error.CapacityOverflow, FrameStorage.Capacity.derive(.{ .nodes = 1, .layout_nodes = 1, .children = 0, .roots = 0 }), );}test "FrameStorage acquires the exact surveyed equation and seals" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_acquisition"), null, null, null, null, null, null, ); } var storage = try FrameStorage.init(std.testing.allocator, .{ .nodes = 5, .layout_nodes = 3, .children = 4, .roots = 2, }); defer storage.deinit(std.testing.allocator); storage.activate(); try std.testing.expectEqual(@as(usize, 2), storage.roots.len); try std.testing.expectEqual(@as(usize, 5), storage.widgets.len); try std.testing.expectEqual(@as(usize, 4), storage.flex_nodes.len); try std.testing.expectEqual(@as(usize, 4), storage.layout_scratch.results.len); try std.testing.expectEqual(@as(usize, 4), storage.layout_scratch.metrics.len); try std.testing.expectEqual(@as(usize, 4), storage.layout_scratch.lines.len); try std.testing.expectEqual(@as(usize, 3), storage.layout_scratch.states.len); try std.testing.expectEqual(@as(usize, 4), storage.layout_scratch.child_indices.len); try std.testing.expect(storage.admits(.{ .nodes = 5, .layout_nodes = 3, .children = 4 }, 2)); try std.testing.expect(!storage.admits(.{ .nodes = 6, .layout_nodes = 3, .children = 4 }, 2)); try std.testing.expect(!storage.admits(.{ .nodes = 5, .layout_nodes = 4, .children = 4 }, 2)); try std.testing.expect(!storage.admits(.{ .nodes = 5, .layout_nodes = 3, .children = 5 }, 2)); try std.testing.expect(!storage.admits(.{ .nodes = 5, .layout_nodes = 3, .children = 4 }, 3));}test "surveySurface counts nodes and children exactly and rejects over-deep trees" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_survey"), null, null, null, null, null, null, ); } const grandchildren = [_]UiNode{ .{ .widget_id = 4 }, .{ .widget_id = 5 }, }; const children = [_]UiNode{ .{ .widget_id = 2, .children = grandchildren[0..] }, .{ .widget_id = 3 }, }; const surface = UiSurfaceTree{ .available_size = .{ .width = 64, .height = 64 }, .root = .{ .widget_id = 1, .children = children[0..] }, }; const facts = try surveySurface(&surface); try std.testing.expectEqual(@as(usize, 5), facts.nodes); try std.testing.expectEqual(@as(usize, 5), facts.layout_nodes); try std.testing.expectEqual(@as(usize, 4), facts.children); var spine: [max_depth + 1]UiNode = undefined; spine[max_depth] = .{ .widget_id = max_depth }; var level: usize = max_depth; while (level > 0) { level -= 1; spine[level] = .{ .widget_id = level, .children = spine[level + 1 ..][0..1], }; } const deep = UiSurfaceTree{ .available_size = .{ .width = 8, .height = 8 }, .root = spine[0], }; try std.testing.expectError(error.SurfaceTooDeep, surveySurface(&deep));}test "Session publishes double-buffered multi-root frames without reallocation" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_session"), null, null, null, null, null, null, ); } var session = Session.init(std.testing.allocator); defer session.deinit(); const first_children = [_]UiNode{ .{ .widget_id = 2, .size = .{ .width = 8, .height = 8 } }, }; const first = UiSurfaceTree{ .available_size = .{ .width = 32, .height = 16 }, .root = .{ .widget_id = 1, .children = first_children[0..] }, }; const second = UiSurfaceTree{ .available_size = .{ .width = 16, .height = 16 }, .root = .{ .widget_id = 10 }, }; const facts = (try surveySurface(&first)).add(try surveySurface(&second)); _ = try session.begin(facts, 2); try session.append(.{ .surface = &first, .root_id = 1, .revision = 7 }, .{}); try session.append(.{ .surface = &second, .root_id = 2, .revision = 7 }, .{}); const frame = session.publish(7); try std.testing.expectEqual(@as(usize, 2), frame.root_count); try std.testing.expectEqual(@as(usize, 3), frame.widget_count); try std.testing.expectEqual(@as(u64, 1), frame.roots[0].root_id); try std.testing.expectEqual(@as(u64, 2), frame.roots[1].root_id); const published_widgets = frame.widgets.ptr; var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); _ = failing.allocator(); _ = try session.begin(facts, 2); try session.append(.{ .surface = &first, .root_id = 1, .revision = 8 }, .{}); try session.append(.{ .surface = &second, .root_id = 2, .revision = 8 }, .{}); try std.testing.expectEqual(@as(u64, 1), frame.roots[0].root_id); try std.testing.expectEqual(published_widgets, frame.widgets.ptr); try std.testing.expect(!failing.has_induced_failure); const next = session.publish(8); try std.testing.expectEqual(@as(usize, 3), next.widget_count); try std.testing.expect(next.widgets.ptr != published_widgets);}test "Session steady builds at admitted demand make no allocator calls" { comptime { @stardustClaim( @import("alloc_phase").capacity.witness(FrameStorage, "gui_frame_session_steady"), null, null, null, null, null, null, ); } var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); var session = Session.init(failing.allocator()); defer session.deinit(); const children = [_]UiNode{ .{ .widget_id = 2, .size = .{ .width = 8, .height = 8 } }, .{ .widget_id = 3, .size = .{ .width = 8, .height = 8 } }, }; const surface = UiSurfaceTree{ .available_size = .{ .width = 32, .height = 16 }, .root = .{ .widget_id = 1, .children = children[0..] }, }; const facts = try surveySurface(&surface); for (0..2) |_| { _ = try session.begin(facts, 1); try session.append(.{ .surface = &surface, .root_id = 1, .revision = 1 }, .{}); _ = session.publish(1); } failing.fail_index = failing.alloc_index; failing.resize_fail_index = failing.resize_index; for (0..8) |iteration| { _ = try session.begin(facts, 1); try session.append(.{ .surface = &surface, .root_id = 1, .revision = iteration }, .{}); const frame = session.publish(iteration); try std.testing.expectEqual(@as(usize, 3), frame.widget_count); } try std.testing.expect(!failing.has_induced_failure);}Source: lib/gui/src/root.zig:11
zig
pub const frame = @import("frame.zig");Complete caller list for frame.Workspace.buildSurface
11 direct callers.
lib.gui.src.frame.test_Workspace_remains_reusable_after_allocation_failure[function] — test source atlib/gui/src/frame.zig:1660in nearest public ownertiny.gui.framelib.gui.src.frame.test_Workspace_retains_frame_storage_at_the_high-water_mark[function] — test source atlib/gui/src/frame.zig:1568in nearest public ownertiny.gui.framelib.gui.src.frame.test_Workspace_warmed_builds_need_no_backing_allocation[function] — test source atlib/gui/src/frame.zig:1614in nearest public ownertiny.gui.framelib.gui.src.frame.test_frame_rects_snap_to_whole_pixels_and_adjacent_children_share_edges[function] — test source atlib/gui/src/frame.zig:1485in nearest public ownertiny.gui.framelib.gui.src.frame.test_layers_propagate_from_a_subtree_root_to_its_descendants[function] — test source atlib/gui/src/frame.zig:1101in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_appends_an_externally_solved_subtree_beneath_an_opaque_viewport[function] — test source atlib/gui/src/frame.zig:1206in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_applies_text_intrinsic_measurement_to_flex_layout[function] — test source atlib/gui/src/frame.zig:1440in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_facts_derive_hovered_widget_from_point[function] — test source atlib/gui/src/frame.zig:1526in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_facts_preserve_roles_actions_and_hit_geometry[function] — test source atlib/gui/src/frame.zig:1144in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_rejects_invalid_solved_spans_before_replacing_the_accepted_frame[function] — test source atlib/gui/src/frame.zig:1293in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_reuses_admitted_solved-subtree_storage[function] — test source atlib/gui/src/frame.zig:1390in nearest public ownertiny.gui.frame
Complete caller list for frame.Workspace.deinit
11 direct callers.
lib.gui.src.frame.test_Workspace_remains_reusable_after_allocation_failure[function] — test source atlib/gui/src/frame.zig:1660in nearest public ownertiny.gui.framelib.gui.src.frame.test_Workspace_retains_frame_storage_at_the_high-water_mark[function] — test source atlib/gui/src/frame.zig:1568in nearest public ownertiny.gui.framelib.gui.src.frame.test_Workspace_warmed_builds_need_no_backing_allocation[function] — test source atlib/gui/src/frame.zig:1614in nearest public ownertiny.gui.framelib.gui.src.frame.test_frame_rects_snap_to_whole_pixels_and_adjacent_children_share_edges[function] — test source atlib/gui/src/frame.zig:1485in nearest public ownertiny.gui.framelib.gui.src.frame.test_layers_propagate_from_a_subtree_root_to_its_descendants[function] — test source atlib/gui/src/frame.zig:1101in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_appends_an_externally_solved_subtree_beneath_an_opaque_viewport[function] — test source atlib/gui/src/frame.zig:1206in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_applies_text_intrinsic_measurement_to_flex_layout[function] — test source atlib/gui/src/frame.zig:1440in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_facts_derive_hovered_widget_from_point[function] — test source atlib/gui/src/frame.zig:1526in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_facts_preserve_roles_actions_and_hit_geometry[function] — test source atlib/gui/src/frame.zig:1144in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_rejects_invalid_solved_spans_before_replacing_the_accepted_frame[function] — test source atlib/gui/src/frame.zig:1293in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_reuses_admitted_solved-subtree_storage[function] — test source atlib/gui/src/frame.zig:1390in nearest public ownertiny.gui.frame
Complete caller list for frame.Workspace.init
11 direct callers.
lib.gui.src.frame.test_Workspace_remains_reusable_after_allocation_failure[function] — test source atlib/gui/src/frame.zig:1660in nearest public ownertiny.gui.framelib.gui.src.frame.test_Workspace_retains_frame_storage_at_the_high-water_mark[function] — test source atlib/gui/src/frame.zig:1568in nearest public ownertiny.gui.framelib.gui.src.frame.test_Workspace_warmed_builds_need_no_backing_allocation[function] — test source atlib/gui/src/frame.zig:1614in nearest public ownertiny.gui.framelib.gui.src.frame.test_frame_rects_snap_to_whole_pixels_and_adjacent_children_share_edges[function] — test source atlib/gui/src/frame.zig:1485in nearest public ownertiny.gui.framelib.gui.src.frame.test_layers_propagate_from_a_subtree_root_to_its_descendants[function] — test source atlib/gui/src/frame.zig:1101in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_appends_an_externally_solved_subtree_beneath_an_opaque_viewport[function] — test source atlib/gui/src/frame.zig:1206in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_applies_text_intrinsic_measurement_to_flex_layout[function] — test source atlib/gui/src/frame.zig:1440in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_facts_derive_hovered_widget_from_point[function] — test source atlib/gui/src/frame.zig:1526in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_facts_preserve_roles_actions_and_hit_geometry[function] — test source atlib/gui/src/frame.zig:1144in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_rejects_invalid_solved_spans_before_replacing_the_accepted_frame[function] — test source atlib/gui/src/frame.zig:1293in nearest public ownertiny.gui.framelib.gui.src.frame.test_surface_frame_reuses_admitted_solved-subtree_storage[function] — test source atlib/gui/src/frame.zig:1390in nearest public ownertiny.gui.frame
Audit
| Definitions | 35 |
|---|---|
| Public names | 35 |
| Members | 37 |
| Version | 26.7.0 |
| Revision | daab053ee433 |