Skip to content
Merged
16 changes: 12 additions & 4 deletions docs/AUTOMATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,16 @@ title card in the middle of the home view instead, for a demo's opening and clos
Captions at home stack with one another; any other caption begins alone — a callout or a title
card ends the captions still showing, so they close as it opens rather than competing with it.

A keyframe starts with the explorer put away (`.layout = .focused`), so the editor has the room;
a demo opens it from the rail when it is about to use it and puts it away after —
`catalog.openFile(s, "README.md")` does all three.
A keyframe never arranges the window: the person's places, panes and floats stay as they have
them, and their documents stay open and showing beside the demo's. A demo that needs the explorer
presses the rail for it only if it is not showing already (`s.skipIfShown(tag)` … `s.endSkip(mark)`
leave out the ops between when `tag` is drawn, `s.skipIfGone(tag)` when it is not; aim it at the
place (`region:role:explorer`), after a `waitIdle`, not at a row inside it); `catalog.openFile(s,
"README.md")` does that and opens the file, and on a phone puts away the sheet it brought up.

A demo owns only what it opened: its `demo://` documents. A file the person opens while it plays is
theirs, and the workbench does not write the panes the demo arranges to `panes.zon`
(`Workbench.panes_held`); they go back as they were when the tape plays to its end.

**One keyframe, then carry on.** A keyframe is a cut, and a cut mid-demo snaps the window back to
a fresh layout in front of the viewer. Chapters continue in the window the last one left; one that
Expand Down Expand Up @@ -343,7 +350,7 @@ one.
.root = "demo://hello",
.files = .{ .{ .path = "hello.md", .text = "# Hello\n" } },
.open = .{"hello.md"}, // the last one is active
.layout = .focused, // .keep | .reset | .focused (default; fizzy: no panel, explorer away)
.layout = .keep, // fizzy leaves the window as it is whatever this says
.settings = .{ .{ .owner = "markdown", .key = "default_md_view", .value = ".split" } },
} },
.chapters = .{ .{ .at = 0, .title = "Hello" } },
Expand Down Expand Up @@ -374,6 +381,7 @@ one.
| `key: "chord"` | Press and release a chord (`enter`, `escape`, `mod+s`, `mod+k mod+c`). |
| `type: "text"` | Type over `ms`; `\n` is Enter, `\t` is Tab. |
| `command: .{ .id, .args }` | Run a command: with no `args`, as its menu row or shortcut would (one that needs arguments asks for them); with `args`, ZON keyed by its parameters (`".{ .line = 1 }"`), as the palette does once they are given. A script writes `s.command(id)` or `s.commandWith(id, .{ .line = 1 })`. |
| `skip: .{ .when, .count }` | Leave out the next `count` ops when `.when` (a wait's `.idle`, `.shown` or `.gone`) holds as the op is reached: a gesture for a state the app is in already. A seek makes the same choice. |
| `wait: .{ .until, .timeout }` | Hold demo time until `.idle`, `.shown = "tag"` or `.gone = "tag"`; give up after `timeout` ms of wall time. |
| `step: .{ .file, .effect, .motion }` | Apply one step of a document's history to `file` of the keyframe before it, shown being made over `ms`: `effect` is what the step did and `motion` how it was made (optional), both in the file's `owner`'s encoding. Steps land together, with no frame between them, so a seek across an hour of drawing is one call. |

Expand Down
71 changes: 3 additions & 68 deletions plugins/workbench/src/Workbench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ pub const Workbench = @This();
/// The split tree the panes live in. `core.widgets.DockLayout`, one panel per leaf.
pub const Panes = core.widgets.DockLayout;

/// A held pane's tabs (`held_tabs`): its grouping, and the surface ids it held, owned.
pub const HeldTabs = struct { grouping: u64, ids: [][]u8 };

allocator: std.mem.Allocator,
decorators: std.ArrayList(BranchDecorator) = .empty,

Expand All @@ -41,17 +38,9 @@ workspaces: std.AutoArrayHashMapUnmanaged(u64, Workspace) = .empty,
/// plugin's own directory whenever it changes (`workbench_layout.savePanes`).
panes: Panes,
panes_dirty: bool = false,
/// A demo has the window (`holdPanes`): nothing is saved, and last session's panes wait.
holding: bool = false,
/// The person's arrangement while a demo has the window, a copy of its own (null where it could
/// not be made, or before there was one), and the pane they were in.
held_panes: ?Panes = null,
held_open: u64 = 0,
/// The person's panes themselves while a demo has the window, and what each held (its
/// assignment, copied): set aside whole, with their documents, which stay open and are never
/// seated in the demo's panes (`workbench_layout.holdPanes`).
held_workspaces: std.AutoArrayHashMapUnmanaged(u64, Workspace) = .empty,
held_tabs: std.ArrayList(HeldTabs) = .empty,
/// Set while a demo plays: the panes it arranges are not written to `panes.zon`, which keeps the
/// person's own until the demo puts them back or lets them go (`Demo.end`, `Demo.keep`).
panes_held: bool = false,
/// Where the panes are arranged when the app arranges them instead of `panes`: each pane a view
/// of the app's, in a place of the app's, beside another by a split of the app's (fizzy's
/// `Workspaces`, behind `FIZZY_WORKSPACES=1`). Null: the panes are this tree's leaves, as before.
Expand Down Expand Up @@ -372,35 +361,6 @@ pub fn documentClosed(self: *Workbench, doc: sdk.DocHandle) void {
if (active.id == doc.id) ws.active = null;
}
}
// One of the person's, closed while a demo has the window: gone from their held panes too.
self.forgetHeld(doc, id);
}

/// `doc`, whose tab is `id`, from the person's held panes (`holdPanes`): its tab, where `id` is
/// given, and any pane's cached handle of it.
fn forgetHeld(self: *Workbench, doc: sdk.DocHandle, id: ?[]const u8) void {
for (self.held_workspaces.values()) |*ws| {
if (ws.active) |active| {
if (active.id == doc.id) ws.active = null;
}
}
const gone = id orelse return;
for (self.held_tabs.items) |*t| {
const k = for (t.ids, 0..) |held, k| {
if (std.mem.eql(u8, held, gone)) break k;
} else continue;
self.allocator.free(t.ids[k]);
std.mem.copyForwards([]u8, t.ids[k..], t.ids[k + 1 ..]);
t.ids = self.allocator.realloc(t.ids, t.ids.len - 1) catch t.ids[0 .. t.ids.len - 1];
}
}

/// Whether `doc` is one of the person's, set aside with their panes while a demo has the window
/// (`holdPanes`).
pub fn holdsDocument(self: *const Workbench, doc: sdk.DocHandle) bool {
if (!self.holding) return false;
const id = sdk.document.surfaceId(runtime.host().arena(), doc.owner.id, doc.owner.documentPath(doc)) catch return false;
return workbench_layout.heldDocument(self, doc, id);
}

/// The tab naming surface `id` goes from whichever pane holds it: a document that will not come
Expand All @@ -417,7 +377,6 @@ pub fn documentDetached(self: *Workbench, doc: sdk.DocHandle) void {
if (active.id == doc.id) ws.active = null;
}
}
self.forgetHeld(doc, null);
}

/// A document's path changed under it (an explorer rename, a Save As): its tab is keyed by
Expand All @@ -427,13 +386,6 @@ pub fn documentDetached(self: *Workbench, doc: sdk.DocHandle) void {
/// starts from `activeDoc()` — Save first among them — quietly does nothing.
pub fn documentRenamed(self: *Workbench, doc: sdk.DocHandle, old_id: []const u8) void {
const new_id = sdk.document.surfaceId(runtime.host().arena(), doc.owner.id, doc.owner.documentPath(doc)) catch return;
// One of the person's, renamed while a demo has the window: its held tab follows.
for (self.held_tabs.items) |t| for (t.ids, 0..) |held, k| {
if (!std.mem.eql(u8, held, old_id)) continue;
const copy = self.allocator.dupe(u8, new_id) catch break;
self.allocator.free(held);
t.ids[k] = copy;
};
const ws = self.swapTabId(old_id, new_id) orelse return;
// Selection is by id too. Only the pane showing this document re-selects, and only
// within itself — this is not a focus change, so `open_workspace_grouping` stays.
Expand Down Expand Up @@ -509,23 +461,6 @@ pub fn rebuildWorkspaces(self: *Workbench) !void {
return workbench_layout.rebuildWorkspaces(self);
}

pub fn holdPanes(self: *Workbench) void {
workbench_layout.holdPanes(self);
}

pub fn onePane(self: *Workbench) void {
workbench_layout.onePane(self);
}

pub fn restorePanes(self: *Workbench) void {
workbench_layout.restorePanes(self);
}

/// The person's panes let go of, their documents joining the open pane (`workbench_layout.keepPanes`).
pub fn keepPanes(self: *Workbench) void {
workbench_layout.keepPanes(self);
}

/// The panes as they stand, for a demo's snapshot (`workbench_layout.arrangement`).
pub const Arrangement = struct { open: u64, groupings: []const u64, tree: [:0]const u8 };

Expand Down
4 changes: 4 additions & 0 deletions plugins/workbench/src/Workspace.zig
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ pub fn draw(self: *Workspace) !dvui.App.Result {
self.active = if (selected) |s| documentOf(s) else null;
const pane_r = pane_box.rectScale().r;
self.pane_rect_physical = pane_r;
// The person's pane, when it has the room for an open beside to split it: a tape that drops a
// tab on the edge asks for this first, or skips the drop (`core.placement`).
if (core.anchor.wanted() and runtime.workbench().open_workspace_grouping == self.grouping and
runtime.workbench().besideEdge() != null) core.anchor.mark(pane_box, "workbench.beside-room", .{});

if (tabs.len > 0) {
self.dropSnapshot();
Expand Down
160 changes: 2 additions & 158 deletions plugins/workbench/src/workbench_layout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ pub fn rebuildWorkspaces(wb: *Workbench) !void {
const host = runtime.host();
const arena = host.arena();

// Not while a demo has the window: last session's panes and tabs come back once it has gone
// (`restorePanes`), not into the middle of it.
if (!wb.restored and !wb.holding) {
if (!wb.restored) {
wb.restored = true;
// Where the app arranges the panes, it brought them back itself — and carries an
// arrangement saved before it did over into its own (fizzy's `Workspaces.carryOver`).
Expand Down Expand Up @@ -81,9 +79,6 @@ pub fn rebuildWorkspaces(wb: *Workbench) !void {
}
}
if (held) continue;
// One of the person's, set aside with its pane while a demo has the window: it waits
// there for them, never seated in the demo's panes.
if (wb.holding and heldDocument(wb, doc, id)) continue;
// The same file under another owner's id (a plugin installed since last session) would
// otherwise sit beside it as a tab that never opens.
const path = doc.owner.documentPath(doc);
Expand Down Expand Up @@ -164,7 +159,7 @@ pub fn drawWorkspaces(wb: *Workbench, index: usize) !dvui.App.Result {
}
if (dock.changed) wb.panes_dirty = true;
dock.deinit();
if (wb.panes_dirty) savePanes(wb);
if (wb.panes_dirty and !wb.panes_held) savePanes(wb);
if (result != .ok) return result;

return .ok;
Expand All @@ -174,8 +169,6 @@ pub fn drawWorkspaces(wb: *Workbench, index: usize) !dvui.App.Result {
/// no plugin directory to offer.
fn savePanes(wb: *Workbench) void {
wb.panes_dirty = false;
// A demo has the window: what it arranges is not the person's (`holdPanes`).
if (wb.holding) return;
if (comptime builtin.target.cpu.arch == .wasm32) return;
const path = panesPath(wb) orelse return;
defer wb.allocator.free(path);
Expand Down Expand Up @@ -255,155 +248,6 @@ fn adoptPanes(wb: *Workbench, loaded: Panes) void {
}
}

/// Set the person's panes aside while a demo has the window, and save nothing until they are put
/// back (`restorePanes`): the demo plays in a pane of its own, and its splits are not theirs. A
/// second hold keeps the first.
///
/// Their panes go aside whole — the workspaces, what each holds, and the tree they sit in — and
/// their documents stay open behind them, untouched: a document of a held pane is never seated in
/// the demo's (`rebuildWorkspaces`). Nothing is saved while holding, so the arrangement on disk
/// stays theirs: a tree that could not be copied is read back from there. Before the first rebuild
/// has restored last session's panes there is no tree of theirs to copy; that restore waits for
/// the demo to end.
pub fn holdPanes(wb: *Workbench) void {
if (wb.holding) return;
wb.holding = true;
wb.held_open = wb.open_workspace_grouping;
if (wb.restored) {
// A copy through a snapshot: a snapshot borrows the pane names from the live tree, which
// the demo's own pane replaces below.
if (wb.panes.snapshot(wb.allocator)) |snap| {
defer snap.deinit(wb.allocator);
wb.held_panes = Panes.fromSnapshot(wb.allocator, snap) catch |err| blk: {
dvui.log.err("workbench: could not set the panes aside ({t}); they come back from disk", .{err});
break :blk null;
};
} else |err| dvui.log.err("workbench: could not set the panes aside ({t}); they come back from disk", .{err});
}
const host = runtime.host();
for (wb.workspaces.values()) |*ws| {
var buf: [32]u8 = undefined;
const ids = host.assignedSurfaces(Workspace.name(&buf, ws.grouping)) orelse continue;
const copy = dupeIds(wb.allocator, ids) catch |err| {
dvui.log.err("workbench: could not set pane {d}'s tabs aside ({t}); they come back from disk", .{ ws.grouping, err });
continue;
};
wb.held_tabs.append(wb.allocator, .{ .grouping = ws.grouping, .ids = copy }) catch |err| {
dvui.log.err("workbench: could not set pane {d}'s tabs aside ({t}); they come back from disk", .{ ws.grouping, err });
freeIds(wb.allocator, copy);
};
}
wb.held_workspaces = wb.workspaces;
wb.workspaces = .empty;
// The demo's pane: a grouping no pane of theirs has.
const stage = wb.newGroupingID();
wb.workspaces.put(wb.allocator, stage, Workspace.init(stage)) catch {};
wb.open_workspace_grouping = stage;
onePane(wb);
}

/// Whether `doc` (its tab `id`) is one of the person's, set aside with their panes.
pub fn heldDocument(wb: *const Workbench, doc: sdk.DocHandle, id: []const u8) bool {
if (wb.held_workspaces.contains(doc.owner.documentGrouping(doc))) return true;
for (wb.held_tabs.items) |t| for (t.ids) |held| if (std.mem.eql(u8, held, id)) return true;
return false;
}

fn dupeIds(gpa: std.mem.Allocator, ids: []const []const u8) ![][]u8 {
const out = try gpa.alloc([]u8, ids.len);
var n: usize = 0;
errdefer freeIds(gpa, out[0..n]);
for (ids, out) |id, *o| {
o.* = try gpa.dupe(u8, id);
n += 1;
}
return out;
}

fn freeIds(gpa: std.mem.Allocator, ids: [][]u8) void {
for (ids) |id| gpa.free(id);
gpa.free(ids);
}

/// One pane, as a demo's keyframe declares the window: the open one, the others closing with
/// their leaves (`rebuildWorkspaces`). Its documents are the keyframe's to open.
pub fn onePane(wb: *Workbench) void {
var one = Panes.init(wb.allocator);
one.owns_panel_ids = true;
one.animated = true;
const root = one.allocNodeForRoot() catch return;
var buf: [32]u8 = undefined;
one.insertTabOwned(root, 0, Workspace.name(&buf, wb.open_workspace_grouping)) catch {
one.deinit();
return;
};
adoptPanes(wb, one);
}

/// The person's panes again (`holdPanes`), and saving with them: the demo's go, theirs come back
/// holding what they held, in their tree — the copy, or what is on disk where there is none.
/// Before last session's panes were ever restored, one pane, for the next rebuild to restore
/// them into.
pub fn restorePanes(wb: *Workbench) void {
if (!wb.holding) return;
wb.holding = false;
const host = runtime.host();
for (wb.workspaces.values()) |*ws| {
var buf: [32]u8 = undefined;
host.assignSurfaces(Workspace.name(&buf, ws.grouping), null) catch {};
ws.deinit();
}
wb.workspaces.deinit(wb.allocator);
wb.workspaces = wb.held_workspaces;
wb.held_workspaces = .empty;
for (wb.held_tabs.items) |t| {
var buf: [32]u8 = undefined;
const ids: []const []const u8 = t.ids;
host.assignSurfaces(Workspace.name(&buf, t.grouping), ids) catch |err|
dvui.log.err("workbench: pane {d}'s tabs did not come back ({t})", .{ t.grouping, err });
freeIds(wb.allocator, t.ids);
}
wb.held_tabs.clearAndFree(wb.allocator);
if (wb.held_panes) |held| {
wb.held_panes = null;
adoptPanes(wb, held);
} else if (wb.restored) {
loadPanes(wb);
} else {
wb.open_workspace_grouping = wb.held_open;
onePane(wb);
}
if (wb.workspaces.contains(wb.held_open)) wb.open_workspace_grouping = wb.held_open;
}

/// The person's panes, let go of rather than put back: a demo is leaving the window as it is
/// (`Demo.keep`), so what it made stays and the person's open documents join its open pane as
/// tabs, in the order they held them. Their panes themselves, and the arrangement set aside with
/// them, are dropped; saving resumes with what is on screen.
pub fn keepPanes(wb: *Workbench) void {
if (!wb.holding) return;
wb.holding = false;
const host = runtime.host();
var buf: [32]u8 = undefined;
const into = wb.workspaces.getPtr(wb.open_workspace_grouping) orelse if (wb.workspaces.count() > 0) &wb.workspaces.values()[0] else null;
for (wb.held_tabs.items) |t| {
host.assignSurfaces(Workspace.name(&buf, t.grouping), null) catch {};
if (into) |ws| for (t.ids) |id| {
const path = sdk.document.pathOfSurfaceId(id) orelse continue;
if (host.docFromPath(path) != null) ws.addTab(id, false);
};
freeIds(wb.allocator, t.ids);
}
wb.held_tabs.clearAndFree(wb.allocator);
for (wb.held_workspaces.values()) |*ws| ws.deinit();
wb.held_workspaces.deinit(wb.allocator);
wb.held_workspaces = .empty;
if (wb.held_panes) |*held| {
held.deinit();
wb.held_panes = null;
}
}

/// The panes as they stand, for a demo's snapshot to put back (`arrange`): which there are, in
/// row order, the one in front, and the tree they sit in as ZON (`Panes.Snapshot`). What each
/// holds is the app's assignment under its name, which the app's layout keeps. All in `arena`.
Expand Down
Loading
Loading