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
4 changes: 3 additions & 1 deletion app/layout/SplitTree.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
//! picker fills them. Closing an empty leaf collapses the branch.
const std = @import("std");
const dvui = @import("dvui");
const core = @import("core");

pub const Side = enum { left, right, top, bottom };
/// The docking widget's side: a pane tree and a place tree name an edge the same way.
pub const Side = core.widgets.DockLayout.Side;

pub fn axisOf(side: Side) dvui.enums.Direction {
return switch (side) {
Expand Down
17 changes: 13 additions & 4 deletions app/layout/State.zig
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ pub const StoreOffer = struct {
title: []const u8,
};

/// An app's answer to where an open beside splits a place (`beside`).
pub const Beside = struct {
ctx: *anyopaque,
/// The edge of place `name` an open beside splits, or null where it joins a place, becomes a
/// card, or `name` holds no documents.
edge: *const fn (ctx: *anyopaque, name: []const u8) ?SplitTree.Side,
};

/// How an application gives a document a home outside the slots its plugins made: a view of its
/// own that holds documents (fizzy's workspaces). With one, a document carried in the view drag
/// lands in any place as a view would, inside a new such view (`ViewDrag.place`).
Expand Down Expand Up @@ -169,10 +177,11 @@ tree_cleared: bool = false,
slide_open: []const u8 = "",
/// A place's view being dragged to another place. Empty `name` when idle.
view_drag: ViewDrag = .{},
/// The edge an open beside splits the documents' place on right now, set each frame by the app
/// while a demo plays: that edge's drop zone is also published as the anchor `drop:beside`, so a
/// tape aims at the place's answer, not at an edge it chose when it was written.
beside: ?SplitTree.Side = null,
/// Set each frame by the app while a demo plays: asked for each place a drag may land in, it
/// names the edge an open beside splits that place on. That edge's drop zone is also published as
/// the anchor `drop:beside`, so a tape aims at the target place's own answer, not at an edge it
/// chose when it was written.
beside: ?Beside = null,
/// Floats are OS windows of their own (the application says so each frame, where it has them): a
/// view let go over no window of the app's opens one there (`ViewDrag.apply`).
floats_windowed: bool = false,
Expand Down
25 changes: 14 additions & 11 deletions app/layout/ViewDrag.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1323,18 +1323,21 @@ pub fn drawZones(l: *Layout, name: []const u8, key: dvui.Id) void {
// is the target, so a demo aims at the bubble rather than an offset from the place's middle.
// Marked only while a tape asks for anchors, and ahead of the offer queue's limit (which has
// nothing to do with the bubble being there).
if (target and !covered and core.anchor.wanted()) for (DropZones.all) |z| {
if (z == .remove and !zones.remove) continue;
const b = zones.bubble(z);
const r: dvui.Rect.Physical = .{ .x = b.c.x - b.r, .y = b.c.y - b.r, .w = 2 * b.r, .h = 2 * b.r };
switch (z) {
.edge => |side| {
core.anchor.markRect(key, r, true, "drop:{t}", .{side});
if (l.state.beside) |aimed_side| if (std.mem.eql(u8, @tagName(aimed_side), @tagName(side))) core.anchor.markRect(key, r, true, "drop:beside", .{});
},
else => core.anchor.markRect(key, r, true, "drop:{t}", .{z}),
if (target and !covered and core.anchor.wanted()) {
const beside = if (l.state.beside) |aim_at| aim_at.edge(aim_at.ctx, name) else null;
for (DropZones.all) |z| {
if (z == .remove and !zones.remove) continue;
const b = zones.bubble(z);
const r: dvui.Rect.Physical = .{ .x = b.c.x - b.r, .y = b.c.y - b.r, .w = 2 * b.r, .h = 2 * b.r };
switch (z) {
.edge => |side| {
core.anchor.markRect(key, r, true, "drop:{t}", .{side});
if (beside == side) core.anchor.markRect(key, r, true, "drop:beside", .{});
},
else => core.anchor.markRect(key, r, true, "drop:{t}", .{z}),
}
}
};
}
// Queued, not drawn: over everything, once every place has drawn (`drawOverlay`).
const now = dvui.currentWindow().frame_time_ns;
if (d.pending_frame != now) {
Expand Down
2 changes: 1 addition & 1 deletion core/widgets/DropZones.zig
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const motion = @import("glass").motion;
const liquid_glass = @import("glass").liquid_glass;
const LiquidField = @import("glass").LiquidField;

pub const Side = enum { left, right, top, bottom };
pub const Side = @import("DockingWidget/Layout.zig").Side;

pub const Zone = union(enum) {
/// Into the place itself: a trade for a place that shows one view, a new tab for one that
Expand Down
22 changes: 11 additions & 11 deletions plugins/workbench/src/Workbench.zig
Original file line number Diff line number Diff line change
Expand Up @@ -233,27 +233,26 @@ 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
/// pane has drawn, with no room to judge.
const Beside = struct { other: ?u64, answer: ?core.placement.Result };
const Beside = struct { other: ?u64, room: ?core.placement.Size, answer: ?core.placement.Result };

fn besideAnswer(self: *Workbench) Beside {
const here = self.open_workspace_grouping;
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, .answer = null };
const room = self.paneSize(here) orelse return .{ .other = other, .room = null, .answer = null };
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 &.{};
return .{ .other = other, .answer = core.placement.place(room, places) };
return .{ .other = other, .room = room, .answer = core.placement.place(room, places) };
}

/// The grouping a document opened beside the person's goes in (`core.placement.place`): another
/// pane that is there, else a new one when the person's has room for two, else the person's own,
/// where it becomes a card. The caller makes it the front card, as a plain open does.
pub fn besideGrouping(self: *Workbench) u64 {
const asked = self.besideAnswer();
const asked = self.besideAnswer(self.open_workspace_grouping);
// Not drawn yet, so no room to judge: beside as before, in the other pane if there is one.
const answer = asked.answer orelse return asked.other orelse self.newGroupingID();
return switch (answer) {
Expand All @@ -263,11 +262,12 @@ pub fn besideGrouping(self: *Workbench) u64 {
};
}

/// The edge of the person's pane an open beside splits it on, or null where it joins a pane or
/// becomes a card. What a demo's `drop:beside` aims at.
pub fn besideEdge(self: *Workbench) ?Panes.Side {
const room = self.paneSize(self.open_workspace_grouping) orelse return null;
return switch (self.besideAnswer().answer orelse return null) {
/// The edge of `grouping`'s pane an open beside it splits on, or null where it joins a pane or
/// becomes a card. What a demo's `drop:beside` aims at, over the pane it is aimed at.
pub fn besideEdge(self: *Workbench, grouping: u64) ?Panes.Side {
const asked = self.besideAnswer(grouping);
const room = asked.room orelse return null;
return switch (asked.answer orelse return null) {
.split => sideFor(room.w, room.h),
.into, .tab => null,
};
Expand Down
2 changes: 1 addition & 1 deletion plugins/workbench/src/Workspace.zig
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ pub fn draw(self: *Workspace) !dvui.App.Result {
// 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", .{});
runtime.workbench().besideEdge(self.grouping) != null) core.anchor.mark(pane_box, "workbench.beside-room", .{});

if (tabs.len > 0) {
self.dropSnapshot();
Expand Down
27 changes: 22 additions & 5 deletions src/editor/Demo.zig
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,8 @@ pub fn frame(self: *Demo) void {
.tape => |bytes| self.playTape(bytes) catch {},
}
}
// A drop beside is aimed at where the workbench would split, as the room is now.
self.editor.app.layout.beside = if (self.active())
if (self.editor.workbench.besideEdge()) |side| std.meta.stringToEnum(SplitTree.Side, @tagName(side)) else null
else
null;
// A drop beside is aimed at where the workbench would split the target place, as its room is now.
self.editor.app.layout.beside = if (self.active()) .{ .ctx = self, .edge = besideEdge } else null;
self.snapshot.beginFrame();
self.live.frame();
self.player.frame();
Expand Down Expand Up @@ -872,6 +869,26 @@ fn commandTitle(ctx: *anyopaque, id: []const u8) ?[]const u8 {
return c.title;
}

/// The edge a drop beside aims at over place `name`: where the workbench would split the pane
/// shown there (a pane of its own tree is named for its grouping). A pane the layout's keywords
/// show is in no list, so Main is the person's pane.
pub fn besideEdge(ctx: *anyopaque, name: []const u8) ?SplitTree.Side {
const editor = from(ctx).editor;
const wb = &editor.workbench;
const grouping = Editor.Workspace.groupingOfName(name) orelse editor.workspaces.groupingIn(name) orelse blk: {
const main = editor.regionFor(sdk.keywords.ide.main) orelse return null;
if (!std.mem.eql(u8, main.name, name)) return null;
var layout = Editor.Layout.init(&editor.app.host, &editor.app.layout, editor.app.gpa, editor.app.host.arena());
var buf: [64]u8 = undefined;
const id = std.mem.print(&buf, Editor.Workspaces.id_prefix ++ "{d}", .{wb.open_workspace_grouping}) catch return null;
if (Editor.Layout.ViewDrag.holderOf(&layout, id)) |holder| {
if (!std.mem.eql(u8, holder, name)) return null;
}
break :blk wb.open_workspace_grouping;
};
return wb.besideEdge(grouping);
}

/// The place a role names, by what it is rather than what the person's layout calls it.
fn resolveRole(ctx: *anyopaque, role: []const u8, buf: []u8) ?[]const u8 {
const keywords = if (std.mem.eql(u8, role, "documents"))
Expand Down
30 changes: 18 additions & 12 deletions src/editor/Workspaces.zig
Original file line number Diff line number Diff line change
Expand Up @@ -289,18 +289,29 @@ fn layoutOf(editor: *Editor) Layout {
return Layout.init(&editor.app.host, &editor.app.layout, editor.app.gpa, editor.app.host.arena());
}

/// The pane shown in place `name`: the person's own when it is there, else the first held.
/// Null where the place holds no pane (a pane the keywords show is in no list: the caller's).
pub fn groupingIn(self: *Workspaces, name: []const u8) ?u64 {
const editor = self.editorOf();
var l = layoutOf(editor);
var buf: [64]u8 = undefined;
var first: ?u64 = null;
for (self.views.keys()) |g| {
const holder = Layout.ViewDrag.holderOf(&l, viewId(&buf, g)) orelse continue;
if (!std.mem.eql(u8, holder, name)) continue;
if (g == editor.workbench.open_workspace_grouping) return g;
first = first orelse g;
}
return first;
}

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 abuf: [64]u8 = undefined;
const place = Layout.ViewDrag.holderOf(&l, viewId(&abuf, anchor)) orelse return false;
const new = Layout.splitOn(&l, place, switch (side) {
.left => .left,
.right => .right,
.top => .top,
.bottom => .bottom,
}) 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;
Expand Down Expand Up @@ -354,12 +365,7 @@ fn open(ctx: *anyopaque, grouping: u64, side: Workbench.Panes.Side) bool {
var l = layoutOf(self.editorOf());
var buf: [64]u8 = undefined;
const place = Layout.ViewDrag.holderOf(&l, viewId(&buf, grouping)) orelse return false;
return Layout.splitOn(&l, place, switch (side) {
.left => .left,
.right => .right,
.top => .top,
.bottom => .bottom,
}) != null;
return Layout.splitOn(&l, place, side) != null;
}

/// The view of the workspace whose only tab is document `id`, if one is.
Expand Down
85 changes: 85 additions & 0 deletions tests/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5547,6 +5547,71 @@ test "dragging a view over its own place shows that place's drop zones" {
try std.testing.expect(fizzy.core.widgets.DropZones.showing(center.id));
}

test "drop: beside marks the tall target's bottom rather than the wide source's right" {
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;
const state = &editor.app.layout;
defer state.regions.deinit(gpa);
defer state.regions_building.deinit(gpa);
defer state.deinitExtents(gpa);
defer state.deinitAssignments(gpa);
defer state.deinitQualified(gpa);
defer state.view_drag.discard(gpa);
const source: dvui.Rect.Physical = .{ .x = 0, .y = 0, .w = 1200, .h = 800 };
const target: dvui.Rect.Physical = .{ .x = 1200, .y = 0, .w = 820, .h = 1180 };
state.registerRegion(gpa, .{ .name = "Source", .keywords = fizzy.sdk.keywords.ide.main, .bounds = source, .size = .{ .w = source.w, .h = source.h } });
state.registerRegion(gpa, .{ .name = "Target", .keywords = &.{"slot"}, .bounds = target, .size = .{ .w = target.w, .h = target.h } });
state.publishRegions();
const Probe = struct {
calls: usize = 0,
fn edge(raw: *anyopaque, name: []const u8) ?fizzy.Editor.Layout.State.SplitTree.Side {
const self: *@This() = @ptrCast(@alignCast(raw));
self.calls += 1;
const room = if (std.mem.eql(u8, name, "Target")) target else source;
return workbench.Workbench.sideFor(room.w, room.h);
}
};
var probe: Probe = .{};
state.beside = .{ .ctx = &probe, .edge = Probe.edge };
defer state.beside = null;
try editor.app.host.registerSurface(.{ .id = "test.view", .title = "View", .keywords = &.{"slot"}, .draw = ManyPanelFrame.draw });
try state.assign(gpa, "Source", &.{"test.view"});
var layout = fizzy.Editor.Layout.init(&editor.app.host, state, gpa, dvui.currentWindow().arena());
const VD = fizzy.Editor.Layout.ViewDrag;
const cw = dvui.currentWindow();
const mouse = cw.mouse_pt;
defer cw.mouse_pt = mouse;
cw.mouse_pt = source.center();
VD.begin(&layout, "Source", source, source);
cw.mouse_pt = target.center();
fizzy.core.anchor.want();
VD.drawZones(&layout, "Target", dvui.parentGet().extendId(@src(), 0));
const beside = dvui.tagGet("drop:beside") orelse return error.TestUnexpectedResult;
const bottom = dvui.tagGet("drop:bottom") orelse return error.TestUnexpectedResult;
const right = dvui.tagGet("drop:right") orelse return error.TestUnexpectedResult;
try std.testing.expectEqual(bottom.rect, beside.rect);
try std.testing.expect(!std.meta.eql(right.rect, beside.rect));
try std.testing.expectEqual(@as(usize, 1), probe.calls);

// Main's implicit pane must not borrow the shape of a workspace held elsewhere.
const wb = &editor.workbench;
const grouping = wb.open_workspace_grouping;
var workspace = workbench.Workspace.init(grouping);
workspace.pane_rect_physical = source;
try wb.workspaces.put(gpa, grouping, workspace);
defer wb.workspaces.deinit(gpa);
var id_buf: [64]u8 = undefined;
const id = try std.mem.print(&id_buf, fizzy.Editor.Workspaces.id_prefix ++ "{d}", .{grouping});
try editor.app.host.registerSurface(.{ .id = id, .title = "Workspace", .keywords = &.{"slot"}, .draw = ManyPanelFrame.draw });
try state.assign(gpa, "Target", &.{id});
editor.demo.editor = editor;
editor.app.host.installFizzyApi(.{ .ctx = editor, .vtable = &fizzy.Editor.fizzy_api_vtable });
try std.testing.expectEqual(@as(?fizzy.Editor.Layout.State.SplitTree.Side, null), fizzy.Editor.Demo.besideEdge(&editor.demo, "Source"));
}

// `core` is not a test root, so its widgets' pure rules are tested here.
const DZ = fizzy.core.widgets.DropZones;

Expand Down Expand Up @@ -11030,6 +11095,26 @@ 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 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.
if (others == 0) {
const Side = fizzy.core.widgets.DockLayout.Side;
const main = (editor.regionFor(fizzy.sdk.keywords.ide.main) orelse return error.TestUnexpectedResult).name;
const sidebar = (editor.regionFor(fizzy.sdk.keywords.ide.sidebar) orelse return error.TestUnexpectedResult).name;
ws.pane_rect_physical = .{ .x = 0, .y = 0, .w = 1200 * s, .h = 800 * s };
try std.testing.expectEqual(@as(?Side, .right), fizzy.Editor.Demo.besideEdge(&editor.demo, main));
try std.testing.expectEqual(@as(?Side, null), fizzy.Editor.Demo.besideEdge(&editor.demo, sidebar));
ws.pane_rect_physical = .{ .x = 0, .y = 0, .w = 820 * s, .h = 1180 * s };
try std.testing.expectEqual(@as(?Side, .bottom), fizzy.Editor.Demo.besideEdge(&editor.demo, main));
var pane_buf: [32]u8 = undefined;
const pane = fizzy.Editor.Workspace.name(&pane_buf, here);
try std.testing.expectEqual(@as(?Side, .bottom), fizzy.Editor.Demo.besideEdge(&editor.demo, pane));
var other_buf: [32]u8 = undefined;
try std.testing.expectEqual(@as(?Side, null), fizzy.Editor.Demo.besideEdge(&editor.demo, fizzy.Editor.Workspace.name(&other_buf, here + 1000)));
try std.testing.expectEqual(@as(?Side, null), wb.besideEdge(here + 1000));
ws.pane_rect_physical = .{ .x = 0, .y = 0, .w = 390 * s, .h = 844 * s };
}
try std.testing.expectEqual(fizzy.core.widgets.DockLayout.Side.right, workbench.Workbench.sideFor(1200, 800));
try std.testing.expectEqual(fizzy.core.widgets.DockLayout.Side.bottom, workbench.Workbench.sideFor(820, 1180));
}
Expand Down
Loading