From ea8396257ca36848048f815bc3bc83672cbf5cad Mon Sep 17 00:00:00 2001 From: Colton Franklin <49629865+foxnne@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:52:36 -0500 Subject: [PATCH 01/23] zig: the 0.17 switch, scaffolded with marked stand-ins (#437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of fizzyedit/plans#5 (`plans/ZIG_0_17_PLAN.md`, step 2; the spike's findings are in fizzyedit/plans#12) The first PR into the `zig/0.17` integration branch. It opens the branch **green on 0.17**, so each PR after it is a reviewable migration with CI on every platform, not a long red stretch. ## What changes - **Toolchain:** `minimum_zig_version = "0.17.0"` everywhere; `ZIG_VERSION` 0.17.0 in `ci.yml`, `web.yml`, `release.yml`, `plugin-loop.yml`. - **Forks, pinned by commit on a `zig-0.17` branch in each** (tagged when `zig/0.17` merges to `main`, per `docs/DEPENDENCIES.md` rule 2): - `foxnne/dvui-dev` `2c899d18`: `fizzy-dev`'s 8 patches rebased onto dvui's `main` (`41f6703`), one-line conflict; passes dvui's tests on 0.17. - `foxnne/zig-objc` `f90394b`: `@typeInfo`, `**`; passes its tests. - `fizzyedit/md4zig` `eb5241c`: `@cImport` → translate-c module; passes its tests. - `foxnne/assetpack` `91eb632`: one line. - `fizzyedit/sdl_zig` `404f499`: five config values 0.17 rejects (still SDL 3.4.16). - zf and nightwatch repinned to their 0.17 commits. - **`scripts/zig-0.17/rewrite.sh`**: the mechanical half, re-run after every merge from `main`, deleted at the end. `zig fmt`'s upgrades, the optimize-mode, `dupeZ`/`bufPrintZ`, `EnumSet`, `std.meta.Int` renames, the `@typeInfo` field loops, and the zon stand-in below. - **Hand edits** from the spike: the build API renames, `**`, `@bitCast` to extern structs, `fingerprint.zig` translated loop for loop, Zoir/`Ast.parse` options, dvui's per-pointer capture (dvui#1014), and others. - **51 `ZIG017` stand-ins in 33 files**, each holding the place of a migration a later PR does properly: - the build system (custom `makeFn` steps, configure-time install paths, `--libc`, the MSVC shim, Velopack's `addPathDir`); - `std.zon.parse`: results now come from the caller's arena. The stand-in parses into a short-lived arena and deep-copies into the old allocator (`zig017ZonDupe`), with a file-local `zig017ZonFree`, so ownership is unchanged and nothing leaks; - the SDK's settings and command schemas reading field records (`sdk/src/zig017_fields.zig`); - Velopack's `@cImport`. - `recorded_sdk_shape_fingerprint` moved (dvui's shape since 2026-10-02). It is released with the merge to `main`. Diff size: 156 files, +2165/−870. Most of it is the script's output and the stand-ins' helpers. Read the script, not its output. ## SDK impact - [ ] None - [ ] Core-only or additive: reaches plugins at the next SDK release - [x] Fingerprint moved: recorded in `sdk/src/version.zig`, `sdk_version` untouched, PR labelled `sdk` - [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag ## Verified - [x] macOS (arm64, 0.17.0): `scripts/check-fmt.sh`; `zig build`; `zig build test` (91/91 steps); `zig build test-integration` (77/77 steps, 523/523 tests); `zig build check-web`; `zig build test-sdk-version`; `zig build check-tests-reached`. The spike also launched the app (window 1.1 s after `main`). - [ ] Windows: CI's cross-compile (not run locally). - [ ] Linux: CI (not run locally). - [x] Web: `zig build check-web`. **Example repos:** `scripts/check-examples.sh` now builds the examples' `zig-0.17` branches (`fizzyedit/example-app` `533c38b`, `example-plugin` `34c94ec`; back to `main` at the merge, marked `ZIG017`). It also now gives `example-app` this tree's SDK everywhere it pins one: its nested `plugins/shader`, and its bundled `example-plugin` (`hello`), whose pinned commit carried SDK 0.2.18. It stops macOS `tar` from adding AppleDouble `._*` files, which the backend took for SDL patches when the script ran on a Mac. All 8 example builds pass locally on macOS. **The plugin dev install is done for real here** (moved down from #438), since a stand-in that skipped it broke the plugin rebuild benchmark: `sdk/plugin_sdk_install.zig` is a program the plugin's build runs, reading `FIZZY_PROFILE`/`HOME` itself at make time, and `FIZZY_PLUGIN_USE_LLVM` becomes `-Dplugin-use-llvm` (0.17 caches the configuration without regard to the environment), which `scripts/plugin-loop/bench.py` passes. ## Follow-ups The next PRs into `zig/0.17`: the build system (next), zon arenas, `@typeInfo` with a fingerprint-equality test, Velopack translate-c, the example repos. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- .github/workflows/plugin-loop.yml | 4 +- .github/workflows/release.yml | 2 +- .github/workflows/web.yml | 2 +- app/App.zig | 8 +- app/Recents.zig | 71 +++++- app/Secrets.zig | 4 +- app/automation/Service.zig | 14 +- app/automation/overlay.zig | 28 +-- app/crash/crash.zig | 20 +- app/crash/image.zig | 14 +- app/keymap/Keymap.zig | 16 +- app/keymap/zon.zig | 30 +-- app/layout/Chooser.zig | 3 +- app/layout/Floats.zig | 4 +- app/layout/Layout.zig | 10 +- app/layout/Region.zig | 2 +- app/restart.zig | 4 +- app/settings/Settings.zig | 83 ++++++- app/settings/SettingsMigration.zig | 89 +++++++- app/settings/SettingsPluginsZon.zig | 52 ++--- app/single_instance/singleton_native.zig | 6 +- app/store/PluginLoader.zig | 12 +- app/store/PluginLoader_web.zig | 2 +- app/store/PluginStore.zig | 18 +- app/store/plugin_repo_asset.zig | 2 +- app/store/readme.zig | 28 +-- app/store/registry/compat.zig | 4 +- app/store/registry/store.zig | 24 +-- app/store/store_icon.zig | 28 +-- app/update/auto_update.zig | 15 +- app/update/update_install.zig | 19 +- app/update/update_notify.zig | 24 +-- app/watch/FolderWatcher.zig | 2 +- app/watch/SettingsWatcher.zig | 6 +- backend/build.zig | 8 +- backend/build.zig.zon | 10 +- backend/src/GpuRenderer.zig | 12 +- backend/src/Health.zig | 81 ++++++- backend/src/SDLBackend.zig | 215 +++++++++++-------- backend/src/platform/dialogs.zig | 8 +- backend/src/platform/geometry.zig | 55 ++++- backend/src/platform/linux_titlebar.zig | 8 +- backend/src/platform/macos_monitor.zig | 22 +- backend/src/platform/menu.zig | 18 +- backend/src/platform/root.zig | 2 +- backend/src/platform/titlebar.zig | 4 +- backend/src/platform/wayland_blur.zig | 8 +- backend/src/platform/win32_titlebar.zig | 44 ++-- backend/src/platform/window.zig | 48 ++--- backend/src/viewports.zig | 8 +- build.zig | 2 +- build.zig.zon | 18 +- build/app.zig | 11 +- build/common.zig | 2 +- build/exe.zig | 8 +- build/msvc.zig | 11 +- build/package.zig | 12 +- build/velopack.zig | 30 +-- build/web.zig | 8 +- core/Fling.zig | 2 +- core/corners.zig | 2 +- core/darwin_spawn.zig | 2 +- core/dialogs.zig | 12 +- core/gfx/FrameTarget.zig | 4 +- core/gfx/LiquidField.zig | 4 +- core/gfx/image.zig | 4 +- core/gfx/liquid_glass.zig | 2 +- core/gfx/perf.zig | 2 +- core/gfx/programs.zig | 6 +- core/gfx/water_surface.zig | 4 +- core/hitch.zig | 16 +- core/lsp/Client.zig | 4 +- core/math/direction.zig | 10 +- core/math/easing.zig | 4 +- core/motion.zig | 2 +- core/paths.zig | 4 +- core/platform.zig | 12 +- core/profile.zig | 47 +++- core/profile/Gpu.zig | 6 +- core/screens.zig | 2 +- core/shell_env.zig | 2 +- core/transport/NativeTransport.zig | 2 +- core/vfs/Fs.zig | 6 +- core/vfs/zip.zig | 6 +- core/widgets/CanvasWidget.zig | 6 +- core/widgets/Split.zig | 32 +-- libs/dvui-singleton-app/src/root.zig | 2 +- libs/dvui-singleton-app/src/unix_impl.zig | 2 +- libs/dvui-singleton-app/src/windows_impl.zig | 12 +- plugins/archive/build.zig.zon | 2 +- plugins/devtools/build.zig.zon | 2 +- plugins/image/build.zig.zon | 2 +- plugins/markdown/build.zig.zon | 6 +- plugins/markdown/src/md/code_language.zig | 4 +- plugins/markdown/src/md/image_format.zig | 2 +- plugins/markdown/src/md/net_image.zig | 24 +-- plugins/markdown/src/md/render_ast.zig | 2 +- plugins/shared/build/helpers.zig | 49 ++++- plugins/text/build.zig.zon | 2 +- plugins/text/src/Document.zig | 6 +- plugins/text/src/TextEditor.zig | 2 +- plugins/workbench/build.zig.zon | 2 +- plugins/workbench/src/FileLoadJob.zig | 14 +- plugins/workbench/src/Workspace.zig | 5 +- plugins/workbench/src/files.zig | 2 +- plugins/workbench/src/workbench_layout.zig | 73 ++++++- scripts/check-examples.sh | 15 +- scripts/plugin-loop/bench.py | 23 +- scripts/zig-0.17/fields.py | 44 ++++ scripts/zig-0.17/rewrite.sh | 43 ++++ scripts/zig-0.17/unused.py | 12 ++ scripts/zig-0.17/zon_standin.pl | 90 ++++++++ sdk/build.zig.zon | 13 +- sdk/plugin_sdk.zig | 199 ++++++++--------- sdk/plugin_sdk_install.zig | 62 ++++++ sdk/replay/BlackBox.zig | 14 +- sdk/replay/Input.zig | 14 +- sdk/replay/Snapshot.zig | 55 ++++- sdk/replay/anchor.zig | 2 +- sdk/src/Command.zig | 18 +- sdk/src/Manifest.zig | 71 +++++- sdk/src/Plugin.zig | 2 +- sdk/src/dvui_context.zig | 2 +- sdk/src/dylib.zig | 12 +- sdk/src/events.zig | 47 +++- sdk/src/fingerprint.zig | 78 +++---- sdk/src/keywords.zig | 2 +- sdk/src/render_bridge.zig | 18 +- sdk/src/settings.zig | 69 +++++- sdk/src/version.zig | 2 +- sdk/src/zig017_fields.zig | 31 +++ sdk/tape/Tape.zig | 49 ++++- sdk/tape/binary.zig | 20 +- sdk/tape/chord.zig | 2 +- sdk/tape/events.zig | 2 +- sdk/tape/ring.zig | 4 +- spikes/windows-composition/src/main.zig | 2 +- spikes/windows-composition/src/overlay.zig | 4 +- src/Entry.zig | 18 +- src/backend/WebBackend.zig | 28 +-- src/backend/backend_native.zig | 42 +++- src/backend/file_assoc.zig | 6 +- src/backend/layout_file.zig | 91 ++++++-- src/editor/DocumentWatcher.zig | 4 +- src/editor/Editor.zig | 38 ++-- src/editor/KeptDocuments.zig | 45 +++- src/editor/KeybindSettings.zig | 2 +- src/editor/Keybinds.zig | 14 +- src/editor/Popout.zig | 2 +- src/editor/caption_buttons.zig | 6 +- src/editor/captures.zig | 2 +- src/editor/dialogs/AboutFizzy.zig | 2 +- src/editor/dialogs/WebSaveAs.zig | 6 +- src/editor/explorer/settings.zig | 2 +- src/editor/layout.zig | 2 +- src/editor/verdict.zig | 49 ++++- tests/bench/bench_tape.zig | 2 +- tests/integration.zig | 165 +++++++++++--- 159 files changed, 2301 insertions(+), 971 deletions(-) create mode 100644 scripts/zig-0.17/fields.py create mode 100755 scripts/zig-0.17/rewrite.sh create mode 100644 scripts/zig-0.17/unused.py create mode 100644 scripts/zig-0.17/zon_standin.pl create mode 100644 sdk/plugin_sdk_install.zig create mode 100644 sdk/src/zig017_fields.zig diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 829d1da78..fc152bd87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ concurrency: cancel-in-progress: true env: - ZIG_VERSION: "0.16.0" + ZIG_VERSION: "0.17.0" jobs: changes: diff --git a/.github/workflows/plugin-loop.yml b/.github/workflows/plugin-loop.yml index 52c992559..8fd20fe89 100644 --- a/.github/workflows/plugin-loop.yml +++ b/.github/workflows/plugin-loop.yml @@ -16,7 +16,7 @@ on: inputs: zig_version: description: "Zig to measure with (a version, or master)" - default: "0.16.0" + default: "0.17.0" backends: description: "Comma-separated: default, llvm, self-hosted" default: "default,llvm,self-hosted" @@ -34,7 +34,7 @@ concurrency: cancel-in-progress: true env: - ZIG_VERSION: ${{ inputs.zig_version || '0.16.0' }} + ZIG_VERSION: ${{ inputs.zig_version || '0.17.0' }} jobs: measure: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3f116108..9a12ca8f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ concurrency: cancel-in-progress: false env: - ZIG_VERSION: "0.16.0" + ZIG_VERSION: "0.17.0" jobs: # Linux + Windows: no fizzy_release environment (no signing secrets needed). diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index e0cb83ca9..fde215db8 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -56,7 +56,7 @@ on: default: "" env: - ZIG_VERSION: "0.16.0" + ZIG_VERSION: "0.17.0" WEBSITE: fizzyedit/website jobs: diff --git a/app/App.zig b/app/App.zig index 276a6cbb5..4621b08c4 100644 --- a/app/App.zig +++ b/app/App.zig @@ -820,11 +820,11 @@ pub fn readPluginReservedField(gpa: std.mem.Allocator, settings_data: ?[:0]const const data = settings_data orelse return null; const plugins = SettingsPluginsZon.extractField(gpa, data, "plugins") orelse return null; defer gpa.free(plugins); - const plugins_z = gpa.dupeZ(u8, plugins) catch return null; + const plugins_z = gpa.dupeSentinel(u8, plugins, 0) catch return null; defer gpa.free(plugins_z); const id_block = SettingsPluginsZon.extractField(gpa, plugins_z, id) orelse return null; defer gpa.free(id_block); - const id_z = gpa.dupeZ(u8, id_block) catch return null; + const id_z = gpa.dupeSentinel(u8, id_block, 0) catch return null; defer gpa.free(id_z); return SettingsPluginsZon.extractField(gpa, id_z, field); } @@ -834,11 +834,11 @@ pub fn readPluginSettingsText(gpa: std.mem.Allocator, settings_data: ?[:0]const const data = settings_data orelse return null; const plugins = SettingsPluginsZon.extractField(gpa, data, "plugins") orelse return null; defer gpa.free(plugins); - const plugins_z = gpa.dupeZ(u8, plugins) catch return null; + const plugins_z = gpa.dupeSentinel(u8, plugins, 0) catch return null; defer gpa.free(plugins_z); const id_block = SettingsPluginsZon.extractField(gpa, plugins_z, id) orelse return null; defer gpa.free(id_block); - const id_z = gpa.dupeZ(u8, id_block) catch return null; + const id_z = gpa.dupeSentinel(u8, id_block, 0) catch return null; defer gpa.free(id_z); return SettingsPluginsZon.extractField(gpa, id_z, "settings"); } diff --git a/app/Recents.zig b/app/Recents.zig index 8a535f74f..07133b1cf 100644 --- a/app/Recents.zig +++ b/app/Recents.zig @@ -49,8 +49,14 @@ pub fn load(allocator: std.mem.Allocator, path: []const u8) !Recents { if (core.fs.readZ(allocator, dvui.io, path) catch null) |data| { defer allocator.free(data); - if (std.zon.parse.fromSliceAlloc(Disk, allocator, data, null, .{ .ignore_unknown_fields = true }) catch null) |disk| { - defer std.zon.parse.free(allocator, disk); + if ((zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Disk, .{ .gpa = allocator, .arena = zig017_arena.allocator(), .source = data, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Disk, allocator, zig017_v) else |zig017_e| zig017_e; + }) catch null) |disk| { + defer zig017ZonFree(allocator, disk); for (disk.folders) |folder| { // A folder on a mount (`gdrive://…`) is not the disk's to check: it exists @@ -166,3 +172,64 @@ pub fn deinit(recents: *Recents, allocator: std.mem.Allocator) void { recents.commands.deinit(allocator); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} diff --git a/app/Secrets.zig b/app/Secrets.zig index 2beff51b3..f4102611b 100644 --- a/app/Secrets.zig +++ b/app/Secrets.zig @@ -138,7 +138,7 @@ fn save(self: *Secrets) !void { const tmp = try std.fmt.allocPrint(self.gpa, "{s}.tmp", .{self.path}); defer self.gpa.free(tmp); { - var file = try std.Io.Dir.createFileAbsolute(self.io, tmp, .{ .permissions = @enumFromInt(0o600), .truncate = true }); + var file = try std.Io.Dir.createFileAbsolute(self.io, tmp, .{ .permissions = @fromBackingInt(@intCast(0o600)), .truncate = true }); defer file.close(self.io); try file.writeStreamingAll(self.io, out.written()); } @@ -162,7 +162,7 @@ test "secrets round-trip through a 0600 file and never through settings" { // On disk: only the owner can read it, and a fresh instance reads it back. const st = try tmp.dir.statFile(io, "secrets", .{}); - if (builtin.os.tag != .windows) try std.testing.expectEqual(@as(u32, 0o600), @as(u32, @intCast(st.permissions.toMode() & 0o777))); + if (builtin.target.os.tag != .windows) try std.testing.expectEqual(@as(u32, 0o600), @as(u32, @intCast(st.permissions.toMode() & 0o777))); var again = try Secrets.init(a, io, dir); defer again.deinit(); try std.testing.expectEqualStrings("1//abc\"quoted\"", again.get("drive.refresh_token").?); diff --git a/app/automation/Service.zig b/app/automation/Service.zig index a301e0ad6..7c1400a56 100644 --- a/app/automation/Service.zig +++ b/app/automation/Service.zig @@ -94,7 +94,7 @@ fn play(ctx: *anyopaque, bytes: []const u8) Api.Play { else => .{ .invalid = @errorName(err) }, }; if (self.current) |t| self.remember(.{ .ticket = t, .outcome = last }); - const ticket: Api.Ticket = @enumFromInt(self.next); + const ticket: Api.Ticket = @fromBackingInt(@intCast(self.next)); self.next += 1; self.current = ticket; return .{ .started = ticket }; @@ -152,22 +152,22 @@ pub fn frameEnded(self: *Service, end_micros: ?u32) bool { } fn snapshot(ctx: *anyopaque) Api.SnapshotTicket { - const s = from(ctx).snapshots orelse return @enumFromInt(0); - return @enumFromInt(s.request()); + const s = from(ctx).snapshots orelse return @fromBackingInt(@intCast(0)); + return @fromBackingInt(@intCast(s.request())); } fn snapshotText(ctx: *anyopaque, ticket: Api.SnapshotTicket) ?[]const u8 { const s = from(ctx).snapshots orelse return null; - return s.get(@intFromEnum(ticket)); + return s.get(@backingInt(ticket)); } fn capture(ctx: *anyopaque, window: []const u8) Api.CaptureTicket { - const c = from(ctx).captures orelse return @enumFromInt(0); - return @enumFromInt(c.request(window)); + const c = from(ctx).captures orelse return @fromBackingInt(@intCast(0)); + return @fromBackingInt(@intCast(c.request(window))); } fn captureImage(ctx: *anyopaque, ticket: Api.CaptureTicket) ?Api.Image { const c = from(ctx).captures orelse return null; - const image = c.get(@intFromEnum(ticket)) orelse return null; + const image = c.get(@backingInt(ticket)) orelse return null; return .{ .pixels = image.pixels, .width = image.width, .height = image.height }; } diff --git a/app/automation/overlay.zig b/app/automation/overlay.zig index 3f0029d88..cbd6b4797 100644 --- a/app/automation/overlay.zig +++ b/app/automation/overlay.zig @@ -618,12 +618,12 @@ fn drawTransport(player: *Player, tape: *const Tape, win: dvui.Rect) f32 { var phys: [5]dvui.Rect.Physical = undefined; for (pieces, &phys) |r, *out| out.* = wrs.rectToPhysical(r); - tr.prev = phys[@intFromEnum(Piece.prev)]; - tr.play = phys[@intFromEnum(Piece.play)]; - tr.next = phys[@intFromEnum(Piece.next)]; - tr.close = phys[@intFromEnum(Piece.close)]; + tr.prev = phys[@backingInt(Piece.prev)]; + tr.play = phys[@backingInt(Piece.play)]; + tr.next = phys[@backingInt(Piece.next)]; + tr.close = phys[@backingInt(Piece.close)]; const hovered: ?Piece = if (tr.pointer) |pt| for (phys, 0..) |r, i| { - if (i != @intFromEnum(Piece.track) and r.contains(pt)) break @enumFromInt(i); + if (i != @backingInt(Piece.track) and r.contains(pt)) break @fromBackingInt(@intCast(i)); } else null else null; barGlass(fw.data().id, &phys, s, hovered, apart); @@ -634,11 +634,11 @@ fn drawTransport(player: *Player, tape: *const Tape, win: dvui.Rect) f32 { const ink = theme().color(.content, .text); const playing = player.state == .playing or (player.state == .seeking and player.after_seek == .play); const play_glyph: Glyph = if (playing) .pause else if (player.state == .ended) .replay else .play; - barIcon(bounds, pieces[@intFromEnum(Piece.prev)], .prev, ink.opacity(0.8)); - barIcon(bounds, pieces[@intFromEnum(Piece.play)], play_glyph, ink); - barIcon(bounds, pieces[@intFromEnum(Piece.next)], .next, ink.opacity(0.8)); - barIcon(bounds, pieces[@intFromEnum(Piece.close)], .close, ink.opacity(0.65)); - tr.track = barTrack(player, tape, bounds, pieces[@intFromEnum(Piece.track)], ink); + barIcon(bounds, pieces[@backingInt(Piece.prev)], .prev, ink.opacity(0.8)); + barIcon(bounds, pieces[@backingInt(Piece.play)], play_glyph, ink); + barIcon(bounds, pieces[@backingInt(Piece.next)], .next, ink.opacity(0.8)); + barIcon(bounds, pieces[@backingInt(Piece.close)], .close, ink.opacity(0.65)); + tr.track = barTrack(player, tape, bounds, pieces[@backingInt(Piece.track)], ink); return std.math.clamp(p * 1.6, 0, 1); } @@ -655,9 +655,9 @@ fn barGeometry(center: dvui.Point, full_w: f32, p: f32, out: *[5]dvui.Rect) f32 var widths = piece_d; var round_w: f32 = 0; for (piece_d, 0..) |d, i| { - if (i != @intFromEnum(Piece.track)) round_w += d; + if (i != @backingInt(Piece.track)) round_w += d; } - widths[@intFromEnum(Piece.track)] = @max(80, full_w - round_w - 4 * bar_gap); + widths[@backingInt(Piece.track)] = @max(80, full_w - round_w - 4 * bar_gap); const gap = bar_gap * apart; var total: f32 = 4 * gap; for (widths) |w| total += w; @@ -678,7 +678,7 @@ fn barGlass(id: dvui.Id, pieces: []const dvui.Rect.Physical, s: f32, hovered: ?P var bubbles: [5]core.dialogs.Bubble = undefined; for (pieces, bubbles[0..pieces.len], 0..) |r, *b, i| b.* = .{ .rect = r, - .light = if (hovered != null and @intFromEnum(hovered.?) == i) core.dialogs.bubble_hover else 0, + .light = if (hovered != null and @backingInt(hovered.?) == i) core.dialogs.bubble_hover else 0, }; core.dialogs.glassBubbles(id, bubbles[0..pieces.len], s, .{ .merge = bar_merge, .shadow = apart }); } @@ -699,7 +699,7 @@ fn barIcon(bounds: dvui.Rect, r: dvui.Rect, glyph: Glyph, color: dvui.Color) voi .stroke_color = .{ .color = color }, .fill_color = .{ .color = color }, }, .{ - .id_extra = @intFromEnum(glyph), + .id_extra = @backingInt(glyph), .rect = .{ .x = r.x + (r.w - d) / 2 - bounds.x, .y = r.y + (r.h - d) / 2 - bounds.y, .w = d, .h = d }, }); } diff --git a/app/crash/crash.zig b/app/crash/crash.zig index 84949fdce..b56154aa5 100644 --- a/app/crash/crash.zig +++ b/app/crash/crash.zig @@ -25,7 +25,7 @@ const builtin = @import("builtin"); pub const image = @import("image.zig"); /// Where crash reports can be written: native targets std can catch faults on. -pub const supported = builtin.cpu.arch != .wasm32 and std.debug.have_segfault_handling_support; +pub const supported = builtin.target.cpu.arch != .wasm32 and std.debug.have_segfault_handling_support; pub const Module = struct { name_buf: [32]u8 = undefined, @@ -90,7 +90,7 @@ pub fn writeReport( try w.print(": {s}\n", .{s.name}), } try w.print("\n{s} {s}, sdk {s}, {t} {t} {t}\n\nmodules:\n", .{ - run.app, run.version, run.sdk, builtin.os.tag, builtin.cpu.arch, builtin.mode, + run.app, run.version, run.sdk, builtin.target.os.tag, builtin.target.cpu.arch, builtin.mode, }); for (modules, 0..) |*m, i| { try w.print(" {d} {s} {s} 0x{x} 0x{x} ", .{ i, m.name(), m.version(), m.image.base, m.image.end - m.image.base }); @@ -144,7 +144,7 @@ var run_info: ?Run = null; /// The report's file, once `writeTo` has named it. var path_buf: [std.fs.max_path_bytes:0]u8 = undefined; var path_len: usize = 0; -var path_w: if (builtin.os.tag == .windows) [std.fs.max_path_bytes:0]u16 else void = undefined; +var path_w: if (builtin.target.os.tag == .windows) [std.fs.max_path_bytes:0]u16 else void = undefined; /// Starts reporting for this run: enters the executable as module 0 and catches aborts and /// traps. Call once, first thing. std's segfault handler is the root's to switch on @@ -154,7 +154,7 @@ pub fn init(run: Run) void { if (!supported) return; run_info = run; addModule(run.app, run.version, @intFromPtr(&init)); - if (builtin.os.tag != .windows) { + if (builtin.target.os.tag != .windows) { const act: std.posix.Sigaction = .{ .handler = .{ .sigaction = onAbortOrTrap }, .mask = std.posix.sigemptyset(), @@ -169,16 +169,16 @@ pub fn init(run: Run) void { /// Nothing is written there unless the run crashes. pub fn writeTo(io: std.Io, config_folder: []const u8) void { if (!supported) return; - const pid = if (builtin.os.tag == .windows) win.GetCurrentProcessId() else std.c.getpid(); + const pid = if (builtin.target.os.tag == .windows) win.GetCurrentProcessId() else std.c.getpid(); const launched = std.Io.Timestamp.now(io, .real).toMilliseconds(); const sep = std.fs.path.sep; - const path = std.fmt.bufPrintZ(&path_buf, "{s}{c}crashes{c}{d}-{d}.txt", .{ config_folder, sep, sep, launched, pid }) catch return; + const path = std.fmt.bufPrintSentinel(&path_buf, "{s}{c}crashes{c}{d}-{d}.txt", .{ config_folder, sep, sep, launched, pid }, 0) catch return; const dir = std.fs.path.dirname(path).?; std.Io.Dir.cwd().createDirPath(io, dir) catch |err| { std.log.warn("crash reports: can't create {s}: {t}", .{ dir, err }); return; }; - if (builtin.os.tag == .windows) { + if (builtin.target.os.tag == .windows) { const n = std.unicode.wtf8ToWtf16Le(&path_w, path) catch return; if (n >= path_w.len) return; path_w[n] = 0; @@ -248,7 +248,7 @@ fn report(what: What, where: Where) void { } fn writeFile(text: []const u8) bool { - if (builtin.os.tag == .windows) { + if (builtin.target.os.tag == .windows) { const h = win.CreateFileW(&path_w, win.GENERIC_WRITE, 0, null, win.CREATE_ALWAYS, win.FILE_ATTRIBUTE_NORMAL, null); if (h == win.INVALID_HANDLE_VALUE) return false; defer _ = win.CloseHandle(h); @@ -261,7 +261,7 @@ fn writeFile(text: []const u8) bool { } fn writeStderr(text: []const u8) void { - if (builtin.os.tag == .windows) { + if (builtin.target.os.tag == .windows) { _ = win.writeAll(win.GetStdHandle(win.STD_ERROR_HANDLE) orelse return, text); } else _ = posixWriteAll(2, text); } @@ -278,7 +278,7 @@ fn posixWriteAll(fd: std.c.fd_t, text: []const u8) bool { /// The file name of the image holding `address`, for a frame in no known module. fn imageName(address: usize, buf: []u8) ?[]const u8 { - if (builtin.os.tag == .windows) return null; + if (builtin.target.os.tag == .windows) return null; var info: Dl_info = undefined; if (dladdr(@ptrFromInt(address), &info) == 0) return null; const path = std.mem.span(info.dli_fname orelse return null); diff --git a/app/crash/image.zig b/app/crash/image.zig index f441d5301..b218209ac 100644 --- a/app/crash/image.zig +++ b/app/crash/image.zig @@ -33,7 +33,7 @@ pub const Image = struct { /// The image `address` lies in, or null where the platform can't say (or it lies in none). pub fn containing(address: usize) ?Image { - return switch (builtin.os.tag) { + return switch (builtin.target.os.tag) { .macos, .ios, .tvos, .visionos, .watchos => macho(address), .linux, .freebsd, .netbsd, .openbsd, .dragonfly => elf(address), .windows => pe(address), @@ -101,9 +101,9 @@ fn elf(address: usize) ?Image { fn gnuBuildId(notes: []const u8) ?[]const u8 { var at: usize = 0; while (at + 12 <= notes.len) { - const namesz = std.mem.readInt(u32, notes[at..][0..4], builtin.cpu.arch.endian()); - const descsz = std.mem.readInt(u32, notes[at + 4 ..][0..4], builtin.cpu.arch.endian()); - const kind = std.mem.readInt(u32, notes[at + 8 ..][0..4], builtin.cpu.arch.endian()); + const namesz = std.mem.readInt(u32, notes[at..][0..4], builtin.target.cpu.arch.endian()); + const descsz = std.mem.readInt(u32, notes[at + 4 ..][0..4], builtin.target.cpu.arch.endian()); + const kind = std.mem.readInt(u32, notes[at + 8 ..][0..4], builtin.target.cpu.arch.endian()); const name_at = at + 12; const desc_at = name_at + std.mem.alignForward(usize, namesz, 4); const next = desc_at + std.mem.alignForward(usize, descsz, 4); @@ -158,12 +158,12 @@ fn pe(address: usize) ?Image { } test "the image holding this code contains it, and names its build" { - if (builtin.os.tag != .macos and builtin.os.tag != .linux and builtin.os.tag != .windows) return error.SkipZigTest; + if (builtin.target.os.tag != .macos and builtin.target.os.tag != .linux and builtin.target.os.tag != .windows) return error.SkipZigTest; const here = @intFromPtr(&containing); const image = containing(here) orelse return error.NoImage; try std.testing.expect(image.contains(here)); // Zig's Mach-O linker always writes an LC_UUID; ELF and PE only carry an id when asked to. - if (builtin.os.tag == .macos) try std.testing.expectEqual(16, image.buildId().len); + if (builtin.target.os.tag == .macos) try std.testing.expectEqual(16, image.buildId().len); } test "a GNU build-id note is found among others" { @@ -172,7 +172,7 @@ test "a GNU build-id note is found among others" { return std.mem.toBytes(v); } }.u; - const other = w(4) ++ w(8) ++ w(1) ++ "GNU\x00".* ++ [_]u8{0} ** 8; + const other = w(4) ++ w(8) ++ w(1) ++ "GNU\x00".* ++ @as([8]u8, @splat(0)); const build_id = w(4) ++ w(5) ++ w(std.elf.NT_GNU_BUILD_ID) ++ "GNU\x00".* ++ "\xde\xad\xbe\xef\x01\x00\x00\x00".*; try std.testing.expectEqualSlices(u8, "\xde\xad\xbe\xef\x01", gnuBuildId(&(other ++ build_id)).?); try std.testing.expectEqual(null, gnuBuildId(&other)); diff --git a/app/keymap/Keymap.zig b/app/keymap/Keymap.zig index 360d62d3c..1f83374f0 100644 --- a/app/keymap/Keymap.zig +++ b/app/keymap/Keymap.zig @@ -60,8 +60,8 @@ pub const When = packed struct { /// context-specific binding always wins over a global one on the same chord. pub fn weight(self: When) u8 { var n: u8 = 0; - inline for (@typeInfo(When).@"struct".fields) |f| { - if (@field(self, f.name)) n += 1; + inline for (@typeInfo(When).@"struct".field_names, @typeInfo(When).@"struct".field_types, @typeInfo(When).@"struct".field_attrs) |f_name, _, _| { + if (@field(self, f_name)) n += 1; } return n; } @@ -79,9 +79,9 @@ pub const When = packed struct { const tok = std.mem.trim(u8, raw, " \t"); if (tok.len == 0) continue; var matched = false; - inline for (@typeInfo(When).@"struct".fields) |f| { - if (eqlCamelOrSnake(tok, f.name)) { - @field(out, f.name) = true; + inline for (@typeInfo(When).@"struct".field_names, @typeInfo(When).@"struct".field_types, @typeInfo(When).@"struct".field_attrs) |f_name, _, _| { + if (eqlCamelOrSnake(tok, f_name)) { + @field(out, f_name) = true; matched = true; } } @@ -205,8 +205,8 @@ fn best(self: Keymap, stroke: Stroke, ctx: When, active_owner: ?[]const u8) ?Bin winner = b; continue; }; - const b_rank = (@as(u16, @intFromEnum(b.source)) << 8) | b.when.weight(); - const w_rank = (@as(u16, @intFromEnum(w.source)) << 8) | w.when.weight(); + 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 // 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; @@ -322,7 +322,7 @@ pub fn conflicts(self: Keymap, gpa: Allocator) ![]Conflict { for (self.bindings.items, 0..) |b, i| { const cmd = b.command orelse continue; - const rank: u16 = (@as(u16, @intFromEnum(b.source)) << 8) | b.when.weight(); + const rank: u16 = (@as(u16, @backingInt(b.source)) << 8) | b.when.weight(); 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; diff --git a/app/keymap/zon.zig b/app/keymap/zon.zig index 5fa7bc3a4..203e9869f 100644 --- a/app/keymap/zon.zig +++ b/app/keymap/zon.zig @@ -111,7 +111,7 @@ pub const File = struct { const Parsed = struct { ast: std.zig.Ast, zoir: std.zig.Zoir }; fn parseZon(gpa: Allocator, source: [:0]const u8) ?Parsed { - var ast = std.zig.Ast.parse(gpa, source, .zon) catch return null; + var ast = std.zig.Ast.parse(gpa, source, .{ .mode = .zon }) catch return null; errdefer ast.deinit(gpa); var zoir = std.zig.ZonGen.generate(gpa, ast, .{ .parse_str_lits = true }) catch { ast.deinit(gpa); @@ -130,12 +130,12 @@ fn findField( container: std.zig.Zoir.Node.Index, name: []const u8, ) ?std.zig.Zoir.Node.Index { - const s = switch (container.get(zoir)) { + const s = switch (container.get(&zoir)) { .struct_literal => |sl| sl, else => return null, }; for (s.names, 0..) |field_name, i| { - if (std.mem.eql(u8, field_name.get(zoir), name)) return s.vals.at(@intCast(i)); + if (std.mem.eql(u8, field_name.get(&zoir), name)) return s.vals.at(@intCast(i)); } return null; } @@ -146,7 +146,7 @@ fn stringField( name: []const u8, ) ?[]const u8 { const node = findField(zoir, container, name) orelse return null; - return switch (node.get(zoir)) { + return switch (node.get(&zoir)) { .string_literal => |s| s, else => null, }; @@ -154,7 +154,7 @@ fn stringField( /// 1-based line/column of a node's first token. fn locationOf(ast: std.zig.Ast, zoir: std.zig.Zoir, node: std.zig.Zoir.Node.Index) struct { line: u32, column: u32 } { - const tok = ast.firstToken(node.getAstNode(zoir)); + const tok = ast.firstToken(node.getAstNode(&zoir)); const start = ast.tokenStart(tok); var line: u32 = 1; var col: u32 = 1; @@ -188,7 +188,7 @@ fn collectBlock( owner_id: ?[]const u8, platform: Platform, ) Allocator.Error!void { - const range = switch (block.get(zoir)) { + const range = switch (block.get(&zoir)) { .array_literal => |r| r, .empty_literal => return, else => { @@ -203,7 +203,7 @@ fn collectBlock( const entry = range.at(i); const loc = locationOf(ast, zoir, entry); - switch (entry.get(zoir)) { + switch (entry.get(&zoir)) { .struct_literal => {}, else => { c.diag(loc.line, loc.column, "binding must be a struct, e.g. .{{ .keys = \"ctrl+s\", .command = \"fizzy.save\" }}", .{}); @@ -226,7 +226,7 @@ fn collectBlock( c.diag(loc.line, loc.column, "binding is missing `.command` (use `.command = null` to unbind)", .{}); continue; }; - const command: ?[]const u8 = switch (command_node.get(zoir)) { + const command: ?[]const u8 = switch (command_node.get(&zoir)) { .string_literal => |s| s, .null => null, else => { @@ -244,8 +244,8 @@ fn collectBlock( // `.args`, when given, is a struct literal of the command's arguments, kept as written. var args_text: ?[]const u8 = null; if (findField(zoir, entry, "args")) |args_node| { - switch (args_node.get(zoir)) { - .struct_literal => args_text = ast.getNodeSource(args_node.getAstNode(zoir)), + switch (args_node.get(&zoir)) { + .struct_literal => args_text = ast.getNodeSource(args_node.getAstNode(&zoir)), .empty_literal => {}, else => { c.diag(loc.line, loc.column, "`.args` must be a struct of the command's arguments, e.g. .{{ .line = 1 }}", .{}); @@ -262,7 +262,7 @@ fn collectBlock( var when_text: ?[]u8 = null; errdefer if (when_text) |w| c.gpa.free(w); if (findField(zoir, entry, "when")) |when_node| { - switch (when_node.get(zoir)) { + switch (when_node.get(&zoir)) { .string_literal => |s| { var unknown: std.ArrayList([]const u8) = .empty; defer unknown.deinit(c.gpa); @@ -332,10 +332,10 @@ pub fn parse(gpa: Allocator, source: [:0]const u8, platform: Platform) Allocator } if (findField(parsed.zoir, .root, "plugins")) |plugins_node| { - switch (plugins_node.get(parsed.zoir)) { + switch (plugins_node.get(&parsed.zoir)) { .struct_literal => |sl| { for (sl.names, 0..) |id_str, i| { - const id = id_str.get(parsed.zoir); + const id = id_str.get(&parsed.zoir); try collectBlock(&c, parsed.ast, parsed.zoir, sl.vals.at(@intCast(i)), id, platform); } }, @@ -577,7 +577,7 @@ test "format round-trips through parse" { const written = try format(a, first.bindings); defer a.free(written); - const written_z = try a.dupeZ(u8, written); + const written_z = try a.dupeSentinel(u8, written, 0); defer a.free(written_z); var second = try parse(a, written_z, .other); @@ -631,7 +631,7 @@ test "backslash keys survive a write/read cycle" { const written = try format(a, f.bindings); defer a.free(written); - const written_z = try a.dupeZ(u8, written); + const written_z = try a.dupeSentinel(u8, written, 0); defer a.free(written_z); var again = try parse(a, written_z, .other); defer again.deinit(a); diff --git a/app/layout/Chooser.zig b/app/layout/Chooser.zig index 6240f55bc..741fd1d89 100644 --- a/app/layout/Chooser.zig +++ b/app/layout/Chooser.zig @@ -303,7 +303,8 @@ fn carryLifted(self: *Chooser) void { } else { f.beginViewDrag(lift.id, lift.rect); // From the motion that lifted it on: a release in the same frame is the drag's too. - if (dvui.currentWindow().capture) |cm| dvui.captureMouseCustom(cm, lift.event); + if (dvui.currentWindow().captures[0]) |cm| // ZIG017: per-pointer capture (dvui#1014); 0 is the mouse + dvui.captureMouseCustom(cm, lift.event); } self.carried = lift.id; dvui.refresh(null, @src(), null); diff --git a/app/layout/Floats.zig b/app/layout/Floats.zig index 2536b42d9..b3f723813 100644 --- a/app/layout/Floats.zig +++ b/app/layout/Floats.zig @@ -472,10 +472,10 @@ fn syncStack(l: *Layout) void { const floats = &l.state.floats; const n = @min(floats.items.items.len, max); var ids: [max]u64 = undefined; - for (floats.items.items[0..n], 0..) |f, i| ids[i] = @intFromEnum(f.win_id); + for (floats.items.items[0..n], 0..) |f, i| ids[i] = @backingInt(f.win_id); const stack = dvui.currentWindow().subwindows.stack.items; const stack_ids = l.arena.alloc(u64, stack.len) catch return; - for (stack, 0..) |sw, i| stack_ids[i] = @intFromEnum(sw.id); + for (stack, 0..) |sw, i| stack_ids[i] = @backingInt(sw.id); var order: [max]usize = undefined; rules.stackOrder(order[0..n], ids[0..n], stack_ids); floats.reorder(order[0..n]); diff --git a/app/layout/Layout.zig b/app/layout/Layout.zig index db18ba16a..f2af5dc34 100644 --- a/app/layout/Layout.zig +++ b/app/layout/Layout.zig @@ -517,7 +517,7 @@ fn claimedElsewhere( if (sdk.keywords.groupKey(r.keywords) == want) continue; if (self.state.assignment(r.name) != null) continue; const theirs = sdk.keywords.strength(r.keywords, s.keywords); - if (@intFromEnum(theirs) > @intFromEnum(mine)) return true; + if (@backingInt(theirs) > @backingInt(mine)) return true; if (theirs == mine and mine_at != null and i < mine_at.?) return true; } return false; @@ -890,7 +890,7 @@ fn resetPack(b: *dvui.BoxWidget) void { b.packed_children = 0; b.total_weight = 0; b.min_space_taken = 0; - if (builtin.mode == .Debug) b.child_id = .zero; + if (builtin.mode == .debug) b.child_id = .zero; } // ── The base layer: regions and keywords ──────────────────────────────────────────────────── @@ -1029,7 +1029,7 @@ pub fn beginPluginRegion(self: *Layout, spec: sdk.RegionSpec) ?sdk.RegionSpec.To self.plugin_regions[self.plugin_depth] = r; self.plugin_depth += 1; - return @enumFromInt(self.plugin_depth); + return @fromBackingInt(@intCast(self.plugin_depth)); } pub fn drawPluginRegionContents(self: *Layout, token: sdk.RegionSpec.Token) !dvui.App.Result { @@ -1108,7 +1108,7 @@ 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. - const depth = @intFromEnum(token); + 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 }); return; @@ -1118,7 +1118,7 @@ pub fn endPluginRegion(self: *Layout, token: sdk.RegionSpec.Token) void { } fn pluginRegion(self: *Layout, token: sdk.RegionSpec.Token) ?*Region { - const depth = @intFromEnum(token); + const depth = @backingInt(token); if (depth == 0 or depth > self.plugin_depth) return null; return &self.plugin_regions[depth - 1]; } diff --git a/app/layout/Region.zig b/app/layout/Region.zig index a8febf4cc..cfb85bae7 100644 --- a/app/layout/Region.zig +++ b/app/layout/Region.zig @@ -955,7 +955,7 @@ fn nameExtra(name: []const u8) usize { } fn extraFor(name: []const u8, side: SplitTree.Side) usize { - return @truncate(nameExtra(name) ^ @as(u64, @intFromEnum(side)) << 8); + return @truncate(nameExtra(name) ^ @as(u64, @backingInt(side)) << 8); } fn idOf(state: *Layout.State, name: []const u8) ?dvui.Id { diff --git a/app/restart.zig b/app/restart.zig index 923648c8a..d2baf6224 100644 --- a/app/restart.zig +++ b/app/restart.zig @@ -141,7 +141,7 @@ fn start(io: std.Io, gpa: std.mem.Allocator, handover_to: ?[]const u8) !void { // 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. - if (comptime builtin.os.tag.isDarwin()) { + if (comptime builtin.target.os.tag.isDarwin()) { _ = try core.darwin_spawn.spawn(gpa, .{ .argv = argv.items, .stdin = .discard, .stdout = .discard, .stderr = .discard, .new_session = true }, null); return; } @@ -150,7 +150,7 @@ fn start(io: std.Io, gpa: std.mem.Allocator, handover_to: ?[]const u8) !void { .stdin = .ignore, .stdout = .ignore, .stderr = .ignore, - .pgid = if (builtin.os.tag == .windows) null else 0, + .pgid = if (builtin.target.os.tag == .windows) null else 0, .create_no_window = true, }); } diff --git a/app/settings/Settings.zig b/app/settings/Settings.zig index 6cb8ad1aa..c4c7ffae3 100644 --- a/app/settings/Settings.zig +++ b/app/settings/Settings.zig @@ -86,7 +86,7 @@ glass_shader: bool = true, /// Whether a float can leave the main window into an OS window of its own, and a view be carried /// past the main window to open one there. Taken at launch: a change waits for a restart, which the /// app says (`Editor.restartPending`). On by default on macOS; `FIZZY_POPOUT` overrides it. -float_windows: bool = builtin.os.tag == .macos, +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, /// macOS 26) rather than the app's, where floats are windows. Applied at once; `FIZZY_NATIVE_GLASS` @@ -126,7 +126,7 @@ plugin_update_mode: PluginUpdateMode = .prompt, /// 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. pub const GlassDefaults = struct { opacity_dark: f32, opacity_light: f32 }; -pub const default_glass: GlassDefaults = if (builtin.target.cpu.arch == .wasm32 or (builtin.os.tag != .macos and builtin.os.tag != .windows)) +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 } else .{ .opacity_dark = 0.7, .opacity_light = 0.3 }; @@ -197,7 +197,7 @@ pub fn load(allocator: std.mem.Allocator, path: []const u8, plugins_dir: ?[]cons pub fn freeParsed(allocator: std.mem.Allocator, parsed: Settings) void { var value = parsed; if (value.theme.ptr == default_theme.ptr) value.theme = ""; - std.zon.parse.free(allocator, value); + zig017ZonFree(allocator, value); } /// Parses `data` (already-read `settings.zon` bytes) into a `Settings` value, ignoring unknown @@ -210,7 +210,13 @@ pub fn freeParsed(allocator: std.mem.Allocator, parsed: Settings) void { /// which owns the live value's long-lived `theme`). pub fn parseOnly(allocator: std.mem.Allocator, data: [:0]const u8) !Settings { @setEvalBranchQuota(10_000); - var parsed = try std.zon.parse.fromSliceAlloc(Settings, allocator, data, null, .{ .ignore_unknown_fields = true }); + var parsed = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Settings, .{ .gpa = allocator, .arena = zig017_arena.allocator(), .source = data, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Settings, allocator, zig017_v) else |zig017_e| zig017_e; + }); SettingsMigration.windowGlass(allocator, data, &parsed); return parsed; } @@ -246,12 +252,12 @@ pub fn serialize(settings: *const Settings, allocator: std.mem.Allocator) ![]u8 defer aw.deinit(); var any = false; - inline for (std.meta.fields(Settings)) |f| { - if (!fieldEqual(f.type, @field(settings, f.name), @field(default_value, f.name))) { + inline for (@typeInfo(Settings).@"struct".field_names, @typeInfo(Settings).@"struct".field_types, @typeInfo(Settings).@"struct".field_attrs) |f_name, f_type, _| { + if (!fieldEqual(f_type, @field(settings, f_name), @field(default_value, f_name))) { if (!any) try aw.writer.writeAll(".{\n"); any = true; - try aw.writer.print(" .{f} = ", .{std.zig.fmtId(f.name)}); - try std.zon.stringify.serialize(@field(settings, f.name), .{}, &aw.writer); + try aw.writer.print(" .{f} = ", .{std.zig.fmtId(f_name)}); + try std.zon.stringify.serialize(@field(settings, f_name), .{}, &aw.writer); try aw.writer.writeAll(",\n"); } } @@ -263,3 +269,64 @@ pub fn serialize(settings: *const Settings, allocator: std.mem.Allocator) ![]u8 pub fn deinit(settings: *Settings, allocator: std.mem.Allocator) void { allocator.free(settings.theme); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} diff --git a/app/settings/SettingsMigration.zig b/app/settings/SettingsMigration.zig index 901819ead..4592718a3 100644 --- a/app/settings/SettingsMigration.zig +++ b/app/settings/SettingsMigration.zig @@ -75,13 +75,17 @@ pub fn migrateToPerPluginEnabled(allocator: std.mem.Allocator, settings_zon_path defer allocator.free(data); @setEvalBranchQuota(10_000); - const legacy = std.zon.parse.fromSliceAlloc(LegacyDisk, allocator, data, null, .{ - .ignore_unknown_fields = true, + const legacy = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(LegacyDisk, .{ .gpa = allocator, .arena = zig017_arena.allocator(), .source = data, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(LegacyDisk, allocator, zig017_v) else |zig017_e| zig017_e; }) catch |err| { dvui.log.warn("settings: could not parse settings.zon for R12 enabled migration ({s}); skipping", .{@errorName(err)}); return; }; - defer std.zon.parse.free(allocator, legacy); + defer zig017ZonFree(allocator, legacy); const blocks = SettingsPluginsZon.listPluginBlocks(allocator, data) catch return; defer SettingsPluginsZon.freeEntries(allocator, blocks); @@ -195,15 +199,15 @@ pub fn migrateToPerPluginEnabled(allocator: std.mem.Allocator, settings_zon_path /// `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.dupeZ(u8, text) catch return false; + const z = gpa.dupeSentinel(u8, text, 0) catch return false; defer gpa.free(z); if (SettingsPluginsZon.extractField(gpa, z, "settings")) |s| { gpa.free(s); return true; } - inline for (@typeInfo(SettingsPluginsZon.Reserved).@"struct".fields) |field| { - if (SettingsPluginsZon.extractField(gpa, z, field.name)) |s| { + inline for (@typeInfo(SettingsPluginsZon.Reserved).@"struct".field_names, @typeInfo(SettingsPluginsZon.Reserved).@"struct".field_types, @typeInfo(SettingsPluginsZon.Reserved).@"struct".field_attrs) |field_name, _, _| { + if (SettingsPluginsZon.extractField(gpa, z, field_name)) |s| { gpa.free(s); return true; } @@ -254,13 +258,80 @@ const LegacyGlass = struct { /// dialogs' blur goes with them, as the roughness that followed it does. pub fn windowGlass(allocator: std.mem.Allocator, data: [:0]const u8, settings: *Settings) void { @setEvalBranchQuota(10_000); - const legacy = std.zon.parse.fromSliceAlloc(LegacyGlass, allocator, data, null, .{ .ignore_unknown_fields = true }) catch return; - defer std.zon.parse.free(allocator, legacy); + const legacy = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(LegacyGlass, .{ .gpa = allocator, .arena = zig017_arena.allocator(), .source = data, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(LegacyGlass, allocator, zig017_v) else |zig017_e| zig017_e; + }) catch return; + defer zig017ZonFree(allocator, legacy); if (legacy.dialog_opacity == null and legacy.dialog_blur == null and legacy.dialog_lift == null and legacy.dialog_detail == null and legacy.dialog_refraction == null) return; - const opaque_window = comptime builtin.target.cpu.arch == .wasm32 or (builtin.os.tag != .macos and builtin.os.tag != .windows); + const opaque_window = comptime builtin.target.cpu.arch == .wasm32 or (builtin.target.os.tag != .macos and builtin.target.os.tag != .windows); if (opaque_window) if (legacy.dialog_opacity) |o| { settings.window_opacity_dark = std.math.clamp(o, 0, 1); settings.window_opacity_light = std.math.clamp(o, 0, 1); }; } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} diff --git a/app/settings/SettingsPluginsZon.zig b/app/settings/SettingsPluginsZon.zig index bb787c75c..e112e2bf5 100644 --- a/app/settings/SettingsPluginsZon.zig +++ b/app/settings/SettingsPluginsZon.zig @@ -43,13 +43,13 @@ pub fn freeEntries(gpa: Allocator, entries: []const Entry) void { /// restriction on `name`). Returns the field's value node, or null if `container` isn't a struct /// literal or has no such field. fn findField(zoir: std.zig.Zoir, container: std.zig.Zoir.Node.Index, name: []const u8) ?std.zig.Zoir.Node.Index { - const node = container.get(zoir); + const node = container.get(&zoir); const s = switch (node) { .struct_literal => |sl| sl, else => return null, }; for (s.names, 0..) |field_name, i| { - if (std.mem.eql(u8, field_name.get(zoir), name)) return s.vals.at(@intCast(i)); + if (std.mem.eql(u8, field_name.get(&zoir), name)) return s.vals.at(@intCast(i)); } return null; } @@ -57,7 +57,7 @@ fn findField(zoir: std.zig.Zoir, container: std.zig.Zoir.Node.Index, name: []con /// Owned copy of `node`'s exact source text (first token's start through last token's end), /// preserving whitespace/comments inside the span verbatim. fn nodeSourceText(gpa: Allocator, ast: std.zig.Ast, zoir: std.zig.Zoir, node: std.zig.Zoir.Node.Index) Allocator.Error![]u8 { - const ast_node = node.getAstNode(zoir); + const ast_node = node.getAstNode(&zoir); const first_tok = ast.firstToken(ast_node); const last_tok = ast.lastToken(ast_node); const start = ast.tokenStart(first_tok); @@ -70,7 +70,7 @@ fn nodeSourceText(gpa: Allocator, ast: std.zig.Ast, zoir: std.zig.Zoir, node: st const Parsed = struct { ast: std.zig.Ast, zoir: std.zig.Zoir }; fn parseZon(gpa: Allocator, source: [:0]const u8) ?Parsed { - var ast = std.zig.Ast.parse(gpa, source, .zon) catch return null; + var ast = std.zig.Ast.parse(gpa, source, .{ .mode = .zon }) catch return null; errdefer ast.deinit(gpa); var zoir = std.zig.ZonGen.generate(gpa, ast, .{ .parse_str_lits = false }) catch { ast.deinit(gpa); @@ -102,7 +102,7 @@ pub fn extractField(gpa: Allocator, source: [:0]const u8, field_name: []const u8 pub fn extractPluginBlob(gpa: Allocator, source: [:0]const u8, id: []const u8) ?[]u8 { const plugins = extractField(gpa, source, "plugins") orelse return null; defer gpa.free(plugins); - const plugins_z = gpa.dupeZ(u8, plugins) catch return null; + const plugins_z = gpa.dupeSentinel(u8, plugins, 0) catch return null; defer gpa.free(plugins_z); return extractField(gpa, plugins_z, id); } @@ -122,7 +122,7 @@ pub fn listPluginBlocks(gpa: Allocator, source: ?[:0]const u8) Allocator.Error![ } const plugins_node = findField(parsed.zoir, .root, "plugins") orelse return &.{}; - const node = plugins_node.get(parsed.zoir); + const node = plugins_node.get(&parsed.zoir); const s = switch (node) { .struct_literal => |sl| sl, else => return &.{}, @@ -137,7 +137,7 @@ pub fn listPluginBlocks(gpa: Allocator, source: ?[:0]const u8) Allocator.Error![ out.deinit(gpa); } for (s.names, 0..) |field_name, i| { - const id = try gpa.dupe(u8, field_name.get(parsed.zoir)); + const id = try gpa.dupe(u8, field_name.get(&parsed.zoir)); errdefer gpa.free(id); const text = try nodeSourceText(gpa, parsed.ast, parsed.zoir, s.vals.at(@intCast(i))); try out.append(gpa, .{ .id = id, .text = text }); @@ -295,7 +295,7 @@ pub fn upsertOne(gpa: Allocator, existing_full_source: ?[:0]const u8, entry: Ent return gpa.dupe(u8, src); } - const ast_node = plugins_node.getAstNode(parsed.zoir); + const ast_node = plugins_node.getAstNode(&parsed.zoir); const last_tok = parsed.ast.lastToken(ast_node); const insert_at = parsed.ast.tokenStart(last_tok); @@ -435,11 +435,11 @@ test "extractPluginBlob returns null when the id is missing from an existing .pl test "nested extractField reaches .plugins..settings" { const plugins = extractField(testing.allocator, sample_source, "plugins").?; defer testing.allocator.free(plugins); - const plugins_z = try testing.allocator.dupeZ(u8, plugins); + const plugins_z = try testing.allocator.dupeSentinel(u8, plugins, 0); defer testing.allocator.free(plugins_z); const id_block = extractField(testing.allocator, plugins_z, "text").?; defer testing.allocator.free(id_block); - const id_z = try testing.allocator.dupeZ(u8, id_block); + const id_z = try testing.allocator.dupeSentinel(u8, id_block, 0); defer testing.allocator.free(id_z); const settings = extractField(testing.allocator, id_z, "settings").?; defer testing.allocator.free(settings); @@ -453,7 +453,7 @@ test "composeMergedText round-trips: overlay replaces one id, others survive unt const composed = try composeMergedText(testing.allocator, fizzy_text, sample_source, &overlay); defer testing.allocator.free(composed); - const composed_z = try testing.allocator.dupeZ(u8, composed); + const composed_z = try testing.allocator.dupeSentinel(u8, composed, 0); defer testing.allocator.free(composed_z); const pixi = extractPluginBlob(testing.allocator, composed_z, "pixi").?; @@ -472,7 +472,7 @@ test "composeMergedText removes an id when overlay text is null" { const composed = try composeMergedText(testing.allocator, fizzy_text, sample_source, &overlay); defer testing.allocator.free(composed); - const composed_z = try testing.allocator.dupeZ(u8, composed); + const composed_z = try testing.allocator.dupeSentinel(u8, composed, 0); defer testing.allocator.free(composed_z); try testing.expect(extractPluginBlob(testing.allocator, composed_z, "pixi") == null); @@ -489,7 +489,7 @@ test "composeMergedText adds a brand-new id when there is no existing .plugins b const composed = try composeMergedText(testing.allocator, fizzy_text, null, &overlay); defer testing.allocator.free(composed); - const composed_z = try testing.allocator.dupeZ(u8, composed); + const composed_z = try testing.allocator.dupeSentinel(u8, composed, 0); defer testing.allocator.free(composed_z); const pixi = extractPluginBlob(testing.allocator, composed_z, "pixi").?; @@ -512,7 +512,7 @@ test "composeMergedText accepts an empty fizzy struct (every fizzy field at its const composed = try composeMergedText(testing.allocator, ".{}", null, &overlay); defer testing.allocator.free(composed); - const composed_z = try testing.allocator.dupeZ(u8, composed); + const composed_z = try testing.allocator.dupeSentinel(u8, composed, 0); defer testing.allocator.free(composed_z); const pixi = extractPluginBlob(testing.allocator, composed_z, "pixi").?; @@ -523,7 +523,7 @@ test "composeMergedText accepts an empty fizzy struct (every fizzy field at its test "upsertOne inserts a new id into an existing .plugins block, leaving the rest untouched" { const composed = try upsertOne(testing.allocator, sample_source, .{ .id = "markdown", .text = ".{ .enabled = true }" }); defer testing.allocator.free(composed); - const composed_z = try testing.allocator.dupeZ(u8, composed); + const composed_z = try testing.allocator.dupeSentinel(u8, composed, 0); defer testing.allocator.free(composed_z); const markdown = extractPluginBlob(testing.allocator, composed_z, "markdown").?; @@ -546,7 +546,7 @@ test "upsertOne adds a .plugins block when none exists yet" { const source: [:0]const u8 = ".{ .theme = 0.35 }"; const composed = try upsertOne(testing.allocator, source, .{ .id = "pixi", .text = ".{ .enabled = true }" }); defer testing.allocator.free(composed); - const composed_z = try testing.allocator.dupeZ(u8, composed); + const composed_z = try testing.allocator.dupeSentinel(u8, composed, 0); defer testing.allocator.free(composed_z); const pixi = extractPluginBlob(testing.allocator, composed_z, "pixi").?; @@ -557,7 +557,7 @@ test "upsertOne adds a .plugins block when none exists yet" { test "upsertOne handles a missing settings.zon (null source)" { const composed = try upsertOne(testing.allocator, null, .{ .id = "pixi", .text = ".{ .enabled = true }" }); defer testing.allocator.free(composed); - const composed_z = try testing.allocator.dupeZ(u8, composed); + const composed_z = try testing.allocator.dupeSentinel(u8, composed, 0); defer testing.allocator.free(composed_z); const pixi = extractPluginBlob(testing.allocator, composed_z, "pixi").?; @@ -601,7 +601,7 @@ test "composed extensions survive extractField + parseExtensions" { ); defer testing.allocator.free(block); - const block_z = try testing.allocator.dupeZ(u8, block); + const block_z = try testing.allocator.dupeSentinel(u8, block, 0); defer testing.allocator.free(block_z); const exts_text = extractField(testing.allocator, block_z, "extensions").?; @@ -642,24 +642,24 @@ test "every Reserved field is discoverable by extractField" { // 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 // sides stay in step for every field, including ones added later. - inline for (@typeInfo(Reserved).@"struct".fields) |field| { + inline for (@typeInfo(Reserved).@"struct".field_names, @typeInfo(Reserved).@"struct".field_types, @typeInfo(Reserved).@"struct".field_attrs) |field_name, field_type, _| { // Compose a block in which this field is guaranteed non-default, so it is actually // emitted (fields are written only when they differ from their default). var reserved: Reserved = .{}; - switch (field.type) { - bool => @field(reserved, field.name) = !@field(reserved, field.name), + 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. - ?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)), + ?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)), } const block = try composePluginIdBlock(testing.allocator, reserved, null); defer testing.allocator.free(block); - const block_z = try testing.allocator.dupeZ(u8, block); + const block_z = try testing.allocator.dupeSentinel(u8, block, 0); defer testing.allocator.free(block_z); - const found = extractField(testing.allocator, block_z, field.name); + const found = extractField(testing.allocator, block_z, field_name); try testing.expect(found != null); testing.allocator.free(found.?); } diff --git a/app/single_instance/singleton_native.zig b/app/single_instance/singleton_native.zig index 51ca33339..9432cc721 100644 --- a/app/single_instance/singleton_native.zig +++ b/app/single_instance/singleton_native.zig @@ -93,7 +93,7 @@ pub fn acquireLock(gpa: std.mem.Allocator, argv: []const []const u8) !void { if (profile.root) |root| { profile_id = try profile.lockId(gpa, app_id, root); id = profile_id.?; - if (builtin.os.tag != .windows) { + if (builtin.target.os.tag != .windows) { run_dir = try profile.runtimeDir(gpa, root); // `/-.sock`, as `dvui-singleton-app` names it; a uid is at most 10 digits. const socket_len = run_dir.?.len + 1 + id.len + 1 + 10 + ".sock".len; @@ -402,7 +402,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.os.tag == .windows) return "."; // unused on Windows + if (builtin.target.os.tag == .windows) return "."; // unused on Windows // 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| { @@ -412,7 +412,7 @@ fn pickUnixSocketDir(buf: *[std.fs.max_path_bytes]u8) []const u8 { return buf[0..len]; } } - if (builtin.os.tag == .linux) { + if (builtin.target.os.tag == .linux) { if (std.c.getenv("XDG_RUNTIME_DIR")) |env_ptr| { const len = std.mem.len(env_ptr); if (len > 0 and len < buf.len) { diff --git a/app/store/PluginLoader.zig b/app/store/PluginLoader.zig index dd0b8f337..fab9e8e04 100644 --- a/app/store/PluginLoader.zig +++ b/app/store/PluginLoader.zig @@ -17,7 +17,7 @@ const crash = @import("crash"); /// Zig 0.16.0's `std.DynLib` dropped Windows support; this thin wrapper restores it for /// Windows while delegating elsewhere. Shape matches `std.DynLib.{open, close, lookup}`. -pub const DynLib = if (builtin.os.tag == .windows) WindowsDynLib else std.DynLib; +pub const DynLib = if (builtin.target.os.tag == .windows) WindowsDynLib else std.DynLib; const WindowsDynLib = struct { const windows = std.os.windows; @@ -100,7 +100,7 @@ pub const PreRegister = struct { /// Platform-specific plugin dylib extension. pub fn pluginExtension() []const u8 { - return switch (builtin.os.tag) { + return switch (builtin.target.os.tag) { .windows => "dll", .macos => "dylib", else => "so", @@ -126,7 +126,7 @@ pub fn builtinPluginPath( } fn nativeEnviron() std.process.Environ { - if (builtin.os.tag == .windows) { + if (builtin.target.os.tag == .windows) { return .{ .block = .global }; } var n: usize = 0; @@ -491,7 +491,7 @@ pub fn register(host: *Host, opened: Opened, pre: ?PreRegister) LoadError!Loaded crash.addModule(o.plugin_id, plugin_version, @intFromPtr(o.register_fn)); errdefer crash.removeModule(@intFromPtr(o.register_fn)); - const status: dylib_api.RegisterStatus = @enumFromInt(o.register_fn(host)); + const status: dylib_api.RegisterStatus = @fromBackingInt(@intCast(o.register_fn(host))); switch (status) { .ok => {}, .err_abi_mismatch => return error.AbiMismatch, @@ -513,7 +513,7 @@ pub fn register(host: *Host, opened: Opened, pre: ?PreRegister) LoadError!Loaded } fn allowAbiWarn() bool { - if (builtin.mode != .Debug) return false; + if (builtin.mode != .debug) return false; if (std.c.getenv("FIZZY_PLUGIN_ABI_WARN")) |v| { return std.mem.eql(u8, std.mem.span(v), "1"); } @@ -634,7 +634,7 @@ pub fn probeVersionInfo(path: []const u8) ?PluginVersionInfo { test "builtin plugin path joins exe_dir/plugins//." { const path = try builtinPluginPath(std.testing.allocator, "/app", "pixi"); defer std.testing.allocator.free(path); - switch (builtin.os.tag) { + switch (builtin.target.os.tag) { .windows => try std.testing.expectEqualStrings("/app/plugins/pixi/pixi.dll", path), .macos => try std.testing.expectEqualStrings("/app/plugins/pixi/pixi.dylib", path), else => try std.testing.expectEqualStrings("/app/plugins/pixi/pixi.so", path), diff --git a/app/store/PluginLoader_web.zig b/app/store/PluginLoader_web.zig index 954afb280..b81bb179e 100644 --- a/app/store/PluginLoader_web.zig +++ b/app/store/PluginLoader_web.zig @@ -263,7 +263,7 @@ pub const Prepared = struct { if (pre) |inject| { self.set_globals(if (inject.gpa) |gpa| @ptrCast(gpa) else null, inject.arg_b, inject.arg_c); } - const status: dylib_api.RegisterStatus = @enumFromInt(self.reg_fn(host)); + const status: dylib_api.RegisterStatus = @fromBackingInt(@intCast(self.reg_fn(host))); switch (status) { .ok => {}, .err_abi_mismatch => return error.AbiMismatch, diff --git a/app/store/PluginStore.zig b/app/store/PluginStore.zig index 9410d5d96..ffa43ba7c 100644 --- a/app/store/PluginStore.zig +++ b/app/store/PluginStore.zig @@ -230,7 +230,7 @@ pub fn queueInstall(id: []const u8) void { pub fn isInstalling(id: []const u8) bool { if (web_in_flight.contains(id)) return true; const job = jobs.get(id) orelse return false; - const status: JobStatus = @enumFromInt(job.status.load(.acquire)); + const status: JobStatus = @fromBackingInt(@intCast(job.status.load(.acquire))); return status == .downloading or status == .downloaded; } @@ -1027,7 +1027,7 @@ pub fn applyPendingUpdate(id: []const u8) void { /// here too. pub fn updateStatus(id: []const u8) ?[]const u8 { if (web_in_flight.contains(id)) return "Updating\u{2026}"; - if (jobs.get(id)) |job| return switch (@as(JobStatus, @enumFromInt(job.status.load(.acquire)))) { + if (jobs.get(id)) |job| return switch (@as(JobStatus, @fromBackingInt(@intCast(job.status.load(.acquire))))) { .downloading => "Updating\u{2026}", .downloaded => "Installing\u{2026}", .failed => "Failed", @@ -1352,7 +1352,7 @@ pub fn tick() void { var i: usize = 0; while (i < jobs.count()) { const job = jobs.values()[i]; - switch (@as(JobStatus, @enumFromInt(job.status.load(.acquire)))) { + switch (@as(JobStatus, @fromBackingInt(@intCast(job.status.load(.acquire))))) { .downloading => i += 1, .failed => { // Nobody asked for a quiet job, so it leaves no failed card to retry from. Log it, @@ -1401,7 +1401,7 @@ pub fn tick() void { const n = @min(@errorName(err).len, job.err_buf.len); @memcpy(job.err_buf[0..n], @errorName(err)[0..n]); job.err_len = n; - job.status.store(@intFromEnum(JobStatus.failed), .release); + job.status.store(@backingInt(JobStatus.failed), .release); i += 1; continue; }; @@ -1634,10 +1634,10 @@ fn worker(job: *Job, io: std.Io) void { const n = @min(@errorName(err).len, job.err_buf.len); @memcpy(job.err_buf[0..n], @errorName(err)[0..n]); job.err_len = n; - job.status.store(@intFromEnum(JobStatus.failed), .release); + job.status.store(@backingInt(JobStatus.failed), .release); return; }; - job.status.store(@intFromEnum(JobStatus.downloaded), .release); + job.status.store(@backingInt(JobStatus.downloaded), .release); } fn removeJob(id: []const u8) void { @@ -1714,7 +1714,7 @@ fn buildJob(id: []const u8, url: []const u8, sha256: []const u8, opts: DownloadO errdefer a.free(dest); job.* = .{ - .status = .init(@intFromEnum(JobStatus.downloading)), + .status = .init(@backingInt(JobStatus.downloading)), .id = id_dup, .url = url_dup, .sha256 = sha_dup, @@ -3070,7 +3070,7 @@ fn drawCardControls(entry: StoreEntry) void { defer ctl.deinit(); // An in-flight / failed install job preempts the normal controls. - if (jobs.get(entry.id)) |job| switch (@as(JobStatus, @enumFromInt(job.status.load(.acquire)))) { + if (jobs.get(entry.id)) |job| switch (@as(JobStatus, @fromBackingInt(@intCast(job.status.load(.acquire))))) { .downloading => { drawInstalling(if (job.is_update) "Updating…" else "Installing…", muted); return; @@ -3215,7 +3215,7 @@ fn drawStoreCardControls(entry: StoreEntry) void { defer ctl.deinit(); // An in-flight / failed install job preempts the normal controls. - if (jobs.get(entry.id)) |job| switch (@as(JobStatus, @enumFromInt(job.status.load(.acquire)))) { + if (jobs.get(entry.id)) |job| switch (@as(JobStatus, @fromBackingInt(@intCast(job.status.load(.acquire))))) { .downloading => { drawInstalling(if (job.is_update) "Updating…" else "Installing…", muted); return; diff --git a/app/store/plugin_repo_asset.zig b/app/store/plugin_repo_asset.zig index f593406ae..c1b25299c 100644 --- a/app/store/plugin_repo_asset.zig +++ b/app/store/plugin_repo_asset.zig @@ -29,7 +29,7 @@ pub fn fetchOk(io: std.Io, url: []const u8, limit: std.Io.Limit) ?[]u8 { if (result.status != .ok) return null; const written = body.written(); - if (limit != .unlimited and limit != .nothing and written.len > @intFromEnum(limit)) return null; + if (limit != .unlimited and limit != .nothing and written.len > @backingInt(limit)) return null; return gpa().dupe(u8, written) catch null; } diff --git a/app/store/readme.zig b/app/store/readme.zig index a0f6e5d17..449fbd3bf 100644 --- a/app/store/readme.zig +++ b/app/store/readme.zig @@ -30,7 +30,7 @@ pub const Readme = struct { /// 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). window: *dvui.Window, - status: std.atomic.Value(u8) = .init(@intFromEnum(Status.idle)), + 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. @@ -48,7 +48,7 @@ pub const Readme = struct { preview: markdown.Preview = .{}, fn statusValue(self: *Readme) Status { - return @enumFromInt(self.status.load(.acquire)); + return @fromBackingInt(@intCast(self.status.load(.acquire))); } /// Start fetching `id`'s README (from its `repo` URL, optionally scoped to `subpath` within @@ -72,7 +72,7 @@ pub const Readme = struct { .io = dvui.io, .window = dvui.currentWindow(), }; - self.status.store(@intFromEnum(Status.fetching), .release); + self.status.store(@backingInt(Status.fetching), .release); return self; } @@ -85,7 +85,7 @@ pub const Readme = struct { return; } self.thread = std.Thread.spawn(.{}, worker, .{self}) catch { - self.status.store(@intFromEnum(Status.failed), .release); + self.status.store(@backingInt(Status.failed), .release); return; }; } @@ -113,7 +113,7 @@ pub const Readme = struct { if (comptime !is_wasm) return; if (self.statusValue() != .fetching) return; if (self.wasm_url_i >= self.wasm_url_len) { - self.status.store(@intFromEnum(Status.not_found), .release); + self.status.store(@backingInt(Status.not_found), .release); return; } switch (web_fetch.request(repo_asset.gpa(), self.wasm_urls[self.wasm_url_i])) { @@ -121,16 +121,16 @@ pub const Readme = struct { .failed => { self.wasm_url_i += 1; if (self.wasm_url_i >= self.wasm_url_len) - self.status.store(@intFromEnum(Status.not_found), .release); + self.status.store(@backingInt(Status.not_found), .release); }, .ready => |body| { const gpa = repo_asset.gpa(); self.bytes = gpa.dupe(u8, body) catch { - self.status.store(@intFromEnum(Status.failed), .release); + self.status.store(@backingInt(Status.failed), .release); return; }; self.image_base = gpa.dupe(u8, self.wasm_urls[self.wasm_url_i]) catch null; - self.status.store(@intFromEnum(Status.ready), .release); + self.status.store(@backingInt(Status.ready), .release); }, } } @@ -184,7 +184,7 @@ pub fn deinit() void { fn fillWasmCandidates(self: *Readme) void { var url_buf: [3][256]u8 = undefined; const candidates = repo_asset.rawGithubUrls(&url_buf, self.repo, self.subpath, readme_filename) orelse { - self.status.store(@intFromEnum(Status.not_found), .release); + self.status.store(@backingInt(Status.not_found), .release); return; }; const gpa = repo_asset.gpa(); @@ -194,7 +194,7 @@ fn fillWasmCandidates(self: *Readme) void { n += 1; } self.wasm_url_len = n; - if (n == 0) self.status.store(@intFromEnum(Status.not_found), .release); + if (n == 0) self.status.store(@backingInt(Status.not_found), .release); } fn worker(self: *Readme) void { @@ -206,14 +206,14 @@ fn worker(self: *Readme) void { if (repo_asset.readLocalAsset(self.io, self.subpath, readme_filename, limit)) |asset| { self.bytes = asset.bytes; self.image_base = asset.dir; - self.status.store(@intFromEnum(Status.ready), .release); + self.status.store(@backingInt(Status.ready), .release); return; } } var url_buf: [3][256]u8 = undefined; const candidates = repo_asset.rawGithubUrls(&url_buf, self.repo, self.subpath, readme_filename) orelse { - self.status.store(@intFromEnum(Status.not_found), .release); + self.status.store(@backingInt(Status.not_found), .release); return; }; @@ -222,9 +222,9 @@ fn worker(self: *Readme) void { self.bytes = body; // `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(@intFromEnum(Status.ready), .release); + self.status.store(@backingInt(Status.ready), .release); return; } } - self.status.store(@intFromEnum(Status.not_found), .release); + self.status.store(@backingInt(Status.not_found), .release); } diff --git a/app/store/registry/compat.zig b/app/store/registry/compat.zig index c1362727b..575018a35 100644 --- a/app/store/registry/compat.zig +++ b/app/store/registry/compat.zig @@ -14,13 +14,13 @@ const builtin = @import("builtin"); pub fn hostKey() []const u8 { // The web: one key for the wasm side module a plugin publishes for the browser. if (builtin.target.cpu.arch == .wasm32) return "web-wasm32"; - const os = switch (builtin.os.tag) { + const os = switch (builtin.target.os.tag) { .macos => "macos", .linux => "linux", .windows => "windows", else => "unknown", }; - const arch = switch (builtin.cpu.arch) { + const arch = switch (builtin.target.cpu.arch) { .aarch64 => "aarch64", .x86_64 => "x86_64", else => "unknown", diff --git a/app/store/registry/store.zig b/app/store/registry/store.zig index 74d816491..7d4f7a247 100644 --- a/app/store/registry/store.zig +++ b/app/store/registry/store.zig @@ -46,7 +46,7 @@ pub const Catalog = struct { io: std.Io, summary_url: []const u8, shard_url: []const u8, - status_value: std.atomic.Value(u8) = .init(@intFromEnum(Status.idle)), + status_value: std.atomic.Value(u8) = .init(@backingInt(Status.idle)), mutex: std.Io.Mutex = .init, summary: ?ParsedSummary = null, shard: ?ParsedShard = null, @@ -92,7 +92,7 @@ pub const Catalog = struct { } pub fn status(self: *Catalog) Status { - return @enumFromInt(self.status_value.load(.acquire)); + return @fromBackingInt(@intCast(self.status_value.load(.acquire))); } /// Kick off a background refresh. No-op while one is already in flight. Must be called from @@ -106,10 +106,10 @@ pub const Catalog = struct { // Release the previous worker's group resources. `status() != .fetching` means it has // already stored its final status, so this returns as soon as that task unwinds. self.tasks.await(self.io) catch {}; - self.status_value.store(@intFromEnum(Status.fetching), .release); + self.status_value.store(@backingInt(Status.fetching), .release); // `concurrent`, not `async`: this must run on its own thread, never inline on the caller's. self.tasks.concurrent(self.io, worker, .{self}) catch { - self.status_value.store(@intFromEnum(Status.failed), .release); + self.status_value.store(@backingInt(Status.failed), .release); return; }; } @@ -118,12 +118,12 @@ pub const Catalog = struct { if (self.wasm_summary_url) |u| self.allocator.free(u); if (self.wasm_shard_url) |u| self.allocator.free(u); self.wasm_summary_url = cacheBust(self.allocator, self.io, self.summary_url) orelse { - self.status_value.store(@intFromEnum(Status.failed), .release); + self.status_value.store(@backingInt(Status.failed), .release); return; }; self.wasm_shard_url = cacheBust(self.allocator, self.io, self.shard_url); self.wasm_phase = .summary; - self.status_value.store(@intFromEnum(Status.fetching), .release); + self.status_value.store(@backingInt(Status.fetching), .release); } /// Advance in-flight browser GETs. UI thread, wasm only — a no-op natively. `fetcher` is the @@ -163,7 +163,7 @@ pub const Catalog = struct { .shard => { const url = self.wasm_shard_url orelse { self.wasm_phase = .idle; - self.status_value.store(@intFromEnum(Status.ready), .release); + self.status_value.store(@backingInt(Status.ready), .release); return; }; switch (fetcher.request(self.allocator, url)) { @@ -172,7 +172,7 @@ pub const Catalog = struct { // every card reads "no compatible build". .failed => { self.wasm_phase = .idle; - self.status_value.store(@intFromEnum(Status.ready), .release); + self.status_value.store(@backingInt(Status.ready), .release); }, .ready => |bytes| { if (std.json.parseFromSlice(registry.ReleaseShard, self.allocator, bytes, .{ @@ -183,7 +183,7 @@ pub const Catalog = struct { self.shard = parsed; } else |_| {} self.wasm_phase = .idle; - self.status_value.store(@intFromEnum(Status.ready), .release); + self.status_value.store(@backingInt(Status.ready), .release); }, } }, @@ -192,7 +192,7 @@ pub const Catalog = struct { fn failWasm(self: *Catalog) void { self.wasm_phase = .idle; - self.status_value.store(@intFromEnum(Status.failed), .release); + self.status_value.store(@backingInt(Status.failed), .release); } /// Runs in the task group. Every failure path — including `error.Canceled` from a shutdown @@ -200,7 +200,7 @@ pub const Catalog = struct { /// 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 { - self.status_value.store(@intFromEnum(Status.failed), .release); + self.status_value.store(@backingInt(Status.failed), .release); return; }; // Best-effort: any failure (network, 404 for a brand-new fingerprint nobody has @@ -216,7 +216,7 @@ pub const Catalog = struct { self.shard = s; } self.mutex.unlock(self.io); - self.status_value.store(@intFromEnum(Status.ready), .release); + self.status_value.store(@backingInt(Status.ready), .release); } /// A joined view of the latest summary + this host's release shard, for the duration the diff --git a/app/store/store_icon.zig b/app/store/store_icon.zig index 55dcfccc7..60b68ea43 100644 --- a/app/store/store_icon.zig +++ b/app/store/store_icon.zig @@ -17,7 +17,7 @@ const icon_filename = "ICON.png"; const Status = enum(u8) { idle, fetching, ready, not_found, failed }; const IconEntry = struct { - status: std.atomic.Value(u8) = .init(@intFromEnum(Status.idle)), + status: std.atomic.Value(u8) = .init(@backingInt(Status.idle)), /// Fetched PNG bytes (app-allocator owned). Written by the worker; decoded on the UI thread. bytes: ?[]u8 = null, /// Decoded once on the UI thread after `bytes` is ready. @@ -28,7 +28,7 @@ const IconEntry = struct { wasm_url_i: usize = 0, fn statusValue(self: *IconEntry) Status { - return @enumFromInt(self.status.load(.acquire)); + return @fromBackingInt(@intCast(self.status.load(.acquire))); } fn deinit(self: *IconEntry) void { @@ -92,7 +92,7 @@ pub fn request(id: []const u8, repo: []const u8, subpath: []const u8) void { return; }; - entry.status.store(@intFromEnum(Status.fetching), .release); + entry.status.store(@backingInt(Status.fetching), .release); if (comptime is_wasm) { fillWasmCandidates(entry, repo_owned, subpath_owned); gpa.free(repo_owned); @@ -102,7 +102,7 @@ pub fn request(id: []const u8, repo: []const u8, subpath: []const u8) void { entry.thread = std.Thread.spawn(.{}, worker, .{ repo_owned, subpath_owned, entry }) catch { gpa.free(repo_owned); gpa.free(subpath_owned); - entry.status.store(@intFromEnum(Status.failed), .release); + entry.status.store(@backingInt(Status.failed), .release); return; }; } @@ -117,7 +117,7 @@ pub fn pump() void { fn pumpEntry(entry: *IconEntry) void { if (entry.statusValue() != .fetching) return; if (entry.wasm_url_i >= entry.wasm_url_len) { - entry.status.store(@intFromEnum(Status.not_found), .release); + entry.status.store(@backingInt(Status.not_found), .release); return; } switch (web_fetch.request(repo_asset.gpa(), entry.wasm_urls[entry.wasm_url_i])) { @@ -125,14 +125,14 @@ fn pumpEntry(entry: *IconEntry) void { .failed => { entry.wasm_url_i += 1; if (entry.wasm_url_i >= entry.wasm_url_len) - entry.status.store(@intFromEnum(Status.not_found), .release); + entry.status.store(@backingInt(Status.not_found), .release); }, .ready => |body| { entry.bytes = repo_asset.gpa().dupe(u8, body) catch { - entry.status.store(@intFromEnum(Status.failed), .release); + entry.status.store(@backingInt(Status.failed), .release); return; }; - entry.status.store(@intFromEnum(Status.ready), .release); + entry.status.store(@backingInt(Status.ready), .release); }, } } @@ -140,7 +140,7 @@ fn pumpEntry(entry: *IconEntry) void { fn fillWasmCandidates(entry: *IconEntry, repo: []const u8, subpath: []const u8) void { var url_buf: [3][256]u8 = undefined; const candidates = repo_asset.rawGithubUrls(&url_buf, repo, subpath, icon_filename) orelse { - entry.status.store(@intFromEnum(Status.not_found), .release); + entry.status.store(@backingInt(Status.not_found), .release); return; }; const gpa = repo_asset.gpa(); @@ -150,7 +150,7 @@ fn fillWasmCandidates(entry: *IconEntry, repo: []const u8, subpath: []const u8) n += 1; } entry.wasm_url_len = n; - if (n == 0) entry.status.store(@intFromEnum(Status.not_found), .release); + if (n == 0) entry.status.store(@backingInt(Status.not_found), .release); } /// Draw the cached icon for `id` into the current dvui parent, at `size` square. Returns true @@ -200,23 +200,23 @@ fn worker(repo_owned: []u8, subpath_owned: []u8, entry: *IconEntry) void { if (repo_asset.readLocalAsset(io, subpath_owned, icon_filename, limit)) |asset| { repo_asset.gpa().free(asset.dir); // an icon has nothing relative to resolve entry.bytes = asset.bytes; - entry.status.store(@intFromEnum(Status.ready), .release); + entry.status.store(@backingInt(Status.ready), .release); return; } } var url_buf: [3][256]u8 = undefined; const candidates = repo_asset.rawGithubUrls(&url_buf, repo_owned, subpath_owned, icon_filename) orelse { - entry.status.store(@intFromEnum(Status.not_found), .release); + entry.status.store(@backingInt(Status.not_found), .release); return; }; for (candidates.slice()) |url| { if (repo_asset.fetchOk(io, url, limit)) |body| { entry.bytes = body; - entry.status.store(@intFromEnum(Status.ready), .release); + entry.status.store(@backingInt(Status.ready), .release); return; } } - entry.status.store(@intFromEnum(Status.not_found), .release); + entry.status.store(@backingInt(Status.not_found), .release); } diff --git a/app/update/auto_update.zig b/app/update/auto_update.zig index bb04b740e..1163ba9a6 100644 --- a/app/update/auto_update.zig +++ b/app/update/auto_update.zig @@ -18,12 +18,7 @@ pub var hooks: Hooks = .{}; pub const impl: bool = build_opts.velopack_enabled and builtin.target.cpu.arch != .wasm32; -const Vpk = if (impl) - @cImport({ - @cInclude("Velopack.h"); - }) -else - struct {}; +const Vpk = struct {}; // ZIG017: @cImport is gone; Velopack.h becomes a translate-c module pub fn logVpkError(prefix: []const u8) void { if (!impl) return; @@ -49,7 +44,7 @@ fn castManager(m: *anyopaque) *Vpk.vpkc_update_manager_t { /// 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.os.tag != .macos) return true; + if (builtin.target.os.tag != .macos) return true; var buf: [std.fs.max_path_bytes]u8 = undefined; const n = std.process.executablePath(io, &buf) catch return false; @@ -93,7 +88,7 @@ fn openGithubManager(allocator: std.mem.Allocator, url: []const u8) error{OutOfM if (!impl) return null; if (url.len == 0) return null; - const repo_url_z = try allocator.dupeZ(u8, url); + const repo_url_z = try allocator.dupeSentinel(u8, url, 0); defer allocator.free(repo_url_z); const source: ?*Vpk.vpkc_update_source_t = Vpk.vpkc_new_source_github(repo_url_z.ptr, null, false); @@ -125,7 +120,7 @@ pub fn openUpdateManager(io: std.Io, allocator: std.mem.Allocator) error{OutOfMe if (std.c.getenv("FIZZY_AUTOUPDATE_URL")) |raw| { const update_url = std.mem.span(raw); if (update_url.len == 0) return null; - const update_url_z = try allocator.dupeZ(u8, update_url); + const update_url_z = try allocator.dupeSentinel(u8, update_url, 0); defer allocator.free(update_url_z); var manager: ?*Vpk.vpkc_update_manager_t = null; if (!Vpk.vpkc_new_update_manager(update_url_z.ptr, null, null, &manager)) { @@ -197,7 +192,7 @@ pub fn appRunHook() void { // CLI flags during the install / update / uninstall lifecycle, and the hooks // registered here are dispatched from inside vpkc_app_run() based on which // flag is set. Hooks must be installed *before* vpkc_app_run runs. - if (comptime builtin.os.tag == .windows) { + if (comptime builtin.target.os.tag == .windows) { Vpk.vpkc_app_set_hook_after_install(hookAfterInstall); Vpk.vpkc_app_set_hook_after_update(hookAfterUpdate); Vpk.vpkc_app_set_hook_before_uninstall(hookBeforeUninstall); diff --git a/app/update/update_install.zig b/app/update/update_install.zig index 24a1adb94..b7d9565b1 100644 --- a/app/update/update_install.zig +++ b/app/update/update_install.zig @@ -19,12 +19,7 @@ const dvui = @import("dvui"); const auto_update = @import("auto_update.zig"); const build_opts = @import("build_opts"); -const Vpk = if (auto_update.impl) - @cImport({ - @cInclude("Velopack.h"); - }) -else - struct {}; +const Vpk = struct {}; // ZIG017: @cImport is gone const UpdateJob = @This(); @@ -41,7 +36,7 @@ allocator: std.mem.Allocator, window: *dvui.Window, io: std.Io, -phase: std.atomic.Value(u8) = .init(@intFromEnum(Phase.queued)), +phase: std.atomic.Value(u8) = .init(@backingInt(Phase.queued)), /// 0–100 from Velopack's download callback. Stays at 0 outside `.downloading`. progress: std.atomic.Value(u8) = .init(0), /// Worker → main publish flag. @@ -141,7 +136,7 @@ pub fn applyAtExit() bool { pub fn currentPhase(job: *const UpdateJob) Phase { const raw = job.phase.load(.acquire); - return @enumFromInt(raw); + return @fromBackingInt(@intCast(raw)); } pub fn progressPercent(job: *const UpdateJob) u8 { @@ -184,7 +179,7 @@ fn setFailed(job: *UpdateJob, e: auto_update.UpdateInstallError) void { } else { job.err_msg_len = 0; } - job.phase.store(@intFromEnum(Phase.failed), .release); + job.phase.store(@backingInt(Phase.failed), .release); } fn workerMain(job: *UpdateJob) void { @@ -202,7 +197,7 @@ fn workerMain(job: *UpdateJob) void { return; } - job.phase.store(@intFromEnum(Phase.downloading), .release); + job.phase.store(@backingInt(Phase.downloading), .release); dvui.refresh(job.window, @src(), null); const mgr_opaque = auto_update.openUpdateManager(job.io, job.allocator) catch { @@ -223,7 +218,7 @@ fn workerMain(job: *UpdateJob) void { switch (result) { Vpk.UPDATE_AVAILABLE => {}, Vpk.NO_UPDATE_AVAILABLE, Vpk.REMOTE_IS_EMPTY => { - job.phase.store(@intFromEnum(Phase.no_update), .release); + job.phase.store(@backingInt(Phase.no_update), .release); return; }, Vpk.UPDATE_ERROR => { @@ -250,6 +245,6 @@ fn workerMain(job: *UpdateJob) void { job.manager = mgr_opaque; job.update_info = u; job.progress.store(100, .monotonic); - job.phase.store(@intFromEnum(Phase.downloaded), .release); + job.phase.store(@backingInt(Phase.downloaded), .release); job.downloaded_unseen.store(true, .release); } diff --git a/app/update/update_notify.zig b/app/update/update_notify.zig index 195ffc128..c6657ada2 100644 --- a/app/update/update_notify.zig +++ b/app/update/update_notify.zig @@ -16,7 +16,7 @@ const Phase = enum(u8) { }; var launched: bool = false; -var phase: std.atomic.Value(u8) = .init(@intFromEnum(Phase.pending)); +var phase: std.atomic.Value(u8) = .init(@backingInt(Phase.pending)); /// Set by `startLaunchCheck` when the "update available" state is faked for UI work /// (`Constants.debug_simulate_update_available`). Nothing may act on it beyond drawing the @@ -44,7 +44,7 @@ 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 /// it ourselves in a custom rect via `dvui.toastsFor(SUBWINDOW_ID)`. -const SUBWINDOW_ID: dvui.Id = @enumFromInt(0xF122_DA70_AD0E_71D0); +const SUBWINDOW_ID: dvui.Id = @fromBackingInt(@intCast(0xF122_DA70_AD0E_71D0)); // Estimated toast size — used for the FloatingWidget bounding rect and for // computing the bottom-left anchor relative to the infobar. The real toast @@ -63,22 +63,22 @@ pub fn startLaunchCheck(io: std.Io, simulate_out_of_date: bool) void { const fake = "9.9.9 (simulated)"; @memcpy(remote_ver[0..fake.len], fake); remote_ver_len = fake.len; - phase.store(@intFromEnum(Phase.done_yes), .release); + phase.store(@backingInt(Phase.done_yes), .release); return; } if (!auto_update.impl) { - phase.store(@intFromEnum(Phase.done_no), .release); + phase.store(@backingInt(Phase.done_no), .release); return; } if (!auto_update.installLayoutSupported(io)) { - phase.store(@intFromEnum(Phase.done_no), .release); + phase.store(@backingInt(Phase.done_no), .release); return; } const thread = std.Thread.spawn(.{}, worker, .{io}) catch { - phase.store(@intFromEnum(Phase.done_no), .release); + phase.store(@backingInt(Phase.done_no), .release); return; }; thread.detach(); @@ -87,7 +87,7 @@ pub fn startLaunchCheck(io: std.Io, simulate_out_of_date: bool) void { fn worker(io: std.Io) void { var ver_buf: [128]u8 = undefined; const summary = auto_update.checkRemoteVersionSummary(io, std.heap.page_allocator, &ver_buf) catch { - phase.store(@intFromEnum(Phase.done_no), .release); + phase.store(@backingInt(Phase.done_no), .release); return; }; switch (summary) { @@ -95,19 +95,19 @@ fn worker(io: std.Io) void { const n = @min(v.len, remote_ver.len); @memcpy(remote_ver[0..n], v[0..n]); remote_ver_len = n; - phase.store(@intFromEnum(Phase.done_yes), .release); + phase.store(@backingInt(Phase.done_yes), .release); }, .no_update, .remote_empty => { - phase.store(@intFromEnum(Phase.done_no), .release); + phase.store(@backingInt(Phase.done_no), .release); }, else => { - phase.store(@intFromEnum(Phase.done_no), .release); + phase.store(@backingInt(Phase.done_no), .release); }, } } pub fn badgeVisible() bool { - return phase.load(.acquire) == @intFromEnum(Phase.done_yes); + return phase.load(.acquire) == @backingInt(Phase.done_yes); } /// Where the launch update check has got to, for anything that must sequence itself *behind* a @@ -138,7 +138,7 @@ pub fn appUpdateState() AppUpdateState { else => return .installing, }; } - return switch (@as(Phase, @enumFromInt(phase.load(.acquire)))) { + return switch (@as(Phase, @fromBackingInt(@intCast(phase.load(.acquire))))) { .pending => .checking, .done_no => .none, .done_yes => .available, diff --git a/app/watch/FolderWatcher.zig b/app/watch/FolderWatcher.zig index 316cf29ba..3ff5f0301 100644 --- a/app/watch/FolderWatcher.zig +++ b/app/watch/FolderWatcher.zig @@ -57,7 +57,7 @@ const max_events: usize = 512; /// of deep paths can't crowd out everything else and no path length is special-cased. const path_arena_bytes: usize = 64 * 1024; -pub const have_impl = switch (builtin.os.tag) { +pub const have_impl = switch (builtin.target.os.tag) { .macos, .linux, .windows => true, else => false, }; diff --git a/app/watch/SettingsWatcher.zig b/app/watch/SettingsWatcher.zig index f713f1b1f..1b59afa7a 100644 --- a/app/watch/SettingsWatcher.zig +++ b/app/watch/SettingsWatcher.zig @@ -27,7 +27,7 @@ const SettingsWatcher = @This(); /// file a moment to finish before `Editor.reconcileExternalSettingsChange` reads it. const debounce_ns: i128 = 200 * std.time.ns_per_ms; -pub const have_impl = switch (builtin.os.tag) { +pub const have_impl = switch (builtin.target.os.tag) { .macos, .linux, .windows => true, else => false, }; @@ -108,7 +108,7 @@ fn classify(config_folder: []const u8, path: []const u8) Kind { const top = rest[0 .. std.mem.indexOfAny(u8, rest, "/\\") orelse rest.len]; for (state_folders) |name| if (std.mem.eql(u8, top, name)) return .ignored; } - const ext = switch (builtin.os.tag) { + const ext = switch (builtin.target.os.tag) { .windows => ".dll", .macos => ".dylib", else => ".so", @@ -139,7 +139,7 @@ pub fn emptyStateFolder(io: std.Io, path: []const u8) void { } test classify { - const ext = switch (builtin.os.tag) { + const ext = switch (builtin.target.os.tag) { .windows => ".dll", .macos => ".dylib", else => ".so", diff --git a/backend/build.zig b/backend/build.zig index a9070c44c..9d27bc4de 100644 --- a/backend/build.zig +++ b/backend/build.zig @@ -23,7 +23,7 @@ pub fn build(b: *std.Build) void { // harness, against the patched library. Arguments after `--` reach the runner: // `--filter `, `--require-gpu `. const test_sdl_step = b.step("test-sdl", "Build and run the suites for fizzy's SDL patches"); - const sdl_optimize: std.builtin.OptimizeMode = if (optimize == .Debug) .ReleaseFast else optimize; + const sdl_optimize: std.builtin.OptimizeMode = if (optimize == .debug) .fast else optimize; if (sdlDependency(b, target, sdl_optimize, null)) |sdl| { const source = patchedSdl(b).?; const sdl_test = b.addLibrary(.{ @@ -54,7 +54,7 @@ pub fn build(b: *std.Build) void { suites.root_module.linkLibrary(sdl_test); suites.root_module.linkLibrary(sdl.artifact("SDL3")); const run = b.addRunArtifact(suites); - if (b.args) |args| run.addArgs(args); + run.addPassthruArgs(); test_sdl_step.dependOn(&run.step); } } @@ -107,7 +107,7 @@ pub fn backendModule(dep: *std.Build.Dependency, dvui_mod: *std.Build.Module, op // device, every window's present. A Debug frame spent much of itself there (`sample`), and a // window's way into full screen ran at half the display's rate in Debug alone. The app's own // code stays Debug; stepping into SDL's C is what a Debug app gives up. - const sdl_optimize: std.builtin.OptimizeMode = if (opts.optimize == .Debug) .ReleaseFast else opts.optimize; + const sdl_optimize: std.builtin.OptimizeMode = if (opts.optimize == .debug) .fast else opts.optimize; const sdl_dep = sdlDependency(b, opts.target, sdl_optimize, opts.macos_sdk); const sdl_translate_c = b.addTranslateC(.{ @@ -145,7 +145,7 @@ pub fn backendModule(dep: *std.Build.Dependency, dvui_mod: *std.Build.Module, op fn patchedSdl(b: *std.Build) ?std.Build.LazyPath { const io = b.graph.io; var names: std.ArrayList([]const u8) = .empty; - var dir = b.build_root.handle.openDir(io, "patches/sdl", .{ .iterate = true }) catch |err| + var dir = b.root.root_dir.handle.openDir(io, "patches/sdl", .{ .iterate = true }) catch |err| std.debug.panic("backend: open patches/sdl: {t}", .{err}); defer dir.close(io); var it = dir.iterate(); diff --git a/backend/build.zig.zon b/backend/build.zig.zon index 715385de8..2fdbf3fd6 100644 --- a/backend/build.zig.zon +++ b/backend/build.zig.zon @@ -2,15 +2,15 @@ .name = .fizzy_backend, .version = "0.1.0", .fingerprint = 0x2bf08855a368b9a8, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .dependencies = .{ // SDL3 for the window, its events and the GPU: upstream SDL, patched at build time with // `patches/sdl` (marler8997/zig-patch), built by fizzyedit/sdl_zig's `fizzy` branch, which // takes the patched tree (`-Dsdl_source`). How the patches are made and how SDL is updated: // fizzy's docs/DEPENDENCIES.md. Pinned here, by the package that uses them, and nowhere else. .sdl = .{ - .url = "https://github.com/fizzyedit/sdl_zig/archive/a99807da9b6217422449a824cb24de4d97f482a3.tar.gz", - .hash = "sdl-1.0.3+3.4.16-i4QD0f8KqgC0YeoFofQA7ZnPQWkyEVe2MJ-jhVnFqYk9", + .url = "https://github.com/fizzyedit/sdl_zig/archive/404f499d42156bdf428497ab08a82467c5a914c5.tar.gz", + .hash = "sdl-1.0.3+3.4.16-i4QD0XwKqgDQGV9OomCmHzkEF4hF1jVodwd7AUzla1LW", .lazy = true, }, // The release sdl_zig's own `.sdl` pins: the same tarball, fetched once. @@ -26,8 +26,8 @@ }, // AppKit from Zig, for the macOS platform pieces (and an app's own, `objcModule`). .zig_objc = .{ - .url = "https://github.com/foxnne/zig-objc/archive/c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz", - .hash = "zig_objc-0.0.0-Ir_Sp9gsAQCPAJc0oF5xoWePHWP6Y6tCphDeyNUThJoi", + .url = "https://github.com/foxnne/zig-objc/archive/f90394bce753153e387117cff6bf5536cd08f0c5.tar.gz", + .hash = "zig_objc-0.0.0-Ir_Sp1orAQDJQI7i3urWjRRHXVlB7ZG4pbKY3H8hX1AU", .lazy = true, }, // Win32 from Zig, for the Windows platform pieces (and an app's own, `win32Module`). diff --git a/backend/src/GpuRenderer.zig b/backend/src/GpuRenderer.zig index 87eccb081..e8772e035 100644 --- a/backend/src/GpuRenderer.zig +++ b/backend/src/GpuRenderer.zig @@ -272,8 +272,8 @@ pub fn primary() ?*GpuRenderer { /// D3D12 the window takes whatever SDL picks, and is opaque. fn createDevice(window: *c.SDL_Window) ?*c.SDL_GPUDevice { const formats = c.SDL_GPU_SHADERFORMAT_SPIRV | c.SDL_GPU_SHADERFORMAT_DXIL | c.SDL_GPU_SHADERFORMAT_MSL; - const debug = builtin.mode == .Debug; - if (builtin.os.tag == .windows and c.SDL_GetWindowFlags(window) & c.SDL_WINDOW_TRANSPARENT != 0) { + const debug = builtin.mode == .debug; + if (builtin.target.os.tag == .windows and c.SDL_GetWindowFlags(window) & c.SDL_WINDOW_TRANSPARENT != 0) { if (c.SDL_CreateGPUDevice(formats, debug, "direct3d12")) |device| return device; log.warn("no D3D12 device ({s}); the transparent window will be opaque", .{c.SDL_GetError()}); } @@ -411,7 +411,7 @@ pub fn destroy(self: *GpuRenderer) void { /// (SDL_GPU tags it sRGB, on claim and on every swapchain-parameter change). See /// `macos_monitor.m`. fn prepareLayer(window: *c.SDL_Window) void { - if (builtin.os.tag != .macos) return; + if (builtin.target.os.tag != .macos) return; const nswindow = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return; fizzy_native_metal_layer_prepare(nswindow); } @@ -657,7 +657,7 @@ pub fn claimViewport(self: *GpuRenderer, window: *c.SDL_Window) !void { log.err("SDL_ClaimWindowForGPUDevice (viewport) failed: {s}", .{c.SDL_GetError()}); return error.GpuClaimWindow; } - const mode: c.SDL_GPUPresentMode = if (builtin.os.tag == .macos) + const mode: c.SDL_GPUPresentMode = if (builtin.target.os.tag == .macos) c.SDL_GPU_PRESENTMODE_VSYNC else if (c.SDL_WindowSupportsGPUPresentMode(self.device, window, c.SDL_GPU_PRESENTMODE_IMMEDIATE)) c.SDL_GPU_PRESENTMODE_IMMEDIATE @@ -1193,7 +1193,7 @@ fn formatSupported(format: dvui.enums.TexturePixelFormat) bool { /// Where R, G, B and A sit in a texel of `format`, as bytes in memory; A null when the format /// has none (opaque). fn layout(format: dvui.enums.TexturePixelFormat) struct { r: u2, g: u2, b: u2, a: ?u2 } { - const little = builtin.cpu.arch.endian() == .little; + const little = builtin.target.cpu.arch.endian() == .little; // The `_8_8_8_8` formats are packed 32-bit words, named from the high byte down: in memory // on a little-endian machine they are the byte order reversed. const f: dvui.enums.TexturePixelFormat = if (!little) format else switch (format) { @@ -1310,7 +1310,7 @@ pub fn programBegin(self: *GpuRenderer, id: u32, textures: []const ?*anyopaque, } pub fn programBlend(self: *GpuRenderer, mode: u8) void { - self.blend_override = @enumFromInt(@as(u2, @truncate(mode))); + self.blend_override = @fromBackingInt(@intCast(@as(u2, @truncate(mode)))); } pub fn programEnd(self: *GpuRenderer) void { diff --git a/backend/src/Health.zig b/backend/src/Health.zig index 57c0c8c02..9b9c806cd 100644 --- a/backend/src/Health.zig +++ b/backend/src/Health.zig @@ -77,11 +77,11 @@ pub const Counts = struct { destroyed: u32 = 0, fn add(a: *Counts, b: Counts) void { - inline for (std.meta.fields(Counts)) |f| @field(a, f.name) +|= @field(b, f.name); + inline for (@typeInfo(Counts).@"struct".field_names, @typeInfo(Counts).@"struct".field_types, @typeInfo(Counts).@"struct".field_attrs) |f_name, _, _| @field(a, f_name) +|= @field(b, f_name); } fn max(a: *Counts, b: Counts) void { - inline for (std.meta.fields(Counts)) |f| @field(a, f.name) = @max(@field(a, f.name), @field(b, f.name)); + inline for (@typeInfo(Counts).@"struct".field_names, @typeInfo(Counts).@"struct".field_types, @typeInfo(Counts).@"struct".field_attrs) |f_name, _, _| @field(a, f_name) = @max(@field(a, f_name), @field(b, f_name)); } }; @@ -284,7 +284,7 @@ test "SDL's log is counted from any thread, and the first error kept" { for (&pool) |*th| th.* = try std.Thread.spawn(.{}, struct { fn run(hp: *Health) void { for (0..each) |_| { - hp.noteLog(.@"error", "x" ** (first_error_cap + 10)); + hp.noteLog(.@"error", &@as([first_error_cap + 10]u8, @splat('x'))); hp.noteLog(.warning, "w"); } } @@ -295,7 +295,7 @@ test "SDL's log is counted from any thread, and the first error kept" { try std.testing.expectEqual(@as(u32, threads * each), s.sdl_errors); try std.testing.expectEqual(@as(u32, threads * each + 1), s.sdl_warnings); // Cut to the cap, written once. - try std.testing.expectEqualStrings("x" ** first_error_cap, s.first_sdl_error); + try std.testing.expectEqualStrings(&@as([first_error_cap]u8, @splat('x')), s.first_sdl_error); } test "a snapshot round-trips through ZON" { @@ -312,10 +312,16 @@ test "a snapshot round-trips through ZON" { var out: std.Io.Writer.Allocating = .init(std.testing.allocator); defer out.deinit(); try std.zon.stringify.serialize(s, .{}, &out.writer); - const text = try std.testing.allocator.dupeZ(u8, out.written()); + const text = try std.testing.allocator.dupeSentinel(u8, out.written(), 0); defer std.testing.allocator.free(text); - const back = try std.zon.parse.fromSliceAlloc(Snapshot, std.testing.allocator, text, null, .{}); - defer std.zon.parse.free(std.testing.allocator, back); + const back = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Snapshot, .{ .gpa = std.testing.allocator, .arena = zig017_arena.allocator(), .source = text, .diagnostics = &zig017_zd }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Snapshot, std.testing.allocator, zig017_v) else |zig017_e| zig017_e; + }); + defer zig017ZonFree(std.testing.allocator, back); try std.testing.expectEqual(s.presents, back.presents); try std.testing.expectEqual(s.os_windows, back.os_windows); try std.testing.expectEqual(s.ns_windows, back.ns_windows); @@ -324,3 +330,64 @@ test "a snapshot round-trips through ZON" { try std.testing.expectEqual(s.last_frame, back.last_frame); try std.testing.expectEqualStrings(s.first_sdl_error, back.first_sdl_error); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} diff --git a/backend/src/SDLBackend.zig b/backend/src/SDLBackend.zig index 1eebc977e..a5f5bdd3c 100644 --- a/backend/src/SDLBackend.zig +++ b/backend/src/SDLBackend.zig @@ -122,7 +122,7 @@ stack_stale: bool = true, /// is skinned so as it opens (`viewportGlass`). window_skin: ?dvui.Color = null, -const cursor_enum_count = @typeInfo(dvui.enums.Cursor).@"enum".fields.len; +const cursor_enum_count = @typeInfo(dvui.enums.Cursor).@"enum".field_names.len; /// The most viewports open at once. pub const max_viewports = 8; @@ -366,7 +366,7 @@ pub fn health() Health.Snapshot { var n: c_int = 0; if (c.SDL_GetWindows(&n)) |windows| c.SDL_free(@ptrCast(windows)); s.os_windows = @intCast(@max(n, 0)); - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { var all: c_uint = 0; var visible: c_uint = 0; fizzy_macos_window_counts(&all, &visible); @@ -412,7 +412,7 @@ const quit_signals = struct { var wake_fd: posix.fd_t = -1; fn install() void { - if (comptime builtin.os.tag == .windows) return; + if (comptime builtin.target.os.tag == .windows) return; if (wake_fd == -1) wake_fd = spawnWaker() orelse return; for (signals, &chained) |sig, *slot| { if (slot.* != null) continue; @@ -450,7 +450,7 @@ const quit_signals = struct { /// Puts SDL's handlers back, so `SDL_Quit` finds its own and resets the signals to their /// defaults. The thread stays parked on its pipe for the rest of the process. fn uninstall() void { - if (comptime builtin.os.tag == .windows) return; + if (comptime builtin.target.os.tag == .windows) return; for (signals, &chained) |sig, *slot| if (slot.*) |old| { posix.sigaction(sig, &old, null); slot.* = null; @@ -473,7 +473,7 @@ const quit_signals = struct { const n = std.c.read(fd, &buf, buf.len); if (n == 0) return; if (n < 0) { - if (std.c._errno().* == @intFromEnum(posix.E.INTR)) continue; + if (std.c._errno().* == @backingInt(posix.E.INTR)) continue; return; } const pool = if (comptime has_objc) objc_autoreleasePoolPush() else null; @@ -557,7 +557,7 @@ fn createWindowRenderer(options: InitOptions, share_device_of: ?*GpuRenderer) !s // adjust window size for content scale if (scale != 1.0 and saved_geometry == null) { - if (builtin.abi.isAndroid()) { + if (builtin.target.abi.isAndroid()) { // log.error fails on Android but SDL_Log will show up in LogCat c.SDL_Log("[ERROR] Android doesn't support SDL_SetWindowSize"); } else { @@ -570,7 +570,7 @@ fn createWindowRenderer(options: InitOptions, share_device_of: ?*GpuRenderer) !s } if (options.min_size) |size| { - if (builtin.abi.isAndroid()) { + if (builtin.target.abi.isAndroid()) { c.SDL_Log("[ERROR] Android doesn't support SDL_SetWindowMinimumSize"); } else { try toErr(c.SDL_SetWindowMinimumSize( @@ -582,7 +582,7 @@ fn createWindowRenderer(options: InitOptions, share_device_of: ?*GpuRenderer) !s } if (options.max_size) |size| { - if (builtin.abi.isAndroid()) { + if (builtin.target.abi.isAndroid()) { c.SDL_Log("[ERROR] Android doesn't support SDL_SetWindowMaximumSize"); } else { try toErr(c.SDL_SetWindowMaximumSize( @@ -653,16 +653,16 @@ pub const WindowGeometry = struct { fn filePath(buf: []u8, options: InitOptions) ?[:0]const u8 { if (options.pref_path) |dir| { if (std.mem.endsWith(u8, dir, std.fs.path.sep_str)) { - return std.fmt.bufPrintZ(buf, "{s}{s}", .{ dir, zon_file_name }) catch null; + return std.fmt.bufPrintSentinel(buf, "{s}{s}", .{ dir, zon_file_name }, 0) catch null; } - return std.fmt.bufPrintZ(buf, "{s}{c}{s}", .{ dir, std.fs.path.sep, zon_file_name }) catch null; + return std.fmt.bufPrintSentinel(buf, "{s}{c}{s}", .{ dir, std.fs.path.sep, zon_file_name }, 0) catch null; } const pref = c.SDL_GetPrefPath(options.org.ptr, options.title.ptr) orelse { logErr("SDL_GetPrefPath in WindowGeometry") catch {}; return null; }; defer c.SDL_free(pref); - return std.fmt.bufPrintZ(buf, "{s}" ++ zon_file_name, .{std.mem.span(@as([*:0]const u8, @ptrCast(pref)))}) catch null; + return std.fmt.bufPrintSentinel(buf, "{s}" ++ zon_file_name, .{std.mem.span(@as([*:0]const u8, @ptrCast(pref)))}, 0) catch null; } fn fromSaved(saved: Saved) ?WindowGeometry { @@ -700,13 +700,13 @@ pub const WindowGeometry = struct { if (data.len >= nul_buf.len) return null; @memcpy(nul_buf[0..data.len], data); nul_buf[data.len] = 0; - const saved = std.zon.parse.fromSlice( - Saved, - std.heap.page_allocator, - nul_buf[0..data.len :0], - null, - .{ .ignore_unknown_fields = true }, - ) catch return null; + const saved = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Saved, .{ .gpa = std.heap.page_allocator, .arena = zig017_arena.allocator(), .source = nul_buf[0..data.len :0], .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Saved, std.heap.page_allocator, zig017_v) else |zig017_e| zig017_e; + }) catch return null; return fromSaved(saved); } @@ -759,7 +759,7 @@ fn configureBackend(back: *SDLBackend, options: InitOptions) !void { back.clear_window_on_begin = true; if (options.icon) |bytes| { - if (builtin.abi.isAndroid()) { + if (builtin.target.abi.isAndroid()) { // log.error fails on Android but SDL_Log will show up in LogCat c.SDL_Log("[ERROR] Android doesn't support setting a custom icon at runtime"); } else { @@ -770,7 +770,7 @@ fn configureBackend(back: *SDLBackend, options: InitOptions) !void { pub fn init(io: std.Io, window: *c.SDL_Window, gpu: *GpuRenderer) SDLBackend { dvui.io = io; - if (builtin.os.tag == .macos) { + if (builtin.target.os.tag == .macos) { fizzy_native_monitor_install(); if (cocoaWindow(window)) |nswindow| { fizzy_native_disable_titlebar_separator(nswindow); @@ -792,7 +792,7 @@ extern "c" fn fizzy_native_viewport_presented(nswindow: ?*anyopaque) void; extern "c" fn fizzy_native_live_resize_next_frame(nswindow: *anyopaque, wait_s: f64, since_start_s: f64) void; fn cocoaWindow(window: *c.SDL_Window) ?*anyopaque { - if (builtin.os.tag != .macos) return null; + if (builtin.target.os.tag != .macos) return null; return c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null); } @@ -800,12 +800,12 @@ fn cocoaWindow(window: *c.SDL_Window) ?*anyopaque { /// inside (`appIterate`): AppKit's tracking loop on macOS; on Windows the modal move/size loop, /// where SDL runs frames from a timer and from each step (fizzyedit/SDL's live-resize patch). fn inLiveResize(self: *SDLBackend) bool { - if (builtin.os.tag == .macos) { + if (builtin.target.os.tag == .macos) { if (cocoaWindow(self.window)) |nswindow| return fizzy_native_in_live_resize(nswindow) != 0; } // The thread's move/size loop: any of fizzy's windows', the main one or a popped-out float's // (`Viewport.win32_loop`), where SDL runs the frame from the loop's timer and from each step. - if (builtin.os.tag == .windows) { + if (builtin.target.os.tag == .windows) { var info = std.mem.zeroes(win32_gui.GUITHREADINFO); info.cbSize = @sizeOf(win32_gui.GUITHREADINFO); if (win32_gui.GetGUIThreadInfo(0, &info) != 0) return info.flags & win32_gui.GUI_INMOVESIZE != 0; @@ -818,7 +818,7 @@ fn inLiveResize(self: *SDLBackend) bool { /// `waitTime`), so an animation keeps the display's rate while no resize step is drawing it /// (`fizzy_native_live_resize_next_frame` in `macos_monitor.m`). fn liveResizeNextFrame(self: *SDLBackend, win: *const dvui.Window, wait_micros: u32) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const nswindow = cocoaWindow(self.window) orelse return; const wait_s: f64 = if (wait_micros == std.math.maxInt(u32)) -1 else @as(f64, @floatFromInt(wait_micros)) / std.time.us_per_s; const since_start_ns = @max(0, self.nanoTime() - win.frame_time_ns); @@ -934,7 +934,7 @@ pub fn cursorShow(_: *SDLBackend, value: ?bool) bool { pub fn native(self: *SDLBackend, _: *dvui.Window) dvui.Window.Native { const props = c.SDL_GetWindowProperties(self.window); - switch (builtin.os.tag) { + switch (builtin.target.os.tag) { .windows => return .{ .hwnd = c.SDL_GetPointerProperty(props, c.SDL_PROP_WINDOW_WIN32_HWND_POINTER, null) }, .macos => return .{ .cocoa_window = c.SDL_GetPointerProperty(props, c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) }, else => return {}, @@ -1099,9 +1099,9 @@ const ViewportKind = enum { window, carry, overlay, menu }; /// (`Viewport.ride`) — none for a menu opened in a float that is out. `dialog`: a dialog's window /// (`Viewport.dialog`). pub fn viewportOpenMenu(self: *SDLBackend, at: viewport_map.Rect, radius: f32, ride: Ride, dialog: bool) ?*Viewport { - if (comptime builtin.os.tag != .macos and builtin.os.tag != .windows) return null; + if (comptime builtin.target.os.tag != .macos and builtin.target.os.tag != .windows) return null; const vp = self.openViewport(at, "", .menu, .os) orelse return null; - switch (comptime builtin.os.tag) { + switch (comptime builtin.target.os.tag) { .macos => { _ = fizzy_macos_viewport_menu(cocoaWindow(vp.window), cocoaWindow(self.window), platform.window.ns_visual_effect_material, radius, @intFromBool(dialog)); vp.ride = switch (ride) { @@ -1130,19 +1130,19 @@ pub fn viewportOpenMenu(self: *SDLBackend, at: viewport_map.Rect, radius: f32, r /// `vp`'s window just above `other`'s, where it is not already (macOS). pub fn viewportOrderAbove(_: *SDLBackend, vp: *Viewport, other: *Viewport) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; fizzy_macos_viewport_order_above(cocoaWindow(vp.window), cocoaWindow(other.window)); } /// `vp`'s window at `other`'s level, just above it (macOS). pub fn viewportLift(_: *SDLBackend, vp: *Viewport, other: *Viewport) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; fizzy_macos_viewport_lift(cocoaWindow(vp.window), cocoaWindow(other.window)); } /// `vp`'s window back at a window's own level, after `viewportLift` (macOS). pub fn viewportSettle(self: *SDLBackend, vp: *Viewport) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; fizzy_macos_viewport_settle(cocoaWindow(vp.window), cocoaWindow(self.window)); self.stack_stale = true; } @@ -1174,7 +1174,7 @@ pub fn viewportMainOffset(_: *SDLBackend, vp: *Viewport, offset: viewport_map.Po /// window): borderless and clear, the pointer passing through it to what is under, kept above every /// window, in no window list, never focused. macOS. Placed and drawn as any viewport. pub fn viewportOpenCarry(self: *SDLBackend, at: viewport_map.Rect) ?*Viewport { - if (comptime builtin.os.tag != .macos) return null; + if (comptime builtin.target.os.tag != .macos) return null; return self.openViewport(at, "", .carry, .os); } @@ -1183,14 +1183,14 @@ pub fn viewportOpenCarry(self: *SDLBackend, at: viewport_map.Rect) ?*Viewport { /// grown to its display, clear but for its glass, the pointer passing through it. macOS 26, where /// `liquidGlassAvailable`. pub fn viewportOpenOverlay(self: *SDLBackend, at: viewport_map.Rect) ?*Viewport { - if (comptime builtin.os.tag != .macos) return null; + if (comptime builtin.target.os.tag != .macos) return null; if (!liquidGlassAvailable()) return null; return self.openViewport(at, "", .overlay, .os); } /// Whether the OS has Liquid Glass to draw (`viewportOpenOverlay`): macOS 26. pub fn liquidGlassAvailable() bool { - if (comptime builtin.os.tag != .macos) return false; + if (comptime builtin.target.os.tag != .macos) return false; return fizzy_macos_liquid_glass_available() != 0; } @@ -1224,7 +1224,7 @@ pub fn viewportOverlayGlass(_: *SDLBackend, vp: *Viewport, shapes: []const Glass /// RGBA rows, `w` by `h`, copied; null, none. Once a drag: its place each frame is cheap, as an /// image under the glass rather than a window of its own presented every frame. pub fn viewportOverlayPhotoImage(_: *SDLBackend, vp: *Viewport, rgba: ?[]const u8, w: u32, h: u32) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const px = rgba orelse { fizzy_macos_viewport_overlay_photo_image(cocoaWindow(vp.window), null, 0, 0); return; @@ -1300,7 +1300,7 @@ fn openViewport(self: *SDLBackend, at: viewport_map.Rect, title_text: [:0]const // A menu's window is a carry window that takes the pointer, never focused: the window it opened // from keeps the keyboard, as an OS menu leaves it. // One framed by the app (`ViewportFrame.app`) is borderless on macOS as well. - const titled = builtin.os.tag == .macos and !carry and frame == .os; + const titled = builtin.target.os.tag == .macos and !carry and frame == .os; const frame_flag: c.SDL_WindowFlags = if (titled) 0 else c.SDL_WINDOW_BORDERLESS; const size_flag: c.SDL_WindowFlags = if (carry) 0 else c.SDL_WINDOW_RESIZABLE; const focus_flag: c.SDL_WindowFlags = if (menu) c.SDL_WINDOW_NOT_FOCUSABLE else 0; @@ -1324,22 +1324,22 @@ fn openViewport(self: *SDLBackend, at: viewport_map.Rect, title_text: [:0]const // its own while the viewport was being resized — put the main window over it. Not on macOS, // where SDL makes it a child window that moves with its parent, and the windows that left // the main one stay where they are when it moves (`plans/POPOUT_WINDOWS_PLAN.md`, decision 2). - if (comptime builtin.os.tag != .macos) _ = c.SDL_SetWindowParent(window, self.window); + if (comptime builtin.target.os.tag != .macos) _ = c.SDL_SetWindowParent(window, self.window); self.viewport_slots[slot] = .{ .window = window, .band = b, .anchor = anchor, .density = d, .screen = placed.screen, .frame = placed.frame, .passive = carry and !menu, .overlay = role == .overlay, .menu = menu, .app_frame = frame == .app }; const vp = &self.viewport_slots[slot].?; // Dressed by `viewportOpenMenu`, and no hit test: nothing in it moves or resizes it. if (menu) return vp; if (role == .overlay) { - if (comptime builtin.os.tag == .macos) fizzy_macos_viewport_overlay(cocoaWindow(window), cocoaWindow(self.window)); + if (comptime builtin.target.os.tag == .macos) fizzy_macos_viewport_overlay(cocoaWindow(window), cocoaWindow(self.window)); return vp; } if (carry) { - if (comptime builtin.os.tag == .macos) fizzy_macos_viewport_carry(cocoaWindow(window), cocoaWindow(self.window), platform.window.ns_visual_effect_material); + if (comptime builtin.target.os.tag == .macos) fizzy_macos_viewport_carry(cocoaWindow(window), cocoaWindow(self.window), platform.window.ns_visual_effect_material); return vp; } // The slot holds it for the window's life, so SDL may keep the pointer. _ = c.SDL_SetWindowHitTest(window, viewportHitTest, vp); - if (comptime builtin.os.tag == .macos) if (frame == .app) { + if (comptime builtin.target.os.tag == .macos) if (frame == .app) { fizzy_macos_viewport_plain(cocoaWindow(window)); fizzy_macos_viewport_windows_item(cocoaWindow(window), title_text.ptr); }; @@ -1357,14 +1357,14 @@ pub fn viewportCarryShape(_: *SDLBackend, vp: *Viewport, radius: ?f32, alpha: f3 /// A carry window's Liquid Glass as the lens (the carried view) or as frost (a float's window growing /// out of it, as its own material is). macOS 26; nothing elsewhere. pub fn viewportCarryLens(_: *SDLBackend, vp: *Viewport, lens: bool) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; fizzy_macos_viewport_carry_lens(cocoaWindow(vp.window), @intFromBool(lens)); } /// Points from `vp`'s window's left edge past the OS's own buttons in its title bar: the traffic /// lights of a titled macOS window, with the margin before them. 0 elsewhere. pub fn viewportButtonsWidth(_: *SDLBackend, vp: *Viewport) f32 { - if (comptime builtin.os.tag != .macos) return 0; + if (comptime builtin.target.os.tag != .macos) return 0; if (vp.passive) return 0; return @floatCast(fizzy_macos_window_buttons_width(cocoaWindow(vp.window))); } @@ -1372,7 +1372,7 @@ pub fn viewportButtonsWidth(_: *SDLBackend, vp: *Viewport) f32 { /// The corner radius of a titled window, points: what a float's window is rounded by /// (`viewportCarryShape` grows the carried glass into it). pub fn windowCornerRadius(_: *SDLBackend) f32 { - if (comptime builtin.os.tag != .macos) return 0; + if (comptime builtin.target.os.tag != .macos) return 0; return @floatCast(fizzy_macos_window_corner_radius()); } @@ -1417,7 +1417,7 @@ pub fn viewportFade(_: *SDLBackend, vp: *Viewport, alpha: f32) void { /// windows anywhere — a window split out of the main one would open where the compositor likes, and /// none could follow a drag (`plans/POPOUT_WINDOWS_PLAN.md`). Floats stay in the main window there. pub fn viewportsAvailable() bool { - if (comptime builtin.os.tag != .linux) return true; + if (comptime builtin.target.os.tag != .linux) return true; const driver = c.SDL_GetCurrentVideoDriver() orelse return false; return !std.mem.eql(u8, std.mem.span(driver), "wayland"); } @@ -1452,7 +1452,7 @@ pub fn viewportClose(self: *SDLBackend, vp: *Viewport) void { _ = c.SDL_RaiseWindow(self.window); }; // The window as SDL made it, for SDL to destroy (`viewportGlass`). - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { platform.macos_monitor.unwatch(v.window); if (c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(v.window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null)) |ns| fizzy_macos_viewport_unglass(ns); } @@ -1472,7 +1472,7 @@ pub fn viewportClose(self: *SDLBackend, vp: *Viewport) void { fn placeWindow(vp: *Viewport, to: viewport_map.ScreenRect, was: viewport_map.ScreenRect) void { if (std.meta.eql(to, was)) return; vp.app_placed_ns = c.SDL_GetTicksNS(); - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { vp.frame_pending = true; return; } @@ -1486,7 +1486,7 @@ pub fn viewportPlace(_: *SDLBackend, vp: *Viewport, frame: viewport_map.Rect) vi // In a fullscreen Space, or on its way into or out of one, its frame is AppKit's: the float // follows the window (`viewportFollowWindow`), never the other way. A window put somewhere in // a Space is taken out of it. - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { if (platform.macos_monitor.osOwnsFrame(vp.window)) return vp.frame; } const placed = viewport_map.place(vp.band, vp.anchor, vp.density, frame); @@ -1594,10 +1594,10 @@ pub fn viewportHints(_: *SDLBackend, vp: *Viewport, hints: ?struct { drag: viewp .drag = win.of(h.drag, o, d), .keep = win.of(h.keep, o, d), .glass = win.of(h.glass, o, d), - .edge = if (builtin.os.tag == .macos or maximized) 0 else h.edge / d, + .edge = if (builtin.target.os.tag == .macos or maximized) 0 else h.edge / d, // Where the OS resizes from no edge (macOS), the float's own resize zones stay its own. - .app_side = if (builtin.os.tag == .macos) h.app_side / d else 0, - .app_corner = if (builtin.os.tag == .macos) h.app_corner / d else 0, + .app_side = if (builtin.target.os.tag == .macos) h.app_side / d else 0, + .app_corner = if (builtin.target.os.tag == .macos) h.app_corner / d else 0, }; } @@ -1613,7 +1613,7 @@ pub fn viewportOsPlaced(_: *SDLBackend, vp: *Viewport) ?viewport_map.Rect { /// when the window is no longer the size it was (resized, snapped or maximized), not just moved. /// Null otherwise — and for a window the OS moved with no press (a keyboard snap). pub fn viewportOsMoveEnded(_: *SDLBackend, vp: *Viewport) ?struct { resized: bool } { - if (comptime builtin.os.tag == .windows) { + if (comptime builtin.target.os.tag == .windows) { if (!vp.win32_loop.ended) return null; vp.win32_loop.ended = false; return .{ .resized = vp.win32_loop.resized }; @@ -1627,7 +1627,7 @@ pub fn viewportOsMoveEnded(_: *SDLBackend, vp: *Viewport) ?struct { resized: boo /// snapping and all, until the button is let go (`win32_titlebar.viewportDragMove`). False where /// that is not done (not Windows): the app goes on moving the window until the release. pub fn viewportDragMove(_: *SDLBackend, vp: *Viewport) bool { - if (comptime builtin.os.tag != .windows) return false; + if (comptime builtin.target.os.tag != .windows) return false; const hwnd = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(vp.window), c.SDL_PROP_WINDOW_WIN32_HWND_POINTER, null) orelse return false; platform.win32_titlebar.viewportDragMove(hwnd); return true; @@ -1638,9 +1638,9 @@ pub fn viewportDragMove(_: *SDLBackend, vp: *Viewport) bool { /// (macOS), the window switcher. pub fn viewportTitle(_: *SDLBackend, vp: *Viewport, text: []const u8) void { var buf: [128]u8 = undefined; - const z = std.fmt.bufPrintZ(&buf, "{s}", .{text[0..@min(text.len, buf.len - 1)]}) catch return; + const z = std.fmt.bufPrintSentinel(&buf, "{s}", .{text[0..@min(text.len, buf.len - 1)]}, 0) catch return; _ = c.SDL_SetWindowTitle(vp.window, z.ptr); - if (comptime builtin.os.tag == .macos) fizzy_macos_viewport_windows_item(cocoaWindow(vp.window), z.ptr); + if (comptime builtin.target.os.tag == .macos) fizzy_macos_viewport_windows_item(cocoaWindow(vp.window), z.ptr); } /// The least the OS may resize `vp`'s window to: `size` physical pixels of the frame, its float's @@ -1689,7 +1689,7 @@ extern fn fizzy_macos_viewport_windows_item(nswindow: ?*anyopaque, title: [*:0]c /// - Elsewhere: none, for now. pub fn viewportGlass(self: *SDLBackend, vp: *Viewport, inset: f32, radius: f32, dark: bool) bool { const props = c.SDL_GetWindowProperties(vp.window); - switch (comptime builtin.os.tag) { + switch (comptime builtin.target.os.tag) { .macos => { const ns = c.SDL_GetPointerProperty(props, c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return false; const main_ns = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(self.window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null); @@ -1759,7 +1759,7 @@ fn density(self: *SDLBackend) f32 { // and the two arrive apart — the frames after a window jumps its size into or out of full screen // read half or twice the density, the main window's picture laid out at the wrong scale in the // corner and floats placed through it shrunk and moved (the user). - if (comptime builtin.os.tag == .macos) if (cocoaWindow(self.window)) |ns| { + if (comptime builtin.target.os.tag == .macos) if (cocoaWindow(self.window)) |ns| { const s = fizzy_macos_window_backing_scale(ns); if (s > 0) return @floatCast(s); }; @@ -1946,12 +1946,12 @@ fn viewportFollowWindow(vp: *Viewport) void { if (std.meta.eql(now, vp.screen)) return; // Placing a window is asynchronous on X11: just after the app put it somewhere, what it says // of itself is still on its way there. - if (comptime builtin.os.tag == .linux) { + if (comptime builtin.target.os.tag == .linux) { if (c.SDL_GetTicksNS() -% vp.app_placed_ns < 250 * std.time.ns_per_ms) return; } // Under a held press, and no move loop to say when it ends (not Windows): the OS is moving it // until the press is let go. - if (comptime builtin.os.tag != .windows) { + if (comptime builtin.target.os.tag != .windows) { if (!vp.os_moving and c.SDL_GetGlobalMouseState(null, null) != 0) { vp.os_moving = true; vp.os_start = vp.screen; @@ -1985,7 +1985,7 @@ fn viewportHitTest(_: ?*c.SDL_Window, area: [*c]const c.SDL_Point, data: ?*anyop pub fn setCursor(self: *SDLBackend, cursor: dvui.enums.Cursor) void { // NOTE: SDL3's UIKit driver has no system cursors, so SDL_CreateSystemCursor always fails there. - if (builtin.os.tag == .ios) return; + if (builtin.target.os.tag == .ios) return; if (cursor == self.cursor_last) return; defer self.cursor_last = cursor; const new_shown_state = if (cursor == .hidden) false else if (self.cursor_last == .hidden) true else null; @@ -1997,7 +1997,7 @@ pub fn setCursor(self: *SDLBackend, cursor: dvui.enums.Cursor) void { if (new_state == false) return; } - const enum_int = @intFromEnum(cursor); + const enum_int = @backingInt(cursor); const tried = self.cursor_backing_tried[enum_int]; if (!tried) { self.cursor_backing_tried[enum_int] = true; @@ -2118,7 +2118,7 @@ pub fn renderPresent(self: *SDLBackend) void { // empty, then with a picture a frame of the drag away from it. var transacted: [max_viewports]bool = @splat(false); var any = false; - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { for (&self.viewport_slots, 0..) |*slot, i| { const vp = if (slot.*) |*v| v else continue; if (vp.app_frame and vp.shown) { @@ -2158,7 +2158,7 @@ pub fn renderPresent(self: *SDLBackend) void { } } } - defer if (comptime builtin.os.tag == .macos) if (any) { + defer if (comptime builtin.target.os.tag == .macos) if (any) { for (&self.viewport_slots, transacted) |*slot, t| { if (!t) continue; const vp = if (slot.*) |*v| v else continue; @@ -2196,7 +2196,7 @@ pub fn renderPresent(self: *SDLBackend) void { // A frame inside a Windows live resize is drawn before the loop goes on: one queued behind // others reaches the screen a composite or more after the size it was drawn for, as an edge // with nothing on it or a frame cut off (docs/WINDOWS_LIVE_RESIZE.md). - self.gpu.present(self.clear_window_on_begin, builtin.os.tag == .windows and self.inLiveResize()); + self.gpu.present(self.clear_window_on_begin, builtin.target.os.tag == .windows and self.inLiveResize()); // Shown once there is a frame in it (or empty, above). Not made key: the window it came out of // keeps the keyboard until the viewport is clicked. for (&self.viewport_slots, first_frame, map_empty) |*slot, show, empty| { @@ -2209,7 +2209,7 @@ pub fn renderPresent(self: *SDLBackend) void { vp.mapped = true; // Riding on its window from here (`Viewport.ride`) — not before it shows: made a child // window, AppKit shows it at once, before it has a picture. - if (comptime builtin.os.tag == .macos) if (vp.ride) |parent| { + if (comptime builtin.target.os.tag == .macos) if (vp.ride) |parent| { _ = c.SDL_SetWindowParent(vp.window, parent); if (!vp.dialog) fizzy_macos_viewport_menu_attached(cocoaWindow(vp.window)); }; @@ -2220,7 +2220,7 @@ pub fn renderPresent(self: *SDLBackend) void { // Over the main window as it first shows: SDL showed it below the key window. Once // (`noteMainForward`): from there it stacks as any window does. And once more as the app first // activates, which brings the main window in front of them (`fizzy_macos_watch_first_activation`). - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { fizzy_macos_watch_first_activation(); if (fizzy_macos_took_first_activation() != 0) for (&self.viewport_slots) |*slot| if (slot.*) |*v| { v.over_main = true; @@ -2317,7 +2317,7 @@ pub fn begin(self: *SDLBackend, arena: std.mem.Allocator) !void { // A float's window standing still on its way into or out of full screen: its picture has // taken this frame's step (`begin_hook`), and the float follows it there — as it did the // window's moves — and back to the window as the way ends. - if (comptime builtin.os.tag == .macos) for (&self.viewport_slots) |*slot| { + if (comptime builtin.target.os.tag == .macos) for (&self.viewport_slots) |*slot| { const vp = if (slot.*) |*v| v else continue; if (vp.picture_at[0] != 0 or vp.picture_at[1] != 0 or platform.window.windowSpacePicture(vp.window) != null) viewportFollowWindow(vp); @@ -2341,7 +2341,7 @@ fn mainPicture(self: *const SDLBackend) ?viewport_map.Rect { /// picture, while it stands still on its way into or out of full screen (`main_picture`). Null: /// the frame is the window. pub fn pictureInWindow(self: *SDLBackend) ?struct { at: dvui.Rect.Physical, all: dvui.Rect.Physical } { - if (comptime builtin.os.tag != .macos) return null; + if (comptime builtin.target.os.tag != .macos) return null; const p = self.mainPicture() orelse return null; // The drawable the frame goes into: the window's size the moment it jumps (the monitor keeps it // so), where SDL's word on its pixel size comes a frame or more later. @@ -2364,7 +2364,7 @@ pub fn pictureInWindow(self: *SDLBackend) ?struct { at: dvui.Rect.Physical, all: /// way into or out of full screen ran choppy and stood still partway for moments at a time (the /// user). The floats keep theirs the same way while the main window moves (`renderPresent`). fn mainHeld(self: *SDLBackend) bool { - if (comptime builtin.os.tag != .macos) return false; + if (comptime builtin.target.os.tag != .macos) return false; // Its own way into or out of full screen is drawn whatever AppKit says of it on the way. if (platform.window.windowMovingItself(self.window)) return false; if (c.SDL_GetWindowFlags(self.window) & c.SDL_WINDOW_OCCLUDED != 0) return true; @@ -2391,7 +2391,7 @@ pub fn pixelSize(self: *SDLBackend) dvui.Size.Physical { } var w: c_int = 0; var h: c_int = 0; - if (builtin.os.tag == .macos) { + if (builtin.target.os.tag == .macos) { if (cocoaWindow(self.window)) |nswindow| { if (fizzy_native_metal_drawable_size(nswindow, &w, &h) != 0) { self.last_pixel_size = .{ .w = @floatFromInt(w), .h = @floatFromInt(h) }; @@ -2788,7 +2788,7 @@ pub fn addEvent(self: *SDLBackend, win: *dvui.Window, event: c.SDL_Event) !bool // events per second of a 60Hz display // // Normalize by 60/refresh_hz on macOS - const mac_wheel_scale: f32 = if (sdl3 and builtin.os.tag == .macos) blk: { + const mac_wheel_scale: f32 = if (sdl3 and builtin.target.os.tag == .macos) blk: { const display = c.SDL_GetDisplayForWindow(self.window); if (display == 0) break :blk 1.0; const mode = c.SDL_GetCurrentDisplayMode(display) orelse break :blk 1.0; @@ -2803,7 +2803,7 @@ pub fn addEvent(self: *SDLBackend, win: *dvui.Window, event: c.SDL_Event) !bool // the right classification immediately. var mouse_type: dvui.enums.MouseType = .unknown; - if (sdl3 and builtin.os.tag == .macos) { + if (sdl3 and builtin.target.os.tag == .macos) { const v = fizzy_native_monitor_last_scroll_precise(); if (v >= 0) { mouse_type = if (v != 0) .trackpad else .mouse; @@ -2854,9 +2854,9 @@ pub fn addEvent(self: *SDLBackend, win: *dvui.Window, event: c.SDL_Event) !bool if (self.log_events) { log.debug("event FOCUS_GAINED\n", .{}); } - if (dvui.accesskit_enabled and builtin.os.tag == .linux) { + if (dvui.accesskit_enabled and builtin.target.os.tag == .linux) { dvui.AccessKit.c.accesskit_unix_adapter_update_window_focus_state(win.accesskit.adapter, true); - } else if (dvui.accesskit_enabled and builtin.os.tag == .macos) { + } else if (dvui.accesskit_enabled and builtin.target.os.tag == .macos) { const events = dvui.AccessKit.c.accesskit_macos_subclassing_adapter_update_view_focus_state(win.accesskit.adapter, true); if (events) |evts| { dvui.AccessKit.c.accesskit_macos_queued_events_raise(evts); @@ -2868,9 +2868,9 @@ pub fn addEvent(self: *SDLBackend, win: *dvui.Window, event: c.SDL_Event) !bool if (self.log_events) { log.debug("event FOCUS_LOST\n", .{}); } - if (dvui.accesskit_enabled and builtin.os.tag == .linux) { + if (dvui.accesskit_enabled and builtin.target.os.tag == .linux) { dvui.AccessKit.c.accesskit_unix_adapter_update_window_focus_state(win.accesskit.adapter, false); - } else if (dvui.accesskit_enabled and builtin.os.tag == .macos) { + } else if (dvui.accesskit_enabled and builtin.target.os.tag == .macos) { const events = dvui.AccessKit.c.accesskit_macos_subclassing_adapter_update_view_focus_state(win.accesskit.adapter, false); if (events) |evts| { dvui.AccessKit.c.accesskit_macos_queued_events_raise(evts); @@ -2885,7 +2885,7 @@ pub fn addEvent(self: *SDLBackend, win: *dvui.Window, event: c.SDL_Event) !bool if (comptime sdl3) { self.trackGeometry(); } - if (dvui.accesskit_enabled and builtin.os.tag == .linux) { + if (dvui.accesskit_enabled and builtin.target.os.tag == .linux) { var x: i32, var y: i32 = .{ undefined, undefined }; _ = c.SDL_GetWindowPosition(win.backend.impl.window, &x, &y); var w: i32, var h: i32 = .{ undefined, undefined }; @@ -2958,16 +2958,16 @@ pub fn SDL_keymod_to_dvui(keymod: u16) dvui.enums.Mod { if (keymod == if (sdl3) c.SDL_KMOD_NONE else c.KMOD_NONE) return dvui.enums.Mod.none; var m: u16 = 0; - if (keymod & (if (sdl3) c.SDL_KMOD_LSHIFT else c.KMOD_LSHIFT) > 0) m |= @intFromEnum(dvui.enums.Mod.lshift); - if (keymod & (if (sdl3) c.SDL_KMOD_RSHIFT else c.KMOD_RSHIFT) > 0) m |= @intFromEnum(dvui.enums.Mod.rshift); - if (keymod & (if (sdl3) c.SDL_KMOD_LCTRL else c.KMOD_LCTRL) > 0) m |= @intFromEnum(dvui.enums.Mod.lcontrol); - if (keymod & (if (sdl3) c.SDL_KMOD_RCTRL else c.KMOD_RCTRL) > 0) m |= @intFromEnum(dvui.enums.Mod.rcontrol); - if (keymod & (if (sdl3) c.SDL_KMOD_LALT else c.KMOD_LALT) > 0) m |= @intFromEnum(dvui.enums.Mod.lalt); - if (keymod & (if (sdl3) c.SDL_KMOD_RALT else c.KMOD_RALT) > 0) m |= @intFromEnum(dvui.enums.Mod.ralt); - if (keymod & (if (sdl3) c.SDL_KMOD_LGUI else c.KMOD_LGUI) > 0) m |= @intFromEnum(dvui.enums.Mod.lcommand); - if (keymod & (if (sdl3) c.SDL_KMOD_RGUI else c.KMOD_RGUI) > 0) m |= @intFromEnum(dvui.enums.Mod.rcommand); + if (keymod & (if (sdl3) c.SDL_KMOD_LSHIFT else c.KMOD_LSHIFT) > 0) m |= @backingInt(dvui.enums.Mod.lshift); + if (keymod & (if (sdl3) c.SDL_KMOD_RSHIFT else c.KMOD_RSHIFT) > 0) m |= @backingInt(dvui.enums.Mod.rshift); + if (keymod & (if (sdl3) c.SDL_KMOD_LCTRL else c.KMOD_LCTRL) > 0) m |= @backingInt(dvui.enums.Mod.lcontrol); + if (keymod & (if (sdl3) c.SDL_KMOD_RCTRL else c.KMOD_RCTRL) > 0) m |= @backingInt(dvui.enums.Mod.rcontrol); + if (keymod & (if (sdl3) c.SDL_KMOD_LALT else c.KMOD_LALT) > 0) m |= @backingInt(dvui.enums.Mod.lalt); + if (keymod & (if (sdl3) c.SDL_KMOD_RALT else c.KMOD_RALT) > 0) m |= @backingInt(dvui.enums.Mod.ralt); + if (keymod & (if (sdl3) c.SDL_KMOD_LGUI else c.KMOD_LGUI) > 0) m |= @backingInt(dvui.enums.Mod.lcommand); + if (keymod & (if (sdl3) c.SDL_KMOD_RGUI else c.KMOD_RGUI) > 0) m |= @backingInt(dvui.enums.Mod.rcommand); - return @as(dvui.enums.Mod, @enumFromInt(m)); + return @as(dvui.enums.Mod, @fromBackingInt(@intCast(m))); } pub fn SDL_keysym_to_dvui(keysym: i32) dvui.enums.Key { @@ -3155,7 +3155,7 @@ const SdlLogPriorityType = blk: { const Opt = @typeInfo(c.SDL_LogOutputFunction); const FnPtr = if (Opt == .optional) @typeInfo(Opt.optional.child) else Opt; const FnT = @typeInfo(FnPtr.pointer.child).@"fn"; - break :blk FnT.params[2].type.?; + break :blk FnT.param_types[2].?; }; fn sdlLogCallback(userdata: ?*anyopaque, category: c_int, priority: SdlLogPriorityType, message: [*c]const u8) callconv(.c) void { @@ -3262,7 +3262,7 @@ pub fn main(main_init: std.process.Init) !u8 { if (sdl3) _ = c.SDL_SetAppMetadata("DVUI App Example", "0.1", "com.example.dvui.app"); - if (builtin.os.tag == .windows) { // optional + if (builtin.target.os.tag == .windows) { // optional // on windows graphical apps have no console, so output goes to nowhere - attach it manually. related: https://github.com/ziglang/zig/issues/4196 dvui.Backend.Common.windowsAttachConsole() catch {}; } @@ -3689,12 +3689,12 @@ const live_resize_trace = struct { const Start = struct { t: f64, probe: [8]f64 }; fn enabled() bool { - if (comptime builtin.os.tag != .macos) return false; + if (comptime builtin.target.os.tag != .macos) return false; return fizzy_live_resize_trace_enabled() != 0; } fn begin(back: *SDLBackend) ?Start { - if (comptime builtin.os.tag != .macos) return null; + if (comptime builtin.target.os.tag != .macos) return null; if (!enabled()) return null; const nswindow = cocoaWindow(back.window) orelse return null; var p: [8]f64 = undefined; @@ -3709,7 +3709,7 @@ const live_resize_trace = struct { } fn end(s: Start, back: *SDLBackend, wait_micros: u32) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const r = frame_rect; const p = s.probe; std.debug.print("[lr] {d:.6} frame {d} src={s} since={d:.1}ms took={d:.1}ms win={d}x{d} layer={d}x{d} drawable={d}x{d} swap={d}x{d} rect={d}x{d} wait={d}us\n", .{ @@ -3759,3 +3759,40 @@ const live_resize_trace = struct { (dvui.Rect.Physical{ .x = 200, .y = r.h - 6, .w = r.w - 400, .h = 6 }).fill(.all(0), .{ .color = .{ .color = yellow } }); } }; + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/backend/src/platform/dialogs.zig b/backend/src/platform/dialogs.zig index 68b6145fa..fbb9ab3c1 100644 --- a/backend/src/platform/dialogs.zig +++ b/backend/src/platform/dialogs.zig @@ -67,7 +67,7 @@ pub fn showSaveFileDialog(cb: *const fn (?[][:0]const u8) void, filters: []const defer alloc().free(default); // Do not use our borderless/custom-frame main window as the dialog parent on Windows: the shell // may inherit extended style and the picker loses normal frame/close affordances. - const parent: ?*c.SDL_Window = if (builtin.os.tag == .windows) null else dvui.currentWindow().backend.impl.window; + const parent: ?*c.SDL_Window = if (builtin.target.os.tag == .windows) null else dvui.currentWindow().backend.impl.window; c.SDL_ShowSaveFileDialog(GenericSaveDialogCallback, @ptrCast(@alignCast(@constCast(cb))), parent, filters.ptr, @intCast(filters.len), default); } @@ -77,7 +77,7 @@ pub fn showOpenFileDialog(cb: *const fn (?[][:0]const u8) void, filters: []const break :blk std.fs.path.joinZ(alloc(), &.{ dir, default_filename }) catch "untitled"; }; defer alloc().free(default); - const parent: ?*c.SDL_Window = if (builtin.os.tag == .windows) null else dvui.currentWindow().backend.impl.window; + const parent: ?*c.SDL_Window = if (builtin.target.os.tag == .windows) null else dvui.currentWindow().backend.impl.window; c.SDL_ShowOpenFileDialog(GenericOpenDialogCallback, @ptrCast(@alignCast(@constCast(cb))), parent, filters.ptr, @intCast(filters.len), default.ptr, true); } @@ -87,7 +87,7 @@ pub fn showOpenFolderDialog(cb: *const fn (?[][:0]const u8) void, default_folder break :blk std.fmt.allocPrintSentinel(alloc(), "{s}", .{dir}, 0) catch "untitled"; }; defer alloc().free(default); - const parent: ?*c.SDL_Window = if (builtin.os.tag == .windows) null else dvui.currentWindow().backend.impl.window; + const parent: ?*c.SDL_Window = if (builtin.target.os.tag == .windows) null else dvui.currentWindow().backend.impl.window; c.SDL_ShowOpenFolderDialog(GenericOpenDialogCallback, @ptrCast(@alignCast(@constCast(cb))), parent, default.ptr, false); } @@ -182,7 +182,7 @@ fn GenericDialogCallback(cb: ?*anyopaque, files: [*c]const [*c]const u8, mode: D }; var allocated: usize = 0; for (0..path_count) |i| { - zig_files[i] = alloc().dupeZ(u8, std.mem.span(files[i])) catch { + zig_files[i] = alloc().dupeSentinel(u8, std.mem.span(files[i]), 0) catch { dvui.log.err("Failed to dupe dialog result path", .{}); for (zig_files[0..allocated]) |f| alloc().free(f); alloc().free(zig_files); diff --git a/backend/src/platform/geometry.zig b/backend/src/platform/geometry.zig index 1683bca6f..f8fb06aa7 100644 --- a/backend/src/platform/geometry.zig +++ b/backend/src/platform/geometry.zig @@ -73,9 +73,15 @@ fn fileLoad(_: ?*anyopaque) ?Geometry { const gpa = std.heap.page_allocator; const data = std.Io.Dir.cwd().readFileAlloc(file_io, path, gpa, .limited(4096)) catch return null; defer gpa.free(data); - const z = gpa.dupeZ(u8, data) catch return null; + const z = gpa.dupeSentinel(u8, data, 0) catch return null; defer gpa.free(z); - const g = std.zon.parse.fromSlice(Geometry, gpa, z, null, .{ .ignore_unknown_fields = true }) catch return null; + const g = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Geometry, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = z, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Geometry, gpa, zig017_v) else |zig017_e| zig017_e; + }) catch return null; return if (g.valid()) g else null; } @@ -128,10 +134,10 @@ fn cocoaWindowOf(window: *c.SDL_Window) ?*anyopaque { /// Where `win` is now, as `Geometry`. pub fn capture(win: *dvui.Window) ?Geometry { - switch (builtin.os.tag) { + switch (builtin.target.os.tag) { .macos => { const cocoa = cocoaWindowOf(win.backend.impl.window) orelse return null; - var out4: [4]f64 = .{0} ** 4; + var out4: [4]f64 = @splat(0); // The windowed frame — the one it had before a fullscreen Space, while it is in one. fizzy_macos_window_current_windowed_frame(cocoa, &out4); const g: Geometry = .{ .x = out4[0], .y = out4[1], .w = out4[2], .h = out4[3] }; @@ -165,7 +171,7 @@ pub fn capture(win: *dvui.Window) ?Geometry { /// Put `win` at `g`: false where it was not (no window, or nowhere on the screens there are now). fn apply(win: *dvui.Window, g: Geometry) bool { - switch (builtin.os.tag) { + switch (builtin.target.os.tag) { .macos => { const cocoa = cocoaWindowOf(win.backend.impl.window) orelse return false; if (!frameOnScreens(.{ .x = g.x, .y = g.y, .w = g.w, .h = g.h })) return false; @@ -233,7 +239,7 @@ var linux_window: ?*c.SDL_Window = null; var linux_watching = false; fn watchLinux(win: *dvui.Window) void { - if (comptime builtin.os.tag == .macos or builtin.os.tag == .windows) return; + if (comptime builtin.target.os.tag == .macos or builtin.target.os.tag == .windows) return; linux_window = win.backend.impl.window; trackLinux(); if (linux_watching) return; @@ -259,3 +265,40 @@ fn trackLinux() void { if (w < 1 or h < 1) return; linux_normal = .{ .x = @floatFromInt(x), .y = @floatFromInt(y), .w = @floatFromInt(w), .h = @floatFromInt(h) }; } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/backend/src/platform/linux_titlebar.zig b/backend/src/platform/linux_titlebar.zig index 75cbbf2cd..2b45246f3 100644 --- a/backend/src/platform/linux_titlebar.zig +++ b/backend/src/platform/linux_titlebar.zig @@ -31,7 +31,7 @@ var requested_insets: Insets = .{}; /// makes the window borderless with those frame insets. Where SDL keeps no margin (X11) /// `frameInsets` reads zero and nothing draws there. pub fn useClientDecorations(insets: Insets) void { - if (builtin.os.tag != .linux) return; + if (builtin.target.os.tag != .linux) return; // Only fizzy's own backend makes the window through a creation hook, against an SDL with // frame insets; dvui's keeps the desktop's decorations. if (comptime @hasDecl(backend, "window_create_hook") and @hasDecl(c, "SDL_PROP_WINDOW_CREATE_WAYLAND_FRAME_INSET_LEFT_NUMBER")) { @@ -52,7 +52,7 @@ fn addCreateProps(props: c.SDL_PropertiesID) void { /// The frame's margins in effect now, in window coordinates: those asked for while the window /// floats on Wayland; zero maximized, tiled or full screen, or where SDL keeps none. pub fn frameInsets(window: *c.SDL_Window) Insets { - if (comptime builtin.os.tag == .linux and @hasDecl(c, "SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER")) { + if (comptime builtin.target.os.tag == .linux and @hasDecl(c, "SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER")) { const props = c.SDL_GetWindowProperties(window); return .{ .left = @floatFromInt(c.SDL_GetNumberProperty(props, c.SDL_PROP_WINDOW_WAYLAND_FRAME_INSET_LEFT_NUMBER, 0)), @@ -71,7 +71,7 @@ pub fn frameInsets(window: *c.SDL_Window) Insets { /// Cheap to call every frame; it is sent to the compositor only when it changes, and taken with /// the frame presented next. pub fn blurBehind(window: *c.SDL_Window, radius: ?f32) bool { - if (comptime builtin.os.tag != .linux) return false; + if (comptime builtin.target.os.tag != .linux) return false; const props = c.SDL_GetWindowProperties(window); const display = c.SDL_GetPointerProperty(props, c.SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, null) orelse return false; const surface = c.SDL_GetPointerProperty(props, c.SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, null) orelse return false; @@ -104,7 +104,7 @@ const resize_frame_points: f32 = 6; /// Apply the chrome to `win` (see `window.setStyle`): idempotent, cheap to call every frame. pub fn applyChrome(win: *dvui.Window) void { - if (builtin.os.tag != .linux) return; + if (builtin.target.os.tag != .linux) return; const window = win.backend.impl.window; if (styled_window == window) return; styled_window = window; diff --git a/backend/src/platform/macos_monitor.zig b/backend/src/platform/macos_monitor.zig index 674309e35..9bf665b28 100644 --- a/backend/src/platform/macos_monitor.zig +++ b/backend/src/platform/macos_monitor.zig @@ -180,7 +180,7 @@ fn macosLiveResizeUpdate(window: *c.SDL_Window) void { /// Wake the SDL event loop from an AppKit notification about `cocoa`'s window. Sync its sizes /// first so the next appIterate begin() sees transition-correct dimensions. export fn fizzy_macos_window_resize_cb(cocoa: ?*anyopaque) void { - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { if (macos_pump_ready) { if (watchedOf(cocoa)) |w| macosSyncSizes(w); } @@ -200,7 +200,7 @@ export fn fizzy_macos_window_wake() void { /// Called from the monitor's 60Hz NSTimer for each window animating: its live sizes into SDL. export fn fizzy_macos_window_pump_sync(cocoa: ?*anyopaque) void { - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { if (!macos_pump_ready) return; if (watchedOf(cocoa)) |w| macosSyncSizes(w); } @@ -211,7 +211,7 @@ export fn fizzy_macos_window_pump_sync(cocoa: ?*anyopaque) void { /// it between frames; a transition's stage, run from inside one, only wakes the loop /// (`macosLiveResizeUpdate`). export fn fizzy_macos_window_pump_render() void { - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { if (!macos_pump_ready) return; const window = macos_monitor_window orelse return; macosLiveResizeUpdate(window); @@ -245,7 +245,7 @@ fn macosAppPresented(back: *Backend.SDLBackend) void { /// Sync AppKit → SDL before `Window.begin` during Space / zoom animations only, for every window /// animating. Registered on the SDL backend from `restoreWindowState`. fn macosAppPreBeginSync(back: *Backend.SDLBackend) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; if (!macos_pump_ready) return; _ = back; // Sync AppKit's live sizes into SDL during Space/zoom animations so dvui lays @@ -279,7 +279,7 @@ var macos_live_resize_saved_vsync: ?c_int = null; /// unless SDL draws the resize itself (`sdl_draws_live_resize` in `macos/window_monitor.m`), /// whose presents wait for no vsync. export fn fizzy_macos_window_live_resize_vsync(cocoa: ?*anyopaque, active: c_int) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const window = macos_monitor_window orelse return; // The main window's: a float's window is drawn with the main one's frame, paced by it. if (cocoa == null or cocoaWindowOf(window) != cocoa) return; @@ -319,7 +319,7 @@ export fn fizzy_macos_window_reset_sync_cache(cocoa: ?*anyopaque) void { /// Reconcile SDL's cached sizes and Metal drawable with `cocoa`'s live AppKit bounds. /// Called at didEnter/didExit so steady state never keeps transition sizes. export fn fizzy_macos_window_commit_steady_state(cocoa: ?*anyopaque) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; if (!macos_pump_ready) return; const w = watchedOf(cocoa) orelse return; fizzy_macos_window_reset_sync_cache(cocoa); @@ -379,7 +379,7 @@ export fn fizzy_macos_origin_nudged(cap_x: f64, cap_y: f64, cur_x: f64, cur_y: f /// through SDL's styles, for fizzy's chrome (`styleTitled`): without that it came back from each /// round trip a title bar shorter. pub fn install(win: *dvui.Window) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const back = win.backend.impl; const cocoa = cocoaWindowOf(back.window) orelse return; if (comptime !own_backend) return fizzy_macos_window_keep_full_size_content(cocoa); @@ -415,7 +415,7 @@ fn addWatched(w: Watched) void { /// followed (`install`): it goes full screen in a Space of its own, and its float follows it there /// and back. Call once its chrome is in place; `unwatch` before it closes. pub fn watch(window: *c.SDL_Window) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const cocoa = cocoaWindowOf(window) orelse return; addWatched(.{ .window = window, .cocoa = cocoa, .follow_position = true }); fizzy_macos_window_install_resize_observer(cocoa); @@ -426,7 +426,7 @@ pub fn watch(window: *c.SDL_Window) void { /// Stop following a float's window (`watch`), as it closes. pub fn unwatch(window: *c.SDL_Window) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const cocoa = cocoaWindowOf(window) orelse return; fizzy_macos_window_uninstall_monitor(cocoa); for (&watched) |*slot| { @@ -439,7 +439,7 @@ pub fn unwatch(window: *c.SDL_Window) void { /// Whether AppKit owns a watched window's frame: in a fullscreen Space, or on its way into or out /// of one, or un-zooming. Nothing may move it then. pub fn osOwnsFrame(window: *c.SDL_Window) bool { - if (comptime builtin.os.tag != .macos) return false; + if (comptime builtin.target.os.tag != .macos) return false; const cocoa = cocoaWindowOf(window) orelse return false; return fizzy_macos_window_in_fullscreen_space(cocoa) != 0 or fizzy_macos_window_transition_sync_active(cocoa) != 0; } @@ -461,7 +461,7 @@ pub const TitlebarState = struct { }; pub fn titlebarState(win: *dvui.Window) TitlebarState { - if (comptime builtin.os.tag != .macos) return .{}; + if (comptime builtin.target.os.tag != .macos) return .{}; const raw_ptr = cocoaWindowOf(win.backend.impl.window); const inset = if (raw_ptr != null) fizzy_macos_window_titlebar_inset(raw_ptr) else 0; const saved = fizzy_macos_window_saved_titlebar_inset(raw_ptr); diff --git a/backend/src/platform/menu.zig b/backend/src/platform/menu.zig index 56485ae67..a75be5918 100644 --- a/backend/src/platform/menu.zig +++ b/backend/src/platform/menu.zig @@ -115,13 +115,13 @@ pub export fn FizzyMenuEnabled(section: c_int, tag: c_int) callconv(.c) bool { const h = hooks orelse return true; if (h.input_blocked(h.ctx)) return false; if (section < 0 or section > 2 or tag < 0) return true; - return h.enabled(h.ctx, .{ .section = @enumFromInt(section), .tag = @intCast(tag) }); + return h.enabled(h.ctx, .{ .section = @fromBackingInt(@intCast(section)), .tag = @intCast(tag) }); } pub export fn FizzyMenuTitle(section: c_int, tag: c_int) callconv(.c) ?[*:0]const u8 { const h = hooks orelse return null; if (section < 0 or section > 2 or tag < 0) return null; - return h.title(h.ctx, .{ .section = @enumFromInt(section), .tag = @intCast(tag) }); + return h.title(h.ctx, .{ .section = @fromBackingInt(@intCast(section)), .tag = @intCast(tag) }); } pub export fn FizzyMenuInputBlocked() callconv(.c) bool { @@ -142,7 +142,7 @@ fn nsString(text: [*:0]const u8) objc.Object { /// A sentinel copy of `text` for AppKit, from the platform allocator; free with `freeZ`. fn dupeZ(text: []const u8) ?[:0]u8 { - return dialogs.allocator().dupeZ(u8, text) catch null; + return dialogs.allocator().dupeSentinel(u8, text, 0) catch null; } fn setImage(item: objc.Object, symbol: [*:0]const u8, description: [*:0]const u8) void { @@ -156,7 +156,7 @@ fn setImage(item: objc.Object, symbol: [*:0]const u8, description: [*:0]const u8 /// Build the bar: `menus` in order after the app menu, Help last, the app menu's About retitled /// `about_title` and routed to `pollAbout`. Once; `hooks` answer for every item from then on. pub fn install(menus: []const Menu, h: Hooks, about_title: ?[:0]const u8) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; if (installed) return; hooks = h; const NSApplication = objc.getClass("NSApplication") orelse return; @@ -276,7 +276,7 @@ pub fn isFixedMenu(id: []const u8) bool { /// first, so this is safe — and cheap enough — to call on every change. An `ExtraMenu` with no /// item in it is left out. pub fn setExtras(menus: []const ExtraMenu, items: []const ExtraItem) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; if (!installed) return; const bar = main_menu orelse return; const t = target orelse return; @@ -335,7 +335,7 @@ pub fn setExtras(menus: []const ExtraMenu, items: []const ExtraItem) void { /// allocator is freed. The fixed menus stay on the bar for the rest of the process, every item /// enabled with its title as built. Not for a bar the app goes on using: `install` is once. pub fn deinit() void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; hooks = null; const gpa = dialogs.allocator(); for (extra_leaves.items) |e| e.parent.msgSend(void, "removeItem:", .{e.item.value}); @@ -351,7 +351,7 @@ pub fn deinit() void { /// Fill list submenu `index` (the `index`th `.list` entry across the bar) with `titles`, in /// order; an item chosen reports its position in `titles`. Hidden while empty. pub fn setList(index: usize, titles: []const []const u8) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; if (index >= list_count) return; const menu = list_menus[index] orelse return; const t = target orelse return; @@ -373,7 +373,7 @@ pub fn setList(index: usize, titles: []const []const u8) void { /// `modifiers`) with `modifiers` (`modifier_command` and the rest); null clears it. An item not /// on the bar now is left alone. pub fn setKeyEquivalent(ref: Ref, key: ?[]const u8, modifiers: c_ulong) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; switch (ref.section) { .bar => if (bar_items.get(ref.tag)) |item| applyKey(item, key, modifiers), .extra => for (extra_leaves.items) |e| if (e.tag == ref.tag) applyKey(e.item, key, modifiers), @@ -398,7 +398,7 @@ fn applyKey(item: objc.Object, key: ?[]const u8, modifiers: c_ulong) void { /// The item chosen in `section` since the last call, if one was. Call once a frame, inside it. pub fn pollActivation(section: Section) ?Activation { - const tag = pending_tag[@intFromEnum(section)].swap(-1, .acq_rel); + const tag = pending_tag[@backingInt(section)].swap(-1, .acq_rel); if (tag < 0) return null; return .{ .ref = .{ .section = section, .tag = @intCast(tag) }, diff --git a/backend/src/platform/root.zig b/backend/src/platform/root.zig index 8b41fd44a..b0aea352d 100644 --- a/backend/src/platform/root.zig +++ b/backend/src/platform/root.zig @@ -27,7 +27,7 @@ comptime { // The menu bar's Objective-C (`macos/menu_target.m`) calls these back, and it is compiled into // every app on macOS (`addPlatformObjC`), so they are kept whether or not the app ever reaches // `menu` — a plain dvui app with no menu bar of its own failed to link without them. - if (@import("builtin").os.tag == .macos) { + if (@import("builtin").target.os.tag == .macos) { _ = &menu.FizzyMenuActivated; _ = &menu.FizzyMenuEnabled; _ = &menu.FizzyMenuTitle; diff --git a/backend/src/platform/titlebar.zig b/backend/src/platform/titlebar.zig index b64c64fcb..afcde8ef4 100644 --- a/backend/src/platform/titlebar.zig +++ b/backend/src/platform/titlebar.zig @@ -28,11 +28,11 @@ const std = @import("std"); const builtin = @import("builtin"); /// Whether the app draws its own title bar on this platform. -pub const active = builtin.os.tag == .windows or builtin.os.tag == .linux; +pub const active = builtin.target.os.tag == .windows or builtin.target.os.tag == .linux; /// Whether the OS asks these hints where a press goes: where the app draws its own title bar, /// and on macOS, where AppKit moves the window from any press in its transparent titlebar's /// region unless the view there says otherwise (`interactiveAt`). -pub const hit_tested = active or builtin.os.tag == .macos; +pub const hit_tested = active or builtin.target.os.tag == .macos; /// A caption button the app draws (Windows 11-style: the app draws them, the backend hit-tests them). pub const TitleBarButton = enum { minimize, maximize, close }; diff --git a/backend/src/platform/wayland_blur.zig b/backend/src/platform/wayland_blur.zig index 828f2cd63..9c3f5eb7e 100644 --- a/backend/src/platform/wayland_blur.zig +++ b/backend/src/platform/wayland_blur.zig @@ -29,7 +29,7 @@ pub const Want = struct { /// Cheap to call every frame: the region is sent only when it changes. The first call binds the /// protocols, two round trips to the compositor. pub fn behind(display: *anyopaque, surface: *anyopaque, want: ?Want) bool { - if (comptime builtin.os.tag != .linux) return false; + if (comptime builtin.target.os.tag != .linux) return false; const s = ready(display) orelse return false; // What the compositor has said since (the blur turned off or on). _ = s.lib.display_dispatch_queue_pending(s.display, s.queue); @@ -118,9 +118,9 @@ fn load() ?Lib { // SDL's own copy, already loaded for the window: never a second one. const handle = std.c.dlopen("libwayland-client.so.0", .{ .LAZY = true, .NOLOAD = true }) orelse return null; var lib: Lib = undefined; - inline for (@typeInfo(Lib).@"struct".fields) |f| { - const sym = std.c.dlsym(handle, "wl_" ++ f.name) orelse return null; - @field(lib, f.name) = @ptrCast(@alignCast(sym)); + inline for (@typeInfo(Lib).@"struct".field_names, @typeInfo(Lib).@"struct".field_types, @typeInfo(Lib).@"struct".field_attrs) |f_name, _, _| { + const sym = std.c.dlsym(handle, "wl_" ++ f_name) orelse return null; + @field(lib, f_name) = @ptrCast(@alignCast(sym)); } return lib; } diff --git a/backend/src/platform/win32_titlebar.zig b/backend/src/platform/win32_titlebar.zig index 66c70f640..9122cdde4 100644 --- a/backend/src/platform/win32_titlebar.zig +++ b/backend/src/platform/win32_titlebar.zig @@ -24,7 +24,7 @@ const DWMWCP_ROUND: u32 = 2; /// Apply the chrome to `win` (see `window.setStyle`): idempotent, cheap to call every frame. pub fn applyChrome(win: *dvui.Window) void { - if (builtin.os.tag != .windows) return; + if (builtin.target.os.tag != .windows) return; const hwnd = getWin32Hwnd(win) orelse return; const hwnd_h = @as(win32.foundation.HWND, @ptrCast(hwnd)); const WS_SYSMENU: isize = 0x00080000; @@ -53,7 +53,7 @@ pub fn applyChrome(win: *dvui.Window) void { _ = win32.graphics.dwm.DwmSetWindowAttribute( hwnd_h, - @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @enumFromInt(DWMWA_WINDOW_CORNER_PREFERENCE)), + @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @fromBackingInt(@intCast(DWMWA_WINDOW_CORNER_PREFERENCE))), &DWMWCP_ROUND, @sizeOf(u32), ); @@ -75,7 +75,7 @@ pub fn applyChrome(win: *dvui.Window) void { const backdrop_type: u32 = DWMSBT_TRANSIENTWINDOW; _ = win32.graphics.dwm.DwmSetWindowAttribute( hwnd_h, - @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @enumFromInt(DWMWA_SYSTEMBACKDROP_TYPE)), + @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @fromBackingInt(@intCast(DWMWA_SYSTEMBACKDROP_TYPE))), &backdrop_type, @sizeOf(u32), ); @@ -103,12 +103,12 @@ pub fn applyChrome(win: *dvui.Window) void { /// The backdrop's tint follows the app's theme: DWM draws Acrylic's dark or light variant by this /// window attribute, which otherwise follows the system's light or dark mode. pub fn setDarkMode(win: *dvui.Window, dark: bool) void { - if (builtin.os.tag != .windows) return; + if (builtin.target.os.tag != .windows) return; const hwnd = getWin32Hwnd(win) orelse return; const value: u32 = @intFromBool(dark); _ = win32.graphics.dwm.DwmSetWindowAttribute( @as(win32.foundation.HWND, @ptrCast(hwnd)), - @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @enumFromInt(DWMWA_USE_IMMERSIVE_DARK_MODE)), + @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @fromBackingInt(@intCast(DWMWA_USE_IMMERSIVE_DARK_MODE))), &value, @sizeOf(u32), ); @@ -116,7 +116,7 @@ pub fn setDarkMode(win: *dvui.Window, dark: bool) void { /// No caption or border tint: the app draws its own title bar in the client area. pub fn clearCaptionColors(win: *dvui.Window) void { - if (builtin.os.tag != .windows) return; + if (builtin.target.os.tag != .windows) return; const hwnd = getWin32Hwnd(win) orelse return; const hwnd_h = @as(win32.foundation.HWND, @ptrCast(hwnd)); @@ -238,7 +238,7 @@ fn win32MicaSubclassProc( const backdrop_type: u32 = DWMSBT_TRANSIENTWINDOW; _ = win32.graphics.dwm.DwmSetWindowAttribute( hWnd, - @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @enumFromInt(DWMWA_SYSTEMBACKDROP_TYPE)), + @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @fromBackingInt(@intCast(DWMWA_SYSTEMBACKDROP_TYPE))), &backdrop_type, @sizeOf(u32), ); @@ -277,8 +277,8 @@ fn win32MicaSubclassProc( // Resize edges/corners only while not maximized. const frame: titlebar.Frame = if (win32.ui.windows_and_messaging.IsZoomed(hWnd) != 0) .{} else .{ - .w = @max(win32.ui.windows_and_messaging.GetSystemMetrics(@as(win32.ui.windows_and_messaging.SYSTEM_METRICS_INDEX, @enumFromInt(SM_CXSIZEFRAME))), 4), - .h = @max(win32.ui.windows_and_messaging.GetSystemMetrics(@as(win32.ui.windows_and_messaging.SYSTEM_METRICS_INDEX, @enumFromInt(SM_CYSIZEFRAME))), 4), + .w = @max(win32.ui.windows_and_messaging.GetSystemMetrics(@as(win32.ui.windows_and_messaging.SYSTEM_METRICS_INDEX, @fromBackingInt(@intCast(SM_CXSIZEFRAME)))), 4), + .h = @max(win32.ui.windows_and_messaging.GetSystemMetrics(@as(win32.ui.windows_and_messaging.SYSTEM_METRICS_INDEX, @fromBackingInt(@intCast(SM_CYSIZEFRAME)))), 4), }; // The caption-button codes are also what make the Win11 snap-layouts flyout appear on // the maximize button. @@ -380,11 +380,11 @@ pub const ViewportLoop = struct { /// shadow; no border. True when DWM gives it the backdrop (Windows 11 22H2 on); false leaves the /// glass on the app's opaque backing. pub fn viewportChrome(hwnd: *anyopaque, main_hwnd: ?*anyopaque, dark: bool, radius_pt: f32, loop: *ViewportLoop) bool { - if (builtin.os.tag != .windows) return false; + if (builtin.target.os.tag != .windows) return false; const h: win32.foundation.HWND = @ptrCast(hwnd); const dwm = win32.graphics.dwm; const corner: u32 = if (radius_pt < 1) DWMWCP_DONOTROUND else if (radius_pt < 6) DWMWCP_ROUNDSMALL else DWMWCP_ROUND; - _ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_WINDOW_CORNER_PREFERENCE), &corner, @sizeOf(u32)); + _ = dwm.DwmSetWindowAttribute(h, @fromBackingInt(@intCast(DWMWA_WINDOW_CORNER_PREFERENCE)), &corner, @sizeOf(u32)); // No border and no caption colour, as the main window has none (`clearCaptionColors`): DWM // treats the window as framed (its caption taken away, `WM_NCCALCSIZE`) and tinted the strip // where a title bar would be — the accent colour, where Windows shows it on title bars — so @@ -393,11 +393,11 @@ pub fn viewportChrome(hwnd: *anyopaque, main_hwnd: ?*anyopaque, dark: bool, radi _ = dwm.DwmSetWindowAttribute(h, dwm.DWMWA_BORDER_COLOR, &none, @sizeOf(u32)); _ = dwm.DwmSetWindowAttribute(h, dwm.DWMWA_CAPTION_COLOR, &none, @sizeOf(u32)); const dark_value: u32 = @intFromBool(dark); - _ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_USE_IMMERSIVE_DARK_MODE), &dark_value, @sizeOf(u32)); + _ = dwm.DwmSetWindowAttribute(h, @fromBackingInt(@intCast(DWMWA_USE_IMMERSIVE_DARK_MODE)), &dark_value, @sizeOf(u32)); // No DWM animation as it shows or closes: it appears and goes exactly where its float is drawn, // in the frame it changes in (macOS: `NSWindowAnimationBehaviorNone`). const no_transitions: u32 = 1; - _ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_TRANSITIONS_FORCEDISABLED), &no_transitions, @sizeOf(u32)); + _ = dwm.DwmSetWindowAttribute(h, @fromBackingInt(@intCast(DWMWA_TRANSITIONS_FORCEDISABLED)), &no_transitions, @sizeOf(u32)); _ = win32.ui.shell.SetWindowSubclass(h, viewportSubclassProc, viewport_subclass_id, @intFromPtr(loop)); // No system menu: with the frame extended over the window, DWM draws the caption buttons // `WS_SYSMENU` brings, over the float's header (as `applyChrome` strips it from the main @@ -440,12 +440,12 @@ const menu_subclass_id: usize = 0x50584933; // "PXI3" /// fallback, so it is kept looking active (`menuSubclassProc`). True when DWM gives it the /// backdrop (Windows 11 22H2 on). pub fn viewportMenuChrome(hwnd: *anyopaque, dark: bool, radius_pt: f32) bool { - if (builtin.os.tag != .windows) return false; + if (builtin.target.os.tag != .windows) return false; const h: win32.foundation.HWND = @ptrCast(hwnd); const dwm = win32.graphics.dwm; const wm = win32.ui.windows_and_messaging; const corner: u32 = if (radius_pt < 1) DWMWCP_DONOTROUND else if (radius_pt < 6) DWMWCP_ROUNDSMALL else DWMWCP_ROUND; - _ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_WINDOW_CORNER_PREFERENCE), &corner, @sizeOf(u32)); + _ = dwm.DwmSetWindowAttribute(h, @fromBackingInt(@intCast(DWMWA_WINDOW_CORNER_PREFERENCE)), &corner, @sizeOf(u32)); // No border and no caption colour, as the main window has none (`clearCaptionColors`): DWM // treats the window as framed (its caption taken away, `WM_NCCALCSIZE`) and tinted the strip // where a title bar would be — the accent colour, where Windows shows it on title bars — so @@ -454,9 +454,9 @@ pub fn viewportMenuChrome(hwnd: *anyopaque, dark: bool, radius_pt: f32) bool { _ = dwm.DwmSetWindowAttribute(h, dwm.DWMWA_BORDER_COLOR, &none, @sizeOf(u32)); _ = dwm.DwmSetWindowAttribute(h, dwm.DWMWA_CAPTION_COLOR, &none, @sizeOf(u32)); const dark_value: u32 = @intFromBool(dark); - _ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_USE_IMMERSIVE_DARK_MODE), &dark_value, @sizeOf(u32)); + _ = dwm.DwmSetWindowAttribute(h, @fromBackingInt(@intCast(DWMWA_USE_IMMERSIVE_DARK_MODE)), &dark_value, @sizeOf(u32)); const no_transitions: u32 = 1; - _ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_TRANSITIONS_FORCEDISABLED), &no_transitions, @sizeOf(u32)); + _ = dwm.DwmSetWindowAttribute(h, @fromBackingInt(@intCast(DWMWA_TRANSITIONS_FORCEDISABLED)), &no_transitions, @sizeOf(u32)); _ = win32.ui.shell.SetWindowSubclass(h, menuSubclassProc, menu_subclass_id, 0); // No system menu, whose caption buttons DWM would draw over the menu with the frame extended // (as for a float's window, `viewportChrome`); SDL's caption style stays, collapsed by its @@ -491,7 +491,7 @@ fn menuSubclassProc( // Always active to DWM, so its backdrop stays Acrylic rather than the inactive fallback. if (uMsg == wm.WM_NCACTIVATE) return win32.ui.shell.DefSubclassProc(hWnd, uMsg, 1, lParam); // SDL sets the window's style again as it likes: never the system menu. - if (uMsg == wm.WM_STYLECHANGING and @as(isize, @bitCast(wParam)) == @intFromEnum(wm.GWL_STYLE)) { + if (uMsg == wm.WM_STYLECHANGING and @as(isize, @bitCast(wParam)) == @backingInt(wm.GWL_STYLE)) { const ss: *wm.STYLESTRUCT = @ptrFromInt(@as(usize, @bitCast(lParam))); ss.styleNew &= ~@as(u32, @intCast(viewport_ws_sysmenu)); } @@ -506,7 +506,7 @@ fn menuSubclassProc( /// every activation, as DWM wants (and as `win32MicaSubclassProc` does for the main window). fn applyViewportBackdrop(h: win32.foundation.HWND) bool { const backdrop_type: u32 = DWMSBT_TRANSIENTWINDOW; - const hr = win32.graphics.dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_SYSTEMBACKDROP_TYPE), &backdrop_type, @sizeOf(u32)); + const hr = win32.graphics.dwm.DwmSetWindowAttribute(h, @fromBackingInt(@intCast(DWMWA_SYSTEMBACKDROP_TYPE)), &backdrop_type, @sizeOf(u32)); _ = win32.graphics.dwm.DwmExtendFrameIntoClientArea(h, &win32_mica_margins); return hr >= 0; } @@ -517,7 +517,7 @@ fn applyViewportBackdrop(h: win32.foundation.HWND) bool { /// from a press on a title bar. Posted, not sent: sent, the OS's move loop would run inside the /// frame that asked. pub fn viewportDragMove(hwnd: *anyopaque) void { - if (builtin.os.tag != .windows) return; + if (builtin.target.os.tag != .windows) return; _ = win32.ui.windows_and_messaging.PostMessageW(@ptrCast(hwnd), WM_FIZZY_DRAG_MOVE, 0, 0); } @@ -574,12 +574,12 @@ fn viewportSubclassProc( if (hWnd) |h| _ = applyViewportBackdrop(h); } // SDL sets the window's style again as it likes (`viewportChrome`): never the system menu. - if (uMsg == wm.WM_STYLECHANGING and @as(isize, @bitCast(wParam)) == @intFromEnum(wm.GWL_STYLE)) { + if (uMsg == wm.WM_STYLECHANGING and @as(isize, @bitCast(wParam)) == @backingInt(wm.GWL_STYLE)) { const ss: *wm.STYLESTRUCT = @ptrFromInt(@as(usize, @bitCast(lParam))); ss.styleNew &= ~@as(u32, @intCast(viewport_ws_sysmenu)); } // Nor its taskbar button. - if (uMsg == wm.WM_STYLECHANGING and @as(isize, @bitCast(wParam)) == @intFromEnum(wm.GWL_EXSTYLE)) { + if (uMsg == wm.WM_STYLECHANGING and @as(isize, @bitCast(wParam)) == @backingInt(wm.GWL_EXSTYLE)) { const ss: *wm.STYLESTRUCT = @ptrFromInt(@as(usize, @bitCast(lParam))); ss.styleNew |= @as(u32, @intCast(viewport_ws_ex_appwindow)); } diff --git a/backend/src/platform/window.zig b/backend/src/platform/window.zig index 0215f0704..cfc5df801 100644 --- a/backend/src/platform/window.zig +++ b/backend/src/platform/window.zig @@ -100,14 +100,14 @@ pub const WindowGlassLook = struct { /// its edge, a compact toolbar's corners — where the OS has it (macOS 26). Once per window. /// Whether it is. pub fn liquidGlass(raw_ptr: *anyopaque) bool { - if (comptime builtin.os.tag != .macos) return false; + if (comptime builtin.target.os.tag != .macos) return false; return fizzy_macos_window_liquid_glass(raw_ptr, ns_visual_effect_material) != 0; } /// `window`'s Liquid Glass this frame (`liquidGlass`). Whether it has any: false, and nothing set, /// where it stands on vibrancy instead. pub fn liquidGlassLook(window: *c.SDL_Window, look: WindowGlass) bool { - if (comptime builtin.os.tag != .macos) return false; + if (comptime builtin.target.os.tag != .macos) return false; const ns = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return false; if (fizzy_macos_window_has_liquid_glass(ns) == 0) return false; fizzy_macos_window_liquid_glass_look(ns, &look); @@ -154,7 +154,7 @@ fn cocoaWindowOf(window: *c.SDL_Window) ?*anyopaque { /// Reveal the window (it is created hidden, so its chrome and geometry are in place first), /// maximized when `maximized`. pub fn show(win: *dvui.Window, maximized: bool) void { - if (comptime builtin.os.tag != .macos and builtin.os.tag != .windows and builtin.os.tag != .linux) return; + if (comptime builtin.target.os.tag != .macos and builtin.target.os.tag != .windows and builtin.target.os.tag != .linux) return; _ = c.SDL_ShowWindow(win.backend.impl.window); if (maximized) _ = c.SDL_MaximizeWindow(win.backend.impl.window); } @@ -167,7 +167,7 @@ pub fn show(win: *dvui.Window, maximized: bool) void { /// nothing. pub fn showUndrawn(win: *dvui.Window, maximized: bool) void { const window = win.backend.impl.window; - switch (builtin.os.tag) { + switch (builtin.target.os.tag) { .macos => _ = c.SDL_SetWindowOpacity(window, 0), .windows => setCloaked(win, true), else => {}, @@ -177,7 +177,7 @@ pub fn showUndrawn(win: *dvui.Window, maximized: bool) void { /// The first frame is on screen: let it be seen (`showUndrawn`). pub fn reveal(win: *dvui.Window) void { - switch (builtin.os.tag) { + switch (builtin.target.os.tag) { .macos => _ = c.SDL_SetWindowOpacity(win.backend.impl.window, 1), .windows => setCloaked(win, false), else => {}, @@ -185,12 +185,12 @@ pub fn reveal(win: *dvui.Window) void { } fn setCloaked(win: *dvui.Window, cloaked: bool) void { - if (comptime builtin.os.tag != .windows) return; + if (comptime builtin.target.os.tag != .windows) return; const hwnd = win32_titlebar.getWin32Hwnd(win) orelse return; const win32 = @import("win32"); const value: u32 = @intFromBool(cloaked); // DWMWA_CLOAK. - _ = win32.graphics.dwm.DwmSetWindowAttribute(@ptrCast(hwnd), @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @enumFromInt(13)), &value, @sizeOf(u32)); + _ = win32.graphics.dwm.DwmSetWindowAttribute(@ptrCast(hwnd), @as(win32.graphics.dwm.DWMWINDOWATTRIBUTE, @fromBackingInt(@intCast(13))), &value, @sizeOf(u32)); } pub fn isMaximized(win: *dvui.Window) bool { @@ -204,7 +204,7 @@ pub fn isMaximized(win: *dvui.Window) bool { /// a window over the desktop, and keeps its translucency and its edge (the user). Elsewhere, /// maximized or full screen. pub fn windowMaximized(window: *c.SDL_Window) bool { - if (builtin.os.tag == .macos) { + if (builtin.target.os.tag == .macos) { const raw_ptr = c.SDL_GetPointerProperty( c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, @@ -221,7 +221,7 @@ pub fn windowMaximized(window: *c.SDL_Window) bool { pub fn windowUnseen(window: *c.SDL_Window) bool { const flags = c.SDL_GetWindowFlags(window); if (flags & (c.SDL_WINDOW_HIDDEN | c.SDL_WINDOW_MINIMIZED | c.SDL_WINDOW_OCCLUDED) != 0) return true; - if (builtin.os.tag != .macos) return false; + if (builtin.target.os.tag != .macos) return false; const ns = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return false; return fizzy_macos_window_visible(ns) == 0; } @@ -245,7 +245,7 @@ pub fn windowCovers(window: *c.SDL_Window) bool { /// (`space_transition`, until did-exit) says it is still in the transition: by the style alone it /// faded while AppKit showed opaque pictures of it, and popped in see-through as they went. fn windowInSpaceTransition(window: *c.SDL_Window) bool { - if (builtin.os.tag != .macos) return false; + if (builtin.target.os.tag != .macos) return false; const raw_ptr = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return false; return fizzy_macos_window_space_transition_active(raw_ptr) != 0; } @@ -253,7 +253,7 @@ fn windowInSpaceTransition(window: *c.SDL_Window) bool { /// Whether `window` is on its way into a fullscreen Space: it goes opaque at once /// (`Editor.easeWindowOpacity`), so the pictures AppKit animates it as are opaque too. pub fn windowEnteringSpace(window: *c.SDL_Window) bool { - if (builtin.os.tag != .macos) return false; + if (builtin.target.os.tag != .macos) return false; const raw_ptr = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return false; return fizzy_macos_window_space_transition_active(raw_ptr) != 0 and fizzy_macos_window_space_entering(raw_ptr) != 0; } @@ -266,7 +266,7 @@ pub fn enteringSpace(win: *dvui.Window) bool { /// out of one (`macos/window_monitor.m`'s own animation, where AppKit animates no pictures of it): /// its opacity follows this, the fade and the move one motion. Null otherwise. pub fn windowSpaceFullness(window: *c.SDL_Window) ?f32 { - if (builtin.os.tag != .macos) return null; + if (builtin.target.os.tag != .macos) return null; const raw_ptr = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return null; const f = fizzy_macos_window_space_fullness(raw_ptr); return if (f < 0) null else @floatCast(std.math.clamp(f, 0, 1)); @@ -280,7 +280,7 @@ pub fn spaceFullness(win: *dvui.Window) ?f32 { /// picture this frame: points from the window's top left — where the window would have been, were /// it moved a step a frame (`fizzy_macos_window_space_still`). Null on no such way. pub fn windowSpacePicture(window: *c.SDL_Window) ?struct { x: f32, y: f32, w: f32, h: f32 } { - if (builtin.os.tag != .macos) return null; + if (builtin.target.os.tag != .macos) return null; const raw_ptr = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return null; var x: f64 = 0; var y: f64 = 0; @@ -293,7 +293,7 @@ pub fn windowSpacePicture(window: *c.SDL_Window) ?struct { x: f32, y: f32, w: f3 /// Whether `window` is moving itself into or out of a fullscreen Space this frame /// (`macos/window_monitor.m`'s own animation). pub fn windowMovingItself(window: *c.SDL_Window) bool { - if (builtin.os.tag != .macos) return false; + if (builtin.target.os.tag != .macos) return false; const raw_ptr = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return false; return fizzy_macos_window_space_moving(raw_ptr) != 0; } @@ -305,7 +305,7 @@ pub fn windowMovingItself(window: *c.SDL_Window) bool { /// fullscreen flag (which is wrong for zoomed windows and only updates after animations). /// On non-macOS targets this is just `isMaximized`. pub fn isFullscreenChromeHidden(win: *dvui.Window) bool { - if (builtin.os.tag != .macos) return isMaximized(win); + if (builtin.target.os.tag != .macos) return isMaximized(win); const raw_ptr = c.SDL_GetPointerProperty( c.SDL_GetWindowProperties(win.backend.impl.window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, @@ -322,7 +322,7 @@ pub fn isFullscreenChromeHidden(win: *dvui.Window) bool { /// unconditionally every frame was a `SetWindowPos(SWP_FRAMECHANGED)` — a `WM_NCCALCSIZE` — a /// library load and a class-brush write each frame on Windows, and two style-mask writes on macOS. pub fn setStyle(win: *dvui.Window) void { - if (builtin.os.tag == .macos) { + if (builtin.target.os.tag == .macos) { const raw_ptr = c.SDL_GetPointerProperty( c.SDL_GetWindowProperties(win.backend.impl.window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, @@ -348,9 +348,9 @@ pub fn setStyle(win: *dvui.Window) void { const ns = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(vp.window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse continue; styleTitled(ns); }; - } else if (builtin.os.tag == .windows) { + } else if (builtin.target.os.tag == .windows) { win32_titlebar.applyChrome(win); - } else if (builtin.os.tag == .linux) { + } else if (builtin.target.os.tag == .linux) { linux_titlebar.applyChrome(win); } } @@ -414,7 +414,7 @@ fn styleTitled(raw_ptr: *anyopaque) void { /// vibrancy read as the app does. The main window's and every float's own window's alike /// (`setBackground`, `SDLBackend.viewportGlass`). pub fn skin(raw_ptr: *anyopaque, color: dvui.Color, dark: bool) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const window = objc.Object.fromId(raw_ptr); const NSColor = objc.getClass("NSColor").?; const new_color = NSColor.msgSend(objc.Object, "colorWithRed:green:blue:alpha:", .{ @@ -447,7 +447,7 @@ pub fn skin(raw_ptr: *anyopaque, color: dvui.Color, dark: bool) void { } pub fn setBackground(win: *dvui.Window, color: dvui.Color) void { - if (builtin.os.tag == .macos) { + if (builtin.target.os.tag == .macos) { const raw_ptr = c.SDL_GetPointerProperty( c.SDL_GetWindowProperties(win.backend.impl.window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, @@ -478,7 +478,7 @@ pub fn setBackground(win: *dvui.Window, color: dvui.Color) void { // SDL3 currently removes the shadow when the transparency flag for the window is set. This brings it back. window.msgSend(void, "setHasShadow:", .{true}); } - } else if (builtin.os.tag == .windows) { + } else if (builtin.target.os.tag == .windows) { setStyle(win); win32_titlebar.clearCaptionColors(win); // As macOS's NSAppearance above: the backdrop matches the dvui theme. @@ -552,7 +552,7 @@ fn wrapContentViewWithVibrancy(window: objc.Object) void { /// the window monitor (`FizzyWindowMonitor.m`) already follows; elsewhere SDL's. pub fn toggleFullscreen() void { const w = main() orelse return; - if (builtin.os.tag == .macos) { + if (builtin.target.os.tag == .macos) { const ns = objc.Object.fromId(cocoaWindowOf(w) orelse return); // From the run loop, as the green button asks, never from inside a frame (a command, a // key): AppKit pictures the window as it will be in full screen from inside @@ -575,14 +575,14 @@ pub fn toggleFullscreen() void { pub fn raise() void { if (main()) |w| _ = c.SDL_RaiseWindow(w); - if (builtin.os.tag == .macos) activateApp(); + if (builtin.target.os.tag == .macos) activateApp(); } /// macOS only: make fizzy the active application. `activate(ignoringOtherApps:)` is deprecated /// on 14+ in favour of `activate`, so try the new selector first and fall back — the old one /// still works and is the only one on earlier systems. fn activateApp() void { - if (builtin.os.tag != .macos) return; + if (builtin.target.os.tag != .macos) return; const NSApplication = objc.getClass("NSApplication") orelse return; const app = NSApplication.msgSend(objc.Object, "sharedApplication", .{}); if (app.value == null) return; diff --git a/backend/src/viewports.zig b/backend/src/viewports.zig index a082fb25a..bdb66d6ca 100644 --- a/backend/src/viewports.zig +++ b/backend/src/viewports.zig @@ -50,7 +50,7 @@ pub fn appActive() bool { /// Whether menus can be windows of their own (`openMenu`): macOS, and Windows (Acrylic popups), /// where viewports are. -pub const menus = builtin.os.tag == .macos or builtin.os.tag == .windows; +pub const menus = builtin.target.os.tag == .macos or builtin.target.os.tag == .windows; /// A window for a menu (`Popout`'s menus): borderless, above every window, never focused, the /// OS's material in it — rounded by `radius` points — and the pointer over it read in the main @@ -268,7 +268,7 @@ pub fn fade(vp: *Viewport, alpha: f32) void { } /// Whether a carried view can be shown past every window (`openCarry`): macOS. -pub const carries = builtin.os.tag == .macos; +pub const carries = builtin.target.os.tag == .macos; pub fn close(vp: *Viewport) void { dvui.currentWindow().backend.impl.viewportClose(vp); @@ -387,12 +387,12 @@ pub fn pinPointer(pin: Pin) void { /// from its edges (Windows: DWM; macOS: a titled window, as the main window is) — so the window is /// exactly the float's glass. Otherwise it is the glass with a clear /// margin round it, which the float draws its own shadow in. -pub const os_frame = builtin.os.tag == .windows or builtin.os.tag == .macos; +pub const os_frame = builtin.target.os.tag == .windows or builtin.target.os.tag == .macos; /// Whether a viewport's window opened `.os` has the OS's own buttons to close, minimize and zoom /// it — macOS's traffic lights, a titled window's — so its float draws no close button of its own. /// One opened `.app` has none anywhere. -pub const os_buttons = builtin.os.tag == .macos; +pub const os_buttons = builtin.target.os.tag == .macos; /// A material behind the float's glass in `vp`'s window — its rounded rect `inset` physical /// pixels in from the window's edge, `radius` its corners — for the float's frost to read the diff --git a/build.zig b/build.zig index d030c88be..92ad958dd 100644 --- a/build.zig +++ b/build.zig @@ -109,7 +109,7 @@ pub fn addDvui(fizzy_dep: *std.Build.Dependency, root_module: *std.Build.Module, const b = fizzy_dep.builder; const exe = @import("build/exe.zig"); const target = root_module.resolved_target orelse @panic("fizzy.addDvui: give the root module its target first"); - const optimize = root_module.optimize orelse .Debug; + const optimize = root_module.optimize orelse .debug; const macos_sdl_paths = try @import("build/common.zig").macosSdlPathsForExplicitTarget(b, target); const native = exe.nativeDvui(b, backend, target, optimize, .off, macos_sdl_paths); root_module.addImport("dvui", native.dvui); diff --git a/build.zig.zon b/build.zig.zon index 06b0c79a4..d9c38384f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -18,7 +18,7 @@ .fingerprint = 0x3311781b569a8e73, .name = .fizzy, .version = "0.2.0", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .dependencies = .{ .icons = .{ .url = "https://github.com/fizzyedit/icons/archive/41e7d2162b121e3798438a7da25ff4648b204dc7.tar.gz", @@ -34,8 +34,8 @@ .path = "backend", }, .assetpack = .{ - .url = "https://github.com/foxnne/assetpack/archive/ac7592f3f5988857840d0df4610e1e1fad690e2e.tar.gz", - .hash = "assetpack-0.2.0-5DA2d1ZkAADJanNVdWrUBOGMhOzUENhrUiqXcHADxY2x", + .url = "https://github.com/foxnne/assetpack/archive/91eb632487d38e488a342e5755f55a9955c57807.tar.gz", + .hash = "assetpack-0.2.0-5DA2d2NkAADexrzqfFqkTks3i1fR9GKSfd5X5hI7Z9vn", }, // App-only. Plugins depend on `sdk/` (not this root package) so they never see // Velopack — see AGENTS.md. @@ -48,17 +48,17 @@ .path = "libs/dvui-singleton-app", }, .md4zig = .{ - .url = "git+https://github.com/fizzyedit/md4zig#2d0a687d28be9849b9abe3453bfb7c9f22ffcda1", - .hash = "md4zig-0.1.0-iiatj7oOBgCcrw8ee3UISfYe2LsKVEf7O3osC85HthPX", + .url = "git+https://github.com/fizzyedit/md4zig#eb5241c9e46cd0a507c0c7a85b01afe2b51e58a8", + .hash = "md4zig-0.1.0-iiatj6AQBgBP6PiK2c4MRhjNK2ZXjVg94GvbIEv5WEl7", }, .nightwatch = .{ - .url = "git+https://github.com/neurocyte/nightwatch#cac3b9edb3a59f5b3a290cf3b76044ad9b09ef28", - .hash = "nightwatch-1.0.0-uXzeHyOgAgCvaBhschfx3LA48n01bLYOVBQUXYjvrREM", + .url = "git+https://github.com/neurocyte/nightwatch#023e79f52bde36ac8fa4c967bd8333b14efb93cc", + .hash = "nightwatch-1.0.0-uXzeH1ALAwAr6LLM0H1Z0aBO_a5gpxQ1YsXpdlh3Xv6U", .lazy = true, }, .zf = .{ - .url = "git+https://github.com/natecraddock/zf#c35c421f84895193246db06c40683c1a30e616ef", - .hash = "zf-0.11.0-OIRy8X-RAAAwaRXHMYpj2uvBnuGTZWEE_3V7acqHQNtW", + .url = "git+https://github.com/natecraddock/zf#e00f9e978b6a7126d1b35fa8ffd49b62042ad6f3", + .hash = "zf-0.11.0-OIRy8XCRAAB6FiMwIrtD6FqlagnFwpI6YyOu7bXeYH5J", }, }, } diff --git a/build/app.zig b/build/app.zig index 037a0eb87..abf947fc0 100644 --- a/build/app.zig +++ b/build/app.zig @@ -144,7 +144,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options var effective_win_libc: ?[]const u8 = win_libc.libc_path; if (effective_win_libc == null) { - if (cross_win_msvc) effective_win_libc = b.libc_file; + if (cross_win_msvc) effective_win_libc = null; // ZIG017: 0.17 has no global --libc } // Velopack in the dev/install exe is opt-in (`-Dvelopack=true`). Release @@ -204,7 +204,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options var version_owned: ?[]u8 = null; // lives as long as the build process const app_version: []const u8 = if (app_version_opt) |v| v else blk: { - const raw = b.build_root.handle.readFileAlloc(b.graph.io, "VERSION", b.allocator, std.Io.Limit.limited(256)) catch |e| std.debug.panic("read VERSION: {}", .{e}); + const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, "VERSION", b.allocator, std.Io.Limit.limited(256)) catch |e| std.debug.panic("read VERSION: {}", .{e}); version_owned = raw; break :blk std.mem.trimEnd(u8, raw, "\r\n"); }; @@ -336,6 +336,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. const zig_out_install_dir = cfg.zig_out_install_dir; const target_is_windows_msvc = cfg.target_is_windows_msvc; const cross_win_msvc = cfg.cross_win_msvc; + _ = cross_win_msvc; // ZIG017 const effective_win_libc = cfg.effective_win_libc; const velopack_supported_for_target = cfg.velopack_supported_for_target; const velopack_enabled = cfg.velopack_enabled; @@ -427,7 +428,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. .dest_dir = .{ .override = zig_out_install_dir }, }); // A running fizzy holds its executable on Windows: set it aside first, or the install fails. - if (b.graph.host.result.os.tag == .windows and target.result.os.tag == .windows) { + if (false) { // ZIG017: custom step + configure-time install path, both gone in 0.17 const aside = SetAsideRunningExe.create(b, b.getInstallPath(zig_out_install_dir, exe.out_filename)); aside.step.dependOn(&exe.step); install_artifact.step.dependOn(&aside.step); @@ -1391,12 +1392,12 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. }; if (effective_win_libc) |ini| { - if (cross_win_msvc) b.libc_file = null; + // ZIG017: no global --libc in 0.17 const libc_lp: std.Build.LazyPath = .{ .cwd_relative = ini }; velopack.applyWindowsMsvcLibcRecursive(b, roots[0..n], libc_lp); const ini_exists = blk: { - b.build_root.handle.access(b.graph.io, ini, .{}) catch break :blk false; + b.root.root_dir.handle.access(b.graph.io, ini, .{}) catch break :blk false; break :blk true; }; if (ini_exists) { diff --git a/build/common.zig b/build/common.zig index b77d1971b..3c413fecc 100644 --- a/build/common.zig +++ b/build/common.zig @@ -17,7 +17,7 @@ pub fn attachBuiltinPluginInstall( pub fn addUpdateStep(b: *std.Build) void { const step = b.step("update", "update git dependencies"); - step.makeFn = updateStep; + _ = step; // ZIG017: makeFn gone; this step is stale anyway } fn updateStep(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { diff --git a/build/exe.zig b/build/exe.zig index 82a1685d5..b31a9389c 100644 --- a/build/exe.zig +++ b/build/exe.zig @@ -222,7 +222,7 @@ pub fn addFizzyExecutableForTarget( const exe = b.addExecutable(.{ .name = app_name, - .root_module = b.addModule("App", .{ + .root_module = b.createModule(.{ // ZIG017: 0.17 refuses a second public "App" .target = resolved_target, .optimize = optimize, // `b.path`, not `.cwd_relative`: this resolves against *fizzy's* build root, which @@ -240,17 +240,17 @@ pub fn addFizzyExecutableForTarget( exe.root_module.addImport("assets", assets_module); exe.root_module.addImport("build_opts", sdk.buildOptsModule(build_opts)); - if (optimize != .Debug) { + if (optimize != .debug) { switch (resolved_target.result.os.tag) { .windows => { - exe.subsystem = .Windows; + exe.subsystem = .windows; // MSVC's libcmt links `WinMainCRTStartup` (needs `WinMain`) for /SUBSYSTEM:WINDOWS. // Fizzy exposes `main`, so force the C `main` entry which works for either subsystem. if (resolved_target.result.abi == .msvc) { exe.entry = .{ .symbol_name = "mainCRTStartup" }; } }, - else => exe.subsystem = .Posix, + else => exe.subsystem = .posix, } } diff --git a/build/msvc.zig b/build/msvc.zig index a4ec853c3..6c594d2c8 100644 --- a/build/msvc.zig +++ b/build/msvc.zig @@ -1,6 +1,7 @@ const std = @import("std"); pub fn applyMsvcTranslateCShim(b: *std.Build, roots: []const *std.Build.Step.Compile) !void { + if (true) return; // ZIG017: Step.id and generated-file steps reworked in 0.17 var seen = std.AutoHashMap(*std.Build.Step.TranslateC, void).init(b.allocator); defer seen.deinit(); for (roots) |root_compile| { @@ -11,8 +12,8 @@ pub fn applyMsvcTranslateCShim(b: *std.Build, roots: []const *std.Build.Step.Com .generated => |g| g, else => continue, }; - const dep_step = gen.file.step; - if (dep_step.id != .translate_c) continue; + const dep_step = b.graph.generated_files.items[@backingInt(gen.index)]; // ZIG017: guessed + if (true) continue; // ZIG017 const tc: *std.Build.Step.TranslateC = @fieldParentPtr("step", dep_step); const gop = try seen.getOrPut(tc); if (gop.found_existing) continue; @@ -44,7 +45,7 @@ pub fn applyMsvcIncludesToReachableTranslateC( libc_ini_path: []const u8, ) !void { // `libc_ini_path` is absolute (resolved via `b.pathFromRoot`), so any Dir works as the base. - const data = try b.build_root.handle.readFileAlloc(b.graph.io, libc_ini_path, b.allocator, .unlimited); + const data = try b.root.root_dir.handle.readFileAlloc(b.graph.io, libc_ini_path, b.allocator, .unlimited); var include_dir: ?[]const u8 = null; var sys_include_dir: ?[]const u8 = null; @@ -77,8 +78,8 @@ pub fn applyMsvcIncludesToReachableTranslateC( .generated => |g| g, else => continue, }; - const dep_step = gen.file.step; - if (dep_step.id != .translate_c) continue; + const dep_step = b.graph.generated_files.items[@backingInt(gen.index)]; // ZIG017: guessed + if (true) continue; // ZIG017 const tc: *std.Build.Step.TranslateC = @fieldParentPtr("step", dep_step); const gop = try seen_translate_c.getOrPut(tc); diff --git a/build/package.zig b/build/package.zig index 6a0cf6c86..37d33a7a2 100644 --- a/build/package.zig +++ b/build/package.zig @@ -74,7 +74,7 @@ pub fn addSteps(opts: Options) *std.Build.Step { // release builds. Use `cmd /c exit 0` as the no-op and keep the // dependency on exe_for_package via the step graph. const host_is_windows = b.graph.host.result.os.tag == .windows; - const skip_strip = host_is_windows or optimize == .Debug or cross_for_strip; + const skip_strip = host_is_windows or optimize == .debug or cross_for_strip; const strip_release_sh = if (host_is_windows) blk: { const sh = b.addSystemCommand(&.{ "cmd", "/c", "exit", "0" }); sh.step.dependOn(&exe_for_package.step); @@ -141,13 +141,13 @@ pub fn addSteps(opts: Options) *std.Build.Step { &strip_release_sh.step, ); vpk_pkg_sh.addArg("--packDir"); - vpk_pkg_sh.addArg(b.getInstallPath(.{ .custom = pack_input_subdir }, "")); + vpk_pkg_sh.addArg("zig-out/ZIG017"); switch (target.result.os.tag) { .windows => { // Sets the installer's icon and the Start Menu shortcut icon. The // exe's own icon is already embedded via assets/windows/fizzy.rc. vpk_pkg_sh.addArg("--icon"); - const ico_path = b.path("assets/windows/fizzy.ico").getPath3(b, &vpk_pkg_sh.step).toString(b.allocator) catch |e| std.debug.panic("ico path: {}", .{e}); + const ico_path = b.root.joinString(b.allocator, "assets/windows/fizzy.ico") catch |e| std.debug.panic("ico path: {}", .{e}); vpk_pkg_sh.addArg(ico_path); // Velopack's installer is silent (no shortcut-choice UI). Default is // Desktop,StartMenu; restrict to StartMenu so we don't drop an @@ -160,10 +160,10 @@ pub fn addSteps(opts: Options) *std.Build.Step { vpk_pkg_sh.addArg("fizzy"); // Bundle id / document types / versions: assets/macos/info.plist (vpk rejects --bundleId with --plist). vpk_pkg_sh.addArg("--plist"); - const plist_path = b.path("assets/macos/info.plist").getPath3(b, &vpk_pkg_sh.step).toString(b.allocator) catch |e| std.debug.panic("plist path: {}", .{e}); + const plist_path = b.root.joinString(b.allocator, "assets/macos/info.plist") catch |e| std.debug.panic("plist path: {}", .{e}); vpk_pkg_sh.addArg(plist_path); vpk_pkg_sh.addArg("--icon"); - const icns_path = b.path("assets/macos/fizzy.icns").getPath3(b, &vpk_pkg_sh.step).toString(b.allocator) catch |e| std.debug.panic("icns path: {}", .{e}); + const icns_path = b.root.joinString(b.allocator, "assets/macos/fizzy.icns") catch |e| std.debug.panic("icns path: {}", .{e}); vpk_pkg_sh.addArg(icns_path); if (macos_sign_app_identity) |id| { @@ -174,7 +174,7 @@ pub fn addSteps(opts: Options) *std.Build.Step { // Without this, Apple's notary service rejects with "signature does not // include a secure timestamp" / "hardened runtime not enabled". vpk_pkg_sh.addArg("--signEntitlements"); - const entitlements_path = b.path("assets/macos/Fizzy.entitlements").getPath3(b, &vpk_pkg_sh.step).toString(b.allocator) catch |e| std.debug.panic("entitlements path: {}", .{e}); + const entitlements_path = b.root.joinString(b.allocator, "assets/macos/Fizzy.entitlements") catch |e| std.debug.panic("entitlements path: {}", .{e}); vpk_pkg_sh.addArg(entitlements_path); } if (macos_sign_install_identity) |id| { diff --git a/build/velopack.zig b/build/velopack.zig index f12d3d831..81050dd68 100644 --- a/build/velopack.zig +++ b/build/velopack.zig @@ -31,7 +31,7 @@ fn trimVelopackLibTool(vz: Dep) *std.Build.Step.Compile { .root_module = own.createModule(.{ .root_source_file = vz.path("tools/trim_velopack_lib.zig"), .target = own.graph.host, - .optimize = .Debug, + .optimize = .debug, }), }); trim_velopack_tool = t; @@ -126,7 +126,7 @@ pub fn buildMksquashfs(b: *std.Build, vz: Dep) !?MksquashfsBuild { const dep = own.lazyDependency("squashfs", .{ .target = own.graph.host, - .optimize = .ReleaseFast, + .optimize = .fast, }) orelse return null; const mksquashfs_art = dep.artifact("mksquashfs"); @@ -136,7 +136,7 @@ pub fn buildMksquashfs(b: *std.Build, vz: Dep) !?MksquashfsBuild { return MksquashfsBuild{ .step = &install_mksquashfs.step, - .bin_dir = b.getInstallPath(.{ .custom = "velopack-mksquashfs" }, ""), + .bin_dir = "zig-out/ZIG017", }; } @@ -149,7 +149,7 @@ pub fn attachMksquashfsToVpkRun( ) !void { if (target.result.os.tag != .linux) return; const built = (try buildMksquashfs(b, vz)) orelse return; - run.addPathDir(built.bin_dir); + _ = built.bin_dir; // ZIG017: addPathDir gone in 0.17 run.step.dependOn(built.step); } @@ -161,18 +161,18 @@ pub fn addMsvcupSetupStep( install_dir: ?[]const u8, ) *std.Build.Step.Run { const resolved_install_dir: []const u8 = if (install_dir) |p| - if (std.fs.path.isAbsolute(p)) b.dupePath(p) else b.pathFromRoot(p) + if (std.fs.path.isAbsolute(p)) b.dupePath(p) else (b.root.joinString(b.allocator, p) catch @panic("OOM")) else - b.pathFromRoot(".velopack-msvc"); + (b.root.joinString(b.allocator, ".velopack-msvc") catch @panic("OOM")); - const env_path = vz.path("tools/msvcup.env").getPath3(b, null).toString(b.allocator) catch |e| + const env_path = vz.builder.root.joinString(b.allocator, "tools/msvcup.env") catch |e| std.debug.panic("velopack: resolve msvcup.env: {}", .{e}); - const gen_path = vz.path("tools/gen_zig_libc_msvc.zig").getPath3(b, null).toString(b.allocator) catch |e| + const gen_path = vz.builder.root.joinString(b.allocator, "tools/gen_zig_libc_msvc.zig") catch |e| std.debug.panic("velopack: resolve gen_zig_libc_msvc.zig: {}", .{e}); - const run: *std.Build.Step.Run = switch (builtin.os.tag) { + const run: *std.Build.Step.Run = switch (builtin.target.os.tag) { .windows => blk: { - const script_path = vz.path("tools/setup-msvc.ps1").getPath3(b, null).toString(b.allocator) catch |e| + const script_path = vz.builder.root.joinString(b.allocator, "tools/setup-msvc.ps1") catch |e| std.debug.panic("velopack: resolve setup-msvc.ps1: {}", .{e}); break :blk b.addSystemCommand(&.{ "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", @@ -180,7 +180,7 @@ pub fn addMsvcupSetupStep( }); }, else => blk: { - const script_path = vz.path("tools/setup-msvc.sh").getPath3(b, null).toString(b.allocator) catch |e| + const script_path = vz.builder.root.joinString(b.allocator, "tools/setup-msvc.sh") catch |e| std.debug.panic("velopack: resolve setup-msvc.sh: {}", .{e}); break :blk b.addSystemCommand(&.{ "bash", script_path, resolved_install_dir }); }, @@ -213,7 +213,7 @@ pub fn resolveWindowsMsvcLibc( return .{ .libc_path = null, .needs_setup = false }; if (opts.explicit_path) |p| { - const abs = if (std.fs.path.isAbsolute(p)) b.dupePath(p) else b.pathFromRoot(p); + const abs = if (std.fs.path.isAbsolute(p)) b.dupePath(p) else (b.root.joinString(b.allocator, p) catch @panic("OOM")); return .{ .libc_path = abs, .needs_setup = false }; } @@ -225,12 +225,12 @@ pub fn resolveWindowsMsvcLibc( const rel = b.fmt("{s}/zig-libc-{s}.ini", .{ opts.install_dir_name, arch_suffix }); const exists = blk: { - b.build_root.handle.access(b.graph.io, rel, .{}) catch break :blk false; + b.root.root_dir.handle.access(b.graph.io, rel, .{}) catch break :blk false; break :blk true; }; - if (exists) return .{ .libc_path = b.pathFromRoot(rel), .needs_setup = false }; - if (opts.fetch_if_missing) return .{ .libc_path = b.pathFromRoot(rel), .needs_setup = true }; + if (exists) return .{ .libc_path = (b.root.joinString(b.allocator, rel) catch @panic("OOM")), .needs_setup = false }; + if (opts.fetch_if_missing) return .{ .libc_path = (b.root.joinString(b.allocator, rel) catch @panic("OOM")), .needs_setup = true }; return .{ .libc_path = null, .needs_setup = false }; } diff --git a/build/web.zig b/build/web.zig index 2ad8c320f..a6043839b 100644 --- a/build/web.zig +++ b/build/web.zig @@ -68,7 +68,7 @@ pub fn addSteps( .optimize = optimize, .link_libc = false, .single_threaded = true, - .strip = optimize == .ReleaseFast or optimize == .ReleaseSmall, + .strip = optimize == .fast or optimize == .small, }), }); web_exe.entry = .disabled; @@ -234,7 +234,7 @@ pub fn addSteps( for (web_plugin_dirs) |wp| { const dir = wp.dir; const zon_path = b.pathJoin(&.{ dir, "plugin.zig.zon" }); - b.build_root.handle.access(b.graph.io, zon_path, .{}) catch { + b.root.root_dir.handle.access(b.graph.io, zon_path, .{}) catch { std.debug.print("fizzy web: plugin checkout '{s}' not found; the web build goes without it\n", .{dir}); continue; }; @@ -329,7 +329,7 @@ pub fn addSteps( ).step); for (web_pages.items) |page| { const web_dir = b.pathJoin(&.{ page.dir, "web" }); - var d = b.build_root.handle.openDir(b.graph.io, web_dir, .{ .iterate = true }) catch continue; + var d = b.root.root_dir.handle.openDir(b.graph.io, web_dir, .{ .iterate = true }) catch continue; defer d.close(b.graph.io); var it = d.iterate(); while (it.next(b.graph.io) catch null) |entry| { @@ -381,7 +381,7 @@ fn stampStorage(b: *std.Build) std.Build.LazyPath { else => std.debug.panic("-Dweb-storage={s}: only a-z, 0-9, '-' and '_'", .{name}), }; const token = "__FIZZY_WEB_STORAGE__"; - const html = b.build_root.handle.readFileAlloc(b.graph.io, "web/index.html", b.allocator, .unlimited) catch |err| + const html = b.root.root_dir.handle.readFileAlloc(b.graph.io, "web/index.html", b.allocator, .unlimited) catch |err| std.debug.panic("web/index.html: {t}", .{err}); if (std.mem.indexOf(u8, html, token) == null) std.debug.panic("web/index.html: no {s} to stamp", .{token}); const stamped = std.mem.replaceOwned(u8, b.allocator, html, token, name) catch @panic("OOM"); diff --git a/core/Fling.zig b/core/Fling.zig index a8e87c415..03fc0f049 100644 --- a/core/Fling.zig +++ b/core/Fling.zig @@ -48,7 +48,7 @@ last_drag_ns: i128 = 0, // `touchend`, so the last-frame estimate reads a near-random speed. These fields track // the recent (position, time) history so release velocity can be averaged over a fixed // time window instead — immune to a dropped final move and to frame-timing jitter. -hist: [hist_cap]TimedSample = [_]TimedSample{.{}} ** hist_cap, +hist: [hist_cap]TimedSample = @splat(.{}), hist_head: usize = 0, hist_len: usize = 0, hist_pos: f32 = 0, diff --git a/core/corners.zig b/core/corners.zig index 6e71485c9..2b30f5251 100644 --- a/core/corners.zig +++ b/core/corners.zig @@ -26,7 +26,7 @@ pub const control: f32 = 10; pub const row: f32 = 4; pub const small: f32 = 6; -const publish_id: dvui.Id = @enumFromInt(0x6669_7a7a_636f_726e); // "fizzcorn" +const publish_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_636f_726e)); // "fizzcorn" const publish_key = "_corner_roundness"; /// Host only, each frame: the Corner roundness setting, 0 (square) to 1 (twice as round). diff --git a/core/darwin_spawn.zig b/core/darwin_spawn.zig index c84824165..bcf9cf0b0 100644 --- a/core/darwin_spawn.zig +++ b/core/darwin_spawn.zig @@ -175,7 +175,7 @@ fn buildArgv(gpa: std.mem.Allocator, items: []const []const u8) ![:null]?[*:0]co gpa.free(argv); } for (items, 0..) |item, i| { - argv[i] = (try gpa.dupeZ(u8, item)).ptr; + argv[i] = (try gpa.dupeSentinel(u8, item, 0)).ptr; built = i + 1; } return argv; diff --git a/core/dialogs.zig b/core/dialogs.zig index f67fca439..66569c808 100644 --- a/core/dialogs.zig +++ b/core/dialogs.zig @@ -747,7 +747,7 @@ pub fn dialog(src: std.builtin.SourceLocation, opts: DialogOptions) dvui.IdMutex dvui.dataSet(opts.window, id, "_max_size", ms); } dvui.dataSet(opts.window, id, "_open", true); - dvui.dataSet(opts.window, id, "_header_kind", @intFromEnum(opts.header_kind)); + dvui.dataSet(opts.window, id, "_header_kind", @backingInt(opts.header_kind)); return id_mutex; } @@ -855,10 +855,10 @@ pub fn dialogWindow(id: dvui.Id) anyerror!void { defer vbox.deinit(); const header_kind: DialogHeaderKind = switch (dvui.dataGet(null, id, "_header_kind", u8) orelse 0) { - @intFromEnum(DialogHeaderKind.none) => .none, - @intFromEnum(DialogHeaderKind.info) => .info, - @intFromEnum(DialogHeaderKind.warning) => .warning, - @intFromEnum(DialogHeaderKind.err) => .err, + @backingInt(DialogHeaderKind.none) => .none, + @backingInt(DialogHeaderKind.info) => .info, + @backingInt(DialogHeaderKind.warning) => .warning, + @backingInt(DialogHeaderKind.err) => .err, else => .none, }; @@ -1653,7 +1653,7 @@ fn bubbleSpinnerPaintCheck(rs: dvui.RectScale, grow: f32, alpha: f32) void { /// Subwindow id used for save-complete toasts. Distinct from the canvas subwindow so /// `Workspace.drawCanvas`'s `toastsShow` won't render them — instead `Editor.drawSaveToasts` /// iterates this id and renders centered cards matching the loading-overlay style. -pub const save_toast_subwindow_id: dvui.Id = @enumFromInt(0xF12_5A4E_71D0_5A4E); +pub const save_toast_subwindow_id: dvui.Id = @fromBackingInt(@intCast(0xF12_5A4E_71D0_5A4E)); /// Custom toast display for save-complete events. Visually matches `Editor.drawLoadingOverlay`: /// content-fill @ 0.85 background, drop shadow, checkmark icon + "Saved " label. diff --git a/core/gfx/FrameTarget.zig b/core/gfx/FrameTarget.zig index 21bbb5056..311456c96 100644 --- a/core/gfx/FrameTarget.zig +++ b/core/gfx/FrameTarget.zig @@ -65,7 +65,7 @@ const skip_unread = builtin.target.cpu.arch == .wasm32; /// should not flip it on and off, and glass coming back finds it still there. const linger_frames: u32 = 60; -const unseen_id: dvui.Id = @enumFromInt(0x6669_7a7a_756e_736e); // "fizzunsn" +const unseen_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_756e_736e)); // "fizzunsn" const unseen_key = "_frame_unseen"; /// Mark the frame running as one nobody will see — a demo catching up (`automation.Player.frames`) @@ -90,7 +90,7 @@ pub fn unseen() bool { return dvui.dataGet(null, unseen_id, unseen_key, bool) orelse false; } -const want_id: dvui.Id = @enumFromInt(0x6669_7a7a_6672_6d77); // "fizzfrmw" +const want_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_6672_6d77)); // "fizzfrmw" const want_key = "_frame_target_wanted"; /// Something will read what is under it this frame (a frost, `BlurBackdrop`) — keep the frame in diff --git a/core/gfx/LiquidField.zig b/core/gfx/LiquidField.zig index 79e934434..426567d50 100644 --- a/core/gfx/LiquidField.zig +++ b/core/gfx/LiquidField.zig @@ -257,7 +257,7 @@ fn enabled() bool { return dvui.dataGet(null, enabled_id, "_liquid_field", bool) orelse true; } -const enabled_id: dvui.Id = @enumFromInt(0x6c69_7166); +const enabled_id: dvui.Id = @fromBackingInt(@intCast(0x6c69_7166)); /// Whether the window is opaque behind its content — no desktop material shows through it (Linux, /// the web) — published each frame by the app. The glass is as see-through as what it covers @@ -336,7 +336,7 @@ pub fn draw(self: *const LiquidField, frost: dvui.Texture, covered: dvui.Rect.Ph if (!h.begin(id, &textures, textures.len, u.vec4s(), uniform_vec4s)) return false; defer h.end(); for ([_]programs.Blend{ .punch, .add }) |pass| { - h.blend(@intFromEnum(pass)); + h.blend(@backingInt(pass)); self.quads(groups, pass == .add, frost); } return true; diff --git a/core/gfx/image.zig b/core/gfx/image.zig index 6f8c214d3..cc3f726b9 100644 --- a/core/gfx/image.zig +++ b/core/gfx/image.zig @@ -187,7 +187,7 @@ pub fn checkerboardTile(width: u32, height: u32, even: [4]u8, odd: [4]u8) ?dvui. const size_f: dvui.Size = .{ .w = @floatFromInt(width), .h = @floatFromInt(height) }; for (buf, 0..) |*p, i| { const rgba = if (math.checker(size_f, i)) even else odd; - p.* = @bitCast(rgba); + p.* = .{ .r = rgba[0], .g = rgba[1], .b = rgba[2], .a = rgba[3] }; } return dvui.textureCreate(buf, .{ @@ -209,7 +209,7 @@ pub fn cachedTexture(source: dvui.ImageSource) dvui.ImageSource { .imageFile => |f| f, else => return source, }; - const id: dvui.Id = @enumFromInt(@as(u64, @intFromPtr(file.bytes.ptr)) ^ file.bytes.len); + const id: dvui.Id = @fromBackingInt(@intCast(@as(u64, @intFromPtr(file.bytes.ptr)) ^ file.bytes.len)); if (dvui.dataGet(null, id, "_image_texture_key", u64)) |key| { if (dvui.textureGetCached(key)) |tex| return .{ .texture = tex }; } diff --git a/core/gfx/liquid_glass.zig b/core/gfx/liquid_glass.zig index 50b5fb199..b898c4ec8 100644 --- a/core/gfx/liquid_glass.zig +++ b/core/gfx/liquid_glass.zig @@ -441,7 +441,7 @@ fn paneKey(r: dvui.Rect.Physical, radii: Radii, scale: f32, mod: dvui.Color, loo } fn meshId(key: u64) dvui.Id { - return @enumFromInt(key); + return @fromBackingInt(@intCast(key)); } fn cachedMesh(key: u64) ?dvui.Triangles { diff --git a/core/gfx/perf.zig b/core/gfx/perf.zig index 6965b43b5..3578b54a0 100644 --- a/core/gfx/perf.zig +++ b/core/gfx/perf.zig @@ -23,7 +23,7 @@ pub inline fn nanoTimestamp() i128 { } /// Enable perf recording in Debug and ReleaseSafe builds. -pub const record: bool = builtin.mode == .Debug or builtin.mode == .ReleaseSafe; +pub const record: bool = builtin.mode == .debug or builtin.mode == .safe; /// Lightweight drawing-specific counters active in ALL build modes. pub var draw_event_count: u32 = 0; diff --git a/core/gfx/programs.zig b/core/gfx/programs.zig index b7c2f2c11..942f4d7ae 100644 --- a/core/gfx/programs.zig +++ b/core/gfx/programs.zig @@ -115,7 +115,7 @@ pub const Hooks = extern struct { end: *const fn () callconv(.c) void, }; -const publish_id: dvui.Id = @enumFromInt(0x6669_7a7a_7072_6f67); // "fizzprog" +const publish_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_7072_6f67)); // "fizzprog" const publish_key = "_programs"; /// Host only, once a frame: publish this backend's programs, if it has any. @@ -236,7 +236,7 @@ pub const Program = struct { }, .asked => {}, } - return switch (@as(Status, @enumFromInt(h.status(self.id)))) { + return switch (@as(Status, @fromBackingInt(@intCast(h.status(self.id))))) { .ready => blk: { if (!self.logged) { self.logged = true; @@ -344,7 +344,7 @@ const Job = struct { const h = hooks() orelse return; if (!h.begin(self.program, &self.textures, self.n_textures, self.uniforms.ptr, @intCast(self.uniforms.len))) return; defer h.end(); - h.blend(@intFromEnum(self.blend)); + h.blend(@backingInt(self.blend)); dvui.renderTriangles(self.triangles, self.tex) catch {}; } }; diff --git a/core/gfx/water_surface.zig b/core/gfx/water_surface.zig index c42c2f2dd..01d267bfc 100644 --- a/core/gfx/water_surface.zig +++ b/core/gfx/water_surface.zig @@ -40,8 +40,8 @@ const max_height: f32 = 1.85; const max_vel: f32 = 10.5; pub const WaterSurface = struct { - height: [grid_n]f32 = .{0} ** grid_n, - vel: [grid_n]f32 = .{0} ** grid_n, + height: [grid_n]f32 = @splat(0), + vel: [grid_n]f32 = @splat(0), /// Re-anchor the field when the focused slot shifts by `slots` (so in-flight /// ripples keep travelling with the shelf). `new[i] = old[i + slots*cols]`. diff --git a/core/hitch.zig b/core/hitch.zig index 0ad354e37..efeeb7936 100644 --- a/core/hitch.zig +++ b/core/hitch.zig @@ -16,7 +16,7 @@ pub const Phase = enum { draw, }; -const phase_count = @typeInfo(Phase).@"enum".fields.len; +const phase_count = @typeInfo(Phase).@"enum".field_names.len; pub var enabled: bool = false; var threshold_ns: u64 = 16 * std.time.ns_per_ms; @@ -47,7 +47,7 @@ pub const Timer = struct { pub fn end(self: Timer) void { self.prof.end(); if (!enabled) return; - phase_ns[@intFromEnum(self.phase)] +%= @intCast(now() - self.start); + phase_ns[@backingInt(self.phase)] +%= @intCast(now() - self.start); } }; @@ -73,12 +73,12 @@ pub fn frameEnd() void { .{ frame_index, ms(total), - ms(phase_ns[@intFromEnum(Phase.watchers)]), - ms(phase_ns[@intFromEnum(Phase.loading_jobs)]), - ms(phase_ns[@intFromEnum(Phase.rebuild_workspaces)]), - ms(phase_ns[@intFromEnum(Phase.plugin_hooks)]), - ms(phase_ns[@intFromEnum(Phase.draw)]), - ms(total -| accounted -| phase_ns[@intFromEnum(Phase.draw)]), + ms(phase_ns[@backingInt(Phase.watchers)]), + ms(phase_ns[@backingInt(Phase.loading_jobs)]), + ms(phase_ns[@backingInt(Phase.rebuild_workspaces)]), + ms(phase_ns[@backingInt(Phase.plugin_hooks)]), + ms(phase_ns[@backingInt(Phase.draw)]), + ms(total -| accounted -| phase_ns[@backingInt(Phase.draw)]), }, ); } diff --git a/core/lsp/Client.zig b/core/lsp/Client.zig index f49e4786d..83e7557c7 100644 --- a/core/lsp/Client.zig +++ b/core/lsp/Client.zig @@ -1243,7 +1243,7 @@ fn spawnProcess(self: *Client, io: std.Io) !void { } fn spawnChild(gpa: std.mem.Allocator, io: std.Io, argv: []const []const u8, cwd: []const u8) !std.process.Child { - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { // Prefers the fuller login-shell-resolved PATH (already resolved above, by // `resolveExecutable` → `shell_env.path`, and cached) over this process's own // inherited one, so the server itself — not just fizzy locating its binary — sees the @@ -1291,7 +1291,7 @@ fn spawnChild(gpa: std.mem.Allocator, io: std.Io, argv: []const []const u8, cwd: /// the very first language-server startup on Windows, independent of which mechanism spawned /// that background thread. fn resolveExecutable(gpa: std.mem.Allocator, io: std.Io, name: []const u8) ?[]u8 { - if (comptime builtin.os.tag == .windows) return null; + if (comptime builtin.target.os.tag == .windows) return null; if (std.mem.indexOfScalar(u8, name, '/') != null) return null; const inherited_path: ?[]const u8 = if (std.c.getenv("PATH")) |p| std.mem.span(p) else null; diff --git a/core/math/direction.zig b/core/math/direction.zig index e9309e68a..6b3ff7a4d 100644 --- a/core/math/direction.zig +++ b/core/math/direction.zig @@ -66,7 +66,7 @@ pub const Direction = enum(u8) { if (vy > 0) d = 0b0000_0001 else if (vy < 0) d = 0b0000_0011; } - return @as(Direction, @enumFromInt(d)); + return @as(Direction, @fromBackingInt(@intCast(d))); }, 8 => { @@ -84,7 +84,7 @@ pub const Direction = enum(u8) { if (vy > 0) d = d | 0b0000_0001 else if (vy < 0) d = d | 0b0000_0011; } - return @as(Direction, @enumFromInt(d)); + return @as(Direction, @fromBackingInt(@intCast(d))); }, else => @compileError("Direction size is unsupported"), }; @@ -107,17 +107,17 @@ pub const Direction = enum(u8) { d = d | 0b0000_0011; } - return @as(Direction, @enumFromInt(d)); + return @as(Direction, @fromBackingInt(@intCast(d))); } /// Returns horizontal axis of the direction. pub fn x(self: Direction) f32 { - return @as(f32, @floatFromInt(@as(i8, @bitCast(@intFromEnum(self))) << 4 >> 6)); + return @as(f32, @floatFromInt(@as(i8, @bitCast(@backingInt(self))) << 4 >> 6)); } /// Returns vertical axis of the direction. pub fn y(self: Direction) f32 { - return @as(f32, @floatFromInt(@as(i8, @bitCast(@intFromEnum(self))) << 6 >> 6)); + return @as(f32, @floatFromInt(@as(i8, @bitCast(@backingInt(self))) << 6 >> 6)); } // /// Returns direction as a F32x4. diff --git a/core/math/easing.zig b/core/math/easing.zig index c62de4aa8..1aea311a5 100644 --- a/core/math/easing.zig +++ b/core/math/easing.zig @@ -59,8 +59,8 @@ test "square and flip are inverses through identity" { } test "ease pins endpoints regardless of curve" { - inline for (@typeInfo(EaseType).@"enum".fields) |f| { - const kind: EaseType = @enumFromInt(f.value); + inline for (@typeInfo(EaseType).@"enum".field_names, @typeInfo(EaseType).@"enum".field_values) |_, f_value| { + const kind: EaseType = @fromBackingInt(@intCast(f_value)); try expectApproxEqAbs(@as(f32, 0.0), ease(0.0, 1.0, 0.0, kind), tolerance); try expectApproxEqAbs(@as(f32, 1.0), ease(0.0, 1.0, 1.0, kind), tolerance); } diff --git a/core/motion.zig b/core/motion.zig index dd52d5af9..00ae9cd5c 100644 --- a/core/motion.zig +++ b/core/motion.zig @@ -41,7 +41,7 @@ pub const default_speed: f32 = 0.5; /// How much faster the fastest speed is than as written, and the slowest slower. pub const speed_range: f32 = 2; -const publish_id: dvui.Id = @enumFromInt(0x6669_7a7a_6d6f_7469); // "fizzmoti" +const publish_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_6d6f_7469)); // "fizzmoti" const publish_key = "_motion"; /// What the host publishes each frame. diff --git a/core/paths.zig b/core/paths.zig index 3673281a0..25f3f9c64 100644 --- a/core/paths.zig +++ b/core/paths.zig @@ -81,7 +81,7 @@ pub fn isNormalizedAbsolute(path: []const u8) bool { if (!std.fs.path.isAbsolute(path)) return false; // On Windows `resolve` also rewrites separators and drive-letter case; not worth // replicating, so only the POSIX shape claims the fast path. - if (builtin.os.tag == .windows) return false; + if (builtin.target.os.tag == .windows) return false; return isNormalizedPosixTail(path); } @@ -102,7 +102,7 @@ fn isNormalizedPosixTail(path: []const u8) bool { } test isNormalizedAbsolute { - if (builtin.os.tag == .windows) return error.SkipZigTest; + if (builtin.target.os.tag == .windows) return error.SkipZigTest; const gpa = std.testing.allocator; // Canonical shapes take the fast path, and agree with `normalize`. diff --git a/core/platform.zig b/core/platform.zig index 905deffb2..7923cb598 100644 --- a/core/platform.zig +++ b/core/platform.zig @@ -1,7 +1,7 @@ //! Runtime platform detection that works on both native and wasm. //! -//! On native, `builtin.os.tag` is the truth. On `wasm32-freestanding` it's -//! always `.freestanding`, so any `builtin.os.tag == .macos` check classifies +//! On native, `builtin.target.os.tag` is the truth. On `wasm32-freestanding` it's +//! always `.freestanding`, so any `builtin.target.os.tag == .macos` check classifies //! macOS web as "not mac" — which breaks ctrl/cmd shortcuts, pan/zoom defaults, //! menu glyphs, etc. This module gives fizzy a single source of truth that //! correctly reflects the running platform. @@ -21,10 +21,10 @@ const dvui = @import("dvui"); /// Cached result. Read via `isMacOS()`. Defaults to the native `os.tag` value; /// on wasm `cacheFromWindow` updates it once the DVUI window is initialized. -var cached_is_macos: bool = builtin.os.tag.isDarwin(); +var cached_is_macos: bool = builtin.target.os.tag.isDarwin(); /// Call once after `dvui.Window.init`. On native this is a no-op (the comptime -/// `builtin.os.tag` default is already correct). On wasm this probes DVUI's +/// `builtin.target.os.tag` default is already correct). On wasm this probes DVUI's /// `"ctrl/cmd"` binding to discover whether the browser's `navigator.platform` /// looked like macOS. pub fn cacheFromWindow(win: *dvui.Window) void { @@ -33,7 +33,7 @@ pub fn cacheFromWindow(win: *dvui.Window) void { cached_is_macos = kb.command orelse false; } -/// True if the running platform is macOS — unlike `builtin.os.tag == .macos`, right on web too. +/// True if the running platform is macOS — unlike `builtin.target.os.tag == .macos`, right on web too. pub inline fn isMacOS() bool { return cached_is_macos; } @@ -46,7 +46,7 @@ pub fn processEnviron() std.process.Environ { const empty: [:null]const ?[*:0]const u8 = &.{}; return .{ .block = .{ .slice = empty } }; } - if (@import("builtin").os.tag == .windows) { + if (@import("builtin").target.os.tag == .windows) { return .{ .block = .global }; } var n: usize = 0; diff --git a/core/profile.zig b/core/profile.zig index 42979ad6b..8e414551d 100644 --- a/core/profile.zig +++ b/core/profile.zig @@ -397,7 +397,7 @@ var host_spans: ?*Spans = null; /// work on other threads. var frame_span: OpenSpan = .{}; -const publish_id: dvui.Id = @enumFromInt(0x6669_7a7a_7970_7266); // "fizzyprf" +const publish_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_7970_7266)); // "fizzyprf" const publish_key = std.fmt.comptimePrint("_profiler{d}", .{abi}); const lookback_key = std.fmt.comptimePrint("_profiler_lookback{d}", .{Lookback.abi}); const causes_key = std.fmt.comptimePrint("_profiler_causes{d}", .{FrameCauses.abi}); @@ -867,7 +867,13 @@ test "a report says what each owner and scope cost, as ZON that reads back" { }; var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); - const r = try std.zon.parse.fromSliceAlloc(Report, arena.allocator(), try arena.allocator().dupeZ(u8, text), null, .{}); + const r = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(arena.allocator()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Report, .{ .gpa = arena.allocator(), .arena = zig017_arena.allocator(), .source = try arena.allocator().dupeSentinel(u8, text, 0), .diagnostics = &zig017_zd }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Report, arena.allocator(), zig017_v) else |zig017_e| zig017_e; + }); // The plugin's own time is what it spent; fizzy's own is what was left once that is taken out. try std.testing.expectEqualStrings("text", r.owners[0].owner); try std.testing.expect(r.owners[0].self_ms >= 1.5); @@ -951,3 +957,40 @@ test "a frame's GPU counts land in its own slot and the window's average, not be p.frameEnd(); try std.testing.expectEqual(@as(f64, 2), p.stats.gpu.?.targets); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/core/profile/Gpu.zig b/core/profile/Gpu.zig index f9966a4aa..6a2c7b547 100644 --- a/core/profile/Gpu.zig +++ b/core/profile/Gpu.zig @@ -27,7 +27,7 @@ target_switches: u32 = 0, /// totals do. pub fn since(self: Gpu, prev: Gpu) Gpu { var out: Gpu = .{}; - inline for (std.meta.fields(Gpu)) |f| @field(out, f.name) = @field(self, f.name) -% @field(prev, f.name); + inline for (@typeInfo(Gpu).@"struct".field_names, @typeInfo(Gpu).@"struct".field_types, @typeInfo(Gpu).@"struct".field_attrs) |f_name, _, _| @field(out, f_name) = @field(self, f_name) -% @field(prev, f_name); return out; } @@ -45,7 +45,7 @@ pub const Sum = struct { frames: u32 = 0, pub fn add(self: *Sum, g: Gpu) void { - inline for (std.meta.fields(Gpu)) |f| @field(self, f.name) += @field(g, f.name); + inline for (@typeInfo(Gpu).@"struct".field_names, @typeInfo(Gpu).@"struct".field_types, @typeInfo(Gpu).@"struct".field_attrs) |f_name, _, _| @field(self, f_name) += @field(g, f_name); self.frames += 1; } @@ -54,7 +54,7 @@ pub const Sum = struct { if (self.frames == 0) return null; const n: f64 = @floatFromInt(self.frames); var out: PerFrame = .{}; - inline for (std.meta.fields(Gpu)) |f| @field(out, f.name) = @as(f64, @floatFromInt(@field(self, f.name))) / n; + inline for (@typeInfo(Gpu).@"struct".field_names, @typeInfo(Gpu).@"struct".field_types, @typeInfo(Gpu).@"struct".field_attrs) |f_name, _, _| @field(out, f_name) = @as(f64, @floatFromInt(@field(self, f_name))) / n; return out; } }; diff --git a/core/screens.zig b/core/screens.zig index f16d0b1d2..0c8de00eb 100644 --- a/core/screens.zig +++ b/core/screens.zig @@ -11,7 +11,7 @@ //! carries its own copy of `core`, and its menus and tooltips must read the same as the app's. const dvui = @import("dvui"); -const key_id: dvui.Id = @enumFromInt(0x6669_7a7a_7363_726e); // "fizzscrn" +const key_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_7363_726e)); // "fizzscrn" /// The screen `r` (natural) is on: the one set this frame (`dvui.screensSet`) its middle is in, /// else the main window's (`mainScreen`). diff --git a/core/shell_env.zig b/core/shell_env.zig index 581e9424c..8de174516 100644 --- a/core/shell_env.zig +++ b/core/shell_env.zig @@ -72,7 +72,7 @@ const Cache = struct { }; fn resolve(gpa: std.mem.Allocator, io: std.Io) !?[]u8 { - if (comptime builtin.os.tag != .macos) return null; + if (comptime builtin.target.os.tag != .macos) return null; const shell = if (std.c.getenv("SHELL")) |s| std.mem.span(s) else "/bin/zsh"; diff --git a/core/transport/NativeTransport.zig b/core/transport/NativeTransport.zig index f62f70090..016422d8c 100644 --- a/core/transport/NativeTransport.zig +++ b/core/transport/NativeTransport.zig @@ -222,7 +222,7 @@ fn perform(job: *Job) vfs.Error!vfs.http.Response { .response_writer = &body.writer, }) catch return error.Http; const bytes = body.toOwnedSlice() catch return error.OutOfMemory; - return .{ .status = @intFromEnum(result.status), .body = bytes }; + return .{ .status = @backingInt(result.status), .body = bytes }; } fn cancel(ptr: *anyopaque, handle: vfs.http.Job) void { diff --git a/core/vfs/Fs.zig b/core/vfs/Fs.zig index cabe926e1..883f51fca 100644 --- a/core/vfs/Fs.zig +++ b/core/vfs/Fs.zig @@ -65,18 +65,18 @@ pub fn Result(comptime T: type) type { }; } -const errors = @typeInfo(Error).error_set.?; +const errors = @typeInfo(Error).error_set.error_names.?; fn codeOf(err: Error) u8 { inline for (errors, 1..) |e, i| { - if (err == @field(Error, e.name)) return i; + if (err == @field(Error, e)) return i; } unreachable; } fn errorOf(code: u8) Error { inline for (errors, 1..) |e, i| { - if (code == i) return @field(Error, e.name); + if (code == i) return @field(Error, e); } return error.Io; } diff --git a/core/vfs/zip.zig b/core/vfs/zip.zig index c6dda19ad..c4e8e6352 100644 --- a/core/vfs/zip.zig +++ b/core/vfs/zip.zig @@ -209,14 +209,14 @@ const dos_epoch_date: u16 = (1 << 5) | 1; fn readStruct(comptime T: type, bytes: []const u8, off: u64) ?T { if (off + @sizeOf(T) > bytes.len) return null; - var v: T = @bitCast(bytes[@intCast(off)..][0..@sizeOf(T)].*); - if (@import("builtin").cpu.arch.endian() == .big) std.mem.byteSwapAllFields(T, &v); + var v: T = std.mem.bytesToValue(T, bytes[@intCast(off)..][0..@sizeOf(T)]); + if (@import("builtin").target.cpu.arch.endian() == .big) std.mem.byteSwapAllFields(T, &v); return v; } fn writeStruct(allocator: Allocator, out: *std.ArrayList(u8), value: anytype) Allocator.Error!void { var v = value; - if (@import("builtin").cpu.arch.endian() == .big) std.mem.byteSwapAllFields(@TypeOf(v), &v); + if (@import("builtin").target.cpu.arch.endian() == .big) std.mem.byteSwapAllFields(@TypeOf(v), &v); try out.appendSlice(allocator, std.mem.asBytes(&v)); } diff --git a/core/widgets/CanvasWidget.zig b/core/widgets/CanvasWidget.zig index dfdd3e842..dacf876d7 100644 --- a/core/widgets/CanvasWidget.zig +++ b/core/widgets/CanvasWidget.zig @@ -557,7 +557,7 @@ pub fn updateTouchGesture(self: *CanvasWidget) void { const me = e.evt.mouse; if (!me.button.touch()) continue; - const slot_signed = @intFromEnum(me.button) - @intFromEnum(dvui.enums.Button.touch0); + const slot_signed = @backingInt(me.button) - @backingInt(dvui.enums.Button.touch0); if (slot_signed < 0 or slot_signed >= self.touches.len) continue; const slot: usize = @intCast(slot_signed); @@ -1095,7 +1095,7 @@ pub fn processEvents(self: *CanvasWidget) void { self.pan_fling_x.begin(); self.pan_fling_y.begin(); if (me.button.touch()) { - const slot_signed = @intFromEnum(me.button) - @intFromEnum(dvui.enums.Button.touch0); + const slot_signed = @backingInt(me.button) - @backingInt(dvui.enums.Button.touch0); if (slot_signed >= 0 and slot_signed < self.touches.len) { self.scroll_pan_touch_slot = @intCast(slot_signed); } @@ -1109,7 +1109,7 @@ pub fn processEvents(self: *CanvasWidget) void { self.empty = .pending; self.empty_is_touch = me.button.touch(); self.empty_slot = if (me.button.touch()) slot: { - const s = @intFromEnum(me.button) - @intFromEnum(dvui.enums.Button.touch0); + const s = @backingInt(me.button) - @backingInt(dvui.enums.Button.touch0); break :slot if (s >= 0 and s < self.touches.len) @intCast(s) else 0; } else 0; self.empty_down = true; diff --git a/core/widgets/Split.zig b/core/widgets/Split.zig index b1c5b5038..df8f7a58d 100644 --- a/core/widgets/Split.zig +++ b/core/widgets/Split.zig @@ -915,8 +915,8 @@ test "a tray takes what it asks for while there is room" { defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const left: dvui.Id = @enumFromInt(0xA1); - const right: dvui.Id = @enumFromInt(0xA2); + const left: dvui.Id = @fromBackingInt(@intCast(0xA1)); + const right: dvui.Id = @fromBackingInt(@intCast(0xA2)); setSize(left, 100); setSize(right, 100); @@ -931,8 +931,8 @@ test "a pull stops when the base is at its minimum and leaves the other tray" { defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const left: dvui.Id = @enumFromInt(0xB1); - const right: dvui.Id = @enumFromInt(0xB2); + const left: dvui.Id = @fromBackingInt(@intCast(0xB1)); + const right: dvui.Id = @fromBackingInt(@intCast(0xB2)); setSize(left, 200); setSize(right, 200); @@ -948,8 +948,8 @@ test "a pull that asks for more than the whole budget still leaves the other tra defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const left: dvui.Id = @enumFromInt(0xC1); - const right: dvui.Id = @enumFromInt(0xC2); + const left: dvui.Id = @fromBackingInt(@intCast(0xC1)); + const right: dvui.Id = @fromBackingInt(@intCast(0xC2)); setSize(left, 200); setSize(right, 200); @@ -964,8 +964,8 @@ test "push_out past min shrinks the tray behind" { defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const inner: dvui.Id = @enumFromInt(0xC3); - const outer: dvui.Id = @enumFromInt(0xC4); + const inner: dvui.Id = @fromBackingInt(@intCast(0xC3)); + const outer: dvui.Id = @fromBackingInt(@intCast(0xC4)); setSize(inner, 0); setSize(outer, 120); @@ -981,8 +981,8 @@ test "push_out on the right does not close the left" { defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const left: dvui.Id = @enumFromInt(0xC5); - const right: dvui.Id = @enumFromInt(0xC6); + const left: dvui.Id = @fromBackingInt(@intCast(0xC5)); + const right: dvui.Id = @fromBackingInt(@intCast(0xC6)); setSize(left, 160); setSize(right, 0); @@ -997,8 +997,8 @@ test "a pull with no flex left reclaims the opposite side" { defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const left: dvui.Id = @enumFromInt(0xC7); - const right: dvui.Id = @enumFromInt(0xC8); + const left: dvui.Id = @fromBackingInt(@intCast(0xC7)); + const right: dvui.Id = @fromBackingInt(@intCast(0xC8)); setSize(left, 0); setSize(right, 980); @@ -1014,8 +1014,8 @@ test "conflicting minimums squeeze the base rather than inverting the layout" { defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const left: dvui.Id = @enumFromInt(0xD1); - const right: dvui.Id = @enumFromInt(0xD2); + const left: dvui.Id = @fromBackingInt(@intCast(0xD1)); + const right: dvui.Id = @fromBackingInt(@intCast(0xD2)); setSize(left, 100); setSize(right, 100); @@ -1034,7 +1034,7 @@ test "a container smaller than the base leaves the trays nothing" { defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const only: dvui.Id = @enumFromInt(0xE1); + const only: dvui.Id = @fromBackingInt(@intCast(0xE1)); setSize(only, 100); // base_min alone exceeds the container: room is zero, not negative. @@ -1047,7 +1047,7 @@ test "a max below the min does not panic in the clamp" { defer t.deinit(); _ = try dvui.testing.step(twoPaneFrame); - const only: dvui.Id = @enumFromInt(0xF1); + const only: dvui.Id = @fromBackingInt(@intCast(0xF1)); setSize(only, 50); // `std.math.clamp` asserts lower <= upper, so a contradictory pair has to be reconciled diff --git a/libs/dvui-singleton-app/src/root.zig b/libs/dvui-singleton-app/src/root.zig index c877bf9aa..b2ab65d42 100644 --- a/libs/dvui-singleton-app/src/root.zig +++ b/libs/dvui-singleton-app/src/root.zig @@ -103,7 +103,7 @@ pub const max_args: u32 = 4096; pub const max_arg_bytes: u32 = 1 << 20; // 1 MiB per arg pub const max_total_bytes: u64 = 32 << 20; // 32 MiB per message -const Primary = if (builtin.os.tag == .windows) +const Primary = if (builtin.target.os.tag == .windows) @import("windows_impl.zig").Primary else @import("unix_impl.zig").Primary; diff --git a/libs/dvui-singleton-app/src/unix_impl.zig b/libs/dvui-singleton-app/src/unix_impl.zig index 300f225a3..a8531a327 100644 --- a/libs/dvui-singleton-app/src/unix_impl.zig +++ b/libs/dvui-singleton-app/src/unix_impl.zig @@ -219,7 +219,7 @@ fn chmodPath(path: []const u8, mode: std.c.mode_t) c_int { } test "shutdown returns while the listener waits in accept" { - if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; const io = std.testing.io; var name_buf: [64]u8 = undefined; const app_id = try std.fmt.bufPrint(&name_buf, "fizzy-singleton-test-{d}", .{std.c.getpid()}); diff --git a/libs/dvui-singleton-app/src/windows_impl.zig b/libs/dvui-singleton-app/src/windows_impl.zig index 1dc9b92ec..51d9a87f3 100644 --- a/libs/dvui-singleton-app/src/windows_impl.zig +++ b/libs/dvui-singleton-app/src/windows_impl.zig @@ -32,11 +32,11 @@ const FILE_ATTRIBUTE_NORMAL: w.DWORD = 0x80; const NMPWAIT_USE_DEFAULT_WAIT: w.DWORD = 0x00000000; -const ERROR_FILE_NOT_FOUND = @as(w.Win32Error, @enumFromInt(2)); -const ERROR_ACCESS_DENIED = @as(w.Win32Error, @enumFromInt(5)); -const ERROR_PIPE_BUSY = @as(w.Win32Error, @enumFromInt(231)); -const ERROR_PIPE_CONNECTED = @as(w.Win32Error, @enumFromInt(535)); -const ERROR_BROKEN_PIPE = @as(w.Win32Error, @enumFromInt(109)); +const ERROR_FILE_NOT_FOUND = @as(w.Win32Error, @fromBackingInt(@intCast(2))); +const ERROR_ACCESS_DENIED = @as(w.Win32Error, @fromBackingInt(@intCast(5))); +const ERROR_PIPE_BUSY = @as(w.Win32Error, @fromBackingInt(@intCast(231))); +const ERROR_PIPE_CONNECTED = @as(w.Win32Error, @fromBackingInt(@intCast(535))); +const ERROR_BROKEN_PIPE = @as(w.Win32Error, @fromBackingInt(@intCast(109))); extern "kernel32" fn CreateFileW( lpFileName: w.LPCWSTR, @@ -172,7 +172,7 @@ pub const Primary = struct { } const ok = ConnectNamedPipe(current, null); - const err = if (ok == .FALSE) w.GetLastError() else @as(w.Win32Error, @enumFromInt(0)); + const err = if (ok == .FALSE) w.GetLastError() else @as(w.Win32Error, @fromBackingInt(@intCast(0))); if (ok == .FALSE and err != ERROR_PIPE_CONNECTED) { log.warn("ConnectNamedPipe failed: {t}", .{err}); w.CloseHandle(current); diff --git a/plugins/archive/build.zig.zon b/plugins/archive/build.zig.zon index f9a5d6380..53350bb27 100644 --- a/plugins/archive/build.zig.zon +++ b/plugins/archive/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .archive, .version = "0.1.0", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .paths = .{ "build.zig", "build.zig.zon", diff --git a/plugins/devtools/build.zig.zon b/plugins/devtools/build.zig.zon index 589381583..9b9662578 100644 --- a/plugins/devtools/build.zig.zon +++ b/plugins/devtools/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .devtools, .version = "0.1.0", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .paths = .{ "build.zig", "build.zig.zon", diff --git a/plugins/image/build.zig.zon b/plugins/image/build.zig.zon index d3e0c6229..2504cff03 100644 --- a/plugins/image/build.zig.zon +++ b/plugins/image/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .image, .version = "0.1.0", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .paths = .{ "build.zig", "build.zig.zon", diff --git a/plugins/markdown/build.zig.zon b/plugins/markdown/build.zig.zon index f0b780ba7..eb78acd41 100644 --- a/plugins/markdown/build.zig.zon +++ b/plugins/markdown/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .markdown, .version = "0.1.0", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .paths = .{ "build.zig", "build.zig.zon", @@ -16,8 +16,8 @@ .path = "../../sdk", }, .md4zig = .{ - .url = "git+https://github.com/fizzyedit/md4zig#2d0a687d28be9849b9abe3453bfb7c9f22ffcda1", - .hash = "md4zig-0.1.0-iiatj7oOBgCcrw8ee3UISfYe2LsKVEf7O3osC85HthPX", + .url = "git+https://github.com/fizzyedit/md4zig#eb5241c9e46cd0a507c0c7a85b01afe2b51e58a8", + .hash = "md4zig-0.1.0-iiatj6AQBgBP6PiK2c4MRhjNK2ZXjVg94GvbIEv5WEl7", }, }, } diff --git a/plugins/markdown/src/md/code_language.zig b/plugins/markdown/src/md/code_language.zig index 607ec0421..80858a465 100644 --- a/plugins/markdown/src/md/code_language.zig +++ b/plugins/markdown/src/md/code_language.zig @@ -121,6 +121,6 @@ test "an empty or unknown tag has no extension" { } test "an absurdly long tag is rejected rather than overflowing the buffer" { - const long = "a" ** (max_tag_len + 40); - try testing.expect(extensionForTag(long) == null); + const long: [max_tag_len + 40]u8 = @splat('a'); + try testing.expect(extensionForTag(&long) == null); } diff --git a/plugins/markdown/src/md/image_format.zig b/plugins/markdown/src/md/image_format.zig index 939bcc785..04fde4353 100644 --- a/plugins/markdown/src/md/image_format.zig +++ b/plugins/markdown/src/md/image_format.zig @@ -27,7 +27,7 @@ pub fn isSvg(bytes: []const u8) bool { if (std.ascii.startsWithIgnoreCase(window, " max_bytes) { - entry.status.store(@intFromEnum(Status.failed), .release); + entry.status.store(@backingInt(Status.failed), .release); return; } const owned = a.dupe(u8, body.written()) catch { - entry.status.store(@intFromEnum(Status.failed), .release); + entry.status.store(@backingInt(Status.failed), .release); return; }; entry.bytes = owned; - entry.status.store(@intFromEnum(Status.ready), .release); + entry.status.store(@backingInt(Status.ready), .release); } /// Allocate the pixel buffer the browser will write into. Same `gpa` as the rest of @@ -240,14 +240,14 @@ export fn FizzyWebImageReady(id: u32, ptr: usize, len: usize, width: u32, height const a = gpa orelse return; if (ptr == 0 or len == 0 or width == 0 or height == 0) { if (ptr != 0 and len != 0) a.free(@as([*]u8, @ptrFromInt(ptr))[0..len]); - if (entryById(id)) |entry| entry.status.store(@intFromEnum(Status.failed), .release); + if (entryById(id)) |entry| entry.status.store(@backingInt(Status.failed), .release); return; } const rgba = @as([*]u8, @ptrFromInt(ptr))[0..len]; const expected = @as(usize, width) * @as(usize, height) * 4; if (len != expected) { a.free(rgba); - if (entryById(id)) |entry| entry.status.store(@intFromEnum(Status.failed), .release); + if (entryById(id)) |entry| entry.status.store(@backingInt(Status.failed), .release); return; } const entry = entryById(id) orelse { @@ -259,12 +259,12 @@ export fn FizzyWebImageReady(id: u32, ptr: usize, len: usize, width: u32, height return; } entry.pixels = .{ .rgba = rgba, .width = width, .height = height }; - entry.status.store(@intFromEnum(Status.ready), .release); + entry.status.store(@backingInt(Status.ready), .release); } export fn FizzyWebImageFailed(id: u32) void { if (comptime !is_wasm) return; const entry = entryById(id) orelse return; if (entry.statusValue() != .fetching) return; - entry.status.store(@intFromEnum(Status.failed), .release); + entry.status.store(@backingInt(Status.failed), .release); } diff --git a/plugins/markdown/src/md/render_ast.zig b/plugins/markdown/src/md/render_ast.zig index 8a13d0fd7..1a2b5b02c 100644 --- a/plugins/markdown/src/md/render_ast.zig +++ b/plugins/markdown/src/md/render_ast.zig @@ -718,7 +718,7 @@ fn markUndecodable(ctx: RenderContext, bytes: []const u8) void { fn textureCacheKey(ptr: [*]const u8) dvui.Texture.Cache.Key { var h = dvui.fnv.init(); h.update(std.mem.asBytes(&ptr)); - const it = @intFromEnum(dvui.enums.TextureInterpolation.linear); + const it = @backingInt(dvui.enums.TextureInterpolation.linear); h.update(std.mem.asBytes(&it)); return h.final(); } diff --git a/plugins/shared/build/helpers.zig b/plugins/shared/build/helpers.zig index 26bf02f59..c277628c7 100644 --- a/plugins/shared/build/helpers.zig +++ b/plugins/shared/build/helpers.zig @@ -56,16 +56,22 @@ pub const IdentityManifest = @import("../../../sdk/src/manifest_identity.zig").I /// collide with the same file when the plugin is built standalone (its own `build.zig` already /// `@import`s it via `plugin_sdk.readManifest`). Panics clearly on a missing/invalid manifest. pub fn readManifestAt(b: *std.Build, zon_rel_path: []const u8) IdentityManifest { - const raw = b.build_root.handle.readFileAlloc(b.graph.io, zon_rel_path, b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e| + const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, zon_rel_path, b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e| std.debug.panic("fizzy: could not read {s}: {}", .{ zon_rel_path, e }); - const src = b.allocator.dupeZ(u8, raw) catch @panic("OOM"); + const src = b.allocator.dupeSentinel(u8, raw, 0) catch @panic("OOM"); const Target = struct { id: []const u8, name: []const u8, version: []const u8, min_sdk_version: []const u8 = "", }; - const parsed = std.zon.parse.fromSliceAlloc(Target, b.allocator, src, null, .{ .ignore_unknown_fields = true }) catch |e| + const parsed = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(b.allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Target, .{ .gpa = b.allocator, .arena = zig017_arena.allocator(), .source = src, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Target, b.allocator, zig017_v) else |zig017_e| zig017_e; + }) catch |e| std.debug.panic("fizzy: failed to parse {s}: {}", .{ zon_rel_path, e }); _ = std.SemanticVersion.parse(parsed.version) catch std.debug.panic("fizzy: {s} .version \"{s}\" is not valid semver", .{ zon_rel_path, parsed.version }); @@ -273,3 +279,40 @@ pub fn installDylib(b: *std.Build, lib: *std.Build.Step.Compile, name: []const u }); b.getInstallStep().dependOn(&install_step.step); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/plugins/text/build.zig.zon b/plugins/text/build.zig.zon index 14a706be2..4a719a92e 100644 --- a/plugins/text/build.zig.zon +++ b/plugins/text/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .text, .version = "0.1.1", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .paths = .{ "build.zig", "build.zig.zon", diff --git a/plugins/text/src/Document.zig b/plugins/text/src/Document.zig index b30fda897..291adf8aa 100644 --- a/plugins/text/src/Document.zig +++ b/plugins/text/src/Document.zig @@ -317,7 +317,7 @@ pub fn fromPath(path: []const u8) !Document { /// The token its editor keeps its measured layout under while hidden (`TextEntryWidget /// .InitOptions.retain_layout`), released when the document closes. pub fn layoutToken(self: *const Document) dvui.data.Token { - return @enumFromInt(std.hash.Wyhash.hash(0x7e47_1a70, std.mem.asBytes(&self.id))); + return @fromBackingInt(@intCast(std.hash.Wyhash.hash(0x7e47_1a70, std.mem.asBytes(&self.id)))); } pub fn deinit(self: *Document) void { @@ -453,7 +453,7 @@ pub fn captureState(self: *const Document, allocator: std.mem.Allocator) ![]u8 { .dirty = self.isDirty(), .focused = self.editor_focused, .untitled = self.unsaved, - .preview_mode = @intFromEnum(self.preview_mode), + .preview_mode = @backingInt(self.preview_mode), .scroll_y = self.scroll_y, .split = self.preview_split_ratio_user, }); @@ -465,7 +465,7 @@ pub fn fingerprint(self: *const Document) u64 { var h = std.hash.Wyhash.init(0x7e47_f1); h.update(self.text.items); const anchor = if (self.sel_cursor == self.sel_start) self.sel_end else self.sel_start; - h.update(std.mem.asBytes(&[_]u64{ anchor, self.sel_cursor, @intFromBool(self.isDirty()), @intFromEnum(self.preview_mode) })); + h.update(std.mem.asBytes(&[_]u64{ anchor, self.sel_cursor, @intFromBool(self.isDirty()), @backingInt(self.preview_mode) })); return h.final(); } diff --git a/plugins/text/src/TextEditor.zig b/plugins/text/src/TextEditor.zig index 7f4ba06b6..611a03a93 100644 --- a/plugins/text/src/TextEditor.zig +++ b/plugins/text/src/TextEditor.zig @@ -307,7 +307,7 @@ fn drawEditor(doc: *Document, ext: []const u8, id_extra: u64, gpa: std.mem.Alloc // setting below only picks *what* it inserts (spaces vs a literal tab), not whether // it does so at all. .tab_inserts_indent = true, - .tab_size = @intFromEnum(plugin_impl.statePtr().settings.tab_size.get()), + .tab_size = @backingInt(plugin_impl.statePtr().settings.tab_size.get()), .insert_spaces = plugin_impl.statePtr().settings.insert_spaces_on_tab.get(), // Same VSCode-style baseline as Tab above — not gated by a setting. .auto_indent_newline = true, diff --git a/plugins/workbench/build.zig.zon b/plugins/workbench/build.zig.zon index 58832c407..eb878241b 100644 --- a/plugins/workbench/build.zig.zon +++ b/plugins/workbench/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .workbench, .version = "0.1.1", - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .paths = .{ "build.zig", "build.zig.zon", diff --git a/plugins/workbench/src/FileLoadJob.zig b/plugins/workbench/src/FileLoadJob.zig index 3b9ac5dfe..0564c5b94 100644 --- a/plugins/workbench/src/FileLoadJob.zig +++ b/plugins/workbench/src/FileLoadJob.zig @@ -43,7 +43,7 @@ target_grouping: u64, window: *dvui.Window, started_at_ns: i128, -phase: std.atomic.Value(u8) = .init(@intFromEnum(Phase.queued)), +phase: std.atomic.Value(u8) = .init(@backingInt(Phase.queued)), progress_num: std.atomic.Value(u32) = .init(0), progress_den: std.atomic.Value(u32) = .init(0), cancelled: std.atomic.Value(bool) = .init(false), @@ -103,30 +103,30 @@ pub fn workerMain(job: *FileLoadJob) void { } if (job.cancelled.load(.monotonic)) { - job.phase.store(@intFromEnum(Phase.cancelled), .release); + job.phase.store(@backingInt(Phase.cancelled), .release); return; } - job.phase.store(@intFromEnum(Phase.reading), .release); + job.phase.store(@backingInt(Phase.reading), .release); const handled = job.owner.loadDocument(job.path, job.doc_buf.ptr) catch |e| { job.err = e; - job.phase.store(@intFromEnum(Phase.failed), .release); + job.phase.store(@backingInt(Phase.failed), .release); return; }; if (!handled) { job.err = error.InvalidFile; - job.phase.store(@intFromEnum(Phase.failed), .release); + job.phase.store(@backingInt(Phase.failed), .release); return; } if (job.cancelled.load(.monotonic)) { job.owner.deinitDocumentBuffer(job.doc_buf.ptr); - job.phase.store(@intFromEnum(Phase.cancelled), .release); + job.phase.store(@backingInt(Phase.cancelled), .release); return; } - job.phase.store(@intFromEnum(Phase.ready), .release); + job.phase.store(@backingInt(Phase.ready), .release); } pub fn elapsedExceeds(job: *const FileLoadJob, threshold_ms: i64) bool { diff --git a/plugins/workbench/src/Workspace.zig b/plugins/workbench/src/Workspace.zig index 7f9ae51dd..05e628cda 100644 --- a/plugins/workbench/src/Workspace.zig +++ b/plugins/workbench/src/Workspace.zig @@ -532,7 +532,8 @@ fn lift(id: []const u8, from: dvui.Rect.Physical, event_num: u16) void { wb.carried_tab_key = std.hash.Wyhash.hash(0, id); runtime.host().beginViewDrag(id, from); if (runtime.host().viewDragSurface() == null) return; - if (dvui.currentWindow().capture) |cm| dvui.captureMouseCustom(cm, event_num); + if (dvui.currentWindow().captures[0]) |cm| // ZIG017: per-pointer capture (dvui#1014); 0 is the mouse + dvui.captureMouseCustom(cm, event_num); } /// Write this pane's tab list and make `focus` its active tab. @@ -787,7 +788,7 @@ fn paneSide(side: sdk.RegionSpec.Drop.Side) core.widgets.DockLayout.Side { fn drawCanvas(self: *Workspace, region: sdk.Host.Region, has_tabs: bool) !void { var content_color = dvui.themeGet().color(.window, .fill); - switch (builtin.os.tag) { + switch (builtin.target.os.tag) { .macos, .windows => { content_color = if (!runtime.host().isMaximized()) content_color.opacity(runtime.host().contentOpacity()) else content_color; }, diff --git a/plugins/workbench/src/files.zig b/plugins/workbench/src/files.zig index 89e5b14b5..9aabcd2b6 100644 --- a/plugins/workbench/src/files.zig +++ b/plugins/workbench/src/files.zig @@ -67,7 +67,7 @@ fn clearNewFilePath() void { runtime.workbench().clearPendingNewFilePath(); } -const open_message = if (builtin.os.tag == .macos) "Reveal in Finder" else "Reveal in File Browser"; +const open_message = if (builtin.target.os.tag == .macos) "Reveal in Finder" else "Reveal in File Browser"; pub const Extension = enum { unsupported, diff --git a/plugins/workbench/src/workbench_layout.zig b/plugins/workbench/src/workbench_layout.zig index f832d29bd..1e64ba59e 100644 --- a/plugins/workbench/src/workbench_layout.zig +++ b/plugins/workbench/src/workbench_layout.zig @@ -174,13 +174,19 @@ fn loadPanes(wb: *Workbench) void { defer wb.allocator.free(path); const data = std.Io.Dir.cwd().readFileAlloc(dvui.io, path, wb.allocator, .limited(1 << 20)) catch return; defer wb.allocator.free(data); - const bytes = wb.allocator.dupeZ(u8, data) catch return; + const bytes = wb.allocator.dupeSentinel(u8, data, 0) catch return; defer wb.allocator.free(bytes); - const snap = std.zon.parse.fromSliceAlloc(Panes.Snapshot, wb.allocator, bytes, null, .{ .ignore_unknown_fields = true }) catch |err| { + const snap = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(wb.allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Panes.Snapshot, .{ .gpa = wb.allocator, .arena = zig017_arena.allocator(), .source = bytes, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Panes.Snapshot, wb.allocator, zig017_v) else |zig017_e| zig017_e; + }) catch |err| { dvui.log.warn("workbench: ignoring {s}: {s}", .{ path, @errorName(err) }); return; }; - defer std.zon.parse.free(wb.allocator, snap); + defer zig017ZonFree(wb.allocator, snap); var loaded = Panes.fromSnapshot(wb.allocator, snap) catch return; loaded.animated = true; @@ -230,3 +236,64 @@ fn panesPath(wb: *Workbench) ?[]u8 { std.Io.Dir.cwd().createDirPath(dvui.io, dir) catch return null; return std.fs.path.join(wb.allocator, &.{ dir, panes_file }) catch null; } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} diff --git a/scripts/check-examples.sh b/scripts/check-examples.sh index 583330f96..1044a87d5 100755 --- a/scripts/check-examples.sh +++ b/scripts/check-examples.sh @@ -27,7 +27,10 @@ cd "$root" work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT -# A directory's files as they are on disk, without build output or version control. +# A directory's files as they are on disk, without build output or version control. On macOS, +# without AppleDouble `._*` files too: bsdtar writes one per file with extended attributes, and +# the backend would take `patches/sdl/._0001-….patch` for a patch. +export COPYFILE_DISABLE=1 pack() { tar -C "$1" -cf - --exclude=./.zig-cache --exclude=./zig-out --exclude=./zig-pkg \ --exclude=./.git --exclude=./.jj --exclude=./.profile . @@ -42,16 +45,24 @@ for repo in example-app example-plugin; do mkdir -p "$work/$repo" pack "$EXAMPLES_DIR/$repo" | tar -x -C "$work/$repo" else - git clone --quiet --depth 1 "https://github.com/fizzyedit/$repo" "$work/$repo" + # ZIG017: the examples' zig-0.17 branches while zig/0.17 is open; main again at the merge. + git clone --quiet --depth 1 --branch "${EXAMPLES_REF:-zig-0.17}" "https://github.com/fizzyedit/$repo" "$work/$repo" fi done (cd "$work/example-plugin" && zig fetch --save=fizzy "$sdk_tarball" >/dev/null) +# example-app bundles example-plugin (`hello`), pinned by URL: the copy here, on this tree's SDK. +pack "$work/example-plugin" | gzip > "$work/example-plugin.tar.gz" ( cd "$work/example-app" + zig fetch --save=hello "$work/example-plugin.tar.gz" >/dev/null zig fetch --save=fizzy "$work/fizzy.tar.gz" >/dev/null # Its replay app depends on the plugin SDK alone: this tree's, packed as a release would be. zig fetch --save=fizzy_sdk "$sdk_tarball" >/dev/null + # So do the plugins it carries, each pinning a released SDK of its own. + for plugin in plugins/*/; do + (cd "$plugin" && zig fetch --save=fizzy "$sdk_tarball" >/dev/null) + done ) # The plugin build installs the plugin: into a profile of its own, not this machine's fizzy. diff --git a/scripts/plugin-loop/bench.py b/scripts/plugin-loop/bench.py index 5eef4edc5..ef88bc0a7 100755 --- a/scripts/plugin-loop/bench.py +++ b/scripts/plugin-loop/bench.py @@ -18,7 +18,7 @@ on the UI thread took (unload, register) — `PluginReloads`. Needs a plugin fizzy does not bundle (fizzy skips a user copy of a bundled one): pass `--plugin` and `--id`. -`--backend llvm|self-hosted` forces the code generator for the plugin (`FIZZY_PLUGIN_USE_LLVM`, +`--backend llvm|self-hosted` forces the code generator for the plugin (`-Dplugin-use-llvm`, read by `sdk/plugin_sdk.zig`); `default` leaves it to Zig. Compiler experiments like this run on Linux CI (`.github/workflows/plugin-loop.yml`), not on a shared workstation: an experimental backend on a large compile can take far more memory than LLVM. @@ -59,13 +59,12 @@ def step_times(summary: str) -> dict: return out -def env_for(profile: str, backend: str) -> dict: - env = dict(os.environ, FIZZY_PROFILE=profile) - if backend == "llvm": - env["FIZZY_PLUGIN_USE_LLVM"] = "1" - elif backend == "self-hosted": - env["FIZZY_PLUGIN_USE_LLVM"] = "0" - return env +def env_for(profile: str) -> dict: + return dict(os.environ, FIZZY_PROFILE=profile) + + +def backend_args(backend: str) -> list: + return {"llvm": ["-Dplugin-use-llvm=true"], "self-hosted": ["-Dplugin-use-llvm=false"]}.get(backend, []) class BuildFailed(Exception): @@ -75,8 +74,8 @@ class BuildFailed(Exception): def build(plugin_dir: str, optimize: str, profile: str, backend: str) -> tuple: start = time.monotonic() proc = subprocess.run( - ["zig", "build", f"-Doptimize={optimize}", "--summary", "all"], - cwd=plugin_dir, env=env_for(profile, backend), capture_output=True, text=True, + ["zig", "build", f"-Doptimize={optimize}", "--summary", "all", *backend_args(backend)], + cwd=plugin_dir, env=env_for(profile), capture_output=True, text=True, ) wall = (time.monotonic() - start) * 1000 if proc.returncode != 0: @@ -179,8 +178,8 @@ def bench_watch(report: Report, plugin_dir: str, optimize: str, profile: str, ba log_path = os.path.join(profile, "watch.log") log = open(log_path, "w") proc = subprocess.Popen( - ["zig", "build", "--watch", "-fincremental", f"-Doptimize={optimize}"], - cwd=plugin_dir, env=env_for(profile, backend), stdout=log, stderr=subprocess.STDOUT, + ["zig", "build", "--watch", "-fincremental", f"-Doptimize={optimize}", *backend_args(backend)], + cwd=plugin_dir, env=env_for(profile), stdout=log, stderr=subprocess.STDOUT, start_new_session=True, ) try: diff --git a/scripts/zig-0.17/fields.py b/scripts/zig-0.17/fields.py new file mode 100644 index 000000000..b4acd1165 --- /dev/null +++ b/scripts/zig-0.17/fields.py @@ -0,0 +1,44 @@ +# rewrite `for () |f| body` to 0.17's struct-of-arrays @typeInfo. +import re, sys +HDR = re.compile(r'(inline )?for \((?:std\.meta\.fields\(([^()]+)\)|(@typeInfo\([^()]+\)\.@"(struct|union|enum)")\.fields)\) \|(\w+)\|') +def body_span(s, i): + # i: index just after the header; body is either a {...} block or a single statement to ';' + j = i + while s[j] == ' ': j += 1 + if s[j] == '{': + d = 0 + for k in range(j, len(s)): + if s[k] == '{': d += 1 + elif s[k] == '}': + d -= 1 + if d == 0: return j, k + 1 + k = s.index(';', j); return j, k + 1 +def conv(s): + out = []; pos = 0 + for m in HDR.finditer(s): + if m.start() < pos: continue + inline, T, ti, kind, v = m.groups() + info = ti if ti else f'@typeInfo({T}).@"struct"' + kind = kind or 'struct' + a, b = body_span(s, m.end()); body = s[a:b] + if kind == 'enum' or re.search(rf'\b{v}\.value\b', body): + if not ti: info = f'@typeInfo({T}).@"enum"' + hdr = f'{inline or ""}for ({info}.field_names, {info}.field_values) |{v}_name, {v}_value|' + else: + hdr = f'{inline or ""}for ({info}.field_names, {info}.field_types, {info}.field_attrs) |{v}_name, {v}_type, {v}_attrs|' + body = re.sub(rf'\b{v}\.name\b', f'{v}_name', body) + body = re.sub(rf'\b{v}\.type\b', f'{v}_type', body) + body = re.sub(rf'\b{v}\.value\b', f'{v}_value', body) + body = re.sub(rf'\b{v}\.defaultValue\(\)', f'{v}_attrs.defaultValue({v}_type)', body) + body = re.sub(rf'\b{v}\.default_value_ptr\b', f'{v}_attrs.default_value_ptr', body) + body = re.sub(rf'\b{v}\.alignment\b', f'{v}_attrs.@"align"', body) + body = re.sub(rf'\b{v}\.is_comptime\b', f'{v}_attrs.@"comptime"', body) + out.append(s[pos:m.start()]); out.append(hdr); out.append(s[m.end():a]); out.append(body); pos = b + out.append(s[pos:]); s = ''.join(out) + s = re.sub(r'(@typeInfo\([^()]+\)\.@"(?:struct|union|enum)")\.fields\.len', r'\1.field_names.len', s) + s = re.sub(r'std\.meta\.fields\(([^()]+)\)\.len', r'std.meta.fieldNames(\1).len', s) + return s +if __name__ == '__main__': + for f in sys.argv[1:]: + s = open(f).read(); n = conv(s) + if n != s: open(f, 'w').write(n); print(f) diff --git a/scripts/zig-0.17/rewrite.sh b/scripts/zig-0.17/rewrite.sh new file mode 100755 index 000000000..5bbe76ff6 --- /dev/null +++ b/scripts/zig-0.17/rewrite.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# The mechanical half of the Zig 0.17 switch (plans/ZIG_0_17_PLAN.md, "Where the work lives"). +# Lives only on the `zig/0.17` branch: re-run after every merge from `main`, so code main adds is +# rewritten too, and delete it in the PR that merges the branch. Idempotent. +# +# Hand migrations are not here. What is here either `zig fmt` does, or is a rename with one +# right answer. `zon_standin.pl` is the exception: a stand-in for the zon arena migration, +# marked ZIG017 wherever it writes, until that PR replaces it. +set -euo pipefail +cd "$(jj root 2>/dev/null || git rev-parse --show-toplevel)" +here=scripts/zig-0.17 +zig=(${ZIG:-zig}) # e.g. ZIG="zig 0.17.0" + +files=$(jj file list | grep -E '\.zig$' | grep -v -E '^(spikes|scripts/zig-0\.17)/') +[ -n "$files" ] || { echo "no .zig files found" >&2; exit 1; } + +# Deprecated in 0.17, removed in 0.18 (lands on main as step 0; a no-op once it has). +perl -pi -e 's/(? dupeSentinel(T, x, 0); std.fmt.bufPrintZ(...) -> bufPrintSentinel(..., 0). +# Method calls only: a local helper that happens to be named dupeZ is left alone. +perl -0pi -e 's/\.dupeZ(\((?:[^()]++|(?1))*\))/my $a=$1; $a =~ s{\)\z}{, 0)}; ".dupeSentinel$a"/ge; + s/\.bufPrintZ(\((?:[^()]++|(?1))*\))/my $a=$1; $a =~ s{\)\z}{, 0)}; ".bufPrintSentinel$a"/ge' $files + +# @typeInfo's struct-of-arrays shape, for field loops that read only names, types and defaults. +python3 $here/fields.py $files >/dev/null +python3 $here/unused.py $here $files >/dev/null + +# Stand-in until the zon arena PR: 0.17's std.zon.parse, results in the old allocator. +perl $here/zon_standin.pl $files >/dev/null + +# Last: zig fmt's own upgrades (@intFromEnum -> @backingInt, ...). +jj file list | grep -E "\.(zig|zon)$" | xargs "${zig[@]}" fmt >/dev/null # everything, spikes too diff --git a/scripts/zig-0.17/unused.py b/scripts/zig-0.17/unused.py new file mode 100644 index 000000000..02b45eb84 --- /dev/null +++ b/scripts/zig-0.17/unused.py @@ -0,0 +1,12 @@ +import re, sys +sys.path.insert(0, sys.argv[1]); from fields import body_span +HDR = re.compile(r'\|(\w+)_name, (\w+)_(type|value)(?:, (\w+)_attrs)?\|') +for f in sys.argv[2:]: + s = open(f).read(); o = s; out = []; pos = 0 + for m in HDR.finditer(s): + v = m.group(1); a, b = body_span(s, m.end()); body = s[a:b] + caps = [f'{v}_name', f'{v}_{m.group(3)}'] + ([f'{v}_attrs'] if m.group(4) else []) + caps = [c if re.search(rf'\b{c}\b', body) else '_' for c in caps] + out.append(s[pos:m.start()]); out.append('|' + ', '.join(caps) + '|'); pos = m.end() + out.append(s[pos:]); s = ''.join(out) + if s != o: open(f, 'w').write(s); print(f) diff --git a/scripts/zig-0.17/zon_standin.pl b/scripts/zig-0.17/zon_standin.pl new file mode 100644 index 000000000..880f80db5 --- /dev/null +++ b/scripts/zig-0.17/zon_standin.pl @@ -0,0 +1,90 @@ +# ZIG017 stand-in: 0.16 std.zon.parse calls on 0.17's API, results in the given allocator. Replaced by the zon arena PR. +use strict; use warnings; +local $/; +our $NEED_DUPE = 0; +my $DUPE = <<'ZIGDUPE'; + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +ZIGDUPE +my $FREE = <<'ZIGFREE'; +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} +ZIGFREE +for my $f (@ARGV) { + open my $fh, '<', $f or die; my $s = <$fh>; close $fh; my $o = $s; + $NEED_DUPE = 0; + $s =~ s{std\.zon\.parse\.(fromSliceAlloc|fromSlice)(\((?:[^()]++|(?2))*\))}{conv($2)}ge; + $s =~ s{std\.zon\.parse\.free\(}{zig017ZonFree(}g; + $s .= $DUPE if $NEED_DUPE && $s !~ /fn zig017ZonDupe\(/; + $s .= $FREE if $s =~ /zig017ZonFree\(/ && $s !~ /fn zig017ZonFree\(/; + if ($s ne $o) { open my $w, '>', $f or die; print $w $s; close $w; print "$f\n"; } +} +sub split_args { my ($a)=@_; $a =~ s/^\(//; $a =~ s/\)$//; my @p; my $d=0; my $cur=''; + for my $c (split //, $a) { if ($c eq ',' && $d==0) { push @p, $cur; $cur=''; next } $d++ if $c =~ /[\(\[\{]/; $d-- if $c =~ /[\)\]\}]/; $cur.=$c } push @p, $cur if $cur =~ /\S/; + s/^\s+|\s+$//g for @p; return @p } +sub conv { my ($a)=@_; my @p=split_args($a); return "std.zon.parse.fromSlice$a" unless @p==5; + my ($t,$al,$src,$diag,$opts)=@p; my $iuf = $opts =~ /ignore_unknown_fields\s*=\s*(\w+)/ ? ", .ignore_unknown_fields = $1" : ""; + if ($diag ne 'null') { + return "(zig017_zon: { break :zig017_zon std.zon.parse.fromSlice($t, .{ .gpa = $al, .arena = $al, .source = $src, .diagnostics = $diag$iuf }); })"; + } + $NEED_DUPE = 1; + return "(zig017_zon: { var zig017_zd: std.zon.parse.Diagnostics = undefined; var zig017_arena: std.heap.ArenaAllocator = .init($al); defer zig017_arena.deinit(); " + . "const zig017_r = std.zon.parse.fromSlice($t, .{ .gpa = $al, .arena = zig017_arena.allocator(), .source = $src, .diagnostics = &zig017_zd$iuf }); " + . "break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe($t, $al, zig017_v) else |zig017_e| zig017_e; })" } diff --git a/sdk/build.zig.zon b/sdk/build.zig.zon index d2d07f997..b4146cfe8 100644 --- a/sdk/build.zig.zon +++ b/sdk/build.zig.zon @@ -2,7 +2,7 @@ .name = .fizzy, .version = "0.2.0", .fingerprint = 0x3311781b63706048, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", // Plugin SDK package — no app-only deps (Velopack, nightwatch, …). Plugins depend on // this directory; the repo-root zon owns the editor build. See AGENTS.md. .paths = .{ @@ -13,6 +13,7 @@ "replay_module.zig", "plugin_sdk_meta.zig", "plugin_sdk_check.zig", + "plugin_sdk_install.zig", "sdk_version.zig", "manifest_identity.zig", "paths.zig", @@ -39,14 +40,14 @@ // `fizzy-sdk-` tag in the fork before the pin lands. `fizzy-dev` itself stays a // working branch and can be rebased freely — the tag, never. .dvui = .{ - // fizzy-sdk-0.2.23 - .url = "https://github.com/foxnne/dvui-dev/archive/f646ec0b17899a2c2f222ecce834c82e235e0486.tar.gz", - .hash = "dvui-0.5.0-dev-AQFJmTQyBAHtzJRJ2BzYtusSfj5mYzF4owYGcDHfyNY_", + // zig-0.17 branch (zig/0.17 pins a commit; tagged when it merges to main) + .url = "https://github.com/foxnne/dvui-dev/archive/2c899d18373f96f3502fd44ca2beedd5fb05feae.tar.gz", + .hash = "dvui-0.6.0-dev-AQFJmeNgBAF1o_avCwV7ojjs2FLb00R-kldfrM3fAMpg", //.path = "../../dvui-dev", }, .zf = .{ - .url = "git+https://github.com/natecraddock/zf#c35c421f84895193246db06c40683c1a30e616ef", - .hash = "zf-0.11.0-OIRy8X-RAAAwaRXHMYpj2uvBnuGTZWEE_3V7acqHQNtW", + .url = "git+https://github.com/natecraddock/zf#e00f9e978b6a7126d1b35fa8ffd49b62042ad6f3", + .hash = "zf-0.11.0-OIRy8XCRAAB6FiMwIrtD6FqlagnFwpI6YyOu7bXeYH5J", }, }, } diff --git a/sdk/plugin_sdk.zig b/sdk/plugin_sdk.zig index 12af52b08..6802c6d10 100644 --- a/sdk/plugin_sdk.zig +++ b/sdk/plugin_sdk.zig @@ -18,8 +18,6 @@ const std = @import("std"); /// Shared with the runtime loader so install + load locations never drift (see its doc comment). /// Lives in this package; keep `localConfigRoot` in sync with `core/paths.zig`. -const core_paths = @import("paths.zig"); - /// The `core` module's import set, shared with the app build so a dependency can't reach four of /// the five `core` compiles and miss this one (see its doc comment). const core_module = @import("core_module.zig"); @@ -32,8 +30,8 @@ const core_module = @import("core_module.zig"); /// release tarball (`fizzy-sdk-v*.tar.gz`) it is vendored at the archive root, which *is* this /// package — so the access check below picks whichever layout is present. fn repoPath(b: *std.Build, sub_path: []const u8) std.Build.LazyPath { - b.build_root.handle.access(b.graph.io, sub_path, .{}) catch { - const root = b.build_root.path orelse @panic("fizzy sdk: missing build_root"); + b.root.access(b.graph.io, sub_path, .{}) catch { + const root = b.root.root_dir.path orelse @panic("fizzy sdk: missing build_root"); return .{ .cwd_relative = b.pathJoin(&.{ root, "..", sub_path }) }; }; return b.path(sub_path); @@ -100,19 +98,26 @@ pub fn readManifest(b: *std.Build) IdentityManifest { } fn readManifestAt(b: *std.Build, rel_path: []const u8) IdentityManifest { - const raw = b.build_root.handle.readFileAlloc(b.graph.io, rel_path, b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e| + b.dependOnFileContents(b.path(rel_path)); + const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, rel_path, b.allocator, std.Io.Limit.limited(256 * 1024)) catch |e| std.debug.panic( "fizzy: could not read {s}: {}\nEvery plugin needs a `plugin.zig.zon` manifest at its root (id/name/version) — see docs/PLUGINS.md.", .{ rel_path, e }, ); - const src = b.allocator.dupeZ(u8, raw) catch @panic("OOM"); + const src = b.allocator.dupeSentinel(u8, raw, 0) catch @panic("OOM"); const Target = struct { id: []const u8, name: []const u8, version: []const u8, min_sdk_version: []const u8 = "", }; - const parsed = std.zon.parse.fromSliceAlloc(Target, b.allocator, src, null, .{ .ignore_unknown_fields = true }) catch |e| + const parsed = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(b.allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Target, .{ .gpa = b.allocator, .arena = zig017_arena.allocator(), .source = src, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Target, b.allocator, zig017_v) else |zig017_e| zig017_e; + }) catch |e| std.debug.panic("fizzy: failed to parse {s}: {}", .{ rel_path, e }); _ = std.SemanticVersion.parse(parsed.version) catch std.debug.panic("fizzy: {s} .version \"{s}\" is not valid semver (expected e.g. 1.2.3)", .{ rel_path, parsed.version }); @@ -253,19 +258,24 @@ pub fn install(b: *std.Build, lib: *std.Build.Step.Compile, opts: InstallOptions // convenience). Not for a wasm build: the browser has no plugins directory, and the desktop // editor would only find a file it cannot open. if (lib.rootModuleTarget().cpu.arch == .wasm32) return; - const dev = b.allocator.create(DevInstall) catch @panic("OOM"); - dev.* = .{ - .step = std.Build.Step.init(.{ - .id = .custom, - .name = b.fmt("install plugin '{s}' into fizzy plugins dir", .{name}), - .owner = b, - .makeFn = DevInstall.make, + const fizzy_dep = fizzyDep(b, .{ .target = b.graph.host, .optimize = .fast }); + const dev_exe = b.addExecutable(.{ + .name = "fizzy-plugin-dev-install", + .root_module = b.createModule(.{ + .root_source_file = fizzy_dep.path("plugin_sdk_install.zig"), + .target = b.graph.host, + .optimize = .fast, }), - .lib = lib, - .name = name, - .file_name = dest, - }; - dev.step.dependOn(&lib.step); + }); + const dev = b.addRunArtifact(dev_exe); + dev.setName(b.fmt("install plugin '{s}' into fizzy plugins dir", .{name})); + dev.addArtifactArg(lib); + dev.addArgs(&.{ name, dest }); + // A relative `FIZZY_PROFILE` is taken from the plugin's own directory. + dev.setCwd(b.path(".")); + // Every install, not only when the dylib changed: the copy in the plugins dir may have been + // replaced or removed since. + dev.has_side_effects = true; b.getInstallStep().dependOn(&dev.step); } @@ -273,7 +283,7 @@ pub fn install(b: *std.Build, lib: *std.Build.Step.Compile, opts: InstallOptions /// abi_fingerprint safety class matches the dylib). Identity comes from `plugin.zig.zon`. /// No dlopen — works for Windows and cross-compiles. fn addSdkMeta(b: *std.Build, lib: *std.Build.Step.Compile) *std.Build.Step { - const optimize = lib.root_module.optimize orelse .Debug; + const optimize = lib.root_module.optimize orelse .debug; // One meta emitter per optimize class — Debug vs ReleaseFast disagree on abi_fingerprint. const step_name = b.fmt("sdk-meta-{s}", .{@tagName(optimize)}); if (b.top_level_steps.get(step_name)) |existing| return &existing.step; @@ -316,13 +326,14 @@ fn addSdkCheck(b: *std.Build) *std.Build.Step { const step = b.step("check", "Print pinned fizzy SDK version + abi fingerprint (and catch legacy release.yml drift)"); const release_yml = ".github/workflows/release.yml"; - b.build_root.handle.access(b.graph.io, release_yml, .{}) catch return step; + dependOnPathExistence(b, release_yml); + b.root.access(b.graph.io, release_yml, .{}) catch return step; - const fizzy_dep = fizzyDep(b, .{ .target = b.graph.host, .optimize = .ReleaseFast }); + const fizzy_dep = fizzyDep(b, .{ .target = b.graph.host, .optimize = .fast }); const check_mod = b.createModule(.{ .root_source_file = fizzy_dep.path("plugin_sdk_check.zig"), .target = b.graph.host, - .optimize = .ReleaseFast, + .optimize = .fast, }); check_mod.addImport("fizzy_sdk", fizzy_dep.module("fizzy_sdk")); const check_exe = b.addExecutable(.{ .name = "fizzy-plugin-sdk-check", .root_module = check_mod }); @@ -332,6 +343,17 @@ fn addSdkCheck(b: *std.Build) *std.Build.Step { return step; } +/// `-Dplugin-use-llvm`, declared on the first plugin a builder makes and remembered for the rest: +/// fizzy's own build makes several plugins on one builder, and an option may be declared only +/// once. An option, not an environment variable: Zig 0.17 caches the build's configuration +/// without regard to the environment, so a variable read here would be seen once and then ignored. +fn pluginUseLlvm(b: *std.Build) ?bool { + const gop = plugin_use_llvm.getOrPut(b.allocator, b) catch @panic("OOM"); + if (!gop.found_existing) gop.value_ptr.* = b.option(bool, "plugin-use-llvm", "Force (true) or forbid (false) LLVM for plugin builds; unset, Zig chooses"); + return gop.value_ptr.*; +} +var plugin_use_llvm: std.AutoHashMapUnmanaged(*std.Build, ?bool) = .empty; + /// Platform extension for a dynamic plugin library, for the given target OS. fn pluginExt(os_tag: std.Target.Os.Tag) []const u8 { return switch (os_tag) { @@ -342,74 +364,6 @@ fn pluginExt(os_tag: std.Target.Os.Tag) []const u8 { }; } -/// Resolve `{local_config}/fizzy/plugins` on the build host — exactly where the app scans for -/// user plugins. Must mirror `known-folders` `.local_configuration` (what the runtime loader -/// uses, see `core/paths.zig`) + `fizzy/plugins`: -/// macOS `~/Library/Application Support/fizzy/plugins` -/// Linux `$XDG_CONFIG_HOME/fizzy/plugins` (or `~/.config/fizzy/plugins`) -/// Windows `%LOCALAPPDATA%/fizzy/plugins` (FOLDERID_LocalAppData — *not* Roaming/`%APPDATA%`) -/// -/// With `FIZZY_PROFILE` set, `/plugins` instead — the plugins directory of a fizzy run -/// with that profile (`app/profile.zig`), so a sandbox build never replaces the plugin a person -/// has installed. A relative profile is taken from the plugin's directory, where `zig build` -/// runs; give an absolute one to share it with a fizzy started elsewhere. -fn fizzyPluginsDir(b: *std.Build) ![]const u8 { - const env = &b.graph.environ_map; - if (env.get("FIZZY_PROFILE")) |profile| { - if (profile.len > 0) return std.fs.path.resolve(b.allocator, &.{ b.build_root.path orelse ".", profile, "plugins" }); - } - const config_root = (try core_paths.localConfigRoot( - b.graph.host.result.os.tag, - b.allocator, - env.get("HOME"), - env.get("XDG_CONFIG_HOME"), - env.get("LOCALAPPDATA"), - )) orelse return error.NoConfigHome; - return std.fs.path.join(b.allocator, &.{ config_root, "fizzy", "plugins" }); -} - -/// Custom step: copy the built dylib into the host's fizzy plugins dir, under its own -/// `{id}/{id}.{ext}` directory (see plans/PLUGIN_MANIFEST_PLAN.md R10). No `.zon` sidecar — the -/// dylib's own exports (`fizzy_plugin_id`/`fizzy_plugin_manifest_zon`/…) are identity's only -/// on-disk-adjacent copy. -const DevInstall = struct { - step: std.Build.Step, - lib: *std.Build.Step.Compile, - /// Bare plugin name (the manifest `id`) — its own subdirectory name under the plugins dir. - name: []const u8, - file_name: []const u8, - - fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) anyerror!void { - const self: *DevInstall = @fieldParentPtr("step", step); - const b = step.owner; - const io = b.graph.io; - - // Skip gracefully if the host has no resolvable config home (e.g. a bare CI runner) so a - // plain `zig build` for packaging never fails on the dev convenience. - const plugins_dir = fizzyPluginsDir(b) catch |err| { - std.log.warn("fizzy: skipping plugin dev install (no config home: {s})", .{@errorName(err)}); - return; - }; - // This plugin's own `{plugins}/{name}` directory, and whatever above it is missing — a - // profile's whole tree, the first time a sandbox is built into. "Already exists" is fine. - const dir = try std.fs.path.join(b.allocator, &.{ plugins_dir, self.name }); - std.Io.Dir.cwd().createDirPath(io, dir) catch {}; - - // `getPath2` is relative to the build root (the runner's cwd); the dest is absolute. - const src = self.lib.getEmittedBin().getPath2(b, step); - const dest = try std.fs.path.join(b.allocator, &.{ dir, self.file_name }); - const data = try std.Io.Dir.cwd().readFileAlloc(io, src, b.allocator, .limited(512 * 1024 * 1024)); - // Sibling temp file + rename, not a truncating write onto `dest`: a running fizzy may be - // reading `dest` right now (`PluginLoader.stableLoadCopyPath` copies from it), and on - // Windows a plain overwrite of a file the editor still has open fails outright. The rename - // also means a crash mid-write never leaves a half-written dylib at the load path. - const tmp = try std.fmt.allocPrint(b.allocator, "{s}.part", .{dest}); - try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = tmp, .data = data }); - try std.Io.Dir.renameAbsolute(tmp, dest, io); - std.log.info("fizzy: installed plugin → {s}", .{dest}); - } -}; - /// A C source file + its compile flags, for `addCModule`. pub const CSourceFile = struct { file: std.Build.LazyPath, @@ -555,12 +509,10 @@ pub fn create(b: *std.Build, opts: CreateOptions) PluginArtifact { // The id a crash report names this exact binary by (fizzy's `app/crash/image.zig`). Mach-O // always carries one (LC_UUID) and PE one with its PDB; an ELF image only when asked. if (opts.target.result.ofmt == .elf) lib.build_id = .fast; - // `FIZZY_PLUGIN_USE_LLVM=1` / `=0` forces the code generator for a plugin build; unset, Zig - // chooses its default for the target. For measuring the rebuild - // loop per backend (`scripts/plugin-loop/bench.py --backend`, `.github/workflows/plugin-loop.yml`): - // an environment variable rather than a `-D` option, because fizzy's own build creates several - // plugins on one builder, and an option may be declared only once. - if (b.graph.environ_map.get("FIZZY_PLUGIN_USE_LLVM")) |v| lib.use_llvm = !std.mem.eql(u8, v, "0"); + // `-Dplugin-use-llvm=true|false` forces the code generator for a plugin build; unset, Zig + // chooses its default for the target. For measuring the rebuild loop per backend + // (`scripts/plugin-loop/bench.py --backend`, `.github/workflows/plugin-loop.yml`). + if (pluginUseLlvm(b)) |v| lib.use_llvm = v; if (is_wasm) { // A wasm *side module* (Emscripten's term): position-independent, no entry, no // threads, loaded into the web host's memory and function table at runtime — the @@ -672,3 +624,56 @@ pub fn installBuiltinPlugin( .dest_sub_path = b.fmt("{s}/{s}.{s}", .{ name, name, ext }), }); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} + +/// Declare to Zig 0.17's configure cache that the build read whether `rel` exists: the file itself +/// when it does, and otherwise the nearest directory that exists, since the maker cannot load a +/// configuration that depends on a path inside a missing directory ("configuration failed: +/// FileSystemFailure"). Making the file changes that directory, so the build still reconfigures. +fn dependOnPathExistence(b: *std.Build, rel: []const u8) void { + b.root.access(b.graph.io, rel, .{}) catch { + var dir = std.fs.path.dirname(rel); + while (dir) |d| : (dir = std.fs.path.dirname(d)) { + b.root.access(b.graph.io, d, .{}) catch continue; + return b.dependOnDirectoryMetadata(b.path(d)); + } + return b.dependOnDirectoryMetadata(b.path(".")); + }; + b.dependOnFileMetadata(b.path(rel)); +} diff --git a/sdk/plugin_sdk_install.zig b/sdk/plugin_sdk_install.zig new file mode 100644 index 000000000..df103837a --- /dev/null +++ b/sdk/plugin_sdk_install.zig @@ -0,0 +1,62 @@ +//! Copy a built plugin into fizzy's plugins directory: `zig build install`'s dev convenience, +//! wired by `fizzy.plugin.install`. A program run by a Run step, because Zig 0.17's build has no +//! custom steps. +//! +//! Usage: `plugin_sdk_install `. It lands at +//! `//`, where `` is `$FIZZY_PROFILE/plugins` when that is set +//! (a relative profile is taken from the working directory, the plugin's own) and otherwise the +//! host's fizzy plugins directory, the one the editor scans. Read here, at make time, so the +//! build's configure step never depends on the environment. +const std = @import("std"); +const builtin = @import("builtin"); +const paths = @import("paths.zig"); + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + const args = try init.minimal.args.toSlice(arena); + if (args.len != 4) { + std.debug.print("usage: plugin_sdk_install \n", .{}); + std.process.exit(2); + } + const src, const name, const file_name = .{ args[1], args[2], args[3] }; + + const plugins_dir = pluginsDir(arena, init.environ_map) catch |err| { + // A bare CI runner has no config home; packaging must not fail on a dev convenience. + std.log.warn("fizzy: skipping plugin dev install (no config home: {t})", .{err}); + return; + }; + // This plugin's own `{plugins}/{name}` directory, and whatever above it is missing — a + // profile's whole tree, the first time a sandbox is built into. + const dir = try std.fs.path.join(arena, &.{ plugins_dir, name }); + const cwd = std.Io.Dir.cwd(); + cwd.createDirPath(io, dir) catch {}; + + const dest = try std.fs.path.join(arena, &.{ dir, file_name }); + const data = try cwd.readFileAlloc(io, src, arena, .limited(512 * 1024 * 1024)); + // Sibling temp file + rename, not a truncating write onto `dest`: a running fizzy may be + // reading `dest` right now (`PluginLoader.stableLoadCopyPath` copies from it), and on Windows + // a plain overwrite of a file the editor still has open fails outright. The rename also means + // a crash mid-write never leaves a half-written dylib at the load path. + const tmp = try std.fmt.allocPrint(arena, "{s}.part", .{dest}); + try cwd.writeFile(io, .{ .sub_path = tmp, .data = data }); + try std.Io.Dir.rename(cwd, tmp, cwd, dest, io); + std.log.info("fizzy: installed plugin → {s}", .{dest}); +} + +/// `{local_config}/fizzy/plugins`, mirroring the runtime loader (`core/paths.zig`), or +/// `/plugins` under `FIZZY_PROFILE` (`app/profile.zig`), so a sandbox build never +/// replaces the plugin a person has installed. +fn pluginsDir(arena: std.mem.Allocator, env: *const std.process.Environ.Map) ![]const u8 { + if (env.get("FIZZY_PROFILE")) |profile| { + if (profile.len > 0) return std.fs.path.resolve(arena, &.{ profile, "plugins" }); + } + const config_root = (try paths.localConfigRoot( + builtin.target.os.tag, + arena, + env.get("HOME"), + env.get("XDG_CONFIG_HOME"), + env.get("LOCALAPPDATA"), + )) orelse return error.NoConfigHome; + return std.fs.path.join(arena, &.{ config_root, "fizzy", "plugins" }); +} diff --git a/sdk/replay/BlackBox.zig b/sdk/replay/BlackBox.zig index 619ddb3da..eec1711e5 100644 --- a/sdk/replay/BlackBox.zig +++ b/sdk/replay/BlackBox.zig @@ -68,11 +68,11 @@ pub fn record(self: *BlackBox, gpa: std.mem.Allocator) void { fn toRecord(e: *const dvui.Event) ?events.Event { return switch (e.evt) { .mouse => |me| switch (me.action) { - .motion => .{ .kind = .motion, .button = @intFromEnum(me.button), .mod = @intFromEnum(me.mod), .x = me.p.x, .y = me.p.y }, - .press => .{ .kind = .press, .button = @intFromEnum(me.button), .mod = @intFromEnum(me.mod), .x = me.p.x, .y = me.p.y }, - .release => .{ .kind = .release, .button = @intFromEnum(me.button), .mod = @intFromEnum(me.mod), .x = me.p.x, .y = me.p.y }, - .wheel_x => |ticks| .{ .kind = .wheel_x, .mod = @intFromEnum(me.mod), .x = ticks }, - .wheel_y => |ticks| .{ .kind = .wheel_y, .mod = @intFromEnum(me.mod), .x = ticks }, + .motion => .{ .kind = .motion, .button = @backingInt(me.button), .mod = @backingInt(me.mod), .x = me.p.x, .y = me.p.y }, + .press => .{ .kind = .press, .button = @backingInt(me.button), .mod = @backingInt(me.mod), .x = me.p.x, .y = me.p.y }, + .release => .{ .kind = .release, .button = @backingInt(me.button), .mod = @backingInt(me.mod), .x = me.p.x, .y = me.p.y }, + .wheel_x => |ticks| .{ .kind = .wheel_x, .mod = @backingInt(me.mod), .x = ticks }, + .wheel_y => |ticks| .{ .kind = .wheel_y, .mod = @backingInt(me.mod), .x = ticks }, .focus, .position => null, }, .key => |ke| .{ @@ -81,8 +81,8 @@ fn toRecord(e: *const dvui.Event) ?events.Event { .repeat => .key_repeat, .up => .key_up, }, - .key = @intFromEnum(ke.code), - .mod = @intFromEnum(ke.mod), + .key = @backingInt(ke.code), + .mod = @backingInt(ke.mod), }, .text => |t| switch (t.action) { .value => |v| .{ .kind = .text, .selected = v.selected }, diff --git a/sdk/replay/Input.zig b/sdk/replay/Input.zig index 8dadef610..850c0aeff 100644 --- a/sdk/replay/Input.zig +++ b/sdk/replay/Input.zig @@ -16,7 +16,7 @@ const Sequencer = @import("tape").Sequencer; const chord = @import("tape").chord; /// The buttons this input has pressed and not let go of. -held: std.EnumSet(Tape.Button) = .initEmpty(), +held: std.EnumSet(Tape.Button) = .empty, /// A target's point in physical pixels: a fraction of the tagged rect (or the window), nudged by /// natural pixels. Only a visible tag has a point. @@ -56,7 +56,7 @@ pub fn releaseHeld(self: *Input) void { _ = cw.addEventMouseButton(dvuiButton(b), .release) catch {}; } } - self.held = .initEmpty(); + self.held = .empty; } pub fn scroll(by: Tape.Scroll) void { @@ -114,11 +114,11 @@ fn dvuiButton(b: Tape.Button) dvui.enums.Button { fn dvuiMod(m: chord.Mods) dvui.enums.Mod { var bits: u16 = 0; - if (m.ctrl) bits |= @intFromEnum(dvui.enums.Mod.lcontrol); - if (m.shift) bits |= @intFromEnum(dvui.enums.Mod.lshift); - if (m.alt) bits |= @intFromEnum(dvui.enums.Mod.lalt); - if (m.command) bits |= @intFromEnum(dvui.enums.Mod.lcommand); - return @enumFromInt(bits); + if (m.ctrl) bits |= @backingInt(dvui.enums.Mod.lcontrol); + if (m.shift) bits |= @backingInt(dvui.enums.Mod.lshift); + if (m.alt) bits |= @backingInt(dvui.enums.Mod.lalt); + if (m.command) bits |= @backingInt(dvui.enums.Mod.lcommand); + return @fromBackingInt(@intCast(bits)); } /// The key events this frame's tape added, by `dvui.Event.num`: what `synthetic` answers from. diff --git a/sdk/replay/Snapshot.zig b/sdk/replay/Snapshot.zig index 3a55b50b1..9c7eb9b58 100644 --- a/sdk/replay/Snapshot.zig +++ b/sdk/replay/Snapshot.zig @@ -121,7 +121,7 @@ pub fn get(self: *const Snapshot, n: u32) ?[]const u8 { return if (n == self.taken and n != 0) self.text.items else null; } -const taking_id: dvui.Id = @enumFromInt(0x6669_7a7a_736e_6170); // "fizzsnap" +const taking_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_736e_6170)); // "fizzsnap" const taking_key = "_snapshot_at"; /// Whether a snapshot is being taken of this frame (`beginFrame`). Shared through the window's @@ -382,7 +382,13 @@ test "a snapshot names what a person acts on, and what a tape aims at" { // It reads back as what it says: two buttons by name, the tagged one by its tag, the mark. var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); - const nodes = try std.zon.parse.fromSliceAlloc([]const Node, arena.allocator(), try arena.allocator().dupeZ(u8, text), null, .{}); + const nodes = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(arena.allocator()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice([]const Node, .{ .gpa = arena.allocator(), .arena = zig017_arena.allocator(), .source = try arena.allocator().dupeSentinel(u8, text, 0), .diagnostics = &zig017_zd }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe([]const Node, arena.allocator(), zig017_v) else |zig017_e| zig017_e; + }); var buttons: usize = 0; var save = false; var close = false; @@ -466,7 +472,13 @@ test "a snapshot has the dialogs dvui shows after its capture, and says which wi var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); - const nodes = try std.zon.parse.fromSliceAlloc([]const Node, arena.allocator(), try arena.allocator().dupeZ(u8, text), null, .{}); + const nodes = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(arena.allocator()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice([]const Node, .{ .gpa = arena.allocator(), .arena = zig017_arena.allocator(), .source = try arena.allocator().dupeSentinel(u8, text, 0), .diagnostics = &zig017_zd }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe([]const Node, arena.allocator(), zig017_v) else |zig017_e| zig017_e; + }); // The windows first, each a node. try std.testing.expect(nodes.len > 3); for (nodes[0..3], [_][]const u8{ "app.window:main", "app.window:band", "app.window:dialog" }) |node, tag| { @@ -498,3 +510,40 @@ test "a snapshot has the dialogs dvui shows after its capture, and says which wi // Taken, nothing more is captured: a dialog drawn in a frame nobody asked for is free. try std.testing.expect(!taking()); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/sdk/replay/anchor.zig b/sdk/replay/anchor.zig index 7036b37a1..e8b32c4fb 100644 --- a/sdk/replay/anchor.zig +++ b/sdk/replay/anchor.zig @@ -17,7 +17,7 @@ const std = @import("std"); const dvui = @import("dvui"); -const publish_id: dvui.Id = @enumFromInt(0x6669_7a7a_616e_6368); // "fizzanch" +const publish_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_616e_6368)); // "fizzanch" /// The frame anchors were last asked for in, by its `frame_time_ns`. Not the `_anchors` bool an /// older copy of this file reads: a plugin built against that one marks nothing until rebuilt, /// rather than reading a value of another type. diff --git a/sdk/src/Command.zig b/sdk/src/Command.zig index 99f7b65ba..1da84a7a0 100644 --- a/sdk/src/Command.zig +++ b/sdk/src/Command.zig @@ -196,13 +196,15 @@ pub fn Params(comptime T: type) type { /// naming where, and returns `error.BadArguments`. pub fn parse(call: *Call) error{ BadArguments, OutOfMemory }!Args { const text = std.mem.trim(u8, call.args, " \t\r\n"); - const source = try call.arena.dupeZ(u8, if (text.len == 0) ".{}" else text); - var diag: std.zon.parse.Diagnostics = .{}; - return std.zon.parse.fromSliceAlloc(Args, call.arena, source, &diag, .{}) catch |err| switch (err) { + const source = try call.arena.dupeSentinel(u8, if (text.len == 0) ".{}" else text, 0); + var diag: std.zon.parse.Diagnostics = undefined; + return (zig017_zon: { + break :zig017_zon 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; - const msg = std.fmt.allocPrint(call.arena, "{f}", .{&diag}) catch "the arguments do not fit"; + const msg = std.fmt.allocPrint(call.arena, "{f}", .{diag.fmt("args")}) catch "the arguments do not fit"; call.message = std.mem.trimEnd(u8, msg, "\n"); return error.BadArguments; }, @@ -222,8 +224,8 @@ pub fn Params(comptime T: type) type { }; } -fn buildParams(comptime T: type) [std.meta.fields(T).len]Param { - const fields = std.meta.fields(T); +fn buildParams(comptime T: type) [std.meta.fieldNames(T).len]Param { + const fields = @import("zig017_fields.zig").fields(T); var out: [fields.len]Param = undefined; for (fields, 0..) |f, i| { if (!settings.isCell(f.type)) @compileError( @@ -261,10 +263,10 @@ fn defaultZon(comptime P: type, comptime value: P) ?[]const u8 { /// `T`'s fields with bare payload types, each keeping its cell's default if it has one. fn Plain(comptime T: type) type { - const fields = std.meta.fields(T); + const fields = @import("zig017_fields.zig").fields(T); var names: [fields.len][:0]const u8 = undefined; var types: [fields.len]type = undefined; - var attrs: [fields.len]std.builtin.Type.StructField.Attributes = undefined; + var attrs: [fields.len]std.lang.Type.Struct.FieldAttributes = undefined; for (fields, 0..) |f, i| { const P = f.type.Payload; names[i] = f.name; diff --git a/sdk/src/Manifest.zig b/sdk/src/Manifest.zig index fac810a57..7cf87ae29 100644 --- a/sdk/src/Manifest.zig +++ b/sdk/src/Manifest.zig @@ -64,8 +64,12 @@ author_url: []const u8 = "", /// could otherwise still read. The tradeoff accepted here is that a misspelled field name is /// ignored rather than diagnosed; the visible symptom is a store listing missing that value. pub fn parse(gpa: std.mem.Allocator, source: [:0]const u8) !Manifest { - const m = try std.zon.parse.fromSliceAlloc(Manifest, gpa, source, null, .{ - .ignore_unknown_fields = true, + const m = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Manifest, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = source, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Manifest, gpa, zig017_v) else |zig017_e| zig017_e; }); errdefer free(gpa, m); _ = std.SemanticVersion.parse(m.version) catch return error.InvalidVersion; @@ -77,7 +81,7 @@ pub fn parse(gpa: std.mem.Allocator, source: [:0]const u8) !Manifest { /// Free a `Manifest` returned by `parse`. pub fn free(gpa: std.mem.Allocator, m: Manifest) void { - std.zon.parse.free(gpa, m); + zig017ZonFree(gpa, m); } test "Manifest parse round-trips identity" { @@ -209,3 +213,64 @@ test "parse rejects a non-semver version" { ; try std.testing.expectError(error.InvalidVersion, parse(gpa, source)); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} diff --git a/sdk/src/Plugin.zig b/sdk/src/Plugin.zig index 41c54ed7a..973d5ef3e 100644 --- a/sdk/src/Plugin.zig +++ b/sdk/src/Plugin.zig @@ -42,7 +42,7 @@ internal: bool = false, /// plugin's table. Leave it alone; call `errorName`. error_name: *const fn (code: ErrorInt) [:0]const u8 = &localErrorName, -pub const ErrorInt = std.meta.Int(.unsigned, @bitSizeOf(anyerror)); +pub const ErrorInt = @Int(.unsigned, @bitSizeOf(anyerror)); /// `@errorName` in whichever binary this is compiled into. pub fn localErrorName(code: ErrorInt) [:0]const u8 { diff --git a/sdk/src/dvui_context.zig b/sdk/src/dvui_context.zig index e1f199773..d72aa9070 100644 --- a/sdk/src/dvui_context.zig +++ b/sdk/src/dvui_context.zig @@ -51,7 +51,7 @@ pub const mouse_drag_threshold: f32 = 3; /// Where the host records whether input is touch, in the shared window's data — every dylib's /// copy of dvui has its own `Dragging.threshold`, and this is how they agree on it. -const input_id: dvui.Id = @enumFromInt(0x6669_7a7a_795f_7463); // "fizzy_tc" +const input_id: dvui.Id = @fromBackingInt(@intCast(0x6669_7a7a_795f_7463)); // "fizzy_tc" const input_key = "_touch_input"; /// Host only, once a frame before anything handles input: note whether the pointer is a finger diff --git a/sdk/src/dylib.zig b/sdk/src/dylib.zig index 926e02b3c..68b90f9f7 100644 --- a/sdk/src/dylib.zig +++ b/sdk/src/dylib.zig @@ -238,8 +238,8 @@ const dvui_shared_state_types = .{ /// no store shard can ever match it, whatever the SDK version says (see `PluginStore`'s /// `host_optimize_matches_store`). pub const optimize_safety_class: []const u8 = switch (builtin.mode) { - .Debug, .ReleaseSafe => "safe", - .ReleaseFast, .ReleaseSmall => "fast", + .debug, .safe => "safe", + .fast, .small => "fast", }; /// Runtime dlopen-time compatibility key, compared live host-vs-plugin (`fingerprintMatches`). @@ -413,12 +413,12 @@ pub fn exportEntry(comptime plugin_mod: type, comptime identity: Identity, compt return tripletFromSemver(identity.version); } fn register(host: ?*Host) callconv(.c) u32 { - if (host == null) return @intFromEnum(RegisterStatus.err_null_host); + if (host == null) return @backingInt(RegisterStatus.err_null_host); if (!version.sdkVersionSatisfies(version.sdk_version, identity.min_sdk_version)) { - return @intFromEnum(RegisterStatus.err_sdk_version); + return @backingInt(RegisterStatus.err_sdk_version); } - plugin_mod.register(host.?) catch return @intFromEnum(RegisterStatus.err_register); - return @intFromEnum(RegisterStatus.ok); + plugin_mod.register(host.?) catch return @backingInt(RegisterStatus.err_register); + return @backingInt(RegisterStatus.ok); } fn setDvuiContext( window: ?*dvui.Window, diff --git a/sdk/src/events.zig b/sdk/src/events.zig index 1ea8aff7c..1b66e4aaa 100644 --- a/sdk/src/events.zig +++ b/sdk/src/events.zig @@ -52,8 +52,14 @@ pub fn matches(pattern: []const u8, name: []const u8) bool { /// an emitter can grow a payload without breaking its listeners. 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.dupeZ(u8, if (text.len == 0) ".{}" else text); - return std.zon.parse.fromSliceAlloc(T, arena, source, null, .{ .ignore_unknown_fields = true }); + const source = try arena.dupeSentinel(u8, if (text.len == 0) ".{}" else text, 0); + return (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(arena); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(T, .{ .gpa = arena, .arena = zig017_arena.allocator(), .source = source, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(T, arena, zig017_v) else |zig017_e| zig017_e; + }); } // ---- what fizzy announces ---- @@ -139,3 +145,40 @@ test "a subscription takes its name, its prefix, or everything" { try std.testing.expect(!matches("fizzy.document.*", "fizzy.folder.opened")); try std.testing.expect(matches("*", "chat.turn_ended")); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/sdk/src/fingerprint.zig b/sdk/src/fingerprint.zig index 71a903697..27c11a37a 100644 --- a/sdk/src/fingerprint.zig +++ b/sdk/src/fingerprint.zig @@ -73,7 +73,7 @@ pub fn hashAll(h_in: u64, comptime types: anytype, comptime depth: comptime_int) fn hashType(h_in: u64, comptime T: type, comptime depth: comptime_int) u64 { comptime { const info = @typeInfo(T); - var h = mixU64(h_in, @intFromEnum(std.meta.activeTag(info))); + var h = mixU64(h_in, @backingInt(std.meta.activeTag(info))); // Bare function and opaque types are comptime-only / unsized; everything else // reached here has a concrete size and alignment worth folding in. if (info != .@"fn" and info != .@"opaque") { @@ -84,25 +84,25 @@ fn hashType(h_in: u64, comptime T: type, comptime depth: comptime_int) u64 { switch (info) { .@"struct" => |s| { - h = mixU64(h, s.fields.len); - for (s.fields, 0..) |f, i| { - h = mixStr(h, f.name); + h = mixU64(h, s.field_names.len); + for (s.field_names, s.field_types, 0..) |f_name, f_type, i| { + h = mixStr(h, f_name); // Packed structs have no byte offsets; fall back to declaration order. - h = mixU64(h, if (s.layout == .@"packed") i else @offsetOf(T, f.name)); - h = hashType(h, f.type, depth - 1); + h = mixU64(h, if (s.layout == .@"packed") i else @offsetOf(T, f_name)); + h = hashType(h, f_type, depth - 1); } }, .@"union" => |u| { - h = mixU64(h, u.fields.len); - for (u.fields) |f| { - h = mixStr(h, f.name); - h = hashType(h, f.type, depth - 1); + h = mixU64(h, u.field_names.len); + for (u.field_names, u.field_types) |f_name, f_type| { + h = mixStr(h, f_name); + h = hashType(h, f_type, depth - 1); } }, .@"enum" => |e| { - h = mixU64(h, e.fields.len); - for (e.fields, 0..) |f, i| { - h = mixStr(h, f.name); + h = mixU64(h, e.field_names.len); + for (e.field_names, 0..) |f_name, i| { + h = mixStr(h, f_name); h = mixU64(h, i); } }, @@ -112,18 +112,18 @@ fn hashType(h_in: u64, comptime T: type, comptime depth: comptime_int) u64 { h = hashType(h, a.child, depth - 1); }, .pointer => |p| { - h = mixU64(h, @intFromEnum(p.size)); - h = mixU64(h, @intFromBool(p.is_const)); + h = mixU64(h, @backingInt(p.size)); + h = mixU64(h, @intFromBool(p.attrs.@"const")); // Follow function pointers so vtable hook signatures are part of the // hash, but never follow data pointers: that would deep-walk types we // only pass by reference (e.g. `*dvui.Window`) and risk reference cycles. if (@typeInfo(p.child) == .@"fn") h = hashType(h, p.child, depth - 1); }, .@"fn" => |fninfo| { - h = mixU64(h, @intFromEnum(std.meta.activeTag(fninfo.calling_convention))); - h = mixU64(h, fninfo.params.len); - for (fninfo.params) |param| { - if (param.type) |pt| { + h = mixU64(h, @backingInt(std.meta.activeTag(fninfo.attrs.@"callconv"))); + h = mixU64(h, fninfo.param_types.len); + for (fninfo.param_types) |param| { + if (param) |pt| { h = hashType(h, pt, depth - 1); } else { h = mixStr(h, "anytype"); @@ -169,7 +169,7 @@ pub fn hashAllShape(h_in: u64, comptime types: anytype, comptime depth: comptime /// target-invariant and is hashed as-is. fn callingConventionShapeKey(cc: std.builtin.CallingConvention) u64 { if (std.meta.eql(cc, std.builtin.CallingConvention.c)) return 0; - return 1 + @intFromEnum(std.meta.activeTag(cc)); + return 1 + @backingInt(std.meta.activeTag(cc)); } /// `std.debug.SafetyLock.state`'s enum has a *different tag set* in safety builds @@ -188,7 +188,7 @@ fn hashFieldTypeShape(h_in: u64, comptime FieldType: type, comptime depth: compt fn hashTypeShape(h_in: u64, comptime T: type, comptime depth: comptime_int) u64 { comptime { const info = @typeInfo(T); - var h = mixU64(h_in, @intFromEnum(std.meta.activeTag(info))); + var h = mixU64(h_in, @backingInt(std.meta.activeTag(info))); switch (info) { .int => { // `usize`/`isize` are pointer-width: 64-bit on native, 32-bit on wasm32. Hashing @@ -205,7 +205,7 @@ fn hashTypeShape(h_in: u64, comptime T: type, comptime depth: comptime_int) u64 h = mixStr(h, "isize"); } else { const i = info.int; - h = mixU64(h, @intFromEnum(i.signedness)); + h = mixU64(h, @backingInt(i.signedness)); h = mixU64(h, i.bits); } }, @@ -216,25 +216,25 @@ fn hashTypeShape(h_in: u64, comptime T: type, comptime depth: comptime_int) u64 switch (info) { .@"struct" => |s| { - h = mixU64(h, s.fields.len); - for (s.fields, 0..) |f, i| { - h = mixStr(h, f.name); + h = mixU64(h, s.field_names.len); + for (s.field_names, s.field_types, 0..) |f_name, f_type, i| { + h = mixStr(h, f_name); h = mixU64(h, i); // declaration position, never a byte offset - h = hashFieldTypeShape(h, f.type, depth - 1); + h = hashFieldTypeShape(h, f_type, depth - 1); } }, .@"union" => |u| { - h = mixU64(h, u.fields.len); - for (u.fields, 0..) |f, i| { - h = mixStr(h, f.name); + h = mixU64(h, u.field_names.len); + for (u.field_names, u.field_types, 0..) |f_name, f_type, i| { + h = mixStr(h, f_name); h = mixU64(h, i); - h = hashFieldTypeShape(h, f.type, depth - 1); + h = hashFieldTypeShape(h, f_type, depth - 1); } }, .@"enum" => |e| { - h = mixU64(h, e.fields.len); - for (e.fields, 0..) |f, i| { - h = mixStr(h, f.name); + h = mixU64(h, e.field_names.len); + for (e.field_names, 0..) |f_name, i| { + h = mixStr(h, f_name); h = mixU64(h, i); } }, @@ -244,15 +244,15 @@ fn hashTypeShape(h_in: u64, comptime T: type, comptime depth: comptime_int) u64 h = hashTypeShape(h, a.child, depth - 1); }, .pointer => |p| { - h = mixU64(h, @intFromEnum(p.size)); - h = mixU64(h, @intFromBool(p.is_const)); + h = mixU64(h, @backingInt(p.size)); + h = mixU64(h, @intFromBool(p.attrs.@"const")); if (@typeInfo(p.child) == .@"fn") h = hashTypeShape(h, p.child, depth - 1); }, .@"fn" => |fninfo| { - h = mixU64(h, callingConventionShapeKey(fninfo.calling_convention)); - h = mixU64(h, fninfo.params.len); - for (fninfo.params) |param| { - if (param.type) |pt| { + h = mixU64(h, callingConventionShapeKey(fninfo.attrs.@"callconv")); + h = mixU64(h, fninfo.param_types.len); + for (fninfo.param_types) |param| { + if (param) |pt| { h = hashTypeShape(h, pt, depth - 1); } else { h = mixStr(h, "anytype"); diff --git a/sdk/src/keywords.zig b/sdk/src/keywords.zig index 2cfe4453a..b613a6c26 100644 --- a/sdk/src/keywords.zig +++ b/sdk/src/keywords.zig @@ -135,7 +135,7 @@ pub fn strength(region: []const []const u8, surface: []const []const u8) Fit { var best: Fit = .none; for (region) |r| for (surface) |s| { const f = fit(r, s); - if (@intFromEnum(f) > @intFromEnum(best)) best = f; + if (@backingInt(f) > @backingInt(best)) best = f; }; return best; } diff --git a/sdk/src/render_bridge.zig b/sdk/src/render_bridge.zig index 5d7bcf53c..1de789e62 100644 --- a/sdk/src/render_bridge.zig +++ b/sdk/src/render_bridge.zig @@ -62,10 +62,10 @@ fn textureCreate( const created = win.backend.textureCreate(pixels, .{ .width = options.width, .height = options.height, - .format = @enumFromInt(options.format), - .interpolation = @enumFromInt(options.interpolation), - .wrap_u = @enumFromInt(options.wrap_u), - .wrap_v = @enumFromInt(options.wrap_v), + .format = @fromBackingInt(@intCast(options.format)), + .interpolation = @fromBackingInt(@intCast(options.interpolation)), + .wrap_u = @fromBackingInt(@intCast(options.wrap_u)), + .wrap_v = @fromBackingInt(@intCast(options.wrap_v)), }) catch return emptyTextureDesc(); return proxy_bridge.textureDescFrom(created); } @@ -107,10 +107,10 @@ fn textureCreateTarget(ctx: ?*anyopaque, options: proxy_bridge.CreateOptions) ca const target = win.backend.textureCreateTarget(.{ .width = options.width, .height = options.height, - .format = @enumFromInt(options.format), - .interpolation = @enumFromInt(options.interpolation), - .wrap_u = @enumFromInt(options.wrap_u), - .wrap_v = @enumFromInt(options.wrap_v), + .format = @fromBackingInt(@intCast(options.format)), + .interpolation = @fromBackingInt(@intCast(options.interpolation)), + .wrap_u = @fromBackingInt(@intCast(options.wrap_u)), + .wrap_v = @fromBackingInt(@intCast(options.wrap_v)), .precision = std.enums.fromInt(dvui.Texture.Precision, options.precision) orelse .default, }) catch return emptyTextureDesc(); return proxy_bridge.textureDescFromTarget(target); @@ -205,7 +205,7 @@ fn openURL(ctx: ?*anyopaque, url: [*]const u8, url_len: usize, new_window: u8) c fn setCursor(ctx: ?*anyopaque, cursor: u8) callconv(.c) void { const win = windowFromCtx(ctx); - win.backend.setCursor(@enumFromInt(cursor)); + win.backend.setCursor(@fromBackingInt(@intCast(cursor))); } fn textInputRect(ctx: ?*anyopaque, has_rect: u8, rect: proxy_bridge.ClipRect) callconv(.c) void { diff --git a/sdk/src/settings.zig b/sdk/src/settings.zig index 0f26b9708..9e345b693 100644 --- a/sdk/src/settings.zig +++ b/sdk/src/settings.zig @@ -185,7 +185,7 @@ fn intBounds(comptime IntT: type) struct { min: i64, max: i64 } { } fn enumChoices(comptime EnumT: type) []const []const u8 { - const enum_fields = std.meta.fields(EnumT); + const enum_fields = @import("zig017_fields.zig").enumFields(EnumT); comptime var names: [enum_fields.len][]const u8 = undefined; inline for (enum_fields, 0..) |f, i| names[i] = f.name; const frozen = names; @@ -219,8 +219,8 @@ pub fn kindFor(comptime T: type, comptime opts: Options) Kind { }; } -fn buildSettings(comptime T: type) [std.meta.fields(T).len]Setting { - const struct_fields = std.meta.fields(T); +fn buildSettings(comptime T: type) [std.meta.fieldNames(T).len]Setting { + const struct_fields = @import("zig017_fields.zig").fields(T); var out: [struct_fields.len]Setting = undefined; inline for (struct_fields, 0..) |f, i| { requireCell(f.type, f.name, T); @@ -241,10 +241,10 @@ fn buildSettings(comptime T: type) [std.meta.fields(T).len]Setting { /// diff against defaults — works on this, which is why the on-disk format never learned that /// cells exist. fn Plain(comptime T: type) type { - const cell_fields = std.meta.fields(T); + const cell_fields = @import("zig017_fields.zig").fields(T); var names: [cell_fields.len][:0]const u8 = undefined; var types: [cell_fields.len]type = undefined; - var attrs: [cell_fields.len]std.builtin.Type.StructField.Attributes = undefined; + var attrs: [cell_fields.len]std.lang.Type.Struct.FieldAttributes = undefined; inline for (cell_fields, 0..) |f, i| { requireCell(f.type, f.name, T); const P = f.type.Payload; @@ -312,7 +312,7 @@ pub const SettingsSchema = struct { /// non-default-only persistence (see `diffSerialize`, R12 in plans/PLUGIN_MANIFEST_PLAN.md) diffs /// every value against. pub fn Schema(comptime T: type) type { - const struct_fields = std.meta.fields(T); + const struct_fields = @import("zig017_fields.zig").fields(T); const built_settings = buildSettings(T); return struct { @@ -458,14 +458,18 @@ pub fn Schema(comptime T: type) type { pub fn applyZon(out: *T, blob: []const u8) void { const gpa = runtime.allocator(); - const blob_z = gpa.dupeZ(u8, blob) catch |err| { + const blob_z = gpa.dupeSentinel(u8, blob, 0) catch |err| { dvui.log.warn("sdk.settings: out of memory parsing settings: {s}", .{@errorName(err)}); return; }; defer gpa.free(blob_z); - const parsed = std.zon.parse.fromSliceAlloc(Payloads, gpa, blob_z, null, .{ - .ignore_unknown_fields = true, + const parsed = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Payloads, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = blob_z, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Payloads, gpa, zig017_v) else |zig017_e| zig017_e; }) catch |err| { dvui.log.warn("sdk.settings: failed to parse settings: {s}", .{@errorName(err)}); return; @@ -635,9 +639,15 @@ pub fn Schema(comptime T: type) type { inline for (struct_fields, 0..) |f, i| { if (i == field_index) { const P = f.type.Payload; - const text_z = gpa.dupeZ(u8, text) catch return false; + const text_z = gpa.dupeSentinel(u8, text, 0) catch return false; defer gpa.free(text_z); - const parsed = std.zon.parse.fromSliceAlloc(P, gpa, text_z, null, .{}) catch return false; + const parsed = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(P, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = text_z, .diagnostics = &zig017_zd }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(P, gpa, zig017_v) else |zig017_e| zig017_e; + }) catch return false; // Strings follow the same ownership rule as `setString`; other payloads are // plain values the parse allocated nothing for. if (comptime typeTagFor(P) == .string) { @@ -1000,3 +1010,40 @@ test "a type with no dedicated control is still a legal setting, edited as zon t // `.other` payloads take part in diff-vs-default like anything else. try testing.expect(try S.diffSerialize(testing.allocator, .{}) == null); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/sdk/src/version.zig b/sdk/src/version.zig index 5b0ec38a5..d4e0a33d7 100644 --- a/sdk/src/version.zig +++ b/sdk/src/version.zig @@ -75,7 +75,7 @@ pub const sdk_version = @import("sdk_version").sdk_version; /// why it is a single target/mode-invariant literal rather than a per-target table. Update this /// value from the `@compileError` it triggers whenever it changes; leave `sdk_version` to the /// next SDK release. -pub const recorded_sdk_shape_fingerprint: u64 = 0x0990c3ac7914768c; +pub const recorded_sdk_shape_fingerprint: u64 = 0xf49851904ed579b9; comptime { if (dylib.sdk_shape_fingerprint != recorded_sdk_shape_fingerprint) { diff --git a/sdk/src/zig017_fields.zig b/sdk/src/zig017_fields.zig new file mode 100644 index 000000000..83fa11e32 --- /dev/null +++ b/sdk/src/zig017_fields.zig @@ -0,0 +1,31 @@ +//! ZIG017: 0.16-shaped field records rebuilt from 0.17's struct-of-arrays `@typeInfo`. +const std = @import("std"); + +pub const Field = struct { + name: [:0]const u8, + type: type, + default_value_ptr: ?*const anyopaque, + + pub inline fn defaultValue(comptime f: Field) ?f.type { + const dp: *const f.type = @ptrCast(@alignCast(f.default_value_ptr orelse return null)); + return dp.*; + } +}; + +pub fn fields(comptime T: type) []const Field { + const s = @typeInfo(T).@"struct"; + var out: [s.field_names.len]Field = undefined; + for (s.field_names, s.field_types, s.field_attrs, 0..) |n, t, a, i| out[i] = .{ .name = n, .type = t, .default_value_ptr = a.default_value_ptr }; + const final = out; + return &final; +} + +pub const EnumField = struct { name: [:0]const u8, value: comptime_int }; + +pub fn enumFields(comptime E: type) []const EnumField { + const e = @typeInfo(E).@"enum"; + var out: [e.field_names.len]EnumField = undefined; + for (e.field_names, e.field_values, 0..) |n, v, i| out[i] = .{ .name = n, .value = v }; + const final = out; + return &final; +} diff --git a/sdk/tape/Tape.zig b/sdk/tape/Tape.zig index 6a0ad70bc..be9ce34ed 100644 --- a/sdk/tape/Tape.zig +++ b/sdk/tape/Tape.zig @@ -440,7 +440,7 @@ pub const Owned = struct { /// (`parse`) otherwise. pub fn load(gpa: std.mem.Allocator, bytes: []const u8, check: Check) !Owned { if (binary.sniff(bytes)) return binary.read(gpa, bytes, check); - const source = try gpa.dupeZ(u8, bytes); + const source = try gpa.dupeSentinel(u8, bytes, 0); defer gpa.free(source); return parse(gpa, source, check); } @@ -449,7 +449,13 @@ pub fn load(gpa: std.mem.Allocator, bytes: []const u8, check: Check) !Owned { pub fn parse(gpa: std.mem.Allocator, source: [:0]const u8, check: Check) !Owned { var arena: std.heap.ArenaAllocator = .init(gpa); errdefer arena.deinit(); - const tape = try std.zon.parse.fromSliceAlloc(Tape, arena.allocator(), source, null, .{}); + const tape = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(arena.allocator()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Tape, .{ .gpa = arena.allocator(), .arena = zig017_arena.allocator(), .source = source, .diagnostics = &zig017_zd }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Tape, arena.allocator(), zig017_v) else |zig017_e| zig017_e; + }); try tape.validate(check); return .{ .arena = arena, .tape = tape }; } @@ -638,7 +644,7 @@ test "a tape round-trips through ZON" { var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); try tape.write(&out.writer); - const text = try testing.allocator.dupeZ(u8, out.written()); + const text = try testing.allocator.dupeSentinel(u8, out.written(), 0); defer testing.allocator.free(text); var back = try parse(testing.allocator, text, .{}); @@ -671,3 +677,40 @@ test "a hand-written tape parses with every default left out" { try testing.expect(owned.tape.ops[2].do.wait.until == .idle); try testing.expectEqual(@as(u32, 10_000), owned.tape.ops[2].do.wait.timeout); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/sdk/tape/binary.zig b/sdk/tape/binary.zig index 6d37c2af6..d721abad1 100644 --- a/sdk/tape/binary.zig +++ b/sdk/tape/binary.zig @@ -142,7 +142,7 @@ pub fn write(gpa: std.mem.Allocator, tape: Tape, w: *std.Io.Writer) !void { try w.writeAll(&magic); try w.writeInt(u16, version, .little); try w.writeInt(u16, 0, .little); - inline for (std.meta.fields(Header)) |f| try w.writeInt(u32, @field(h, f.name), .little); + inline for (@typeInfo(Header).@"struct".field_names, @typeInfo(Header).@"struct".field_types, @typeInfo(Header).@"struct".field_attrs) |f_name, _, _| try w.writeInt(u32, @field(h, f_name), .little); var at: u32 = 0; for (table.list.items) |s| { @@ -171,7 +171,7 @@ pub fn write(gpa: std.mem.Allocator, tape: Tape, w: *std.Io.Writer) !void { }, .press, .release => |btn| { kind = if (op.do == .press) .press else .release; - small = @intFromEnum(btn); + small = @backingInt(btn); }, .scroll => |s| { kind = .scroll; @@ -194,13 +194,13 @@ pub fn write(gpa: std.mem.Allocator, tape: Tape, w: *std.Io.Writer) !void { kind = .wait; b = wt.timeout; switch (wt.until) { - .idle => small = @intFromEnum(UntilKind.idle), + .idle => small = @backingInt(UntilKind.idle), .shown => |s| { - small = @intFromEnum(UntilKind.shown); + small = @backingInt(UntilKind.shown); a = table.index(s); }, .gone => |s| { - small = @intFromEnum(UntilKind.gone); + small = @backingInt(UntilKind.gone); a = table.index(s); }, } @@ -208,7 +208,7 @@ pub fn write(gpa: std.mem.Allocator, tape: Tape, w: *std.Io.Writer) !void { } try w.writeInt(u32, op.at, .little); try w.writeInt(u32, op.ms, .little); - try w.writeByte(@intFromEnum(kind)); + try w.writeByte(@backingInt(kind)); try w.writeByte(small); try w.writeInt(u16, 0, .little); try w.writeInt(u32, a, .little); @@ -221,7 +221,7 @@ pub fn write(gpa: std.mem.Allocator, tape: Tape, w: *std.Io.Writer) !void { var settings_at: u32 = 0; for (tape.keyframes) |kf| { try w.writeInt(u32, table.index(kf.root), .little); - try w.writeByte(@intFromEnum(kf.layout)); + try w.writeByte(@backingInt(kf.layout)); try w.splatByteAll(0, 3); for ([_]u32{ files_at, count(kf.files.len), open_at, count(kf.open.len), settings_at, count(kf.settings.len) }) |v| try w.writeInt(u32, v, .little); @@ -243,7 +243,7 @@ pub fn write(gpa: std.mem.Allocator, tape: Tape, w: *std.Io.Writer) !void { var clear_at: u32 = 0; for (tape.captions) |c| { for ([_]u32{ c.at, c.ms, table.index(c.title), table.index(c.text) }) |v| try w.writeInt(u32, v, .little); - try w.writeByte(@intFromEnum(c.place)); + try w.writeByte(@backingInt(c.place)); try w.splatByteAll(0, 3); for ([_]u32{ table.index(c.near), clear_at, count(c.clear.len) }) |v| try w.writeInt(u32, v, .little); clear_at += count(c.clear.len); @@ -332,7 +332,7 @@ pub fn read(gpa: std.mem.Allocator, bytes: []const u8, check: Tape.Check) ReadEr if (tape_version < oldest_version or tape_version > version) return error.UnsupportedVersion; _ = try r.int(u16); // flags: none yet var h: Header = undefined; - inline for (std.meta.fields(Header)) |f| @field(h, f.name) = try r.int(u32); + inline for (@typeInfo(Header).@"struct".field_names, @typeInfo(Header).@"struct".field_types, @typeInfo(Header).@"struct".field_attrs) |f_name, _, _| @field(h, f_name) = try r.int(u32); // The string table: offsets, then the bytes they index. const offsets = try r.take(try sizeOf(@as(usize, h.strings) + 1, 4)); @@ -603,7 +603,7 @@ test "binary and ZON are the same tape, converted either way" { var zon: std.Io.Writer.Allocating = .init(testing.allocator); defer zon.deinit(); try tape.write(&zon.writer); - const source = try testing.allocator.dupeZ(u8, zon.written()); + const source = try testing.allocator.dupeSentinel(u8, zon.written(), 0); defer testing.allocator.free(source); var from_zon = try Tape.parse(testing.allocator, source, .{}); defer from_zon.deinit(); diff --git a/sdk/tape/chord.zig b/sdk/tape/chord.zig index 6ba439de9..28fc52159 100644 --- a/sdk/tape/chord.zig +++ b/sdk/tape/chord.zig @@ -11,7 +11,7 @@ const keyToSpelling = @import("key.zig").toSpelling; /// Which physical modifier `mod+` resolves to. Passed in rather than detected so both branches /// are testable without a window — and because fizzy already has to distinguish these at runtime -/// (`fizzy.core.platform.isMacOS()` differs from `builtin.os.tag` on wasm). +/// (`fizzy.core.platform.isMacOS()` differs from `builtin.target.os.tag` on wasm). pub const Platform = enum { mac, other }; pub const Mods = packed struct { diff --git a/sdk/tape/events.zig b/sdk/tape/events.zig index 501318d8c..2f67e62a8 100644 --- a/sdk/tape/events.zig +++ b/sdk/tape/events.zig @@ -60,7 +60,7 @@ pub const Event = extern struct { _pad1: u32 = 0, pub fn valid(self: Event) bool { - return @intFromEnum(self.kind) <= @intFromEnum(Kind.app_quit); + return @backingInt(self.kind) <= @backingInt(Kind.app_quit); } }; diff --git a/sdk/tape/ring.zig b/sdk/tape/ring.zig index 46b2f4bd6..e423ac0f4 100644 --- a/sdk/tape/ring.zig +++ b/sdk/tape/ring.zig @@ -159,7 +159,7 @@ pub fn Ring(comptime name: []const u8, comptime Frame: type, comptime Record: ty /// Frames `from` up to (not including) `to`, their records and payloads, as one file: the /// header and the three arrays, the records' `first` and `bytes_at` rebased to the file's. pub fn write(self: *const Self, w: *std.Io.Writer, from: u64, to: u64) !void { - std.debug.assert(builtin.cpu.arch.endian() == .little); + std.debug.assert(builtin.target.cpu.arch.endian() == .little); const lo = @max(from, self.oldestWhole()); const hi = @min(to, self.frames_n); var n_records: u64 = 0; @@ -315,7 +315,7 @@ test "the rings keep the newest, and a stretch starts at the oldest frame still for (0..TestRing.frames_cap + 10) |i| ring.appendFrame(.{ .t_ns = @intCast(i) }, &.{.{ .button = @intCast(i % 16) }}, &.{}); try std.testing.expectEqual(@as(u64, 10), ring.oldestWhole()); // Payloads outrunning their ring: a frame whose bytes were overwritten is not whole. - for (0..8) |_| ring.appendFrame(.{ .t_ns = 100 }, &.{.{ .button = 1 }}, &.{"x" ** 64}); + for (0..8) |_| ring.appendFrame(.{ .t_ns = 100 }, &.{.{ .button = 1 }}, &.{&@as([64]u8, @splat('x'))}); const oldest = ring.frame(ring.oldestWhole()); const r = ring.records[@intCast(oldest.first % TestRing.records_cap)]; try std.testing.expect(r.bytes_at >= ring.bytes_n - TestRing.bytes_cap); diff --git a/spikes/windows-composition/src/main.zig b/spikes/windows-composition/src/main.zig index 712d38415..b7cb45889 100644 --- a/spikes/windows-composition/src/main.zig +++ b/spikes/windows-composition/src/main.zig @@ -186,7 +186,7 @@ pub fn main() !void { _ = c.SDL_SetGPUAllowedFramesInFlight(gpu, st.frames_in_flight); }, c.SDLK_D => st.clip_delay = (st.clip_delay + 1) % 4, - c.SDLK_T => st.timing = @enumFromInt((@intFromEnum(st.timing) + 1) % 3), + c.SDLK_T => st.timing = @fromBackingInt(@intCast((@backingInt(st.timing) + 1) % 3)), c.SDLK_B => st.use_backdrop = !st.use_backdrop, c.SDLK_SPACE => st.paused = !st.paused, c.SDLK_UP => st.speed *= 1.5, diff --git a/spikes/windows-composition/src/overlay.zig b/spikes/windows-composition/src/overlay.zig index ec29e6fac..d0de398a8 100644 --- a/spikes/windows-composition/src/overlay.zig +++ b/spikes/windows-composition/src/overlay.zig @@ -25,8 +25,8 @@ pub const Variant = enum { pub fn next(v: ?Variant) ?Variant { const cur = v orelse return .layered; - const i = @intFromEnum(cur) + 1; - return if (i >= @typeInfo(Variant).@"enum".fields.len) null else @enumFromInt(i); + const i = @backingInt(cur) + 1; + return if (i >= @typeInfo(Variant).@"enum".fields.len) null else @fromBackingInt(@intCast(i)); } }; diff --git a/src/Entry.zig b/src/Entry.zig index 0708f394c..3645ff4f4 100644 --- a/src/Entry.zig +++ b/src/Entry.zig @@ -61,10 +61,10 @@ const start_options_base: dvui.App.StartOptions = .{ .icon = icon, // Linux too, for the rounded corners outside the window's fill (`Editor`): Vulkan composites // the swapchain's alpha on Wayland. Its fill stays opaque — nothing blurs behind it there. - .transparent = if (builtin.os.tag == .macos or builtin.os.tag == .windows or builtin.os.tag == .linux) true else false, + .transparent = if (builtin.target.os.tag == .macos or builtin.target.os.tag == .windows or builtin.target.os.tag == .linux) true else false, // macOS: Cancel-leading dialog/footer order; other platforms: OK-leading (matches dialog header close vs icon). .window_init_options = .{ - .button_order = if (builtin.os.tag.isDarwin()) .cancel_ok else .ok_cancel, + .button_order = if (builtin.target.os.tag.isDarwin()) .cancel_ok else .ok_cancel, }, }; @@ -97,12 +97,12 @@ fn startOptions() dvui.App.StartOptions { // bundle's `.icns` — so the Dock icon visibly loses its background the moment fizzy // launches. Inside a bundle the `.icns` is already the right icon; leave it alone. Loose // dev builds have no bundle icon at all, so there we still want the runtime one. - if (comptime builtin.os.tag == .macos) { + if (comptime builtin.target.os.tag == .macos) { if (runningFromAppBundle(main_init.io)) opts.icon = null; } // A profile is the config folder itself (`app.profile`), for SDL's preferences too. const pref: ?[:0]const u8 = if (@import("app").profile.root) |root| - std.fmt.bufPrintZ(&pref_path_buf, "{s}", .{root}) catch null + std.fmt.bufPrintSentinel(&pref_path_buf, "{s}", .{root}, 0) catch null else paths.configFolderZ(&pref_path_buf, main_init.io, fizzy.core.platform.processEnviron(), ".", AppInfo.current.config_dir); if (pref) |pref_path| { @@ -123,7 +123,7 @@ fn startOptions() dvui.App.StartOptions { fizzy.backend.setSdlAppMetadata(AppInfo.display_name_z, app_version_z, AppInfo.bundle_id_z); // Linux: fizzy draws its own title bar and the window's shadow (`linux_titlebar`), which // SDL has to know before the window is made. - if (comptime builtin.os.tag == .linux) { + if (comptime builtin.target.os.tag == .linux) { const in = Constants.linux_window_shadow_insets; fizzy.backend.useClientDecorations(.{ .left = in.left, .top = in.top, .right = in.right, .bottom = in.bottom }); } @@ -276,7 +276,7 @@ const sdk_version = std.fmt.comptimePrint("{f} ({x})", .{ fn logFn(comptime level: std.log.Level, comptime scope: @EnumLiteral(), comptime format: []const u8, args: anytype) void { // A refresh record, counted as a frame's cause; printed only for `FIZZY_LOG_REFRESH`. if (fizzy.core.profile.interceptLog(level, scope, format, args) and !refresh_log_active) return; - if (comptime scope == .dvui and level == .debug and @import("builtin").mode != .Debug) return; + if (comptime scope == .dvui and level == .debug and @import("builtin").mode != .debug) return; if (!refresh_log_active) fizzy.OutputLog.append(level, scope, format, args); dvui.App.logFn(level, scope, format, args); } @@ -356,7 +356,7 @@ pub fn AppInit(win: *dvui.Window) !void { app_ptr.* = .{ .allocator = allocator, .window = win, - .root_path = try allocator.dupeZ(u8, path), + .root_path = try allocator.dupeSentinel(u8, path, 0), }; const editor_ptr = try allocator.create(Editor); @@ -395,7 +395,7 @@ pub fn AppInit(win: *dvui.Window) !void { // near the top of AppInit while the window was hidden). The window opens at // its saved windowed geometry; fullscreen/maximize are not restored. Windows // chrome goes here. - if (builtin.os.tag != .macos) { + if (builtin.target.os.tag != .macos) { fizzy.backend.setWindowStyle(win); } @@ -506,7 +506,7 @@ fn gpuTotals() ?fizzy.core.profile.Gpu { if (comptime !@hasDecl(dvui.backend, "gpuTotals")) return null; const t = dvui.backend.gpuTotals(); var out: fizzy.core.profile.Gpu = .{}; - inline for (std.meta.fields(fizzy.core.profile.Gpu)) |f| @field(out, f.name) = @truncate(@field(t, f.name)); + inline for (@typeInfo(fizzy.core.profile.Gpu).@"struct".field_names, @typeInfo(fizzy.core.profile.Gpu).@"struct".field_types, @typeInfo(fizzy.core.profile.Gpu).@"struct".field_attrs) |f_name, _, _| @field(out, f_name) = @truncate(@field(t, f_name)); return out; } diff --git a/src/backend/WebBackend.zig b/src/backend/WebBackend.zig index fc3a0675b..03be8830f 100644 --- a/src/backend/WebBackend.zig +++ b/src/backend/WebBackend.zig @@ -327,14 +327,14 @@ export fn dvui_c_alloc(size: usize) ?*anyopaque { //log.debug("dvui_c_alloc {d} failed", .{size}); return null; }; - std.mem.writeInt(u64, buffer[0..@sizeOf(u64)], buffer.len, builtin.cpu.arch.endian()); + std.mem.writeInt(u64, buffer[0..@sizeOf(u64)], buffer.len, builtin.target.cpu.arch.endian()); //log.debug("dvui_c_alloc {*} {d}", .{ buffer.ptr + 8, size }); return buffer.ptr + 8; } pub export fn dvui_c_free(ptr: ?*anyopaque) void { const buffer = @as([*]align(8) u8, @ptrCast(@alignCast(ptr orelse return))) - 8; - const len = std.mem.readInt(u64, buffer[0..@sizeOf(u64)], builtin.cpu.arch.endian()); + const len = std.mem.readInt(u64, buffer[0..@sizeOf(u64)], builtin.target.cpu.arch.endian()); //log.debug("dvui_c_free {?*} {d}", .{ ptr, len - 8 }); gpa.free(buffer[0..@intCast(len)]); @@ -349,7 +349,7 @@ export fn dvui_c_realloc_sized(ptr: ?*anyopaque, oldsize: usize, newsize: usize) } //const buffer = @as([*]u8, @ptrCast(ptr.?)) - 8; - //const len = std.mem.readInt(u64, buffer[0..@sizeOf(u64)], builtin.cpu.arch.endian()); + //const len = std.mem.readInt(u64, buffer[0..@sizeOf(u64)], builtin.target.cpu.arch.endian()); //const slice = buffer[0..@intCast(len)]; //log.debug("dvui_c_realloc_sized buffer {*} {d}", .{ ptr, len }); @@ -361,7 +361,7 @@ export fn dvui_c_realloc_sized(ptr: ?*anyopaque, oldsize: usize, newsize: usize) dvui_c_free(ptr); return newptr; - //std.mem.writeInt(usize, slice[0..@sizeOf(usize)], slice.len, builtin.cpu.arch.endian()); + //std.mem.writeInt(usize, slice[0..@sizeOf(usize)], slice.len, builtin.target.cpu.arch.endian()); //return slice.ptr + 16; } @@ -385,7 +385,7 @@ fn dvui_c_ldexp(x: f64, n: c_int) callconv(.c) f64 { // See https://github.com/ziglang/zig/issues/23358 comptime { switch (builtin.mode) { - .Debug => { + .debug => { @export(&dvui_c_ldexp, .{ .name = "dvui_c_ldexp" }); }, else => {}, @@ -497,17 +497,17 @@ fn add_event_raw(w: *dvui.Window, which: u8, int1: u32, int2: u32, float1: f32, _ = try w.addEventText(.{ .text = str }); }, 8 => { - const touch: dvui.enums.Button = @enumFromInt(@intFromEnum(dvui.enums.Button.touch0) + int1); + const touch: dvui.enums.Button = @fromBackingInt(@intCast(@backingInt(dvui.enums.Button.touch0) + int1)); _ = try w.addEventPointer(.{ .button = touch, .action = .press, .xynorm = .{ .x = float1, .y = float2 } }); touchPoints[int1] = .{ .x = float1, .y = float2 }; }, 9 => { - const touch: dvui.enums.Button = @enumFromInt(@intFromEnum(dvui.enums.Button.touch0) + int1); + const touch: dvui.enums.Button = @fromBackingInt(@intCast(@backingInt(dvui.enums.Button.touch0) + int1)); _ = try w.addEventPointer(.{ .button = touch, .action = .release, .xynorm = .{ .x = float1, .y = float2 } }); touchPoints[int1] = null; }, 10 => { - const touch: dvui.enums.Button = @enumFromInt(@intFromEnum(dvui.enums.Button.touch0) + int1); + const touch: dvui.enums.Button = @fromBackingInt(@intCast(@backingInt(dvui.enums.Button.touch0) + int1)); var dx: f32 = 0; var dy: f32 = 0; if (touchPoints[int1]) |p| { @@ -665,12 +665,12 @@ fn web_mod_code_to_dvui(wmod: u8) dvui.enums.Mod { if (wmod == 0) return .none; var m: u16 = 0; - if (wmod & 0b0001 > 0) m |= @intFromEnum(dvui.enums.Mod.lshift); - if (wmod & 0b0010 > 0) m |= @intFromEnum(dvui.enums.Mod.lcontrol); - if (wmod & 0b0100 > 0) m |= @intFromEnum(dvui.enums.Mod.lalt); - if (wmod & 0b1000 > 0) m |= @intFromEnum(dvui.enums.Mod.lcommand); + if (wmod & 0b0001 > 0) m |= @backingInt(dvui.enums.Mod.lshift); + if (wmod & 0b0010 > 0) m |= @backingInt(dvui.enums.Mod.lcontrol); + if (wmod & 0b0100 > 0) m |= @backingInt(dvui.enums.Mod.lalt); + if (wmod & 0b1000 > 0) m |= @backingInt(dvui.enums.Mod.lcommand); - return @as(dvui.enums.Mod, @enumFromInt(m)); + return @as(dvui.enums.Mod, @fromBackingInt(@intCast(m))); } //pub fn addAllEvents(self: *WebBackend, win: *dvui.Window) !void { @@ -815,7 +815,7 @@ fn command(texture: ?dvui.Texture, maybe_clipr: ?dvui.Rect.Physical, index_start c.clip = @as(i32, b) << 1; } else if (texture) |t| { const blend = blends.get(@intFromPtr(t.ptr)) orelse .over; - c.clip = @as(i32, @intFromEnum(blend)) << 1; + c.clip = @as(i32, @backingInt(blend)) << 1; } if (maybe_clipr) |clipr| { c.clip |= 1; diff --git a/src/backend/backend_native.zig b/src/backend/backend_native.zig index 34e113811..21d4c74bb 100644 --- a/src/backend/backend_native.zig +++ b/src/backend/backend_native.zig @@ -120,7 +120,7 @@ pub const revealWindow = platform.window.reveal; /// the frame is restored on top of the chrome, so the chrome's own resizing cannot move it. pub fn restoreWindowState(win: *dvui.Window) void { platform.window.attach(win); - if (comptime builtin.os.tag == .macos) setWindowStyle(win); + if (comptime builtin.target.os.tag == .macos) setWindowStyle(win); if (win.backend.impl.init_opts_save) |opts| if (opts.pref_path) |dir| { layout_store_dir = dir; platform.geometry.setStore(.{ .load = layoutStoreLoad, .save = layoutStoreSave }); @@ -143,7 +143,7 @@ pub const WindowGlassLook = platform.window.WindowGlassLook; /// The main window's Liquid Glass this frame, where it is a window of Liquid Glass (macOS 26): /// whether it is — its colour is then under the glass, and the frame draws no base of its own. pub fn windowGlass(win: *dvui.Window, look: WindowGlassLook) bool { - if (comptime builtin.os.tag != .macos) return false; + if (comptime builtin.target.os.tag != .macos) return false; return platform.window.liquidGlassLook(win.backend.impl.window, look.native()); } @@ -159,7 +159,7 @@ var layout_store_dir: []const u8 = ""; fn layoutStoreLoad(_: ?*anyopaque) ?platform.geometry.Geometry { const gpa = std.heap.page_allocator; const f = loadWindowFile(gpa, layout_store_dir); - defer std.zon.parse.free(gpa, f); + defer zig017ZonFree(gpa, f); if (f.w < 1 or f.h < 1) return null; return .{ .x = f.x, .y = f.y, .w = f.w, .h = f.h, .state = if (f.maximized) .maximized else .normal }; } @@ -168,7 +168,7 @@ fn layoutStoreSave(_: ?*anyopaque, g: platform.geometry.Geometry) void { // Read-modify-write: the regions and tree on disk stay as they are. const gpa = std.heap.page_allocator; var f = loadWindowFile(gpa, layout_store_dir); - defer std.zon.parse.free(gpa, f); + defer zig017ZonFree(gpa, f); f.x = g.x; f.y = g.y; f.w = g.w; @@ -182,7 +182,7 @@ fn layoutStoreSave(_: ?*anyopaque, g: platform.geometry.Geometry) void { /// pane mid-transition; a zoom without a Space keeps the full strip. Fizzy's titlebar heights /// over the window's state (`platform.macos_monitor.titlebarState`). pub fn titlebarStripHeight(win: *dvui.Window) f32 { - if (builtin.os.tag != .macos) return Constants.titlebar_height; + if (builtin.target.os.tag != .macos) return Constants.titlebar_height; const t = platform.macos_monitor.titlebarState(win); const in: platform.window_layout.StripInputs = .{ .collapsed = t.collapsed, @@ -254,7 +254,7 @@ const native_menus: []const platform.menu.Menu = blk: { /// Build the macOS menu bar from `menu_model`. Once; safe to call again. pub fn setupMacOSMenuBar() void { - if (builtin.os.tag != .macos) return; + if (builtin.target.os.tag != .macos) return; platform.menu.install(native_menus, .{ .enabled = menuEnabled, .title = menuTitle, @@ -270,7 +270,7 @@ pub fn setupMacOSMenuBar() void { /// Let go of the menu bar before the editor its hooks read is freed. No-op off macOS. pub fn teardownMacOSMenuBar() void { - if (builtin.os.tag != .macos) return; + if (builtin.target.os.tag != .macos) return; platform.menu.deinit(); } @@ -320,7 +320,7 @@ fn menuTitle(_: ?*anyopaque, ref: platform.menu.Ref) ?[*:0]const u8 { /// Rebuild every plugin-contributed native menu and item from the host's registry: on every /// plugin load, unload and hide-toggle. Each item's tag is its index in `host.native_menu_items`. pub fn rebuildDynamicNativeMenus() void { - if (builtin.os.tag != .macos) return; + if (builtin.target.os.tag != .macos) return; const host = &fizzy.editor().app.host; var menus: std.ArrayListUnmanaged(platform.menu.ExtraMenu) = .empty; defer menus.deinit(alloc()); @@ -346,7 +346,7 @@ var native_recent_count: usize = 0; /// Fill the Recent Folders submenu from the recents, newest first. AppKit menus are retained /// state, so this has to run whenever the list changes. pub fn rebuildNativeRecentFolders() void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; const folders = fizzy.editor().app.recents.folders.items; const titles = alloc().alloc([]const u8, folders.len) catch return; defer alloc().free(titles); @@ -396,3 +396,27 @@ pub fn setDynamicNativeMenuShortcut(index: usize, key: ?[]const u8, modifier_mas pub fn setSdlAppMetadata(name: [*:0]const u8, version: [*:0]const u8, identifier: [*:0]const u8) void { _ = sdl3.SDL_SetAppMetadata(name, version, identifier); } +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} diff --git a/src/backend/file_assoc.zig b/src/backend/file_assoc.zig index 50808807d..81cf8af0a 100644 --- a/src/backend/file_assoc.zig +++ b/src/backend/file_assoc.zig @@ -79,20 +79,20 @@ pub const ExtAssoc = struct { }; pub fn registerAll() void { - if (comptime builtin.os.tag != .windows) return; + if (comptime builtin.target.os.tag != .windows) return; impl.registerAll() catch |err| { std.log.warn("file_assoc: register failed: {t}", .{err}); }; } pub fn unregisterAll() void { - if (comptime builtin.os.tag != .windows) return; + if (comptime builtin.target.os.tag != .windows) return; impl.unregisterAll() catch |err| { std.log.warn("file_assoc: unregister failed: {t}", .{err}); }; } -const impl = if (builtin.os.tag == .windows) WindowsImpl else NoopImpl; +const impl = if (builtin.target.os.tag == .windows) WindowsImpl else NoopImpl; const NoopImpl = struct { fn registerAll() !void {} diff --git a/src/backend/layout_file.zig b/src/backend/layout_file.zig index 162248396..3254f0c57 100644 --- a/src/backend/layout_file.zig +++ b/src/backend/layout_file.zig @@ -89,15 +89,15 @@ const legacy_window_file = "window.zon"; fn windowFilePath(buf: []u8, dir: []const u8, name: []const u8) ?[:0]const u8 { const sep = std.fs.path.sep_str; if (std.mem.endsWith(u8, dir, sep)) { - return std.fmt.bufPrintZ(buf, "{s}{s}", .{ dir, name }) catch null; + return std.fmt.bufPrintSentinel(buf, "{s}{s}", .{ dir, name }, 0) catch null; } - return std.fmt.bufPrintZ(buf, "{s}{s}{s}", .{ dir, sep, name }) catch null; + return std.fmt.bufPrintSentinel(buf, "{s}{s}{s}", .{ dir, sep, name }, 0) catch null; } /// Reads every field of `window.zon`, falling back to `SavedFrame`'s own defaults for whatever /// is missing or unparseable (never null — simplifies every caller, which only cares about the /// subset of fields it owns). -/// Reads `window.zon` into `gpa`-owned memory. Free with `std.zon.parse.free(gpa, frame)` — the +/// Reads `window.zon` into `gpa`-owned memory. Free with `zig017ZonFree(gpa, frame)` — the /// region list holds allocated names now, so the old return-by-value-and-forget will not do. pub fn loadWindowFile(gpa: std.mem.Allocator, dir: []const u8) SavedFrame { var path_buf: [1024]u8 = undefined; @@ -109,17 +109,17 @@ pub fn loadWindowFile(gpa: std.mem.Allocator, dir: []const u8) SavedFrame { break :blk readCapped(gpa, legacy) catch return .{}; }; defer gpa.free(data); - const data_z = gpa.dupeZ(u8, data) catch return .{}; + const data_z = gpa.dupeSentinel(u8, data, 0) catch return .{}; defer gpa.free(data_z); // `fromSliceAlloc`, not `fromSlice`: the region list holds allocated names, and `fromSlice` // asserts at comptime that the result contains no pointers. - return std.zon.parse.fromSliceAlloc( - SavedFrame, - gpa, - data_z, - null, - .{ .ignore_unknown_fields = true }, - ) catch .{}; + return (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(SavedFrame, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = data_z, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(SavedFrame, gpa, zig017_v) else |zig017_e| zig017_e; + }) catch .{}; } pub fn writeWindowFile(dir: []const u8, f: SavedFrame) void { @@ -141,7 +141,7 @@ pub fn writeWindowFile(dir: []const u8, f: SavedFrame) void { pub fn saveRegions(dir: []const u8, regions: []const SavedRegion) void { const gpa = std.heap.page_allocator; var f = loadWindowFile(gpa, dir); - defer std.zon.parse.free(gpa, f); + defer zig017ZonFree(gpa, f); const keep = f.regions; f.regions = regions; writeWindowFile(dir, f); @@ -154,7 +154,7 @@ pub fn saveRegions(dir: []const u8, regions: []const SavedRegion) void { pub fn saveTree(dir: []const u8, tree: ?core.widgets.DockLayout.Snapshot) void { const gpa = std.heap.page_allocator; var f = loadWindowFile(gpa, dir); - defer std.zon.parse.free(gpa, f); + defer zig017ZonFree(gpa, f); const keep = f.tree; f.tree = tree; writeWindowFile(dir, f); @@ -165,7 +165,7 @@ pub fn saveTree(dir: []const u8, tree: ?core.widgets.DockLayout.Snapshot) void { /// Caller owns the result (`DockLayout.deinit`). pub fn loadTree(gpa: std.mem.Allocator, dir: []const u8) ?core.widgets.DockLayout { const f = loadWindowFile(gpa, dir); - defer std.zon.parse.free(gpa, f); + defer zig017ZonFree(gpa, f); const snap = f.tree orelse return null; var dock = core.widgets.DockLayout.fromSnapshot(gpa, snap) catch return null; dock.animated = true; @@ -176,7 +176,7 @@ pub fn loadTree(gpa: std.mem.Allocator, dir: []const u8) ?core.widgets.DockLayou /// `freeRegions`. pub fn loadRegions(gpa: std.mem.Allocator, dir: []const u8) []SavedRegion { const f = loadWindowFile(gpa, dir); - defer std.zon.parse.free(gpa, f); + defer zig017ZonFree(gpa, f); const out = gpa.alloc(SavedRegion, f.regions.len) catch return &.{}; var n: usize = 0; for (f.regions) |r| { @@ -234,3 +234,64 @@ pub fn freeRegions(gpa: std.mem.Allocator, regions: []SavedRegion) void { } gpa.free(regions); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} diff --git a/src/editor/DocumentWatcher.zig b/src/editor/DocumentWatcher.zig index 68b720134..9921327e7 100644 --- a/src/editor/DocumentWatcher.zig +++ b/src/editor/DocumentWatcher.zig @@ -30,7 +30,7 @@ const debounce_ns: i128 = 200 * std.time.ns_per_ms; /// Same ceiling as the text plugin's open path — guards against hashing something huge. const max_hash_bytes: usize = 64 * 1024 * 1024; -pub const have_impl = switch (builtin.os.tag) { +pub const have_impl = switch (builtin.target.os.tag) { .macos, .linux, .windows => true, else => false, }; @@ -65,7 +65,7 @@ by_path: std.StringHashMapUnmanaged(u64) = .empty, /// `.modified` for regular files — the case nightwatch's README calls out as the supported way /// to watch single files. Its directory-tree trade-off (mtime-diff scans miss pure content /// writes) doesn't apply to us: we only ever pass file paths. -const doc_variant: if (have_impl) @import("nightwatch").Variant else void = switch (builtin.os.tag) { +const doc_variant: if (have_impl) @import("nightwatch").Variant else void = switch (builtin.target.os.tag) { .macos, .freebsd, .openbsd, .netbsd, .dragonfly => .kqueuedir, else => if (have_impl) @import("nightwatch").default_variant else {}, }; diff --git a/src/editor/Editor.zig b/src/editor/Editor.zig index 28138a09d..9d53529e1 100644 --- a/src/editor/Editor.zig +++ b/src/editor/Editor.zig @@ -25,7 +25,7 @@ const build_opts = @import("build_opts"); /// dvui's demo window (View › Show DVUI Demo), in Debug builds only. Calling it at all links the /// whole of it — every example, widgetpedia's raw images, the demos' own source for its code /// viewer — into a release, where no one opens it: on the web that is download size. -pub const dvui_demo = builtin.mode == .Debug; +pub const dvui_demo = builtin.mode == .debug; const fizzy = @import("../fizzy.zig"); const dvui = @import("dvui"); @@ -672,7 +672,7 @@ pub fn loadUserPlugins(editor: *Editor, config_folder: []const u8) void { const plugins_dir = std.fs.path.join(editor.app.gpa, &.{ config_folder, "plugins" }) catch return; defer editor.app.gpa.free(plugins_dir); - const ext_suffix: []const u8 = switch (builtin.os.tag) { + const ext_suffix: []const u8 = switch (builtin.target.os.tag) { .windows => ".dll", .macos => ".dylib", else => ".so", @@ -870,7 +870,7 @@ pub fn builtinManifest(editor: *Editor, id: []const u8) ?sdk.Manifest { inline for (bundled_plugins) |m| { if (std.mem.eql(u8, m.plugin_id, id)) { const frame_gpa = editor.app.arena.allocator(); - const zon = frame_gpa.dupeZ(u8, m.plugin_options.manifest_zon) catch return null; + const zon = frame_gpa.dupeSentinel(u8, m.plugin_options.manifest_zon, 0) catch return null; return sdk.Manifest.parse(frame_gpa, zon) catch return null; } } @@ -2256,7 +2256,7 @@ pub fn postInit(editor: *Editor) !void { Watch.wake.setHook(wakeEventLoop); // What a Windows build set aside to install over this executable while it ran. - if (comptime builtin.os.tag == .windows) { + if (comptime builtin.target.os.tag == .windows) { var buf: [std.fs.max_path_bytes]u8 = undefined; if (std.process.executablePath(dvui.io, &buf)) |n| ExecutableWatcher.removeSetAside(buf[0..n]) else |_| {} } @@ -2517,7 +2517,7 @@ fn fizzyIsMacOS(_: *anyopaque) bool { } fn fizzyAppliesNativeWindowOpacity(_: *anyopaque) bool { if (comptime builtin.target.cpu.arch == .wasm32) return false; - return builtin.os.tag == .macos or builtin.os.tag == .windows; + return builtin.target.os.tag == .macos or builtin.target.os.tag == .windows; } fn fizzyPanZoomScheme(ctx: *anyopaque) sdk.EditorAPI.PanZoomScheme { const editor = fizzyCtx(ctx); @@ -3050,7 +3050,7 @@ pub fn drawActiveCenter(editor: *Editor) !dvui.App.Result { self.slot.packed_children = 0; self.slot.total_weight = 0; self.slot.min_space_taken = 0; - if (builtin.mode == .Debug) self.slot.child_id = .zero; + if (builtin.mode == .debug) self.slot.child_id = .zero; } }; @@ -3894,7 +3894,7 @@ pub fn windowGlassLook(opacity: f32, size: dvui.Size.Natural) fizzy.backend.Wind /// there is none; clear at 0. Every window fizzy draws stands on it — the main window, a float's /// own window, the carry window (`Popout.base`) — so side by side they are one colour. pub fn windowBase(opacity: f32) dvui.Color { - const material = switch (builtin.os.tag) { + const material = switch (builtin.target.os.tag) { .macos, .windows => true, .linux => fizzy.editor().blurred_behind, else => false, @@ -3932,7 +3932,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { // frost over a blur at the full frost, and its own colour and bend. Elsewhere — the web, // Linux, Windows, where there is no OS glass to match — the app's frosted pane (`frosted`). const opacity = @max(if (dark) editor.app.settings.window_opacity_dark else editor.app.settings.window_opacity_light, glass_look.min_opacity); - const lens = builtin.os.tag == .macos and editor.app.settings.glass_shader; + const lens = builtin.target.os.tag == .macos and editor.app.settings.glass_shader; const pane = glass_look.frosted(opacity); fizzy.core.dialogs.publishStyle(.{ .modal_dim = editor.app.settings.modal_dim, @@ -3955,7 +3955,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { 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`). - fizzy.core.screens.publishMainTop(if (builtin.os.tag == .macos) fizzy.backend.titlebarStripHeight(dvui.currentWindow()) else 0); + fizzy.core.screens.publishMainTop(if (builtin.target.os.tag == .macos) fizzy.backend.titlebarStripHeight(dvui.currentWindow()) else 0); fizzy.core.LiquidField.publishEnabled(editor.app.settings.glass_shader); if (comptime builtin.target.cpu.arch == .wasm32) { // Plugins the page has finished linking since last frame register now. @@ -3983,7 +3983,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { // (`backend.blurBehind`), asked for while the window is translucent at all, its corners the // frame's (square maximized, as `draw` draws it). Behind `FIZZY_BLUR_BEHIND=1` until it has been // seen on GNOME and KDE (`plans/WINDOWS_LINUX_GLASS_PLAN.md`). - if (builtin.os.tag == .linux and Popout.envSwitch("FIZZY_BLUR_BEHIND") orelse false) { + if (builtin.target.os.tag == .linux and Popout.envSwitch("FIZZY_BLUR_BEHIND") orelse false) { const win = dvui.currentWindow(); const radius: f32 = if (fizzy.backend.isMaximized(win)) 0 else Constants.linux_window_radius; editor.blurred_behind = fizzy.backend.blurBehind(win, if (editor.window_opacity_anim.value > 0 and editor.window_opacity_anim.value < 1) radius else null); @@ -3991,7 +3991,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { // Translucent over the desktop's material on macOS and Windows, and on Linux where the // compositor blurs behind the window; opaque elsewhere, where the window's alpha is only its // shape (`LiquidField.publishOpaqueWindow`). - fizzy.core.LiquidField.publishOpaqueWindow(builtin.os.tag != .macos and builtin.os.tag != .windows and !editor.blurred_behind); + fizzy.core.LiquidField.publishOpaqueWindow(builtin.target.os.tag != .macos and builtin.target.os.tag != .windows and !editor.blurred_behind); // Out of a fullscreen Space, the window stops covering the desktop on AppKit's and SDL's own // time, after the transition, and nothing wakes a frame for it: it sat opaque until the mouse // moved (the user). From the moment it sets out of the Space, frames keep coming until it no @@ -4221,7 +4221,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { // And on Wayland the window's shadow is fizzy's too, drawn in a margin round the frame // that the compositor knows is not the window (`frameInsets`, zero when maximized, tiled // or full screen). - const linux_windowed = builtin.os.tag == .linux and !fizzy.backend.isMaximized(dvui.currentWindow()); + const linux_windowed = builtin.target.os.tag == .linux and !fizzy.backend.isMaximized(dvui.currentWindow()); const insets = fizzy.backend.frameInsets(dvui.currentWindow()); const shadowed = insets.x > 0 or insets.y > 0 or insets.w > 0 or insets.h > 0; var overall_box = dvui.box( @@ -4243,7 +4243,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { // Non-macOS: a thin strip below the top edge so the in-window title row (menu, etc.) is not flush // against the window border (complements the system caption area on Windows 11). - if (builtin.os.tag != .macos) { + if (builtin.target.os.tag != .macos) { var top_inset = dvui.box( @src(), .{ .dir = .horizontal }, @@ -4264,7 +4264,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { // overlay at the top-right corner (y=0) hosts the min/max/close buttons; a drag rect is pushed // across the top so empty space (gaps between widgets) drags the window. Menu items and sidebar // buttons push themselves as interactive rects so clicks on them still reach DVUI. - if (builtin.os.tag == .windows or builtin.os.tag == .linux) { + if (builtin.target.os.tag == .windows or builtin.target.os.tag == .linux) { fizzy.backend.resetTitleBarHints(); const window_rect_natural = dvui.windowRect(); @@ -4277,7 +4277,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { (insets.y + title_strip_h) * scale, @intFromFloat(window_rect_natural.w * scale), ); - } else if (builtin.os.tag == .macos) { + } else if (builtin.target.os.tag == .macos) { // Collapse while zoomed/fullscreen (chrome overlays on hover); grow with // AppKit safe-area inset when restoring to a normal window. const title_strip_h = fizzy.backend.titlebarStripHeight(dvui.currentWindow()); @@ -4303,7 +4303,7 @@ pub fn tick(editor: *Editor) !dvui.App.Result { // Top-right overlay on Windows and Linux: minimize / maximize / close, each in its // desktop's style (`caption_buttons`). Floating, so it takes no space in overall_box's // layout — the main UI below fills the entire window. - if (builtin.os.tag == .windows or builtin.os.tag == .linux) { + if (builtin.target.os.tag == .windows or builtin.target.os.tag == .linux) { const win_rect = dvui.windowRect(); const frame: dvui.Rect = .{ .x = insets.x, .y = insets.y, .w = win_rect.w - insets.x - insets.w, .h = win_rect.h - insets.y - insets.h }; caption_buttons.draw(frame, if (linux_windowed) Constants.linux_window_radius else null); @@ -5729,7 +5729,7 @@ pub fn paste(editor: *Editor) !void { /// Forwards `name`'s keybind (e.g. "copy"/"paste") as a synthetic key-down event to whichever /// widget currently holds dvui keyboard focus. On macOS, Cmd+C/Cmd+V only ever reach us as a -/// native menu action (see `Keybinds.tick`'s `builtin.os.tag != .macos` guard), and that path +/// native menu action (see `Keybinds.tick`'s `builtin.target.os.tag != .macos` guard), and that path /// otherwise only knows how to talk to the active *document* (`runActiveDocCommand`). Any other /// focused widget with its own copy/paste handling — fizzy's Output Panel, a plugin's search /// box, dvui's own text-selection widgets — never sees the raw keystroke at all, unlike on @@ -6055,7 +6055,7 @@ pub fn openInFileBrowser(editor: *Editor, path: []const u8) !void { // `environ` array captured at startup, which any `unsetenv` elsewhere in the process (SDL, // a plugin, a system framework) shrinks *in place* — leaving a NULL before the captured // length and segfaulting the whole app on the next spawn. See `darwin_spawn.zig`. - if (builtin.os.tag == .macos) { + if (builtin.target.os.tag == .macos) { // `posix_spawn` (unlike `posix_spawnp`) does not search `$PATH`, so name `open` in full. const child = fizzy.core.darwin_spawn.spawn(editor.app.gpa, .{ .argv = &.{ "/usr/bin/open", path }, @@ -6080,7 +6080,7 @@ pub fn openInFileBrowser(editor: *Editor, path: []const u8) !void { } // `start` is a cmd.exe builtin, not a standalone executable, so spawning it directly // (bypassing the shell) always fails on Windows — reveal via explorer.exe instead. - if (builtin.os.tag == .windows) { + if (builtin.target.os.tag == .windows) { const arg = try std.fmt.allocPrint(editor.app.gpa, "/select,{s}", .{path}); defer editor.app.gpa.free(arg); _ = std.process.run(editor.app.gpa, dvui.io, .{ .argv = &.{ "explorer.exe", arg } }) catch { diff --git a/src/editor/KeptDocuments.zig b/src/editor/KeptDocuments.zig index 895309273..b4b4c7bc8 100644 --- a/src/editor/KeptDocuments.zig +++ b/src/editor/KeptDocuments.zig @@ -359,7 +359,13 @@ pub fn loadSession(gpa: std.mem.Allocator, dir: []const u8) ?KeptDocuments { defer gpa.free(text); var arena: std.heap.ArenaAllocator = .init(gpa); defer arena.deinit(); - const saved = std.zon.parse.fromSliceAlloc(Saved, arena.allocator(), text, null, .{ .ignore_unknown_fields = true }) catch |err| { + const saved = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(arena.allocator()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Saved, .{ .gpa = arena.allocator(), .arena = zig017_arena.allocator(), .source = text, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Saved, arena.allocator(), zig017_v) else |zig017_e| zig017_e; + }) catch |err| { dvui.log.warn("restart: the last session could not be read ({t}); documents open from disk", .{err}); return null; }; @@ -489,3 +495,40 @@ fn recover(editor: *Editor, path: []const u8, unsaved: []const u8) void { dvui.log.warn("reload: the unsaved contents of {s} are in {s}", .{ path, out }); dvui.toast(@src(), .{ .message = std.fmt.allocPrint(editor.app.arena.allocator(), "Unsaved changes to {s} were kept in {s}", .{ std.fs.path.basename(path), out }) catch "Unsaved changes were kept in fizzy's recovered folder." }); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/src/editor/KeybindSettings.zig b/src/editor/KeybindSettings.zig index 1bb1433bb..58869a337 100644 --- a/src/editor/KeybindSettings.zig +++ b/src/editor/KeybindSettings.zig @@ -704,7 +704,7 @@ fn directShortcut(editor: *fizzy.Editor, id: []const u8, platform: Keymap.Platfo // Prefer the highest-source binding (user > plugin > profile > dvui). var best = found[0]; for (found[1..]) |b| { - if (@intFromEnum(b.source) >= @intFromEnum(best.source)) best = b; + if (@backingInt(b.source) >= @backingInt(best.source)) best = b; } const keys = Keymap.formatKeys(arena, best.stroke, platform) catch return null; return .{ .keys = keys, .stroke = best.stroke }; diff --git a/src/editor/Keybinds.zig b/src/editor/Keybinds.zig index 2956f429b..c8f29d887 100644 --- a/src/editor/Keybinds.zig +++ b/src/editor/Keybinds.zig @@ -42,7 +42,7 @@ const menu_model = @import("menu_model.zig"); /// "fizzy default unless a plugin wants it" — that needs the layered `Command.default_keys` /// resolution in step C, not another entry in this function. /// -/// Runtime mac detection — `builtin.os.tag.isDarwin()` is `false` for +/// Runtime mac detection — `builtin.target.os.tag.isDarwin()` is `false` for /// wasm32-freestanding, so macOS web users would otherwise get the Windows (Ctrl) /// bindings. `fizzy.core.platform.isMacOS()` reads DVUI's `navigator.platform`-derived /// choice on web and uses `os.tag` on native. @@ -883,8 +883,8 @@ fn shadowedByHigherLayer(editor: *Editor, binding: Keymap.Binding, command_id: [ else if (binding.owner_id) |b| (if (other.owner_id) |o| std.mem.eql(u8, b, o) else false) else false; if (!same_owner) continue; - const mine = (@as(u16, @intFromEnum(binding.source)) << 8) | binding.when.weight(); - const theirs = (@as(u16, @intFromEnum(other.source)) << 8) | other.when.weight(); + const mine = (@as(u16, @backingInt(binding.source)) << 8) | binding.when.weight(); + const theirs = (@as(u16, @backingInt(other.source)) << 8) | other.when.weight(); if (theirs > mine) return true; } return false; @@ -899,7 +899,7 @@ fn shadowedByHigherLayer(editor: *Editor, binding: Keymap.Binding, command_id: [ /// theirs (`backend_native.rebuildDynamicNativeMenus` tags them the same way). Neither needs a /// command↔item table: both name a command, and the chord comes from the keymap. pub fn syncNativeMenuShortcuts(editor: *Editor) void { - if (comptime builtin.os.tag != .macos) return; + if (comptime builtin.target.os.tag != .macos) return; for (menu_model.flat_commands, 0..) |item, tag| { if (nativeShortcutFor(editor, item.id)) |s| { @@ -962,7 +962,7 @@ fn bestBinding(editor: *Editor, command: []const u8) ?Keymap.Binding { if (b.args.len > 0) continue; if (!std.mem.eql(u8, cmd, command)) continue; if (best) |cur| { - if (@intFromEnum(b.source) >= @intFromEnum(cur.source)) best = b; + if (@backingInt(b.source) >= @backingInt(cur.source)) best = b; } else best = b; } return best; @@ -977,7 +977,7 @@ fn bestBinding(editor: *Editor, command: []const u8) ?Keymap.Binding { /// equivalents, AppKit runs them, and a command reachable from both paths would fire twice. /// This is the exact condition `syncNativeMenuShortcuts` stamps, so the two cannot disagree. pub fn nativeMenuOwnsChord(editor: *Editor, id: []const u8) bool { - if (comptime builtin.os.tag != .macos) return false; + if (comptime builtin.target.os.tag != .macos) return false; if (!menu_model.contains(id) and !nativeMenuItemFor(editor, id)) return false; return nativeShortcutFor(editor, id) != null; } @@ -1231,7 +1231,7 @@ pub fn webKeyBound(key_len: usize, mods_bits: u32) bool { }; var buf: [16]u8 = undefined; const key = webKey(web_key_buf[0..key_len], &buf) orelse return false; - const function_key = @intFromEnum(key) >= @intFromEnum(Keymap.Key.f1) and @intFromEnum(key) <= @intFromEnum(Keymap.Key.f25); + const function_key = @backingInt(key) >= @backingInt(Keymap.Key.f1) and @backingInt(key) <= @backingInt(Keymap.Key.f25); if (!mods.ctrl and !mods.command and !function_key) return false; // Recording a new binding: every such chord is data for the settings pane, not the browser's. diff --git a/src/editor/Popout.zig b/src/editor/Popout.zig index be816ab8c..82df6d6b5 100644 --- a/src/editor/Popout.zig +++ b/src/editor/Popout.zig @@ -257,7 +257,7 @@ fn popOut(f: *Floats.Float) void { if (o.* == null) break o; } else return; var title_buf: [96]u8 = undefined; - const title = std.fmt.bufPrintZ(&title_buf, "{s}", .{f.titleText()}) catch "Fizzy"; + const title = std.fmt.bufPrintSentinel(&title_buf, "{s}", .{f.titleText()}, 0) catch "Fizzy"; const s = dvui.windowNaturalScale(); // Where it goes in the main window's frame, whole — not where it was last drawn: made last // frame, it may have been drawn as the carried glass it lands from, a drop's size. diff --git a/src/editor/caption_buttons.zig b/src/editor/caption_buttons.zig index 82ea11b09..e347f2e24 100644 --- a/src/editor/caption_buttons.zig +++ b/src/editor/caption_buttons.zig @@ -17,7 +17,7 @@ const Constants = @import("Constants.zig"); const Button = fizzy.backend.TitleBarButton; pub const Style = enum { windows, gnome }; -pub const style: Style = if (builtin.os.tag == .linux) .gnome else .windows; +pub const style: Style = if (builtin.target.os.tag == .linux) .gnome else .windows; /// Draws the buttons in the top-right corner of `frame`, the window's frame within it (inside the /// margin for its shadow on Linux). `corner_radius`: the window's own where fizzy rounds it @@ -54,7 +54,7 @@ fn drawWindows(frame: dvui.Rect, corner_radius: ?f32) void { var b = dvui.box(@src(), .{ .dir = .horizontal }, .{ .role = .button, .label = .{ .text = nameOf(button, maximized) }, - .id_extra = @intFromEnum(button), + .id_extra = @backingInt(button), .min_size_content = .{ .w = cell_w, .h = cell_h }, .expand = .vertical, .background = hover, @@ -94,7 +94,7 @@ fn drawGnome(frame: dvui.Rect) void { var b = dvui.box(@src(), .{ .dir = .horizontal }, .{ .role = .button, .label = .{ .text = nameOf(button, maximized) }, - .id_extra = @intFromEnum(button), + .id_extra = @backingInt(button), .min_size_content = .{ .w = d, .h = d }, .margin = .{ .x = if (i == 0) 0 else gap }, }); diff --git a/src/editor/captures.zig b/src/editor/captures.zig index 07cd049b2..8e752e941 100644 --- a/src/editor/captures.zig +++ b/src/editor/captures.zig @@ -76,7 +76,7 @@ fn writeMeta(editor: *Editor, w: *std.Io.Writer, arena: std.mem.Allocator, why: .why = why, .seconds = seconds, .frames = frames, - .fizzy = .{ .version = fizzy.version, .optimize = @tagName(builtin.mode), .os = @tagName(builtin.os.tag), .arch = @tagName(builtin.cpu.arch) }, + .fizzy = .{ .version = fizzy.version, .optimize = @tagName(builtin.mode), .os = @tagName(builtin.target.os.tag), .arch = @tagName(builtin.target.cpu.arch) }, .window = .{ .w = px.w, .h = px.h, .scale = dvui.windowNaturalScale() }, .plugins = plugins.items, }, .{}, w); diff --git a/src/editor/dialogs/AboutFizzy.zig b/src/editor/dialogs/AboutFizzy.zig index b63491d91..c74aa5b5e 100644 --- a/src/editor/dialogs/AboutFizzy.zig +++ b/src/editor/dialogs/AboutFizzy.zig @@ -234,7 +234,7 @@ pub fn dialog(id: dvui.Id) anyerror!bool { _ = dvui.spacer(@src(), .{ .min_size_content = .{ .w = 8, .h = 8 } }); if (comptime auto_update.impl) { - if (!auto_update.installLayoutSupported(dvui.io) and builtin.os.tag == .macos) { + if (!auto_update.installLayoutSupported(dvui.io) and builtin.target.os.tag == .macos) { dvui.labelNoFmt( @src(), "In-app updates need a packaged .app (Velopack). A zig-out binary is not an app bundle.", diff --git a/src/editor/dialogs/WebSaveAs.zig b/src/editor/dialogs/WebSaveAs.zig index 962d1e89c..6b9e14ad4 100644 --- a/src/editor/dialogs/WebSaveAs.zig +++ b/src/editor/dialogs/WebSaveAs.zig @@ -185,10 +185,10 @@ fn exportFileName(gpa: std.mem.Allocator, name: []const u8, patterns: []const u8 var first: []const u8 = ""; var it = std.mem.tokenizeScalar(u8, patterns, ';'); while (it.next()) |pattern| { - if (std.mem.eql(u8, pattern, "*")) return gpa.dupeZ(u8, name); - if (typed.len > 0 and std.ascii.eqlIgnoreCase(pattern, typed)) return gpa.dupeZ(u8, name); + if (std.mem.eql(u8, pattern, "*")) return gpa.dupeSentinel(u8, name, 0); + if (typed.len > 0 and std.ascii.eqlIgnoreCase(pattern, typed)) return gpa.dupeSentinel(u8, name, 0); if (first.len == 0) first = pattern; } - if (first.len == 0) return gpa.dupeZ(u8, name); + if (first.len == 0) return gpa.dupeSentinel(u8, name, 0); return std.fmt.allocPrintSentinel(gpa, "{s}.{s}", .{ name, first }, 0); } diff --git a/src/editor/explorer/settings.zig b/src/editor/explorer/settings.zig index 762789e99..b8f03fa36 100644 --- a/src/editor/explorer/settings.zig +++ b/src/editor/explorer/settings.zig @@ -315,7 +315,7 @@ const window_items = [_]Item{ .draw = drawRestartOnRebuild, .inline_control = true, }, -}) ++ (if (builtin.os.tag != .macos or builtin.target.cpu.arch == .wasm32) [_]Item{} else [_]Item{ +}) ++ (if (builtin.target.os.tag != .macos or builtin.target.cpu.arch == .wasm32) [_]Item{} else [_]Item{ .{ .label = "Native glass", .key = "native_glass", diff --git a/src/editor/layout.zig b/src/editor/layout.zig index 6e2436426..dc733a171 100644 --- a/src/editor/layout.zig +++ b/src/editor/layout.zig @@ -35,7 +35,7 @@ pub fn layout(ctx: ?*anyopaque, f: *Layout) !dvui.App.Result { editor.infobar.draw(editor) catch dvui.log.err("Failed to draw infobar", .{}); - if (builtin.os.tag != .macos or Menu.debug_force_on_macos) { + if (builtin.target.os.tag != .macos or Menu.debug_force_on_macos) { const r = try Menu.draw(editor); if (r != .ok) return r; } diff --git a/src/editor/verdict.zig b/src/editor/verdict.zig index 97a8796e4..0e690246a 100644 --- a/src/editor/verdict.zig +++ b/src/editor/verdict.zig @@ -126,7 +126,7 @@ var path: []const u8 = ""; var expect: Expect = .{}; var start_ns: i128 = 0; var waited_frames: u32 = 0; -var failures: [@typeInfo(Failure).@"enum".fields.len]Failure = undefined; +var failures: [@typeInfo(Failure).@"enum".field_names.len]Failure = undefined; var failure_count: usize = 0; var ended_buf: [64]u8 = undefined; var tape_buf: [128]u8 = undefined; @@ -188,9 +188,15 @@ pub fn init(gpa: std.mem.Allocator, process_io: std.Io, profile: ?[]const u8) ?u fn readExpect(gpa: std.mem.Allocator, file: []const u8) !Expect { const bytes = try std.Io.Dir.cwd().readFileAlloc(io, file, gpa, .limited(1 << 16)); defer gpa.free(bytes); - const source = try gpa.dupeZ(u8, bytes); + const source = try gpa.dupeSentinel(u8, bytes, 0); defer gpa.free(source); - return std.zon.parse.fromSlice(Expect, gpa, source, null, .{}); + return (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Expect, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = source, .diagnostics = &zig017_zd }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Expect, gpa, zig017_v) else |zig017_e| zig017_e; + }); } /// Once a displayed frame, after the app's: watch the tape start and end. True: the tape has @@ -360,3 +366,40 @@ fn over(limit: ?u32, value: u32) bool { const max = limit orelse return false; return value > max; } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} diff --git a/tests/bench/bench_tape.zig b/tests/bench/bench_tape.zig index d142f0658..6c0dfd740 100644 --- a/tests/bench/bench_tape.zig +++ b/tests/bench/bench_tape.zig @@ -84,7 +84,7 @@ test "bench tape: ZON against the binary form" { var zon: std.Io.Writer.Allocating = .init(std.testing.allocator); defer zon.deinit(); const zon_write = try best(zonWrite, .{ t, &zon }); - const source = try std.testing.allocator.dupeZ(u8, zon.written()); + const source = try std.testing.allocator.dupeSentinel(u8, zon.written(), 0); defer std.testing.allocator.free(source); const zon_read = try best(zonRead, .{source}); diff --git a/tests/integration.zig b/tests/integration.zig index 4b0e13f49..16872511f 100644 --- a/tests/integration.zig +++ b/tests/integration.zig @@ -688,8 +688,8 @@ fn markdownSettle() !void { // always the same shape: which blocks are still owed a measure, and in what state. Printing // the tally is what turned "the layout never settles" into "164 of 185 blocks were never // measured once, because the resettle budget was gated on being near the viewport". - var counts = [_]usize{0} ** 4; - for (md_preview.rs.blocks.heights.items) |e| counts[@intFromEnum(e.state)] += 1; + var counts = @as([4]usize, @splat(0)); + for (md_preview.rs.blocks.heights.items) |e| counts[@backingInt(e.state)] += 1; std.debug.print( "\nnever settled: blocks={d} estimated={d} measured={d} settled={d} deferred={d} pending_measure={d}\n", .{ md_preview.rs.blocks.heights.items.len, counts[0], counts[1], counts[2], counts[3], md_render_ast.stats.pending_measure }, @@ -1619,8 +1619,8 @@ test "markdown preview: stops asking for frames so the app can sleep" { try markdownAwaitParse(); if (!try markdownReachesIdle()) { - var counts = [_]usize{0} ** 4; - for (md_preview.rs.blocks.heights.items) |e| counts[@intFromEnum(e.state)] += 1; + var counts = @as([4]usize, @splat(0)); + for (md_preview.rs.blocks.heights.items) |e| counts[@backingInt(e.state)] += 1; std.debug.print( "\nnever idle: estimated={d} measured={d} settled={d} deferred={d} pending_measure={d}\n", .{ counts[0], counts[1], counts[2], counts[3], md_render_ast.stats.pending_measure }, @@ -3762,7 +3762,7 @@ test "a place closing for good is not still holding its padding and its sash" { const DropFinishFrame = struct { var editor: ?*fizzy.Editor = null; - const key: dvui.Id = @enumFromInt(0xd7_0b_f1_4e); + const key: dvui.Id = @fromBackingInt(@intCast(0xd7_0b_f1_4e)); fn frame() anyerror!dvui.App.Result { const e = editor.?; @@ -4319,7 +4319,7 @@ test "drop: a place with room for the wheel keeps it" { const ShapeFrame = struct { var wheel: DZ.Wheel = undefined; - const key: dvui.Id = @enumFromInt(0x5a_a9_e0_01); + const key: dvui.Id = @fromBackingInt(@intCast(0x5a_a9_e0_01)); fn frame() anyerror!dvui.App.Result { _ = DZ.draw(key, wheel, 1, .{}); @@ -5162,7 +5162,13 @@ test "verdict: the soak tape is a demo, and its expectations read" { // It ends on a wait, so what closed last has time to close before the windows are counted. try std.testing.expect(owned.tape.ops[owned.tape.ops.len - 1].do == .wait); - const expect = try std.zon.parse.fromSlice(fizzy.verdict.Expect, std.testing.allocator, @embedFile("soak_expect"), null, .{}); + const expect = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(fizzy.verdict.Expect, .{ .gpa = std.testing.allocator, .arena = zig017_arena.allocator(), .source = @embedFile("soak_expect"), .diagnostics = &zig017_zd }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(fizzy.verdict.Expect, std.testing.allocator, zig017_v) else |zig017_e| zig017_e; + }); try std.testing.expect(!expect.allow_leaks); try std.testing.expectEqual(@as(u32, 0), expect.max_sdl_errors); try std.testing.expectEqual(@as(?u32, null), expect.os_windows); @@ -5178,7 +5184,7 @@ test "demo: the hand-written sample tape parses and round-trips" { var out: std.Io.Writer.Allocating = .init(std.testing.allocator); defer out.deinit(); try owned.tape.write(&out.writer); - const again = try std.testing.allocator.dupeZ(u8, out.written()); + const again = try std.testing.allocator.dupeSentinel(u8, out.written(), 0); defer std.testing.allocator.free(again); var back = try automation.Tape.parse(std.testing.allocator, again, automation.Input.check); defer back.deinit(); @@ -5330,10 +5336,16 @@ test "float: the saved layout brings it back, and Reset Layout takes it away" { defer out.deinit(); try std.zon.stringify.serializeMaxDepth(regions, .{}, &out.writer, 64); gpa.free(regions); - const text = try gpa.dupeZ(u8, out.written()); + const text = try gpa.dupeSentinel(u8, out.written(), 0); defer gpa.free(text); - const saved = try std.zon.parse.fromSliceAlloc([]fizzy.backend.SavedRegion, gpa, text, null, .{ .ignore_unknown_fields = true }); - defer std.zon.parse.free(gpa, saved); + const saved = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice([]fizzy.backend.SavedRegion, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = text, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe([]fizzy.backend.SavedRegion, gpa, zig017_v) else |zig017_e| zig017_e; + }); + defer zig017ZonFree(gpa, saved); editor.app.layout.resetLayout(gpa); try dvui.testing.settle(ManyPanelFrame.frame); @@ -5383,7 +5395,7 @@ test "float: a drag aims at a float's place over it, at nothing over its header, .rect = .{ .x = 100, .y = 50, .w = 150, .h = 130 }, .home = "Panel", .fresh = false, - .win_id = @enumFromInt(0xf10a7), + .win_id = @fromBackingInt(@intCast(0xf10a7)), .bounds = window, .header = header, }); @@ -5437,7 +5449,7 @@ const FloatOverMain = struct { .rect = .{ .x = 100, .y = 50, .w = 150, .h = 130 }, .home = "Panel", .fresh = false, - .win_id = @enumFromInt(0xf10a7), + .win_id = @fromBackingInt(@intCast(0xf10a7)), .bounds = win, .header = head, }); @@ -6656,17 +6668,29 @@ const HeardEvents = struct { fn handler(state: *anyopaque, e: sdk.events.Event) void { const self: *HeardEvents = @ptrCast(@alignCast(state)); const gpa = std.heap.smp_allocator; - const src = gpa.dupeZ(u8, e.payload) catch return; + const src = gpa.dupeSentinel(u8, e.payload, 0) catch return; defer gpa.free(src); // A batch of disk changes is heard by its first path, everything else by its `.path`. const path = if (std.mem.eql(u8, e.name, "fizzy.files.changed")) files: { - const changed = std.zon.parse.fromSliceAlloc(sdk.events.FilesChanged, gpa, src, null, .{ .ignore_unknown_fields = true }) catch return; - defer std.zon.parse.free(gpa, changed); + const changed = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(sdk.events.FilesChanged, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = src, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(sdk.events.FilesChanged, gpa, zig017_v) else |zig017_e| zig017_e; + }) catch return; + defer zig017ZonFree(gpa, changed); if (changed.events.len == 0) return; break :files gpa.dupe(u8, changed.events[0].path) catch return; } else plain: { - const payload = std.zon.parse.fromSliceAlloc(sdk.events.Path, gpa, src, null, .{ .ignore_unknown_fields = true }) catch return; - defer std.zon.parse.free(gpa, payload); + const payload = (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(gpa); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(sdk.events.Path, .{ .gpa = gpa, .arena = zig017_arena.allocator(), .source = src, .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(sdk.events.Path, gpa, zig017_v) else |zig017_e| zig017_e; + }) catch return; + defer zig017ZonFree(gpa, payload); break :plain gpa.dupe(u8, payload.path) catch return; }; self.events.append(gpa, .{ .name = gpa.dupe(u8, e.name) catch return, .path = path }) catch gpa.free(path); @@ -6969,7 +6993,13 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes .ok => |r| r orelse return error.TestUnexpectedResult, else => return error.TestUnexpectedResult, }; - return std.zon.parse.fromSliceAlloc(Read, h.arena(), try h.arena().dupeZ(u8, zon), null, .{ .ignore_unknown_fields = true }); + return (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(h.arena()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Read, .{ .gpa = h.arena(), .arena = zig017_arena.allocator(), .source = try h.arena().dupeSentinel(u8, zon, 0), .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Read, h.arena(), zig017_v) else |zig017_e| zig017_e; + }); } }.read; @@ -7007,7 +7037,13 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes try std.testing.expectEqualStrings(notes, front.owner.documentPath(front)); const Caret = struct { caret: struct { line: u32, column: u32 } }; const out = host.callCommand("text.read", notes_args, host.arena()).ok.?; - const at = try std.zon.parse.fromSliceAlloc(Caret, host.arena(), try host.arena().dupeZ(u8, out), null, .{ .ignore_unknown_fields = true }); + const at = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(host.arena()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Caret, .{ .gpa = host.arena(), .arena = zig017_arena.allocator(), .source = try host.arena().dupeSentinel(u8, out, 0), .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Caret, host.arena(), zig017_v) else |zig017_e| zig017_e; + }); try std.testing.expectEqual(@as(u32, 3), at.caret.line); try std.testing.expectEqual(@as(u32, 4), at.caret.column); } @@ -7235,7 +7271,13 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes // has passed on screen, which a few test frames are not. profile: struct { frame: struct { fps: f64 } }, }; - const stats = try std.zon.parse.fromSliceAlloc(Stats, host.arena(), try host.arena().dupeZ(u8, zon), null, .{ .ignore_unknown_fields = true }); + const stats = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(host.arena()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Stats, .{ .gpa = host.arena(), .arena = zig017_arena.allocator(), .source = try host.arena().dupeSentinel(u8, zon, 0), .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Stats, host.arena(), zig017_v) else |zig017_e| zig017_e; + }); try std.testing.expect(for (stats.plugins) |pl| { if (std.mem.eql(u8, pl.id, "devtools")) break pl.link == .bundled; } else false); @@ -7255,7 +7297,13 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes else => return error.TestUnexpectedResult, }; const Written = struct { path: []const u8, frames: u64 }; - const written = try std.zon.parse.fromSliceAlloc(Written, host.arena(), try host.arena().dupeZ(u8, zon), null, .{ .ignore_unknown_fields = true }); + const written = try (zig017_zon: { + var zig017_zd: std.zon.parse.Diagnostics = undefined; + var zig017_arena: std.heap.ArenaAllocator = .init(host.arena()); + defer zig017_arena.deinit(); + const zig017_r = std.zon.parse.fromSlice(Written, .{ .gpa = host.arena(), .arena = zig017_arena.allocator(), .source = try host.arena().dupeSentinel(u8, zon, 0), .diagnostics = &zig017_zd, .ignore_unknown_fields = true }); + break :zig017_zon if (zig017_r) |zig017_v| zig017ZonDupe(Written, host.arena(), zig017_v) else |zig017_e| zig017_e; + }); try std.testing.expect(written.frames > 0); const input_path = try std.fs.path.join(host.arena(), &.{ written.path, "input.bin" }); const bytes = try std.Io.Dir.cwd().readFileAlloc(dvui.io, input_path, host.arena(), .unlimited); @@ -7433,7 +7481,7 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes // A quit straight after a pane slid shut (`snapshotClosingPane`): the pane still holds its // picture, and the quit comes after the last frame, when nothing can be freed at a frame's end. { - const pixels = [_]u8{0} ** (4 * 4 * 4); + const pixels: [4 * 4 * 4]u8 = @splat(0); const snap = try t.window.backend.textureCreate(&pixels, .{ .width = 4, .height = 4 }); editor.workbench.workspaces.values()[0].closing_snapshot = snap; } @@ -7620,7 +7668,7 @@ test "automation service: refused while something drives the app, or for a demo' // A demo's tape opens on a keyframe; garbage is no tape at all. Both say why. try std.testing.expectEqualStrings("KeyframeInLiveTape", api.play(demo).invalid); try std.testing.expect(api.play("not a tape") == .invalid); - try std.testing.expect(api.outcome(@enumFromInt(12345)) == .unknown); + try std.testing.expect(api.outcome(@fromBackingInt(@intCast(12345))) == .unknown); // A demo loaded: refused, and nothing is settled while it plays. service_demo_loaded = true; @@ -7631,7 +7679,7 @@ test "automation service: refused while something drives the app, or for a demo' // One tape at a time, and only its own ticket stops it. const ticket = try started(api.play(live)); try std.testing.expect(api.play(live) == .busy); - api.stop(@enumFromInt(@intFromEnum(ticket) + 1)); + api.stop(@fromBackingInt(@intCast(@backingInt(ticket) + 1))); try std.testing.expect(api.outcome(ticket) == .playing); api.stop(ticket); try std.testing.expect(api.outcome(ticket) == .stopped); @@ -7700,7 +7748,11 @@ test "the output panel settles at its tail with a line wider than it anywhere in defer t.deinit(); defer fizzy.OutputLog.clear(); - const wide = "a line far wider than the panel " ** 12; + const wide = comptime blk: { + var s: []const u8 = ""; + for (0..12) |_| s = s ++ "a line far wider than the panel "; + break :blk s; + }; const rows = 40; // The wide line at each of the last rows in turn, wherever the top of the view falls. for (0..20) |from_tail| { @@ -7871,3 +7923,64 @@ test "a live tape drags a floating window by its header" { std.debug.print("float x {d:.1} (from 300) after {d} frames\n", .{ live_float.x, frames }); try std.testing.expectApproxEqAbs(@as(f32, 150), live_float.x, 2); } + +// ZIG017 stand-in: a deep copy of a zon result out of the parse's arena into the caller's +// allocator, where the code before 0.17 expects to free it. Goes with the zon arena PR. +fn zig017ZonDupe(comptime T: type, gpa: std.mem.Allocator, value: T) error{OutOfMemory}!T { + switch (@typeInfo(T)) { + .pointer => |p| switch (p.size) { + .one => { + const out = try gpa.create(p.child); + out.* = try zig017ZonDupe(p.child, gpa, value.*); + return out; + }, + .slice => { + const out = if (comptime p.sentinel()) |s| try gpa.allocSentinel(p.child, value.len, s) else try gpa.alloc(p.child, value.len); + for (out, value) |*o, v| o.* = try zig017ZonDupe(p.child, gpa, v); + return out; + }, + else => comptime unreachable, + }, + .array => |a| { + var out: T = undefined; + for (&out, value) |*o, v| o.* = try zig017ZonDupe(a.child, gpa, v); + return out; + }, + .@"struct" => |s| { + var out: T = value; + inline for (s.field_names, s.field_types, s.field_attrs) |n, FT, attrs| { + if (!attrs.@"comptime") @field(out, n) = try zig017ZonDupe(FT, gpa, @field(value, n)); + } + return out; + }, + .@"union" => |u| if (u.tag_type == null) return value else switch (value) { + inline else => |payload, tag| return @unionInit(T, @tagName(tag), try zig017ZonDupe(@TypeOf(payload), gpa, payload)), + }, + .optional => |o| return if (value) |v| try zig017ZonDupe(o.child, gpa, v) else null, + else => return value, + } +} +// ZIG017 stand-in: 0.16's `std.zon.parse.free`, which 0.17 removed (results now come from an +// arena the caller owns). Goes with the zon arena PR. +fn zig017ZonFree(gpa: std.mem.Allocator, value: anytype) void { + switch (@typeInfo(@TypeOf(value))) { + .pointer => |p| switch (p.size) { + .one => { + zig017ZonFree(gpa, value.*); + gpa.destroy(value); + }, + .slice => { + for (value) |item| zig017ZonFree(gpa, item); + gpa.free(value); + }, + else => comptime unreachable, + }, + .array => for (value) |item| zig017ZonFree(gpa, item), + .@"struct" => |s| inline for (s.field_names) |n| zig017ZonFree(gpa, @field(value, n)), + .@"union" => |u| if (u.tag_type != null) switch (value) { + inline else => |payload| zig017ZonFree(gpa, payload), + }, + .optional => if (value) |some| zig017ZonFree(gpa, some), + else => {}, + } +} From 9a22df2e68192b1f24c0cb568489306ffa73956a Mon Sep 17 00:00:00 2001 From: Colton Franklin <49629865+foxnne@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:10:11 -0500 Subject: [PATCH 02/23] zig: the build's custom steps become helper programs (#438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of fizzyedit/plans#5 (`plans/ZIG_0_17_PLAN.md`, step 2: the build system). **Stacked on #437**: based on `zig/017-scaffold`, so this diff is only the app's build-system work. The plugin dev install and `-Dplugin-use-llvm` moved down into #437. It goes to `zig/0.17` once #437 merges. ## What changes Zig 0.17 runs `build.zig` in a configurer and the graph in a separate maker. There are no custom steps (`makeFn`), no install paths at configure time, and the configuration is cached. - **"Set aside a running executable"** (Windows hosts) becomes `build/set_aside.zig`, run before the install. Its directory comes from a new `common.installDirPath`: a `LazyPath` relative to the install prefix, resolved when the build runs, in place of `b.getInstallPath`. The packaging script's `--packDir` uses the same helper. - **The `update` step and root `update.zig` are deleted.** They pulled dvui-dev `main` and a zigwin32 0.16 branch, neither of which fizzy pins any more. - **Configure-time reads are declared** (`dependOnFileContents`/`Metadata`/`DirectoryContents`): `VERSION`, each plugin's `plugin.zig.zon`, `web/index.html`, the web plugin checkouts and their `web/` dirs, `release.yml`, `patches/sdl`. Without that, 0.17 reuses a stale configuration (checked: a changed `VERSION` now reaches the binary). Existence checks and directory opens go through `b.root`'s own `Path` API. - **A path that may not exist** (a web plugin checkout and its `web/` directory; #437 does the same for a plugin's `release.yml`) is declared by `dependOnPathExistence`: the file when it exists, otherwise the nearest directory that does. 0.17's maker cannot load a configuration that depends on a path inside a missing directory ("configuration failed: FileSystemFailure"); a plugin without `.github/` hit it. Checked by building `plugins/image` copied without its `.github/`. **Left for the release-packaging PR**, each still marked `ZIG017`, since they run only in release builds and need the release workflow to verify: - the global `--libc` (→ `-Dwindows-libc`) and the MSVC translate-c shim; - Velopack's `addPathDir` for `mksquashfs`; - the `FIZZY_MACOS_*` signing fallbacks and `SDKROOT`, which are environment reads at configure. ## SDK impact - [x] None: no `sdk/` file changes here (the plugin install moved to #437). - [ ] Core-only or additive: reaches plugins at the next SDK release - [ ] Fingerprint moved - [ ] SDK release ## Verified - [x] macOS (arm64, 0.17.0): - `scripts/check-fmt.sh`, `zig build`, `zig build test` (91/91 steps), `zig build test-integration` (77/77 steps), `zig build check-web`, `zig build test-sdk-version`, `zig build check-tests-reached`. - `scripts/check-examples.sh` against the examples' `zig-0.17` branches: all 8 builds pass. - Standalone `cd plugins/image && FIZZY_PROFILE= zig build` installs into `/plugins/image/`, again on a rebuild with no change. A clean `-Doptimize=ReleaseFast` plugin build fetches no Velopack (AGENTS.md's acceptance test). - The set-aside step, forced on for a macOS run: nothing to do on a first build (install dir absent), checks the installed exe on the next. - [x] Windows: `zig build -Dtarget=x86_64-windows-gnu -Dnative-backend=fizzy -Dno-emit` configures and compiles. Not run on a Windows host. - [ ] Linux: CI. - [x] Web: `zig build check-web`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- backend/build.zig | 3 +- build.zig | 3 ++ build.zig.zon | 1 - build/app.zig | 67 ++++++++---------------------- build/common.zig | 37 +++++------------ build/exe.zig | 2 +- build/msvc.zig | 2 +- build/package.zig | 3 +- build/set_aside.zig | 32 ++++++++++++++ build/velopack.zig | 2 +- build/web.zig | 26 +++++++++++- plugins/shared/build/helpers.zig | 1 + update.zig | 71 -------------------------------- 13 files changed, 96 insertions(+), 154 deletions(-) create mode 100644 build/set_aside.zig delete mode 100644 update.zig diff --git a/backend/build.zig b/backend/build.zig index 9d27bc4de..43bd5ac22 100644 --- a/backend/build.zig +++ b/backend/build.zig @@ -145,7 +145,8 @@ pub fn backendModule(dep: *std.Build.Dependency, dvui_mod: *std.Build.Module, op fn patchedSdl(b: *std.Build) ?std.Build.LazyPath { const io = b.graph.io; var names: std.ArrayList([]const u8) = .empty; - var dir = b.root.root_dir.handle.openDir(io, "patches/sdl", .{ .iterate = true }) catch |err| + b.dependOnDirectoryContents(b.path("patches/sdl")); + var dir = b.root.openDir(io, "patches/sdl", .{ .iterate = true }) catch |err| std.debug.panic("backend: open patches/sdl: {t}", .{err}); defer dir.close(io); var it = dir.iterate(); diff --git a/build.zig b/build.zig index 92ad958dd..998ac705f 100644 --- a/build.zig +++ b/build.zig @@ -12,6 +12,9 @@ pub fn build(b: *std.Build) !void { const windows_msvc_libc_opt = b.option([]const u8, "windows-msvc-libc", "zig libc manifest for *-windows-msvc when cross-compiling; forwarded by packageall for Windows children") orelse null; const fetch_msvc_opt = b.option(bool, "fetch-msvc", "If *-windows-msvc libc is missing under .velopack-msvc/, run msvcup-setup first (downloads MSVC+SDK; requires network). Defaults to true on Windows hosts targeting *-windows-msvc.") orelse null; + // ZIG017 (release packaging PR): the FIZZY_MACOS_* fallbacks below are read at configure, and + // 0.17 caches the configuration without regard to the environment; scripts/release.sh and + // release.yml pass the -D options instead. const macos_sign_app_identity = b.option([]const u8, "macos-sign-app", "macOS codesign identity for the app bundle (e.g. 'Developer ID Application: NAME (TEAMID)')") orelse b.graph.environ_map.get("FIZZY_MACOS_SIGN_APP"); const macos_sign_install_identity = b.option([]const u8, "macos-sign-installer", "macOS codesign identity for the installer pkg (e.g. 'Developer ID Installer: NAME (TEAMID)')") orelse diff --git a/build.zig.zon b/build.zig.zon index d9c38384f..07de075c6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,7 +8,6 @@ "build", "build.zig", "build.zig.zon", - "update.zig", "sdk", "backend", "assets", diff --git a/build/app.zig b/build/app.zig index abf947fc0..753050182 100644 --- a/build/app.zig +++ b/build/app.zig @@ -144,7 +144,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options var effective_win_libc: ?[]const u8 = win_libc.libc_path; if (effective_win_libc == null) { - if (cross_win_msvc) effective_win_libc = null; // ZIG017: 0.17 has no global --libc + if (cross_win_msvc) effective_win_libc = null; // ZIG017 (release packaging PR): 0.17 has no global --libc; becomes -Dwindows-libc } // Velopack in the dev/install exe is opt-in (`-Dvelopack=true`). Release @@ -204,6 +204,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options var version_owned: ?[]u8 = null; // lives as long as the build process const app_version: []const u8 = if (app_version_opt) |v| v else blk: { + b.dependOnFileContents(b.path("VERSION")); const raw = b.root.root_dir.handle.readFileAlloc(b.graph.io, "VERSION", b.allocator, std.Io.Limit.limited(256)) catch |e| std.debug.panic("read VERSION: {}", .{e}); version_owned = raw; break :blk std.mem.trimEnd(u8, raw, "\r\n"); @@ -259,8 +260,6 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options const workbench_opts = b.addOptions(); workbench_opts.addOption(bool, "file_tree", workbench_file_tree); - common.addUpdateStep(b); - const msvcup_before_compile: ?*std.Build.Step.Run = if (vz) |v| velopack.addMsvcupSetupStep(b, v, ".velopack-msvc") else null; const msvcup_setup_step = b.step("msvcup-setup", "Download MSVC SDK into .velopack-msvc/ via velopack-zig (writes zig-libc-*.ini)"); if (msvcup_before_compile) |m| msvcup_setup_step.dependOn(&m.step); @@ -336,7 +335,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. const zig_out_install_dir = cfg.zig_out_install_dir; const target_is_windows_msvc = cfg.target_is_windows_msvc; const cross_win_msvc = cfg.cross_win_msvc; - _ = cross_win_msvc; // ZIG017 + _ = cross_win_msvc; // ZIG017 (release packaging PR) const effective_win_libc = cfg.effective_win_libc; const velopack_supported_for_target = cfg.velopack_supported_for_target; const velopack_enabled = cfg.velopack_enabled; @@ -428,8 +427,20 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. .dest_dir = .{ .override = zig_out_install_dir }, }); // A running fizzy holds its executable on Windows: set it aside first, or the install fails. - if (false) { // ZIG017: custom step + configure-time install path, both gone in 0.17 - const aside = SetAsideRunningExe.create(b, b.getInstallPath(zig_out_install_dir, exe.out_filename)); + if (b.graph.host.result.os.tag == .windows and target.result.os.tag == .windows) { + const aside_exe = b.addExecutable(.{ + .name = "set-aside", + .root_module = b.createModule(.{ + .root_source_file = b.path("build/set_aside.zig"), + .target = b.graph.host, + .optimize = .fast, + }), + }); + const aside = b.addRunArtifact(aside_exe); + aside.setName("set aside a running executable"); + aside.addDirectoryArg(common.installDirPath(zig_out_install_dir)); + aside.addArg(exe.out_filename); + aside.has_side_effects = true; aside.step.dependOn(&exe.step); install_artifact.step.dependOn(&aside.step); } @@ -1392,7 +1403,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. }; if (effective_win_libc) |ini| { - // ZIG017: no global --libc in 0.17 + // ZIG017 (release packaging PR): no global --libc in 0.17 const libc_lp: std.Build.LazyPath = .{ .cwd_relative = ini }; velopack.applyWindowsMsvcLibcRecursive(b, roots[0..n], libc_lp); @@ -1445,45 +1456,3 @@ fn addTestBinaryArgs( } else try addTestBinaryArgs(b, run, seen, dep); } } - -/// Windows will not let a file a running process was started from be written or deleted, but it -/// will let it be renamed. So before the install, an installed executable that is running (it -/// cannot be opened for writing) is renamed aside, to `.