a73x

7f85da50

refactor: one weight split per axis, and a leaf root inserts by splitting

a73x   2026-08-29 10:01

Commit message
refactor: one weight split per axis, and a leaf root inserts by splitting

flattenNode's stacked and beside arms each spelled the same twelve lines
of weight division plus largest-remainder-first fixup; weightSplit owns
them, so both axes always add back to the space they were given and the
rail reservation stays the beside arm's alone. Tree.insert's leaf-root
branch was split(focus, tile, .stacked) written out again, down to the
{1,1} weights, and now calls it.

Pinned by wall_test_layout and layout.zig's own tests: resize stays
gain-only, and the flattened rects (rails included) are unchanged.

src/client/layout.zig
Old New
@@ -193,27 +193,14 @@ pub const Tree = struct {
193 /// `.stacked` container holding `[old, new]` — matching today's stripes. 193 /// `.stacked` container holding `[old, new]` — matching today's stripes.
194 pub fn insert(self: *Tree, focus: u8, tile: u8) !void { 194 pub fn insert(self: *Tree, focus: u8, tile: u8) !void {
195 const found = self.findLeaf(focus) orelse return error.NotFound; 195 const found = self.findLeaf(focus) orelse return error.NotFound;
196 if (found.parent) |p| { 196 // A leaf root is a stacked split of itself, which is what `split`
197 const new_node = try self.alloc.create(Node); 197 // already does when the focused leaf has no parent.
198 new_node.* = .{ .leaf = tile }; 198 const p = found.parent orelse return self.split(focus, tile, .stacked);
199 const w = p.weights.items[found.index]; 199 const new_node = try self.alloc.create(Node);
200 try p.children.insert(self.alloc, found.index + 1, new_node); 200 new_node.* = .{ .leaf = tile };
201 try p.weights.insert(self.alloc, found.index + 1, w); 201 const w = p.weights.items[found.index];
202 } else { 202 try p.children.insert(self.alloc, found.index + 1, new_node);
203 // Root is a leaf — wrap it in a stacked container. The old 203 try p.weights.insert(self.alloc, found.index + 1, w);
204 // node stays a leaf child; a new node holds the container.
205 const c = try self.alloc.create(Container);
206 c.* = .{ .orient = .stacked, .children = .empty, .weights = .empty };
207 try c.children.append(self.alloc, found.node);
208 const new_node = try self.alloc.create(Node);
209 new_node.* = .{ .leaf = tile };
210 try c.children.append(self.alloc, new_node);
211 try c.weights.append(self.alloc, 1);
212 try c.weights.append(self.alloc, 1);
213 const c_node = try self.alloc.create(Node);
214 c_node.* = .{ .container = c };
215 self.root = c_node;
216 }
217 } 204 }
218 205
219 /// Replace the focused leaf with a two-child container of the forced 206 /// Replace the focused leaf with a two-child container of the forced
@@ -821,6 +808,25 @@ fn flattenFullscreen(
821 } 808 }
822 } 809 }
823 810
811 /// One axis of a container, cut by weight: `avail` cells across `weights`,
812 /// the division's remainder handed out a cell at a time from the first
813 /// child, so the parts always add back to `avail`. Caller frees.
814 fn weightSplit(alloc: std.mem.Allocator, weights: []const u32, avail: u16, total: u64) ![]u16 {
815 const base = try alloc.alloc(u16, weights.len);
816 var sum: u64 = 0;
817 for (weights, 0..) |w, i| {
818 base[i] = @intCast(@as(u64, avail) * w / total);
819 sum += base[i];
820 }
821 var rem = avail - @as(u16, @intCast(sum));
822 var i: usize = 0;
823 while (rem > 0 and i < weights.len) : (i += 1) {
824 base[i] += 1;
825 rem -= 1;
826 }
827 return base;
828 }
829
824 fn flattenNode( 830 fn flattenNode(
825 alloc: std.mem.Allocator, 831 alloc: std.mem.Allocator,
826 placed: *std.ArrayList(Placed), 832 placed: *std.ArrayList(Placed),
@@ -849,19 +855,8 @@ fn flattenNode(
849 855
850 switch (c.orient) { 856 switch (c.orient) {
851 .stacked => { 857 .stacked => {
852 var base = try alloc.alloc(u16, n); 858 const base = try weightSplit(alloc, c.weights.items, rows, total_weight);
853 defer alloc.free(base); 859 defer alloc.free(base);
854 var sum: u64 = 0;
855 for (c.weights.items, 0..) |w, i| {
856 base[i] = @intCast(@as(u64, rows) * w / total_weight);
857 sum += base[i];
858 }
859 var rem = rows - @as(u16, @intCast(sum));
860 var i: usize = 0;
861 while (rem > 0 and i < n) : (i += 1) {
862 base[i] += 1;
863 rem -= 1;
864 }
865 var cur_top = top; 860 var cur_top = top;
866 for (c.children.items, 0..) |child, ci| { 861 for (c.children.items, 0..) |child, ci| {
867 try flattenNode(alloc, placed, rails, child, cur_top, left, base[ci], cols, floors); 862 try flattenNode(alloc, placed, rails, child, cur_top, left, base[ci], cols, floors);
@@ -872,20 +867,8 @@ fn flattenNode(
872 // Reserve n-1 columns for rails, then split cols by weight. 867 // Reserve n-1 columns for rails, then split cols by weight.
873 const rail_count: u16 = @intCast(n - 1); 868 const rail_count: u16 = @intCast(n - 1);
874 if (cols < rail_count) return error.TooSmall; 869 if (cols < rail_count) return error.TooSmall;
875 const avail = cols - rail_count; 870 const base = try weightSplit(alloc, c.weights.items, cols - rail_count, total_weight);
876 var base = try alloc.alloc(u16, n);
877 defer alloc.free(base); 871 defer alloc.free(base);
878 var sum: u64 = 0;
879 for (c.weights.items, 0..) |w, i| {
880 base[i] = @intCast(@as(u64, avail) * w / total_weight);
881 sum += base[i];
882 }
883 var rem = avail - @as(u16, @intCast(sum));
884 var i: usize = 0;
885 while (rem > 0 and i < n) : (i += 1) {
886 base[i] += 1;
887 rem -= 1;
888 }
889 var cur_left = left; 872 var cur_left = left;
890 for (c.children.items, 0..) |child, ci| { 873 for (c.children.items, 0..) |child, ci| {
891 try flattenNode(alloc, placed, rails, child, top, cur_left, rows, base[ci], floors); 874 try flattenNode(alloc, placed, rails, child, top, cur_left, rows, base[ci], floors);