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 (`![shot](assets/shot.png)`) 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');