Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/src/GpuRenderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,10 @@ fn acquireSwapchain(self: *GpuRenderer) !bool {
return false;
}
const cmd = try self.ensureCmd();
// No pass may be open while a drawable is acquired, the uploads' copy pass included. An app
// drawing its frame straight into the window (any dvui app; fizzy draws into a target first)
// had one open from the frame's uploads, and SDL asserted on every frame.
self.endCopy();
var tex: ?*c.SDL_GPUTexture = null;
var w: u32 = 0;
var h: u32 = 0;
Expand Down
10 changes: 5 additions & 5 deletions backend/src/platform/menu.zig
Original file line number Diff line number Diff line change
Expand Up @@ -105,31 +105,31 @@ fn selector(name: [*:0]const u8) ?*anyopaque {
return FizzyGetSelector(name);
}

export fn FizzyMenuActivated(section: c_int, tag: c_int, from_key: bool) void {
pub export fn FizzyMenuActivated(section: c_int, tag: c_int, from_key: bool) void {
if (section < 0 or section > 2 or tag < 0) return;
if (section == 0) pending_from_key.store(from_key, .release);
pending_tag[@intCast(section)].store(tag, .release);
}

export fn FizzyMenuEnabled(section: c_int, tag: c_int) callconv(.c) bool {
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) });
}

export fn FizzyMenuTitle(section: c_int, tag: c_int) callconv(.c) ?[*:0]const u8 {
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) });
}

export fn FizzyMenuInputBlocked() callconv(.c) bool {
pub export fn FizzyMenuInputBlocked() callconv(.c) bool {
const h = hooks orelse return false;
return h.input_blocked(h.ctx);
}

export fn FizzyMenuAbout() callconv(.c) void {
pub export fn FizzyMenuAbout() callconv(.c) void {
pending_about.store(true, .release);
}

Expand Down
13 changes: 13 additions & 0 deletions backend/src/platform/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,16 @@ pub const geometry = @import("geometry.zig");
pub const macos_monitor = @import("macos_monitor.zig");
pub const window_layout = @import("window_layout.zig");
pub const menu = @import("menu.zig");

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) {
_ = &menu.FizzyMenuActivated;
_ = &menu.FizzyMenuEnabled;
_ = &menu.FizzyMenuTitle;
_ = &menu.FizzyMenuInputBlocked;
_ = &menu.FizzyMenuAbout;
}
}
65 changes: 65 additions & 0 deletions backend/src/viewports.zig
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,71 @@ pub fn present(vp: *Viewport, target: ?dvui.TextureTarget) void {
dvui.currentWindow().backend.impl.viewportPresent(vp, target);
}

/// A viewport's picture this frame, drawn as dvui draws the frame: into a target, from the top left
/// of the part of the frame the viewport shows, until `end`. In it go the subwindows the viewport shows (`subwindow`) — a float and
/// what opened in it, a menu, a carried view — and whatever the app draws under them. Begun after
/// `Window.drawRetained`, so the dialogs and toasts are subwindows by then; then `present`.
pub const Picture = struct {
/// The frame's own target, back at `end`.
prev: dvui.RenderTarget,
/// The picture's.
rt: dvui.RenderTarget,

/// `target` cleared, and drawn into from now as `part` of the frame (physical).
pub fn begin(target: dvui.Texture.Target, part: Rect) Picture {
target.clear();
return again(target, part);
}

/// `target` drawn into again as `part` of the frame, what is in it kept: more for a picture
/// begun and ended earlier this frame. A window handed its picture (`present`) copies it only
/// when the frame ends, so what goes over it last — a demo's pointer — can still go in.
pub fn again(target: dvui.Texture.Target, part: Rect) Picture {
var rt = dvui.currentWindow().render_target;
rt.texture = target;
rt.offset = .{ .x = part.x, .y = part.y };
rt.rendering = true;
return .{ .prev = dvui.renderTarget(rt), .rt = rt };
}

/// Drawn into from now as the part of the frame from `at` (physical): the same drawing again,
/// from another part of the frame — a layer drawn across several windows' parts of it.
pub fn moveTo(self: *Picture, at: dvui.Point.Physical) void {
self.rt.offset = at;
_ = dvui.renderTarget(self.rt);
}

/// Subwindow `sw`'s drawing this frame, in the picture. `take`: out of the frame too, so dvui's
/// replay into the main window — and any picture after this one — draws none of it.
pub fn subwindow(_: Picture, sw: *dvui.Subwindows.Subwindow, take: bool) void {
const cw = dvui.currentWindow();
const cmds = sw.render_cmds;
const after = sw.render_cmds_after;
if (take) {
sw.render_cmds = .empty;
sw.render_cmds_after = .empty;
}
cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "drawing a subwindow into a viewport", .{});
cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "drawing a subwindow into a viewport", .{});
}

/// Done: drawing goes to the frame's own target again.
pub fn end(self: Picture) void {
_ = dvui.renderTarget(self.prev);
}
};

/// `slot`'s target at `w` by `h` pixels, for a `Picture`: the one in it where it is that size,
/// else a new one, the old let go after the frame. Null where none can be made.
pub fn sizedTarget(slot: *?dvui.Texture.Target, w: u32, h: u32) ?dvui.Texture.Target {
if (slot.*) |t| if (t.width != w or t.height != h) {
t.destroyLater();
slot.* = null;
};
if (slot.* == null) slot.* = dvui.textureCreateTarget(.{ .width = w, .height = h, .interpolation = .nearest }) catch return null;
return slot.*.?;
}

/// Where `vp`'s window is now, in the main window's part of the frame (physical pixels from
/// its top left): where its float goes when it comes back.
pub fn inMain(vp: *const Viewport) Rect {
Expand Down
14 changes: 14 additions & 0 deletions backend/src/viewports_none.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ pub fn place(_: *Viewport, frame: Rect) Rect {
return frame;
}
pub fn present(_: *Viewport, _: ?dvui.TextureTarget) void {}
pub const Picture = struct {
pub fn begin(_: dvui.Texture.Target, _: Rect) Picture {
return .{};
}
pub fn again(_: dvui.Texture.Target, _: Rect) Picture {
return .{};
}
pub fn moveTo(_: *Picture, _: dvui.Point.Physical) void {}
pub fn subwindow(_: Picture, _: *dvui.Subwindows.Subwindow, _: bool) void {}
pub fn end(_: Picture) void {}
};
pub fn sizedTarget(_: *?dvui.Texture.Target, _: u32, _: u32) ?dvui.Texture.Target {
return null;
}
pub fn inMain(_: *const Viewport) Rect {
return .{};
}
Expand Down
32 changes: 32 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,35 @@ pub fn buildApp(fizzy_dep: *std.Build.Dependency, plugins: []const BundledPlugin
opts.app_plugins = plugins;
try @import("build/app.zig").construct(fizzy_dep.builder, d.target, d.optimize, opts, d.cfg);
}

/// What a dvui app of its own builds on (`addDvui`): fizzy's backend, with OS windows besides the
/// main one, or dvui's own SDL3 backend, with the main window only.
pub const NativeBackend = @import("build/exe.zig").NativeBackend;

/// A plain dvui app — dvui's widgets and `dvui.App`, nothing of fizzy the editor — on `backend`:
/// `root_module` gets `dvui` (fizzy's pin), its `backend`, and `viewports_none`, so the same code
/// builds on either backend:
///
/// ```zig
/// const backend = @import("backend");
/// const viewports = if (@hasDecl(backend, "viewports")) backend.viewports else @import("viewports_none");
/// ```
///
/// On fizzy's backend it also gets `platform` (the window's chrome and state), and on macOS the
/// backend's Objective-C, compiled into it. Give `root_module` its target and optimize mode first.
pub fn addDvui(fizzy_dep: *std.Build.Dependency, root_module: *std.Build.Module, backend: NativeBackend) !void {
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 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);
root_module.addImport("backend", native.backend);
const backend_dep = exe.backendDependency(b);
root_module.addImport("viewports_none", @import("fizzy_backend").viewportsNoneModule(backend_dep, native.dvui));
if (backend == .fizzy) {
root_module.addImport("platform", exe.platformModule(b, native, target, optimize, macos_sdl_paths));
@import("fizzy_backend").addPlatformObjC(backend_dep, root_module, exe.backendOptions(target, optimize, macos_sdl_paths));
}
}
53 changes: 33 additions & 20 deletions build/exe.zig
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,42 @@ pub fn defaultNativeBackend(target: std.Build.ResolvedTarget) NativeBackend {
}

/// dvui as the executable links it, and the backend under it.
const NativeDvui = struct {
pub const NativeDvui = struct {
dep: *std.Build.Dependency,
dvui: *std.Build.Module,
backend: *std.Build.Module,
};

/// dvui on the native backend asked for: fizzy's (`fizzyNativeDvui`), or dvui's own SDL3 backend.
/// The executable's, and a dvui app's of its own (`fizzy.addDvui`, in `build.zig`).
pub fn nativeDvui(
b: *std.Build,
native_backend: NativeBackend,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
accesskit: dvui.AccesskitOptions,
macos_sdl_paths: ?MacosSdlPaths,
) NativeDvui {
return switch (native_backend) {
.fizzy => fizzyNativeDvui(b, target, optimize, accesskit, macos_sdl_paths),
.sdl3 => blk: {
const dep = if (macos_sdl_paths) |p|
sdk.dvuiDependency(b, .{
.target = target,
.optimize = optimize,
.backend = .sdl3,
.accesskit = accesskit,
.system_include_path = p.include,
.system_framework_path = p.framework,
.library_path = p.lib,
})
else
sdk.dvuiDependency(b, .{ .target = target, .optimize = optimize, .backend = .sdl3, .accesskit = accesskit });
break :blk .{ .dep = dep, .dvui = dep.module("dvui_sdl3"), .backend = dep.module("sdl3") };
},
};
}

/// dvui in its `custom` mode with fizzy's backend linked under it — as `build/web.zig` does for
/// the web. Every option is the one dvui's own `sdl3` mode sets, so dvui's shape, and with it
/// the plugin ABI fingerprint, is the same either way.
Expand Down Expand Up @@ -139,7 +169,7 @@ pub fn backendOptions(target: std.Build.ResolvedTarget, optimize: std.builtin.Op

/// Window and platform pieces for apps on either SDL3 backend (the backend package's
/// `platform`). Fizzy's own backend imports it as well.
fn platformModule(
pub fn platformModule(
b: *std.Build,
native: NativeDvui,
target: std.Build.ResolvedTarget,
Expand Down Expand Up @@ -175,24 +205,7 @@ pub fn addFizzyExecutableForTarget(
/// `bundled_plugins` module under its plugin id.
app_plugins: []const sdk.BundledPlugin,
) !FizzyExecutable {
const native: NativeDvui = switch (native_backend) {
.fizzy => fizzyNativeDvui(b, resolved_target, optimize, accesskit, macos_sdl_paths),
.sdl3 => blk: {
const dep = if (macos_sdl_paths) |p|
sdk.dvuiDependency(b, .{
.target = resolved_target,
.optimize = optimize,
.backend = .sdl3,
.accesskit = accesskit,
.system_include_path = p.include,
.system_framework_path = p.framework,
.library_path = p.lib,
})
else
sdk.dvuiDependency(b, .{ .target = resolved_target, .optimize = optimize, .backend = .sdl3, .accesskit = accesskit });
break :blk .{ .dep = dep, .dvui = dep.module("dvui_sdl3"), .backend = dep.module("sdl3") };
},
};
const native = nativeDvui(b, native_backend, resolved_target, optimize, accesskit, macos_sdl_paths);
const dvui_dep = native.dep;
const dvui_mod = native.dvui;

Expand Down
13 changes: 11 additions & 2 deletions sdk/replay/overlay.zig
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub const Options = struct {
/// (`LiveDriver.Hand`).
pub fn drawLive(driver: *const LiveDriver, opts: Options) void {
if (!driver.playing()) return;
const fw = layer(@src(), .{ .rect = .cast(dvui.windowRect()), .name = "LivePointer" }, .{});
const fw = layer(@src(), .{ .rect = pointerRect(driver.seq.pointer), .name = "LivePointer" }, .{});
defer fw.deinit();

const now = dvui.frameTimeNS();
Expand All @@ -38,14 +38,23 @@ pub fn drawLive(driver: *const LiveDriver, opts: Options) void {
/// A demo's pointer, while it drives, timed in demo time: a seek shows what playing showed.
pub fn drawPlayer(player: *Player, opts: Options) void {
if (!player.driving()) return;
const fw = layer(@src(), .{ .rect = .cast(dvui.windowRect()), .name = "DemoPointer" }, .{});
const fw = layer(@src(), .{ .rect = pointerRect(player.seq.pointer), .name = "DemoPointer" }, .{});
defer fw.deinit();
const ripple: ?Ripple = if (player.last_press) |press| .{ .pt = press.pt, .age_ms = player.seq.now - press.at } else null;
// Out of the way of the words while the tape types, as a desktop's pointer is.
const shown = player.pointerShown(opts.fade_ms);
paintPointer(player.seq.pointer, ripple, shown, player.input.held.count() > 0);
}

/// The pointer's layer: round its tip, natural, so its middle is where the pointer is. An app that
/// shows parts of its frame in OS windows of their own (fizzy's floats, menus and dialogs) gives
/// each window the layers whose middle is in it, and the pointer is then drawn on whatever window
/// it is over. What it draws is clipped to the screen it is on (`dvui.screenFor`), not to this.
fn pointerRect(p: Sequencer.Point) dvui.Rect {
const s = dvui.windowNaturalScale();
return .{ .x = p.x / s - 16, .y = p.y / s - 16, .w = 32, .h = 32 };
}

/// A floating widget that takes no input, raised to the top. A floating widget otherwise stays
/// just above the window it was made in, under anything opened after it; re-added as a subwindow
/// of its own it can be raised, and each call puts the new one above the last.
Expand Down
Loading
Loading