Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 150 additions & 9 deletions app/layout/Layout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ extents_changed: bool = false,
/// Every surface `draw` ran this frame, so `captureUnplaced` knows which ones genuinely drew
/// nowhere — as opposed to drew before the picker asked. Arena-backed, per frame.
drawn: std.ArrayListUnmanaged(*Surface) = .empty,
/// Views put away or shown mid-frame (`hideLater`, `revealLater`), in the order asked, for
/// `applyLater` once nothing walks the places. Arena-backed, per frame.
later: std.ArrayListUnmanaged(Later) = .empty,

/// Open regions, innermost last. Every region pushes; its `deinit` pops. This is what lets
/// `split` know which axis it divides and which neighbour it resizes, without a shape having to
Expand Down Expand Up @@ -578,23 +581,161 @@ pub fn surface(self: *Layout, id: []const u8) ?*Surface {

/// Bring `id` into sight wherever it lives: select it in the one region that shows it (the rule
/// `claimedElsewhere` states: its assignment, else the place that accepts it best), and open that
/// region if the user shut it or a narrow window folded it away. False when no region shows it:
/// the shape has no place for it, or it is hidden.
/// region, and the tray it was split out of, if the user shut it or a narrow window folded it
/// away. False when no region shows it: the shape has no place for it, or it is hidden.
///
/// What a plugin's "Show X" command asks for (`Host.revealSurface`): it names the view, never the
/// place, so it lands wherever the user moved the view, in any shape.
pub fn reveal(self: *Layout, id: []const u8) bool {
const s = self.surface(id) orelse return false;
if (s.hidden) return false;
const at = self.locate(s, .any) orelse return false;
self.selectIn(at.place, id);
if (at.place.isClosed() or at.place.isFolded()) at.place.open();
if (at.tray) |t| if (t.isClosed() or t.isFolded()) t.open();
return true;
}

/// Put `id` out of sight, as the user would: `reveal` undone (`Host.hideSurface`; the contract is
/// `docs/PLUGINS.md` §3.1.2, "Showing and putting away"). In a tray, or a place split off one,
/// the tray shuts; in a float, the view alone goes home (`ViewDrag.floatViewHome`) and the tray it
/// lands selected in shuts, folded or not; anywhere else nothing changes. It changes the lists
/// the shape and the floats walk while they draw, so mid-frame it is `hideLater`'s, never this.
pub fn hide(self: *Layout, id: []const u8) bool {
const s = self.surface(id) orelse return false;
switch (self.away(s) orelse return false) {
.shut => |tray| tray.close(),
.float => |float| {
if (!ViewDrag.floatViewHome(self, float, s.id)) return false;
// Last frame's places still list the float's, whose keywords alone might show this
// view again: home is looked for in the main window only.
const home = self.locate(s, .main_window) orelse return true;
const shown = self.selectedIn(home.place) orelse return true;
if (shown != s) return true;
const tray = home.tray orelse return !self.inSight(home, s);
if (!tray.isClosed()) tray.close();
},
}
return true;
}

/// `hide`, asked mid-frame (a button in a surface): kept for `applyLater`, which the application
/// calls once the shape and the floats have drawn, in the order asked with `revealLater`'s. The
/// answer is the one `hide` would give: it is in sight in a tray, or in a float it will leave
/// for a place that does not leave it in sight.
pub fn hideLater(self: *Layout, id: []const u8) bool {
const s = self.surface(id) orelse return false;
const will = switch (self.away(s) orelse return false) {
.shut => true,
.float => |float| self.floatHomeHides(float, s),
};
self.keepLater(id, .hide) catch return false;
return will;
}

/// `reveal`, asked mid-frame: kept for `applyLater` with the hides, so a hide and then a reveal
/// in one frame end with the view shown. The answer is the one `reveal` would give.
pub fn revealLater(self: *Layout, id: []const u8) bool {
const s = self.surface(id) orelse return false;
if (s.hidden) return false;
_ = self.locate(s, .any) orelse return false;
self.keepLater(id, .reveal) catch return false;
return true;
}

fn keepLater(self: *Layout, id: []const u8, act: Later.Act) !void {
try self.later.append(self.arena, .{ .id = try self.arena.dupe(u8, id), .act = act });
}

/// Apply what `hideLater` and `revealLater` kept, in the order they were asked. The application
/// calls it after the shape, the floats, the drag overlay and the picker have drawn, before it
/// reads `extents_changed`.
pub fn applyLater(self: *Layout) void {
for (self.later.items) |l| switch (l.act) {
.hide => _ = self.hide(l.id),
.reveal => _ = self.reveal(l.id),
};
self.later.clearRetainingCapacity();
}

/// Whether taking `s` out of float `float` leaves it out of sight, as `hide` finds once it has:
/// where it will land (`ViewDrag.floatHomeFor`) is a tray, which shuts, or a place shut or folded.
/// Landing where no place names it (its keywords bring it back), it is not selected anywhere.
fn floatHomeHides(self: *Layout, float: []const u8, s: *const Surface) bool {
const name = ViewDrag.floatHomeFor(self, float, s.id) orelse return true;
const r = ViewDrag.regionNamed(self.state, name) orelse return true;
if (self.trayOf(r) != null) return true;
return r.isClosed() or r.isFolded();
}

/// A hide or reveal asked mid-frame.
const Later = struct {
id: []const u8,
act: Act,

const Act = enum { hide, reveal };
};

/// What putting `s` away does, worked out before anything changes; null when it is not in sight
/// or nothing round it shuts.
const Away = union(enum) {
/// The tray to shut.
shut: *const Region,
/// The float to take it out of.
float: []const u8,
};

fn away(self: *Layout, s: *const Surface) ?Away {
if (s.hidden) return null;
const at = self.locate(s, .open) orelse return null;
if (!self.inSight(at, s)) return null;
if (self.state.floats.rootOf(at.place.name)) |float| {
if (!ViewDrag.canFloatViewHome(self, float, s.id)) return null;
return .{ .float = float };
}
return .{ .shut = at.tray orelse return null };
}

/// Where a view is: the place showing it, and the tray that place is or was split out of.
const Located = struct { place: *const Region, tray: ?*const Region };

/// The one place showing `s` (the rule `claimedElsewhere` states: its assignment, else the place
/// that accepts it best), among this frame's places so far and last frame's rest, with its tray.
/// `main_window` leaves out the floats' places, `open` only the places of a float flying shut
/// (what it holds is going home: there is nothing there to put away, though `reveal` still finds
/// a view not home yet).
fn locate(self: *Layout, s: *const Surface, among: enum { any, open, main_window }) ?Located {
var it = self.declaredRegions();
while (it.next()) |r| {
for (self.matchingIn(r)) |m| if (m == s) {
self.selectIn(r, id);
if (r.isClosed() or r.isFolded()) r.open();
return true;
const place = find: while (it.next()) |r| {
if (self.state.floats.rootOf(r.name)) |float| switch (among) {
.any => {},
.main_window => continue,
.open => {
const i = self.state.floats.find(float) orelse continue;
if (self.state.floats.items.items[i].closing) continue;
},
};
}
return false;
for (self.matchingIn(r)) |m| if (m == s) break :find r;
} else return null;
return .{ .place = place, .tray = self.trayOf(place) };
}

/// The tray `place` is, or the one a split of it grew from (`SplitTree.Forest.rootOf`); null in
/// none.
fn trayOf(self: *Layout, place: *const Region) ?*const Region {
if (place.tray) return place;
const root = self.state.splits.rootOf(place.name) orelse return null;
const tray = ViewDrag.regionNamed(self.state, root) orelse return null;
return if (tray.tray) tray else null;
}

/// Whether `s` is what the place `at` shows now: its selection, with neither the place nor its
/// tray shut or folded.
fn inSight(self: *Layout, at: Located, s: *const Surface) bool {
if (at.place.isClosed() or at.place.isFolded()) return false;
if (at.tray) |t| if (t.isClosed() or t.isFolded()) return false;
const shown = self.selectedIn(at.place) orelse return false;
return shown == s;
}

/// Surfaces that appear in no region of the last completed shape — neither assigned to one nor
Expand Down
2 changes: 1 addition & 1 deletion app/layout/Picker.zig
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ fn removeRegion(f: *Layout, region: *const Layout.Region) void {
_ = ViewDrag.closeEmptied(f, region.name);
} else if (forget) {
if (region.id != .zero and (dvui.dataGet(null, region.id, "_size", f32) orelse 0) > 0) {
Split.close(region.id);
Split.close(region.id, region.reopen_extent);
f.extents_changed = true;
} else if (f.state.splits.collapse(gpa, region.name)) {
if (f.state.clearExtent(gpa, region.name)) f.extents_changed = true;
Expand Down
77 changes: 56 additions & 21 deletions app/layout/Region.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ id: dvui.Id = .zero,
/// width under a horizontal parent, the height under a vertical one, in points. One number,
/// because a region only ever divides its parent one way; the other axis is the parent's.
default_extent: f32 = 0,
/// What it opens back to: the extent the user last left it at, persisted by name, else
/// `default_extent`. Filled in as it registers (`State.registerRegion`), so a command between
/// frames shuts and opens it at the user's size.
reopen_extent: f32 = 0,
/// Clip set while the region is open, restored on `deinit`.
prev_clip: ?dvui.Rect.Physical = null,
/// The box this region is. A region **is** a `dvui.box`: same layout mechanics, same
Expand All @@ -64,6 +68,11 @@ kind_slot: bool = false,
pane_cards: bool = false,
/// A tray the next split should keep targeting. Center and grouping boxes are not.
resize: bool = false,
/// A tray: a place the shape declared `resize`, which shuts and opens again — the one kind of place
/// `Layout.hide` shuts. Not a place a split made (`State.isMinted`), which is `resize` too but goes
/// only by being emptied or joined. Registered with the region, so a command between frames reads
/// it.
tray: bool = false,
/// An empty closed tray can disappear — a runtime split, an endless edge.
forget_when_empty: bool = false,
/// How this place lays its children.
Expand Down Expand Up @@ -127,7 +136,7 @@ pub fn deinit(self: *Region) void {

// Opening and shutting from outside the layout — a rail button, a command, a keybind. These
// work on the copy `Editor.regionFor` hands back as well as on a live one, which is why they
// touch only `id` and `default_extent`: everything they need is a persisted extent under an id,
// touch only `id` and `reopen_extent`: everything they need is a persisted extent under an id,
// never a widget pointer.

pub fn isClosed(self: Region) bool {
Expand All @@ -141,13 +150,20 @@ pub fn close(self: Region) void {
// Deliberately before `Split.close`: shutting a region is also the user withdrawing the peek
// that kept it open on a narrow window (see `collapseTarget`).
dvui.dataSet(null, self.id, "_peek", false);
Split.close(self.id);
// Only a tray is shut when it keeps no size; anything else is left as it was.
Split.close(self.id, if (self.tray) self.openTo() else null);
}

/// `reopen_extent`, or `default_extent` for a region that has not registered (the shape's own
/// copy, mid-draw).
fn openTo(self: Region) f32 {
return if (self.reopen_extent > 0) self.reopen_extent else self.default_extent;
}

pub fn open(self: Region) void {
// Opening is an explicit answer to the auto-collapse, and outlives it.
dvui.dataSet(null, self.id, "_peek", true);
Split.open(self.id, self.default_extent);
Split.open(self.id, self.openTo());
}

/// Auto-collapse is a *default*, not a lock: a `collapsible` region folds itself away when the
Expand Down Expand Up @@ -379,6 +395,13 @@ pub fn init(self: *Layout, src: std.builtin.SourceLocation, init_opts: InitOptio
break :blk regionKeywords(self, if (parent) |p| p.prefix else "", init_opts.keywords);
};

// What a tray opens to when the user has never dragged it: the content the shape's
// `min_size_content` asks for along its parent's axis, with the padding and border round it.
const default_extent: f32 = if (init_opts.resize) switch (axis) {
.horizontal => (opts.min_size_content orelse dvui.Size{}).w,
.vertical => (opts.min_size_content orelse dvui.Size{}).h,
} + alongAxis(opts.paddingGet(), axis) + alongAxis(opts.borderGet(), axis) else 0;

const probe: Region = .{
.name = init_opts.name,
.keywords = keywords,
Expand All @@ -400,14 +423,23 @@ pub fn init(self: *Layout, src: std.builtin.SourceLocation, init_opts: InitOptio
if (init_opts.hide_when_empty and keywords.len > 0 and matches.len == 0 and !cleared) {
// A boundary with nothing on one side is not a boundary.
if (parent) |p| p.pending_split = null;
// Not drawn, but still declared: what it is sized and shut at is read, so dvui keeps it
// for as long as the place is here, and it comes back as it was rather than seeded open.
if (init_opts.resize) {
_ = dvui.dataGet(null, id, "_size", f32);
_ = dvui.dataGet(null, id, "_open", f32);
_ = dvui.dataGet(null, id, "_shown", f32);
}
// Still a place, even with nothing in it: it stays in the registry so the picker and
// the settings table can put something back. Emptied through the picker and then
// gone from the picker was a region the user could not get back.
self.state.registerRegion(self.gpa, .{
register(self, init_opts, .{
.name = init_opts.name,
.keywords = keywords,
.shows = init_opts.shows,
.id = id,
// Folded away empty, it still opens to the shape's size, not to nothing.
.default_extent = default_extent,
.by_name = init_opts.by_name,
.kind_slot = init_opts.kind_slot,
.forget_when_empty = init_opts.forget_when_empty,
Expand All @@ -421,7 +453,7 @@ pub fn init(self: *Layout, src: std.builtin.SourceLocation, init_opts: InitOptio
const tree_origin = init_opts.omit_edge != null and self.state.splits.root(init_opts.name) != null;
if (!tree_origin and init_opts.name.len > 0) {
if (self.state.splits.root(init_opts.name)) |node| {
if (node.kind == .branch) return try initTree(self, src, init_opts, opts, node);
if (node.kind == .branch) return try initTree(self, src, init_opts, opts, node, default_extent);
}
}
// The split declared before this region divides it from the one before: drawn now, with
Expand All @@ -436,16 +468,9 @@ pub fn init(self: *Layout, src: std.builtin.SourceLocation, init_opts: InitOptio
// frame (mid-animation it is between the two), `default_extent` what it opens to when the
// user has never dragged it.
var extent: f32 = 0;
var default_extent: f32 = 0;
if (init_opts.resize) {
const given = opts.min_size_content orelse dvui.Size{};
// The shape's `min_size_content` is the content it wants to open to; the extent is the
// whole region round it.
const default: f32 = switch (axis) {
.horizontal => given.w,
.vertical => given.h,
} + alongAxis(opts.paddingGet(), axis) + alongAxis(opts.borderGet(), axis);
default_extent = default;
const default = default_extent;
// Seeded from what the user last left this region at, by name — so a layout persists
// across restarts without the framework knowing which regions an app has.
if (dvui.dataGet(null, id, "_size", f32) == null) {
Expand Down Expand Up @@ -549,7 +574,7 @@ pub fn init(self: *Layout, src: std.builtin.SourceLocation, init_opts: InitOptio
// A leftover leaf that kept a minted name must not overwrite the pinned
// subtree registered under that name — the parent sash still targets it.
const leftover_leaf = init_opts.omit_edge != null and !init_opts.resize;
if (keywords.len > 0 and !tree_origin and !leftover_leaf) self.state.registerRegion(self.gpa, .{
if (keywords.len > 0 and !tree_origin and !leftover_leaf) register(self, init_opts, .{
.name = init_opts.name,
.keywords = keywords,
.shows = init_opts.shows,
Expand Down Expand Up @@ -1123,6 +1148,8 @@ fn initTree(
init_opts: InitOptions,
opts: dvui.Options,
node: *SplitTree.Node,
/// `init`'s: what this tray opens to, padding and border included.
default_extent: f32,
) anyerror!Region {
const parent: ?*Layout.Container = if (self.depth == 0) null else &self.containers[self.depth - 1];
const axis: dvui.enums.Direction = if (parent) |p| p.dir else .horizontal;
Expand All @@ -1133,17 +1160,12 @@ fn initTree(

const branch = node.kind.branch;
var box_opts = opts;
var default_extent: f32 = 0;
// Forgotten this frame, the tree `branch` points into may be freed: its children wait for
// the next frame (`persistExtent` asked for it).
var dropped = false;
if (init_opts.resize) {
const given = opts.min_size_content orelse dvui.Size{};
const default: f32 = switch (axis) {
.horizontal => given.w,
.vertical => given.h,
};
default_extent = default;
const default = default_extent;
if (dvui.dataGet(null, id, "_size", f32) == null) {
dvui.dataSet(null, id, "_size", self.state.extent(init_opts.name, default));
}
Expand Down Expand Up @@ -1193,7 +1215,7 @@ fn initTree(
const box = dvui.box(src, .{ .dir = branch.dir }, box_opts);
if (init_opts.resize) Split.recordEdges(id, box.data(), axis);

if (keywords.len > 0) self.state.registerRegion(self.gpa, .{
if (keywords.len > 0) register(self, init_opts, .{
.name = init_opts.name,
.keywords = keywords,
.shows = init_opts.shows,
Expand Down Expand Up @@ -1477,6 +1499,16 @@ fn placePrefix(keywords: []const []const u8, parent_prefix: []const u8) []const
return keywords[0];
}

/// Register `entry`, a region `init_opts` declared, as a tray when it is one (`Region.tray`): the
/// one place that decides it.
fn register(self: *Layout, init_opts: InitOptions, entry: Region) void {
var r = entry;
// A place a split made is `resize` too, but goes by being emptied or joined, never shut; one
// that forgets itself empty (`forget_when_empty`) could not be opened again once forgotten.
r.tray = init_opts.resize and !init_opts.forget_when_empty and !self.state.isMinted(init_opts.name);
self.state.registerRegion(self.gpa, r);
}

/// Draw a region into an already-open box — `init` minus geometry. Keyword qualification,
/// registry, corner button, contents. The box is the caller's (a dockspace leaf cell).
pub fn fillInBox(self: *Layout, init_opts: InitOptions, box: *dvui.BoxWidget) !void {
Expand All @@ -1490,6 +1522,9 @@ pub fn fillInBox(self: *Layout, init_opts: InitOptions, box: *dvui.BoxWidget) !v
.id = box.data().id,
.by_name = init_opts.by_name,
.kind_slot = init_opts.kind_slot,
// Never a tray, whatever `resize` says: the box and its size are the caller's (a
// dockspace cell), so there is no extent here to shut and open again.
.tray = false,
.forget_when_empty = init_opts.forget_when_empty,
.dir = init_opts.dir,
});
Expand Down
2 changes: 2 additions & 0 deletions app/layout/State.zig
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ pub fn joinable(self: *const State, a: []const u8, b: []const u8) ?SplitTree.For
pub fn registerRegion(self: *State, gpa: std.mem.Allocator, entry: Region) void {
var r = entry;
r.layer = self.layer_building;
const kept = self.extent(r.name, 0);
r.reopen_extent = if (kept > 0) kept else r.default_extent;
if (r.name.len > 0) r.shows = self.showsOf(r.name, r.shows);
self.regions_building.append(gpa, r) catch {};
}
Expand Down
Loading
Loading