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
14 changes: 14 additions & 0 deletions backend/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
98 changes: 89 additions & 9 deletions backend/src/SDLBackend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down Expand Up @@ -1074,19 +1075,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 @@ -1465,6 +1543,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.
97 changes: 96 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 @@ -1288,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;
Expand Down Expand Up @@ -1558,9 +1582,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 +1600,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 +1739,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
5 changes: 3 additions & 2 deletions build/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 },
Expand Down
Loading
Loading