Files
playpen/src/OpenLayoutDialog.zig
T
2026-08-11 14:26:12 -04:00

171 lines
5.3 KiB
Zig

//! "Open layout": one field per parameter, prefilled with its default.
//!
//! A layout with no parameters never gets here — the caller opens it straight
//! away rather than showing an empty dialog with nothing to fill in.
//!
//! The values are handed back as borrowed slices pointing into the entries,
//! which is safe because the callback runs while the dialog is still up and
//! the view copies everything during substitution.
const std = @import("std");
const gtk = @import("gtk");
const Layouts = @import("Layouts.zig");
const OpenLayoutDialog = @This();
pub const Callback = *const fn (
ctx: ?*anyopaque,
layout: *Layouts.Layout,
bindings: []const Layouts.Binding,
) void;
alloc: std.mem.Allocator,
window: *gtk.Window,
layout: *Layouts.Layout,
/// One entry per parameter, in the layout's own order.
entries: []*gtk.Entry,
on_open: Callback,
ctx: ?*anyopaque,
pub fn present(
alloc: std.mem.Allocator,
parent: *gtk.Window,
layout: *Layouts.Layout,
on_open: Callback,
ctx: ?*anyopaque,
) !void {
const self = try alloc.create(OpenLayoutDialog);
errdefer alloc.destroy(self);
const entries = try alloc.alloc(*gtk.Entry, layout.parameters.len);
errdefer alloc.free(entries);
self.* = .{
.alloc = alloc,
.window = gtk.Window.new(),
.layout = layout,
.entries = entries,
.on_open = on_open,
.ctx = ctx,
};
var title_buf: [128]u8 = undefined;
const title = std.fmt.bufPrintZ(&title_buf, "Open “{s}”", .{layout.name}) catch "Open layout";
self.window.setTitle(title);
self.window.setTransientFor(parent);
self.window.setModal(1);
self.window.setDefaultSize(480, -1);
self.window.as(gtk.Widget).addCssClass("playpen-dialog");
const content = gtk.Box.new(.vertical, 12);
content.as(gtk.Widget).addCssClass("playpen-dialog-content");
const fields = gtk.Grid.new();
fields.setRowSpacing(8);
fields.setColumnSpacing(12);
for (layout.parameters, 0..) |param, i| {
const label = gtk.Label.new(null);
// The description is what the author wrote for a human; the bare name
// is the fallback so a parameter is never unlabelled.
var label_buf: [128]u8 = undefined;
const text = if (param.description.len > 0) param.description else param.name;
label.setText(std.fmt.bufPrintZ(&label_buf, "{s}", .{text}) catch "parameter");
label.setXalign(0);
label.as(gtk.Widget).addCssClass("playpen-dialog-label");
fields.attach(label.as(gtk.Widget), 0, @intCast(i), 1, 1);
const entry = gtk.Entry.new();
entry.as(gtk.Widget).setHexpand(1);
setEntryText(entry, param.default);
var hint_buf: [128]u8 = undefined;
entry.setPlaceholderText(
std.fmt.bufPrintZ(&hint_buf, "{{{{{s}}}}}", .{param.name}) catch null,
);
// Enter anywhere in the form opens the layout, so a one-parameter
// layout is type-and-go.
_ = gtk.Entry.signals.activate.connect(
entry,
*OpenLayoutDialog,
&onEntryActivate,
self,
.{},
);
fields.attach(entry.as(gtk.Widget), 1, @intCast(i), 1, 1);
entries[i] = entry;
}
content.append(fields.as(gtk.Widget));
const buttons = gtk.Box.new(.horizontal, 8);
buttons.as(gtk.Widget).setHalign(.end);
const cancel = gtk.Button.newWithLabel("Cancel");
_ = gtk.Button.signals.clicked.connect(cancel, *OpenLayoutDialog, &onCancel, self, .{});
buttons.append(cancel.as(gtk.Widget));
const open = gtk.Button.newWithLabel("Open");
open.as(gtk.Widget).addCssClass("suggested-action");
_ = gtk.Button.signals.clicked.connect(open, *OpenLayoutDialog, &onOpen, self, .{});
buttons.append(open.as(gtk.Widget));
content.append(buttons.as(gtk.Widget));
self.window.setChild(content.as(gtk.Widget));
_ = gtk.Widget.signals.destroy.connect(
self.window,
*OpenLayoutDialog,
&onDestroy,
self,
.{},
);
self.window.present();
if (entries.len > 0) _ = entries[0].as(gtk.Widget).grabFocus();
}
fn setEntryText(entry: *gtk.Entry, text: []const u8) void {
var buf: [1024]u8 = undefined;
const z = std.fmt.bufPrintZ(&buf, "{s}", .{text}) catch return;
entry.as(gtk.Editable).setText(z);
}
fn submit(self: *OpenLayoutDialog) void {
var storage: [16]Layouts.Binding = undefined;
const count = @min(self.entries.len, storage.len);
for (self.entries[0..count], 0..) |entry, i| {
storage[i] = .{
.name = self.layout.parameters[i].name,
.value = std.mem.span(entry.as(gtk.Editable).getText()),
};
}
// The callback builds the tab while this window is still alive, so the
// borrowed entry text stays valid for the whole substitution pass.
self.on_open(self.ctx, self.layout, storage[0..count]);
self.window.destroy();
}
fn onOpen(_: *gtk.Button, self: *OpenLayoutDialog) callconv(.c) void {
self.submit();
}
fn onEntryActivate(_: *gtk.Entry, self: *OpenLayoutDialog) callconv(.c) void {
self.submit();
}
fn onCancel(_: *gtk.Button, self: *OpenLayoutDialog) callconv(.c) void {
self.window.destroy();
}
fn onDestroy(_: *gtk.Window, self: *OpenLayoutDialog) callconv(.c) void {
self.alloc.free(self.entries);
self.alloc.destroy(self);
}