Repository navigation
backend: tear-off across a title bar, and back in at once - #605
Merged
Merged
Conversation
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, and dvui held its top below the title bar, away from the pointer: the window jumped off the pointer as it crossed (the user). The tear-off now asks whose content the pointer is over (`windowContentAtPointer`, new beside `windowAtPointer`): over a title bar, none, and the window stays out until the pointer reaches the content.
A torn-off window moved by the OS went back into the main window only once all of it lay inside the main window, and only on a frame after the OS's move let go, which waited for the next event; then it shrank back through a card of its last picture, its title bar's place becoming dvui's header's, and flashed a title on the way (the user: "takes more events", "pops a title"). Now it goes back in as the OS's move lets go with the pointer over the main window's content: frames keep coming while the OS moves it, 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; and the window, over it, closes the frame after (`closing`), so neither is missing for a frame. The card, its drawing and `mainHolds` go.
Going back into the main window, a torn-off window snapped into its floating window (the user). Its window now stays over the floating window for 150 ms with its last picture, the pointer passing through it (`viewports.held`), and fades out (`viewports.fade`): the OS's title bar dissolves into dvui's header while the content, the same in both, stays still.
foxnne
marked this pull request as ready for review
October 19, 2026 00:49
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, #594). Three commits, all from the user's testing on
run-dvui.1. A window dragged over a title bar stays out of it.
windowContentAtPointer, new besidewindowAtPointer). 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:
Now:
pointerOverMainContent).osMoving), so the let-go is seen at once.closing), so neither is missing for a frame.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").Merge,drawCard) andmainHoldsare gone.While there:
viewportInMaingets its doc comment back. It had sat aboveviewportMainHoldssince that was added.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-version,zig build test-integrationall pass.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