From 12c2103b5bc8231a64cc4bc6e7b53cc6332e036c Mon Sep 17 00:00:00 2001
From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 16:49:20 -0500
Subject: [PATCH 1/4] core: touch-button (codex)
Agent-Team: linux
Worker: codex
---
core/form_factor.zig | 3 +
core/sizing_tests.zig | 1 +
core/widgets.zig | 58 ++++++++++++++++
core/widgets/Button.zig | 149 ++++++++++++++++++++++++++++++++++++++++
scripts/lint-allow.zon | 30 ++++++++
scripts/lint.zig | 32 +++++++--
6 files changed, 269 insertions(+), 4 deletions(-)
create mode 100644 core/widgets/Button.zig
diff --git a/core/form_factor.zig b/core/form_factor.zig
index f21235431..02705b6c5 100644
--- a/core/form_factor.zig
+++ b/core/form_factor.zig
@@ -5,6 +5,9 @@
//! per frame, since a 2-in-1's pointer changes at runtime. `core.platform` holds the live value.
const std = @import("std");
+/// Minimum touch hit width and height in points.
+pub const min_hit: f32 = 44;
+
pub const FormFactor = enum { desktop, tablet, mobile };
/// The person's Layout setting (`settings.zon`'s `layout_mode`): `auto` follows what was detected,
diff --git a/core/sizing_tests.zig b/core/sizing_tests.zig
index 4901906a2..c942c7476 100644
--- a/core/sizing_tests.zig
+++ b/core/sizing_tests.zig
@@ -10,6 +10,7 @@
//! nothing else: every bug this pair has had was a dvui event-routing or layout-settle rule rather
//! than arithmetic, and reading the code caught none of them.
test {
+ _ = @import("widgets/Button.zig");
_ = @import("glass.zig");
_ = @import("widgets/Split.zig");
_ = @import("widgets/ContextWidget.zig");
diff --git a/core/widgets.zig b/core/widgets.zig
index c8773046e..7eb0cac1d 100644
--- a/core/widgets.zig
+++ b/core/widgets.zig
@@ -24,6 +24,8 @@ const draw = @import("draw.zig");
const anchor = @import("replay").anchor;
const icon_tex = @import("gfx/icon.zig");
+pub const Button = @import("widgets/Button.zig");
+
pub const CanvasWidget = @import("widgets/CanvasWidget.zig");
pub const ReorderWidget = @import("widgets/ReorderWidget.zig");
pub const FloatingWindowWidget = @import("widgets/FloatingWindowWidget.zig");
@@ -476,3 +478,59 @@ pub fn tabCloseButtonOptions(over: dvui.Options) dvui.Options {
.ninepatch_press = &dvui.Ninepatch.none,
}));
}
+
+pub fn button(src: std.builtin.SourceLocation, label_str: []const u8, init_opts: Button.InitOptions, opts: dvui.Options) bool {
+ var bw: Button = undefined;
+ bw.init(src, init_opts, opts);
+ bw.processEvents();
+ bw.drawBackground();
+ const click = bw.clicked();
+ dvui.labelNoFmt(@src(), label_str, .{ .align_x = 0.5, .align_y = 0.5 }, opts.strip().override(bw.style()).override(.{ .gravity_x = 0.5, .gravity_y = 0.5 }));
+ bw.drawFocus();
+ bw.deinit();
+
+ return click;
+}
+
+pub fn buttonIcon(src: std.builtin.SourceLocation, name: []const u8, tvg_bytes: []const u8, init_opts: Button.InitOptions, icon_opts: dvui.IconRenderOptions, opts: dvui.Options) bool {
+ const defaults = dvui.Options{ .padding = dvui.Rect.all(4), .label = .{ .text = name } };
+ var bw: Button = undefined;
+ bw.init(src, init_opts, defaults.override(opts));
+ bw.processEvents();
+ bw.drawBackground();
+ icon_tex.icon(
+ @src(),
+ name,
+ tvg_bytes,
+ icon_opts,
+ opts.strip().override(bw.style()).override(.{ .gravity_x = 0.5, .gravity_y = 0.5, .min_size_content = opts.min_size_content, .expand = .ratio, .color_text = opts.color_text, .role = .none }),
+ );
+
+ const click = bw.clicked();
+ bw.drawFocus();
+ bw.deinit();
+ return click;
+}
+
+pub const ButtonLabelAndIconOptions = dvui.ButtonLabelAndIconOptions;
+
+pub fn buttonLabelAndIcon(src: std.builtin.SourceLocation, combined_opts: ButtonLabelAndIconOptions, opts: dvui.Options) bool {
+ var bw: Button = undefined;
+ bw.init(src, combined_opts.button_opts, opts);
+ bw.processEvents();
+ const options = opts.strip().override(bw.style()).override(.{ .gravity_y = 0.5 });
+ bw.drawBackground();
+ {
+ var outer_hbox = dvui.box(src, .{ .dir = .horizontal }, .{ .expand = .horizontal, .gravity_y = 0.5 });
+ defer outer_hbox.deinit();
+ icon_tex.icon(@src(), combined_opts.icon_label orelse combined_opts.label, combined_opts.tvg_bytes, .{}, options.strip().override(.{ .gravity_x = if (combined_opts.icon_first) 0.0 else 1.0, .color_text = opts.color_text }));
+ dvui.labelEx(@src(), "{s}", .{combined_opts.label}, .{ .align_x = 0.5 }, options.strip().override(.{ .expand = .both }));
+ }
+
+ const click = bw.clicked();
+
+ bw.drawFocus();
+
+ bw.deinit();
+ return click;
+}
diff --git a/core/widgets/Button.zig b/core/widgets/Button.zig
new file mode 100644
index 000000000..12e15a728
--- /dev/null
+++ b/core/widgets/Button.zig
@@ -0,0 +1,149 @@
+//! dvui's button with a minimum touch hit area independent of its drawn and layout rectangles.
+const std = @import("std");
+const dvui = @import("dvui");
+const form_factor = @import("../form_factor.zig");
+const Button = @This();
+
+pub const InitOptions = dvui.ButtonWidget.InitOptions;
+
+inner: dvui.ButtonWidget,
+
+pub fn init(self: *Button, src: std.builtin.SourceLocation, init_opts: InitOptions, opts: dvui.Options) void {
+ self.inner.init(src, init_opts, opts);
+}
+
+/// The physical click rectangle, centred on the border and never smaller than it.
+pub fn hitRect(self: *Button) dvui.Rect.Physical {
+ const rs = self.data().borderRectScale();
+ var rect = rs.r;
+ if (dvui.pointerKind() == .touch) {
+ const minimum = form_factor.min_hit * rs.s;
+ const w = @max(rect.w, minimum);
+ const h = @max(rect.h, minimum);
+ rect.x -= (w - rect.w) / 2;
+ rect.y -= (h - rect.h) / 2;
+ rect.w = w;
+ rect.h = h;
+ }
+ return rect;
+}
+
+pub fn processEvents(self: *Button) void {
+ self.inner.click = dvui.clicked(self.data(), .{
+ .rect = self.hitRect(),
+ .hovered = &self.inner.hover,
+ .touch_drag = self.inner.init_options.touch_drag,
+ });
+}
+
+pub fn matchEvent(self: *Button, e: *dvui.Event) bool {
+ return self.inner.matchEvent(e);
+}
+
+pub fn processHover(self: *Button) void {
+ self.inner.processHover();
+}
+
+pub fn drawBackground(self: *Button) void {
+ self.inner.drawBackground();
+}
+
+pub fn drawFocus(self: *Button) void {
+ self.inner.drawFocus();
+}
+
+pub fn style(self: *Button) dvui.Options {
+ return self.inner.style();
+}
+
+pub fn focused(self: *Button) bool {
+ return self.inner.focused();
+}
+
+pub fn hovered(self: *Button) bool {
+ return self.inner.hovered();
+}
+
+pub fn pressed(self: *Button) bool {
+ return self.inner.pressed();
+}
+
+pub fn clicked(self: *Button) bool {
+ return self.inner.clicked();
+}
+
+pub fn widget(self: *Button) dvui.Widget {
+ return self.inner.widget();
+}
+
+pub fn data(self: *Button) *dvui.WidgetData {
+ return self.inner.data();
+}
+
+pub fn rectFor(self: *Button, id: dvui.Id, min_size: dvui.Size, e: dvui.Options.Expand, g: dvui.Options.Gravity) dvui.Rect {
+ return self.inner.rectFor(id, min_size, e, g);
+}
+
+pub fn screenRectScale(self: *Button, rect: dvui.Rect) dvui.RectScale {
+ return self.inner.screenRectScale(rect);
+}
+
+pub fn minSizeForChild(self: *Button, size: dvui.Size) void {
+ self.inner.minSizeForChild(size);
+}
+
+pub fn deinit(self: *Button) void {
+ defer if (dvui.widgetIsAllocated(self)) dvui.widgetFree(self);
+ self.inner.deinit();
+}
+
+test "touch clicks six points outside a 32 point border without changing its drawn rect" {
+ const widgets = @import("../widgets.zig");
+ _ = &widgets.button;
+ _ = &widgets.buttonIcon;
+ _ = &widgets.buttonLabelAndIcon;
+ const Frame = struct {
+ // The test callback writes these on the testing window's thread.
+ var border: dvui.Rect.Physical = .{};
+ // The test callback writes this on the testing window's thread.
+ var click: bool = false;
+
+ fn draw() !dvui.App.Result {
+ var button: Button = undefined;
+ button.init(@src(), .{}, .{
+ .rect = .{ .x = 50, .y = 50, .w = 32, .h = 32 },
+ .margin = dvui.Rect.all(0),
+ });
+ defer button.deinit();
+ button.processEvents();
+ button.drawBackground();
+ border = button.data().borderRectScale().r;
+ click = button.clicked();
+ return .ok;
+ }
+ };
+ var t = try dvui.testing.init(.{ .window_size = .{ .w = 200, .h = 200 } });
+ defer t.deinit();
+ try dvui.testing.settle(Frame.draw);
+ const original = Frame.border;
+ const cw = dvui.currentWindow();
+ const scale = cw.natural_scale;
+ const point: dvui.Point.Physical = .{ .x = original.x - 6 * scale, .y = original.center().y };
+ const norm: dvui.Point = .{ .x = point.x / (200 * scale), .y = point.y / (200 * scale) };
+ _ = try cw.addEventPointer(.{ .button = .touch0, .action = .press, .xynorm = norm });
+ _ = try dvui.testing.step(Frame.draw);
+ _ = try cw.addEventPointer(.{ .button = .touch0, .action = .release, .xynorm = norm });
+ _ = try dvui.testing.step(Frame.draw);
+ try std.testing.expectEqual(.touch, dvui.pointerKind());
+ try std.testing.expect(Frame.click);
+ try std.testing.expectEqual(original, Frame.border);
+
+ _ = try cw.addEventMouseMotion(.{ .pt = point });
+ _ = try cw.addEventMouseButton(.left, .press);
+ _ = try dvui.testing.step(Frame.draw);
+ _ = try cw.addEventMouseButton(.left, .release);
+ _ = try dvui.testing.step(Frame.draw);
+ try std.testing.expectEqual(.mouse, dvui.pointerKind());
+ try std.testing.expect(!Frame.click);
+ try std.testing.expectEqual(original, Frame.border);
+}
diff --git a/scripts/lint-allow.zon b/scripts/lint-allow.zon
index 8e5f8773b..2b5042a93 100644
--- a/scripts/lint-allow.zon
+++ b/scripts/lint-allow.zon
@@ -103,4 +103,34 @@
.{ .rule = .@"raw-scroll-area", .file = "src/editor/dialogs/FileTypeDefaults.zig", .count = 1 },
.{ .rule = .@"raw-scroll-area", .file = "src/editor/dialogs/PluginUpdates.zig", .count = 1 },
.{ .rule = .@"raw-scroll-area", .file = "tests/integration.zig", .count = 3 },
+ .{ .rule = .@"raw-button", .file = "app/ActivityFeed.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "app/layout/Picker.zig", .count = 3 },
+ .{ .rule = .@"raw-button", .file = "app/layout/Region.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "app/store/PluginStore.zig", .count = 17 },
+ .{ .rule = .@"raw-button", .file = "app/update/update_notify.zig", .count = 4 },
+ .{ .rule = .@"raw-button", .file = "core/dialogs.zig", .count = 4 },
+ .{ .rule = .@"raw-button", .file = "core/widgets/ContextWidget.zig", .count = 2 },
+ .{ .rule = .@"raw-button", .file = "core/widgets/DockingWidget.zig", .count = 2 },
+ .{ .rule = .@"raw-button", .file = "core/widgets/TreeWidget.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "core/widgets/glass_button.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "plugins/dashboard/src/View.zig", .count = 2 },
+ .{ .rule = .@"raw-button", .file = "plugins/devtools/src/Dashboard.zig", .count = 2 },
+ .{ .rule = .@"raw-button", .file = "plugins/text/src/TextEditor.zig", .count = 2 },
+ .{ .rule = .@"raw-button", .file = "plugins/text/src/widgets/TextEntryWidget.zig", .count = 4 },
+ .{ .rule = .@"raw-button", .file = "plugins/workbench/src/Workspace.zig", .count = 4 },
+ .{ .rule = .@"raw-button", .file = "plugins/workbench/src/files.zig", .count = 3 },
+ .{ .rule = .@"raw-button", .file = "sdk/replay/Snapshot.zig", .count = 4 },
+ .{ .rule = .@"raw-button", .file = "sdk/src/Host.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "src/editor/Accounts.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "src/editor/Editor.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "src/editor/FileTypeSettings.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "src/editor/Infobar.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "src/editor/KeybindSettings.zig", .count = 2 },
+ .{ .rule = .@"raw-button", .file = "src/editor/LayoutSettings.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "src/editor/OutputPanel.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "src/editor/dialogs/SaveQuestion.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "src/editor/explorer/settings.zig", .count = 2 },
+ .{ .rule = .@"raw-button", .file = "src/editor/layout_mobile.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "tests/bench/bench_replay.zig", .count = 1 },
+ .{ .rule = .@"raw-button", .file = "tests/integration.zig", .count = 2 },
}
diff --git a/scripts/lint.zig b/scripts/lint.zig
index bbceba2b8..ccfbd7ce9 100644
--- a/scripts/lint.zig
+++ b/scripts/lint.zig
@@ -12,6 +12,7 @@ const Rule = enum {
@"changelog-words",
@"raw-transient-surface",
@"raw-scroll-area",
+ @"raw-button",
};
const Finding = struct { rule: Rule, line: usize, message: []const u8 };
const Allow = struct { rule: Rule, file: []const u8, count: usize };
@@ -249,7 +250,7 @@ const RawCall = struct {
name: []const u8,
rule: Rule,
message: []const u8,
- call_only: bool = false,
+ next: ?std.zig.Token.Tag = null,
};
const raw_calls = blk: {
const surfaces = [_][]const u8{
@@ -257,7 +258,7 @@ const raw_calls = blk: {
"popup", "PopupWidget", "floatingWindow", "FloatingWindowWidget",
"dialog", "toast", "contextMenu", "dropdown",
};
- var calls: [surfaces.len + 1]RawCall = undefined;
+ var calls: [surfaces.len + 5]RawCall = undefined;
for (surfaces, 0..) |name, i| calls[i] = .{
.name = name,
.rule = .@"raw-transient-surface",
@@ -267,7 +268,13 @@ const raw_calls = blk: {
.name = "scrollArea",
.rule = .@"raw-scroll-area",
.message = "dvui.scrollArea bypasses touch scrolling; use core.widgets.ScrollArea.init",
- .call_only = true,
+ .next = .l_paren,
+ };
+ for ([_][]const u8{ "button", "buttonIcon", "buttonLabelAndIcon", "ButtonWidget" }, 0..) |name, i| calls[surfaces.len + 1 + i] = .{
+ .name = name,
+ .rule = .@"raw-button",
+ .message = "raw dvui button bypasses touch hit targets; use core.widgets.Button or its verbs",
+ .next = if (i == 3) .equal else .l_paren,
};
break :blk calls;
};
@@ -286,7 +293,9 @@ fn rawDvuiCalls(
const name = tree.tokenSlice(@intCast(i + 2));
for (raw_calls) |banned| {
if (!std.mem.eql(u8, name, banned.name)) continue;
- if (banned.call_only and (i + 3 >= tags.len or tags[i + 3] != .l_paren)) continue;
+ if (banned.next) |next| {
+ if (i + 3 >= tags.len or tags[i + 3] != next) continue;
+ }
const line = tree.tokenLocation(0, @intCast(i)).line + 1;
if (silenced(suppressions, banned.rule, line)) break;
try findings.append(allocator, .{
@@ -888,3 +897,18 @@ test "a file over its raw-scroll-area budget fails" {
try std.testing.expect(seen[0]);
try std.testing.expect(std.mem.endsWith(u8, output.written(), "raw-scroll-area: count 2 exceeds allowance 1\n"));
}
+
+test "raw-button catches verbs and declarations but allows type references" {
+ var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena.deinit();
+ const findings = try analyze(
+ arena.allocator(),
+ "fn f() void { _ = dvui.button(a,b,c,d); _ = dvui.buttonIcon(a,b,c,d,e,f); _ = dvui.buttonLabelAndIcon(a,b,c); var b: dvui.ButtonWidget = undefined; }\n" ++
+ "const InitOptions = dvui.ButtonWidget.InitOptions;\n" ++
+ "fn g(b: *dvui.ButtonWidget) void {}\n" ++
+ "// lint-allow(raw-button): wrapper owns the dvui implementation\n" ++
+ "var b: dvui.ButtonWidget = undefined;",
+ );
+ try std.testing.expectEqual(@as(usize, 4), findings.len);
+ for (findings) |finding| try std.testing.expectEqual(Rule.@"raw-button", finding.rule);
+}
From 9878f0aa83942f48cf40ee90c2d980b879f7bd5a Mon Sep 17 00:00:00 2001
From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 18:31:01 -0500
Subject: [PATCH 2/4] core: touch-button (codex)
Agent-Team: linux
Worker: codex
---
core/widgets.zig | 13 ++++++++++++-
core/widgets/Button.zig | 22 +++++++++++++++++++++-
2 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/core/widgets.zig b/core/widgets.zig
index 7eb0cac1d..68297c4eb 100644
--- a/core/widgets.zig
+++ b/core/widgets.zig
@@ -19,6 +19,7 @@ const dvui = @import("dvui");
const builtin = @import("builtin");
const icons = @import("icons");
const platform = @import("platform.zig");
+const keyboard = @import("keyboard.zig");
const dialogs = @import("dialogs.zig");
const draw = @import("draw.zig");
const anchor = @import("replay").anchor;
@@ -228,7 +229,7 @@ pub fn textEntryDeinit(te: *dvui.TextEntryWidget) void {
const focused = wd.id == dvui.focusedWidgetId();
te.deinit();
// The entry's own scroll area must be closed before scrolling the enclosing area.
- @import("keyboard.zig").scrollIntoView(&wd, focused);
+ keyboard.scrollIntoView(&wd, focused);
}
/// A context menu's surface, opened at the point the pointer was pressed. Every right-click
@@ -479,6 +480,10 @@ pub fn tabCloseButtonOptions(over: dvui.Options) dvui.Options {
}));
}
+// Keep these button helpers in step with dvui's helpers when updating the dvui pin.
+/// Draw a labelled button and return whether it was clicked this frame.
+/// Uses a centred minimum 44 pt touch target; mouse, drawing and layout follow dvui.
+/// The label, interaction options and styling have the same contract as `dvui.button`.
pub fn button(src: std.builtin.SourceLocation, label_str: []const u8, init_opts: Button.InitOptions, opts: dvui.Options) bool {
var bw: Button = undefined;
bw.init(src, init_opts, opts);
@@ -492,6 +497,9 @@ pub fn button(src: std.builtin.SourceLocation, label_str: []const u8, init_opts:
return click;
}
+/// Draw the TinyVG icon in `tvg_bytes`, identified by `name`, and return a click this frame.
+/// Uses a centred minimum 44 pt touch target and the cached icon renderer.
+/// Interaction, icon and styling options follow `dvui.buttonIcon`.
pub fn buttonIcon(src: std.builtin.SourceLocation, name: []const u8, tvg_bytes: []const u8, init_opts: Button.InitOptions, icon_opts: dvui.IconRenderOptions, opts: dvui.Options) bool {
const defaults = dvui.Options{ .padding = dvui.Rect.all(4), .label = .{ .text = name } };
var bw: Button = undefined;
@@ -514,6 +522,9 @@ pub fn buttonIcon(src: std.builtin.SourceLocation, name: []const u8, tvg_bytes:
pub const ButtonLabelAndIconOptions = dvui.ButtonLabelAndIconOptions;
+/// Draw the label and icon described by `combined_opts` and return a click this frame.
+/// Uses a centred minimum 44 pt touch target and the cached icon renderer.
+/// Combined and styling options follow `dvui.buttonLabelAndIcon`.
pub fn buttonLabelAndIcon(src: std.builtin.SourceLocation, combined_opts: ButtonLabelAndIconOptions, opts: dvui.Options) bool {
var bw: Button = undefined;
bw.init(src, combined_opts.button_opts, opts);
diff --git a/core/widgets/Button.zig b/core/widgets/Button.zig
index 12e15a728..e1d3ffbe9 100644
--- a/core/widgets/Button.zig
+++ b/core/widgets/Button.zig
@@ -8,6 +8,8 @@ pub const InitOptions = dvui.ButtonWidget.InitOptions;
inner: dvui.ButtonWidget,
+/// Install a button using dvui interaction and layout options; pair with `deinit`.
+/// Call during a frame, then process events before drawing its background and children.
pub fn init(self: *Button, src: std.builtin.SourceLocation, init_opts: InitOptions, opts: dvui.Options) void {
self.inner.init(src, init_opts, opts);
}
@@ -28,7 +30,11 @@ pub fn hitRect(self: *Button) dvui.Rect.Physical {
return rect;
}
+/// Consume this frame’s clicks using the touch hit rectangle; call once before drawing.
pub fn processEvents(self: *Button) void {
+ // TODO(touch-target arbitration): choose the nearest border before consuming overlapping
+ // targets; processing order wins today, including over a neighbour’s border. Resolve before
+ // migrating tightly packed toolbars.
self.inner.click = dvui.clicked(self.data(), .{
.rect = self.hitRect(),
.hovered = &self.inner.hover,
@@ -36,64 +42,78 @@ pub fn processEvents(self: *Button) void {
});
}
+/// Test an event against dvui’s widget event routing, without consuming it.
pub fn matchEvent(self: *Button, e: *dvui.Event) bool {
return self.inner.matchEvent(e);
}
+/// Update hover state using dvui’s border rectangle.
pub fn processHover(self: *Button) void {
self.inner.processHover();
}
+/// Draw the styled border and background without expanding them to the touch target.
pub fn drawBackground(self: *Button) void {
self.inner.drawBackground();
}
+/// Draw the keyboard focus indicator after drawing the button’s children.
pub fn drawFocus(self: *Button) void {
self.inner.drawFocus();
}
+/// The current dvui style, including hover and press state, for styling children.
pub fn style(self: *Button) dvui.Options {
return self.inner.style();
}
+/// Whether this button owns keyboard focus in the current window.
pub fn focused(self: *Button) bool {
return self.inner.focused();
}
+/// Whether this frame’s event processing marked the button hovered.
pub fn hovered(self: *Button) bool {
return self.inner.hovered();
}
+/// Whether this button currently holds pointer capture.
pub fn pressed(self: *Button) bool {
return self.inner.pressed();
}
+/// Whether event processing activated the button this frame.
pub fn clicked(self: *Button) bool {
return self.inner.clicked();
}
+/// The inner dvui widget and its layout vtable, valid until `deinit`.
pub fn widget(self: *Button) dvui.Widget {
return self.inner.widget();
}
+/// Borrow the inner widget data; valid until `deinit`.
pub fn data(self: *Button) *dvui.WidgetData {
return self.inner.data();
}
+/// Delegate child placement to the inner widget using dvui layout coordinates.
pub fn rectFor(self: *Button, id: dvui.Id, min_size: dvui.Size, e: dvui.Options.Expand, g: dvui.Options.Gravity) dvui.Rect {
return self.inner.rectFor(id, min_size, e, g);
}
+/// Convert a local rectangle to physical screen coordinates using the inner widget.
pub fn screenRectScale(self: *Button, rect: dvui.Rect) dvui.RectScale {
return self.inner.screenRectScale(rect);
}
+/// Report a child’s minimum size to the inner widget’s layout.
pub fn minSizeForChild(self: *Button, size: dvui.Size) void {
self.inner.minSizeForChild(size);
}
+/// Finish layout and restore the parent; releases a dvui-allocated widget if applicable.
pub fn deinit(self: *Button) void {
- defer if (dvui.widgetIsAllocated(self)) dvui.widgetFree(self);
self.inner.deinit();
}
From 52ec9e9c5f44c2eafc6e3e31b6f66da32eef38bd Mon Sep 17 00:00:00 2001
From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 19:31:23 -0500
Subject: [PATCH 3/4] core: touch-button (codex)
Agent-Team: linux
Worker: codex
---
core/widgets/Button.zig | 51 ++++++++++++++++++++++++++++++-----------
1 file changed, 38 insertions(+), 13 deletions(-)
diff --git a/core/widgets/Button.zig b/core/widgets/Button.zig
index e1d3ffbe9..04606bc16 100644
--- a/core/widgets/Button.zig
+++ b/core/widgets/Button.zig
@@ -4,10 +4,10 @@ const dvui = @import("dvui");
const form_factor = @import("../form_factor.zig");
const Button = @This();
-pub const InitOptions = dvui.ButtonWidget.InitOptions;
-
inner: dvui.ButtonWidget,
+pub const InitOptions = dvui.ButtonWidget.InitOptions;
+
/// Install a button using dvui interaction and layout options; pair with `deinit`.
/// Call during a frame, then process events before drawing its background and children.
pub fn init(self: *Button, src: std.builtin.SourceLocation, init_opts: InitOptions, opts: dvui.Options) void {
@@ -30,10 +30,10 @@ pub fn hitRect(self: *Button) dvui.Rect.Physical {
return rect;
}
-/// Consume this frame’s clicks using the touch hit rectangle; call once before drawing.
+/// Consume this frame's clicks using the touch hit rectangle; call once before drawing.
pub fn processEvents(self: *Button) void {
// TODO(touch-target arbitration): choose the nearest border before consuming overlapping
- // targets; processing order wins today, including over a neighbour’s border. Resolve before
+ // targets; processing order wins today, including over a neighbour's border. Resolve before
// migrating tightly packed toolbars.
self.inner.click = dvui.clicked(self.data(), .{
.rect = self.hitRect(),
@@ -42,12 +42,12 @@ pub fn processEvents(self: *Button) void {
});
}
-/// Test an event against dvui’s widget event routing, without consuming it.
+/// Test an event against dvui's widget event routing, without consuming it.
pub fn matchEvent(self: *Button, e: *dvui.Event) bool {
return self.inner.matchEvent(e);
}
-/// Update hover state using dvui’s border rectangle.
+/// Update hover state using dvui's border rectangle.
pub fn processHover(self: *Button) void {
self.inner.processHover();
}
@@ -57,7 +57,7 @@ pub fn drawBackground(self: *Button) void {
self.inner.drawBackground();
}
-/// Draw the keyboard focus indicator after drawing the button’s children.
+/// Draw the keyboard focus indicator after drawing the button's children.
pub fn drawFocus(self: *Button) void {
self.inner.drawFocus();
}
@@ -72,7 +72,7 @@ pub fn focused(self: *Button) bool {
return self.inner.focused();
}
-/// Whether this frame’s event processing marked the button hovered.
+/// Whether this frame's event processing marked the button hovered.
pub fn hovered(self: *Button) bool {
return self.inner.hovered();
}
@@ -107,7 +107,7 @@ pub fn screenRectScale(self: *Button, rect: dvui.Rect) dvui.RectScale {
return self.inner.screenRectScale(rect);
}
-/// Report a child’s minimum size to the inner widget’s layout.
+/// Report a child's minimum size to the inner widget's layout.
pub fn minSizeForChild(self: *Button, size: dvui.Size) void {
self.inner.minSizeForChild(size);
}
@@ -118,10 +118,6 @@ pub fn deinit(self: *Button) void {
}
test "touch clicks six points outside a 32 point border without changing its drawn rect" {
- const widgets = @import("../widgets.zig");
- _ = &widgets.button;
- _ = &widgets.buttonIcon;
- _ = &widgets.buttonLabelAndIcon;
const Frame = struct {
// The test callback writes these on the testing window's thread.
var border: dvui.Rect.Physical = .{};
@@ -167,3 +163,32 @@ test "touch clicks six points outside a 32 point border without changing its dra
try std.testing.expect(!Frame.click);
try std.testing.expectEqual(original, Frame.border);
}
+
+test "button verbs draw and accept a touch outside their borders" {
+ const widgets = @import("../widgets.zig");
+ const Frame = struct {
+ // The draw callback writes the click results on the testing window's thread.
+ var clicks: [3]bool = .{ false, false, false };
+
+ fn draw() !dvui.App.Result {
+ const opts: dvui.Options = .{ .rect = .{ .x = 50, .y = 50, .w = 32, .h = 32 }, .margin = dvui.Rect.all(0) };
+ clicks[0] = widgets.button(@src(), "Label", .{}, opts);
+ clicks[1] = widgets.buttonIcon(@src(), "Icon", dvui.entypo.cross, .{}, .{}, opts.override(.{ .rect = .{ .x = 50, .y = 100, .w = 32, .h = 32 } }));
+ clicks[2] = widgets.buttonLabelAndIcon(@src(), .{ .label = "Both", .tvg_bytes = dvui.entypo.cross }, opts.override(.{ .rect = .{ .x = 50, .y = 150, .w = 32, .h = 32 } }));
+ return .ok;
+ }
+ };
+ var t = try dvui.testing.init(.{ .window_size = .{ .w = 200, .h = 200 } });
+ defer t.deinit();
+ try dvui.testing.settle(Frame.draw);
+ const cw = dvui.currentWindow();
+ for (0..3) |i| {
+ // Six points left of each border, at its vertical centre.
+ const norm: dvui.Point = .{ .x = 44.0 / 200.0, .y = (66.0 + 50.0 * @as(f32, @floatFromInt(i))) / 200.0 };
+ _ = try cw.addEventPointer(.{ .button = .touch0, .action = .press, .xynorm = norm });
+ _ = try dvui.testing.step(Frame.draw);
+ _ = try cw.addEventPointer(.{ .button = .touch0, .action = .release, .xynorm = norm });
+ _ = try dvui.testing.step(Frame.draw);
+ for (Frame.clicks, 0..) |activated, j| try std.testing.expectEqual(i == j, activated);
+ }
+}
From 2d08bfc3b4a9342b0eba4fb377267bf02c133090 Mon Sep 17 00:00:00 2001
From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com>
Date: Thu, 29 Oct 2026 06:21:28 -0500
Subject: [PATCH 4/4] core: touch-button (codex)
Agent-Team: linux
Worker: codex
---
android/AGENTS.md | 17 +
android/AndroidManifest.xml | 20 +
android/CLAUDE.md | 1 +
android/README.md | 56 +++
android/src/FizzyActivity.java | 9 +
android/src/main.zig | 77 ++++
app/ActivityFeed.zig | 2 +-
app/App.zig | 112 ++---
app/AppInfo.zig | 6 +-
app/HistoryTransport.zig | 8 +-
app/Liquid.zig | 6 +-
app/Recents.zig | 6 +-
app/RecentsMigration.zig | 4 +-
app/Secrets.zig | 12 +-
app/automation/Calm.zig | 12 +-
app/automation/Service.zig | 6 +-
app/automation/overlay.zig | 52 +--
app/control/json.zig | 6 +-
app/control/openapi.zig | 2 +-
app/control/routes.zig | 2 +-
app/control/screen_diff.zig | 4 +-
app/crash/crash.zig | 2 +-
app/dome.zig | 14 +-
app/dome_rules.zig | 2 +-
app/keymap/Keymap.zig | 38 +-
app/keymap/dvui_adapter.zig | 4 +-
app/keymap/zon.zig | 8 +-
app/layout/Chooser.zig | 46 +--
app/layout/Drop.zig | 14 +-
app/layout/Floats.zig | 98 ++---
app/layout/Layout.zig | 212 +++++-----
app/layout/Picker.zig | 48 +--
app/layout/Rail.zig | 18 +-
app/layout/Region.zig | 178 ++++----
app/layout/Seed.zig | 4 +-
app/layout/Shape.zig | 8 +-
app/layout/SidePane.zig | 8 +-
app/layout/SplitTree.zig | 8 +-
app/layout/State.zig | 74 ++--
app/layout/Tree.zig | 2 +-
app/layout/ViewDrag.zig | 364 ++++++++--------
app/layout/float_rules.zig | 22 +-
app/layout/place_commands.zig | 2 +-
app/mcp_names.zig | 6 +-
app/profile.zig | 16 +-
app/remote/RemoteViewHost.zig | 2 +-
app/restart.zig | 16 +-
app/root.zig | 32 +-
app/settings/PluginSettingsPane.zig | 14 +-
app/settings/SettingRow.zig | 16 +-
app/settings/Settings.zig | 43 +-
app/settings/SettingsMigration.zig | 61 ++-
app/settings/SettingsPluginsZon.zig | 60 +--
app/single_instance/singleton.zig | 2 +-
app/single_instance/singleton_native.zig | 20 +-
app/single_instance/singleton_web.zig | 2 +-
app/store/PluginLoader.zig | 50 +--
app/store/PluginLoader_web.zig | 14 +-
app/store/PluginManager.zig | 20 +-
app/store/PluginPage.zig | 24 +-
app/store/PluginStore.zig | 396 +++++++++---------
app/store/plugin_repo_asset.zig | 6 +-
app/store/readme.zig | 12 +-
app/store/registry/compat.zig | 2 +-
app/store/registry/registry.zig | 30 +-
app/store/registry/store.zig | 32 +-
app/store/store_icon.zig | 6 +-
app/store/web_fetch.zig | 4 +-
app/update/auto_update.zig | 10 +-
app/update/update_install.zig | 26 +-
app/update/update_notify.zig | 30 +-
app/view/ViewHost.zig | 4 +-
app/watch/ExecutableWatcher.zig | 6 +-
app/watch/FolderWatcher.zig | 24 +-
app/watch/SettingsWatcher.zig | 28 +-
app/watch/folder_events.zig | 22 +-
app/watch/wake.zig | 4 +-
build.zig | 3 +
build/android.zig | 113 +++++
build/app.zig | 26 +-
core/anim.zig | 1 +
core/core.zig | 3 +
core/person.zig | 68 +++
core/rearrange.zig | 503 +++++++++++++++++++++++
core/widgets/ScrollArea.zig | 7 +-
plugins/dashboard/build.zig.zon | 2 +-
scripts/lint-allow.zon | 1 +
scripts/lint.zig | 11 +-
scripts/test-web-text-input.sh | 3 +-
sdk/src/Command.zig | 7 +-
sdk/src/EditorAPI.zig | 32 +-
sdk/src/Host.zig | 305 +++++++++++---
sdk/src/Manifest.zig | 14 +-
sdk/src/Plugin.zig | 145 +++++--
sdk/src/RegionSpec.zig | 8 +-
sdk/src/Step.zig | 3 +-
sdk/src/dylib.zig | 10 +-
sdk/src/events.zig | 22 +-
sdk/src/language.zig | 74 +++-
sdk/src/menu.zig | 2 +-
sdk/src/render_bridge.zig | 5 +-
sdk/src/sdk.zig | 6 +-
sdk/src/services/automation.zig | 3 +-
sdk/src/services/files.zig | 7 +-
sdk/src/services/markdown.zig | 14 +-
sdk/src/services/plugins.zig | 7 +-
sdk/src/settings.zig | 15 +-
sdk/tape/binary.zig | 14 +-
src/editor/Editor.zig | 87 +++-
src/editor/demos/tour.zig | 2 +-
src/editor/explorer/settings.zig | 19 +-
tests/integration.zig | 182 ++++++++
web/fizzy-worker.js | 15 +-
web/fizzy.js | 110 +++--
web/tests/embed.test.mjs | 38 +-
web/tests/wheel-input.test.mjs | 118 ++++++
116 files changed, 3226 insertions(+), 1413 deletions(-)
create mode 100644 android/AGENTS.md
create mode 100644 android/AndroidManifest.xml
create mode 100644 android/CLAUDE.md
create mode 100644 android/README.md
create mode 100644 android/src/FizzyActivity.java
create mode 100644 android/src/main.zig
create mode 100644 build/android.zig
create mode 100644 core/person.zig
create mode 100644 core/rearrange.zig
create mode 100644 web/tests/wheel-input.test.mjs
diff --git a/android/AGENTS.md b/android/AGENTS.md
new file mode 100644
index 000000000..67fa3e90b
--- /dev/null
+++ b/android/AGENTS.md
@@ -0,0 +1,17 @@
+# android/: the Android hello
+
+The host shell of an APK (`AndroidManifest.xml`, `src/FizzyActivity.java`) and a hello
+(`src/main.zig`: dvui on SDL3, a tap changes what is drawn). The build is `build/android.zig`
+(`zig build apk -Dandroid-sdk=
`); `README.md` has the steps and the emulator run. The hello
+is not an app entry: fizzy on Android is `build/exe.zig` with an Android target.
+
+- **SDL needs its Java `SDLActivity`.** SDL3 has no NativeActivity mode, so "no Java at all" would
+ mean a second platform layer beside SDL (input, surface, IME over JNI). Java stays until the
+ Zig windowing library (plans#51) grows an Android backend.
+- **Zig cannot link the library:** it ships no Android libc, so the NDK's clang does, with the
+ hello and SDL archives whole (SDL finds its JNI entry points and `SDL_main` by name).
+- **minSdk is 24:** `std.Io` calls `preadv64`/`pwritev64`, which bionic has from API 24.
+- **Use a Zig global cache of its own** (`ZIG_GLOBAL_CACHE_DIR`) when the shared one holds dvui's
+ other lazy dependencies: Zig unpacks them into `zig-pkg`, and cairo/expat then fail to compile.
+- **Nothing in CI builds this yet.** `zig build apk` needs the NDK and SDK; a compile-only step
+ is for when CI has them.
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
new file mode 100644
index 000000000..f588abfce
--- /dev/null
+++ b/android/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/CLAUDE.md b/android/CLAUDE.md
new file mode 100644
index 000000000..43c994c2d
--- /dev/null
+++ b/android/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
diff --git a/android/README.md b/android/README.md
new file mode 100644
index 000000000..1c7f19749
--- /dev/null
+++ b/android/README.md
@@ -0,0 +1,56 @@
+# Android hello
+
+An SDL3 window with dvui drawing, packaged as an APK: the first step of fizzyedit/plans#60. A tap
+on the button changes what is drawn. Plugins are compiled in (there are no plugin libraries on
+mobile); this hello draws no plugin yet, and it is dvui's own SDL3 backend, not fizzy's.
+
+## Build
+
+`zig build apk` (`build/android.zig`, in the root build graph) cross-compiles `android/src/main.zig`, dvui and SDL3, links them with the NDK's clang into
+`libfizzy_hello.so`, and packages `zig-out/fizzy-hello.apk` with the Android SDK's tools (`javac`,
+`d8`, `aapt2`, `zipalign`, `apksigner`). There is no Gradle project: SDL's Java side is compiled
+from the same SDL source the native side is built from, with the 9-line `src/FizzyActivity.java`.
+
+You need a JDK 17 or newer on `PATH` and the Android SDK (platform 35, build-tools 35.0.0, NDK
+27.2.12479018) on a Linux x86_64 host. A debug keystore is made once with
+`keytool -genkey -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
+-keypass android -keyalg RSA -validity 10000 -dname CN=debug`.
+
+```sh
+export ANDROID_HOME=$HOME/android/sdk
+zig build apk -Dandroid-sdk=$ANDROID_HOME -Dandroid-arch=x86_64 # the emulator on an x86_64 host
+zig build apk -Dandroid-sdk=$ANDROID_HOME # a phone (aarch64, the default)
+```
+
+dvui's `sdl3` dependency is lazy: the first `zig build apk` configures, fetches it and configures
+again. Other options: `-Dandroid-ndk`, `-Dandroid-build-tools`, `-Dandroid-platform`, `-Dandroid-keystore`. The app id is `dev.fizzy.hello` (`build/bundle_id.zig`).
+
+## Run in the emulator (pop-os)
+
+The x86_64 emulator needs hardware acceleration, so `/dev/kvm` must be writable by you:
+`sudo usermod -aG kvm $USER`, then log in again.
+
+```sh
+SDK=$ANDROID_HOME
+$SDK/cmdline-tools/latest/bin/sdkmanager "system-images;android-35;google_apis;x86_64" "emulator"
+$SDK/cmdline-tools/latest/bin/avdmanager create avd -n fizzy -k "system-images;android-35;google_apis;x86_64" -d pixel_6
+$SDK/emulator/emulator -avd fizzy -no-snapshot & # add -no-window -gpu swiftshader_indirect headless
+$SDK/platform-tools/adb install -r zig-out/fizzy-hello.apk
+$SDK/platform-tools/adb shell am start -n dev.fizzy.hello/dev.fizzyedit.hello.FizzyActivity
+$SDK/platform-tools/adb shell input tap 540 1200 # the tap button
+$SDK/platform-tools/adb exec-out screencap -p > hello.png
+```
+
+## Pitfalls
+
+See `AGENTS.md`: the Zig cache, minSdk 24, linking with the NDK's clang, and why Java stays
+(SDL3 runs only under its `SDLActivity`; a NativeActivity would be a second platform layer).
+
+## What is not here yet
+
+This hello is dvui on dvui's own SDL3 backend and its lazy `sdl3` package: a second SDL pin beside
+`backend/build.zig.zon`'s, kept as a stated, temporary exception. Fizzy's backend (`backend/`: its
+GPU renderer, glass, OS windows) has no Android target in its build yet, and linking the hello
+against it, with SDL's Java sources from the same pinned `sdl_source`, was not attempted here. The
+editor is not built for Android either, and `app/store/registry/compat.zig`'s `hostKey()` must map
+Android to `android-aarch64` before any Android build downloads a plugin.
diff --git a/android/src/FizzyActivity.java b/android/src/FizzyActivity.java
new file mode 100644
index 000000000..7a7c80546
--- /dev/null
+++ b/android/src/FizzyActivity.java
@@ -0,0 +1,9 @@
+package dev.fizzyedit.hello;
+
+import org.libsdl.app.SDLActivity;
+
+public class FizzyActivity extends SDLActivity {
+ protected String[] getLibraries() {
+ return new String[] { "fizzy_hello" }; // SDL3 is statically linked into it
+ }
+}
diff --git a/android/src/main.zig b/android/src/main.zig
new file mode 100644
index 000000000..d2f0888a0
--- /dev/null
+++ b/android/src/main.zig
@@ -0,0 +1,77 @@
+//! The Android hello: a dvui App on the SDL3 backend. A tap on the button changes what is drawn.
+const std = @import("std");
+const builtin = @import("builtin");
+const dvui = @import("dvui");
+
+pub const dvui_app: dvui.App = .{
+ .config = .{
+ .options = .{
+ .size = .{ .w = 800.0, .h = 600.0 },
+ .min_size = .{ .w = 250.0, .h = 350.0 },
+ .title = "Fizzy",
+ },
+ },
+ .frameFn = appFrame,
+};
+pub const main = dvui.App.main;
+
+// SDL's activity looks this symbol up in the library and calls it on its own thread. This is a
+// static lib linked into a JNI library, so zig's start.zig never runs and never builds the
+// std.process.Init dvui.App.main needs; hand-build the same minimal Init here.
+export fn SDL_main(argc: c_int, argv: [*][*:0]u8) callconv(.c) c_int {
+ return runDvuiMain(argc, argv) catch |err| {
+ std.log.err("SDL_main failed: {t}", .{err});
+ return 1;
+ };
+}
+
+extern "c" var environ: ?[*:null]?[*:0]u8;
+
+fn runDvuiMain(argc: c_int, argv: [*][*:0]u8) !u8 {
+ const gpa = std.heap.c_allocator;
+
+ var arena_allocator: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
+ defer arena_allocator.deinit();
+
+ const args_vector: std.process.Args.Vector = argv[0..@intCast(argc)];
+ const no_environ: [0:null]?[*:0]u8 = .{};
+ const environ_block: std.process.Environ.Block = .{ .slice = std.mem.span(environ orelse &no_environ) };
+
+ var threaded: std.Io.Threaded = .init(gpa, .{
+ .argv0 = .init(.{ .vector = args_vector }),
+ .environ = .{ .block = environ_block },
+ });
+ defer threaded.deinit();
+
+ var environ_map = try std.process.Environ.createMap(.{ .block = environ_block }, gpa);
+ defer environ_map.deinit();
+
+ const preopens = try std.process.Preopens.init(arena_allocator.allocator());
+
+ return dvui.App.main(.{
+ .minimal = .{ .args = .{ .vector = args_vector }, .environ = .{ .block = environ_block } },
+ .arena = &arena_allocator,
+ .gpa = gpa,
+ .io = threaded.io(),
+ .environ_map = &environ_map,
+ .preopens = preopens,
+ });
+}
+pub const panic = dvui.App.panic;
+pub const std_options: std.Options = .{
+ .logFn = dvui.App.logFn,
+};
+
+var taps: u32 = 0;
+
+fn appFrame() !dvui.App.Result {
+ var root = dvui.box(@src(), .{}, .{ .expand = .both, .style = .window, .background = true });
+ defer root.deinit();
+
+ dvui.label(@src(), "Fizzy on Android", .{}, .{ .font = .theme(.title), .gravity_x = 0.5 });
+ dvui.label(@src(), "{s} on {s}", .{ @tagName(dvui.backend.kind), @tagName(builtin.cpu.arch) }, .{ .gravity_x = 0.5 });
+ dvui.label(@src(), "taps: {d}", .{taps}, .{ .gravity_x = 0.5, .tag = "tap-count" });
+
+ if (dvui.button(@src(), "Tap me", .{}, .{ .gravity_x = 0.5, .min_size_content = .{ .w = 240, .h = 120 }, .tag = "tap-button" })) taps += 1;
+ return .ok;
+}
diff --git a/app/ActivityFeed.zig b/app/ActivityFeed.zig
index a92302d00..6a76c777f 100644
--- a/app/ActivityFeed.zig
+++ b/app/ActivityFeed.zig
@@ -50,7 +50,7 @@ const Action = struct {
pub const Act = enum {
/// Redo step `tag` ("Undid Claude's Draw").
redo,
- /// Go back to before the last run of author `tag` ("Claude · Draw").
+ /// Go back to before the last run of author `tag` ("Claude . Draw").
go_back,
/// The same, told that it also takes back some of the person's own steps.
go_back_anyway,
diff --git a/app/App.zig b/app/App.zig
index 5565f6a8d..993da7309 100644
--- a/app/App.zig
+++ b/app/App.zig
@@ -2,7 +2,7 @@
//!
//! Plugins, their lifecycle and file-type ownership; the open documents and the save/close
//! bookkeeping; settings, recents and the keymap as the user configured them; the layout
-//! state and the watchers. Fizzy's `Editor` embeds one and adds what is fizzy's own — its
+//! state and the watchers. Fizzy's `Editor` embeds one and adds what is fizzy's own - its
//! shape, its menus, its commands and settings groups. The methods still live on `Editor`
//! and move here section by section; the fields moved first so every caller already names
//! the right owner.
@@ -36,7 +36,7 @@ pub const FileStamp = struct {
};
/// Three-way form of `readPluginEnabled`: `.unset` (no `.enabled` field on record at all) is
-/// what separates a freshly dropped-in plugin from one the user deliberately switched off — both
+/// what separates a freshly dropped-in plugin from one the user deliberately switched off - both
/// are "not enabled", but only the first is an undecided offer (see `undecided_plugin_ids`).
pub const PluginEnabledState = enum { unset, enabled, disabled };
@@ -59,11 +59,11 @@ gpa: std.mem.Allocator,
layout: Layout.State = .{},
/// The per-frame `Layout`, live only while the shape is drawing. A plugin declaring a region
-/// through `Host.region` needs it, and it is a local in the draw loop by design — nothing about a
+/// through `Host.region` needs it, and it is a local in the draw loop by design - nothing about a
/// layout should outlive the frame that drew it.
frame_layout: ?*Layout = null,
-/// Small per-frame allocations — path joins, null terminations, labels. Never freed one by
+/// Small per-frame allocations - path joins, null terminations, labels. Never freed one by
/// one; reset (`retain_capacity`) every frame.
arena: std.heap.ArenaAllocator,
@@ -94,7 +94,7 @@ plugin_data_refused: std.ArrayList([]u8) = .empty,
/// Plugin registry + service locator exposed to plugins
host: Host,
-/// The ids this executable links in statically, as its build listed them — see
+/// The ids this executable links in statically, as its build listed them - see
/// `build/sdk.zig`'s `bundledPluginsModule`. Borrowed comptime strings; the runtime fills this
/// in when it registers them.
///
@@ -107,7 +107,7 @@ bundled_plugin_ids: []const []const u8 = &.{},
files_service: sdk.services.files.Api = undefined,
/// The project's file set, shared with every plugin through `host.files`. The app owns it
-/// because more than one plugin reads it — see `core.FileTable`. Its `env` is wired in
+/// because more than one plugin reads it - see `core.FileTable`. Its `env` is wired in
/// `postInit`, since the ignore rules and the watcher it asks about live on this same `Editor`
/// and so need its final address.
file_table: core.FileTable,
@@ -119,12 +119,12 @@ loaded_plugin_libs: std.ArrayList(PluginLoader.LoadedLib) = .empty,
/// Runtime bookkeeping of user-plugin ids that are present on disk but not loaded
/// (explicitly disabled, or freshly dropped into `plugins/` with no `.enabled = true`).
-/// Persistence lives per-plugin as `.plugins..enabled` in `settings.zon` (R12) — this
+/// Persistence lives per-plugin as `.plugins..enabled` in `settings.zon` (R12) - this
/// list is only the UI/skip-load set for the current session. Freed in `deinit`.
disabled_plugin_ids: std.ArrayList([]const u8) = .empty,
/// Subset of `disabled_plugin_ids`: on disk with **no `.plugins..enabled` field on record at
-/// all** — i.e. a build the user dropped in (or `zig build install`ed from a plugin repo) that
+/// all** - i.e. a build the user dropped in (or `zig build install`ed from a plugin repo) that
/// fizzy has never been told to run. Distinct from a plugin the user deliberately switched off,
/// which carries an explicit `.enabled = false`: that one is a settled decision and must stay
/// quiet, while this one is an undecided offer and gets the store's "Load" button plus the
@@ -135,29 +135,29 @@ undecided_plugin_ids: std.ArrayList([]const u8) = .empty,
/// Runtime bookkeeping of user-plugin ids the user opted **out** of store updates for
/// (`.plugins..auto_update = false` in `settings.zon`). Stored as the opt-out set rather than
-/// the opt-in one because auto-update defaults to *on* — an absent entry is the overwhelmingly
+/// the opt-in one because auto-update defaults to *on* - an absent entry is the overwhelmingly
/// common case and must not need a list membership to be right. Whether an update the pass finds
/// is applied silently or offered in the update window is `Settings.plugin_update_mode`, one
/// app-wide choice. Seeded by `seedPluginFlags`; freed in `deinit`.
auto_update_off_ids: std.ArrayList([]const u8) = .empty,
-/// Fizzy-only pending `.plugins.` reserved-field writes (id → changed fields), drained by
+/// Fizzy-only pending `.plugins.` reserved-field writes (id -> changed fields), drained by
/// `writeMergedSettings` alongside `host.plugin_settings_pending`. Never touched by a
-/// plugin itself — only by `setPluginEnabled` / `setPluginAutoUpdate` / store install. Keys are
+/// plugin itself - only by `setPluginEnabled` / `setPluginAutoUpdate` / store install. Keys are
/// app-allocator-owned.
plugin_flags_pending: std.array_hash_map.String(PendingPluginFlags) = .empty,
-/// Fizzy-only pending `.plugins..extensions` writes (id → the plugin's complete new list).
+/// Fizzy-only pending `.plugins..extensions` writes (id -> the plugin's complete new list).
/// A separate map from `plugin_flags_pending` because the value is owned, variable-length data
/// rather than two bools: both the key and every extension string are app-allocator-owned and
/// freed when `writeMergedSettings` drains it. Written only by `resolveExtensionConflict`.
plugin_extensions_pending: std.array_hash_map.String([]const []const u8) = .empty,
-/// In-memory `ext → owning plugin id` map, rebuilt by `rebuildExtensionOwnerCache` from the
+/// In-memory `ext -> owning plugin id` map, rebuilt by `rebuildExtensionOwnerCache` from the
/// persisted per-plugin `.extensions` lists of the *currently loaded* plugins. Backs
/// `EditorAPI.extensionOwnerOverride`, i.e. step 1 of `Host.pluginForExtension`.
///
-/// **Every string here is duped and owned by the Editor** — never a slice borrowed from
+/// **Every string here is duped and owned by the Editor** - never a slice borrowed from
/// `plugin.id`, which lives inside a dylib image that `dlclose` can unmap out from under us
/// (see `Host.unregisterPlugin`'s ordering note).
extension_owner: std.array_hash_map.String([]const u8) = .empty,
@@ -167,9 +167,9 @@ extension_owner: std.array_hash_map.String([]const u8) = .empty,
/// Shortcuts. Owned duped strings, freed on the next rebuild.
extension_conflicts: std.ArrayList(ExtensionConflict) = .empty,
-/// Snapshot of dvui's *built-in* keybinds (`char_left`, `copy`, `next_widget`, …), taken in
+/// Snapshot of dvui's *built-in* keybinds (`char_left`, `copy`, `next_widget`, ...), taken in
/// `init` before fizzy adds its own. `Window.init` installs these once and never again,
-/// so `rebuildKeybinds` — which wipes the map to drop an unloaded plugin's binds — must put
+/// so `rebuildKeybinds` - which wipes the map to drop an unloaded plugin's binds - must put
/// them back or every widget's caret/clipboard handling silently dies after the first plugin
/// load or unload. Keys are dvui's own static literals; only the map itself is owned here.
dvui_default_keybinds: std.StringHashMapUnmanaged(dvui.enums.Keybind) = .empty,
@@ -179,12 +179,12 @@ dvui_default_keybinds: std.StringHashMapUnmanaged(dvui.enums.Keybind) = .empty,
/// the image their strings live in.
keymap: Keymap = .{},
-/// Parsed `keybinds.zon`. Held because `keymap` borrows its command-id and owner-id strings —
+/// Parsed `keybinds.zon`. Held because `keymap` borrows its command-id and owner-id strings -
/// it must outlive the keymap and is replaced wholesale on every rebuild.
keybinds_overrides: ?Keymap.zon.File = null,
/// `Keymap.conflicts()` for the current keymap, worked out when first asked for (fizzy's
-/// `Keybinds.conflicts`) — owned, dropped on the next rebuild. Null: not asked for since.
+/// `Keybinds.conflicts`) - owned, dropped on the next rebuild. Null: not asked for since.
keybind_conflicts: ?[]Keymap.Conflict = null,
/// User plugins that failed to load this session, so the UI can tell the author what
@@ -209,7 +209,7 @@ folder: ?[]const u8 = null,
/// Folder strings unlinked from `folder` but possibly still borrowed by the frame in progress.
///
/// `EditorAPI.folder` hands plugins the pointer itself, and a plugin can close or switch the
-/// project from *inside* its own draw — the file tree's project row does exactly that from its
+/// project from *inside* its own draw - the file tree's project row does exactly that from its
/// context menu, then keeps drawing with the `path` it read at the top of the function. Freeing
/// there is a use-after-free in every caller still holding the slice, so the old string is
/// parked here and released at the top of the next frame instead (the same shape as the
@@ -236,14 +236,14 @@ pending_save_as_path: ?[]u8 = null,
/// After Save As from "Save and Close", close this file id once save completes.
pending_close_file_id: ?u64 = null,
-/// Files whose async save was kicked off by "Save and Close" (single-doc) — once
+/// Files whose async save was kicked off by "Save and Close" (single-doc) - once
/// `File.isSaving()` clears, `tickPendingSaveCloses` closes the file. Set is fine
/// because at most one entry per file (saveAsync no-ops while already saving).
pending_close_after_save: std.AutoArrayHashMapUnmanaged(u64, void) = .empty,
/// "Save all and quit" queue. Walked by `advanceSaveAllQuit`: items move from this
/// queue into `quit_saves_in_flight` when their save kicks off, then drop out when
-/// their save completes and the file closes. Non-empty (or in-flight non-empty) ⇒
+/// their save completes and the file closes. Non-empty (or in-flight non-empty) =>
/// save-all quit in progress.
quit_save_all_ids: std.ArrayList(u64) = .empty,
@@ -263,7 +263,7 @@ pending_app_close: bool = false,
/// Hash of the last serialized settings.zon text written or captured at startup; avoids
/// redundant writes without keeping a full duplicate copy of the text around. Doubles as the
/// "was that change ours" filter for `settings_watcher` (see R11 in
-/// plans/PLUGIN_MANIFEST_PLAN.md) — a change to disk whose hash matches this is our own last
+/// plans/PLUGIN_MANIFEST_PLAN.md) - a change to disk whose hash matches this is our own last
/// write, not an external edit worth reconciling.
settings_last_saved_hash: ?u64 = null,
@@ -273,11 +273,11 @@ settings_dirty: bool = false,
/// Monotonic deadline (`perf.nanoTimestamp()`): autosave runs when dirty and `now >= deadline`.
settings_save_deadline_ns: i128 = 0,
-/// Watches `/` recursively (via nightwatch — see R12) for external `settings.zon`
+/// Watches `/` recursively (via nightwatch - see R12) for external `settings.zon`
/// changes and newly-created plugin directories, reconciling them live via `tick`. Null on wasm,
-/// on an unsupported OS, or if starting the watch failed — all best-effort: fizzy must never
+/// on an unsupported OS, or if starting the watch failed - all best-effort: fizzy must never
/// fail to launch because the watcher couldn't start, it just silently doesn't get live
-/// reconciliation. Set up in `postInit` (needs `editor` at its final heap address — see
+/// reconciliation. Set up in `postInit` (needs `editor` at its final heap address - see
/// `SettingsWatcher.start`'s doc comment), torn down first in `deinit`.
settings_watcher: ?SettingsWatcher = null,
@@ -294,15 +294,15 @@ control_state: if (builtin.target.cpu.arch == .wasm32) void else ControlState =
executable_watcher: ?ExecutableWatcher = null,
/// Recursive watch on the open root folder, announced to plugins as `fizzy.files.changed`.
-/// Same final-address constraint as the two above — started in `postInit`, retargeted whenever
+/// Same final-address constraint as the two above - started in `postInit`, retargeted whenever
/// the root folder changes.
folder_watcher: ?FolderWatcher = null,
/// One id's pending fizzy-reserved `.plugins.` field writes. A null field means "not
-/// touched this cycle" — `writeMergedSettings` reads that half back off disk instead, so
+/// touched this cycle" - `writeMergedSettings` reads that half back off disk instead, so
/// toggling auto-update can never clobber a concurrent enable/disable or vice versa.
/// One persisted `.extensions` entry fizzy declined to honor, surfaced to the user rather than
-/// silently ignored — the same treatment `Keymap.Conflict` gets in the Keyboard Shortcuts pane.
+/// silently ignored - the same treatment `Keymap.Conflict` gets in the Keyboard Shortcuts pane.
///
/// **Fizzy never produces one of these itself.** `resolveExtensionConflict` is the app's single
/// writer and always strips an extension from every other plugin's list before adding it, so a
@@ -322,7 +322,7 @@ pub const ExtensionConflict = struct {
pub const Kind = enum {
/// Two or more loaded plugins persist the same extension. Alphabetically-first id wins.
duplicate,
- /// The plugin no longer offers `ext` via `fileTypes` — an update dropped support — and
+ /// The plugin no longer offers `ext` via `fileTypes` - an update dropped support - and
/// it is not the fallback editor, so the entry no longer means anything.
stale,
};
@@ -333,7 +333,7 @@ pub const PendingPluginFlags = struct {
/// (see `SettingsPluginsZon.Reserved.enabled`); use `erase` to get back to "no decision".
enabled: ?bool = null,
auto_update: ?bool = null,
- /// Wipe fizzy's decision record for this id — `.enabled` removed entirely and `.extensions`
+ /// Wipe fizzy's decision record for this id - `.enabled` removed entirely and `.extensions`
/// emptied, leaving the plugin's own `.settings` block untouched. Uninstall's flag, and the
/// one thing that makes a later reinstall ask about file types again. Applied before the
/// other fields, so an `.{ .erase = true, .enabled = true }` would still end up enabled.
@@ -343,7 +343,7 @@ pub const PendingPluginFlags = struct {
/// Scan `/plugins/` for user-installed plugin dylibs and load each one.
///
/// Each sub-directory that contains `plugin.` is attempted in iteration order.
-/// Failures are logged and skipped — a bad plugin never prevents the others from loading.
+/// Failures are logged and skipped - a bad plugin never prevents the others from loading.
/// Built-in plugin IDs ("workbench", "text") are never overridden; any
/// user directory whose name collides with an already-registered plugin is skipped.
///
@@ -359,12 +359,12 @@ pub const FailedPlugin = struct {
/// Optional version / SDK detail when the dylib could be opened for probing.
detail: ?[]const u8 = null,
/// The plugin's own declared version, probed straight from the dylib without registering
- /// it. Lets the store show "current version" for a build that is on disk but rejected —
+ /// it. Lets the store show "current version" for a build that is on disk but rejected -
/// null only when the dylib couldn't even be opened for probing.
plugin_version: ?std.SemanticVersion = null,
/// The rejected build's mtime + size when it was recorded, so
/// `reconcileFailedPluginBinaries` can tell "still the same broken file" from "the author
- /// rebuilt it". Both zero when the stat failed, which reads as "never matches" — the safe
+ /// rebuilt it". Both zero when the stat failed, which reads as "never matches" - the safe
/// direction, same as `PluginLoader.LoadedLib`'s stamp.
source_mtime_ns: i128 = 0,
source_size: u64 = 0,
@@ -378,7 +378,7 @@ pub const PendingReveal = struct {
character: u32,
};
-/// True when `id` takes store updates — the default for every plugin, so this answers "not on the
+/// True when `id` takes store updates - the default for every plugin, so this answers "not on the
/// opt-out list". Says nothing about *how* an update is applied: that is
/// `Settings.plugin_update_mode`, which `PluginStore`'s pass reads once for all plugins.
pub fn isPluginAutoUpdate(app: *const App, id: []const u8) bool {
@@ -396,7 +396,7 @@ pub fn isPluginDisabled(app: *App, id: []const u8) bool {
}
/// True when `id` is on disk but fizzy has never been told whether to run it (no
-/// `.plugins..enabled` on record) — see `undecided_plugin_ids`. The store draws these with a
+/// `.plugins..enabled` on record) - see `undecided_plugin_ids`. The store draws these with a
/// "Load" button rather than the settled Enabled checkbox a deliberately-disabled plugin gets.
pub fn isPluginUndecided(app: *const App, id: []const u8) bool {
for (app.undecided_plugin_ids.items) |d| {
@@ -415,7 +415,7 @@ pub fn isValidPluginId(id: []const u8) bool {
return true;
}
-/// Mirror `id`'s auto-update flag into the runtime opt-out list. Runtime bookkeeping only —
+/// Mirror `id`'s auto-update flag into the runtime opt-out list. Runtime bookkeeping only -
/// persistence goes through `setPluginAutoUpdate`.
pub fn trackAutoUpdate(app: *App, id: []const u8, on: bool) !void {
if (on) {
@@ -435,7 +435,7 @@ pub fn trackAutoUpdate(app: *App, id: []const u8, on: bool) !void {
}
/// Add `id` to the runtime disabled bookkeeping list if not already present. Does **not**
-/// write settings — a freshly dropped-in plugin stays off-disk-silent until the user enables it.
+/// write settings - a freshly dropped-in plugin stays off-disk-silent until the user enables it.
pub fn trackDisabledPlugin(app: *App, id: []const u8) !void {
if (app.isPluginDisabled(id)) return;
if (!isValidPluginId(id)) return error.InvalidPluginId;
@@ -464,7 +464,7 @@ pub fn untrackDisabledPlugin(app: *App, id: []const u8) void {
}
}
-/// Drop `id`'s undecided status — called from every path that records an explicit decision
+/// Drop `id`'s undecided status - called from every path that records an explicit decision
/// (`setPluginEnabledPersisted`, either direction) or removes the plugin entirely
/// (`uninstallPlugin`).
pub fn untrackUndecidedPlugin(app: *App, id: []const u8) void {
@@ -476,7 +476,7 @@ pub fn untrackUndecidedPlugin(app: *App, id: []const u8) void {
}
}
-/// How many on-disk plugins are waiting for a load decision — what the sidebar's Plugins badge
+/// How many on-disk plugins are waiting for a load decision - what the sidebar's Plugins badge
/// counts (see `Sidebar.drawOption`). Kept honest by `pruneMissingUndecidedPlugins`.
pub fn undecidedPluginCount(app: *const App) usize {
return app.undecided_plugin_ids.items.len;
@@ -650,7 +650,7 @@ pub fn clearFailedUserPlugin(app: *App, id: []const u8) void {
}
}
-/// `Host.pluginForExtension`, but pretending `skip` is not loaded — what would open `ext` if this
+/// `Host.pluginForExtension`, but pretending `skip` is not loaded - what would open `ext` if this
/// plugin had never arrived. Mirrors that function's resolution order exactly, including its
/// alphabetical tie-break, so the "prior owner" shown in the dialog is the one the user would
/// actually have gotten.
@@ -763,7 +763,7 @@ pub fn abortSaveAllQuit(app: *App) void {
app.pending_quit_continue = false;
}
-/// Move the current folder string out of `folder` without freeing it — see `folder_retired`.
+/// Move the current folder string out of `folder` without freeing it - see `folder_retired`.
pub fn retireFolder(app: *App) void {
const folder = app.folder orelse return;
app.folder = null;
@@ -1018,7 +1018,7 @@ pub fn recordCaseClash(app: *App, id: []const u8, other: []const u8, path: ?[]co
app.recordPluginFailure(id, why, null, if (probe) |info| info.plugin_version else null, if (path) |p| .of(p) else .{});
}
-/// `/plugins//.` — where a user plugin's build lives. Caller frees.
+/// `/plugins//.` - where a user plugin's build lives. Caller frees.
pub fn userPluginPath(gpa: std.mem.Allocator, app: *App, id: []const u8) ![]u8 {
const file_name = try PluginLoader.pluginFilename(id, gpa);
defer gpa.free(file_name);
@@ -1047,7 +1047,7 @@ pub fn pluginHasDirtyDocs(app: *App, plugin: *sdk.Plugin) bool {
}
/// Open documents of extension `ext` whose owner is no longer the plugin that would open `ext`
-/// today — i.e. documents left behind by a reassignment. Reassigning never closes anything, so
+/// today - i.e. documents left behind by a reassignment. Reassigning never closes anything, so
/// these keep working under their original owner; this is what lets the File Types table offer
/// to reopen them. Returned ids are stable across the call; the slice is caller-owned.
pub fn staleOpenDocsForExtension(app: *App, gpa: std.mem.Allocator, ext: []const u8) ![]u64 {
@@ -1063,7 +1063,7 @@ pub fn staleOpenDocsForExtension(app: *App, gpa: std.mem.Allocator, ext: []const
return out.toOwnedSlice(gpa);
}
-/// Reads `.plugins..auto_update` from already-loaded `settings_data`. **Defaults to true** —
+/// Reads `.plugins..auto_update` from already-loaded `settings_data`. **Defaults to true** -
/// a missing `.plugins`, a missing id, or an omitted field all mean "keep this plugin current";
/// only an explicit `false` opts out. Deliberately the inverse of `readPluginEnabled`'s default:
/// a plugin has to be turned on by hand, but once it is on it tracks the store unless told
@@ -1074,7 +1074,7 @@ pub fn readPluginAutoUpdate(gpa: std.mem.Allocator, settings_data: ?[:0]const u8
return !std.mem.eql(u8, std.mem.trim(u8, text, " \t\r\n"), "false");
}
-/// Reads `.plugins..enabled` from already-loaded `settings_data` (null source → false).
+/// Reads `.plugins..enabled` from already-loaded `settings_data` (null source -> false).
/// Missing `.plugins` / missing id / missing or non-`true` `.enabled` all mean disabled.
pub fn readPluginEnabled(gpa: std.mem.Allocator, settings_data: ?[:0]const u8, id: []const u8) bool {
return readPluginEnabledState(gpa, settings_data, id) == .enabled;
@@ -1087,7 +1087,7 @@ pub fn readPluginEnabledState(gpa: std.mem.Allocator, settings_data: ?[:0]const
}
/// Reads `.plugins..extensions` from already-loaded `settings_data`. Empty (never null) when
-/// absent or unparseable — a hand-edited file must degrade to "no explicit choice", not an error.
+/// absent or unparseable - a hand-edited file must degrade to "no explicit choice", not an error.
/// Caller frees with `SettingsPluginsZon.freeExtensions`.
pub fn readPluginExtensions(gpa: std.mem.Allocator, settings_data: ?[:0]const u8, id: []const u8) []const []const u8 {
const text = readPluginReservedField(gpa, settings_data, id, "extensions") orelse return &.{};
@@ -1128,7 +1128,7 @@ pub fn readPluginSettingsText(gpa: std.mem.Allocator, settings_data: ?[:0]const
/// Wake a frame once `remaining_ns` has elapsed so a debounced save actually fires while the app
/// is otherwise idle. Without this, the very event that dirtied the state (a settings-pane click,
/// a splitter drag release) is typically the *last* one before the app goes back to sleep, so no
-/// frame ever runs to observe the deadline — the write, and everything that hangs off it (the
+/// frame ever runs to observe the deadline - the write, and everything that hangs off it (the
/// open settings.zon tab's reload via `writeMergedSettings`' `notifyPathChanged`), would wait on
/// some unrelated input instead. Same pattern/rationale as `drawSaveToasts`' threshold wakeup.
/// In-frame only; both callers run inside `tick`. `id_extra` keeps the two debounces from
@@ -1219,7 +1219,7 @@ pub fn closeDocumentResources(app: *App, doc: sdk.DocHandle) void {
/// Queue the project close; the teardown runs at the top of the next frame
/// (`applyPendingFolderClose`).
///
-/// Deferred because this is reachable from inside a plugin's draw — the file tree's
+/// Deferred because this is reachable from inside a plugin's draw - the file tree's
/// project-row context menu calls it through `Host.closeProjectFolder` and then goes on to
/// draw the project row, its rows, and its ignore-screened listings using the folder it read
/// before the menu ran. Tearing all of that down underneath the draw that is still using it
@@ -1273,7 +1273,7 @@ pub fn migrateFlatPluginLayout(allocator: std.mem.Allocator, plugins_dir: []cons
}
}
-/// Drop undecided entries whose `plugins//` directory is gone — the author deleted or moved
+/// Drop undecided entries whose `plugins//` directory is gone - the author deleted or moved
/// the build instead of answering the offer. Without this the rail badge would advertise a
/// decision the user can no longer make. Called from `reconcileDiscoveredPlugins`, which the
/// watcher already runs for any change under `/` (a deleted directory very much included).
@@ -1295,14 +1295,14 @@ pub fn pruneMissingUndecidedPlugins(app: *App, plugins_dir: []const u8) void {
/// For every currently-loaded plugin with a registered settings schema, re-reads its
/// `.plugins..settings` blob and applies it if (and only if) it actually changed since the
-/// last time we applied one — `SettingsSchema.last_applied_hash` is what stops a change to *one*
+/// last time we applied one - `SettingsSchema.last_applied_hash` is what stops a change to *one*
/// plugin's settings from spuriously renotifying *every other* loaded plugin just because the
/// whole file's hash moved (see `reconcileExternalSettingsChange`'s hash-gate, which only tells
/// us the file changed, not which plugin's part of it did).
pub fn reconcilePluginSettings(app: *App) void {
for (app.host.settings_schemas.items) |*schema| {
const blob = app.host.loadPluginSettings(schema.owner.id) orelse {
- // Settings section removed (all-defaults) — apply an empty blob so the live value
+ // Settings section removed (all-defaults) - apply an empty blob so the live value
// resets to T's own declared defaults, but only when we previously had something.
if (schema.last_applied_hash == 0) continue;
schema.access.applyBlob(schema.value, schema.owner, ".{}");
@@ -1322,7 +1322,7 @@ pub fn reconcilePluginSettings(app: *App) void {
/// Record a load failure for `id` (probing the on-disk build at `path` for its version/detail),
/// replacing any earlier record for the same id.
///
-/// Every path that loads a user plugin routes its failures here — the startup scan *and* the live
+/// Every path that loads a user plugin routes its failures here - the startup scan *and* the live
/// ones (`loadUserPluginById`, reached from enable, store install, and update), so a failed
/// plugin is always in one of the three lists the store's installed pane is built from
/// (loaded / disabled / failed) rather than vanishing from the UI.
@@ -1335,7 +1335,7 @@ pub fn recordLoadFailure(app: *App, id: []const u8, path: []const u8, err: Plugi
null;
defer if (detail) |d| app.gpa.free(d);
// `recordPluginFailure` appends unconditionally; drop any prior record so repeated attempts
- // (enable → fail → enable → fail) leave one row, not a growing pile of duplicate cards.
+ // (enable -> fail -> enable -> fail) leave one row, not a growing pile of duplicate cards.
app.clearFailedUserPlugin(id);
app.recordPluginFailure(id, reason, detail, if (probe) |info| info.plugin_version else null, .of(path));
}
@@ -1379,7 +1379,7 @@ pub fn recordPluginFailure(
/// `id`'s `.extensions` as they stand: a decision still waiting in `plugin_extensions_pending`
/// over what `settings_data` has on disk. The pending list is the newer answer, and it can wait a
-/// while — on the web a flush only marks settings dirty for the next autosave — so anything that
+/// while - on the web a flush only marks settings dirty for the next autosave - so anything that
/// reads ownership back must read it here, not from the file alone. Caller frees with
/// `SettingsPluginsZon.freeExtensions`.
pub fn pluginExtensions(app: *App, settings_data: ?[:0]const u8, id: []const u8) []const []const u8 {
@@ -1440,7 +1440,7 @@ pub const loads_plugins = if (@hasDecl(build_opts, "dynamic_plugins")) build_opt
/// Whether a bundled plugin should be loaded from `{exe_dir}/plugins//` rather than the
/// copy linked in: never on wasm or in a link-only build, not when the build pinned it static
-/// (`build_opts.static_`), and not when `FIZZY_STATIC_=1` is set — the bisection
+/// (`build_opts.static_`), and not when `FIZZY_STATIC_=1` is set - the bisection
/// switch for dylib loading trouble.
pub fn bundledDylibEnabled(gpa: std.mem.Allocator, comptime id: []const u8) bool {
if (comptime builtin.target.cpu.arch == .wasm32 or !loads_plugins) return false;
@@ -1460,7 +1460,7 @@ pub fn bundledDylibEnabled(gpa: std.mem.Allocator, comptime id: []const u8) bool
}
/// Load `{exe_dir}/plugins//.{ext}` and register it through its dylib entry. `extra`
-/// is the entry's third argument — what a plugin's dylib convention asks the application for,
+/// is the entry's third argument - what a plugin's dylib convention asks the application for,
/// null for most.
pub fn loadBundledDylib(app: *App, exe_dir: []const u8, id: []const u8, extra: ?*anyopaque) !void {
if (comptime builtin.target.cpu.arch == .wasm32) return;
diff --git a/app/AppInfo.zig b/app/AppInfo.zig
index bf6d7ce9f..d17387de3 100644
--- a/app/AppInfo.zig
+++ b/app/AppInfo.zig
@@ -1,5 +1,5 @@
//! Everything that makes a fizzy-based application *this* application rather than fizzy: the
-//! window title, bundle id, config folder, registry URL, theme names, exe name — one record an
+//! window title, bundle id, config folder, registry URL, theme names, exe name - one record an
//! app built on fizzy fills in, instead of literals scattered through the tree.
//!
//! Values arrive from the build (`build_opts`) so the same record is available at comptime for
@@ -9,8 +9,8 @@ const build_opts = @import("build_opts");
const AppInfo = @This();
-/// Short lowercase identifier. The executable name, the Velopack packId, and — unless
-/// `config_dir` overrides it — the on-disk config directory.
+/// Short lowercase identifier. The executable name, the Velopack packId, and - unless
+/// `config_dir` overrides it - the on-disk config directory.
name: []const u8,
/// Human-facing name: window title, About dialog, installer title.
diff --git a/app/HistoryTransport.zig b/app/HistoryTransport.zig
index bf3caa1ad..ccaed2b14 100644
--- a/app/HistoryTransport.zig
+++ b/app/HistoryTransport.zig
@@ -306,7 +306,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
if (opts.corner) |c| {
self.liquid.step(now, c.point());
// Into a vial, the new step splashes the surface, as a drop landing would: after the
- // step, so a step landing after an idle spell — when the liquid starts afresh, still —
+ // step, so a step landing after an idle spell - when the liquid starts afresh, still -
// splashes all the same. Only into a vial: a liquid nothing steps would hold its
// splashes for when a vial came.
if (stepped) self.liquid.splashIn();
@@ -521,7 +521,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
const home: ?dvui.Rect.Physical = if (from) |f| if (opts.corner) |c| c.home(s) else dome.bud(f) else null;
const travel: f32 = if (from) |f| dome.bud(f).h else 0;
// Out of a corner vial, which is whole at rest, the glass is whole throughout: eased toward a
- // dome's quiet presence, it was glass at partial alpha — a hole — and the vial at full alpha
+ // dome's quiet presence, it was glass at partial alpha - a hole - and the vial at full alpha
// the frame it went back in, a flash.
const prev_alpha = dvui.alpha(if (opts.corner != null and from != null) 1 else if (from != null) dome.glassAlpha(self.presence.value, p) else p);
defer dvui.alphaSet(prev_alpha);
@@ -653,8 +653,8 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
return pressed;
}
-/// When each bubble buds out of the grip, as a share of the growing — the line first, then the
-/// nearest the grip outward: export, redo, play, undo, the tab — each on its own moment so none
+/// When each bubble buds out of the grip, as a share of the growing - the line first, then the
+/// nearest the grip outward: export, redo, play, undo, the tab - each on its own moment so none
/// travels stacked on another; and how high each arcs, so their paths fan out. As `phys`: undo,
/// line, redo, export, play; then the tab.
const bud_delays = [6]f32{ 0.3, 0, 0.16, 0.08, 0.23, 0.36 };
diff --git a/app/Liquid.zig b/app/Liquid.zig
index 70c4a9b61..f5b4abbc9 100644
--- a/app/Liquid.zig
+++ b/app/Liquid.zig
@@ -9,8 +9,8 @@
//! moved. A new step splashes it (`splashIn`), an undo or redo knocks it (`knock`), and its level
//! eases to the history's fill (`setLevel`).
//!
-//! **It lets the app sleep.** `settled` says when nothing moves — the surface flat and still, the
-//! level where it is going — and the caller asks for frames only while it is not. A long gap
+//! **It lets the app sleep.** `settled` says when nothing moves - the surface flat and still, the
+//! level where it is going - and the caller asks for frames only while it is not. A long gap
//! between frames (the app slept, or its frames come slower than the liquid can follow) starts it
//! afresh, still, at its level: frames that never come quickly enough never keep it moving.
//!
@@ -39,7 +39,7 @@ count: u32 = 0,
const tension: f32 = 900;
const stiffness: f32 = 30;
const damping: f32 = 2.6;
-/// Pixels of tip per pixel-per-second² of acceleration across, at the vessel's edges.
+/// Pixels of tip per pixel-per-second^2 of acceleration across, at the vessel's edges.
const tip_gain: f32 = 0.00045;
/// How hard a splash pushes the column it lands on down, pixels per second.
const splash: f32 = 90;
diff --git a/app/Recents.zig b/app/Recents.zig
index e1e67a65a..989afc384 100644
--- a/app/Recents.zig
+++ b/app/Recents.zig
@@ -35,7 +35,7 @@ last_save_folder: ?[]const u8 = null,
last_open_folder: ?[]const u8 = null,
/// Oldest first: the last row is the folder opened most recently.
folders: std.array_list.Managed(Folder),
-/// Commands run from the command palette — what it lists as recently used (`RecentCommands`).
+/// Commands run from the command palette - what it lists as recently used (`RecentCommands`).
commands: RecentCommands = .{},
/// The app version whose offer to move to /Applications the user put off (`update.move_offer`);
/// the offer comes back with any other. Owned by the list's allocator.
@@ -73,13 +73,13 @@ pub fn load(allocator: std.mem.Allocator, path: []const u8) !Recents {
var disk_diag: std.zon.parse.Diagnostics = undefined;
if (std.zon.parse.fromSlice(Disk, .{ .gpa = allocator, .arena = disk_arena.allocator(), .source = data, .diagnostics = &disk_diag, .ignore_unknown_fields = true }) catch null) |disk| {
for (disk.folders, 0..) |folder, row| {
- // A folder on a mount (`gdrive://…`) is not the disk's to check: it exists
+ // A folder on a mount (`gdrive://...`) is not the disk's to check: it exists
// whenever its plugin is signed in, which is decided at open time.
const present = core.paths.isMountPath(folder) or core.LocalFs.isDirAbsolute(dvui.io, folder);
if (present) {
const canon = canonicalize(allocator, folder) catch continue;
- // Duplicates collapse onto the *later* row — the list is ordered
+ // Duplicates collapse onto the *later* row - the list is ordered
// oldest-first, so the last spelling of a directory is the recent one.
for (folders.items, 0..) |existing, i| {
if (std.mem.eql(u8, existing.path, canon)) {
diff --git a/app/RecentsMigration.zig b/app/RecentsMigration.zig
index 7d7072dd0..1f4e2e59d 100644
--- a/app/RecentsMigration.zig
+++ b/app/RecentsMigration.zig
@@ -13,7 +13,7 @@ const LegacyRecents = struct {
folders: [][]const u8,
};
-/// On-disk shape of `recents.zon` — mirrors `Recents.Disk`.
+/// On-disk shape of `recents.zon` - mirrors `Recents.Disk`.
const Disk = struct {
last_save_folder: []const u8 = "",
last_open_folder: []const u8 = "",
@@ -22,7 +22,7 @@ const Disk = struct {
/// If `zon_path` doesn't exist yet but its sibling `.json` does, parse the legacy
/// JSON, write an equivalent `zon_path`, and delete the JSON file. No-op (including on
-/// any failure along the way — the caller falls back to defaults as usual) otherwise.
+/// any failure along the way - the caller falls back to defaults as usual) otherwise.
pub fn migrateIfNeeded(allocator: std.mem.Allocator, zon_path: []const u8) void {
// The web never had the JSON file.
if (comptime @import("builtin").target.cpu.arch == .wasm32) return;
diff --git a/app/Secrets.zig b/app/Secrets.zig
index a43116fe1..061911df7 100644
--- a/app/Secrets.zig
+++ b/app/Secrets.zig
@@ -1,12 +1,12 @@
-//! Credentials a plugin holds on the user's behalf — a refresh token, an API key — kept out of
+//! Credentials a plugin holds on the user's behalf - a refresh token, an API key - kept out of
//! `settings.zon`, which is exported, diffed, watched and shown in a pane.
//!
//! The seam is the point: `get`/`set`/`remove` by key, with the backend chosen by the app.
-//! Today that is one backend, `secrets` in the config folder — one `key=base64` line per
+//! Today that is one backend, `secrets` in the config folder - one `key=base64` line per
//! entry, created `0600`, read whole; the OS stores (Keychain, libsecret, DPAPI) slot in
//! behind the same three calls without a plugin noticing. On the web the same file goes where
//! `core.fs` puts the app's other files, the page's storage: private to this origin but not to
-//! the page's scripts, so what a web plugin keeps here should be short-lived — Drive keeps its
+//! the page's scripts, so what a web plugin keeps here should be short-lived - Drive keeps its
//! hour's access token, never a refresh token.
//!
//! Keys are plugin-namespaced by convention (`drive.refresh_token`); values are opaque bytes.
@@ -20,7 +20,7 @@ const Secrets = @This();
gpa: std.mem.Allocator,
io: std.Io,
-/// `/secrets` — on the web, the page-storage key `core.fs` files it under.
+/// `/secrets` - on the web, the page-storage key `core.fs` files it under.
path: []const u8,
values: std.array_hash_map.String([]u8) = .empty,
loaded: bool = false,
@@ -55,7 +55,7 @@ pub fn get(self: *Secrets, key: []const u8) ?[]const u8 {
///
/// Once the map holds `v` it owns it, so nothing here may free it on a later failure: an
/// `errdefer` that outlived the `put` left the map pointing at freed memory, and the next `get`
-/// handed that out — on the web, where `save` used to always fail, every single time.
+/// handed that out - on the web, where `save` used to always fail, every single time.
pub fn set(self: *Secrets, key: []const u8, value: []const u8) !void {
if (value.len == 0) return self.remove(key);
self.load();
@@ -84,7 +84,7 @@ pub fn remove(self: *Secrets, key: []const u8) !void {
try self.save();
}
-/// One `key=base64(value)` per line — nothing a settings parser or a shell glob would ever
+/// One `key=base64(value)` per line - nothing a settings parser or a shell glob would ever
/// pick up by accident, and any bytes survive. Loaded once, lazily.
fn load(self: *Secrets) void {
if (self.loaded) return;
diff --git a/app/automation/Calm.zig b/app/automation/Calm.zig
index ec4e5dee2..b82eeefca 100644
--- a/app/automation/Calm.zig
+++ b/app/automation/Calm.zig
@@ -1,11 +1,11 @@
//! Where a callout is, frame to frame: a guide's calm rather than a label stuck to the pointer.
//!
-//! A callout keeps its spot while the spot is still good — on the window, clear of everything the
-//! viewer is watching, near enough to what it is about — and moves only on the tape's beats: when
+//! A callout keeps its spot while the spot is still good - on the window, clear of everything the
+//! viewer is watching, near enough to what it is about - and moves only on the tape's beats: when
//! what it is about changes (a new subject), or when its spot stops being good (a bar grew over it,
//! the subject moved away). A pointer's wiggle is neither, so it stays put through one. When it
//! moves, it goes once to the best spot by the new subject (`choose`), along a gentle curve eased in
-//! and out, over 300–600 ms by how far it goes; with motion off it jumps there instead, with the same
+//! and out, over 300-600 ms by how far it goes; with motion off it jumps there instead, with the same
//! calm about when.
//!
//! Pure: the overlay hands it what it measured (`In`), in natural units and demo time, and draws
@@ -170,7 +170,7 @@ fn fits(self: *const Calm, in: In) bool {
return on and apart(r, near) <= reach;
}
-/// The ease of its travel: slow away, slow in, no overshoot — a guide does not swing.
+/// The ease of its travel: slow away, slow in, no overshoot - a guide does not swing.
fn easeInOut(t: f32) f32 {
const u = std.math.clamp(t, 0, 1);
return if (u < 0.5) 4 * u * u * u else 1 - std.math.pow(f32, -2 * u + 2, 3) / 2;
@@ -183,8 +183,8 @@ pub const Spot = struct {
covered: f32 = 0,
};
-/// The best spot for it: beside what it is about — right, below, left or above it, then the
-/// window's corners — taking only one that covers none of it and nothing it must keep clear, and
+/// The best spot for it: beside what it is about - right, below, left or above it, then the
+/// window's corners - taking only one that covers none of it and nothing it must keep clear, and
/// among those the nearest, the side the tape asks for (`In.side`) first, and `here` (where it is)
/// if it is one of them. Failing every one, the one that covers least.
///
diff --git a/app/automation/Service.zig b/app/automation/Service.zig
index b8d7b7a4f..e8bddc5b9 100644
--- a/app/automation/Service.zig
+++ b/app/automation/Service.zig
@@ -1,6 +1,6 @@
//! The `automation` service (`sdk.services.automation`) over a `LiveDriver`: what an app that
//! plays live tapes offers its plugins. The service's doc says what a plugin sees; this is the
-//! bookkeeping behind it — tickets, the outcomes of the last few tapes, and the arbitration with
+//! bookkeeping behind it - tickets, the outcomes of the last few tapes, and the arbitration with
//! whatever else drives the app.
//!
//! An app owns one beside its driver and registers `&service.api` once it is at its final
@@ -8,7 +8,7 @@
//! the next tape starts.
//!
//! **Settled** includes dvui itself: no refresh asked for and no animation running. dvui says so
-//! only as `Window.end`'s return, so the app hands that over after each frame (`frameEnded`) —
+//! only as `Window.end`'s return, so the app hands that over after each frame (`frameEnded`) -
//! fizzy from its backends' `frame_ended_hook`. An app that never does is taken to be quiet, and
//! `settled` falls back to the stage and the driver.
const Service = @This();
@@ -25,7 +25,7 @@ const Api = sdk.services.automation.Api;
gpa: std.mem.Allocator,
driver: *LiveDriver,
-/// Something else that drives the app, refusing a tape while it does — fizzy's demos.
+/// Something else that drives the app, refusing a tape while it does - fizzy's demos.
other: ?Other = null,
/// Told who each live tape plays for (`OnAuthor`). Null: nobody listens, and authors are dropped.
on_author: ?OnAuthor = null,
diff --git a/app/automation/overlay.zig b/app/automation/overlay.zig
index 5cdaab483..5f004525b 100644
--- a/app/automation/overlay.zig
+++ b/app/automation/overlay.zig
@@ -1,11 +1,11 @@
//! What a viewer sees of a demo, over the app: the pointer the tape moves and a ripple where it
//! clicks, the keys it presses, its captions, and the bar to drive it with.
//!
-//! Everything here is a function of the player's state and demo time — the caption showing is
-//! whichever the tape has at `now`, the keystroke pill the last key op within a second of it —
+//! Everything here is a function of the player's state and demo time - the caption showing is
+//! whichever the tape has at `now`, the keystroke pill the last key op within a second of it -
//! so a seek shows exactly what live play showed at that moment, and there is nothing to rewind.
//!
-//! The cards are the app's floating surface — a menu's, a tooltip's: in the window's material
+//! The cards are the app's floating surface - a menu's, a tooltip's: in the window's material
//! (`core.material`), Liquid Glass where the window is made of it, as the key pills and the bar
//! are, else frosted at the dialog style (blur, opacity, refraction); cut with its corners,
//! shadowed with its ring, and opening and closing on its curves (`Reveal`). With the blur off,
@@ -13,7 +13,7 @@
//!
//! Each part is a subwindow that takes no input (events fall through to the app; the bar is
//! hit-tested by `Player.frame` from the rects recorded here), raised above every other subwindow
-//! each frame in drawing order, the pointer last — it has to be drawn over the menu it opened and
+//! each frame in drawing order, the pointer last - it has to be drawn over the menu it opened and
//! over the caption explaining it.
const std = @import("std");
const dvui = @import("dvui");
@@ -58,7 +58,7 @@ pub fn draw(player: *Player) void {
/// Draw the pointer of the live tape `driver` is playing, if any, and the ripple where it last
/// clicked (`replay.overlay`), its fade scaled for reduced motion. A live tape has no captions,
-/// keys or bar — it is not something to watch, only to see. Call once a frame, after everything
+/// keys or bar - it is not something to watch, only to see. Call once a frame, after everything
/// else in the frame has drawn.
pub fn drawLive(driver: *LiveDriver) void {
plain.drawLive(driver, .{ .fade_ms = motion.durationMs(plain_fade_ms) });
@@ -84,13 +84,13 @@ fn theme() dvui.Theme {
// ---- the glass -----------------------------------------------------------------------------
/// A card's corners: a floating surface's (`dialogs.surfaceCorners`), resolved against the theme
-/// up front — the frost is handed them directly, and unresolved it would draw them square.
+/// up front - the frost is handed them directly, and unresolved it would draw them square.
fn corners() dvui.CornerRect {
const t = theme();
return core.dialogs.surfaceCorners().finalize(&t);
}
-/// A card's own options: the space it takes and nothing it paints — `glass` lays the surface
+/// A card's own options: the space it takes and nothing it paints - `glass` lays the surface
/// down under what it holds, kept clear of its corners however round they are.
fn cardOptions(pad: dvui.Rect) dvui.Options {
const inset = core.dialogs.cornerInset(corners().tl.radius());
@@ -103,7 +103,7 @@ fn cardOptions(pad: dvui.Rect) dvui.Options {
}
/// How long a card takes to open: a little quicker than a menu (`motion.open_us`), since a demo's
-/// cards come and go all the while — in ms at the user's speed.
+/// cards come and go all the while - in ms at the user's speed.
fn openMs() f64 {
return motion.durationMs(@as(f32, @floatFromInt(motion.open_us)) / 1000 * 0.7);
}
@@ -112,7 +112,7 @@ fn closeMs() f64 {
return motion.durationMs(260);
}
-/// How far open a card is: its glass formed (`form`, 0…1) and grown out of its origin (`grow`,
+/// How far open a card is: its glass formed (`form`, 0...1) and grown out of its origin (`grow`,
/// past 1 while it overshoots), and the alpha of what it holds.
const Reveal = struct {
form: f32 = 1,
@@ -121,19 +121,19 @@ const Reveal = struct {
const shut: Reveal = .{ .form = 0, .grow = 0, .alpha = 0 };
- /// How much of its place a card this open takes up, 0…1, for what is stacked on it.
+ /// How much of its place a card this open takes up, 0...1, for what is stacked on it.
fn room(self: Reveal) f32 {
return std.math.clamp(self.grow, 0, 1);
}
- /// `u` (0…1) of the way through opening, as a tooltip or a menu opens: the glass forming and
+ /// `u` (0...1) of the way through opening, as a tooltip or a menu opens: the glass forming and
/// what it holds fading in together (`motion.fade`) while it grows into place (`motion.enter`).
fn opening(u: f32) Reveal {
const f = motion.fade(u);
return .{ .form = f, .grow = motion.enter(u), .alpha = f };
}
- /// `w` (0…1) of the way through closing, as a window closes: drawn back into its origin on
+ /// `w` (0...1) of the way through closing, as a window closes: drawn back into its origin on
/// the leaving curve (`motion.exit`), the glass unforming with it, what it holds opaque for
/// the first half of the way and gone as it arrives.
fn closing(w: f32) Reveal {
@@ -155,14 +155,14 @@ const Reveal = struct {
}
};
-/// The surface under a card — `wd` a box that paints nothing of its own (`cardOptions`) — `r` of
+/// The surface under a card - `wd` a box that paints nothing of its own (`cardOptions`) - `r` of
/// the way open, grown out of `origin` (a point in it, as fractions), in the material every
/// floating surface wears (`core.dialogs.dialogFrost`): the frost at the dialog style, clear
/// glass where the blur is off and the glass program draws, with the shadow ring round it; with
/// neither, the dialog fill over its shadow. What the card draws after this is cut to the glass
/// as it grows.
///
-/// Before the alpha for its contents is set: glass forms rather than fades (`r.form`) — a frost
+/// Before the alpha for its contents is set: glass forms rather than fades (`r.form`) - a frost
/// replaces what it covers, and at partial alpha would punch a hole.
fn glass(wd: *dvui.WidgetData, origin: dvui.Point, r: Reveal) void {
const s = wd.borderRectScale().s;
@@ -192,7 +192,7 @@ fn glass(wd: *dvui.WidgetData, origin: dvui.Point, r: Reveal) void {
dvui.clipSet(dvui.clipGet().intersect(rect));
}
-/// The glass under a pill — a key just pressed, the seek's progress — `r` of the way open, grown
+/// The glass under a pill - a key just pressed, the seek's progress - `r` of the way open, grown
/// out of `origin` as `glass` grows a card: a capsule of the same glass as the transport bar's
/// bubbles (`core.dialogs.glassBubbles`), so the player's controls and what it shows are one
/// material. What the pill draws after this is cut to the glass as it grows.
@@ -226,8 +226,8 @@ const stack_gap: f32 = 14;
/// The demo's words and keys at the current moment, each where it belongs: a title card in the
/// middle of the home view (`Caption.Place.middle`); a callout beside the action for a caption
-/// that narrates the pointer (`Tape.besideAction`); and everything else — captions about no one
-/// thing, and the keys just pressed — stacked together at home (`drawStack`). All kept whole on
+/// that narrates the pointer (`Tape.besideAction`); and everything else - captions about no one
+/// thing, and the keys just pressed - stacked together at home (`drawStack`). All kept whole on
/// the window, above `floor`.
fn drawCaptions(player: *Player, tape: *const Tape, win: dvui.Rect, floor: f32) void {
const now = player.now();
@@ -291,7 +291,7 @@ const Stacked = struct {
};
/// The popups at home, stacked up from its foot: the newest lowest, each older one above those
-/// after it — as far above as they are open, so the stack pushes up as one opens below and
+/// after it - as far above as they are open, so the stack pushes up as one opens below and
/// settles back as one below it closes.
fn drawStack(player: *Player, tape: *const Tape, items: []Stacked, win: dvui.Rect, floor: f32) void {
std.sort.insertion(Stacked, items, {}, Stacked.before);
@@ -338,7 +338,7 @@ fn drawTitle(stage: Stage, tape: *const Tape, c: Tape.Caption, index: usize, sho
}
/// A caption that narrates what the pointer does: a callout beside what it is about, where the
-/// viewer is looking — or, while there is nothing drawn yet to sit beside, at the foot of the home
+/// viewer is looking - or, while there is nothing drawn yet to sit beside, at the foot of the home
/// view. It keeps its spot until the tape's focus moves on or the spot stops being good, then
/// travels there once, calmly (`Calm`). Each caption (`index`) its own widget, so none is placed by
/// the size of the one before it.
@@ -457,7 +457,7 @@ const Subject = struct { rect: dvui.Rect, key: u64 };
/// What a caption sits beside: what it names (`Caption.near`), else what the pointer is aimed at in
/// its time (`Tape.aimedAt`), looking `lead_ms` ahead so it sets off with the pointer rather than
-/// after it — the point the pointer goes to, when that is a whole pane, and the pointer itself
+/// after it - the point the pointer goes to, when that is a whole pane, and the pointer itself
/// while what it goes to is still to be drawn (a row in a pane that is opening). Null when there is
/// nothing to sit beside.
fn subjectOf(player: *Player, tape: *const Tape, c: Tape.Caption, win: dvui.Rect) ?Subject {
@@ -468,8 +468,8 @@ fn subjectOf(player: *Player, tape: *const Tape, c: Tape.Caption, win: dvui.Rect
const until = player.now() + lead_ms;
while (ahead < tape.ops.len and @as(f64, @floatFromInt(tape.ops[ahead].at)) <= until) ahead += 1;
const aim = tape.aimedAt(c, ahead) orelse return null;
- // One subject wherever on it the pointer goes — on a pane, how far the point moves is left to
- // the callout's reach (`Calm.reach`) — and another while it is still to be drawn, so the
+ // One subject wherever on it the pointer goes - on a pane, how far the point moves is left to
+ // the callout's reach (`Calm.reach`) - and another while it is still to be drawn, so the
// callout goes to it once it is rather than staying by the pointer.
var h = std.hash.Wyhash.init(1);
h.update(aim.tag);
@@ -482,7 +482,7 @@ fn subjectOf(player: *Player, tape: *const Tape, c: Tape.Caption, win: dvui.Rect
}
/// What a callout must not cover, besides what it is beside: what the caption says to keep clear
-/// (`Caption.clear`); what the pointer goes to in the caption's time — the thing, or the point on
+/// (`Caption.clear`); what the pointer goes to in the caption's time - the thing, or the point on
/// a pane, since covering an editor is unavoidable but covering where in it the pointer goes is
/// not; every bar out under the overlay (`dome.barsOut`: a document's history bar, a corner
/// vial), which is what a step brings out. Never the pointer itself: a callout holds its place
@@ -616,7 +616,7 @@ fn keysCard(player: *Player, op: Tape.Op, shown: Reveal) void {
// ---- the transport bar ---------------------------------------------------------------------
/// The bar's pieces, left to right: round buttons for chapter back, play/pause and chapter
-/// forward, the scrubber's capsule, and a round close — separate bubbles of one liquid glass.
+/// forward, the scrubber's capsule, and a round close - separate bubbles of one liquid glass.
const Piece = enum { prev, play, next, track, close };
/// Each piece's diameter, points; the track's width is what the bar leaves it.
const piece_d = [_]f32{ 36, 44, 36, 44, 32 };
@@ -1055,8 +1055,8 @@ fn barTrack(player: *Player, tape: *const Tape, bounds: dvui.Rect, r: dvui.Rect,
return line;
}
-/// The bar opening when `want` turns true and closing when it turns false — 0 shut to 1 open,
-/// linear in time over `bar_grow_us` at the user's speed, the curves being `dome.pullFrom`'s —
+/// The bar opening when `want` turns true and closing when it turns false - 0 shut to 1 open,
+/// linear in time over `bar_grow_us` at the user's speed, the curves being `dome.pullFrom`'s -
/// kept on `tr` from frame to frame. Wall time (`now_ns`, `Player.wallNs`): it answers the
/// viewer's pointer, not the demo. Turned round part way, it carries on from as open as it is.
fn barProgress(tr: *Player.Transport, now_ns: i128, want: bool) f32 {
diff --git a/app/control/json.zig b/app/control/json.zig
index a9e7d64e8..3567c6db8 100644
--- a/app/control/json.zig
+++ b/app/control/json.zig
@@ -2,8 +2,8 @@
//! `Host.callCommand` as ZON, and its ZON result goes back out as JSON. The one place the two
//! meet, so the plugin boundary never sees JSON (plans/CONTROL_SERVER_PLAN.md).
//!
-//! The mapping is the obvious one both ways — objects and structs, arrays and tuples, strings,
-//! numbers, booleans, null — with one thing JSON cannot say: an enum. Going in, a string is an
+//! The mapping is the obvious one both ways - objects and structs, arrays and tuples, strings,
+//! numbers, booleans, null - with one thing JSON cannot say: an enum. Going in, a string is an
//! enum literal where the command's parameter is an enumeration (`enum_keys`); everywhere else it
//! is a string. Going out, an enum literal is its name as a string. A caller who needs an enum
//! deeper than a top-level argument sends ZON instead (`Content-Type: application/zon`).
@@ -13,7 +13,7 @@ const std = @import("std");
pub const Error = error{ OutOfMemory, NotJson, NotAnObject, NotZon, NotUtf8 };
-/// `json`, an object of arguments, as a ZON struct literal: `{"line": 12}` → `.{.line=12}`. A
+/// `json`, an object of arguments, as a ZON struct literal: `{"line": 12}` -> `.{.line=12}`. A
/// top-level string whose key is in `enum_keys` becomes an enum literal. An empty body is no
/// arguments, `""`.
pub fn zonFromJson(arena: std.mem.Allocator, json: []const u8, enum_keys: []const []const u8) Error![]u8 {
diff --git a/app/control/openapi.zig b/app/control/openapi.zig
index facad1b7f..5c9d7e538 100644
--- a/app/control/openapi.zig
+++ b/app/control/openapi.zig
@@ -8,7 +8,7 @@
//! The bounds and suggested choices a number declares are the palette's hints, never checked by
//! `Command.Params`, so they go in its description, not its schema, where a generated client would
//! refuse values the server takes. A command's result has no declared type yet, so its 200 response
-//! is any JSON value; the seam for typing it is a result type beside `Command.Params(…)` that
+//! is any JSON value; the seam for typing it is a result type beside `Command.Params(...)` that
//! `commandPath` would read.
const std = @import("std");
const sdk = @import("fizzy_sdk");
diff --git a/app/control/routes.zig b/app/control/routes.zig
index 278114ee3..46559e9bc 100644
--- a/app/control/routes.zig
+++ b/app/control/routes.zig
@@ -16,7 +16,7 @@
//! Bodies are JSON, converted at this edge (`json.zig`). A request sent as `application/zon` is
//! passed through as ZON and answered in ZON. Errors are HTTP's: 404 for an unknown command, 400
//! for arguments that do not fit, 409 for a command not enabled now, 500 for one that ran and
-//! failed, each with `{"error": "…"}`.
+//! failed, each with `{"error": "..."}`.
const std = @import("std");
const sdk = @import("fizzy_sdk");
const AppInfo = @import("../AppInfo.zig");
diff --git a/app/control/screen_diff.zig b/app/control/screen_diff.zig
index 8dbae99e0..014537e48 100644
--- a/app/control/screen_diff.zig
+++ b/app/control/screen_diff.zig
@@ -1,12 +1,12 @@
//! The screen as changes on a base: what a snapshot (`replay.Snapshot`) says, each node given a key
-//! that stays the same from one frame to the next, and what changed between two of them — the
+//! that stays the same from one frame to the next, and what changed between two of them - the
//! nodes added, removed, and changed in name, place or focus. An agent reads what its action did
//! instead of finding it in two near-identical screens; a stream sends a frame's changes instead of
//! the whole screen (plans/CONTROL_SERVER_PLAN.md).
//!
//! **Keys.** A node with a tag is its tag: tags are what widgets publish to be found by. One without
//! is the path to it from the nearest node with a tag, a `role:name` per step, numbered among its
-//! like siblings (`fizzy.window:main/tab:notes.md`, `…/button:#2`). A snapshot's `parent` is an
+//! like siblings (`fizzy.window:main/tab:notes.md`, `.../button:#2`). A snapshot's `parent` is an
//! index, which shifts whenever anything above changes; a key does not. Keys are unique within a
//! screen: one already given out is never given again.
//!
diff --git a/app/crash/crash.zig b/app/crash/crash.zig
index b9e2788a1..e7ec40999 100644
--- a/app/crash/crash.zig
+++ b/app/crash/crash.zig
@@ -55,7 +55,7 @@ pub const Module = struct {
/// What crashed, as the first line of a report says it.
pub const What = union(enum) {
panic: []const u8,
- /// `code` is the signal's `si_code` (why it was sent: no mapping, no permission, …).
+ /// `code` is the signal's `si_code` (why it was sent: no mapping, no permission, ...).
signal: struct { name: []const u8, address: ?usize = null, code: ?i32 = null },
};
diff --git a/app/dome.zig b/app/dome.zig
index 2afcafbfb..aa55d443e 100644
--- a/app/dome.zig
+++ b/app/dome.zig
@@ -74,8 +74,8 @@ pub fn emerge(p: f32, delay: f32) f32 {
/// button sliding. It changes from `home`'s shape to a drop up to `size` across as it sets off,
/// travels small and inflates as it nears its place; along an arc, up and then across, rather than
/// straight, `arc` times the usual height (so bubbles leaving together fan out rather than travel
-/// stacked); on one smooth curve (`smooth`) both ways, so going back is coming played backwards —
-/// in the reverse order, as the later a bubble buds, the sooner it is in — and one arriving takes
+/// stacked); on one smooth curve (`smooth`) both ways, so going back is coming played backwards -
+/// in the reverse order, as the later a bubble buds, the sooner it is in - and one arriving takes
/// `home`'s shape. A dome's home is its bud (`bud`); a corner vial's is the vial's own circle
/// (`Corner.home`), so the bubbles going back become the vial, and the glass the bar ends as is the
/// vial at rest. All physical.
@@ -106,7 +106,7 @@ pub fn pullFrom(home: dvui.Rect.Physical, size: f32, to: dvui.Rect.Physical, p:
/// while it travels, as a share of its place's size; and how far along it starts to inflate.
const pull_arc: f32 = 0.14;
// Not much smaller than its place: the glass's rim is one for the whole field, fitted to its
-// smallest shape, and a tiny drop is all rim — a darker drop among the rest.
+// smallest shape, and a tiny drop is all rim - a darker drop among the rest.
const pull_drop: f32 = 0.7;
const pull_inflate: f32 = 0.55;
/// How far along a bubble has gone from its home's size to its drop's.
@@ -318,7 +318,7 @@ fn fillTube(tube: dvui.Rect.Physical, tilt: f32, color: dvui.Color) void {
pour(rules.cut(vessel, .{ tube.x + tube.w, tube.y + tube.h / 2 }, .{ @cos(a), @sin(a) }, &kept), color);
}
-// ── The grip as a corner vial ────────────────────────────────────────────────────────────────
+// -- The grip as a corner vial ----------------------------------------------------------------
//
// The history's grip in the bottom-right corner of the card its document is on, the way a place's
// arrange button is in its top-right (`layout/Region.zig`, `cornerPiece`): an orb on the card's
@@ -385,8 +385,8 @@ pub const Corner = struct {
}
/// Where the pointer is on it: the square from the corner that lies inside the quarter drawn
- /// (its far corner on the circle), so nothing past the glass — the document's last columns,
- /// a scroll bar's end — is taken for the grip's.
+ /// (its far corner on the circle), so nothing past the glass - the document's last columns,
+ /// a scroll bar's end - is taken for the grip's.
pub fn hit(self: Corner, s: f32) dvui.Rect.Physical {
const d = self.radiusPx(s) * std.math.sqrt1_2;
const p = self.point();
@@ -576,7 +576,7 @@ fn pourStream(lip: [2]f32, end: [2]f32, tail: f32, head: f32, th: f32, color: dv
r.fill(.all(hw), .{ .color = .{ .color = color }, .fade = 1 });
}
-// ── Bars out this frame ──────────────────────────────────────────────────────────────────────
+// -- Bars out this frame ----------------------------------------------------------------------
//
// Each transport's bar that is out, and each corner vial at rest, says where it is (`publishBar`),
// so a bar that would cover it can stand clear (`barsOut`): the tape's bar, out along the bottom
diff --git a/app/dome_rules.zig b/app/dome_rules.zig
index 8f75c0e15..8b69edb9b 100644
--- a/app/dome_rules.zig
+++ b/app/dome_rules.zig
@@ -191,7 +191,7 @@ pub fn roundedOutline(x: f32, y: f32, w: f32, h: f32, r_in: f32, out: []Pt) []Pt
pub const corner_points = 10;
/// The part of convex `poly` on the near side of the line through `at` whose far side `away` (a
-/// unit vector) points to (`(p - at) · away <= 0` is kept), into `out` (at least `poly.len + 1`
+/// unit vector) points to (`(p - at) . away <= 0` is kept), into `out` (at least `poly.len + 1`
/// long). Still convex: a liquid is its vessel cut by its surface.
///
/// A point within `on_line` of the line counts as kept. A line just touching the vessel (an
diff --git a/app/keymap/Keymap.zig b/app/keymap/Keymap.zig
index 1f83374f0..bb43fb4b0 100644
--- a/app/keymap/Keymap.zig
+++ b/app/keymap/Keymap.zig
@@ -1,7 +1,7 @@
-//! Keybinding resolution: chord → command id.
+//! Keybinding resolution: chord -> command id.
//!
//! **Invariant: nothing under `keymap/` may import dvui.** dvui's keybind map is a
-//! `name → Keybind` lookup where `Keybind` is a single key plus modifier flags — it cannot hold
+//! `name -> Keybind` lookup where `Keybind` is a single key plus modifier flags - it cannot hold
//! a chord, and it is matched by *bind name* rather than by command, which is why today's
//! `Keybinds.tick()` is a hardcoded if-chain wired straight to `fizzy.editor().*` calls with
//! nothing addressable by a config file. This module owns the real table; the dvui-facing
@@ -10,7 +10,7 @@
//!
//! Staying dvui-free also puts this in the `addTest`-per-pure-root list in `build/app.zig`.
//!
-//! This file is the single test root for the tree — Zig collects `test` blocks only from an
+//! This file is the single test root for the tree - Zig collects `test` blocks only from an
//! artifact's root module, and relative imports are part of that same module.
const std = @import("std");
@@ -30,7 +30,7 @@ pub const ParseError = chord_mod.ParseError;
const Allocator = std.mem.Allocator;
-/// Context gate — VSCode's `when` clauses, reduced to a fixed set. Deliberately a small closed
+/// Context gate - VSCode's `when` clauses, reduced to a fixed set. Deliberately a small closed
/// set rather than an expression language: a binding either applies in a context or it doesn't,
/// and every entry here has to be *supplied* by fizzy each frame, so an open-ended grammar
/// would just be a way to write conditions nothing ever satisfies.
@@ -56,7 +56,7 @@ pub const When = packed struct {
return true;
}
- /// How many flags a binding constrains — the specificity used to break ties, so a
+ /// How many flags a binding constrains - the specificity used to break ties, so a
/// context-specific binding always wins over a global one on the same chord.
pub fn weight(self: When) u8 {
var n: u8 = 0;
@@ -70,7 +70,7 @@ pub const When = packed struct {
return @as(u6, @bitCast(a)) == @as(u6, @bitCast(b));
}
- /// Parse a `when` string. Unknown names are *not* an error — they're reported so a config
+ /// Parse a `when` string. Unknown names are *not* an error - they're reported so a config
/// written against a newer fizzy round-trips instead of being silently dropped.
pub fn parse(text: []const u8, unknown: ?*std.ArrayList([]const u8), gpa: ?Allocator) !When {
var out: When = .{};
@@ -93,7 +93,7 @@ pub const When = packed struct {
}
};
-/// `completionVisible` and `completion_visible` both name the same flag — the ZON file leans
+/// `completionVisible` and `completion_visible` both name the same flag - the ZON file leans
/// snake_case, VSCode docs use camelCase, and there's no value in making people care.
fn eqlCamelOrSnake(input: []const u8, snake: []const u8) bool {
if (std.ascii.eqlIgnoreCase(input, snake)) return true;
@@ -125,7 +125,7 @@ pub const Source = enum(u8) {
pub const Binding = struct {
stroke: Stroke,
- /// Command id to run, or null to *unbind* this chord — how a user turns off a default
+ /// Command id to run, or null to *unbind* this chord - how a user turns off a default
/// without having to know what it was bound to.
command: ?[]const u8,
/// The command's arguments as ZON, keyed by its parameters (`.{ .line = 1 }`), or empty to
@@ -142,7 +142,7 @@ pub const Binding = struct {
pub const Resolution = union(enum) {
/// No binding; the event should fall through to whatever else wants it.
none,
- /// First half of a chord matched — swallow the key and wait for the second stroke.
+ /// First half of a chord matched - swallow the key and wait for the second stroke.
pending,
command: Invocation,
/// An explicit unbind matched. Distinct from `.none` because the key *was* claimed: the
@@ -183,7 +183,7 @@ pub fn add(self: *Keymap, gpa: Allocator, b: Binding) !void {
try self.bindings.append(gpa, b);
}
-/// Cancel a half-entered chord — call on focus loss or Escape, so a stray `ctrl+k` doesn't
+/// Cancel a half-entered chord - call on focus loss or Escape, so a stray `ctrl+k` doesn't
/// silently eat the next keystroke minutes later.
pub fn cancelPending(self: *Keymap) void {
self.pending = null;
@@ -207,7 +207,7 @@ fn best(self: Keymap, stroke: Stroke, ctx: When, active_owner: ?[]const u8) ?Bin
};
const b_rank = (@as(u16, @backingInt(b.source)) << 8) | b.when.weight();
const w_rank = (@as(u16, @backingInt(w.source)) << 8) | w.when.weight();
- // Owner-scoped bindings beat otherwise equal-rank global ones — that's the whole
+ // Owner-scoped bindings beat otherwise equal-rank global ones - that's the whole
// point of active-owner conflict resolution (pixi Export vs fizzy Quick Open).
const b_owner_boost: u16 = if (b.owner_id != null) 1 else 0;
const w_owner_boost: u16 = if (w.owner_id != null) 1 else 0;
@@ -238,7 +238,7 @@ fn opensChord(self: Keymap, c: Chord, ctx: When, active_owner: ?[]const u8) bool
/// Feed one key press. Stateful: consecutive calls complete a chord.
/// `active_owner` is the focused document's plugin id, or null when none.
pub fn resolve(self: *Keymap, c: Chord, ctx: When, active_owner: ?[]const u8) Resolution {
- // Modifier keys alone never resolve, and must not cancel a pending chord — otherwise
+ // Modifier keys alone never resolve, and must not cancel a pending chord - otherwise
// pressing Ctrl for the second half of `ctrl+k ctrl+c` would abort the chord.
if (keyIsModifier(c.key)) return .none;
@@ -274,14 +274,14 @@ pub fn resolve(self: *Keymap, c: Chord, ctx: When, active_owner: ?[]const u8) Re
return .none;
}
-/// What `resolve` would answer for `c`, leaving the chord state as it was — for asking about a
+/// What `resolve` would answer for `c`, leaving the chord state as it was - for asking about a
/// key before it is delivered (the web page's `keydown`) without feeding it twice.
pub fn peek(self: Keymap, c: Chord, ctx: When, active_owner: ?[]const u8) Resolution {
var copy = self;
return copy.resolve(c, ctx, active_owner);
}
-/// Every binding currently mapped to `command` with no arguments — its shortcuts, for rendering
+/// Every binding currently mapped to `command` with no arguments - its shortcuts, for rendering
/// hints and the Keyboard Shortcuts pane. A binding that carries arguments runs something a menu
/// row does not, so it is not one.
pub fn bindingsFor(self: Keymap, gpa: Allocator, command: []const u8) ![]Binding {
@@ -303,7 +303,7 @@ pub fn bindingsFor(self: Keymap, gpa: Allocator, command: []const u8) ![]Binding
/// 1. Duplicate entries for the same command (e.g. `fizzy.openFolder` from both the dvui
/// `open_folder` bind and the profile default) keep only the highest-ranked claim.
/// 2. Within one owner bucket (all-global, or one plugin), only *adjacent* ranks are
-/// reported — if A shadows B and B shadows C, "A shadows C" is omitted because C is
+/// reported - if A shadows B and B shadows C, "A shadows C" is omitted because C is
/// already dead under B.
/// 3. Owner-scoped claims fork against the top global on that chord (context-dependent).
pub fn conflicts(self: Keymap, gpa: Allocator) ![]Conflict {
@@ -326,7 +326,7 @@ pub fn conflicts(self: Keymap, gpa: Allocator) ![]Conflict {
for (claims.items) |*c| {
if (!c.stroke.eql(b.stroke) or !c.when.eql(b.when)) continue;
if (!std.mem.eql(u8, c.command, cmd) or !std.mem.eql(u8, c.args, b.args)) continue;
- // Later equal-rank wins — same tie-break `best()` uses.
+ // Later equal-rank wins - same tie-break `best()` uses.
if (rank >= c.rank) {
c.rank = rank;
c.owner_id = b.owner_id;
@@ -422,7 +422,7 @@ pub fn conflicts(self: Keymap, gpa: Allocator) ![]Conflict {
}
}
- // Owner-scoped top vs global top — context-dependent, either can win.
+ // Owner-scoped top vs global top - context-dependent, either can win.
if (global_top) |gt| {
for (bucket_keys.items, 0..) |key, bi| {
if (key == null) continue;
@@ -504,7 +504,7 @@ test "an unmatched second stroke is swallowed, not misfired" {
defer k.deinit(a);
try t.expect(k.resolve(keys("ctrl+k").first, .{}, null) == .pending);
- // `ctrl+x` must NOT run cut here — it was typed as the tail of an abandoned chord.
+ // `ctrl+x` must NOT run cut here - it was typed as the tail of an abandoned chord.
try t.expect(k.resolve(keys("ctrl+x").first, .{}, null) == .unbound);
// ...and afterwards it works normally again.
try t.expectEqualStrings("fizzy.cut", k.resolve(keys("ctrl+x").first, .{}, null).command.id);
@@ -540,7 +540,7 @@ test "null command unbinds and claims the key" {
.{ .stroke = keys("ctrl+b"), .command = null, .source = .user },
});
defer k.deinit(a);
- // Not `.none` — the whole point is to stop the profile binding firing.
+ // Not `.none` - the whole point is to stop the profile binding firing.
try t.expect(k.resolve(keys("ctrl+b").first, .{}, null) == .unbound);
}
diff --git a/app/keymap/dvui_adapter.zig b/app/keymap/dvui_adapter.zig
index 65b063137..3acca27f8 100644
--- a/app/keymap/dvui_adapter.zig
+++ b/app/keymap/dvui_adapter.zig
@@ -5,7 +5,7 @@
//! projects single-stroke bindings back *out* into `dvui.Window.keybinds`.
//!
//! That projection is not optional. `TextEntryWidget` calls `ke.matchBind("char_left")` and
-//! `Menu.zig` calls `cw.keybinds.get("save")` to render accelerators — both read the dvui map
+//! `Menu.zig` calls `cw.keybinds.get("save")` to render accelerators - both read the dvui map
//! directly. Chords have no representation there at all (`dvui.enums.Keybind` is one key plus
//! modifier flags), so they exist only in the resolver.
@@ -60,7 +60,7 @@ pub fn chordFrom(ke: dvui.Event.Key) ?Chord {
}
/// Lower a chord to a dvui keybind. Every modifier is stated explicitly (rather than left null)
-/// so a projected bind matches only the exact combination — `matchKeyBind` treats a null field
+/// so a projected bind matches only the exact combination - `matchKeyBind` treats a null field
/// as "don't care", which would make `ctrl+s` also fire on `ctrl+shift+s`.
pub fn toKeybind(c: Chord) dvui.enums.Keybind {
return .{
diff --git a/app/keymap/zon.zig b/app/keymap/zon.zig
index e6003400e..941840db7 100644
--- a/app/keymap/zon.zig
+++ b/app/keymap/zon.zig
@@ -1,4 +1,4 @@
-//! Reading and writing `keybinds.zon` — the user's keybinding overrides.
+//! Reading and writing `keybinds.zon` - the user's keybinding overrides.
//!
//! Only overrides live on disk. Defaults stay in code, so a user who has never rebound anything
//! has no `keybinds.zon` at all (same principle as R12's "omit all-default blocks" in
@@ -300,7 +300,7 @@ fn collectBlock(
}
}
-/// Parse a `keybinds.zon` source. Only allocation failure is an error — everything else lands in
+/// Parse a `keybinds.zon` source. Only allocation failure is an error - everything else lands in
/// `File.diagnostics`.
pub fn parse(gpa: Allocator, source: [:0]const u8, platform: Platform) Allocator.Error!File {
var c: Collector = .{ .gpa = gpa };
@@ -358,7 +358,7 @@ pub fn parse(gpa: Allocator, source: [:0]const u8, platform: Platform) Allocator
};
}
-/// In-place: `shell.save` → `fizzy.save`. Both prefixes are 5 chars + `.`.
+/// In-place: `shell.save` -> `fizzy.save`. Both prefixes are 5 chars + `.`.
fn migrateLegacyShellCommandId(cmd: []u8) void {
if (cmd.len >= 6 and std.mem.eql(u8, cmd[0..6], "shell.")) {
@memcpy(cmd[0..5], "fizzy");
@@ -401,7 +401,7 @@ pub fn format(gpa: Allocator, bindings: []const OwnedBinding) ![]u8 {
}
if (wrote_fizzy) try w.writeAll(" },\n");
- // Distinct plugin ids, in first-seen order — stable output, no allocation for a sort.
+ // Distinct plugin ids, in first-seen order - stable output, no allocation for a sort.
var wrote_plugins = false;
for (bindings, 0..) |b, i| {
const id = b.owner_id orelse continue;
diff --git a/app/layout/Chooser.zig b/app/layout/Chooser.zig
index f68f7c4d8..642ad8702 100644
--- a/app/layout/Chooser.zig
+++ b/app/layout/Chooser.zig
@@ -1,7 +1,7 @@
//! A **chooser**: the views of one place, laid out as something to pick from.
//!
//! The tab strip above a place holding several views and the icon rail beside the sidebar are the same
-//! control — a list of a place's views, where picking one selects it there — differing only in
+//! control - a list of a place's views, where picking one selects it there - differing only in
//! which way they run, where they sit, and what an item looks like. This is that control. What it
//! does is the same everywhere; what it looks like is the caller's:
//!
@@ -20,19 +20,19 @@
//!
//! * a click (a tap, for a finger) selects the view in its place;
//! * dragging an item lifts it into the ordinary view drag, the one drag a view is ever carried
-//! in: along a chooser — this one, or another place's — it is carried as a tab in glass, and
+//! in: along a chooser - this one, or another place's - it is carried as a tab in glass, and
//! the chooser under it opens a slot where it would go in (`ViewDrag.offerChooser` with where
//! along it); let go there it goes in (`ViewDrag.insertInto`), which back on its own chooser
-//! moves it along the place's list — kept as the place's order (`State.order`), or its
+//! moves it along the place's list - kept as the place's order (`State.order`), or its
//! assignment's order when it has one, never as a new assignment, which would freeze a keyword
//! place against views installed later. Off the choosers it is the view drag over the places,
-//! to land on another place or split one — the gesture the corner chooser starts;
+//! to land on another place or split one - the gesture the corner chooser starts;
//! * a finger drag that moves on at once scrolls instead, and one held still first lifts
//! (`core.widgets.Tabs`).
//!
//! **What the caller decides:** what an item looks like (`label` and `icon` are the two stock
-//! looks), where the chooser sits — inside its place or beside it, before the place is even
-//! declared this frame — which views it lists and in what groups (the rail keeps two, a scrolling
+//! looks), where the chooser sits - inside its place or beside it, before the place is even
+//! declared this frame - which views it lists and in what groups (the rail keeps two, a scrolling
//! list and a pinned footer: two choosers over one place), anything drawn between items, and what
//! picking the already-selected item means (`reselected`).
//!
@@ -55,14 +55,14 @@ pub const Options = struct {
scroll: bool = true,
/// Two choosers over one place in one frame (the rail's list and its footer) differ by this.
id_extra: usize = 0,
- /// Options for the chooser's outermost box (`expand`, `min_size_content`, …).
+ /// Options for the chooser's outermost box (`expand`, `min_size_content`, ...).
outer: dvui.Options = .{},
/// Shade the ends the items continue past when they scroll.
scroll_shadows: bool = false,
};
pub const ItemOptions = struct {
- /// Show the view's title on hover — for a chooser whose items do not say it themselves.
+ /// Show the view's title on hover - for a chooser whose items do not say it themselves.
tooltip: bool = false,
/// A further line under the title in that tooltip (what a badge means, say).
tooltip_detail: ?[]const u8 = null,
@@ -97,7 +97,7 @@ const Lift = struct { id: []const u8, rect: dvui.Rect.Physical, event: u16 };
/// What a chooser keeps between frames, in dvui's store under wherever it is drawn, so it lives
/// as long as the chooser does and two never share one.
const Kept = struct {
- /// Where the slot opens: before the item drawn at this index, or past the last — read last
+ /// Where the slot opens: before the item drawn at this index, or past the last - read last
/// frame from where the carried view was over the chooser. Null when nothing carried is.
slot_at: ?usize = null,
/// How long, along the chooser, the item last lifted off it was, and which it was (a hash of
@@ -109,7 +109,7 @@ const Kept = struct {
item_len: f32 = 0,
};
-/// A chooser for `place`: a declared region, or `.{ .keywords = … }` for the place with those
+/// A chooser for `place`: a declared region, or `.{ .keywords = ... }` for the place with those
/// keywords. Draw items with `item`, then `deinit`.
pub fn init(src: std.lang.SourceLocation, f: *Layout, place: Region, opts: Options) Chooser {
const key = place.selectionKey();
@@ -154,7 +154,7 @@ pub fn init(src: std.lang.SourceLocation, f: *Layout, place: Region, opts: Optio
};
}
-/// The place's views, in its order — less the one the view drag is carrying, which is in the
+/// The place's views, in its order - less the one the view drag is carrying, which is in the
/// hand. Draw an item for whichever of them this chooser lists.
pub fn views(self: *const Chooser) []const *Layout.Surface {
return self.all;
@@ -234,12 +234,12 @@ pub const Item = struct {
}
};
-/// The view picked this frame, if any — already selected in its place.
+/// The view picked this frame, if any - already selected in its place.
pub fn picked(self: *const Chooser) ?[]const u8 {
return self.picked_id;
}
-/// The view picked this frame that was *already* selected — a second click on the showing tab.
+/// The view picked this frame that was *already* selected - a second click on the showing tab.
/// Nothing is done for it; the caller says what it means (the rail closes or opens its place).
pub fn reselected(self: *const Chooser) ?[]const u8 {
return self.picked_again_id;
@@ -282,8 +282,8 @@ fn measureItems(self: *Chooser) void {
}
/// An item lifted this frame (`Tabs.Tab.lifted`): carry it in the view drag, from where it stood.
-/// Out of an open place it is the place's own drag, which the place drives — it carries what the
-/// place shows, so it is shown first — and out of a shut one (a rail beside a closed sidebar) a
+/// Out of an open place it is the place's own drag, which the place drives - it carries what the
+/// place shows, so it is shown first - and out of a shut one (a rail beside a closed sidebar) a
/// view carried loose, as a card lifted out of the picker is: the place is not drawing to drive a
/// drag. Either way the slot it leaves, if it is carried back along here, is its own length.
fn carryLifted(self: *Chooser) void {
@@ -311,12 +311,12 @@ fn carryLifted(self: *Chooser) void {
}
/// While a view is carried, the chooser is somewhere it can go: into this chooser's place, as one
-/// of its views, where along it the pointer is — a rail beside a sidebar as much as a strip inside
+/// of its views, where along it the pointer is - a rail beside a sidebar as much as a strip inside
/// a panel, and the chooser it was lifted off as much as any other. It says so to the drag
/// (`ViewDrag.offerChooser`), which lands a release over it there (`ViewDrag.insertInto`); and
/// while it is the chooser the view is over, it opens a slot there next frame (`openSlot`). It
-/// reaches half its thickness past itself toward its place's inside — below a strip, right of a
-/// rail — so a hand drifting a little off it is still among its items.
+/// reaches half its thickness past itself toward its place's inside - below a strip, right of a
+/// rail - so a hand drifting a little off it is still among its items.
fn offerDrop(self: *Chooser) void {
const f = self.layout;
self.kept.slot_at = null;
@@ -327,7 +327,7 @@ fn offerDrop(self: *Chooser) void {
const p = dvui.currentWindow().mouse_pt;
const at = self.insertAt(p);
ViewDrag.offerChooser(f, name, bounds, true, at.insert);
- // The chooser the view is over, and its place could take it — the drag's own reading, which
+ // The chooser the view is over, and its place could take it - the drag's own reading, which
// asks which window is on top there and what the place accepts. Of two choosers over one
// place (the rail's list and footer), the one the view is over by its bounds.
const o = ViewDrag.chooserAt(f.state, p) orelse return;
@@ -339,8 +339,8 @@ fn offerDrop(self: *Chooser) void {
const reach: f32 = 1.5;
/// Where a view let go at `p` goes in: before the first item (not the carried one) whose middle is
-/// past it along the chooser — read as if the open slot were not there, so the slot does not chase
-/// the pointer — else after this chooser's last item, which the place may follow with views this
+/// past it along the chooser - read as if the open slot were not there, so the slot does not chase
+/// the pointer - else after this chooser's last item, which the place may follow with views this
/// chooser does not list. `index` counts the items drawn before it, the carried one left out.
fn insertAt(self: *const Chooser, p: dvui.Point.Physical) struct { index: usize, insert: ViewDrag.Insert } {
const a = if (self.across()) p.x else p.y;
@@ -360,7 +360,7 @@ fn insertAt(self: *const Chooser, p: dvui.Point.Physical) struct { index: usize,
return .{ .index = index, .insert = if (last) |id| .{ .after = id } else .end };
}
-/// The declared region a chooser's place is — by its own name, or the one with its keywords.
+/// The declared region a chooser's place is - by its own name, or the one with its keywords.
fn regionOf(f: *Layout, place: *const Region) ?Region {
if (place.name.len > 0) {
for (f.state.regions.items) |r| if (std.mem.eql(u8, r.name, place.name)) return r;
@@ -375,7 +375,7 @@ fn placeName(f: *Layout, place: *const Region) ?[]const u8 {
return if (r.name.len > 0) r.name else null;
}
-// ── The two stock looks ────────────────────────────────────────────────────────────────────────
+// -- The two stock looks ------------------------------------------------------------------------
/// A tab's look: the view's title, uppercase, in the heading font, highlighted when selected and
/// brightened under the pointer.
diff --git a/app/layout/Drop.zig b/app/layout/Drop.zig
index b60fa90df..ffbbfc385 100644
--- a/app/layout/Drop.zig
+++ b/app/layout/Drop.zig
@@ -1,7 +1,7 @@
//! What releasing a dragged view does, as a value.
//!
//! One rule: **the edge you release on is where the dragged view ends up.**
-//! The middle of another place is a trade — or, when the two places are the
+//! The middle of another place is a trade - or, when the two places are the
//! halves of one split, a join: they are one place again, holding both. The
//! middle of the place the view came out of floats it, into a window of its
//! own over the layout (`Floats`). Both the drop zones and the commit read this
@@ -18,8 +18,8 @@
//!
//! A self-split cannot move the view onto the leaf: the view is *already* in
//! the origin, and remounting a surface into a fresh slot tears down every
-//! widget under it — scroll positions, sash sizes, focus. So the origin keeps
-//! it and the leaf opens empty on the far side — which is also the only way the
+//! widget under it - scroll positions, sash sizes, focus. So the origin keeps
+//! it and the leaf opens empty on the far side - which is also the only way the
//! view can stay under the pointer, where the user dropped it.
const std = @import("std");
const dvui = @import("dvui");
@@ -51,7 +51,7 @@ pub const Plan = union(enum) {
float,
pub const Split = struct {
- /// The edge the pointer chose — where the dragged view ends up.
+ /// The edge the pointer chose - where the dragged view ends up.
landing: Side,
/// Where the empty leaf opens. Opposite `landing` when the origin
/// keeps the view, so the view stays on the edge it was dropped on.
@@ -65,7 +65,7 @@ pub const Plan = union(enum) {
/// Read a pointer position against a place: the drop's own reading (`DropZones`), so what the
/// drop shows under the pointer is what a release does. Its middle is a swap, each side of
/// its ring a split on that side, the trash (offered with `remove`) a removal, and off the drop
-/// nothing — null.
+/// nothing - null.
pub fn kindAt(bounds: dvui.Rect.Physical, mouse: dvui.Point.Physical, scale: f32, remove: bool) ?Kind {
return kindAtDisc(bounds, mouse, 0, scale, remove);
}
@@ -91,7 +91,7 @@ pub fn kindAtDisc(bounds: dvui.Rect.Physical, c: dvui.Point.Physical, r: f32, sc
/// not the place the view was lifted from, whether the two are the halves of
/// one split (`halves`, `SplitTree.Forest.joinable`), and whether the view may
/// float (`can_float`, `float_rules.canFloat`). The middle of your own place is
-/// not a trade with yourself: it floats the view, or — where it cannot float —
+/// not a trade with yourself: it floats the view, or - where it cannot float -
/// does nothing, null.
pub fn plan(kind: Kind, self_drop: bool, halves: bool, can_float: bool) ?Plan {
return switch (kind) {
@@ -106,7 +106,7 @@ pub fn plan(kind: Kind, self_drop: bool, halves: bool, can_float: bool) ?Plan {
}
test "the drop's middle is a swap, each side a split, the trash a removal, off it nothing" {
- // An 800×600 place: the bubbles sit round (400, 300) — the sides 108 out, the trash 88 out
+ // An 800x600 place: the bubbles sit round (400, 300) - the sides 108 out, the trash 88 out
// on the diagonal.
const r: dvui.Rect.Physical = .{ .x = 0, .y = 0, .w = 800, .h = 600 };
try std.testing.expectEqual(@as(?Kind, .swap), kindAt(r, .{ .x = 400, .y = 300 }, 1, false));
diff --git a/app/layout/Floats.zig b/app/layout/Floats.zig
index 91ce807ef..97deaa34d 100644
--- a/app/layout/Floats.zig
+++ b/app/layout/Floats.zig
@@ -4,8 +4,8 @@
//! or closed from its header, it goes back.
//!
//! A float is a place like any other: its name keys its assignment, its splits (`Float 1/r1`)
-//! live in the split forest, and the saved layout keeps it. What only a float has — where its
-//! window is, where it came from, how it is landing or closing — is kept here, in z order. The
+//! live in the split forest, and the saved layout keeps it. What only a float has - where its
+//! window is, where it came from, how it is landing or closing - is kept here, in z order. The
//! rules it follows are values in `float_rules.zig`.
const std = @import("std");
const dvui = @import("dvui");
@@ -56,23 +56,23 @@ pub const Float = struct {
closing: bool = false,
/// Its window's id, once it has drawn: what dvui's subwindow stack knows it by.
win_id: dvui.Id = .zero,
- /// Its window last frame, physical — what a drag reads occlusion against.
+ /// Its window last frame, physical - what a drag reads occlusion against.
bounds: dvui.Rect.Physical = .{},
- /// Its header last frame, physical — the handle that moves it. Nothing is aimed at over it.
+ /// Its header last frame, physical - the handle that moves it. Nothing is aimed at over it.
header: dvui.Rect.Physical = .{},
/// Its header's close button last frame, physical: the part of the header that does not move
- /// it — what the OS leaves to the app when it moves a popped-out float's window by its header.
+ /// it - what the OS leaves to the app when it moves a popped-out float's window by its header.
header_close: dvui.Rect.Physical = .{},
- /// Its title last frame — the view it shows, as its header says (`titleText`): what its OS
+ /// Its title last frame - the view it shows, as its header says (`titleText`): what its OS
/// window is called, out of the main window.
title_buf: [96]u8 = undefined,
title_len: u8 = 0,
/// A ghost of itself while a view carried out of it is aimed elsewhere (`ViewDrag.ghosted`): 0
/// itself, 1 its ghost (`ghostLook`), and on to `gone` when it closes as one. As a ghost it
- /// still draws its place — the drag is held by that place's corner button — but nothing of the
+ /// still draws its place - the drag is held by that place's corner button - but nothing of the
/// live float shows: its photograph is what does.
aside: Fade = .{},
- /// The float as the drag found it — window, glass, view and shadow — photographed from the
+ /// The float as the drag found it - window, glass, view and shadow - photographed from the
/// frame at the lift (`liftedFrom`): what fades to the ghost, faint and out of focus, and back
/// whenever the view is aimed at it again or let go over nothing. A photograph, not the live
/// float under an alpha: a view may draw in ways no alpha reaches (its own triangles, an alpha
@@ -107,7 +107,7 @@ pub const Viewport = struct {
/// what stands behind the glass (`Popout.backing`) is the main window's base over it, as
/// translucent as the main window. Without one, that is opaque.
material: bool = false,
- /// Its OS window is framed by the OS — its corners, shadow and resizing from its edges (macOS's
+ /// Its OS window is framed by the OS - its corners, shadow and resizing from its edges (macOS's
/// titled window, DWM on Windows): the window is the float's glass, and the float resizes
/// nothing itself.
os_frame: bool = false,
@@ -117,10 +117,10 @@ pub const Viewport = struct {
/// Natural units from its window's left edge past the OS's own buttons (`os_buttons`), this
/// frame (`Popout`): the part of its header that is theirs, not the float's to be moved by.
buttons_w: f32 = 0,
- /// The OS asked to close its window (its close button, ⌘W): it closes as from its header.
+ /// The OS asked to close its window (its close button, CmdW): it closes as from its header.
close_asked: bool = false,
/// Physical: from where the float is drawn in its band to where its window is over the main
- /// window's frame, this frame (`Popout`) — for a drag to read the window where it lies over the
+ /// window's frame, this frame (`Popout`) - for a drag to read the window where it lies over the
/// main window's places (`ViewDrag.mapOccluders`).
main_delta: dvui.Point.Physical = .{},
/// Its window lies under the main window in the OS's stacking (`Popout`): a float a window like
@@ -140,8 +140,8 @@ fn osButtons(f: Float) bool {
return false;
}
-/// Natural units a float out of the main window draws past its window rect — its shadow's reach,
-/// less the margin its rect already holds — and its OS window holds round it, clear: the float
+/// Natural units a float out of the main window draws past its window rect - its shadow's reach,
+/// less the margin its rect already holds - and its OS window holds round it, clear: the float
/// draws the shadow it draws round its glass in the main window, and looks the same in either.
/// Its rect is the same size out as in, so going out and coming back moves it and nothing else.
pub fn outReach() f32 {
@@ -202,11 +202,11 @@ pub const Fade = struct {
pub const aside_ms: f32 = 300;
/// A float's ghost: how much of it shows, and how far it is out of focus (0 sharp, 1 its whole
-/// frost) — enough to say where it is and that it is coming back, little enough that what it lies
+/// frost) - enough to say where it is and that it is coming back, little enough that what it lies
/// over reads through it.
const ghost_alpha: f32 = 0.22;
const ghost_blur: f32 = 0.55;
-/// Where `Float.aside` goes for a ghost closing — its last view landed elsewhere: past the ghost,
+/// Where `Float.aside` goes for a ghost closing - its last view landed elsewhere: past the ghost,
/// to nothing.
const gone: f32 = 2;
/// The leading traffic light of a titled macOS window, points: its diameter, and how far its left
@@ -216,7 +216,7 @@ const traffic_light_diameter: f32 = 12;
const traffic_light_inset: f32 = 7;
/// How a float looks `v` of the way aside (`Float.aside`): itself at 0, its ghost at 1, nothing
-/// at `gone` — how much of it shows, and how far out of focus.
+/// at `gone` - how much of it shows, and how far out of focus.
pub fn ghostLook(v: f32) struct { alpha: f32, blur: f32 } {
const in = smoothstep(std.math.clamp(v, 0, 1));
const out = smoothstep(std.math.clamp(v - 1, 0, 1));
@@ -283,14 +283,14 @@ fn dropAsidePhoto(f: *Float) void {
pub fn endLanding(f: *Float) void {
const l = f.landing orelse return;
if (l.photo) |tex| {
- // Destroyed with the frame — and only in one. With no window (teardown) the GPU is going
+ // Destroyed with the frame - and only in one. With no window (teardown) the GPU is going
// with the process.
if (dvui.current_window != null) dvui.Texture.destroyLater(tex);
}
f.landing = null;
}
-/// Every float gone — Reset Layout.
+/// Every float gone - Reset Layout.
pub fn clear(self: *Floats) void {
for (self.items.items) |*f| {
endLanding(f);
@@ -328,7 +328,7 @@ pub fn fromRules(r: rules.Rect) dvui.Rect {
return .{ .x = r.x, .y = r.y, .w = r.w, .h = r.h };
}
-// ── Its ghost ───────────────────────────────────────────────────────────────────────────────────
+// -- Its ghost -----------------------------------------------------------------------------------
/// A drag has just lifted `view` out of place `place`. When that place is a float's (or a split
/// of one), photograph the float now, while the frame it was last drawn in shows it whole and
@@ -350,8 +350,8 @@ pub fn liftedFrom(l: *Layout, place: []const u8, view: []const u8) void {
/// The photograph of a float fading to its ghost, `blur` of the way to its frost and at `alpha`, in
/// the window's own rounded shape with its shadow round it fading too. As `core.anim.blit` mixes
-/// a snapshot with its frost — the sharp picture over the frost, at what makes the two `alpha` of
-/// the mix between them — but cut to the window's corners, so the defocus stays the window's.
+/// a snapshot with its frost - the sharp picture over the frost, at what makes the two `alpha` of
+/// the mix between them - but cut to the window's corners, so the defocus stays the window's.
fn drawAsidePhoto(photo: *Photo, blur: f32, alpha: f32) void {
const a = std.math.clamp(alpha, 0, 1);
if (a <= 0.001) return;
@@ -392,7 +392,7 @@ fn holdsView(l: *Layout, name: []const u8, view: []const u8) bool {
return false;
}
-// ── Closing ─────────────────────────────────────────────────────────────────────────────────────
+// -- Closing -------------------------------------------------------------------------------------
pub const Closing = enum {
/// Its last view left it: there is nothing to send anywhere.
@@ -424,7 +424,7 @@ pub fn flyShut(state: *Layout.State, i: usize) void {
const f = &state.floats.items.items[i];
if (f.closing) return;
endLanding(f);
- // Marked, not removed: `draw` may be walking the list, and drops it when it comes to it —
+ // Marked, not removed: `draw` may be walking the list, and drops it when it comes to it -
// at once if it never drew a window or was a ghost, after the flight shut if it did.
f.closing = true;
// Out of the main window, its OS window just goes, as any window does when closed: the
@@ -442,12 +442,12 @@ pub fn flyShut(state: *Layout.State, i: usize) void {
dvui.refresh(null, @src(), null);
}
-// ── Drawing ─────────────────────────────────────────────────────────────────────────────────────
+// -- Drawing -------------------------------------------------------------------------------------
-/// How long a float takes to grow out of the carried glass into its window, as written — the
+/// How long a float takes to grow out of the carried glass into its window, as written - the
/// card's own change of shape (`ViewDrag.morphProgress`) and a dialog's open.
const landing_ms: f32 = 300;
-/// How opaque the landing photograph is at its start — the carried card's own (`ViewDrag`).
+/// How opaque the landing photograph is at its start - the carried card's own (`ViewDrag`).
const photo_opacity: f32 = 0.8;
/// How far a landing has got in time: 0 at the release, 1 in its window.
@@ -464,7 +464,7 @@ pub fn landedAt(land: Landing) f32 {
/// Draw every float, bottom to top: each a glass window holding its place. Called by the
/// application after its shape has run and before it publishes the shape's regions, from the
/// base window (`Layout.drawFloats`), so a float's places register this frame like any other's
-/// — each stamped with its float's layer, the `n`th from the bottom being `n`.
+/// - each stamped with its float's layer, the `n`th from the bottom being `n`.
pub fn draw(l: *Layout) void {
const floats = &l.state.floats;
if (floats.items.items.len == 0) return;
@@ -502,7 +502,7 @@ fn drawOne(l: *Layout, i: usize) bool {
const scale = cw.natural_scale;
if (state.floats.items.items[i].closing and state.floats.items.items[i].win_id == .zero) return false;
// Not drawn before: made this frame by a release inside the place it left, which has drawn
- // that view already this frame — or brought back from a saved layout before any place has
+ // that view already this frame - or brought back from a saved layout before any place has
// said it is not theirs. Its place goes in the registry, so from the next frame it claims the
// view away from where it was; the view in it starts then. Drawn now, the view would be drawn
// twice in one frame.
@@ -521,7 +521,7 @@ fn drawOne(l: *Layout, i: usize) bool {
.by_name = true,
});
// Brought back, its window starts with its view, next frame. Landing, its window is the
- // carried glass from this frame on — glass and photograph, the view not yet — so the drop
+ // carried glass from this frame on - glass and photograph, the view not yet - so the drop
// let go never leaves a frame with nothing where it was. And the window's own first frame,
// which lays its header out from nothing and makes its glass, is this one, not one of the
// landing's.
@@ -538,12 +538,12 @@ fn drawOne(l: *Layout, i: usize) bool {
if (f.landing) |*land| land.clock.step(now);
if (!f.closing) {
f.aside.toward(if (ghosted) 1 else 0, now);
- // The drag is over and the view is not in it any more — it landed elsewhere: the
+ // The drag is over and the view is not in it any more - it landed elsewhere: the
// photograph is of what the float was, so it comes back as it is now, the live float
// fading in.
if (!carried_out and f.aside_photo != null and !holdsView(l, f.name, f.aside_view)) dropAsidePhoto(f);
} else if (f.aside.to > 0 or f.aside.at() > 0.01) {
- // Closing as a ghost — its last view landed elsewhere: what is left of it finishes
+ // Closing as a ghost - its last view landed elsewhere: what is left of it finishes
// going, then so does it, with no flight shut.
f.aside.toward(gone, now);
}
@@ -554,14 +554,14 @@ fn drawOne(l: *Layout, i: usize) bool {
const closing_aside = first.closing and first.aside.to > 0;
if (closing_aside and (first.aside_photo == null or aside >= gone)) return false;
// A ghost, on its way to or from one, or going: its photograph is what shows, and nothing of
- // the live float. By where it is heading as well as where it is — the frame it sets off it
+ // the live float. By where it is heading as well as where it is - the frame it sets off it
// has not moved yet.
const stepping = first.aside.to > 0 or aside > 0;
const as_photo = first.aside_photo != null and (closing_aside or stepping);
- // Back for good: the live float again. Not while the drag is on — it may go back to its ghost.
+ // Back for good: the live float again. Not while the drag is on - it may go back to its ghost.
if (!as_photo and first.aside_photo != null and !carried_out) dropAsidePhoto(&state.floats.items.items[i]);
- // With no photograph it is the live float that fades to its ghost — its view whole, where it
- // can be drawn into a picture of itself (`Whole`), though not out of focus — and gone, nothing
+ // With no photograph it is the live float that fades to its ghost - its view whole, where it
+ // can be drawn into a picture of itself (`Whole`), though not out of focus - and gone, nothing
// of it shows.
// In an OS window of its own, the window fades whole, its material and all (`Popout`): its
// content stays as it is in it.
@@ -591,7 +591,7 @@ fn drawOne(l: *Layout, i: usize) bool {
// forming a second time. Fading to its ghost under its alpha, the glass dissolves as a closing
// window's does; as a photograph, the window draws no glass at all.
// In an OS window the OS frames (`Viewport.os_frame`), it is a window as the main window is,
- // drawn as it is: no glass and no shadow of its own — the window's base stands under it, its
+ // drawn as it is: no glass and no shadow of its own - the window's base stands under it, its
// chrome at the window's opacity over the window's material (`Popout.backing`), and the OS
// draws the shadow. Imitating a glass window in the main window out there, its frost had to read
// the main window's picture behind it, which the OS moves the window over faster than fizzy can
@@ -602,7 +602,7 @@ fn drawOne(l: *Layout, i: usize) bool {
var frost = if (as_photo or plain) null else dialogs.dialogFrost();
if (frost) |*fr| {
fr.form = shown;
- // Landing, it grows into its window — past it and back, when motion is playful — and its
+ // Landing, it grows into its window - past it and back, when motion is playful - and its
// glass is captured that size from the start.
if (landing) {
const grown = scale * (1 + core.motion.overshoot_max);
@@ -611,15 +611,15 @@ fn drawOne(l: *Layout, i: usize) bool {
}
var shadow = dialogs.surfaceShadow();
shadow.alpha *= shown;
- // Everything else the window draws — its fill, header, the view — fades with it.
+ // Everything else the window draws - its fill, header, the view - fades with it.
const prev_alpha_window = dvui.alpha(shown);
defer dvui.alphaSet(prev_alpha_window);
// Held by the pointer as the frame begins: the user is moving or resizing it. Read before the
- // window runs, because it lets go of the pointer on the release while it handles its events —
+ // window runs, because it lets go of the pointer on the release while it handles its events -
// a last move and the release in one frame would otherwise read as not held, and snap back.
const held_before = first.win_id != .zero and dvui.captured(first.win_id);
var win_rect = rect;
- // One window, one header and one place, whatever it is showing — the same widgets, so the
+ // One window, one header and one place, whatever it is showing - the same widgets, so the
// float keeps its place among the others and its view keeps its state through a drag.
var win = core.widgets.floatingWindow(@src(), .{
.rect = &win_rect,
@@ -646,7 +646,7 @@ fn drawOne(l: *Layout, i: usize) bool {
drawAsidePhoto(&state.floats.items.items[i].aside_photo.?, look.blur, look.alpha);
}
- // Flying shut: the glass alone, gone when it lands — or, a ghost, its photograph alone.
+ // Flying shut: the glass alone, gone when it lands - or, a ghost, its photograph alone.
if (first.closing) {
const flown = if (dvui.animationGet(win_id, "_close_x")) |a| a.done() else true;
// Out of the main window, its OS window shuts with it, placed from its rect as ever
@@ -666,9 +666,9 @@ fn drawOne(l: *Layout, i: usize) bool {
if (dvui.eventMatch(e, .{ .id = win_id, .r = bounds })) dvui.raiseSubwindow(win_id);
};
- // A ghost, its header and place are drawn as ever — the place's corner button holds the drag —
+ // A ghost, its header and place are drawn as ever - the place's corner button holds the drag -
// but clipped to nothing, so nothing of them shows however the view draws. Fresh, the header
- // too — laid out, for the next frame to draw it where it goes — and the place not at all.
+ // too - laid out, for the next frame to draw it where it goes - and the place not at all.
const prev_clip_live = dvui.clipGet();
if (hide_live or fresh) dvui.clipSet(.{});
var open = true;
@@ -696,8 +696,8 @@ fn drawOne(l: *Layout, i: usize) bool {
open = false;
}
}
- // Moved by its header only: the rest is the view's. Not while it lands — it is going where
- // the drop put it — nor while it is out of the way. Not over its OS window's own buttons
+ // Moved by its header only: the rest is the view's. Not while it lands - it is going where
+ // the drop put it - nor while it is out of the way. Not over its OS window's own buttons
// either, which are theirs: the move cursor showed over the traffic lights.
const buttons_w = if (first.viewport) |vp| (if (vp.os_buttons) vp.buttons_w * scale else 0) else 0;
const drag_area: dvui.Rect.Physical = .{ .x = header.x + buttons_w, .y = header.y, .w = @max(0, header.w - buttons_w), .h = header.h };
@@ -707,7 +707,7 @@ fn drawOne(l: *Layout, i: usize) bool {
// The view fades with the window round it: in over the photograph it grew out of as it
// lands; in when the float comes back from a drag without the view that landed
// elsewhere; out when it fades to its ghost with no photograph to fade as. It fades
- // whole — drawn into a picture of itself, laid down at the fade (`Whole`) — since a view
+ // whole - drawn into a picture of itself, laid down at the fade (`Whole`) - since a view
// may draw where no alpha reaches (the workbench's home page sets its own; a view drawing
// its own triangles is handed alpha to apply itself), and under an alpha alone that much
// of it was there at once, ahead of its window. With nothing to draw into (no targets, a
@@ -720,7 +720,7 @@ fn drawOne(l: *Layout, i: usize) bool {
const prev_alpha = if (whole != null) dvui.alpha(1) else dvui.alpha(view_fade);
if (whole != null) dvui.alphaSet(1);
// Inset from the glass's sides and foot by its corner radius: a view that draws to its
- // edges — a document's canvas and rulers — would otherwise run flush to the glass and
+ // edges - a document's canvas and rulers - would otherwise run flush to the glass and
// under its rounded corners. The header is its top. In the shape's card where it has one
// (`Layout.card`), as the window's places are: the glass then frames the card as the
// window's fill frames its own, and the places a split makes, and the panes a workbench
@@ -765,7 +765,7 @@ fn drawOne(l: *Layout, i: usize) bool {
if (held and !f.closing and !win_rect.equals(vp.rect)) vp.rect = fromRules(rules.resized(toRules(win_rect)));
return true;
}
- // Moved or resized by the user: remember where, no smaller than a float may be. Only theirs —
+ // Moved or resized by the user: remember where, no smaller than a float may be. Only theirs -
// the window holds a float on screen when it shrinks (`FloatingWindowWidget`), and that is
// shown, not kept, so the float is back where they left it when the window grows again.
if (held and !landing and !f.closing and !win_rect.equals(f.rect)) {
@@ -799,7 +799,7 @@ const Whole = struct {
};
/// The photograph the float grew out of, over its body (below `header`), cropped to fill it, at
-/// `fade` of its carried opacity — in its window, or the carried glass its OS window grows out of
+/// `fade` of its carried opacity - in its window, or the carried glass its OS window grows out of
/// (`Popout`). `radius` is physical.
pub fn drawPhoto(tex: dvui.Texture, size: dvui.Size.Physical, bounds: dvui.Rect.Physical, header: dvui.Rect.Physical, radius: f32, fade: f32) void {
if (fade <= 0.01) return;
diff --git a/app/layout/Layout.zig b/app/layout/Layout.zig
index 358ba8fec..927c9158a 100644
--- a/app/layout/Layout.zig
+++ b/app/layout/Layout.zig
@@ -1,4 +1,4 @@
-//! What an app's shape declares its regions with — the whole of how an app lays itself out.
+//! What an app's shape declares its regions with - the whole of how an app lays itself out.
//!
//! A region says which keywords it accepts; a plugin's `sdk.Surface` says which it carries. The
//! intersection is the match set the region draws, so neither side names the other and an app
@@ -7,26 +7,26 @@
//! The three pieces, all in this directory: `Layout` is the live view (which surfaces exist,
//! which match a region, which is selected), `Region.zig` is a named area accepting keywords,
//! and fizzy's own shape lives in `src/editor/layout.zig`. The resizable division
-//! between two regions is `core.widgets.Split`, and a tab strip is `core.widgets.Tabs` — both in
+//! between two regions is `core.widgets.Split`, and a tab strip is `core.widgets.Tabs` - both in
//! `core` rather than here because a plugin dylib draws the same ones the app does.
//!
//! ## Who draws the tabs
//!
//! There are two forms, both supported, and the difference is *what the tabs represent*:
//!
-//! **The app draws them** when the tabs represent surfaces — several plugins each contributing
+//! **The app draws them** when the tabs represent surfaces - several plugins each contributing
//! a pane to one region. The app owns the strip because no single plugin can: they must share
//! it. Fizzy's bottom panel is this. A shape writes `f.tabs(kw)` then draws the selected
//! surface, or passes `.content = Layout.tabbed` to the region.
//!
-//! **The plugin draws them** when the tabs represent something only the plugin knows about —
+//! **The plugin draws them** when the tabs represent something only the plugin knows about -
//! its own documents, timelines, layers. Then the plugin registers *one* surface and draws the
//! entire region: its own tab strip, its own splits, its own content. Fizzy's main area is
//! already exactly this: `workbench` registers one surface and draws document tabs and splits
//! inside it (`Workspace.drawTabs`), and neither fizzy nor any shape knows how many tabs there
//! are.
//!
-//! Nothing distinguishes the two at registration — a surface is a surface, and drawing a tab
+//! Nothing distinguishes the two at registration - a surface is a surface, and drawing a tab
//! strip inside your own area needs no permission.
//!
//! ## The shape of a layout
@@ -48,14 +48,14 @@
//! One object with two verbs: declare a region, or put a draggable split between the last one
//! and the next. Reads top to bottom, no `showFirst` / `showSecond` / `rest()` branching, and N
//! regions on an axis rather than a forced tree of two-child panes. Nesting is how you cross
-//! axes — a region's content can be another layout.
+//! axes - a region's content can be another layout.
//!
-//! A layout mixes `region` / `split` with whatever dvui the app wants to draw — a menu, an
+//! A layout mixes `region` / `split` with whatever dvui the app wants to draw - a menu, an
//! infobar, a rail. The app's own pointer arrives as `ctx` (`Host.layout_ctx`; fizzy passes
//! `*Editor`). `region` still does double duty: with keywords it is a place surfaces draw;
//! without, it is a container for more regions.
//!
-//! `dir` is what a container region orients — **its child regions and splits**, not content. A
+//! `dir` is what a container region orients - **its child regions and splits**, not content. A
//! horizontal region lays its children left to right, and a `split` inside it is therefore a
//! vertical bar you drag left and right. The split inherits the containing region's axis rather
//! than restating it, so direction is declared in exactly one place; an explicit `dir` on a
@@ -63,7 +63,7 @@
//!
//! ## How a region gets its size
//!
-//! From `expand`, which is dvui's existing meaning rather than a new concept — so there is no
+//! From `expand`, which is dvui's existing meaning rather than a new concept - so there is no
//! separate sizing vocabulary to learn, and the three cases fall out of one field:
//!
//! **Fit to content.** A region that does not expand along its parent's axis takes its size from
@@ -73,13 +73,13 @@
//!
//! **Stretch, with a draggable boundary.** `.expand = .both` on both neighbours means neither
//! has an opinion, so the `split` between them owns the boundary and persists it. A split
-//! position is only meaningful when both sides stretch — which is why size belongs on the split
+//! position is only meaningful when both sides stretch - which is why size belongs on the split
//! rather than on the region.
//!
//! **Fixed.** Fit-to-content plus a minimum: the icon rail is `.expand = .vertical` with a 40pt
//! minimum width, and needs no split at all.
//!
-//! The hazard in fit-to-content is that it hands size control to whatever plugin draws there —
+//! The hazard in fit-to-content is that it hands size control to whatever plugin draws there -
//! a surface with a wide minimum makes the region wide. So a fitting region should carry a
//! `max_size` guard, again dvui's existing `max_size_content`. An app that does not want a
//! plugin dictating its proportions uses the stretch form instead.
@@ -88,28 +88,28 @@
//!
//! **App authors** use only this: `region` and `split`, with keywords assigned per region. They
//! never touch dvui. That is the whole point of the level existing, and it is why the surface
-//! is two verbs rather than a widget toolkit — a small enough vocabulary to hold in your head
+//! is two verbs rather than a widget toolkit - a small enough vocabulary to hold in your head
//! and, prospectively, to check at comptime (a layout that declares a region twice, or splits
//! outside a container, is a compile error rather than a confusing frame).
//!
//! **Plugin authors** work a level down: raw dvui for their own content, plus fizzy's mid-level
-//! constructs where one exists — `core.widgets.CanvasWidget` for zoom/pan surfaces, `Tabs`, the
+//! constructs where one exists - `core.widgets.CanvasWidget` for zoom/pan surfaces, `Tabs`, the
//! dialog chrome in `core.dialogs.dialog`, scroll areas with edge shadows, context menus. Those
//! are content blocks, not layout, and they live in `core` precisely so a dylib can reach them.
//!
//! It also leaves room for the thing this is ultimately for: once regions are the only unit, a
//! region can be dragged to move or re-split it at runtime, which is how a Premiere- or
-//! Blender-style app would work. Fizzy itself stays rigid — its shape is fixed by
-//! `src/editor/layout.zig` — but nothing in the model prevents an app from letting the user rearrange.
+//! Blender-style app would work. Fizzy itself stays rigid - its shape is fixed by
+//! `src/editor/layout.zig` - but nothing in the model prevents an app from letting the user rearrange.
//!
//! ## Layered regions and blur
//!
//! A tray that blurs what is behind it (the bottom panel over the editor; a scroll edge over its
-//! own overflowing content) is designed but not built — see `LAYERS.md` in this directory. The
+//! own overflowing content) is designed but not built - see `LAYERS.md` in this directory. The
//! short version, because it is the decision most likely to be re-derived wrongly: blur is a
//! **property of a region** (`.blur_behind`), never a layout verb that reverses render order. An
//! app author must not have to reason about paint order to place a panel, and reversing paint
-//! order does not reverse dvui's event routing — declaration order and hit-test order would stop
+//! order does not reverse dvui's event routing - declaration order and hit-test order would stop
//! agreeing.
//!
//! Note the core pieces come through the named `core` module, never by relative path: a file may
@@ -135,14 +135,14 @@ pub const center_keywords = sdk.keywords.ide.main;
pub const Surface = sdk.Surface;
-/// The app's pointer for this frame — `Host.layout_ctx`, or fizzy's `*Editor`.
+/// The app's pointer for this frame - `Host.layout_ctx`, or fizzy's `*Editor`.
/// Same `?*anyopaque` as `Surface.draw` and `Region.Content`.
ctx: ?*anyopaque = null,
/// The registries this layout matches against and selects in.
host: *sdk.Host,
/// Per-frame scratch: match lists handed to a shape live until the frame ends.
arena: std.mem.Allocator,
-/// What persists between frames — the declared regions, their extents, the user's keyword
+/// What persists between frames - the declared regions, their extents, the user's keyword
/// overrides. Owned by the application; a `Layout` is per-frame and this is not.
state: *State,
/// Long-lived allocations the state makes (a remembered extent's name).
@@ -152,14 +152,14 @@ gpa: std.mem.Allocator,
form_factor: core.platform.FormFactor = .desktop,
/// The card a place wears when it asks for one (`Region.InitOptions.card`): its fill, corners and
/// padding. Set by the shape before it declares its places, and worn by the floats' places too,
-/// which are drawn after the shape — so a view, and the places a split makes, look the same in a
+/// which are drawn after the shape - so a view, and the places a split makes, look the same in a
/// float as in the window. Null, no card.
card: ?dvui.Options = null,
/// Set when a region's remembered extent changed this frame. The application decides what that
-/// means — fizzy debounces a write to `layout.zon`.
+/// means - fizzy debounces a write to `layout.zon`.
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.
+/// nowhere - as opposed to drew before the picker asked. Arena-backed, per frame.
drawn: std.ArrayList(*Surface) = .empty,
/// Views a plugin asked to put away during this frame (`hideLater`), put away once it has drawn
/// (`applyHides`). Arena-backed, per frame.
@@ -174,7 +174,7 @@ unsized: std.ArrayList(*Surface) = .empty,
containers: [max_nesting]Container = undefined,
depth: usize = 0,
-/// Open regions declared by *plugins*, innermost last — see `beginPluginRegion`. A separate
+/// Open regions declared by *plugins*, innermost last - see `beginPluginRegion`. A separate
/// stack because these are the ones nobody scoped with a `defer`: the app holds them on the
/// plugin's behalf between two vtable calls. Bounded by `max_nesting` for the same reason the
/// container stack is: a layout this deep is a mistake to report, not a case to support.
@@ -186,21 +186,21 @@ pub const PendingSplit = struct { src: std.lang.SourceLocation, opts: Split.Opti
pub const Container = struct {
dir: dvui.enums.Direction,
/// The dotted name of the region this container *is*, which the regions declared inside it
- /// qualify their keywords with — `"main"` here makes a child's `{"document"}` into
+ /// qualify their keywords with - `"main"` here makes a child's `{"document"}` into
/// `{"main.document"}`. Empty for a container that accepts nothing itself: a pure grouping
/// box is not a place, so it passes its own parent's name through rather than inventing a
/// level of vocabulary a shape never wrote.
///
/// A region's *first* keyword, not all of them. `main` names the same place as `center` and
/// `workspace`, and qualifying under every synonym would multiply the vocabulary by three to
- /// say one thing — while `accepts` already lets a surface reach the sub-place by kind
+ /// say one thing - while `accepts` already lets a surface reach the sub-place by kind
/// (`document`) without naming any ancestor at all.
prefix: []const u8 = "",
- /// The base region's declared minimum along the axis — `min_size_content` on the region in
+ /// The base region's declared minimum along the axis - `min_size_content` on the region in
/// this container that is not resizable. The app declares it; the framework only reads it.
base_min: f32 = 0,
/// Every resizable region in this container. A split needs them all so `push_out` can
- /// shrink the trays behind the one being dragged. Arena-backed, this frame only — no count cap.
+ /// shrink the trays behind the one being dragged. Arena-backed, this frame only - no count cap.
resizables: std.ArrayList(dvui.Id) = .empty,
/// Packed splits in this container, each `Split.handle_size`. The drag
/// budget subtracts this so a handle is never eaten by a tray.
@@ -208,7 +208,7 @@ pub const Container = struct {
/// The container's own box, for measuring how near the pointer is to a split inside it.
box: ?*dvui.BoxWidget = null,
/// A split declared and not yet drawn. A split is a boundary *between* two regions, so it is
- /// drawn when the region after it opens — and not at all if that region declines to exist
+ /// drawn when the region after it opens - and not at all if that region declines to exist
/// (`hide_when_empty`). Drawing it eagerly left a handle with nothing behind it, still
/// draggable, still resizing a region that was not there.
pending_split: ?PendingSplit = null,
@@ -217,14 +217,14 @@ pub const Container = struct {
card: ?dvui.Options = null,
/// The panes (`beginPluginRegion`) opened directly in this region so far this frame.
panes: u8 = 0,
- /// The most recent resizable child. A `split` drags *this* region's stored extent — the
- /// neighbour before it — which is the whole of the resize mechanism: there are no ratios and
+ /// The most recent resizable child. A `split` drags *this* region's stored extent - the
+ /// neighbour before it - which is the whole of the resize mechanism: there are no ratios and
/// no boundary table, just one number per resizable region.
last_resizable: ?dvui.Id = null,
/// A non-resizable child has been declared (the leftover, or a grouping box around it).
saw_base: bool = false,
- /// How far this container reaches along `axis`, in points — its width when horizontal, its
+ /// How far this container reaches along `axis`, in points - its width when horizontal, its
/// height when vertical. The same single number a region calls its extent, measured for the
/// thing regions sit inside.
pub fn extent(self: *Container, axis: dvui.enums.Direction) f32 {
@@ -245,7 +245,7 @@ pub const max_nesting = 16;
/// word, so the new side stays empty until the picker fills it.
pub const slot_keywords: []const []const u8 = &.{"slot"};
-// How long a region takes to fold away or come back is `core.anim.slide` — one home for the
+// How long a region takes to fold away or come back is `core.anim.slide` - one home for the
// timing, because a sidebar and a document pane travelling at different speeds reads as broken
// without ever looking wrong in a screenshot.
@@ -271,7 +271,7 @@ pub const handle_dist = Split.handle_dist;
// Points rather than a fraction of the parent, and per region rather than a table of boundaries,
// because that is what `dvui.box` already understands: a child that does not expand along the
// axis takes its minimum, and the ones that do share the remainder. Reusing that means there is
-// no second sizing model — a fixed icon rail, a dragged sidebar and a stretching main area are
+// no second sizing model - a fixed icon rail, a dragged sidebar and a stretching main area are
// the same mechanism with different numbers, and a window resize grows the stretchy half rather
// than rescaling the sidebar.
@@ -279,7 +279,7 @@ pub const handle_dist = Split.handle_dist;
/// chose and the keywords decide.
///
/// Callers hand over keywords, not a region name, because that is what a shape and a pane both
-/// have in hand (`f.matching(keywords)`); the region is found in the registry by keyword group —
+/// have in hand (`f.matching(keywords)`); the region is found in the registry by keyword group -
/// this frame's set first, since a region registers before it draws its contents, then last
/// frame's for anything asked between shapes. Two regions declared with identical keywords share
/// an assignment, exactly as they already share a selection.
@@ -329,7 +329,7 @@ fn orderName(self: *Layout, r: *const Region) []const u8 {
}
/// `items` with the views `State.order` names for `name` first, in that order, and the rest
-/// after in the order they came. An order never adds or drops a view — which views a place
+/// after in the order they came. An order never adds or drops a view - which views a place
/// holds is its assignment's or its keywords' business.
///
/// A place keeps its order: the first time it shows a view its order does not name, the view is
@@ -423,7 +423,7 @@ fn visibleNow(self: *Layout, s: *const Surface) bool {
return false;
}
-/// `matchingIn` with takeovers left out — the list a *trigger* is chosen from.
+/// `matchingIn` with takeovers left out - the list a *trigger* is chosen from.
fn plainMatchingIn(self: *Layout, r: *const Region) []const *Surface {
const assigned = if (r.by_name) self.state.assignment(r.name) else self.assignedFor(r.keywords);
var out: std.ArrayList(*Surface) = .empty;
@@ -448,12 +448,12 @@ fn plainMatchingIn(self: *Layout, r: *const Region) []const *Surface {
return out.items;
}
-/// Whether a plugin has declared a place made for `s` — a region of its own (`host.region`) that
+/// Whether a plugin has declared a place made for `s` - a region of its own (`host.region`) that
/// accepts `s`'s kind. An open document is the case: the Workspace's panes accept `document`, and
/// that is where a document lives, as a tab.
///
/// **Tab content is not a place's content.** A document drawn straight into Main or the sidebar
-/// would have no tab, no split, and no way for Save or Undo to find it — they act on the active
+/// would have no tab, no split, and no way for Save or Undo to find it - they act on the active
/// document, and only a pane has one. So a plain place (Main, the sidebar, the panel, a split
/// leaf) neither matches such a surface by keyword, nor shows it by assignment, nor takes it in a
/// drop, and its picker does not offer it. An app with no such slot (one not using the Workspace)
@@ -477,8 +477,8 @@ pub fn offers(self: *Layout, r: *const Region, s: *const Surface) bool {
/// Is `s` at home in some region other than the one with `keywords`?
///
-/// **A surface is drawn in one place.** It has one `ctx` and one set of state behind it — a
-/// tree's selection, a scroll position, what has focus, the keys it answers — so two copies in
+/// **A surface is drawn in one place.** It has one `ctx` and one set of state behind it - a
+/// tree's selection, a scroll position, what has focus, the keys it answers - so two copies in
/// one frame would be two widgets fighting over the same state. Where it lives, in order:
///
/// 1. Where it is assigned. An assignment is a claim: Files dragged onto Main leaves the
@@ -500,7 +500,7 @@ fn claimedElsewhere(
) bool {
const want = sdk.keywords.groupKey(keywords);
// A plain place's assignment claims nothing a slot was made for (`slotted`): it cannot show
- // it, and a stale entry there — a layout saved before that rule — would otherwise take a
+ // it, and a stale entry there - a layout saved before that rule - would otherwise take a
// document away from its pane and leave it drawn nowhere.
const plain_cannot = self.slotted(s);
var it = self.declaredRegions();
@@ -536,7 +536,7 @@ fn claimedElsewhere(
/// frame's that this frame has not reached yet.
///
/// Both halves, not whichever is non-empty. A region is registered before it draws, so while the
-/// sidebar draws, the regions declared after it — Main among them — exist only in last frame's
+/// sidebar draws, the regions declared after it - Main among them - exist only in last frame's
/// set. Reading this frame's alone missed every claim made further down the shape: Files
/// dragged from the sidebar onto Main went on drawing in the sidebar too, until something else
/// was picked there.
@@ -579,7 +579,7 @@ const DeclaredRegions = struct {
}
};
-/// A surface by id, regardless of keywords — how an app places a plugin it ships with and
+/// A surface by id, regardless of keywords - how an app places a plugin it ships with and
/// therefore knows by name (fizzy does this for `workbench.panes`).
pub fn surface(self: *Layout, id: []const u8) ?*Surface {
return self.host.surfaceById(id);
@@ -617,8 +617,8 @@ pub const Hide = union(enum) {
/// 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.
+/// 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;
@@ -672,7 +672,7 @@ pub fn applyHides(self: *Layout) bool {
return any;
}
-/// Surfaces that appear in no region of the last completed shape — neither assigned to one nor
+/// 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.
pub fn unplaced(self: *Layout) []const *Surface {
@@ -711,8 +711,8 @@ pub fn select(self: *Layout, keywords: []const []const u8, s: *const Surface) vo
self.host.setSelectionFor(keywords, s.id);
}
-/// `selected` / `select` for a specific region. A by-name region keeps its own selection —
-/// two document panes must not share an active tab — under a key that folds the name into the
+/// `selected` / `select` for a specific region. A by-name region keeps its own selection -
+/// two document panes must not share an active tab - under a key that folds the name into the
/// keyword group's; an app region's key is the keyword group's alone, so a chooser written
/// against keywords (the icon rail) and the region it chooses for still agree.
pub fn selectedIn(self: *Layout, r: *const Region) ?*Surface {
@@ -723,8 +723,8 @@ pub fn selectIn(self: *Layout, r: *const Region, id: []const u8) void {
self.host.setSelectionForKey(r.selectionKey(), id);
}
-/// The selection out of `items`: an active takeover if one is among them — it annexes the
-/// region for as long as its trigger holds, which is the whole point of it — else the remembered
+/// The selection out of `items`: an active takeover if one is among them - it annexes the
+/// region for as long as its trigger holds, which is the whole point of it - else the remembered
/// id if it is still there, else the first.
fn pick(items: []const *Surface, current: ?[]const u8) ?*Surface {
if (items.len == 0) return null;
@@ -736,12 +736,12 @@ fn pick(items: []const *Surface, current: ?[]const u8) ?*Surface {
}
/// Draw one surface into the current parent, wrapped in the swap cross-fade so every region gets
-/// it for free. Keyed by **surface id**, never the parent box id — a box id moves with the
+/// it for free. Keyed by **surface id**, never the parent box id - a box id moves with the
/// surrounding layout and would restart the fade on changes that are not content swaps (see the
/// warning at workbench `src/Workspace.zig:768`).
///
/// While the picker is collecting (`State.snapshots_wanted`), a surface without a snapshot is
-/// drawn through a texture target and the result both kept and put on screen — the pixels are
+/// drawn through a texture target and the result both kept and put on screen - the pixels are
/// the same ones, so nothing blinks, and the surface still ran exactly once. Not once capture has
/// failed for it, or here at all (`State.captureFailed`, `State.capture_unavailable`).
pub fn draw(self: *Layout, s: *Surface) !dvui.App.Result {
@@ -770,8 +770,8 @@ fn drawSolid(self: *Layout, s: *Surface) !dvui.App.Result {
}
/// `s.draw` into a texture the size of the current parent's content, then that texture onto the
-/// screen where the surface would have drawn. Null when there is nothing to capture into — a
-/// zero-sized parent, or a backend without render targets — and the caller draws normally. A
+/// screen where the surface would have drawn. Null when there is nothing to capture into - a
+/// zero-sized parent, or a backend without render targets - and the caller draws normally. A
/// failure is remembered for the picker's session (`State.recordCaptureFailed`), so it is not
/// tried every frame.
///
@@ -809,7 +809,7 @@ fn drawCaptured(self: *Layout, s: *Surface) !?dvui.App.Result {
}
/// What a view drag draws over every place at once: each place's drop and the pane across the
-/// two halves of a split a drop would join (`ViewDrag.drawOverlay`) — over the card riding the
+/// two halves of a split a drop would join (`ViewDrag.drawOverlay`) - over the card riding the
/// pointer too. It spans places, so no one place can draw it; the application calls this after
/// its shape has run, from the base window.
pub fn drawDragOverlay(self: *Layout) void {
@@ -818,13 +818,13 @@ pub fn drawDragOverlay(self: *Layout) void {
/// The views floating over the window (`Floats`), each a glass window holding its place. The
/// application calls this after its shape has run and before it publishes the shape's regions,
-/// from the base window — so a float's places are this frame's, like the shape's — and before
+/// from the base window - so a float's places are this frame's, like the shape's - and before
/// `drawDragOverlay`, which goes over them.
pub fn drawFloats(self: *Layout) void {
Floats.draw(self);
}
-/// Close float `name`, every view in it going back to the place it floated out of — the picker's
+/// Close float `name`, every view in it going back to the place it floated out of - the picker's
/// Remove, as the float's own close button does. Re-docking needs nothing of this: a view dragged
/// out of a float by its corner button lands like any other, and the float shuts behind it.
pub fn closeFloat(self: *Layout, name: []const u8) void {
@@ -890,7 +890,7 @@ pub fn captureUnplaced(self: *Layout) void {
defer box.deinit();
// A subtree drawn for the first time is not yet what it will look like: dvui lays it
// out from last frame's sizes, which it has none of, and anything inside that reveals
- // itself starts hidden. So it is drawn unphotographed for a few frames first — the same
+ // itself starts hidden. So it is drawn unphotographed for a few frames first - the same
// settle-then-show the reveal does on screen, just without an audience: a few frames, and
// the reveal's whole fade, however fast the frames come.
const now = dvui.frameTimeNS();
@@ -918,7 +918,7 @@ const offscreen_warmup_frames: u8 = 10;
/// Whether surface `s` may be drawn offscreen for its picture (`captureUnplaced`, chosen by
/// `Picker.draw`): capture works here, it has no picture and no failed capture, it is not hidden,
-/// and it drew nowhere this frame — one on screen is captured where it lives, never drawn twice.
+/// and it drew nowhere this frame - one on screen is captured where it lives, never drawn twice.
pub fn mayDrawOffscreen(self: *const Layout, s: *const Surface) bool {
const state = self.state;
if (state.capture_unavailable or s.hidden) return false;
@@ -942,13 +942,13 @@ pub fn drawSelected(self: *Layout, keywords: []const []const u8) !dvui.App.Resul
}
/// `drawSelected` for a specific region. A by-name region must not draw the first assignment
-/// that happens to share its keywords — that is how every edge tray showed the same surface.
+/// that happens to share its keywords - that is how every edge tray showed the same surface.
pub fn drawSelectedIn(self: *Layout, r: *const Region) !dvui.App.Result {
const s = self.selectedIn(r) orelse return .ok;
return self.drawSwapped(r.selectionKey(), s, null, .none);
}
-/// `s` into `box`, blurring from whatever `slot` showed before — the swap every region gets,
+/// `s` into `box`, blurring from whatever `slot` showed before - the swap every region gets,
/// for a place that draws its own chooser (the explorer's body, a bottom-panel pane). `slot`
/// is any key stable for the place; `box` is the parent `s` draws into, whose packing is reset
/// after the outgoing view is photographed into it. `bleed` names the edges the blur feathers
@@ -986,7 +986,7 @@ fn drawSwapped(self: *Layout, slot: u64, s: *Surface, pack: ?*dvui.BoxWidget, bl
var ctx: Ctx = .{ .layout = self, .id = tr.prev_id, .pack = pack };
const had = tr.prev_id.len > 0;
- // Nothing to swap from — the first thing this place shows: fade it in instead.
+ // Nothing to swap from - the first thing this place shows: fade it in instead.
if (!had) {
tr.prev_id = s.id;
tr.prev_key = std.hash.Wyhash.hash(0, s.id);
@@ -995,8 +995,8 @@ fn drawSwapped(self: *Layout, slot: u64, s: *Surface, pack: ?*dvui.BoxWidget, bl
// The region's own fill, when it has one, over the window base (`Backdrop`): the view drawn
// into it may not paint its whole area, and a snapshot without it has holes the blur turns
// grey.
- // The nearest enclosing box that paints one: a document draws no background of its own —
- // the pane's card behind it is — so the direct parent is often bare.
+ // The nearest enclosing box that paints one: a document draws no background of its own -
+ // the pane's card behind it is - so the direct parent is often bare.
const backdrop: ?core.anim.Backdrop = blk: {
var w: dvui.Widget = dvui.parentGet();
while (true) {
@@ -1042,9 +1042,9 @@ fn resetPack(b: *dvui.BoxWidget) void {
if (builtin.mode == .debug) b.child_id = .zero;
}
-// ── The base layer: regions and keywords ────────────────────────────────────────────────────
+// -- The base layer: regions and keywords ----------------------------------------------------
//
-// Everything above is the vocabulary — which surfaces exist, which match, which is selected.
+// Everything above is the vocabulary - which surfaces exist, which match, which is selected.
// This is the layer a *layout* is written against: a shape declares regions, and the framework
// owns the mechanism (paned trees, split ratios, persistence, collapse animation, auto-hide).
//
@@ -1053,20 +1053,20 @@ fn resetPack(b: *dvui.BoxWidget) void {
pub const Region = @import("Region.zig");
pub const Chooser = @import("Chooser.zig");
-/// Runtime subdivision of a place — see `State.splits`.
+/// Runtime subdivision of a place - see `State.splits`.
pub const SplitTree = @import("SplitTree.zig");
-/// What a released view-drag does — see `app/layout/SPLITS.md`.
+/// What a released view-drag does - see `app/layout/SPLITS.md`.
pub const Drop = @import("Drop.zig");
-/// Carrying a view from one place to another — the gesture `Drop` decides for.
+/// Carrying a view from one place to another - the gesture `Drop` decides for.
pub const ViewDrag = @import("ViewDrag.zig");
/// What a place's menu does to a place, as commands (`Picker`'s header).
pub const place_commands = @import("place_commands.zig");
-/// The views floating over the window, each in a place of its own — see `Floats.zig`.
+/// The views floating over the window, each in a place of its own - see `Floats.zig`.
pub const Floats = @import("Floats.zig");
/// The rules a float follows, as values (std-only).
pub const float_rules = @import("float_rules.zig");
const Picker = @import("Picker.zig");
-/// The arrangement a shape starts from — see `Seed.zig`. `Layout.Seed` is the tree union.
+/// The arrangement a shape starts from - see `Seed.zig`. `Layout.Seed` is the tree union.
pub const Seed = @import("Seed.zig").Tree;
/// Walker `Layout.tree` returns over a seed-backed dockspace.
pub const Tree = @import("Tree.zig");
@@ -1080,20 +1080,20 @@ test {
_ = @import("Picker.zig");
_ = @import("State.zig");
}
-/// Declare a region — the verb form of `Region.init`, so a shape writes `f.region(...)` beside
+/// Declare a region - the verb form of `Region.init`, so a shape writes `f.region(...)` beside
/// `f.split(...)` and never names the type. Same arrangement as `dvui.box` over `BoxWidget.init`.
pub const region = Region.init;
-/// Divide a named place horizontally or vertically — the picker's Split.
+/// Divide a named place horizontally or vertically - the picker's Split.
pub const splitNamed = Region.splitNamed;
-/// Divide a named place from a specific edge — a view-drag drop onto that side.
+/// Divide a named place from a specific edge - a view-drag drop onto that side.
pub const splitOn = Region.splitOn;
/// Mark the split that minted a place as fizzy's own, so it follows its space's shape (`core.placement`).
pub const markAdaptive = Region.markAdaptive;
-/// Divide a named place the way its space's shape asks, the split adaptive — the menu's Split.
+/// Divide a named place the way its space's shape asks, the split adaptive - the menu's Split.
pub const splitAdaptive = Region.splitAdaptive;
/// Make the split a place is one half of follow its space's shape, or stop.
pub const setAdaptive = Region.setAdaptive;
-/// Move the visible surface from one place to another — a view-drag drop.
+/// Move the visible surface from one place to another - a view-drag drop.
pub const placeVisible = ViewDrag.place;
/// Walk `seed` as a `dockspace(header = .none)`. First call (and Reset Layout) convert the
@@ -1117,7 +1117,7 @@ pub fn tree(
}
/// Draw a region into an already-open tree leaf: keyword qualification, registry, corner
-/// button, contents. Geometry belongs to the tree — there is no box to `deinit`.
+/// button, contents. Geometry belongs to the tree - there is no box to `deinit`.
pub fn regionIn(self: *Layout, leaf: Tree.Leaf, init_opts: Region.InitOptions) !void {
const box = leaf.panel.dockspace.content_box orelse return;
var opts = init_opts;
@@ -1128,16 +1128,16 @@ pub fn regionIn(self: *Layout, leaf: Tree.Leaf, init_opts: Region.InitOptions) !
try Region.fillInBox(self, opts, box);
}
-// ── Regions a plugin declares ───────────────────────────────────────────────────────────────────
+// -- Regions a plugin declares -------------------------------------------------------------------
//
-// A plugin has no `*Layout` — it is a dylib, it holds a `Host`, and the layout API takes a
+// A plugin has no `*Layout` - it is a dylib, it holds a `Host`, and the layout API takes a
// `@src()` and returns a widget. So the three calls it makes through the vtable land here, on the
-// live `Layout` the app is running its shape with (`Host.region` → `EditorAPI.beginRegion` →
-// `Editor.beginPluginRegion` → this).
+// live `Layout` the app is running its shape with (`Host.region` -> `EditorAPI.beginRegion` ->
+// `Editor.beginPluginRegion` -> this).
//
// The regions themselves are ordinary: same `Region.init`, same registry, same claiming, same
// picker. What is different is only that nobody wrote a `@src()` for them, so the id comes from
-// this function's source location plus the caller's `key` — which is why `RegionSpec.key` has to
+// this function's source location plus the caller's `key` - which is why `RegionSpec.key` has to
// be stable and unique among one caller's regions.
pub fn beginPluginRegion(self: *Layout, spec: sdk.RegionSpec) ?sdk.RegionSpec.Token {
@@ -1207,8 +1207,8 @@ pub fn drawPluginRegionContents(self: *Layout, token: sdk.RegionSpec.Token) !dvu
// more, which a large document can afford because it keeps its layout while hidden.
const key = r.selectionKey();
// One waiting warm-up a frame (`State.warmIn`), under the parent the surface will be shown
- // in, clipped to nothing, then the parent's packing reset — the same pass a swap's capture
- // makes — so the shown surface lays out as if the warm-up had not happened.
+ // in, clipped to nothing, then the parent's packing reset - the same pass a swap's capture
+ // makes - so the shown surface lays out as if the warm-up had not happened.
if (self.state.takeWarm(key)) |id| {
defer self.gpa.free(id);
if (self.host.surfaceById(id)) |w| if (w != s) {
@@ -1223,7 +1223,7 @@ pub fn drawPluginRegionContents(self: *Layout, token: sdk.RegionSpec.Token) !dvu
}
// A document lifted off its tab strip has no place of its own to be photographed from
// (`ViewDrag.beginLoose`): its card's preview is taken here, from the pane that is showing
- // it, from this very draw and no other — as a region's card is (`Region.drawContentsPhotographed`).
+ // it, from this very draw and no other - as a region's card is (`Region.drawContentsPhotographed`).
if (ViewDrag.previewWanted(self, s.id)) {
const rect = dvui.parentGet().data().contentRectScale().r.intersect(dvui.clipGet());
if (ViewDrag.photographFromFrame(self, rect)) return self.drawSwapped(key, s, body, .none);
@@ -1237,7 +1237,7 @@ pub fn drawPluginRegionContents(self: *Layout, token: sdk.RegionSpec.Token) !dvu
return self.drawSwapped(key, s, body, .none);
}
-/// A plugin region's contents and selection, by token — what a plugin's own chooser (a tab
+/// A plugin region's contents and selection, by token - what a plugin's own chooser (a tab
/// strip) is drawn from and writes back to.
pub fn pluginRegionMatching(self: *Layout, token: sdk.RegionSpec.Token) []const *Surface {
const r = self.pluginRegion(token) orelse return &.{};
@@ -1260,7 +1260,7 @@ pub fn offerPluginRegionChooser(self: *Layout, token: sdk.RegionSpec.Token, boun
ViewDrag.offerChooser(self, r.name, bounds, true, null);
// Over it, and it could take what is carried: the drag's own reading (`chooserAt`, which
// asks whether the place is one the view can land in). A place lifted out of the layout over
- // a document pane's strip is over no chooser at all — the strip opening a slot for it said
+ // a document pane's strip is over no chooser at all - the strip opening a slot for it said
// it could go in, and the drag, rightly, did not take it there.
const under = ViewDrag.chooserAt(self.state, dvui.currentWindow().mouse_pt) orelse return false;
return std.mem.eql(u8, under.name, r.name);
@@ -1274,7 +1274,7 @@ pub fn pluginRegionSelect(self: *Layout, token: sdk.RegionSpec.Token, id: []cons
pub fn endPluginRegion(self: *Layout, token: sdk.RegionSpec.Token) void {
// Strictly nested, like the boxes they are. Closing out of order would deinit the wrong box
// and dvui would report it as a stack mismatch two widgets later, naming neither the plugin
- // nor the region — so say it here while the token still means something.
+ // nor the region - so say it here while the token still means something.
const depth = @backingInt(token);
if (depth != self.plugin_depth) {
dvui.log.err("plugin region closed out of order ({d} of {d} open)", .{ depth, self.plugin_depth });
@@ -1290,23 +1290,23 @@ fn pluginRegion(self: *Layout, token: sdk.RegionSpec.Token) ?*Region {
return &self.plugin_regions[depth - 1];
}
-/// What persists behind a `Layout` between frames — selections, declared regions, sizes.
+/// What persists behind a `Layout` between frames - selections, declared regions, sizes.
pub const State = @import("State.zig");
-/// A draggable divider between the region before it and the region after it — `dvui.separator`
+/// A draggable divider between the region before it and the region after it - `dvui.separator`
/// with a drag.
///
/// It takes no direction: a split divides the axis of the region it sits in, so direction is
/// declared once, on the container. Dragging it changes the stored extent of the nearest
-/// preceding `resize` region, which is the entire resize model — one number per resizable
+/// preceding `resize` region, which is the entire resize model - one number per resizable
/// region, no ratios, no boundary table, and `dvui.box` doing the layout.
/// The options are `Split.Options` itself, never a copy: this once had a second struct with the
-/// same three fields, whose defaults drifted — `min` went to zero in one place so a region could
+/// same three fields, whose defaults drifted - `min` went to zero in one place so a region could
/// be dragged shut and stayed 40 here, which silently won and pinned every split 40pt from its
/// end. Two structs describing one thing will always end up disagreeing about it.
/// Unlike `region`, this stays here rather than moving next to its type: `Split` lives in `core`
-/// so a plugin can draw one without a `Layout` at all, and everything this adds — finding the
-/// neighbour to resize, and the container constraint to resolve against — is layout state that
+/// so a plugin can draw one without a `Layout` at all, and everything this adds - finding the
+/// neighbour to resize, and the container constraint to resolve against - is layout state that
/// `core` cannot see.
pub fn split(self: *Layout, src: std.lang.SourceLocation, opts: Split.Options) void {
const c = self.innermost() orelse {
@@ -1317,8 +1317,8 @@ pub fn split(self: *Layout, src: std.lang.SourceLocation, opts: Split.Options) v
c.pending_split = .{ .src = src, .opts = opts };
}
-/// Draw the split waiting in the innermost container, now that `after` — the region it divides
-/// from the one before — is about to open. Called by `Region.init` for every region that exists.
+/// Draw the split waiting in the innermost container, now that `after` - the region it divides
+/// from the one before - is about to open. Called by `Region.init` for every region that exists.
pub fn drawPendingSplit(self: *Layout, after: ?dvui.Id) void {
const c = self.innermost() orelse return;
const pending = c.pending_split orelse return;
@@ -1385,7 +1385,7 @@ pub fn packSplitSized(
/// This is the piece that makes the two forms of region explicit in a layout:
///
/// ```zig
-/// // "this region IS x" — one surface fills it, no chooser at all. An app that just wants a
+/// // "this region IS x" - one surface fills it, no chooser at all. An app that just wants a
/// // terminal at the bottom writes only this.
/// try f.drawSelected(bottom);
///
@@ -1395,15 +1395,15 @@ pub fn packSplitSized(
/// ```
///
/// Nothing here is privileged: it lists `f.matching`, reads `f.isSelected` and writes
-/// `f.select`, so an app that wants a different-looking chooser — a dropdown, a segmented
-/// control, a radial menu — writes its own loop and calls the same three functions. The rail
+/// `f.select`, so an app that wants a different-looking chooser - a dropdown, a segmented
+/// control, a radial menu - writes its own loop and calls the same three functions. The rail
/// (the icon rail) is the same idea drawn as icons, and it sits in a *different place* from its
/// body, which is exactly why these are two widgets rather than one region mode.
///
/// **No in-tree caller yet, deliberately.** Fizzy's own bottom panel uses the richer `Panel`
/// path (which draws its own strip *and* supports splitting the bottom into several panes), and
/// `studio.zig` demonstrates the single-surface form. This is the plain tabbed form in between,
-/// and it exists as consumer API rather than as fizzy's own code — the first shape that wants
+/// and it exists as consumer API rather than as fizzy's own code - the first shape that wants
/// tabs without splits uses it as-is instead of copying `Panel`.
pub fn tabs(f: *Layout, keywords: []const []const u8) void {
const place: Region = .{ .keywords = keywords };
@@ -1426,7 +1426,7 @@ pub fn beginViewDrag(f: *Layout, id: []const u8, from: dvui.Rect.Physical) void
dvui.refresh(null, @src(), null);
}
-/// `beginViewDrag` for several things lifted together — a selection out of a file tree, a folder —
+/// `beginViewDrag` for several things lifted together - a selection out of a file tree, a folder -
/// the first the one in hand (`ViewDrag.beginLooseMany`): what a plugin's `Host.beginViewDragMany`
/// reaches.
pub fn beginViewDragMany(f: *Layout, items: []const sdk.EditorAPI.Carried, from: dvui.Rect.Physical) void {
@@ -1440,15 +1440,15 @@ pub fn beginViewDragMany(f: *Layout, items: []const sdk.EditorAPI.Carried, from:
/// Tab strip for one place: a horizontal `Chooser` with the stock `label` look. `tabs` is this
/// keyed by keywords; a by-name place (a minted split leaf) must not share another place's
-/// selection. Nothing is drawn for a place with one view or none — a single Output is just Output
-/// — nor while a takeover has annexed the place (`Surface.takeover_when`): it is the place, not a
+/// selection. Nothing is drawn for a place with one view or none - a single Output is just Output
+/// - nor while a takeover has annexed the place (`Surface.takeover_when`): it is the place, not a
/// tab beside what it covers. True when the strip was drawn.
pub fn tabsIn(f: *Layout, r: *const Region) bool {
const items = f.matchingIn(r);
if (items.len <= 1) return false;
for (items) |s| if (s.takeover_when != null) return false;
// Across the whole place, not just its tabs: a view carried anywhere along it goes in among
- // them — past the last one, at the end.
+ // them - past the last one, at the end.
var strip = Chooser.init(@src(), f, r.*, .{ .outer = .{ .expand = .horizontal } });
defer strip.deinit();
if (r.name.len > 0) core.anchor.mark(strip.strip.outer.data(), "tabs:{s}", .{r.name});
@@ -1467,7 +1467,7 @@ pub fn tabsIn(f: *Layout, r: *const Region) bool {
/// that parent's layout after the photograph as if the view were its only child. Drawn straight
/// into the place alongside the strip, the blur ran over the strip too and the view laid out as
/// though the strip were not there. In its own box the strip stays put and sharp, inside the
-/// place's card, and only what changed dissolves — the same as the bottom panel.
+/// place's card, and only what changed dissolves - the same as the bottom panel.
pub fn tabbedIn(f: *Layout, r: *const Region) !dvui.App.Result {
_ = f.tabsIn(r);
var body = dvui.box(@src(), .{ .dir = .vertical }, .{
@@ -1481,7 +1481,7 @@ pub fn tabbedIn(f: *Layout, r: *const Region) !dvui.App.Result {
}
/// The plain tabbed region: a strip of tabs, then the selected surface beneath it. The
-/// `content` function form of the two-line recipe `tabs` documents above — pass it as
+/// `content` function form of the two-line recipe `tabs` documents above - pass it as
/// `.content = Layout.tabbed` and the region is tabbed.
///
/// the icon rail deliberately has no counterpart here. It is a chooser that sits *beside* the
diff --git a/app/layout/Picker.zig b/app/layout/Picker.zig
index 743443d90..1026a6784 100644
--- a/app/layout/Picker.zig
+++ b/app/layout/Picker.zig
@@ -1,6 +1,6 @@
//! Place settings: what one region shows, and how it shows it.
//!
-//! Opened by name from anywhere — the settings table, a region's own corner button — through
+//! Opened by name from anywhere - the settings table, a region's own corner button - through
//! `State.openPicker`, and drawn once per frame by the application through `draw` so it floats
//! above whatever opened it. One at a time.
//!
@@ -8,16 +8,16 @@
//! (`Layout.tabbedIn`). A card toggles its view in or out of the place.
//!
//! Cards assign a surface here. That is how a sidebar view stays in the
-//! sidebar, and how a plugin readme lands in the center — keywords are the
+//! sidebar, and how a plugin readme lands in the center - keywords are the
//! default guess, an assignment is the answer. Reset undoes it.
//! The cards are listed under the place holding them, or else the place their
-//! keywords are for — this place first, then each other place by its name, then
-//! "Other" — so a view made for the panel reads as one, wherever the picker was
+//! keywords are for - this place first, then each other place by its name, then
+//! "Other" - so a view made for the panel reads as one, wherever the picker was
//! opened, and a view the user moved reads under where it went.
//!
//! A card is also a handle. Dragged out of the popup it becomes the same
//! floating view the corner button lifts out of a place (`ViewDrag`), and
-//! lands the same way — swapped onto a place, or split off one of its edges —
+//! lands the same way - swapped onto a place, or split off one of its edges -
//! with the popup gone and the drag previewing on the places themselves. The
//! picker keeps driving that drag until the button comes up, because the
//! card that started it went away with the popup.
@@ -29,7 +29,7 @@
//! while it does), clear, reset to the layout's default, delete (in red). A **pinned**
//! place (Sidebar, Main, Panel, Center) can be cleared or reset to its keywords;
//! it cannot be deleted. A **created** place (`Main/r1`) can. A split keeps this
-//! place's view beside the new empty one — see `SPLITS.md`.
+//! place's view beside the new empty one - see `SPLITS.md`.
const std = @import("std");
const dvui = @import("dvui");
const core = @import("core");
@@ -111,7 +111,7 @@ pub fn ask(self: *Picker, action: Action, name: []const u8) bool {
return true;
}
-/// Whether `action` applies to place `region`: `allows`, and Clear only while it shows something —
+/// Whether `action` applies to place `region`: `allows`, and Clear only while it shows something -
/// which takes the live layout to know. The one rule the menu's buttons and the requests both
/// follow.
fn applies(f: *Layout, region: *const Layout.Region, action: Action) bool {
@@ -129,7 +129,7 @@ pub fn allows(state: *State, name: []const u8, action: Action) bool {
.adaptive => state.adaptiveOf(name) != null,
.clear => !created,
.reset => !created and state.assignment(name) != null,
- // On the tree, any leaf with a sibling can go — a declared place's pin moves to the
+ // On the tree, any leaf with a sibling can go - a declared place's pin moves to the
// sibling. Off it, only a minted leaf can. A float can always go: it closes, and what it
// holds goes home. A minted half split again goes by merging its other half into it
// (`ViewDrag.closeEmptied`); with that half split too there is nothing to merge, and
@@ -187,7 +187,7 @@ fn applyRequest(self: *Picker, f: *Layout, req: *const Request) void {
const preview: dvui.Size = .{ .w = 150, .h = 100 };
const columns: usize = 2;
const list_height: f32 = 420;
-/// Points of the popup around its contents, top and bottom — its 8pt padding each side — with
+/// Points of the popup around its contents, top and bottom - its 8pt padding each side - with
/// a little to spare so rounding never tips it into scrolling.
const popup_chrome_h: f32 = 2 * 8 + 10;
@@ -205,8 +205,8 @@ pub fn close(self: *Picker, gpa: std.mem.Allocator) void {
self.is_open = false;
}
-/// Draw the popup if it is open. Call once per frame after the shape has run. Closing —
-/// by the user or because the region vanished — also discards the snapshots.
+/// Draw the popup if it is open. Call once per frame after the shape has run. Closing -
+/// by the user or because the region vanished - also discards the snapshots.
pub fn draw(self: *Picker, f: *Layout) void {
self.applyRequests(f);
if (self.lifted) return self.drawLifted(f);
@@ -229,7 +229,7 @@ pub fn draw(self: *Picker, f: *Layout) void {
// A float's place opens its picker in the float's window: a popup shuts itself the moment the
// window it was opened from is not the focused one, and the press on the corner button that
- // opened it focused the float — opened from the app's own window, it shut as it opened.
+ // opened it focused the float - opened from the app's own window, it shut as it opened.
const float_win: ?dvui.Id = if (state.floatRoot(region.name)) |root| win: {
const i = state.floats.find(root) orelse break :win null;
const id = state.floats.items.items[i].win_id;
@@ -241,7 +241,7 @@ pub fn draw(self: *Picker, f: *Layout) void {
};
// The one floating surface (`core.dialogs`): frosted, the dialogs' fill, corners and shadow
- // — what the command palette, the dialogs and every menu wear. It had its own card (an
+ // - what the command palette, the dialogs and every menu wear. It had its own card (an
// opaque-ish content fill, no blur), so it read as a different kind of thing from them.
var popup = core.widgets.popup(@src(), .{
.open_flag = &self.is_open,
@@ -260,7 +260,7 @@ pub fn draw(self: *Picker, f: *Layout) void {
// The title, the controls and the filter stay put; only the views under them scroll. The
// popup scrolls whatever does not fit the window, so the list takes only the room left
- // under this — measured as it last laid out — and the popup never has anything to scroll.
+ // under this - measured as it last laid out - and the popup never has anything to scroll.
const filter_text, const head_h = head: {
var box = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .horizontal });
defer box.deinit();
@@ -382,8 +382,8 @@ pub fn draw(self: *Picker, f: *Layout) void {
if (state.store_catalog) |store| drawStoreSection(f, ®ion, store, &row, &col, &query);
}
-/// The surfaces this region's picker offers — what the region may show at all (`Layout.offers`:
-/// no document in a plain place, only documents in a document pane) — that match `query`, best
+/// The surfaces this region's picker offers - what the region may show at all (`Layout.offers`:
+/// no document in a plain place, only documents in a document pane) - that match `query`, best
/// first. In registry order while the query is empty. Indices into `host.surfaces`.
fn offered(f: *Layout, region: *const Layout.Region, query: *const core.fuzzy.Query) []core.fuzzy.Ranked(usize) {
var out: std.ArrayList(core.fuzzy.Ranked(usize)) = .empty;
@@ -406,7 +406,7 @@ const Place = struct {
};
/// The places the picker sorts cards under: `region` itself first, then each other place the
-/// shape declared, in its order — never a document pane, which offers only documents. Places
+/// shape declared, in its order - never a document pane, which offers only documents. Places
/// resolved by keywords are one per keyword group (the icon rail and the explorer it chooses for
/// are one place); a place resolved by name (a split's leaf, a float) is its own.
fn placesFor(f: *Layout, region: *const Layout.Region) []const Place {
@@ -496,7 +496,7 @@ test "a view a place holds is listed under that place, whatever its keywords" {
}
/// The place being edited, when no card falls under it: its header still leads, so the popup
-/// says which place it fills — a split's new leaf accepts no view's keywords until one is put in.
+/// says which place it fills - a split's new leaf accepts no view's keywords until one is put in.
fn emptyHome(region: *const Layout.Region, query: *const core.fuzzy.Query) void {
var box = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .horizontal });
defer box.deinit();
@@ -508,7 +508,7 @@ fn emptyHome(region: *const Layout.Region, query: *const core.fuzzy.Query) void
});
}
-/// A heading over a run of cards — a place's name, or the store.
+/// A heading over a run of cards - a place's name, or the store.
fn sectionHeader(title: []const u8, id_extra: usize) void {
dvui.labelNoFmt(@src(), title, .{}, .{
.id_extra = id_extra,
@@ -519,7 +519,7 @@ fn sectionHeader(title: []const u8, id_extra: usize) void {
}
/// Where a view comes from, as its card says it: the plugin's name, or "Built-in" for one the app
-/// itself registered (Output, Settings). Not the app's own name — this picker is the framework's,
+/// itself registered (Output, Settings). Not the app's own name - this picker is the framework's,
/// and an app built on it is not fizzy.
fn ownerName(s: *const sdk.Surface) []const u8 {
return if (s.owner) |p| p.display_name else "Built-in";
@@ -542,7 +542,7 @@ fn searchText(f: *Layout, s: *const sdk.Surface) []const u8 {
return text.items;
}
-/// How long a finger holds still on a card before it lifts rather than scrolls the list — the
+/// How long a finger holds still on a card before it lifts rather than scrolls the list - the
/// same hold as every other draggable thing in a scrolling list.
const hold_to_lift_ns = core.widgets.Tabs.touch_hold_ns;
@@ -607,7 +607,7 @@ fn card(f: *Layout, s: *const sdk.Surface, on: bool, id_extra: usize) Hit {
// A finger on a card is also how the list under it scrolls, so a finger has to hold still
// for a moment before the card lifts; moving sooner scrolls, as it does anywhere else in the
// list. A mouse lifts on any drag. Read ahead of the button's own events, because the button
- // gives the pointer up the moment a finger moves past dvui's threshold — it takes every touch
+ // gives the pointer up the moment a finger moves past dvui's threshold - it takes every touch
// drag for a scroll, and with it went the only drag a card could start.
const armed = armedTouch(id);
var lifted: ?Hit = null;
@@ -750,7 +750,7 @@ fn drawLifted(self: *Picker, f: *Layout) void {
const cm = looseCapture();
// Not `captureMouseMaintain`: that walks the subwindow stack and drops
// the capture on meeting a modal subwindow above the current one, and
- // the popup this drag came out of — modal — stays on the stack for one
+ // the popup this drag came out of - modal - stays on the stack for one
// frame after it stops being drawn. Re-asserting the capture from the
// last event on keeps it without retargeting anything already routed.
dvui.captureMouseCustom(cm, dvui.currentWindow().event_num);
@@ -780,7 +780,7 @@ fn drawLifted(self: *Picker, f: *Layout) void {
}
/// One of the header's controls: an icon, a tooltip naming it, and its command run when pressed.
-/// Published as `region.menu.:` while it can be pressed — a dimmed one swallows the
+/// Published as `region.menu.:` 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.
/// Delete wears the theme's error colour, a toggle that is on (`lit`) the highlight's. Disabled,
/// it keeps its place (dimmed, no hover).
diff --git a/app/layout/Rail.zig b/app/layout/Rail.zig
index eef483634..3ff041c1f 100644
--- a/app/layout/Rail.zig
+++ b/app/layout/Rail.zig
@@ -26,8 +26,8 @@ pub const Host = struct {
};
};
-/// Which end of the rail a view sits at. The app's own views — registered by fizzy, owned by no
-/// plugin (the store, settings) — are the rail's footer: always in view, the same in every
+/// Which end of the rail a view sits at. The app's own views - registered by fizzy, owned by no
+/// plugin (the store, settings) - are the rail's footer: always in view, the same in every
/// install. What plugins contribute is the list above, in the place's order, and it is the part
/// that grows: with more than fit, it scrolls, passing under the footer rather than pushing it
/// off the end. A rule about who registered a view, not a list of ids, so a view the app adds
@@ -38,7 +38,7 @@ fn isFooter(view: *const Layout.Surface) bool {
/// What the sidebar wants the caller to do this frame. We defer the call out to the caller
/// because the sidebar runs *before* the side pane's `paned` is re-created for this
-/// frame — dereferencing the side pane's `paned` (e.g. via `peekClose`/`open`) from inside the
+/// frame - dereferencing the side pane's `paned` (e.g. via `peekClose`/`open`) from inside the
/// sidebar click handler would touch last frame's freed widget, which on wasm32 trips
/// "reached unreachable code".
pub const Action = enum { none, open, close };
@@ -47,8 +47,8 @@ pub const Action = enum { none, open, close };
/// region accepts, rather than every surface in `host.surfaces`. That is what
/// makes a user's keyword override actually move an icon out of (or into) this rail.
///
-/// Two vertical `Layout.Chooser`s over the sidebar's place — the plugin views scrolling above,
-/// fizzy's own pinned below — so the rail picks, and carries a dragged icon in the view drag,
+/// Two vertical `Layout.Chooser`s over the sidebar's place - the plugin views scrolling above,
+/// fizzy's own pinned below - so the rail picks, and carries a dragged icon in the view drag,
/// along the rail to reorder it or off it to another place, the same way every tab strip does.
/// What is the rail's own
/// is only how an icon looks and what picking the showing one means: it opens or closes the
@@ -135,7 +135,7 @@ const footer_glass_ramp: f32 = 12;
/// The footer's glass: the dialogs' frost over the list scrolling under it, so an icon passing
/// beneath is blurred out of the way rather than drawn through the footer's own. Only as much as
-/// there is list under it — with the rail's usual handful of plugin views there is none, and the
+/// there is list under it - with the rail's usual handful of plugin views there is none, and the
/// footer is the bare rail it always was.
fn drawFooterGlass(id: dvui.Id, rs: dvui.RectScale, under: f32) void {
const g = std.math.clamp(under / (footer_glass_ramp * rs.s), 0, 1);
@@ -177,7 +177,7 @@ fn pickAction(host: Host, c: *const Chooser, so_far: Action) Action {
/// Returns the bottom of the item's cell, in physical pixels.
fn drawIcon(host: Host, c: *Chooser, view: *Layout.Surface, index: usize) f32 {
const undecided_count = host.vtable.undecidedPluginCount(host.ctx, view.id);
- // Say what the dot means — a bare badge on a 20px rail icon is otherwise a riddle.
+ // Say what the dot means - a bare badge on a 20px rail icon is otherwise a riddle.
const detail: ?[]const u8 = if (undecided_count > 0)
dvui.currentWindow().arena().print("{d} plugin{s} ready to load", .{
undecided_count,
@@ -193,14 +193,14 @@ fn drawIcon(host: Host, c: *Chooser, view: *Layout.Surface, index: usize) f32 {
// Register the icon as interactive in the title bar so clicks reach DVUI even when it
// overlaps the top drag strip where fizzy draws its own title bar (Windows, Linux). Only the
- // topmost icon(s) actually sit inside the strip — anything below is registered harmlessly.
+ // topmost icon(s) actually sit inside the strip - anything below is registered harmlessly.
host.vtable.iconRect(host.ctx, it.data().rectScale().r);
Chooser.icon(view, it.selected, it.hovered(), rail_icon, index);
// Attention badge, same idiom (and colour) as the infobar's app-update dot: a plugin build
// is sitting in `plugins/` waiting for the user to say whether to load it, and the card that
- // offers it is inside this very view. Purely condition-driven — it disappears when the last
+ // offers it is inside this very view. Purely condition-driven - it disappears when the last
// undecided build is loaded, switched off, or removed, not when the tab is merely opened.
if (undecided_count > 0) {
const brs = it.data().rectScale();
diff --git a/app/layout/Region.zig b/app/layout/Region.zig
index 58ba03cb3..672a86ea2 100644
--- a/app/layout/Region.zig
+++ b/app/layout/Region.zig
@@ -1,22 +1,22 @@
//! A declared region: an area that accepts keywords and draws the surfaces matching them.
//!
-//! Note this is **not** docking in the draggable-panel sense — a region's place is fixed by the
+//! Note this is **not** docking in the draggable-panel sense - a region's place is fixed by the
//! shape that declares it. Real docking (dvui has dockable panels now) would be a layer *above*
//! this that lets the user move regions at runtime, and is the natural basis for a
//! Premiere-style shape. Named seam, not built.
//!
//! `init` + `deinit`, like any dvui widget, with the verb form on the namespace above it:
-//! `f.region(@src(), .{ … }, .{ … })` in a shape, which is `Layout.region` re-exporting this
+//! `f.region(@src(), .{ ... }, .{ ... })` in a shape, which is `Layout.region` re-exporting this
//! file's `init`. Same arrangement as `dvui.box` over `BoxWidget.init`, and `core.widgets.split`
//! over `Split.init`.
//!
-//! `region` draws the region's own contents — its chrome, if it declared any, and the active
-//! matching surface — and leaves the caller positioned in the *remaining* space, so whatever the
+//! `region` draws the region's own contents - its chrome, if it declared any, and the active
+//! matching surface - and leaves the caller positioned in the *remaining* space, so whatever the
//! layout writes next lands there. That is what removes the `showFirst`/`showSecond` pairs from
//! shapes.
//!
//! The third argument is `dvui.Options` and goes to the region's box, the way
-//! `dvui.box` takes them. Fill, corners, margin, padding — those are the app's.
+//! `dvui.box` takes them. Fill, corners, margin, padding - those are the app's.
//! The framework does not paint a card. Fizzy's explorer is a region with
//! `background = false`; a shape that wants a rounded tray sets that itself.
const std = @import("std");
@@ -31,7 +31,7 @@ const ViewDrag = @import("ViewDrag.zig");
const Region = @This();
-/// The human-facing name the shape gave this region — "Sidebar", "Main", "Panel". What a user
+/// The human-facing name the shape gave this region - "Sidebar", "Main", "Panel". What a user
/// picks from when moving a surface somewhere else, so it is worth a real word.
name: []const u8 = "",
/// What kinds of surface this region accepts, as its shape declared them.
@@ -40,14 +40,14 @@ keywords: []const []const u8 = &.{},
owns_selection: bool = false,
/// This region's widget id, stable across frames from the shape's `@src()`.
id: dvui.Id = .zero,
-/// How far this region reaches along its parent's axis when it has never been dragged — the
+/// How far this region reaches along its parent's axis when it has never been dragged - the
/// width under a horizontal parent, the height under a vertical one, in points. One number,
/// because a region only ever divides its parent one way; the other axis is the parent's.
default_extent: f32 = 0,
/// Clip set while the region is open, restored on `deinit`.
prev_clip: ?dvui.Rect.Physical = null,
/// The box this region is. A region **is** a `dvui.box`: same layout mechanics, same
-/// options, same lifetime rules — so an app author who has written any dvui already knows
+/// options, same lifetime rules - so an app author who has written any dvui already knows
/// how this behaves, and a split is a separator between two of them.
box: ?*dvui.BoxWidget = null,
layout: ?*Layout = null,
@@ -63,7 +63,7 @@ kind_slot: bool = false,
pane_cards: bool = false,
/// A tray the next split should keep targeting. Center and grouping boxes are not.
resize: bool = false,
-/// An empty closed tray can disappear — a runtime split, an endless edge.
+/// An empty closed tray can disappear - a runtime split, an endless edge.
forget_when_empty: bool = false,
/// How this place lays its children.
dir: dvui.enums.Direction = .vertical,
@@ -72,7 +72,7 @@ dir: dvui.enums.Direction = .vertical,
size: dvui.Size = .{},
/// This frame's border in physical pixels, for hit-testing a view drag.
bounds: dvui.Rect.Physical = .{},
-/// What a view dropped here does, for a plugin's region — `sdk.RegionSpec.on_drop`.
+/// What a view dropped here does, for a plugin's region - `sdk.RegionSpec.on_drop`.
on_drop: ?*const fn (ctx: ?*anyopaque, drop: sdk.RegionSpec.Drop) bool = null,
drop_ctx: ?*anyopaque = null,
/// Which window it is drawn in: 0 the main window, `n` the `n`th float from the bottom
@@ -80,11 +80,11 @@ drop_ctx: ?*anyopaque = null,
/// layer under the pointer.
layer: u16 = 0,
-/// The key this region's selection lives under in the host — see `Layout.selectedIn`.
+/// The key this region's selection lives under in the host - see `Layout.selectedIn`.
///
/// A by-name place keeps its selection under its name alone. A place split into others is known
-/// by that name twice — the branch that holds the split (keyed to `slot`) and the leaf left
-/// showing its views (keyed to its own keywords) — and a key folding in the keywords gave the
+/// by that name twice - the branch that holds the split (keyed to `slot`) and the leaf left
+/// showing its views (keyed to its own keywords) - and a key folding in the keywords gave the
/// two different selections: a drag out of the place read the branch's and carried a view the
/// place was not showing.
pub fn selectionKey(self: *const Region) u64 {
@@ -99,7 +99,7 @@ pub fn namedSelectionKey(name: []const u8) u64 {
pub fn deinit(self: *Region) void {
// Plugin panes skip the corner button, so their drop zones are drawn
- // here — still clipped — after their own chrome has drawn.
+ // here - still clipped - after their own chrome has drawn.
if (self.kind_slot) {
if (self.layout) |l| {
if (self.box) |b| ViewDrag.drawZones(l, self.name, b.data().id);
@@ -115,7 +115,7 @@ pub fn deinit(self: *Region) void {
// A tray the next split should keep targeting is `resize`. Center and grouping
// boxes are not: after they close, the following split sizes the region after
// it. Clearing only for empty-keyword groups left Center holding the last top
- // tray, so the Center–bottom split grew the top instead of the bottom.
+ // tray, so the Center-bottom split grew the top instead of the bottom.
if (!self.resize and l.depth > 0) {
l.containers[l.depth - 1].last_resizable = null;
}
@@ -124,14 +124,14 @@ pub fn deinit(self: *Region) void {
}
}
-// Opening and shutting from outside the layout — a rail button, a command, a keybind. These
+// Opening and shutting from outside the layout - a rail button, a command, a keybind. These
// work on the copy `Editor.regionFor` hands back as well as on a live one, which is why they
// touch only `id` and `default_extent`: everything they need is a persisted extent under an id,
// never a widget pointer.
pub fn isClosed(self: Region) bool {
- // Absence is not zero: a region that was never sized — a stretchy one that takes what is
- // left, now that every region registers — has no stored extent and is not shut.
+ // Absence is not zero: a region that was never sized - a stretchy one that takes what is
+ // left, now that every region registers - has no stored extent and is not shut.
const stored = dvui.dataGet(null, self.id, "_size", f32) orelse return false;
return stored <= 0;
}
@@ -150,14 +150,14 @@ pub fn open(self: Region) void {
}
/// Auto-collapse is a *default*, not a lock: a `collapsible` region folds itself away when the
-/// container runs out of room, unless the user has said otherwise since — by tapping its rail
+/// container runs out of room, unless the user has said otherwise since - by tapping its rail
/// icon, or by dragging its split back out. That answer is the peek, and it lives under the
/// region's id beside `_size`.
///
/// Without it, the collapse ran every frame and won every argument: on a mobile-width window the
/// explorer shut itself the instant it was opened, and a drag moved the split for exactly as
/// long as the button was held. The peek is forgotten again when the region is shut, or when the
-/// window grows back past `collapse_below` — so making the window small again collapses it, as
+/// window grows back past `collapse_below` - so making the window small again collapses it, as
/// it should.
fn collapseTarget(id: dvui.Id, chosen: f32, available: f32) f32 {
const narrow = available > 0 and available < InitOptions.collapse_below;
@@ -180,7 +180,7 @@ fn collapseTarget(id: dvui.Id, chosen: f32, available: f32) f32 {
peek = true;
dvui.dataSet(null, id, "_peek", true);
}
- // Shut — dragged closed, or closed from a button. Re-arm.
+ // Shut - dragged closed, or closed from a button. Re-arm.
if (chosen <= 0 and peek) {
peek = false;
dvui.dataSet(null, id, "_peek", false);
@@ -190,7 +190,7 @@ fn collapseTarget(id: dvui.Id, chosen: f32, available: f32) f32 {
/// Whether this region is folded away by the auto-collapse: the container is too narrow for it
/// and the user has not asked for it since. Distinct from `isClosed`, which asks whether the
-/// user *shut* it — a folded region keeps the extent it was left at, so widening the window puts
+/// user *shut* it - a folded region keeps the extent it was left at, so widening the window puts
/// it back where it was, and `isClosed` is false the whole time it is invisible.
///
/// A rail button that toggles a region has to ask this one. Reading `isClosed` there makes the
@@ -201,7 +201,7 @@ pub fn isFolded(self: Region) bool {
}
/// Whether this region is only open because the user asked for it on a container too narrow to
-/// hold it — the state a mobile-width layout draws a "close this" button for, and the one where a
+/// hold it - the state a mobile-width layout draws a "close this" button for, and the one where a
/// shape may want to shut its other regions to make room.
///
/// False on a window with room for the region: there the answer is just "open", and nothing
@@ -214,7 +214,7 @@ pub fn isPeeking(self: Region) bool {
/// How a region draws its own contents.
///
-/// Null means "draw the selected matching surface" — the "this region IS x" form. A function
+/// Null means "draw the selected matching surface" - the "this region IS x" form. A function
/// means the region has chrome of its own: a tab strip above its content, a titled scroll pane,
/// a splittable panel. It is handed the region's keywords and returns when the region is full.
///
@@ -222,17 +222,17 @@ pub fn isPeeking(self: Region) bool {
/// `panel_chrome`) named *fizzy's own* furniture from inside the generic layer. That is the
/// case AGENTS.md calls a bug in `Layout` rather than a special case: a shape is supposed to be
/// ordinary code over this API, and an app copying `src/editor/layout.zig` could not have written those two
-/// values itself. As a function pointer it is just `explorerPane` in `src/editor/layout.zig` —
+/// values itself. As a function pointer it is just `explorerPane` in `src/editor/layout.zig` -
/// app code, passed in, replaceable by the app's own loop over `matching` / `selected` / `draw`,
/// which is the governing test for everything here. (The bottom panel had one too, until
/// a place's own strip, drawn whenever it holds more than one view, did the same job generically.)
///
/// It also retired the two values nothing used (`.tabs`, `.icons`); `Layout.tabbed` is the
-/// first of those as a plain function, and the icon rail was never this shape to begin with —
+/// first of those as a plain function, and the icon rail was never this shape to begin with -
/// it sits *beside* the region it chooses for, so fizzy's `layout.zig` calls it directly and reads the
/// action it returns.
pub const Content = struct {
- /// Whatever the shape needs to draw its chrome — for fizzy's own shapes, the application.
+ /// Whatever the shape needs to draw its chrome - for fizzy's own shapes, the application.
/// A `Layout` deliberately does not carry it: the layout mechanism knows about surfaces,
/// regions and splits, and nothing about whose furniture is being drawn. Same `ctx` idiom
/// as `Surface.draw`, for the same reason.
@@ -251,9 +251,9 @@ pub const Error = error{
Failed,
};
-/// What a region *is*, as opposed to how it is laid out — which is `dvui.Options`, unchanged.
+/// What a region *is*, as opposed to how it is laid out - which is `dvui.Options`, unchanged.
pub const InitOptions = struct {
- /// Below this container extent, a `collapsible` region folds itself away — the width at
+ /// Below this container extent, a `collapsible` region folds itself away - the width at
/// which a sidebar beside a document stops being a layout and starts being two slivers. A
/// number rather than a policy an app configures: it is about how small a pane can be before
/// it is useless, not about what the app is for.
@@ -264,8 +264,8 @@ pub const InitOptions = struct {
/// What kinds of surface this region accepts. Empty means it hosts nothing itself and is
/// purely a container for other regions.
keywords: []const []const u8 = &.{},
- /// The region chooses among what it holds itself — a plugin's own tab strip
- /// (`sdk.RegionSpec.owns_selection`) — so the app draws no strip for it. Otherwise a region
+ /// The region chooses among what it holds itself - a plugin's own tab strip
+ /// (`sdk.RegionSpec.owns_selection`) - so the app draws no strip for it. Otherwise a region
/// holding two or more views gets the app's strip above them (`Layout.tabbedIn`), and one
/// holding one shows just that view. A region with `content` draws its own chrome, so the
/// strip is that function's to draw or not.
@@ -301,23 +301,23 @@ pub const InitOptions = struct {
/// Drop this region from persisted extents when it is closed and has no assigned surface.
/// An empty tray dragged shut disappears; one the user filled can close and reopen.
forget_when_empty: bool = false,
- /// Tree-internal edge that already has a sash — the leaf does not draw a create-handle there.
+ /// Tree-internal edge that already has a sash - the leaf does not draw a create-handle there.
omit_edge: ?SplitTree.Side = null,
/// Start shut when the window has no room to show this region beside everything else.
///
/// Closing itself needs no flag: a region slides continuously from its full size to nothing,
/// because a pinned box is exactly the size it is pinned to and a region clips what it holds.
- /// There is no threshold and nothing snaps — a split that jumps the last stretch is a split
+ /// There is no threshold and nothing snaps - a split that jumps the last stretch is a split
/// that fights you.
collapsible: bool = false,
/// When the view in this region lays itself out as panes of its own (`Host.region`: the
- /// workbench's document panes), each pane wears this region's card — its fill, corners and
- /// padding from the `dvui.Options` — and the region wears none: the gap between two panes then
+ /// workbench's document panes), each pane wears this region's card - its fill, corners and
+ /// padding from the `dvui.Options` - and the region wears none: the gap between two panes then
/// shows what is behind the region, as the gap between two places does, and the split there
/// reads as one. A region showing one pane, or none, keeps its card. Decided from the last
/// frame's panes, so a second pane opening wears its card a frame after it appears.
pane_cards: bool = false,
- /// Wear the layout's card (`Layout.card`) — under whatever of its fill, corners and padding
+ /// Wear the layout's card (`Layout.card`) - under whatever of its fill, corners and padding
/// the region's own `dvui.Options` set. A float's place asks for it; so can a place the shape
/// draws, rather than spelling the card out. The places a split makes of it wear it too, and
/// with `pane_cards` the panes its view opens.
@@ -327,10 +327,10 @@ pub const InitOptions = struct {
/// Declare a region: an area that hosts matching surfaces, holds other regions, or both.
///
/// It is a `dvui.box`. The second argument says what the region *is*; the third is dvui's own
-/// `Options` and is passed to that box — `expand`, `min_size_content`, `padding`,
+/// `Options` and is passed to that box - `expand`, `min_size_content`, `padding`,
/// `margin`, `background`, `corners`, `gravity`, all of it. A rounded fill is an
/// Option the shape sets. A sash gap is a packed split (`handle_size` child),
-/// not a margin on the card — plugin surfaces fill this box's content rect.
+/// not a margin on the card - plugin surfaces fill this box's content rect.
///
/// Scope it and `deinit` it the way you would any box:
///
@@ -349,21 +349,21 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
return .{};
}
- // The container this region lands in, if any — the layout's own stack, read directly
+ // The container this region lands in, if any - the layout's own stack, read directly
// rather than through an accessor, so nothing about it appears on the app-facing API.
const parent: ?*Layout.Container = if (self.depth == 0) null else &self.containers[self.depth - 1];
const axis: dvui.enums.Direction = if (parent) |p| p.dir else .horizontal;
const id = dvui.parentGet().extendId(src, opts.idExtra());
// Declared inside another region, this one's keywords name a place *within* it: `{"document"}`
- // inside the main area accepts `main.document`. The shape writes the short word — a sub-region
- // should not have to know, or repeat, what it is nested in — and a plugin still reaches it by
+ // inside the main area accepts `main.document`. The shape writes the short word - a sub-region
+ // should not have to know, or repeat, what it is nested in - and a plugin still reaches it by
// kind alone. See `Layout.Container.prefix` and `sdk.keywords.Fit`.
//
// A leftover origin sits inside the split grouping box. That box already
// qualified under the shape's parent and registered as this name. Doing
// it again under the box prefix made `main.main`, the grouping box kept
- // the exact claim, and Split → Vertical drew two empty sides.
+ // the exact claim, and Split -> Vertical drew two empty sides.
const keywords = blk: {
if (init_opts.omit_edge != null and init_opts.name.len > 0 and self.state.splits.root(init_opts.name) != null) {
var i = self.state.regions_building.items.len;
@@ -387,13 +387,13 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
// Stored, not the drag preview: a swap onto Main poses this place as an
// empty hole (or as a view that does not match these keywords), and
// collapsing then lets Main eat the column until the pointer comes back.
- // `hide_when_empty` is "nothing belongs here when idle" — the picker
- // emptied it, every panel view is toggled off — not the view sitting
+ // `hide_when_empty` is "nothing belongs here when idle" - the picker
+ // emptied it, every panel view is toggled off - not the view sitting
// on the pointer. Shape places stay; only a drop commits the hole.
const matches = self.matchingIn(&probe);
// Cleared through the picker is not "nothing belongs here": the user asked for an empty
// place and expects to see it, hatch and all, until they put something back. Only a
- // place nothing *chose* — every view toggled off, no assignment — folds away.
+ // place nothing *chose* - every view toggled off, no assignment - folds away.
const cleared = if (self.state.assignment(init_opts.name)) |a| a.len == 0 else false;
if (init_opts.hide_when_empty and keywords.len > 0 and matches.len == 0 and !cleared) {
// A boundary with nothing on one side is not a boundary.
@@ -423,7 +423,7 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
}
}
// The split declared before this region divides it from the one before: drawn now, with
- // this region known — so a split never precedes a region that is not there.
+ // this region known - so a split never precedes a region that is not there.
if (init_opts.resize) self.drawPendingSplit(id) else self.drawPendingSplit(null);
// A resizable region's extent along its parent's axis is whatever the user last dragged it
@@ -444,7 +444,7 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
.vertical => given.h,
} + alongAxis(opts.paddingGet(), axis) + alongAxis(opts.borderGet(), axis);
default_extent = default;
- // Seeded from what the user last left this region at, by name — so a layout persists
+ // Seeded from what the user last left this region at, by name - so a layout persists
// across restarts without the framework knowing which regions an app has.
if (dvui.dataGet(null, id, "_size", f32) == null) {
dvui.dataSet(null, id, "_size", self.state.extent(init_opts.name, default));
@@ -469,16 +469,16 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
if (dvui.dataGet(null, id, "_fold", bool) orelse false) target = 0;
const turned = dvui.dataGet(null, id, "_turned", bool) orelse false;
- // Ease toward the target when it moved for a reason other than a drag — the collapse
+ // Ease toward the target when it moved for a reason other than a drag - the collapse
// when the window runs out of room, and the restore when it comes back.
//
// A drag is exempt, and stays exempt without a flag: the split writes `_shown` alongside
- // `_size`, so the two agree and nothing kicks off. Easing a drag would be wrong anyway —
+ // `_size`, so the two agree and nothing kicks off. Easing a drag would be wrong anyway -
// a split should sit under the pointer, not lag behind it on a curve.
//
// `Split.eased` rather than a curve of its own: a region and a document pane sliding at
// different speeds is the kind of wrongness nobody can name but everybody feels. It also
- // carries the asymmetry — out with a little overshoot, in without any (`core.anim.slide`).
+ // carries the asymmetry - out with a little overshoot, in without any (`core.anim.slide`).
extent = Split.eased(id, target);
// The ease starts from the last shown extent, which a drag or a wider window may have
// left above the cap: bound what is drawn, so motion stays within it.
@@ -491,10 +491,10 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
// wider than the size the user dragged it to simply stays wider, and the split appears to
// stop responding once it reaches that content's natural width. Pinning the maximum too
// makes the stored size exact and stops a plugin's content dictating the app's
- // proportions — the hazard `layout.zig` names in its sizing notes.
+ // proportions - the hazard `layout.zig` names in its sizing notes.
//
- // The extent is the region's whole reach — its border edge to border edge, what a split
- // measures from the fixed edge to the pointer — so its own padding and border come out of
+ // The extent is the region's whole reach - its border edge to border edge, what a split
+ // measures from the fixed edge to the pointer - so its own padding and border come out of
// it before it becomes a content size. Taken as the content size, a place card's padding
// was added on top: a split grabbed at rest jumped by the card's padding either side.
box_opts.padding = foldedPadding(box_opts.paddingGet(), extent, axis);
@@ -520,7 +520,7 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
// dvui clamps a widget's reported min size with `max_size_content`, so capping it along
// the parent's axis stops the plugin inside from reserving space the app never granted.
// Without this the bottom panel cannot be dragged open past whatever the editor above it
- // wants to be — the neighbour's content, not the layout, decides how far a split travels.
+ // wants to be - the neighbour's content, not the layout, decides how far a split travels.
// The region clips anyway, so nothing escapes; it just stops pushing back.
//
// An explicit `max_size_content` from the shape wins: that is the app deciding, which is
@@ -548,12 +548,12 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
}
}
- // Findable from outside the layout by the keywords it accepts — a rail button or a command
+ // Findable from outside the layout by the keywords it accepts - a rail button or a command
// opening and shutting it, the settings pane listing where a panel can go. Every region that
- // hosts surfaces registers, resizable or not — a stretchy region like the main area must be
+ // hosts surfaces registers, resizable or not - a stretchy region like the main area must be
// in the registry too, or the picker never offers it and its surfaces read as unplaced.
// A leftover leaf that kept a minted name must not overwrite the pinned
- // subtree registered under that name — the parent sash still targets it.
+ // subtree registered under that name - the parent sash still targets it.
const leftover_leaf = init_opts.omit_edge != null and !init_opts.resize;
if (keywords.len > 0 and !tree_origin and !leftover_leaf) self.state.registerRegion(self.gpa, .{
.name = init_opts.name,
@@ -614,11 +614,11 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
// A region clips what it holds. Contents draw at their own natural size, so without this a
// region squeezed narrower than its contents simply spills them over its neighbour instead
- // of getting smaller — which is what a half-closed sidebar looked like.
+ // of getting smaller - which is what a half-closed sidebar looked like.
const clip_to = box.data().contentRectScale().r;
const prev_clip = dvui.clip(clip_to);
- // A plugin region (`manual_contents`) draws its own chrome — workspace
+ // A plugin region (`manual_contents`) draws its own chrome - workspace
// tabs, the fileless welcome. The place picker lives on the shape region
// that hosts the plugin (Main), not on every document pane inside it.
// An empty workspace pane was otherwise treated as a vacant slot: the
@@ -630,11 +630,11 @@ pub fn init(self: *Layout, src: std.lang.SourceLocation, init_opts: InitOptions,
// Drawn at every size except none. Skipping content at *zero* is just not doing work nobody
// can see; skipping it below a threshold would be a policy, and it would also break the
- // layered form later — a tray blurring what is behind it needs the region underneath to have
+ // layered form later - a tray blurring what is behind it needs the region underneath to have
// drawn, at every size the tray takes.
//
// Photographed, when the drag needs the card's picture, from this very
- // draw and no other — see `drawContentsPhotographed`.
+ // draw and no other - see `drawContentsPhotographed`.
if (!shut_now and keywords.len > 0 and !init_opts.manual_contents) {
// The place keeps drawing its view while the card of it rides the pointer: the app
// under a drag does not change, the drop zones lie over it (`cornerButton`).
@@ -673,15 +673,15 @@ const corner_quarter_depth: f32 = 12;
/// the glass read as a lighter material than the drop zones'), and its shadow's strength.
const corner_orb_light: f32 = 0.05;
const corner_orb_shadow: f32 = 0.3;
-/// The one glyph, in points across — one size at rest and grown: it moves into the button, it
-/// does not grow — and how far its resting grey is from the window's colour toward white (black in
+/// The one glyph, in points across - one size at rest and grown: it moves into the button, it
+/// does not grow - and how far its resting grey is from the window's colour toward white (black in
/// a light theme).
const corner_glyph_size: f32 = 7;
const corner_glyph_grey: f32 = 0.22;
/// The grown orb's radius, in points: a circle on the place's corner point, the same on every
/// place, which its outline cuts to the corner's quarter (further into it, the rounder the place).
const corner_grown_radius: f32 = 30;
-/// How much of what a rounded place's curve cuts off its grown circle gets back, 0…1: none keeps
+/// How much of what a rounded place's curve cuts off its grown circle gets back, 0...1: none keeps
/// one radius everywhere, all of it one depth.
const corner_curve_reach: f32 = 0.5;
/// A finger's target for the corner, in points: a square centred on the card's outer corner, so
@@ -699,7 +699,7 @@ const corner_touch_ms: i32 = 3000;
/// an event to the first widget that runs and paints the last one on top, and a corner button
/// under a scroll area needs both to be it.
///
-/// An empty region shows the button always — there is nothing else to find it behind. A filled
+/// An empty region shows the button always - there is nothing else to find it behind. A filled
/// one shows a drop of glass coming out of its card's top-right edge (`LiquidField.edge`): mostly
/// outside the place and cut by it, so only its swell shows, joined to the edge by a neck. Only
/// the corner brings the button up, and only from outside the view's content (the card's padding
@@ -726,12 +726,12 @@ fn cornerButton(self: *Layout, opts: InitOptions, keywords: []const []const u8,
const carded = box.data().options.backgroundGet();
const piece = cornerPiece(rs, corners, carded);
// Where the button stands while it is up, physical: the pointer anywhere on it keeps it up. Its
- // corner's orb grown where it is, under the pointer that brought it up — an empty place's too,
+ // corner's orb grown where it is, under the pointer that brought it up - an empty place's too,
// whose orb is always out.
const crs = box.data().contentRectScale();
const button_r = piece.button;
// The corner brings the button up only from outside the view's content (the card's padding
- // and its curve), so reaching for a control in the view's top right never does — and from the
+ // and its curve), so reaching for a control in the view's top right never does - and from the
// corner's quarter, which reaches a little into the content on a round card, and
// round it: pointing at the glyph always brings the button up. A place with no padding (the
// explorer) has nothing outside its content, and the glyph's square is its whole target.
@@ -776,7 +776,7 @@ fn cornerButton(self: *Layout, opts: InitOptions, keywords: []const []const u8,
const pressing = dvui.dataGet(null, id, "_chooser_press", bool) orelse false;
if (self.state.view_drag.active()) ViewDrag.tick(self);
// Not while a view is carried: the button is the way to pick or lift a view, and over a drag
- // it came and went with every place the pointer crossed. (It still runs below — it is the
+ // it came and went with every place the pointer crossed. (It still runs below - it is the
// handle the drag of this place is held by.)
const available = !self.state.view_drag.active() and (!filled or picker_here or hovered or tapped or pressing);
const alpha = chooserFade(id, if (available) 1 else 0);
@@ -790,7 +790,7 @@ fn cornerButton(self: *Layout, opts: InitOptions, keywords: []const []const u8,
ftb.init();
defer ftb.deinit();
- // `corners` (above) are the box fill's — `finalize(theme)` turns `.all(r)` into the theme's
+ // `corners` (above) are the box fill's - `finalize(theme)` turns `.all(r)` into the theme's
// kind, and fizzy's theme is square, so the ring was a second square outline sitting on the
// rounded card.
const theme = dvui.themeGet();
@@ -799,11 +799,11 @@ fn cornerButton(self: *Layout, opts: InitOptions, keywords: []const []const u8,
ViewDrag.drawZones(self, opts.name, box.data().id);
// The ring says which place the chooser is for, so it has a fade of its own, driven only by
// what asks that: the pointer at the corner, the picker open here, the button held. It
- // shared the button's, which a drag raises while it is over the place (`drop_here`) — and
+ // shared the button's, which a drag raises while it is over the place (`drop_here`) - and
// at the release the ring, held off only by the drag, came on at the button's full alpha
// for the frames that fade took to run down: a stroke flashed round the place a view landed in.
// Held for the whole of a drag out of this place, so the place being carried from stays
- // marked — the only place a drag lights, and it goes out as the drag ends.
+ // marked - the only place a drag lights, and it goes out as the drag ends.
// Not round a float that is a ghost of itself for the drag (`Floats`): nothing of the live
// float shows, and the ring is drawn past the clip that hides the rest. Firm again, under the
// view aimed back at it, it is marked as any place carried from is.
@@ -862,7 +862,7 @@ fn cornerButton(self: *Layout, opts: InitOptions, keywords: []const []const u8,
if (ring_alpha > 0.01) {
// Under the region's own border rect, not the content clip `cornerButton` runs inside:
// the ring is on the border, in the padding, so a region with padding (every card but
- // the explorer's) clipped it away entirely — only the explorer ever showed which place
+ // the explorer's) clipped it away entirely - only the explorer ever showed which place
// its chooser was for. And inset by half its width, so all of it is inside that rect.
const prev_clip = dvui.clipGet();
dvui.clipSet(rs.r);
@@ -879,7 +879,7 @@ fn cornerButton(self: *Layout, opts: InitOptions, keywords: []const []const u8,
dvui.clipSet(rs.r);
defer dvui.clipSet(bw_clip);
var bw: dvui.ButtonWidget = undefined;
- // `touch_drag`: this button is a handle — dragging it is how a view is moved. Without it
+ // `touch_drag`: this button is a handle - dragging it is how a view is moved. Without it
// dvui takes a finger dragging past the threshold for a scroll, drops the capture and ends
// the drag, so the chooser only ever moved under a mouse.
bw.init(@src(), .{ .draw_focus = false, .touch_drag = true }, .{
@@ -935,7 +935,7 @@ fn cornerButton(self: *Layout, opts: InitOptions, keywords: []const []const u8,
dvui.dataRemove(null, box.data().id, "_chooser_press");
dvui.captureMouse(null, e.num);
dvui.dragEnd();
- // Whatever the release did — landed, or nothing (its own middle, off every place) —
+ // Whatever the release did - landed, or nothing (its own middle, off every place) -
// the frame after it is different: the card is gone and the zones start to go. A
// drag held still asks for no frames of its own, so this one has to be asked for.
dvui.refresh(null, @src(), null);
@@ -1016,7 +1016,7 @@ fn cornerShape(piece: CornerPiece, t: f32) core.widgets.orb.Shape {
return core.orb_geometry.lerp(rest, grown, t);
}
-/// The corner's orb, lit `lit` under the pointer (0…1, past 1 held): the orb's material, the same at
+/// The corner's orb, lit `lit` under the pointer (0...1, past 1 held): the orb's material, the same at
/// every stage of growing (only its shape changes, `cornerShape`).
fn cornerLook(piece: CornerPiece, lit: f32) core.widgets.orb.Options {
return .{
@@ -1039,8 +1039,8 @@ fn cornerGlyphAt(piece: CornerPiece, t: f32) dvui.Point.Physical {
/// The corner's glyph: the button's grid, drawn as its own squares centred on `c` (physical), the
/// grid's corner on whole pixels (on fractional ones the cells came out of different sizes).
///
-/// `t` (0…1) is how far open it is. Closed, three cells in an L — top left, bottom left, bottom
-/// right — the empty quarter where the place's corner curves. Opening, one after another: the
+/// `t` (0...1) is how far open it is. Closed, three cells in an L - top left, bottom left, bottom
+/// right - the empty quarter where the place's corner curves. Opening, one after another: the
/// top-left cell slides right into the top right (`glyph_slide`), the bottom-left slides up into
/// the room it left (`glyph_lift`), and the bottom-right grows left into the room that one left
/// (`glyph_grow`), a bar the grid's width: a layout, two panes over one. Closing is the same played
@@ -1077,7 +1077,7 @@ fn drawCornerGlyph(c: dvui.Point.Physical, scale: f32, size: f32, color: dvui.Co
bar.w += step * widen;
bar.fill(corner, .{ .color = .{ .color = color }, .fade = 0 });
}
-/// Where in the glyph's opening (0…1) the top-left cell slides right, the bottom-left slides up
+/// Where in the glyph's opening (0...1) the top-left cell slides right, the bottom-left slides up
/// into the room it left, and the bottom-right grows left into the room that one left: each a
/// little after the one before, so they follow one another round.
const glyph_slide = [2]f32{ 0, 0.5 };
@@ -1132,7 +1132,7 @@ fn cornerSpring(t: f32) f32 {
/// How hard the corner's orb carries past its size: about a tenth of it at the peak.
const corner_overshoot: f32 = 1.7;
-/// How formed the corner's glass is, 0…1, linear in time: up over `corner_form_ms` (longer than its
+/// How formed the corner's glass is, 0...1, linear in time: up over `corner_form_ms` (longer than its
/// growing, so its edge is still settling once its shape has stopped), and straight back to 0
/// when it goes, so it forms anew each time it comes out.
fn cornerForm(id: dvui.Id, want: f32) f32 {
@@ -1243,7 +1243,7 @@ fn drawContentsPhotographed(
fn persistExtent(self: *Layout, opts: InitOptions, id: dvui.Id, chosen: f32, shown: f32) bool {
_ = shown;
// A live drag writes a zero on the way through closed; forgetting here removes the
- // region and the next frame recreates a sentinel under the pointer — the pop.
+ // region and the next frame recreates a sentinel under the pointer - the pop.
const dragging = dvui.dataGet(null, id, "_drag", bool) orelse false;
if (opts.forget_when_empty and !dragging and chosen <= 0) forget: {
// A place split again is every place in it (`SplitTree.Forest.collapse`): kept while any
@@ -1588,7 +1588,7 @@ fn packTreeSplit(self: *Layout, src: std.lang.SourceLocation, branch: SplitTree.
///
/// Full width nearly always: a place dragged shut keeps its sash, because that is the handle you
/// drag it back out by, and several shut places in a row read as the several handles they are.
-/// A place on its way *out* is the exception — emptied, its extent sent to zero, waiting for the
+/// A place on its way *out* is the exception - emptied, its extent sent to zero, waiting for the
/// curve to finish before the leaf is dropped. Its sash is 10pt that the pair keeps until the
/// drop and loses in a single frame at the end, which is the step the eye reads as a pop after
/// an otherwise smooth close. Following the place down costs nothing to grab, because in a
@@ -1608,7 +1608,7 @@ fn sashWidth(self: *Layout, name: []const u8, target: dvui.Id) f32 {
/// A card's padding, folded away over the last stretch of a close.
///
/// Padding is room the place takes up, so a card closed to nothing is still as wide as its own
-/// inset — 16pt of stripe that sits there until something removes the place, and then goes in
+/// inset - 16pt of stripe that sits there until something removes the place, and then goes in
/// one frame. Once the extent is under the inset the place is narrower than the gap it wants to
/// keep inside itself, which is the point at which the inset stops meaning anything; from there
/// it comes in proportionally, so nothing reaches nothing.
@@ -1686,7 +1686,7 @@ fn drawTreeNode(
o.resize = false;
o.omit_edge = omit;
o.by_name = true;
- // A leftover Panel must still draw — hiding it leaves a
+ // A leftover Panel must still draw - hiding it leaves a
// hole with no sash, no card, and no corner control.
o.hide_when_empty = false;
}
@@ -1802,7 +1802,7 @@ fn placePrefix(keywords: []const []const u8, parent_prefix: []const u8) []const
return keywords[0];
}
-/// Draw a region into an already-open box — `init` minus geometry. Keyword qualification,
+/// Draw a region into an already-open box - `init` minus geometry. Keyword qualification,
/// registry, corner button, contents. The box is the caller's (a dockspace leaf cell).
pub fn fillInBox(self: *Layout, init_opts: InitOptions, box: *dvui.BoxWidget) !void {
const parent: ?*Layout.Container = if (self.depth == 0) null else &self.containers[self.depth - 1];
@@ -1844,7 +1844,7 @@ pub fn fillInBox(self: *Layout, init_opts: InitOptions, box: *dvui.BoxWidget) !v
}
/// Divide `name` on `axis`, keeping its view and opening an empty place beside
-/// it on the trailing side. `axis` is the layout direction — `.horizontal` is a
+/// it on the trailing side. `axis` is the layout direction - `.horizontal` is a
/// vertical divider (side by side): the menu's Split Right (`layout.place.splitRight`).
pub fn splitNamed(self: *Layout, name: []const u8, axis: dvui.enums.Direction) void {
const side: SplitTree.Side = switch (axis) {
@@ -1866,8 +1866,8 @@ pub fn splitOn(self: *Layout, name: []const u8, side: SplitTree.Side) ?[]const u
return splitOnSized(self, name, side, ViewDrag.placeSize(self.state, name) orelse return null, 0.5);
}
-/// `splitOn` for a place of `size` that need not have been drawn yet — a split of a half another
-/// split made this frame, as a saved arrangement is rebuilt — with `share` of it for the new half.
+/// `splitOn` for a place of `size` that need not have been drawn yet - a split of a half another
+/// split made this frame, as a saved arrangement is rebuilt - with `share` of it for the new half.
/// Only the split forest's: a seed's dock tree splits its own drawn leaves (`splitOn`).
pub fn splitOnSized(self: *Layout, name: []const u8, side: SplitTree.Side, size: dvui.Size, share: f32) ?[]const u8 {
const span = switch (SplitTree.axisOf(side)) {
@@ -1882,8 +1882,8 @@ pub fn splitOnSized(self: *Layout, name: []const u8, side: SplitTree.Side, size:
}
};
intern.state = self.state;
- // Halved the way the preview halves it — the sash out of the middle, both
- // sides the same — so what slid open is the size that lands.
+ // Halved the way the preview halves it - the sash out of the middle, both
+ // sides the same - so what slid open is the size that lands.
const want = @max(1, (span - Split.handle_size) * std.math.clamp(share, 0, 1));
const new = self.state.splits.split(self.gpa, intern.go, name, side, want, null) orelse return null;
if (self.state.setExtent(self.gpa, new, want)) self.extents_changed = true;
diff --git a/app/layout/Seed.zig b/app/layout/Seed.zig
index 4e1c904ba..4b4f8ff78 100644
--- a/app/layout/Seed.zig
+++ b/app/layout/Seed.zig
@@ -11,7 +11,7 @@ const DockLayout = core.widgets.DockLayout;
const Seed = @This();
-/// A binary tree of splits with named leaves at the fringes — the same shape as
+/// A binary tree of splits with named leaves at the fringes - the same shape as
/// `DockLayout.Snapshot.Tree`, with region spec on the leaves instead of tab slugs.
pub const Tree = union(enum) {
split: Tree.Split,
@@ -87,7 +87,7 @@ fn snapshotNode(node: *const Tree, allocator: std.mem.Allocator) !DockLayout.Sna
}
}
-/// `{origin}/{l|r|t|b}{n}` — the same mint as `SplitTree`, unique among `dock`'s panel ids.
+/// `{origin}/{l|r|t|b}{n}` - the same mint as `SplitTree`, unique among `dock`'s panel ids.
pub fn mintName(dock: *const DockLayout, origin: []const u8, side: DockLayout.Side, buf: []u8) ?[]const u8 {
const letter: u8 = switch (side) {
.left => 'l',
diff --git a/app/layout/Shape.zig b/app/layout/Shape.zig
index 887931b73..2fd76b85c 100644
--- a/app/layout/Shape.zig
+++ b/app/layout/Shape.zig
@@ -7,7 +7,7 @@
//! the endless shape's (fizzyedit/example-app) *loops*, making a new edge tray each time the user drags one open.
//! Data cannot answer those without becoming a language, and the interpreter for that language
//! would be the configurable layout engine fizzy deliberately does not have. So this covers only
-//! the static case — a fixed arrangement of named places — and says so rather than growing
+//! the static case - a fixed arrangement of named places - and says so rather than growing
//! conditionals later. When an app needs a condition, it copies a shape file and writes one.
//!
//! **What it can say:** nesting and direction, a region's name and keywords, its starting extent,
@@ -42,12 +42,12 @@ resize: bool = false,
collapsible: bool = false,
/// Collapse while nothing matches, rather than holding empty space open.
hide_when_empty: bool = false,
-/// The places inside this one. A spec with children is a container — its own `name`/`keywords`
-/// may still make it host surfaces — and one without is a leaf place. A subtree is a `Shape`
+/// The places inside this one. A spec with children is a container - its own `name`/`keywords`
+/// may still make it host surfaces - and one without is a leaf place. A subtree is a `Shape`
/// like any other, which is why the whole file is one type.
children: []const Shape = &.{},
-/// Draw the whole tree. One `@src()` serves every region and every split — identity comes from
+/// Draw the whole tree. One `@src()` serves every region and every split - identity comes from
/// each node's position in a depth-first walk, which is stable as long as the file is.
///
/// Uses nothing an app's own shape file could not: `Layout.region` and `Layout.split`, the same
diff --git a/app/layout/SidePane.zig b/app/layout/SidePane.zig
index f8adb9283..1e5fa5f6a 100644
--- a/app/layout/SidePane.zig
+++ b/app/layout/SidePane.zig
@@ -19,7 +19,7 @@ collapse_btn_anim_started: bool = false,
/// caller reads it after `draw`.
collapse_requested: bool = false,
-/// Draws the pane *chrome* — header, scroll policy, collapse button — around whichever
+/// Draws the pane *chrome* - header, scroll policy, collapse button - around whichever
/// surface currently matches `keywords`. The chrome is the app's; the body is the plugin's.
///
/// The body is resolved through the layout's match set rather than the registry, so a user's
@@ -54,7 +54,7 @@ pub fn draw(
// A surface that scrolls itself (`Surface.scrolls_itself`) gets exactly the space this region
// has, on both axes, and decides for itself. Scrolling it here as well meant two policies on
- // one direction: a second vertical bar on top of its own, and horizontally a ratchet — the
+ // one direction: a second vertical bar on top of its own, and horizontally a ratchet - the
// pane capped its width at its viewport, this made room for that width, and the viewport
// could then never be narrower than it had just been told to be.
const self_scrolling = if (f.selected(keywords)) |view| view.scrolls_itself else false;
@@ -72,7 +72,7 @@ pub fn draw(
// No bars at all around a surface that scrolls itself, rather than bars decided against a size
// that only says there is nothing to scroll: the scroll area decides on its bars from the size
- // it was last given, a frame old — while the window shrank, every frame of it, last frame's
+ // it was last given, a frame old - while the window shrank, every frame of it, last frame's
// height overflowed this frame's, and a bar showed beside the pane's own until it stopped.
const bar: dvui.ScrollInfo.ScrollBarMode = if (self_scrolling) .hide else .auto_overlay;
var scroll = core.widgets.ScrollArea.init(@src(), .{ .scroll_info = &self.scroll_info, .horizontal_bar = bar, .vertical_bar = bar }, .{
@@ -87,7 +87,7 @@ pub fn draw(
// Through the layout, not `Host.selectedSurface`: the host remembers which
// view was chosen here and hands it back whether or not this place still
// holds it. Drag Files out to the main area and it is claimed by what it
- // was dropped on — the rail drops the icon, and the host would still hand
+ // was dropped on - the rail drops the icon, and the host would still hand
// back Files to draw in the body, until a click on some other icon moved
// the remembered choice. `f.selected` reads the choice against what the
// place actually shows, which is the only reading that can never say that.
diff --git a/app/layout/SplitTree.zig b/app/layout/SplitTree.zig
index 76c0955f3..50f3a45b0 100644
--- a/app/layout/SplitTree.zig
+++ b/app/layout/SplitTree.zig
@@ -48,7 +48,7 @@ pub const Node = struct {
pub const Branch = struct {
dir: dvui.enums.Direction,
side: Side,
- /// The leaf that was split — keeps keywords and assignment.
+ /// The leaf that was split - keeps keywords and assignment.
origin: []const u8,
/// The leaf minted on this split. Kept even after that side is split again,
/// so a nested tree still writes every link to disk.
@@ -324,7 +324,7 @@ pub const Forest = struct {
return null;
}
- /// Two places one split made — the two leaves of one branch — as the place a join keeps and
+ /// Two places one split made - the two leaves of one branch - as the place a join keeps and
/// the one it closes. Null for any other pair: only a split's own two halves are one place
/// again when joined, so that is the only pair the middle of a place joins.
///
@@ -383,7 +383,7 @@ pub const Forest = struct {
}
}
- /// The place whose split `name` is a part of — the root its tree is keyed by — or `name`
+ /// The place whose split `name` is a part of - the root its tree is keyed by - or `name`
/// itself when it was never split or is a root. Null when the forest has never heard of it.
pub fn rootOf(self: *const Forest, name: []const u8) ?[]const u8 {
var it = self.roots.iterator();
@@ -416,7 +416,7 @@ pub const Forest = struct {
}
}
- /// Drop `root`'s whole tree: the place is gone, and every split of it with it — a float
+ /// Drop `root`'s whole tree: the place is gone, and every split of it with it - a float
/// closing. Nothing to do for a place never split.
pub fn forget(self: *Forest, gpa: std.mem.Allocator, root_name: []const u8) void {
const kv = self.roots.fetchRemove(root_name) orelse return;
diff --git a/app/layout/State.zig b/app/layout/State.zig
index b593019d3..1c200c56b 100644
--- a/app/layout/State.zig
+++ b/app/layout/State.zig
@@ -24,11 +24,11 @@ const State = @This();
/// on request (`snapshots_wanted`), one per surface per request, never on an ordinary frame.
pub const Snapshot = struct {
texture: dvui.Texture,
- /// The size it was drawn at, in points — the aspect the card should keep.
+ /// The size it was drawn at, in points - the aspect the card should keep.
natural: dvui.Size,
};
-/// A view being carried from one place to another — see `ViewDrag.zig`.
+/// A view being carried from one place to another - see `ViewDrag.zig`.
pub const ViewDrag = @import("ViewDrag.zig");
/// What the picker needs from a plugin store without importing one. The store's module graph
@@ -51,8 +51,8 @@ pub const Beside = struct {
/// lands in any place as a view would, inside a new such view (`ViewDrag.place`).
pub const Documents = struct {
ctx: *anyopaque,
- /// A new view holding the documents `ids`, the first shown — a document not open yet opened
- /// in it — for the layout to place like any view it lands. Its id, or null when none went in.
+ /// A new view holding the documents `ids`, the first shown - a document not open yet opened
+ /// in it - for the layout to place like any view it lands. Its id, or null when none went in.
house: *const fn (ctx: *anyopaque, ids: []const []const u8) ?[]const u8,
/// The view holding document `id` and nothing else, if one does: carried alone, the
/// document carries that view, which lands as itself rather than inside a new one.
@@ -74,7 +74,7 @@ pub const StoreCatalog = struct {
/// Keyed by group rather than by region so two regions written with the same keywords share a
/// selection with no wiring between them (see `layout/Layout.zig`).
selection: std.AutoHashMapUnmanaged(u64, []const u8) = .empty,
-/// What a region shows, by the name its shape declared — the user's answer to "what goes
+/// What a region shows, by the name its shape declared - the user's answer to "what goes
/// *here*", which overrides keyword matching wholesale for that region. An entry with no
/// surfaces is a region the user deliberately emptied; a region with no entry shows whatever
/// its keywords attract, which is every region's starting state.
@@ -89,11 +89,11 @@ selection: std.AutoHashMapUnmanaged(u64, []const u8) = .empty,
assignments: std.StringHashMapUnmanaged([]const []const u8) = .empty,
/// The order the user dragged a place's views into, by region name. Separate from `assignments`
/// on purpose: an assignment says *which* views a place holds, and writing one down for a place
-/// its keywords fill would freeze it — a plugin installed later would never appear there. An
+/// its keywords fill would freeze it - a plugin installed later would never appear there. An
/// order only sorts: views it names come first, in its order; the rest follow as they would
/// have. Keys and ids gpa-owned, like `assignments`.
orders: std.StringHashMapUnmanaged([]const []const u8) = .empty,
-/// Surface id → its snapshot. Keys are gpa-owned copies: a surface can unregister (plugin
+/// Surface id -> its snapshot. Keys are gpa-owned copies: a surface can unregister (plugin
/// unloaded) while the picker is open.
snapshots: std.StringHashMapUnmanaged(Snapshot) = .empty,
/// While true, `Layout.draw` captures any surface it draws that has no snapshot yet, and
@@ -124,7 +124,7 @@ unmarked_scrollers: if (builtin.mode == .debug) UnmarkedScrollers else void = if
/// one place above everything else (`Picker.draw`).
picker: Picker = .{},
/// Filled in by the application when it has a plugin store. Null means the picker only lists
-/// loaded surfaces — tests, and an app that never switched the store on.
+/// loaded surfaces - tests, and an app that never switched the store on.
store_catalog: ?StoreCatalog = null,
/// Filled in by the application when documents can have homes outside their slots. Null means a
/// document goes only into a slot made for it.
@@ -134,26 +134,26 @@ documents: ?Documents = null,
pending_store_region: []const u8 = "",
pending_store_plugin: []const u8 = "",
-/// Regions the last completed shape declared — what `Editor.regionFor` answers from.
+/// Regions the last completed shape declared - what `Editor.regionFor` answers from.
///
/// Deliberately the *previous* frame's set rather than the one being built: a command can run
/// between frames (a native menu item is dispatched before the shape has drawn anything), and a
-/// command that drives a region — Toggle Explorer, Toggle Panel — must find one. Reading a
+/// command that drives a region - Toggle Explorer, Toggle Panel - must find one. Reading a
/// half-built list gave it nothing, and the toggle silently did half its job: the menu title
/// flipped, because that reads a bool, and the sidebar never moved.
///
/// Answering from last frame is correct rather than merely convenient: a region's id comes from
/// its shape's `@src()`, so it is the same id this frame will declare. The one case it cannot
-/// help is a command dispatched before the *first* shape has ever run — there is genuinely no
-/// region yet — which is why this is a swap rather than a claim that ordering no longer matters.
+/// help is a command dispatched before the *first* shape has ever run - there is genuinely no
+/// region yet - which is why this is a swap rather than a claim that ordering no longer matters.
regions: std.ArrayList(Region) = .empty,
/// The set the shape currently running is declaring. Swapped into `regions` when it finishes.
regions_building: std.ArrayList(Region) = .empty,
-/// Every region's remembered extent in **points** — width under a horizontal parent, height
-/// under a vertical one — by the name its shape declared, loaded from
+/// Every region's remembered extent in **points** - width under a horizontal parent, height
+/// under a vertical one - by the name its shape declared, loaded from
/// `layout.zon` at startup and written back debounced.
///
-/// Replaces `explorer_ratio` / `panel_ratio`, which named the two regions fizzy happens to have —
+/// Replaces `explorer_ratio` / `panel_ratio`, which named the two regions fizzy happens to have -
/// so an app with a "Stack" and a "Strip" could persist nothing, and fizzy's own furniture was
/// baked into a framework's on-disk format. Those two survive only for the legacy shell.
extents: std.StringHashMapUnmanaged(f32) = .empty,
@@ -195,12 +195,12 @@ layer_building: u16 = 0,
/// past the last share it.
card_area: [card_layers]dvui.Rect.Physical = @splat(.{}),
card_area_building: [card_layers]dvui.Rect.Physical = @splat(.{}),
-/// Explorer/panel split ratios — "window shape" state persisted in `window.zon`, not
+/// Explorer/panel split ratios - "window shape" state persisted in `window.zon`, not
/// `settings.zon` (dragging a splitter fires every frame; keeping it out of the settings file
/// means normal window use never dirties a git-tracked settings.zon). Loaded once at startup
/// (see `init`); defaults match the pre-move `Settings` field defaults.
/// Debounced-save bookkeeping for the ratios above, separate from `settings_dirty`/
-/// `settings_save_deadline_ns` — sidebar/panel dragging must not force a settings.zon write
+/// `settings_save_deadline_ns` - sidebar/panel dragging must not force a settings.zon write
/// attempt on every drag frame.
dirty: bool = false,
save_deadline_ns: i128 = 0,
@@ -225,15 +225,15 @@ names: std.StringHashMapUnmanaged(void) = .empty,
// ---- the app's side of a region ---------------------------------------------------------------
//
-// Regions are declared by a shape and driven from outside it — a rail button, a command, a
+// Regions are declared by a shape and driven from outside it - a rail button, a command, a
// keybind. These four are that seam, and they live on the state rather than on the application
// because nothing here is fizzy's: a name, an extent, and the set the last shape declared.
-/// The region accepting `keywords`, or null when this app's shape declared none — a normal
+/// The region accepting `keywords`, or null when this app's shape declared none - a normal
/// state, not an error.
///
/// `intersects`, not `accepts`: a caller here holds a *vocabulary* and wants the region that
-/// speaks it — Toggle Explorer asks for `ide.sidebar` and must find the sidebar whether the
+/// speaks it - Toggle Explorer asks for `ide.sidebar` and must find the sidebar whether the
/// shape declared it qualified or not. Whether a *surface* belongs in a region is the asymmetric
/// question, and that one goes through `accepts`.
pub fn regionFor(self: *State, keywords: []const []const u8) ?Region {
@@ -247,7 +247,7 @@ pub fn regionFor(self: *State, keywords: []const []const u8) ?Region {
/// main area becomes `{"main.document"}`. Interned, and stable for as long as the state lives.
///
/// Interned rather than arena-allocated because the registry is read a frame *later* than it is
-/// written — `regionFor` answers a command dispatched between frames from the last completed
+/// written - `regionFor` answers a command dispatched between frames from the last completed
/// shape, and an arena string would be freed by then. There are as many of these as a shape has
/// nested regions, which is a handful, so the pool is a list and a linear scan.
///
@@ -329,7 +329,7 @@ pub fn deinitQualified(self: *State, gpa: std.mem.Allocator) void {
self.names.deinit(gpa);
}
-/// A copy of `name` that outlives the frame — the registry, the assignment table and the picker
+/// A copy of `name` that outlives the frame - the registry, the assignment table and the picker
/// all hold a region's name across frames. Same string in, same slice out.
pub fn internName(self: *State, gpa: std.mem.Allocator, name: []const u8) []const u8 {
if (self.names.getKey(name)) |k| return k;
@@ -369,7 +369,7 @@ pub fn clearDock(self: *State) void {
self.tree_cleared = true;
}
-/// On the tree, a leaf that has a sibling to collapse into — minted or declared, since a
+/// On the tree, a leaf that has a sibling to collapse into - minted or declared, since a
/// declared place's pin moves to whatever is left. Off the tree, never (the old split forest
/// has its own rule, `isMinted`).
pub fn canRemove(self: *const State, name: []const u8) bool {
@@ -379,7 +379,7 @@ pub fn canRemove(self: *const State, name: []const u8) bool {
}
/// The heir of a closed pinned place takes its name: whatever this state keyed by the heir's
-/// old name — its assignment, order, extent — is now the place's, and the closed
+/// old name - its assignment, order, extent - is now the place's, and the closed
/// place's own entries go with it.
pub fn renamePlace(self: *State, gpa: std.mem.Allocator, from: []const u8, to: []const u8) void {
self.unassign(gpa, to);
@@ -497,7 +497,7 @@ pub fn siblingLeaf(self: *const State, name: []const u8) ?[]const u8 {
return self.splits.siblingLeaf(name);
}
-/// Whether `name` is one half of a split the user made — minted by it, or the place that was
+/// Whether `name` is one half of a split the user made - minted by it, or the place that was
/// split, with the minted half beside it. Such a place can go: emptied it closes, and the trash
/// takes it (`ViewDrag`). A place the shape declared, never split, cannot.
pub fn userSplitPart(self: *const State, name: []const u8) bool {
@@ -506,7 +506,7 @@ pub fn userSplitPart(self: *const State, name: []const u8) bool {
return self.isMinted(sibling);
}
-/// The two halves of one split — the pair a drop in the middle of one joins — as the place
+/// The two halves of one split - the pair a drop in the middle of one joins - as the place
/// that stays and the one that closes; null for any other pair. The live dock tree when there is
/// one: its two leaves under one split, a declared (pinned) leaf always the one kept and two
/// declared leaves never a pair, since neither may go. Otherwise the split forest
@@ -565,11 +565,11 @@ pub fn assignment(self: *State, name: []const u8) ?[]const []const u8 {
return self.assignments.get(name);
}
-/// Set what region `name` shows. An empty list is a real choice — "nothing here" — distinct from
+/// Set what region `name` shows. An empty list is a real choice - "nothing here" - distinct from
/// `unassign`, which hands the region back to its keywords. A surface lives in one
/// place: putting it here takes it out of every other assignment.
///
-/// `surfaces` may borrow from any assignment, this region's included — read, edit, write back
+/// `surfaces` may borrow from any assignment, this region's included - read, edit, write back
/// is how a pane adds a tab. So past `setAssignment`, which frees the old lists, only this
/// region's new, owned copy is read: its key and list, taken out of the map first, since
/// rewriting the other regions can grow the map and move its entries.
@@ -673,8 +673,8 @@ pub fn unassign(self: *State, gpa: std.mem.Allocator, name: []const u8) void {
gpa.free(kv.value);
}
-/// Forget everything kept under place `name` — what it holds, their order and its
-/// extent — because the place itself is gone for good: a float closed, whose number the next
+/// Forget everything kept under place `name` - what it holds, their order and its
+/// extent - because the place itself is gone for good: a float closed, whose number the next
/// float may reuse. Returns true when an extent went, which the app may want to save.
pub fn forgetPlace(self: *State, gpa: std.mem.Allocator, name: []const u8) bool {
self.unassign(gpa, name);
@@ -828,7 +828,7 @@ pub fn deinitExtents(self: *State, gpa: std.mem.Allocator) void {
self.floats.deinit(gpa);
}
-/// Drop every per-place overlay texture. Safe to call twice — the map is
+/// Drop every per-place overlay texture. Safe to call twice - the map is
/// emptied. Tests that only tear down extents or assignments both reach here.
pub const Warm = struct { key: u64, id: []u8 };
@@ -849,7 +849,7 @@ pub fn discardSwaps(self: *State) void {
}
/// The overlay for this place, created on first use. Null only if the map
-/// cannot grow — the caller then draws without a transition.
+/// cannot grow - the caller then draws without a transition.
pub fn swapFor(self: *State, gpa: std.mem.Allocator, key: u64) ?*core.anim.Transition {
if (self.swaps.get(key)) |t| return t;
const t = gpa.create(core.anim.Transition) catch return null;
@@ -861,7 +861,7 @@ pub fn swapFor(self: *State, gpa: std.mem.Allocator, key: u64) ?*core.anim.Trans
return t;
}
-/// Draw the surface `id` once, invisibly, in the place `key` — before it is ever shown there.
+/// Draw the surface `id` once, invisibly, in the place `key` - before it is ever shown there.
/// A view that measures itself on its first draw (a large text document laying out every line)
/// does that now, while it is not what anyone is waiting on, instead of on the click that
/// shows it. Drawn in its own place because widget ids follow their parents: a warm-up drawn
@@ -897,7 +897,7 @@ pub fn resetLayout(self: *State, gpa: std.mem.Allocator) void {
self.floats.clear();
}
-/// Forget what the user arranged — extents, assignments, orders, Single/Multiple and the splits —
+/// Forget what the user arranged - extents, assignments, orders, Single/Multiple and the splits -
/// and any drag or transition under way, keeping each place's widget state (how wide it is drawn,
/// shut or not), the seed tree and the floats. For putting an arrangement back on top of it
/// (`Editor.applySavedRegions`), as a demo's snapshot does; `resetLayout` forgets the rest too.
@@ -1006,8 +1006,8 @@ pub fn requestSlideOpen(self: *State, name: []const u8) void {
self.slide_open = name;
}
-/// Whether `name` should ease open this frame, and from where (0…1 of its target): always from
-/// nothing — a new leaf appears by growing into its space.
+/// Whether `name` should ease open this frame, and from where (0...1 of its target): always from
+/// nothing - a new leaf appears by growing into its space.
pub fn takeSlideOpen(self: *State, name: []const u8) ?f32 {
if (self.slide_open.len == 0 or !std.mem.eql(u8, self.slide_open, name)) return null;
self.slide_open = "";
@@ -1042,7 +1042,7 @@ pub fn clearExtent(self: *State, gpa: std.mem.Allocator, name: []const u8) bool
}
/// Remember a region's extent. Returns true when the value actually changed, so the application
-/// can decide what "remember" means — fizzy debounces a write to `layout.zon`; another app might
+/// can decide what "remember" means - fizzy debounces a write to `layout.zon`; another app might
/// do nothing at all.
pub fn setExtent(self: *State, gpa: std.mem.Allocator, name: []const u8, value: f32) bool {
const gop = self.extents.getOrPut(gpa, name) catch return false;
diff --git a/app/layout/Tree.zig b/app/layout/Tree.zig
index 4547b42ae..33d978e32 100644
--- a/app/layout/Tree.zig
+++ b/app/layout/Tree.zig
@@ -53,7 +53,7 @@ pub fn leaf(self: *Tree) ?Leaf {
pub fn deinit(self: *Tree) void {
if (self.dock.changed) self.layout.state.markDirty();
self.dock.deinit();
- // A collapse may have handed a declared place's pin — and its name — to a sibling.
+ // A collapse may have handed a declared place's pin - and its name - to a sibling.
if (self.layout.state.dock) |*d| {
if (d.takeRenamed()) |r| {
defer d.allocator.free(r.from);
diff --git a/app/layout/ViewDrag.zig b/app/layout/ViewDrag.zig
index 73e449dc8..172366c0b 100644
--- a/app/layout/ViewDrag.zig
+++ b/app/layout/ViewDrag.zig
@@ -11,7 +11,7 @@
//!
//! **The app under a drag does not change.** The dragged surface is
//! photographed once, at lift, and a card of it rides the pointer; its place
-//! goes on drawing it, and no place poses it as a preview of landing there —
+//! goes on drawing it, and no place poses it as a preview of landing there -
//! one view shown in two places at once was harder to read than a card over
//! a window that stays put. The layout moves after the drop, with the
//! animations every split and swap already has.
@@ -19,10 +19,10 @@
//! **The place under the pointer shows its drop zones** (`drawZones`,
//! `core.widgets.DropZones`), all five of its options at once; the one under
//! the pointer is the one a release takes. Moving to another place, its zones
-//! clear as the new place's come in — the window is never covered in targets,
+//! clear as the new place's come in - the window is never covered in targets,
//! and the one change on screen is where the pointer is. The middle of the
//! other half of a split is a join, which its middle bubble says with its icon
-//! (`DropZones.Center.join`) — no other drop previews what it leaves, so neither does this.
+//! (`DropZones.Center.join`) - no other drop previews what it leaves, so neither does this.
const std = @import("std");
const dvui = @import("dvui");
const core = @import("core");
@@ -39,15 +39,15 @@ const DropZones = core.widgets.DropZones;
const ViewDrag = @This();
-/// The pointer while a view is carried, from the press that lifts it — a tab, a place's corner
-/// button, a picker card (each passes it to `dvui.dragPreStart`) — to the release: the same
+/// The pointer while a view is carried, from the press that lifts it - a tab, a place's corner
+/// button, a picker card (each passes it to `dvui.dragPreStart`) - to the release: the same
/// whatever is under it. dvui has no closed "grabbing" hand; this is its hand.
pub const cursor: dvui.enums.Cursor = .hand;
/// Interned name of the place the view was lifted from. Empty when idle, so
/// `active()` is the one question everything else asks first.
name: []const u8 = "",
-/// Physical size of the source when the drag began — the card shrinks from it.
+/// Physical size of the source when the drag began - the card shrinks from it.
from: dvui.Size.Physical = .{},
/// The lifted surface as it last drew. The floating card is this texture.
texture: ?dvui.Texture = null,
@@ -57,8 +57,8 @@ texture_rect: dvui.Rect.Physical = .{},
/// fits its picture to it (`drawDrop`).
photo_box: dvui.Rect = .{ .x = 0, .y = 0, .w = 1, .h = 1 },
/// Its pixels, premultiplied RGBA rows of `photo_size`, kept where the app shows the picture under
-/// the OS's glass (`core.dialogs.carry_windows`) — handed over once, as an image (`photo_gen` new
-/// with each picture) — and freed with the drag (`discard`), both given the one allocator the layout
+/// the OS's glass (`core.dialogs.carry_windows`) - handed over once, as an image (`photo_gen` new
+/// with each picture) - and freed with the drag (`discard`), both given the one allocator the layout
/// owns it with. Null elsewhere.
photo_pixels: ?[]dvui.Color.PMA = null,
photo_size: [2]u32 = .{ 0, 0 },
@@ -76,7 +76,7 @@ moved_id: []const u8 = "",
/// The place it was lifted from held other views beside it (`begin`): the trash takes just this
/// one (`removable`), rather than leaving a place the shape declared empty.
from_several: bool = false,
-/// Everything lifted together with it (`beginLooseMany`) — a selection out of a file tree — in the
+/// Everything lifted together with it (`beginLooseMany`) - a selection out of a file tree - in the
/// order it was, the one in hand first: carried in the one glass, which shows them all, and landing
/// together (`sdk.RegionSpec.Drop.others`). Empty for one view carried alone. Ids and labels are
/// interned (`State.internName`).
@@ -95,8 +95,8 @@ lifting: bool = false,
/// and a card or a tab after it hangs from the pointer's corner (`drawFloat`).
been_drop: bool = false,
/// The carried shape as last drawn, whatever it was drawn as: its rect and corner radius,
-/// physical. A change of what the view is carried as grows the new shape out of this one —
-/// position, size and corners — so the lift from what was grabbed, the tab over a strip and the
+/// physical. A change of what the view is carried as grows the new shape out of this one -
+/// position, size and corners - so the lift from what was grabbed, the tab over a strip and the
/// drop of glass off one are one shape changing, never one swapped for another.
shape_rect: dvui.Rect.Physical = .{},
shape_radius: f32 = 0,
@@ -105,22 +105,22 @@ morph_rect: dvui.Rect.Physical = .{},
morph_radius: f32 = 0,
card_start_ns: i128 = 0,
/// A loose drag's hold on what it lifted: the middle of what was grabbed, from the pointer, when it
-/// was lifted (`beginLoose`). The card keeps it while it is under the pointer — the face of a row
-/// lifted out of a list stays where it was in the row — and is brought in under the pointer where
+/// was lifted (`beginLoose`). The card keeps it while it is under the pointer - the face of a row
+/// lifted out of a list stays where it was in the row - and is brought in under the pointer where
/// it is not (`drawFloat`). dvui's own drag offset is the list's, from a press the lift may have
/// come long after, and of a rect the card is not.
grab: dvui.Point.Physical = .{},
/// Where the hand holds the drop (`holdHand`): the pointer, kept still through a tremble of it.
/// What the drop rides off and aims with, while it is carried as one; null when it is not.
hand: ?dvui.Point.Physical = null,
-/// The middle of the bubble the drop is drawn toward (`drop_pull`) — moving onto a bubble newly
+/// The middle of the bubble the drop is drawn toward (`drop_pull`) - moving onto a bubble newly
/// aimed at from `aim_from` to `aim_to`, `aim_move` of the way there (`pour`), by time, not by the
/// hand; null once it is joined to none.
aim_at: ?dvui.Point.Physical = null,
aim_from: dvui.Point.Physical = .{},
aim_to: ?dvui.Point.Physical = null,
aim_move: f32 = 1,
-/// How far the drop is drawn into the bubble it is aimed at, 0…1 and past 1 as it pours in
+/// How far the drop is drawn into the bubble it is aimed at, 0...1 and past 1 as it pours in
/// (`drop_pull` of the way at 1): from `pull_from` toward `pull_to`, `pull_move` of the way there.
pull: f32 = 0,
pull_from: f32 = 0,
@@ -138,14 +138,14 @@ drop_n: usize = 0,
drop_radius: f32 = 0,
/// Carried by a finger: the drop rides up and left of it, where the finger does not cover it.
drop_touch: bool = false,
-/// How far the drop is joined to the bubble it is aimed at, 0…1, eased: its photograph fades by
+/// How far the drop is joined to the bubble it is aimed at, 0...1, eased: its photograph fades by
/// `aim_fade` of it (`drawDrop`).
drop_aim: f32 = 0,
/// The places this drag can land on, and where they were, frozen at lift.
targets: [max_targets]Target = undefined,
target_count: usize = 0,
-/// Choosers a view can be dropped into — a rail, a tab strip — as they offered themselves while
+/// Choosers a view can be dropped into - a rail, a tab strip - as they offered themselves while
/// drawing (`offerChooser`): this frame's, and last frame's for a release handled before this
/// frame's choosers have drawn.
offers: [max_offers]Offer = undefined,
@@ -165,7 +165,7 @@ last_pending_count: usize = 0,
occluders: [Floats.max]Occluder = undefined,
occluder_count: usize = 0,
/// Whether the float the view is carried out of is firm (`settleGhost`): itself, covering what it
-/// lies over, rather than its ghost. It starts firm — the drag lifts out of it — and stays so
+/// lies over, rather than its ghost. It starts firm - the drag lifts out of it - and stays so
/// until the view is aimed off it.
ghost_firm: bool = true,
/// Where and when the view came to rest over the ghost (`settleGhost`), while a drop beneath lies
@@ -174,7 +174,7 @@ ghost_rest_at: dvui.Point.Physical = .{},
ghost_rest_ns: i128 = 0,
/// What the carried view is drawn as. `drop` where the glass program draws and the pointer is off
-/// every list, `tab` over a list (a tab strip, a rail), `preview` — a card of its photograph — off
+/// every list, `tab` over a list (a tab strip, a rail), `preview` - a card of its photograph - off
/// lists with no glass program.
pub const Mode = enum {
preview,
@@ -189,7 +189,7 @@ pub const Mode = enum {
};
/// A place's drop, queued while the place draws and drawn over everything once they all have
-/// (`drawOverlay`) — over the card riding the pointer too, which would otherwise sit on the
+/// (`drawOverlay`) - over the card riding the pointer too, which would otherwise sit on the
/// drop it is being aimed at.
pub const PendingDrop = struct {
key: dvui.Id,
@@ -198,7 +198,7 @@ pub const PendingDrop = struct {
clip: dvui.Rect.Physical,
};
-/// A chooser a carried view is over: a place's tab strip, a rail. Chrome, not content — the
+/// A chooser a carried view is over: a place's tab strip, a rail. Chrome, not content - the
/// place's drop zones and the card's preview stay off it (`interiorBounds`, `drawFloat`).
pub const Offer = struct {
/// Interned place name.
@@ -209,7 +209,7 @@ pub const Offer = struct {
/// The window it is drawn in (`Region.layer`).
layer: u16 = 0,
/// Where along it the view goes in, for a chooser the app draws (`Chooser`): a release puts it
- /// there in its place's list (`insertInto`) — back on the chooser it came off, a reorder. Null
+ /// there in its place's list (`insertInto`) - back on the chooser it came off, a reorder. Null
/// for a plugin's chooser, which says where along it itself, from the release's point
/// (`RegionSpec.Drop.on_chooser`).
at: ?Insert = null,
@@ -244,7 +244,7 @@ pub const Occluder = struct {
/// The float the view is carried out of (`carriedOutOf`). It covers only while it is firm.
source: bool = false,
/// A float in an OS window of its own: `bounds` and `header` are where that window lies over
- /// the main window's frame, and these where the float is drawn in the window's band — the
+ /// the main window's frame, and these where the float is drawn in the window's band - the
/// pointer is read in one or the other (`SDLBackend.heldPoint`), by whether the window lets it
/// through to the main window (a ghost does).
band_bounds: ?dvui.Rect.Physical = null,
@@ -263,11 +263,11 @@ pub const Occluder = struct {
};
/// What lies under a point, as far as which window: the topmost float there (0, the main window,
-/// when none is), and whether the point is on that float's header — its handle, never a drop.
+/// when none is), and whether the point is on that float's header - its handle, never a drop.
const Under = struct { layer: u16 = 0, header: bool = false };
/// What lies under `p`: the topmost float there, the one the view is carried out of only while it
-/// is firm (`settleGhost`) — a ghost covers nothing.
+/// is firm (`settleGhost`) - a ghost covers nothing.
fn under(state: *const Layout.State, p: dvui.Point.Physical) Under {
const d = &state.view_drag;
var out: Under = .{};
@@ -280,7 +280,7 @@ fn under(state: *const Layout.State, p: dvui.Point.Physical) Under {
}
/// Whether `p`, aimed with a drop of radius `r`, is on the drop of the place under it in window
-/// `layer` (`zoneBounds`) — under the ghost of the float the view is carried out of, the one place it
+/// `layer` (`zoneBounds`) - under the ghost of the float the view is carried out of, the one place it
/// can be under a float and still be aimed at.
fn onDropBeneath(state: *const Layout.State, layer: u16, p: dvui.Point.Physical, r: f32) bool {
const d = &state.view_drag;
@@ -305,7 +305,7 @@ fn sourceOccluder(d: *const ViewDrag) ?Occluder {
}
/// How long the view must rest over the ghost of the float it is carried out of before it firms
-/// up, while a drop beneath lies under it (`settleGhost`), in milliseconds as they pass — not
+/// up, while a drop beneath lies under it (`settleGhost`), in milliseconds as they pass - not
/// motion, which can be off: it is the user saying they mean the float, not an animation.
pub const ghost_rest_ms: i128 = 240;
/// Points the view may drift and still be resting.
@@ -319,12 +319,12 @@ const ghost_rest_slop: f32 = 10;
/// the view is off it: what it lies over shows through, and covers nothing (`under`). The drops of the places it
/// lies over sit clear of it where there is room for them (`zoneBounds`), the same firm or ghost,
/// so a drop never moves as the ghost comes and goes. With every drop clear of it, aimed back over
-/// it, it firms up at once — the float again, with its own places' drops — and stays firm until the
+/// it, it firms up at once - the float again, with its own places' drops - and stays firm until the
/// view is aimed off it, so its own middle and edges can be reached wherever they are.
///
/// A drop with too little room clear of it sits under the ghost instead, reached through it: the
/// view over that drop is aimed at it, not the float. And while one does, the ghost firms only for
-/// a rest over it, off such a drop or on its header (`ghost_rest_ms`) — at once, it would firm as
+/// a rest over it, off such a drop or on its header (`ghost_rest_ms`) - at once, it would firm as
/// the view was carried across it to the drop, and that drop could never be reached.
pub fn settleGhost(state: *Layout.State) bool {
const d = &state.view_drag;
@@ -337,7 +337,7 @@ pub fn settleGhost(state: *Layout.State) bool {
// Over the float by the pointer, not the drop it carries, which rides off to one side of it:
// lifted from the float's corner button, the drop started out past the float's edge, and the
// float went toward its ghost, firmed as the drop crossed back over it on the way out, and went
- // again — a hitch in the middle of every drag out of a float.
+ // again - a hitch in the middle of every drag out of a float.
if (!g.covers(mouse)) {
d.ghost_firm = false;
d.ghost_rest_ns = 0;
@@ -349,7 +349,7 @@ pub fn settleGhost(state: *Layout.State) bool {
for (d.occluders[0..d.occluder_count]) |o| {
if (!o.source and o.covers(a.p) and o.layer > below) below = o.layer;
}
- // On a drop beneath it the view is aimed at that drop, lit as one — never the float — except
+ // On a drop beneath it the view is aimed at that drop, lit as one - never the float - except
// over the ghost's header, its handle: a drop under a ghost is its whole size, and over a small
// ghost it can leave nowhere else to rest.
const on_header = g.onHeader(mouse);
@@ -423,7 +423,7 @@ pub fn active(self: ViewDrag) bool {
/// The source of a view lifted out of the picker rather than out of a place.
/// No shape declares a place by this name, so every question the gesture
-/// asks of its source — its bounds, its assignment, whether it may shut —
+/// asks of its source - its bounds, its assignment, whether it may shut -
/// answers "none", which is exactly what a view in nobody's hands has.
pub const loose_source = "\x00picker";
@@ -433,8 +433,8 @@ pub fn loose(self: ViewDrag) bool {
}
/// A chooser, drawing during a drag, offering itself as somewhere the view can go: into place
-/// `name`, as one of its views — at `at` in its list, when the chooser knows where along it the
-/// pointer is. Its place may sit elsewhere — a rail beside a sidebar — or the chooser inside it;
+/// `name`, as one of its views - at `at` in its list, when the chooser knows where along it the
+/// pointer is. Its place may sit elsewhere - a rail beside a sidebar - or the chooser inside it;
/// either way over the chooser the drop is into the place, not a split of it.
pub fn offerChooser(l: *Layout, name: []const u8, bounds: dvui.Rect.Physical, into: bool, at: ?Insert) void {
const d = &l.state.view_drag;
@@ -457,8 +457,8 @@ pub fn offerChooser(l: *Layout, name: []const u8, bounds: dvui.Rect.Physical, in
}
/// The chooser under `p` that could take what is carried, if one offered itself this frame or
-/// the last. A strip whose place cannot take it — a document pane's tabs under a view that is no
-/// document — is no chooser for this drag: read as one, it hid every place's zones and turned the
+/// the last. A strip whose place cannot take it - a document pane's tabs under a view that is no
+/// document - is no chooser for this drag: read as one, it hid every place's zones and turned the
/// card into a tab over a strip it could never go into, so a split document area, a strip on every
/// pane, was a maze to aim a view across. The strip of the place the view came out of always is.
/// A strip a float covers is none either: only the topmost window under `p` is read (`under`).
@@ -529,7 +529,7 @@ fn forgetPixels(self: *ViewDrag, gpa: std.mem.Allocator) void {
/// The carried view's picture under the OS's glass (`ViewDrag.photo_under`): where its head is in
/// the frame, its corners, how much of it shows, and where the whole picture lies (`photoFit`),
-/// physical — past the head, cut off there.
+/// physical - past the head, cut off there.
pub const PhotoUnder = struct {
rect: dvui.Rect.Physical,
radius: f32,
@@ -538,7 +538,7 @@ pub const PhotoUnder = struct {
};
/// Where in the captured view what it shows is, as shares of the picture's width and height: the
-/// box round everything it drew, read once from the capture's pixels at lift — a capture of the
+/// box round everything it drew, read once from the capture's pixels at lift - a capture of the
/// view alone is clear wherever it draws nothing. A drop showed the middle of its picture, and
/// Files, whose rows are at its top, showed nothing (the user). The whole where nothing could be
/// read.
@@ -579,9 +579,9 @@ const photo_zoom_least: f32 = 0.6;
/// What of the carried view's picture its drop shows (`photoFit`), as shares of the picture: what
/// the view shows (`box`, `contentBox`), where that is narrow enough to read shrunk into the square
-/// inside the round drop (`side`, physical) — and where it is not, the square of it that reads at
+/// inside the round drop (`side`, physical) - and where it is not, the square of it that reads at
/// `photo_zoom_least` with the most drawn in it: of those with nearly the most, the highest and
-/// then the furthest left, as a view reads — its heading and first rows, an editor's first lines,
+/// then the furthest left, as a view reads - its heading and first rows, an editor's first lines,
/// the middle of a canvas, rather than the whole of it too small to make out. Graded once, at
/// lift, on a coarse grid of how much each part of the picture draws.
fn focusBox(arena: std.mem.Allocator, px: []const dvui.Color.PMA, w_: u32, h_: u32, box: dvui.Rect, side: f32) dvui.Rect {
@@ -638,7 +638,7 @@ fn focusBox(arena: std.mem.Allocator, px: []const dvui.Color.PMA, w_: u32, h_: u
/// Where the whole of the carried view's picture lies for a drop's head `r` (physical): what it
/// shows (`photo_box`) as wide as the square inside the round head, its top at that square's top
-/// and its middle on the head's — Files' rows across the head from its top rather than a strip of
+/// and its middle on the head's - Files' rows across the head from its top rather than a strip of
/// them lost in its middle (the user). Never larger than it was taken. Past the head, it is cut
/// off there.
fn photoFit(d: ViewDrag, r: dvui.Rect.Physical) dvui.Rect.Physical {
@@ -656,8 +656,8 @@ fn photoFit(d: ViewDrag, r: dvui.Rect.Physical) dvui.Rect.Physical {
///
/// A photograph is taken *from the place's own draw*, never from a second
/// one. Drawing a subtree twice in a frame gives every widget inside it a
-/// duplicate id — dvui paints the lot red and the two copies fight over the
-/// same stored state — and the subtree under a place is the whole editor.
+/// duplicate id - dvui paints the lot red and the two copies fight over the
+/// same stored state - and the subtree under a place is the whole editor.
pub const Shot = struct {
/// The lifted view, for the floating card. Taken once, at lift.
card: bool = false,
@@ -675,14 +675,14 @@ pub fn shotWanted(l: *Layout, is_source: bool) Shot {
}
/// Keep what the source's draw recorded as the card's picture, and put it on the screen the
-/// draw was taken from — the place goes on showing its view under the drag.
+/// draw was taken from - the place goes on showing its view under the drag.
pub fn keepShot(l: *Layout, shot: Shot, pic: *dvui.Picture) void {
const d = &l.state.view_drag;
if (shot.card) {
d.takePicture(l.gpa, pic);
if (d.texture) |tex| {
// Over what the frame already has under the place, as its draw would have gone: the
- // capture is clear where the place draws nothing — the window's base shows there — and
+ // capture is clear where the place draws nothing - the window's base shows there - and
// put back as a dissolve (`blit`) it replaced that base with nothing, the window
// see-through under the place for the frame it was lifted on.
core.anim.blitOpaque(tex, null, d.texture_rect, 0, 1);
@@ -695,7 +695,7 @@ pub fn keepShot(l: *Layout, shot: Shot, pic: *dvui.Picture) void {
pic.stop();
}
-/// The card's picture from the frame as last drawn — what the place showed a moment ago — rather
+/// The card's picture from the frame as last drawn - what the place showed a moment ago - rather
/// than from a capture of this frame's draw. Inside a capture a frosted pane has nothing behind
/// it to frost and draws dark, and that copy is what `keepShot` puts back on the screen: the
/// place flashed dark for the frame it was lifted on. False where there is no last frame to copy
@@ -752,8 +752,8 @@ pub fn begin(l: *Layout, name: []const u8, from: dvui.Rect.Physical, grabbed: dv
d.from = from.size();
d.start_ns = dvui.currentWindow().frame_time_ns;
d.ghost_firm = true;
- // The carried glass grows out of what was grabbed — the place's grid button, its tab or rail
- // icon — with its photograph growing in it: not glass the size of the whole place shrinking
+ // The carried glass grows out of what was grabbed - the place's grid button, its tab or rail
+ // icon - with its photograph growing in it: not glass the size of the whole place shrinking
// into it, which drew the view whole for a moment before it was carried.
liftShape(d, grabbed);
if (visibleId(l, name)) |id| d.moved_id = id;
@@ -764,7 +764,7 @@ pub fn begin(l: *Layout, name: []const u8, from: dvui.Rect.Physical, grabbed: dv
Floats.liftedFrom(l, name, d.moved_id);
}
-/// The carried shape starts at `from` — what was grabbed: a tab, a card, a place's grid button —
+/// The carried shape starts at `from` - what was grabbed: a tab, a card, a place's grid button -
/// and grows from there into whatever it is first carried as.
fn liftShape(d: *ViewDrag, from: dvui.Rect.Physical) void {
const radius = core.corners.scaled(core.corners.card) * dvui.currentWindow().natural_scale;
@@ -780,7 +780,7 @@ fn liftShape(d: *ViewDrag, from: dvui.Rect.Physical) void {
}
/// Begin carrying surface `id` from the picker, or from a plugin's own list (`Host.beginViewDrag`):
-/// a tab off its strip, a file out of a tree — which may be a document not open yet, carried by
+/// a tab off its strip, a file out of a tree - which may be a document not open yet, carried by
/// the id it will have (`unopened`). There is no source place, so nothing is photographed: the
/// float starts at the card that was grabbed and shows the picture the card showed, which the
/// caller hands over (`State.stealSnapshot`) and the drag destroys.
@@ -792,9 +792,9 @@ pub fn beginLoose(l: *Layout, id: []const u8, from: dvui.Rect.Physical, texture:
}
/// Begin carrying several things lifted together out of a plugin's own list
-/// (`Host.beginViewDragMany`) — a selection out of a file tree — the first the one in hand: a
+/// (`Host.beginViewDragMany`) - a selection out of a file tree - the first the one in hand: a
/// loose drag of it (`beginLoose`), carrying the rest with it (`with`). With anything among them no
-/// place takes — a folder, by no id — it is carried by `held_id` and lands nowhere (`held`).
+/// place takes - a folder, by no id - it is carried by `held_id` and lands nowhere (`held`).
pub fn beginLooseMany(l: *Layout, items: []const sdk.EditorAPI.Carried, from: dvui.Rect.Physical) void {
if (items.len == 0) return;
var held = false;
@@ -838,22 +838,22 @@ fn beginLooseAs(l: *Layout, moved: []const u8, from: dvui.Rect.Physical, texture
mapTargets(l, d);
}
-// ── What is under the pointer ───────────────────────────────────────────────────────────────────
+// -- What is under the pointer -------------------------------------------------------------------
/// Photograph the places, the way the card photographs the view.
///
/// **A drag must not change the map it is being read against.** Places can
-/// move under a drag — a split easing shut, a window resized — and a hit-test
+/// move under a drag - a split easing shut, a window resized - and a hit-test
/// read against the live layout would chase them. Frozen at lift, it is a pure function of where the pointer is,
/// and the drag is as steady as your hand.
fn mapTargets(l: *Layout, d: *ViewDrag) void {
d.target_count = 0;
defer mapOccluders(l, d);
- // Carried by an id no place knows (`held`): no place takes it — not even a shape place, which
- // takes any view — so none shows its zones.
+ // Carried by an id no place knows (`held`): no place takes it - not even a shape place, which
+ // takes any view - so none shows its zones.
if (std.mem.eql(u8, d.moved_id, held_id)) return;
const surface_kw = draggedKeywords(l);
- // A tab's content goes to a slot made for it, never into a plain place (`Layout.slotted`) —
+ // A tab's content goes to a slot made for it, never into a plain place (`Layout.slotted`) -
// only beside one, where the application gives it a home (`State.documents`).
const slotted = slottedId(l, d.moved_id) and l.state.documents == null;
// Last frame's registry: complete, where this frame's is still being
@@ -890,7 +890,7 @@ fn mapOccluders(l: *Layout, d: *ViewDrag) void {
// In an OS window of its own: where that window lies over the main window's places, and
// where the float is drawn in its band besides (`Occluder.band_bounds`).
// Under the main window in the OS's stacking (`Floats.Viewport.under_main`), it covers
- // nothing of it: read there it is the main window's places, whole — where its window
+ // nothing of it: read there it is the main window's places, whole - where its window
// still shows, past the main window's edge, it is read in its band as ever. Taken as over
// it, a float clicked behind the main window cut its places' drops down to the strip
// beside it (the user).
@@ -905,10 +905,10 @@ fn mapOccluders(l: *Layout, d: *ViewDrag) void {
}
}
-/// Whether the live drag is carrying a view out of float `name` — out of its place, or a place a
-/// split of it made. The float is a ghost of itself meanwhile while the view is aimed off it — it
+/// Whether the live drag is carrying a view out of float `name` - out of its place, or a place a
+/// split of it made. The float is a ghost of itself meanwhile while the view is aimed off it - it
/// fades to a faint, blurred picture of itself (`Floats.draw`) and covers nothing (`under`), so
-/// what it lies over, usually where the view is going, can be seen and aimed at — and firms up
+/// what it lies over, usually where the view is going, can be seen and aimed at - and firms up
/// again when the view is aimed back over it, to be dropped into. When the drag ends it comes back:
/// as it was if the view is let go over nothing, without the view if it landed elsewhere, and not
/// at all if that left it empty.
@@ -936,9 +936,9 @@ fn kindAt(l: *Layout, dest: []const u8, a: Aim, scale: f32) ?Drop.Kind {
return Drop.kindAtDisc(dest_b, a.p, a.r, scale, removable(l));
}
-/// What the carried view aims with: the middle and radius of the drop it is carried as — the
+/// What the carried view aims with: the middle and radius of the drop it is carried as - the
/// drop is what is aimed, and it rides off the pointer (up and left of a finger) so it can be
-/// seen, so it is its overlap with a bubble that chooses, not where the finger is — or the
+/// seen, so it is its overlap with a bubble that chooses, not where the finger is - or the
/// pointer itself, for a card or a tab.
pub const Aim = struct { p: dvui.Point.Physical, r: f32 = 0 };
@@ -946,7 +946,7 @@ pub fn aim(l: *Layout) Aim {
return aimFor(l, dvui.currentWindow().mouse_pt);
}
-/// `aim` for the pointer at `mouse` — a release's own point.
+/// `aim` for the pointer at `mouse` - a release's own point.
pub fn aimFor(l: *Layout, mouse: dvui.Point.Physical) Aim {
const d = &l.state.view_drag;
if (d.active() and carriedAsDrop(l.state, mouse)) _ = holdHand(d, mouse);
@@ -969,7 +969,7 @@ const hand_still: f32 = 3;
/// Where the hand holds the drop, the pointer at `mouse`: where it was, until the pointer is more
/// than `hand_still` from it, and then dragged along behind the pointer at that distance. A hand
/// holding a press still trembles, and the drop moved with each tremble: joined to a bubble, the
-/// OS re-formed the join each frame, and it shimmered (the user). Nothing eases or swings — it
+/// OS re-formed the join each frame, and it shimmered (the user). Nothing eases or swings - it
/// moves only as far as the hand does, and at once. Called as often as a frame likes: the same
/// pointer leaves it where it is.
fn holdHand(d: *ViewDrag, mouse: dvui.Point.Physical) dvui.Point.Physical {
@@ -991,8 +991,8 @@ fn holdHand(d: *ViewDrag, mouse: dvui.Point.Physical) dvui.Point.Physical {
/// a photograph to show in it, and not over a list (where it is a tab).
fn carriedAsDrop(state: *const Layout.State, mouse: dvui.Point.Physical) bool {
const d = &state.view_drag;
- // A bubble only round a photograph. With none — a file not open yet, a document open in no pane
- // — it is carried as its tab in glass, its icon and its name (`drawTabFace`): a bubble with an
+ // A bubble only round a photograph. With none - a file not open yet, a document open in no pane
+ // - it is carried as its tab in glass, its icon and its name (`drawTabFace`): a bubble with an
// icon in it read as a thing of its own rather than the tab it is.
return core.LiquidField.ready() and d.texture != null and chooserAt(state, mouse) == null;
}
@@ -1007,7 +1007,7 @@ fn dropCenter(mouse: dvui.Point.Physical, r: f32, touch: bool) dvui.Point.Physic
/// (`State.userSplitPart`) the trash takes what is carried, and the half, emptied, closes into the
/// other; out of a place holding others beside it (the sidebar, the bottom panel) it takes just
/// that view, back to the picker. A place the shape declared, holding only the view carried and
-/// never split, offers none — the trash would only leave it empty. A view carried out of the
+/// never split, offers none - the trash would only leave it empty. A view carried out of the
/// picker is in no place to leave.
pub fn removable(l: *Layout) bool {
return removableIn(l.state);
@@ -1017,13 +1017,13 @@ fn removableIn(state: *const Layout.State) bool {
const d = state.view_drag;
if (!d.active() or d.loose()) return false;
// Out of a float, as out of a split the user made: the view leaves it, and a float its last
- // view leaves closes — the view, claimed by no place then, back where its keywords put it.
+ // view leaves closes - the view, claimed by no place then, back where its keywords put it.
if (state.floatRoot(d.name) != null) return true;
if (state.userSplitPart(d.name)) return true;
return d.from_several and regionNamed(state, d.name) != null;
}
-/// The part of place `name` a carried view's zones cover: the place less its own chooser — a tab
+/// The part of place `name` a carried view's zones cover: the place less its own chooser - a tab
/// strip across its top or foot, which offered itself while the drag was on (`offerChooser`).
/// The strip is chrome: over it the view goes into the place's list, not onto a zone, so the
/// zones, the edge the pointer is read against and a split's halves are all cut from the rest.
@@ -1055,7 +1055,7 @@ pub fn interiorBounds(state: *const Layout.State, name: []const u8) ?dvui.Rect.P
}
/// The part of place `name` its drop sits in: its interior (`interiorBounds`) less every float over
-/// it — a float drawn in a window above the place's, as the drag mapped them (`mapOccluders`). A
+/// it - a float drawn in a window above the place's, as the drag mapped them (`mapOccluders`). A
/// drop under a float could not be aimed at, so it goes where it can be: the middle of the part
/// left clear, fitted there as a wheel or a strip as anywhere (`DropZones.uncovered`). A float's
/// own places are in its window, so theirs are inside it. Null when floats cover all of it: that
@@ -1068,8 +1068,8 @@ pub fn interiorBounds(state: *const Layout.State, name: []const u8) ?dvui.Rect.P
/// it is the same rect, so no drop moves or changes size as the ghost comes and goes; firm, the
/// float takes the pointer over it, so a drop under it is not aimed at until it fades again.
///
-/// Everything that reads or draws a place's drop reads this — its zones (`drawZones`), the release
-/// (`kindAt`), the self-split (`targetAtAim`) — so what shows is what a release takes.
+/// Everything that reads or draws a place's drop reads this - its zones (`drawZones`), the release
+/// (`kindAt`), the self-split (`targetAtAim`) - so what shows is what a release takes.
pub fn zoneBounds(state: *const Layout.State, name: []const u8) ?dvui.Rect.Physical {
if (!state.view_drag.active()) return interiorBounds(state, name);
const clear = zoneBoundsAs(state, name, true);
@@ -1086,7 +1086,7 @@ pub fn zoneBounds(state: *const Layout.State, name: []const u8) ?dvui.Rect.Physi
/// The least share of the size it would have with no ghost there that a drop shrinks to, to stay
/// clear of the ghost of the float the view is carried out of (`zoneBounds`); any smaller, and it
/// keeps that size, under the ghost. Two thirds or so: the drop's bubbles rest apart now, and the
-/// wider wheel shrinks further to fit beside a ghost — at three quarters, chosen for the narrower
+/// wider wheel shrinks further to fit beside a ghost - at three quarters, chosen for the narrower
/// one, it sat under the ghost where it had fitted beside it.
pub const ghost_min_share: f32 = 0.65;
@@ -1128,11 +1128,11 @@ pub fn targetAtAim(l: *Layout, a: Aim, source: []const u8) ?[]const u8 {
// reached through it, and a float's header is its handle, no drop.
const u = under(state, mouse);
if (u.header) return null;
- // Over a chooser, its place — as one of its views, never a split — or nowhere, over the
+ // Over a chooser, its place - as one of its views, never a split - or nowhere, over the
// app's own strip of the place the view came out of.
if (chooserAt(state, mouse)) |o| return if (o.into) o.name else null;
// The source's own edge is a self-split, and it outranks any pane nested
- // inside it — otherwise a document filling the place always wins on area
+ // inside it - otherwise a document filling the place always wins on area
// and its own edges become unreachable.
if (layerOf(state, source) == u.layer) {
if (interiorBounds(state, source)) |bounds| {
@@ -1156,7 +1156,7 @@ pub fn targetAtAim(l: *Layout, a: Aim, source: []const u8) ?[]const u8 {
return best;
}
-/// The window place `name` is drawn in (`Region.layer`), as the drag mapped it — or as it last
+/// The window place `name` is drawn in (`Region.layer`), as the drag mapped it - or as it last
/// registered, for a place the drag did not map.
fn layerOf(state: *const Layout.State, name: []const u8) u16 {
if (frozen(state, name)) |t| return t.layer;
@@ -1166,7 +1166,7 @@ fn layerOf(state: *const Layout.State, name: []const u8) u16 {
/// What the view being carried is, for deciding which places will take it.
/// The surface lifted at the start, not whatever the source place happens to
-/// be showing — mid-swap it is showing the *other* view, and reading that
+/// be showing - mid-swap it is showing the *other* view, and reading that
/// would change which places accept the drop halfway through it.
fn draggedKeywords(l: *Layout) []const []const u8 {
const id = l.state.view_drag.moved_id;
@@ -1214,7 +1214,7 @@ pub fn accepts(r: Region, surface_kw: []const []const u8) bool {
}
pub fn placeBounds(state: *const Layout.State, name: []const u8) ?dvui.Rect.Physical {
- // Mid-drag, a place is where it was when the drag began — see
+ // Mid-drag, a place is where it was when the drag began - see
// `mapTargets`. Everything the gesture measures reads this, so the zones,
// the edge the pointer is tested against and the split a drop settles are
// all cut from the same rect.
@@ -1247,7 +1247,7 @@ pub fn regionNamed(state: *const Layout.State, name: []const u8) ?*const Region
return null;
}
-/// The surface a place is showing — the selected one, not its whole
+/// The surface a place is showing - the selected one, not its whole
/// assignment. A multi place drags the tab you can see, not all of them.
pub fn visibleId(l: *Layout, name: []const u8) ?[]const u8 {
if (regionNamed(l.state, name)) |r| {
@@ -1257,7 +1257,7 @@ pub fn visibleId(l: *Layout, name: []const u8) ?[]const u8 {
return if (ids.len > 0) ids[0] else null;
}
-// ── The live drag ───────────────────────────────────────────────────────────────────────────────
+// -- The live drag -------------------------------------------------------------------------------
/// Keep a live drag moving: its view named, and frames coming while it rides the pointer.
/// Called from every place that draws during a drag; idempotent within a frame.
@@ -1270,7 +1270,7 @@ pub fn tick(l: *Layout) void {
}
}
-// ── Painting ────────────────────────────────────────────────────────────────────────────────────
+// -- Painting ------------------------------------------------------------------------------------
/// Whether the pointer is over `name` as the place a release would land on.
fn aimedAt(l: *Layout, name: []const u8) bool {
@@ -1286,20 +1286,20 @@ fn aimedAt(l: *Layout, name: []const u8) bool {
/// over them. `key` is any id stable for the place.
pub fn drawZones(l: *Layout, name: []const u8, key: dvui.Id) void {
// Only the place under the pointer: its zones come in as the pointer arrives and clear as it
- // leaves for another, whose come in over it — for as long as they are still showing, so a
+ // leaves for another, whose come in over it - for as long as they are still showing, so a
// place left mid-fade finishes going.
// Over a chooser the drop is into its place, shown by the chooser (`Chooser`); the places'
// own zones step back.
// With the drag over, a drop still going is `drawOverlay`'s to finish where it was: the place
// it was over may be renamed, moved or gone now, and looking it up again forgot the drop
- // mid-way — it vanished instead of running back together.
+ // mid-way - it vanished instead of running back together.
if (!l.state.view_drag.active()) return;
const over_chooser = chooserAt(l.state, dvui.currentWindow().mouse_pt) != null;
const aimed = aimedAt(l, name) and !over_chooser;
const target = aimed and isTarget(l, name);
if (!target and !DropZones.showing(key)) return;
// Over the place less its own strip (`interiorBounds`): the strip takes the view into the
- // place's list, and the zones are for its content — the part of it no float lies over
+ // place's list, and the zones are for its content - the part of it no float lies over
// (`zoneBounds`). All of it under floats, it has no drop: one still going finishes where it was.
const whole = interiorBounds(l.state, name) orelse {
DropZones.forget(key);
@@ -1362,14 +1362,14 @@ pub fn drawZones(l: *Layout, name: []const u8, key: dvui.Id) void {
}
/// What a view drag draws over every place at once, after they have all drawn: the drops they
-/// queued (`drawZones`), and over them the card riding the pointer — in a layer of its own over the
+/// queued (`drawZones`), and over them the card riding the pointer - in a layer of its own over the
/// window and every float on it.
pub fn drawOverlay(l: *Layout) void {
const d = &l.state.view_drag;
const now = dvui.currentWindow().frame_time_ns;
if (d.active()) followAcross(d, dvui.currentWindow().mouse_pt);
const queued = if (d.pending_frame == now) d.pending[0..d.pending_count] else d.pending[0..0];
- // This frame's drops, and any from last frame still going that no place asked for this time —
+ // This frame's drops, and any from last frame still going that no place asked for this time -
// the place a drop just landed on can be gone or changed by now, and its drop still has to run
// back together and shrink away rather than vanish mid-way.
// Whether the float the view is carried out of is firm, kept for the frames after this one
@@ -1403,17 +1403,17 @@ pub fn drawOverlay(l: *Layout) void {
defer layer.deinit();
// On every screen (`core.screens`): a place in a float popped out into its own window has
// its drop drawn here, at that window's part of the frame, and so does the carried view
- // when the pointer is there — clipped to the main window, both were dropped as they were
+ // when the pointer is there - clipped to the main window, both were dropped as they were
// drawn, and the app copies this layer into every such window as well as the main one.
core.screens.markEverywhere(layer.data().id);
- // The drops, then the card over them: one layer, so their order is the order drawn — the
+ // The drops, then the card over them: one layer, so their order is the order drawn - the
// card's glass showing the drop it is aimed at blurred through it, its top left just off the
// pointer so the bubble under the pointer stays in view.
const scale = dvui.currentWindow().natural_scale;
const prev_clip = dvui.clipGet();
const mouse = dvui.currentWindow().mouse_pt;
// The view as a drop, run together with the drop it is over: reaching a bubble, it bridges
- // into it — which bubble a release takes, said by the glass itself.
+ // into it - which bubble a release takes, said by the glass itself.
const carried = if (d.active()) dropShapes(l, drops[0..n]) else d.drop_shapes[0..0];
var taken = false;
var clips: [max_offers]dvui.Rect.Physical = undefined;
@@ -1425,7 +1425,7 @@ pub fn drawOverlay(l: *Layout) void {
if (!taken and over and !core.dialogs.carry_windows) look.carried = carried;
// The icons go over the carried view, which is laid on the drop after it: the bubble it
// is about to be dropped in says what it does through it. Where the OS draws the glass,
- // over its glass too — after the drag as well, while the drops go (below).
+ // over its glass too - after the drag as well, while the drops go (below).
if (d.active() or core.native_glass.on()) look.icons = .later;
// Carrying the view, the drop is not held to its place: the carried drop reaches past it.
clips[i] = if (look.carried.len > 0) prev_clip else p.clip;
@@ -1454,9 +1454,9 @@ pub fn drawOverlay(l: *Layout) void {
}
/// A layer of the drag's over every float, drawn on every screen (`core.screens.allPixels`). A
-/// floating widget stays just above the window it was made in — this one the app's own, which every
+/// floating widget stays just above the window it was made in - this one the app's own, which every
/// float is over, so the drops on a float's place and the view carried over a float were drawn under
-/// its glass — and is re-added as a window of its own and raised, as the demo overlay's layers are
+/// its glass - and is re-added as a window of its own and raised, as the demo overlay's layers are
/// (`automation/overlay.zig`). It takes no pointer events: what is under it still takes them, the
/// drag's hold on the pointer included.
fn layerOver(layer: *dvui.FloatingWidget, src: std.lang.SourceLocation) void {
@@ -1468,10 +1468,10 @@ fn layerOver(layer: *dvui.FloatingWidget, src: std.lang.SourceLocation) void {
}
/// Where carried things are windows of their own (`core.dialogs.carry_windows`: fizzy's carry
-/// window), what is carried — a card, a tab, a drop — is drawn in a layer of its own that only its
+/// window), what is carried - a card, a tab, a drop - is drawn in a layer of its own that only its
/// window draws (`core.screens.markCarried`): the window is all of it, the OS's material its
-/// glass, and nothing in the app's own windows follows it. A copy there — its glass, a drop's tail,
-/// the drop it ran into — was drawn at another moment than the window server moved the window, and
+/// glass, and nothing in the app's own windows follows it. A copy there - its glass, a drop's tail,
+/// the drop it ran into - was drawn at another moment than the window server moved the window, and
/// trailed behind it. Whether it is; the layer is the caller's to `deinit` then.
fn ownWindowLayer(layer: *dvui.FloatingWidget, src: std.lang.SourceLocation) bool {
if (!core.dialogs.carry_windows) return false;
@@ -1480,9 +1480,9 @@ fn ownWindowLayer(layer: *dvui.FloatingWidget, src: std.lang.SourceLocation) boo
return true;
}
-/// A drop coming in where another is going — the place the view is aimed at now lying under the
+/// A drop coming in where another is going - the place the view is aimed at now lying under the
/// one it was, as a float's place does under the place beneath it when the float firms up from its
-/// ghost — takes over the going one's glass and changes into itself (`DropZones.handOff`), rather
+/// ghost - takes over the going one's glass and changes into itself (`DropZones.handOff`), rather
/// than coming in over it as it goes: both in one spot read as the drop opening and shutting twice.
/// Returns how many of `drops` are left, the ones handed off taken out.
fn handOffDrops(drops: []PendingDrop) usize {
@@ -1510,7 +1510,7 @@ fn handOffDrops(drops: []PendingDrop) usize {
return n;
}
-/// Points: the radius of the view carried as a drop — near the drop zones' middle bubble's (a
+/// Points: the radius of the view carried as a drop - near the drop zones' middle bubble's (a
/// quarter under it), so what is carried reads about as big as where it goes, and is still seen
/// beside a finger.
const drop_r: f32 = 52;
@@ -1519,20 +1519,20 @@ const drop_r: f32 = 52;
const drop_pull: f32 = 0.55;
/// Before it is aimed at one, the drop leans toward the nearest bubble of the place under the
-/// pointer as it comes within `lean_reach` points of it, edge to edge — up to `drop_lean` of the
-/// way there as they touch — so it is drawn in from a little way off rather than only once it is
+/// pointer as it comes within `lean_reach` points of it, edge to edge - up to `drop_lean` of the
+/// way there as they touch - so it is drawn in from a little way off rather than only once it is
/// on the bubble (the user: more attraction near the bubbles). Where the hand holds it decides it
/// (`holdHand`), nothing else: nothing eases or swings.
const drop_lean: f32 = 0.22;
const lean_reach: f32 = 56;
/// The farthest the pointer moves in a frame within one window's part of the frame, physical
-/// pixels: past it, it went from one window to another — a float out of the main window is drawn
+/// pixels: past it, it went from one window to another - a float out of the main window is drawn
/// in a band 100000 pixels on (`Floats.Viewport`).
const across_jump: f32 = 30000;
-/// The pointer gone from one window's part of the frame to another's in one frame — out of a
-/// float's window over the main window, or back — the carried view goes with it as it is: where
+/// The pointer gone from one window's part of the frame to another's in one frame - out of a
+/// float's window over the main window, or back - the carried view goes with it as it is: where
/// the hand holds it and the shape it is changing from move by the same jump. Left, they swept back across
/// the band and the view showed for a frame or two in the window it had left.
fn followAcross(d: *ViewDrag, mouse: dvui.Point.Physical) void {
@@ -1564,7 +1564,7 @@ fn followAcross(d: *ViewDrag, mouse: dvui.Point.Physical) void {
d.shape_rect.y += dy;
}
-/// The view carried as a drop this frame, where the hand holds it (`holdHand`) — or none
+/// The view carried as a drop this frame, where the hand holds it (`holdHand`) - or none
/// where it is carried as a card (`drawFloat`): no glass program, no photograph, or over a list.
fn dropShapes(l: *Layout, drops: []const PendingDrop) []const core.LiquidField.Shape {
const d = &l.state.view_drag;
@@ -1597,12 +1597,12 @@ fn dropShapes(l: *Layout, drops: []const PendingDrop) []const core.LiquidField.S
},
else => {},
};
- // Off where the hand holds it (`holdHand`), so the bubble under the pointer stays in view —
+ // Off where the hand holds it (`holdHand`), so the bubble under the pointer stays in view -
// below and right of a mouse; up and left of a finger, the finger at the drop's bottom-right
// corner, where the hand holding it covers none of it.
const at = dropCenter(holdHand(d, mouse), R, d.drop_touch);
// And drawn toward the bubble it is aimed at, far enough that the two run together: by time,
- // never by the hand — joined, it moves only as the hand does. A bubble newly aimed at is eased
+ // never by the hand - joined, it moves only as the hand does. A bubble newly aimed at is eased
// onto, without swinging past it.
var aimed: ?dvui.Point.Physical = null;
for (drops) |p| {
@@ -1612,8 +1612,8 @@ fn dropShapes(l: *Layout, drops: []const PendingDrop) []const core.LiquidField.S
}
const ease = std.math.clamp(dt / aim_s, 0, 1);
var moving = false;
- // Drawn into a bubble newly aimed at, and over to another, as one motion from where it was —
- // past the bubble and back where motion is playful, as it pours in (`pour`) — and let go
+ // Drawn into a bubble newly aimed at, and over to another, as one motion from where it was -
+ // past the bubble and back where motion is playful, as it pours in (`pour`) - and let go
// smoothly: by time, so a hand held still sets nothing going (the user: as strong and snappy
// as the app's own glass).
const pull_want: f32 = if (aimed != null) 1 else 0;
@@ -1681,8 +1681,8 @@ fn dropShapes(l: *Layout, drops: []const PendingDrop) []const core.LiquidField.S
const pull = drop_pull * d.pull;
if (d.aim_at) |a| centre = .{ .x = centre.x + (a.x - centre.x) * pull, .y = centre.y + (a.y - centre.y) * pull };
- // From the shape it was last drawn as — what was grabbed at the lift, the tab it was over a
- // strip — to the drop, on the card's own curve: that rounded rect closing into a circle.
+ // From the shape it was last drawn as - what was grabbed at the lift, the tab it was over a
+ // strip - to the drop, on the card's own curve: that rounded rect closing into a circle.
const t = morphProgress(d.*, now);
const grown = std.math.clamp(t, 0, 1);
const to: dvui.Rect.Physical = .{ .x = centre.x - R, .y = centre.y - R, .w = 2 * R, .h = 2 * R };
@@ -1703,8 +1703,8 @@ fn modeAt(l: *Layout, mouse: dvui.Point.Physical) Mode {
/// A change of what the view is carried as: the new shape sets out from the one last drawn.
///
-/// The first, at the lift, is from what was grabbed, which already shows what it is carried as —
-/// a tab lifted along a strip is carried as the tab it was — so it grows into its shape with its
+/// The first, at the lift, is from what was grabbed, which already shows what it is carried as -
+/// a tab lifted along a strip is carried as the tab it was - so it grows into its shape with its
/// face whole, rather than fading its own face back in.
fn noteMode(d: *ViewDrag, mode: Mode, now: i128) void {
const first = d.lifting;
@@ -1738,8 +1738,8 @@ fn morphProgress(d: ViewDrag, now: i128) f32 {
return if (dur <= 0) 1 else core.motion.enter(@floatCast(std.math.clamp(elapsed / dur, 0, 1)));
}
-/// The view carried as a drop: its glass — run in with the drop it is over when that took it
-/// (`taken`), its own otherwise — and its photograph inside the head, cropped to fill it.
+/// The view carried as a drop: its glass - run in with the drop it is over when that took it
+/// (`taken`), its own otherwise - and its photograph inside the head, cropped to fill it.
fn drawDrop(l: *Layout, taken: bool) void {
const d = &l.state.view_drag;
const scale = dvui.currentWindow().natural_scale;
@@ -1756,7 +1756,7 @@ fn drawDrop(l: *Layout, taken: bool) void {
}
if (!taken and (!own_window or !core.dialogs.carry_material)) {
// At the merge it is drawn at inside a place's drop (`DropZones.merge`): drawn alone at a
- // wider one, between two places — over the sash between them — the drop grew by some 8% of
+ // wider one, between two places - over the sash between them - the drop grew by some 8% of
// its radius, and shrank back as the pointer reached either side.
var field: core.LiquidField = .{ .merge_px = DropZones.merge * scale };
for (d.drop_shapes[0..d.drop_n]) |sh| field.add(sh);
@@ -1781,7 +1781,7 @@ fn drawDrop(l: *Layout, taken: bool) void {
}
const shown = contentIn(d.*, morphProgress(d.*, dvui.currentWindow().frame_time_ns));
// On the OS's glass, where the app hands it over (`core.native_glass.under`) and has its pixels
- // to: what the view shows, fitted to it (`photoFit`), over the drop's frost — clear what is
+ // to: what the view shows, fitted to it (`photoFit`), over the drop's frost - clear what is
// carried, and the drop the bubbles' glass (the user).
if (own_window and core.native_glass.on() and core.native_glass.under() and d.photo_pixels != null) {
d.photo_under = .{ .rect = head, .radius = d.drop_radius, .alpha = shown * (1 - aim_fade_under * d.drop_aim), .image = photoFit(d.*, head) };
@@ -1817,7 +1817,7 @@ fn drawDropLabel(l: *Layout, head: dvui.Rect.Physical, shown: f32) void {
const title_raw = dropTitle(l, d.*) orelse return;
if (title_raw.len == 0 or shown <= 0.01) return;
const hn = head.toNatural();
- // As the explorer's headings are (`Chooser.label`): uppercase, in the heading font — in the
+ // As the explorer's headings are (`Chooser.label`): uppercase, in the heading font - in the
// window's text colour, not the heading's highlight (the user).
var upper: [128]u8 = undefined;
const title = if (title_raw.len <= upper.len) std.ascii.upperString(&upper, title_raw) else title_raw;
@@ -1854,7 +1854,7 @@ fn drawDropLabel(l: *Layout, head: dvui.Rect.Physical, shown: f32) void {
}
/// Whether dropping the view lifted from `source` in the middle of `dest` joins them: the two
-/// halves of one split (`State.joinable`), with the view the last thing `source` shows — a
+/// halves of one split (`State.joinable`), with the view the last thing `source` shows - a
/// place of one, or a place of tabs down to this one. Carried out of a place that keeps other
/// views, it is only moving, and the middle means what it means anywhere else.
fn joins(l: *Layout, source: []const u8, dest: []const u8) bool {
@@ -1864,7 +1864,7 @@ fn joins(l: *Layout, source: []const u8, dest: []const u8) bool {
return holding(l, source).len <= 1;
}
-/// Whether `name` is somewhere the dragged view could land — one of the places mapped at lift
+/// Whether `name` is somewhere the dragged view could land - one of the places mapped at lift
/// (`mapTargets`), or the place it came from.
fn isTarget(l: *Layout, name: []const u8) bool {
const d = l.state.view_drag;
@@ -1874,7 +1874,7 @@ fn isTarget(l: *Layout, name: []const u8) bool {
return false;
}
-/// Whether `key`'s zones are on screen — the place the drag is over, or fading out after it.
+/// Whether `key`'s zones are on screen - the place the drag is over, or fading out after it.
pub fn zonesShowing(l: *Layout, name: []const u8, key: dvui.Id) bool {
return (isTarget(l, name) and aimedAt(l, name)) or DropZones.showing(key);
}
@@ -1896,7 +1896,7 @@ pub fn drawFloat(l: *Layout, taken: bool) void {
}
const mouse = dvui.currentWindow().mouse_pt;
const now = dvui.currentWindow().frame_time_ns;
- // Over a chooser — a tab strip, a rail — the view is going into a list, and the card is a
+ // Over a chooser - a tab strip, a rail - the view is going into a list, and the card is a
// tab: the preview of a place is for the places' insides. What it is carried as was settled
// this frame (`dropShapes`, `noteMode`); each change grows from the shape it was, the way a
// dialog grows open.
@@ -1917,18 +1917,18 @@ pub fn drawFloat(l: *Layout, taken: bool) void {
const off = dvui.dragOffset();
// The pointer keeps its place on the card: the card's top left stays where it was from the
// pointer when it was grabbed, and the card grows right and down from there into what it
- // shows — so what the tab was held by is still under the pointer, and the card grows away
+ // shows - so what the tab was held by is still under the pointer, and the card grows away
// from the place it is aimed at rather than over it. Pulled in only as far as keeps the
// pointer on the card: a place grabbed far from its corner shrinks to a card far smaller.
// Once it has been a drop, the grab point is long gone: the card hangs down and right from the
- // pointer, just inside its top left, as the drop did — kept at the grab point, a place grabbed
+ // pointer, just inside its top left, as the drop did - kept at the grab point, a place grabbed
// far from its corner hung up and left of the pointer, anchored at its bottom right (the user).
const inset = 8 * scale;
const tl: dvui.Point.Physical = if (d.been_drop) .{ .x = mouse.x - inset, .y = mouse.y - inset } else if (d.loose()) blk: {
- // Lifted out of a list — a row of a file tree, a card of the picker — it keeps its middle
+ // Lifted out of a list - a row of a file tree, a card of the picker - it keeps its middle
// where the middle of what was grabbed was, so a row's name stays where it was in the row
- // as the row narrows to it. Grabbed off what it shows — out along the row past its name,
- // rows lifted together far from the one under the pointer — it is brought in under the
+ // as the row narrows to it. Grabbed off what it shows - out along the row past its name,
+ // rows lifted together far from the one under the pointer - it is brought in under the
// pointer as it grows into itself (`morphProgress`), the pointer kept off its very end: a
// pill's, the middle of its round end.
const keep_x = if (show_photo) inset else @min(target.h, target.w) / 2;
@@ -1979,8 +1979,8 @@ pub fn drawFloat(l: *Layout, taken: bool) void {
const shown = contentIn(d.*, t);
if (if (show_photo) d.texture else null) |tex| {
// The photograph (backed by the content fill, `backed`), inset in the glass, its corners
- // following the card's: at `photo_opacity`, so the glass — and what is under the card,
- // through it — shows as the card moves.
+ // following the card's: at `photo_opacity`, so the glass - and what is under the card,
+ // through it - shows as the card moves.
const inner = dvui.CornerRect.round(@max(0, radius / scale - card_padding));
dvui.renderTexture(tex, fw.data().contentRectScale(), .{ .corners = inner, .colormod = dvui.Color.white.opacity(photo_opacity * shown) }) catch {};
} else {
@@ -1998,12 +1998,12 @@ const card_padding: f32 = 6;
/// How opaque the card's photograph is over its glass.
const photo_opacity: f32 = 0.8;
/// How much of the drop's photograph goes while it is joined to the bubble it is aimed at: nearly
-/// opaque over the bubble it read as a disc under it, not glass running into glass (the user) — the
+/// opaque over the bubble it read as a disc under it, not glass running into glass (the user) - the
/// bubble lights itself (`DropZones`).
const aim_fade: f32 = 0.75;
/// How much of the drop's photograph goes as it joins a bubble where it lies under the OS's glass:
-/// little — the bubble's glass bends and blurs it where the drop runs into it, and it reads through
-/// that (the user) — only enough that the bubble's icon reads over it.
+/// little - the bubble's glass bends and blurs it where the drop runs into it, and it reads through
+/// that (the user) - only enough that the bubble's icon reads over it.
const aim_fade_under: f32 = 0.3;
/// Seconds the drop's photograph takes to fade as it joins a bubble, and to come back.
const aim_s: f32 = 0.09;
@@ -2019,7 +2019,7 @@ fn joinStep(dt: f32) f32 {
}
/// How far through being drawn into a bubble the drop is at `u` of `join_ms`: quick, then past it
-/// and back, as water pours in, as far past as motion is playful (`core.motion`) — none at the low
+/// and back, as water pours in, as far past as motion is playful (`core.motion`) - none at the low
/// end, an ease out only. Time alone moves it.
fn pour(u: f32) f32 {
const play = std.math.clamp((core.motion.level() - 0.5) * 2, 0, 1);
@@ -2033,8 +2033,8 @@ fn smooth(u: f32) f32 {
return x * x * (3 - 2 * x);
}
-/// Points: the tab face on a card with no photograph — a file icon, the title and, when there
-/// are unsaved changes, the dirty dot — and the gaps between them.
+/// Points: the tab face on a card with no photograph - a file icon, the title and, when there
+/// are unsaved changes, the dirty dot - and the gaps between them.
const face_icon: f32 = 16;
const face_gap: f32 = 6;
const face_dot: f32 = 7;
@@ -2070,7 +2070,7 @@ fn pillSize(l: *Layout, d: ViewDrag, title: []const u8, scale: f32) dvui.Size.Ph
fn drawTabFace(l: *Layout, d: ViewDrag, title: []const u8) void {
// As the strip draws the tab it was (`Workspace`'s tab row): the icon in the control colour,
// in the file tree's glyph slot, a plain file glyph where no plugin draws one; the title as a
- // selected tab's — it is the tab in hand. The face drawn before the tab became glass is the
+ // selected tab's - it is the tab in hand. The face drawn before the tab became glass is the
// strip's own, so anything else here changed on the way back.
const theme = dvui.themeGet();
const icon_color = core.widgets.treeGlyphColor(theme);
@@ -2103,7 +2103,7 @@ fn drawTabFace(l: *Layout, d: ViewDrag, title: []const u8) void {
};
}
-/// What the carried thing is called on its glass: the view's title, a document's file name — or
+/// What the carried thing is called on its glass: the view's title, a document's file name - or
/// what the first of several lifted together, or something no place takes, was lifted as.
fn carriedTitle(l: *Layout, d: ViewDrag) []const u8 {
if (d.with_n > 0) return itemTitle(l, d.with[0]);
@@ -2116,7 +2116,7 @@ fn itemTitle(l: *Layout, it: Carried) []const u8 {
}
/// Carried as the rows it was lifted as rather than one tab's face: several lifted together, or
-/// something no place takes — a folder, by its name and a folder's icon.
+/// something no place takes - a folder, by its name and a folder's icon.
fn asRows(d: ViewDrag) bool {
return d.with_n > 1 or d.held;
}
@@ -2209,7 +2209,7 @@ fn floatTarget(from: dvui.Size.Physical, scale: f32) dvui.Size.Physical {
return .{ .w = w, .h = h };
}
-// ── The landing ─────────────────────────────────────────────────────────────────────────────────
+// -- The landing ---------------------------------------------------------------------------------
/// Release at `mouse`. Does nothing unless the pointer is somewhere a drop
/// means something, so letting go over the window frame cancels.
@@ -2217,22 +2217,22 @@ pub fn apply(l: *Layout, source: []const u8, mouse: dvui.Point.Physical) void {
// Nothing lands what no place takes: the plugin that lifted it has read the release itself.
if (l.state.view_drag.held) return;
// Let go over no window of the app's, where floats are OS windows of their own: a float opens
- // there, in a window of its own, with the view in it — as a tab torn off onto the desktop opens
+ // there, in a window of its own, with the view in it - as a tab torn off onto the desktop opens
// a window where it lands.
if (l.state.floats_windowed and overNoWindow(mouse)) return floatAway(l, source, mouse);
if (chooserAt(l.state, mouse)) |o| {
if (!o.into) return;
if (dropOnPluginChooser(l, source, o.name, mouse)) return;
- // Where along it the chooser said: in the place's list there — on the chooser it came
+ // Where along it the chooser said: in the place's list there - on the chooser it came
// off, moved along it.
if (o.at) |at| return insertInto(l, source, o.name, at);
- // Its own place's chooser, saying nothing of where along it: no move. Never a float —
+ // Its own place's chooser, saying nothing of where along it: no move. Never a float -
// that is the middle of the place, not its list.
if (std.mem.eql(u8, o.name, source)) return;
place(l, source, o.name, .swap);
return;
}
- // Where the carried view aims — the drop's middle where it is carried as one — not where the
+ // Where the carried view aims - the drop's middle where it is carried as one - not where the
// pointer is, so the release lands where the glass showed it would.
const a = aimFor(l, mouse);
const dest = targetAtAim(l, a, source) orelse return;
@@ -2243,7 +2243,7 @@ pub fn apply(l: *Layout, source: []const u8, mouse: dvui.Point.Physical) void {
}
/// A release over a plugin region's own chooser: straight to the plugin's `on_drop`, as into the
-/// region with where along its strip — even for the region the view came out of, whose strip
+/// region with where along its strip - even for the region the view came out of, whose strip
/// reorders it, which `place` would refuse as no move. False when `dest` is not a plugin region
/// with a drop and a chooser of its own (`owns_selection`), for `place` to handle: the strip over
/// a region that leaves choosing to the app is the app's (`insertInto`).
@@ -2260,17 +2260,17 @@ fn dropOnPluginChooser(l: *Layout, source: []const u8, dest: []const u8, mouse:
return true;
}
-/// Put the view carried out of `source` into place `dest`'s list at `at`, and show it there — let
+/// Put the view carried out of `source` into place `dest`'s list at `at`, and show it there - let
/// go over a chooser the app drew (`Chooser`), where along it. Into the place it came out of, it
/// moves along the list: the place's order, kept as its assignment when it has one, else as an
-/// order (`State.setOrder`) — never as a new assignment, which would freeze a place its keywords
+/// order (`State.setOrder`) - never as a new assignment, which would freeze a place its keywords
/// fill against views registered later. Into another place, it leaves the one it was in, as any
/// drop into a place's middle does, and the list it goes into is written down.
pub fn insertInto(l: *Layout, source: []const u8, dest: []const u8, at: Insert) void {
const moved = ownId(l.arena, movedFrom(l, source) orelse return) orelse return;
const r = regionNamed(l.state, dest) orelse return;
if (!accepts(r.*, keywordsOf(l, moved))) return;
- // A document — open, or a file out of the tree — along a plain place's chooser goes in there
+ // A document - open, or a file out of the tree - along a plain place's chooser goes in there
// inside a new view holding it (`house`).
if (!r.kind_slot and slottedId(l, moved)) {
if (l.state.documents == null) return;
@@ -2282,7 +2282,7 @@ pub fn insertInto(l: *Layout, source: []const u8, dest: []const u8, at: Insert)
const held = holding(l, dest);
const along = containsId(held, moved);
// The whole list it goes into: the assignment, ids of plugins not loaded now included so they
- // keep their slots; or what the place shows by keyword, then — moving along an order —
+ // keep their slots; or what the place shows by keyword, then - moving along an order -
// whatever an earlier order named that is not showing now.
var base: std.ArrayList([]const u8) = .empty;
base.appendSlice(l.arena, held) catch return;
@@ -2352,7 +2352,7 @@ pub fn place(l: *Layout, source: []const u8, dest: []const u8, kind: Drop.Kind)
// Split onto its own place, a view stays where it is (the empty leaf opens beside it) only
// when it is all the place holds. Beside others it goes to the leaf under the pointer and they
// stay: left in the origin, the view and the others kept the place between them and the leaf
- // opened empty on the far side — a tab dragged to the top of its own strip's place moved the
+ // opened empty on the far side - a tab dragged to the top of its own strip's place moved the
// tabs left behind to the bottom, as though another view had been carried.
const same = std.mem.eql(u8, source, dest);
// A document dropped on its own slot's middle floats, in a view holding it (`floatsDocument`).
@@ -2365,7 +2365,7 @@ pub fn place(l: *Layout, source: []const u8, dest: []const u8, kind: Drop.Kind)
// The trash is about what is carried, not where it was let go.
if (plan == .remove) return remove(l, source);
const moved = ownId(l.arena, movedFrom(l, source) orelse return) orelse return;
- // Out of its own place into a window of its own: the framework's to do, not the place's —
+ // Out of its own place into a window of its own: the framework's to do, not the place's -
// a plugin region's own drop is never asked.
if (plan == .float) {
floatOut(l, source, moved, null);
@@ -2429,7 +2429,7 @@ pub fn place(l: *Layout, source: []const u8, dest: []const u8, kind: Drop.Kind)
/// A document carried onto a plain place, which only a slot made for it takes: it lands there
/// inside a new view of the application's that holds it, and what was carried with it
-/// (`State.documents`), as a view carried there would land — an edge splits the place and the
+/// (`State.documents`), as a view carried there would land - an edge splits the place and the
/// new half holds it; the middle adds it beside what the place holds. Where the document came from is the slot's plugin's to tidy: its tab moves, as a drop
/// into another slot moves it.
fn house(l: *Layout, dest: []const u8, plan: Drop.Plan, moved: []const u8) void {
@@ -2453,7 +2453,7 @@ fn house(l: *Layout, dest: []const u8, plan: Drop.Plan, moved: []const u8) void
/// The view a document carried alone carries with it (`State.Documents.alone`), and the place it
/// is in: the only tab of its workspace takes the workspace along, so the drop moves that, as
/// a drag of the workspace itself would, rather than making a new one round the document and
-/// leaving the old to close — a round trip that drew the document anew twice, and blinked.
+/// leaving the old to close - a round trip that drew the document anew twice, and blinked.
fn carriedView(l: *Layout, moved: []const u8) ?struct { view: []const u8, from: []const u8 } {
const docs = l.state.documents orelse return null;
if (l.state.view_drag.with_n > 1) return null;
@@ -2462,7 +2462,7 @@ fn carriedView(l: *Layout, moved: []const u8) ?struct { view: []const u8, from:
return .{ .view = view, .from = from };
}
-/// Land view `view`, shown in place `from`, on `dest` as `plan` says — what `place` does with a
+/// Land view `view`, shown in place `from`, on `dest` as `plan` says - what `place` does with a
/// view carried out of its place, for one a document carried along (`carriedView`).
fn landView(l: *Layout, from: []const u8, dest: []const u8, plan: Drop.Plan, view: []const u8) void {
switch (plan) {
@@ -2498,10 +2498,10 @@ fn houseView(l: *Layout, moved: []const u8) ?[]const u8 {
}
/// Whether the view carried out of `source` floats when it is dropped on the middle of `source`
-/// (`float_rules.canFloat`): not out of the picker, not a document — its place is the slot a
-/// plugin made for it — and not a view already alone in a float nobody split.
+/// (`float_rules.canFloat`): not out of the picker, not a document - its place is the slot a
+/// plugin made for it - and not a view already alone in a float nobody split.
/// Over no window of the app's: past the main window, and on no screen a float's window shows
-/// (`core.screens`) — the pointer is read in the main window's frame there (`SDLBackend.heldPoint`).
+/// (`core.screens`) - the pointer is read in the main window's frame there (`SDLBackend.heldPoint`).
fn overNoWindow(mouse: dvui.Point.Physical) bool {
if (dvui.windowRectPixels().contains(mouse)) return false;
const s = dvui.windowNaturalScale();
@@ -2510,7 +2510,7 @@ fn overNoWindow(mouse: dvui.Point.Physical) bool {
/// The view carried out of `source`, let go over no window of the app's (`apply`): a float of its
/// own opens round where it was let go (`floatOut`), which the application puts in an OS window of
-/// its own. Any view that may float — a view alone in a float's window too: its window closes, and
+/// its own. Any view that may float - a view alone in a float's window too: its window closes, and
/// one opens where it was let go.
fn floatAway(l: *Layout, source: []const u8, mouse: dvui.Point.Physical) void {
// Out of the picker too (a loose drag): the view it carries, wherever it is now.
@@ -2537,7 +2537,7 @@ fn canFloat(l: *Layout, source: []const u8) bool {
}
/// Whether the document carried loose floats when it is dropped on the middle of `dest`: the
-/// slot it is a tab of — its own workspace's, in fizzy — where the application gives documents
+/// slot it is a tab of - its own workspace's, in fizzy - where the application gives documents
/// views of their own (`State.documents`), as a view dropped on its own place's middle floats.
/// Not when the view it would float in is already alone in a float nobody split.
fn floatsDocument(l: *Layout, dest: []const u8) bool {
@@ -2567,7 +2567,7 @@ fn floatDocument(l: *Layout, moved: []const u8, at: ?dvui.Point.Physical) void {
}
/// Float `moved` out of `source`: a new float (`Floats`) holding just it, where `float_rules`
-/// opens one — out of another float, a step down and right of that one — growing out of the glass
+/// opens one - out of another float, a step down and right of that one - growing out of the glass
/// it was carried in, with the photograph the drag took of it. The source loses the view; a place
/// of several keeps the rest.
fn floatOut(l: *Layout, source: []const u8, moved: []const u8, at: ?dvui.Point.Physical) void {
@@ -2577,11 +2577,11 @@ fn floatOut(l: *Layout, source: []const u8, moved: []const u8, at: ?dvui.Point.P
var buf: [32]u8 = undefined;
const name = state.internName(l.gpa, float_rules.nextName(&buf, state.floats.names(l.arena)));
const window = Floats.toRules(dvui.windowRect());
- // Carried out of the picker (a loose drag), it floats out of whichever place holds it now —
- // its home when the float closes — or, held nowhere, out of none: its keywords take it home.
+ // Carried out of the picker (a loose drag), it floats out of whichever place holds it now -
+ // its home when the float closes - or, held nowhere, out of none: its keywords take it home.
const holder: ?[]const u8 = if (std.mem.eql(u8, source, loose_source)) holderOf(l, moved) else source;
const d = &state.view_drag;
- // Held nowhere — a document's new view (`floatDocument`) — it comes out of the pane its
+ // Held nowhere - a document's new view (`floatDocument`) - it comes out of the pane its
// photograph was taken in.
const photo_rect: ?dvui.Rect.Physical = if (d.active() and d.texture != null) d.texture_rect else null;
const src = (if (holder) |h| placeBounds(state, h) else null) orelse photo_rect orelse dvui.windowRectPixels();
@@ -2590,7 +2590,7 @@ fn floatOut(l: *Layout, source: []const u8, moved: []const u8, at: ?dvui.Point.P
float_rules.nudged(Floats.toRules(state.floats.items.items[i].rect), window)
else
float_rules.initialRect(Floats.toRules(src.toNatural()), window);
- // The glass it was carried in, when a drag let go of it here — a loose one too, the document
+ // The glass it was carried in, when a drag let go of it here - a loose one too, the document
// in it floating in a view it carried (`floatDocument`); the place itself, when nothing was
// carried (the picker, a test).
const carried = d.active() and (std.mem.eql(u8, d.name, source) or d.loose()) and d.shape_rect.w > 0;
@@ -2599,8 +2599,8 @@ fn floatOut(l: *Layout, source: []const u8, moved: []const u8, at: ?dvui.Point.P
// The window the place's size, its view inside it below its header (the user: a window no
// larger than the place it came out of).
if (carried and d.texture != null and out_of == null) rect = float_rules.asTaken(Floats.toRules(src.toNatural()), window);
- // Let go over no window of the app's (`floatAway`): its size, out there — not held on the main
- // window — its top left where the carried glass's was, which rode below and right of the
+ // Let go over no window of the app's (`floatAway`): its size, out there - not held on the main
+ // window - its top left where the carried glass's was, which rode below and right of the
// pointer: the glass grows into it right and down from where it was let go, rather than out
// round the pointer. With nothing carried, round where it was let go.
if (at) |p| {
@@ -2655,12 +2655,12 @@ pub fn holderOf(l: *Layout, id: []const u8) ?[]const u8 {
/// (`float_rules.goHome`): into the list of `home`, the place the float came out of, when the user
/// had arranged it; let go where their keywords show them, which for a place its keywords fill is
/// home again; and any other view into home's list all the same, or another place where home
-/// cannot take it — a view merged into the float from elsewhere may have keywords no place
+/// cannot take it - a view merged into the float from elsewhere may have keywords no place
/// answers, and let go it would be shown nowhere. Each is selected where it lands, so the view
/// the user had in front of them is in front of them again.
pub fn sendHome(l: *Layout, leaves: []const []const u8, home: []const u8) void {
const state = l.state;
- // No home (a saved float whose home was lost). Not looked up — an unnamed place would answer
+ // No home (a saved float whose home was lost). Not looked up - an unnamed place would answer
// to "".
const r = if (home.len > 0) regionNamed(state, home) else null;
// Every view out of the float's places first, so what a place's keywords show is read with
@@ -2721,7 +2721,7 @@ fn mainPlaceFor(l: *Layout, id: []const u8) ?*const Region {
return null;
}
-/// An empty place carried somewhere — the place itself is what moves, there being nothing in it.
+/// An empty place carried somewhere - the place itself is what moves, there being nothing in it.
/// The trash or another place's middle: it goes, closing into the half beside it (an empty place
/// dropped on an empty one leaves one empty place). Another place's edge: it goes from where it
/// was and opens there instead, an empty half of that place. Onto itself, nothing. Only a half of
@@ -2743,8 +2743,8 @@ fn placeEmpty(l: *Layout, source: []const u8, dest: []const u8, kind: Drop.Kind)
dvui.refresh(null, @src(), null);
}
-/// What the trash does with the view carried out of `source`: a document closes — the ordinary
-/// close, which asks about unsaved changes — and any other view leaves its place, back to the
+/// What the trash does with the view carried out of `source`: a document closes - the ordinary
+/// close, which asks about unsaved changes - and any other view leaves its place, back to the
/// picker it can be placed from again.
fn remove(l: *Layout, source: []const u8) void {
const moved = ownId(l.arena, movedFrom(l, source) orelse return) orelse return;
@@ -2765,22 +2765,22 @@ fn remove(l: *Layout, source: []const u8) void {
/// The surface a drop from `source` lands: the one the live drag lifted when
/// it is this drag's source (a picker drag carries a view its source may not
-/// be showing — or, loose, has no source at all), else what the place shows.
+/// be showing - or, loose, has no source at all), else what the place shows.
fn movedFrom(l: *Layout, source: []const u8) ?[]const u8 {
const d = l.state.view_drag;
if (d.active() and d.moved_id.len > 0 and std.mem.eql(u8, d.name, source)) return d.moved_id;
return visibleId(l, source);
}
-/// Land the view. A place takes it beside what it holds — as a tab, once it holds more than one
-/// — and the source simply loses it. Only a name no place answers to trades: the view there
+/// Land the view. A place takes it beside what it holds - as a tab, once it holds more than one
+/// - and the source simply loses it. Only a name no place answers to trades: the view there
/// goes back where the new one came from, because nowhere else is anywhere.
fn swap(l: *Layout, source: []const u8, dest: []const u8, moved: []const u8) void {
const other_raw = visibleId(l, dest);
const other = if (other_raw) |o| ownId(l.arena, o) else null;
// Dropped on a place that already holds it (a tab dropped back into its own strip's place):
- // it is already there, so show it and leave the list alone — claiming it would shrink the
+ // it is already there, so show it and leave the list alone - claiming it would shrink the
// place to it.
const declared = regionNamed(l.state, dest) != null;
if (declared) {
@@ -2843,8 +2843,8 @@ fn containsId(ids: []const []const u8, id: []const u8) bool {
/// What a place is holding: the list it was given, or the one its keywords
/// attract when it has never been given one. A place usually has no
-/// list of its own — the sidebar's tabs are every plugin that asked for the
-/// sidebar — and reading only the assignment there says "nothing", so a drop
+/// list of its own - the sidebar's tabs are every plugin that asked for the
+/// sidebar - and reading only the assignment there says "nothing", so a drop
/// onto the rail would leave it holding one lone view.
fn holding(l: *Layout, name: []const u8) []const []const u8 {
if (l.state.assignment(name)) |ids| return ids;
@@ -2882,7 +2882,7 @@ fn takeOut(l: *Layout, name: []const u8, moved: []const u8, give: ?[]const u8) v
///
/// The selection is remembered per place and only *read* against what the
/// place shows, so a chooser that has already dropped the icon can sit beside
-/// a body still drawing what that icon used to choose — which is what dragging
+/// a body still drawing what that icon used to choose - which is what dragging
/// Files out of the sidebar looked like until you clicked another icon.
fn reselect(l: *Layout, name: []const u8, gone: []const u8, kept: []const []const u8) void {
const key = if (regionNamed(l.state, name)) |r| r.selectionKey() else slotKey(name);
@@ -2916,7 +2916,7 @@ fn selectNamed(l: *Layout, name: []const u8, id: []const u8) void {
/// minted leaf is not furniture: it was a container for the view that has
/// just been carried out of it, and leaving a blank rectangle behind makes
/// the user tidy up after their own drag. `State.isMinted` is exactly that
-/// distinction — a place the tree is allowed to drop.
+/// distinction - a place the tree is allowed to drop.
///
/// The leaf a split *mints* is empty on purpose and is never passed here: it
/// is the room being made, not room left over.
@@ -2961,8 +2961,8 @@ fn closable(l: *Layout, name: []const u8) bool {
pub const Merged = struct { gone: []const u8, into: []const u8 };
/// Close the emptied place `name`, one half of a split the user made, into the other half. A
-/// minted half closes outright (`closeMinted`). The half that was split keeps its name — it is
-/// the shape's place, or the name its own parent split knows it by — so it takes the other half's
+/// minted half closes outright (`closeMinted`). The half that was split keeps its name - it is
+/// the shape's place, or the name its own parent split knows it by - so it takes the other half's
/// views and that half closes instead: the same one place either way, under the name the layout
/// knows. A minted half split again is that case too: closed outright it would take its whole
/// split with it. Returns that merge, when it was one. A place no user split made, or whose other
@@ -2989,7 +2989,7 @@ pub fn closeEmptied(l: *Layout, name: []const u8) ?Merged {
return .{ .gone = sibling, .into = into };
}
-/// Close a minted place, whatever it holds — sliding its split shut where it was drawn.
+/// Close a minted place, whatever it holds - sliding its split shut where it was drawn.
fn closeMinted(l: *Layout, name: []const u8) void {
if (!l.state.isMinted(name)) return;
// A seed's dock tree closes its own leaves, easing the split shut over it. A float's splits
@@ -3005,7 +3005,7 @@ fn closeMinted(l: *Layout, name: []const u8) void {
l.extents_changed = true;
return;
};
- // Never drawn at a size — or not drawn at all yet, made this frame — so there is nothing to
+ // Never drawn at a size - or not drawn at all yet, made this frame - so there is nothing to
// slide: drop it outright.
if (l.state.splits.collapse(l.gpa, name)) {
if (l.state.clearExtent(l.gpa, name)) l.extents_changed = true;
diff --git a/app/layout/float_rules.zig b/app/layout/float_rules.zig
index 9454abdc1..092c64769 100644
--- a/app/layout/float_rules.zig
+++ b/app/layout/float_rules.zig
@@ -1,7 +1,7 @@
//! The rules a floating view follows, as values: where its window opens, what keeps it on
//! screen, what it is called, how the floats stack, when a view may float and where it goes when
//! its float closes. `Floats.zig` and `ViewDrag.zig` apply them; nothing here draws or holds
-//! state. std-only, so the rules are unit-tested without a Window (`build/app.zig`) — tests in
+//! state. std-only, so the rules are unit-tested without a Window (`build/app.zig`) - tests in
//! the dvui-facing layout files are never collected.
const std = @import("std");
@@ -26,7 +26,7 @@ pub const Rect = struct {
}
};
-/// A float opens at this share of the place it came out of — big enough to read the view in,
+/// A float opens at this share of the place it came out of - big enough to read the view in,
/// small enough that what it floated off is still there round it.
pub const share: f32 = 0.6;
/// The smallest a float opens: a view shrunk below this is not one anybody can use.
@@ -37,7 +37,7 @@ pub const open_max_share: f32 = 0.9;
/// How far a float keeps from the window's edge when it is placed for the user.
pub const margin: f32 = 8;
/// How far a float floated out of another float steps down and right, so the two never sit
-/// exactly over one another — the step dvui's own windows take (`window_avoid`).
+/// exactly over one another - the step dvui's own windows take (`window_avoid`).
pub const nudge: f32 = 24;
/// The smallest the user may resize a float to: its header and a little of its view.
pub const resize_min_w: f32 = 160;
@@ -61,7 +61,7 @@ pub fn initialRect(source: Rect, window: Rect) Rect {
/// Where a float opens for a view carried out of `source` and let go as a drop with its picture:
/// the place itself, its size and where it was, kept inside the window and no smaller than a float
-/// may be resized to — so the picture it grows with lands on the view as the float shows it (the
+/// may be resized to - so the picture it grows with lands on the view as the float shows it (the
/// user: the size the popped out region was, aligned with its snapshot).
pub fn asTaken(source: Rect, window: Rect) Rect {
const cap_w = @max(0, window.w - 2 * margin);
@@ -93,7 +93,7 @@ pub fn reachable(r: Rect, window: Rect) Rect {
}, window);
}
-/// `r` moved, not resized, to lie `margin` inside `window` — or as close to its top left as it
+/// `r` moved, not resized, to lie `margin` inside `window` - or as close to its top left as it
/// can when it is too large to.
fn inside(r: Rect, window: Rect) Rect {
var out = r;
@@ -109,7 +109,7 @@ pub fn resized(r: Rect) Rect {
return .{ .x = r.x, .y = r.y, .w = @max(r.w, resize_min_w), .h = @max(r.h, resize_min_h) };
}
-/// `a` toward `b` by `t` (0…1): a landing float on its way from the carried drop to its window.
+/// `a` toward `b` by `t` (0...1): a landing float on its way from the carried drop to its window.
pub fn lerp(a: Rect, b: Rect, t: f32) Rect {
return .{
.x = std.math.lerp(a.x, b.x, t),
@@ -140,7 +140,7 @@ pub fn isUnder(name: []const u8, root: []const u8) bool {
/// The floats' order once dvui has had its say: `out[i]` is the index into `ids` of the float
/// `i`th from the bottom. `ids` are the floats' window ids in their current order, `stack` the
-/// window stack bottom to top — a press raises a window there. A float not in the stack (it has
+/// window stack bottom to top - a press raises a window there. A float not in the stack (it has
/// not drawn yet: it was made this frame) keeps its place among the others like it, above
/// every float that is.
pub fn stackOrder(out: []usize, ids: []const u64, stack: []const u64) void {
@@ -164,7 +164,7 @@ pub fn stackOrder(out: []usize, ids: []const u64, stack: []const u64) void {
pub const Carried = struct {
/// Out of the picker (or a plugin's own drag): it has no place to float out of.
loose: bool = false,
- /// A tab's content — an open document — whose place is the slot a plugin made for it.
+ /// A tab's content - an open document - whose place is the slot a plugin made for it.
slotted: bool = false,
/// Already the only view of a float nobody split: floating it again would only move it.
alone_in_float: bool = false,
@@ -183,7 +183,7 @@ pub const Home = struct {
/// It holds what the user put there (an assignment), rather than what its keywords choose.
assigned: bool,
/// Let go, the view's keywords would show it in a place of the main window's of their own
- /// accord — a view merged into the float from elsewhere may have keywords no place answers.
+ /// accord - a view merged into the float from elsewhere may have keywords no place answers.
keywords_place: bool,
};
@@ -192,7 +192,7 @@ pub const Home = struct {
pub const GoHome = enum {
/// Added to its home's list, beside what is there: a tab, when something is.
add,
- /// Let go: its keywords bring it back wherever they choose — its home, when that is a place
+ /// Let go: its keywords bring it back wherever they choose - its home, when that is a place
/// its keywords fill. Writing it into such a place would freeze the place's list against
/// every view a plugin registers from then on.
keywords,
@@ -212,7 +212,7 @@ pub fn goHome(home: Home) GoHome {
return .elsewhere;
}
-// ── Tests ──────────────────────────────────────────────────────────────────────────────────────
+// -- Tests --------------------------------------------------------------------------------------
const test_window: Rect = .{ .w = 1600, .h = 1000 };
diff --git a/app/layout/place_commands.zig b/app/layout/place_commands.zig
index f51fc1611..39bad1da7 100644
--- a/app/layout/place_commands.zig
+++ b/app/layout/place_commands.zig
@@ -1,6 +1,6 @@
//! What a place's menu does to the place (`Picker`'s header): split it, clear it, put it back
//! to the layout's default, delete it. Each is a command, so the palette, a keymap, a tape or an
-//! agent reaches it as the menu's button does — the button runs the command.
+//! agent reaches it as the menu's button does - the button runs the command.
//!
//! A command only asks: it writes `Picker.request`, and the picker carries the request out in
//! its next draw, where the live `Layout` is. A command may run outside a frame (a control
diff --git a/app/mcp_names.zig b/app/mcp_names.zig
index 488261996..202474ec0 100644
--- a/app/mcp_names.zig
+++ b/app/mcp_names.zig
@@ -3,7 +3,7 @@
//! each id is spelled as a name every client accepts, and found again when a `tools/call` names it.
//!
//! The rule: `.` becomes `__`, any other character outside the set becomes `_`. An id that spells
-//! back to itself (`text.insert` ↔ `text__insert`, which is nearly every id) keeps that name, and
+//! back to itself (`text.insert` <-> `text__insert`, which is nearly every id) keeps that name, and
//! `unspell` finds it from the name alone. One that does not (it has `__`, or a character outside
//! the set) keeps its name too unless another id has it; then it gets `_` and six hex digits of its
//! id's hash, so which id keeps the plain name does not hang on the order plugins load in. A name
@@ -25,7 +25,7 @@ pub const Name = struct {
}
};
-/// `id` spelled as a name: `.` → `__`, anything outside `[A-Za-z0-9_-]` → `_`, and past `max_len`,
+/// `id` spelled as a name: `.` -> `__`, anything outside `[A-Za-z0-9_-]` -> `_`, and past `max_len`,
/// cut and suffixed. Empty only for an empty id.
pub fn plain(id: []const u8) Name {
return if (fits(id)) spell(id) else suffixed(id, 0);
@@ -62,7 +62,7 @@ pub fn reversible(id: []const u8) bool {
return std.mem.eql(u8, unspell(name.slice(), &back), id);
}
-/// A name read back as an id: `__` → `.`. Only an id that is `reversible` comes back as it was.
+/// A name read back as an id: `__` -> `.`. Only an id that is `reversible` comes back as it was.
pub fn unspell(name: []const u8, out: *[max_len]u8) []const u8 {
var i: usize = 0;
var n: usize = 0;
diff --git a/app/profile.zig b/app/profile.zig
index 828e9cd58..63bd03700 100644
--- a/app/profile.zig
+++ b/app/profile.zig
@@ -1,12 +1,12 @@
//! A profile: one directory holding everything an app keeps for a person between runs, so a run
-//! can be pointed somewhere else entirely — a sandbox for a test or an agent, a portable install
+//! can be pointed somewhere else entirely - a sandbox for a test or an agent, a portable install
//! on a stick, two configurations side by side.
//!
//! `--profile ` (or `--profile=`) on the command line, else `FIZZY_PROFILE` in the
//! environment, names it. Under it:
//!
//! / the config folder: settings.zon, keybinds.zon, recents.zon, layout.zon,
-//! secrets, palettes/ — everything `App.config_folder` holds
+//! secrets, palettes/ - everything `App.config_folder` holds
//! /plugins/ the plugins directory, as it is under the config folder; a plugin's own
//! `zig build` installs here too when `FIZZY_PROFILE` is set
//! (`sdk/plugin_sdk.zig`)
@@ -17,7 +17,7 @@
//! forwards its arguments to the person's everyday instance, or to a run with another profile.
//!
//! Without one, nothing changes: the platform's config folder and the system temp directory.
-//! Native only — a browser tab has neither argv nor an environment.
+//! Native only - a browser tab has neither argv nor an environment.
const std = @import("std");
/// The command-line flag that names a profile.
@@ -25,7 +25,7 @@ pub const flag = "--profile";
/// The environment variable that names one when the flag is absent.
pub const env_var = "FIZZY_PROFILE";
-/// The profile this run uses — absolute, normalized — or null for the platform's own places.
+/// The profile this run uses - absolute, normalized - or null for the platform's own places.
/// Set once at startup, by `single_instance.earlyStartup`, before anything reads a path.
pub var root: ?[]const u8 = null;
@@ -37,8 +37,8 @@ pub const Extracted = struct {
rest: []const []const u8,
};
-/// Take the profile flag out of `argv` — `--profile ` or `--profile=`, the last one
-/// winning — so the directory is never mistaken for a path to open, or forwarded to another
+/// Take the profile flag out of `argv` - `--profile ` or `--profile=`, the last one
+/// winning - so the directory is never mistaken for a path to open, or forwarded to another
/// instance as one. `argv[0]` stays first. `rest` is allocated with `gpa` and borrows `argv`'s
/// strings. A trailing `--profile` with nothing after it is dropped and names nothing.
pub fn extract(gpa: std.mem.Allocator, argv: []const []const u8) !Extracted {
@@ -73,7 +73,7 @@ pub fn runtimeDir(gpa: std.mem.Allocator, profile_root: []const u8) ![]u8 {
}
/// The single-instance lock's name for a run with `profile_root`: the app's own id with a short
-/// hash of the root appended, so each profile has a lock of its own — on Windows, where the lock
+/// hash of the root appended, so each profile has a lock of its own - on Windows, where the lock
/// is a named pipe with no directory to put it in, as much as on Unix. Caller owns.
pub fn lockId(gpa: std.mem.Allocator, app_id: []const u8, profile_root: []const u8) ![:0]u8 {
const h: u32 = @truncate(std.hash.Wyhash.hash(0, profile_root));
@@ -103,7 +103,7 @@ test "extract takes the profile out of argv, in either spelling" {
try testing.expectEqual(@as(?[]const u8, null), c.profile);
try testing.expectEqual(@as(usize, 3), c.rest.len);
- // A trailing flag, or an empty value, names nothing — and is not left behind as a path.
+ // A trailing flag, or an empty value, names nothing - and is not left behind as a path.
const d = try extract(gpa, &.{ "fizzy", "/work", "--profile" });
defer gpa.free(d.rest);
try testing.expectEqual(@as(?[]const u8, null), d.profile);
diff --git a/app/remote/RemoteViewHost.zig b/app/remote/RemoteViewHost.zig
index cb58bcee6..8f3c3f4c2 100644
--- a/app/remote/RemoteViewHost.zig
+++ b/app/remote/RemoteViewHost.zig
@@ -1,5 +1,5 @@
//! A `ViewHost` whose surface is on the other side of the control socket: a view a remote client
-//! (an adapter in Blender, Unreal, …) asked for. Like a pop-out it is a window the app lays out
+//! (an adapter in Blender, Unreal, ...) asked for. Like a pop-out it is a window the app lays out
//! and draws into the one frame, but the OS never shows it; the client does, from the frames it
//! asks for. Input the client sends waits here as dvui events until the frame injects it.
//!
diff --git a/app/restart.zig b/app/restart.zig
index 15d8c1edb..cdeeb1ce2 100644
--- a/app/restart.zig
+++ b/app/restart.zig
@@ -1,6 +1,6 @@
//! Restarting the application: quitting the way it always quits, then starting again.
//!
-//! One path for every reason to restart — a setting the app takes only at launch, an update
+//! One path for every reason to restart - a setting the app takes only at launch, an update
//! downloaded and waiting to be swapped in. `request` posts the app's ordinary quit, so unsaved
//! documents are asked about first and a quit the user calls off calls the restart off with it.
//! Once the app has torn down, `relaunch` starts it again: through Velopack when an update is
@@ -90,11 +90,11 @@ pub fn requested() bool {
}
/// Once a frame, before the app reads the frame's quit events. `quitting`: the app is still in
-/// the middle of a quit — a question about unsaved documents open, saves running.
+/// the middle of a quit - a question about unsaved documents open, saves running.
///
/// The restart's quit goes in ahead of the frame's own, so it is answered like any other: with
/// nothing unsaved the frame ends closing, and `relaunch` runs at teardown. Still here two frames
-/// on and no longer quitting, the user kept the app open — and called the restart off with it.
+/// on and no longer quitting, the user kept the app open - and called the restart off with it.
pub fn tick(quitting: bool) void {
if (comptime !supported) return;
// An update the user asked for has finished downloading: installing it is a restart.
@@ -112,7 +112,7 @@ pub fn tick(quitting: bool) void {
quit_frames +|= 1;
}
-/// At the very end of teardown — after the app's windows, its documents and its single-instance
+/// At the very end of teardown - after the app's windows, its documents and its single-instance
/// listener are gone, so the new instance owns the lock rather than handing its launch to this
/// one. No-op unless a restart was asked for.
pub fn relaunch(io: std.Io, gpa: std.mem.Allocator) void {
@@ -136,7 +136,7 @@ pub fn relaunch(io: std.Io, gpa: std.mem.Allocator) void {
}
/// This executable again, as a process of its own that nothing waits on, on the same profile. On
-/// macOS through `core.darwin_spawn` — std's spawn crashes after any `unsetenv`, and it passes a
+/// macOS through `core.darwin_spawn` - std's spawn crashes after any `unsetenv`, and it passes a
/// fixed environment, so a profile named by `FIZZY_PROFILE` goes on as the flag. The files this
/// run was started with are not passed again: the session brings back what is still open.
fn relaunchSelf(io: std.Io, gpa: std.mem.Allocator) void {
@@ -157,7 +157,7 @@ fn start(io: std.Io, gpa: std.mem.Allocator, handover_to: ?[]const u8, log: ?std
if (handover_to) |dir| argv.appendSliceAssumeCapacity(&.{ handover_flag, dir });
// Out of reach of whatever started this instance: a terminal (a build task's `zig build run`)
// closes once this one quits, and its hangup would take the new one with it. macOS: a session
- // of its own. Linux: a process group of its own, which std offers where it offers no setsid —
+ // of its own. Linux: a process group of its own, which std offers where it offers no setsid -
// out of the terminal's foreground group, which is what the hangup is sent to. Windows: a
// console of its own (no-op for a Release build, which has none), so closing the launcher's
// console does not close it.
@@ -327,8 +327,8 @@ var give_up_ns: i96 = 0;
const give_up_after_ns = 30 * std.time.ns_per_s;
/// Once a frame while this instance was started by a handover: true when the caller should try
-/// the single-instance lock — the old instance has let go of it, or has taken too long and it is
-/// tried anyway — and say how that went (`tookLock`).
+/// the single-instance lock - the old instance has let go of it, or has taken too long and it is
+/// tried anyway - and say how that went (`tookLock`).
pub fn pollReleased(io: std.Io) bool {
if (comptime !supported) return false;
const dir = handed_from orelse return false;
diff --git a/app/root.zig b/app/root.zig
index 0bc201a3d..dad0868eb 100644
--- a/app/root.zig
+++ b/app/root.zig
@@ -1,8 +1,8 @@
-//! `app` — framework an application switches on, as opposed to `core` (what an app and a plugin
+//! `app` - framework an application switches on, as opposed to `core` (what an app and a plugin
//! both draw with) and `sdk` (the contract a plugin is written against).
//!
//! The distinction is the dylib boundary, not taste: everything here is compiled into an
-//! application and never into a plugin, so it may depend on things a plugin must never link —
+//! application and never into a plugin, so it may depend on things a plugin must never link -
//! the network, the filesystem, an installer, `dlopen` itself.
//!
//! What lives here is opt-in per app. Fizzy switches all of it on; a smaller app takes what it
@@ -10,16 +10,16 @@
//!
//! `store` the plugin store: registry catalog, install/update/uninstall, load-failure
//! reporting, and the pane that draws all of it. Point it at your own registry with
-//! `AppInfo.registry_url` — `plugins.yourapp.com` is a config value, not a fork.
+//! `AppInfo.registry_url` - `plugins.yourapp.com` is a config value, not a fork.
//!
//! **An app tells the store about itself once**, through `store.Manager` (see its file), and the
-//! store then talks to nothing else — no `Editor`, no globals belonging to fizzy. That seam is
+//! store then talks to nothing else - no `Editor`, no globals belonging to fizzy. That seam is
//! what makes this framework rather than fizzy's own source.
/// What makes an application *this* application rather than fizzy: its name, bundle id, config
/// directory, version, update feed and plugin registry. Every value arrives from the app's own
/// `build_opts`, so `AppInfo.current` is the identity of whichever app is being built.
pub const AppInfo = @import("AppInfo.zig");
-/// The host runtime an application embeds — see the file.
+/// The host runtime an application embeds - see the file.
pub const App = @import("App.zig");
/// User configuration on disk: `settings.zon` (the app's own record plus every plugin's
@@ -51,7 +51,7 @@ pub const history_transport = @import("history_transport.zig");
pub const dome = @import("dome.zig");
pub const dome_rules = @import("dome_rules.zig");
-/// Key chords → command ids: the table, its ZON form, chord matching, and the dvui adapter.
+/// Key chords -> command ids: the table, its ZON form, chord matching, and the dvui adapter.
pub const keymap = struct {
pub const Keymap = @import("keymap/Keymap.zig");
pub const key = @import("tape").key;
@@ -62,20 +62,20 @@ pub const keymap = struct {
/// Regions and the splits between them: the whole of how an application divides its window.
///
-/// A minimal app is exactly this — regions laid out by dvui's boxes, a split where the user
+/// A minimal app is exactly this - regions laid out by dvui's boxes, a split where the user
/// should be able to drag one edge, and plugins drawing into whichever region accepts their
/// keywords. Nothing here knows what an explorer, a panel or a document is; fizzy's own shape
/// (`src/editor/layout.zig`) is ordinary code on top, and an app writes its own.
pub const layout = struct {
pub const Layout = @import("layout/Layout.zig");
pub const Region = @import("layout/Region.zig");
- /// A place's views as something to pick from — a tab strip, an icon rail, or anything drawn
+ /// A place's views as something to pick from - a tab strip, an icon rail, or anything drawn
/// over the same reorder, select and drag-off behaviour.
pub const Chooser = @import("layout/Chooser.zig");
pub const Rail = @import("layout/Rail.zig");
pub const SidePane = @import("layout/SidePane.zig");
pub const Seed = @import("layout/Seed.zig").Tree;
- /// One place in a static shape written as data, for `-Dapp-layout=.zon` — a tree of
+ /// One place in a static shape written as data, for `-Dapp-layout=.zon` - a tree of
/// them is the whole shape. See its own doc comment for what data can and cannot say.
pub const Shape = @import("layout/Shape.zig");
pub const Tree = @import("layout/Tree.zig");
@@ -92,12 +92,12 @@ pub const view = struct {
/// Watching the filesystem for changes the application should react to.
///
/// Opt-in: an app that does not want a folder watcher simply never starts one, and the cost is
-/// zero. Each watcher's job ends at "this settled, off the watcher thread" — what is worth
+/// zero. Each watcher's job ends at "this settled, off the watcher thread" - what is worth
/// reacting to, and what reacting means, is the app's, which is what `Sink` on each of them is.
pub const watch = struct {
/// On-disk changes under the open project folder, coalesced and filtered by the app.
pub const FolderWatcher = @import("watch/FolderWatcher.zig");
- /// Changes under the app's own config folder — its settings file, its plugin directory.
+ /// Changes under the app's own config folder - its settings file, its plugin directory.
pub const SettingsWatcher = @import("watch/SettingsWatcher.zig");
/// The app's own executable: a rebuild of it restarts the app into the new build.
pub const ExecutableWatcher = @import("watch/ExecutableWatcher.zig");
@@ -156,12 +156,12 @@ pub const control = struct {
}
};
-/// Quitting the way the app always quits — unsaved documents asked about first — then starting
+/// Quitting the way the app always quits - unsaved documents asked about first - then starting
/// again: for a setting the app takes only at launch, and for installing a downloaded `update`.
/// The app asks with `request`, runs `tick` each frame and `relaunch` at the end of teardown.
pub const restart = @import("restart.zig");
-/// Demos that drive the real app — a tape of timed input played into dvui, which a viewer can
+/// Demos that drive the real app - a tape of timed input played into dvui, which a viewer can
/// pause, take over, rewind and resume. `Tape` is the format, `Sequencer` the deterministic engine
/// that replays it, `Script` the high-level way to write one, `Player` + `overlay` the dvui half
/// (input injection, the pointer and captions a viewer sees), `Stage` what the app fills in.
@@ -174,7 +174,7 @@ pub const automation = struct {
pub const Player = @import("replay").Player;
/// Tape input as dvui events: the half of a sequencer's sink every player of tapes shares.
pub const Input = @import("replay").Input;
- /// Plays a live tape — input on the app as it is, a step at a time — beside the `Player`.
+ /// Plays a live tape - input on the app as it is, a step at a time - beside the `Player`.
pub const LiveDriver = @import("replay").LiveDriver;
/// What is on screen, as text: roles, names, tags, rects (`replay.Snapshot`).
pub const Snapshot = @import("replay").Snapshot;
@@ -191,7 +191,7 @@ pub const automation = struct {
/// One application per machine, and a second launch handing its argv to the first.
///
/// The lock, the socket and the argv plumbing are the same for every app; what "open this path"
-/// means is not, which is `singleton.Sink`. The app also sets its own `app_id` — the lock is per
+/// means is not, which is `singleton.Sink`. The app also sets its own `app_id` - the lock is per
/// application, and two fizzy-based apps must not fight over one.
pub const single_instance = @import("single_instance/singleton.zig");
/// `--profile ` / `FIZZY_PROFILE`: one root for the config folder, the plugins, the lock and
@@ -202,7 +202,7 @@ pub const quiet = @import("quiet.zig");
pub const store = struct {
pub const Store = @import("store/PluginStore.zig");
- /// A plugin's store page as a document — the owner of `store://…` pages.
+ /// A plugin's store page as a document - the owner of `store://...` pages.
pub const Page = @import("store/PluginPage.zig");
pub const Manager = @import("store/PluginManager.zig");
/// Runtime plugin loading: `dlopen` natively; on the web a wasm side module the page links
diff --git a/app/settings/PluginSettingsPane.zig b/app/settings/PluginSettingsPane.zig
index c0ad25169..d845dbc53 100644
--- a/app/settings/PluginSettingsPane.zig
+++ b/app/settings/PluginSettingsPane.zig
@@ -4,7 +4,7 @@
//! **this file draws all controls** so every plugin's settings share the same appearance.
//! Plugins do not supply a `draw` callback.
//!
-//! Which controls appear, and where, is `SettingsTree`'s decision — it owns the Settings pane,
+//! Which controls appear, and where, is `SettingsTree`'s decision - it owns the Settings pane,
//! draws each field's label (with search highlighting), and calls `drawField` for the control
//! body only. Loaded-with-settings-only still holds: a plugin gets rows only if it registered a
//! schema. Enabling/disabling a plugin is the Plugins store tab's job (`PluginStore.zig`), never
@@ -15,14 +15,14 @@ const core = @import("core");
const sdk = @import("fizzy_sdk");
const settings = sdk.settings;
-/// Draw the control for `field` — no label. `SettingsTree` draws the setting name above this.
+/// Draw the control for `field` - no label. `SettingsTree` draws the setting name above this.
pub fn drawField(schema: *const settings.SettingsSchema, field: settings.Setting, field_index: usize, id_extra: usize) !void {
const access = schema.access;
const value = schema.value;
switch (field.kind) {
// Drawn by `SettingRow.beginInlineControl` on the description's line, so it is
- // deliberately label-less and non-expanding — the description *is* its label.
+ // deliberately label-less and non-expanding - the description *is* its label.
.bool => {
var b = access.getBool(value, field_index);
if (dvui.checkbox(@src(), &b, null, .{
@@ -114,8 +114,8 @@ pub fn drawField(schema: *const settings.SettingsSchema, field: settings.Setting
}
/// Fallback control for a setting whose type the pane has no dedicated widget for (a struct, an
-/// array, an optional…): the value as zon text, edited in place. Same commit-on-Enter,
-/// re-seed-when-unfocused contract as `drawString` — plus a rejected parse simply doesn't
+/// array, an optional...): the value as zon text, edited in place. Same commit-on-Enter,
+/// re-seed-when-unfocused contract as `drawString` - plus a rejected parse simply doesn't
/// commit, so the next unfocused frame reverts the entry to the live value.
fn drawZon(schema: *const settings.SettingsSchema, field_index: usize, id_extra: usize) !void {
const access = schema.access;
@@ -129,7 +129,7 @@ fn drawZon(schema: *const settings.SettingsSchema, field_index: usize, id_extra:
entry.processEvents();
entry.draw();
// A paste from the field's menu is an edit made while the field was not focused, so it
- // commits here — there will be no Enter for it, and the resync above would undo it.
+ // commits here - there will be no Enter for it, and the resync above would undo it.
const pasted = core.widgets.textEntryMenu(&entry);
if ((entry.enter_pressed or pasted) and !std.mem.eql(u8, entry.getText(), current)) {
if (access.setZonText(value, field_index, entry.getText())) {
@@ -139,7 +139,7 @@ fn drawZon(schema: *const settings.SettingsSchema, field_index: usize, id_extra:
core.widgets.textEntryDeinit(&entry);
}
-/// Text entry for a `[]const u8` setting. Committed on Enter only — the entry re-seeds itself
+/// Text entry for a `[]const u8` setting. Committed on Enter only - the entry re-seeds itself
/// from the live value whenever it isn't focused, so an abandoned edit reverts on blur and an
/// external change (a hand-edit reconciled by `SettingsWatcher`) shows up here without the pane
/// needing its own change notification. `Access.setString` copies the bytes into schema-owned
diff --git a/app/settings/SettingRow.zig b/app/settings/SettingRow.zig
index 1c2b23377..62131e75f 100644
--- a/app/settings/SettingRow.zig
+++ b/app/settings/SettingRow.zig
@@ -2,16 +2,16 @@
//!
//! A setting reads as a small self-contained group, VSCode-style:
//!
-//! Insert spaces on tab ← name (`header`), sentence case, bold
-//! insert_spaces_on_tab ← the key it has in settings.zon, dim and smaller
-//! Insert spaces instead of a… ← description (`description`), wrapped
-//! [▁▁▁▁▁ control ▁▁▁▁▁] ← the control, full width
+//! Insert spaces on tab <- name (`header`), sentence case, bold
+//! insert_spaces_on_tab <- the key it has in settings.zon, dim and smaller
+//! Insert spaces instead of a... <- description (`description`), wrapped
+//! [_____ control _____] <- the control, full width
//!
//! Booleans are the one exception: their checkbox sits *before* the description on one line
//! (`beginInlineControl`), because a full-width control for two states is all frame and no
//! information.
//!
-//! Name and key are both search-highlighted — the settings search matches either, so a row that
+//! Name and key are both search-highlighted - the settings search matches either, so a row that
//! survived on a key match has to be able to show why.
//!
//! `SettingsTree` owns *which* rows appear and in what order; this file owns what one row looks
@@ -41,7 +41,7 @@ pub fn header(name: []const u8, key: []const u8, query: *const fuzzy.Query) void
tl.deinit();
}
- // The key is what the user types in settings.zon, so it gets the mono face — and enough
+ // The key is what the user types in settings.zon, so it gets the mono face - and enough
// dimming that the pane reads as a list of names, not a list of identifiers.
var tl = dvui.textLayout(@src(), .{ .break_lines = false }, .{
.background = false,
@@ -64,7 +64,7 @@ pub fn description(text: []const u8) void {
});
}
-/// The same description on a checkbox's line — vertically centred against the checkbox, and
+/// The same description on a checkbox's line - vertically centred against the checkbox, and
/// inset far enough to clear it.
pub fn descriptionInline(text: []const u8) void {
drawDescription(text, .{
@@ -85,7 +85,7 @@ fn drawDescription(text: []const u8, over: dvui.Options) void {
tl.deinit();
}
-/// Row for a control that shares its line with the description — the checkbox goes in first, the
+/// Row for a control that shares its line with the description - the checkbox goes in first, the
/// description wraps in whatever width is left. Caller deinits.
pub fn beginInlineControl() *dvui.BoxWidget {
return dvui.box(@src(), .{ .dir = .horizontal }, .{
diff --git a/app/settings/Settings.zig b/app/settings/Settings.zig
index 83ae05ac7..3aaa03ada 100644
--- a/app/settings/Settings.zig
+++ b/app/settings/Settings.zig
@@ -53,7 +53,7 @@ font_title_size: f32 = 9,
font_heading_size: f32 = 8,
font_mono_size: f32 = 8,
-/// The window's glass, per theme (`core.glass_look`): its opacity — above 0 frosted glass at one
+/// The window's glass, per theme (`core.glass_look`): its opacity - above 0 frosted glass at one
/// frost, the window's colour over it from a little to the window's fill, opaque, at 1; 0 a clear
/// window, no glass at all, which the settings pane does not offer. On macOS and Windows the window
/// itself is that glass, and every glass the app draws (dialogs, menus, drops) is too; elsewhere,
@@ -77,13 +77,8 @@ modal_dim: f32 = 0.0,
/// theme's own corner. All the way round by default, on every platform.
corner_roundness: f32 = 1.0,
-/// How the interface moves, 0 to 1: 0 is off (nothing animates), up to 0.5 (minimal) plain even
-/// motion, toward 1 (playful) an overshoot past the target and back, arriving on time throughout.
-/// Published each frame through `core.motion`, which every animation reads. Playful by default.
-motion: f32 = 1,
-
/// How fast the interface moves, 0 (slow) to 1 (fast): half as fast to twice as fast as written,
-/// 0.5 as written. Never stops motion — that is `motion` at 0.
+/// 0.5 as written. Never stops motion - that is `motion` at 0.
motion_speed: f32 = core.motion.default_speed,
/// What the app's windows are made of (`core.material`): opaque (`none`), clear (`transparent`),
@@ -97,8 +92,8 @@ window_material: core.material.Kind = .none,
/// app's renderer can draw it, whatever the windows are made of, else the richest glass below it.
in_app_material: ?core.material.Kind = null,
-/// Whether glass is drawn by the glass program (`core.LiquidField`) where the backend has one —
-/// shapes that run together, in one pass a pixel — rather than by its meshes. On; a switch to
+/// Whether glass is drawn by the glass program (`core.LiquidField`) where the backend has one -
+/// shapes that run together, in one pass a pixel - rather than by its meshes. On; a switch to
/// compare the two.
glass_shader: bool = true,
@@ -107,18 +102,18 @@ glass_shader: bool = true,
/// app says (`Editor.restartPending`). On by default on macOS; `FIZZY_POPOUT` overrides it.
float_windows: bool = builtin.target.os.tag == .macos,
-/// Whether a view drag's glass — the carried view and the drop zones — is the OS's own (Liquid Glass,
+/// Whether a view drag's glass - the carried view and the drop zones - is the OS's own (Liquid Glass,
/// macOS 26) rather than the app's, where floats are windows. Applied at once; `FIZZY_NATIVE_GLASS`
/// overrides it.
native_glass: bool = true,
-/// Whether menus open in OS windows of their own — the OS's material, shadow and corners, over every
-/// window, past the window's edge — rather than drawn inside the window they open from, where floats
+/// Whether menus open in OS windows of their own - the OS's material, shadow and corners, over every
+/// window, past the window's edge - rather than drawn inside the window they open from, where floats
/// are windows. Applied at once; `FIZZY_NATIVE_MENUS` overrides it.
native_menus: bool = true,
-/// Whether dialogs open in OS windows of their own — the OS's material, shadow and corners, riding
-/// on the main window — rather than drawn inside it, where floats are windows. Applied at once;
+/// Whether dialogs open in OS windows of their own - the OS's material, shadow and corners, riding
+/// on the main window - rather than drawn inside it, where floats are windows. Applied at once;
/// `FIZZY_NATIVE_DIALOGS` overrides it.
native_dialogs: bool = true,
@@ -155,14 +150,14 @@ input_scheme: InputScheme = .auto,
/// that visit and is never stored here.
layout_mode: LayoutMode = .auto,
-/// How updates found by `PluginStore`'s post-launch pass are applied — prompted, or silent.
+/// How updates found by `PluginStore`'s post-launch pass are applied - prompted, or silent.
/// Only covers plugins that haven't individually opted out via `.plugins..auto_update`.
plugin_update_mode: PluginUpdateMode = .prompt,
/// The window's glass as it comes, per platform. Where the window is the glass (macOS, Windows), as
/// fizzy's windows have been: mostly the window's colour in a dark theme, little in a light one,
/// frosted. Where the window is opaque (the web, Linux), the glass the app draws over it: little
-/// colour, well frosted — the dialogs' glass as it was.
+/// colour, well frosted - the dialogs' glass as it was.
pub const GlassDefaults = struct { opacity_dark: f32, opacity_light: f32 };
pub const default_glass: GlassDefaults = if (builtin.target.cpu.arch == .wasm32 or (builtin.target.os.tag != .macos and builtin.target.os.tag != .windows))
.{ .opacity_dark = 0.2, .opacity_light = 0.2 }
@@ -194,12 +189,12 @@ pub fn setThemeName(settings: *Settings, allocator: std.mem.Allocator, name: []c
settings.theme = copy;
}
-/// Loads settings (`theme` is always heap-owned after successful return — see `setThemeName` / `deinit`).
+/// Loads settings (`theme` is always heap-owned after successful return - see `setThemeName` / `deinit`).
/// One-shot migrates any pre-R10 `/.settings.zon` files into this file's
/// `.plugins.` field first (see `SettingsMigration.mergeLegacyPerPluginFiles`), then any
/// pre-R12 flat plugin blocks + top-level `disabled_plugins` into nested
/// `.{ .enabled, .settings }` (see `SettingsMigration.migrateToPerPluginEnabled`). Unknown
-/// fields (including `.plugins`, which this struct doesn't itself model — see
+/// fields (including `.plugins`, which this struct doesn't itself model - see
/// `serialize`'s doc comment) are ignored, both for forward-compat with newer on-disk shapes and
/// because `.plugins` is read separately, per-plugin, via `Host.loadPluginSettings`.
pub fn load(allocator: std.mem.Allocator, path: []const u8, plugins_dir: ?[]const u8) !Settings {
@@ -227,9 +222,9 @@ pub fn load(allocator: std.mem.Allocator, path: []const u8, plugins_dir: ?[]cons
}
/// Parses `data` (already-read `settings.zon` bytes) into a `Settings` value, ignoring unknown
-/// fields (forward-compat + `.plugins` is handled separately — see `load`'s doc comment above).
+/// fields (forward-compat + `.plugins` is handled separately - see `load`'s doc comment above).
/// Unlike `load`, this returns the raw parse error on failure instead of falling back to
-/// defaults — defaulting is only correct when there's no existing live state to fall back *to*
+/// defaults - defaulting is only correct when there's no existing live state to fall back *to*
/// (startup); `Editor.reconcileExternalSettingsChange` (external hand-edit reconciliation, R11)
/// has existing state and must not let a transient torn-write read reset every fizzy field and
/// every plugin's settings. The result's strings live in `arena` (`allocator` is the parse's
@@ -248,7 +243,7 @@ pub fn parseOnly(allocator: std.mem.Allocator, arena: std.mem.Allocator, data: [
return parsed;
}
-/// The value every field is diffed against by `serialize` — this struct's own declared defaults.
+/// The value every field is diffed against by `serialize` - this struct's own declared defaults.
const default_value: Settings = .{};
fn fieldEqual(comptime FT: type, a: FT, b: FT) bool {
@@ -261,17 +256,17 @@ fn fieldEqual(comptime FT: type, a: FT, b: FT) bool {
}
/// Serialize fizzy's own fields as `.{ ...fizzy fields... }`, emitting **only the fields that
-/// differ from the declared defaults above** — the same non-default-only rule plugin settings
+/// differ from the declared defaults above** - the same non-default-only rule plugin settings
/// follow (`sdk.settings.Schema(T).diffSerialize`, R12 in plans/PLUGIN_MANIFEST_PLAN.md), so both
/// halves of `settings.zon` record just what the user actually changed. An untouched fizzy
/// serializes to `.{}`; a field hand-written back at its default drops out on the next write.
/// Reading back is unaffected: `load`/`parseOnly` fill every missing field from these same
/// defaults (they are the declared defaults themselves: `load` copies `theme` before keeping it).
///
-/// This is *not* the whole `settings.zon` file — plugin settings are a separate
+/// This is *not* the whole `settings.zon` file - plugin settings are a separate
/// `.plugins = .{ . = .{...}, ... }` field spliced on afterward by
/// `Editor.composeSettingsText`/`SettingsPluginsZon.composeMergedText` (R10).
-/// `Editor.writeMergedSettings` is the only writer of `settings.zon` — there is no standalone
+/// `Editor.writeMergedSettings` is the only writer of `settings.zon` - there is no standalone
/// fizzy-only save path anymore, since writing this half alone would silently drop the
/// `.plugins` half.
pub fn serialize(settings: *const Settings, allocator: std.mem.Allocator) ![]u8 {
diff --git a/app/settings/SettingsMigration.zig b/app/settings/SettingsMigration.zig
index b2c37a286..a446a4ee6 100644
--- a/app/settings/SettingsMigration.zig
+++ b/app/settings/SettingsMigration.zig
@@ -1,10 +1,10 @@
-//! One-shot migrations for `settings.zon` layout changes — see plans/PLUGIN_MANIFEST_PLAN.md.
+//! One-shot migrations for `settings.zon` layout changes - see plans/PLUGIN_MANIFEST_PLAN.md.
//!
-//! 1. `mergeLegacyPerPluginFiles` — pre-R10 one-file-per-plugin (`/.settings.zon`)
+//! 1. `mergeLegacyPerPluginFiles` - pre-R10 one-file-per-plugin (`/.settings.zon`)
//! into the merged `.plugins.` field (R10).
-//! 2. `migrateToPerPluginEnabled` — pre-R12 flat `.plugins. = .{ }` + top-level
-//! `disabled_plugins` into nested `.{ .enabled = …, .settings = .{ … } }` (R12).
-//! 3. `windowGlass` — the dialogs' own glass settings (`dialog_opacity`, `dialog_blur`, …) into the
+//! 2. `migrateToPerPluginEnabled` - pre-R12 flat `.plugins. = .{ }` + top-level
+//! `disabled_plugins` into nested `.{ .enabled = ..., .settings = .{ ... } }` (R12).
+//! 3. `windowGlass` - the dialogs' own glass settings (`dialog_opacity`, `dialog_blur`, ...) into the
//! window's opacity, which every glass now reads.
const builtin = @import("builtin");
const std = @import("std");
@@ -65,9 +65,9 @@ const LegacyDisk = struct {
};
/// Rewrites pre-R12 flat `.plugins.` blocks + top-level `disabled_plugins` into the nested
-/// `.{ .enabled = …, .settings = .{ … } }` shape (and drops `disabled_plugins` from the file).
+/// `.{ .enabled = ..., .settings = .{ ... } }` shape (and drops `disabled_plugins` from the file).
/// Also writes `.enabled = true` for any on-disk plugin directory that was previously enabled by
-/// default (absent from `disabled_plugins`) but had no settings block — under R12, absence means
+/// default (absent from `disabled_plugins`) but had no settings block - under R12, absence means
/// disabled. Idempotent: already-nested blocks are left alone; a file with no legacy list and no
/// flat blocks is a no-op. Best-effort throughout.
pub fn migrateToPerPluginEnabled(allocator: std.mem.Allocator, settings_zon_path: []const u8, plugins_dir: ?[]const u8) void {
@@ -137,7 +137,7 @@ pub fn migrateToPerPluginEnabled(allocator: std.mem.Allocator, settings_zon_path
if (id.len == 0 or id[0] == '.') continue;
if (containsId(legacy.disabled_plugins, id)) continue;
if (hasBlock(blocks, id)) continue;
- // Already covered by an overlay entry? (shouldn't be — no prior block)
+ // Already covered by an overlay entry? (shouldn't be - no prior block)
if (overlayHas(overlay.items, id)) continue;
needs_write = true;
const new_block = SettingsPluginsZon.composePluginIdBlock(allocator, .{ .enabled = true }, null) catch continue;
@@ -155,8 +155,8 @@ pub fn migrateToPerPluginEnabled(allocator: std.mem.Allocator, settings_zon_path
if (!needs_write) return;
- // Regenerate fizzy's own fields from a Settings parse (unknown fields — including the legacy
- // `disabled_plugins` list — are ignored), so the rewritten file drops that list for free.
+ // Regenerate fizzy's own fields from a Settings parse (unknown fields - including the legacy
+ // `disabled_plugins` list - are ignored), so the rewritten file drops that list for free.
var parsed_arena: std.heap.ArenaAllocator = .init(allocator);
defer parsed_arena.deinit();
const parsed = Settings.parseOnly(allocator, parsed_arena.allocator(), data) catch |err| {
@@ -188,13 +188,13 @@ pub fn migrateToPerPluginEnabled(allocator: std.mem.Allocator, settings_zon_path
/// A block counts as modern the moment it carries **any** fizzy-reserved field, or a nested
/// `.settings`. Getting this wrong is destructive in a way that is easy to miss: a block judged
/// "flat" has its entire contents wrapped into `.settings` and stamped `.enabled = true`, which
-/// would silently reinterpret fizzy-reserved data as plugin-author settings — an `.extensions`
+/// would silently reinterpret fizzy-reserved data as plugin-author settings - an `.extensions`
/// assignment would stop routing files and reappear as an unknown author field.
///
/// The reserved names come from `SettingsPluginsZon.Reserved` itself rather than a hand-kept
/// list, so adding a field there can never leave this check behind. (It already had: only
-/// `enabled` was listed, so an `.auto_update = false`-only block — which the app itself can
-/// write for a disabled plugin — was being swallowed too.)
+/// `enabled` was listed, so an `.auto_update = false`-only block - which the app itself can
+/// write for a disabled plugin - was being swallowed too.)
fn isAlreadyNested(gpa: std.mem.Allocator, text: []const u8) bool {
const z = gpa.dupeSentinel(u8, text, 0) catch return false;
defer gpa.free(z);
@@ -248,7 +248,7 @@ const LegacyGlass = struct {
};
/// Called from `Settings.parseOnly`, on every platform the web included: the window's glass read
-/// from settings written before it took the dialogs' — those with any of the dialogs' own glass
+/// from settings written before it took the dialogs' - those with any of the dialogs' own glass
/// settings in them, which the next write drops (unknown to `Settings`), so this runs until then
/// and never after. Where the window is opaque the opacity is the dialogs', the window's own having
/// been only the window's. Its frost is no setting any more (`glass_look.frost_level`), so the
@@ -267,3 +267,36 @@ pub fn windowGlass(allocator: std.mem.Allocator, data: [:0]const u8, settings: *
settings.window_opacity_light = std.math.clamp(o, 0, 1);
};
}
+
+const LegacyMotion = struct {
+ motion: ?f32 = null,
+};
+
+/// Moves the app's saved level into the shared preferences without replacing a person's choice.
+/// Persists the shared file before dropping the field, so a failed write can be retried.
+pub fn personMotion(gpa: std.mem.Allocator, io: std.Io, settings_path: []const u8, person_dir: []const u8) !void {
+ const data = core.fs.readZ(gpa, io, settings_path) catch return;
+ defer gpa.free(data);
+ var arena = std.heap.ArenaAllocator.init(gpa);
+ defer arena.deinit();
+ var diag: std.zon.parse.Diagnostics = undefined;
+ const legacy = std.zon.parse.fromSlice(LegacyMotion, .{
+ .gpa = gpa,
+ .arena = arena.allocator(),
+ .source = data,
+ .diagnostics = &diag,
+ .ignore_unknown_fields = true,
+ }) catch return;
+ const level = legacy.motion orelse return;
+ var person = core.person.load(io, person_dir);
+ if (person.motion == null) {
+ person.motion = level;
+ try core.person.save(io, person_dir, person);
+ }
+ const parsed = try Settings.parseOnly(gpa, arena.allocator(), data);
+ const own = try Settings.serialize(&parsed, gpa);
+ defer gpa.free(own);
+ const composed = try SettingsPluginsZon.composeMergedText(gpa, own, data, &.{});
+ defer gpa.free(composed);
+ try core.fs.write(io, settings_path, composed);
+}
diff --git a/app/settings/SettingsPluginsZon.zig b/app/settings/SettingsPluginsZon.zig
index d88b970b4..136738e39 100644
--- a/app/settings/SettingsPluginsZon.zig
+++ b/app/settings/SettingsPluginsZon.zig
@@ -1,5 +1,5 @@
//! Reads/writes the `.plugins = .{ . = .{...}, ... }` sub-tree of the merged
-//! `settings.zon` — see plans/PLUGIN_MANIFEST_PLAN.md R10/R12.
+//! `settings.zon` - see plans/PLUGIN_MANIFEST_PLAN.md R10/R12.
//!
//! Every plugin's block lives as a real nested ZON struct literal keyed by plugin id, not an
//! escaped-string blob. On-disk shape after R12:
@@ -13,12 +13,12 @@
//! ```
//!
//! `extractField` finds a named field's value **by exact source byte span** (via
-//! `std.zig.Ast`/`std.zig.ZonGen`'s `Zoir` — the same machinery `std.zon.parse` uses
+//! `std.zig.Ast`/`std.zig.ZonGen`'s `Zoir` - the same machinery `std.zon.parse` uses
//! internally) rather than re-serializing it, so hand-formatting/comments inside a block
//! survive untouched. Callers compose nesting themselves:
-//! `extractField(settings, "plugins")` → `extractField(that, id)` → `extractField(that, "settings")`.
+//! `extractField(settings, "plugins")` -> `extractField(that, id)` -> `extractField(that, "settings")`.
//!
-//! Pure text-in/text-out — no `dvui`/filesystem dependency, so this is unit-testable directly.
+//! Pure text-in/text-out - no `dvui`/filesystem dependency, so this is unit-testable directly.
const std = @import("std");
const Allocator = std.mem.Allocator;
@@ -26,7 +26,7 @@ pub const Entry = struct {
id: []const u8,
/// Verbatim ZON source text for this id's whole block (e.g.
/// `.{ .enabled = true, .settings = .{ .tab_size = 4 } }`), or `null` meaning "remove this
- /// id's block entirely" (R12 — all-default settings + disabled, or an explicit removal).
+ /// id's block entirely" (R12 - all-default settings + disabled, or an explicit removal).
text: ?[]const u8,
};
@@ -38,7 +38,7 @@ pub fn freeEntries(gpa: Allocator, entries: []const Entry) void {
gpa.free(entries);
}
-/// Finds `container`'s field named `name` (an exact-string compare — `Zoir` already resolves
+/// Finds `container`'s field named `name` (an exact-string compare - `Zoir` already resolves
/// both plain and `@"..."`-quoted field names to plain strings, so there's no bare-identifier
/// restriction on `name`). Returns the field's value node, or null if `container` isn't a struct
/// literal or has no such field.
@@ -66,7 +66,7 @@ fn nodeSourceText(gpa: Allocator, ast: std.zig.Ast, zoir: std.zig.Zoir, node: st
}
/// Parses `source` as ZON and returns the `Ast`+`Zoir` pair, or null on any parse/generate
-/// failure (malformed file — callers fall back to "nothing found", same as a missing file).
+/// failure (malformed file - callers fall back to "nothing found", same as a missing file).
const Parsed = struct { ast: std.zig.Ast, zoir: std.zig.Zoir };
fn parseZon(gpa: Allocator, source: [:0]const u8) ?Parsed {
@@ -107,7 +107,7 @@ pub fn extractPluginBlob(gpa: Allocator, source: [:0]const u8, id: []const u8) ?
return extractField(gpa, plugins_z, id);
}
-/// Enumerates **every** field in `source`'s `.plugins` struct literal — not filtered to any known
+/// Enumerates **every** field in `source`'s `.plugins` struct literal - not filtered to any known
/// set of ids. Used by the **write path**: a plugin's block must survive every save regardless of
/// whether that plugin is currently loaded, disabled, or even uninstalled (mirrors the existing
/// "uninstall only deletes the dylib, settings persist" behavior). Empty slice (never null) if
@@ -156,11 +156,11 @@ fn leadingSpaces(line: []const u8) usize {
/// Writes `text` to `writer`, prefixing every line *after the first* with `indent` spaces.
/// Used when splicing a (possibly multi-line) ZON blob after `= ` on an already-indented
-/// parent line — the first line stays on that parent line; continuations deepen correctly.
+/// parent line - the first line stays on that parent line; continuations deepen correctly.
///
/// Continuation lines are first **dedented** by their common leading whitespace so a block
/// that was previously written under `.plugins` (and later re-extracted verbatim by
-/// `listPluginBlocks`) does not accumulate another `indent` on every autosave — that was
+/// `listPluginBlocks`) does not accumulate another `indent` on every autosave - that was
/// producing runaway nesting like `.enabled` at column 20 after a few UI toggles.
fn writeNested(writer: *std.Io.Writer, text: []const u8, indent: usize) !void {
var strip: usize = std.math.maxInt(usize);
@@ -193,7 +193,7 @@ fn writeNested(writer: *std.Io.Writer, text: []const u8, indent: usize) !void {
/// Composes the final `settings.zon` text: `fizzy_fields_text` (fizzy's own
/// `Settings.serialize` output, just `.{ ...fizzy's own fields... }`) with a `.plugins = .{ ... }`
/// field appended, built from every entry already in `existing_full_source` (preserved verbatim,
-/// in their original order — including ids not touched this cycle, per `listPluginBlocks`'s
+/// in their original order - including ids not touched this cycle, per `listPluginBlocks`'s
/// doc comment) with `overlay`'s entries upserted by id (replacing the text of an existing id in
/// place, or appended if new). An overlay entry with `text == null` **deletes** that id from the
/// result entirely (R12). Field names always go through `std.zig.fmtId` (quotes only when
@@ -245,7 +245,7 @@ pub fn composeMergedText(
else
trimmed, " \t\r\n");
try aw.writer.writeAll(body);
- // `body` is fizzy's own fields with their closing `}` stripped — its last field has no
+ // `body` is fizzy's own fields with their closing `}` stripped - its last field has no
// trailing comma unless `Settings.serialize` happens to emit one, so add one here (unless
// the struct was empty) before appending `.plugins` as another field.
if (body.len > 0 and body[body.len - 1] != ',' and body[body.len - 1] != '{') {
@@ -255,7 +255,7 @@ pub fn composeMergedText(
for (order.items) |id| {
const text = by_id.get(id).?;
try aw.writer.print(" .{f} = ", .{std.zig.fmtId(id)});
- // `.plugins` is at 4 spaces; each id entry at 8 — continuations of a multi-line id
+ // `.plugins` is at 4 spaces; each id entry at 8 - continuations of a multi-line id
// block deepen from there.
try writeNested(&aw.writer, text, 8);
try aw.writer.writeAll(",\n");
@@ -307,7 +307,7 @@ pub fn upsertOne(gpa: Allocator, existing_full_source: ?[:0]const u8, entry: Ent
return aw.toOwnedSlice();
}
-/// The fizzy-reserved half of a plugin's block — everything beside the author's own `.settings`.
+/// The fizzy-reserved half of a plugin's block - everything beside the author's own `.settings`.
/// Each field is written only when it differs from its default, so a plugin sitting at defaults
/// still composes to `.{}` (and, with no settings either, is dropped from the file entirely).
pub const Reserved = struct {
@@ -315,30 +315,30 @@ pub const Reserved = struct {
/// dormant until the user (or a store install) turns it on.
///
/// **Tri-state, and the field's *presence* is load-bearing.** `null` (absent from disk) means
- /// nobody has decided yet — a build that appeared in `plugins/` on its own; `false` (written
+ /// nobody has decided yet - a build that appeared in `plugins/` on its own; `false` (written
/// out explicitly, unlike every other default in this file) means the user deliberately
/// switched it off. Both are "do not load", but only the first is an open offer, and fizzy
/// distinguishes them everywhere it matters: the rail badge, the store's Load button, and
/// whether loading raises the file-type ownership prompt. Uninstall erases the field, which
- /// is what makes a later reinstall ask again — see `Editor.clearPluginOwnershipRecord`.
+ /// is what makes a later reinstall ask again - see `Editor.clearPluginOwnershipRecord`.
enabled: ?bool = null,
- /// Whether this plugin takes store updates at all. Absent means **true** — the inverse of
+ /// Whether this plugin takes store updates at all. Absent means **true** - the inverse of
/// `enabled`'s default, so only a deliberate opt-out is ever written. *How* an update is
/// applied (silently, or through the update window) is one app-wide choice, not a per-plugin
- /// one — see `Settings.plugin_update_mode`.
+ /// one - see `Settings.plugin_update_mode`.
auto_update: bool = true,
/// Extensions (each with its dot) this plugin is the user's chosen default owner for.
- /// Written as a plain ZON tuple (`.extensions = .{ ".png", ".jpg" }`) — `&.{...}` is Zig
+ /// Written as a plain ZON tuple (`.extensions = .{ ".png", ".jpg" }`) - `&.{...}` is Zig
/// syntax that `std.zig.Zoir` will not parse, so `extractField` could never read it back.
- /// Absent means "no explicit choice for anything" — routing then falls through to the
+ /// Absent means "no explicit choice for anything" - routing then falls through to the
/// unique claimant, else the fallback editor (see `Host.pluginForExtension`).
///
/// The plugin's identity is expressed *structurally*, by which `.plugins.` block the
- /// list sits in — a plugin id is never written as a value here. Only an explicit user
+ /// list sits in - a plugin id is never written as a value here. Only an explicit user
/// decision (the install-time dialog, or the File Types settings table) ever writes this;
/// no load or reconcile path does.
///
- /// May legitimately name an extension the plugin does not offer via `fileTypes` — that is
+ /// May legitimately name an extension the plugin does not offer via `fileTypes` - that is
/// exactly what "keep Plain Text for `.foo`" records on the fallback editor's own block.
extensions: []const []const u8 = &.{},
};
@@ -371,7 +371,7 @@ pub fn freeExtensions(gpa: Allocator, exts: []const []const u8) void {
}
/// Composes one plugin id's on-disk block: `.{ .enabled = true, .settings = }` with each
-/// half omitted when not applicable (`.enabled` only when a decision exists — see its tri-state
+/// half omitted when not applicable (`.enabled` only when a decision exists - see its tri-state
/// note, so `false` *is* written; `.auto_update` only when `false`, `.extensions` only when
/// non-empty; `.settings` only when present).
/// Multi-line `settings_text` (as produced by `Schema(T).diffSerialize`) is re-indented under
@@ -505,7 +505,7 @@ test "listPluginBlocks retains a disabled/unloaded plugin's block untouched" {
test "composeMergedText accepts an empty fizzy struct (every fizzy field at its default)" {
// Since fizzy serializes non-default fields only (`Settings.serialize`, R12), an
- // untouched fizzy hands this `.{}` — the `.plugins` field must still splice on as valid ZON
+ // untouched fizzy hands this `.{}` - the `.plugins` field must still splice on as valid ZON
// rather than producing a stray leading comma.
const overlay = [_]Entry{.{ .id = "pixi", .text = ".{ .enabled = true }" }};
@@ -624,7 +624,7 @@ test "parseExtensions tolerates a hand-edited or malformed list" {
defer freeExtensions(testing.allocator, empty);
try testing.expectEqual(@as(usize, 0), empty.len);
- // Unterminated string / trailing garbage must not error — this file is user-editable.
+ // Unterminated string / trailing garbage must not error - this file is user-editable.
const ragged = try parseExtensions(testing.allocator, ".{ \".md\", \".mdx");
defer freeExtensions(testing.allocator, ragged);
try testing.expectEqual(@as(usize, 1), ragged.len);
@@ -637,7 +637,7 @@ test "parseExtensions tolerates a hand-edited or malformed list" {
test "every Reserved field is discoverable by extractField" {
// `SettingsMigration.isAlreadyNested` decides whether a `.plugins.` block is R12-shaped
- // or a pre-R12 flat one by probing for each reserved field by name — and a block judged flat
+ // or a pre-R12 flat one by probing for each reserved field by name - and a block judged flat
// has its whole contents wrapped into `.settings` and stamped `.enabled = true`. So a
// reserved field that composes to something `extractField` cannot find back is silently
// destructive: it would be reinterpreted as a plugin-author setting. This asserts the two
@@ -649,7 +649,7 @@ test "every Reserved field is discoverable by extractField" {
switch (field_type) {
bool => @field(reserved, field_name) = !@field(reserved, field_name),
// Tri-state: absent is the default, so either concrete value is "non-default" and
- // must be emitted. `false` is the interesting one — it used to be omitted.
+ // must be emitted. `false` is the interesting one - it used to be omitted.
?bool => @field(reserved, field_name) = false,
[]const []const u8 => @field(reserved, field_name) = &.{".probe"},
else => @compileError("extend this test for Reserved field type " ++ @typeName(field_type)),
@@ -667,8 +667,8 @@ test "every Reserved field is discoverable by extractField" {
test "composePluginIdBlock records enabled as a tri-state" {
// Absent means "never decided" (a build dropped into plugins/ that nobody has answered for);
- // an explicit `false` is the user's decision to keep it off. Collapsing the two — which the
- // non-default-only rule would otherwise do — silently demotes a disabled plugin back to an
+ // an explicit `false` is the user's decision to keep it off. Collapsing the two - which the
+ // non-default-only rule would otherwise do - silently demotes a disabled plugin back to an
// unanswered offer on the next launch.
const unset = try composePluginIdBlock(testing.allocator, .{}, ".{ .a = 1 }");
defer testing.allocator.free(unset);
@@ -684,7 +684,7 @@ test "composePluginIdBlock records enabled as a tri-state" {
}
test "composePluginIdBlock writes auto_update only when opted out" {
- // True is the absent state — a plugin that takes updates adds no bytes to the file.
+ // True is the absent state - a plugin that takes updates adds no bytes to the file.
const default_on = try composePluginIdBlock(testing.allocator, .{ .enabled = true }, null);
defer testing.allocator.free(default_on);
try testing.expectEqualStrings(".{\n .enabled = true,\n}", default_on);
diff --git a/app/single_instance/singleton.zig b/app/single_instance/singleton.zig
index a97af7e0e..c26230cbd 100644
--- a/app/single_instance/singleton.zig
+++ b/app/single_instance/singleton.zig
@@ -24,7 +24,7 @@ pub const drainPending = impl.drainPending;
pub const queuePath = impl.queuePath;
pub const collectAndResolveArgv = impl.collectAndResolveArgv;
pub const resolveArgs = impl.resolveArgs;
-/// What the application does with a forwarded path. Native only — one browser tab is one app,
+/// What the application does with a forwarded path. Native only - one browser tab is one app,
/// so the web layer has nothing to forward.
pub const setSink = if (builtin.target.cpu.arch == .wasm32) noopSetSink else impl.setSink;
diff --git a/app/single_instance/singleton_native.zig b/app/single_instance/singleton_native.zig
index 1d013ac33..aa8367075 100644
--- a/app/single_instance/singleton_native.zig
+++ b/app/single_instance/singleton_native.zig
@@ -9,7 +9,7 @@
//! * `drainPending` is called from the top of each frame to open queued
//! paths in the editor.
//!
-//! Path dispatch: directories → `editor.setProjectFolder`, files → `editor.openFilePath`.
+//! Path dispatch: directories -> `editor.setProjectFolder`, files -> `editor.openFilePath`.
const std = @import("std");
const builtin = @import("builtin");
@@ -26,7 +26,7 @@ const log = std.log.scoped(.singleton);
/// not fizzy's: two apps built on fizzy would otherwise share one lock, and launching the second
/// would forward its argv to the first and exit.
/// The application's identity, set by the app before `earlyStartup`. The lock is *per
-/// application* — two different fizzy-based apps must not fight over one — so this cannot be
+/// application* - two different fizzy-based apps must not fight over one - so this cannot be
/// read from fizzy's own identity the way it used to be.
pub var app_id: [:0]const u8 = "app.unnamed";
pub var app_name: []const u8 = "app";
@@ -95,7 +95,7 @@ pub fn tryAcquireLock(gpa: std.mem.Allocator, argv: []const []const u8) !bool {
var socket_dir = pickUnixSocketDir(&socket_dir_buf);
// A profile has a lock of its own, named for it, and keeps its socket in its runtime
- // directory — unless that path would not fit a socket address, when the temp directory
+ // directory - unless that path would not fit a socket address, when the temp directory
// holds it instead, still under the profile's own name.
var id: [:0]const u8 = app_id;
var profile_id: ?[:0]u8 = null;
@@ -225,7 +225,7 @@ fn queueArgvPaths(argv: []const []const u8) void {
/// What the application does with a path a second launch forwarded to this one.
///
/// The lock, the socket and the argv plumbing are the same for every app; what "open this" means
-/// is not — fizzy sets a project folder for a directory and opens a document for a file, having
+/// is not - fizzy sets a project folder for a directory and opens a document for a file, having
/// first walked up for a project marker. An app with no documents can point both of these at
/// something else entirely.
pub const Sink = struct {
@@ -235,7 +235,7 @@ pub const Sink = struct {
/// A file was forwarded. `project_root` is the marker directory found above it, when the app
/// asked for that search and one exists.
openFile: *const fn (ctx: *anyopaque, path: []const u8, project_root: ?[]const u8) anyerror!void,
- /// Whether to search upward for a project marker before opening a file — fizzy only does it
+ /// Whether to search upward for a project marker before opening a file - fizzy only does it
/// when nothing is open yet.
wantsProjectRoot: *const fn (ctx: *anyopaque) bool,
};
@@ -252,14 +252,14 @@ fn dispatchPath(path: []const u8) !void {
const to = sink orelse return error.NoSink;
// A path on a mounted filesystem is not the disk's to inspect: the app decides what it
- // is once the mount answers. Handed over as a file — a folder there is opened from the
+ // is once the mount answers. Handed over as a file - a folder there is opened from the
// explorer, not from argv.
if (core.paths.isMountPath(path)) {
try to.openFile(to.ctx, path, null);
return;
}
- // Try as directory first: openDirAbsolute succeeds → it's a folder.
+ // Try as directory first: openDirAbsolute succeeds -> it's a folder.
if (std.Io.Dir.openDirAbsolute(io, path, .{})) |dir| {
var d = dir;
d.close(io);
@@ -310,7 +310,7 @@ fn dirHasProjectMarker(gpa: std.mem.Allocator, dir: []const u8) bool {
}
/// Walk `argv` once via this zig's `Args.Iterator` API and return a slice
-/// `[argv[0], abs_path_1, abs_path_2, …]` owned by `gpa`. Flags (entries
+/// `[argv[0], abs_path_1, abs_path_2, ...]` owned by `gpa`. Flags (entries
/// starting with `-`) pass through unchanged; everything else is resolved
/// to an absolute path so the singleton primary doesn't need to know the
/// secondary's working directory. Resolution failures pass the original
@@ -413,7 +413,7 @@ pub fn freeResolvedArgv(gpa: std.mem.Allocator, argv: []const []const u8) void {
/// Normalizing (not just joining) matters for the common `fizzy .` invocation: a plain join
/// yields `/.`, which names the right directory but is a distinct string from ``
-/// everywhere downstream — see `core.paths.normalize`.
+/// everywhere downstream - see `core.paths.normalize`.
fn resolveAbsolute(gpa: std.mem.Allocator, cwd: []const u8, path: []const u8) ![]u8 {
if (std.fs.path.isAbsolute(path) or core.paths.isMountPath(path)) return core.paths.normalize(gpa, path);
if (cwd.len == 0) return error.NoCwd;
@@ -422,7 +422,7 @@ fn resolveAbsolute(gpa: std.mem.Allocator, cwd: []const u8, path: []const u8) ![
fn pickUnixSocketDir(buf: *[std.fs.max_path_bytes]u8) []const u8 {
if (builtin.target.os.tag == .windows) return "."; // unused on Windows
- // Prefer TMPDIR (macOS sets it per-user) → /tmp fallback.
+ // Prefer TMPDIR (macOS sets it per-user) -> /tmp fallback.
// Use the libc env to avoid plumbing `process.Environ` here.
if (std.c.getenv("TMPDIR")) |env_ptr| {
const len = std.mem.len(env_ptr);
diff --git a/app/single_instance/singleton_web.zig b/app/single_instance/singleton_web.zig
index c3b2070a6..c31ccfdc8 100644
--- a/app/single_instance/singleton_web.zig
+++ b/app/single_instance/singleton_web.zig
@@ -34,7 +34,7 @@ pub fn queuePath(_: []const u8) void {}
/// On native this iterates the OS argv and resolves file args to absolute paths.
/// On web there's no argv (the page URL could theoretically carry one in
-/// `?file=…` later, but that's a future concern); return a minimal `argv[0]`
+/// `?file=...` later, but that's a future concern); return a minimal `argv[0]`
/// only so `freeResolvedArgv` has something to free.
pub fn collectAndResolveArgv(
gpa: std.mem.Allocator,
diff --git a/app/store/PluginLoader.zig b/app/store/PluginLoader.zig
index 3361358be..a02ead63b 100644
--- a/app/store/PluginLoader.zig
+++ b/app/store/PluginLoader.zig
@@ -108,7 +108,7 @@ pub fn pluginExtension() []const u8 {
};
}
-/// Bare `{name}.{ext}` filename (no directory) — the basename inside a plugin's own
+/// Bare `{name}.{ext}` filename (no directory) - the basename inside a plugin's own
/// `{plugins_dir}/{name}/` directory (see plans/PLUGIN_MANIFEST_PLAN.md R10; every plugin, built-in
/// or third-party, gets its own directory rather than sitting flat in `plugins/`).
pub fn pluginFilename(name: []const u8, allocator: std.mem.Allocator) ![]const u8 {
@@ -158,7 +158,7 @@ var load_tmp_seq: std.atomic.Value(u64) = .init(0);
/// Every path this process has handed to `dlopen`/`LoadLibrary` (loads *and* probes), so
/// `loadAndRegister` can tell a first open of a path from a re-open. See `markPathOpened`.
var opened_paths: std.StringHashMapUnmanaged(void) = .empty;
-/// Spinlock (`std.atomic.Mutex` has no blocking `lock()`) — the critical section is one hashmap
+/// Spinlock (`std.atomic.Mutex` has no blocking `lock()`) - the critical section is one hashmap
/// probe plus at most one insert, never I/O.
var opened_paths_lock: std.atomic.Mutex = .unlocked;
@@ -173,13 +173,13 @@ fn lockOpenedPaths() void {
/// stale-image problem only exists for a path this process has *already* opened; the first open
/// of a path in a fresh process cannot hit it. That distinction is worth a lot on macOS: the
/// first `dlopen` of a newly-created file costs ~110-140ms of one-time system security
-/// validation (size-independent — a 20-line stub dylib pays the same), and the result is cached
+/// validation (size-independent - a 20-line stub dylib pays the same), and the result is cached
/// against the file itself, so a *second* open of an untouched file is ~0.5ms. Copying every
/// plugin to a brand-new file each launch therefore re-bought that penalty every single launch,
/// once per plugin (~480ms for three plugins); loading the stable installed file in place pays
/// it once, on the launch right after an install.
///
-/// Entries (and their duped keys) are kept until the app quits (`deinitOpenedPaths`) — the set is
+/// Entries (and their duped keys) are kept until the app quits (`deinitOpenedPaths`) - the set is
/// bounded by the number of distinct plugin paths, and its whole purpose is to outlive
/// individual load attempts.
///
@@ -208,7 +208,7 @@ pub fn deinitOpenedPaths(gpa: std.mem.Allocator) void {
opened_paths = .empty;
}
-/// `{dirname(path)}/.load-tmp` — where fresh-copy load attempts land, beside the plugin itself.
+/// `{dirname(path)}/.load-tmp` - where fresh-copy load attempts land, beside the plugin itself.
fn loadTempDir(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
const dir = std.fs.path.dirname(path) orelse ".";
return std.fs.path.join(allocator, &.{ dir, ".load-tmp" });
@@ -217,13 +217,13 @@ fn loadTempDir(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
/// Copies `path` to a freshly, uniquely-named sibling file and returns that path (caller frees).
///
/// Works around `dlopen`/`LoadLibrary` caching an image by path: once a given path has been
-/// opened in this process — even briefly, even if the load was then rejected and `close()`d —
+/// opened in this process - even briefly, even if the load was then rejected and `close()`d -
/// `dyld` on macOS in particular can hand back the *same stale image* on a later `dlopen` of that
/// path, even after the file underneath was replaced on disk (e.g. the plugin store installing a
/// fixed build over one that failed an ABI check). Loading from a path this process has never
/// opened before sidesteps the cache entirely, regardless of the OS loader's exact behavior.
///
-/// Only reached for a *re-open* of a path — `markPathOpened` explains why paying this on a first
+/// Only reached for a *re-open* of a path - `markPathOpened` explains why paying this on a first
/// open (i.e. all of startup) is pure loss.
fn copyToFreshLoadPath(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
const tmp_dir = try loadTempDir(allocator, path);
@@ -238,7 +238,7 @@ fn copyToFreshLoadPath(allocator: std.mem.Allocator, path: []const u8) ![]const
return dest;
}
-/// `{dirname(path)}/.load-copy` — the *stable* mapped-copy directory. Deliberately separate from
+/// `{dirname(path)}/.load-copy` - the *stable* mapped-copy directory. Deliberately separate from
/// `.load-tmp`, whose whole contents `sweepLoadTempDir` wipes at startup.
fn loadCopyDir(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
const dir = std.fs.path.dirname(path) orelse ".";
@@ -251,10 +251,10 @@ fn loadCopyDir(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
/// correctness requirement rather than a preference:
///
/// * Windows: `LoadLibrary` holds a section object on the file it maps for the library's
-/// lifetime, so mapping the installed file would lock it — the store's
+/// lifetime, so mapping the installed file would lock it - the store's
/// temp-file-plus-rename install (`plugin_store/download.zig`) could no longer replace a
/// loaded plugin.
-/// * POSIX: replacing a *mapped* file in place is worse than a failed rename — the plugin
+/// * POSIX: replacing a *mapped* file in place is worse than a failed rename - the plugin
/// SDK's dev install (`plugin_sdk.zig`'s `DevInstall`) writes the dylib with a truncating
/// `writeFile`, so a plugin author running `zig build install` while fizzy has that plugin
/// loaded would rewrite the bytes under a live mapping. That is a `SIGBUS`/garbage-code
@@ -263,7 +263,7 @@ fn loadCopyDir(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
/// Unlike `copyToFreshLoadPath` the copy is *reused across launches*: its name carries the source's
/// mtime and size, so an unchanged plugin maps a file that already exists (and that the OS has
/// already validated/scanned) instead of a brand-new one. That is what keeps this off the
-/// per-launch first-open cost — see `markPathOpened`. A changed plugin lands on a new name, and the
+/// per-launch first-open cost - see `markPathOpened`. A changed plugin lands on a new name, and the
/// copies of superseded builds are swept.
fn stableLoadCopyPath(allocator: std.mem.Allocator, path: []const u8, st: std.Io.File.Stat) ![]const u8 {
const copy_dir = try loadCopyDir(allocator, path);
@@ -291,7 +291,7 @@ fn stableLoadCopyPath(allocator: std.mem.Allocator, path: []const u8, st: std.Io
/// Best-effort delete of a fresh-load temp copy, safe to call right after a successful
/// `dlopen`/`LoadLibrary`. On POSIX the already-mapped pages keep the inode alive after unlink,
/// so the loaded plugin is unaffected; on Windows the file is still open for the library's
-/// lifetime, so this silently no-ops — `sweepLoadTempDir` catches it on the next launch instead.
+/// lifetime, so this silently no-ops - `sweepLoadTempDir` catches it on the next launch instead.
fn cleanupFreshLoadPath(path: []const u8) void {
std.Io.Dir.deleteFileAbsolute(dvui.io, path) catch {};
}
@@ -303,7 +303,7 @@ fn sweepOneTempDir(dir_path: []const u8) void {
}
/// As `sweepOneTempDir`, but keeps the entry named `keep` (used for the stable Windows load copy
-/// that was just written — everything *else* in that directory is a superseded build).
+/// that was just written - everything *else* in that directory is a superseded build).
fn sweepOneTempDirExcept(dir_path: []const u8, keep: ?[]const u8) void {
var dir = std.Io.Dir.cwd().openDir(dvui.io, dir_path, .{ .iterate = true }) catch return;
defer dir.close(dvui.io);
@@ -316,13 +316,13 @@ fn sweepOneTempDirExcept(dir_path: []const u8, keep: ?[]const u8) void {
}
/// Clears out leftover fresh-load temp copies from a previous run. Call once at startup, before
-/// any plugin is loaded this session — nothing can be mapped from this directory yet, so every
+/// any plugin is loaded this session - nothing can be mapped from this directory yet, so every
/// file in it is safe to remove outright (this is exactly what `cleanupFreshLoadPath` couldn't
/// do on Windows while the plugin was still loaded, or after a crash on any platform).
///
/// Since R10, each plugin gets its own `{plugins_dir}/{id}/` directory, so `copyToFreshLoadPath`'s
/// temp dir is now per-plugin (`{plugins_dir}/{id}/.load-tmp`) rather than one shared
-/// `{plugins_dir}/.load-tmp` — sweep every plugin subdirectory's, plus the old flat location in
+/// `{plugins_dir}/.load-tmp` - sweep every plugin subdirectory's, plus the old flat location in
/// case a leftover from before the R10 upgrade is still sitting there.
pub fn sweepLoadTempDir(allocator: std.mem.Allocator, plugins_dir: []const u8) void {
{
@@ -342,7 +342,7 @@ pub fn sweepLoadTempDir(allocator: std.mem.Allocator, plugins_dir: []const u8) v
}
}
-/// A plugin binary opened and checked, not yet registered — everything `loadAndRegister` does
+/// A plugin binary opened and checked, not yet registered - everything `loadAndRegister` does
/// that touches no host state, so `open` runs on any thread. The `dlopen` in it is what a load
/// costs (on macOS ~180 ms for a file the system has not seen, a rebuilt plugin every time);
/// `register` finishes it on the UI thread in microseconds. Dropped unregistered with `close`.
@@ -380,12 +380,12 @@ pub fn loadAndRegister(
pub fn open(allocator: std.mem.Allocator, path: []const u8, expected_id: []const u8) LoadError!Opened {
// First open of this path in this process: no stale-image risk, so load the installed file
// directly and skip the copy (and, on macOS, the ~110ms first-open validation a brand-new
- // file would cost). Any later open of the same path — a reload after the store wrote a new
- // build over it, or after a probe already opened it — goes through the fresh copy.
- // Never map the installed file itself — see `stableLoadCopyPath`. Two kinds of copy:
- // * first open of this path → the per-build *stable* copy, which an unchanged plugin
+ // file would cost). Any later open of the same path - a reload after the store wrote a new
+ // build over it, or after a probe already opened it - goes through the fresh copy.
+ // Never map the installed file itself - see `stableLoadCopyPath`. Two kinds of copy:
+ // * first open of this path -> the per-build *stable* copy, which an unchanged plugin
// re-uses across launches (so this costs nothing on a normal launch).
- // * re-open of a path this process already opened → a unique throwaway copy, the only
+ // * re-open of a path this process already opened -> a unique throwaway copy, the only
// case where the loader's stale-image behavior is actually in play.
const reopen = markPathOpened(allocator, path);
const src_stat: ?std.Io.File.Stat = std.Io.Dir.cwd().statFile(dvui.io, path, .{}) catch null;
@@ -398,7 +398,7 @@ pub fn open(allocator: std.mem.Allocator, path: []const u8, expected_id: []const
var lib = DynLib.open(load_path) catch return error.DylibOpenFailed;
errdefer lib.close();
- // Only the throwaway copy gets deleted — the stable copy is what the next launch wants to
+ // Only the throwaway copy gets deleted - the stable copy is what the next launch wants to
// find still sitting there.
if (reopen) cleanupFreshLoadPath(load_path);
@@ -559,12 +559,12 @@ pub fn probeName(allocator: std.mem.Allocator, path: []const u8) ?[]u8 {
}
/// The store-facing half of a plugin's embedded manifest, read straight off the dylib. Every
-/// field is owned by the caller — free with `freeProbedManifest`.
+/// field is owned by the caller - free with `freeProbedManifest`.
///
/// One struct (and one `dlopen` + zon parse) rather than a `probeX` per field: these all come out
/// of the *same* embedded `fizzy_plugin_manifest_zon` blob, and the store needs several of them
/// for the same card. Per-field probes meant re-opening the dylib once per field and a parallel
-/// cache per field in `PluginStore` — see `manifest_cache` there, which is now the only one.
+/// cache per field in `PluginStore` - see `manifest_cache` there, which is now the only one.
pub const ProbedManifest = struct {
description: []u8 = &.{},
tags: [][]u8 = &.{},
@@ -572,7 +572,7 @@ pub const ProbedManifest = struct {
author_url: []u8 = &.{},
};
-/// Best-effort read of `ProbedManifest` straight from the dylib, **without registering it** —
+/// Best-effort read of `ProbedManifest` straight from the dylib, **without registering it** -
/// same shape as `probeName`, but there's no legacy C export to fall back to (these postdate the
/// fixed C-ABI export list; they only ever lived in the embedded manifest zon). Null when the
/// dylib or its manifest couldn't be read at all, which callers can distinguish from a
diff --git a/app/store/PluginLoader_web.zig b/app/store/PluginLoader_web.zig
index 8b263b637..ebdb3afc6 100644
--- a/app/store/PluginLoader_web.zig
+++ b/app/store/PluginLoader_web.zig
@@ -1,13 +1,13 @@
//! Runtime plugin loading on the web: a plugin built as a wasm *side module* (see
-//! `sdk/plugin_sdk.zig`'s `create` for wasm32 and `plans/REVIEW_2026-09.md` §2), fetched and
+//! `sdk/plugin_sdk.zig`'s `create` for wasm32 and `plans/REVIEW_2026-09.md` section2), fetched and
//! linked into the page by `web/fizzy.js`'s `loadPlugin`, which hands back one function-table
//! index per entry point. On wasm32 a function pointer *is* a table index, so from there this
//! runs the same sequence the desktop loader runs (`PluginLoader.loadAndRegister`): fingerprint,
//! versions, id, inject the host's globals, `fizzy_plugin_register`.
//!
//! The fetch is asynchronous, so a load is two halves: `begin` asks the page, and the page's
-//! `FizzyWebPluginReady` lands in `pump`'s callback on the next frame. There is no unload — a
-//! side module cannot leave the table — so "disable" is "hide", as the review notes.
+//! `FizzyWebPluginReady` lands in `pump`'s callback on the next frame. There is no unload - a
+//! side module cannot leave the table - so "disable" is "hide", as the review notes.
const std = @import("std");
const builtin = @import("builtin");
const dvui = @import("dvui");
@@ -62,7 +62,7 @@ pub const PreRegister = struct {
arg_c: ?*anyopaque = null,
};
-/// Entry points by name → table index, in the order `web/fizzy.js`'s `pluginEntryPoints`
+/// Entry points by name -> table index, in the order `web/fizzy.js`'s `pluginEntryPoints`
/// lists them (the same order as `plugin_sdk.dylib_exports`).
pub const WebDynLib = struct {
indices: [entry_names.len]u32,
@@ -175,7 +175,7 @@ pub fn init(gpa: std.mem.Allocator) void {
/// whatever its file happens to be called.
///
/// `sha256` is the hash the registry published for this download, as lowercase hex. The page
-/// checks the bytes against it before compiling anything, and refuses the module on a mismatch —
+/// checks the bytes against it before compiling anything, and refuses the module on a mismatch -
/// the check the desktop downloader has always done, which a web install skipped entirely. Empty
/// skips it, which is right for a file served beside the app and wrong for anything else.
pub fn begin(gpa: std.mem.Allocator, id: []const u8, url: []const u8, sha256: []const u8, cb: ArrivedFn, ctx: ?*anyopaque) error{OutOfMemory}!u32 {
@@ -252,7 +252,7 @@ fn readVersionTriplet(get_fn: ?dylib_api.GetSdkVersionFn) std.SemanticVersion {
return .{ .major = 0, .minor = 0, .patch = 0 };
}
-/// The versions a linked module declares, read whether or not `prepare` accepts it — the web's
+/// The versions a linked module declares, read whether or not `prepare` accepts it - the web's
/// `probeVersionInfo`, so a refused build is reported with the same detail as one on disk. Only
/// the triplet getters run, which `prepare` already calls on a module whose fingerprint is wrong.
pub fn versionInfo(lib: WebDynLib) PluginVersionInfo {
@@ -345,7 +345,7 @@ pub fn prepare(url: []const u8, expected_id: []const u8, lib_in: WebDynLib) Load
};
}
-/// Check and register in one step — the desktop loader's shape, for a plain load.
+/// Check and register in one step - the desktop loader's shape, for a plain load.
pub fn loadAndRegister(
host: *Host,
allocator: std.mem.Allocator,
diff --git a/app/store/PluginManager.zig b/app/store/PluginManager.zig
index ad4dfebbc..2b1a9cb7e 100644
--- a/app/store/PluginManager.zig
+++ b/app/store/PluginManager.zig
@@ -1,18 +1,18 @@
//! What the plugin store needs from the application hosting it.
//!
-//! The store — registry catalog, install/update jobs, the store pane — is framework an app
+//! The store - registry catalog, install/update jobs, the store pane - is framework an app
//! either switches on or leaves out, exactly like watching and installers. It was reaching
//! `fizzy.editor()` in sixty-odd places, which made "any fizzy app gets a plugin store" false:
//! the code was fizzy's, not the framework's.
//!
//! This is the seam. An application fills one of these in once at startup and the store talks to
-//! nothing else — no `Editor`, no fizzy globals. The store is compiled *into* an app and never
+//! nothing else - no `Editor`, no fizzy globals. The store is compiled *into* an app and never
//! into a dylib, so this is a plain `{ctx, vtable}` pair with no ABI implications; it is here
//! because the store has one host per process, not because anything crosses a boundary.
//!
//! Everything on it is genuinely the app's: which plugins the user disabled, which failed to
//! load, where they are installed, and what happens when one is installed or removed. The store
-//! decides none of that — it draws it and asks.
+//! decides none of that - it draws it and asks.
const std = @import("std");
const sdk = @import("fizzy_sdk");
/// Via `app.zig` so this picks up the wasm stub on a web build: `LoadedLib` holds a `DynLib`,
@@ -30,8 +30,8 @@ pub const Failure = struct {
plugin_version: ?std.SemanticVersion = null,
};
-/// How the app applies available updates. The store reads it; where it is stored — a settings
-/// file, a flag, nowhere — is the app's business.
+/// How the app applies available updates. The store reads it; where it is stored - a settings
+/// file, a flag, nowhere - is the app's business.
pub const UpdateMode = enum {
/// Collect them and let the user apply them.
prompt,
@@ -60,7 +60,7 @@ config_folder: []const u8,
/// only its build can say what that is.
bundled_plugin_ids: []const []const u8 = &.{},
/// Where this app's plugin registry lives. `plugins.yourapp.com` is a config value, not a
-/// fork — the store is the same code pointed somewhere else.
+/// fork - the store is the same code pointed somewhere else.
registry_url: []const u8,
/// The running executable's path, for the dev-tree asset fallback (`plugin_repo_asset.zig`
/// walks up from here looking for a sibling checkout before it reaches for the network).
@@ -70,13 +70,13 @@ vtable: *const VTable,
/// Whether an update to the *application itself* is in flight. Plugin builds are made against
/// one SDK generation, so the builds that pair with a newer app only become visible once it is
-/// running — the store waits rather than offering the older ones.
+/// running - the store waits rather than offering the older ones.
pub const AppUpdate = enum {
/// The launch check has not answered yet.
checking,
/// A newer application exists, or is installing.
pending,
- /// Nothing waiting — go ahead.
+ /// Nothing waiting - go ahead.
none,
};
@@ -157,11 +157,11 @@ pub const VTable = struct {
reconcileDiscovered: *const fn (ctx: *anyopaque) void,
/// See `AppUpdate`. An app with no self-updater returns `.none`.
appUpdate: *const fn (ctx: *anyopaque) AppUpdate,
- /// Tell the user updates are waiting, however this app does that — fizzy opens its "Plugin
+ /// Tell the user updates are waiting, however this app does that - fizzy opens its "Plugin
/// updates" window. The store has already collected them (`pendingUpdates`); this is only
/// the invitation, and doing nothing is a valid answer for an app that applies silently.
offerUpdates: *const fn (ctx: *anyopaque) void,
- /// Bring the main region into view — on a narrow layout the store's README page is drawn
+ /// Bring the main region into view - on a narrow layout the store's README page is drawn
/// somewhere the user cannot see yet. A no-op is a perfectly good implementation.
revealMain: *const fn (ctx: *anyopaque) void,
};
diff --git a/app/store/PluginPage.zig b/app/store/PluginPage.zig
index ac2149bfb..9b8f8c418 100644
--- a/app/store/PluginPage.zig
+++ b/app/store/PluginPage.zig
@@ -8,7 +8,7 @@
//!
//! The mount is what makes that possible without a new host API: `DocumentIo` opens any path a
//! mount claims by reading its bytes, on the desktop and in the browser alike. So the page's
-//! "file" is one line — the plugin's id — and everything else is looked up from the catalog at
+//! "file" is one line - the plugin's id - and everything else is looked up from the catalog at
//! draw time, where it is anyway (a plugin installs, updates, or goes away while its page is
//! open, and the page should say so without being reloaded).
const std = @import("std");
@@ -25,19 +25,19 @@ const DocHandle = sdk.DocHandle;
pub const extension = ".fizzyplugin";
/// The mount every page lives under. A page's path is `store://pages/`,
-/// so the tab shows the plugin's name — a tab strip is a row of file names, and "Google Drive"
+/// so the tab shows the plugin's name - a tab strip is a row of file names, and "Google Drive"
/// is what that plugin is called.
///
/// Scheme *and* authority, with no trailing slash, because that is what a mount prefix is here:
/// `FileTable.pathOnMount` asks that the rest of the path start with `/`, so a bare `store://`
-/// matched nothing and every page fell through to the disk — "Failed to open file" for a file
+/// matched nothing and every page fell through to the disk - "Failed to open file" for a file
/// that was right there in memory. `gdrive://` has the same shape for the same reason.
pub const mount_prefix = "store://pages";
pub const plugin_id = "fizzy.store";
/// What the store gives this module so a page can draw itself without importing `PluginStore`
-/// (which imports this file). One vtable, filled in at registration — the same `{ctx, vtable}`
+/// (which imports this file). One vtable, filled in at registration - the same `{ctx, vtable}`
/// seam the app uses everywhere it needs to name fizzy from underneath.
pub const PageDraw = struct {
/// Draw the header for `plugin_id`: title, version, publisher, and the page's controls.
@@ -48,7 +48,7 @@ pub const PageDraw = struct {
/// store cannot say, which shows the "no README" state rather than fetching nothing.
source: *const fn (plugin_id: []const u8) ?Source,
/// What this plugin is called, for the tab. Null when the store has never heard of it, and
- /// then the tab falls back to the file name — which is the id, and still says something.
+ /// then the tab falls back to the file name - which is the id, and still says something.
title: *const fn (plugin_id: []const u8) ?[]const u8,
/// Draw the page's tab strip with `tab` selected; returns the tab after any click. Drawn by
/// the store because it is the store's styling, chosen per page because the tab is a
@@ -108,7 +108,7 @@ pub const Document = struct {
const State = struct {
docs: std.AutoArrayHashMapUnmanaged(u64, Document) = .empty,
mounted: bool = false,
- /// What the pages' filesystem has answered and not yet delivered — see `pages_fs`.
+ /// What the pages' filesystem has answered and not yet delivered - see `pages_fs`.
answers: ?core.vfs.Deferred = null,
};
@@ -187,7 +187,7 @@ pub fn open(host: *sdk.Host, id: []const u8, grouping: u64) !void {
}
/// A page's address: the plugin's id, not its name. The path is what a saved layout stores and
-/// reopens from, so it has to be the stable thing — and the tab still reads "Google Drive",
+/// reopens from, so it has to be the stable thing - and the tab still reads "Google Drive",
/// because `documentTitle` answers that separately.
fn pathFor(gpa: std.mem.Allocator, id: []const u8) ![]u8 {
return gpa.print("{s}/{s}{s}", .{ mount_prefix, id, extension });
@@ -209,7 +209,7 @@ fn focus(host: *sdk.Host, doc: Document) !void {
// ---- the pages' filesystem -------------------------------------------------------------------
//
// There is nothing behind it. A page's whole content is the plugin id in its own path, so this
-// answers a read by parsing the path it was asked about — which is what makes a page restorable
+// answers a read by parsing the path it was asked about - which is what makes a page restorable
// from a saved layout with no state at all. A `Mem` filled in as pages opened could not: on the
// next run the layout asks for `store://pages/pixi.fizzyplugin` before anything has opened one,
// the file is not there, and the tab is dropped with "NotFound".
@@ -230,7 +230,7 @@ const fs_vtable: core.vfs.Fs.VTable = .of(.{
});
/// Every answer is known on the spot, and still arrives from `pump`, as `Fs` promises: the
-/// open that asked has to have its bookkeeping in place — the loading tab the page lands in —
+/// open that asked has to have its bookkeeping in place - the loading tab the page lands in -
/// before the page exists.
fn answers() *core.vfs.Deferred {
return &state.answers.?;
@@ -288,7 +288,7 @@ fn documentStackAlign(_: *anyopaque) usize {
return @alignOf(Document);
}
fn loadDocument(_: *anyopaque, path: []const u8, out_doc: *anyopaque) anyerror!void {
- // Never reached in practice — every page lives on the mount, so `DocumentIo` reads the bytes
+ // Never reached in practice - every page lives on the mount, so `DocumentIo` reads the bytes
// and calls `loadDocumentFromBytes`. Kept because the vtable requires it and because a page
// path could be restored from a layout after the mount is gone.
_ = path;
@@ -362,7 +362,7 @@ fn drawDocument(_: *anyopaque, handle: DocHandle) anyerror!void {
defer page.deinit();
const draw = page_draw orelse return;
- // The header answers "is this plugin still a thing the store knows about" — a page can
+ // The header answers "is this plugin still a thing the store knows about" - a page can
// outlive the plugin it is about (uninstalled, or a registry that no longer lists it).
if (!draw.header(doc.plugin)) {
dvui.labelNoFmt(@src(), "The store no longer has this plugin.", .{}, .{
@@ -406,7 +406,7 @@ fn docBuf(ptr: *anyopaque) *Document {
}
/// The handle carries the pointer this module returned from `registerOpenDocument`, so trust it
-/// — but only after checking the id is one of ours, since a handle for another owner's document
+/// - but only after checking the id is one of ours, since a handle for another owner's document
/// would otherwise be reinterpreted as a page.
fn docFrom(handle: DocHandle) ?*Document {
return state.docs.getPtr(handle.id);
diff --git a/app/store/PluginStore.zig b/app/store/PluginStore.zig
index 593e2f959..c65ab25f5 100644
--- a/app/store/PluginStore.zig
+++ b/app/store/PluginStore.zig
@@ -1,4 +1,4 @@
-//! Fizzy built-in: the **Plugins** sidebar tab — discover / install / update / enable / disable
+//! Fizzy built-in: the **Plugins** sidebar tab - discover / install / update / enable / disable
//! / uninstall plugins. Registered above Settings.
//!
//! Downloads run on a worker thread (`Job`); the actual live load happens on the main thread in
@@ -8,7 +8,7 @@
//!
//! The web build installs too. The catalog and READMEs load through the browser's `fetch`, and a
//! plugin published for `web-wasm32` is fetched and linked by the page rather than downloaded to
-//! a directory (`queueInstall` → `installFromUrl`, `applyWebUpdate` → `updateFromUrl`); a plugin
+//! a directory (`queueInstall` -> `installFromUrl`, `applyWebUpdate` -> `updateFromUrl`); a plugin
//! with no web build is simply not offered there. Two things stay desktop-only: the SHA-256 the
//! registry publishes, which the page does not check, and the download queue below.
const std = @import("std");
@@ -18,8 +18,8 @@ const sdk = @import("fizzy_sdk");
const icons = @import("icons");
const store = @import("registry/store.zig");
// The real loader, not `app.store.Loader`: what this file wants from it are the *manifest
-// probe* helpers, which compile everywhere. Only the `LoadedLib` list — which carries a
-// `DynLib` — has to come from the wasm stub, and that arrives through `PluginManager`.
+// probe* helpers, which compile everywhere. Only the `LoadedLib` list - which carries a
+// `DynLib` - has to come from the wasm stub, and that arrives through `PluginManager`.
const Page = @import("PluginPage.zig");
const PluginLoader = @import("PluginLoader.zig");
const PluginManager = @import("PluginManager.zig");
@@ -58,20 +58,20 @@ var installed_scroll_info: dvui.ScrollInfo = .{ .horizontal = .auto };
/// Detail-page header: scrolls sideways below `detail_header_min_w` instead of crushing the
/// info column (and wrapping the description into a skyscraper). Vertical is never owned here.
var detail_header_scroll: dvui.ScrollInfo = .{ .horizontal = .auto, .vertical = .none };
-/// `hashId` of the entry whose header `detail_header_scroll` last showed — used to zero the
+/// `hashId` of the entry whose header `detail_header_scroll` last showed - used to zero the
/// horizontal offset on page switches so a scrolled-over previous plugin doesn't leave you
/// looking at the middle of the next one's header.
var detail_header_scroll_entry: usize = 0;
/// Floor under the detail header row (logo + info + install controls). The header *expands*
/// with the pane above this; below it the scroll area takes over horizontally. Same role as
-/// `card_min_w` on the list cards — a stable content min, not a viewport-locked width.
+/// `card_min_w` on the list cards - a stable content min, not a viewport-locked width.
const detail_header_min_w: f32 = 480;
/// Applied as `max_size_content.w` on header text so labels/layouts don't report their full
/// unwrapped width as min size (which would inflate the header past `detail_header_min_w` and
/// make the horizontal scrollbar appear while the pane is still wider than the floor). Same
-/// trick as `card_text_no_floor` — see that comment. The widgets still *draw* at the info
+/// trick as `card_text_no_floor` - see that comment. The widgets still *draw* at the info
/// column's real (expanded) width via `.expand = .horizontal`.
const detail_header_text_no_floor: f32 = 1;
@@ -109,7 +109,7 @@ const Job = struct {
refused: ?[]u8 = null,
/// This job's download worker, as a cancelable `Io` task group rather than a detached
/// `std.Thread`. The worker writes `err_buf`/`err_len`/`status` back into the `Job`, so the
- /// `Job` must not be freed while it is alive — and a detached thread gave `freeJob` no way to
+ /// `Job` must not be freed while it is alive - and a detached thread gave `freeJob` no way to
/// know. Nothing in the native download path sets a network timeout (the web's fetches
/// have one, `web/fizzy.js`'s `fetchPluginBytes`), so a stalled or
/// blackholed release host keeps that worker running arbitrarily long: quitting mid-download
@@ -120,7 +120,7 @@ const Job = struct {
///
/// Same shape as `core/lsp/Client.zig`'s `tasks` and the store catalog's refresh worker. Not
/// threadsafe against its own `await`/`cancel`, so every group operation stays on the UI
- /// thread (`draw` → `startDownload`, `tick`, `deinit`).
+ /// thread (`draw` -> `startDownload`, `tick`, `deinit`).
tasks: std.Io.Group = .init,
};
@@ -139,15 +139,15 @@ const PendingAction = union(enum) {
var pending_actions: std.ArrayList(PendingAction) = .empty;
/// Last-known display name per plugin id (app-allocator owned). A sideloaded plugin only exposes
-/// its display name while loaded — once disabled it is unloaded and we'd otherwise fall back to the
-/// bare id, which changes the A→Z sort position (e.g. "Terminal" → "ghostty") every time it is
+/// its display name while loaded - once disabled it is unloaded and we'd otherwise fall back to the
+/// bare id, which changes the A->Z sort position (e.g. "Terminal" -> "ghostty") every time it is
/// toggled. We remember the name the first time we see it (loaded plugin or registry row) and reuse
/// it as the stable title for the disabled/failed states. (Session-scoped: a plugin disabled before
/// it was ever loaded this session still shows its id until enabled once.)
var name_cache: std.array_hash_map.String([]u8) = .empty;
/// Last-known installed version per plugin id (app-allocator owned keys, plain values). Like
-/// `name_cache`, a sideloaded plugin only exposes its version while loaded — once disabled it is
+/// `name_cache`, a sideloaded plugin only exposes its version while loaded - once disabled it is
/// unloaded, and a failed build's version is only known if the dylib could be probed. We remember
/// the version the first time we see it (loaded plugin, failed-load probe, or on-disk probe) and
/// reuse it as the "current version" for disabled/failed cards.
@@ -156,7 +156,7 @@ var version_cache: std.array_hash_map.String(std.SemanticVersion) = .empty;
/// Disk-probed manifest fields per plugin id (app-allocator owned keys and values; an all-empty
/// entry means "probed, genuinely has nothing" so it isn't re-probed every frame either). Backs
/// the `PluginLoader.probeManifestInfo` fallback shared by `descriptionFor`/`tagsFor`/`authorFor`/
-/// `authorUrlFor` — registry and built-in values are already cheap and never touch this. One
+/// `authorUrlFor` - registry and built-in values are already cheap and never touch this. One
/// cache rather than one per field, because a single `dlopen` + parse yields all of them at once.
/// Cleared by `refreshDiskScan`.
var manifest_cache: std.array_hash_map.String(PluginLoader.ProbedManifest) = .empty;
@@ -165,7 +165,7 @@ var manifest_cache: std.array_hash_map.String(PluginLoader.ProbedManifest) = .em
/// `refreshDiskScan`. The installed pane's in-memory sources (loaded plugins, disabled ids,
/// recorded load failures) each describe a *state* fizzy put a plugin into; this describes
/// what is actually on disk, which is the only thing that guarantees a broken plugin always has
-/// a card to uninstall or reinstall from — even one that arrived in a state nothing recorded
+/// a card to uninstall or reinstall from - even one that arrived in a state nothing recorded
/// (a half-finished install, a sideloaded directory, a build fizzy never tried to load).
var disk_ids: std.ArrayList([]u8) = .empty;
/// Set when the plugins directory may have changed (first draw, Refresh, and after any install /
@@ -176,7 +176,7 @@ var disk_scan_dirty = true;
/// Ask `draw` to re-list the plugins directory (and re-probe names/versions) on its next pass.
/// Exposed for `Editor.reconcileDiscoveredPlugins`: the watcher discovering a dropped-in build is
/// exactly a "the plugins dir changed" event, and without this its card would draw from the stale
-/// scan — bare id, or missing entirely until the user hit Refresh.
+/// scan - bare id, or missing entirely until the user hit Refresh.
pub fn markDiskScanDirty() void {
disk_scan_dirty = true;
}
@@ -188,7 +188,7 @@ pub const CatalogOffer = struct {
/// Catalog plugins that are not installed here and have a build this host can install.
/// Arena-backed; empty when the catalog has never loaded. `presentLocally` and the host key do
-/// the filtering on both targets — in a browser "installed" means linked into this page, and a
+/// the filtering on both targets - in a browser "installed" means linked into this page, and a
/// plugin with no `web-wasm32` download is simply not offered.
pub fn uninstalledCatalog(arena: std.mem.Allocator) []const CatalogOffer {
const c = &(catalog orelse return &.{});
@@ -254,7 +254,7 @@ pub fn queueInstall(id: []const u8) void {
// straight from its release URL, and the app registers it when it lands.
// Marked *before* the call: `installFromUrl` can finish inside it (a build already
// running under this id reports success straight away), and marking afterwards would
- // set a flag the report had already cleared — a card spinning with nothing behind it.
+ // set a flag the report had already cleared - a card spinning with nothing behind it.
markWebInFlight(id);
app.installFromUrl(id, dl.url, dl.sha256) catch |err| {
clearWebInFlight(id);
@@ -387,7 +387,7 @@ fn resolveTitle(id: []const u8, fallback: []const u8) []const u8 {
return name_cache.get(id) orelse fallback;
}
-/// The best-known display name for `id` — the live plugin's `display_name` when loaded,
+/// The best-known display name for `id` - the live plugin's `display_name` when loaded,
/// otherwise whatever `resolveTitle` remembers (a disabled/failed plugin seen earlier this
/// session), falling back to the bare id. Used by `PluginSettingsPane` to label a disabled
/// plugin's Enabled-toggle-only row without duplicating this cache's lookup logic.
@@ -398,9 +398,9 @@ pub fn displayName(id: []const u8) []const u8 {
/// Query the real display name + version of every on-disk plugin that isn't currently loaded,
/// straight from its dylib (embedded `plugin.zig.zon` via `fizzy_plugin_manifest_zon`, falling
-/// back to the `fizzy_plugin_name`/version exports — no register, no on-disk sidecar), seeding
+/// back to the `fizzy_plugin_name`/version exports - no register, no on-disk sidecar), seeding
/// `name_cache` and `version_cache`. Covers plugins disabled before they were loaded this session
-/// as well as builds that never loaded at all (wrong SDK, etc.) — a broken plugin still shows its
+/// as well as builds that never loaded at all (wrong SDK, etc.) - a broken plugin still shows its
/// real name and version on its card. Cheap and bounded: only runs on first draw / Refresh, and
/// only probes ids whose name or version we don't already know.
fn probeOnDiskInfo() void {
@@ -409,7 +409,7 @@ fn probeOnDiskInfo() void {
defer a.free(plugins_dir);
for (disk_ids.items) |id| {
- if (app.host.pluginById(id) != null) continue; // loaded → info comes from the live plugin
+ if (app.host.pluginById(id) != null) continue; // loaded -> info comes from the live plugin
const have_name = name_cache.get(id) != null;
const have_version = version_cache.get(id) != null;
if (have_name and have_version) continue; // already known (registry / prior probe)
@@ -440,7 +440,7 @@ fn refreshLocalInfo() void {
}
/// The application hosting this store. Everything the store needs from its app goes through
-/// here — see `PluginManager.zig`.
+/// here - see `PluginManager.zig`.
const app = &PluginManager.current;
pub fn register(manager: PluginManager) !void {
@@ -453,7 +453,7 @@ pub fn register(manager: PluginManager) !void {
const fp_hex = try manager.gpa.print("0x{x}", .{dylib.abi_fingerprint});
defer manager.gpa.free(fp_hex);
catalog = try store.Catalog.init(manager.gpa, dvui.io, url, fp_hex);
- // The owner of `store://…` pages, and the mount they live on. Registered before the store's
+ // The owner of `store://...` pages, and the mount they live on. Registered before the store's
// own surface so a layout restored with a page open finds its owner already there.
try Page.register(host, .{
.header = drawPageHeader,
@@ -512,13 +512,13 @@ fn drawDetailHeader(entry: StoreEntry) void {
defer scroll.deinit();
// Deliberately *not* keyed by `entry.id`, unlike the cards in the list: there is only ever
- // one detail header, and a per-entry id made it a brand-new widget on every page switch —
+ // one detail header, and a per-entry id made it a brand-new widget on every page switch -
// with no min size cached from the previous frame it laid out at zero height, which is the
// one frame of headerless page users saw before it snapped. A stable id keeps the cache.
//
// Expand to fill the scroll viewport when wider than the floor; report a fixed min so the
// scrollbar only appears once the pane is actually narrower than the content can go. Do
- // *not* lock min=max to the viewport — that made wrap width track the resize every frame and
+ // *not* lock min=max to the viewport - that made wrap width track the resize every frame and
// flashed the scrollbar whenever a long token briefly exceeded the shrinking column.
var header_box = dvui.box(@src(), .{ .dir = .horizontal }, .{
.expand = .horizontal,
@@ -529,7 +529,7 @@ fn drawDetailHeader(entry: StoreEntry) void {
});
defer header_box.deinit();
- // 1. Logo — same fetch-or-fallback chain the card list uses.
+ // 1. Logo - same fetch-or-fallback chain the card list uses.
{
var logo = dvui.box(@src(), .{ .dir = .horizontal }, .{
.gravity_y = 0.0,
@@ -537,7 +537,7 @@ fn drawDetailHeader(entry: StoreEntry) void {
});
defer logo.deinit();
if (repoSource(entry)) |src| StoreIcon.request(entry.id, src.repo, src.subpath);
- // 60 rather than the full 64 of `logo`'s box — just enough breathing room that the
+ // 60 rather than the full 64 of `logo`'s box - just enough breathing room that the
// image doesn't touch the header's own edges, per the "less empty space" ask.
const drew = StoreIcon.draw(entry.id, 60) or app.host.drawPluginIcon(entry.id);
if (!drew) {
@@ -552,7 +552,7 @@ fn drawDetailHeader(entry: StoreEntry) void {
}
// 2. Toggles + install/update/uninstall controls. `drawDetailControls` right-justifies itself
- // (its own `gravity_x = 1.0` box) — laid out before the expanding info column for the same
+ // (its own `gravity_x = 1.0` box) - laid out before the expanding info column for the same
// reason `drawCard` does: an expand-horizontal sibling's reported min width is its full
// unwrapped text, which would otherwise eat all the remaining space and starve these controls
// to zero width (see `drawCard`'s comment on this exact ordering).
@@ -569,7 +569,7 @@ fn drawDetailHeader(entry: StoreEntry) void {
defer info.deinit();
const title_font = dvui.Font.theme(.body);
- // Single-line ellipsize (LabelWidget default) — wrapping the display-size title to one
+ // Single-line ellipsize (LabelWidget default) - wrapping the display-size title to one
// word per line was the resize hitch that made the scrollbar appear mid-drag.
dvui.labelNoFmt(@src(), entry.title, .{}, .{
.font = title_font.withSize(title_font.size * 3 - 1).withWeight(.bold),
@@ -594,7 +594,7 @@ fn drawDetailHeader(entry: StoreEntry) void {
// Cap reported min width; wrap still uses the real expanded column width.
.max_size_content = .{ .w = detail_header_text_no_floor, .h = std.math.floatMax(f32) },
.margin = dvui.Rect.all(0),
- // `TextLayoutWidget`'s own default padding is `Rect.all(6)` — unlike the labels
+ // `TextLayoutWidget`'s own default padding is `Rect.all(6)` - unlike the labels
// above, which each override `.padding` down to just a bottom gap (`.{ .h = N }`,
// leaving left at 0). Left unset here, that default 6px left inset was the only
// thing pushing this line out of alignment with the other three.
@@ -608,27 +608,27 @@ fn drawDetailHeader(entry: StoreEntry) void {
}
/// Only ever hand a `http`/`https` URL to `dvui.openURL`. `author_url` is author-controlled text
-/// out of a `plugin.zig.zon`, and `openURL` ultimately reaches the OS URL handler — a `file:`,
+/// out of a `plugin.zig.zon`, and `openURL` ultimately reaches the OS URL handler - a `file:`,
/// `smb:`, or custom-scheme URL there would let a plugin manifest launch an arbitrary registered
/// handler on the user's machine just by being listed in the store.
fn isSafeExternalUrl(url: []const u8) bool {
return std.ascii.startsWithIgnoreCase(url, "https://") or std.ascii.startsWithIgnoreCase(url, "http://");
}
-/// The credit line under the plugin id: `publisher · author`.
+/// The credit line under the plugin id: `publisher . author`.
///
/// Two different kinds of claim, deliberately shown together rather than collapsed into one
/// "author" string. `publisher` is derived from where the binary is actually published and is the
/// half a user can rely on; `author` is a free string the plugin says about itself. Either may be
-/// absent — a sideloaded plugin has no publisher, and a plugin that never sets `.author` has no
-/// credit — in which case only the other is drawn, and the row is skipped entirely if neither
+/// absent - a sideloaded plugin has no publisher, and a plugin that never sets `.author` has no
+/// credit - in which case only the other is drawn, and the row is skipped entirely if neither
/// exists. Each is a link only when there is somewhere meaningful to go.
fn drawAuthorLine(entry: StoreEntry, muted: dvui.Color) void {
const publisher = publisherFor(entry);
const author = authorFor(entry);
if (publisher == null and author == null) return;
- // Expand the row (not the individual links) so credit names stay packed on the left —
+ // Expand the row (not the individual links) so credit names stay packed on the left -
// `expand = .horizontal` on both publisher and author made them split the row and look
// centered. Cap the row's reported min so long names can't inflate the header floor.
var row = dvui.box(@src(), .{ .dir = .horizontal }, .{
@@ -641,7 +641,7 @@ fn drawAuthorLine(entry: StoreEntry, muted: dvui.Color) void {
if (publisher) |p| {
// Publisher links to the account itself, built from the publisher name rather than from
- // any URL the plugin supplied — the whole point of this half is that it isn't
+ // any URL the plugin supplied - the whole point of this half is that it isn't
// author-controlled.
const url = dvui.currentWindow().arena().print("https://github.com/{s}", .{p}) catch "";
drawCreditLink(@src(), p, if (url.len > 0) url else null, muted);
@@ -664,10 +664,10 @@ fn drawAuthorLine(entry: StoreEntry, muted: dvui.Color) void {
}
}
-/// One name in the credit line — a plain dim label, or an underlined highlight-colored link when
+/// One name in the credit line - a plain dim label, or an underlined highlight-colored link when
/// `url` is non-null. Styling matches the text plugin's hover-doc links (`TextEditor.zig`):
/// `Font.withUnderline` + `color(.highlight, .fill)`, so every clickable bit of prose in the app
-/// reads the same way. A name with nowhere to go stays deliberately unstyled — underlining
+/// reads the same way. A name with nowhere to go stays deliberately unstyled - underlining
/// something unclickable is worse than leaving it plain.
fn drawCreditLink(src: std.lang.SourceLocation, text: []const u8, url: ?[]const u8, muted: dvui.Color) void {
const body = dvui.Font.theme(.body);
@@ -695,13 +695,13 @@ fn drawCreditLink(src: std.lang.SourceLocation, text: []const u8, url: ?[]const
/// Simple two-tab strip: no fill of its own (reads as part of the pane behind it, not a
/// separate bar), regular body text, and the selected tab marked by a `window`-text-colored
-/// underline rather than a filled pill. No drag/drop and no scroll area — there are only ever
+/// underline rather than a filled pill. No drag/drop and no scroll area - there are only ever
/// two tabs here.
fn tabButton(src: std.lang.SourceLocation, label: []const u8, id_extra: usize, selected: bool) bool {
const theme = dvui.themeGet();
- // Sized to the label's own width (no expand), so the underline drawn below — which does
- // expand horizontally, but only within this column — lines up under the text exactly rather
+ // Sized to the label's own width (no expand), so the underline drawn below - which does
+ // expand horizontally, but only within this column - lines up under the text exactly rather
// than spanning the whole strip.
var col = dvui.box(@src(), .{ .dir = .vertical }, .{ .id_extra = id_extra, .margin = .{ .x = 2 } });
defer col.deinit();
@@ -728,7 +728,7 @@ fn tabButton(src: std.lang.SourceLocation, label: []const u8, id_extra: usize, s
return clicked;
}
-/// CHANGELOG's content until real GitHub Releases fetching lands (tracked separately) — an
+/// CHANGELOG's content until real GitHub Releases fetching lands (tracked separately) - an
/// empty-state hint rather than a blank pane, matching how the workbench's own empty states read.
fn drawChangelogPlaceholder() void {
var box = dvui.box(@src(), .{ .dir = .vertical }, .{
@@ -761,7 +761,7 @@ fn drawChangelogPlaceholder() void {
}
/// `FIZZY_PLUGIN_REGISTRY_URL` overrides the app's catalog *base* URL (used for local E2E
-/// testing) — `store.Catalog.init` appends `/summary.json` and `//releases.json`
+/// testing) - `store.Catalog.init` appends `/summary.json` and `//releases.json`
/// to whatever this returns. Owned for the process lifetime (freed in `deinit`).
fn resolveRegistryUrl() []const u8 {
if (comptime builtin.target.cpu.arch == .wasm32) return app.registry_url;
@@ -831,8 +831,8 @@ pub fn deinit() void {
}
}
-/// Frees a job and everything it owns. Cancels and awaits its download worker first — see
-/// `Job.tasks` — so nothing can be writing into the `Job` as it goes away. No-op for a job whose
+/// Frees a job and everything it owns. Cancels and awaits its download worker first - see
+/// `Job.tasks` - so nothing can be writing into the `Job` as it goes away. No-op for a job whose
/// worker never started or has already finished.
fn freeJob(job: *Job) void {
job.tasks.cancel(dvui.io);
@@ -859,7 +859,7 @@ pub const PendingUpdate = struct {
///
/// Owned here rather than re-resolved from the catalog on click, for two reasons: registry
/// strings are only valid while the catalog lock is held (the refresh worker frees the arena),
- /// and the update window draws its cards *inside* that lock — so a click that reached back for
+ /// and the update window draws its cards *inside* that lock - so a click that reached back for
/// the catalog would re-enter a non-reentrant mutex on the UI thread and hang the app.
url: []u8,
sha256: []u8,
@@ -870,9 +870,9 @@ pub const PendingUpdate = struct {
/// fizzy upgrade, most often) and the store build for *this* host replaces it. Presented as a
/// repair rather than an upgrade, since the version number often doesn't move.
repair: bool,
- /// An Update was clicked (or Update all) — the row shows progress instead of a button.
+ /// An Update was clicked (or Update all) - the row shows progress instead of a button.
started: bool = false,
- /// The last attempt failed (the download, not the load — see `tick`). The row stays on the
+ /// The last attempt failed (the download, not the load - see `tick`). The row stays on the
/// offer with its button back, labelled Retry.
failed: bool = false,
/// Why the last web attempt failed, shown beside Retry (`webLoadFailed`); empty when unknown.
@@ -892,7 +892,7 @@ const AutoPhase = enum {
idle,
/// A catalog refresh is in flight purely for the update check.
fetching,
- /// The check ran (or was skipped, or the fetch failed). One attempt per session — the store
+ /// The check ran (or was skipped, or the fetch failed). One attempt per session - the store
/// tab's own Refresh button is the manual retry.
done,
};
@@ -920,7 +920,7 @@ pub fn dismissPendingUpdates() void {
clearPendingUpdates();
}
-/// Take `id` off the offer — its newer build is now on disk, so there is nothing left to offer.
+/// Take `id` off the offer - its newer build is now on disk, so there is nothing left to offer.
/// The window shows exactly the plugins that are still out of date, and closes itself once the
/// last row goes (see `PluginUpdates.dialog`).
fn dropPendingUpdate(id: []const u8) void {
@@ -940,7 +940,7 @@ fn dropPendingUpdate(id: []const u8) void {
}
/// Hand `id`'s row its button back after a failed attempt, so the offer can be retried by hand
-/// rather than sitting on a permanent "Updating…".
+/// rather than sitting on a permanent "Updating...".
fn markPendingUpdateFailed(id: []const u8) void {
const row = pendingRowFor(id) orelse return;
row.started = false;
@@ -949,14 +949,14 @@ fn markPendingUpdateFailed(id: []const u8) void {
/// Start the download for one offered update. Safe to call for a row already started (the job is
/// simply replaced) and from inside the card list's own draw, since it touches no catalog state.
-/// Ids whose web load or update is in flight — the page is fetching and linking, and the host
+/// Ids whose web load or update is in flight - the page is fetching and linking, and the host
/// has not yet accepted or refused the module. `app.gpa`-owned; one entry per id.
var web_in_flight: std.array_hash_map.String(void) = .empty;
/// The web's plugins directory: every id the page remembers a build for (it brings each back at
/// startup) or that loaded this visit. `refreshDiskScan` lists the ones the page still remembers
-/// into `disk_ids`, so everything the desktop does with a plugin on disk — the Installed card,
-/// Uninstall and Reinstall, the update pass — applies here too. `app.gpa`-owned keys.
+/// into `disk_ids`, so everything the desktop does with a plugin on disk - the Installed card,
+/// Uninstall and Reinstall, the update pass - applies here too. `app.gpa`-owned keys.
var web_known: std.array_hash_map.String(void) = .empty;
/// Startup loads the page asked for that have not landed yet. The update pass waits for these:
/// until a remembered build has been accepted or refused, the pass cannot tell a plugin that is
@@ -1020,7 +1020,7 @@ pub fn webLoadSucceeded(id: []const u8) void {
}
/// It did not: the fetch failed, the module was refused, or the plugin it would have replaced
-/// has unsaved documents. The offer stays, wearing Retry — the running plugin was never torn
+/// has unsaved documents. The offer stays, wearing Retry - the running plugin was never torn
/// down, so there is something to go back to.
pub fn webLoadFailed(id: []const u8, reason: []const u8) void {
clearWebInFlight(id);
@@ -1039,11 +1039,11 @@ pub fn webLoadFailed(id: []const u8, reason: []const u8) void {
/// The web's update: the new build is fetched and checked while the running one keeps working,
/// and only takes the id over once it has passed (`Editor.updateWebPlugin`). The old module
-/// stays linked in the page — nothing can unlink it — but it owns nothing after the swap.
+/// stays linked in the page - nothing can unlink it - but it owns nothing after the swap.
fn applyWebUpdate(id: []const u8, url: []const u8, sha256: []const u8) void {
// Marked *before* the call, as `queueInstall` does: the load can report inside it (a build
- // already in flight for this id, a refusal), and marking afterwards overwrote that report —
- // the row stayed on "Update" (or on "Updating…" forever) with the work already done or
+ // already in flight for this id, a refusal), and marking afterwards overwrote that report -
+ // the row stayed on "Update" (or on "Updating..." forever) with the work already done or
// already failed. Not dropped here either: the fetch has only *started*;
// `webLoadSucceeded` retires the row.
markWebInFlight(id);
@@ -1070,7 +1070,7 @@ pub fn applyPendingUpdate(id: []const u8) void {
return;
}
// Whether this is an unload-and-reload or a fresh install is decided by what is *running*
- // now, not by what the row said when it was built — the plugin may have been enabled or
+ // now, not by what the row said when it was built - the plugin may have been enabled or
// disabled while the window sat open.
startDownloadUrl(row.id, row.url, row.sha256, .{
.is_update = app.host.pluginById(id) != null,
@@ -1092,7 +1092,7 @@ pub fn updateStatus(id: []const u8) ?[]const u8 {
.failed => "Failed",
};
// A row that finished is gone from the list entirely (`dropPendingUpdate`), so anything still
- // here with no job either hasn't started or failed — both want their button back.
+ // here with no job either hasn't started or failed - both want their button back.
return null;
}
@@ -1158,7 +1158,7 @@ fn drawUpdateCardControls(entry: StoreEntry) void {
applyPendingUpdate(entry.id);
}
-/// "v0.3.9 → v0.4.1", or what a repair actually is — the update window's equivalent of a store
+/// "v0.3.9 -> v0.4.1", or what a repair actually is - the update window's equivalent of a store
/// card's `installed vX / store vY` line, in the same slot.
fn updateInfoLine(buf: []u8, row: PendingUpdate) []const u8 {
if (row.repair) {
@@ -1168,7 +1168,7 @@ fn updateInfoLine(buf: []u8, row: PendingUpdate) []const u8 {
return std.mem.print(buf, "v{d}.{d}.{d} → v{s}", .{ from.major, from.minor, from.patch, row.to }) catch row.to;
}
-/// The offered updates as store cards — the same shell the Plugins tab draws, so the two never
+/// The offered updates as store cards - the same shell the Plugins tab draws, so the two never
/// drift apart. Cards here are inert apart from their Update button: clicking one must not swap
/// the README center out from under a modal dialog, and the hover toggles belong to the store.
pub fn drawPendingUpdateCards() void {
@@ -1189,7 +1189,7 @@ pub fn drawPendingUpdateCards() void {
}
}
-/// True while any offered update still has an Update button to press — the window's "Update all"
+/// True while any offered update still has an Update button to press - the window's "Update all"
/// hides itself once there is nothing left to start.
pub fn anyPendingUpdateUnstarted() bool {
for (pending_updates.items) |u| {
@@ -1199,7 +1199,7 @@ pub fn anyPendingUpdateUnstarted() bool {
}
/// Drives the once-per-session update check from `tick`: kick one catalog refresh, then act on
-/// it when it lands. Deliberately *not* run before `Editor.loadUserPlugins` — that is a
+/// it when it lands. Deliberately *not* run before `Editor.loadUserPlugins` - that is a
/// synchronous startup path and the fetch has no timeout, so gating startup on it would hang
/// launch behind a captive portal or a dead registry host. Everything loads first; a plugin whose
/// build this fizzy can't load is then repaired live, no restart needed.
@@ -1226,7 +1226,7 @@ fn autoUpdateTick() void {
}
// Plugins go *after* Fizzy itself. Store builds are made against one SDK
// generation, so the plugin builds that match the Fizzy release we're about to
- // install only become visible to us once we are running it — see
+ // install only become visible to us once we are running it - see
// `PluginManager.AppUpdate`.
// The web has no self-update to wait for: the page is whatever the site serves.
if (comptime builtin.target.cpu.arch != .wasm32) switch (app.appUpdate()) {
@@ -1235,7 +1235,7 @@ fn autoUpdateTick() void {
.checking => return,
// A newer Fizzy exists. Say nothing about plugins yet, in either mode: the
// builds that pair with that Fizzy are in a shard we can't see from here, so
- // anything we could offer now is the *older* build — or, without
+ // anything we could offer now is the *older* build - or, without
// `releaseSdkSatisfied`, one that wouldn't load at all. The user takes the app
// update when they choose; this pass runs again on the relaunch, against the
// new SDK's shard, and only then is there a plugin story worth telling.
@@ -1270,7 +1270,7 @@ fn autoUpdateTick() void {
}
}
-/// True if any plugin on disk is a candidate for the update check at all — installed, not
+/// True if any plugin on disk is a candidate for the update check at all - installed, not
/// bundled, not disabled, and not opted out.
fn anyUpdatableInstalled() bool {
for (disk_ids.items) |id| {
@@ -1314,7 +1314,7 @@ fn collectPendingUpdates() void {
if (repair) {
// Enabled on record but not running: the build on disk was rejected. The store build
// is the way back in even at the same version number (different fingerprint), but
- // never *older* than what is already there — that would downgrade an author's own
+ // never *older* than what is already there - that would downgrade an author's own
// newer local build the moment it failed to load once.
if (installed) |known| {
if (rel_ver.order(known) == .lt) continue;
@@ -1330,7 +1330,7 @@ fn collectPendingUpdates() void {
}
/// Own copies of one row's strings and append it. Any allocation failure unwinds the ones already
-/// taken and drops the row — a plugin missing from the window is a far better outcome than a leak
+/// taken and drops the row - a plugin missing from the window is a far better outcome than a leak
/// or a half-built entry.
fn appendPendingUpdate(
id: []const u8,
@@ -1374,8 +1374,8 @@ fn appendPendingUpdate(
///
/// Repairs (`PendingUpdate.repair`) are the exception to `.prompt`: the plugin is enabled but not
/// running, which after a Fizzy update is the normal state of every plugin built against the old
-/// SDK. Prompting there offers the user a choice they don't have — declining leaves the plugin
-/// dead rather than merely older — so repairs always go through silently, and the common case
+/// SDK. Prompting there offers the user a choice they don't have - declining leaves the plugin
+/// dead rather than merely older - so repairs always go through silently, and the common case
/// (Fizzy updates, its plugins follow on the next launch) is seamless in both modes. Only genuine
/// version upgrades of *working* plugins are worth asking about.
fn offerPendingUpdates() void {
@@ -1421,7 +1421,7 @@ pub fn tick() void {
if (catalog) |*c| c.pump(web_fetch);
StoreIcon.pump();
// Enable, Disable, Uninstall and the auto-update toggle all queue here and are applied
- // nowhere else — without this the web store's buttons lit up and did nothing. The
+ // nowhere else - without this the web store's buttons lit up and did nothing. The
// download queue below is the desktop's alone: on the web a plugin arrives through the
// page's loader instead (`applyWebUpdate`, `queueInstall`).
applyPendingActions();
@@ -1446,7 +1446,7 @@ pub fn tick() void {
markPendingUpdateFailed(job.id);
jobs.swapRemoveAt(i);
freeJob(job);
- continue; // do not advance i — swapRemove moved a new entry into slot i
+ continue; // do not advance i - swapRemove moved a new entry into slot i
}
i += 1;
},
@@ -1490,18 +1490,18 @@ pub fn tick() void {
continue;
};
// Installed + loaded: drop the job so the card shows normal installed state, and
- // take the plugin off the update offer — it is no longer out of date.
+ // take the plugin off the update offer - it is no longer out of date.
dropPendingUpdate(job.id);
jobs.swapRemoveAt(i);
freeJob(job);
- // do not advance i — swapRemove moved a new entry into slot i
+ // do not advance i - swapRemove moved a new entry into slot i
},
}
}
}
/// Select `entry`, or clear the selection if it is already the selected card. Selection is a
-/// card highlight and which card's flyout is open — nothing more. The page is a document
+/// card highlight and which card's flyout is open - nothing more. The page is a document
/// (`PluginPage`), opened deliberately from that flyout.
fn toggleSelect(entry: StoreEntry) void {
if (selectedId()) |sid| {
@@ -1551,7 +1551,7 @@ fn drawPageOtherTab(_: u8) void {
}
/// A store page's header, drawn into the page document (`PluginPage`). False when the store has
-/// never heard of this id — a page can outlive the plugin it is about.
+/// never heard of this id - a page can outlive the plugin it is about.
fn drawPageHeader(id: []const u8) bool {
const snap = if (catalog) |*c| c.acquire() else null;
defer if (catalog) |*c| c.release();
@@ -1580,7 +1580,7 @@ fn pageSource(id: []const u8) ?Page.Source {
}
/// The `StoreEntry` for one id, in the same priority order the store list builds its entries in
-/// (see `draw`'s entry-building pass) — just for one id instead of the whole list. `snapshot` is
+/// (see `draw`'s entry-building pass) - just for one id instead of the whole list. `snapshot` is
/// the caller's already-acquired catalog snapshot (or null when the catalog has never loaded);
/// this makes no locking decisions of its own.
fn entryFor(id: []const u8, snapshot: ?store.Catalog.Snapshot) ?StoreEntry {
@@ -1606,7 +1606,7 @@ fn entryFor(id: []const u8, snapshot: ?store.Catalog.Snapshot) ?StoreEntry {
}
/// `entry`'s on-disk manifest fields, probed once and memoised in `manifest_cache`. A dylib open
-/// + zon parse per call is far too costly to redo every frame — these resolvers run once per
+/// + zon parse per call is far too costly to redo every frame - these resolvers run once per
/// *listed card* (see `drawCardShell`), not just for the selected detail header. Returns an
/// all-empty `ProbedManifest` when the plugin has no readable dylib, which is cached too so a
/// missing/unreadable file isn't retried every frame. `manifest_cache` is cleared by
@@ -1635,9 +1635,9 @@ fn probedManifestFor(id: []const u8) PluginLoader.ProbedManifest {
return probed;
}
-/// Best-known one-line description for `entry`: the registry's own (freshest — a published
+/// Best-known one-line description for `entry`: the registry's own (freshest - a published
/// `summary.json` entry may know more than a locally built manifest), else the plugin's own
-/// manifest (`Editor.builtinManifest` for a built-in — no dylib to probe — or `probedManifestFor`
+/// manifest (`Editor.builtinManifest` for a built-in - no dylib to probe - or `probedManifestFor`
/// against its on-disk dylib for anything else). Null when none of the three have one.
fn descriptionFor(entry: StoreEntry) ?[]const u8 {
if (entry.registry) |r| {
@@ -1650,7 +1650,7 @@ fn descriptionFor(entry: StoreEntry) ?[]const u8 {
return if (probed.description.len > 0) probed.description else null;
}
-/// Best-known tag list for `entry` — same three-tier fallback as `descriptionFor`. Empty slice,
+/// Best-known tag list for `entry` - same three-tier fallback as `descriptionFor`. Empty slice,
/// never null, when none of the three have any: every call site only ever iterates the result.
fn tagsFor(entry: StoreEntry) []const []const u8 {
if (entry.registry) |r| {
@@ -1660,7 +1660,7 @@ fn tagsFor(entry: StoreEntry) []const []const u8 {
return probedManifestFor(entry.id).tags;
}
-/// Best-known **cosmetic** author credit for `entry` — self-asserted, never a trust signal (see
+/// Best-known **cosmetic** author credit for `entry` - self-asserted, never a trust signal (see
/// `publisherFor` for the attestable half). Same three-tier fallback as `descriptionFor`: a
/// registry entry's own `author` wins, since it's PR-reviewed and editable without a release.
fn authorFor(entry: StoreEntry) ?[]const u8 {
@@ -1676,7 +1676,7 @@ fn authorFor(entry: StoreEntry) ?[]const u8 {
/// Optional custom link for `authorFor`'s credit. Same three-tier fallback as the rest; the
/// catalog carries it so an *uninstalled* store plugin's credit is still clickable, since there
-/// is no local dylib to probe until it's installed. Only ever `http`/`https` — see
+/// is no local dylib to probe until it's installed. Only ever `http`/`https` - see
/// `drawAuthorLine`.
fn authorUrlFor(entry: StoreEntry) ?[]const u8 {
if (entry.registry) |r| {
@@ -1691,10 +1691,10 @@ fn authorUrlFor(entry: StoreEntry) ?[]const u8 {
/// The **attestable** half of the credit line: who actually published the binary this host would
/// install. Derived server-side at ingest from the release URL the plugin's `manifest.json` was
-/// fetched from — never self-asserted, so unlike `authorFor` it can't be spoofed by editing a
+/// fetched from - never self-asserted, so unlike `authorFor` it can't be spoofed by editing a
/// `plugin.zig.zon`. Built-ins ship inside the fizzy binary itself, so they're attributed to the
/// fizzy org directly rather than through the registry. Null when the store has no entry (a
-/// sideloaded or purely local plugin has no publisher at all — that's the point).
+/// sideloaded or purely local plugin has no publisher at all - that's the point).
fn publisherFor(entry: StoreEntry) ?[]const u8 {
if (isBundled(entry.id)) return fizzy_publisher;
if (entry.registry) |r| {
@@ -1703,16 +1703,16 @@ fn publisherFor(entry: StoreEntry) ?[]const u8 {
return null;
}
-/// Owner of `fizzy_repo_url` — the publisher every bundled built-in is attributed to.
+/// Owner of `fizzy_repo_url` - the publisher every bundled built-in is attributed to.
const fizzy_publisher = "fizzyedit";
/// Runs in `job.tasks`. `io` is passed in rather than read from `dvui.io` here so the worker never
/// races the UI thread writing that global (same reason `core/lsp/Client.zig` threads `io` through
/// its task mains). A shutdown mid-download lands on `.failed` via `error.Canceled`, which nothing
-/// outlives — `freeJob` is what awaited us.
+/// outlives - `freeJob` is what awaited us.
fn worker(job: *Job, io: std.Io) void {
// Whatever happens below, the result is only ever *applied* by `tick` on the UI thread, so
- // every exit path has to wake a sleeping app — see `Job.win`.
+ // every exit path has to wake a sleeping app - see `Job.win`.
defer dvui.refresh(job.win, @src(), null);
store.download.download(app.gpa, io, job.url, job.sha256, job.dest) catch |err| {
const n = @min(@errorName(err).len, job.err_buf.len);
@@ -1742,7 +1742,7 @@ fn startDownload(id: []const u8, release: store.ShardRelease, opts: DownloadOpti
/// Guarded at comptime like `startDownload`, and it has to be: this is the path that writes a
/// dylib into the plugins directory, which a browser has neither the filesystem nor the `dlopen`
/// for. `applyPendingUpdate` reaches here without passing through `startDownload`, so one guard
-/// upstream is not enough — and since the store became its own module, an unguarded path is a
+/// upstream is not enough - and since the store became its own module, an unguarded path is a
/// wasm *compile* error rather than dead code.
fn startDownloadUrl(id: []const u8, url: []const u8, sha256: []const u8, opts: DownloadOptions) void {
if (comptime builtin.target.cpu.arch == .wasm32) return;
@@ -1750,7 +1750,7 @@ fn startDownloadUrl(id: []const u8, url: []const u8, sha256: []const u8, opts: D
// Before anything is written into `plugins/`: on macOS and Windows a case variant's
// folder is that folder, and the download would overwrite the plugin installed there.
// Refused, the job fails at once and says why: on its card, or as the update offer marked
- // failed for the automatic pass (`tick`), never left "Installing…".
+ // failed for the automatic pass (`tick`), never left "Installing...".
var why_buf: [512]u8 = undefined;
if (refusal(&why_buf, id)) |why| {
var refused = opts;
@@ -1862,7 +1862,7 @@ fn installedVersion(id: []const u8) ?std.SemanticVersion {
return null;
}
-/// The recorded load-failure for `id`, if any — asked of the app rather than read off a
+/// The recorded load-failure for `id`, if any - asked of the app rather than read off a
/// `StoreEntry`, so it surfaces even when a registry row (which takes merge precedence over the
/// local `.failed` row, see `draw`) shadows the same id.
fn failedInfo(id: []const u8) ?PluginManager.Failure {
@@ -1872,7 +1872,7 @@ fn failedInfo(id: []const u8) ?PluginManager.Failure {
/// The plugin's current on-disk version, regardless of whether it is loaded, disabled, or a
/// rejected (failed) build: the live loaded version when running, else a failed build's probed
/// version, else the last version we remembered (loaded earlier this session, or probed while
-/// disabled — see `probeOnDiskInfo`).
+/// disabled - see `probeOnDiskInfo`).
fn currentVersion(id: []const u8) ?std.SemanticVersion {
if (installedVersion(id)) |v| return v;
if (failedInfo(id)) |f| {
@@ -1882,10 +1882,10 @@ fn currentVersion(id: []const u8) ?std.SemanticVersion {
}
/// The highest version published across *every* fingerprint of `entry`'s registry row, regardless
-/// of host compatibility — "what the author has shipped", shown on every card with a store
+/// of host compatibility - "what the author has shipped", shown on every card with a store
/// presence (see `infoLine`). Distinct from `selectedRelease`, which only considers this host's
/// release (from the fetched shard) and drives the Install/Update actions. Precomputed
-/// server-side into `summary.json`'s `latest_version` — the client only ever fetches its own
+/// server-side into `summary.json`'s `latest_version` - the client only ever fetches its own
/// fingerprint's shard, so it can't itself see what other fingerprints have published.
fn latestRegistryVersion(entry: StoreEntry) ?[]const u8 {
const r = entry.registry orelse return null;
@@ -1894,7 +1894,7 @@ fn latestRegistryVersion(entry: StoreEntry) ?[]const u8 {
/// Whether `id` is compiled into this executable. Asked of the build (`App.bundled_plugin_ids`)
/// rather than answered from a list here: this was four hardcoded names, and the fifth bundled
-/// plugin — archive — was shown as a stray local build the user could uninstall, which is not a
+/// plugin - archive - was shown as a stray local build the user could uninstall, which is not a
/// thing that can happen to something linked into the binary. An app built on fizzy bundles
/// whatever it likes, so a list here could only ever be fizzy's own, and out of date.
fn isBundled(id: []const u8) bool {
@@ -1907,21 +1907,21 @@ fn isBundled(id: []const u8) bool {
/// One deterministic row in the store tree, merged from the registry index plus the local
/// plugin/disabled/failed lists.
///
-/// **Lifetime:** every slice here is *borrowed* with one of three lifetimes — registry strings
+/// **Lifetime:** every slice here is *borrowed* with one of three lifetimes - registry strings
/// are valid only while the catalog lock is held (the worker frees the arena on `refresh`),
/// `plugin.display_name`/`plugin.id` live in dylib/static memory only while the plugin is
-/// loaded, and disabled ids are app-allocator-owned. The whole build → sort → draw pass runs
+/// loaded, and disabled ids are app-allocator-owned. The whole build -> sort -> draw pass runs
/// inside a single `catalog.acquire()`/`release()` scope and the dvui frame arena, so none of
/// these are retained past the lock release or across frames.
const StoreEntry = struct {
id: []const u8,
title: []const u8,
/// `on_disk` is the catch-all for a plugin directory that exists but is in none of the
- /// fizzy's in-memory states (not loaded, not disabled, no recorded failure) — see `disk_ids`.
+ /// fizzy's in-memory states (not loaded, not disabled, no recorded failure) - see `disk_ids`.
kind: enum { registry, local, disabled, failed, on_disk },
registry: ?store.SummaryEntry = null,
/// This host's release for `id`, if the fetched shard has one (already fingerprint-resolved
- /// server-side — see `store.ShardRelease`). Still needs an arch check; see `selectedRelease`.
+ /// server-side - see `store.ShardRelease`). Still needs an arch check; see `selectedRelease`.
release: ?store.ShardRelease = null,
plugin: ?*sdk.Plugin = null,
/// Fuzzy-match score against the current filter, assigned by `rankEntries`. Lower is better;
@@ -1942,7 +1942,7 @@ fn containsId(entries: []const StoreEntry, id: []const u8) bool {
return false;
}
-/// A→Z by display title (case-insensitive ASCII), tie-broken on id for stability.
+/// A->Z by display title (case-insensitive ASCII), tie-broken on id for stability.
fn entryLess(_: void, lhs: StoreEntry, rhs: StoreEntry) bool {
return switch (std.ascii.orderIgnoreCase(lhs.title, rhs.title)) {
.lt => true,
@@ -1955,7 +1955,7 @@ fn entryLess(_: void, lhs: StoreEntry, rhs: StoreEntry) bool {
///
/// Identity fields (id, title) and prose fields (description, author, tags) are scored separately
/// and the prose side is penalised, so a plugin *named* "theme" always outranks one that merely
-/// mentions themes in its description — otherwise a long description full of common words can
+/// mentions themes in its description - otherwise a long description full of common words can
/// score better than the name the user was actually typing.
fn scoreEntry(entry: StoreEntry, query: *const fuzzy.Query) ?f64 {
if (query.isEmpty()) return 0;
@@ -1973,7 +1973,7 @@ fn scoreEntry(entry: StoreEntry, query: *const fuzzy.Query) ?f64 {
var best = fuzzy.scoreBest(identity[0..n], query, .{ .plain = true });
// `descriptionFor`/`tagsFor` already fall back past the registry (built-in manifest, then a
- // dylib probe), so a plugin with no registry entry yet still scores on its own prose — only
+ // dylib probe), so a plugin with no registry entry yet still scores on its own prose - only
// `author` has no such fallback (it's registry-only, see `plans/PLUGIN_MANIFEST_PLAN.md`).
const author = if (entry.registry) |r| r.author else "";
var prose = fuzzy.scoreBest(&.{ descriptionFor(entry) orelse "", author }, query, .{ .plain = true });
@@ -1992,13 +1992,13 @@ fn scoreEntry(entry: StoreEntry, query: *const fuzzy.Query) ?f64 {
/// Added to description/author/tag hits so they rank below any name/id hit.
const prose_match_penalty: f64 = 2.0;
-/// Best-first, falling back to the A→Z order for equal scores.
+/// Best-first, falling back to the A->Z order for equal scores.
fn entryScoreLess(_: void, lhs: StoreEntry, rhs: StoreEntry) bool {
if (lhs.score != rhs.score) return lhs.score < rhs.score;
return entryLess({}, lhs, rhs);
}
-/// Drop non-matching entries and order what's left. With no query this is the plain A→Z list the
+/// Drop non-matching entries and order what's left. With no query this is the plain A->Z list the
/// store has always shown; with one, the best match is the first card.
fn rankEntries(entries: *std.ArrayList(StoreEntry), query: *const fuzzy.Query) void {
if (query.isEmpty()) {
@@ -2021,7 +2021,7 @@ fn rankEntries(entries: *std.ArrayList(StoreEntry), query: *const fuzzy.Query) v
fn draw(_: ?*anyopaque) anyerror!dvui.App.Result {
// Unlike the old flat list, the tab now fills the full explorer viewport height
// (`expand = .both`, not just `.horizontal`) so the section below the tab strip gets a
- // genuinely bounded height — it then scrolls its own overflow (see
+ // genuinely bounded height - it then scrolls its own overflow (see
// `drawStoreSection`/`drawInstalledSection`) rather than the whole tab growing forever and
// leaning on the explorer's own scrollArea (`files.zig`'s tree still does that; we don't).
var vbox = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .both });
@@ -2037,7 +2037,7 @@ fn draw(_: ?*anyopaque) anyerror!dvui.App.Result {
try drawHeader();
- // Filter row — the app's one filter control, the same as the file tree's.
+ // Filter row - the app's one filter control, the same as the file tree's.
const filter_text = core.widgets.filterRow(@src(), "Filter...", .{ .margin = .{ .y = 4 } });
var query = fuzzy.Query.init(filter_text);
@@ -2047,7 +2047,7 @@ fn draw(_: ?*anyopaque) anyerror!dvui.App.Result {
// Registry availability is a property of the *store* pane alone. The installed pane is built
// entirely from local state (loaded plugins, disabled ids, load failures, the plugins
- // directory) and needs no network at all, so it always draws immediately — offline, behind a
+ // directory) and needs no network at all, so it always draws immediately - offline, behind a
// captive portal, or while the very first fetch is still in flight. Installed cards simply
// render their local half until a snapshot arrives to enrich them (see `have_snapshot`).
have_snapshot = maybe_snapshot != null;
@@ -2055,7 +2055,7 @@ fn draw(_: ?*anyopaque) anyerror!dvui.App.Result {
const arena = dvui.currentWindow().arena();
// Store entries (STORE tab): one row per registry plugin, independent of local install
- // state — a pure "what does the store publish" list. See `drawStoreCard`.
+ // state - a pure "what does the store publish" list. See `drawStoreCard`.
var store_entries: std.ArrayList(StoreEntry) = .empty;
if (maybe_snapshot) |snap| {
for (snap.summary.plugins) |entry| {
@@ -2071,8 +2071,8 @@ fn draw(_: ?*anyopaque) anyerror!dvui.App.Result {
}
rankEntries(&store_entries, &query);
- // Installed entries (INSTALLED tab): everything genuinely present locally — loaded,
- // disabled-on-disk, sideloaded, or a failed/rejected build — enriched with a matching
+ // Installed entries (INSTALLED tab): everything genuinely present locally - loaded,
+ // disabled-on-disk, sideloaded, or a failed/rejected build - enriched with a matching
// registry row (for "store vX" / Update-availability) wherever the registry knows the id too.
var installed_entries: std.ArrayList(StoreEntry) = .empty;
for (app.host.plugins.items) |plugin| {
@@ -2091,7 +2091,7 @@ fn draw(_: ?*anyopaque) anyerror!dvui.App.Result {
}) catch {};
}
// Disabled plugins are unloaded (not in `host.plugins`) but remain on disk; reuse the name +
- // version we remembered while they were loaded so they keep their A→Z position and current
+ // version we remembered while they were loaded so they keep their A->Z position and current
// version across enable/disable.
for (app.disabledIds()) |id| {
if (!std.unicode.utf8ValidateSlice(id)) continue;
@@ -2120,7 +2120,7 @@ fn draw(_: ?*anyopaque) anyerror!dvui.App.Result {
}
// Anything else sitting in the plugins directory. Nothing in fizzy's memory describes
// these (a half-finished install, a sideloaded directory, a build from a previous session we
- // never tried to load), but they are on disk, so they get a card — and therefore always an
+ // never tried to load), but they are on disk, so they get a card - and therefore always an
// Uninstall, and a Reinstall when the store has a build for this host (see `drawCardControls`).
for (disk_ids.items) |id| {
if (isBundled(id)) continue;
@@ -2169,18 +2169,18 @@ fn drawPaneTabs(status: store.Status) void {
}
}
-/// True while the catalog holds a parsed snapshot — i.e. the registry half of every card is real
+/// True while the catalog holds a parsed snapshot - i.e. the registry half of every card is real
/// rather than merely unknown-so-far. Set once per `draw` (under the catalog lock) and read by the
/// card drawing helpers, which run inside that same pass.
///
/// The distinction matters because "the store has no build for this host" and "we have not been
/// able to ask the store" look identical from a null `entry.release`, and only the first is a
-/// genuine, user-actionable problem. Offline, the second is what's true — so the red
+/// genuine, user-actionable problem. Offline, the second is what's true - so the red
/// "No compatible build in store" text is suppressed rather than accusing every installed plugin
/// of being unpublished. See `drawCardControls`/`drawStoreCardControls`.
var have_snapshot = false;
-/// Centered "Fetching store…" spinner, drawn inside the *store* pane only while the very first
+/// Centered "Fetching store..." spinner, drawn inside the *store* pane only while the very first
/// catalog fetch is in flight. The installed pane above it is unaffected: it renders from local
/// state and never waits on the network (see `draw`).
fn drawFetchingPlaceholder() void {
@@ -2249,7 +2249,7 @@ fn drawUnreachablePlaceholder() void {
}
/// How many cards the entry animation staggers across before every later card shares the last
-/// card's delay — without a cap, a large store would spend seconds trickling cards in.
+/// card's delay - without a cap, a large store would spend seconds trickling cards in.
const card_stagger_cap: usize = 10;
/// Slide a card in from the left with a slight overshoot the first time its id is drawn,
@@ -2277,7 +2277,7 @@ fn cardAnimator(src: std.lang.SourceLocation, entry: StoreEntry, index: usize) *
/// `explorer/Explorer.zig` uses, so a narrow pane or an overly-wide card still scrolls with the
/// usual visual hint instead of clipping silently. Returns the shown count (unused).
///
-/// This pane — and only this pane — owns the registry's loading/offline states: the spinner while
+/// This pane - and only this pane - owns the registry's loading/offline states: the spinner while
/// the first fetch is in flight, the unreachable empty state when it fails, and a small inline
/// spinner beside the header while a refresh runs over already-shown cards.
fn drawStoreSection(entries: []const StoreEntry, filter_text: []const u8, status: store.Status) usize {
@@ -2290,7 +2290,7 @@ fn drawStoreSection(entries: []const StoreEntry, filter_text: []const u8, status
.vertical_bar = .auto_overlay,
}, .{ .expand = .both, .background = false });
- // `entries` is already filtered and ranked by `rankEntries` — best match first.
+ // `entries` is already filtered and ranked by `rankEntries` - best match first.
var shown: usize = 0;
for (entries) |entry| {
var anim = cardAnimator(@src(), entry, shown);
@@ -2300,7 +2300,7 @@ fn drawStoreSection(entries: []const StoreEntry, filter_text: []const u8, status
}
if (shown == 0) {
if (!have_snapshot) {
- // Nothing cached yet, so the empty list says nothing about the store's contents —
+ // Nothing cached yet, so the empty list says nothing about the store's contents -
// it's still loading, or we couldn't ask. `.idle` can only be a fetch about to start
// (see `draw`'s `first_draw_done`), so it reads as loading too.
switch (status) {
@@ -2327,7 +2327,7 @@ fn drawStoreSection(entries: []const StoreEntry, filter_text: []const u8, status
/// INSTALLED tab: everything genuinely present locally, grouped under "Local" (sideloaded dylibs)
/// and "Built-in" (bundled + static built-ins) headers. This is the *only* place enable/disable,
-/// update, uninstall, and failed-to-load detail show up — see `drawCard`. Returns the shown
+/// update, uninstall, and failed-to-load detail show up - see `drawCard`. Returns the shown
/// count (unused).
fn drawInstalledSection(entries: []const StoreEntry, filter_text: []const u8) usize {
var pane_box = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .both, .background = false });
@@ -2396,14 +2396,14 @@ fn isBuiltIn(id: []const u8) bool {
return isBundled(id);
}
-/// INSTALLED-tab card: full state — enabled checkbox, update/uninstall, failed-to-load detail —
+/// INSTALLED-tab card: full state - enabled checkbox, update/uninstall, failed-to-load detail -
/// via `drawCardControls`/`infoLine` (which still shows "installed vX"). See `drawCardShell`.
fn drawCard(entry: StoreEntry) void {
var buf: [192]u8 = undefined;
drawCardShell(entry, drawCardControls, infoLine(&buf, entry), .{ .show_failure = true, .selection_toggles = true, .compact = true });
}
-/// STORE-tab card: browse-only — just an install button or a "no compatible build" message via
+/// STORE-tab card: browse-only - just an install button or a "no compatible build" message via
/// `drawStoreCardControls`/`storeInfoLine` (never "installed vX": that's the INSTALLED tab's job,
/// even for a store plugin the user happens to already have installed). See `drawCardShell`.
fn drawStoreCard(entry: StoreEntry) void {
@@ -2413,7 +2413,7 @@ fn drawStoreCard(entry: StoreEntry) void {
/// Static floor under a card's (and so the whole explorer tab's) width: below this, the
/// scrollArea takes over horizontally rather than squeezing the card further. Deliberately a
-/// constant rather than content-derived (see `drawCardShell`) — roughly enough room for a short
+/// constant rather than content-derived (see `drawCardShell`) - roughly enough room for a short
/// title, a few words of description, and the install/update controls without either crowding
/// into the other. A fraction of the app width was the other option on the table, but that would
/// mean plumbing the window size down to every card just to move a number that has no reason to
@@ -2421,17 +2421,17 @@ fn drawStoreCard(entry: StoreEntry) void {
const card_min_w: f32 = 280;
/// Applied as `max_size_content.w` to every card text line (title/description/author/row2) below.
-/// This is *not* a render-width cap — `max_size_content` only clamps a widget's *reported* min
+/// This is *not* a render-width cap - `max_size_content` only clamps a widget's *reported* min
/// size (see `WidgetData.minSizeSetAndRefresh`), never the rect it actually gets handed to draw
/// into. With `.expand = .horizontal`, that rect still comes from the card's real (fluid) width;
/// `LabelWidget`'s default `ellipsize = true` then truncates the render to fit it. What this
-/// *does* do is stop a plain label's other behavior — reporting its full unwrapped text width as
-/// its min size — from ever reaching the card and inflating `card_min_w` into "however long the
+/// *does* do is stop a plain label's other behavior - reporting its full unwrapped text width as
+/// its min size - from ever reaching the card and inflating `card_min_w` into "however long the
/// longest description happens to be", which is exactly what the scrollArea must not do.
const card_text_no_floor: f32 = 1;
/// The one card text line that does *not* get `card_text_no_floor`: the title. A card whose name
-/// has been squeezed away is unusable — you can't tell which plugin the controls belong to — so
+/// has been squeezed away is unusable - you can't tell which plugin the controls belong to - so
/// the title reports up to this much width and the scrollArea grows a horizontal bar rather than
/// eating into it. Bounded by construction (unlike a description, whose length is unbounded and
/// author-controlled): a title longer than this still reports only this much and ellipsizes, so
@@ -2445,16 +2445,16 @@ const card_title_min_w: f32 = 96;
/// the info column's own margin does the job, so the sides go to zero.
const card_text_padding: dvui.Rect = .{ .x = 0, .y = 1, .w = 0, .h = 1 };
-/// Shared card shell: a clickable container (logo + info, and — for a card with
-/// `selection_toggles` — its controls in the panel beside it). Clicking anywhere
+/// Shared card shell: a clickable container (logo + info, and - for a card with
+/// `selection_toggles` - its controls in the panel beside it). Clicking anywhere
/// outside the controls selects the plugin (its README shows in the center). The controls consume
/// their own clicks so the card-level click never double-fires. `controls` draws the
/// bottom-right state controls (differs between the store and installed cards, see
/// `drawStoreCardControls`/`drawCardControls`); `row2_text` is the already-formatted dim
/// monospace id/version line (differs likewise, see `storeInfoLine`/`infoLine`); `show_failure`
/// gates the failed-to-load detail block, which only makes sense on an installed-pane card.
-/// The per-card switches. A struct rather than four positional bools — the list cards, the store
-/// cards and the update window each want a different combination, and `(…, true, false, true)`
+/// The per-card switches. A struct rather than four positional bools - the list cards, the store
+/// cards and the update window each want a different combination, and `(..., true, false, true)`
/// says nothing at the call site.
const CardOptions = struct {
/// Surface a rejected build's load-failure detail under the info line.
@@ -2467,7 +2467,7 @@ const CardOptions = struct {
///
/// The *store* pane keeps it: every card there is a registry row that has both, so reserving
/// the lines costs nothing and keeps a long browse list on an even rhythm. Installed and
- /// update cards don't — a sideloaded or locally built plugin often has neither — and there the
+ /// update cards don't - a sideloaded or locally built plugin often has neither - and there the
/// reservation is just a hole between the title and the rest of the card.
compact: bool = false,
};
@@ -2528,11 +2528,11 @@ fn drawCardShell(entry: StoreEntry, controls: *const fn (StoreEntry) void, row2_
}
// 2. State controls, bottom-right. `gravity_y = 1.0` on this wrapper (rather than the
- // shared `controls(entry)`'s own centered `gravity_y = 0.5` — that function is reused
+ // shared `controls(entry)`'s own centered `gravity_y = 0.5` - that function is reused
// as-is by the detail header, where centered is right) bottom-anchors it within the
// card row, reading as "underneath the text stack" without actually nesting inside the
// (vertical) info column, which would put it after `bw.processEvents()` below and lose
- // click priority — see the note there. Must still be laid out *before* the
+ // click priority - see the note there. Must still be laid out *before* the
// (expand-horizontal) info column: a `gravity_x = 1.0` child reserves its natural width
// from the right edge of whatever's left regardless of draw order, but an `expand`-ing
// sibling's bottom-up min size would otherwise eat 100% of what's left and starve this
@@ -2547,7 +2547,7 @@ fn drawCardShell(entry: StoreEntry, controls: *const fn (StoreEntry) void, row2_
});
defer ctl_anchor.deinit();
// A card with a flyout keeps its actions there, beside the toggles, so the body is
- // pure content — which is what lets a rejected build's failure message wrap across
+ // pure content - which is what lets a rejected build's failure message wrap across
// the whole card instead of squeezing into a column beside a row of buttons.
if (!opts.selection_toggles) controls(entry);
}
@@ -2557,12 +2557,12 @@ fn drawCardShell(entry: StoreEntry, controls: *const fn (StoreEntry) void, row2_
// captures press/release for its own text-selection (see its `processEvent`) regardless
// of whether anyone actually wants to select that text. If the info column ran first, it
// would silently steal every click landing on the failure text (the only remaining
- // `textLayout` below — everything else in the column is a plain label), so this has to
+ // `textLayout` below - everything else in the column is a plain label), so this has to
// run after the (already-first) controls but before the (still-to-come) text.
bw.processEvents();
- // 3. Info column: title (largest, bold) — description (single line, ellipsized) —
- // author — then the dim id/version line. Every line below is always drawn, even with an
+ // 3. Info column: title (largest, bold) - description (single line, ellipsized) -
+ // author - then the dim id/version line. Every line below is always drawn, even with an
// empty string, so every card reserves the same four lines of height regardless of which
// fields a given plugin actually has (see the "cards that are all the same size" ask).
{
@@ -2594,7 +2594,7 @@ fn drawCardShell(entry: StoreEntry, controls: *const fn (StoreEntry) void, row2_
}
}
- // Single line, ellipsized (LabelWidget's default) rather than wrapped — a tooltip
+ // Single line, ellipsized (LabelWidget's default) rather than wrapped - a tooltip
// picks up the full text only when it was actually truncated.
const description: []const u8 = descriptionFor(entry) orelse "";
if (!opts.compact or description.len > 0) {
@@ -2643,12 +2643,12 @@ fn drawCardShell(entry: StoreEntry, controls: *const fn (StoreEntry) void, row2_
});
// A local build that is on disk but rejected at load time (ABI/SDK mismatch, id
- // collision, etc.) — this is the *only* place that surfaces it (there is no more
+ // collision, etc.) - this is the *only* place that surfaces it (there is no more
// startup dialog), so it must carry every diagnostic detail we have. It wraps across
// the card's full width: nothing shares this row any more, since every control moved
// into the flyout beside the card. The zero-floor max (as on the labels above) keeps the wrapped
// text from reporting its unwrapped width as the card's min size. Store (upper-pane)
- // cards never show this — see `show_failure`.
+ // cards never show this - see `show_failure`.
if (opts.show_failure) if (failedInfo(entry.id)) |f| {
var fail_wrap = dvui.box(@src(), .{ .dir = .vertical }, .{
.expand = .horizontal,
@@ -2684,7 +2684,7 @@ fn drawCardShell(entry: StoreEntry, controls: *const fn (StoreEntry) void, row2_
}
// The info column's failure-detail text layout sets an ibeam cursor on hover (for its own
- // text selection) regardless of whether the card claimed its clicks above — restore the
+ // text selection) regardless of whether the card claimed its clicks above - restore the
// plain card cursor now that it's had its turn this frame ("last cursorSet call wins" for
// the frame).
if (bw.hover) dvui.cursorSet(.arrow);
@@ -2736,7 +2736,7 @@ fn flyoutClear() void {
/// controls in a row without squeezing the title and description into nothing. Putting them
/// beside the card the user has chosen keeps the resting card readable and costs one tap.
///
-/// Selection rather than hover, because a hover-only control does not exist on a touch screen —
+/// Selection rather than hover, because a hover-only control does not exist on a touch screen -
/// and the web build is the one most likely to be used from a phone. Selection is the same
/// gesture on both: a click or a tap.
fn drawSelectionToggles(
@@ -2757,7 +2757,7 @@ fn drawSelectionToggles(
const scale = dvui.windowNaturalScale();
// Anchor to the card's right edge *or* the visible edge of the list, whichever comes first.
// A card is wider than a narrow sidebar (`card_min_w` + horizontal scroll), so its own right
- // edge is often somewhere out under the center pane — anchoring there would leave the panel
+ // edge is often somewhere out under the center pane - anchoring there would leave the panel
// floating in the middle of the editor, detached from the row it belongs to. Clipping is also
// what the user sees: only the drawn part of the card can be clicked.
const clip = dvui.clipGet();
@@ -2770,8 +2770,8 @@ fn drawSelectionToggles(
};
// The same frosted panel the account flyout and every dialog use (`core.widgets.Popover`):
- // one definition of what a floating surface in fizzy looks like — the blur behind it, the
- // fill, the corners, the shadow — rather than this one hand-rolling a translucent box that
+ // one definition of what a floating surface in fizzy looks like - the blur behind it, the
+ // fill, the corners, the shadow - rather than this one hand-rolling a translucent box that
// only resembled them.
//
// Centred on the card's middle, and grown out of it.
@@ -2799,7 +2799,7 @@ fn drawSelectionToggles(
}
// The page, first: it is what the card is *about*, and the one row that is the same on every
- // card in both panes. It opens as a preview tab like any other document — keeping one is the
+ // card in both panes. It opens as a preview tab like any other document - keeping one is the
// tab's own menu, not a row here.
{
var r = core.widgets.Popover.row(@src(), .{});
@@ -2807,20 +2807,20 @@ fn drawSelectionToggles(
r.deinit();
if (r.clicked) {
// Grouping 0: the pane the user is in. The workbench's own default
- // (`open_workspace_grouping`), which is what "open this where I am" means — the store
+ // (`open_workspace_grouping`), which is what "open this where I am" means - the store
// has no business choosing a pane, and "open to the side" is the tab strip's job.
Page.open(app.host, entry.id, 0) catch |err|
reportError("could not open the page for '{s}': {s}", .{ entry.id, @errorName(err) });
}
}
drawToggleControls(entry, .compact);
- // Whichever controls this card's pane owns — the installed pane's full set, the store pane's
- // Install/Uninstall — so both panes' cards are the same object: content in the body, every
+ // Whichever controls this card's pane owns - the installed pane's full set, the store pane's
+ // Install/Uninstall - so both panes' cards are the same object: content in the body, every
// control in the panel beside the selected one.
//
// Wrapped in a normally-packed row of its own: those functions' boxes set `gravity_y = 0.5`
// (right for the detail header, where it centres against the header), and a gravity-in-(0,1)
- // *direct* child of a vertical box is treated as positioned/overlay by `BoxWidget.rectFor` —
+ // *direct* child of a vertical box is treated as positioned/overlay by `BoxWidget.rectFor` -
// it would be drawn on top of the toggles above instead of under them.
{
var actions = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .horizontal, .margin = .{ .y = 2 } });
@@ -2851,7 +2851,7 @@ fn drawToggleControls(entry: StoreEntry, size: ToggleSize) void {
const failed = entry.kind == .failed or app.isFailed(entry.id);
const untracked = !loaded and !disabled and !failed and (entry.kind == .on_disk or isOnDisk(entry.id));
- // A *failed* build is the one case with nothing to toggle — it already tried and lost, and
+ // A *failed* build is the one case with nothing to toggle - it already tried and lost, and
// gets the Retry/Reinstall buttons on the card instead.
// Each control gets its own normally-packed row: these are drawn into *vertical* parents (the
// card flyout, the detail header's toggle column), where a `gravity_y = 0.5` direct child is
@@ -2874,7 +2874,7 @@ fn drawToggleControls(entry: StoreEntry, size: ToggleSize) void {
}
}
-/// Detail-page header controls: the same actions a card carries, plus the two toggles inline —
+/// Detail-page header controls: the same actions a card carries, plus the two toggles inline -
/// the header has the width for them, and there is no card to hover there.
fn drawDetailControls(entry: StoreEntry) void {
var row = dvui.box(@src(), .{ .dir = .horizontal }, .{ .gravity_x = 1.0, .gravity_y = 0.5 });
@@ -2887,7 +2887,7 @@ fn drawDetailControls(entry: StoreEntry) void {
drawCardControls(entry);
}
-/// True when `entry` is genuinely present on disk in some form — loaded, disabled-on-disk,
+/// True when `entry` is genuinely present on disk in some form - loaded, disabled-on-disk,
/// sideloaded local, a failed/rejected build, or a protected bundled built-in. Mirrors the
/// "present on disk" condition `drawCardControls` uses to decide whether to show
/// enable/uninstall controls at all; `infoLine` reuses it so a plugin that was *never*
@@ -2910,12 +2910,12 @@ fn releaseDate(entry: StoreEntry) ?[]const u8 {
return if (rel.published.len > 0) rel.published else null;
}
-/// Compose the dim `id · store v{latest} · installed v{current}` line into `buf`, skipping
+/// Compose the dim `id . store v{latest} . installed v{current}` line into `buf`, skipping
/// parts we don't have:
-/// * `store v{latest}` — the highest version published in the registry, shown for *any*
+/// * `store v{latest}` - the highest version published in the registry, shown for *any*
/// plugin with a store presence, independent of whether it is installed or host-compatible.
-/// * `installed v{current}` — the plugin's own current version, shown only when `entry` is
-/// genuinely installed (see `isInstalled`) — never for a plugin the user hasn't installed.
+/// * `installed v{current}` - the plugin's own current version, shown only when `entry` is
+/// genuinely installed (see `isInstalled`) - never for a plugin the user hasn't installed.
fn infoLine(buf: []u8, entry: StoreEntry) []const u8 {
var latest_buf: [40]u8 = undefined;
const latest: ?[]const u8 = if (latestRegistryVersion(entry)) |lv|
@@ -2945,7 +2945,7 @@ fn infoLine(buf: []u8, entry: StoreEntry) []const u8 {
n += 1;
}
// A build that is present but has never been run reads as installed-and-working otherwise;
- // say so, so the Load button beside it has a reason. Only for the never-decided case — a
+ // say so, so the Load button beside it has a reason. Only for the never-decided case - a
// plugin the user switched off deliberately doesn't need telling.
if (!isBundled(entry.id) and app.host.pluginById(entry.id) == null and
app.isUndecided(entry.id))
@@ -2956,8 +2956,8 @@ fn infoLine(buf: []u8, entry: StoreEntry) []const u8 {
return joinParts(buf, parts[0..n]);
}
-/// Compose the dim `id · store v{latest}` line for a store (upper-pane) card into `buf`. Unlike
-/// `infoLine`, this never shows install state — the STORE tab is a pure "what does the store
+/// Compose the dim `id . store v{latest}` line for a store (upper-pane) card into `buf`. Unlike
+/// `infoLine`, this never shows install state - the STORE tab is a pure "what does the store
/// publish" list, even for a plugin the user happens to already have installed (see `drawCard`).
fn storeInfoLine(buf: []u8, entry: StoreEntry) []const u8 {
var latest_buf: [40]u8 = undefined;
@@ -2986,8 +2986,8 @@ const store_optimize_class = "fast";
/// False when this Fizzy is a `Debug`/`ReleaseSafe` build. Such a host folds the `"safe"`
/// optimize class into its `abi_fingerprint` (see `dylib.optimize_safety_class`), so it fetches a
-/// shard URL the store never publishes under, and *every* plugin — including ones whose SDK
-/// version matches this host exactly — reads "No compatible build in store". That message points
+/// shard URL the store never publishes under, and *every* plugin - including ones whose SDK
+/// version matches this host exactly - reads "No compatible build in store". That message points
/// at the store, but the cause is entirely local and comptime-known, so say so instead. Same
/// condition the local load path reports as `error.AbiOptimizeMismatch`.
const host_optimize_matches_store = std.mem.eql(u8, dylib.optimize_safety_class, store_optimize_class);
@@ -2996,12 +2996,12 @@ const host_optimize_matches_store = std.mem.eql(u8, dylib.optimize_safety_class,
/// `card_text_no_floor`, just with a usable floor instead of ~0). The message sits in the controls
/// column, which is *not* expand-horizontal: whatever it reports, it takes out of the info column
/// beside it. Left uncapped, a long message plus the icon reserved so much of a narrowed card that
-/// the title/description — all of which report ~0 and yield — collapsed to nothing while the error
+/// the title/description - all of which report ~0 and yield - collapsed to nothing while the error
/// text alone stayed fully drawn. Capped, it ellipsizes (its tooltip carries the full text either
/// way) and the title keeps its own floor below.
const no_build_msg_max_w: f32 = 110;
-/// The "nothing here to install" message, shown wherever no host-compatible release resolved —
+/// The "nothing here to install" message, shown wherever no host-compatible release resolved -
/// identical in both panes (store card, installed card, detail header) so a card never changes
/// width just by which list it's in. Out-of-class hosts (see `host_optimize_matches_store`) get
/// the optimize-mode wording plus the same alert icon a failed local load carries; the tooltip
@@ -3036,10 +3036,10 @@ fn drawNoStoreBuild(entry: StoreEntry, opts: dvui.Options) void {
// Web included: it once had no plugin binaries at all, which made the "Needs release"
// wording a lie there, but a web build installs wasm side modules now and a `ReleaseSafe`
// one is out of class exactly like a native `ReleaseSafe` one. Exempting wasm only hid the
- // reason — a ReleaseSafe web app reported "No store build" against every plugin in a healthy
+ // reason - a ReleaseSafe web app reported "No store build" against every plugin in a healthy
// registry, which reads as the store being empty rather than as this build being unservable.
const optimize_mismatch = !host_optimize_matches_store;
- // The store *does* have this host's build — it just needs a newer Fizzy. Nothing about the
+ // The store *does* have this host's build - it just needs a newer Fizzy. Nothing about the
// plugin is wrong, so point at the app update instead of at a missing build.
const needs_newer_fizzy = !optimize_mismatch and releaseNeedsNewerFizzy(entry);
@@ -3114,7 +3114,7 @@ fn drawNoStoreBuild(entry: StoreEntry, opts: dvui.Options) void {
}
}
-/// Join `parts` with " · ", truncating (rather than overflowing) if `buf` is too small.
+/// Join `parts` with " . ", truncating (rather than overflowing) if `buf` is too small.
fn joinParts(buf: []u8, parts: []const []const u8) []const u8 {
var len: usize = 0;
for (parts, 0..) |p, i| {
@@ -3131,7 +3131,7 @@ fn joinParts(buf: []u8, parts: []const []const u8) []const u8 {
return buf[0..len];
}
-/// True when this host's SDK satisfies `rel`'s `min_sdk_version` — i.e. `PluginLoader` will
+/// True when this host's SDK satisfies `rel`'s `min_sdk_version` - i.e. `PluginLoader` will
/// actually accept the build rather than rejecting it with `error.SdkVersionMismatch`.
///
/// Being in this host's fingerprint shard is not sufficient. `sdk_version` bumps for every
@@ -3139,7 +3139,7 @@ fn joinParts(buf: []u8, parts: []const []const u8) []const u8 {
/// the SDK types, so an SDK release that adds a host function (or tightens a behavior contract)
/// leaves the fingerprint alone. A plugin rebuilt against that SDK therefore lands in the exact
/// shard an older Fizzy fetches, looks like a plain version bump, and fails to load once
-/// downloaded. `min_sdk_version` is the field that carries the difference — this is where it is
+/// downloaded. `min_sdk_version` is the field that carries the difference - this is where it is
/// checked, so an out-of-date Fizzy is never offered a build it cannot run.
fn releaseSdkSatisfied(rel: store.ShardRelease) bool {
const min = rel.minSdk() orelse return true;
@@ -3165,7 +3165,7 @@ fn releaseNeedsNewerFizzy(entry: StoreEntry) bool {
return !releaseSdkSatisfied(r);
}
-/// The compatible registry release when it is a *newer* version than the one currently loaded —
+/// The compatible registry release when it is a *newer* version than the one currently loaded -
/// i.e. an update is available. Returns null when the plugin isn't loaded (we only know a live
/// plugin's version), has no host-compatible release, or is already up to date.
fn updateRelease(entry: StoreEntry) ?store.ShardRelease {
@@ -3176,10 +3176,10 @@ fn updateRelease(entry: StoreEntry) ?store.ShardRelease {
}
/// Right-justified controls whose shape depends on install state (see plan Phase 1R-c):
-/// * available in store → a single install button (down-to-line arrow);
-/// * installed → an Enabled checkbox + an Uninstall button;
-/// * protected bundled fallback (workbench/text/markdown) → no controls;
-/// * bundled built-in → not store-manageable (no uninstall).
+/// * available in store -> a single install button (down-to-line arrow);
+/// * installed -> an Enabled checkbox + an Uninstall button;
+/// * protected bundled fallback (workbench/text/markdown) -> no controls;
+/// * bundled built-in -> not store-manageable (no uninstall).
fn drawCardControls(entry: StoreEntry) void {
const theme = dvui.themeGet();
const muted = theme.color(.window, .fill).lerp(theme.color(.window, .text), 0.7);
@@ -3217,21 +3217,21 @@ fn drawCardControls(entry: StoreEntry) void {
const loaded = app.host.pluginById(entry.id) != null;
const disabled = app.isDisabled(entry.id);
const failed = entry.kind == .failed or app.isFailed(entry.id);
- // On disk, never loaded, and nothing in memory describes it — a build dropped into
+ // On disk, never loaded, and nothing in memory describes it - a build dropped into
// `plugins//` that fizzy has not classified yet (no `.plugins..enabled` on record, no
// load attempt, no failure). `Editor.reconcileDiscoveredPlugins` normally promotes these to
// `.disabled` (which carries the Enabled checkbox), but it runs off the watcher, so a card can
- // still be drawn in this state — it must offer a way *in*, not just Uninstall.
+ // still be drawn in this state - it must offer a way *in*, not just Uninstall.
const untracked = !loaded and !disabled and !failed and (entry.kind == .on_disk or isOnDisk(entry.id));
// A build sitting in the plugins dir that isn't running: it failed to load (ABI/SDK mismatch,
// etc.), or it is simply there with nothing in memory describing it. Either way it is on disk
// like any installed plugin, so it must stay actionable (reinstall / uninstall) rather than
- // dead-ending at a bare "Failed" label — or, worse, at no card at all.
+ // dead-ending at a bare "Failed" label - or, worse, at no card at all.
const broken = failed or untracked;
// On disk, never run, and nobody has decided about it: a local `zig build install` or a
// hand-dropped build. `untracked` is the same situation a frame or two earlier, before the
// watcher (or Refresh) classified it. This is an *offer*, not a fault, so it gets one primary
- // "Load" button instead of the broken-build repair controls — and loading it goes through the
+ // "Load" button instead of the broken-build repair controls - and loading it goes through the
// same first-load path a store install does, file-association prompt included
// (`Editor.setPluginEnabled`).
const undecided = !loaded and !failed and (app.isUndecided(entry.id) or untracked);
@@ -3240,16 +3240,16 @@ fn drawCardControls(entry: StoreEntry) void {
if (loaded or disabled or entry.kind == .local or entry.kind == .disabled or broken) {
// Enabled / Auto-update are not here: on a list card they live in the flyout at the
// card's right edge (`drawSelectionToggles`), and on the detail page they sit beside these
- // buttons (`drawDetailControls`). Only actions — update, repair, uninstall — are inline
+ // buttons (`drawDetailControls`). Only actions - update, repair, uninstall - are inline
// on a card, so a row of four controls can't crowd the text out of a narrow sidebar.
// Replace with a host-compatible registry build, just before uninstall:
- // * loaded & strictly newer → in-place Update (unload + reload);
- // * broken/not loaded → Reinstall: download this host's build fresh over the one
+ // * loaded & strictly newer -> in-place Update (unload + reload);
+ // * broken/not loaded -> Reinstall: download this host's build fresh over the one
// on disk and load it. Nothing is loaded to unload, so route through the install path
- // (is_update = false → `installAndLoadPlugin`, which re-enables the plugin, loads the
+ // (is_update = false -> `installAndLoadPlugin`, which re-enables the plugin, loads the
// freshly downloaded file and clears the failure record). This is the way out of a
// wrong-SDK build: `selectedRelease` only ever hands back a release this Fizzy's ABI
- // fingerprint, os-arch and SDK version can all load — even when it carries the same
+ // fingerprint, os-arch and SDK version can all load - even when it carries the same
// version number as the broken one already on disk.
if (loaded) {
if (comptime builtin.target.cpu.arch == .wasm32) {
@@ -3264,7 +3264,7 @@ fn drawCardControls(entry: StoreEntry) void {
startDownload(entry.id, rel, .{ .is_update = true });
}
// The store lists a newer version, but not one this Fizzy can take: say why, rather
- // than show "store v0.1.32 · installed v0.1.31" and no Update button with no reason.
+ // than show "store v0.1.32 . installed v0.1.31" and no Update button with no reason.
if (updateRelease(entry) == null) drawUpdateBlocked(entry);
} else if (undecided) {
if (dvui.button(@src(), "Load", .{}, .{ .gravity_y = 0.5, .margin = .{ .x = 4 } }))
@@ -3282,13 +3282,13 @@ fn drawCardControls(entry: StoreEntry) void {
startDownload(entry.id, rel, .{ .is_update = false });
} else if (!have_snapshot or untracked) {
// No catalog yet (offline, or the first fetch is still running), so we genuinely
- // don't know whether a build exists — say nothing rather than claim there is none.
+ // don't know whether a build exists - say nothing rather than claim there is none.
// Uninstall below still works; the card gains a Reinstall once a snapshot lands.
// An untracked build stays quiet too: its Enabled checkbox is the action here, and
// "no store build" is irrelevant noise for something never installed from the store.
} else {
// No build for this host in the fetched shard, so there is nothing to reinstall
- // *from*. Say why (short form — this card also carries the wrapped failure text,
+ // *from*. Say why (short form - this card also carries the wrapped failure text,
// and the controls row shares its width with it) instead of leaving a lone trash
// icon next to an unexplained "Failed to load".
drawNoStoreBuild(entry, .{ .margin = .{ .x = 4 } });
@@ -3312,12 +3312,12 @@ fn drawCardControls(entry: StoreEntry) void {
return;
}
- // Same "we couldn't ask" caveat as the broken branch above — an unfetched catalog is not
+ // Same "we couldn't ask" caveat as the broken branch above - an unfetched catalog is not
// evidence of a missing build.
if (!have_snapshot) return;
// Registry row with no host-compatible release and nothing on disk: the *store* hasn't
- // published a build for this exact Fizzy version/arch yet — nothing the user can fix
+ // published a build for this exact Fizzy version/arch yet - nothing the user can fix
// locally (unlike a failed local build, handled above), so the wording and the tooltip
// both point at "the store doesn't have one" rather than "rebuild your plugin".
drawNoStoreBuild(entry, .{});
@@ -3325,7 +3325,7 @@ fn drawCardControls(entry: StoreEntry) void {
/// Upper-pane (store) card controls: an in-flight job status, Install, Uninstall for something
/// already here, or a "no compatible build" message. Enable/disable and update stay the installed
-/// pane's job (`drawCardControls`) — those are settings for a plugin you have, and this pane is
+/// pane's job (`drawCardControls`) - those are settings for a plugin you have, and this pane is
/// for deciding whether to have it.
///
/// Never Install beside something already installed: that would re-fetch a build the app is
@@ -3353,8 +3353,8 @@ fn drawStoreCardControls(entry: StoreEntry) void {
.downloaded => {}, // about to complete in tick(); fall through
};
- // The web has no download queue — its install is the page fetching and linking a module
- // (`queueInstall` → `installFromUrl`) — so the in-flight state lives in `web_in_flight`
+ // The web has no download queue - its install is the page fetching and linking a module
+ // (`queueInstall` -> `installFromUrl`) - so the in-flight state lives in `web_in_flight`
// rather than in `jobs`, and asking `isInstalling` is what covers both. Without this the
// button simply sat there during a web install, which reads as a button that does nothing.
if (isInstalling(entry.id)) {
@@ -3377,7 +3377,7 @@ fn drawStoreCardControls(entry: StoreEntry) void {
if (selectedRelease(entry) != null) {
// A word, not an arrow: this is the one control on the card that commits to something,
// and "Install" is the only label it could carry. (The icon it replaced was a
- // down-to-line arrow, which read as "download" — a file landing somewhere — when what
+ // down-to-line arrow, which read as "download" - a file landing somewhere - when what
// happens is a plugin arriving in the app.) `queueInstall` rather than `startDownload`
// so the web takes its own path, which has no download queue at all.
if (dvui.button(@src(), "Install", .{}, .{ .gravity_y = 0.5 }))
@@ -3411,7 +3411,7 @@ const RepoSource = struct {
subpath: []const u8 = "",
};
-/// The fizzy monorepo — source of truth for the bundled built-ins (workbench, text, markdown), whose
+/// The fizzy monorepo - source of truth for the bundled built-ins (workbench, text, markdown), whose
/// `README.md` / `ICON.png` live at `plugins//` rather than at a repo root.
const fizzy_repo_url = "https://github.com/fizzyedit/fizzy";
@@ -3436,7 +3436,7 @@ fn readmeSource(entry: StoreEntry) ?RepoSource {
}
/// Where a bundled built-in's source sits in the fizzy repo: `plugins/`, which is the shape
-/// every one of them has (`plugins/text/`, `plugins/archive/`, …).
+/// every one of them has (`plugins/text/`, `plugins/archive/`, ...).
///
/// Derived from the id, never a list: `isBundled` asks the build, so any plugin it says yes to
/// must resolve here too.
@@ -3466,7 +3466,7 @@ fn drawHeader() !void {
.{ .gravity_x = 1.0, .corners = .all(1000000) },
)) {
if (catalog) |*c| c.refresh();
- // Classify anything that appeared in the plugins dir since the last pass — same call the
+ // Classify anything that appeared in the plugins dir since the last pass - same call the
// file watcher makes, so Refresh is a manual stand-in for it on a platform (or a mount)
// where the watcher never fired. It deliberately only *offers* a discovered build (it
// becomes an undecided "Load" card); Refresh never loads or executes a plugin.
@@ -3517,7 +3517,7 @@ pub fn queueSetEnabled(id: []const u8, enabled: bool) void {
}
/// Queue an auto-update opt in/out for `id`. Deliberately does **not** clear other queued actions
-/// for the same id (see `removePendingForId`) — it changes nothing about the plugin's load state.
+/// for the same id (see `removePendingForId`) - it changes nothing about the plugin's load state.
fn queueSetAutoUpdate(id: []const u8, on: bool) void {
const dup = app.gpa.dupe(u8, id) catch {
reportError("'{s}' could not be queued", .{id});
diff --git a/app/store/plugin_repo_asset.zig b/app/store/plugin_repo_asset.zig
index 69cbd5cbe..6ea961f44 100644
--- a/app/store/plugin_repo_asset.zig
+++ b/app/store/plugin_repo_asset.zig
@@ -1,4 +1,4 @@
-//! Shared helpers for fetching static assets (README.md, ICON.png, …) from a plugin's
+//! Shared helpers for fetching static assets (README.md, ICON.png, ...) from a plugin's
//! GitHub repository over HTTPS, with a dev-tree fallback that walks up from the executable.
const std = @import("std");
const PluginManager = @import("PluginManager.zig");
@@ -46,7 +46,7 @@ pub const LocalAsset = struct {
};
/// Walk up from the executable directory looking for `{subpath}/{filename}` on disk. Covers dev
-/// trees (`zig-out/bin` → repo root) and sideloaded checkouts; packaged installs fall through to
+/// trees (`zig-out/bin` -> repo root) and sideloaded checkouts; packaged installs fall through to
/// the GitHub fetch below.
pub fn readLocalAsset(io: std.Io, subpath: []const u8, filename: []const u8, limit: std.Io.Limit) ?LocalAsset {
if (comptime @import("builtin").target.cpu.arch == .wasm32) return null;
@@ -90,7 +90,7 @@ pub const UrlList = struct {
/// Derive candidate raw asset URLs from a GitHub repository link, optionally scoped to
/// `subpath` within that repo. Returns null for hosts we can't map. `url_storage` must be
-/// owned by the caller (e.g. a stack buffer local to the calling worker thread) — README and
+/// owned by the caller (e.g. a stack buffer local to the calling worker thread) - README and
/// icon workers run concurrently on separate threads, so a shared/global buffer here would race.
pub fn rawGithubUrls(url_storage: *[3][256]u8, repo: []const u8, subpath: []const u8, filename: []const u8) ?UrlList {
var r = repo;
diff --git a/app/store/readme.zig b/app/store/readme.zig
index 449fbd3bf..555e2ac20 100644
--- a/app/store/readme.zig
+++ b/app/store/readme.zig
@@ -28,14 +28,14 @@ pub const Readme = struct {
subpath: []u8,
io: std.Io,
/// Captured at select time so the worker can wake the blocked event loop when the fetch
- /// finishes (otherwise the UI stays on "Loading…" until an unrelated input event).
+ /// finishes (otherwise the UI stays on "Loading..." until an unrelated input event).
window: *dvui.Window,
status: std.atomic.Value(u8) = .init(@backingInt(Status.idle)),
/// Fetched README bytes (app-allocator owned). Written once by the worker before it flips
/// `status` to `ready` with release ordering; read on the UI thread only after an acquire
/// load sees `ready`, so no lock is needed for the bytes themselves.
bytes: ?[]u8 = null,
- /// Where the README was found — the dev-tree directory, or the raw URL it was fetched from.
+ /// Where the README was found - the dev-tree directory, or the raw URL it was fetched from.
/// Written by the worker alongside `bytes`; the preview resolves the README's own relative
/// images (``) against it, which for the fetched case means pulling
/// them from the same repo over HTTPS.
@@ -52,7 +52,7 @@ pub const Readme = struct {
}
/// Start fetching `id`'s README (from its `repo` URL, optionally scoped to `subpath` within
- /// that repo). Null when the strings could not be copied — the caller has no README to draw.
+ /// that repo). Null when the strings could not be copied - the caller has no README to draw.
pub fn init(id: []const u8, repo: []const u8, subpath: []const u8) ?Readme {
const gpa = repo_asset.gpa();
const id_owned = gpa.dupe(u8, id) catch return null;
@@ -77,7 +77,7 @@ pub const Readme = struct {
}
/// Kick off the fetch. Separate from `init` because the worker takes a pointer to the value,
- /// which has to be at its final address first — a `Readme` returned by value and then moved
+ /// which has to be at its final address first - a `Readme` returned by value and then moved
/// into a document would hand the thread a dangling one.
pub fn start(self: *Readme) void {
if (comptime is_wasm) {
@@ -145,7 +145,7 @@ pub const Readme = struct {
.failed => placeholder("Could not fetch the README.", true),
.ready => {
const bytes = self.bytes orelse return;
- // Transparent — the page behind this draws its own background (matching every
+ // Transparent - the page behind this draws its own background (matching every
// other pane in the app); without this the preview's own scroll area painted a
// visibly different `.content`-styled fill on top of it.
markdown.drawPreview(&self.preview, bytes, repo_asset.gpa(), .{
@@ -220,7 +220,7 @@ fn worker(self: *Readme) void {
for (candidates.slice()) |url| {
if (repo_asset.fetchOk(self.io, url, limit)) |body| {
self.bytes = body;
- // `url` lives in the worker's stack buffer — the preview needs it every frame.
+ // `url` lives in the worker's stack buffer - the preview needs it every frame.
self.image_base = repo_asset.gpa().dupe(u8, url) catch null;
self.status.store(@backingInt(Status.ready), .release);
return;
diff --git a/app/store/registry/compat.zig b/app/store/registry/compat.zig
index 575018a35..ff604fd73 100644
--- a/app/store/registry/compat.zig
+++ b/app/store/registry/compat.zig
@@ -1,7 +1,7 @@
//! Host identity for the plugin store: the `os-arch` key used both as a `downloads` map key and
//! as a path segment when building a plugin's download URL.
//!
-//! Fingerprint compatibility is no longer matched here — a `registry.ReleaseShard` is already
+//! Fingerprint compatibility is no longer matched here - a `registry.ReleaseShard` is already
//! scoped to exactly one `abi_fingerprint` (see `store.Catalog`, which fetches the shard for the
//! running host's own `dylib.abi_fingerprint`), so the only thing left to check client-side is
//! whether that shard's release has a binary for this `os-arch` (see
diff --git a/app/store/registry/registry.zig b/app/store/registry/registry.zig
index 420121e1c..4c81e0895 100644
--- a/app/store/registry/registry.zig
+++ b/app/store/registry/registry.zig
@@ -2,17 +2,17 @@
//! `catalog//releases.json` documents, plus a fetch + parse path for each.
//!
//! The catalog is split into two tiers to keep per-session bandwidth bounded regardless of how
-//! much release history a plugin has accumulated (see PLAN.md § store export):
-//! * `summary.json` — every registered plugin's browse-list metadata (id/name/description/
+//! much release history a plugin has accumulated (see PLAN.md section store export):
+//! * `summary.json` - every registered plugin's browse-list metadata (id/name/description/
//! author/homepage/tags/date_added/latest_version), independent of this host's Fizzy build.
-//! * `/releases.json` — scoped to exactly one ABI fingerprint, so it holds at
+//! * `/releases.json` - scoped to exactly one ABI fingerprint, so it holds at
//! most one release per plugin (the newest the store has for that fingerprint) instead of
//! every version ever published. The client only ever needs its own host fingerprint's shard.
//!
//! Both are aggregated server-side from each author's manifest (see PLAN.md) and served
//! read-only over HTTPS; this module never writes either.
//!
-//! Pure of dvui/globals — callers pass `allocator` and a `std.Io`. The parse half is
+//! Pure of dvui/globals - callers pass `allocator` and a `std.Io`. The parse half is
//! unit-tested; the network half (`fetchSummary`/`fetchReleaseShard`) is exercised by the
//! Chunk 5/7 E2E.
const std = @import("std");
@@ -25,14 +25,14 @@ pub const Download = struct {
};
/// One plugin's browse-list metadata, from `catalog/summary.json`. Carries no per-release
-/// download info — that's what `ReleaseShard` is for — but does carry `latest_version`, the
+/// download info - that's what `ReleaseShard` is for - but does carry `latest_version`, the
/// highest version the author has *ever* published across every fingerprint, so the store can
/// still show "store v{latest}" for a plugin even when this host has no compatible build yet.
pub const SummaryEntry = struct {
id: []const u8 = "",
name: []const u8 = "",
description: []const u8 = "",
- /// Cosmetic, self-asserted display credit — the author's own claim about themselves, which
+ /// Cosmetic, self-asserted display credit - the author's own claim about themselves, which
/// the aggregator will fall back to reading out of their `manifest.json`. Never a trust
/// signal; pair it with `publisher` when showing attribution.
author: []const u8 = "",
@@ -63,16 +63,16 @@ pub const Summary = struct {
}
};
-/// One plugin's release for the *one* `abi_fingerprint` a `ReleaseShard` is scoped to — the
+/// One plugin's release for the *one* `abi_fingerprint` a `ReleaseShard` is scoped to - the
/// newest the store has for that fingerprint. Unlike the old single-file index, there is no
/// per-release `abi_fingerprint` field here: it's implied by which shard this came from, and no
-/// client-side "pick the newest compatible" step is needed — the shard is already resolved.
+/// client-side "pick the newest compatible" step is needed - the shard is already resolved.
pub const ShardRelease = struct {
version: []const u8 = "",
min_sdk_version: []const u8 = "",
fizzy_sdk_version: []const u8 = "",
published: []const u8 = "",
- /// `os-arch` → binary. Dynamic JSON object, so parsed via `std.json.ArrayHashMap`.
+ /// `os-arch` -> binary. Dynamic JSON object, so parsed via `std.json.ArrayHashMap`.
downloads: std.json.ArrayHashMap(Download) = .{},
/// The binary for `os_arch` (e.g. `compat.hostKey()`), or null when this release has none.
@@ -89,7 +89,7 @@ pub const ShardRelease = struct {
/// `min_sdk_version` check. Callers must gate on this before offering an install or update
/// (see `PluginStore.releaseSdkSatisfied`).
///
- /// An unparseable value reads as "no requirement" — the load-time check is still the
+ /// An unparseable value reads as "no requirement" - the load-time check is still the
/// backstop, and a malformed field must not silently hide every build of a plugin.
pub fn minSdk(self: ShardRelease) ?std.SemanticVersion {
if (self.min_sdk_version.len == 0) return null;
@@ -101,13 +101,13 @@ pub const ReleaseShard = struct {
schema: u32 = 0,
generated: []const u8 = "",
abi_fingerprint: []const u8 = "",
- /// Plugin id → its one release for this fingerprint. Dynamic JSON object.
+ /// Plugin id -> its one release for this fingerprint. Dynamic JSON object.
releases: std.json.ArrayHashMap(ShardRelease) = .{},
- /// A shard with no releases at all — used when this host's fingerprint has no shard file
+ /// A shard with no releases at all - used when this host's fingerprint has no shard file
/// yet (a brand-new Fizzy SDK generation nobody has republished for) or the shard fetch
/// failed; every plugin then correctly shows "no compatible build in store" rather than the
- /// whole catalog failing (only `summary.json` failing to load is a hard catalog failure —
+ /// whole catalog failing (only `summary.json` failing to load is a hard catalog failure -
/// see `store.Catalog`).
pub const empty: ReleaseShard = .{};
@@ -134,9 +134,9 @@ pub fn parseReleaseShard(allocator: std.mem.Allocator, bytes: []const u8) !std.j
}
/// HTTPS GET + parse a catalog document. The catalog is served from a CDN, so a plain GET can
-/// return a stale cached copy — a plugin published minutes ago wouldn't show until the edge
+/// return a stale cached copy - a plugin published minutes ago wouldn't show until the edge
/// cache expired. To make Refresh always reflect the latest publish, we defeat caching two ways:
-/// a unique cache-busting query param (distinct URL → guaranteed edge miss) plus `no-cache`
+/// a unique cache-busting query param (distinct URL -> guaranteed edge miss) plus `no-cache`
/// request headers.
fn fetchJson(comptime T: type, allocator: std.mem.Allocator, io: std.Io, url: []const u8) !std.json.Parsed(T) {
var client: std.http.Client = .{ .allocator = allocator, .io = io };
diff --git a/app/store/registry/store.zig b/app/store/registry/store.zig
index 2aa9276bf..7f8bbada7 100644
--- a/app/store/registry/store.zig
+++ b/app/store/registry/store.zig
@@ -1,5 +1,5 @@
//! Plugin-store backend: catalog fetch (summary + this host's release shard) + verified download,
-//! plus a `Catalog` that owns the latest parsed documents. Pure of dvui/globals — the caller
+//! plus a `Catalog` that owns the latest parsed documents. Pure of dvui/globals - the caller
//! supplies `allocator`, a `std.Io`, and the host's own `abi_fingerprint` as a hex string (the
//! caller already computes this from `sdk.dylib.abi_fingerprint`; this layer stays free of
//! SDK-specific concerns). The store UI drives `Catalog` and tracks per-plugin install state
@@ -19,7 +19,7 @@ pub const SummaryEntry = registry.SummaryEntry;
pub const ReleaseShard = registry.ReleaseShard;
pub const ShardRelease = registry.ShardRelease;
-/// Lifecycle of the catalog fetch (not a per-plugin install state — that lives in the UI).
+/// Lifecycle of the catalog fetch (not a per-plugin install state - that lives in the UI).
pub const Status = enum(u8) { idle, fetching, ready, failed };
const ParsedSummary = std.json.Parsed(registry.Summary);
@@ -28,7 +28,7 @@ const ParsedShard = std.json.Parsed(registry.ReleaseShard);
/// Owns the latest parsed `summary.json` + this host's `/releases.json`.
/// Native refresh runs off the UI thread in an `Io.Group` worker; the web build has no threads
/// and instead `pump`s browser GETs on the UI thread. Shared state is guarded by a
-/// `std.Io.Mutex` on native — lock with `dvui.io`. The owner must outlive any in-flight refresh
+/// `std.Io.Mutex` on native - lock with `dvui.io`. The owner must outlive any in-flight refresh
/// (in the app it is `Editor`-owned, app-lifetime).
///
/// Read access goes through `acquire`/`release`: hold the lock across any read of the returned
@@ -36,7 +36,7 @@ const ParsedShard = std.json.Parsed(registry.ReleaseShard);
///
/// The summary and the shard fail independently: a summary fetch failure is a hard catalog
/// failure (nothing to browse), but a shard fetch failure just means no install/update info is
-/// available this round — the shard falls back to `registry.ReleaseShard.empty` (or whatever was
+/// available this round - the shard falls back to `registry.ReleaseShard.empty` (or whatever was
/// last fetched successfully), so the browse list still renders with every plugin showing "no
/// compatible build in store" rather than the whole tab erroring out. A fresh `abi_fingerprint`
/// generation with no shard published yet looks identical to a network hiccup from here, which is
@@ -55,7 +55,7 @@ pub const Catalog = struct {
/// Shutdown has to *stop* an in-flight fetch, not merely wait for it. Nothing in the fetch
/// path sets a network timeout, so a registry host that accepts nothing and refuses nothing
/// (blackholed route, captive portal) parks the worker in a blocking connect for as long as
- /// the OS allows — with a `std.Thread`, `deinit`'s `join` inherits that wait and the whole
+ /// the OS allows - with a `std.Thread`, `deinit`'s `join` inherits that wait and the whole
/// app hangs at quit with no diagnostics. A `std.Io.Group` is the same shape the LSP client
/// uses (`core/lsp/Client.zig`'s `tasks`): `cancel` requests cancelation *and* awaits, and
/// `Io.Threaded` interrupts the blocked syscall so the worker returns `error.Canceled`
@@ -77,8 +77,8 @@ pub const Catalog = struct {
}
pub fn deinit(self: *Catalog) void {
- // Cancels *and* awaits, so the worker is provably finished — and therefore done touching
- // `summary`/`shard` — before they're freed below. No-op when nothing is in flight.
+ // Cancels *and* awaits, so the worker is provably finished - and therefore done touching
+ // `summary`/`shard` - before they're freed below. No-op when nothing is in flight.
// Wasm has no worker; the browser GETs are abandoned with the cache in `web_fetch`.
if (comptime !is_wasm) self.tasks.cancel(self.io);
if (self.summary) |*p| p.deinit();
@@ -96,7 +96,7 @@ pub const Catalog = struct {
}
/// Kick off a background refresh. No-op while one is already in flight. Must be called from
- /// the UI thread — `Io.Group` is explicitly not threadsafe against its own `await`/`cancel`.
+ /// the UI thread - `Io.Group` is explicitly not threadsafe against its own `await`/`cancel`.
pub fn refresh(self: *Catalog) void {
if (self.status() == .fetching) return;
if (comptime is_wasm) {
@@ -126,8 +126,8 @@ pub const Catalog = struct {
self.status_value.store(@backingInt(Status.fetching), .release);
}
- /// Advance in-flight browser GETs. UI thread, wasm only — a no-op natively. `fetcher` is the
- /// wasm `web_fetch` module (`request(allocator, url)` → pending / ready / failed); it is
+ /// Advance in-flight browser GETs. UI thread, wasm only - a no-op natively. `fetcher` is the
+ /// wasm `web_fetch` module (`request(allocator, url)` -> pending / ready / failed); it is
/// passed in rather than imported so this file stays a standalone test root (relative
/// `@import` cannot leave `plugin_store/`). Native callers pass `{}`.
// anytype: The browser fetch module or native empty stub is supplied without a web import.
@@ -155,7 +155,7 @@ pub const Catalog = struct {
self.summary = parsed;
self.wasm_phase = .shard;
// Ask for the shard now, not on the next pump: the web draws a frame only
- // when something asks for one, and nothing may — the update pass runs with
+ // when something asks for one, and nothing may - the update pass runs with
// no store tab open to keep a spinner turning.
self.pump(fetcher);
},
@@ -196,8 +196,8 @@ pub const Catalog = struct {
self.status_value.store(@backingInt(Status.failed), .release);
}
- /// Runs in the task group. Every failure path — including `error.Canceled` from a shutdown
- /// mid-fetch — lands on `.failed`, which is also the right resting state for a canceled
+ /// Runs in the task group. Every failure path - including `error.Canceled` from a shutdown
+ /// mid-fetch - lands on `.failed`, which is also the right resting state for a canceled
/// refresh: the app is on its way down, and a later `refresh` would overwrite it anyway.
fn worker(self: *Catalog) void {
const fresh_summary = registry.fetchSummary(self.allocator, self.io, self.summary_url) catch {
@@ -228,12 +228,12 @@ pub const Catalog = struct {
};
/// Lock the catalog and return the latest snapshot, or null if the summary has never loaded
- /// successfully. The slices stay valid until the matching `release` — hold the lock across
+ /// successfully. The slices stay valid until the matching `release` - hold the lock across
/// any read of them. Pair with `release`.
///
/// **The lock is held either way**, including when this returns null. It used to unlock on
/// the null path, which made the pairing conditional while every caller writes an
- /// unconditional `defer release()` — so drawing the store before the first summary arrived
+ /// unconditional `defer release()` - so drawing the store before the first summary arrived
/// (offline, or the first launch of a fresh install) unlocked an unlocked mutex and panicked
/// in `std.Io`. A lock whose release depends on the return value is a lock nobody can use
/// correctly.
@@ -254,7 +254,7 @@ pub const Catalog = struct {
};
/// Counted, not clocked. `Clock.boot` reads zero on the web, so every "refresh" built the
-/// identical `?_=0` URL — which the in-page fetch cache (`web_fetch`, keyed by URL) then
+/// identical `?_=0` URL - which the in-page fetch cache (`web_fetch`, keyed by URL) then
/// answered from memory forever. Refresh appeared to do nothing until the page was reloaded,
/// which was the only thing that cleared that map. The clock is still mixed in so two runs of
/// the app do not start from the same number on a platform where it works.
diff --git a/app/store/store_icon.zig b/app/store/store_icon.zig
index 60b68ea43..09e25d8ae 100644
--- a/app/store/store_icon.zig
+++ b/app/store/store_icon.zig
@@ -2,7 +2,7 @@
//!
//! Fetches each plugin's `ICON.png` from its repository over HTTPS on a worker thread (same
//! repo/subpath convention as `readme.zig`), caches decoded `dvui.ImageSource`s by plugin id,
-//! and draws them on store cards — including before the plugin is installed.
+//! and draws them on store cards - including before the plugin is installed.
const std = @import("std");
const builtin = @import("builtin");
const dvui = @import("dvui");
@@ -51,7 +51,7 @@ const IconEntry = struct {
// Stores *pointers* to heap-allocated entries, not entries by value: a worker thread holds a
// raw `*IconEntry` for the lifetime of its fetch, and `request()` for a *different* id can grow
// the map (rehashing the backing array) at any time. If entries lived in the map by value, that
-// rehash would move/invalidate the struct a still-running worker is writing into — the pointer
+// rehash would move/invalidate the struct a still-running worker is writing into - the pointer
// stored here is stable across rehashes even though the map's own storage isn't.
var icons: std.StringHashMapUnmanaged(*IconEntry) = .empty;
@@ -64,7 +64,7 @@ fn freeImage(source: dvui.ImageSource) void {
}
/// Begin fetching `ICON.png` for `id` from `repo` (optionally under `subpath`) if not already
-/// requested. Idempotent — safe to call every frame from each store card.
+/// requested. Idempotent - safe to call every frame from each store card.
pub fn request(id: []const u8, repo: []const u8, subpath: []const u8) void {
if (repo.len == 0) return;
const gpa = repo_asset.gpa();
diff --git a/app/store/web_fetch.zig b/app/store/web_fetch.zig
index 8bc57939c..89dc89a14 100644
--- a/app/store/web_fetch.zig
+++ b/app/store/web_fetch.zig
@@ -1,7 +1,7 @@
-//! Browser `fetch` for the wasm build — GET a URL, get the body bytes back.
+//! Browser `fetch` for the wasm build - GET a URL, get the body bytes back.
//!
//! The markdown preview's image path rasterizes via `Image` (SVG badges, CORS overlays).
-//! Everything else that just needs bytes — the store catalog, plugin READMEs, card icons —
+//! Everything else that just needs bytes - the store catalog, plugin READMEs, card icons -
//! goes through here. Same handshake as the image trio: Zig starts the load with
//! `fizzy_web_fetch`, JS writes the body through `FizzyWebFetchAlloc` / `FizzyWebFetchReady`.
//!
diff --git a/app/update/auto_update.zig b/app/update/auto_update.zig
index 55f7555e6..3c534d9fd 100644
--- a/app/update/auto_update.zig
+++ b/app/update/auto_update.zig
@@ -4,7 +4,7 @@ const build_opts = @import("build_opts");
const install_location = @import("install_location.zig");
const macos_bundle = @import("macos_bundle.zig");
-/// What the application wants done at Velopack's lifecycle points — after an install, after an
+/// What the application wants done at Velopack's lifecycle points - after an install, after an
/// update, before an uninstall.
///
/// Fizzy registers its file associations here; an app that claims no file types leaves these
@@ -44,7 +44,7 @@ fn castManager(m: *anyopaque) *Vpk.vpkc_update_manager_t {
/// Velopack's macOS locator expects the process image under `*.app/Contents/MacOS/*`, and the
/// bundle where Velopack can write the new version: in an Applications folder, not translocated by
/// Gatekeeper (`install_location.updatable`). Loose binaries from `zig build` / `zig-out/.../fizzy`
-/// are not supported — skip the C API so we don't spam logs or Velopack errors on every frame.
+/// are not supported - skip the C API so we don't spam logs or Velopack errors on every frame.
pub fn installLayoutSupported(io: std.Io) bool {
if (!impl) return false;
if (builtin.target.os.tag != .macos) return true;
@@ -132,7 +132,7 @@ pub fn openUpdateManager(io: std.Io, allocator: std.mem.Allocator) error{OutOfMe
return @ptrCast(manager.?);
}
- // Fast path: no fallback configured — single source, no extra probe (the
+ // Fast path: no fallback configured - single source, no extra probe (the
// caller's own check is the only network round-trip), matching the original
// behavior exactly.
if (!hasFallbackUrl()) {
@@ -182,9 +182,9 @@ pub fn freeUpdateManager(m: ?*anyopaque) void {
/// Set inside a fast-callback hook to record that Velopack dispatched an
/// install/update/uninstall lifecycle event. `appRunHook` reads this after
-/// `vpkc_app_run` returns and exits the process — Velopack's docs promise an
+/// `vpkc_app_run` returns and exits the process - Velopack's docs promise an
/// auto-exit but the runtime we link against doesn't always deliver it,
-/// leaving the installer's "Installing fizzy…" window hanging until its 30 s
+/// leaving the installer's "Installing fizzy..." window hanging until its 30 s
/// timeout marks the hook failed (even though our work succeeded).
var lifecycle_hook_fired: bool = false;
diff --git a/app/update/update_install.zig b/app/update/update_install.zig
index 235ed8da0..fb3c89e56 100644
--- a/app/update/update_install.zig
+++ b/app/update/update_install.zig
@@ -1,16 +1,16 @@
//! Background install of a Velopack update.
//!
//! `vpkc_download_updates` and `vpkc_wait_exit_then_apply_updates` are both blocking
-//! calls — running them on the GUI thread freezes the window for the full download
+//! calls - running them on the GUI thread freezes the window for the full download
//! window. This module owns a worker thread (one at a time, latched on a module-level
-//! singleton) that runs both steps off-thread and publishes phase + 0–100 progress
+//! singleton) that runs both steps off-thread and publishes phase + 0-100 progress
//! back through atomics so the dialog can render a status line and a progress bar.
//!
//! The worker only downloads. Once the update is on disk the job latches
//! `.downloaded` and the app restarts the way it always does (`restart`): unsaved
//! documents are asked about first, and at teardown `applyAtExit` hands the waiting
//! update to Velopack, which swaps the install once this process has exited and
-//! starts the new version. A restart called off leaves it downloaded — Velopack
+//! starts the new version. A restart called off leaves it downloaded - Velopack
//! applies it on the next launch on its own. On failure the job latches `.failed`
//! with an error code; callers wipe the singleton when the user dismisses the dialog.
@@ -37,9 +37,9 @@ window: *dvui.Window,
io: std.Io,
phase: std.atomic.Value(u8) = .init(@backingInt(Phase.queued)),
-/// 0–100 from Velopack's download callback. Stays at 0 outside `.downloading`.
+/// 0-100 from Velopack's download callback. Stays at 0 outside `.downloading`.
progress: std.atomic.Value(u8) = .init(0),
-/// Worker → main publish flag.
+/// Worker -> main publish flag.
done: std.atomic.Value(bool) = .init(false),
/// Set on `.failed`. Read only after `phase == .failed`.
err: ?auto_update.UpdateInstallError = null,
@@ -53,7 +53,7 @@ update_info: ?*anyopaque = null,
downloaded_unseen: std.atomic.Value(bool) = .init(false),
// ----------------------------------------------------------------------------
-// Singleton — one in-flight install at a time. Both the About dialog and the
+// Singleton - one in-flight install at a time. Both the About dialog and the
// launch toast share this so they can't kick concurrent downloads.
// ----------------------------------------------------------------------------
@@ -64,7 +64,7 @@ pub fn currentJob() ?*UpdateJob {
}
/// Returns the singleton if a worker is already running; otherwise spawns one and
-/// returns the new job. Caller does not own the pointer — call `clear()` when the
+/// returns the new job. Caller does not own the pointer - call `clear()` when the
/// user dismisses the UI to free it.
pub fn startOrGet(allocator: std.mem.Allocator, io: std.Io) !*UpdateJob {
if (current) |j| return j;
@@ -87,7 +87,7 @@ pub fn startOrGet(allocator: std.mem.Allocator, io: std.Io) !*UpdateJob {
}
/// Drop the singleton if the worker has published `done`. No-op while the worker
-/// is still running, and for a downloaded update — that one waits for `applyAtExit`.
+/// is still running, and for a downloaded update - that one waits for `applyAtExit`.
/// Safe to call repeatedly.
pub fn clearIfFinished() void {
const j = current orelse return;
@@ -97,7 +97,7 @@ pub fn clearIfFinished() void {
j.allocator.destroy(j);
}
-/// True once, on the GUI thread, when an update the user asked for has finished downloading —
+/// True once, on the GUI thread, when an update the user asked for has finished downloading -
/// the cue to restart into it (`restart.tick`).
pub fn takeDownloaded() bool {
const j = current orelse return false;
@@ -112,7 +112,7 @@ pub fn downloaded() bool {
/// At teardown, on a restart: hand a downloaded update to Velopack, which waits for this process
/// to exit, swaps the install and starts the new version. False when there is none, or Velopack
-/// would not take it — the app then starts itself again as it is.
+/// would not take it - the app then starts itself again as it is.
pub fn applyAtExit() bool {
if (!auto_update.impl) return false;
if (!downloaded()) return false;
@@ -125,7 +125,7 @@ pub fn applyAtExit() bool {
j.manager = null;
j.update_info = null;
}
- // silent=false allows the elevation prompt — /Applications is root-owned, so the bundle swap
+ // silent=false allows the elevation prompt - /Applications is root-owned, so the bundle swap
// needs admin rights; restart=true starts the new version once it is in place.
if (!Vpk.vpkc_wait_exit_then_apply_updates(mgr, info.TargetFullRelease, false, true, null, 0)) {
auto_update.logVpkError("fizzy autoupdate: wait_exit_then_apply_updates failed");
@@ -148,8 +148,8 @@ pub fn phaseLabel(p: Phase) []const u8 {
// `.queued` is only ever visible for the brief window between
// `startOrGet` returning and the worker actually transitioning to
// `.downloading`. Both callers (toast + About dialog) have already
- // confirmed an update is available, so we say "Downloading…" here
- // too instead of leaking a "Checking…" / "Preparing…" state that
+ // confirmed an update is available, so we say "Downloading..." here
+ // too instead of leaking a "Checking..." / "Preparing..." state that
// would contradict the prior step.
.queued, .downloading => "Downloading update…",
.downloaded => "Restarting to update…",
diff --git a/app/update/update_notify.zig b/app/update/update_notify.zig
index cfb9f5f77..a22f60264 100644
--- a/app/update/update_notify.zig
+++ b/app/update/update_notify.zig
@@ -1,7 +1,7 @@
//! Background check for Velopack/GitHub updates on launch. Network work runs on a
//! side thread so startup never blocks. When an update is detected, we arm a DVUI
//! toast with a "Relaunch to update" button so the user can install in one click
-//! while it's visible (or use Help → Check for Updates / the infobar button later).
+//! while it's visible (or use Help -> Check for Updates / the infobar button later).
const std = @import("std");
const dvui = @import("dvui");
@@ -29,7 +29,7 @@ var simulated: bool = false;
var remote_ver: [128]u8 = undefined;
var remote_ver_len: usize = 0;
-// Latched so we only arm the toast once per app session — calling `toastAdd`
+// Latched so we only arm the toast once per app session - calling `toastAdd`
// every frame would queue an unbounded stack of toasts.
var toast_armed: bool = false;
/// Latched while a progress toast is registered. Both the launch toast button and
@@ -44,11 +44,11 @@ const TOAST_TIMEOUT_US: i32 = 10 * std.time.us_per_s;
const PROGRESS_TOAST_TIMEOUT_US: i32 = std.math.maxInt(i32);
/// Stable, non-null subwindow id for this toast. Using a non-null id makes DVUI's
-/// default `toastsShow(null, …)` (run in `Window.end`) skip it, so we can render
+/// default `toastsShow(null, ...)` (run in `Window.end`) skip it, so we can render
/// it ourselves in a custom rect via `dvui.toastsFor(SUBWINDOW_ID)`.
const SUBWINDOW_ID: dvui.Id = @fromBackingInt(@intCast(0xF122_DA70_AD0E_71D0));
-// Estimated toast size — used for the FloatingWidget bounding rect and for
+// Estimated toast size - used for the FloatingWidget bounding rect and for
// computing the bottom-left anchor relative to the infobar. The real toast
// auto-sizes inside, but the FloatingWidget needs an initial rect.
// Distance from the window's left edge to the toast's left edge (window-natural pixels).
@@ -162,13 +162,13 @@ pub fn badgeVisible() bool {
}
/// Where the launch update check has got to, for anything that must sequence itself *behind* a
-/// Fizzy update — today that is `PluginStore`'s auto-update pass. A store plugin is built against
+/// Fizzy update - today that is `PluginStore`'s auto-update pass. A store plugin is built against
/// one Fizzy SDK generation, so the plugin builds that match a pending Fizzy release only become
-/// visible to us once we are running it: Fizzy goes first (always by the user's own hand — see
+/// visible to us once we are running it: Fizzy goes first (always by the user's own hand - see
/// `tick`; nothing here ever installs the app update on its own), and plugins follow on the
/// relaunch.
pub const AppUpdateState = enum {
- /// The launch check hasn't answered yet — callers that must sequence behind it should wait.
+ /// The launch check hasn't answered yet - callers that must sequence behind it should wait.
checking,
/// Up to date, no feed, unsupported install layout, or the check failed. Nothing to wait for.
none,
@@ -179,7 +179,7 @@ pub const AppUpdateState = enum {
};
pub fn appUpdateState() AppUpdateState {
- // A simulated update never installs, so treat it as "nothing pending" for sequencing —
+ // A simulated update never installs, so treat it as "nothing pending" for sequencing -
// otherwise the debug flag would silently disable plugin updates.
if (simulated) return .none;
if (comptime auto_update.impl) {
@@ -205,7 +205,7 @@ pub fn tick() void {
if (!badgeVisible()) return;
toast_armed = true;
- // Non-null subwindow_id so DVUI's automatic toast render skips it — we draw
+ // Non-null subwindow_id so DVUI's automatic toast render skips it - we draw
// it ourselves via `drawAbove` so the placement is anchored to the infobar.
const id_mutex = dvui.toastAdd(null, @src(), 0, SUBWINDOW_ID, displayUpdateToast, TOAST_TIMEOUT_US);
id_mutex.mutex.unlock(dvui.io);
@@ -224,11 +224,11 @@ pub fn drawAbove(infobar_top_y_physical: f32, gap_above_infobar: f32) void {
// Anchor the toast's bottom-left corner at (TOAST_MARGIN, infobar_top - gap)
// in physical screen pixels. Using `from` instead of a fixed `rect` lets the
// FloatingWidget self-size to the toast pill's natural dimensions, so its
- // bottom edge sits EXACTLY at the anchor — no leftover slack between the
+ // bottom edge sits EXACTLY at the anchor - no leftover slack between the
// floating widget and the toast inside.
//
- // `from_gravity_x = 1` → the anchor point is the floating's left edge.
- // `from_gravity_y = 0` → the anchor point is the floating's bottom edge.
+ // `from_gravity_x = 1` -> the anchor point is the floating's left edge.
+ // `from_gravity_y = 0` -> the anchor point is the floating's bottom edge.
const scale = dvui.windowNaturalScale();
const anchor_physical: dvui.Point.Physical = .{
.x = TOAST_MARGIN * scale,
@@ -376,8 +376,8 @@ fn displayProgressToast(id: dvui.Id) !void {
.padding = .{ .x = 4, .w = 12 },
});
- // Velopack only streams download progress (0–100). Other phases get the
- // label only — no bar — since we have nothing meaningful to show.
+ // Velopack only streams download progress (0-100). Other phases get the
+ // label only - no bar - since we have nothing meaningful to show.
if (cur_phase == .downloading) {
const pct: f32 = @as(f32, @floatFromInt(job.progressPercent())) / 100.0;
dvui.progress(@src(), .{ .percent = pct }, .{
@@ -390,7 +390,7 @@ fn displayProgressToast(id: dvui.Id) !void {
// Terminal states get a dismiss button so the user can clear the toast.
// Active phases (`queued`/`downloading`, and `downloaded` while its restart
- // runs) don't — the app is on its way to the new version.
+ // runs) don't - the app is on its way to the new version.
const terminal = cur_phase == .failed or cur_phase == .no_update;
if (waiting) {
if (dvui.button(@src(), "Restart to update", .{}, .{
diff --git a/app/view/ViewHost.zig b/app/view/ViewHost.zig
index c654ec14f..75a1dbfce 100644
--- a/app/view/ViewHost.zig
+++ b/app/view/ViewHost.zig
@@ -41,7 +41,7 @@ pub const VTable = struct {
/// The colour under the frame at `opacity`, `fill` over the material where the host makes its
/// surface of one; null where it makes none and the app draws its own base.
material_base: *const fn (ptr: *anyopaque, fill: dvui.Color, opacity: f32) ?dvui.Color,
- /// Whether the person asked the host to close it (its close button, ⌘W), read each frame. A
+ /// Whether the person asked the host to close it (its close button, CmdW), read each frame. A
/// host whose close is the app's quit (a main window, which dvui's app runner closes) says
/// false.
close_requested: *const fn (ptr: *anyopaque) bool,
@@ -71,7 +71,7 @@ pub fn closeRequested(self: ViewHost) bool {
return self.vtable.close_requested(self.ptr);
}
-/// A host's vtable from a type's methods of the same names: `T.begin(*T)`, `T.end(*T)`, … .
+/// A host's vtable from a type's methods of the same names: `T.begin(*T)`, `T.end(*T)`, ... .
pub fn vtableOf(comptime T: type) *const VTable {
const S = struct {
fn self(ptr: *anyopaque) *T {
diff --git a/app/watch/ExecutableWatcher.zig b/app/watch/ExecutableWatcher.zig
index a4a14cb49..f8ebb464e 100644
--- a/app/watch/ExecutableWatcher.zig
+++ b/app/watch/ExecutableWatcher.zig
@@ -1,6 +1,6 @@
//! Watches the app's own executable, so a rebuild of it restarts the app into the new build
-//! (`restart`), keeping every document (`KeptDocuments`). The loop a plugin has — build, and the
-//! running app picks it up — for the app itself: for anyone working on fizzy from source, and for
+//! (`restart`), keeping every document (`KeptDocuments`). The loop a plugin has - build, and the
+//! running app picks it up - for the app itself: for anyone working on fizzy from source, and for
//! an agent iterating on it (plans/RESTART_AND_CRASHES_PLAN.md, step 2).
//!
//! The same shape as `SettingsWatcher`: nightwatch's thread sets a flag and wakes the app, and
@@ -43,7 +43,7 @@ pending: Stamp = .{},
/// This executable and its stamp as the process started (`noteLaunch`).
var launch: struct { path_buf: [std.fs.max_path_bytes]u8 = undefined, path_len: usize = 0, stamp: ?Stamp = null } = .{};
-/// First thing in `main`: the executable this process was started from, as it is now — before the
+/// First thing in `main`: the executable this process was started from, as it is now - before the
/// window, the GPU and the plugins, during which a build can land. `init` compares against it.
pub fn noteLaunch(io: std.Io) void {
if (comptime !have_impl) return;
diff --git a/app/watch/FolderWatcher.zig b/app/watch/FolderWatcher.zig
index 2e692d5b3..51bed566d 100644
--- a/app/watch/FolderWatcher.zig
+++ b/app/watch/FolderWatcher.zig
@@ -3,14 +3,14 @@
//!
//! The third nightwatch adapter in this directory, and the only one whose output leaves fizzy.
//! `SettingsWatcher` reconciles `settings.zon`; `DocumentWatcher` reloads open tabs. Neither
-//! helps a plugin that cares about files nobody has open — a file tree that should show what an
+//! helps a plugin that cares about files nobody has open - a file tree that should show what an
//! agent just created, a link indexer whose graph goes stale when a wikilink is deleted from a
//! closed file, a language server owing `didChangeWatchedFiles`.
//!
//! Three things belong on this side of the SDK boundary rather than in each plugin:
//!
//! 1. **Thread hop.** Nightwatch calls its handler on its own thread. Plugins are dylibs; a
-//! callback arriving on a thread the plugin never created — possibly mid-unload — is a
+//! callback arriving on a thread the plugin never created - possibly mid-unload - is a
//! crash. Events are buffered here and handed over from `tick`, on the UI thread.
//! 2. **Ignore rules.** Only fizzy knows them (`IgnoreRules`). Filtering here means `.git`,
//! build output and gitignored paths never reach a plugin, instead of every plugin
@@ -23,13 +23,13 @@
//! without any plugin noticing.
//!
//! Not every folder is watched. Where the backend watches folder by folder (inotify), hidden
-//! folders (`.git`, `.zig-cache`, …), `node_modules` and `zig-pkg` never are
+//! folders (`.git`, `.zig-cache`, ...), `node_modules` and `zig-pkg` never are
//! (`folder_events.skipWatchDir`), and a tree past `folder_events.max_dir_watches`, or past
//! what the OS allows, is watched only in part. Elsewhere events from hidden folders are dropped
-//! anyway (`record`). `active` — `Host.folderWatchActive` — is true only while the tree is
+//! anyway (`record`). `active` - `Host.folderWatchActive` - is true only while the tree is
//! covered, so a plugin knows when to keep its own rescan.
//!
-//! `have_impl` is false on wasm and any unsupported OS — the watcher is simply not started
+//! `have_impl` is false on wasm and any unsupported OS - the watcher is simply not started
//! there, and `Host.folderWatchActive` reports false so a plugin knows to keep its own slow
//! rescan (same degrade-gracefully spirit as `SettingsWatcher` / `DocumentWatcher`).
const builtin = @import("builtin");
@@ -69,7 +69,7 @@ pub const have_impl = switch (builtin.target.os.tag) {
};
/// Spin lock over `std.atomic.Mutex` (which is try-lock only). A blocking primitive here would
-/// mean `std.Io.Mutex` and therefore `dvui.io` on nightwatch's thread — the one thing the doc
+/// mean `std.Io.Mutex` and therefore `dvui.io` on nightwatch's thread - the one thing the doc
/// comments on the other two adapters single out as not to be touched from a watcher callback.
/// Both critical sections are a bounded memcpy or a pointer swap, so there is nothing to block
/// on for long enough to be worth a real wait.
@@ -91,7 +91,7 @@ folder: ?[]u8 = null,
impl: if (have_impl) Impl else void = if (have_impl) .{} else {},
/// Guards `shared` only. Held for a bounded memcpy on the producer and a pointer swap on the
-/// consumer — never across a plugin call.
+/// consumer - never across a plugin call.
mutex: Spin = .{},
shared: Buf,
staging: Buf,
@@ -103,7 +103,7 @@ out: []PathEvent,
const Impl = if (have_impl) struct {
const nightwatch = @import("nightwatch");
- /// `Default` on purpose — this watches a whole tree, which is the case every backend's
+ /// `Default` on purpose - this watches a whole tree, which is the case every backend's
/// default variant is built for. On macOS that is FSEvents (see the `macos_fsevents` build
/// option), which watches the subtree from a single stream; the kqueue fallback would want
/// a file descriptor per directory *and* per file, and a project folder is exactly the
@@ -370,7 +370,7 @@ const Impl = if (have_impl) struct {
if (self.folder == null) return;
// Where the backend watches folder by folder, the walker follows folders coming and
- // going — before the dot reject, which would hide a folder moved out of view.
+ // going - before the dot reject, which would hide a folder moved out of view.
if (!Backend.watches_recursively and kind == .created) markAnnounced(impl, path);
if (!Backend.watches_recursively and object == .dir) switch (kind) {
.created => queueFolder(self, .appeared, path, ""),
@@ -407,7 +407,7 @@ const Impl = if (have_impl) struct {
}
} else void;
-/// Allocates the ring buffers. Does not start nightwatch — `setFolder` does, once a folder is
+/// Allocates the ring buffers. Does not start nightwatch - `setFolder` does, once a folder is
/// open and `self` is at its final address.
pub fn init(gpa: Allocator) !FolderWatcher {
if (comptime !have_impl) return error.Unsupported;
@@ -452,7 +452,7 @@ pub fn active(self: *const FolderWatcher) bool {
}
/// Point the watcher at `path` (or nowhere, when null). Must be called only once `self` is at
-/// its **final** address — nightwatch retains `&self.impl.handler` for the watcher's lifetime,
+/// its **final** address - nightwatch retains `&self.impl.handler` for the watcher's lifetime,
/// the same constraint `SettingsWatcher.start` documents.
///
/// Best-effort throughout: a folder that can't be watched is a degraded experience, never a
@@ -537,7 +537,7 @@ fn stopWatch(self: *FolderWatcher) void {
/// What the application does with a settled batch, and which of them it wants at all.
///
/// The watcher's job ends at "these paths changed, coalesced, off the watcher thread". Deciding
-/// that `.zig-cache/` is noise, and deciding who hears about the rest, is the application's — so
+/// that `.zig-cache/` is noise, and deciding who hears about the rest, is the application's - so
/// both are here rather than inside the watcher, which is what lets this file belong to any app.
pub const Sink = struct {
ctx: *anyopaque,
diff --git a/app/watch/SettingsWatcher.zig b/app/watch/SettingsWatcher.zig
index 74014b670..e0f6f0ba3 100644
--- a/app/watch/SettingsWatcher.zig
+++ b/app/watch/SettingsWatcher.zig
@@ -1,4 +1,4 @@
-//! Watches `/` (recursive) for external changes while fizzy is running — see
+//! Watches `/` (recursive) for external changes while fizzy is running - see
//! plans/PLUGIN_MANIFEST_PLAN.md R11/R12.
//!
//! Thin adapter over [`neurocyte/nightwatch`](https://github.com/neurocyte/nightwatch): one
@@ -9,7 +9,7 @@
//! `dvui.io` / a shared allocator from the callback) and a ~200ms coalesce on the main thread
//! via `tick`.
//!
-//! `have_impl` is false on wasm and any unsupported OS — the watcher is simply not started
+//! `have_impl` is false on wasm and any unsupported OS - the watcher is simply not started
//! there (same "no filesystem" degrade-gracefully spirit as every other native-only guard in
//! this codebase), not a hard error.
const builtin = @import("builtin");
@@ -35,13 +35,13 @@ pub const have_impl = switch (builtin.target.os.tag) {
gpa: Allocator,
/// Owned copy of the watched config folder path.
config_folder: []const u8,
-/// Nightwatch handler + instance — only meaningful when `have_impl`. Stored as opaque
+/// Nightwatch handler + instance - only meaningful when `have_impl`. Stored as opaque
/// optional so this file still type-checks on wasm (where nightwatch isn't linked).
impl: if (have_impl) Impl else void = if (have_impl) .{} else {},
/// Set by nightwatch's handler thread; consumed/extended by `tick` on the main thread.
raw_dirty: std.atomic.Value(bool) = .init(false),
/// A plugin binary arrived whole (`isPluginBinary`): reconciled on the next tick, with no
-/// coalesce — the install renamed a finished file into place, so there is nothing to wait for,
+/// coalesce - the install renamed a finished file into place, so there is nothing to wait for,
/// and the 200 ms was most of how long a rebuilt plugin took to be noticed.
binary_dirty: std.atomic.Value(bool) = .init(false),
/// Main-thread coalesce deadline (`perf.nanoTimestamp()`); 0 = nothing pending.
@@ -49,12 +49,12 @@ coalesce_deadline_ns: i128 = 0,
const Impl = if (have_impl) struct {
const nightwatch = @import("nightwatch");
- /// `Default.Handler` — nightwatch's root module doesn't re-export `types.Handler` directly.
+ /// `Default.Handler` - nightwatch's root module doesn't re-export `types.Handler` directly.
const Handler = nightwatch.Default.Handler;
handler: Handler = .{ .vtable = &vtable },
nw: ?nightwatch.Default = null,
- /// Set in `start` once `SettingsWatcher` is at its final address — avoids a
+ /// Set in `start` once `SettingsWatcher` is at its final address - avoids a
/// `@fieldParentPtr` alignment dance from the nested `impl` field.
raw_dirty: ?*std.atomic.Value(bool) = null,
binary_dirty: ?*std.atomic.Value(bool) = null,
@@ -96,8 +96,8 @@ const Kind = enum { binary, ignored, other };
/// `.so`, `.dll`) is complete when its event arrives: the plugin SDK's install writes
/// `.part` and renames it into place (`plugin_sdk.zig`, `DevInstall`), and the store does the
/// same. The `.part` itself, and fizzy's own load copies (`.load-copy/`, `.load-tmp/`, written when
-/// it loads a plugin), say nothing new. Everything else — `settings.zon` above all, which an editor
-/// may write in several bursts — waits for the coalesce.
+/// it loads a plugin), say nothing new. Everything else - `settings.zon` above all, which an editor
+/// may write in several bursts - waits for the coalesce.
fn classify(config_folder: []const u8, path: []const u8) Kind {
if (std.mem.find(u8, path, ".load-copy") != null or std.mem.find(u8, path, ".load-tmp") != null) return .ignored;
// The app's own state beside its settings, written while it runs: a crash checkpoint every few
@@ -162,7 +162,7 @@ test classify {
try std.testing.expectEqual(Kind.binary, classify("/cfg", "/cfg/plugins/checkpoint/checkpoint" ++ ext));
}
-/// Sets up bookkeeping but does **not** start nightwatch yet — see `start`'s doc comment.
+/// Sets up bookkeeping but does **not** start nightwatch yet - see `start`'s doc comment.
/// `config_folder` is copied; caller retains ownership of the passed slice.
pub fn init(gpa: Allocator, config_folder: []const u8) !SettingsWatcher {
if (comptime !have_impl) return error.Unsupported;
@@ -175,10 +175,10 @@ pub fn init(gpa: Allocator, config_folder: []const u8) !SettingsWatcher {
}
/// Starts nightwatch and registers a recursive watch on `config_folder`. Must be called only
-/// once `self` is at its **final** address — nightwatch retains `&self.impl.handler` for the
+/// once `self` is at its **final** address - nightwatch retains `&self.impl.handler` for the
/// watcher's lifetime, so calling this before `self` is copied into `editor.settings_watcher`
/// would leave it pointing at stack memory. Mirrors why `Editor.postInit` exists separately
-/// from `Editor.init` — call this from `postInit`, not `init`.
+/// from `Editor.init` - call this from `postInit`, not `init`.
pub fn start(self: *SettingsWatcher) !void {
if (comptime have_impl) {
const nightwatch = @import("nightwatch");
@@ -209,8 +209,8 @@ pub fn stop(self: *SettingsWatcher) void {
/// What the application reconciles when this tree settles.
///
/// One callback, not four: the watcher knows that something under the config folder changed and
-/// nothing more. Which passes that implies — re-read the settings file, notice a rebuilt plugin
-/// dylib, notice a newly dropped-in one — is the application's business, and fizzy's order
+/// nothing more. Which passes that implies - re-read the settings file, notice a rebuilt plugin
+/// dylib, notice a newly dropped-in one - is the application's business, and fizzy's order
/// between them is fizzy's reasoning (see `Editor.configChanged`).
pub const Sink = struct {
ctx: *anyopaque,
@@ -218,7 +218,7 @@ pub const Sink = struct {
};
/// Call once per frame. Cheap no-op unless the watcher thread actually saw a change. Coalesces
-/// a burst of raw events (~200ms) on the main thread before reconciling — except a plugin binary
+/// a burst of raw events (~200ms) on the main thread before reconciling - except a plugin binary
/// arriving whole, which reconciles on this tick unless a coalesce is already under way.
pub fn tick(self: *SettingsWatcher, sink: Sink) void {
const now = core.perf.nanoTimestamp();
diff --git a/app/watch/folder_events.zig b/app/watch/folder_events.zig
index 4a494e6eb..88363f2f5 100644
--- a/app/watch/folder_events.zig
+++ b/app/watch/folder_events.zig
@@ -1,4 +1,4 @@
-//! Buffering and filtering for `FolderWatcher` — the half that runs on nightwatch's thread —
+//! Buffering and filtering for `FolderWatcher` - the half that runs on nightwatch's thread -
//! and the walk that decides which folders it watches.
//!
//! Split out and std-only on purpose. `FolderWatcher.zig` reaches `fizzy.zig` and `dvui`, so it
@@ -17,7 +17,7 @@ const builtin = @import("builtin");
/// a watch leaves alone.
///
/// Pure string work: no allocation, no filesystem, no host call, so it is safe to run on the
-/// watcher's own thread. With `dotName` it is not the authoritative ignore check — fizzy's
+/// watcher's own thread. With `dotName` it is not the authoritative ignore check - fizzy's
/// `IgnoreRules` is, and that runs later on the UI thread. It exists so a `git checkout` or a
/// build churning a cache directory can't fill the ring before anyone gets to apply the real
/// rules.
@@ -56,7 +56,7 @@ pub const max_dir_watches: u32 = 16384;
/// Folders a watch never descends into, by name: hidden ones (`.git`, `.zig-cache`, `.venv`,
/// `.bun`) and the dependency trees a tool writes, `node_modules` and `zig-pkg`. They hold most
/// of a project's directories and none of what a person edits. Anything else a project chooses
-/// to ignore (`target`, `zig-out`, …) is still watched, and `IgnoreRules` drops its events.
+/// to ignore (`target`, `zig-out`, ...) is still watched, and `IgnoreRules` drops its events.
pub fn skipWatchDir(name: []const u8) bool {
if (name.len == 0 or name[0] == '.') return true;
return std.mem.eql(u8, name, "node_modules") or std.mem.eql(u8, name, "zig-pkg");
@@ -211,7 +211,7 @@ pub const Watches = struct {
}
/// `dir` was created, and is watched only now: whatever was made in it before then had
- /// no event, so it is announced — unless `announced`, when the backend already did (a
+ /// no event, so it is announced - unless `announced`, when the backend already did (a
/// folder moved in from outside the tree, which inotify reports with all it holds).
// anytype: Native watch backends and test contexts supply add/remove/created/say callbacks.
pub fn appeared(self: *Watches, io: std.Io, dir: []const u8, announced: bool, ctx: anytype) void {
@@ -701,7 +701,7 @@ pub const Doorbell = struct {
/// Nothing here allocates, and that is the whole point: the producer runs on a thread nightwatch
/// owns, where reaching for a shared allocator is exactly what the other watcher adapters in
/// this directory are careful never to do. When the buffer fills, the overflow is *reported*
-/// (`truncated`) rather than absorbed — a branch switch emits events by the tens of thousands,
+/// (`truncated`) rather than absorbed - a branch switch emits events by the tens of thousands,
/// and the useful answer for a consumer at that point is "rescan", not a longer list it still
/// has to walk.
pub fn Ring(comptime Kind: type, comptime Object: type) type {
@@ -717,7 +717,7 @@ pub fn Ring(comptime Kind: type, comptime Object: type) type {
object: Object,
};
- /// Flat backing store for paths — one arena rather than a fixed slot per event, so a
+ /// Flat backing store for paths - one arena rather than a fixed slot per event, so a
/// handful of deep paths can't crowd out everything else and no path length is a
/// special case.
paths: []u8,
@@ -738,7 +738,7 @@ pub fn Ring(comptime Kind: type, comptime Object: type) type {
}
/// Nothing to report. Distinct from `count == 0`, which is also true for a batch whose
- /// every event was dropped — and that batch still has to go out.
+ /// every event was dropped - and that batch still has to go out.
pub fn empty(self: *const Self) bool {
return self.count == 0 and !self.truncated;
}
@@ -1271,7 +1271,7 @@ test "a folder leaving the tree gives back its watches and what was waiting belo
w.walk(testing.io, &b);
w.settled = true;
- // `src-x` sorts between `src` and `src/…`; it must survive `src` going.
+ // `src-x` sorts between `src` and `src/...`; it must survive `src` going.
try t.tmp.dir.createDirPath(testing.io, "src-x");
w.appeared(testing.io, t.path("src-x"), false, &b);
const src = t.own("src");
@@ -1427,7 +1427,7 @@ test "inside and the segment check need a separator after the folder" {
test "a walk that stops partway owes the folder only what it hadn't said" {
// P holds files and D and E; the walk says P's entries, walks D whole, then runs out at
- // E. Resumed, it must say what is in E — and nothing in P or D again.
+ // E. Resumed, it must say what is in E - and nothing in P or D again.
var t: TestTree = .{ .tmp = undefined };
try t.init();
defer t.tmp.cleanup();
@@ -1451,7 +1451,7 @@ test "a walk that stops partway owes the folder only what it hadn't said" {
w.retry();
w.catchUp(testing.io, &b);
try testing.expect(w.whole());
- // P: f.txt, D, E; D: d1, g.txt; E: e1, h.txt — seven, each once.
+ // P: f.txt, D, E; D: d1, g.txt; E: e1, h.txt - seven, each once.
try testing.expectEqual(@as(usize, 7), b.created_files + b.created_dirs);
try testing.expect(said_first < 7);
// Only what wasn't watched was tried: the refused folder and what is below it.
@@ -1547,7 +1547,7 @@ test "running out of memory anywhere leaves no watch untracked" {
defer t.tmp.cleanup();
try t.tmp.dir.createDirPath(testing.io, "new/sub");
try t.tmp.dir.writeFile(testing.io, .{ .sub_path = "new/f.txt", .data = "" });
- // Fail the first, then the second, … allocation of a whole session: walk, a folder that
+ // Fail the first, then the second, ... allocation of a whole session: walk, a folder that
// appears, a rename, a folder going. Whatever fails, every live watch is one `paths`
// holds (so it can be removed), and nothing leaks.
var fail_at: usize = 0;
diff --git a/app/watch/wake.zig b/app/watch/wake.zig
index 55dd46299..02a65721d 100644
--- a/app/watch/wake.zig
+++ b/app/watch/wake.zig
@@ -2,7 +2,7 @@
//!
//! Every watcher here runs its own thread and has the same need: it has buffered something, and
//! the UI thread may be parked in the event loop with nothing to draw. Waking it is a single call
-//! into the *application's* backend — `window.backend.refresh()` for fizzy — which is the one
+//! into the *application's* backend - `window.backend.refresh()` for fizzy - which is the one
//! thing in this directory that cannot be answered without knowing whose window it is.
//!
//! So the application answers it once, at startup, and the watchers stop caring. A single
@@ -11,7 +11,7 @@
//! never set this) degrades to "the change is picked up on the next frame something else causes"
//! rather than failing.
//!
-//! Safe to call from any thread — that is the whole point of it. See `Editor.fizzyRefresh`'s doc
+//! Safe to call from any thread - that is the whole point of it. See `Editor.fizzyRefresh`'s doc
//! comment for how that was verified: one call reliably wakes the blocked loop for exactly one
//! frame.
var hook: ?*const fn () void = null;
diff --git a/build.zig b/build.zig
index 27fa52d9c..18677b4a7 100644
--- a/build.zig
+++ b/build.zig
@@ -81,6 +81,7 @@ pub fn build(b: *std.Build) !void {
const optimize = b.standardOptimizeOption(.{});
const base: @import("build/app.zig").Options = .{
+ .look = .{ .motion = b.option(f32, "look-motion", "Initial motion level until the person chooses one") },
.windows_msvc_libc_opt = windows_msvc_libc_opt,
.fetch_msvc_opt = fetch_msvc_opt,
.macos_sign_app_identity = macos_sign_app_identity,
@@ -111,6 +112,8 @@ pub fn build(b: *std.Build) !void {
} else null,
};
+ if (b.pkg_hash.len == 0) @import("build/android.zig").add(b);
+
// A consumer that bundles plugins of its own cannot say so through `b.dependency`
// options (a plugin is a module from another package, not a value), so it passes
// `defer-app` here and calls `buildApp` below with its plugin modules instead.
diff --git a/build/android.zig b/build/android.zig
new file mode 100644
index 000000000..e95c3e878
--- /dev/null
+++ b/build/android.zig
@@ -0,0 +1,113 @@
+//! `zig build apk -Dandroid-sdk=`: the Android hello (`android/`), an SDL3 window with dvui
+//! drawing, packaged as an APK with the Android SDK's own tools. See `android/README.md`.
+const std = @import("std");
+const sdk = @import("sdk.zig");
+
+pub fn add(b: *std.Build) void {
+ const apk_step = b.step("apk", "Build zig-out/fizzy-hello.apk (needs -Dandroid-sdk)");
+ // Options only, never the environment: see `build.zig`. Without the SDK the step says so, and
+ // dvui's Android dependencies are never configured for a desktop build.
+ const sdk_dir = b.option([]const u8, "android-sdk", "The Android SDK directory (platform 35, build-tools 35.0.0, NDK 27.2.12479018)") orelse {
+ apk_step.dependOn(&b.addFail("zig build apk: pass -Dandroid-sdk=").step);
+ return;
+ };
+ const arch = b.option(enum { aarch64, x86_64 }, "android-arch", "aarch64 (phones, the default) or x86_64 (the emulator on an x86_64 host)") orelse .aarch64;
+ const ndk_version = b.option([]const u8, "android-ndk", "NDK under /ndk") orelse "27.2.12479018";
+ const build_tools = b.option([]const u8, "android-build-tools", "Build-tools under /build-tools") orelse "35.0.0";
+ const platform = b.option([]const u8, "android-platform", "Platform under /platforms") orelse "android-35";
+ const keystore = b.option([]const u8, "android-keystore", "Debug keystore (alias androiddebugkey, password android)") orelse
+ b.pathJoin(&.{ b.graph.environ_map.get("HOME") orelse ".", ".android/debug.keystore" });
+ // The activity's Java package stays `dev.fizzyedit.hello` (its source); this is the app's id.
+ const app_id = b.fmt("dev.fizzy.{s}", .{@import("bundle_id.zig").component(b.allocator, "hello")});
+
+ const target = b.resolveTargetQuery(.{
+ .cpu_arch = switch (arch) {
+ .aarch64 => .aarch64,
+ .x86_64 => .x86_64,
+ },
+ .os_tag = .linux,
+ .abi = .android,
+ .android_api_level = 24, // Zig's std.Io calls preadv64/pwritev64, which bionic has from API 24
+ });
+ const optimize: std.lang.OptimizeMode = .ReleaseSafe;
+ const abi = switch (arch) {
+ .aarch64 => "arm64-v8a",
+ .x86_64 => "x86_64",
+ };
+ const triple = b.fmt("{s}-linux-android", .{@tagName(arch)});
+ const sysroot = b.pathJoin(&.{ sdk_dir, "ndk", ndk_version, "toolchains/llvm/prebuilt/linux-x86_64/sysroot" });
+
+ const dvui_dep = sdk.dvuiDependency(b, .{
+ .target = target,
+ .optimize = optimize,
+ .backend = .sdl3,
+ .accesskit = .off,
+ .android_include_path = std.Build.LazyPath{ .cwd_relative = b.pathJoin(&.{ sysroot, "usr/include" }) },
+ });
+
+ const mod = b.createModule(.{
+ .root_source_file = b.path("android/src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ .pic = true, // linked into the app's JNI .so
+ });
+ mod.addImport("dvui", dvui_dep.module("dvui_sdl3"));
+ const hello = b.addLibrary(.{ .name = "fizzy_hello", .root_module = mod });
+ // Debug C code (SDL, stb) calls __ubsan_handle_*, which the NDK linker has no runtime for.
+ hello.bundle_ubsan_rt = true;
+ hello.bundle_compiler_rt = true; // ubsan_rt needs __extendxftf2, which the NDK lacks
+
+ // dvui builds its SDL3 only once its lazy `sdl3` dependency is fetched, which is after the first
+ // configure pass; `dvui_dep.artifact("SDL3")` panics on that pass instead of waiting.
+ const sdl3 = for (dvui_dep.builder.install_tls.step.dependencies.items) |dep| {
+ const install = dep.cast(std.Build.Step.InstallArtifact) orelse continue;
+ if (std.mem.eql(u8, install.artifact.name, "SDL3")) break install.artifact;
+ } else return;
+
+ // Zig ships no Android libc, so the NDK's clang links. SDL finds its JNI entry points and
+ // `SDL_main` by name at runtime, so both archives link whole.
+ const link = b.addSystemCommand(&.{ b.pathJoin(&.{ sysroot, "../bin/clang" }), b.fmt("--target={s}24", .{triple}), "-shared" });
+ const so = link.addPrefixedOutputFileArg("-o", "libfizzy_hello.so");
+ link.addArgs(&.{ "-s", "-Wl,--gc-sections", "-Wl,-z,max-page-size=16384", "-Wl,--whole-archive" });
+ link.addArtifactArg(hello);
+ link.addArtifactArg(sdl3);
+ link.addArgs(&.{
+ "-Wl,--no-whole-archive",
+ "-landroid",
+ "-llog",
+ "-lEGL",
+ "-lGLESv1_CM",
+ "-lGLESv2",
+ "-lOpenSLES",
+ "-ldl",
+ "-lm",
+ });
+
+ // SDL's Java side (the same source the native side is built from), dexed with FizzyActivity,
+ // beside the manifest and the library.
+ const package = b.addSystemCommand(&.{
+ "sh", "-ec",
+ \\so=$1 manifest=$2 activity=$3 sdl_java=$4 jar=$5 out=$6 bt=$7 abi=$8 keystore=$9 app_id=${10}
+ \\bt=$(cd "$bt" && pwd) # absolute: aapt runs from the staging directory
+ \\stage=$(mktemp -d); trap 'rm -rf "$stage"' EXIT
+ \\mkdir -p "$stage/classes" "$stage/lib/$abi"
+ \\javac --release 17 -cp "$jar" -d "$stage/classes" "$sdl_java"/org/libsdl/app/*.java "$activity"
+ \\"$bt/d8" --lib "$jar" --min-api 24 --output "$stage" $(find "$stage/classes" -name '*.class')
+ \\"$bt/aapt2" link -o "$stage/base.apk" --manifest "$manifest" -I "$jar" --min-sdk-version 24 --target-sdk-version 35 --rename-manifest-package "$app_id"
+ \\cp "$so" "$stage/lib/$abi/libfizzy_hello.so"
+ \\(cd "$stage" && "$bt/aapt" add base.apk classes.dex "lib/$abi/libfizzy_hello.so" >/dev/null)
+ \\"$bt/zipalign" -f -P 16 4 "$stage/base.apk" "$out"
+ \\"$bt/apksigner" sign --ks "$keystore" --ks-pass pass:android "$out"
+ ,
+ "package",
+ });
+ package.addFileArg(so);
+ package.addFileArg(b.path("android/AndroidManifest.xml"));
+ package.addFileArg(b.path("android/src/FizzyActivity.java"));
+ package.addDirectoryArg(dvui_dep.namedLazyPath("sdl3_android_java"));
+ package.addArg(b.pathJoin(&.{ sdk_dir, "platforms", platform, "android.jar" }));
+ const apk = package.addOutputFileArg("fizzy-hello.apk");
+ package.addArgs(&.{ b.pathJoin(&.{ sdk_dir, "build-tools", build_tools }), abi, keystore, app_id });
+
+ apk_step.dependOn(&b.addInstallFile(apk, "fizzy-hello.apk").step);
+}
diff --git a/build/app.zig b/build/app.zig
index 05307456a..bf90536c8 100644
--- a/build/app.zig
+++ b/build/app.zig
@@ -33,7 +33,14 @@ pub const WebPluginDir = struct {
/// The type of `Chrome.sidebar_edge`, for the build option.
const ChromeEdge = enum { left, right };
+/// Initial presentation preferences, used until the person makes a shared choice.
+pub const Look = struct {
+ /// Motion level from 0 (off) to 1 (playful). The OS reduced-motion request wins.
+ motion: ?f32 = null,
+};
+
pub const Options = struct {
+ look: Look = .{},
/// Every plugin the application links, fizzy's own among them, each in the executables its
/// `targets` names — see `build.zig`'s `buildApp`.
app_plugins: []const @import("sdk.zig").BundledPlugin = &@import("plugins.zig").entries.all,
@@ -490,6 +497,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options
build_opts.addOption(bool, "app_repo_url_set", app_repo_url_set);
build_opts.addOption([]const u8, "app_issues_url", app_issues_url);
build_opts.addOption(bool, "velopack_enabled", velopack_enabled);
+ build_opts.addOption(?f32, "look_motion", opts.look.motion);
// Off, the app runs only the plugins it links (`buildApp`'s list): no plugin dylib is built,
// and nothing loads at runtime — not fizzy's own dylibs, not the user plugins directory, not
// the store, not a web page's `?plugin=`. Comptime, so none of it is checked per frame.
@@ -807,6 +815,7 @@ fn native(
pack_opts.addOption(bool, "app_repo_url_set", cfg.app_repo_url_set);
pack_opts.addOption([]const u8, "app_issues_url", cfg.app_issues_url);
pack_opts.addOption(bool, "velopack_enabled", true);
+ pack_opts.addOption(?f32, "look_motion", opts.look.motion);
pack_opts.addOption(bool, "static_workbench", static_workbench);
pack_opts.addOption(bool, "static_text", static_text);
pack_opts.addOption(bool, "static_image", static_image);
@@ -1098,6 +1107,7 @@ fn addTests(
// Hold / slop / tap rules of touch selection. std-only (see TouchHold.zig) — the dvui
// half is widgets/SelectableText.zig.
.{ "fizzy-touch-hold-tests", "core/TouchHold.zig" },
+ .{ "fizzy-person-tests", "core/person.zig" },
// A motion's clock stepped by its frames, a long one held to a step. std-only (see
// FrameClock.zig); the floats' landing and ghost run on it.
.{ "fizzy-frame-clock-tests", "backend/src/glass/FrameClock.zig" },
@@ -1124,6 +1134,8 @@ fn addTests(
// Where a new view goes by the shape of the room, and which way an adaptive split runs.
// std-only (see placement.zig); the workbench and the docking widget ask it.
.{ "fizzy-placement-tests", "core/placement.zig" },
+ // Keyed layout motion and release arithmetic, independent of drawing.
+ .{ "fizzy-rearrange-tests", "core/rearrange.zig" },
// A document state as the bytes that differ from another: how a track's checkpoints
// stay small. std-only (see state_delta.zig); `Tracks` keeps its checkpoints with it.
.{ "fizzy-state-delta-tests", "app/state_delta.zig" },
@@ -1203,7 +1215,7 @@ fn addTests(
tour_tests.root_module.addImport("tape", b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("sdk/tape/root.zig"),
+ .root_source_file = b.dependency("fizzy_sdk", .{}).path("tape/root.zig"),
}));
try unit_test_artifacts.append(b.allocator, tour_tests);
@@ -1262,7 +1274,7 @@ fn addTests(
keymap_module.addImport("tape", b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("sdk/tape/root.zig"),
+ .root_source_file = b.dependency("fizzy_sdk", .{}).path("tape/root.zig"),
}));
try unit_test_artifacts.append(b.allocator, b.addTest(.{
.name = "fizzy-keymap-tests",
@@ -1344,7 +1356,7 @@ fn addTests(
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("sdk/remote/fake_fizzy.zig"),
+ .root_source_file = b.dependency("fizzy_sdk", .{}).path("remote/fake_fizzy.zig"),
}),
});
const fake_options = b.addOptions();
@@ -1354,7 +1366,7 @@ fn addTests(
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("sdk/remote/root.zig"),
+ .root_source_file = b.dependency("fizzy_sdk", .{}).path("remote/root.zig"),
.imports = &.{.{ .name = "fake_fizzy_options", .module = fake_options.createModule() }},
}),
.filters = test_filters,
@@ -1463,13 +1475,13 @@ fn addTests(
const replay_only = b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("sdk/replay/root.zig"),
+ .root_source_file = b.dependency("fizzy_sdk", .{}).path("replay/root.zig"),
});
replay_only.addImport("dvui", dvui_testing_dep.module("dvui_testing"));
replay_only.addImport("tape", b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("sdk/tape/root.zig"),
+ .root_source_file = b.dependency("fizzy_sdk", .{}).path("tape/root.zig"),
}));
const replay_tests = b.addTest(.{
.name = "fizzy-replay-tests",
@@ -2055,7 +2067,7 @@ fn addTests(
bench_module.addImport("tape", b.createModule(.{
.target = target,
.optimize = optimize,
- .root_source_file = b.path("sdk/tape/root.zig"),
+ .root_source_file = b.dependency("fizzy_sdk", .{}).path("tape/root.zig"),
}));
const bench_tape = b.addTest(.{ .name = "fizzy-bench-tape", .root_module = bench_module });
diff --git a/core/anim.zig b/core/anim.zig
index b563b165c..ce89360ae 100644
--- a/core/anim.zig
+++ b/core/anim.zig
@@ -19,6 +19,7 @@ const BlurBackdrop = @import("glass").BlurBackdrop;
const dialogs = @import("dialogs.zig");
const FrameTarget = @import("glass").FrameTarget;
pub const crossfade = @import("crossfade.zig");
+pub const rearrange = @import("rearrange.zig");
pub const Kind = crossfade.Kind;
/// How a pane *travels* when the layout moves it: a region folding away, a sidebar coming back,
diff --git a/core/core.zig b/core/core.zig
index bd4cc23ba..856d1a507 100644
--- a/core/core.zig
+++ b/core/core.zig
@@ -144,5 +144,8 @@ pub const transport = struct {
/// Fixed Fizzy accent colours — theme-independent. See `palette.zig`.
pub const palette = @import("palette.zig");
+/// Preferences shared by applications, independent of app-local settings.
+pub const person = @import("person.zig");
+
/// Input timestamps shared through the dvui window, including plugin images.
pub const input_time = @import("input_time.zig");
diff --git a/core/person.zig b/core/person.zig
new file mode 100644
index 000000000..5c3fdbd1e
--- /dev/null
+++ b/core/person.zig
@@ -0,0 +1,68 @@
+//! The person's preferences, shared by every application built on fizzy.
+const std = @import("std");
+const builtin = @import("builtin");
+const fs = @import("fs.zig");
+
+pub const Person = struct {
+ motion: ?f32 = null,
+};
+
+/// Missing or invalid preferences leave the application's defaults in effect.
+pub fn load(io: std.Io, dir: []const u8) Person {
+ // Preferences contain one scalar; bounded scratch also bounds malformed input.
+ var scratch: [8192]u8 = undefined;
+ var fixed = std.heap.FixedBufferAllocator.init(&scratch);
+ const gpa = fixed.allocator();
+ const path = if (builtin.target.cpu.arch == .wasm32) "fizzyedit.person" else std.fs.path.join(gpa, &.{ dir, "person.zon" }) catch return .{};
+ const data = fs.readZ(gpa, io, path) catch return .{};
+ var diag: std.zon.parse.Diagnostics = undefined;
+ const person = std.zon.parse.fromSlice(Person, .{ .gpa = gpa, .arena = gpa, .source = data, .diagnostics = &diag, .ignore_unknown_fields = true }) catch return .{};
+ if (person.motion) |level| if (!std.math.isFinite(level) or level < 0 or level > 1) return .{};
+ return person;
+}
+
+/// Atomically replaces the preferences. Creates `dir` if needed on desktop.
+pub fn save(io: std.Io, dir: []const u8, person: Person) !void {
+ var buffer: [128]u8 = undefined;
+ const data = if (person.motion) |level|
+ try std.mem.print(&buffer, ".{{ .motion = {d} }}", .{level})
+ else
+ ".{}";
+ if (builtin.target.cpu.arch == .wasm32) return fs.write(io, "fizzyedit.person", data);
+ const cwd = std.Io.Dir.cwd();
+ try cwd.createDirPath(io, dir);
+ var folder = try cwd.openDir(io, dir, .{});
+ defer folder.close(io);
+ var atomic = try folder.createFileAtomic(io, "person.zon", .{ .replace = true });
+ defer atomic.deinit(io);
+ try atomic.file.writeStreamingAll(io, data);
+ try atomic.replace(io);
+}
+
+/// The person's choice supersedes an application's initial look.
+pub fn motionLevel(person: Person, app_default: ?f32) f32 {
+ return person.motion orelse app_default orelse 1;
+}
+
+test "preferences round trip, invalid files, and atomic replacement" {
+ const io = std.testing.io;
+ var tmp = std.testing.tmpDir(.{ .iterate = true });
+ defer tmp.cleanup();
+ const dir = try tmp.dir.realPathFileAlloc(io, ".", std.testing.allocator);
+ defer std.testing.allocator.free(dir);
+ try std.testing.expectEqual(Person{}, load(io, dir));
+ try tmp.dir.writeFile(io, .{ .sub_path = "person.zon", .data = "garbage" });
+ try std.testing.expectEqual(Person{}, load(io, dir));
+ try tmp.dir.writeFile(io, .{ .sub_path = "person.zon", .data = ".{ .motion = 0.4, .future = true }" });
+ try std.testing.expectEqual(@as(?f32, 0.4), load(io, dir).motion);
+ try save(io, dir, .{ .motion = 0.4 });
+ try std.testing.expectEqual(@as(?f32, 0.4), load(io, dir).motion);
+ try save(io, dir, .{ .motion = 0.8 });
+ var iter = tmp.dir.iterate();
+ const entry = (try iter.next(io)).?;
+ try std.testing.expectEqualStrings("person.zon", entry.name);
+ try std.testing.expectEqual(@as(?std.Io.Dir.Entry, null), try iter.next(io));
+ try std.testing.expectEqual(@as(f32, 0.3), motionLevel(.{}, 0.3));
+ try std.testing.expectEqual(@as(f32, 0.8), motionLevel(load(io, dir), 0.3));
+ try std.testing.expectEqual(@as(f32, 0.8), motionLevel(load(io, dir), 1));
+}
diff --git a/core/rearrange.zig b/core/rearrange.zig
new file mode 100644
index 000000000..5c339c659
--- /dev/null
+++ b/core/rearrange.zig
@@ -0,0 +1,503 @@
+//! Keyed layout motion, independent of drawing and of the track's motion model.
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+/// An item's identity across layouts, chosen by the app.
+pub const Key = u64;
+/// Container-local coordinates in points; scrolling animates nothing.
+pub const Rect = struct { x: f32 = 0, y: f32 = 0, w: f32 = 0, h: f32 = 0 };
+/// Track lanes: x, y, w, h, presence (0 gone, 1 fully there).
+pub const lanes = 5;
+pub const Phase = enum {
+ /// First placement: invisible while the layout measures it.
+ measuring,
+ entering,
+ present,
+ /// Absent from the layout; retired when presence reaches zero.
+ leaving,
+};
+pub const Shown = struct { rect: Rect, presence: f32, phase: Phase };
+pub const EndOptions = struct {
+ /// Resizing follows the layout immediately and retires leaving items.
+ follow: bool = false,
+};
+
+/// Owns each item's track. Track needs pos: [lanes]f32, snap(to), and
+/// step(target, now_ns), returning true while moving. Repeated times must move nothing.
+/// The owner releases all storage with deinit using the same allocator as place/end.
+// lint-allow(fn-length): type factory contains the set declarations; each method is under 150 lines.
+pub fn Rearrange(comptime Track: type) type {
+ if (!@hasField(Track, "pos")) @compileError("Rearrange Track needs field pos");
+ if (@FieldType(Track, "pos") != [lanes]f32) @compileError("Rearrange Track.pos must be [lanes]f32");
+ if (!@hasDecl(Track, "snap")) @compileError("Rearrange Track needs snap");
+ if (!@hasDecl(Track, "step")) @compileError("Rearrange Track needs step");
+ return struct {
+ const Self = @This();
+ pub const Entry = struct {
+ track: Track,
+ /// The destination currently chased by the track.
+ target: Rect,
+ /// This frame's placement, applied unless measurement holds the frame.
+ next: Rect,
+ phase: Phase,
+ };
+ items: std.AutoArrayHashMapUnmanaged(Key, Entry) = .empty,
+ /// Keys placed since begin, in layout order.
+ placed: std.ArrayList(Key) = .empty,
+ // Membership stays constant-time without changing the public entry shape.
+ placed_set: std.AutoHashMapUnmanaged(Key, void) = .empty,
+ // A key joins after its first measuring frame; removed with its entry.
+ measured: std.AutoHashMapUnmanaged(Key, void) = .empty,
+ moving: bool = false,
+ ended: bool = false,
+ last_end_ns: i128 = 0,
+
+ pub fn deinit(self: *Self, gpa: Allocator) void {
+ self.items.deinit(gpa);
+ self.placed.deinit(gpa);
+ self.placed_set.deinit(gpa);
+ self.measured.deinit(gpa);
+ self.* = .{};
+ }
+
+ /// Start a frame, forgetting which keys were placed.
+ pub fn begin(self: *Self) void {
+ self.ended = false;
+ self.placed.clearRetainingCapacity();
+ self.placed_set.clearRetainingCapacity();
+ }
+
+ /// Last placement wins for duplicates; their first position in layout order is kept.
+ pub fn place(self: *Self, gpa: Allocator, key: Key, target: Rect) Allocator.Error!void {
+ // Reserve before mutation so allocation failure leaves placement consistent.
+ try self.items.ensureUnusedCapacity(gpa, 1);
+ try self.placed.ensureUnusedCapacity(gpa, 1);
+ try self.placed_set.ensureUnusedCapacity(gpa, 1);
+ const result = self.items.getOrPutAssumeCapacity(key);
+ if (!result.found_existing) {
+ var track: Track = .{};
+ track.snap(values(target, 0));
+ result.value_ptr.* = .{ .track = track, .target = target, .next = target, .phase = .measuring };
+ }
+ result.value_ptr.next = target;
+ const placement = self.placed_set.getOrPutAssumeCapacity(key);
+ if (!placement.found_existing) self.placed.appendAssumeCapacity(key);
+ }
+
+ /// Diff and step every track. True while anything moves or measures.
+ pub fn end(self: *Self, gpa: Allocator, now_ns: i128, opts: EndOptions) Allocator.Error!bool {
+ if (self.ended and self.last_end_ns == now_ns and !opts.follow) return !self.settled();
+ // A measuring entry created by place has not survived an end yet. Keep that
+ // distinction outside Entry so its public shape stays the track/rect/phase seam.
+ var hold = false;
+ for (self.placed.items) |key| {
+ const entry = self.items.getPtr(key).?;
+ if (entry.phase == .measuring and !self.measured.contains(key)) hold = true;
+ }
+ try self.measured.ensureUnusedCapacity(gpa, @intCast(self.placed.items.len));
+ self.ended = true;
+ self.last_end_ns = now_ns;
+ self.moving = false;
+ var i = self.items.count();
+ // Reverse ordered removal preserves insertion order and unvisited indices.
+ while (i > 0) {
+ i -= 1;
+ const key = self.items.keys()[i];
+ const entry = &self.items.values()[i];
+ const placed = self.placed_set.contains(key);
+ if (!placed) {
+ if (opts.follow or entry.phase == .measuring) {
+ self.items.orderedRemoveAt(i);
+ _ = self.measured.remove(key);
+ continue;
+ }
+ if (entry.phase != .leaving) {
+ // Exit from the pose the person saw, even halfway through a move.
+ entry.target = rectOf(entry.track.pos);
+ entry.phase = .leaving;
+ }
+ } else if (opts.follow) {
+ entry.target = entry.next;
+ entry.track.snap(values(entry.next, 1));
+ entry.phase = .present;
+ continue;
+ } else if (entry.phase == .measuring) {
+ if (!self.measured.contains(key)) {
+ entry.target = entry.next;
+ entry.track.snap(values(entry.next, 0));
+ self.measured.putAssumeCapacity(key, {});
+ _ = entry.track.step(values(entry.target, 0), now_ns);
+ self.moving = true;
+ continue;
+ }
+ entry.track.snap(values(entry.next, 0));
+ entry.target = entry.next;
+ entry.phase = .entering;
+ } else if (entry.phase == .leaving) {
+ entry.phase = .present;
+ }
+ if (placed and !hold) entry.target = entry.next;
+ const moving = entry.track.step(values(entry.target, if (placed) 1 else 0), now_ns);
+ if (entry.phase == .leaving and !moving and entry.track.pos[4] <= 0.001) {
+ self.items.orderedRemoveAt(i);
+ _ = self.measured.remove(key);
+ continue;
+ }
+ if (entry.phase == .entering and !moving) entry.phase = .present;
+ self.moving = self.moving or moving;
+ }
+ return !self.settled();
+ }
+
+ /// Draw pose, or null after retirement. The result owns no storage.
+ pub fn shown(self: *const Self, key: Key) ?Shown {
+ const entry = self.items.get(key) orelse return null;
+ return .{ .rect = rectOf(entry.track.pos), .presence = entry.track.pos[4], .phase = entry.phase };
+ }
+
+ /// Replaces out with leaving keys in insertion order, then placed keys in layout order.
+ /// The caller owns out and frees it with gpa.
+ pub fn drawOrder(self: *const Self, gpa: Allocator, out: *std.ArrayList(Key)) Allocator.Error!void {
+ out.clearRetainingCapacity();
+ try out.ensureTotalCapacity(gpa, self.items.count());
+ for (self.items.keys(), self.items.values()) |key, entry| {
+ if (entry.phase == .leaving and !self.placed_set.contains(key)) out.appendAssumeCapacity(key);
+ }
+ for (self.placed.items) |key| {
+ if (self.items.contains(key)) out.appendAssumeCapacity(key);
+ }
+ }
+
+ /// Nothing is measuring, entering, leaving or moving.
+ pub fn settled(self: *const Self) bool {
+ if (self.moving) return false;
+ for (self.items.values()) |entry| {
+ if (entry.phase != .present) return false;
+ }
+ return true;
+ }
+
+ /// Give placed, visible tracks this velocity without moving their poses.
+ /// Track.kick is required only when this method is used.
+ pub fn fling(self: *Self, vel: [lanes]f32) void {
+ if (!@hasDecl(Track, "kick")) @compileError("Rearrange Track needs kick for fling");
+ for (self.placed.items) |key| {
+ const entry = self.items.getPtr(key).?;
+ if (entry.phase != .measuring and entry.phase != .leaving) entry.track.kick(vel);
+ }
+ }
+ };
+}
+
+/// Release policy for a row of items.
+pub const Snap = struct {
+ /// Coast decay in inverse seconds, matching Fling.Tuning.decay.
+ decay: f32 = 4.0,
+ /// Minimum items/s that guarantees movement in the release direction.
+ min_flick: f32 = 0.6,
+ /// Maximum distance from the item shown at press; null leaves it uncapped.
+ max_step: ?u32 = null,
+};
+
+/// Released item index, with pos in items and vel in items/s. Empty rows return zero.
+/// Assumes finite pos/vel, positive finite decay and nonnegative finite min_flick.
+pub fn snapIndex(from: u32, pos: f32, vel: f32, count: u32, s: Snap) u32 {
+ if (count <= 1) return 0;
+ // Saturation keeps extreme finite inputs safe before signed arithmetic and clamping.
+ const limit: f64 = @floatFromInt(std.math.maxInt(i64) - std.math.maxInt(u32));
+ const rest = @as(f64, pos) + @as(f64, vel) / s.decay;
+ var index: i64 = @intFromFloat(std.math.clamp(@round(rest), -limit, limit));
+ if (@abs(vel) >= s.min_flick) {
+ const edge = if (vel > 0) @floor(@as(f64, pos)) + 1 else @ceil(@as(f64, pos)) - 1;
+ const flick: i64 = @intFromFloat(std.math.clamp(edge, -limit, limit));
+ if (vel > 0) index = @max(index, flick) else if (vel < 0) index = @min(index, flick);
+ }
+ if (s.max_step) |cap| index = std.math.clamp(index, @as(i64, from) - cap, @as(i64, from) + cap);
+ return @intCast(std.math.clamp(index, 0, @as(i64, count) - 1));
+}
+
+fn values(rect: Rect, presence: f32) [lanes]f32 {
+ return .{ rect.x, rect.y, rect.w, rect.h, presence };
+}
+
+fn rectOf(pos: [lanes]f32) Rect {
+ return .{ .x = pos[0], .y = pos[1], .w = pos[2], .h = pos[3] };
+}
+
+const Linear = struct {
+ pos: [lanes]f32 = @splat(0),
+ time: ?i128 = null,
+ kicked: ?[lanes]f32 = null,
+
+ pub fn snap(self: *Linear, to: [lanes]f32) void {
+ self.pos = to;
+ }
+
+ pub fn step(self: *Linear, target: [lanes]f32, now_ns: i128) bool {
+ const elapsed: f32 = @floatFromInt(@max(0, now_ns - (self.time orelse now_ns)));
+ self.time = now_ns;
+ var moving = false;
+ for (&self.pos, target, 0..) |*pos, to, lane| {
+ // Rect lanes travel one point/ms; presence travels 0.01/ms.
+ const distance = elapsed / std.time.ns_per_ms * (if (lane == 4) @as(f32, 0.01) else 1);
+ pos.* += std.math.clamp(to - pos.*, -distance, distance);
+ moving = moving or pos.* != to;
+ }
+ return moving;
+ }
+
+ pub fn kick(self: *Linear, vel: [lanes]f32) void {
+ self.kicked = vel;
+ }
+};
+const testing = std.testing;
+const Set = Rearrange(Linear);
+const test_allocator = testing.allocator;
+const ms = std.time.ns_per_ms;
+
+fn frame(set: *Set, keys: []const Key, xs: []const f32, time: i128, follow: bool) !void {
+ set.begin();
+ std.debug.assert(keys.len == xs.len);
+ for (keys, xs) |key, x| try set.place(test_allocator, key, .{ .x = x });
+ _ = try set.end(test_allocator, time * ms, .{ .follow = follow });
+}
+
+test "rearrange: a key placed in both layouts moves from its shown rect to its new target" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ try frame(&set, &.{1}, &.{0}, 0, false);
+ try frame(&set, &.{1}, &.{0}, 100, false);
+ try frame(&set, &.{1}, &.{100}, 150, false);
+ try testing.expectEqual(@as(f32, 50), set.shown(1).?.rect.x);
+ try frame(&set, &.{1}, &.{100}, 200, false);
+ try testing.expectEqual(@as(f32, 100), set.shown(1).?.rect.x);
+ try testing.expect(set.settled());
+}
+
+test "rearrange: a new key is measured for one frame and holds the others" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ try frame(&set, &.{1}, &.{0}, 0, false);
+ try frame(&set, &.{1}, &.{0}, 100, false);
+ try frame(&set, &.{ 2, 1 }, &.{ 20, 100 }, 150, false);
+ try testing.expectEqual(Phase.measuring, set.shown(2).?.phase);
+ try testing.expectEqual(@as(f32, 0), set.shown(2).?.presence);
+ try testing.expectEqual(@as(f32, 0), set.shown(1).?.rect.x);
+ try frame(&set, &.{ 2, 1 }, &.{ 20, 100 }, 200, false);
+ try testing.expectEqual(Phase.entering, set.shown(2).?.phase);
+ try testing.expectEqual(@as(f32, 50), set.shown(1).?.rect.x);
+ // Another measuring key holds the destination, but an existing move keeps stepping.
+ try frame(&set, &.{ 3, 2, 1 }, &.{ 30, 20, 500 }, 225, false);
+ try testing.expectEqual(@as(f32, 75), set.shown(1).?.rect.x);
+ try testing.expectEqual(@as(f32, 100), set.items.get(1).?.target.x);
+}
+
+test "rearrange: a key left out leaves from where it was shown and is retired at presence 0" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ try frame(&set, &.{1}, &.{0}, 0, false);
+ try frame(&set, &.{1}, &.{0}, 100, false);
+ try frame(&set, &.{1}, &.{100}, 150, false);
+ try frame(&set, &.{}, &.{}, 175, false);
+ try testing.expectEqual(@as(f32, 50), set.shown(1).?.rect.x);
+ try testing.expectEqual(@as(f32, 0.75), set.shown(1).?.presence);
+ try frame(&set, &.{}, &.{}, 250, false);
+ try testing.expectEqual(null, set.shown(1));
+}
+
+test "rearrange: a key placed again while leaving turns around without a jump" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ try frame(&set, &.{1}, &.{0}, 0, false);
+ try frame(&set, &.{1}, &.{0}, 100, false);
+ try frame(&set, &.{}, &.{}, 150, false);
+ const before = set.shown(1).?;
+ try frame(&set, &.{1}, &.{100}, 151, false);
+ const after = set.shown(1).?;
+ try testing.expect(@abs(after.rect.x - before.rect.x) <= 1);
+ try testing.expect(@abs(after.presence - before.presence) <= 0.010001);
+ try testing.expectEqual(Phase.present, after.phase);
+}
+
+test "rearrange: follow puts every item on its target in one frame" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ try frame(&set, &.{ 1, 2, 3, 4 }, &.{ 0, 0, 0, 0 }, 0, false);
+ try frame(&set, &.{ 1, 2, 3, 4 }, &.{ 100, 200, 300, 400 }, 50, false);
+ try frame(&set, &.{ 1, 2, 3, 5 }, &.{ 100, 200, 300, 500 }, 75, true);
+ for ([_]Key{ 1, 2, 3, 5 }, [_]f32{ 100, 200, 300, 500 }) |key, x| {
+ try testing.expectEqual(Shown{ .rect = .{ .x = x }, .presence = 1, .phase = .present }, set.shown(key).?);
+ }
+ try testing.expectEqual(null, set.shown(4));
+ try testing.expect(set.settled());
+}
+
+test "rearrange: draw order is leaving first, then layout order" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ var order: std.ArrayList(Key) = .empty;
+ defer order.deinit(test_allocator);
+ try frame(&set, &.{ 3, 1, 2 }, &.{ 0, 0, 0 }, 0, false);
+ try frame(&set, &.{ 3, 1, 2 }, &.{ 0, 0, 0 }, 100, false);
+ try frame(&set, &.{ 3, 2 }, &.{ 0, 0 }, 150, false);
+ try set.drawOrder(test_allocator, &order);
+ try testing.expectEqualSlices(Key, &.{ 1, 3, 2 }, order.items);
+}
+
+test "rearrange: a measuring key not placed again is dropped, never drawn" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ var order: std.ArrayList(Key) = .empty;
+ defer order.deinit(test_allocator);
+ try frame(&set, &.{1}, &.{0}, 0, false);
+ try frame(&set, &.{}, &.{}, 1, false);
+ try testing.expectEqual(null, set.shown(1));
+ try set.drawOrder(test_allocator, &order);
+ try testing.expectEqual(@as(usize, 0), order.items.len);
+}
+
+test "rearrange: the same frame time twice moves nothing" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ try frame(&set, &.{ 1, 2 }, &.{ 0, 0 }, 0, false);
+ const measuring = set.shown(1).?;
+ _ = try set.end(test_allocator, 0, .{});
+ try testing.expectEqual(measuring, set.shown(1).?);
+ try frame(&set, &.{ 1, 2 }, &.{ 0, 0 }, 100, false);
+ try frame(&set, &.{ 1, 2 }, &.{ 100, 200 }, 150, false);
+ const before = [_]Shown{ set.shown(1).?, set.shown(2).? };
+ _ = try set.end(test_allocator, 150 * ms, .{});
+ try testing.expectEqual(before, [_]Shown{ set.shown(1).?, set.shown(2).? });
+}
+
+test "rearrange: fling gives placed tracks the speed, not leaving or measuring ones" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ try frame(&set, &.{ 1, 2 }, &.{ 0, 0 }, 0, false);
+ try frame(&set, &.{ 1, 2 }, &.{ 0, 0 }, 100, false);
+ try frame(&set, &.{1}, &.{0}, 125, false);
+ set.begin();
+ try set.place(test_allocator, 1, .{});
+ try set.place(test_allocator, 2, .{});
+ try set.place(test_allocator, 3, .{});
+ const velocity: [lanes]f32 = .{ 300, 0, 0, 0, 0 };
+ set.fling(velocity);
+ try testing.expectEqual(velocity, set.items.get(1).?.track.kicked.?);
+ try testing.expectEqual(null, set.items.get(2).?.track.kicked);
+ try testing.expectEqual(null, set.items.get(3).?.track.kicked);
+}
+
+test "rearrange: snapIndex" {
+ const Case = struct { pos: f32, vel: f32 = 0, from: u32 = 2, count: u32 = 5, cap: ?u32 = null, want: u32 };
+ const cases = [_]Case{
+ .{ .pos = 2.4, .want = 2 }, // Slow release below half.
+ .{ .pos = 2.6, .want = 3 }, // Slow release above half.
+ .{ .pos = 2.1, .vel = 0.7, .want = 3 }, // Positive flick beats rounding.
+ .{ .pos = 2.9, .vel = -0.7, .want = 2 }, // Negative flick beats rounding.
+ .{ .pos = 2, .vel = 40, .cap = 1, .want = 3 }, // Pager caps a hard flick.
+ .{ .pos = 2, .vel = 40, .want = 4 }, // Uncapped flick reaches the end.
+ .{ .pos = 0.2, .vel = -40, .want = 0 }, // Lower bound.
+ .{ .pos = 2, .count = 0, .want = 0 }, // Empty row.
+ .{ .pos = 100, .vel = 40, .count = 1, .want = 0 }, // Singleton row.
+ };
+ for (cases) |case| try testing.expectEqual(case.want, snapIndex(case.from, case.pos, case.vel, case.count, .{ .max_step = case.cap }));
+}
+
+test "rearrange: a press on a moving row keeps every shown rect" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ const keys = [_]Key{ 1, 2, 3 };
+ try frame(&set, &keys, &.{ 0, 0, 0 }, 0, false);
+ try frame(&set, &keys, &.{ 0, 0, 0 }, 100, false);
+ try frame(&set, &keys, &.{ 100, 200, 300 }, 150, false);
+ const poses = [_]Rect{ set.shown(1).?.rect, set.shown(2).?.rect, set.shown(3).?.rect };
+ set.begin();
+ for (keys, poses) |key, pose| try set.place(test_allocator, key, pose);
+ set.fling(@splat(0));
+ try testing.expect(!try set.end(test_allocator, 151 * ms, .{ .follow = true }));
+ for (keys, poses) |key, pose| try testing.expectEqual(pose, set.shown(key).?.rect);
+ try testing.expect(set.settled());
+}
+
+test "rearrange: duplicate placement keeps the last rect and draws once" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ var order: std.ArrayList(Key) = .empty;
+ defer order.deinit(test_allocator);
+ set.begin();
+ try set.place(test_allocator, 1, .{ .x = 10 });
+ try set.place(test_allocator, 1, .{ .x = 20 });
+ _ = try set.end(test_allocator, 0, .{});
+ try testing.expectEqual(@as(f32, 20), set.shown(1).?.rect.x);
+ try set.drawOrder(test_allocator, &order);
+ try testing.expectEqualSlices(Key, &.{1}, order.items);
+}
+
+test "rearrange: a track without kick can run a set" {
+ const NoKick = struct {
+ pos: [lanes]f32 = @splat(0),
+ pub fn snap(self: *@This(), to: [lanes]f32) void {
+ self.pos = to;
+ }
+ pub fn step(self: *@This(), target: [lanes]f32, _: i128) bool {
+ self.pos = target;
+ return false;
+ }
+ };
+ var set: Rearrange(NoKick) = .{};
+ defer set.deinit(test_allocator);
+ set.begin();
+ try set.place(test_allocator, 1, .{});
+ try testing.expect(!try set.end(test_allocator, 0, .{ .follow = true }));
+}
+
+test "rearrange: allocation failures release all owned storage" {
+ try testing.checkAllAllocationFailures(test_allocator, allocationFailure, .{});
+}
+
+fn allocationFailure(allocator: Allocator) !void {
+ var set: Set = .{};
+ defer set.deinit(allocator);
+ var order: std.ArrayList(Key) = .empty;
+ defer order.deinit(allocator);
+ set.begin();
+ try set.place(allocator, 1, .{ .x = 10 });
+ _ = try set.end(allocator, 0, .{});
+ try set.drawOrder(allocator, &order);
+}
+
+test "rearrange: measurement uses the second layout's complete rect" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ set.begin();
+ try set.place(test_allocator, 1, .{ .x = 10, .y = 20, .w = 30, .h = 40 });
+ _ = try set.end(test_allocator, 0, .{});
+ const measured: Rect = .{ .x = 100, .y = 200, .w = 300, .h = 400 };
+ set.begin();
+ try set.place(test_allocator, 1, measured);
+ try testing.expect(try set.end(test_allocator, 50 * ms, .{}));
+ const shown = set.shown(1).?;
+ try testing.expectEqual(measured, shown.rect);
+ try testing.expectEqual(@as(f32, 0.5), shown.presence);
+ try testing.expectEqual(Phase.entering, shown.phase);
+}
+
+test "rearrange: follow retires leaving items and reveals measuring items" {
+ var set: Set = .{};
+ defer set.deinit(test_allocator);
+ try frame(&set, &.{ 1, 2 }, &.{ 0, 0 }, 0, false);
+ try frame(&set, &.{ 1, 2 }, &.{ 0, 0 }, 100, false);
+ try frame(&set, &.{ 2, 3 }, &.{ 100, 200 }, 125, false);
+ try testing.expectEqual(Phase.leaving, set.shown(1).?.phase);
+ try testing.expectEqual(Phase.measuring, set.shown(3).?.phase);
+ set.begin();
+ const target: Rect = .{ .x = 20, .y = 30, .w = 40, .h = 50 };
+ try set.place(test_allocator, 2, target);
+ try set.place(test_allocator, 3, target);
+ try testing.expect(!try set.end(test_allocator, 126 * ms, .{ .follow = true }));
+ try testing.expectEqual(null, set.shown(1));
+ for ([_]Key{ 2, 3 }) |key| {
+ try testing.expectEqual(Shown{ .rect = target, .presence = 1, .phase = .present }, set.shown(key).?);
+ }
+ try testing.expect(set.settled());
+}
diff --git a/core/widgets/ScrollArea.zig b/core/widgets/ScrollArea.zig
index af99281d7..064f53715 100644
--- a/core/widgets/ScrollArea.zig
+++ b/core/widgets/ScrollArea.zig
@@ -622,6 +622,7 @@ test "coast displacement follows each frame gap" {
var previous_y = t_drawn_y;
// frameAfter draws at the current timestamp, then begins the next frame with this gap.
// Alternating long and short gaps exposes a displacement applied one frame late.
+ var max_error: f32 = 0;
for ([_]u64{ 8, 25, 8, 25, 8, 25, 8, 25 }) |next_gap| {
const time = dvui.currentWindow().frame_time_ns;
try frameAfter(next_gap);
@@ -630,11 +631,15 @@ test "coast displacement follows each frame gap" {
const to_s = @as(f64, @floatFromInt(time - release_time)) / std.time.ns_per_s;
const expected: f32 = @floatCast(release_speed / tuning.decay *
(@exp(-tuning.decay * from_s) - @exp(-tuning.decay * to_s)));
+ std.debug.print("coast frame: gap={d} ms, drawn={d:.6} px, integral={d:.6} px, error={d:.6} px\n", .{
+ @divTrunc(time - previous_time, std.time.ns_per_ms), displacement, expected, displacement - expected,
+ });
// Check what reaches the renderer against the release integral, including its decay.
- try std.testing.expectApproxEqAbs(expected, displacement, 0.0001);
+ max_error = @max(max_error, @abs(displacement - expected));
previous_time = time;
previous_y = t_drawn_y;
}
+ try std.testing.expectApproxEqAbs(@as(f32, 0), max_error, 0.0001);
}
test "a coast requests another frame even when its timestamp has not advanced" {
diff --git a/plugins/dashboard/build.zig.zon b/plugins/dashboard/build.zig.zon
index 071dea80b..afe1f2a69 100644
--- a/plugins/dashboard/build.zig.zon
+++ b/plugins/dashboard/build.zig.zon
@@ -10,7 +10,7 @@
"src",
"static",
},
- .fingerprint = 0x6b1f0c2d9a3e5481,
+ .fingerprint = 0x5c94fff8587ea711,
.dependencies = .{
.fizzy = .{
.path = "../../sdk",
diff --git a/scripts/lint-allow.zon b/scripts/lint-allow.zon
index 2b5042a93..1783264d5 100644
--- a/scripts/lint-allow.zon
+++ b/scripts/lint-allow.zon
@@ -103,6 +103,7 @@
.{ .rule = .@"raw-scroll-area", .file = "src/editor/dialogs/FileTypeDefaults.zig", .count = 1 },
.{ .rule = .@"raw-scroll-area", .file = "src/editor/dialogs/PluginUpdates.zig", .count = 1 },
.{ .rule = .@"raw-scroll-area", .file = "tests/integration.zig", .count = 3 },
+ .{ .rule = .@"raw-button", .file = "android/src/main.zig", .count = 1 },
.{ .rule = .@"raw-button", .file = "app/ActivityFeed.zig", .count = 1 },
.{ .rule = .@"raw-button", .file = "app/layout/Picker.zig", .count = 3 },
.{ .rule = .@"raw-button", .file = "app/layout/Region.zig", .count = 1 },
diff --git a/scripts/lint.zig b/scripts/lint.zig
index ccfbd7ce9..8155e8a56 100644
--- a/scripts/lint.zig
+++ b/scripts/lint.zig
@@ -870,9 +870,11 @@ test "dvui.scrollArea is flagged" {
test "a lint-allow with a reason silences it" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
- const findings = try analyze(arena.allocator(), "// lint-allow(raw-scroll-area): wrapper implementation\n" ++
- "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); }");
- try std.testing.expectEqual(@as(usize, 0), findings.len);
+ for ([_][:0]const u8{
+ "// lint-allow(raw-scroll-area): wrapper implementation\n" ++
+ "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); }",
+ "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); } // lint-allow(raw-scroll-area): wrapper implementation",
+ }) |source| try std.testing.expectEqual(@as(usize, 0), (try analyze(arena.allocator(), source)).len);
}
test "core.widgets.ScrollArea.init is not flagged" {
@@ -896,6 +898,9 @@ test "a file over its raw-scroll-area budget fails" {
try std.testing.expect(try reportFile(&output.writer, "over.zig", findings, &allowances, &seen, false));
try std.testing.expect(seen[0]);
try std.testing.expect(std.mem.endsWith(u8, output.written(), "raw-scroll-area: count 2 exceeds allowance 1\n"));
+ output.clearRetainingCapacity();
+ try std.testing.expect(!try reportFile(&output.writer, "over.zig", findings[0..1], &allowances, &seen, false));
+ try std.testing.expectEqualStrings("", output.written());
}
test "raw-button catches verbs and declarations but allows type references" {
diff --git a/scripts/test-web-text-input.sh b/scripts/test-web-text-input.sh
index f8b17ffb5..44eae9334 100755
--- a/scripts/test-web-text-input.sh
+++ b/scripts/test-web-text-input.sh
@@ -1,6 +1,6 @@
#!/bin/sh
# The hidden input's tests (web/tests/text-input.test.js) against the web.js the web build
-# writes: `zig build web` first. Node also runs the pointer-input and two-copy embed checks.
+# writes: `zig build web` first. Node also runs the wheel, pointer-input and two-copy embed checks.
# CI builds the web assets and runs both suites. Use --embed-only for the build-independent
# Node check; hidden-input tests also support jsc.
set -eu
@@ -12,6 +12,7 @@ fi
js=zig-out/web/web.js
[ -f "$js" ] || { echo "test-web-text-input: no $js; run zig build web" >&2; exit 1; }
if command -v node >/dev/null; then
+ node web/tests/wheel-input.test.mjs
node web/tests/pointer-input.test.mjs
node web/tests/keyboard-inset.test.mjs
node web/tests/text-input.test.js "$PWD/$js"
diff --git a/sdk/src/Command.zig b/sdk/src/Command.zig
index 75a0ee2de..0c3f40818 100644
--- a/sdk/src/Command.zig
+++ b/sdk/src/Command.zig
@@ -222,7 +222,12 @@ pub fn Params(comptime T: type) type {
const text = std.mem.trim(u8, call.args, " \t\r\n");
const source = try call.arena.dupeSentinel(u8, if (text.len == 0) ".{}" else text, 0);
var diag: std.zon.parse.Diagnostics = undefined;
- return std.zon.parse.fromSlice(Args, .{ .gpa = call.arena, .arena = call.arena, .source = source, .diagnostics = &diag }) catch |err| switch (err) {
+ return std.zon.parse.fromSlice(Args, .{
+ .gpa = call.arena,
+ .arena = call.arena,
+ .source = source,
+ .diagnostics = &diag,
+ }) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.ParseZon => {
call.bad_args = true;
diff --git a/sdk/src/EditorAPI.zig b/sdk/src/EditorAPI.zig
index b438969ce..db54a58a9 100644
--- a/sdk/src/EditorAPI.zig
+++ b/sdk/src/EditorAPI.zig
@@ -114,7 +114,8 @@ pub const SaveState = struct {
};
/// Resolved canvas zoom/pan control style (mouse: scroll zooms, middle-button pans;
-/// trackpad: scroll pans, ctrl/cmd+scroll zooms). Matches `core.widgets.CanvasWidget.PanZoomScheme`.
+/// trackpad: scroll pans, ctrl/cmd+scroll zooms). Matches
+/// `core.widgets.CanvasWidget.PanZoomScheme`.
pub const PanZoomScheme = enum { mouse, trackpad };
/// One of the things carried together in the app's view drag (`beginViewDragMany`): a row of a
@@ -179,7 +180,13 @@ pub const VTable = struct {
/// Installed into the dylib's `core.dialogs.host_chrome` at load.
dialogWindow: *const fn (ctx: *anyopaque) dvui.Dialog.DisplayFn,
/// The host's `core.dialogs.frostPane`, for a plugin's own floating surfaces.
- frostPane: *const fn (ctx: *anyopaque, id: dvui.Id, rect: dvui.Rect.Physical, corners: dvui.CornerRect, scale: f32) bool,
+ frostPane: *const fn (
+ ctx: *anyopaque,
+ id: dvui.Id,
+ rect: dvui.Rect.Physical,
+ corners: dvui.CornerRect,
+ scale: f32,
+ ) bool,
/// Whether the OS window is currently maximized (always false on web).
isMaximized: *const fn (ctx: *anyopaque) bool,
/// Runtime macOS detection (uses `navigator.platform` on web, `os.tag` on native).
@@ -254,7 +261,13 @@ pub const VTable = struct {
/// `open_side` opens into a new grouping when the path is not already open; it is ignored
/// when the target is open anywhere, which just focuses it where it lives (mirroring the
/// file tree's "Open to the side"). Returns false when no plugin can open `path` at all.
- revealPosition: *const fn (ctx: *anyopaque, path: []const u8, line: u32, character: u32, open_side: bool) anyerror!bool,
+ revealPosition: *const fn (
+ ctx: *anyopaque,
+ path: []const u8,
+ line: u32,
+ character: u32,
+ open_side: bool,
+ ) anyerror!bool,
/// The split governing the region matching these keywords, or null when the app's layout
/// declared no such split — a normal answer, not an error.
///
@@ -298,9 +311,10 @@ pub const VTable = struct {
/// `beginViewDrag` for several things lifted together, from `from` (where they were — the
/// selected rows together): `items`, the first the one in hand, carried in the one glass, which
/// shows them all. Let go on a place, it takes them all (`RegionSpec.Drop.others`). With
- /// anything in it no place takes (`Carried.id` empty), no place takes any of it: it is carried as the rest are, and only the plugin that
- /// lifted it takes it back — reading the drag as its own while it is over it
- /// (`viewDragSurface`); let go anywhere else, nothing moves. The items are copied.
+ /// anything in it no place takes (`Carried.id` empty), no place takes any of it: it is carried
+ /// as the rest are, and only the plugin that lifted it takes it back — reading the drag as its
+ /// own while it is over it (`viewDragSurface`); let go anywhere else, nothing moves. The items
+ /// are copied.
beginViewDragMany: *const fn (ctx: *anyopaque, items: []const Carried, from: dvui.Rect.Physical) void,
/// The region's own chooser — the tab strip a plugin draws for it — is at `bounds` this
/// frame. While a view is carried it is chrome, not content: over it the drop is into the
@@ -600,11 +614,13 @@ pub fn isRemotePath(self: EditorAPI, path: []const u8) bool {
}
pub fn openOrFocusFileAtGrouping(self: EditorAPI, path: []const u8, grouping: u64) !?usize {
- return self.vtable.openOrFocusFileAtGrouping(self.ctx, path, grouping) catch |err| self.failed("openOrFocusFileAtGrouping", err);
+ return self.vtable.openOrFocusFileAtGrouping(self.ctx, path, grouping) catch |err|
+ self.failed("openOrFocusFileAtGrouping", err);
}
pub fn revealPosition(self: EditorAPI, path: []const u8, line: u32, character: u32, open_side: bool) !bool {
- return self.vtable.revealPosition(self.ctx, path, line, character, open_side) catch |err| self.failed("revealPosition", err);
+ return self.vtable.revealPosition(self.ctx, path, line, character, open_side) catch |err|
+ self.failed("revealPosition", err);
}
pub fn beginRegion(self: EditorAPI, spec: RegionSpec) ?RegionSpec.Token {
diff --git a/sdk/src/Host.zig b/sdk/src/Host.zig
index dd7953a97..b41935a20 100644
--- a/sdk/src/Host.zig
+++ b/sdk/src/Host.zig
@@ -528,7 +528,8 @@ pub fn explorerRect(self: *Host) dvui.Rect {
return if (self.fizzy_api) |a| a.explorerRect() else .{};
}
-/// The explorer scroll area's virtual content size (fizzy layout). Zero size if fizzy isn't installed.
+/// The explorer scroll area's virtual content size (fizzy layout). Zero size if fizzy isn't
+/// installed.
pub fn explorerVirtualSize(self: *Host) dvui.Size {
return if (self.fizzy_api) |a| a.explorerVirtualSize() else .{};
}
@@ -828,9 +829,9 @@ pub fn isPathIgnored(
}
/// True when fizzy has a live filesystem watcher on the open root folder — i.e. when
-/// `fizzy.files.changed` (`events.FilesChanged`) will actually be emitted. A plugin that must stay correct
-/// (an index, a file tree) should keep a slow rescan for when this is false, and can skip it
-/// entirely when it is true.
+/// `fizzy.files.changed` (`events.FilesChanged`) will actually be emitted. A plugin that must stay
+/// correct (an index, a file tree) should keep a slow rescan for when this is false, and can skip
+/// it entirely when it is true.
pub fn folderWatchActive(self: *Host) bool {
return if (self.fizzy_api) |a| a.folderWatchActive() else false;
}
@@ -1008,7 +1009,11 @@ pub fn registerPlugin(self: *Host, plugin: *Plugin) !void {
if (self.pluginByIdIgnoringCase(plugin.id)) |p| {
// The loader reports a duplicate by its id; this one it could not explain.
if (!std.mem.eql(u8, p.id, plugin.id))
- std.log.warn("plugin '{s}' not registered: '{s}' is registered already, and ids differing only in case are one id", .{ plugin.id, p.id });
+ std.log.warn("plugin '{s}' not registered: '{s}' is registered already, " ++
+ "and ids differing only in case are one id", .{
+ plugin.id,
+ p.id,
+ });
return error.DuplicatePluginId;
}
try self.plugins.append(self.allocator, plugin);
@@ -1729,7 +1734,8 @@ pub fn registerCommand(self: *Host, cmd: Command) !void {
// Its own base again (a reload of its registrations): the new one stands in place.
b.* = cmd;
} else {
- if (slot.bases.items.len > 0) std.log.warn("command {s}: {s} registered it again; {s}'s is kept, this one held in reserve (set `replaces` to override it)", .{
+ if (slot.bases.items.len > 0) std.log.warn("command {s}: {s} registered it again; " ++
+ "{s}'s is kept, this one held in reserve (set `replaces` to override it)", .{
cmd.id,
if (cmd.owner) |o| o.id else "the host",
if (slot.bases.items[0].owner) |o| o.id else "the host",
@@ -2035,7 +2041,12 @@ fn intern(self: *Host, who: Author) u32 {
self.allocator.free(plugin);
return self.internFailed();
};
- self.authors.append(self.allocator, .{ .kind = who.kind, .plugin = plugin, .session = who.session, .label = label }) catch {
+ self.authors.append(self.allocator, .{
+ .kind = who.kind,
+ .plugin = plugin,
+ .session = who.session,
+ .label = label,
+ }) catch {
self.allocator.free(plugin);
self.allocator.free(label);
return self.internFailed();
@@ -2079,12 +2090,15 @@ fn noteAuthor(self: *Host, doc_id: u64, step: Step) void {
.undone, .redone => {
doc.top = step.top;
const made = self.stepAuthor(doc_id, step.id);
- if (!made.same(who)) self.emitZon(if (step.kind == .undone) "fizzy.history.undone" else "fizzy.history.redone", events.StepTaken{
- .path = if (self.docById(doc_id)) |d| d.owner.documentPath(d) else "",
- .step = step.id,
- .made_by = made,
- .by = who,
- });
+ if (!made.same(who)) self.emitZon(
+ if (step.kind == .undone) "fizzy.history.undone" else "fizzy.history.redone",
+ events.StepTaken{
+ .path = if (self.docById(doc_id)) |d| d.owner.documentPath(d) else "",
+ .step = step.id,
+ .made_by = made,
+ .by = who,
+ },
+ );
},
}
}
@@ -2129,7 +2143,12 @@ pub fn documentEdited(self: *Host, doc_id: u64) void {
self.activity_seq += 1;
self.activity_run = self.command_runs;
const agent: ?u32 = if (who.kind == .agent) self.acting.author orelse self.input_author else null;
- self.activity.append(self.allocator, .{ .seq = self.activity_seq, .doc_id = doc_id, .title = title, .agent = agent }) catch {
+ self.activity.append(self.allocator, .{
+ .seq = self.activity_seq,
+ .doc_id = doc_id,
+ .title = title,
+ .agent = agent,
+ }) catch {
self.allocator.free(title);
return;
};
@@ -2137,7 +2156,8 @@ pub fn documentEdited(self: *Host, doc_id: u64) void {
}
/// Run a command's body as the running command, so the edits it makes are its own.
-/// `replaced`: reached through `callReplaced`. It and an override count as overriding (`inProgress`).
+/// `replaced`: reached through `callReplaced`. It and an override count as overriding
+/// (`inProgress`).
fn running(self: *Host, c: Command, replaced: bool) ?Command {
const outer = self.running_command;
self.running_command = c;
@@ -2350,10 +2370,18 @@ pub fn callCommand(self: *Host, id: []const u8, args: []const u8, results: std.m
/// `callCommand` for a caller that can wait: a command answering in a later frame
/// (`Command.Call.later`) gives its ticket, and `sink` hears the answer once it comes
/// (`finishCall`). A null sink drops it, as `callCommand` does.
-pub fn callCommandAwaiting(self: *Host, id: []const u8, args: []const u8, results: std.mem.Allocator, sink: ?CallSink) AwaitedOutcome {
+pub fn callCommandAwaiting(
+ self: *Host,
+ id: []const u8,
+ args: []const u8,
+ results: std.mem.Allocator,
+ sink: ?CallSink,
+) AwaitedOutcome {
// A copy, not the registry's entry: the body may register, replace or unregister commands.
const c = (self.command(id) orelse return .{ .now = .unknown }).*;
- if (self.inProgress(id)) return .{ .now = .{ .failed = "it called itself; an override reaches what it replaced with callReplaced" } };
+ if (self.inProgress(id)) return .{
+ .now = .{ .failed = "it called itself; an override reaches what it replaced with callReplaced" },
+ };
return self.callResolved(c, args, results, false, sink);
}
@@ -2361,10 +2389,21 @@ pub fn callCommandAwaiting(self: *Host, id: []const u8, args: []const u8, result
/// older override, else the base, the way `callCommand` would. How an override adds to a command
/// rather than taking it over: calling `id` itself would run the override again, and is refused.
/// `.unknown` when `caller` has no override of `id`, or it replaced nothing.
-pub fn callReplaced(self: *Host, caller: *Plugin, id: []const u8, args: []const u8, results: std.mem.Allocator) CommandOutcome {
+pub fn callReplaced(
+ self: *Host,
+ caller: *Plugin,
+ id: []const u8,
+ args: []const u8,
+ results: std.mem.Allocator,
+) CommandOutcome {
const slot = self.command_slots.get(id) orelse return .unknown;
const i = slot.overrideOf(caller) orelse return .unknown;
- const below = if (i > 0) slot.overrides.items[i - 1] else if (slot.bases.items.len > 0) slot.bases.items[0] else return .unknown;
+ const below = if (i > 0)
+ slot.overrides.items[i - 1]
+ else if (slot.bases.items.len > 0)
+ slot.bases.items[0]
+ else
+ return .unknown;
return switch (self.callResolved(below, args, results, true, null)) {
.now => |out| out,
.later => .{ .ok = null },
@@ -2379,7 +2418,14 @@ fn inProgress(self: *Host, id: []const u8) bool {
}
/// `replaced`: reached through `callReplaced`, so running inside an override (`inProgress`).
-fn callResolved(self: *Host, c: Command, args: []const u8, results: std.mem.Allocator, replaced: bool, sink: ?CallSink) AwaitedOutcome {
+fn callResolved(
+ self: *Host,
+ c: Command,
+ args: []const u8,
+ results: std.mem.Allocator,
+ replaced: bool,
+ sink: ?CallSink,
+) AwaitedOutcome {
const owner = c.owner orelse return .{ .now = .unknown };
if (c.isEnabled) |enabled| {
if (!enabled(owner.state)) return .{ .now = .disabled };
@@ -2388,7 +2434,9 @@ fn callResolved(self: *Host, c: Command, args: []const u8, results: std.mem.Allo
defer prof.end();
const run_with = c.runWith orelse {
const trimmed = std.mem.trim(u8, args, " \t\r\n");
- if (trimmed.len > 0 and !std.mem.eql(u8, trimmed, ".{}")) return .{ .now = .{ .bad_args = "this command takes no arguments" } };
+ if (trimmed.len > 0 and !std.mem.eql(u8, trimmed, ".{}")) return .{
+ .now = .{ .bad_args = "this command takes no arguments" },
+ };
const outer = self.running(c, replaced);
defer self.ran(c, outer, replaced);
c.run.?(owner.state) catch |err| return .{ .now = .{ .failed = owner.errorName(err) } };
@@ -2410,12 +2458,22 @@ fn callResolved(self: *Host, c: Command, args: []const u8, results: std.mem.Allo
return .{ .now = if (call.bad_args) .{ .bad_args = msg } else .{ .failed = msg } };
};
self.last_showing_ms = call.showing_ms;
- if (call.failed) return .{ .now = if (call.bad_args) .{ .bad_args = call.message orelse "bad arguments" } else .{ .failed = call.message orelse "failed" } };
+ if (call.failed) return .{
+ .now = if (call.bad_args)
+ .{ .bad_args = call.message orelse "bad arguments" }
+ else
+ .{ .failed = call.message orelse "failed" },
+ };
if (!call.deferred) return .{ .now = .{ .ok = call.result } };
// Nobody waits: started, with nothing kept for an answer nobody hears (`callWanted` is false).
const s = sink orelse return .{ .later = ticket };
// Out of memory: answered now as failed, rather than owed to a waiter that never hears.
- self.calls_pending.append(self.allocator, .{ .ticket = ticket, .owner = owner, .sink = s, .acting = self.acted() }) catch
+ self.calls_pending.append(self.allocator, .{
+ .ticket = ticket,
+ .owner = owner,
+ .sink = s,
+ .acting = self.acted(),
+ }) catch
return .{ .now = .{ .failed = "out of memory" } };
return .{ .later = ticket };
}
@@ -2529,7 +2587,13 @@ pub fn documentOpenedFor(self: *Host, ext: []const u8, path: []const u8, bytes:
/// Non-blocking hover lookup: the first provider with a cached/ready hover result for
/// `byte_offset` in `bytes` (the document at `path`), or null. See
/// `LanguageSupport.VTable.hover`.
-pub fn hoverFor(self: *Host, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize) ?language.HoverResult {
+pub fn hoverFor(
+ self: *Host,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+) ?language.HoverResult {
for (self.language_support.items) |*ls| {
const hook = ls.vtable.hover orelse continue;
const owner = ls.owner orelse continue;
@@ -2540,7 +2604,13 @@ pub fn hoverFor(self: *Host, ext: []const u8, path: []const u8, bytes: []const u
/// The first provider that resolves a definition for `byte_offset` in `bytes` (the document
/// at `path`), or null. May block briefly — see `LanguageSupport.VTable.gotoDefinition`.
-pub fn gotoDefinitionFor(self: *Host, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize) ?language.DefinitionLocation {
+pub fn gotoDefinitionFor(
+ self: *Host,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+) ?language.DefinitionLocation {
for (self.language_support.items) |*ls| {
const hook = ls.vtable.gotoDefinition orelse continue;
const owner = ls.owner orelse continue;
@@ -2552,7 +2622,13 @@ pub fn gotoDefinitionFor(self: *Host, ext: []const u8, path: []const u8, bytes:
/// Non-blocking inline-completion lookup: the first provider with a cached/ready suggestion
/// for the cursor at `byte_offset` in `bytes` (the document at `path`), or null. See
/// `LanguageSupport.VTable.completion`.
-pub fn completionFor(self: *Host, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize) ?[]const language.CompletionItem {
+pub fn completionFor(
+ self: *Host,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+) ?[]const language.CompletionItem {
for (self.language_support.items) |*ls| {
const hook = ls.vtable.completion orelse continue;
const owner = ls.owner orelse continue;
@@ -2565,7 +2641,14 @@ pub fn completionFor(self: *Host, ext: []const u8, path: []const u8, bytes: []co
/// resolved documentation string for candidate `index` from the completion result at
/// `byte_offset` in `bytes` (the document at `path`), or null. See
/// `LanguageSupport.VTable.resolveCompletionDocumentation`.
-pub fn resolveCompletionDocumentationFor(self: *Host, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize, index: usize) ?[]const u8 {
+pub fn resolveCompletionDocumentationFor(
+ self: *Host,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+ index: usize,
+) ?[]const u8 {
for (self.language_support.items) |*ls| {
const hook = ls.vtable.resolveCompletionDocumentation orelse continue;
const owner = ls.owner orelse continue;
@@ -2577,7 +2660,13 @@ pub fn resolveCompletionDocumentationFor(self: *Host, ext: []const u8, path: []c
/// Non-blocking signature-help lookup: the first provider with a cached/ready result for the
/// call the cursor at `byte_offset` in `bytes` (the document at `path`) currently sits inside,
/// or null. See `LanguageSupport.VTable.signatureHelp`.
-pub fn signatureHelpFor(self: *Host, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize) ?language.SignatureHelpResult {
+pub fn signatureHelpFor(
+ self: *Host,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+) ?language.SignatureHelpResult {
for (self.language_support.items) |*ls| {
const hook = ls.vtable.signatureHelp orelse continue;
const owner = ls.owner orelse continue;
@@ -2790,7 +2879,9 @@ fn dispatchNewDocument(self: *Host, candidate: Candidate, parent_path: ?[]const
return;
};
var args: std.Io.Writer.Allocating = .init(self.arena());
- std.zon.stringify.serialize(.{ .parent_path = parent_path orelse "" }, .{ .whitespace = false }, &args.writer) catch return;
+ std.zon.stringify.serialize(.{ .parent_path = parent_path orelse "" }, .{
+ .whitespace = false,
+ }, &args.writer) catch return;
switch (self.callCommand(c.id, args.written(), self.arena())) {
.ok => {},
.failed => |why| dvui.log.err("New File: {s}: {s}", .{ c.id, why }),
@@ -3126,13 +3217,48 @@ test "unregisterPlugin removes a plugin's contributions, service, and resets act
try host.registerPlugin(&keeper);
try host.registerPlugin(&plugin);
- try host.registerSurface(.{ .id = "keeper.view", .owner = &keeper, .title = "K", .keywords = keywords.ide.sidebar, .draw = noopCenter });
- try host.registerSurface(.{ .id = "victim.view", .owner = &plugin, .title = "V", .keywords = keywords.ide.sidebar, .draw = noopCenter });
- try host.registerSurface(.{ .id = "victim.bottom", .owner = &plugin, .title = "V", .keywords = keywords.ide.panel, .draw = noopCenter });
- try host.registerSurface(.{ .id = "victim.center", .owner = &plugin, .title = "V", .keywords = keywords.ide.main, .draw = noopCenter });
+ try host.registerSurface(.{
+ .id = "keeper.view",
+ .owner = &keeper,
+ .title = "K",
+ .keywords = keywords.ide.sidebar,
+ .draw = noopCenter,
+ });
+ try host.registerSurface(.{
+ .id = "victim.view",
+ .owner = &plugin,
+ .title = "V",
+ .keywords = keywords.ide.sidebar,
+ .draw = noopCenter,
+ });
+ try host.registerSurface(.{
+ .id = "victim.bottom",
+ .owner = &plugin,
+ .title = "V",
+ .keywords = keywords.ide.panel,
+ .draw = noopCenter,
+ });
+ try host.registerSurface(.{
+ .id = "victim.center",
+ .owner = &plugin,
+ .title = "V",
+ .keywords = keywords.ide.main,
+ .draw = noopCenter,
+ });
try host.registerMenu(.{ .id = "victim.menu", .owner = &plugin, .draw = noopDraw });
- try host.registerMenuSection(.{ .id = "victim.section", .parent_menu_id = "fizzy.menu.view", .owner = &plugin, .draw = noopDraw });
- try host.registerNativeMenuItem(.{ .id = "victim.native", .parent_menu_id = "fizzy.menu.view", .owner = &plugin, .title = "V", .run = noopDraw });
+ try host.registerMenuSection(.{
+ .id = "victim.section",
+ .parent_menu_id = "fizzy.menu.view",
+ .owner = &plugin,
+ .draw = noopDraw,
+ });
+ try host.registerNativeMenuItem(.{
+ .id = "victim.native",
+ .parent_menu_id = "fizzy.menu.view",
+ .owner = &plugin,
+ .title = "V",
+ .run = noopDraw,
+ });
try host.registerCommand(.{ .id = "victim.cmd", .owner = &plugin, .title = "V", .run = noopRun });
try host.registerFileRowFillColor(.{ .owner = &plugin, .color = noColor });
try host.registerFileRowDecorator(.{ .owner = &plugin, .draw = noDecoration });
@@ -3367,7 +3493,13 @@ test "an override adds to what it replaced through callReplaced, and cannot call
var app = Plugin{ .state = &host, .vtable = &vtable, .id = "app", .display_name = "App" };
S.app = &app;
try host.registerCommand(.{ .id = "pixi.pack", .owner = &pixi, .title = "Pack", .run = S.base });
- try host.registerCommand(.{ .id = "pixi.pack", .owner = &app, .title = "Pack More", .run = S.override, .replaces = true });
+ try host.registerCommand(.{
+ .id = "pixi.pack",
+ .owner = &app,
+ .title = "Pack More",
+ .run = S.override,
+ .replaces = true,
+ });
try testing.expect(host.callCommand("pixi.pack", "", testing.allocator) == .ok);
try testing.expectEqual(@as(u32, 1), S.override_runs);
@@ -3413,7 +3545,13 @@ test "a plain command may run itself; an override running its id is refused" {
testing.log_level = .err;
defer testing.log_level = log_level;
try host.registerCommand(.{ .id = "t.wrapped", .owner = &t, .title = "Wrapped", .run = S.countdown });
- try host.registerCommand(.{ .id = "t.wrapped", .owner = &app, .title = "Wrapper", .run = S.override, .replaces = true });
+ try host.registerCommand(.{
+ .id = "t.wrapped",
+ .owner = &app,
+ .title = "Wrapper",
+ .run = S.override,
+ .replaces = true,
+ });
try host.runCommand("t.wrapped");
try testing.expectEqual(error.CommandRanItself, S.ran_itself.?);
}
@@ -3431,7 +3569,13 @@ test "a New File command cannot be overridden yet" {
fn f(_: *anyopaque) anyerror!void {}
}.f;
try host.registerCommand(.{ .id = "pixi.new.sprite", .owner = &pixi, .title = "Sprite", .run = noopRun });
- try host.registerCommand(.{ .id = "pixi.new.sprite", .owner = &app, .title = "App Sprite", .run = noopRun, .replaces = true });
+ try host.registerCommand(.{
+ .id = "pixi.new.sprite",
+ .owner = &app,
+ .title = "App Sprite",
+ .run = noopRun,
+ .replaces = true,
+ });
try testing.expectEqualStrings("Sprite", host.command("pixi.new.sprite").?.title);
}
@@ -3463,7 +3607,13 @@ test "a claim on a document holds an override of its owner's command too" {
host.installFizzyApi(.{ .ctx = &ctx, .vtable = &api_vt });
// An override registered before pixi's own command, and before pixi loads: held by name.
- try host.registerCommand(.{ .id = "pixi.undo", .owner = &app, .title = "App Undo", .run = S.run, .replaces = true });
+ try host.registerCommand(.{
+ .id = "pixi.undo",
+ .owner = &app,
+ .title = "App Undo",
+ .run = S.run,
+ .replaces = true,
+ });
_ = try host.claimDocument(&holder, 7, "Holder");
try testing.expectError(error.DocumentClaimed, host.runCommand("pixi.undo"));
try host.registerPlugin(&pixi);
@@ -3481,7 +3631,13 @@ test "a claim on a document holds an override of its owner's command too" {
try host.registerPlugin(&acme);
try host.registerPlugin(&paint);
S.owner = &paint;
- try host.registerCommand(.{ .id = "acme.paint.undo", .owner = &app, .title = "App Paint Undo", .run = S.run, .replaces = true });
+ try host.registerCommand(.{
+ .id = "acme.paint.undo",
+ .owner = &app,
+ .title = "App Paint Undo",
+ .run = S.run,
+ .replaces = true,
+ });
try testing.expectError(error.DocumentClaimed, host.runCommand("acme.paint.undo"));
S.owner = &acme;
try host.runCommand("acme.paint.undo");
@@ -3722,10 +3878,27 @@ test "commands with arguments: callCommand, runCommand's fallback, and listing"
var plugin = Plugin{ .state = &state, .vtable = &vtable, .id = "t", .display_name = "T" };
// A command that could never run is refused at registration.
- try testing.expectError(error.CommandCannotRun, host.registerCommand(.{ .id = "t.none", .owner = &plugin, .title = "None" }));
+ try testing.expectError(error.CommandCannotRun, host.registerCommand(.{
+ .id = "t.none",
+ .owner = &plugin,
+ .title = "None",
+ }));
- try host.registerCommand(.{ .id = "t.goTo", .owner = &plugin, .title = "Go To", .params = S.GoTo.params, .runWith = S.GoTo.bind(S.goTo), .isEnabled = S.isEnabled });
- try host.registerCommand(.{ .id = "t.tidy", .owner = &plugin, .title = "Tidy", .params = S.Tidy.params, .runWith = S.Tidy.bind(S.tidy) });
+ try host.registerCommand(.{
+ .id = "t.goTo",
+ .owner = &plugin,
+ .title = "Go To",
+ .params = S.GoTo.params,
+ .runWith = S.GoTo.bind(S.goTo),
+ .isEnabled = S.isEnabled,
+ });
+ try host.registerCommand(.{
+ .id = "t.tidy",
+ .owner = &plugin,
+ .title = "Tidy",
+ .params = S.Tidy.params,
+ .runWith = S.Tidy.bind(S.tidy),
+ });
try host.registerCommand(.{ .id = "t.plain", .owner = &plugin, .title = "Plain", .run = S.plain });
try testing.expectEqual(@as(usize, 3), host.commandCount());
@@ -3795,7 +3968,13 @@ test "a surface's title can change: the Host keeps the copy, and frees it with t
try host.registerPlugin(&plugin);
// A plugin renames its own surface; the title it passed need not outlive the call.
- try host.registerSurface(.{ .id = "chat.chats", .owner = &plugin, .title = "Chats", .keywords = keywords.ide.sidebar, .draw = noopCenter });
+ try host.registerSurface(.{
+ .id = "chat.chats",
+ .owner = &plugin,
+ .title = "Chats",
+ .keywords = keywords.ide.sidebar,
+ .draw = noopCenter,
+ });
{
var scratch = "Chats (3)".*;
try host.setSurfaceTitle("chat.chats", &scratch);
@@ -3807,7 +3986,13 @@ test "a surface's title can change: the Host keeps the copy, and frees it with t
try host.setSurfaceTitle("no.such", "ignored");
// A document's tab keeps its file name until its owner names it, then follows the name.
- try host.registerSurface(.{ .id = "chat.doc:/chats/a.chat", .owner = &plugin, .title = "a.chat", .keywords = keywords.ide.main, .draw = noopCenter });
+ try host.registerSurface(.{
+ .id = "chat.doc:/chats/a.chat",
+ .owner = &plugin,
+ .title = "a.chat",
+ .keywords = keywords.ide.main,
+ .draw = noopCenter,
+ });
const doc: DocHandle = .{ .ptr = undefined, .id = 1, .owner = &plugin };
host.documentTitleChanged(doc);
try testing.expectEqualStrings("a.chat", host.surfaceById("chat.doc:/chats/a.chat").?.title);
@@ -3840,7 +4025,10 @@ test "a step is its author's, the person's when nobody else acts, and the top fo
host.endAct(outer);
try std.testing.expect(host.stepAuthor(doc, 2).same(claude));
try std.testing.expectEqualStrings("Claude", host.stepAuthor(doc, 3).label);
- try std.testing.expectEqual(host.step_authors.getPtr(doc).?.steps.get(2).?.run, host.step_authors.getPtr(doc).?.steps.get(3).?.run);
+ try std.testing.expectEqual(
+ host.step_authors.getPtr(doc).?.steps.get(2).?.run,
+ host.step_authors.getPtr(doc).?.steps.get(3).?.run,
+ );
try std.testing.expectEqual(@as(u64, 3), host.topStep(doc).?.id);
// The person's step after it is theirs, and the top.
@@ -3977,7 +4165,10 @@ test "a command answering later: its waiter hears it once, a caller that cannot
// A caller that waits gets the ticket, and hears the answer when it comes, once.
var heard: LaterFixture.Heard = .{};
- const out = host.callCommandAwaiting("t.slow", "", results.allocator(), .{ .ctx = &heard, .done = LaterFixture.Heard.done });
+ const out = host.callCommandAwaiting("t.slow", "", results.allocator(), .{
+ .ctx = &heard,
+ .done = LaterFixture.Heard.done,
+ });
try testing.expectEqual(fixture.ticket, out.later);
try testing.expect(host.callWanted(fixture.ticket));
host.finishCall(fixture.ticket, .{ .ok = ".{ .n = 1 }" });
@@ -4023,7 +4214,10 @@ test "a command answering later is finished as the author who called it" {
var heard: LaterFixture.Heard = .{ .host = &host };
const outer = host.act(.{ .kind = .agent, .plugin = "agent", .session = 7, .label = "Claude" });
const run = host.acting.run;
- _ = host.callCommandAwaiting("t.slow", "", results.allocator(), .{ .ctx = &heard, .done = LaterFixture.Heard.done });
+ _ = host.callCommandAwaiting("t.slow", "", results.allocator(), .{
+ .ctx = &heard,
+ .done = LaterFixture.Heard.done,
+ });
host.endAct(outer);
// A later frame: nobody acts now, yet the answer is the caller's.
@@ -4073,7 +4267,7 @@ const NestedFixture = struct {
}
};
-test "later answers: nested calls keep their own tickets, an early answer is the caller's now, a gone waiter is never called" {
+test "later answers: nested tickets, early answers, and gone waiters" {
var host = Host.init(testing.allocator);
defer host.deinit();
const vtable = Plugin.VTable{};
@@ -4081,7 +4275,12 @@ test "later answers: nested calls keep their own tickets, an early answer is the
var plugin = Plugin{ .state = &fixture, .vtable = &vtable, .id = "t", .display_name = "T" };
try host.registerCommand(.{ .id = "t.inner", .owner = &plugin, .title = "Inner", .runWith = NestedFixture.inner });
try host.registerCommand(.{ .id = "t.outer", .owner = &plugin, .title = "Outer", .runWith = NestedFixture.outer });
- try host.registerCommand(.{ .id = "t.cached", .owner = &plugin, .title = "Cached", .runWith = NestedFixture.cached });
+ try host.registerCommand(.{
+ .id = "t.cached",
+ .owner = &plugin,
+ .title = "Cached",
+ .runWith = NestedFixture.cached,
+ });
var results: std.heap.ArenaAllocator = .init(testing.allocator);
defer results.deinit();
@@ -4108,7 +4307,11 @@ test "later answers: nested calls keep their own tickets, an early answer is the
var waiter_state: u8 = 0;
var waiter = Plugin{ .state = &waiter_state, .vtable = &waiter_vtable, .id = "w", .display_name = "W" };
var unheard: LaterFixture.Heard = .{};
- _ = host.callCommandAwaiting("t.inner", "", results.allocator(), .{ .ctx = &unheard, .done = LaterFixture.Heard.done, .owner = &waiter });
+ _ = host.callCommandAwaiting("t.inner", "", results.allocator(), .{
+ .ctx = &unheard,
+ .done = LaterFixture.Heard.done,
+ .owner = &waiter,
+ });
host.withdrawPlugin(&waiter);
try testing.expect(!host.callWanted(fixture.inner_ticket));
host.finishCall(fixture.inner_ticket, .{ .ok = null });
diff --git a/sdk/src/Manifest.zig b/sdk/src/Manifest.zig
index ee31afbce..663241e8f 100644
--- a/sdk/src/Manifest.zig
+++ b/sdk/src/Manifest.zig
@@ -1,13 +1,13 @@
//! The declarative `plugin.zig.zon` manifest (`Manifest`): identity/version metadata a plugin
//! author declares, and fizzy reads back out of a loaded dylib.
//!
-//! See `plans/PLUGIN_MANIFEST_PLAN.md`'s "Locked decisions": the author's `plugin.zig.zon`
-//! declares `id`/`name`/`version`/`min_sdk_version`/`description`/`tags` and nothing else (R16
-//! added `description` — the store's plugin detail page needs one for every plugin, not just ones
-//! with a registry entry; R17 added `tags` the same way, so browse-list search/categorization
-//! also works for a plugin with no registry entry yet). Capability (which hooks a plugin implements, what it registers) has no
-//! declare-and-audit list — `plugin.zig`'s `register()` + vtable is the single source of truth,
-//! enforced by nothing beyond normal Zig compilation.
+//! See `plans/PLUGIN_MANIFEST_PLAN.md`'s "Locked decisions": the author's `plugin.zig.zon` declares
+//! `id`/`name`/`version`/`min_sdk_version`/`description`/`tags` and nothing else (R16 added
+//! `description` — the store's plugin detail page needs one for every plugin, not just ones with a
+//! registry entry; R17 added `tags` the same way, so browse-list search/categorization also works
+//! for a plugin with no registry entry yet). Capability (which hooks a plugin implements, what it
+//! registers) has no declare-and-audit list — `plugin.zig`'s `register()` + vtable is the single
+//! source of truth, enforced by nothing beyond normal Zig compilation.
//!
//! The typed, `std.SemanticVersion`-based shape actually baked into a dylib's C-ABI exports is
//! `dylib.Identity` — build-injected from this same `plugin.zig.zon`, never parsed from it at
diff --git a/sdk/src/Plugin.zig b/sdk/src/Plugin.zig
index f90911765..1637da3fa 100644
--- a/sdk/src/Plugin.zig
+++ b/sdk/src/Plugin.zig
@@ -182,38 +182,64 @@ pub const VTable = struct {
loadDocument: ?*const fn (state: *anyopaque, path: []const u8, out_doc: *anyopaque) anyerror!void = null,
/// `loadDocument`, but from in-memory bytes (browser file picker). `path` is used
/// for extension detection + display name. Synchronous (web has no load worker).
- loadDocumentFromBytes: ?*const fn (state: *anyopaque, path: []const u8, bytes: []const u8, out_doc: *anyopaque) anyerror!void = null,
+ loadDocumentFromBytes: ?*const fn (
+ state: *anyopaque,
+ path: []const u8,
+ bytes: []const u8,
+ out_doc: *anyopaque,
+ ) anyerror!void = null,
/// Open `bytes` as an untitled document named `title` (`untitled-1`), as `createDocument`
/// would have made it with these contents: never saved, and dirty. `bytes` are what
/// `documentBytes` encoded for it under `bytes_path` (`bytesPath`: the name Save As offers,
/// `untitled.pixi`), which says their format. How a crash or a restart brings back a document
/// that was never saved; without it, one comes back only as far as its state restores
/// into an empty document. Synchronous.
- loadUntitledDocument: ?*const fn (state: *anyopaque, title: []const u8, bytes_path: []const u8, bytes: []const u8, out_doc: *anyopaque) anyerror!void = null,
+ loadUntitledDocument: ?*const fn (
+ state: *anyopaque,
+ title: []const u8,
+ bytes_path: []const u8,
+ bytes: []const u8,
+ out_doc: *anyopaque,
+ ) anyerror!void = null,
/// Size of the plugin's document type for stack/heap staging buffers (`loadDocument`, etc.).
documentStackSize: ?*const fn (state: *anyopaque) usize = null,
documentStackAlign: ?*const fn (state: *anyopaque) usize = null,
documentIdFromBuffer: ?*const fn (state: *anyopaque, doc: *anyopaque) u64 = null,
deinitDocumentBuffer: ?*const fn (state: *anyopaque, doc: *anyopaque) void = null,
setDocumentGroupingOnBuffer: ?*const fn (state: *anyopaque, doc: *anyopaque, grouping: u64) void = null,
- createDocument: ?*const fn (state: *anyopaque, path: []const u8, grid: EditorAPI.NewDocGrid, out_doc: *anyopaque) anyerror!void = null,
+ createDocument: ?*const fn (
+ state: *anyopaque,
+ path: []const u8,
+ grid: EditorAPI.NewDocGrid,
+ out_doc: *anyopaque,
+ ) anyerror!void = null,
saveDocument: ?*const fn (state: *anyopaque, doc: DocHandle) anyerror!void = null,
- /// Saving, in two halves, so the UI thread does only the first (plans/PLUGIN_INTERACTION_PLAN.md,
- /// "Every save is a snapshot then an encode"). fizzy writes the bytes wherever the document
- /// lives (the disk, a mounted drive, a zip, the browser's download), so an owner never knows
- /// what a `gdrive://` path is. `saveDocument` stays the disk-only path for an owner that writes
- /// its files itself.
+ /// Saving, in two halves, so the UI thread does only the first
+ /// (plans/PLUGIN_INTERACTION_PLAN.md, "Every save is a snapshot then an encode"). fizzy writes
+ /// the bytes wherever the document lives (the disk, a mounted drive, a zip, the browser's
+ /// download), so an owner never knows what a `gdrive://` path is. `saveDocument` stays the
+ /// disk-only path for an owner that writes its files itself.
///
/// On the UI thread, as a save of `doc` to `path` starts: take what the save needs and nothing
/// more (a text buffer's copy, a canvas's layers, a composite read back from the GPU), quickly:
/// the person is waiting on this frame. Remember which edit it stands for, so
/// `documentWritten` cleans the document up to that edit and not past it. `path` is where it
/// is going: a Save As may change the format. Owned by the plugin until `releaseSnapshot`.
- documentSnapshot: ?*const fn (state: *anyopaque, doc: DocHandle, path: []const u8, allocator: std.mem.Allocator) anyerror!*anyopaque = null,
+ documentSnapshot: ?*const fn (
+ state: *anyopaque,
+ doc: DocHandle,
+ path: []const u8,
+ allocator: std.mem.Allocator,
+ ) anyerror!*anyopaque = null,
/// On a fizzy worker thread: `snapshot` encoded as `path`'s format (its extension says which).
/// Touches nothing but the snapshot: no dvui, no GPU, no document, no plugin state another
/// thread changes. The bytes are allocated with `allocator` and owned by the caller.
- encodeSnapshot: ?*const fn (state: *anyopaque, snapshot: *anyopaque, path: []const u8, allocator: std.mem.Allocator) anyerror![]u8 = null,
+ encodeSnapshot: ?*const fn (
+ state: *anyopaque,
+ snapshot: *anyopaque,
+ path: []const u8,
+ allocator: std.mem.Allocator,
+ ) anyerror![]u8 = null,
/// On the UI thread, once the encode is done or the save abandoned: free a snapshot.
releaseSnapshot: ?*const fn (state: *anyopaque, snapshot: *anyopaque) void = null,
/// fizzy wrote a snapshot's bytes to `path`. Clean the document up to the edit that snapshot
@@ -228,7 +254,12 @@ pub const VTable = struct {
/// nothing the person sees change. Owned by the caller, allocated with `allocator`. Absent =
/// fizzy takes the save's two halves instead (`documentSnapshot` + `encodeSnapshot`), and
/// whatever those do to the document happens on every checkpoint too.
- documentBytes: ?*const fn (state: *anyopaque, doc: DocHandle, path: []const u8, allocator: std.mem.Allocator) anyerror![]u8 = null,
+ documentBytes: ?*const fn (
+ state: *anyopaque,
+ doc: DocHandle,
+ path: []const u8,
+ allocator: std.mem.Allocator,
+ ) anyerror![]u8 = null,
closeDocument: ?*const fn (state: *anyopaque, doc: DocHandle) void = null,
/// Reload `doc` from its on-disk path, replacing in-memory contents and clearing dirty
/// state / undo history as appropriate. Called by fizzy's document watcher when a
@@ -244,7 +275,11 @@ pub const VTable = struct {
/// the next: start them with a version the next build checks (`restoreDocumentState`).
/// Absent = a demo cannot snapshot a moment with this document open, and a reload of this
/// plugin refuses while the document has unsaved changes.
- captureDocumentState: ?*const fn (state: *anyopaque, doc: DocHandle, allocator: std.mem.Allocator) anyerror![]u8 = null,
+ captureDocumentState: ?*const fn (
+ state: *anyopaque,
+ doc: DocHandle,
+ allocator: std.mem.Allocator,
+ ) anyerror![]u8 = null,
/// Put `doc` back to what `captureDocumentState` returned for it, in place: the same
/// document in the same pane, its contents, caret and scroll as they were, dirty or clean
/// as it was. Undo history may be dropped. `bytes` is only valid for the call, and may come
@@ -280,7 +315,13 @@ pub const VTable = struct {
/// to be shown being made over that long (a replay at pace); 0 lands it at once (a seek). A
/// step that does not fit what `doc` holds is an error, and `doc` is left as it was. Absent =
/// tapes of this owner's documents cannot be played.
- applyStep: ?*const fn (state: *anyopaque, doc: DocHandle, effect: []const u8, motion: []const u8, ms: u32) anyerror!void = null,
+ applyStep: ?*const fn (
+ state: *anyopaque,
+ doc: DocHandle,
+ effect: []const u8,
+ motion: []const u8,
+ ms: u32,
+ ) anyerror!void = null,
/// Put `doc`'s history back as its track recorded it (`RestoredHistory`), in place of what it
/// holds: a document opened again picks up the history it had. `doc` already stands where
/// the history leaves it; nothing in it changes, and it is not dirty for it. Each entry keeps
@@ -301,8 +342,15 @@ pub const VTable = struct {
/// Drop the registry entry after `closeDocument` has torn down resources.
unregisterDocument: ?*const fn (state: *anyopaque, id: u64) void = null,
- /// Bind a document to a workbench pane before `drawDocument` (canvas id, workspace handle, center flag).
- bindDocumentToPane: ?*const fn (state: *anyopaque, doc: DocHandle, canvas_id: dvui.Id, workspace_handle: *anyopaque, center: bool) void = null,
+ /// Bind a document to a workbench pane before `drawDocument` (canvas id, workspace handle,
+ /// center flag).
+ bindDocumentToPane: ?*const fn (
+ state: *anyopaque,
+ doc: DocHandle,
+ canvas_id: dvui.Id,
+ workspace_handle: *anyopaque,
+ center: bool,
+ ) void = null,
documentGrouping: ?*const fn (state: *anyopaque, doc: DocHandle) u64 = null,
setDocumentGrouping: ?*const fn (state: *anyopaque, doc: DocHandle, grouping: u64) void = null,
documentPath: ?*const fn (state: *anyopaque, doc: DocHandle) []const u8 = null,
@@ -310,7 +358,11 @@ pub const VTable = struct {
documentHasNativeExtension: ?*const fn (state: *anyopaque, doc: DocHandle) bool = null,
/// True when `saveDocument` can write the document without Save As (e.g. `.fiz` or flat image).
documentHasRecognizedSaveExtension: ?*const fn (state: *anyopaque, doc: DocHandle) bool = null,
- documentDefaultSaveAsFilename: ?*const fn (state: *anyopaque, doc: DocHandle, allocator: std.mem.Allocator) anyerror![]const u8 = null,
+ documentDefaultSaveAsFilename: ?*const fn (
+ state: *anyopaque,
+ doc: DocHandle,
+ allocator: std.mem.Allocator,
+ ) anyerror![]const u8 = null,
// ---- render hooks (the plugin draws its own dvui UI into the host window) ----
// Sidebar/explorer panes and bottom-panel tabs are NOT vtable hooks — plugins
// contribute them as named, owned `Surface`s via `Host.registerSurface`, and the app's
@@ -394,7 +446,12 @@ pub const VTable = struct {
/// straight away. Strings in `allocator` (the frame's arena), copied by fizzy. fizzy asks it,
/// shows the saving and its end, and once the person has said to go ahead it does not ask
/// again while the document stays open. The owner never drives a save itself.
- saveQuestion: ?*const fn (state: *anyopaque, doc: DocHandle, path: []const u8, allocator: std.mem.Allocator) ?SaveQuestion = null,
+ saveQuestion: ?*const fn (
+ state: *anyopaque,
+ doc: DocHandle,
+ path: []const u8,
+ allocator: std.mem.Allocator,
+ ) ?SaveQuestion = null,
// ---- settings ----
/// [requested] This plugin's persisted settings blob changed while the plugin is loaded —
@@ -435,7 +492,9 @@ pub fn assertEditorVTable(comptime vt: VTable) void {
// without this hook opens every document in the pane the user was already in and "Open to
// the side" appears to do nothing. The image plugin shipped that way, and nothing —
// not a log line, not a failed load — said so.
- if (vt.setDocumentGroupingOnBuffer == null) @compileError("Editor vtable missing required hook: setDocumentGroupingOnBuffer (a document that ignores its grouping cannot be opened to the side)");
+ if (vt.setDocumentGroupingOnBuffer == null)
+ @compileError("Editor vtable missing required hook: setDocumentGroupingOnBuffer " ++
+ "(a document that ignores its grouping cannot be opened to the side)");
if (vt.documentGrouping == null) @compileError("Editor vtable missing required hook: documentGrouping");
if (vt.isDirty == null) @compileError("Editor vtable missing required hook: isDirty");
if (vt.saveDocument == null) @compileError("Editor vtable missing required hook: saveDocument");
@@ -448,7 +507,8 @@ pub fn assertUtilityVTable(comptime vt: VTable) void {
comptime {
if (vt.loadDocument != null) @compileError("Utility vtable must not implement document hook: loadDocument");
if (vt.drawDocument != null) @compileError("Utility vtable must not implement document hook: drawDocument");
- if (vt.registerOpenDocument != null) @compileError("Utility vtable must not implement document hook: registerOpenDocument");
+ if (vt.registerOpenDocument != null)
+ @compileError("Utility vtable must not implement document hook: registerOpenDocument");
if (vt.createDocument != null) @compileError("Utility vtable must not implement document hook: createDocument");
}
}
@@ -464,7 +524,8 @@ pub fn fileTypes(self: Plugin) []const []const u8 {
pub fn contributeKeybinds(self: Plugin, win: *dvui.Window) !void {
const prof = core.profile.begin(self.id, "contributeKeybinds");
defer prof.end();
- if (self.vtable.contributeKeybinds) |f| f(self.state, win) catch |err| return self.failed("contributeKeybinds", err);
+ if (self.vtable.contributeKeybinds) |f| f(self.state, win) catch |err|
+ return self.failed("contributeKeybinds", err);
}
pub fn tickKeybinds(self: Plugin) !void {
@@ -482,7 +543,8 @@ pub fn drawOverlay(self: Plugin) !void {
pub fn registerOpenDocument(self: Plugin, file: *anyopaque) !*anyopaque {
const prof = core.profile.begin(self.id, "registerOpenDocument");
defer prof.end();
- return if (self.vtable.registerOpenDocument) |f| (f(self.state, file) catch |err| return self.failed("registerOpenDocument", err)) else error.Unsupported;
+ return if (self.vtable.registerOpenDocument) |f| (f(self.state, file) catch |err|
+ return self.failed("registerOpenDocument", err)) else error.Unsupported;
}
pub fn documentPtr(self: Plugin, id: u64) ?*anyopaque {
@@ -503,7 +565,13 @@ pub fn unregisterDocument(self: Plugin, id: u64) void {
if (self.vtable.unregisterDocument) |f| f(self.state, id);
}
-pub fn bindDocumentToPane(self: Plugin, doc: DocHandle, canvas_id: dvui.Id, workspace_handle: *anyopaque, center: bool) void {
+pub fn bindDocumentToPane(
+ self: Plugin,
+ doc: DocHandle,
+ canvas_id: dvui.Id,
+ workspace_handle: *anyopaque,
+ center: bool,
+) void {
const prof = core.profile.begin(self.id, "bindDocumentToPane");
defer prof.end();
if (self.vtable.bindDocumentToPane) |f| f(self.state, doc, canvas_id, workspace_handle, center);
@@ -530,7 +598,8 @@ pub fn documentPath(self: Plugin, doc: DocHandle) []const u8 {
pub fn setDocumentPath(self: Plugin, doc: DocHandle, path: []const u8) !void {
const prof = core.profile.begin(self.id, "setDocumentPath");
defer prof.end();
- if (self.vtable.setDocumentPath) |f| f(self.state, doc, path) catch |err| return self.failed("setDocumentPath", err);
+ if (self.vtable.setDocumentPath) |f| f(self.state, doc, path) catch |err|
+ return self.failed("setDocumentPath", err);
}
pub fn documentHasNativeExtension(self: Plugin, doc: DocHandle) bool {
@@ -572,7 +641,13 @@ pub fn loadDocumentFromBytes(self: Plugin, path: []const u8, bytes: []const u8,
}
/// `loadUntitledDocument`. Returns whether the plugin handled it (`false` = no hook).
-pub fn loadUntitledDocument(self: Plugin, title: []const u8, bytes_path: []const u8, bytes: []const u8, out_doc: *anyopaque) !bool {
+pub fn loadUntitledDocument(
+ self: Plugin,
+ title: []const u8,
+ bytes_path: []const u8,
+ bytes: []const u8,
+ out_doc: *anyopaque,
+) !bool {
const prof = core.profile.begin(self.id, "loadUntitledDocument");
defer prof.end();
if (self.vtable.loadUntitledDocument) |f| {
@@ -600,12 +675,18 @@ pub fn documentSnapshot(self: Plugin, doc: DocHandle, path: []const u8, allocato
if (!self.canSaveThroughHost()) return null;
const prof = core.profile.begin(self.id, "documentSnapshot");
defer prof.end();
- return self.vtable.documentSnapshot.?(self.state, doc, path, allocator) catch |err| self.failed("documentSnapshot", err);
+ return self.vtable.documentSnapshot.?(self.state, doc, path, allocator) catch |err|
+ self.failed("documentSnapshot", err);
}
/// The second half, on a worker. No profiler scope and no logging here: neither is the worker's
/// to touch. The caller reports a failure from the UI thread.
-pub fn encodeSnapshot(self: Plugin, snapshot: *anyopaque, path: []const u8, allocator: std.mem.Allocator) anyerror![]u8 {
+pub fn encodeSnapshot(
+ self: Plugin,
+ snapshot: *anyopaque,
+ path: []const u8,
+ allocator: std.mem.Allocator,
+) anyerror![]u8 {
return self.vtable.encodeSnapshot.?(self.state, snapshot, path, allocator);
}
@@ -648,7 +729,8 @@ pub fn saveBytes(self: Plugin, doc: DocHandle, path: []const u8, allocator: std.
pub fn documentWritten(self: Plugin, doc: DocHandle, path: []const u8) !void {
const prof = core.profile.begin(self.id, "documentWritten");
defer prof.end();
- if (self.vtable.documentWritten) |f| f(self.state, doc, path) catch |err| return self.failed("documentWritten", err);
+ if (self.vtable.documentWritten) |f| f(self.state, doc, path) catch |err|
+ return self.failed("documentWritten", err);
}
pub fn canSaveThroughHost(self: Plugin) bool {
@@ -853,13 +935,15 @@ pub fn setDocumentGroupingOnBuffer(self: Plugin, doc: *anyopaque, grouping: u64)
pub fn createDocument(self: Plugin, path: []const u8, grid: EditorAPI.NewDocGrid, out_doc: *anyopaque) !void {
const prof = core.profile.begin(self.id, "createDocument");
defer prof.end();
- if (self.vtable.createDocument) |f| (f(self.state, path, grid, out_doc) catch |err| return self.failed("createDocument", err)) else return error.Unsupported;
+ if (self.vtable.createDocument) |f| (f(self.state, path, grid, out_doc) catch |err|
+ return self.failed("createDocument", err)) else return error.Unsupported;
}
pub fn documentDefaultSaveAsFilename(self: Plugin, doc: DocHandle, allocator: std.mem.Allocator) ![]const u8 {
const prof = core.profile.begin(self.id, "documentDefaultSaveAsFilename");
defer prof.end();
- return if (self.vtable.documentDefaultSaveAsFilename) |f| (f(self.state, doc, allocator) catch |err| return self.failed("documentDefaultSaveAsFilename", err)) else error.Unsupported;
+ return if (self.vtable.documentDefaultSaveAsFilename) |f| (f(self.state, doc, allocator) catch |err|
+ return self.failed("documentDefaultSaveAsFilename", err)) else error.Unsupported;
}
pub fn saveQuestion(self: Plugin, doc: DocHandle, path: []const u8, allocator: std.mem.Allocator) ?SaveQuestion {
@@ -911,7 +995,8 @@ pub fn needsContinuousRepaint(self: Plugin) bool {
return if (self.vtable.needsContinuousRepaint) |f| f(self.state) else false;
}
-/// Allocate a buffer suitable for staging `loadDocument` / `createDocument`. Caller frees `backing`.
+/// Allocate a buffer suitable for staging `loadDocument` / `createDocument`. Caller frees
+/// `backing`.
pub fn allocDocumentBuffer(self: Plugin, allocator: std.mem.Allocator) !struct { backing: []u8, buf: []u8 } {
const size = self.documentStackSize();
const align_req = self.documentStackAlign();
diff --git a/sdk/src/RegionSpec.zig b/sdk/src/RegionSpec.zig
index 44d103aa2..a4495ddc2 100644
--- a/sdk/src/RegionSpec.zig
+++ b/sdk/src/RegionSpec.zig
@@ -3,10 +3,10 @@
//!
//! The app declares the shape — rail, sidebar, main, panel — and until now that was the only way a
//! region could exist, so everything the framework grew around regions (the corner button and its
-//! picker, its tab strip, assignments persisted by name, the settings table, keyword claiming) stopped
-//! at the surface a plugin draws. Inside it a plugin had to invent its own subdivision, which is
-//! exactly what the workbench did with workspaces and tab strips: a second implementation of the
-//! same idea, addressable by nothing.
+//! picker, its tab strip, assignments persisted by name, the settings table, keyword claiming)
+//! stopped at the surface a plugin draws. Inside it a plugin had to invent its own subdivision,
+//! which is exactly what the workbench did with workspaces and tab strips: a second implementation
+//! of the same idea, addressable by nothing.
//!
//! A plugin's region is not a hole in that design, because it is not a new place in the app's
//! vocabulary: its keywords are **qualified by the region it is declared inside**
diff --git a/sdk/src/Step.zig b/sdk/src/Step.zig
index 5b0a7b7cd..5d7775d9d 100644
--- a/sdk/src/Step.zig
+++ b/sdk/src/Step.zig
@@ -1,4 +1,5 @@
-//! One step of a document's history, as its owner reports it to whoever records (`Host.recordStep`):
+//! One step of a document's history, as its owner reports it to whoever records
+//! (`Host.recordStep`):
//! what the document became, not the input that made it, so it replays exactly at any window size
//! and in any layout (`plans/RECORDING_PLAN.md`).
//!
diff --git a/sdk/src/dylib.zig b/sdk/src/dylib.zig
index 216d5259d..f6b8bf6b3 100644
--- a/sdk/src/dylib.zig
+++ b/sdk/src/dylib.zig
@@ -39,7 +39,8 @@ const wikilink_service = @import("services/wikilink.zig");
/// `gpa` is always the app allocator. `arg_b`/`arg_c` are two generic injection slots whose
/// meaning is defined by the receiving plugin's `set_globals` (they are *not* fixed roles).
/// The conventions in this tree:
-/// - third-party (`exportEntry`): `arg_b` = the `*Host`, `arg_c` = unused (a plugin owns its state)
+/// - third-party (`exportEntry`): `arg_b` = the `*Host`, `arg_c` = unused (a plugin owns its
+/// state)
/// - workbench / code: `arg_b` = `*Host`, `arg_c` = the plugin's own state
/// - pixi: `arg_b` = the plugin's `*State`, `arg_c` = `*Packer` (historical; takes no host here)
pub const SetGlobalsFn = *const fn (
@@ -394,7 +395,12 @@ pub fn semverFromTriplet(t: VersionTriplet) std.SemanticVersion {
/// pub const std_options: std.Options = sdk.dylib.stdOptions(opts.id);
pub fn stdOptions(comptime id: []const u8) std.Options {
const Impl = struct {
- fn logFn(comptime level: std.log.Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype) void {
+ fn logFn(
+ comptime level: std.log.Level,
+ comptime scope: @EnumLiteral(),
+ comptime format: []const u8,
+ args: anytype,
+ ) void {
// A wasm side module has no stderr of its own (std's default sink wants a
// threaded Io); the host's panel is the only sink there.
if (comptime @import("builtin").target.cpu.arch != .wasm32) dvui.App.logFn(level, scope, format, args);
diff --git a/sdk/src/events.zig b/sdk/src/events.zig
index 1d918a649..1a3d3a8be 100644
--- a/sdk/src/events.zig
+++ b/sdk/src/events.zig
@@ -45,7 +45,8 @@ pub const Declaration = struct {
/// Whether `pattern` (a name, `prefix*`, or `*`) takes the event `name`.
pub fn matches(pattern: []const u8, name: []const u8) bool {
- if (pattern.len > 0 and pattern[pattern.len - 1] == '*') return std.mem.startsWith(u8, name, pattern[0 .. pattern.len - 1]);
+ if (pattern.len > 0 and pattern[pattern.len - 1] == '*')
+ return std.mem.startsWith(u8, name, pattern[0 .. pattern.len - 1]);
return std.mem.eql(u8, pattern, name);
}
@@ -55,7 +56,13 @@ pub fn parse(comptime T: type, arena: std.mem.Allocator, event: Event) !T {
const text = std.mem.trim(u8, event.payload, " \t\r\n");
const source = try arena.dupeSentinel(u8, if (text.len == 0) ".{}" else text, 0);
var diag: std.zon.parse.Diagnostics = undefined;
- return std.zon.parse.fromSlice(T, .{ .gpa = arena, .arena = arena, .source = source, .diagnostics = &diag, .ignore_unknown_fields = true });
+ return std.zon.parse.fromSlice(T, .{
+ .gpa = arena,
+ .arena = arena,
+ .source = source,
+ .diagnostics = &diag,
+ .ignore_unknown_fields = true,
+ });
}
// ---- what fizzy announces ----
@@ -133,7 +140,8 @@ pub const PathEvent = struct {
/// - **Best-effort.** Not every platform has a watcher (`Host.folderWatchActive`), and one can drop
/// changes under load: then `truncated` is set and `events` is incomplete, so rescan rather
/// than trust the list (a build, a branch switch, an npm install). Treat it as a prompt to go
-/// look, not a ledger; a plugin that must stay correct keeps a slow rescan for when no watch runs.
+/// look, not a ledger; a plugin that must stay correct keeps a slow rescan for when no watch
+/// runs.
/// - The project's file set (`Host.files`) is already updated by the time it is heard.
pub const FilesChanged = struct {
events: []const PathEvent,
@@ -151,13 +159,17 @@ test "a payload parses as its type, skipping what the type lacks" {
defer arena.deinit();
const changed = try parse(FilesChanged, arena.allocator(), .{
.name = "fizzy.files.changed",
- .payload = ".{ .events = .{ .{ .path = \"/p/a.md\", .kind = .renamed, .object = .file, .old_path = \"/p/b.md\" } }, .truncated = true, .later = 1 }",
+ .payload = ".{ .events = .{ .{ .path = \"/p/a.md\", .kind = .renamed, .object = .file, " ++
+ ".old_path = \"/p/b.md\" } }, .truncated = true, .later = 1 }",
});
try std.testing.expectEqual(@as(usize, 1), changed.events.len);
try std.testing.expectEqualStrings("/p/b.md", changed.events[0].old_path);
try std.testing.expectEqual(PathEvent.Kind.renamed, changed.events[0].kind);
try std.testing.expect(changed.truncated);
- const closed = try parse(Path, arena.allocator(), .{ .name = "fizzy.folder.closed", .payload = ".{ .path = \"/p\" }" });
+ const closed = try parse(Path, arena.allocator(), .{
+ .name = "fizzy.folder.closed",
+ .payload = ".{ .path = \"/p\" }",
+ });
try std.testing.expectEqualStrings("/p", closed.path);
}
diff --git a/sdk/src/language.zig b/sdk/src/language.zig
index e5e53f732..d65721453 100644
--- a/sdk/src/language.zig
+++ b/sdk/src/language.zig
@@ -55,7 +55,14 @@ pub const LanguageSupport = struct {
/// rather than through shared SDK state: preview providers are commonly separate
/// dylibs, and each `.dylib` gets its own private copy of every SDK global, so a
/// write from the host's copy is invisible inside the plugin's.
- previewPane: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, bytes: []const u8, id_extra: u64, gpa: std.mem.Allocator) anyerror!void = null,
+ previewPane: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ id_extra: u64,
+ gpa: std.mem.Allocator,
+ ) anyerror!void = null,
/// Scroll the preview for `id_extra` so that 0-based source `line` is in view. Called
/// just before `previewPane` on the frame a reveal lands (`workbench.revealPosition`),
/// and not otherwise — a reveal is a one-shot event, so it is its own call rather than an
@@ -65,7 +72,13 @@ pub const LanguageSupport = struct {
/// line to its own layout; one that can't is free to leave this null, and the raw editor
/// still moves. Nothing guarantees the pane has ever been drawn for this `id_extra` yet,
/// so treat it as "remember this and apply it when you next draw".
- previewReveal: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, line: u32, id_extra: u64) void = null,
+ previewReveal: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ line: u32,
+ id_extra: u64,
+ ) void = null,
/// Keep 0-based source `line` at `at` of the preview's height — 0 its top, 1 its bottom —
/// which is where the caret sits in the source view beside it, so the preview follows the
/// caret as it moves and as text is typed. Called just before `previewPane`, on the frames
@@ -73,14 +86,26 @@ pub const LanguageSupport = struct {
/// pane; never alongside a `previewReveal`, which wins. Like a reveal it is one-shot: the
/// preview scrolls freely between calls, and only the next move of the caret brings it
/// back. A provider that cannot map a line to its own layout leaves this null.
- previewFollow: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, line: u32, at: f32, id_extra: u64) void = null,
+ previewFollow: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ line: u32,
+ at: f32,
+ id_extra: u64,
+ ) void = null,
/// Non-blocking: called when the text editor opens (or reloads) a document. Intended
/// for language-server warmup — spawn the server and send `textDocument/didOpen` so
/// analysis can start before the first hover/completion, rather than paying cold-start
/// latency on first use. Gate on `ext` yourself and no-op when the file isn't yours.
/// Must never block; kick off async work as a side effect. `bytes`/`path` are only
/// valid for the duration of the call — copy anything the background work will need.
- documentOpened: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, bytes: []const u8) void = null,
+ documentOpened: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ ) void = null,
/// Non-blocking: return a cached/ready hover result for `byte_offset` in `bytes`
/// (the document at `path`). Called every frame the mouse dwells over a token, so it
/// must never block — kick off async work as a side effect. Three-state return:
@@ -91,11 +116,23 @@ pub const LanguageSupport = struct {
/// thread, call `sdk.refresh()` so the idle GUI wakes and redraws (mouse may not move
/// again). `bytes` is the live document text and `path` the document's path, same
/// convention as `previewPane`.
- hover: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize) ?HoverResult = null,
+ hover: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+ ) ?HoverResult = null,
/// May block briefly (a few hundred ms) — called once per Ctrl/Cmd+click, not every
/// frame. Returns the definition location for the symbol at `byte_offset` in `bytes`
/// (the document at `path`), or null when none is found.
- gotoDefinition: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize) ?DefinitionLocation = null,
+ gotoDefinition: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+ ) ?DefinitionLocation = null,
/// Non-blocking, same convention as `hover`: return cached/ready completion
/// candidates for the cursor at `byte_offset` in `bytes` (the document at `path`), or
/// null when there's nothing to show yet. Called every frame the cursor sits in a
@@ -107,7 +144,13 @@ pub const LanguageSupport = struct {
/// scrollable dropdown list, plus — for the current one, when its `insert_text` literally
/// extends what's already typed — the remainder as ghost text after the caret; Up/Down
/// changes which one is "current" for both.
- completion: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize) ?[]const CompletionItem = null,
+ completion: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+ ) ?[]const CompletionItem = null,
/// Non-blocking, same convention as `hover`/`completion`: many language servers send
/// completion candidates with an empty/placeholder `documentation` up front (a lazy-load
/// optimization — full docs only sent on demand via LSP `completionItem/resolve`).
@@ -120,7 +163,14 @@ pub const LanguageSupport = struct {
/// which case the caller falls back to whatever `documentation` the original
/// `CompletionItem` already carried. Called every frame a completion candidate's info
/// panel would be visible, so it must never block.
- resolveCompletionDocumentation: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize, index: usize) ?[]const u8 = null,
+ resolveCompletionDocumentation: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+ index: usize,
+ ) ?[]const u8 = null,
/// Non-blocking, same convention as `completion`: return a cached/ready signature
/// help result for the call the cursor at `byte_offset` in `bytes` (the document at
/// `path`) currently sits inside, or null when there's nothing to show yet (including
@@ -128,7 +178,13 @@ pub const LanguageSupport = struct {
/// for rather than the caller filtering it out). When the cache is populated from a
/// background thread, call `sdk.refresh()` so the idle GUI wakes. Called every frame
/// the cursor is inside an open call's parens while typing, so it must never block.
- signatureHelp: ?*const fn (state: *anyopaque, ext: []const u8, path: []const u8, bytes: []const u8, byte_offset: usize) ?SignatureHelpResult = null,
+ signatureHelp: ?*const fn (
+ state: *anyopaque,
+ ext: []const u8,
+ path: []const u8,
+ bytes: []const u8,
+ byte_offset: usize,
+ ) ?SignatureHelpResult = null,
/// Return true when this provider can format `ext` (including the dot, e.g. ".zig").
/// Non-blocking and side-effect-free — called far more often than `format` itself (an
/// owner asks it every frame to decide whether to show/enable a "Format Document" menu
diff --git a/sdk/src/menu.zig b/sdk/src/menu.zig
index 4c9c2f589..97148741a 100644
--- a/sdk/src/menu.zig
+++ b/sdk/src/menu.zig
@@ -63,7 +63,7 @@ pub fn submenu(
var anim = dvui.animate(src, .{ .kind = .alpha, .duration = 250_000 }, .{ .expand = .both });
defer anim.deinit();
- // lint-allow(raw-transient-surface): TODO(seam): a plugin flyout cannot reach core's frosted floatingMenu from sdk; fix with an sdk release
+ // lint-allow(raw-transient-surface): TODO(seam): SDK cannot use core's frosted floatingMenu
var fw = dvui.floatingMenu(src, .{ .from = r }, .{});
defer fw.deinit();
diff --git a/sdk/src/render_bridge.zig b/sdk/src/render_bridge.zig
index 1de789e62..0d0763b1f 100644
--- a/sdk/src/render_bridge.zig
+++ b/sdk/src/render_bridge.zig
@@ -150,7 +150,10 @@ fn textureFromTarget(ctx: ?*anyopaque, target: *const proxy_bridge.TextureDesc)
return proxy_bridge.textureDescFrom(tex);
}
-fn textureFromTargetTemp(ctx: ?*anyopaque, target: *const proxy_bridge.TextureDesc) callconv(.c) proxy_bridge.TextureDesc {
+fn textureFromTargetTemp(
+ ctx: ?*anyopaque,
+ target: *const proxy_bridge.TextureDesc,
+) callconv(.c) proxy_bridge.TextureDesc {
const win = windowFromCtx(ctx);
const tex_target = targetFromDesc(target) catch return emptyTextureDesc();
const tex = win.backend.textureFromTargetTemp(tex_target) catch return emptyTextureDesc();
diff --git a/sdk/src/sdk.zig b/sdk/src/sdk.zig
index 1b2f2f08c..a62ee7cc0 100644
--- a/sdk/src/sdk.zig
+++ b/sdk/src/sdk.zig
@@ -111,9 +111,9 @@ pub const Manifest = @import("Manifest.zig");
/// Services: capability offered by name and version rather than by the ABI.
///
-/// `"files"`, `"automation"` and `"demos"` are the application's; the rest are plugins'. Every one of them is
-/// optional — a caller handles null and offers what it can, which is what lets one plugin build
-/// run in applications that have wildly different amounts of machinery.
+/// `"files"`, `"automation"` and `"demos"` are the application's; the rest are plugins'. Every one
+/// of them is optional — a caller handles null and offers what it can, which is what lets one
+/// plugin build run in applications that have wildly different amounts of machinery.
pub const services = struct {
pub const files = @import("services/files.zig");
pub const workbench = @import("services/workbench.zig");
diff --git a/sdk/src/services/automation.zig b/sdk/src/services/automation.zig
index f469a8caa..60b1e97c0 100644
--- a/sdk/src/services/automation.zig
+++ b/sdk/src/services/automation.zig
@@ -91,7 +91,8 @@ pub const Api = struct {
pub const VTable = struct {
play: *const fn (ctx: *anyopaque, bytes: []const u8) Play,
- /// `play`, the tape acting for `author` (`Host.act`): an agent's pointer tape is the agent's.
+ /// `play`, the tape acting for `author` (`Host.act`): an agent's pointer tape is the
+ /// agent's.
playAs: *const fn (ctx: *anyopaque, bytes: []const u8, author: Author) Play,
stop: *const fn (ctx: *anyopaque, ticket: Ticket) void,
outcome: *const fn (ctx: *anyopaque, ticket: Ticket) Outcome,
diff --git a/sdk/src/services/files.zig b/sdk/src/services/files.zig
index 85f7c50c8..cec675d63 100644
--- a/sdk/src/services/files.zig
+++ b/sdk/src/services/files.zig
@@ -37,7 +37,12 @@ pub const Api = struct {
createDir: *const fn (ctx: *anyopaque, path: []const u8) anyerror!void,
/// Rename `path` to `new_path`, rewriting the path of every open document it names — the
/// one document for a file, everything beneath it for a directory.
- rename: *const fn (ctx: *anyopaque, path: []const u8, new_path: []const u8, kind: std.Io.File.Kind) anyerror!void,
+ rename: *const fn (
+ ctx: *anyopaque,
+ path: []const u8,
+ new_path: []const u8,
+ kind: std.Io.File.Kind,
+ ) anyerror!void,
/// Delete `path` (a file, or an empty directory) and close whatever that removed from
/// under the editor. An unsaved document still raises the app's normal unsaved-close
/// prompt: the file being gone from disk is exactly when those edits are the only copy.
diff --git a/sdk/src/services/markdown.zig b/sdk/src/services/markdown.zig
index 2ce8bfbbf..66520ce1e 100644
--- a/sdk/src/services/markdown.zig
+++ b/sdk/src/services/markdown.zig
@@ -44,8 +44,18 @@ pub const Api = struct {
};
pub const VTable = struct {
- render: *const fn (ctx: *anyopaque, bytes: []const u8, gpa: std.mem.Allocator, opts: RenderOptions) anyerror!void,
- renderInline: *const fn (ctx: *anyopaque, bytes: []const u8, gpa: std.mem.Allocator, opts: InlineOptions) anyerror!void,
+ render: *const fn (
+ ctx: *anyopaque,
+ bytes: []const u8,
+ gpa: std.mem.Allocator,
+ opts: RenderOptions,
+ ) anyerror!void,
+ renderInline: *const fn (
+ ctx: *anyopaque,
+ bytes: []const u8,
+ gpa: std.mem.Allocator,
+ opts: InlineOptions,
+ ) anyerror!void,
release: *const fn (ctx: *anyopaque, id_extra: u64) void,
defaultView: *const fn (ctx: *anyopaque) DefaultView,
setDefaultView: *const fn (ctx: *anyopaque, view: DefaultView) void,
diff --git a/sdk/src/services/plugins.zig b/sdk/src/services/plugins.zig
index 33fd89d61..335c5abbc 100644
--- a/sdk/src/services/plugins.zig
+++ b/sdk/src/services/plugins.zig
@@ -6,9 +6,10 @@
//! surfaces, commands, services, menus and the rest it registered.
//!
//! Consumers: anything that waits on a plugin's rebuild (an agent's build loop watches the
-//! generation move), a plugin author's status line, the store's page for a plugin, a
-//! development overlay, the dashboard's Plugins section (plans/DASHBOARD_PLAN.md). A reload that fails leaves the running build in place: `state` stays
-//! `loaded`, the generation stays put, and `why` says what went wrong.
+//! generation move), a plugin author's status line, the store's page for a plugin, a development
+//! overlay, the dashboard's Plugins section (plans/DASHBOARD_PLAN.md). A reload that fails leaves
+//! the running build in place: `state` stays `loaded`, the generation stays put, and `why` says
+//! what went wrong.
const std = @import("std");
pub const Api = struct {
diff --git a/sdk/src/settings.zig b/sdk/src/settings.zig
index 5a5027e7c..093ae87a3 100644
--- a/sdk/src/settings.zig
+++ b/sdk/src/settings.zig
@@ -518,7 +518,8 @@ pub fn Schema(comptime T: type) type {
if (!all_copied) {
inline for (info.field_names, info.field_types, 0..) |f_name, Cell, i| {
if (comptime typeTagFor(Cell.Payload) == .string) {
- if (i < copied and @field(parsed, f_name).ptr != @field(default_payloads, f_name).ptr) gpa.free(@field(parsed, f_name));
+ if (i < copied and @field(parsed, f_name).ptr != @field(default_payloads, f_name).ptr)
+ gpa.free(@field(parsed, f_name));
}
}
dvui.log.warn("sdk.settings: out of memory keeping settings", .{});
@@ -694,7 +695,12 @@ pub fn Schema(comptime T: type) type {
var arena: std.heap.ArenaAllocator = .init(gpa);
defer arena.deinit();
var diag: std.zon.parse.Diagnostics = undefined;
- var parsed = std.zon.parse.fromSlice(P, .{ .gpa = gpa, .arena = arena.allocator(), .source = text_z, .diagnostics = &diag }) catch return false;
+ var parsed = std.zon.parse.fromSlice(P, .{
+ .gpa = gpa,
+ .arena = arena.allocator(),
+ .source = text_z,
+ .diagnostics = &diag,
+ }) catch return false;
// Strings follow the same ownership rule as `setString`: copied out of the
// parse's arena, the schema's to free. Other payloads are plain values.
if (comptime typeTagFor(P) == .string) {
@@ -768,7 +774,10 @@ pub fn Schema(comptime T: type) type {
const gpa = runtime.host().allocator;
const notify_blob = fullSerialize(gpa, value) catch |err| {
- dvui.log.warn("sdk.settings: failed to serialize '{s}' for notification: {s}", .{ owner.id, @errorName(err) });
+ dvui.log.warn("sdk.settings: failed to serialize '{s}' for notification: {s}", .{
+ owner.id,
+ @errorName(err),
+ });
return;
};
defer gpa.free(notify_blob);
diff --git a/sdk/tape/binary.zig b/sdk/tape/binary.zig
index f839c86be..f1d6b7245 100644
--- a/sdk/tape/binary.zig
+++ b/sdk/tape/binary.zig
@@ -29,13 +29,13 @@
//! a recording that is shared — its effects are mostly runs of the same few bytes. Reading
//! inflates it into the one copy, exactly that long, then reads as ever.
//!
-//! **Versions.** `version` is bumped for any change a reader of the old one would misread; a
-//! reader refuses a version it does not know (`error.UnsupportedVersion`) rather than guess, and
-//! reads every older one it does. 2 gave a `command` op its arguments, in the record's second
-//! string; a version-1 command has none. 3 added steps, files' owner and format, the blob table
-//! and the deflated form. 4 added the `skip` op. A keyframe once had a layout byte (ahead of three of
-//! padding), which nothing read and is now four bytes of padding: older tapes read the same, and
-//! an older reader reads a newer tape's zero as `keep`, the only layout that ever did anything.
+//! **Versions.** `version` is bumped for any change a reader of the old one would misread; a reader
+//! refuses a version it does not know (`error.UnsupportedVersion`) rather than guess, and reads
+//! every older one it does. 2 gave a `command` op its arguments, in the record's second string; a
+//! version-1 command has none. 3 added steps, files' owner and format, the blob table and the
+//! deflated form. 4 added the `skip` op. A keyframe once had a layout byte (ahead of three of
+//! padding), which nothing read and is now four bytes of padding: older tapes read the same, and an
+//! older reader reads a newer tape's zero as `keep`, the only layout that ever did anything.
const std = @import("std");
const flate = std.compress.flate;
const Tape = @import("Tape.zig");
diff --git a/src/editor/Editor.zig b/src/editor/Editor.zig
index 3914f6ff8..1832d987d 100644
--- a/src/editor/Editor.zig
+++ b/src/editor/Editor.zig
@@ -210,6 +210,18 @@ chrome: Chrome = .configured,
/// answer. That is fine for deciding who owns a clipboard verb: focus doesn't change between
/// the keystroke and the frame that handles it.
text_input_focused: bool = false,
+/// Shared preferences last loaded from disk, with any pending local choice.
+person: core.person.Person = .{},
+/// Shared config directory, owned by `app.gpa` until deinit.
+person_dir: []const u8,
+/// Timestamp of the last loaded or saved shared file.
+person_mtime: ?i128 = null,
+/// Native focus state from the preceding frame.
+person_app_active: bool = false,
+/// Browser focus notification last handled by tick.
+person_focus_epoch: u32 = 0,
+/// Pending shared preference save deadline; null when clean.
+person_save_deadline_ns: ?i128 = null,
/// Whether any plugin asked to keep painting this frame — one poll of every plugin's
/// `needsContinuousRepaint`, sampled in `tick` and read by everything that needs the answer.
@@ -456,6 +468,7 @@ pub fn init(
.file_table = .init(app.allocator, dvui.io),
.secrets = try .init(app.allocator, dvui.io, config_folder),
},
+ .person_dir = try personDir(app.allocator, profile_root, config_root),
.explorer = try app.allocator.create(Explorer),
.infobar = try .init(),
.last_titlebar_color = dvui.themeGet().color(.control, .fill),
@@ -478,6 +491,7 @@ pub fn init(
{
const settings_path = try std.fs.path.join(app.allocator, &.{ editor.app.config_folder, "settings.zon" });
defer app.allocator.free(settings_path);
+ editor.loadPerson(settings_path);
editor.app.settings = try Settings.load(app.allocator, settings_path, plugins_dir);
}
@@ -3933,7 +3947,6 @@ pub fn reconcileExternalSettingsChange(editor: *Editor) void {
editor.app.settings.content_opacity = parsed.content_opacity;
editor.app.settings.modal_dim = parsed.modal_dim;
editor.app.settings.corner_roundness = parsed.corner_roundness;
- editor.app.settings.motion = parsed.motion;
editor.app.settings.motion_speed = parsed.motion_speed;
editor.app.settings.glass_shader = parsed.glass_shader;
editor.app.settings.float_windows = parsed.float_windows;
@@ -4375,7 +4388,71 @@ pub fn restartPending(_: *const Editor) bool {
return Popout.restartPending();
}
+/// Where `person.zon` lives, owned by the caller. A profile keeps its own, so a sandbox instance
+/// never writes the person's real shared preferences; otherwise it is `fizzyedit/` beside every
+/// fizzy app's config folder.
+pub fn personDir(gpa: std.mem.Allocator, profile_root: ?[]const u8, config_root: []const u8) std.mem.Allocator.Error![]const u8 {
+ if (profile_root) |root| return gpa.dupe(u8, root);
+ return std.fs.path.join(gpa, &.{ config_root, "fizzyedit" });
+}
+
+/// Loads shared preferences after a best-effort migration; disk errors never prevent startup.
+pub fn loadPerson(editor: *Editor, settings_path: []const u8) void {
+ @import("app").settings.Migration.personMotion(editor.app.gpa, dvui.io, settings_path, editor.person_dir) catch |err| {
+ std.log.warn("Could not migrate person preferences: {s}", .{@errorName(err)});
+ };
+ editor.person = core.person.load(dvui.io, editor.person_dir);
+ editor.person_mtime = editor.personMtime();
+}
+
+fn personMtime(editor: *const Editor) ?i128 {
+ if (comptime builtin.target.cpu.arch == .wasm32) return null;
+ const path = std.fs.path.join(editor.app.gpa, &.{ editor.person_dir, "person.zon" }) catch return null;
+ defer editor.app.gpa.free(path);
+ const stat = std.Io.Dir.cwd().statFile(dvui.io, path, .{}) catch return null;
+ return stat.mtime.nanoseconds;
+}
+
+/// Re-reads shared preferences on focus when another app has replaced the file.
+pub fn reloadPerson(editor: *Editor) void {
+ if (editor.person_save_deadline_ns != null) return;
+ const mtime = editor.personMtime();
+ if (comptime builtin.target.cpu.arch == .wasm32) {
+ editor.person = core.person.load(dvui.io, editor.person_dir);
+ } else if (mtime != editor.person_mtime) {
+ editor.person = core.person.load(dvui.io, editor.person_dir);
+ }
+ editor.person_mtime = mtime;
+}
+
+/// Debounces shared preference writes while a slider is changing.
+pub fn markPersonDirty(editor: *Editor) void {
+ editor.person_save_deadline_ns = core.perf.nanoTimestamp() + Settings.autosave_timeout_ns;
+ dvui.refresh(null, @src(), null);
+}
+
+/// Flushes the local shared preference choice to disk.
+pub fn savePerson(editor: *Editor) void {
+ core.person.save(dvui.io, editor.person_dir, editor.person) catch |err| {
+ dvui.log.err("Could not save person preferences: {s}", .{@errorName(err)});
+ editor.person_save_deadline_ns = core.perf.nanoTimestamp() + Settings.autosave_timeout_ns;
+ return;
+ };
+ editor.person_mtime = editor.personMtime();
+ editor.person_save_deadline_ns = null;
+}
+
pub fn tick(editor: *Editor) !dvui.App.Result {
+ const web_focus = struct {
+ extern "fizzy" fn fizzy_web_focus_epoch() u32;
+ };
+ const epoch = if (comptime builtin.target.cpu.arch == .wasm32) web_focus.fizzy_web_focus_epoch() else 0;
+ const active = fizzy.backend.viewports.appActive();
+ if ((active and !editor.person_app_active) or epoch != editor.person_focus_epoch) {
+ editor.reloadPerson();
+ }
+ editor.person_app_active = active;
+ editor.person_focus_epoch = epoch;
// (A playing demo has already had its turn: `Entry.frameOnce` runs `Demo.frame` first.)
// A restart handing the window to the new instance: before anything reads this frame's input.
if (comptime restart.supported) editor.driveHandover();
@@ -4434,7 +4511,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result {
// A demo replaying to a seek wants every frame to land where it is going: no motion — but the
// glass keeps its edge, the transport's among it, scrubbed or not.
const playback: f32 = if (editor.demo.player.state == .playing) editor.demo.player.seq.speed else 1;
- fizzy.core.motion.publish(editor.app.settings.motion, editor.app.settings.motion_speed, dvui.currentWindow().backend.prefersReducedMotion(), editor.demo.fast, playback);
+ fizzy.core.motion.publish(core.person.motionLevel(editor.person, build_opts.look_motion), editor.app.settings.motion_speed, dvui.currentWindow().backend.prefersReducedMotion(), editor.demo.fast, playback);
fizzy.core.programs.publishHost();
// Where the main window's title bar takes presses, not the app (macOS): floating windows,
// menus and tooltips are kept below it (`core.screens.mainScreen`).
@@ -4994,6 +5071,10 @@ pub fn tick(editor: *Editor) !dvui.App.Result {
editor.saveSettingsGuarded() catch |err| {
dvui.log.err("Failed to autosave settings ({s})", .{@errorName(err)});
};
+ if (editor.person_save_deadline_ns) |deadline| {
+ const remaining = deadline - core.perf.nanoTimestamp();
+ if (remaining > 0) App.scheduleSaveWakeup(remaining, 2) else editor.savePerson();
+ }
editor.saveWindowRatiosGuarded();
_ = editor.app.arena.reset(.retain_capacity);
@@ -7293,6 +7374,8 @@ pub fn rawCloseFileID(editor: *Editor, id: u64) !void {
}
pub fn deinit(editor: *Editor) !void {
+ if (editor.person_save_deadline_ns != null) editor.savePerson();
+ defer editor.app.gpa.free(editor.person_dir);
// Freed however this ends: the fallible steps below may return early.
defer editor.app.deinit();
// A demo playing at quit leaves the user's layout and settings as `Demo.begin` flushed them:
diff --git a/src/editor/demos/tour.zig b/src/editor/demos/tour.zig
index a6615a70c..daa93272b 100644
--- a/src/editor/demos/tour.zig
+++ b/src/editor/demos/tour.zig
@@ -7,6 +7,7 @@
//! fresh one. The explorer is pressed open only if it is not showing already. A snapshot holds the window's
//! arrangement too (`Demo.Snapshot`), so a seek back across a split or a float puts the window
//! back as it was then, rather than replaying from the keyframe.
+const std = @import("std");
const Tape = @import("tape").Tape;
const Script = @import("tape").Script;
const catalog = @import("catalog.zig");
@@ -201,7 +202,6 @@ pub fn build(s: *Script) !void {
}
test "a frost-first tour names the frosted glass it shows" {
- const std = @import("std");
var s: Script = .init(std.testing.allocator, "tour", "Tour");
s.lacks = &.{"liquid"};
try build(&s);
diff --git a/src/editor/explorer/settings.zig b/src/editor/explorer/settings.zig
index 65dd8cb44..0015d9475 100644
--- a/src/editor/explorer/settings.zig
+++ b/src/editor/explorer/settings.zig
@@ -12,6 +12,7 @@
//! by `PluginSettingsPane` (see `SettingsTree`), so a plugin's controls look identical to these.
const builtin = @import("builtin");
const std = @import("std");
+const build_opts = @import("build_opts");
const fizzy = @import("../../fizzy.zig");
const icons = @import("icons");
@@ -558,14 +559,24 @@ fn drawCornerRoundness() void {
}
fn drawMotion() void {
- const settings = &fizzy.editor().app.settings;
+ const editor = fizzy.editor();
+ var level = fizzy.core.person.motionLevel(editor.person, build_opts.look_motion);
+ if (editor.person.motion == null) dvui.label(@src(), "System default ({d:0.01})", .{level}, .{});
+ dvui.labelNoFmt(@src(), "Applies to every fizzy app.", .{}, .{});
+ if (editor.person.motion != null and core.widgets.button(@src(), "Reset to system default", .{}, .{})) {
+ editor.person.motion = null;
+ level = fizzy.core.person.motionLevel(editor.person, build_opts.look_motion);
+ editor.savePerson();
+ dvui.refresh(null, @src(), null);
+ }
if (dvui.sliderEntry(@src(), "{d:0.01}", .{
- .value = &settings.motion,
+ .value = &level,
.interval = 0.01,
.max = 1.0,
.min = 0.0,
}, .{ .expand = .horizontal })) {
- fizzy.editor().markSettingsDirty();
+ editor.person.motion = level;
+ editor.markPersonDirty();
dvui.refresh(null, @src(), null);
}
// The three names along the slider's length, the one it is nearest lit.
@@ -574,7 +585,7 @@ fn drawMotion() void {
const names = [_][]const u8{ "Off", "Minimal", "Playful" };
const at = [_]f32{ 0, 0.5, 1 };
const system_off = dvui.currentWindow().backend.prefersReducedMotion();
- const v: f32 = if (system_off) 0 else settings.motion;
+ const v: f32 = if (system_off) 0 else level;
var nearest: usize = 0;
for (at, 0..) |a, i| {
if (@abs(v - a) < @abs(v - at[nearest])) nearest = i;
diff --git a/tests/integration.zig b/tests/integration.zig
index 48214d971..5895c2b97 100644
--- a/tests/integration.zig
+++ b/tests/integration.zig
@@ -6602,6 +6602,186 @@ fn peakOf(comptime f: fn (f32, f32) f32, lv: f32) f32 {
return peak;
}
+test "motion: failed migration leaves startup preferences usable and legacy data intact" {
+ var ctx = try shim.init(std.testing.allocator);
+ defer ctx.deinit(std.testing.allocator);
+ const gpa = std.testing.allocator;
+ const io = std.testing.io;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const root = try tmp.dir.realPathFileAlloc(io, ".", gpa);
+ defer gpa.free(root);
+ const settings = try std.fs.path.join(gpa, &.{ root, "settings.zon" });
+ defer gpa.free(settings);
+ const blocked = try std.fs.path.join(gpa, &.{ root, "blocked" });
+ defer gpa.free(blocked);
+ try tmp.dir.writeFile(io, .{ .sub_path = "settings.zon", .data = ".{ .motion = 0.6 }" });
+ try tmp.dir.writeFile(io, .{ .sub_path = "blocked", .data = "a file cannot be a config directory" });
+ ctx.editor.app.gpa = gpa;
+ ctx.editor.person_dir = blocked;
+ ctx.editor.loadPerson(settings);
+ try std.testing.expectEqual(@as(?f32, null), ctx.editor.person.motion);
+ const data = try fizzy.core.fs.read(gpa, io, settings);
+ defer gpa.free(data);
+ try std.testing.expectEqualStrings(".{ .motion = 0.6 }", data);
+ try tmp.dir.deleteFile(io, "blocked");
+ ctx.editor.loadPerson(settings);
+ try std.testing.expectEqual(@as(?f32, 0.6), ctx.editor.person.motion);
+}
+
+test "motion: a profiled instance keeps person.zon under its profile root" {
+ const gpa = std.testing.allocator;
+ const io = std.testing.io;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const root = try tmp.dir.realPathFileAlloc(io, ".", gpa);
+ defer gpa.free(root);
+
+ const profiled = try fizzy.Editor.personDir(gpa, root, "/the/persons/config");
+ defer gpa.free(profiled);
+ try std.testing.expectEqualStrings(root, profiled);
+ try fizzy.core.person.save(io, profiled, .{ .motion = 0.2 });
+ try tmp.dir.access(io, "person.zon", .{});
+
+ const shared = try fizzy.Editor.personDir(gpa, null, "/the/persons/config");
+ defer gpa.free(shared);
+ const want_shared = try std.fs.path.join(gpa, &.{ "/the/persons/config", "fizzyedit" });
+ defer gpa.free(want_shared);
+ try std.testing.expectEqualStrings(want_shared, shared);
+}
+
+test "motion: migration and focus share the person's choice across app configs" {
+ var ctx = try shim.init(std.testing.allocator);
+ defer ctx.deinit(std.testing.allocator);
+ const gpa = std.testing.allocator;
+ const io = std.testing.io;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const root = try tmp.dir.realPathFileAlloc(io, ".", gpa);
+ defer gpa.free(root);
+ const person_dir = try std.fs.path.join(gpa, &.{ root, "person" });
+ defer gpa.free(person_dir);
+ const Migration = @import("app").settings.Migration;
+ try tmp.dir.createDirPath(io, "first");
+ try tmp.dir.createDirPath(io, "second");
+ const first = try std.fs.path.join(gpa, &.{ root, "first", "settings.zon" });
+ defer gpa.free(first);
+ const second = try std.fs.path.join(gpa, &.{ root, "second", "settings.zon" });
+ defer gpa.free(second);
+ try fizzy.core.fs.write(io, first, ".{ .motion = 0.6 }");
+ try Migration.personMotion(gpa, io, first, person_dir);
+ try std.testing.expectEqual(@as(?f32, 0.6), fizzy.core.person.load(io, person_dir).motion);
+ const migrated = try fizzy.core.fs.read(gpa, io, first);
+ defer gpa.free(migrated);
+ try std.testing.expect(std.mem.find(u8, migrated, ".motion =") == null);
+
+ try fizzy.core.person.save(io, person_dir, .{ .motion = 0.2 });
+ try fizzy.core.fs.write(io, second, ".{ .motion = 0.6 }");
+ try Migration.personMotion(gpa, io, second, person_dir);
+ try std.testing.expectEqual(@as(?f32, 0.2), fizzy.core.person.load(io, person_dir).motion);
+
+ const editor = ctx.editor;
+ editor.app.gpa = gpa;
+ editor.person_dir = person_dir;
+ editor.person_mtime = null;
+ editor.reloadPerson();
+ try std.testing.expectEqual(@as(f32, 0.2), fizzy.core.person.motionLevel(editor.person, 1));
+ try fizzy.core.person.save(io, person_dir, .{ .motion = 0.8 });
+ // Coarse filesystem clocks can give consecutive replacements the same timestamp.
+ const shared = try tmp.dir.openFile(io, "person/person.zon", .{ .mode = .read_write });
+ defer shared.close(io);
+ try shared.setTimestamps(io, .{ .modify_timestamp = .{ .new = .{ .nanoseconds = @intCast(editor.person_mtime.? + std.time.ns_per_s) } } });
+ editor.reloadPerson();
+ try std.testing.expectEqual(@as(f32, 0.8), fizzy.core.person.motionLevel(editor.person, 0.3));
+ const M = fizzy.core.motion;
+ M.publish(fizzy.core.person.motionLevel(.{}, 0.3), M.default_speed, false, false, 1);
+ try std.testing.expectEqual(@as(f32, 0.3), M.level());
+ for ([_]f32{ 0.3, 1 }) |app_level| {
+ M.publish(fizzy.core.person.motionLevel(editor.person, app_level), M.default_speed, false, false, 1);
+ try std.testing.expectEqual(@as(f32, 0.8), M.level());
+ }
+ for ([_]?f32{ null, 0, 0.8, 1 }) |level| {
+ M.publish(fizzy.core.person.motionLevel(.{ .motion = level }, 1), M.default_speed, true, false, 1);
+ try std.testing.expect(M.off());
+ }
+}
+
+test "motion: pending shared choices stay in memory until flushed" {
+ var ctx = try shim.init(std.testing.allocator);
+ defer ctx.deinit(std.testing.allocator);
+ const gpa = std.testing.allocator;
+ const io = std.testing.io;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const dir = try tmp.dir.realPathFileAlloc(io, ".", gpa);
+ defer gpa.free(dir);
+ ctx.editor.app.gpa = gpa;
+ ctx.editor.person_dir = dir;
+ try fizzy.core.person.save(io, dir, .{ .motion = 0.2 });
+ ctx.editor.person.motion = 0.8;
+ ctx.editor.markPersonDirty();
+ try std.testing.expectEqual(@as(?f32, 0.2), fizzy.core.person.load(io, dir).motion);
+ ctx.editor.reloadPerson();
+ try std.testing.expectEqual(@as(?f32, 0.8), ctx.editor.person.motion);
+ ctx.editor.savePerson();
+ try std.testing.expectEqual(@as(?f32, 0.8), fizzy.core.person.load(io, dir).motion);
+ try std.testing.expectEqual(@as(?i128, null), ctx.editor.person_save_deadline_ns);
+}
+
+test "motion: settings reset saves null and draws the shared default" {
+ var ctx = try shim.init(std.testing.allocator);
+ defer ctx.deinit(std.testing.allocator);
+ const gpa = std.testing.allocator;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const dir = try tmp.dir.realPathFileAlloc(std.testing.io, ".", gpa);
+ defer gpa.free(dir);
+ ctx.editor.app.gpa = gpa;
+ ctx.editor.person_dir = dir;
+ ctx.editor.person.motion = 0.8;
+ const frame = struct {
+ fn draw() !dvui.App.Result {
+ var box = dvui.box(@src(), .{ .dir = .vertical }, .{ .expand = .horizontal });
+ defer box.deinit();
+ for (fizzy.Explorer.settings.groups) |group| for (group.items) |item| {
+ if (std.mem.eql(u8, item.key, "motion")) item.draw.?();
+ };
+ return .ok;
+ }
+ }.draw;
+ dvui.debug.captureFrame();
+ defer dvui.debug.clearCaptures(dvui.currentWindow().gpa);
+ _ = try dvui.testing.step(frame);
+ _ = try dvui.testing.step(frame);
+ var reset: ?dvui.Point.Physical = null;
+ for (dvui.debug.lastCapture().?.widgets.items) |widget| {
+ if (widget.text) |text| if (std.mem.eql(u8, text, "Reset to system default")) {
+ reset = widget.rect_border.center();
+ };
+ }
+ _ = try dvui.currentWindow().addEventMouseMotion(.{ .pt = reset orelse return error.TestUnexpectedResult });
+ try dvui.testing.click(.left);
+ _ = try dvui.testing.step(frame);
+ try std.testing.expectEqual(@as(?f32, null), ctx.editor.person.motion);
+ try std.testing.expectEqual(@as(?f32, null), fizzy.core.person.load(std.testing.io, dir).motion);
+ dvui.debug.captureFrame();
+ _ = try dvui.testing.step(frame);
+ _ = try dvui.testing.step(frame);
+ var default_shown = false;
+ var shared_shown = false;
+ var reset_shown = false;
+ for (dvui.debug.lastCapture().?.widgets.items) |widget| {
+ if (widget.text) |text| {
+ if (std.mem.eql(u8, text, "Reset to system default")) reset_shown = true;
+ if (std.mem.startsWith(u8, text, "System default (")) default_shown = true;
+ if (std.mem.eql(u8, text, "Applies to every fizzy app.")) shared_shown = true;
+ }
+ }
+ try std.testing.expect(default_shown);
+ try std.testing.expect(shared_shown);
+ try std.testing.expect(!reset_shown);
+}
+
test "motion: every curve starts at 0 and lands on 1, at every level" {
const M = fizzy.core.motion;
var lv: f32 = 0;
@@ -10770,6 +10950,8 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes
try editor.postInit();
try dvui.testing.settle(headlessFrame);
try std.testing.expectEqualStrings(root, editor.app.config_folder);
+ // The person's choices stay in the profile too, not in the shared config folder.
+ try std.testing.expectEqualStrings(root, editor.person_dir);
// A plugin's data folder (`Host.pluginDataFolder`): under the profile, made on the first ask,
// the same borrowed path after, one per plugin.
{
diff --git a/web/fizzy-worker.js b/web/fizzy-worker.js
index 931434647..11681821e 100644
--- a/web/fizzy-worker.js
+++ b/web/fizzy-worker.js
@@ -69,6 +69,8 @@ const functionTable = new WebAssembly.Table({ initial: 4096, element: "anyfunc"
//
// A worker has no `localStorage`, and the app reads its settings synchronously. The page sends
// the entries filed under its `storageName` at startup; writes land here at once and go back to the page to keep.
+let focusEpoch = 0;
+const fileKey = (path) => path === "fizzyedit.person" ? path : env.storageName + ".file:" + path;
function storageGet(key) {
return Object.prototype.hasOwnProperty.call(env.storage, key) ? env.storage[key] : null;
}
@@ -222,18 +224,19 @@ const fizzyImports = (target) => ({
fizzy_web_now_real_ms() {
return Date.now();
},
+ fizzy_web_focus_epoch() { return focusEpoch; },
fizzy_web_storage_get(keyPtr, keyLen, bufPtr, bufLen) {
- const value = storageGet(env.storageName + ".file:" + str(keyPtr, keyLen));
+ const value = storageGet(fileKey(str(keyPtr, keyLen)));
if (value === null) return 0xffffffff;
const bytes = utf8encode(value);
if (bytes.length <= bufLen) new Uint8Array(mem(), bufPtr, bytes.length).set(bytes);
return bytes.length;
},
fizzy_web_storage_set(keyPtr, keyLen, valPtr, valLen) {
- storageSet(env.storageName + ".file:" + str(keyPtr, keyLen), str(valPtr, valLen));
+ storageSet(fileKey(str(keyPtr, keyLen)), str(valPtr, valLen));
},
fizzy_web_storage_remove(keyPtr, keyLen) {
- storageRemove(env.storageName + ".file:" + str(keyPtr, keyLen));
+ storageRemove(fileKey(str(keyPtr, keyLen)));
},
fizzy_web_plugin_fingerprint(fpPtr, fpLen) {
pluginFingerprint = str(fpPtr, fpLen);
@@ -713,6 +716,12 @@ onmessage = (e) => {
case "init":
start(m).catch((err) => postMessage({ type: "error", message: err instanceof Error ? err.message : String(err) }));
break;
+ case "personFocus":
+ if (m.value === null) delete env.storage["fizzyedit.person"];
+ else env.storage["fizzyedit.person"] = m.value;
+ focusEpoch++;
+ if (dvuiApp) dvuiApp.requestRender();
+ break;
case "size":
env.size = m.size;
applyForm();
diff --git a/web/fizzy.js b/web/fizzy.js
index 07d977fee..0d24ae3dd 100644
--- a/web/fizzy.js
+++ b/web/fizzy.js
@@ -7,6 +7,60 @@
// Stamped by the build (`build/web.zig`): the hash of what it loads, and the storage name
// (`-Dweb-storage`).
+// Normalize wheel axes without browser side effects. The returned state retains the
+// smallest deltas; the caller resets lowestBatch after a second without wheel input.
+export function wheelTicks(ev, state) {
+ const lowest = [...state.lowest];
+ const lowestBatch = [...state.lowestBatch];
+ const events = [];
+ for (const [i, delta, shiftSizes] of [[0, ev.deltaX, [9, 40]], [1, ev.deltaY, []]]) {
+ if (delta === 0) continue;
+ lowest[i] = Math.min(Math.abs(delta), lowest[i]);
+ lowestBatch[i] = Math.min(Math.abs(delta), lowestBatch[i]);
+ let ticks = -delta;
+ let trackpad = 0;
+ const low = lowestBatch[i];
+ const touchpadAdj = 0.025;
+ if (ev.deltaMode !== 0) {
+ ticks /= low;
+ } else if (i === 1 && ev.wheelDeltaY === -3 * delta) {
+ // macOS trackpads can produce multiples of 120 during a fast flick too.
+ trackpad = 1;
+ ticks = ticks / lowest[i] * touchpadAdj;
+ } else if (i === 1 && ev.wheelDeltaY && ev.wheelDeltaY % 120 === 0) {
+ // Chromium scales deltaY with Windows display scaling, but a notch stays 120.
+ ticks = ev.wheelDeltaY / 120;
+ } else if (low >= 100 || low === 16 || shiftSizes.includes(low) || low === 4.000244140625) {
+ ticks /= low;
+ if (low === 4.000244140625) ticks *= touchpadAdj;
+ } else {
+ trackpad = 1;
+ ticks = ticks / lowest[i] * touchpadAdj;
+ }
+ events.push({ axis: i, ticks, trackpad });
+ }
+ return { events, state: { lowest, lowestBatch } };
+}
+
+// One wheel reader per canvas, for the worker path and the page's own loop. The classifier's
+// batch minima reset after a second without wheel input: the user may have switched devices.
+// A trackpad pinch is the pinch listener's.
+export function wheelInput(canvas, send, now = () => Date.now()) {
+ let state = { lowest: [99999, 99999], lowestBatch: [99999, 99999] };
+ let lastMs = now();
+ canvas.addEventListener("wheel", (ev) => {
+ if (ev.ctrlKey && ev.deltaMode === 0) return;
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ const ms = now();
+ if (ms - lastMs > 1000) state.lowestBatch = [99999, 99999];
+ lastMs = ms;
+ const result = wheelTicks(ev, state);
+ state = result.state;
+ for (const { axis, trackpad, ticks } of result.events) send(axis, trackpad, ticks);
+ }, { capture: true, passive: false });
+}
+
// Pointer input is batched before crossing the worker boundary. Times are absolute
// performance-clock milliseconds; subtracting the receiver's origin preserves latency.
export function pointerInput(canvas, send, clockOrigin = performance.timeOrigin, done = () => {}) {
@@ -203,6 +257,12 @@ export class Fizzy {
run() {
const opts = this.opts;
const storageName = this.storageName;
+ let focusEpoch = 0;
+ window.addEventListener("focus", () => {
+ focusEpoch++;
+ if (dvuiApp) dvuiApp.requestRender();
+ });
+ const fileKey = (path) => path === "fizzyedit.person" ? path : storageName + ".file:" + path;
const persists = opts.persist !== false;
const onReady = opts.onReady ?? (() => {});
const onError = opts.onError ?? (() => {});
@@ -415,8 +475,9 @@ export class Fizzy {
},
// `core.fs` on the web: the app's own small files (settings.zon, recents.zon,
// keybinds.zon) live in localStorage, keyed by the path the app would have used.
+ fizzy_web_focus_epoch() { return focusEpoch; },
fizzy_web_storage_get(keyPtr, keyLen, bufPtr, bufLen) {
- const key = storageName + ".file:" + utf8.decode(new Uint8Array(wasmInstance.exports.memory.buffer, keyPtr, keyLen));
+ const key = fileKey(utf8.decode(new Uint8Array(wasmInstance.exports.memory.buffer, keyPtr, keyLen)));
let value = null;
try { value = store.getItem(key); } catch (_) {}
if (value === null) return 0xffffffff;
@@ -426,12 +487,12 @@ export class Fizzy {
},
fizzy_web_storage_set(keyPtr, keyLen, valPtr, valLen) {
const mem = wasmInstance.exports.memory.buffer;
- const key = storageName + ".file:" + utf8.decode(new Uint8Array(mem, keyPtr, keyLen));
+ const key = fileKey(utf8.decode(new Uint8Array(mem, keyPtr, keyLen)));
const value = utf8.decode(new Uint8Array(mem, valPtr, valLen));
try { store.setItem(key, value); } catch (err) { console.warn("fizzy: could not store", key, err); }
},
fizzy_web_storage_remove(keyPtr, keyLen) {
- const key = storageName + ".file:" + utf8.decode(new Uint8Array(wasmInstance.exports.memory.buffer, keyPtr, keyLen));
+ const key = fileKey(utf8.decode(new Uint8Array(wasmInstance.exports.memory.buffer, keyPtr, keyLen)));
try { store.removeItem(key); } catch (_) {}
},
// What the page remembers for an id, so the host can load it again after a disable
@@ -906,8 +967,8 @@ export class Fizzy {
// (every modern browser synthesizes this). Two things to do here in the capture phase,
// before dvui's bubble-phase listener on the canvas:
// 1. preventDefault + stopImmediatePropagation — keeps the browser from page-zooming
- // and keeps dvui's wheel handler from folding the tiny pinch delta into its
- // `scroll_lowest` minimum (an all-time min that never resets and otherwise
+ // and keeps the wheel reader from folding the tiny pinch delta into its
+ // `lowest` minimum (an all-time min that never resets and otherwise
// poisons trackpad scroll sensitivity for the rest of the session).
// 2. Forward the magnification into the wasm trackpad accumulator
// (`FizzyWebTrackpadMagnification`). The canvas widget drains it each frame and
@@ -1052,10 +1113,13 @@ export class Fizzy {
reduce: media("(prefers-reduced-motion: reduce)"),
});
const storage = {};
+ window.addEventListener("focus", () => {
+ try { post({ type: "personFocus", value: store.getItem("fizzyedit.person") }); } catch (_) {}
+ });
try {
for (let i = 0; i < store.length; i++) {
const key = store.key(i);
- if (key && key.startsWith(storageName + ".")) storage[key] = store.getItem(key);
+ if (key && (key === "fizzyedit.person" || key.startsWith(storageName + "."))) storage[key] = store.getItem(key);
}
} catch (_) {}
@@ -1273,35 +1337,7 @@ export class Fizzy {
canvas.addEventListener("contextmenu", (ev) => ev.preventDefault());
pointerInput(canvas, post, performance.timeOrigin, oskCheck);
- const scrollLowest = [99999, 99999];
- let scrollLowestBatch = [99999, 99999];
- let scrollLastMs = Date.now();
- canvas.addEventListener("wheel", (ev) => {
- ev.preventDefault();
- if (Date.now() - scrollLastMs > 1000) scrollLowestBatch = [99999, 99999];
- scrollLastMs = Date.now();
- const touchpadAdj = 0.025;
- const axis = (i, delta, shiftSizes) => {
- if (delta === 0) return;
- scrollLowest[i] = Math.min(Math.abs(delta), scrollLowest[i]);
- scrollLowestBatch[i] = Math.min(Math.abs(delta), scrollLowestBatch[i]);
- let ticks = -delta;
- let trackpad = 0;
- const low = scrollLowestBatch[i];
- if (ev.deltaMode !== 0) {
- ticks /= low;
- } else if (low >= 100 || low === 16 || shiftSizes.includes(low) || low === 4.000244140625) {
- ticks /= low;
- if (low === 4.000244140625) ticks *= touchpadAdj;
- } else {
- trackpad = 1;
- ticks = ticks / scrollLowest[i] * touchpadAdj;
- }
- event(4, i, trackpad, ticks, 0);
- };
- axis(0, ev.deltaX, [9, 40]);
- axis(1, ev.deltaY, []);
- }, { passive: false });
+ wheelInput(canvas, (axis, trackpad, ticks) => event(4, axis, trackpad, ticks, 0));
canvas.addEventListener("pointerdown", () => inputFilter.clear());
const keydown = (ev) => {
@@ -1397,6 +1433,10 @@ export class Fizzy {
}
app.requestRender();
}, performance.timeOrigin, () => app.oskCheck());
+ wheelInput(dvuiCanvas, (axis, trackpad, ticks) => {
+ app.instance.exports.add_event(4, axis, trackpad, ticks, 0);
+ app.requestRender();
+ });
for (const name of ["touchstart", "touchmove", "touchend", "touchcancel", "mousedown", "mousemove", "mouseup"]) {
dvuiCanvas.addEventListener(name, (ev) => ev.stopImmediatePropagation(), { capture: true });
}
diff --git a/web/tests/embed.test.mjs b/web/tests/embed.test.mjs
index a626bebe1..716335246 100644
--- a/web/tests/embed.test.mjs
+++ b/web/tests/embed.test.mjs
@@ -75,6 +75,7 @@ let fetches = 0;
let compiles = 0;
const bound = [];
const toggles = [];
+const storageApis = [];
const insets = [];
const memory = new WebAssembly.Memory({ initial: 1 });
globalThis.fetch = (url) => { fetches++; return Promise.resolve({ url }); };
@@ -84,6 +85,7 @@ WebAssembly.instantiate = async (module, imports) => {
bound.push([]);
insets.push([]);
toggles.push(imports.fizzy.fizzy_web_toggle_fullscreen);
+ storageApis.push(imports.fizzy);
return {
exports: {
memory: { buffer: memory.buffer },
@@ -98,11 +100,11 @@ WebAssembly.Table = class {};
const { Fizzy } = await import(pathToFileURL(path.join(dir, "fizzy.js")).href);
-const mount = async (parent) => {
+const mount = async (parent, options = {}) => {
const canvas = document.createElement("canvas");
canvas.parentElement = parent;
const ready = new Promise((resolve, reject) => {
- new Fizzy({ canvas, worker: false, persist: false, onReady: resolve, onError: reject, onInstance: () => {} }).run();
+ new Fizzy({ canvas, worker: false, persist: false, ...options, onReady: resolve, onError: reject, onInstance: () => {} }).run();
});
await ready;
return canvas;
@@ -162,6 +164,38 @@ toggles[1]();
assert.equal(document.fullscreenElement, parentA, "leaving the second embed reveals the first");
toggles[0]();
assert.equal(document.fullscreenElement, null, "leaving the first embed exits full screen");
+// Independent app namespaces share only the person's preferences.
+const stored = new Map();
+window.localStorage = {
+ getItem: (key) => stored.get(key) ?? null,
+ setItem: (key, value) => stored.set(key, value),
+ removeItem: (key) => stored.delete(key),
+ key: (i) => [...stored.keys()][i] ?? null,
+ get length() { return stored.size; },
+};
+await mount(new Element(), { persist: true, storage: "left" });
+await mount(new Element(), { persist: true, storage: "right" });
+const encoder = new TextEncoder();
+const put = (api, key, value) => {
+ const k = encoder.encode(key), v = encoder.encode(value);
+ new Uint8Array(memory.buffer, 0, k.length).set(k);
+ new Uint8Array(memory.buffer, 256, v.length).set(v);
+ api.fizzy_web_storage_set(0, k.length, 256, v.length);
+};
+const get = (api, key) => {
+ const k = encoder.encode(key);
+ new Uint8Array(memory.buffer, 0, k.length).set(k);
+ const n = api.fizzy_web_storage_get(0, k.length, 512, 256);
+ return n === 0xffffffff ? null : new TextDecoder().decode(new Uint8Array(memory.buffer, 512, n));
+};
+put(storageApis[2], "fizzyedit.person", ".{ .motion = 0.4 }");
+assert.equal(stored.get("fizzyedit.person"), ".{ .motion = 0.4 }");
+assert.equal(get(storageApis[3], "fizzyedit.person"), ".{ .motion = 0.4 }");
+put(storageApis[2], "settings.zon", ".{}");
+assert.equal(get(storageApis[3], "settings.zon"), null);
+const epoch = storageApis[3].fizzy_web_focus_epoch();
+window.dispatch("focus", {});
+assert.equal(storageApis[3].fizzy_web_focus_epoch(), epoch + 1);
console.log("embed.test: ok");
// Deliver text input as soon as the handler is installed, before setup completes.
diff --git a/web/tests/wheel-input.test.mjs b/web/tests/wheel-input.test.mjs
new file mode 100644
index 000000000..2dd09d0f5
--- /dev/null
+++ b/web/tests/wheel-input.test.mjs
@@ -0,0 +1,118 @@
+import { readFileSync } from 'node:fs';
+import assert from 'node:assert/strict';
+
+const source = readFileSync(new URL('../fizzy.js', import.meta.url), 'utf8');
+// Import the whole module with only its browser dependencies replaced.
+const moduleSource = source.replace(/import \{ Dvui, TextInput \} from "[^"\n]+";/,
+ 'class Dvui {} class TextInput {}');
+globalThis.document = { fullscreenEnabled: false };
+const { wheelTicks, wheelInput } = await import(`data:text/javascript;base64,${Buffer.from(moduleSource).toString('base64')}`);
+delete globalThis.document;
+const initial = () => ({ lowest: [99999, 99999], lowestBatch: [99999, 99999] });
+const event = (deltaY, extra = {}) => ({ deltaX: 0, deltaY, deltaMode: 0, ...extra });
+const vertical = (result) => Array.from(result.events, ({ axis, ticks, trackpad }) => ({ axis, ticks, trackpad }));
+
+for (const deltaY of [96, 66.67, 57.14, 100]) {
+ for (const direction of [1, -1]) {
+ const state = initial();
+ assert.deepEqual(vertical(wheelTicks(event(deltaY * direction, { wheelDeltaY: -120 * direction }), state)),
+ [{ axis: 1, ticks: -direction, trackpad: 0 }]);
+ assert.deepEqual(state, initial(), 'classification does not mutate the supplied state');
+ }
+}
+assert.deepEqual(vertical(wheelTicks(event(96, { wheelDeltaY: -240 }), initial())),
+ [{ axis: 1, ticks: -2, trackpad: 0 }]);
+
+for (const wheelDeltaY of [undefined, 0, -30]) {
+ let state = initial();
+ for (const deltaY of [3.5, 7, 12]) {
+ const result = wheelTicks(event(deltaY, { wheelDeltaY }), state);
+ assert.deepEqual(vertical(result), [{ axis: 1, ticks: -deltaY / 3.5 * 0.025, trackpad: 1 }]);
+ state = result.state;
+ }
+}
+// macOS trackpad flicks can also have legacy deltas that are multiples of 120.
+for (const direction of [1, -1]) {
+ for (const deltas of [[40, 80, 120], [3.5, 40, 80, 120]]) {
+ let state = initial();
+ for (const magnitude of deltas) {
+ const deltaY = magnitude * direction;
+ const result = wheelTicks(event(deltaY, { wheelDeltaY: -3 * deltaY }), state);
+ assert.deepEqual(vertical(result),
+ [{ axis: 1, ticks: -deltaY / deltas[0] * 0.025, trackpad: 1 }]);
+ state = result.state;
+ }
+ }
+}
+for (const deltaMode of [1, 2]) {
+ let state = initial();
+ for (const deltaY of [3, 6]) {
+ const result = wheelTicks(event(deltaY, { deltaMode, wheelDeltaY: -120 }), state);
+ assert.deepEqual(vertical(result), [{ axis: 1, ticks: -deltaY / 3, trackpad: 0 }]);
+ state = result.state;
+ }
+}
+for (const deltaY of [100, 16, 4.000244140625]) {
+ assert.deepEqual(vertical(wheelTicks(event(deltaY), initial())),
+ [{ axis: 1, ticks: deltaY === 4.000244140625 ? -0.025 : -1, trackpad: 0 }]);
+}
+for (const deltaX of [9, 40]) {
+ assert.deepEqual(vertical(wheelTicks(event(0, { deltaX, wheelDeltaY: -120 }), initial())),
+ [{ axis: 0, ticks: -1, trackpad: 0 }]);
+}
+assert.deepEqual(vertical(wheelTicks(event(0, { wheelDeltaY: 0 }), initial())), []);
+// A new batch forgets its minimum but retains the device-wide trackpad minimum.
+const trackpadState = wheelTicks(event(3.5), initial()).state;
+const nextBatch = { ...trackpadState, lowestBatch: [99999, 99999] };
+assert.deepEqual(vertical(wheelTicks(event(7), nextBatch)), [{ axis: 1, ticks: -0.05, trackpad: 1 }]);
+assert.deepEqual(vertical(wheelTicks(event(96, { wheelDeltaY: -120 }), trackpadState)),
+ [{ axis: 1, ticks: -1, trackpad: 0 }]);
+console.log('wheel input: scaled Windows notches, trackpads, line/page modes and fallback axes passed');
+
+function reader() {
+ let ms = 0;
+ let listener;
+ const sent = [];
+ const canvas = { addEventListener(name, fn, options) {
+ assert.equal(name, 'wheel');
+ assert.equal(options.capture, true);
+ assert.equal(options.passive, false);
+ listener = fn;
+ } };
+ wheelInput(canvas, (...args) => sent.push(args), () => ms);
+ return { sent, emit(deltaY, extra = {}, elapsed = 0) {
+ ms += elapsed;
+ let prevented = false;
+ let stopped = false;
+ listener({ ...event(deltaY), ctrlKey: false, ...extra,
+ preventDefault() { prevented = true; },
+ stopImmediatePropagation() { stopped = true; },
+ });
+ return { prevented, stopped };
+ } };
+}
+{
+ const r = reader();
+ assert.deepEqual(r.emit(66.67, { wheelDeltaY: -120 }), { prevented: true, stopped: true },
+ 'a notch is consumed before dvui sees it');
+ assert.deepEqual(r.sent, [[1, 0, -1]], 'a notch on a 150% display sends one tick');
+}
+{
+ const r = reader();
+ r.emit(3.5);
+ r.emit(7);
+ r.emit(100, {}, 500);
+ assert.deepEqual(r.sent.map((a) => a[1]), [1, 1, 1]);
+ r.emit(100, {}, 1100);
+ assert.deepEqual(r.sent.at(-1), [1, 0, -1], 'the batch minima reset after a second');
+ r.emit(7);
+ assert.deepEqual(r.sent.at(-1), [1, 1, -0.05], 'the all-time minimum is not reset');
+}
+{
+ const r = reader();
+ assert.deepEqual(r.emit(3.5, { ctrlKey: true }), { prevented: false, stopped: false });
+ assert.deepEqual(r.sent, [], "a pinch is the pinch listener's");
+ r.emit(100, { deltaX: 100 });
+ assert.deepEqual(r.sent, [[0, 0, -1], [1, 0, -1]], 'both axes');
+}
+console.log('wheel reader: capture, scaled notches, batch reset, retained minimum, pinch and both axes passed');