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
42 changes: 31 additions & 11 deletions app/layout/Picker.zig
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,10 @@ pub fn draw(self: *Picker, f: *Layout) void {
.padding = .{ .y = 4 },
});
defer controls.deinit();
if (modeButton(@src(), "Single", region.shows == .one, 1)) {
if (modeButton(@src(), "Single", "picker.mode:one", region.shows == .one, 1)) {
setShows(f, &region, .one);
}
if (modeButton(@src(), "Multiple", region.shows == .many, 2)) {
if (modeButton(@src(), "Multiple", "picker.mode:many", region.shows == .many, 2)) {
setShows(f, &region, .many);
}

Expand All @@ -189,6 +189,7 @@ pub fn draw(self: *Picker, f: *Layout) void {
.style = .control,
});
const split_from = split.activeRect();
core.anchor.mark(split.data(), "picker.action:split", .{});
{
var label = dvui.box(@src(), .{ .dir = .horizontal }, .{});
defer label.deinit();
Expand All @@ -211,14 +212,14 @@ pub fn draw(self: *Picker, f: *Layout) void {
.frost = core.widgets.menuFrost(),
}, core.widgets.menuSurfaceOptions());
defer choices.deinit();
if (core.widgets.menuRow(@src(), "Vertical", .{ .icon = icons.tvg.lucide.@"columns-2" }) != null) {
if (core.widgets.menuRow(@src(), "Vertical", .{ .icon = icons.tvg.lucide.@"columns-2", .anchor = "picker.split:vertical" }) != null) {
choices.close();
f.splitNamed(region.name, .horizontal);
self.close(gpa);
state.discardSnapshots(gpa);
return;
}
if (core.widgets.menuRow(@src(), "Horizontal", .{ .icon = icons.tvg.lucide.@"rows-2", .id_extra = 1 }) != null) {
if (core.widgets.menuRow(@src(), "Horizontal", .{ .icon = icons.tvg.lucide.@"rows-2", .id_extra = 1, .anchor = "picker.split:horizontal" }) != null) {
choices.close();
f.splitNamed(region.name, .vertical);
self.close(gpa);
Expand All @@ -228,17 +229,17 @@ pub fn draw(self: *Picker, f: *Layout) void {
}
}
if (!created) {
if (actionButton(@src(), "Clear", showing, 2)) {
if (actionButton(@src(), "Clear", "picker.action:clear", showing, 2)) {
clearRegion(f, region.name);
}
if (actionButton(@src(), "Defaults", assigned != null, 4)) {
if (actionButton(@src(), "Defaults", "picker.action:defaults", assigned != null, 4)) {
state.unassign(gpa, region.name);
state.markDirty();
dvui.refresh(null, @src(), null);
}
}
if (removable) {
if (actionButton(@src(), "Remove", true, 3)) {
if (actionButton(@src(), "Remove", "picker.action:remove", true, 3)) {
removeRegion(f, &region);
self.close(gpa);
state.discardSnapshots(gpa);
Expand Down Expand Up @@ -690,9 +691,9 @@ fn setShows(f: *Layout, region: *const Layout.Region, shows: Layout.Region.Shows
dvui.refresh(null, @src(), null);
}

fn modeButton(src: std.builtin.SourceLocation, label: []const u8, on: bool, id_extra: usize) bool {
fn modeButton(src: std.builtin.SourceLocation, label: []const u8, name: []const u8, on: bool, id_extra: usize) bool {
const theme = dvui.themeGet();
return dvui.button(src, label, .{}, .{
return namedButton(src, label, name, .{
.font = actionFont(),
.padding = .{ .x = 6, .y = 2, .w = 6, .h = 2 },
.margin = .{ .x = 4 },
Expand All @@ -709,9 +710,11 @@ fn actionFont() dvui.Font {

/// One of the picker's actions. Disabled, it keeps its place (dimmed, no hover, never clicked)
/// rather than leaving a gap the row closes up over.
fn actionButton(src: std.builtin.SourceLocation, label: []const u8, enabled: bool, id_extra: usize) bool {
fn actionButton(src: std.builtin.SourceLocation, label: []const u8, name: []const u8, enabled: bool, id_extra: usize) bool {
const theme = dvui.themeGet();
const clicked = dvui.button(src, label, .{}, .{
// Named only while it can be pressed: a dimmed one swallows the click, and a driver waiting
// for the name should wait for the action, not for the button.
const clicked = namedButton(src, label, if (enabled) name else null, .{
.font = actionFont(),
.padding = .{ .x = 6, .y = 2, .w = 6, .h = 2 },
.margin = .{ .w = 4 },
Expand All @@ -724,6 +727,23 @@ fn actionButton(src: std.builtin.SourceLocation, label: []const u8, enabled: boo
return enabled and clicked;
}

/// `dvui.button`, named `name` (when there is one) for a demo or an agent to aim at (`core.anchor`, `picker.*` in
/// docs/AUTOMATION.md). The row's buttons come and go with the place — Remove sits where Split
/// sat on a place with nothing to Clear — so a point read off one place's picker can land on
/// another action in the next; a name cannot.
fn namedButton(src: std.builtin.SourceLocation, label: []const u8, name: ?[]const u8, opts: dvui.Options) bool {
var bw: dvui.ButtonWidget = undefined;
bw.init(src, .{}, opts);
defer bw.deinit();
if (name) |n| core.anchor.mark(bw.data(), "{s}", .{n});
bw.processEvents();
bw.drawBackground();
const clicked = bw.clicked();
dvui.labelNoFmt(@src(), label, .{ .align_x = 0.5, .align_y = 0.5 }, opts.strip().override(bw.style()).override(.{ .gravity_x = 0.5, .gravity_y = 0.5 }));
bw.drawFocus();
return clicked;
}

fn contains(list: []const *sdk.Surface, id: []const u8) bool {
for (list) |s| if (std.mem.eql(u8, s.id, id)) return true;
return false;
Expand Down
4 changes: 4 additions & 0 deletions core/widgets.zig
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ pub const MenuRowOptions = struct {
/// The command the row runs, if it names one: the row is then a demo's to point at, as
/// `fizzy.command:<id>` (`core.anchor`).
command: ?[]const u8 = null,
/// A name of its own for a row that runs no command, for a demo or an agent to aim at
/// (`core.anchor`).
anchor: ?[]const u8 = null,
};

/// One menu row, the same everywhere: an icon column, the label straight after it, and the
Expand All @@ -153,6 +156,7 @@ pub fn menuRow(src: std.builtin.SourceLocation, label: []const u8, opts: MenuRow
var mi = menuItem(src, .{ .submenu = opts.submenu }, menuRowOptions(.{ .expand = .horizontal, .id_extra = opts.id_extra }));
const ret: ?dvui.Rect.Natural = if (opts.enabled) mi.activeRect() else null;
if (opts.command) |id| anchor.mark(mi.data(), "fizzy.command:{s}", .{id});
if (opts.anchor) |name| anchor.mark(mi.data(), "{s}", .{name});

// Closed before `mi`: it is `mi`'s child, and dvui's widget stack is strictly LIFO.
var row = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal, .id_extra = opts.id_extra });
Expand Down
4 changes: 4 additions & 0 deletions docs/AUTOMATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ user's motion settings, timed in demo time.
| `fizzy.window:main` | the main window |
| `fizzy.window:dialog:<title>`, `fizzy.window:float:<title>`, `fizzy.window:menu` | an OS window of the app's own: a dialog's, a popped-out view's, a menu's (a second of the same name gets `:2`) |
| `region:<name>` | a layout region (`Main`, `Panel`, …) |
| `picker:<view id>` | a view's card in a place's picker |
| `picker.mode:one`, `picker.mode:many` | the picker's Single / Multiple |
| `picker.action:split`, `:clear`, `:defaults`, `:remove` | the picker's actions, each only while it can be pressed (a dimmed Clear has no name) |
| `picker.split:vertical`, `picker.split:horizontal` | the rows of the picker's Split menu |

A demo's files live under its keyframe's `root`, so a path is `demo://tour/src/main.zig`.
`src/editor/demos/catalog.zig` has helpers (`catalog.file(s, "src/main.zig")`) that build these.
Expand Down
32 changes: 32 additions & 0 deletions tests/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2797,6 +2797,38 @@ const FizzySplitFrame = struct {
}
};

test "picker: its controls are named for a tape or an agent to aim at, while they can be pressed" {
var ctx = try shim.init(std.testing.allocator);
defer ctx.deinit(std.testing.allocator);

const editor = ctx.editor;
editor.app.gpa = std.testing.allocator;
defer editor.app.layout.regions.deinit(editor.app.gpa);
defer editor.app.layout.regions_building.deinit(editor.app.gpa);
defer editor.app.layout.deinitExtents(editor.app.gpa);
defer editor.app.layout.deinitAssignments(editor.app.gpa);
defer editor.app.layout.deinitQualified(editor.app.gpa);
defer editor.app.layout.picker.close(editor.app.gpa);

PickerFrame.editor = editor;
defer PickerFrame.editor = null;

try dvui.testing.settle(PickerFrame.frame);
editor.app.layout.picker.open(editor.app.gpa, "Main", null);
_ = try dvui.testing.step(PickerFrame.frame);
_ = try dvui.testing.step(PickerFrame.frame);

for ([_][]const u8{ "picker.mode:one", "picker.mode:many", "picker.action:split" }) |name| {
if (dvui.tagGet(name) == null) {
std.debug.print("no anchor {s}\n", .{name});
return error.TestExpectedEqual;
}
}
// Main shows nothing, so Clear is dimmed and has no name; a shape's place has no Remove.
try std.testing.expect(dvui.tagGet("picker.action:clear") == null);
try std.testing.expect(dvui.tagGet("picker.action:remove") == null);
}

test "a fizzy Main split is a removable slot, not main.slot" {
var ctx = try shim.init(std.testing.allocator);
defer ctx.deinit(std.testing.allocator);
Expand Down
Loading