Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ea83962
zig: the 0.17 switch, scaffolded with marked stand-ins (#437)
foxnne Oct 16, 2026
9a22df2
zig: the build's custom steps become helper programs (#438)
foxnne Oct 16, 2026
2491f4e
zig: release packaging on 0.17, with Velopack 1.2.161 (#448)
foxnne Oct 16, 2026
b18213c
backend: no crash quitting a plain dvui app on macOS (#480)
foxnne Oct 17, 2026
a59ff14
backend: a plain dvui app's floating windows open as OS windows (#481)
foxnne Oct 17, 2026
67cc6ea
macos: a plain dvui app's windows take fizzy's window shape (#482)
foxnne Oct 17, 2026
78a5019
macos: child windows stay above the main window (#483)
foxnne Oct 17, 2026
46ddcd8
zig/0.17: merge main (40 commits, to #469)
foxnne Oct 17, 2026
6d94889
zig/0.17: merge main (40 commits, to #469) (#486)
foxnne Oct 17, 2026
28766fa
zig: settings and command schemas read 0.17's @typeInfo (#488)
foxnne Oct 17, 2026
b45233a
zig: a lifted view keeps its drag through dvui's captureMouseGet (#489)
foxnne Oct 17, 2026
a1feb78
docs: the Windows traps of the Zig 0.17 move (#494)
foxnne Oct 18, 2026
300d797
core: a transparent blit reads its pixels as bytes on Zig 0.17 (#496)
foxnne Oct 18, 2026
1b6ad2c
sdk: a plugin can install a program beside itself (#498)
foxnne Oct 18, 2026
0be3e64
zig/0.17: record #486's merge of main as a parent
foxnne Oct 18, 2026
47a0c1e
zig/0.17: merge main (6 commits, to #490)
foxnne Oct 18, 2026
87ae2ae
zig: what 0.16 needed goes with the move (#505)
foxnne Oct 18, 2026
3513b9f
macos: a plain dvui app's OS windows are ordinary document windows (#…
foxnne Oct 18, 2026
eb452e8
backend: OS windows for the floating windows that ask, or all by choi…
foxnne Oct 18, 2026
7336d4a
macos: a float's full screen at the display's rate, clear and rounded…
foxnne Oct 18, 2026
2dc6fc4
macos: the pointer reaches the window it is over, not one behind it (…
foxnne Oct 18, 2026
4eca7f6
core: one window material, and the kinds each backend can show
foxnne Oct 18, 2026
d2128cb
app: every glass draws in the window material
foxnne Oct 18, 2026
9f5fc85
backend: a floating window dragged past the app's windows tears off (…
foxnne Oct 18, 2026
9e8e530
zig/0.17: merge main (22 commits, to #509) (#558)
foxnne Oct 18, 2026
e118432
zig/0.17: merge into backend/window-materials (to #558)
foxnne Oct 18, 2026
18c1631
Merge backend/window-materials into app/glass-material
foxnne Oct 18, 2026
54014c7
zig: settings own only strings, and builds read through b.root (#561)
foxnne Oct 18, 2026
39574be
core: one window material, and the kinds each backend can show (#546)
foxnne Oct 18, 2026
0b0f9e0
Merge zig/0.17 into app/glass-material
foxnne Oct 18, 2026
ae5e719
backend: plain dvui app windows made of the window material (#565)
foxnne Oct 18, 2026
9d9dbee
Merge main into app/glass-material
foxnne Oct 18, 2026
e17b4ce
macos: a window's glass bevel follows a live resize (#576)
foxnne Oct 18, 2026
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
14 changes: 14 additions & 0 deletions backend/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,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;

Expand Down Expand Up @@ -136,6 +149,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;
Expand Down
98 changes: 89 additions & 9 deletions backend/src/SDLBackend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const GpuRenderer = @import("GpuRenderer.zig");
pub const viewport_map = @import("viewport_map.zig");
const TouchSlots = @import("touch_slots.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`).
Expand Down Expand Up @@ -1077,19 +1078,96 @@ 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
/// 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) };
if (plain_dressed) |d| if (std.meta.eql(d, colors)) return;
plain_dressed = colors;
platform.window.nativeTitleBar(self.window, colors[0], colors[1], theme.dark);
for (&self.viewport_slots) |*slot| if (slot.*) |*v| if (v.native) {
platform.window.nativeTitleBar(v.window, colors[0], colors[1], theme.dark);
};
const material = self.effectiveMaterial();
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
Expand Down Expand Up @@ -1468,6 +1546,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`):
Expand Down
File renamed without changes.
131 changes: 130 additions & 1 deletion backend/src/platform/macos/visual_effect_view.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -1243,6 +1261,34 @@ int fizzy_macos_window_has_liquid_glass(void *nswindow) {
*/
/* The glass parts beside SDL's view in `window`'s frame view (`window_glass_ids`), once; 1 where they
* are (or already were), 0 where the OS has no Liquid Glass. A titled window's and a menu's alike. */
/* A window glass part's feather mask (`windowFeatherMask`) sized with the part as AppKit resizes
* it, not at the next frame's look: a mask left at the old size for a frame, anchored at the
* bottom, masked the frost out along the top of a window growing under a live resize, its old
* corner showing as a diagonal (the user). One observer for every part
* (`windowGlassMaskFollows`), each part's registration gone with it (`windowGlassRemove`). */
@interface FizzyGlassMaskFollower : NSObject
@end

@implementation FizzyGlassMaskFollower
- (void)partResized:(NSNotification *)note {
CALayer *layer = [(NSView *)[note object] layer];
CALayer *mask = [layer mask];
if (mask == nil || CGRectEqualToRect([mask frame], [layer bounds])) return;
[CATransaction begin];
[CATransaction setDisableActions:YES];
[mask setFrame:[layer bounds]];
[CATransaction commit];
}
@end

static FizzyGlassMaskFollower *g_glass_mask_follower = nil;

static void windowGlassMaskFollows(NSView *part) {
if (g_glass_mask_follower == nil) g_glass_mask_follower = [[FizzyGlassMaskFollower alloc] init];
[part setPostsFrameChangedNotifications:YES];
[[NSNotificationCenter defaultCenter] addObserver:g_glass_mask_follower selector:@selector(partResized:) name:NSViewFrameDidChangeNotification object:part];
}

static int windowGlassInstall(NSWindow *window, long blur_material) {
if (window == nil || !fizzy_macos_liquid_glass_available()) return 0;
if (windowGlassPart(window, window_glass_under) != nil) return 1;
Expand Down Expand Up @@ -1283,11 +1329,23 @@ static int windowGlassInstall(NSWindow *window, long blur_material) {
/* Its own layer, which keeps the feather's mask: one AppKit lends a view not asking
* for a layer (the glass, the blur) drops the mask set on it. */
[part setWantsLayer:YES];
if (k == window_glass_over || k == window_glass_fill) windowGlassMaskFollows(part);
[frame addSubview:part positioned:NSWindowBelow relativeTo:content];
}
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++) {
NSView *part = windowGlassPart(window, k);
if (part == nil) continue;
if (g_glass_mask_follower != nil) [[NSNotificationCenter defaultCenter] removeObserver:g_glass_mask_follower name:NSViewFrameDidChangeNotification object:part];
[part removeFromSuperview];
}
}

int fizzy_macos_window_liquid_glass(void *nswindow, long blur_material) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)nswindow;
Expand Down Expand Up @@ -1558,9 +1616,14 @@ 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;
/* 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, 1);
CGColorRef color = CGColorCreateSRGB(r, g, b, backdrop.fill);
backdrop.layer.backgroundColor = color;
CGColorRelease(color);
const BOOL full = ([window styleMask] & NSWindowStyleMaskFullScreen) != 0;
Expand All @@ -1571,6 +1634,66 @@ 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 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];
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];
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 && !liquid;
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;
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];
#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. */
Expand Down Expand Up @@ -1650,6 +1773,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;
Expand Down
11 changes: 11 additions & 0 deletions backend/src/platform/window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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, ns_visual_effect_material);
}
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
/// light or dark by the app's theme, not the system's, so its title bar, traffic lights and
Expand Down
Loading
Loading