From b30c48c8665ffe192ecffbe8e7d8dec805558596 Mon Sep 17 00:00:00 2001 From: foxnne Date: Mon, 19 Oct 2026 09:44:54 -0500 Subject: [PATCH] automation: a demo's captions and bar stay in the main window While a tape played, an OS window over the main one (a dialog's, as About Fizzy or the unsaved-changes prompt; a menu's; a float's) drew the tape's caption inside itself, cut at its edge. The popout code hands every layer that takes no input to whichever OS window its middle is over (`Popout.overlaysFrame`), which is right for the demo's pointer and wrong for the captions, keys and bar: they are the stage's. Each of those layers is now marked as the main window's (`core.screens.markMain`), and `overlaysFrame` leaves them to it. The pointer, and a callout beside what it acts on, still go into the window they are over. The bar, left under a dialog's window, no longer takes the presses meant for that window: the app hands the player its OS windows' rects each frame (`replay.Player.Transport.covered`). Co-Authored-By: Claude Opus 5.5 --- app/automation/overlay.zig | 21 +++++++++++++++++---- core/screens.zig | 29 +++++++++++++++++++++++++++++ sdk/replay/Player.zig | 9 +++++++++ src/editor/Editor.zig | 11 +++++++++++ src/editor/Popout.zig | 8 +++++--- 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/app/automation/overlay.zig b/app/automation/overlay.zig index d2a64d848..ba2f99abd 100644 --- a/app/automation/overlay.zig +++ b/app/automation/overlay.zig @@ -19,7 +19,7 @@ const std = @import("std"); const dvui = @import("dvui"); const core = @import("core"); const Player = @import("replay").Player; -/// The pointer, and the layer every part of this overlay is drawn in. +/// The pointer, and the layer every part of this overlay is drawn in (`layer`). const plain = @import("replay").overlay; const LiveDriver = @import("replay").LiveDriver; const Input = @import("replay").Input; @@ -66,7 +66,16 @@ pub fn drawLive(driver: *LiveDriver) void { /// How long the tape's pointer takes to step aside when it types, and back when it moves. const plain_fade_ms = (plain.Options{}).fade_ms; -const layer = plain.layer; + +/// A part of the overlay in a layer of its own (`replay.overlay.layer`), kept to the main window +/// (`core.screens.markMain`): the captions, keys and bar are the stage's, and stay on it while a +/// dialog or a menu is open in a window of its own over it. Only the pointer, and a callout beside +/// what it acts on (`drawCallout`), go into those windows. Every part goes through this. +fn layer(src: std.builtin.SourceLocation, opts: dvui.Options, init: dvui.FloatingWidget.InitOptions) *dvui.FloatingWidget { + const fw = plain.layer(src, opts, init); + core.screens.markMain(fw.data().id); + return fw; +} fn theme() dvui.Theme { return dvui.themeGet(); @@ -337,7 +346,8 @@ fn drawCallout(player: *Player, tape: *const Tape, c: Tape.Caption, index: usize // Its size as last drawn, to keep the whole of it clear. const src = @src(); const size: dvui.Size = dvui.minSizeGet(dvui.parentGet().extendId(src, index + 1)) orelse .{}; - const spot = callout(player, tape, c, win, size, floor) orelse blk: { + const beside = callout(player, tape, c, win, size, floor); + const spot = beside orelse blk: { const home = (if (tape.home.len > 0) tagRect(tape.home) else null) orelse win; break :blk Spot{ .at = .{ .x = home.x + home.w / 2 - size.w / 2, @@ -348,12 +358,15 @@ fn drawCallout(player: *Player, tape: *const Tape, c: Tape.Caption, index: usize const w = @min(440, win.w - 32); // Where it belongs while it opens — it is placed by its size, which it only has once drawn — // and gliding after. - const fw = layer(src, .{ .id_extra = index + 1, .max_size_content = .{ .w = w, .h = win.h } }, .{ + const fw = plain.layer(src, .{ .id_extra = index + 1, .max_size_content = .{ .w = w, .h = win.h } }, .{ .from = dvui.windowRectScale().pointToPhysical(glide(c, spot.at, size.w == 0 or shown.alpha < 1)), .from_gravity_x = 1, .from_gravity_y = 1, }); defer fw.deinit(); + // Beside what the pointer acts on, it goes where that is, as the pointer does: over a dialog's + // button, into the dialog's window. At the foot of the home view it is the stage's (`layer`). + if (beside == null) core.screens.markMain(fw.data().id); // It opens out of the side nearest what it is beside, as a popover does from its anchor. captionCard(c, shown, spot.origin, w); } diff --git a/core/screens.zig b/core/screens.zig index 0c8de00eb..8ecb2c9e6 100644 --- a/core/screens.zig +++ b/core/screens.zig @@ -71,6 +71,35 @@ const Everywhere = struct { ids: [8]dvui.Id = undefined, }; +/// A floating thing that belongs to the main window this frame — a demo's captions, its keys and +/// its bar, which are the stage's: drawn in the main window and nowhere else. Without it, a layer +/// that takes no input goes into whichever of the app's OS windows (a menu's, a dialog's, a +/// float's) its middle is over, as the demo's pointer does, and a caption at the foot of the main +/// window was drawn inside a dialog over it, cut at that window's edge (`isMain`). +pub fn markMain(id: dvui.Id) void { + const now = dvui.currentWindow().frame_time_ns; + const m = dvui.dataGetPtrDefault(null, key_id, "_main", Main, .{}); + if (m.frame != now) m.* = .{ .frame = now }; + if (m.n < m.ids.len) { + m.ids[m.n] = id; + m.n += 1; + } +} + +/// Whether `id` was marked as the main window's this frame (`markMain`). +pub fn isMain(id: dvui.Id) bool { + const m = dvui.dataGetPtr(null, key_id, "_main", Main) orelse return false; + if (m.frame != dvui.currentWindow().frame_time_ns) return false; + for (m.ids[0..m.n]) |i| if (i == id) return true; + return false; +} + +const Main = struct { + frame: i128 = 0, + n: u8 = 0, + ids: [64]dvui.Id = undefined, +}; + /// What is carried, in a layer of its own this frame, where the app shows it in a window of its /// own over every other (fizzy's carry window): that window takes the layer's drawing whole, and /// the app's own windows draw none of it. A copy left in them, under the carry window, was drawn diff --git a/sdk/replay/Player.zig b/sdk/replay/Player.zig index ad27c6137..327aa3060 100644 --- a/sdk/replay/Player.zig +++ b/sdk/replay/Player.zig @@ -218,6 +218,11 @@ pub const Transport = struct { scrub: ?f64 = null, /// The real pointer, physical, as last seen. pointer: ?dvui.Point.Physical = null, + /// Where an app shows parts of its frame in OS windows of their own (a dialog's, a menu's) over + /// the window the bar is in, those windows' rects last frame, physical: the app's to set. The + /// bar is drawn under them, so a press there is theirs, not the bar's. + covered: [8]dvui.Rect.Physical = undefined, + covered_n: u8 = 0, /// When the real pointer was last on the bar, or the dome brought it (ns, `wallNs`): while /// playing, the bar shows for a while after, then goes back into its dome. stirred_ns: ?i128 = null, @@ -868,6 +873,10 @@ fn transportTakes(self: *Player, e: *dvui.Event, me: dvui.Event.Mouse) bool { e.handle(@src(), wd); return true; } + for (tr.covered[0..tr.covered_n]) |r| if (r.contains(me.p)) { + tr.pointer = null; + return false; + }; // A press on the dome brings the bar out of it at once, and is not the app's. if (tr.dome) |d| if (d.contains(me.p)) { if (me.action == .press and me.button.pointer() and !tr.closing) { diff --git a/src/editor/Editor.zig b/src/editor/Editor.zig index 50fd6ea1c..2e03dad47 100644 --- a/src/editor/Editor.zig +++ b/src/editor/Editor.zig @@ -4797,6 +4797,17 @@ pub fn tick(editor: *Editor) !dvui.App.Result { // out and removes itself when the timer expires. editor.drawSaveToasts(); + // The app's OS windows over the main one: a demo's bar stays in the main window, under them, + // so a press on one is that window's, not the bar's (`replay.Player.Transport.covered`). + if (editor.demo.player.tape() != null) { + var buf: [8]Popout.Shown = undefined; + const tr = &editor.demo.player.transport; + tr.covered_n = 0; + for (Popout.windowsShown(&buf)) |w| { + tr.covered[tr.covered_n] = w.part; + tr.covered_n += 1; + } + } // Over everything the frame drew: a demo's pointer, keystrokes, captions and transport bar, // or a live tape's pointer. @import("app").automation.overlay.draw(&editor.demo.player); diff --git a/src/editor/Popout.zig b/src/editor/Popout.zig index 7c8472dc6..a95157ea6 100644 --- a/src/editor/Popout.zig +++ b/src/editor/Popout.zig @@ -1358,14 +1358,16 @@ 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. +/// window shows, so the pointer is seen on whatever it is over; over none of them, or marked as the +/// main window's (`core.screens.markMain`), 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; + if (fizzy.core.screens.isMain(sw.id)) continue; const mid = sw.rect_pixels.center(); const Into = struct { target: dvui.Texture.Target, part: viewports.Rect }; const into: ?Into = blk: {