Skip to content
Merged
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
22 changes: 22 additions & 0 deletions docs/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ inventing a kind of panel never touches the ABI. Fizzy's shipped shapes accept t
| `sdk.keywords.ide.sidebar` | `sidebar`, `explorer` | things you pick *from* — file trees, outlines |
| `sdk.keywords.ide.panel` | `bottom`, `panel`, `output` | things a task *produces* — logs, diagnostics |
| `sdk.keywords.ide.main` | `main`, `center`, `workspace` | the thing being worked on |
| `sdk.keywords.ide.home` | `home` | what the main area shows while nothing is open (below) |
| `sdk.document.keywords` | `document` | one open document (the app registers these, see below) |

A keyword may name a place *inside* another, dot-separated (`main.document`). `keywords.Fit`
Expand Down Expand Up @@ -632,6 +633,27 @@ if (!host.hideSurface(id)) _ = host.revealSurface(id); // toggle it
drawn, since the places it changes are being drawn. `revealSurface` acts at once, so a hide
asked first in a frame still lands after a reveal asked later in it: make one call a frame.

**A home page.** What the main area shows while nothing is open (the workbench's logo, New File,
Open Folder) is a place too. While its one document pane is empty, the workbench declares a
region named `Home` inside it with keywords `sdk.keywords.ide.home` (`{"home"}`) and
`shows = .one`, and draws the surface that region accepts. With no such surface the region
declines, and the workbench draws its own page. A start page is just a surface that says what it
is:

```zig
try host.registerSurface(.{ .id = "graphl.start", .title = "Start",
.keywords = sdk.keywords.ide.home, .draw = drawStart });
```

It needs no startup code. The place exists only while nothing is open, so a document opening
puts the home page away, and so does a restored session landing a few frames after startup. The
last document closing brings it back. None of the shape's own regions accept a `home` surface,
so it never shows up as a tab in Main. When two plugins offer a home page, the first one
registered shows until someone chooses. An app chooses with
`host.assignSurfaces("Home", &.{"graphl.start"})`, the same list the picker writes, and a user
chooses in Settings › Layout. A shape without the workbench has no `Home` place unless it
declares a region that takes `home` itself.

**Dragging a view onto a region is the app's gesture, everywhere.** While a view is dragged,
the region under the pointer shows its drop (`core.widgets.DropZones`): a cluster of glass
bubbles in its middle — the middle for into the place, one on each side for a split there, and a
Expand Down
25 changes: 24 additions & 1 deletion plugins/workbench/src/Workspace.zig
Original file line number Diff line number Diff line change
Expand Up @@ -845,10 +845,33 @@ fn drawCanvas(self: *Workspace, region: sdk.Host.Region, has_tabs: bool) !void {
dvui.alphaSet(1.0);
defer dvui.alphaSet(alpha);

try self.drawHomePage();
if (!try drawHome(runtime.host(), self.grouping)) try self.drawHomePage();
}
}

/// The name of the place an app's home page goes, for `host.assignSurfaces` to choose between two.
pub const home_region = "Home";

/// An app's own home page, in place of ours: the `Home` place, which shows a surface keyworded
/// `home` (`sdk.keywords.ide.home`). Declared only from the sole pane while it is empty, so it
/// exists exactly while nothing is open — a document opening, or a restored session's landing
/// a few frames after startup, puts the home away, and the last one closing brings it back.
/// False when no surface is a home page (the place declines), for ours to be drawn instead.
///
/// Takes its host so a test can declare it without the plugin's runtime (`plugins/AGENTS.md`).
pub fn drawHome(host: *sdk.Host, grouping: u64) !bool {
var home = host.region(.{
.name = home_region,
.keywords = sdk.keywords.ide.home,
.shows = .one,
.key = grouping,
.hide_when_empty = true,
}) orelse return false;
defer home.deinit();
_ = try home.drawContents();
return true;
}

fn dropSnapshot(self: *Workspace) void {
if (self.closing_snapshot) |t| dvui.textureDestroyLater(t);
self.closing_snapshot = null;
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/keywords.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ pub const ide = struct {
pub const panel: []const []const u8 = &.{ "bottom", "panel", "output" };
/// The main area: documents, canvases — the thing being worked on.
pub const main: []const []const u8 = &.{ "main", "center", "workspace" };
/// The home page: what the main area shows while nothing is open, in place of the
/// workbench's own (its logo, New File, Open Folder). The workbench declares that place,
/// named `Home`, only while its one document pane is empty, so a document opening puts the
/// home page away (`docs/PLUGINS.md` §3.1.2).
pub const home: []const []const u8 = &.{"home"};
};

/// The studio shape (`src/editor/layout/studio.zig`): no file explorer, a large canvas, a
Expand Down
5 changes: 3 additions & 2 deletions src/editor/Editor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2426,9 +2426,10 @@ fn drawSettingsPane(_: ?*anyopaque) anyerror!dvui.App.Result {
}

// ---- EditorAPI: fizzy-provided read/utility surface for plugins ----------
// Installed on the Host in `postInit`; `ctx` is this `*Editor`.
// Installed on the Host in `postInit`; `ctx` is this `*Editor`. Public for a test that drives a
// plugin through `Host` (`tests/integration.zig`, the workbench's home page).

const fizzy_api_vtable: sdk.EditorAPI.VTable = .{
pub const fizzy_api_vtable: sdk.EditorAPI.VTable = .{
.arena = fizzyArena,
.extensionOwnerOverride = fizzyExtensionOwnerOverride,
.folder = fizzyFolder,
Expand Down
135 changes: 135 additions & 0 deletions tests/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6696,6 +6696,141 @@ test "workbench: where a tab carried over a strip goes in is read as if its open
try std.testing.expectEqual(@as(usize, 3), pane.insertIndexAt(500, 3));
}

// An app's home page in place of the workbench's: a surface keyworded `home`, shown in the `Home`
// place the sole document pane declares while it is empty (`Workspace.drawHome`). The pane here is
// declared as `Workspace.draw` declares it, through the plugin's `Host`, so the place is the one
// the workbench makes.
const HomeFrame = struct {
var editor: ?*fizzy.Editor = null;
/// Which home surface drew this frame (its `ctx`), and how many times one did.
var home: ?usize = null;
var home_draws: usize = 0;
var doc_draws: usize = 0;
/// The workbench would have drawn its own page this frame.
var builtin_home = false;

const pane_key: u64 = 1;
const doc = "test.doc:/notes.txt";

fn drawHome(ctx: ?*anyopaque) anyerror!dvui.App.Result {
home = @intFromPtr(ctx);
home_draws += 1;
return .ok;
}

fn drawDoc(_: ?*anyopaque) anyerror!dvui.App.Result {
doc_draws += 1;
return .ok;
}

fn registerHome(e: *fizzy.Editor, id: []const u8, n: usize) !void {
try e.app.host.registerSurface(.{
.id = id,
.title = "Start",
.keywords = fizzy.sdk.keywords.ide.home,
.ctx = @ptrFromInt(n),
.draw = drawHome,
});
}

fn frame() anyerror!dvui.App.Result {
const e = editor.?;
home = null;
home_draws = 0;
doc_draws = 0;
builtin_home = false;
var layout = fizzy.Editor.Layout.init(&e.app.host, &e.app.layout, e.app.gpa, dvui.currentWindow().arena());
e.app.frame_layout = &layout;
defer e.app.frame_layout = null;
{
var main = try layout.region(@src(), .{
.name = "Main",
.keywords = fizzy.sdk.keywords.ide.main,
}, .{ .expand = .both });
defer main.deinit();

var pane = e.app.host.region(.{
.name = "Pane 1",
.keywords = fizzy.sdk.document.keywords,
.shows = .many,
.key = pane_key,
}) orelse return error.TestUnexpectedResult;
defer pane.deinit();
if (pane.matching().len > 0) {
_ = try pane.drawContents();
} else {
builtin_home = !try workbench.Workspace.drawHome(&e.app.host, pane_key);
}
}
e.app.layout.publishRegions();
return .ok;
}
};

test "workbench: a home surface shows while nothing is open, and a document puts it away" {
var ctx = try shim.init(std.testing.allocator);
defer ctx.deinit(std.testing.allocator);

const editor = ctx.editor;
const gpa = std.testing.allocator;
editor.app.gpa = gpa;
defer editor.app.layout.regions.deinit(gpa);
defer editor.app.layout.deinitSwaps(gpa);
defer editor.app.layout.regions_building.deinit(gpa);
defer editor.app.layout.deinitQualified(gpa);
editor.app.host.installFizzyApi(.{ .ctx = editor, .vtable = &fizzy.Editor.fizzy_api_vtable });

HomeFrame.editor = editor;
defer HomeFrame.editor = null;

// No plugin offers a home page: the place declines, and the workbench draws its own.
try dvui.testing.settle(HomeFrame.frame);
try std.testing.expect(HomeFrame.builtin_home);
try std.testing.expectEqual(@as(usize, 0), HomeFrame.home_draws);

// One does: it is the home page, drawn once, in the Home place and not in Main beside it.
try HomeFrame.registerHome(editor, "test.home", 1);
try dvui.testing.settle(HomeFrame.frame);
try std.testing.expect(!HomeFrame.builtin_home);
try std.testing.expectEqual(@as(usize, 1), HomeFrame.home_draws);
try std.testing.expectEqual(@as(?usize, 1), HomeFrame.home);

// A restored session: the pane already lists a document that has not landed. Nothing is open
// yet, so the home stays...
try editor.app.host.assignSurfaces("Pane 1", &.{HomeFrame.doc});
try dvui.testing.settle(HomeFrame.frame);
try std.testing.expectEqual(@as(usize, 1), HomeFrame.home_draws);

// ...until it lands, a few frames later, and the home is put away: drawn nowhere, Main
// included, with nothing on the plugin's side.
try editor.app.host.registerSurface(.{
.id = HomeFrame.doc,
.title = "notes.txt",
.keywords = fizzy.sdk.document.keywords,
.draw = HomeFrame.drawDoc,
});
try dvui.testing.settle(HomeFrame.frame);
try std.testing.expectEqual(@as(usize, 1), HomeFrame.doc_draws);
try std.testing.expectEqual(@as(usize, 0), HomeFrame.home_draws);
try std.testing.expect(!HomeFrame.builtin_home);

// The last document closing brings it back.
editor.app.host.unregisterSurface(HomeFrame.doc);
try dvui.testing.settle(HomeFrame.frame);
try std.testing.expectEqual(@as(usize, 1), HomeFrame.home_draws);
try std.testing.expectEqual(@as(?usize, 1), HomeFrame.home);

// Two plugins offer one: the first registered shows, until the app chooses the other by
// assigning the place, as the picker would.
try HomeFrame.registerHome(editor, "test.home2", 2);
try dvui.testing.settle(HomeFrame.frame);
try std.testing.expectEqual(@as(?usize, 1), HomeFrame.home);
try editor.app.host.assignSurfaces(workbench.Workspace.home_region, &.{"test.home2"});
try dvui.testing.settle(HomeFrame.frame);
try std.testing.expectEqual(@as(usize, 1), HomeFrame.home_draws);
try std.testing.expectEqual(@as(?usize, 2), HomeFrame.home);
}

// -- rows carried out of a tree and back --------------------------------------------------------

// A tree of two folders and two files, as the explorer draws one: `F` open with `c` inside it, `G`
Expand Down
Loading