From 2f9c0fb86235620b43d6c1034dfeb3dc596c338e Mon Sep 17 00:00:00 2001 From: foxnne Date: Sun, 18 Oct 2026 11:43:27 -0500 Subject: [PATCH 1/2] backend: plain dvui app windows made of the window material The backend owns window_material (a kind, field for field with core.material's, and an opacity) and applies it to the windows it dresses itself (dressPlainWindows: a plain dvui app's main window and its OS-framed floats), falling back through the same order to what it can show. The theme's window colour stays the backdrop, at the opacity asked; under it the OS's material: vibrancy for frost and blur, an NSGlassEffectView for liquid. It follows the backdrop through full screen (glass_follow). Fizzy's Entry hands it core.material's effective value each frame after the publish: one decision, one applier (setWindowMaterial). Fizzy's own windows, which it dresses itself, are not made of it yet: the next step of plans#27. Co-Authored-By: Claude Opus 5.5 --- backend/src/SDLBackend.zig | 47 ++++++++- .../src/platform/macos/visual_effect_view.m | 96 ++++++++++++++++++- backend/src/platform/window.zig | 11 +++ src/Entry.zig | 8 ++ 4 files changed, 160 insertions(+), 2 deletions(-) diff --git a/backend/src/SDLBackend.zig b/backend/src/SDLBackend.zig index 7a102087f..7d5b72ecf 100644 --- a/backend/src/SDLBackend.zig +++ b/backend/src/SDLBackend.zig @@ -1074,6 +1074,45 @@ fn addEventWinRecursive(self: *SDLBackend, event: *c.SDL_Event, win: *dvui.Windo /// The theme's window colours `dressPlainWindows` last gave the windows. var plain_dressed: ?[2]dvui.Color = null; +/// The material `dressPlainWindows` last gave the windows. +var plain_material: ?WindowMaterial = null; + +/// The kinds of window material, field for field with fizzy's `core.material.Kind`: from the +/// plainest to the richest, what `effectiveMaterial` falls back through. +pub const MaterialKind = enum(u8) { none, transparent, blur, frost, liquid }; + +pub const WindowMaterial = struct { + kind: MaterialKind = .none, + /// How much of the theme's window colour covers what is behind, 0 (clear) to 1. Ignored by + /// `none`. + opacity: f32 = 1, +}; + +/// What the windows this backend dresses itself are made of (`dressPlainWindows`: a plain dvui +/// app's main window and its OS-framed floats): set by the app (`setWindowMaterial`), shown as the +/// backend can (`effectiveMaterial`). Fizzy hands it `core.material`'s effective value each frame; +/// fizzy's own windows, which it dresses itself (`platform.window.app_dresses_windows`), are not +/// made of it yet. +pub var window_material: WindowMaterial = .{}; + +/// The windows' material from now on (`window_material`), applied as it changes. +pub fn setWindowMaterial(material: MaterialKind, opacity: f32) void { + window_material = .{ .kind = material, .opacity = opacity }; +} + +/// `window_material`, or the best kind below it this backend can show (`materialKinds`). +fn effectiveMaterial(self: *SDLBackend) WindowMaterial { + const offered = self.materialKinds(); + var k = @backingInt(window_material.kind); + while (k > 0) : (k -= 1) { + const material: MaterialKind = @fromBackingInt(k); + const has = switch (material) { + inline else => |t| @field(offered, @tagName(t)), + }; + if (has) return .{ .kind = material, .opacity = window_material.opacity }; + } + return .{ .kind = .none, .opacity = window_material.opacity }; +} /// The OS's own title bar on a plain dvui app's windows — the main one and each of the OS's own /// frame (`Viewport.native`) — in the theme's colour: set again only when the theme's colours @@ -1081,11 +1120,15 @@ var plain_dressed: ?[2]dvui.Color = null; fn dressPlainWindows(self: *SDLBackend) void { const theme = dvui.themeGet(); const colors: [2]dvui.Color = .{ theme.color(.window, .fill), theme.color(.window, .text) }; - if (plain_dressed) |d| if (std.meta.eql(d, colors)) return; + const material = self.effectiveMaterial(); + if (plain_dressed) |d| if (std.meta.eql(d, colors)) if (plain_material) |m| if (std.meta.eql(m, material)) return; plain_dressed = colors; + plain_material = material; platform.window.nativeTitleBar(self.window, colors[0], colors[1], theme.dark); + platform.window.plainMaterial(self.window, @backingInt(material.kind), material.opacity); for (&self.viewport_slots) |*slot| if (slot.*) |*v| if (v.native) { platform.window.nativeTitleBar(v.window, colors[0], colors[1], theme.dark); + platform.window.plainMaterial(v.window, @backingInt(material.kind), material.opacity); }; } @@ -1420,6 +1463,8 @@ fn openViewport(self: *SDLBackend, at: viewport_map.Rect, title_text: [:0]const if (comptime builtin.target.os.tag == .macos) fizzy_macos_viewport_still(cocoaWindow(window)); const theme = dvui.themeGet(); platform.window.nativeTitleBar(window, theme.color(.window, .fill), theme.color(.window, .text), theme.dark); + // Made of what the main window is (`dressPlainWindows`). + if (plain_material) |m| platform.window.plainMaterial(window, @backingInt(m.kind), m.opacity); // Through Spaces, zooms and live resizes as the main window goes. if (comptime builtin.target.os.tag == .macos) platform.macos_monitor.watch(window); // Its part of the frame below the title bar's band (`platform.macos_monitor.titlebarStrip`): diff --git a/backend/src/platform/macos/visual_effect_view.m b/backend/src/platform/macos/visual_effect_view.m index 6ac668448..34c359e3f 100644 --- a/backend/src/platform/macos/visual_effect_view.m +++ b/backend/src/platform/macos/visual_effect_view.m @@ -114,6 +114,10 @@ - (NSView *)hitTest:(NSPoint)point { * (`fizzy_macos_window_glass_follow`). */ @interface FizzyPlainBackdropView : NSView @property double radius; +/* How much of the window's colour covers what is behind it (`fizzy_macos_window_plain_material`): + * 1, opaque, for no material. */ +@property double fill; +@property double r, g, b; @end @implementation FizzyPlainBackdropView @@ -123,6 +127,20 @@ - (NSView *)hitTest:(NSPoint)point { } @end +/* A plain dvui app's window's material under its backdrop (`fizzy_macos_window_plain_material`): the + * OS's frost or blur (vibrancy) or Liquid Glass, behind the window, where the backdrop is — it + * follows the backdrop through full screen (`fizzy_macos_window_glass_follow`). */ +@interface FizzyPlainMaterialView : NSView +@property int kind; +@end + +@implementation FizzyPlainMaterialView +- (NSView *)hitTest:(NSPoint)point { + (void)point; + return nil; +} +@end + /* * A float popped out of the main window into a window of its own (fizzy's viewports): its glass * shows the desktop behind it through vibrancy, as the main window's chrome does, so the float's @@ -1536,9 +1554,13 @@ void fizzy_macos_window_plain_backdrop(void *nswindow, double r, double g, doubl #endif } backdrop.radius = radius; + backdrop.r = r; + backdrop.g = g; + backdrop.b = b; + if (backdrop.fill <= 0) backdrop.fill = 1; [CATransaction begin]; [CATransaction setDisableActions:YES]; - CGColorRef color = CGColorCreateSRGB(r, g, b, 1); + CGColorRef color = CGColorCreateSRGB(r, g, b, backdrop.fill); backdrop.layer.backgroundColor = color; CGColorRelease(color); const BOOL full = ([window styleMask] & NSWindowStyleMaskFullScreen) != 0; @@ -1549,6 +1571,72 @@ void fizzy_macos_window_plain_backdrop(void *nswindow, double r, double g, doubl } } +/* A plain dvui app's window made of `kind` (`core.material`'s order: 0 none, 1 transparent, 2 blur, + * 3 frost, 4 liquid): its backdrop's colour `opacity` opaque over what is behind (opaque for none), + * and under it the OS's material — a plain blur or frost (vibrancy), or Liquid Glass (macOS 26) — + * or nothing. After `fizzy_macos_window_plain_backdrop`; again as the material changes. */ +void fizzy_macos_window_plain_material(void *nswindow, int kind, double opacity) { + @autoreleasepool { + NSWindow *window = (__bridge NSWindow *)nswindow; + NSView *frame = [[window contentView] superview]; + if (window == nil || frame == nil) return; + FizzyPlainBackdropView *backdrop = nil; + FizzyPlainMaterialView *mat = nil; + for (NSView *v in [frame subviews]) { + if ([v isKindOfClass:[FizzyPlainBackdropView class]]) backdrop = (FizzyPlainBackdropView *)v; + if ([v isKindOfClass:[FizzyPlainMaterialView class]]) mat = (FizzyPlainMaterialView *)v; + } + if (backdrop == nil) return; + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + backdrop.fill = kind == 0 ? 1 : fmin(fmax(opacity, 0), 1); + CGColorRef color = CGColorCreateSRGB(backdrop.r, backdrop.g, backdrop.b, backdrop.fill); + backdrop.layer.backgroundColor = color; + CGColorRelease(color); + /* The OS's material under it, made again only as its kind changes. */ + const BOOL wants = kind >= 2; + if (mat != nil && (!wants || mat.kind != kind)) { + [mat removeFromSuperview]; + mat = nil; + } + if (wants && mat == nil) { + mat = [[FizzyPlainMaterialView alloc] initWithFrame:[backdrop frame]]; + mat.kind = kind; + /* Sized with the window, as the backdrop is: without, a window resized kept its glass + * at the size it was made at (the user). */ + [mat setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [mat setWantsLayer:YES]; + mat.layer.masksToBounds = YES; + mat.layer.cornerRadius = backdrop.layer.cornerRadius; + NSView *inner = nil; + if (kind == 4) { + Class glass = NSClassFromString(@"NSGlassEffectView"); + if (glass != nil) { + inner = [[glass alloc] initWithFrame:[mat bounds]]; + if ([inner respondsToSelector:@selector(setCornerRadius:)]) [inner setValue:@(backdrop.radius) forKey:@"cornerRadius"]; + } + } + if (inner == nil) { + NSVisualEffectView *v = [[NSVisualEffectView alloc] initWithFrame:[mat bounds]]; + v.blendingMode = NSVisualEffectBlendingModeBehindWindow; + v.state = NSVisualEffectStateActive; + /* Frost the window's own frosted material; blur the plainer one of full screen UI. */ + v.material = kind == 3 ? NSVisualEffectMaterialUnderWindowBackground : NSVisualEffectMaterialFullScreenUI; + inner = v; + } + [inner setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [mat addSubview:inner]; + [frame addSubview:mat positioned:NSWindowBelow relativeTo:backdrop]; +#if !__has_feature(objc_arc) + [inner release]; + [mat release]; +#endif + } + [CATransaction commit]; + [window invalidateShadow]; + } +} + /* The OS's shadow and 1-point outline round a titled window, drawn by the backdrop's own layer * while `on` (`fizzy_macos_window_glass_follow`): a soft shadow below it, and an outline light on a * dark window and dark on a light one. */ @@ -1628,6 +1716,12 @@ void fizzy_macos_window_glass_follow(void *nswindow) { * draws no shadow or outline round a full-screen window: the backdrop draws them * round itself, where the window seems to be, until it lands and the OS's are back. */ backdropFrame(v.layer, still, radius, window); + /* Its material under it, where it is and in its shape. */ + for (NSView *m in [frame subviews]) { + if (![m isKindOfClass:[FizzyPlainMaterialView class]]) continue; + if (!NSEqualRects([m frame], want)) [m setFrame:want]; + if (m.layer.cornerRadius != radius) m.layer.cornerRadius = radius; + } continue; } if (![v isKindOfClass:[FizzyViewportGlassView class]]) continue; diff --git a/backend/src/platform/window.zig b/backend/src/platform/window.zig index e95ed016e..189f3e547 100644 --- a/backend/src/platform/window.zig +++ b/backend/src/platform/window.zig @@ -445,6 +445,17 @@ pub fn nativeTitleBar(window: *c.SDL_Window, color: dvui.Color, text: dvui.Color } } +/// A window the backend dresses itself (`nativeTitleBar`) made of material `kind` (`core.material`'s +/// order: 0 none, 1 transparent, 2 blur, 3 frost, 4 liquid), its colour `opacity` over it +/// (`SDLBackend.dressPlainWindows`). macOS; Windows (Acrylic) and Linux (the compositor's blur) are +/// to follow it. +pub fn plainMaterial(window: *c.SDL_Window, kind: u8, opacity: f32) void { + if (comptime builtin.target.os.tag != .macos) return; + const raw_ptr = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return; + fizzy_macos_window_plain_material(raw_ptr, kind, opacity); +} +extern fn fizzy_macos_window_plain_material(nswindow: ?*anyopaque, kind: c_int, opacity: f64) void; + /// A window of fizzy's skinned as the app is: its background — what AppKit draws its title bar /// with, the one a fullscreen window reveals at the top among them — `color`, and its appearance /// light or dark by the app's theme, not the system's, so its title bar, traffic lights and diff --git a/src/Entry.zig b/src/Entry.zig index 1026a1116..d055f7bf5 100644 --- a/src/Entry.zig +++ b/src/Entry.zig @@ -552,6 +552,14 @@ fn frameOnce() !dvui.App.Result { // asked for until the app has a setting for it, so each platform shows the best it can; // `FIZZY_GLASS` (`none`, `transparent`, `blur`, `frost`) asks for less, to see each. fizzy.core.material.publish((fizzy.core.material.Material{ .kind = materialAsked() }).effective(materialOffered())); + // And the backend, which makes the windows it dresses itself of it: the one decision, one + // applier (`SDLBackend.window_material`). + if (comptime @hasDecl(dvui.backend, "setWindowMaterial")) { + const m = fizzy.core.material.get(); + dvui.backend.setWindowMaterial(switch (m.kind) { + inline else => |k| @field(dvui.backend.MaterialKind, @tagName(k)), + }, m.opacity); + } // A float asked out of the main window, or back into it, goes before anything is drawn // (`Editor.Popout`: on by default on macOS, `FIZZY_POPOUT` elsewhere). Editor.Popout.beginFrame(&fizzy.editor().app.layout); From fecb626e6d7449704da9ead06b8355491f304784 Mon Sep 17 00:00:00 2001 From: foxnne Date: Sun, 18 Oct 2026 12:27:53 -0500 Subject: [PATCH 2/2] backend: a plain dvui app's Liquid Glass is fizzy's window glass A plain dvui app's window made of Liquid Glass stood on one NSGlassEffectView clipped to the window's corners (masksToBounds), which looked unlike fizzy's windows (no clearing bevel, no frost body) and cost a lot to composite. It now gets fizzy's own window glass: the same parts (`windowGlassInstall`) and the same look each frame (`glass_look.window` at the material's opacity, the bevel by the window's size), over a clear backdrop. Switching to another material takes the parts away again. `glass_look` moves from `core/gfx/` into the backend (`backend/src/`) as a std-only module of its own, so the backend can apply it: the backend's build makes one instance per build (`glassLookModule`), and `core` takes that one where it is linked beside the backend (`core_module.addImports`), or makes its own from the file (web, tests, the plugin SDK). The SDK tarball vendors the file beside `core/`. --- backend/build.zig | 14 +++++ backend/src/SDLBackend.zig | 59 +++++++++++++++---- {core/gfx => backend/src}/glass_look.zig | 0 .../src/platform/macos/visual_effect_view.m | 45 +++++++------- backend/src/platform/window.zig | 4 +- build/app.zig | 5 +- build/exe.zig | 7 ++- build/web.zig | 2 +- core/core.zig | 2 +- core/gfx/LiquidField.zig | 2 +- scripts/pack-sdk.sh | 15 +++-- sdk/core_module.zig | 23 ++++++++ sdk/plugin_sdk.zig | 17 +----- 13 files changed, 131 insertions(+), 64 deletions(-) rename {core/gfx => backend/src}/glass_look.zig (100%) diff --git a/backend/build.zig b/backend/build.zig index 43bd5ac22..d1ece982d 100644 --- a/backend/build.zig +++ b/backend/build.zig @@ -93,6 +93,19 @@ pub const Options = struct { /// it. dvui imports it by name, and a second module over the same file in one compilation is an /// error. var backend_modules: std.AutoHashMapUnmanaged(*std.Build.Module, *std.Build.Module) = .empty; +var glass_look_module: ?*std.Build.Module = null; + +/// What every glass is at the window's opacity (`src/glass_look.zig`), std-only: one module for the +/// whole build, so the backend and the app's `core` that a build links together share it (a file +/// may belong to only one module). Pass it to `core_module.addImports` for a `core` linked beside +/// this backend. +pub fn glassLookModule(dep: *std.Build.Dependency) *std.Build.Module { + if (glass_look_module) |m| return m; + const m = dep.builder.createModule(.{ .root_source_file = dep.builder.path("src/glass_look.zig") }); + glass_look_module = m; + return m; +} + /// The `platform` module for each backend module, for the same reason. var platform_modules: std.AutoHashMapUnmanaged(*std.Build.Module, *std.Build.Module) = .empty; @@ -134,6 +147,7 @@ pub fn backendModule(dep: *std.Build.Dependency, dvui_mod: *std.Build.Module, op addMacosSdk(m, opts); m.addCSourceFile(.{ .file = b.path("src/macos_monitor.m") }); } + m.addImport("glass_look", glassLookModule(dep)); dvui_mod.addImport("backend", m); backend_modules.put(b.allocator, dvui_mod, m) catch @panic("OOM"); return m; diff --git a/backend/src/SDLBackend.zig b/backend/src/SDLBackend.zig index ab8944a23..1cebdf331 100644 --- a/backend/src/SDLBackend.zig +++ b/backend/src/SDLBackend.zig @@ -17,7 +17,8 @@ const dvui = @import("dvui"); const GpuRenderer = @import("GpuRenderer.zig"); pub const viewport_map = @import("viewport_map.zig"); /// The OS windows besides the main one an app draws with (`Viewport`). -pub const viewports = @import("viewports.zig"); +pub const glass_look = @import("glass_look"); +const viewports = @import("viewports.zig"); /// dvui's floating windows as OS windows of their own from the one frame (`floatingWindowOs`). pub const os_windows = @import("os_windows.zig"); /// What this backend keeps count of as it runs (`health`). @@ -1115,21 +1116,55 @@ fn effectiveMaterial(self: *SDLBackend) WindowMaterial { } /// The OS's own title bar on a plain dvui app's windows — the main one and each of the OS's own -/// frame (`Viewport.native`) — in the theme's colour: set again only when the theme's colours -/// change. A window opening is dressed as it opens (`viewportOpen`). +/// frame (`Viewport.native`) — in the theme's colour, made of the window material: set again only +/// when the theme's colours or the material change. A window opening is dressed as it opens +/// (`viewportOpen`). Liquid Glass is fizzy's own window glass, its look set each frame +/// (`plainGlass`). fn dressPlainWindows(self: *SDLBackend) void { const theme = dvui.themeGet(); const colors: [2]dvui.Color = .{ theme.color(.window, .fill), theme.color(.window, .text) }; const material = self.effectiveMaterial(); - if (plain_dressed) |d| if (std.meta.eql(d, colors)) if (plain_material) |m| if (std.meta.eql(m, material)) return; - plain_dressed = colors; - plain_material = material; - platform.window.nativeTitleBar(self.window, colors[0], colors[1], theme.dark); - platform.window.plainMaterial(self.window, @backingInt(material.kind), material.opacity); - for (&self.viewport_slots) |*slot| if (slot.*) |*v| if (v.native) { - platform.window.nativeTitleBar(v.window, colors[0], colors[1], theme.dark); - platform.window.plainMaterial(v.window, @backingInt(material.kind), material.opacity); - }; + const same = if (plain_dressed) |d| if (plain_material) |m| std.meta.eql(d, colors) and std.meta.eql(m, material) else false else false; + if (!same) { + plain_dressed = colors; + plain_material = material; + platform.window.nativeTitleBar(self.window, colors[0], colors[1], theme.dark); + platform.window.plainMaterial(self.window, @backingInt(material.kind), material.opacity); + for (&self.viewport_slots) |*slot| if (slot.*) |*v| if (v.native) { + platform.window.nativeTitleBar(v.window, colors[0], colors[1], theme.dark); + platform.window.plainMaterial(v.window, @backingInt(material.kind), material.opacity); + }; + } + if (material.kind != .liquid) return; + plainGlass(self.window, material.opacity, colors[0]); + for (&self.viewport_slots) |*slot| if (slot.*) |*v| if (v.native) plainGlass(v.window, material.opacity, colors[0]); +} + +/// `window`'s Liquid Glass this frame, as fizzy's own windows have it (`Editor.windowGlassLook`): +/// what `glass_look` says at `opacity`, in the window's colour, its clearing bevel by its size. +/// Nothing where the window stands on no glass. +fn plainGlass(window: *c.SDL_Window, opacity: f32, fill: dvui.Color) void { + if (comptime builtin.target.os.tag != .macos) return; + var w: c_int = 0; + var h: c_int = 0; + _ = c.SDL_GetWindowSize(window, &w, &h); + const look = glass_look.window(opacity); + const band = glass_look.band(@as(f32, @floatFromInt(@min(w, h))) / 2); + _ = platform.window.liquidGlassLook(window, (platform.window.WindowGlassLook{ + .under_variant = look.under.variant, + .under_style = look.under.style, + .over_variant = look.over.variant, + .over_style = look.over.style, + .frost = look.frost, + .blur = look.blur, + .glass = look.glass, + .fill = fill, + .fill_opacity = look.fill, + .top_fill = look.top_fill, + .radius = @floatCast(fizzy_macos_window_corner_radius()), + .clear = band.clear, + .feather = band.feather, + }).native()); } /// A floating window of dvui's (not modal) shown in an OS window of its own, its frame the OS's diff --git a/core/gfx/glass_look.zig b/backend/src/glass_look.zig similarity index 100% rename from core/gfx/glass_look.zig rename to backend/src/glass_look.zig diff --git a/backend/src/platform/macos/visual_effect_view.m b/backend/src/platform/macos/visual_effect_view.m index f5c92dc7d..0cfa5cfb3 100644 --- a/backend/src/platform/macos/visual_effect_view.m +++ b/backend/src/platform/macos/visual_effect_view.m @@ -1306,6 +1306,12 @@ static int windowGlassInstall(NSWindow *window, long blur_material) { return 1; } +/* `window`'s glass parts gone (`windowGlassInstall`): a plain dvui app's window made of another + * material (`fizzy_macos_window_plain_material`). */ +static void windowGlassRemove(NSWindow *window) { + for (int k = 0; k < window_glass_parts; k++) [windowGlassPart(window, k) removeFromSuperview]; +} + int fizzy_macos_window_liquid_glass(void *nswindow, long blur_material) { @autoreleasepool { NSWindow *window = (__bridge NSWindow *)nswindow; @@ -1579,7 +1585,8 @@ void fizzy_macos_window_plain_backdrop(void *nswindow, double r, double g, doubl backdrop.r = r; backdrop.g = g; backdrop.b = b; - if (backdrop.fill <= 0) backdrop.fill = 1; + /* Opaque until it has a material (`fizzy_macos_window_plain_material`); clear on glass. */ + if (backdrop.fill <= 0 && windowGlassPart(window, window_glass_under) == nil) backdrop.fill = 1; [CATransaction begin]; [CATransaction setDisableActions:YES]; CGColorRef color = CGColorCreateSRGB(r, g, b, backdrop.fill); @@ -1595,9 +1602,12 @@ void fizzy_macos_window_plain_backdrop(void *nswindow, double r, double g, doubl /* A plain dvui app's window made of `kind` (`core.material`'s order: 0 none, 1 transparent, 2 blur, * 3 frost, 4 liquid): its backdrop's colour `opacity` opaque over what is behind (opaque for none), - * and under it the OS's material — a plain blur or frost (vibrancy), or Liquid Glass (macOS 26) — - * or nothing. After `fizzy_macos_window_plain_backdrop`; again as the material changes. */ -void fizzy_macos_window_plain_material(void *nswindow, int kind, double opacity) { + * and under it the OS's material — a plain blur or frost (vibrancy) — or nothing. Liquid Glass + * (macOS 26) is fizzy's own window glass instead (`windowGlassInstall`, `blur_material` its plain + * blur), over a clear backdrop: its colour is the glass's, set each frame + * (`fizzy_macos_window_liquid_glass_look`). After `fizzy_macos_window_plain_backdrop`; again as the + * material changes. */ +void fizzy_macos_window_plain_material(void *nswindow, int kind, double opacity, long blur_material) { @autoreleasepool { NSWindow *window = (__bridge NSWindow *)nswindow; NSView *frame = [[window contentView] superview]; @@ -1611,12 +1621,14 @@ void fizzy_macos_window_plain_material(void *nswindow, int kind, double opacity) if (backdrop == nil) return; [CATransaction begin]; [CATransaction setDisableActions:YES]; - backdrop.fill = kind == 0 ? 1 : fmin(fmax(opacity, 0), 1); + const BOOL liquid = kind == 4 && windowGlassInstall(window, blur_material); + if (!liquid) windowGlassRemove(window); + backdrop.fill = liquid ? 0 : kind == 0 ? 1 : fmin(fmax(opacity, 0), 1); CGColorRef color = CGColorCreateSRGB(backdrop.r, backdrop.g, backdrop.b, backdrop.fill); backdrop.layer.backgroundColor = color; CGColorRelease(color); /* The OS's material under it, made again only as its kind changes. */ - const BOOL wants = kind >= 2; + const BOOL wants = kind >= 2 && !liquid; if (mat != nil && (!wants || mat.kind != kind)) { [mat removeFromSuperview]; mat = nil; @@ -1630,22 +1642,11 @@ void fizzy_macos_window_plain_material(void *nswindow, int kind, double opacity) [mat setWantsLayer:YES]; mat.layer.masksToBounds = YES; mat.layer.cornerRadius = backdrop.layer.cornerRadius; - NSView *inner = nil; - if (kind == 4) { - Class glass = NSClassFromString(@"NSGlassEffectView"); - if (glass != nil) { - inner = [[glass alloc] initWithFrame:[mat bounds]]; - if ([inner respondsToSelector:@selector(setCornerRadius:)]) [inner setValue:@(backdrop.radius) forKey:@"cornerRadius"]; - } - } - if (inner == nil) { - NSVisualEffectView *v = [[NSVisualEffectView alloc] initWithFrame:[mat bounds]]; - v.blendingMode = NSVisualEffectBlendingModeBehindWindow; - v.state = NSVisualEffectStateActive; - /* Frost the window's own frosted material; blur the plainer one of full screen UI. */ - v.material = kind == 3 ? NSVisualEffectMaterialUnderWindowBackground : NSVisualEffectMaterialFullScreenUI; - inner = v; - } + NSVisualEffectView *inner = [[NSVisualEffectView alloc] initWithFrame:[mat bounds]]; + inner.blendingMode = NSVisualEffectBlendingModeBehindWindow; + inner.state = NSVisualEffectStateActive; + /* Frost the window's own frosted material; blur the plainer one of full screen UI. */ + inner.material = kind == 3 ? NSVisualEffectMaterialUnderWindowBackground : NSVisualEffectMaterialFullScreenUI; [inner setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; [mat addSubview:inner]; [frame addSubview:mat positioned:NSWindowBelow relativeTo:backdrop]; diff --git a/backend/src/platform/window.zig b/backend/src/platform/window.zig index 189f3e547..fb1546bfa 100644 --- a/backend/src/platform/window.zig +++ b/backend/src/platform/window.zig @@ -452,9 +452,9 @@ pub fn nativeTitleBar(window: *c.SDL_Window, color: dvui.Color, text: dvui.Color pub fn plainMaterial(window: *c.SDL_Window, kind: u8, opacity: f32) void { if (comptime builtin.target.os.tag != .macos) return; const raw_ptr = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse return; - fizzy_macos_window_plain_material(raw_ptr, kind, opacity); + fizzy_macos_window_plain_material(raw_ptr, kind, opacity, ns_visual_effect_material); } -extern fn fizzy_macos_window_plain_material(nswindow: ?*anyopaque, kind: c_int, opacity: f64) void; +extern fn fizzy_macos_window_plain_material(nswindow: ?*anyopaque, kind: c_int, opacity: f64, blur_material: c_long) void; /// A window of fizzy's skinned as the app is: its background — what AppKit draws its title bar /// with, the one a fullscreen window reveals at the top among them — `color`, and its appearance diff --git a/build/app.zig b/build/app.zig index 3749a78e7..b5924c1e5 100644 --- a/build/app.zig +++ b/build/app.zig @@ -849,7 +849,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. // One material on two sliders: what every glass is at the window's opacity and roughness, // the OS's form and the app's. std-only (see glass_look.zig); `LiquidField` and `Popout` // apply it. - .{ "fizzy-glass-look-tests", "core/gfx/glass_look.zig" }, + .{ "fizzy-glass-look-tests", "backend/src/glass_look.zig" }, // What the app's windows are made of, and the fallback to what the backend can show. // std-only but for `publish`/`get`, which reach dvui and are not under test here. .{ "fizzy-material-tests", "core/material.zig" }, @@ -1071,7 +1071,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. .optimize = optimize, .root_source_file = b.path("core/core.zig"), }); - const icons_test = core_mod.addImports(b, core_module_test, dvui_testing_dep.module("dvui_testing"), b.dependency("fizzy_sdk", .{}).builder, target, optimize); + const icons_test = core_mod.addImports(b, core_module_test, dvui_testing_dep.module("dvui_testing"), b.dependency("fizzy_sdk", .{}).builder, target, optimize, null); fizzy_test_module.addImport("core", core_module_test); if (icons_test) |icons| fizzy_test_module.addImport("icons", icons); @@ -1527,6 +1527,7 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. if (icons_test) |icons| split_tests_module.addImport("icons", icons); // `core/widgets.zig` names widgets through `replay`'s anchors; the test `core`'s own. split_tests_module.addImport("replay", core_module_test.import_table.get("replay").?); + split_tests_module.addImport("glass_look", core_module_test.import_table.get("glass_look").?); inline for (.{ .{ "fizzy-sdk-tests", sdk_tests_module }, diff --git a/build/exe.zig b/build/exe.zig index a29af3d61..b67a1c5b2 100644 --- a/build/exe.zig +++ b/build/exe.zig @@ -266,6 +266,9 @@ pub fn addFizzyExecutableForTarget( // On dvui's own SDL3 backend (`-Dnative-backend=sdl3`), which has no viewports. exe.root_module.addImport("viewports_none", fizzy_backend.viewportsNoneModule(backendDependency(b), dvui_mod)); + // The glass look the backend dresses OS windows with; `core` takes the same module. + const glass_look = fizzy_backend.glassLookModule(backendDependency(b)); + // Shared `core` module (gfx/math/fs/generated atlas/platform/paths/dvui hub + // generic widgets). Import set is shared with the plugin SDK path — see sdk/core_module.zig. const core_module = b.createModule(.{ @@ -273,7 +276,7 @@ pub fn addFizzyExecutableForTarget( .optimize = optimize, .root_source_file = b.path("core/core.zig"), }); - const icons_module = core_mod.addImports(b, core_module, dvui_mod, b.dependency("fizzy_sdk", .{}).builder, resolved_target, optimize); + const icons_module = core_mod.addImports(b, core_module, dvui_mod, b.dependency("fizzy_sdk", .{}).builder, resolved_target, optimize, glass_look); exe.root_module.addImport("core", core_module); if (icons_module) |icons| exe.root_module.addImport("icons", icons); @@ -282,7 +285,7 @@ pub fn addFizzyExecutableForTarget( .optimize = optimize, .root_source_file = b.path("core/core.zig"), }); - _ = core_mod.addImports(b, core_proxy_module, dvui_proxy_mod, b.dependency("fizzy_sdk", .{}).builder, resolved_target, optimize); + _ = core_mod.addImports(b, core_proxy_module, dvui_proxy_mod, b.dependency("fizzy_sdk", .{}).builder, resolved_target, optimize, glass_look); // `macos_fsevents` is load-bearing for `FolderWatcher`: it watches a whole project folder, // and the kqueue fallback needs a file descriptor per directory *and* per file — exactly the diff --git a/build/web.zig b/build/web.zig index b0895e4e3..84b199c90 100644 --- a/build/web.zig +++ b/build/web.zig @@ -161,7 +161,7 @@ pub fn addSteps( .link_libc = false, .single_threaded = true, }); - const icons_web = core_mod.addImports(b, core_module_web, dvui_web, b.dependency("fizzy_sdk", .{}).builder, web_target, optimize); + const icons_web = core_mod.addImports(b, core_module_web, dvui_web, b.dependency("fizzy_sdk", .{}).builder, web_target, optimize, null); web_exe.root_module.addImport("core", core_module_web); if (icons_web) |icons| web_exe.root_module.addImport("icons", icons); const sdk_module_web = sdk.wireSdkModule(b, web_target, optimize, dvui_web, dvui_web_proxy_bridge, core_module_web, web_exe.root_module); diff --git a/core/core.zig b/core/core.zig index 368bd8724..8d9c2ac1a 100644 --- a/core/core.zig +++ b/core/core.zig @@ -55,7 +55,7 @@ pub const corners = @import("corners.zig"); pub const liquid_glass = @import("gfx/liquid_glass.zig"); pub const liquid_blob = @import("gfx/liquid_blob.zig"); /// One material on one value: every glass at the window's opacity, the OS's form and the app's. -pub const glass_look = @import("gfx/glass_look.zig"); +pub const glass_look = @import("glass_look"); pub const LiquidField = @import("gfx/LiquidField.zig"); pub const programs = @import("gfx/programs.zig"); pub const Spring = @import("Spring.zig"); diff --git a/core/gfx/LiquidField.zig b/core/gfx/LiquidField.zig index b216613a5..fe9332052 100644 --- a/core/gfx/LiquidField.zig +++ b/core/gfx/LiquidField.zig @@ -29,7 +29,7 @@ const std = @import("std"); const dvui = @import("dvui"); const programs = @import("programs.zig"); const liquid_glass = @import("liquid_glass.zig"); -const glass_look = @import("glass_look.zig"); +const glass_look = @import("glass_look"); const LiquidField = @This(); diff --git a/scripts/pack-sdk.sh b/scripts/pack-sdk.sh index ba6b05c65..cfda5bc06 100755 --- a/scripts/pack-sdk.sh +++ b/scripts/pack-sdk.sh @@ -4,7 +4,8 @@ # Zig has no "depend on a subdirectory of this archive" — plugins must fetch a # package whose root build.zig.zon has no Velopack. This script stages `sdk/` as # the package root. The SDK's own source already lives inside it (`sdk/src/`), so only -# `core/` is vendored in beside it — `plugin_sdk.repoPath` resolves both (see that function). +# `core/` and the glass look `core` imports (`backend/src/glass_look.zig`) are vendored in +# beside it — `core_module.repoPath` resolves them (see that function). # # Usage: # ./scripts/pack-sdk.sh # writes zig-out/sdk/fizzy-sdk-vX.Y.Z.tar.gz @@ -38,9 +39,11 @@ mkdir -p "$pkg" # Package root = the sdk/ build surface plus its own src/ (no app-only deps). cp -R sdk/. "$pkg/" -# The one module source that lives outside the package in-repo, and the license every copy of -# it carries. +# The module sources that live outside the package in-repo, and the license every copy of +# them carries. cp -R core "$pkg/core" +mkdir -p "$pkg/backend/src" +cp backend/src/glass_look.zig "$pkg/backend/src/glass_look.zig" cp LICENSE "$pkg/LICENSE" # Drop build/editor-only noise if any leaked into the copy (none expected). @@ -60,14 +63,14 @@ text = open(path).read() paths = re.search(r'\.paths = \.\{\n(.*?)\n \},', text, re.S) if not paths: sys.exit("pack-sdk: no .paths list in the package's build.zig.zon") -for entry in ("core", "LICENSE"): +for entry in ("core", "backend", "LICENSE"): if f'"{entry}"' not in paths.group(1): text = text[:paths.end(1)] + f'\n "{entry}",' + text[paths.end(1):] paths = re.search(r'\.paths = \.\{\n(.*?)\n \},', text, re.S) text = re.sub(r'\.version = "[^"]*"', f'.version = "{version}"', text, count=1) paths = re.search(r'\.paths = \.\{\n(.*?)\n \},', text, re.S) -if '"core",' not in paths.group(1) or '"LICENSE",' not in paths.group(1): - sys.exit("pack-sdk: could not add core and LICENSE to the package's .paths") +if any(f'"{e}",' not in paths.group(1) for e in ("core", "backend", "LICENSE")): + sys.exit("pack-sdk: could not add core, backend and LICENSE to the package's .paths") open(path, "w").write(text) PY diff --git a/sdk/core_module.zig b/sdk/core_module.zig index ded8dca2f..68dd348f9 100644 --- a/sdk/core_module.zig +++ b/sdk/core_module.zig @@ -22,6 +22,10 @@ const std = @import("std"); /// not compile against this `core` at all. `sdk_pkg` is this package's builder, whose root holds /// `tape/` and `replay/` in both layouts: `b` from the SDK's own build, the `fizzy_sdk` /// dependency's builder from the repo root. +/// +/// `glass_look` is the backend's (`backend/build.zig`'s `glassLookModule`) where the build links +/// `core` beside fizzy's backend, which applies the same looks to OS windows; null makes one from +/// the file, beside this package in-repo and vendored into it in the release tarball. pub fn addImports( b: *std.Build, mod: *std.Build.Module, @@ -29,8 +33,12 @@ pub fn addImports( sdk_pkg: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, + glass_look: ?*std.Build.Module, ) ?*std.Build.Module { mod.addImport("dvui", dvui_mod); + mod.addImport("glass_look", glass_look orelse sdk_pkg.createModule(.{ + .root_source_file = repoPath(sdk_pkg, "backend/src/glass_look.zig"), + })); mod.addImport("zf", zfModule(b, target, optimize)); // Demos and recordings: `tape` is the format and its engine (std-only), `replay` drives a @@ -47,6 +55,21 @@ pub fn addImports( return null; } +/// LazyPath to a source tree this package needs (`src/…` for the SDK itself, `core/…` for the +/// shared floor, `backend/src/glass_look.zig`). +/// +/// The SDK's own source lives inside this package (`sdk/src/`) and resolves directly. `core/` +/// and the glass look do not: in-repo they sit beside this package (`../core/…`), and in the +/// release tarball (`fizzy-sdk-v*.tar.gz`) they are vendored at the archive root, which *is* this +/// package — so the access check below picks whichever layout is present. +pub fn repoPath(b: *std.Build, sub_path: []const u8) std.Build.LazyPath { + b.root.access(b.graph.io, sub_path, .{}) catch { + const root = b.root.root_dir.path orelse @panic("fizzy sdk: missing build_root"); + return .{ .cwd_relative = b.pathJoin(&.{ root, "..", sub_path }) }; + }; + return b.path(sub_path); +} + /// The fuzzy matcher behind `core.fuzzy` — shared by fizzy and, through `core`, by every /// plugin. `with_tui = false` matters: zf's default build wires up its standalone terminal /// binary, whose `libvaxis` dependency would otherwise be fetched into every plugin build for a diff --git a/sdk/plugin_sdk.zig b/sdk/plugin_sdk.zig index b5edb1e6b..b02100691 100644 --- a/sdk/plugin_sdk.zig +++ b/sdk/plugin_sdk.zig @@ -22,20 +22,7 @@ const std = @import("std"); /// the five `core` compiles and miss this one (see its doc comment). const core_module = @import("core_module.zig"); -/// LazyPath to a source tree this package needs (`src/…` for the SDK itself, `core/…` for the -/// shared floor). -/// -/// The SDK's own source lives inside this package (`sdk/src/`) and resolves directly. `core/` -/// is the one tree that does not: in-repo it sits beside this package (`../core/…`), and in the -/// release tarball (`fizzy-sdk-v*.tar.gz`) it is vendored at the archive root, which *is* this -/// package — so the access check below picks whichever layout is present. -fn repoPath(b: *std.Build, sub_path: []const u8) std.Build.LazyPath { - b.root.access(b.graph.io, sub_path, .{}) catch { - const root = b.root.root_dir.path orelse @panic("fizzy sdk: missing build_root"); - return .{ .cwd_relative = b.pathJoin(&.{ root, "..", sub_path }) }; - }; - return b.path(sub_path); -} +const repoPath = core_module.repoPath; /// C-ABI entry symbols every plugin dylib must export. pub const dylib_exports = [_][]const u8{ @@ -594,7 +581,7 @@ pub fn exportModules( .root_source_file = repoPath(b, "core/core.zig"), .link_libc = !is_wasm, }); - _ = core_module.addImports(b, core_mod, dvui_proxy_mod, b, target, optimize); + _ = core_module.addImports(b, core_mod, dvui_proxy_mod, b, target, optimize, null); const sdk_mod = b.addModule("fizzy_sdk", .{ .target = target,