diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/layout.zig | 77 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 55 |
2 files changed, 114 insertions, 18 deletions
diff --git a/src/layout.zig b/src/layout.zig index cbf0fd96..09cf23b3 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -416,10 +416,71 @@ pub fn acmePlace(p: *Pardes, c: usize, origin: ?usize, id: usize) void { /// its tag (acme's minht keeps one). pub const min_body_rows = 2; -fn minRows(p: *Pardes, id: usize) u16 { +/// The rows a placement, and a `size`, leave a pane: its tag at its width +/// and `min_body_rows`. The one minimum both ask. +pub fn minRows(p: *Pardes, id: usize) u16 { return p.wantedTagRows(id) + min_body_rows; } +/// Whether every column has the rows for each of its unfolded panes' +/// `minRows` (a folded one its tag row): the panes' own minima, not how +/// the weights happen to share the rows, so a size that fits keeps fitting +/// when it grows. One pane alone in its column always fits. +pub fn minimaFit(p: *Pardes) bool { + compute(p); + const avail = p.screen_h -| (p.topBarHeight() + p.columnBarHeight()); + for (0..p.ncol) |c| { + if (p.col_n[c] < 2) continue; + var need: u32 = 0; + for (p.col_panes[c][0..p.col_n[c]]) |k| { + const pane = p.panes[k] orelse continue; + need += if (pane.collapsed) BOX_H else minRows(p, k); + } + if (need > avail) return false; + } + return true; +} + +/// After a resize the minima fit: each unfolded pane the resize took below +/// its `minRows` (it had them in `before`) gets them back, a row at a time +/// from whichever pane of its column has the most above its own. A pane +/// dragged short before keeps what it had. +pub fn restoreMinima(p: *Pardes, before: *const [MAX_PANES]Rect) void { + compute(p); + for (0..p.ncol) |c| { + const n = p.col_n[c]; + if (n < 2) continue; + var hs: [MAX_PANES]u16 = undefined; + var mins: [MAX_PANES]u16 = undefined; + var short = false; + for (p.col_panes[c][0..n], 0..) |k, i| { + const pane = p.panes[k] orelse return; + if (pane.collapsed) break; + hs[i] = p.rects[k].h; + mins[i] = minRows(p, k); + if (hs[i] < mins[i] and before[k].h >= mins[i]) short = true; + } else { + if (!short) continue; + for (p.col_panes[c][0..n], 0..) |k, i| { + if (hs[i] >= mins[i] or before[k].h < mins[i]) continue; + while (hs[i] < mins[i]) { + var giver: ?usize = null; + var spare: u16 = 0; + for (0..n) |j| if (j != i and hs[j] -| mins[j] > spare) { + spare = hs[j] - mins[j]; + giver = j; + }; + const g = giver orelse break; + hs[g] -= 1; + hs[i] += 1; + } + } + for (p.col_panes[c][0..n], 0..) |k, i| p.panes[k].?.vweight = @floatFromInt(@max(1, hs[i])); + } + } + compute(p); +} + /// After placing `id`: whether it, and every pane of its column that the /// placement shrank, still has its tag and `min_body_rows` (from `before`, /// the rects as they were). A pane alone in its column always has room: the @@ -437,20 +498,6 @@ pub fn placedRoomy(p: *Pardes, id: usize, before: *const [MAX_PANES]Rect) bool { return true; } -/// Whether every unfolded pane sharing its column has its tag and -/// `min_body_rows` (one alone in its column always fits). -pub fn everyPaneRoomy(p: *Pardes) bool { - compute(p); - for (0..p.ncol) |c| { - if (p.col_n[c] < 2) continue; - for (p.col_panes[c][0..p.col_n[c]]) |k| { - const pane = p.panes[k] orelse continue; - if (!pane.collapsed and p.rects[k].h < minRows(p, k)) return false; - } - } - return true; -} - /// Places `id` last in column `c`, where acme's coladd puts a new window, /// with half the rows of the column's tallest unfolded pane, when that /// leaves both their rows; false, and `id` in no column, when not. diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 02201de0..665913fc 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -521,15 +521,20 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply { if (!p.unattended) return refuse(p, req, "size: a frontend is attached and owns the size", line); if (apply) { const was: [2]u16 = .{ p.screen_w, p.screen_h }; + const before = p.rects; p.update(.{ .resize = .{ .cols = size[0], .rows = size[1] } }); p.sync(); // Every pane keeps its tag and two rows, as placement - // leaves them: a size too small for that is refused. - if (!pardes.layout.everyPaneRoomy(p)) { + // leaves them (layout.minRows, the one minimum): a size + // too small for the panes' minima is refused, and one + // they fit gives back rows the resize took under them. + if (!pardes.layout.minimaFit(p)) { p.update(.{ .resize = .{ .cols = was[0], .rows = was[1] } }); p.sync(); return refuse(p, req, "size: too small for the panes, each its tag and 2 rows", line); } + pardes.layout.restoreMinima(p, &before); + p.sync(); } continue; } @@ -548,7 +553,8 @@ pub fn writeRoot(p: *Pardes, req: Req) Reply { const size_min = [2]u16{ 20, 6 }; const size_max = [2]u16{ 4096, 4096 }; -const e_size = std.fmt.comptimePrint("size takes <cols> <rows>, at least {d}x{d}, at most {d}x{d}", .{ size_min[0], size_min[1], size_max[0], size_max[1] }); +// "invalid": the word 9ns reads as EINVAL (enameToErrno), as winsize's. +const e_size = std.fmt.comptimePrint("invalid size: takes <cols> <rows>, at least {d}x{d}, at most {d}x{d}", .{ size_min[0], size_min[1], size_max[0], size_max[1] }); /// `size <cols> <rows>`, within size_min and size_max. fn sizeOf(line: []const u8) ?[2]u16 { @@ -1703,6 +1709,49 @@ test "size sets the screen of a session no frontend is attached to, and only of try testing.expectEqual(@as(u16, 40), p.screen_h); } +test "size is monotonic: growing is never refused, and a size once taken is taken again" { + const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + p.unattended = true; + const root_ctl = @intFromEnum(tree.TopFile.ctl); + const new = @intFromEnum(tree.TopFile.new); + // The dogfood's repro: Newcol, three pane/new, a Tty, then pane/new + // until a column has no room (ENOSPC). + try testing.expectEqual(Status.ok, wr(p, root_ctl, "Newcol\n").reply.status); + for (0..3) |_| try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status); + try testing.expectEqual(Status.ok, wr(p, Node.of(p.panes[p.active].?.serial, .ctl), "Tty\n").reply.status); + p.sync(); + while (call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status == .ok) p.sync(); + p.sync(); + var line: [32]u8 = undefined; + // Down a row at a time (59, 58, 57, 56 and on) until one is refused: + // once refused, every smaller one is too. + var taken: [60]bool = @splat(false); + var rows: u16 = 59; + var refused = false; + while (rows >= 20) : (rows -= 1) { + taken[rows] = wr(p, root_ctl, try std.fmt.bufPrint(&line, "size 160 {d}\n", .{rows})).reply.status == .ok; + if (refused) try testing.expect(!taken[rows]); + if (!taken[rows]) refused = true; + } + try testing.expect(refused); // the columns are full: some size is too small + // Back up: each size is taken again exactly when it was on the way down. + rows = 20; + while (rows <= 59) : (rows += 1) { + const ok = wr(p, root_ctl, try std.fmt.bufPrint(&line, "size 160 {d}\n", .{rows})).reply.status == .ok; + try testing.expectEqual(taken[rows], ok); + } + for ([_]u16{ 60, 61, 64, 70, 100 }) |grown| { + try testing.expectEqual(Status.ok, wr(p, root_ctl, try std.fmt.bufPrint(&line, "size 160 {d}\n", .{grown})).reply.status); + try testing.expectEqual(grown, p.screen_h); + } + // Out of range is EINVAL, in words 9ns reads as that. + const bad = wr(p, root_ctl, "size 5 2\n"); + try testing.expectEqual(E.INVAL, bad.errno()); + try testing.expectStringStartsWith(bad.reply.ename, "invalid size"); +} + test "Repl with no such language names a few whole, and where the rest are" { const p = try th.withTerm(testing.allocator); defer p.deinit(); |
