Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/WINDOWS_LINUX_GLASS_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,16 @@ Each step is useful by itself, and the spikes come before anything that depends
`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.
thing on Windows to the OS's own UI. *Built, run in a Windows 11 ARM VM on WARP:*
`win32_titlebar.viewportMenuChrome` dresses the menu's window (Acrylic, DWM's corners and shadow,
no system menu, no transitions), owned by the window it opens from, and keeps it looking active
to DWM, which otherwise draws a never-activated window's backdrop as its solid fallback. On with
floats as windows (`FIZZY_POPOUT=1`), as on macOS. In the VM, menus and the unsaved-changes
dialog open in windows of their own, stacked just above the main window, which keeps the
keyboard; the first click on an item acts; a dialog's window hides and comes back with a
minimized and restored main window. The look itself (Acrylic rather than its fallback, DWM's
corners) and whether the kept-active trick holds need a hardware GPU **(check)**: the VM's DWM
draws every backdrop as its fallback.
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.
Expand Down
5 changes: 3 additions & 2 deletions src/backend/backend_native.zig
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,9 @@ pub const viewports = struct {
return Impl.appActive();
}

/// Whether menus can be windows of their own (`openMenu`): macOS, where viewports are.
pub const menus = supported and builtin.os.tag == .macos;
/// Whether menus can be windows of their own (`openMenu`): macOS, and Windows (Acrylic popups),
/// where viewports are.
pub const menus = supported and (builtin.os.tag == .macos or builtin.os.tag == .windows);

/// A window for a menu (`Popout`'s menus): borderless, above every window, never focused, the
/// OS's material in it — rounded by `radius` points — and the pointer over it read in the main
Expand Down
41 changes: 30 additions & 11 deletions src/backend/native/SDLBackend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -924,20 +924,39 @@ const ViewportKind = enum { window, carry, overlay, menu };
/// A menu in a window of its own (`Popout`'s menus): borderless, kept above every window by its
/// level, in no window list, never made key — the window it opened from keeps the keyboard, and
/// its first click acts. Placed in the main window's frame (`viewportPlaceMain`), the pointer over it
/// read there (`viewportMainOffset`). Its material is the OS's (`viewportLiquidGlass`, vibrancy
/// before macOS 26). macOS.
/// read there (`viewportMainOffset`). Its material is the OS's: on macOS Liquid Glass
/// (`viewportLiquidGlass`, vibrancy before macOS 26), on Windows Acrylic
/// (`win32_titlebar.viewportMenuChrome`), with the window's base drawn under the menu.
/// `radius`, points: the menu's corners, which vibrancy is masked to (Liquid Glass takes them from
/// its look each frame). `ride`: the window it moves with (`Viewport.ride`) — none for a menu opened
/// in a float that is out. `dialog`: a dialog's window (`Viewport.dialog`).
/// its look each frame; DWM rounds as near it as it rounds). `ride`: the window it moves with
/// (`Viewport.ride`) — none for a menu opened in a float that is out. `dialog`: a dialog's window
/// (`Viewport.dialog`).
pub fn viewportOpenMenu(self: *SDLBackend, at: viewport_map.Rect, radius: f32, ride: Ride, dialog: bool) ?*Viewport {
if (comptime builtin.os.tag != .macos) return null;
if (comptime builtin.os.tag != .macos and builtin.os.tag != .windows) return null;
const vp = self.openViewport(at, "", .menu) orelse return null;
_ = fizzy_macos_viewport_menu(cocoaWindow(vp.window), cocoaWindow(self.window), platform.window.ns_visual_effect_material, radius, @intFromBool(dialog));
vp.ride = switch (ride) {
.none => null,
.main => self.window,
.viewport => |parent| parent.window,
};
switch (comptime builtin.os.tag) {
.macos => {
_ = fizzy_macos_viewport_menu(cocoaWindow(vp.window), cocoaWindow(self.window), platform.window.ns_visual_effect_material, radius, @intFromBool(dialog));
vp.ride = switch (ride) {
.none => null,
.main => self.window,
.viewport => |parent| parent.window,
};
},
.windows => {
// Owned by the window it opens from, as `openViewport` makes every viewport the main
// window's: over it, hidden and minimized with it. An owned window is not carried
// when its owner moves, so it rides on nothing (`viewportPlaceRiding` places it each
// frame).
switch (ride) {
.none, .main => {},
.viewport => |parent| _ = c.SDL_SetWindowParent(vp.window, parent.window),
}
if (c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(vp.window), c.SDL_PROP_WINDOW_WIN32_HWND_POINTER, null)) |hwnd|
_ = platform.win32_titlebar.viewportMenuChrome(hwnd, dvui.themeGet().dark, radius);
},
else => unreachable,
}
vp.dialog = dialog;
return vp;
}
Expand Down
66 changes: 66 additions & 0 deletions src/backend/native/platform/win32_titlebar.zig
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,72 @@ pub fn viewportChrome(hwnd: *anyopaque, main_hwnd: ?*anyopaque, dark: bool, radi
const viewport_ws_sysmenu: isize = 0x00080000;
const viewport_ws_ex_appwindow: isize = 0x00040000;

const menu_subclass_id: usize = 0x50584933; // "PXI3"

/// Dress a menu's or a dialog's window (`SDLBackend.viewportOpenMenu`): Acrylic behind it, the
/// material of Windows 11's own menus and flyouts, in the app's light or dark (`dark`); its corners
/// rounded by DWM as near `radius_pt` (points) as DWM rounds; DWM's shadow, no border, no DWM
/// animation. It is never activated, and DWM draws an inactive window's backdrop as a solid
/// fallback, so it is kept looking active (`menuSubclassProc`). True when DWM gives it the
/// backdrop (Windows 11 22H2 on).
pub fn viewportMenuChrome(hwnd: *anyopaque, dark: bool, radius_pt: f32) bool {
if (builtin.os.tag != .windows) return false;
const h: win32.foundation.HWND = @ptrCast(hwnd);
const dwm = win32.graphics.dwm;
const wm = win32.ui.windows_and_messaging;
const corner: u32 = if (radius_pt < 1) DWMWCP_DONOTROUND else if (radius_pt < 6) DWMWCP_ROUNDSMALL else DWMWCP_ROUND;
_ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_WINDOW_CORNER_PREFERENCE), &corner, @sizeOf(u32));
const none: u32 = dwm.DWMWA_COLOR_NONE;
_ = dwm.DwmSetWindowAttribute(h, dwm.DWMWA_BORDER_COLOR, &none, @sizeOf(u32));
const dark_value: u32 = @intFromBool(dark);
_ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_USE_IMMERSIVE_DARK_MODE), &dark_value, @sizeOf(u32));
const no_transitions: u32 = 1;
_ = dwm.DwmSetWindowAttribute(h, @enumFromInt(DWMWA_TRANSITIONS_FORCEDISABLED), &no_transitions, @sizeOf(u32));
_ = win32.ui.shell.SetWindowSubclass(h, menuSubclassProc, menu_subclass_id, 0);
// No system menu, whose caption buttons DWM would draw over the menu with the frame extended
// (as for a float's window, `viewportChrome`); SDL's caption style stays, collapsed by its
// `WM_NCCALCSIZE`, for DWM to treat the window as framed and give it a backdrop at all.
const style = wm.GetWindowLongPtrW(h, wm.GWL_STYLE);
if (style & viewport_ws_sysmenu != 0) {
_ = wm.SetWindowLongPtrW(h, wm.GWL_STYLE, style & ~viewport_ws_sysmenu);
const SWP_NOSIZE: u32 = 0x0001;
const SWP_NOMOVE: u32 = 0x0002;
const SWP_NOZORDER: u32 = 0x0004;
const SWP_NOACTIVATE: u32 = 0x0010;
const SWP_FRAMECHANGED: u32 = 0x0020;
_ = wm.SetWindowPos(h, null, 0, 0, 0, 0, @bitCast(SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED));
}
const ok = applyViewportBackdrop(h);
// Drawn active from the start: it will never be sent an activation of its own.
_ = wm.SendMessageW(h, wm.WM_NCACTIVATE, 1, 0);
return ok;
}

fn menuSubclassProc(
hWnd: ?win32.foundation.HWND,
uMsg: u32,
wParam: win32.foundation.WPARAM,
lParam: win32.foundation.LPARAM,
uIdSubclass: usize,
dwRefData: usize,
) callconv(.winapi) win32.foundation.LRESULT {
_ = uIdSubclass;
_ = dwRefData;
const wm = win32.ui.windows_and_messaging;
// Always active to DWM, so its backdrop stays Acrylic rather than the inactive fallback.
if (uMsg == wm.WM_NCACTIVATE) return win32.ui.shell.DefSubclassProc(hWnd, uMsg, 1, lParam);
// SDL sets the window's style again as it likes: never the system menu.
if (uMsg == wm.WM_STYLECHANGING and @as(isize, @bitCast(wParam)) == @intFromEnum(wm.GWL_STYLE)) {
const ss: *wm.STYLESTRUCT = @ptrFromInt(@as(usize, @bitCast(lParam)));
ss.styleNew &= ~@as(u32, @intCast(viewport_ws_sysmenu));
}
if (uMsg == wm.WM_DWMCOMPOSITIONCHANGED) if (hWnd) |h| {
_ = applyViewportBackdrop(h);
};
if (uMsg == wm.WM_NCDESTROY) _ = win32.ui.shell.RemoveWindowSubclass(hWnd, menuSubclassProc, menu_subclass_id);
return win32.ui.shell.DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

/// The backdrop, and the frame extended over the whole window for it to show through — again on
/// every activation, as DWM wants (and as `win32MicaSubclassProc` does for the main window).
fn applyViewportBackdrop(h: win32.foundation.HWND) bool {
Expand Down
7 changes: 4 additions & 3 deletions src/editor/Popout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1162,8 +1162,9 @@ fn backing(target: dvui.Rect.Physical, window: dvui.Rect.Physical, opacity: f32)
bounds.fill(corners, .{ .color = .{ .color = Editor.windowBase(opacity) } });
}

/// Whether menus are windows of their own (`menuFrame`): where the OS can show them (macOS), with
/// floats as windows, as the setting says (`Settings.native_menus`, `FIZZY_NATIVE_MENUS` over it).
/// Whether menus are windows of their own (`menuFrame`): where the OS can show them (macOS; Windows,
/// as Acrylic popups), with floats as windows, as the setting says (`Settings.native_menus`,
/// `FIZZY_NATIVE_MENUS` over it).
fn nativeMenus() bool {
if (comptime !viewports.menus) return false;
if (!viewports.available()) return false;
Expand Down Expand Up @@ -1269,7 +1270,7 @@ fn menuFrame() void {
const placed = viewports.placeRiding(m.viewport, .{ .x = place.x, .y = place.y, .w = place.w, .h = place.h }, .{ .x = key.x, .y = key.y, .w = key.w, .h = key.h });
const shown: dvui.Rect.Physical = .{ .x = frame.x + (placed.x - place.x), .y = frame.y + (placed.y - place.y), .w = placed.w, .h = placed.h };
// Its material: Liquid Glass on the slider, no lighter than a menu's text needs; vibrancy
// before macOS 26, with the window's colour drawn under the menu.
// before macOS 26, or Acrylic on Windows, with the window's colour drawn under the menu.
const op = @max(std.math.clamp(fizzy.editor().window_opacity, 0, 1), menu_opacity_floor);
var look = Editor.windowGlassLook(op, .{ .w = shown.w / s, .h = shown.h / s });
look.radius = corner;
Expand Down
Loading