Skip to content

Native windows plan: fizzy's backend between SDL and dvui, and the drag's glass in Liquid Glass - #226

Merged
foxnne merged 11 commits into
mainfrom
claude/native-windows-plan
Oct 8, 2026
Merged

foxnne merged 11 commits into
mainfrom
claude/native-windows-plan

Conversation

@foxnne

@foxnne foxnne commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

A plan, as you asked: docs/NATIVE_WINDOWS_PLAN.md. It covers strengthening fizzy's control of its native windows, getting the drag's glass to look and merge like Control Center's on macOS 26, and keeping up with SDL as it moves. Docs only.

What Liquid Glass does, measured with throwaway AppKit programs on macOS 26.5:

The drag's glass on macOS 26: one overlay window per screen during a drag, holding the bubble's head and tail and every drop-zone bubble as glass views in one container. They merge and refract as Liquid Glass. The app's windows draw none of the drag while it shows.

Other platforms: over a window the drag is the app's own glass, merging as it did before #224. Outside every window it is a carry window with a simple material.

The backend as the layer between SDL and dvui:

  • SDL keeps the portable plumbing. Fizzy's backend owns window policy and composition, including a native-layers interface for glass.
  • SDL3 adopts windows an app creates (SDL_PROP_WINDOW_CREATE_COCOA_WINDOW_POINTER, ..._WIN32_HWND_POINTER, …). Fizzy can make its own NSWindow and view, so title bar drags (where the float-over-title-bar bug lives), live resize and the glass hierarchy become fizzy's code. That replaces SDL patches and runtime method swaps.
  • Keeping up with SDL: fewer patches, a scheduled job that rebases the patch stack onto SDL's latest release and builds fizzy, and an acceptance test per patch.

Updated 2026-10-05:

  • Your principle: Liquid Glass as much as possible where the OS has it, private API where it's stable and falls back gracefully, every platform as close as it can get, and the whole range from opaque to glass underneath.
  • The overlay is built (Native glass, one material, and full screen as the window itself: the drag's glass in Liquid Glass, every glass on one slider #227) and on by default on macOS.
  • Fable's 2026-10-04 backend review is folded in:
    • one WindowChrome per OS window;
    • SDL patched only where SDL owns the code path (Space/zoom resize events, title-bar drag regions, Win32 caption hit-testing). This replaces the first draft's plan to hand SDL windows fizzy creates; that stays as a fallback;
    • the backend cleanup comes first;
    • packages;
    • peer-or-palette and colour tagging as open questions;
    • a backlog of everything else it found.
  • Materials from opaque to glass across platforms, with the main window on the same material and the opacity slider running across it. Reduce Transparency and Reduce Motion win over looks.
  • Screen-edge tiling for a carried view, done by fizzy. The OS only tiles windows the window server itself is dragging.

Steps:

  1. Pop-out and settings fixes: a restored float stays in front, picker drags float off the desktop, menus keep off the edge, settings scroll steady, the carried view is always a window (Liquid Glass on macOS 26) #224 (done)
  2. Overlay prototype behind FIZZY_NATIVE_GLASS=1
  3. Native layers in the backend
  4. The other platforms' path
  5. Fizzy's own macOS windows, then Windows
  6. The SDL path

Open questions:

  • Whether a private glass variant is acceptable for fizzy's look at all.
  • Which material Control Center uses.
  • What SDL stops doing for a window it adopts.
  • Several screens.

🤖 Generated with Claude Code

… the drag's glass in Liquid Glass

What Liquid Glass does, as measured on macOS 26.5 (public styles frost; a private variant is a
lens that refracts across windows; containers merge glass within one window, over Metal too); the
drag's bubble and drop zones as one overlay of Liquid Glass on macOS 26, and the simpler path
elsewhere; fizzy's backend owning its native windows (SDL3 adopts windows fizzy makes) so title
bar drags, live resize and glass leave SDL patches and runtime method swaps; and keeping up with
SDL's releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s from opaque to glass, screen-edge tiling

The user's principle (Liquid Glass as much as possible where the OS has it, private API when stable
and built to fall back, every platform as close as it can, opaque through glass underneath); the
overlay marked built and on by default; Fable's 2026-10-04 backend review folded in — one
WindowChrome per window, SDL patches only where SDL owns the path (replacing the first draft's
adopted-windows plan, kept as a fallback), the backend cleanup first, packages, peer-or-palette,
colour, and a backlog of what else it found; materials from opaque to glass across platforms, the
main window on the same material, accessibility; screen-edge tiling done by fizzy, since the OS
only tiles windows the window server drags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tive forms of every glass surface

The user's design: one slider from clear refracting glass through blur and the window's colour to
opaque in the window's colour, for everything drawn as glass; every glass surface in two forms on
it — in-app (the shader, kept: the web and every platform without native window glass, tuned to
look like macOS's glass) and native (the OS's glass in the OS's own style, for surfaces that are OS
windows, and NSMenu for context menus on macOS). The native way along it as built in #227 (a
two-layer crossfade between Liquid Glass variants, the window's colour under the glass so it keeps
its shine, flat colour only at the top), and why the aimed bubble is lit in that colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne force-pushed the claude/native-windows-plan branch from f91b1c9 to 15457e8 Compare October 5, 2026 17:48
foxnne and others added 7 commits October 5, 2026 13:05
…ch platform's own glass, the in-app glass behind it

From three studies (Windows, Linux, the in-app glass against Apple's): one vocabulary of roles
(window, transient, carry, drop, scrim), the slider and each platform's policy probe; one pure
mapping module; each platform's realization in its own style — Liquid Glass on macOS 26, DWM's
Mica/Acrylic/Smoke along the slider the way Windows Terminal maps its opacity on Windows 11, the
compositor's blur region (ext-background-effect-v1, KDE's older protocol) on Linux where offered
and opaque where not, the in-app glass on the web — what to skip on each, Windows composition as a
phase 2; the in-app glass tuned to Apple's (an inward bevel pull that folds and magnifies, which is
a shader change in four copies), and the rules that keep it small. The native frost now fades out
toward the edges, its rim the clear lens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s the library's first plugin consumer

The user's candidate: pixi's corner glass buttons and its colour-dropper magnifier as one liquid
layer — the orb pinching off the magnifier button, floating round the pointer with pixi's pixel
zoom and crosshair in its middle and a refracting rim, merging with the buttons — and what the
library needs for it: glass outside drags with groups, content over the glass from any code, a
magnifier look, motion from core, all reachable from a plugin through core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s built, compact toolbar corners

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…up windows that fizzy draws

The user's decision (2026-10-06): a context menu is an OS popup window with fizzy's own menu drawn
in it — Liquid Glass on macOS 26 (vibrancy before), DWM Acrylic on Windows 11, the compositor's
blur region on Linux — not NSMenu or a Win32 menu, and not an overlay inside the main window. The
plan gains the section: SDL popup windows parented to where the menu opens (an xdg_popup on
Wayland), the menu copied out of the frame as floats copy theirs, input mapped back, the display
as the menu's screen; the open questions (keyboard focus, dismissal, several displays). Steps
renumbered with it as step 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne marked this pull request as ready for review October 8, 2026 13:38
@foxnne
foxnne merged commit 03f6066 into main Oct 8, 2026
@foxnne
foxnne deleted the claude/native-windows-plan branch October 8, 2026 13:38
foxnne added a commit that referenced this pull request Oct 8, 2026
Part of #226 (`docs/NATIVE_WINDOWS_PLAN.md`)

## What changes

Adds `docs/WINDOWS_LINUX_GLASS_PLAN.md`, the research behind the plan's
Windows Phase 2 and its Linux blur region. It takes apart what the macOS
pop-out and Liquid Glass work rest on, piece by piece per OS, and how
Windows and Linux can supply each piece, with sources.

The findings:
- No OS but macOS 26 merges or refracts glass. Elsewhere fizzy keeps the
shape (LiquidField's smooth union) and the OS supplies only the blur:
- **Windows:** the visual layer's host backdrop, clipped to the field's
outline.
- **Wayland:** `ext-background-effect-v1` (GNOME 51, Plasma 6.7, niri),
with `org_kde_kwin_blur` as the fallback for older Plasma.
- Carried views on Windows go into one overlay per display.
- X11 is deprioritised.
- Step 0 for both platforms: the SPIR-V and DXIL glass programs predate
#227.

The plan's Windows decisions stand: the slider along DWM's backdrop
types, one activation group, an Acrylic carry window, and the drop zones
in the app's glass. Per-shape glass through Windows.UI.Composition is
still the plan's "Phase 2, only with a go-ahead".

Steps 3 (Windows menus as Acrylic popups) and 4 (the Linux blur) are
built in the two PRs stacked on this one.

This is a cloud session's research. Its claims marked **(spike)** were
not run.

## SDK impact

- [x] None

## Verified

Docs only.

## Follow-ups

The two step PRs stacked on this one.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
Part of #226

## What changes

Two pieces pixi's dropper magnifier (fizzyedit/pixi#3) needs to be a
clear liquid orb over its zoom, with no frost and strong refraction:
native Liquid Glass on macOS, the app's glass everywhere else. And the
SDK release that ships them, so the orb can be tried against `main` as
soon as this merges.

**`core.native_glass.offered`**
- The app publishes each frame whether the OS draws glass declared
outside a view drag (`publishOffered`, from `Popout.beginFrame`'s
`nativeGlass()`).
- A plugin that sees it declares its glass with `native_glass.add`.
Fizzy's overlay of Liquid Glass already stays up while any glass is
declared.
- Pixi declares a clear piece (`frost = 0`, the lens alone) over a zoom
it draws in the window, so the OS's lens refracts the window beneath.
- Off macOS 26, or with native glass off, `offered()` is false.

**`LiquidField.drawPicture` / `pictureMargin` / `lens`, and
`glass_look.forLens`**
- The app's glass as a lens over a picture the plugin drew. The middle
is the picture as it is, with no frost, colour or lift; the rim bends
and lights it.
- It reads no capture, so the middle stays pixel-exact.
- On the web, where no look is published, it is the earlier glass.
`pictureMargin` says how far past the shapes the picture should reach
for that glass's rim, at the strongest shape lens.

**Tried and dropped:** pixi tried the drop zones' frosted glass with its
zoom laid over it. On a Mac it read as a thick grey border or a flat
rim.

**Merged with main** (#234–#236, #239, #240).

## 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

`sdk_version` goes 0.2.17 → 0.2.18 (ef95301). Since `sdk-v0.2.17` this
release carries:
- this PR's core additions;
- #223, #224 and #227's core (the one-slider glass, `glass_look`,
`core.native_glass`, `core.screens`'s menus and dialogs).

The fingerprint has not moved, so installed plugins keep loading.
Merging tags `sdk-v0.2.18` and, per #240, asks the store plugins to
repin.

## Verified

- `zig test core/gfx/glass_look.zig`: 13 pass.
- `zig fmt --check` and `zig ast-check` on the changed files.
- `tests/integration.zig` compiles `drawPicture` and checks
`pictureMargin`.
- CI: Linux, macOS, Windows, integration and the Windows cross-build on
this head.
- [x] macOS: pixi's orb tried locally against this branch by the
maintainer, who approved it.
- [ ] Windows:
- [ ] Linux:
- [ ] Web:

## Follow-ups

- pixi#3 repinned to `sdk-v0.2.18` once it's tagged.
- The orb pinching off pixi's sample button and merging with it (the
plan's next step for this consumer).

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

https://claude.ai/code/session_01Euk6iCavQdW7MG5FN2vvFw

---------

Co-authored-by: Claude <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
…=1 (#237)

Part of #226 (`docs/NATIVE_WINDOWS_PLAN.md`, "Linux — the compositor's
blur"); step 4 of `docs/WINDOWS_LINUX_GLASS_PLAN.md` (#236).

## What changes

With `FIZZY_BLUR_BEHIND=1`, on a Wayland compositor that blurs, the
window's base goes translucent at the window opacity over the desktop's
blur, as it already does over macOS's vibrancy and Windows' Acrylic.
Elsewhere, and without the switch, nothing changes: the window stays
opaque.

- **`platform/wayland_blur.zig`:** binds `ext-background-effect-v1`
(GNOME 51, Plasma 6.7, niri) or KDE's `org_kde_kwin_blur`.
- It uses SDL's own libwayland-client (`RTLD_NOLOAD`), with both
protocols declared by hand, on an event queue of its own.
- The region is sent only when it changes, and it lands with the commit
the frame's present makes.
- "No blur" is an empty region on the ext protocol, and the blur object
unset and released on KDE's protocol (KWin blurs the whole window behind
an empty one).
  - The first probe logs what it found.
- **`platform/blur_region.zig`** (std-only, unit tested): steps the
rounded frame into rectangles.
- **`linux_titlebar.blurBehind` and `Editor.tick`:** the blur covers the
frame inside the shadow's margin while the window is translucent, and is
square when maximized.

Off by default until someone sees it on a desktop that blurs.

## SDK impact

- [x] None

## Verified

- [x] Linux, Ubuntu 26.04 / GNOME 50 (aarch64 UTM VM, native Wayland,
`FIZZY_BLUR_BEHIND=1 WAYLAND_DEBUG=client`). GNOME 50 has neither
protocol, so this covers the "absent" path only:
  - one `get_registry` on fizzy's own unnamed queue;
- the log line `ext-background-effect-v1 absent, org_kde_kwin_blur
absent`;
  - no `wl_display.error`;
  - fizzy alive after 12 s, window opaque.
- [x] Protocol details:
- opcodes checked against wayland.app (`ext-background-effect-v1`) and
plasma-wayland-protocols' `blur.xml`;
- KWin's empty-region-means-whole-window rule read in
`src/plugins/blur/blur.cpp` on `Plasma/6.6`. Master only takes a
non-empty region.
- [x] A stub compositor speaking both protocols, in the cloud session
that wrote this: the region applied on commit, unchanged frames sent
nothing, cleared when opaque, KDE's unset, nothing asked when
capabilities = 0.
- [x] `fizzy-blur-region-tests` (5 tests).
- [x] Gates: `zig build`, `test`, `test-integration`, `check-web`,
`test-sdk-version`, and the Windows and Linux cross-builds.
- [ ] Not seen where the blur would actually show: GNOME 51, Plasma
6.7+, or Plasma ≤6.6 (KDE's protocol and the "off" path). Please try one
of those before this goes default-on:
  - the blur sits inside the frame, not in the shadow margin;
  - the corners line up;
  - maximized is square and full;
  - opacity 1 gives no blur.
- [ ] macOS / Windows / Web: no change there (the code is
`comptime`-gated to Linux; it builds on all of them).

Relies on the `envSwitch` fix (#235, merged): before it,
`FIZZY_BLUR_BEHIND` read as `FIZZY_POPOUT`.

## Follow-ups

- Default-on once seen on GNOME 51 or Plasma.
- Wayland popups (menus and dialogs as `xdg_popup`s) are the other half
of step 4, not built.

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

Co-authored-by: Claude <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
Part of #226 (`docs/NATIVE_WINDOWS_PLAN.md`, Phase 2); step 5 of
`docs/WINDOWS_LINUX_GLASS_PLAN.md` (#236).

## What changes

Adds `spikes/windows-composition/`, a standalone Windows program
answering the four questions the plan's Phase 2 (the drag's glass as an
overlay over the desktop's blur) waits on. Nothing in fizzy changes. The
research doc's step 5 records the answers.

How it works:
- **It uses fizzy's own pins** of fizzyedit/SDL and zigwin32.
- **The window is fizzy's kind:** a transparent SDL window presenting on
D3D12 through the fork's topmost DirectComposition target.
- **Under it:** a Windows.UI.Composition target that isn't topmost,
holding the host backdrop, cut to a pill (∪ an orbiting circle) that
sweeps across the window. Five modes:
  - no clip;
  - a rounded-rect clip;
  - a Direct2D path rebuilt every frame;
  - an AlphaMask effect over a swapchain the app presents;
- the app's picture hosted in composition's own tree (the fallback for
question 1).
- **An overlay over the whole display** tries four click-through styles.
- **WinRT is bound by hand.** Every GUID and vtable slot was read from
the OS's own WinMetadata with System.Reflection.Metadata, and each is
cited in `src/winrt.zig`. `IGeometrySource2D` and `IGraphicsEffect` are
COM objects written in Zig.

At about 1,700 lines this is over CONTRIBUTING's 800-line guide. It's
throwaway code in `spikes/`, built by nothing else.

## SDK impact

- [x] None

## Verified

Windows 11 ARM VM (`fizzy-win11`, build 26300, D3D12 on WARP, 60 Hz),
aarch64-windows-gnu, driven by `SendInput`. Offsets were measured from
screenshots: the clip's glass against the red box SDL presents round the
pill, six samples per run, with a direction mark telling lead from lag.

1. **Two targets on one HWND: yes.** `CreateDesktopWindowTarget(hwnd,
FALSE)` on SDL's claimed window succeeds and draws under SDL's picture.
Static, the two align to the pixel.
2. **Path clip rebuilt every frame: works and is antialiased** (1–2 px).
Composition calls `GetGeometry` once per frame. With SDL's VSYNC present
the clip **leads** the picture by the present queue:

   | SDL present | Clip delay | Glass vs SDL's rim |
   |---|---|---|
   | VSYNC (frames in flight 1 or 2) | 0 | ~2.2 frames ahead (35 ms) |
   | VSYNC | 1 | 1.3 frames ahead |
   | VSYNC | **2** | **0 ±0.5 px** |
   | MAILBOX / IMMEDIATE | 0 | **0 ±0.5 px** |

So fizzy either gives composition the shape from as many frames back as
the present queue is deep (measured, not assumed), or has the fork
present through a frame-latency waitable swapchain.
3. **Effect-graph mask: yes.** D2D's AlphaMask with a
`CompositionEffectSourceParameter` over
`CreateCompositionSurfaceForSwapChain` shows the glass exactly where the
presented mask is.
4. **Overlay:**

   | Style | Same-thread click | Another process | Tree shows |
   |---|---|---|---|
   | `TRANSPARENT \| LAYERED` | passes | passes | yes |
   | `TRANSPARENT \| LAYERED`, alpha 255 | passes | passes | yes |
   | `TRANSPARENT` alone | blocked | blocked | yes |
| `HTTRANSPARENT` | passes | **blocked** (though `WindowFromPoint` says
it passes) | yes |

A drag under the layered overlay keeps its capture (30/30 motion
events). `SDL_SetWindowOpacity(0.6)` (layered) leaves both SDL's picture
and the composition tree drawing.

- [ ] **The blur on a hardware GPU.** The VM draws the host backdrop
black. Run it, press `3` (path) with the backdrop on, and check the blur
and the edge, at clip delay 0 and 2 (`D`).
- [ ] **The present queue's depth at 60/120/144 Hz** on hardware, and
Windows 11 23H2/24H2.
- [ ] macOS / Linux / Web: n/a (Windows only; not part of fizzy's
build).

## Follow-ups

- If the blur holds on hardware: the overlay (step 6), with the clip
delayed by the measured present-queue depth, or a waitable swapchain in
the SDL fork.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
…#238)

Part of #226 (`docs/NATIVE_WINDOWS_PLAN.md`); step 3 of
`docs/WINDOWS_LINUX_GLASS_PLAN.md` (#236).

## What changes

With floats as windows (`FIZZY_POPOUT=1` on Windows), menus and dialogs
now leave the main window as they do on macOS (`viewports.menus`). Each
one is a borderless window owned by the window it opens from, never
activated, and dressed by DWM (`win32_titlebar.viewportMenuChrome`):
- Acrylic (`DWMSBT_TRANSIENTWINDOW`, the material of Windows 11's own
menus);
- DWM's rounded corners and shadow;
- no border, no system menu, no DWM transitions.

DWM draws a never-activated window's backdrop as its solid fallback, so
the menu's window is kept looking active (`WM_NCACTIVATE`). An owned
window isn't carried when its owner moves, so a menu's window rides on
nothing and is placed each frame.

## SDK impact

- [x] None

## Verified

Windows 11 ARM VM (`fizzy-win11`, WARP, aarch64-windows-gnu Debug),
driven with real input (`SendInput`) and checked against the window list
(`EnumWindows` plus DWM attributes) and screenshots:
- [x] The File and Help menus and the "Unsaved changes" dialog each open
in a window of their own:
  - owned by the main window and directly above it in the z-order;
  - `WS_EX_NOACTIVATE`, no `WS_SYSMENU`;
  - backdrop type Acrylic (3), corner preference round;
  - the main window stays the foreground window.
- [x] The first click on an item acts: New File opens `untitled-1`, and
Open Files opens the Open dialog. A temporary log showed the press
arriving on the menu's window and mapping into the menu's dvui
subwindow, not what lies under it.
- [x] The dialog's window hides when the main window is minimized and
comes back on restore.
- [x] Gates: `zig build`, `test`, `test-integration`, `check-web`,
`test-sdk-version`, and the Windows and Linux cross-builds.
- [ ] **The look on a hardware GPU.** The VM's DWM draws every backdrop
as its fallback, so these need a real machine:
  - Acrylic rather than a solid colour;
  - rounded corners and a shadow;
  - whether the `WM_NCACTIVATE` trick keeps the Acrylic.
- [ ] A menu opened over a float's window. Not exercised; creating a
float needs a view drag.
- [ ] macOS / Linux / Web: unchanged (`viewports.menus` was already on
for macOS, and this is the Windows branch).

Seen while testing, not caused by this change:
- A menu closes when fizzy is deactivated. My harness's own process
start deactivated it, which made a later click fall through to the main
window.
- A dialog drifts a few pixels left over each minimize/restore. It does
the same drawn in-window (`FIZZY_NATIVE_DIALOGS=0`).

## Follow-ups

- NATIVE_WINDOWS_PLAN's Phase 1 on Windows: the slider along DWM's
backdrop types, one activation group, and a policy probe.
- Caption buttons in a Windows float's header (in
`app/layout/Floats.zig`).

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

Co-authored-by: Claude <noreply@anthropic.com>
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