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 b3044fab5..f8bdd4e02 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`). @@ -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 @@ -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`): 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 ebd3ba7c4..0cfa5cfb3 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 @@ -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; @@ -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; @@ -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. */ @@ -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; diff --git a/backend/src/platform/window.zig b/backend/src/platform/window.zig index e95ed016e..fb1546bfa 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, 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 diff --git a/build/app.zig b/build/app.zig index 3065ea672..2169089b8 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 36750ad65..c472c5d4e 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..5ddeb4293 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,20 @@ 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 { + return .{ .cwd_relative = b.root.joinString(b.allocator, b.pathJoin(&.{ "..", sub_path })) catch @panic("OOM") }; + }; + 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 80466f23d..3fba1382f 100644 --- a/sdk/plugin_sdk.zig +++ b/sdk/plugin_sdk.zig @@ -22,19 +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 { - return .{ .cwd_relative = b.root.joinString(b.allocator, b.pathJoin(&.{ "..", sub_path })) catch @panic("OOM") }; - }; - 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{ @@ -593,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, diff --git a/src/Entry.zig b/src/Entry.zig index 5b9bf73e3..2cd3b0fce 100644 --- a/src/Entry.zig +++ b/src/Entry.zig @@ -553,6 +553,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);