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 }