Repository navigation
backend: dvui's Debug window tears off too - #594
Merged
Merged
Conversation
dvui draws its Debug window, dialogs and toasts late, in drawRetained, which the backend ran after the tear-off looked for the floating window being dragged, and only with OS windows open. So the Debug window was not yet drawn this frame (`used`) nor noted as closable when the tear-off looked, and never tore off (the user; diagnosed by the session on #584). With tear-off on, the retained layer is now drawn first; drawRetained keeps to once a frame, and Window.end would have drawn it at this point anyway.
3 tasks done
foxnne
added a commit
that referenced
this pull request
Oct 19, 2026
Part of the multi-window plan (fizzyedit/plans#26): the tear-off (#577, #591, #594). Three commits, all from the user's testing on `run-dvui`. **1. A window dragged over a title bar stays out of it.** - A floating window torn off and dragged over another of the app's windows goes back into it. - Over that window's title bar it went in too. Then dvui held its top below the title bar, away from the pointer, so the window jumped off the pointer as it crossed. - The tear-off now asks whose *content* the pointer is over (`windowContentAtPointer`, new beside `windowAtPointer`). Over a title bar the answer is none, and the window stays out until the pointer reaches the content. **2. A torn-off window let go over the app goes back in at once, fading into the floating window.** Before, a torn-off window moved by the OS: - went back into the main window only once *all* of it lay inside the main window; - only on a frame after the OS's move let go, which waited for the next event ("takes more events"); - then shrank back through a card of its last picture, its title bar's place becoming dvui's header's, flashing a title on the way ("pops a title"). Now: - It goes back in when the OS's move lets go with the pointer over the main window's content (`pointerOverMainContent`). - Frames keep coming while the OS moves a window (`osMoving`), so the let-go is seen at once. - The same frame draws the floating window where the window was, dvui's header where its title bar was. The OS window, over it, closes the frame after (`closing`), so neither is missing for a frame. - Rather than snapping, the OS window stays over the floating window for 150 ms with its last picture, the pointer passing through it (`viewports.held`), and fades out (`viewports.fade`): its title bar dissolves into dvui's header while the content stays still (a third commit, from the user's "we need some transition"). - The card, its drawing (`Merge`, `drawCard`) and `mainHolds` are gone. While there: `viewportInMain` gets its doc comment back. It had sat above `viewportMainHolds` since that was added. ## SDK impact - [x] None ## Verified - [x] macOS (arm64, Zig 0.17.0): `scripts/check-fmt.sh`, `zig build`, `zig build test`, `zig build check-tests-reached`, `zig build check-web`, `zig build test-sdk-version`, `zig build test-integration` all pass. - [x] By hand on `run-dvui` (the user, macOS 26): drag a torn-off window across the main window's title bar; let one go over the main window, partly outside it; the fade back in. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
6 of 10 tasks
foxnne
added a commit
that referenced
this pull request
Oct 19, 2026
…584) Part of #260 (step 5 of `plans/WORKSPACES_PLAN.md`: floats and tear-off). ## What changes Behind `FIZZY_WORKSPACES=1`, a document tab can float. It floats inside a workspace of its own, so the new window is a tabbed area of documents holding just that one. - **Torn off over the desktop** (where floats are OS windows, macOS today): a tab let go over no window of the app's opens a float there, holding a new workspace with that document. Before, nothing happened: a document could not float. - **Dropped on its own pane's middle:** the same, the float opening over the pane. The drop's middle shows the float icon there, as it does for a view over its own place. - **A workspace's only tab carries the workspace** (`State.Documents.alone`, from #462): the workspace itself floats, rather than a new one being made round the document and the old one closing behind it. Already alone in a float nobody split, it doesn't float again; the pane takes the drop. - **Its float closes when the workspace empties** (its last tab closed or carried out), as any float its last view leaves. - **Closing the float gives its documents to the main window's workspace**: the one the person was in, when that is in the main window, else the first there. They go in after its own tabs, in order, and the one the float showed is shown. The emptied workspace then goes. With no workspace in the main window, it goes home as a view, as before. How: `State.Documents` gets one more member, `fold(id) -> bool`, which `ViewDrag.sendHome` asks before sending a closing float's view home. `ViewDrag.floatDocument` floats the view a document carries alone (`carriedView`), or a new one (`houseView`). A new workspace floats out of the pane its photograph was taken in, at that pane's size. With the flag off, `documents` is null, and nothing changes. ## SDK impact - [x] None. `State.Documents` is the app framework's (`app/layout`), not the plugin boundary. ## Verified - [x] `zig build`, `zig build test`, `zig build test-integration`, `zig build check-web`, `zig build test-sdk-version`, `zig build check-tests-reached`, `scripts/check-fmt.sh`. - [x] New integration test, "float: a document tab dropped on its own pane's middle floats in a view holding it". It covers one tab of two floating in a new view, a lone tab floating its workspace, no second float for one already alone, and closing the float folding instead of going home. - [x] macOS, a sandbox profile run with `FIZZY_WORKSPACES=1`, driven through the agent plugin (`fizzy_act`), each checked by `fizzy_screen` and a capture: - `tear-c.txt`, one of two tabs, dropped on its own pane's middle: `Float 1` opens over the pane in an OS window. It holds a new workspace (`Pane 3`) with only `tear-c.txt`, and the pane keeps `tt-b.pixi`. - Closing `tear-c.txt` in the float: the float closes, and the main window is unchanged. - `tt-a.pixi`, its workspace's only tab, dropped on its own pane's middle: that workspace (`Pane 4`) floats, and the emptied half of Main closes into the other. - [x] macOS, with the tear-out fixes since merged into a local build (#591, #592, #594): a tab carried past the window's edge opens its own window, holding a new workspace with just that document (`Float 1`, `Pane 6`, `tear-d.txt`). - [x] Review: `fold` copied the workspace's tab list but not the id strings in it, then emptied the workspace, and `State.setAssignment` frees those strings. It now copies the ids too. The gates pass on the branch merged with current `main`. - [ ] macOS, still to see with a real mouse: a tab torn off over the desktop (my run was interrupted by a person's input), and a float closed from its OS close button folding into the main workspace (the integration test covers the layout side). - [ ] Windows: - [ ] Linux: - [ ] Web: builds; the flag is never on there. ## Follow-ups - A window from ⌘N (step 8) stays open when it empties. The float needs to remember which kind it is then. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
10 of 11 tasks
foxnne
added a commit
that referenced
this pull request
Oct 19, 2026
Part of the window material (fizzyedit/plans#27): step 2. A plain dvui app's frame draws through `FrameTarget`, so glass drawn in the frame (a frost or blur under an in-app floating window) can read what is under it. Stacked on #586. ## What changes A plain dvui app on fizzy's backend (one that doesn't dress its own windows) now draws its frame into a window-sized texture, `glass.FrameTarget`, which is blitted onto the window at the end of the frame. Fizzy has always done this for its own frame in `src/Entry.zig`. The app's code doesn't change and the window looks the same. ## Why A floating window drawn in the window's material has to read what is under it. When the frame is already in a texture, `BlurBackdrop` copies from a texture the GPU holds. When the frame goes straight to the swapchain, the only way to get those pixels is to read the window back through the CPU, which costs a GPU sync and a copy each way. The next step builds the material on top of this one. ## How - `backend/build.zig`: `backendModule` imports `glass` as `glassModule(dep, dvui_mod)`. That's the same module instance per dvui that the app's `core` takes, so no file belongs to two modules. Glass imports dvui and `glass_look` and never the backend, so there's no new cycle. - `SDLBackend.zig`: `plainFrameBegin` and `plainFrameEnd` hold a `plain_frame: glass.FrameTarget` on the backend. Both frame loops call them: the blocking run loop and `appIterate`. Both return at once when `platform.window.app_dresses_windows` is set. Fizzy sets it in `AppInit` on every OS, so fizzy is never wrapped twice. - The order in each loop is: `Window.begin` → events → `os_windows.beforeFrame` → **`plainFrameBegin`** → `app.frameFn` → `os_windows.afterFrame` → `dressPlainWindows` → (`live_resize_trace.overlay`) → **`plainFrameEnd`** → `Window.end`. This mirrors Entry, which does `Popout.beginFrame` → `FrameTarget.begin` → tick → `Popout.endFrame` → `FrameTarget.end`. - `afterFrame` runs while the target is bound. Its `drawRetained` only queues the dialogs and toasts. Every picture it draws (the OS windows' parts of the frame, the tear-off cards, the live picture) goes through `viewports.Picture`, which binds its own target and gives the frame's target back at `end`. Subwindows taken into an OS window are taken with `.take = true`, so `FrameTarget.end` never draws them a second time. - `FrameTarget.end` runs `Window.endRendering` (the deferred subwindows still in the main window: floating windows, dialogs, toasts, dvui's debug window) into the target, then blits. `Window.end` sees that rendering is done and skips it. - The live-resize trace overlay queues into the main subwindow, so it goes before `plainFrameEnd`. - `FrameTarget.init` runs once, on the first frame. It turns off `clear_window_on_begin`, which on the GPU renderer only decides whether an empty frame presents a cleared window. - On macOS a plain app's frame is laid below the title band (`places_main_picture`). `FrameTarget.end` already places the blit at `pictureInWindow().at`, so on a frame where the target bound, `plainFrameBegin` zeroes `gpu.window_offset`. Otherwise the blit would be offset twice. On a frame that draws straight to the window (no render targets, or the target couldn't be made), the backend still applies the offset. - I added a rule to `backend/AGENTS.md` (never both wraps; anything drawn into the main frame goes before `plainFrameEnd`). ## Measured cost Setup: dvui's demo (`fizzyedit/example-app`, `zig build dvui`, Debug, macOS, 120 Hz). For the "animating" case I forced a redraw every frame. I alternated runs with the wrap on and off through a temporary environment switch and a per-frame `CLOCK_THREAD_CPUTIME_ID` timer, neither committed. Other sessions had the machine heavily loaded (load average 17–46). | | frames/s | main-thread CPU / frame | |---|---|---| | wrap on (5 runs) | 94 avg (113–120 in quiet stretches) | 2.22 ms avg | | wrap off (5 runs) | 99 avg (110–120 in quiet stretches) | 1.95 ms avg | The spread inside a single run (1.3–2.8 ms) is larger than the gap between on and off. The fps dips in both columns came with *lower* CPU per frame, so they were waits on the GPU or display under contention, not frame work. `sample` (5 s, wrap on, about 600 frames) attributes about 3 samples to `FrameTarget.begin` and about 3 to the blit's own drawing. The window bind in `FrameTarget.end` (61 samples) is mostly the flush that encodes the frame's own pass, work that otherwise happens at present. Most of the main thread's time was the `nextDrawable` wait, which is headroom. Idle, no frames run either way. The new GPU cost is one full-window textured quad plus the target's clear pass per frame. Fps held at 120 both ways when the machine was quiet. ## SDK impact None. `zig build test-sdk-version` passes and the fingerprint didn't move: the plugin boundary isn't touched, and the backend isn't part of the SDK. ## Verified - [x] `scripts/check-fmt.sh` - [x] `zig build` - [x] `zig build test` - [x] `zig build check-tests-reached` - [x] `zig build check-web` - [x] `zig build test-sdk-version` - [x] `zig build test-integration` (rc 0) - [x] `fizzyedit/example-app`'s dvui demo, built against this tree on macOS: a window capture shows the frame drawn correctly through the target, with dvui's in-app floating demo window (a deferred subwindow) and the frame below the title band. - [x] By hand on `run-dvui` (the user, macOS 26), with the wrap on: OS-window floats, tear-off and back in (with #591, #594, #605), and in-app windows in each material (#589). - [x] Gates on #589's head, which holds this: `scripts/check-fmt.sh`, `zig build`, `test`, `check-tests-reached`, `check-web`, `test-sdk-version`, `test-integration` all pass. - [ ] Not exercised: a full-screen transition, Linux and Windows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the multi-window plan (fizzyedit/plans#26): the tear-off (#577, #591).
The bug (the user, on
run-dvui): dvui's Debug window, opened from the demo window, never tore off; the demo window itself did. Diagnosed by the session working on #584.Why:
Window.drawRetained.os_windows.afterFrameonly after the tear-off had looked for the floating window being dragged (tearFrame), and only while OS windows were open.used), and hadn't been noted as able to close (closable, whichfloatingWindowOsnotes as dvui begins it). It was never the dragged window, and never tore off. Anything else dvui draws indrawRetainedwas the same.The fix: with tear-off on (
viewports.tears),afterFramedraws the retained layer first.drawRetainedkeeps to once a frame, andWindow.endwould have drawn it at this point anyway, so dialogs and toasts are drawn no differently.SDK impact
Verified
scripts/check-fmt.sh,zig build,zig build test,zig build check-tests-reached,zig build check-web,zig build test-sdk-versionpass.zig build test-integrationpasses. Its first run aborted inautomation service: tape bytes in, a ticket back, the outcome by ticket("replaying to 4860 ms did not reach what playing did"); a rerun with no change passed. That test runs on dvui's testing backend, which never reaches this code (viewports.tearsis false there): flaky under load, not this change.run-dvui: open the Debug window from the demo and drag it out.🤖 Generated with Claude Code