Skip to content

backend: viewports.Picture, fizzy.addDvui, the pointer over OS windows - #326

Merged
foxnne merged 7 commits into
mainfrom
backend/viewport-picture
Oct 10, 2026
Merged

foxnne merged 7 commits into
mainfrom
backend/viewport-picture

Conversation

@foxnne

@foxnne foxnne commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #281.

This PR now carries three changes. #329 and #330 were stacked on it, and they merged into this branch, not into main. Squash-merging it lands all three on main as one commit:

  1. viewports.Picture, below: one way to draw an OS window's picture.
  2. build: a dvui app of its own on fizzy's backend (fizzy.addDvui) #329: fizzy.addDvui, a dvui app of its own on fizzy's backend or on dvui's. It also fixes a copy pass left open while the main window took its drawable, and the menu-bar exports a dvui app didn't link.
  3. popout: a demo's pointer shows over float, menu and dialog windows #330: a demo's pointer shows over float, menu and dialog windows.

main (#324, #325) is merged in. #324 landed squashed, and its two files here are the same plus Picture, Picture.again and sizedTarget. After the merge, zig build, test (514/515), test-integration (358/358), check-web, test-sdk-version and the Windows cross-build pass on macOS.


viewports.Picture is the one way to draw an OS window's picture. Begin it on a target at the part of the frame the window shows, draw subwindows' drawing into it (taken out of the frame, or copied), end it, then present. Popout drew its four kinds of window (float, menu or dialog, carry, overlay) with four near-copies of those steps. The windowing plan's §3, "One way to fill a window", asks for one. It is also what a plain dvui app on this backend needs to put a dvui.floatingWindow into an OS window, which is what example-app's run-dvui builds on next.

Changes

  • backend/src/viewports.zig adds:
    • Picture.begin(target, part) clears the target and makes it dvui's render target, offset to part of the frame.
    • Picture.subwindow(sw, take) replays a subwindow's commands into it. With take, it also empties them, so dvui's replay into the main window draws none of it.
    • Picture.moveTo(at) draws the same layer again from another part of the frame (the overlay draws carried things at each window's part).
    • Picture.end() goes back to the frame's own target.
    • sizedTarget(slot, w, h) keeps a target the size asked for, or makes a new one.
  • src/editor/Popout.zig: the four sites use them, with the same order of drawing and the same take-or-copy choices. The float window keeps its own keepTarget, which holds a bigger texture through a full-screen transition.
  • backend/src/viewports_none.zig (backend: the viewports API is the backend package's, with a stand-in #324's stand-in): no-op Picture and sizedTarget, so it keeps every name viewports has. The web, the integration tests and dvui's SDL3 backend analyse Popout against it.

Verification

  • zig build, test, test-integration, check-web, test-sdk-version, the backend's tests, and the Windows and Linux cross-builds pass on macOS: test 514/515 (1 skipped), test-integration 358/358, backend 46/46.
  • Sandbox, macOS:
    • Soak: the soak tape (tests/tapes/soak.zon: floats out into OS windows three times, full screen, dialogs) ends in a passing verdict, with 1,426 viewport presents, OS windows back to one at the end, no leaks, and nothing logged as an error.
    • Menu in a float: a tape that floats the explorer out and right-clicks a file in it passes too.
    • Not seen: I couldn't screenshot the windows, because this session has no screen-recording permission. So the pictures themselves weren't looked at, only that each window got one every frame.

🤖 Generated with Claude Code

foxnne and others added 2 commits October 9, 2026 22:11
A dvui app on fizzy's backend opens its OS windows through `@import("backend").viewports`,
the API fizzy's Popout used from `src/backend/backend_native.zig`, moved into
`backend/src/viewports.zig` as it was. Fizzy re-exports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Popout's float, menu, carry and overlay windows each drew their picture with a copy of the same
steps; `viewports.Picture` and `sizedTarget` in the backend package are those steps once, for any
dvui app on the backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne and others added 2 commits October 10, 2026 06:52
`viewports_none` (backend/src/viewports_none.zig) has every name `viewports` has, each saying
there are no OS windows: the web's stand-in moved into the package, for fizzy on the web, on
dvui's testing backend and on dvui's own SDL3 backend, and for any dvui app built on either kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stand-in gains Picture and sizedTarget, so it keeps every name viewports has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from backend/viewports-api to main October 10, 2026 13:03
@foxnne
foxnne enabled auto-merge (squash) October 10, 2026 13:04
foxnne and others added 3 commits October 10, 2026 08:04
Part of #281: what example-app's `run-dvui` builds on. Stacked on #326
(on #324); review those first. The example-app side is
fizzyedit/example-app#4.

**`fizzy.addDvui(fizzy_dep, root_module, backend)`** gives an outside
app's module dvui on one of two backends:
- `.fizzy`: fizzy's own, where each floating window can be an OS window
of its own;
- `.sdl3`: dvui's own SDL3 backend, which has the main window only.

The module gets `dvui` (fizzy's pin), its `backend`, and
`viewports_none` (#324), so the same app code builds on either:

```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`, and on macOS the backend's
Objective-C compiled into it. `build/exe.zig`'s backend switch is now
`nativeDvui`, shared by fizzy's own executable and `addDvui`.

## Two fixes a plain dvui app needed

1. **The main window's drawable was acquired with a copy pass still
open** (`GpuRenderer.acquireSwapchain`). The frame's texture uploads had
left it open, and SDL asserted on every frame ("Cannot acquire a
swapchain texture during a pass"). Fizzy never hit this because it draws
its frame into a target first. Any dvui app draws straight into the
window. The fix ends the copy pass before acquiring, as `presentInto`
and `clearPass` already do.
2. **`platform`'s menu-bar exports weren't emitted** (`FizzyMenu*`).
`macos/menu_target.m` calls them and is compiled into every macOS app on
the backend, but Zig emits an `export fn` only when analysis reaches its
file. Fizzy reaches `platform/menu.zig` through its menu bar; a dvui app
with none failed to link. Now `platform/root.zig` references them, and
they're `pub`.

## Verification

- **Fizzy:** `zig build`, `test` (514/515, 1 skipped),
`test-integration` (358/358), `check-web`, `test-sdk-version`, the
backend's tests (46/46), and the Windows and Linux cross-builds pass on
macOS. The soak tape ends in a passing verdict (1,429 viewport presents,
no leaks, nothing logged as an error).
- **example-app's `run-dvui`, built against this branch, on macOS:**
- **On fizzy's backend:** it opens its main window and two floating
windows. Each is an OS window of its own, opened over its place in the
main window (window list: main at 540,332; floats at 652,476 and
684,500, each 280×212). No assertions or warnings, and it quits cleanly
on SIGTERM.
- **With `-Ddvui-backend=sdl3`:** the same app shows only its main
window, with the floating windows in it. No warnings.
- **Not seen:** I couldn't screenshot, since this session has no
screen-recording permission. Window positions and the logs are what I
checked, not how they look.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
)

Stacked on #326 (on #324); review those first.

A demo or live tape's pointer was drawn only in the main window. Over a
float's OS window, a menu or a dialog, it vanished, so a viewer couldn't
see it reach a float's close button, a menu row or a dialog's buttons.
Reported by @foxnne.

## Why

- **The pointer's layer was the size of the main window.**
`replay.overlay` made it a subwindow covering the whole window. Popout
gives each OS window the subwindows whose middle is in its part of the
frame, so the pointer layer always counted as the main window's.
- **Even placed right, a float's window would have claimed it before a
menu or dialog over the float could.** Floats are drawn first
(`windowFrame` before `menuFrame`). Dialogs are made in `drawRetained`,
after the pointer was raised, so they stack above it.

## Changes

- **`sdk/replay/overlay.zig`:** the pointer's layer is a small rect
round its tip, so its middle is where the pointer is. Drawing is still
clipped to the screen the pointer is on (`dvui.screenFor`), not to that
rect. Any dvui app that splits its frame across windows by subwindow
middle gets the same behaviour.
- **`Popout.overlaysFrame`, new, last in `endFrame`:** each subwindow
that takes no input (`mouse_events` false: the pointer and its click
ripple, a tooltip) is drawn into the topmost window over its middle.
That's a menu's or dialog's window, else a float's. Over none of them,
it's left to the main window.
- `windowFrame` now leaves such subwindows for this pass, unless they
are drawn everywhere (a drag's layer).
  - Carried layers stay the carry window's.
- **`viewports.Picture.again`** (and its stand-in): draws more into a
picture already handed over this frame, keeping what's there. `present`
only names the target; the window copies it when the frame ends. `begin`
is now `clear` plus `again`.

## Verification

- `zig build`, `test` (514/515, 1 skipped), `test-integration`
(358/358), `check-web`, `test-sdk-version` and the Windows cross-build
pass on macOS.
- **Sandbox, macOS:**
- **Pointer in a float:** a tape floated the explorer out and moved the
pointer onto a file in it. A temporary trace in `overlaysFrame` (removed
before the commit) showed the pointer layer going into the float's
window while over it (middle at `200354,388`, the float's band). The
other input-less layer stayed with the main window.
- **Soak:** the soak tape still ends in a passing verdict, with no leaks
and nothing logged as an error.
- **Not seen:**
- The menu and dialog windows. The sandbox's menus close on the next
frame because it isn't the active app, and this session has no
screen-recording permission for screenshots.
- The drawing itself. The routing is what's verified, not what it looks
like.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#324 landed on main squashed; its files here are the same plus Picture, Picture.again and sizedTarget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne foxnne changed the title backend: one way to draw a viewport's picture (viewports.Picture) backend: viewports.Picture, fizzy.addDvui, the pointer over OS windows Oct 10, 2026
@foxnne
foxnne merged commit eec22bb into main Oct 10, 2026
9 of 10 checks passed
@foxnne
foxnne deleted the backend/viewport-picture branch October 10, 2026 13:17
@foxnne foxnne mentioned this pull request Oct 10, 2026
2 of 8 tasks
foxnne added a commit that referenced this pull request Oct 10, 2026
foxnne added a commit that referenced this pull request Oct 10, 2026
## What changes

`sdk_version` goes from 0.2.20 to 0.2.21. Merging tags `sdk-v0.2.21`,
publishes its tarball,
and asks the store plugins to repin.

**Why now.** The plugin boundary's fingerprint moved since `sdk-v0.2.20`
(`0x7b455e7a…` →
`0x1bf417f6…`, by #331). Store plugins built against 0.2.20 do not load
in a fizzy built from
`main`, and fizzyed.it/app is built from `main`. The chat plugin
(`fizzyedit/chat`) also needs
#331's `Host.documentTitleChanged`, and builds against a local fork
until this is out.

## SDK impact

- [ ] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [ ] Fingerprint moved: recorded in `sdk/src/version.zig`,
`sdk_version` untouched, PR labelled `sdk`
- [x] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the
last `sdk-v*` tag

**The `sdk` PRs since `sdk-v0.2.20`:**
- #331: a surface's title can change (`Host.setSurfaceTitle`, the Host
keeping its own copy),
and a document's tab follows its owner (`Host.documentTitleChanged`).
`documentTitle`'s slice
  now need only last the call.

**Also reaching plugins (`core/`, `sdk/replay`):**
- #325: floats' screens are dvui's (`core/screens.zig`).
- #326: `replay.overlay` draws through the backend's viewport picture.
- #301: `sdk/replay_module.zig` follows `replay-app`'s move to
fizzyedit/example-app.

## Verified

- [x] macOS:
  - `zig build` and `test-sdk-version` on this head.
  - `scripts/pack-sdk.sh` packs `fizzy-sdk-v0.2.21.tar.gz`.
- Every store plugin (fresh pulls of pixi, zig, drive, atlas and ghostty
`main`) builds against
this tree's `sdk/` with `zig build --fork=<sdk>` (31/31 and 28/28 steps)
into a throwaway
profile. The fork matched, and the built plugin reports `pinned fizzy
SDK: 0.2.21`. The
fingerprint moved, so each repin PR is a real rebuild, but none should
open as a draft for a
    broken build. The agent and chat plugins build too (22/22, 19/19).
- [ ] Windows:
- [ ] Linux: CI.
- [ ] Web:

## Follow-ups

- Each repin PR merged and its plugin tagged (the plugin repos' tags
stay the maintainer's).
- `fizzyedit/agent` and `fizzyedit/chat` repin to the `sdk-v0.2.21`
tarball.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant