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
6 changes: 3 additions & 3 deletions app/settings/Settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ motion: f32 = 1,
motion_speed: f32 = core.motion.default_speed,

/// What the app's windows are made of (`core.material`): opaque (`none`), clear (`transparent`),
/// blurred, frosted, or Liquid Glass. Each platform shows the best it has at or below the kind asked
/// (`Material.effective`): Liquid Glass on macOS 26 and on the web (drawn in the page), frost on
/// Windows, blur where a Linux compositor blurs. Applied at once; `FIZZY_GLASS` overrides it.
/// blurred, frosted, or Liquid Glass. A kind the platform cannot draw is drawn as the richest glass
/// below it that it can, else opaque (`Material.effective`), and stays saved for a machine that
/// can. Applied at once; `FIZZY_GLASS` overrides it.
window_material: core.material.Kind = .liquid,

/// Whether glass is drawn by the glass program (`core.LiquidField`) where the backend has one —
Expand Down
2 changes: 2 additions & 0 deletions backend/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ pub const unit_tests = [_]struct { name: []const u8, root: []const u8 }{
.{ .name = "backend-window-layout-tests", .root = "src/platform/window_layout.zig" },
// The hit test Windows' WM_NCHITTEST and Linux's SDL hit test both answer from.
.{ .name = "backend-titlebar-tests", .root = "src/platform/titlebar.zig" },
// Which window materials each platform can draw, why not the others, and the fallback.
.{ .name = "backend-materials-tests", .root = "src/materials.zig" },
// The rounded frame Linux's blur behind the window is stepped into (`wayland_blur`).
.{ .name = "backend-blur-region-tests", .root = "src/platform/blur_region.zig" },
// Where a viewport's OS window is on the desktop and where its part of the frame lies.
Expand Down
60 changes: 35 additions & 25 deletions backend/src/SDLBackend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const dvui = @import("dvui");

const GpuRenderer = @import("GpuRenderer.zig");
pub const viewport_map = @import("viewport_map.zig");
pub const materials = @import("materials.zig");
const TouchSlots = @import("touch_slots.zig");
const glass_look = @import("glass_look");
/// The OS windows besides the main one an app draws with (`Viewport`).
Expand Down Expand Up @@ -101,6 +102,8 @@ sdl_quit: bool = true,
clear_window_on_begin: bool = false,
/// See `mainPicture`.
main_picture: ?viewport_map.Rect = null,
/// What this platform can draw (`materialPlatform`), probed once a frame: cleared in `begin`.
material_platform: ?materials.Platform = null,
/// The main window's frame goes down where its picture is (`mainPicture`): a plain dvui app's,
/// whose picture is also below the title bar (`platform.macos_monitor.titlebarStrip`). Its frame
/// target lays it there (`plain_frame`, `pictureInWindow`); the backend moves what is drawn
Expand Down Expand Up @@ -1125,8 +1128,8 @@ var plain_material: ?WindowMaterial = null;
var plain_base: dvui.Color = .{};

/// 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 };
/// plainest to the richest, what `effectiveMaterial` falls back through (`materials.effective`).
pub const MaterialKind = materials.Kind;

pub const WindowMaterial = struct {
kind: MaterialKind = .none,
Expand Down Expand Up @@ -1158,33 +1161,26 @@ pub fn setWindowLook(color: dvui.Color, opacity: f32) void {
window_material.opacity = opacity;
}

/// `window_material`, or the best kind below it this backend can show (`materialKinds`), its
/// colour less opaque in a light theme (`light_tint`) where the app gave none of its own.
/// `window_material` as this backend can show it (`materials.effective`: the richest glass at or
/// below it, else opaque; clear only when asked), its colour less opaque in a light theme (`light_tint`) where the app gave none of its own.
pub fn effectiveMaterial(self: *SDLBackend) WindowMaterial {
return self.materialAt(window_material.opacity);
}

/// `effectiveMaterial` with the window's colour at `opacity` instead of the main window's: a float's
/// own window, eased opaque on its own as it is zoomed or goes full screen (`viewportMaterialBase`).
pub fn materialAt(self: *SDLBackend, window_opacity: f32) WindowMaterial {
const offered = self.materialKinds();
// In a light theme the window's colour is light over a desktop that is mostly darker, and at
// the opacity a dark theme reads well at, little showed through it (the user): a light theme's
// colour covers `light_tint` as much. Fizzy's own defaults are the same ratio (`app/settings`:
// 0.7 dark, 0.3 light), which fizzy's people set apart, and fizzy hands its own over with its
// colour; a plain app has the one value.
const opacity = if (window_material.color != null or dvui.themeGet().dark) window_opacity else window_opacity * light_tint;
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)),
};
// Glass keeps a little of the window's colour (`material_min_opacity`); a clear window may
// have none.
if (has) return .{ .kind = material, .opacity = if (material == .transparent) opacity else @max(opacity, material_min_opacity), .color = window_material.color };
}
return .{ .kind = .none, .opacity = opacity, .color = window_material.color };
const drawn = materials.effective(self.materialPlatform(), window_material.kind);
// Glass keeps a little of the window's colour (`material_min_opacity`); a clear or opaque
// window may have none.
const is_glass = drawn != .none and drawn != .transparent;
return .{ .kind = drawn, .opacity = if (is_glass) @max(opacity, material_min_opacity) else opacity, .color = window_material.color };
}

/// The least of the window's colour a material other than a clear window covers what is behind
Expand Down Expand Up @@ -1384,10 +1380,7 @@ pub fn floatingWindowBackground(self: *SDLBackend, id: dvui.Id, rect: dvui.Rect.
}

/// The kinds of window material this backend can show (fizzy's `core.material`, field for field):
/// only a kind every surface it puts glass on can show. A clear window everywhere (`transparent`);
/// on macOS vibrancy (`frost`) and a plain blur, and Liquid Glass from macOS 26; on Windows
/// Acrylic (`frost`); on Linux the compositor's blur behind a window, where it has one
/// (`wayland_blur`).
/// only a kind every surface it puts glass on can show, as `materials.missing` answers it.
pub const MaterialKinds = struct {
none: bool = true,
transparent: bool = false,
Expand Down Expand Up @@ -1458,12 +1451,28 @@ pub fn pointerHeld(_: *SDLBackend) bool {
}

pub fn materialKinds(self: *SDLBackend) MaterialKinds {
return switch (comptime builtin.target.os.tag) {
.macos => .{ .transparent = true, .blur = true, .frost = true, .liquid = liquidGlassAvailable() },
.windows => .{ .transparent = true, .frost = true },
.linux => .{ .transparent = true, .blur = platform.linux_titlebar.blurOffered(self.window) },
else => .{},
const p = self.materialPlatform();
var kinds: MaterialKinds = .{};
inline for (comptime std.enums.values(MaterialKind)) |k| @field(kinds, @tagName(k)) = materials.missing(p, k) == null;
return kinds;
}

/// Why `material` cannot be drawn here, one static line for a tooltip; null when it can
/// (`materials.missing`).
pub fn materialMissing(self: *SDLBackend, material: MaterialKind) ?[]const u8 {
return materials.missing(self.materialPlatform(), material);
}

/// What this platform can draw this frame: the OS's Liquid Glass, the compositor's blur.
fn materialPlatform(self: *SDLBackend) materials.Platform {
if (self.material_platform) |p| return p;
const p: materials.Platform = .{
.os = builtin.target.os.tag,
.liquid_glass = liquidGlassAvailable(),
.compositor_blur = platform.linux_titlebar.blurOffered(self.window),
};
self.material_platform = p;
return p;
}

// ---------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -2966,6 +2975,7 @@ pub fn prefersReducedMotion(_: *@This()) bool {

pub fn begin(self: *SDLBackend, arena: std.mem.Allocator) !void {
self.arena = arena;
self.material_platform = null;
if (self.begin_hook) |hook| hook(self);
// Its step for this frame taken (`begin_hook`): where the main window's picture is now.
self.main_picture = if (platform.window.windowSpacePicture(self.window)) |p| .{ .x = p.x, .y = p.y, .w = p.w, .h = p.h } else null;
Expand Down
101 changes: 101 additions & 0 deletions backend/src/materials.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! Which window materials a platform can draw, and why not the others: the one answer the
//! backend's `materialKinds` and an app's material setting both read, so a material is offered,
//! listed and drawn by the same rule on every platform.
//!
//! std-only, with tests (`build.zig`'s `unit_tests`); `SDLBackend` fills in what it found
//! at run time (`Platform`).
const std = @import("std");

/// The kinds of window material, from the plainest to the richest, field for field with fizzy's
/// `core.material.Kind`.
pub const Kind = enum(u8) { none, transparent, blur, frost, liquid };

/// What the running platform can do, as the backend found it.
pub const Platform = struct {
os: std.Target.Os.Tag,
/// The OS draws Liquid Glass: macOS 26 and later.
liquid_glass: bool = false,
/// The compositor blurs behind a window: Wayland with `ext-background-effect-v1` or KDE's
/// `org_kde_kwin_blur` (`wayland_blur`).
compositor_blur: bool = false,
};

/// Why `kind` cannot be drawn on `platform`, one line for a tooltip; null when it can. The text
/// is static.
pub fn missing(platform: Platform, kind: Kind) ?[]const u8 {
if (kind == .none) return null;
return switch (platform.os) {
.macos => switch (kind) {
.liquid => if (platform.liquid_glass) null else "Not available before macOS 26: needs Liquid Glass",
else => null,
},
.windows => switch (kind) {
.blur => "Not available on Windows: its blur is Acrylic, under Frost",
.liquid => "Not available on Windows: needs Liquid Glass, which only macOS 26 draws",
else => null,
},
.linux => switch (kind) {
.transparent => null,
.blur => if (platform.compositor_blur) null else "Not available on Linux yet: needs compositor blur",
// TODO(fizzyedit/plans#27, linux-blur): frost over the compositor's blur.
.frost => "Not available on Linux yet: needs frost over compositor blur",
.liquid => "Not available on Linux: needs Liquid Glass, which only macOS 26 draws",
.none => unreachable, // Answered above.
},
else => "Not available on this platform",
};
}

/// The kind drawn for `asked` on `platform`: `asked` where it is offered, else the richest glass
/// below it that is, else `none`. A clear window is drawn only when asked for: it is not glass,
/// and in glass's place it reads as a broken window.
pub fn effective(platform: Platform, asked: Kind) Kind {
if (missing(platform, asked) == null) return asked;
var k = @backingInt(asked);
while (k > @backingInt(Kind.blur)) {
k -= 1;
if (missing(platform, @fromBackingInt(k)) == null) return @fromBackingInt(k);
}
return .none;
}

test "a kind not offered falls back to the richest glass below it, never to a clear window" {
try std.testing.expectEqual(Kind.frost, effective(.{ .os = .macos }, .liquid));
try std.testing.expectEqual(Kind.frost, effective(.{ .os = .windows }, .liquid));
try std.testing.expectEqual(Kind.none, effective(.{ .os = .windows }, .blur));
try std.testing.expectEqual(Kind.none, effective(.{ .os = .linux }, .liquid));
try std.testing.expectEqual(Kind.none, effective(.{ .os = .linux }, .frost));
try std.testing.expectEqual(Kind.blur, effective(.{ .os = .linux, .compositor_blur = true }, .liquid));
try std.testing.expectEqual(Kind.transparent, effective(.{ .os = .linux }, .transparent));
try std.testing.expectEqual(Kind.none, effective(.{ .os = .freestanding }, .transparent));
}

test "opaque is drawn everywhere" {
for ([_]std.Target.Os.Tag{ .macos, .windows, .linux, .freestanding }) |os| {
try std.testing.expect(missing(.{ .os = os }, .none) == null);
}
}

test "macOS draws every kind, Liquid Glass from macOS 26" {
try std.testing.expect(missing(.{ .os = .macos, .liquid_glass = true }, .liquid) == null);
try std.testing.expect(missing(.{ .os = .macos }, .liquid) != null);
for ([_]Kind{ .transparent, .blur, .frost }) |k| try std.testing.expect(missing(.{ .os = .macos }, k) == null);
}

test "Windows draws clear and frost" {
const p: Platform = .{ .os = .windows, .liquid_glass = true, .compositor_blur = true };
try std.testing.expect(missing(p, .transparent) == null);
try std.testing.expect(missing(p, .frost) == null);
try std.testing.expect(missing(p, .blur) != null);
try std.testing.expect(missing(p, .liquid) != null);
}

test "Linux draws clear, and blur only where the compositor blurs, each missing kind saying why" {
try std.testing.expect(missing(.{ .os = .linux }, .transparent) == null);
try std.testing.expect(missing(.{ .os = .linux }, .blur) != null);
try std.testing.expect(missing(.{ .os = .linux, .compositor_blur = true }, .blur) == null);
for ([_]Kind{ .frost, .liquid }) |k| {
const why = missing(.{ .os = .linux, .compositor_blur = true }, k) orelse return error.TestUnexpectedResult;
try std.testing.expect(std.mem.startsWith(u8, why, "Not available on Linux"));
}
}
32 changes: 24 additions & 8 deletions core/material.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
//! app draws reads it (`get`), so they look as one.
//!
//! The backend says which kinds it can show; a kind asked for and not offered falls back to the
//! best one below it that is (`Material.effective`). The app publishes the result each frame before
//! anything draws (`publish`): in dvui's data, as `native_glass` is, so a plugin's glass reads the
//! same value as fizzy's.
//! richest glass below it that is, else to an opaque window, never to a clear one
//! (`Material.effective`). What was asked stays as it was saved. The app publishes the result each
//! frame before anything draws (`publish`): in dvui's data, as `native_glass` is, so a plugin's
//! glass reads the same value as fizzy's.
//!
//! `Material`, `Kind`, `Kinds` and `effective` are std-only (a unit-test root in `build/app.zig`);
//! only `publish` and `get` reach dvui.
Expand All @@ -19,17 +20,21 @@ pub const Material = struct {
/// `none`; `transparent` at 1 looks as `none` does.
opacity: f32 = 1,

/// `kind`, or the best kind below it in `offered`: `none` at the least.
/// `kind` where `offered` has it, else the richest glass below it that `offered` has, else
/// `none`. A clear window only when asked for: in glass's place it reads as a broken window.
pub fn effective(self: Material, offered: Kinds) Material {
if (offered.has(self.kind)) return self;
var k = @backingInt(self.kind);
while (k > 0) : (k -= 1) {
while (k > @backingInt(Kind.blur)) {
k -= 1;
if (offered.has(@fromBackingInt(k))) return .{ .kind = @fromBackingInt(k), .opacity = self.opacity };
}
return .{ .kind = .none, .opacity = self.opacity };
}
};

/// From the plainest to the richest: what `Material.effective` falls back through.
/// From the plainest to the richest, as the settings list them: what `Material.effective` falls
/// back through.
pub const Kind = enum(u8) { none, transparent, blur, frost, liquid };

/// The kinds a backend offers. A backend offers a kind only if every surface it puts glass on can
Expand Down Expand Up @@ -63,12 +68,23 @@ pub fn get() Material {
return dvui.dataGet(null, dvui.Id.update(.zero, key), "_material", Material) orelse .{};
}

test "effective falls back to the best kind offered below the one asked" {
test "effective falls back to the richest glass offered below the one asked" {
const mac_old: Kinds = .{ .transparent = true, .blur = true, .frost = true };
try std.testing.expectEqual(Kind.frost, (Material{ .kind = .liquid }).effective(mac_old).kind);
try std.testing.expectEqual(Kind.blur, (Material{ .kind = .blur }).effective(mac_old).kind);
const windows: Kinds = .{ .transparent = true, .frost = true };
try std.testing.expectEqual(Kind.transparent, (Material{ .kind = .blur }).effective(windows).kind);
try std.testing.expectEqual(Kind.frost, (Material{ .kind = .liquid }).effective(windows).kind);
try std.testing.expectEqual(Kind.none, (Material{ .kind = .liquid }).effective(.{}).kind);
try std.testing.expectEqual(@as(f32, 0.4), (Material{ .kind = .liquid, .opacity = 0.4 }).effective(windows).opacity);
}

test "glass not offered falls back to opaque, never to a clear window" {
const windows: Kinds = .{ .transparent = true, .frost = true };
try std.testing.expectEqual(Kind.none, (Material{ .kind = .blur }).effective(windows).kind);
const linux: Kinds = .{ .transparent = true };
inline for (.{ Kind.blur, Kind.frost, Kind.liquid }) |k| {
try std.testing.expectEqual(Kind.none, (Material{ .kind = k }).effective(linux).kind);
}
try std.testing.expectEqual(Kind.transparent, (Material{ .kind = .transparent }).effective(linux).kind);
try std.testing.expectEqual(Kind.blur, (Material{ .kind = .liquid }).effective(.{ .transparent = true, .blur = true }).kind);
}
15 changes: 14 additions & 1 deletion src/Entry.zig
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,18 @@ fn materialOffered() fizzy.core.material.Kinds {
return k;
}

/// Why the backend cannot draw `kind`, one static line for a tooltip; null when it can. The
/// backend's own reason where it gives one (`SDLBackend.materialMissing`), else a plain one for a
/// kind it does not offer.
pub fn materialMissing(kind: fizzy.core.material.Kind) ?[]const u8 {
if (comptime @hasDecl(dvui.backend, "materialMissing")) {
return dvui.currentWindow().backend.impl.materialMissing(switch (kind) {
inline else => |k| @field(dvui.backend.MaterialKind, @tagName(k)),
});
}
return if (materialOffered().has(kind)) null else "Not available with this backend";
}

/// The kind of window material asked for: `FIZZY_GLASS` where it names one, else the setting
/// (`Settings.window_material`).
fn materialAsked() fizzy.core.material.Kind {
Expand Down Expand Up @@ -562,7 +574,8 @@ fn frameOnce() !dvui.App.Result {
}
// What the app's windows are made of, as the backend can show it, for every glass drawn this
// frame (`core.material`): before `Popout`, whose OS glass is offered only for `liquid`. The
// setting asks (Liquid Glass by default, so each platform shows the best it can);
// setting asks (Liquid Glass by default, so each platform shows the richest glass it has, or
// an opaque window), and is never rewritten by what this platform draws;
// `FIZZY_GLASS` (`none`, `transparent`, `blur`, `frost`, `liquid`) overrides it.
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
Expand Down
Loading
Loading