From 13d5b183ca0baf0095760acfc79241e8a0a9a278 Mon Sep 17 00:00:00 2001 From: foxnne Date: Thu, 8 Oct 2026 09:13:52 -0500 Subject: [PATCH 1/2] popout: each FIZZY_* switch reads its own environment variable `Popout.envSwitch` cached its answer in a struct declared inside the function, and the struct did not name the variable. Zig gives a struct like that one type for every comptime argument, so every switch shared one cache: whichever was read first in a run answered for all of them. In practice `FIZZY_POPOUT` is read first (`beginFrame`), so `FIZZY_NATIVE_GLASS=0`, `FIZZY_NATIVE_MENUS` and `FIZZY_NATIVE_DIALOGS` were ignored and took `FIZZY_POPOUT`'s value instead (unset: the settings). The struct now holds the name, which makes it a type of its own per switch. Seen while testing a new switch in the Ubuntu VM: it never turned on. Reproduced outside fizzy with the same function in a 15-line program on Zig 0.16.0 (`ENV_B=1` read as null, `ENV_A=1` made B true), fixed there the same way. --- src/editor/Popout.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/editor/Popout.zig b/src/editor/Popout.zig index 06acbcd3..e6524e88 100644 --- a/src/editor/Popout.zig +++ b/src/editor/Popout.zig @@ -142,13 +142,17 @@ pub fn restartPending() bool { /// rules then. For testing and sandboxes, over the settings. fn envSwitch(comptime name: [:0]const u8) ?bool { if (comptime builtin.target.cpu.arch == .wasm32) return null; + // One cache per variable: the struct names `name`, so each switch gets a type of its own. + // Without it Zig makes one type for every switch, and the first one read answered for all + // (`FIZZY_NATIVE_GLASS=0` read as whatever `FIZZY_POPOUT` was). const Cache = struct { + const variable = name; var read = false; var value: ?bool = null; }; if (!Cache.read) { Cache.read = true; - Cache.value = if (std.c.getenv(name)) |v| !std.mem.eql(u8, std.mem.span(v), "0") else null; + Cache.value = if (std.c.getenv(Cache.variable)) |v| !std.mem.eql(u8, std.mem.span(v), "0") else null; } return Cache.value; } From c8de945b7badb9259ac39438ad55c8d3c965eb69 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 12:40:52 +0000 Subject: [PATCH 2/2] docs: native windows and glass on Windows and Linux, the research What the macOS pop-out and Liquid Glass work rests on, taken apart into the pieces each OS has to supply, and how Windows and Linux can supply them, with sources. No OS but macOS 26 merges or refracts glass (Microsoft has said Windows is not getting it), so elsewhere the shape stays fizzy's own (LiquidField's smooth union) and the OS supplies only the blur under it: on Windows the visual layer's host backdrop clipped to the field's outline (a backdrop cannot be a mask brush's source), on Wayland ext-background-effect-v1, which GNOME 51 and Plasma 6.7 now ship, its region applied with the surface commit. Carried views on Windows go into one overlay per display rather than a window moved under the pointer. X11 is deprioritised as its sessions are removed. Step 0 for both: the SPIR-V and DXIL glass programs predate #227 and still draw the earlier glass. It works under docs/NATIVE_WINDOWS_PLAN.md (#226), whose Windows decisions stand: the slider along DWM's backdrop types, one activation group, an Acrylic carry window, the drop zones in the app's glass. This file is the research for that plan's Phase 2 (per-shape glass through Windows.UI.Composition, which waits on a go-ahead) and for its Linux blur region. Co-Authored-By: Claude Opus 5.5 --- docs/WINDOWS_LINUX_GLASS_PLAN.md | 398 +++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 docs/WINDOWS_LINUX_GLASS_PLAN.md diff --git a/docs/WINDOWS_LINUX_GLASS_PLAN.md b/docs/WINDOWS_LINUX_GLASS_PLAN.md new file mode 100644 index 00000000..9975452e --- /dev/null +++ b/docs/WINDOWS_LINUX_GLASS_PLAN.md @@ -0,0 +1,398 @@ +# Native windows and glass on Windows and Linux + +Status: in progress. Research under `docs/NATIVE_WINDOWS_PLAN.md`. Steps 3 (Windows menus and +dialogs as Acrylic popups) and 4 (the Linux window material) are built in PRs of their own, each +behind a flag; nothing else here is. + +An investigation, not yet a design anyone has agreed to. macOS has floats as OS windows by default, +and on macOS 26 the OS draws a view drag's glass (`docs/POPOUT_WINDOWS_PLAN.md`, "P6"; +`core/native_glass.zig`). This asks what the same experience takes on Windows and Linux, what each +OS gives fizzy to build it from, and in what order to build it. The facts about each OS were +gathered in October 2026, with sources linked. Anything marked **(spike)** was not run, and is the +first thing to check. + +**The plan this works under is `docs/NATIVE_WINDOWS_PLAN.md`** (#226). Its decisions stand: on Windows, the slider runs along DWM's backdrop types as Windows Terminal maps its opacity +(no backdrop, then Acrylic, then Mica), fizzy's windows act as one activation group, a carry +window is DWM Acrylic, and the drop zones stay the app's glass, with no merging. Per-shape glass +over the desktop through Windows.UI.Composition is its "Phase 2, only with a go-ahead". This file +is the research for that phase (the overlay below) and for the Linux blur region, which its +"Linux — the compositor's blur" already designs. + +## What macOS does, taken apart + +The macOS work is one experience, but it rests on several separate things the OS provides. Each one +has a different answer on the other platforms, so they are listed separately: + +| # | Piece | macOS (built) | Windows today | Linux today | +|---|---|---|---|---| +| 1 | **Window material**: a blur of the desktop behind the main window and each float's window | vibrancy / Liquid Glass (`visual_effect_view.m`) | Acrylic through DWM (`win32_titlebar.zig`) | none: opaque (`LiquidField.publishOpaqueWindow`) | +| 2 | **OS frame**: corners, shadow, resizing from the edges, snapping | a titled `NSWindow` | DWM, with the app's hit test (built for the main window and floats) | client-side; Wayland frame insets for the shadow | +| 3 | **Window buttons on a float** | traffic lights (`viewports.os_buttons`) | none yet: the float's header needs caption buttons | the app's own | +| 4 | **Menus and dialogs in windows of their own** | `viewportOpenMenu` | not built | not built | +| 5 | **Carry window**: what a dragged view is carried as, shown past every window, in its own shape, with the material and a shadow, clicks passing through | `viewportOpenCarry` (`viewports.carries`) | not built: the drag is cut off at the main window's edge | not built | +| 6 | **The OS's glass**: drop bubbles and the carried drop merging like liquid, refracting what is behind them across every app | `NSGlassEffectContainerView` in the overlay (`viewports.liquidGlass`) | no OS equivalent | no OS equivalent | +| 7 | **One transaction**: a window's place, its glass and its picture reach the screen together | one Core Animation transaction a frame (`renderPresent`, SDL patches 3–5) | none: a composition commit and a swapchain present take separate routes | Wayland: the surface commit; X11: none | + +1–3 are mostly done on Windows. 4 and 5 are ordinary windowing work. 6 is where the platforms +really differ, and 7 decides whether whatever is built for 5 and 6 looks native or trails a frame +behind. + +## The split that makes it portable: the OS supplies the blur, fizzy supplies the shape + +On macOS 26 the OS does both jobs: it shapes the glass (merging the pieces, refracting) and fills +it with material. Nothing on Windows or Linux is like `NSGlassEffectContainerView`. Microsoft said +in August 2026 that there are "no immediate changes" planned to Windows' transparency +([Windows Latest](https://www.windowslatest.com/2026/08/10/microsoft-reveals-why-windows-11-wont-get-liquid-glass-style-ui-even-though-windows-vista-did-it-first-with-aero/)). +The Linux compositors that refract ([hyprglass](https://github.com/hyprnux/hyprglass)) do it as +compositor effects an app cannot ask for. What both platforms *can* do is blur what is behind a +window and clip that blur to a shape the app gives. Fizzy already computes the merged shape +itself: the smooth minimum in `LiquidField` (`shaders/liquid_glass.*`) and its CPU twin +`liquid_blob` are what the app's own glass draws, in every window, on every platform. + +So everywhere except macOS 26: + +- **the shape is fizzy's**: the merging, necks, rim light, lift, tint, icons and the carried + photograph are drawn by the app into a transparent window, as `LiquidField` already draws them + in-window; +- **the material is the OS's**: the OS blurs what is behind that window, inside an outline the app + hands it each frame (a clip path on Windows, a region on Wayland), traced from the same field. + +`core.native_glass` already holds the piece list this needs (rects, radii, `lit`, `alpha`, `frost`, +the merge distance), and `Popout.overlayFrame` already sizes and places a window around the glass. +What changes is the backend. Instead of handing the pieces to Liquid Glass, it draws them as the +app's glass with no frost of its own, and hands the OS the outline of the pieces that take frost. + +Three things are lost against macOS 26: + +- **No bending of what is behind.** The OS's blur is only a blur. No Windows or Linux API refracts + another app's pixels (Windows' composition effects take only built-in shaders, and displacement + is not among them; + [composition effects](https://learn.microsoft.com/en-us/windows/apps/develop/composition/composition-effects)). + Within fizzy's own windows that never mattered, because the app refracts its own frame. Over + the desktop, the clear lens at the bottom of the slider (`core/gfx/glass_look.zig`) can only be + clear: rim light, no bend. "Windows: real refraction" below is the way round it, at a cost. +- **The blur's strength is the OS's.** Windows' host backdrop arrives blurred: a Microsoft + engineer said it is blurred "for security reasons" + ([WindowsCompositionSamples #202](https://github.com/Microsoft/WindowsCompositionSamples/issues/202)). + Wayland compositors choose the blur radius themselves. The slider's frost (`rough_by`) can only + fade that blur in and out, not grow it. The user has already rejected that look once ("blurred + everything at every point"), so the mapping needs looking at again for these platforms. +- **The outline is a frame late, or stepped.** On Windows the clip and the picture travel by + different routes (below). On Wayland the region is in sync, but made of whole rectangles. + +## Step 0 (both platforms): the glass programs are out of date + +`core/gfx/shaders/compiled/{spv,dxil}` were last compiled in #206 (2026-10-03). +`liquid_glass.fragment.hlsl` changed in #227, and `LiquidField.publishLook` notes that the +precompiled Vulkan and D3D12 programs still draw the earlier glass. Until they are recompiled with +`shadercross` (the commands are at the top of the HLSL), Windows and Linux do not draw the lens or +the one-slider look even in-window. Recompiling them is cheap, and every comparison below assumes +it has been done. + +## Windows + +### What Windows gives + +- **DWM system backdrops**, `DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE)` (Windows 11 22H2+): + Mica, Mica Alt and Acrylic (`DWMSBT_TRANSIENTWINDOW`) behind the whole window, with the frame + extended over the client area. Corners come from `DWMWA_WINDOW_CORNER_PREFERENCE` (none, small + or round, which are the system's radii, not any radius), and the shadow is DWM's + ([enum](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_systembackdrop_type)). + DWM ignores a backdrop on a plain `WS_POPUP`. The window has to keep a caption style that + `WM_NCCALCSIZE` collapses away, which is already how fizzy's float windows are made. The material + falls back to a solid colour when the window is inactive, in Battery Saver, or with transparency + turned off. This is what fizzy uses today. It covers a whole window only, and its outline is one + of three corner radii. +- **`DwmEnableBlurBehindWindow`** has blurred nothing since Windows 8. Today it only turns on + per-pixel alpha, which is all SDL does with it for `SDL_WINDOW_TRANSPARENT` (an empty region at + creation) + ([docs](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmenableblurbehindwindow)). + The undocumented `SetWindowCompositionAttribute` Acrylic lags while dragging, and misbehaves with + per-pixel alpha from 22H2 on: a Windows 10 fallback at most + ([window-vibrancy](https://github.com/tauri-apps/window-vibrancy)). +- **DirectComposition**: fizzyedit/SDL already presents a transparent window's D3D12 swapchain + through it (`docs/DEPENDENCIES.md`, SDL patch 2), as the content of the root visual on a + **topmost** target (`D3D12_INTERNAL_SetCompositionContent`). It composites alpha correctly, but + has no public brush that reads what is behind the window. +- **Windows.UI.Composition** (the visual layer, usable from plain Win32: a `DispatcherQueue` on the + thread, then `ICompositorDesktopInterop::CreateDesktopWindowTarget(hwnd, topmost)`; + [visual layer with Win32](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/ui/using-the-visual-layer-with-win32)). + - `CreateHostBackdropBrush` paints a blur of what is behind the window: other apps, the + desktop, fizzy's own windows. Win32 windows may use it from Windows 11 (22000) once they set + `DWMWA_USE_HOSTBACKDROPBRUSH` + ([brush](https://learn.microsoft.com/en-us/uwp/api/windows.ui.composition.compositor.createhostbackdropbrush), + [attribute](https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute)). + - The backdrop can be cut to any shape by a `CompositionGeometricClip` over a + `CompositionPathGeometry`, which can change every frame. `RectangleClip` has per-corner radii + (a pill is a rect rounded by half its height). + - The backdrop **cannot** be the source of a `CompositionMaskBrush`: it fails with "Unsupported + source brush type" + ([microsoft-ui-xaml #12063](https://github.com/microsoft/microsoft-ui-xaml/issues/12063), + [WindowsCompositionSamples #103](https://github.com/Microsoft/WindowsCompositionSamples/issues/103)). + An effect graph taking the backdrop and a mask surface together (as Acrylic itself mixes the + backdrop with a noise surface) is untried. + - It hosts a swapchain (`ICompositorInterop::CreateCompositionSurfaceForSwapChain`), and casts + shaped shadows (`DropShadow` with a mask). + - Its changes are committed on the dispatcher's own tick, separately from any swapchain present: + expect up to a frame between them + ([Makepad #1190](https://github.com/makepad/makepad/pull/1190); Avalonia applies its + properties on the render thread just before presenting, + [#22115](https://github.com/AvaloniaUI/Avalonia/pull/22115)). +- **Capture with self-exclusion**: `SetWindowDisplayAffinity(WDA_EXCLUDEFROMCAPTURE)` keeps a window + out of every capture, and Windows.Graphics.Capture or DXGI Desktop Duplication then gives the app + what is under it, as a texture. +- **SDL**: `SDL_SetWindowOpacity` makes the window layered (`WS_EX_LAYERED`). SDL's draft "dockable + windows" ([#16432](https://github.com/libsdl-org/SDL/pull/16432), milestone 3.6) moves a window + with the pointer while the button is held. + +Community attempts at Liquid Glass on Windows +([LiquidGlassWinUI3](https://github.com/JamesLinYJ/LiquidGlassWinUI3): host backdrop, blur, rounded +clip) confirm the pieces work. None of them merges shapes, and none refracts anything but its own +content. + +### The plan + +**Windows, menus and dialogs: DWM, as now.** Float windows already wear Acrylic, DWM's corners and +shadow, Snap and their own taskbar buttons. What's left is P4's list: caption buttons in the +float's header, `win32_titlebar` state per HWND, persistence, and checking the look on a real GPU +(so far the VM has only shown Acrylic's solid fallback). Menus and dialogs in windows of their own +are the cheapest native win on Windows. Each is a popup owned by the window it opened from and +never activated, framed as the floats are (a caption style collapsed in `WM_NCCALCSIZE`, which +DWM needs to draw a backdrop), with `DWMSBT_TRANSIENTWINDOW` and `DWMWCP_ROUND`: the material and +the corners of Windows 11's own menus and flyouts. `viewportOpenMenu` gains a Windows branch +through `win32_titlebar.viewportChrome`. + +**Carried views and the drag's glass: one overlay, never a moving window.** On macOS a carry +window moves under the pointer, and stays in step because its frame changes in the transaction its +picture is presented in. Windows has no such transaction between `SetWindowPos` and a present, and +the plan has already seen what that costs: a window moved in one step and sized in another showed +half-done (`POPOUT_WINDOWS_PLAN.md`, "What P2 found"). So on Windows, nothing that follows the +pointer is a window that moves: + +- **One overlay per display**, made when a drag starts. It is topmost, never activated, passes + clicks through, has no taskbar entry (`WS_EX_TOOLWINDOW`) and no DWM transitions. It uses + `WS_EX_NOREDIRECTIONBITMAP`, so a window the size of the display costs no bitmap of its own; + only its visuals cost memory. A host backdrop hit-tests as opaque, so clicks must pass through + by style (`WS_EX_TRANSPARENT | WS_EX_LAYERED`), not by an empty hit test. Whether that style + leaves the composition tree alone is **(spike)**. +- **Its picture is a swapchain the size of the glass's area**, not of the display. + `Popout.overlayArea` already keeps that area steady with room to spare, because presenting a + display-sized picture each frame halved a drag's frame rate on macOS. On Windows the area moves + as the visual's offset inside the overlay, not as the window's place. +- **Carry and native glass become one path.** The carried card or tab, the drop's head and tail, + and the bubbles are all pieces in the same overlay. They are drawn by the app's `LiquidField`, + so they merge exactly as they do in-window, over the OS's blur. macOS needs two paths because + Liquid Glass is only on macOS 26; Windows needs only this one. + +**The material under the glass: Windows.UI.Composition's host backdrop, clipped to the field.** +DWM's system backdrop can't draw a pill, a disc or a merging drop. The visual layer can. In the +overlay: + +1. Under SDL's topmost DirectComposition target, a `DesktopWindowTarget` that is **not** topmost + (`CreateDesktopWindowTarget(hwnd, FALSE)`). Whether a Windows.UI.Composition target and a + DirectComposition target can share an HWND like this is **(spike) #1**. If they can't, the SDL + fork gains a property that hands the swapchain to the app instead of making a target of its + own, and fizzy hosts it in its own tree (`CreateCompositionSurfaceForSwapChain`). +2. In it, a `SpriteVisual` filled with the host backdrop and clipped by a + `CompositionPathGeometry`: the outline of the field where it takes frost, traced each frame. + `liquid_blob` already samples that field and runs marching squares over it, so tracing its edge + into a path (a Direct2D path geometry, which `CompositionPath` takes) is a small step from code + that exists. A card or tab that merges with nothing is cheaper as a `RectangleClip` with corner + radii. A clip edge that turns out aliased **(spike)** sits under the rim light the app draws + over it, so the clip goes a pixel inside the rim. +3. The window sets `DWMWA_USE_HOSTBACKDROPBRUSH`. + +The clip is a composition property, and the glass drawn over it is a swapchain present. The two can +land a frame apart, so the blur trails the rim on a fast drag. Setting the clip immediately before +the present, as Avalonia does, makes them usually coincide. Whether "usually" is good enough is for +PresentMon and the user's eye to decide **(spike) #2**. If it isn't, there is one more thing to try, +an exact match: an effect graph that masks the backdrop with a second swapchain the app presents +alongside the picture. The shape would then change only through presents, as on macOS it changes +only in the transaction. The mask brush can't do this (above). Whether an effect graph can, and +whether it takes a swapchain-backed surface as an input, is **(spike) #3**. Effects are built from +`IGraphicsEffect` descriptions, which fizzy would implement by hand, in the same way the SDL patch +declares DirectComposition's interfaces by vtable slot. + +**Windows: real refraction, if the clear lens over other apps matters.** The only way to bend +another app's pixels on Windows is to have them, and Windows does allow that. Mark the overlay +`WDA_EXCLUDEFROMCAPTURE` (Windows 10 2004+) so it never appears in any capture, and capture the +display under it with Windows.Graphics.Capture, or DXGI Desktop Duplication on the same adapter. +The app's own `LiquidField` then frosts and refracts that capture exactly as it does its own frame +in-window, the same at every point of the slider, and close to macOS 26. + +It costs: +- a display-sized capture each frame while a drag is on (started at the press, and it takes a + few frames to start); +- what is behind the glass lagging it by about a frame; +- protected content showing black; +- Windows.Graphics.Capture's border or consent prompt; +- the perception of an editor capturing the screen. + +Worth a spike behind a flag. Not the default. + +## Linux + +### What Linux gives + +What a window can ask of the desktop depends on the compositor, and in 2026 the answer has changed: + +- **`ext-background-effect-v1`** is now the cross-desktop blur-behind protocol. KWin has it from + Plasma 6.7, which dropped KDE's own `org_kde_kwin_blur` on Wayland. Mutter has it from GNOME 51 + (September 2026), so GNOME now blurs behind apps that ask + ([Phoronix](https://www.phoronix.com/news/GNOME-Mutter-Background-Blur)). niri has it from 26.04; + Hyprland in its development builds; COSMIC is working on it; wlroots/sway do not + ([scenefx #136](https://github.com/wlrfx/scenefx/issues/136)). + - One object per surface. + - Its blur region is a `wl_region` (whole rectangles, so no radius and no anti-aliasing), + double-buffered and applied on the surface's next commit. A region changes in the same commit + as the picture. + - The compositor chooses the blur's strength (Mutter's defaults: radius 24, saturation 1.25, + a little noise; KWin: a global setting). + - Whether compositors honour it on popups and subsurfaces is unverified. +- **No placing toplevels**, still. What a Wayland client has instead: + - **`xdg_popup`**: placed relative to a parent and kept on screen by the compositor. Moving one + (`reposition`, v3) waits for a configure round trip, so it lags anything that follows the + pointer. + - **Synchronized subsurfaces**: their position is applied with the parent's commit. + - **The drag icon of a `wl_data_device` drag**: the compositor moves it with the pointer itself, + so it never lags, and it never takes input. + - **`xdg-toplevel-drag`**: drags a real toplevel. KWin has it; Mutter's is a merge request + ([!4107](https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4107)). SDL's draft + "dockable windows" ([#16432](https://github.com/libsdl-org/SDL/pull/16432), milestone 3.6) + uses it, and so works on KDE but not GNOME. + - **An empty `set_input_region`** passes clicks through. +- **X11 is going away.** GNOME 50 removed the X11 session, and Plasma 6.8 (due around 14 October + 2026) is Wayland-only + ([Phoronix](https://www.phoronix.com/news/KDE-Plasma-68-Wayland-Exclusive)). On X11 a window + can be placed anywhere and be override-redirect. ARGB windows are translucent only under a + compositing manager, and input shapes (XShape) pass clicks through. KWin blurs behind + `_KDE_NET_WM_BLUR_BEHIND_REGION`; picom blurs by its own rules. A property change reaches the + compositor asynchronously to the present. + +Others: Zed blurs on KDE and notes that rounded corners are impossible there. Ghostty and +Alacritty blur on KDE only, and Ghostty's broke when Plasma 6.7 dropped `org_kde_kwin_blur` +([ghostty #13041](https://github.com/ghostty-org/ghostty/discussions/13041)). +Qt has `KWindowEffects::enableBlurBehind`; GTK has nothing yet. + +### The plan + +Wayland comes first. It is the default on every major desktop, and now has a blur protocol that +both GNOME and KDE implement. It allows less than X11 in where windows go, but X11 sessions are +being removed. + +**The window material, wherever the compositor has a blur.** Today the Linux window is opaque and +the app's glass knows it (`LiquidField.publishOpaqueWindow`). Where `ext-background-effect-v1` is +advertised (with `org_kde_kwin_blur` as a fallback for older Plasma), the main window asks for a +blur over its frame, not over the shadow margin in its frame insets, and publishes itself as not +opaque. The window opacity slider then means on Linux what it means on macOS and Windows. The +rounded corners are stepped rectangles a pixel high along each corner's arc, which a blur hides. +The region is surface state, so the request goes out before the frame's present (whose Vulkan WSI +makes the `wl_surface.commit`, on the same connection). A resize then changes the blur and the +picture in the same commit. Where no compositor offers it (sway, GNOME before 51), nothing changes: +the window stays opaque. At the top of the slider the window sends an empty region rather than +none, so a compositor that blurs translucent windows by default (Hyprland) does not; on KDE's own +protocol, where an empty region blurs the whole window, it unsets the blur instead. + +**Wayland: popups yes, floats not yet.** A Wayland client still can't place a toplevel, so floats +stay in the main window, as decided. Menus, dialogs and tooltips, though, can leave the main +window as `xdg_popup`s (`SDL_CreatePopupWindow`), placed relative to their parent and kept on +screen by the compositor. That is the Wayland form of `viewportOpenMenu`, blurred behind where the +compositor allows it. They hold still, so the reposition lag doesn't matter. `viewportsAvailable` +stops being one switch: "can place windows" and "can open popups" become separate questions. + +**Wayland: carrying a view past the window, later.** A view dragged past the main window's edge is +cut off there today. Wayland's own answer to "a picture that follows the pointer anywhere" is the +**drag icon**: the compositor moves it with no lag, it passes clicks through by definition, and the +client can redraw it every frame, so it isn't the still picture an OS drag image is on macOS. +Letting go over nothing could then open a float through `xdg-toplevel-drag`, the protocol made for +tabs torn out of browsers, once floats as windows are possible on Wayland at all. SDL #16432 is +how SDL means to get there. Both need SDL changes or fizzy's own Wayland code on SDL's +`wl_display`: SDL has no drag source on Wayland, and `start_drag` needs the serial of the press. +This is the largest Linux item, and the last. + +**X11: only what is cheap.** Floats as windows already work there (behind `FIZZY_POPOUT=1`). +An overlay like Windows' would be possible: an override-redirect ARGB window with an empty input +shape, opened only when a compositing manager owns `_NET_WM_CM_S` (without one an ARGB +window is black). Its blur would be a property that lags the present. With X11 sessions going +away, this is not worth building beyond keeping what works working. + +## What changes in fizzy + +The seams are already in the right places. Most of the macOS work is gated on the OS at a handful +of capability flags and backend functions, and the plan fills those in rather than adding new +paths: + +- **`viewports.carries`** (`backend_native.zig`, macOS only today) turns on carry windows, carried + pictures without their place's background (`dialogs.carry_windows`), and drawing past the main + window (`screens.publishBeyond`). On Windows it becomes true with the overlay, which serves as + the carry window there. +- **`viewports.liquidGlass()`** is a yes/no answer today. It becomes a three-way capability: + `none` (the app's glass in-window, as now), `os_glass` (macOS 26: the OS shapes and fills it), + and `os_backdrop` (Windows: the app shapes it and the OS fills it). `Popout.nativeGlass` and + `overlayFrame` branch on it. Under `os_backdrop`, `glassBase` is replaced by the app's own glass + (`LiquidField` with no frost of its own), and the backend gets the frost's outline instead of + `GlassShape`s. +- **`core.native_glass.Shape.frost`** already says how much blur each piece takes: all of it for a + bubble with an icon, none for the carried view's clear lens. That decides which pieces the + outline includes. +- **`SDLBackend.renderPresent`** has a macOS-only block that opens a transaction, applies frames, + shapes and glass, and commits after the presents. Windows gets a block in the same place that + sets the overlay's clip and offset right before the present. Wayland sets the blur region in + the same place, and needs no commit of its own. +- **`viewportOpenMenu`** and `nativeMenus` / `nativeDialogs` are macOS only. Windows gets them as + Acrylic popups, and Wayland as `xdg_popup`s, even though Wayland can't have floats as windows. +- **`viewports.os_buttons`** stays macOS only. On Windows the float's header draws + `caption_buttons.zig`, and `win32_titlebar`'s hover and press state moves from globals to + per-HWND state (`dwRefData`), as `POPOUT_WINDOWS_PLAN.md` "Next" already lists. +- **The ghost fade** (`viewportFade`, `SDL_SetWindowOpacity`) makes the window layered on Windows + (`WS_EX_LAYERED`). That is untested with a DirectComposition swapchain and a DWM backdrop + **(spike)**. The composition tree's root opacity is the alternative. +- **Calling the visual layer from Zig.** Windows.UI.Composition is WinRT: activation factories, + `IInspectable`, `HSTRING`s. zig's MinGW headers carry no C++/WinRT, so fizzy declares the dozen + interfaces it calls by vtable slot. fizzyedit/SDL already does this for DirectComposition, and + `win32_titlebar.zig` already does it for DWM. The bulk is in the clip path + (`IGeometrySource2DInterop` over a Direct2D path) and, if spike 3 is tried, the effect + descriptions. + +## Order of work + +Each step is useful by itself, and the spikes come before anything that depends on their answer. + +1. **Recompile the glass programs** (SPIR-V and DXIL; step 0). Windows and Linux then draw the + current lens and slider in-window. +2. **Windows: float chrome and lifecycle** (P4): caption buttons in the float header, per-HWND + `win32_titlebar` state, minimize and close with the main window, and the look checked on a + hardware GPU. This is what lets `float_windows` default to on for Windows, as it does on macOS. +3. **Windows: menus and dialogs as Acrylic popups** (`viewportOpenMenu`). Small, and the closest + thing on Windows to the OS's own UI. +4. **Linux: the window material** through `ext-background-effect-v1` (GNOME 51, Plasma 6.7, niri), + and **Wayland popups** for menus and dialogs. A protocol binding on SDL's `wl_display`, and the + region set before the present. +5. **Windows spikes**, in a scratch app before fizzy, on a hardware GPU and on WARP, Windows 11 + 23H2 and 24H2: + 1. a Windows.UI.Composition target that is not topmost, under SDL's topmost DirectComposition + target, on one HWND; + 2. a host backdrop clipped by a path rebuilt every frame: its edge, and how far it trails a + present (PresentMon); + 3. a host backdrop masked through an effect graph by a swapchain the app presents; + 4. the overlay's click-through and z-order with a drag in flight, and `WS_EX_LAYERED` on a + window with a composition swapchain and a DWM backdrop. +6. **Windows: the overlay** (`viewports.carries`, the `os_backdrop` glass), if spikes 1 and 2 + hold, and with the go-ahead `NATIVE_WINDOWS_PLAN.md` asks for (its Phase 2). If they don't, the + fallback is the overlay with the app's glass and no OS blur under it. + That still carries a view past every window, which is the part users will notice most. +7. **Optional: real refraction on Windows** through capture, behind a flag, if the clear lens over + other apps turns out to matter. +8. **Wayland carry** through the drag icon, then `xdg-toplevel-drag` (or SDL #16432) for floats as + windows on KDE. + +## Still open + +- **What "native" should mean on Windows** is settled in `NATIVE_WINDOWS_PLAN.md`: Mica for + long-lived windows at the top of the slider (Windows Terminal's mapping), Acrylic for transient + surfaces (menus, carries, and the overlay if Phase 2 goes ahead). +- **The slider where the OS owns the blur.** On Windows and Wayland the frost's strength is fixed, + so the lower half of `glass_look`'s slider (clear lens to whole frost) needs its own mapping + there, or a decision that those platforms start at frost. +- **Mixed DPI.** Every viewport uses the main window's density (`POPOUT_WINDOWS_PLAN.md`, P5). A + per-display overlay makes this matter sooner, as soon as one spans a second display.