diff --git a/app/layout/Region.zig b/app/layout/Region.zig index bbe5bf638..01c54b483 100644 --- a/app/layout/Region.zig +++ b/app/layout/Region.zig @@ -1437,7 +1437,9 @@ fn initTree( /// A turn is the new half sliding shut along the old axis (`_fold`, which its place reads as a /// target of zero), then easing open along the new one. Its share of the box is kept through the /// turn (`_share`, taken from the extent when that was last chosen, not from a box that has since -/// changed shape), and follows a drag while turned. +/// changed shape), and follows a drag while turned. That drag is shown and nothing more: turning +/// back takes the share from the saved one again (`_seen` is unread while turned, so dvui drops it +/// and the first frame back reads the saved share anew). fn orientBranch(self: *Layout, saved: SplitTree.Branch, id: dvui.Id) SplitTree.Branch { if (!saved.adaptive) return saved; const dims = dvui.dataGet(null, id, "_dims", dvui.Size) orelse return saved; @@ -1462,10 +1464,10 @@ fn orientBranch(self: *Layout, saved: SplitTree.Branch, id: dvui.Id) SplitTree.B } } } else if (chosen) |extent| { - // Dragged since the branch set it: the drag is the new share. + // Dragged since the branch set it: the drag is the share drawn, and is not saved. const set = dvui.dataGet(null, id, "_set", f32) orelse extent; const room = alongSize(drawn.dir, dims); - if (set != extent and room > 0) setShare(self, saved.created, id, std.math.clamp(extent / room, 0, 1)); + if (set != extent and room > 0) dvui.dataSet(null, id, "_share", std.math.clamp(extent / room, 0, 1)); } const share = dvui.dataGet(null, id, "_share", f32) orelse return drawn; diff --git a/plugins/workbench/src/Workbench.zig b/plugins/workbench/src/Workbench.zig index e9848c5a6..f35d9f30f 100644 --- a/plugins/workbench/src/Workbench.zig +++ b/plugins/workbench/src/Workbench.zig @@ -231,20 +231,32 @@ pub fn sideBeside(self: *const Workbench, anchor: u64) Panes.Side { } /// What opening beside the person's pane asks of `core.placement.place`: the other pane that is -/// there, if any, and the answer over the person's room and that pane's. Null before the person's +/// shown, if any, and the answer over the person's room and that pane's. Null before the person's /// pane has drawn, with no room to judge. const Beside = struct { other: ?u64, room: ?core.placement.Size, answer: ?core.placement.Result }; +/// A pane other than `here` that is not sliding shut. +fn otherPane(self: *Workbench, here: u64) ?u64 { + for (self.workspaces.values()) |ws| { + if (ws.grouping != here and !self.paneClosing(ws.grouping)) return ws.grouping; + } + return null; +} + fn besideAnswer(self: *Workbench, here: u64) Beside { - const other = for (self.workspaces.values()) |ws| { - if (ws.grouping != here and !self.paneClosing(ws.grouping)) break ws.grouping; - } else null; - const room = self.paneSize(here) orelse return .{ .other = other, .room = null, .answer = null }; + const room = self.paneSize(here) orelse return .{ .other = self.otherPane(here), .room = null, .answer = null }; + // `place` takes the first of these whatever its size, so a pane is a candidate only if the + // person can be sent to it: drawn, and in a place of the app's. var others: [1]core.placement.Size = undefined; - const places: []const core.placement.Size = if (other) |g| blk: { - others[0] = self.paneSize(g) orelse .{ .w = 0, .h = 0 }; - break :blk &others; - } else &.{}; + var other: ?u64 = null; + for (self.workspaces.values()) |ws| { + if (ws.grouping == here or self.paneClosing(ws.grouping)) continue; + if (self.places) |p| if (!p.placed(p.ctx, ws.grouping)) continue; + others[0] = self.paneSize(ws.grouping) orelse continue; + other = ws.grouping; + break; + } + const places: []const core.placement.Size = if (other != null) &others else &.{}; return .{ .other = other, .room = room, .answer = core.placement.place(room, places) }; } diff --git a/src/editor/Workspaces.zig b/src/editor/Workspaces.zig index 0e31bfa65..3b614299f 100644 --- a/src/editor/Workspaces.zig +++ b/src/editor/Workspaces.zig @@ -119,6 +119,7 @@ pub fn sync(self: *Workspaces) void { self.settle(); } var first: ?u64 = null; + var l = layoutOf(editor); for (wb.workspaces.keys()) |g| { if (self.assigned(g)) continue; if (first == null) { @@ -126,11 +127,10 @@ pub fn sync(self: *Workspaces) void { continue; } // The anchor's room asks the side, when it has one yet. - var l = layoutOf(editor); var abuf: [64]u8 = undefined; const room = if (Layout.ViewDrag.holderOf(&l, viewId(&abuf, first.?))) |holder| Layout.ViewDrag.placeSize(l.state, holder) else null; const side = if (room) |r| Workbench.sideFor(r.w, r.h) else .right; - _ = beside(self, g, first.?, side, true); + _ = besideIn(&l, g, first.?, side, true); } } @@ -307,15 +307,18 @@ pub fn groupingIn(self: *Workspaces, name: []const u8) ?u64 { fn beside(ctx: *anyopaque, grouping: u64, anchor: u64, side: Workbench.Panes.Side, adaptive: bool) bool { const self: *Workspaces = @ptrCast(@alignCast(ctx)); - const editor = self.editorOf(); - var l = layoutOf(editor); + var l = layoutOf(self.editorOf()); + return besideIn(&l, grouping, anchor, side, adaptive); +} + +fn besideIn(l: *Layout, grouping: u64, anchor: u64, side: Workbench.Panes.Side, adaptive: bool) bool { var abuf: [64]u8 = undefined; - const place = Layout.ViewDrag.holderOf(&l, viewId(&abuf, anchor)) orelse return false; - const new = Layout.splitOn(&l, place, side) orelse return false; + const place = Layout.ViewDrag.holderOf(l, viewId(&abuf, anchor)) orelse return false; + const new = Layout.splitOn(l, place, side) orelse return false; var buf: [64]u8 = undefined; const id = l.arena.dupe(u8, viewId(&buf, grouping)) catch return false; l.state.assign(l.gpa, new, &.{id}) catch return false; - if (adaptive) Layout.markAdaptive(&l, new); + if (adaptive) Layout.markAdaptive(l, new); l.state.markDirty(); return true; } diff --git a/tests/integration.zig b/tests/integration.zig index eb08d8558..7e211b8c0 100644 --- a/tests/integration.zig +++ b/tests/integration.zig @@ -4060,6 +4060,74 @@ test "an adaptive split saved beside comes back as the same share stacked" { try std.testing.expectApproxEqAbs(0.5, bottom.h / (top.h + bottom.h), 0.03); } +// A sash dragged while an adaptive split is turned is shown and nothing more: the share the saved +// extent gave comes back with the turn back, and the next turn uses it, not the drag's. +test "a drag while an adaptive split is turned is not kept when it turns back" { + var ctx = try shim.init(std.testing.allocator); + defer ctx.deinit(std.testing.allocator); + + const editor = ctx.editor; + editor.app.gpa = std.testing.allocator; + defer editor.app.layout.regions.deinit(editor.app.gpa); + defer editor.app.layout.regions_building.deinit(editor.app.gpa); + defer editor.app.layout.deinitExtents(editor.app.gpa); + defer editor.app.layout.deinitAssignments(editor.app.gpa); + defer editor.app.layout.deinitQualified(editor.app.gpa); + + ColumnCardFrame.editor = editor; + defer ColumnCardFrame.editor = null; + + ctx.t.backend.size = .{ .w = 1180, .h = 820 }; + ctx.t.backend.size_pixels = .{ .w = 1180, .h = 820 }; + _ = try dvui.testing.step(ColumnCardFrame.frame); + try dvui.testing.settle(ColumnCardFrame.frame); + { + var layout = fizzy.Editor.Layout.init(&editor.app.host, &editor.app.layout, editor.app.gpa, dvui.currentWindow().arena()); + try std.testing.expectEqualStrings("Main/r1", layout.splitOn("Main", .right) orelse return error.TestExpectedEqual); + layout.markAdaptive("Main/r1"); + } + try dvui.testing.settle(ColumnCardFrame.frame); + const old = try ColumnCardFrame.sizeOf(editor, "Main"); + const new = try ColumnCardFrame.sizeOf(editor, "Main/r1"); + const share = new.w / (old.w + new.w); + const saved_extent = editor.app.layout.extent("Main/r1", 0); + editor.app.layout.dirty = false; + + ctx.t.backend.size = .{ .w = 820, .h = 1180 }; + ctx.t.backend.size_pixels = .{ .w = 820, .h = 1180 }; + for (0..40) |_| _ = try dvui.testing.step(ColumnCardFrame.frame); + + // The sash dragged to a third of the room, as `Split` writes a drag. + const room = (try ColumnCardFrame.sizeOf(editor, "Main")).h + (try ColumnCardFrame.sizeOf(editor, "Main/r1")).h; + const leaf = for (editor.app.layout.regions.items) |r| { + if (std.mem.eql(u8, r.name, "Main/r1")) break r.id; + } else return error.TestExpectedEqual; + dvui.dataSet(null, leaf, "_size", room / 3); + dvui.dataSet(null, leaf, "_shown", room / 3); + for (0..4) |_| _ = try dvui.testing.step(ColumnCardFrame.frame); + const dragged = try ColumnCardFrame.sizeOf(editor, "Main/r1"); + try std.testing.expectApproxEqAbs(room / 3, dragged.h, room * 0.05); + try std.testing.expect(!editor.app.layout.dirty); + try std.testing.expectEqual(saved_extent, editor.app.layout.extent("Main/r1", 0)); + + // Back across: the saved share, not the drag's. + ctx.t.backend.size = .{ .w = 1180, .h = 820 }; + ctx.t.backend.size_pixels = .{ .w = 1180, .h = 820 }; + for (0..40) |_| _ = try dvui.testing.step(ColumnCardFrame.frame); + const left = try ColumnCardFrame.sizeOf(editor, "Main"); + const right = try ColumnCardFrame.sizeOf(editor, "Main/r1"); + try std.testing.expectApproxEqAbs(share, right.w / (left.w + right.w), 0.03); + + // And upright again: the share is still the saved one. + ctx.t.backend.size = .{ .w = 820, .h = 1180 }; + ctx.t.backend.size_pixels = .{ .w = 820, .h = 1180 }; + for (0..40) |_| _ = try dvui.testing.step(ColumnCardFrame.frame); + const top = try ColumnCardFrame.sizeOf(editor, "Main"); + const bottom = try ColumnCardFrame.sizeOf(editor, "Main/r1"); + try std.testing.expectApproxEqAbs(share, bottom.h / (top.h + bottom.h), 0.03); + try std.testing.expect(!editor.app.layout.dirty); +} + test "an adaptive split saved without a share loads with the ratio absent" { var ctx = try shim.init(std.testing.allocator); defer ctx.deinit(std.testing.allocator); @@ -11350,6 +11418,16 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes // With another pane there, it takes the view; without, the person's own does. const taken = wb.besideGrouping(); if (others > 0) try std.testing.expect(taken != here) else try std.testing.expectEqual(here, taken); + // A pane that has not drawn is no place to send the view: it joins the person's pane. + if (others > 0) { + const other = for (wb.workspaces.values()) |*o| { + if (o.grouping != here) break o; + } else return error.TestUnexpectedResult; + const other_rect = other.pane_rect_physical; + defer other.pane_rect_physical = other_rect; + other.pane_rect_physical = null; + try std.testing.expectEqual(here, wb.besideGrouping()); + } // A demo's `drop:beside` is the answer for the place it is aimed at, not the person's pane's // for every place: Main's is the pane's edge in its own room, which turns with the room, // and a place that holds no documents has none.