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
69 changes: 69 additions & 0 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 a plugin asked to put away during this frame (`hideLater`), put away once it has drawn
/// (`applyHides`). Arena-backed, per frame.
hides: std.ArrayListUnmanaged([]const u8) = .empty,
/// Surfaces drawn this frame in a place with no room, so not captured (`drawCaptured`): no frame
/// is asked for them until they have some. Arena-backed, per frame.
unsized: std.ArrayListUnmanaged(*Surface) = .empty,
Expand Down Expand Up @@ -600,6 +603,72 @@ pub fn reveal(self: *Layout, id: []const u8) bool {
return false;
}

/// What putting a view away does (`hide`).
pub const Hide = union(enum) {
/// Shut this tray, which shows the view.
tray: Region,
/// Close this float, the view's only place.
float: []const u8,
};

/// What `hide` would do for `id`, from the one region that shows it (as `reveal` finds it). A
/// tray: a place the shape declared `resize` (not one a split made, which is `forget_when_empty`),
/// in the main window, open and not folded, with `id` the view it shows. A float: one not closing,
/// with `id` its only view. Null for anything else — Main, a split's half, a plugin's pane, a view
/// behind another or out of sight, a float with other views — and `hide` then changes nothing.
pub fn hideTarget(self: *Layout, id: []const u8) ?Hide {
const s = self.surface(id) orelse return null;
if (s.hidden) return null;
var it = self.declaredRegions();
const r = find: while (it.next()) |place| {
for (self.matchingIn(place)) |m| if (m == s) break :find place;
} else return null;
if (r.layer > 0) {
const name = self.state.floatRoot(r.name) orelse return null;
const i = self.state.floats.find(name) orelse return null;
if (self.state.floats.items.items[i].closing) return null;
var views: usize = 0;
for (self.state.splits.leavesUnder(self.arena, name)) |leaf| {
if (self.state.assignment(leaf)) |ids| views += ids.len;
}
return if (views == 1) .{ .float = name } else null;
}
if (!r.resize or r.forget_when_empty or r.isClosed() or r.isFolded()) return null;
const shown = self.selectedIn(r) orelse return null;
return if (shown == s) .{ .tray = r.* } else null;
}

/// Put `id` away now, as the user would (`Host.hideSurface`): shut its tray as the tray's own
/// close does, or close its float as the float's close button does, the view going home. False,
/// changing nothing, where `hideTarget` finds nothing to do. Not while the shape or the floats
/// draw, which walk what this changes: `hideLater` is for then.
pub fn hide(self: *Layout, id: []const u8) bool {
switch (self.hideTarget(id) orelse return false) {
.tray => |r| r.close(),
.float => |name| self.closeFloat(name),
}
return true;
}

/// `hide` asked during the frame: answered now, by the same `hideTarget`, and done by
/// `applyHides` once the shape, the floats and the picker have drawn. A `reveal` asked later in
/// the same frame acts at once, so it comes first: a caller toggling a view makes one call a frame.
pub fn hideLater(self: *Layout, id: []const u8) bool {
if (self.hideTarget(id) == null) return false;
const kept = self.arena.dupe(u8, id) catch return false;
self.hides.append(self.arena, kept) catch return false;
return true;
}

/// Do the hides asked during this frame (`hideLater`), in order, each asking `hideTarget` again.
/// The application calls this once its frame has drawn. True when any of them acted.
pub fn applyHides(self: *Layout) bool {
var any = false;
for (self.hides.items) |id| any = self.hide(id) or any;
self.hides.clearRetainingCapacity();
return any;
}

/// Surfaces that appear in no region of the last completed shape — neither assigned to one nor
/// attracted by keywords to an unassigned one. Never silently lost: the settings UI lists these
/// so a user (or the plugin author) can see the gap and fix it.
Expand Down
1 change: 1 addition & 0 deletions app/layout/Region.zig
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ pub fn init(self: *Layout, src: std.builtin.SourceLocation, init_opts: InitOptio
.default_extent = default_extent,
.by_name = init_opts.by_name,
.kind_slot = init_opts.kind_slot,
.resize = init_opts.resize,
.forget_when_empty = init_opts.forget_when_empty,
.dir = init_opts.dir,
.on_drop = init_opts.on_drop,
Expand Down
25 changes: 25 additions & 0 deletions docs/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,31 @@ Outside a region's draw, `host.assignSurfaces(name, ids)` / `assignedSurfaces(na
the picker uses — a tab dragged to another pane is an assignment edit, and session restore is
the assignment list read back.

**Showing and putting away a view.** A plugin names its view, never a place, so a "Show X"
command works wherever the user moved it, in any app's shape:

```zig
_ = host.revealSurface(id); // show it
if (!host.hideSurface(id)) _ = host.revealSurface(id); // toggle it
```

- `revealSurface(id)` selects `id` in the region that shows it (where the user moved it, floats
included) and opens that region if it was shut or folded away. False when no region shows it.
- `hideSurface(id)` puts it away as the user would, in two cases only:
- **It is what a tray shows.** A tray is a place the shape declared resizable and the user did
not make by splitting (fizzy's sidebar and bottom panel). If it is open and showing `id`, it
shuts, with whatever else it holds, as its own close does. `revealSurface` opens it again at
the size the user left it.
- **It is alone in a float.** The float closes as its close button does, and the view goes back
to the place it floated out of. There it may well be in sight again.

Anything else returns false and changes nothing: a view in Main, in a place a split made (of
a tray too), in a plugin's pane, behind another view, in a float with others, or already out
of sight (its tray shut or folded).
- **Asked from a draw.** Mid-frame, `hideSurface` answers at once and acts when the frame has
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.

**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
7 changes: 7 additions & 0 deletions sdk/src/EditorAPI.zig
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@ pub const VTable = struct {
/// Bring the surface `id` into sight wherever it lives: selected in the region showing it,
/// that region opened if it was shut. False when no region shows it.
revealSurface: *const fn (ctx: *anyopaque, id: []const u8) bool,
/// Put the surface `id` away: shut the open tray showing it, or close the float it is alone
/// in. False, changing nothing, otherwise. During a frame it acts once the frame has drawn.
hideSurface: *const fn (ctx: *anyopaque, id: []const u8) bool,
/// Draw the app's glyph for a declared file kind ("image", "source", …), or return false if
/// this app has no glyph for it.
///
Expand Down Expand Up @@ -655,6 +658,10 @@ pub fn revealSurface(self: EditorAPI, id: []const u8) bool {
return self.vtable.revealSurface(self.ctx, id);
}

pub fn hideSurface(self: EditorAPI, id: []const u8) bool {
return self.vtable.hideSurface(self.ctx, id);
}

pub fn drawFileKindGlyph(self: EditorAPI, kind: []const u8, color: dvui.Color) bool {
return self.vtable.drawFileKindGlyph(self.ctx, kind, color);
}
Expand Down
17 changes: 17 additions & 0 deletions sdk/src/Host.zig
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,23 @@ pub fn revealSurface(self: *Host, id: []const u8) bool {
return a.revealSurface(id);
}

/// Put the surface `id` away, as the user would: `revealSurface`'s other half, so a button can
/// toggle its view (`if (!host.hideSurface(id)) _ = host.revealSurface(id);`). Only two cases
/// act, and anything else returns false and changes nothing:
///
/// - the view is what a tray shows (a place the shape declared resizable: fizzy's sidebar and
/// bottom panel), and the tray is open: the tray shuts, with whatever else it holds, as its own
/// close does. `revealSurface` opens it again at the size the user left it;
/// - the view is alone in a float: the float closes as its close button does, and the view goes
/// back to the place it floated out of.
///
/// Asked during a frame (from a surface's draw), it answers at once and acts once the frame has
/// drawn. Full contract: `docs/PLUGINS.md` §3.1.2, "Showing and putting away a view".
pub fn hideSurface(self: *Host, id: []const u8) bool {
const a = self.fizzy_api orelse return false;
return a.hideSurface(id);
}

pub fn revealPosition(self: *Host, path: []const u8, line: u32, character: u32, open_side: bool) !bool {
return if (self.fizzy_api) |a| a.revealPosition(path, line, character, open_side) else false;
}
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/version.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub const sdk_version = @import("sdk_version").sdk_version;
/// why it is a single target/mode-invariant literal rather than a per-target table. Update this
/// value from the `@compileError` it triggers whenever it changes; leave `sdk_version` to the
/// next SDK release.
pub const recorded_sdk_shape_fingerprint: u64 = 0x23ccb903d7a4fb41;
pub const recorded_sdk_shape_fingerprint: u64 = 0x1dd3a67d781503a7;

comptime {
if (dylib.sdk_shape_fingerprint != recorded_sdk_shape_fingerprint) {
Expand Down
28 changes: 25 additions & 3 deletions src/editor/Editor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2479,6 +2479,7 @@ const fizzy_api_vtable: sdk.EditorAPI.VTable = .{
.assignedRegionNames = fizzyAssignedRegionNames,
.selectInRegion = fizzySelectInRegion,
.revealSurface = fizzyRevealSurface,
.hideSurface = fizzyHideSurface,
.drawFileKindGlyph = fizzyDrawFileKindGlyph,
.closeDocById = fizzyCloseDocById,
.setProjectFolder = fizzySetProjectFolder,
Expand Down Expand Up @@ -4641,15 +4642,18 @@ pub fn tick(editor: *Editor) !dvui.App.Result {
// `regionFor` answered from the previous frame, which is what lets a command
// dispatched between frames drive a region (see `Layout.State.regions`).
editor.app.layout.publishRegions();
// A drag or a collapse moved a region: fizzy's answer to "remember that" is a
// debounced write to `layout.zon`.
if (layout.extents_changed) editor.markWindowRatiosDirty();
// Over every place: a drag's join spans two of them.
layout.drawDragOverlay();
// The picker photographs surfaces that drew nowhere this frame, and floats above
// everything the shape drew.
layout.captureUnplaced();
editor.app.layout.picker.draw(&layout);
// Everything that walks the places has drawn: what plugins asked to put away during
// the frame (`Host.hideSurface`) goes now.
if (layout.applyHides()) editor.afterHide();
// A drag, a collapse, the picker or a hide moved a region: fizzy's answer to
// "remember that" is a debounced write to `layout.zon`.
if (layout.extents_changed) editor.markWindowRatiosDirty();
editor.app.frame_layout = null;

// A region is a box, so a shape that declares one and never scopes it leaves the box
Expand Down Expand Up @@ -5212,6 +5216,24 @@ fn fizzyRevealSurface(ctx: *anyopaque, id: []const u8) bool {
return layout.reveal(id);
}

fn fizzyHideSurface(ctx: *anyopaque, id: []const u8) bool {
const editor = fizzyCtx(ctx);
// Mid-frame the shape and the floats are walking what a hide changes: done at the frame's end.
if (editor.app.frame_layout) |layout| return layout.hideLater(id);
var layout: Layout = .init(&editor.app.host, &editor.app.layout, editor.app.gpa, editor.app.arena.allocator());
if (!layout.hide(id)) return false;
editor.afterHide();
if (layout.extents_changed) editor.markWindowRatiosDirty();
return true;
}

/// A hide acted (`Layout.hide`). One that shut the sidebar goes through the explorer's own close,
/// as the rail's does, so the `closed` that Toggle Explorer and its menu title read agrees.
fn afterHide(editor: *Editor) void {
const side = editor.regionFor(sdk.keywords.ide.sidebar) orelse return;
if (side.isClosed() and !editor.explorer.closed) editor.explorer.close(editor);
}

fn fizzyAssignedRegionNames(ctx: *anyopaque) []const []const u8 {
const editor = fizzyCtx(ctx);
const arena = editor.app.arena.allocator();
Expand Down
Loading
Loading