diff --git a/backend/src/GpuRenderer.zig b/backend/src/GpuRenderer.zig index cb11db39c..21aff8296 100644 --- a/backend/src/GpuRenderer.zig +++ b/backend/src/GpuRenderer.zig @@ -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; diff --git a/backend/src/platform/menu.zig b/backend/src/platform/menu.zig index b8cc34ce9..56485ae67 100644 --- a/backend/src/platform/menu.zig +++ b/backend/src/platform/menu.zig @@ -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); } diff --git a/backend/src/platform/root.zig b/backend/src/platform/root.zig index b456a1709..8fadd88fd 100644 --- a/backend/src/platform/root.zig +++ b/backend/src/platform/root.zig @@ -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; + } +} diff --git a/backend/src/viewports.zig b/backend/src/viewports.zig index 2a89da637..112faa6fb 100644 --- a/backend/src/viewports.zig +++ b/backend/src/viewports.zig @@ -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 { diff --git a/backend/src/viewports_none.zig b/backend/src/viewports_none.zig index e7dc2061f..e7c85a64c 100644 --- a/backend/src/viewports_none.zig +++ b/backend/src/viewports_none.zig @@ -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 .{}; } diff --git a/build.zig b/build.zig index b4c140e69..1f072f17d 100644 --- a/build.zig +++ b/build.zig @@ -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)); + } +} diff --git a/build/exe.zig b/build/exe.zig index 28fed66e8..010402751 100644 --- a/build/exe.zig +++ b/build/exe.zig @@ -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. @@ -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, @@ -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; diff --git a/sdk/replay/overlay.zig b/sdk/replay/overlay.zig index 5459cc340..b92f61ec9 100644 --- a/sdk/replay/overlay.zig +++ b/sdk/replay/overlay.zig @@ -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(); @@ -38,7 +38,7 @@ 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. @@ -46,6 +46,15 @@ pub fn drawPlayer(player: *Player, opts: Options) void { 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. diff --git a/src/editor/Popout.zig b/src/editor/Popout.zig index 18f38ceeb..0760db946 100644 --- a/src/editor/Popout.zig +++ b/src/editor/Popout.zig @@ -52,6 +52,8 @@ const MenuOut = struct { id: dvui.Id, viewport: *viewports.Viewport, target: ?dvui.Texture.Target = null, + /// The part of the frame its picture shows this frame, once drawn (`overlaysFrame`). + part: ?viewports.Rect = null, /// Drawn this frame: a menu not drawn has closed, and its window goes. seen: bool = false, /// On Liquid Glass (`viewports.windowGlass`), else vibrancy with the menu's colour drawn over it. @@ -98,6 +100,8 @@ const Out = struct { /// the texture is kept larger through a fullscreen transition (`keepTarget`). target: ?dvui.Texture.Target = null, drawn: [2]u32 = .{ 0, 0 }, + /// The part of the frame its picture shows this frame, once drawn (`overlaysFrame`). + part: ?viewports.Rect = null, /// What its window is called now: the float's title, as its header says (`Floats.Float.titleText`). title_buf: [96]u8 = undefined, title_len: u8 = 0, @@ -327,6 +331,12 @@ fn release(o: *Out) void { pub fn endFrame(state: *State) void { if (!enabled()) return; cover_count = 0; + for (&outs) |*slot| if (slot.*) |*o| { + o.part = null; + }; + for (&menus) |*slot| if (slot.*) |*m| { + m.part = null; + }; for (&outs) |*slot| { if (slot.*) |*o| windowFrame(state, o); } @@ -340,6 +350,7 @@ pub fn endFrame(state: *State) void { } carryFrame(state); menuFrame(); + overlaysFrame(); // A drag's carry window no float grew out of in the frame after the drag: gone. if (spare) |*sp| { if (spare_kept) { @@ -419,13 +430,7 @@ fn carryFrame(state: *State) void { // The carried view's own layer (`core.screens.markCarried`), taken from it: dvui's replay into the main // window, and the float windows' (`windowFrame`), draw nothing of it. for (cw.subwindows.stack.items) |*sw| { - if (!fizzy.core.screens.isCarried(sw.id)) continue; - const cmds = sw.render_cmds; - const after = sw.render_cmds_after; - sw.render_cmds = .empty; - sw.render_cmds_after = .empty; - cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "replaying a carried view into its window", .{}); - cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "replaying a carried view into its window", .{}); + if (fizzy.core.screens.isCarried(sw.id)) drawing.picture.subwindow(sw, true); } } @@ -543,18 +548,8 @@ fn overlayFrame(state: *State) void { // them lands in the overlay, the rest outside it. const w: u32 = @intFromFloat(@max(1, @round(shown.w))); const h: u32 = @intFromFloat(@max(1, @round(shown.h))); - if (o.target) |t| if (t.width != w or t.height != h) { - t.destroyLater(); - o.target = null; - }; - if (o.target == null) o.target = dvui.textureCreateTarget(.{ .width = w, .height = h, .interpolation = .nearest }) catch return; - const target = o.target.?; - target.clear(); - var rt = cw.render_target; - rt.texture = target; - rt.offset = .{ .x = shown.x, .y = shown.y }; - rt.rendering = true; - const prev = dvui.renderTarget(rt); + const target = viewports.sizedTarget(&o.target, w, h) orelse return; + var picture: viewports.Picture = .begin(target, placed); glassBase(placed_shapes[0..n], shown, s, spacing, look.top_fill); var offsets: [max_out + 1]dvui.Point.Physical = undefined; offsets[0] = .{ .x = shown.x, .y = shown.y }; @@ -565,18 +560,12 @@ fn overlayFrame(state: *State) void { } for (cw.subwindows.stack.items) |*sw| { if (!fizzy.core.screens.isCarried(sw.id)) continue; - const cmds = sw.render_cmds; - const after = sw.render_cmds_after; - sw.render_cmds = .empty; - sw.render_cmds_after = .empty; - for (offsets[0..n_off]) |off| { - rt.offset = off; - _ = dvui.renderTarget(rt); - cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "replaying over the overlay's glass", .{}); - cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "replaying over the overlay's glass", .{}); + for (offsets[0..n_off], 0..) |off, i| { + picture.moveTo(off); + picture.subwindow(sw, i == n_off - 1); } } - _ = dvui.renderTarget(prev); + picture.end(); viewports.present(o.viewport, target); } @@ -682,10 +671,9 @@ fn inMainFrame(r: dvui.Rect.Physical, main_px: dvui.Rect.Physical) ?dvui.Rect.Ph return r.offsetPoint(.{ .x = cv.in_main.x - cv.band.x, .y = cv.in_main.y - cv.band.y }); } -/// A carry window's picture under way (`carryBegin`): the frame's own target to go back to, and -/// the part of the frame the window shows. +/// A carry window's picture under way (`carryBegin`), and the part of the frame the window shows. const CarryDrawing = struct { - prev: dvui.RenderTarget, + picture: viewports.Picture, shown: dvui.Rect.Physical, }; @@ -702,18 +690,8 @@ fn carryBegin(c: *Carry, place: dvui.Rect.Physical, shape: dvui.Rect.Physical, r viewports.carryShape(c.viewport, radius, alpha); const w: u32 = @intFromFloat(@max(1, @round(shown.w))); const h: u32 = @intFromFloat(@max(1, @round(shown.h))); - if (c.target) |t| if (t.width != w or t.height != h) { - t.destroyLater(); - c.target = null; - }; - if (c.target == null) c.target = dvui.textureCreateTarget(.{ .width = w, .height = h, .interpolation = .nearest }) catch return null; - const target = c.target.?; - target.clear(); - var rt = cw.render_target; - rt.texture = target; - rt.offset = .{ .x = shown.x, .y = shown.y }; - rt.rendering = true; - const prev = dvui.renderTarget(rt); + const target = viewports.sizedTarget(&c.target, w, h) orelse return null; + const picture: viewports.Picture = .begin(target, .{ .x = shown.x, .y = shown.y, .w = shown.w, .h = shown.h }); { const prev_clip = dvui.clipGet(); defer dvui.clipSet(prev_clip); @@ -727,12 +705,12 @@ fn carryBegin(c: *Carry, place: dvui.Rect.Physical, shape: dvui.Rect.Physical, r shape.fill(dvui.CornerRect.Physical.all(radius), .{ .color = .{ .color = color } }); } } - return .{ .prev = prev, .shown = shown }; + return .{ .picture = picture, .shown = shown }; } /// `carryBegin`'s picture done: handed to its window. fn carryEnd(c: *Carry, drawing: CarryDrawing) void { - _ = dvui.renderTarget(drawing.prev); + drawing.picture.end(); if (c.target) |t| viewports.present(c.viewport, t); } @@ -1112,13 +1090,9 @@ fn windowFrame(state: *State, o: *Out) void { const target = keepTarget(o, w, h) orelse return; // Transparent where the float is not: past its corners. - target.clear(); - var rt = cw.render_target; - rt.texture = target; - rt.offset = .{ .x = shown.x, .y = shown.y }; - rt.rendering = true; - const prev = dvui.renderTarget(rt); - defer _ = dvui.renderTarget(prev); + const picture: viewports.Picture = .begin(target, shown); + defer picture.end(); + o.part = shown; // Zoomed or full screen there is no desktop behind it: opaque, eased there and back with the // OS's transitions as the main window's base is. Where it has no material it is opaque // throughout. @@ -1139,16 +1113,10 @@ fn windowFrame(state: *State, o: *Out) void { // A carried view is its carry window's (`carryFrame`); a menu, its own (`menuFrame`). if (fizzy.core.screens.isCarried(sw.id)) continue; if (fizzy.core.screens.isMenu(sw.id) and nativeMenus()) continue; + // What takes no input goes over everything, last (`overlaysFrame`). + if (!sw.mouse_events and !fizzy.core.screens.isEverywhere(sw.id)) continue; const mine = area.contains(sw.rect_pixels.center()); - if (!mine and !fizzy.core.screens.isEverywhere(sw.id)) continue; - const cmds = sw.render_cmds; - const after = sw.render_cmds_after; - if (mine) { - sw.render_cmds = .empty; - sw.render_cmds_after = .empty; - } - cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "replaying a float into its window", .{}); - cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "replaying a float into its window", .{}); + if (mine or fizzy.core.screens.isEverywhere(sw.id)) picture.subwindow(sw, mine); } viewports.present(o.viewport, target); } @@ -1318,19 +1286,9 @@ fn menuFrame() void { m.glass = viewports.windowGlass(m.viewport, look); const w: u32 = @intFromFloat(@max(1, @round(shown.w))); const h: u32 = @intFromFloat(@max(1, @round(shown.h))); - if (m.target) |t| if (t.width != w or t.height != h) { - t.destroyLater(); - m.target = null; - }; - if (m.target == null) m.target = dvui.textureCreateTarget(.{ .width = w, .height = h, .interpolation = .nearest }) catch continue; - const target = m.target.?; - target.clear(); - var rt = cw.render_target; - rt.texture = target; - rt.offset = .{ .x = shown.x, .y = shown.y }; - rt.rendering = true; - const prev = dvui.renderTarget(rt); - defer _ = dvui.renderTarget(prev); + const target = viewports.sizedTarget(&m.target, w, h) orelse continue; + m.part = .{ .x = shown.x, .y = shown.y, .w = shown.w, .h = shown.h }; + const picture: viewports.Picture = .begin(target, m.part.?); if (!m.glass) { const prev_clip = dvui.clipGet(); defer dvui.clipSet(prev_clip); @@ -1340,13 +1298,8 @@ fn menuFrame() void { defer dvui.alphaSet(prev_alpha); frame.fill(dvui.CornerRect.Physical.all(corner * s), .{ .color = .{ .color = Editor.windowBase(op) } }); } - const cmds = sw.render_cmds; - const after = sw.render_cmds_after; - sw.render_cmds = .empty; - sw.render_cmds_after = .empty; - cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "replaying a menu into its window", .{}); - cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "replaying a menu into its window", .{}); - _ = dvui.renderTarget(prev); + picture.subwindow(sw, true); + picture.end(); viewports.present(m.viewport, target); } } @@ -1383,6 +1336,35 @@ fn dialogsRide() void { }; } +/// What takes no input, drawn last: a demo's pointer and the ripple of its click +/// (`replay.overlay`, its layer round the pointer's tip), a tooltip. Each goes into the topmost of +/// the app's windows over its middle — a menu's or a dialog's, else a float's — over all that +/// window shows, so the pointer is seen on whatever it is over; over none of them it is left to +/// the main window. Those windows' pictures are handed over already (`windowFrame`, `menuFrame`): +/// a window copies its picture only when the frame ends, so this still goes in. +fn overlaysFrame() void { + const cw = dvui.currentWindow(); + for (cw.subwindows.stack.items) |*sw| { + if (sw.mouse_events) continue; + if (fizzy.core.screens.isCarried(sw.id) or fizzy.core.screens.isEverywhere(sw.id)) continue; + const mid = sw.rect_pixels.center(); + const Into = struct { target: dvui.Texture.Target, part: viewports.Rect }; + const into: ?Into = blk: { + for (menus) |slot| if (slot) |m| if (m.part) |p| if (m.target) |t| { + if ((dvui.Rect.Physical{ .x = p.x, .y = p.y, .w = p.w, .h = p.h }).contains(mid)) break :blk .{ .target = t, .part = p }; + }; + for (outs) |slot| if (slot) |o| if (o.part) |p| if (o.target) |t| { + if ((dvui.Rect.Physical{ .x = p.x, .y = p.y, .w = p.w, .h = p.h }).contains(mid)) break :blk .{ .target = t, .part = p }; + }; + break :blk null; + }; + const w = into orelse continue; + const picture: viewports.Picture = .again(w.target, w.part); + picture.subwindow(sw, true); + picture.end(); + } +} + /// Menu `id`'s window (`menuFrame`), opened at `place` (physical, in the main window's frame) where /// it has none yet — riding on `ride`; a dialog's (`dialog`) in that window's stacking. Null where /// no more windows can be opened: the menu is drawn in its window then.