lib/arrange/src/flow.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const types = @import("types.zig");
3
4 const JustifyContent = types.JustifyContent;
5 const Node = types.Node;
6
7 pub const ChildMetrics = struct {
8 index: usize,
9 state_index: usize = 0,
10 base_main: f32,
11 base_cross: f32,
12 target_main: f32,
13 min_main: f32 = 0,
14 max_main: ?f32 = null,
15 flex_grow: f32 = 0,
16 flex_shrink: f32 = 0,
17 frozen: bool = false,
18 };
19
20 pub const Line = struct {
21 items: []ChildMetrics = &.{},
22 cross_size: f32 = 0,
23 };
24
25 pub const LineOffsets = struct {
26 offset: f32,
27 between_gap: f32,
28 };
29
30 pub fn resolveFlexibleLengths(line: *Line, main_limit: f32, gap: f32) void {
31 if (line.items.len == 0) return;
32
33 const gap_total = gap * @as(f32, @floatFromInt(line.items.len - 1));
34 const available = main_limit - gap_total;
35 var hypothetical_sum: f32 = 0;
36 for (line.items) |item| hypothetical_sum += hypotheticalMain(item);
37 const growing = hypothetical_sum < available;
38
39 for (line.items) |*item| {
40 const hypothetical = hypotheticalMain(item.*);
41 const factor = if (growing) item.flex_grow else item.flex_shrink;
42 const violates = if (growing)
43 item.base_main > hypothetical
44 else
45 item.base_main < hypothetical;
46 item.target_main = if (factor == 0 or violates)
47 hypothetical
48 else
49 item.base_main;
50 item.frozen = factor == 0 or violates;
51 }
52 const initial_free = available - occupiedMain(line);
53
54 for (0..line.items.len + 1) |_| {
55 var remaining = available - occupiedMain(line);
56 const raw_factor_sum = rawFlexibleFactorSum(line, growing);
57 if (raw_factor_sum < 1) {
58 const scaled = initial_free * raw_factor_sum;
59 if (@abs(scaled) < @abs(remaining)) remaining = scaled;
60 }
61 const factor_sum = flexibleFactorSum(line, growing);
62 if (factor_sum == 0) {
63 constrainTargets(line);
64 return;
65 }
66 distributeRemaining(line, growing, remaining, factor_sum);
67
68 const violation = totalViolation(line);
69 if (@abs(violation) <= 0.001) {
70 constrainTargets(line);
71 return;
72 }
73
74 const frozen_before = frozenCount(line);
75 freezeViolations(line, violation);
76 std.debug.assert(frozenCount(line) > frozen_before);
77 }
78 unreachable;
79 }
80
81 pub fn hypotheticalMain(item: ChildMetrics) f32 {
82 var size = @max(item.base_main, item.min_main);
83 if (item.max_main) |maximum| size = @min(size, maximum);
84 return size;
85 }
86
87 pub fn resolveLineCrossSizeKnown(node: *const Node, line: *Line, cross_limit: ?f32) f32 {
88 var line_cross: f32 = 0;
89 for (line.items) |item| {
90 line_cross = @max(line_cross, item.base_cross);
91 }
92 if (node.style.wrap == .no_wrap) {
93 if (cross_limit) |limit| return limit;
94 }
95 return line_cross;
96 }
97
98 pub fn justifyLine(
99 justify_content: JustifyContent,
100 line: *const Line,
101 main_limit: f32,
102 gap: f32,
103 ) LineOffsets {
104 if (line.items.len == 0) {
105 return .{ .offset = 0, .between_gap = gap };
106 }
107
108 var occupied: f32 = 0;
109 for (line.items, 0..) |item, index| {
110 occupied += item.target_main;
111 if (index + 1 < line.items.len) occupied += gap;
112 }
113 const extra_space = main_limit - occupied;
114 if (extra_space < 0) {
115 return .{ .offset = 0, .between_gap = gap };
116 }
117
118 return switch (justify_content) {
119 .flex_start => .{ .offset = 0, .between_gap = gap },
120 .flex_end => .{ .offset = extra_space, .between_gap = gap },
121 .center => .{ .offset = extra_space / 2, .between_gap = gap },
122 .space_between => if (line.items.len == 1)
123 .{ .offset = 0, .between_gap = gap }
124 else
125 .{
126 .offset = 0,
127 .between_gap = gap + extra_space / @as(f32, @floatFromInt(line.items.len - 1)),
128 },
129 .space_around => blk: {
130 const share = extra_space / @as(f32, @floatFromInt(line.items.len));
131 break :blk .{
132 .offset = share / 2,
133 .between_gap = gap + share,
134 };
135 },
136 .space_evenly => blk: {
137 const share = extra_space / @as(f32, @floatFromInt(line.items.len + 1));
138 break :blk .{
139 .offset = share,
140 .between_gap = gap + share,
141 };
142 },
143 };
144 }
145
146 fn occupiedMain(line: *const Line) f32 {
147 var occupied: f32 = 0;
148 for (line.items) |item| {
149 occupied += if (item.frozen) item.target_main else item.base_main;
150 }
151 return occupied;
152 }
153
154 fn flexibleFactorSum(line: *const Line, growing: bool) f32 {
155 var sum: f32 = 0;
156 for (line.items) |item| {
157 if (item.frozen) continue;
158 sum += if (growing)
159 item.flex_grow
160 else
161 item.flex_shrink * item.base_main;
162 }
163 return sum;
164 }
165
166 fn rawFlexibleFactorSum(line: *const Line, growing: bool) f32 {
167 var sum: f32 = 0;
168 for (line.items) |item| {
169 if (item.frozen) continue;
170 sum += if (growing) item.flex_grow else item.flex_shrink;
171 }
172 return sum;
173 }
174
175 fn distributeRemaining(
176 line: *Line,
177 growing: bool,
178 remaining: f32,
179 factor_sum: f32,
180 ) void {
181 for (line.items) |*item| {
182 if (item.frozen) continue;
183 const factor = if (growing)
184 item.flex_grow
185 else
186 item.flex_shrink * item.base_main;
187 const distributed = if (growing)
188 @max(remaining, 0) * factor / factor_sum
189 else
190 @max(-remaining, 0) * factor / factor_sum;
191 item.target_main = if (growing)
192 item.base_main + distributed
193 else
194 item.base_main - distributed;
195 }
196 }
197
198 fn totalViolation(line: *const Line) f32 {
199 var total: f32 = 0;
200 for (line.items) |item| {
201 if (item.frozen) continue;
202 total += constrainedMain(item, item.target_main) - item.target_main;
203 }
204 return total;
205 }
206
207 fn freezeViolations(line: *Line, total: f32) void {
208 for (line.items) |*item| {
209 if (item.frozen) continue;
210 const constrained = constrainedMain(item.*, item.target_main);
211 const violation = constrained - item.target_main;
212 item.target_main = constrained;
213 if ((total > 0 and violation > 0) or (total < 0 and violation < 0)) {
214 item.frozen = true;
215 }
216 }
217 }
218
219 fn constrainTargets(line: *Line) void {
220 for (line.items) |*item| {
221 item.target_main = constrainedMain(item.*, item.target_main);
222 }
223 }
224
225 fn constrainedMain(item: ChildMetrics, size: f32) f32 {
226 var constrained = @max(size, item.min_main);
227 if (item.max_main) |maximum| {
228 constrained = @min(constrained, maximum);
229 }
230 return constrained;
231 }
232
233 fn frozenCount(line: *const Line) usize {
234 var count: usize = 0;
235 for (line.items) |item| count += @intFromBool(item.frozen);
236 return count;
237 }