From 96ddc9ec6366da8c0e8f57dddf57d34ab64ffa04 Mon Sep 17 00:00:00 2001 From: foxnne Date: Mon, 19 Oct 2026 20:07:23 -0500 Subject: [PATCH] settings: materials the platform can't draw are listed but disabled On Linux the Window material setting offered Frost, Blur and Liquid Glass and then drew them as a clear window ("shown as Clear"). A material the platform cannot draw is now listed, greyed and not selectable, with a tooltip saying why. One place answers which materials a platform draws and why not the others (backend/src/materials.zig); the backend's materialKinds and the setting both read it, so the coming Linux blur only flips that answer. A saved kind that cannot be drawn here falls back to the richest glass below it, else to an opaque window, never to a clear one; the saved value is left as it is. Co-Authored-By: Claude Opus 5.5 --- app/settings/Settings.zig | 6 +- backend/build.zig | 2 + backend/src/SDLBackend.zig | 60 ++++++++++-------- backend/src/materials.zig | 101 +++++++++++++++++++++++++++++++ core/material.zig | 32 +++++++--- src/Entry.zig | 15 ++++- src/editor/Keybinds.zig | 14 ++++- src/editor/explorer/settings.zig | 40 ++++++++---- 8 files changed, 218 insertions(+), 52 deletions(-) create mode 100644 backend/src/materials.zig diff --git a/app/settings/Settings.zig b/app/settings/Settings.zig index 29565e6e6..b33f8ba34 100644 --- a/app/settings/Settings.zig +++ b/app/settings/Settings.zig @@ -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 — diff --git a/backend/build.zig b/backend/build.zig index 0b7ba8d55..6ea3942c8 100644 --- a/backend/build.zig +++ b/backend/build.zig @@ -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. diff --git a/backend/src/SDLBackend.zig b/backend/src/SDLBackend.zig index f66a6d615..6bf8b14cb 100644 --- a/backend/src/SDLBackend.zig +++ b/backend/src/SDLBackend.zig @@ -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`). @@ -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 @@ -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, @@ -1158,8 +1161,8 @@ 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); } @@ -1167,24 +1170,17 @@ pub fn effectiveMaterial(self: *SDLBackend) WindowMaterial { /// `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 @@ -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, @@ -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; } // --------------------------------------------------------------------------------------------- @@ -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; diff --git a/backend/src/materials.zig b/backend/src/materials.zig new file mode 100644 index 000000000..42038be42 --- /dev/null +++ b/backend/src/materials.zig @@ -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")); + } +} diff --git a/core/material.zig b/core/material.zig index 24eabd905..f2298b4c5 100644 --- a/core/material.zig +++ b/core/material.zig @@ -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. @@ -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 @@ -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); +} diff --git a/src/Entry.zig b/src/Entry.zig index be6775a31..457b740b3 100644 --- a/src/Entry.zig +++ b/src/Entry.zig @@ -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 { @@ -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 diff --git a/src/editor/Keybinds.zig b/src/editor/Keybinds.zig index 7ff0dc67e..82bcbd68a 100644 --- a/src/editor/Keybinds.zig +++ b/src/editor/Keybinds.zig @@ -259,10 +259,11 @@ fn cmdDemoPlayNamed(state: *anyopaque, args: DemoByName.Args, call: *sdk.Command } const WindowMaterial = sdk.Command.Params(struct { - kind: sdk.Command.Arg([]const u8, .{ .description = "What the window is made of: none (opaque), transparent, blur, frost or liquid. Each platform shows the best it has at or below it. Empty steps to the next." }) = .init(""), + kind: sdk.Command.Arg([]const u8, .{ .description = "What the window is made of: none (opaque), transparent, blur, frost or liquid. Saved as given; one this platform cannot draw is drawn as the richest glass below it, else opaque. Empty steps to the next it can draw." }) = .init(""), }); -/// The next material, round from Liquid Glass back to opaque, as a row or a shortcut runs it. +/// The next material this platform can draw, round from Liquid Glass back to opaque, as a row or +/// a shortcut runs it. fn cmdWindowMaterialStep(state: *anyopaque) anyerror!void { _ = stepWindowMaterial(editorFromState(state)); } @@ -280,7 +281,14 @@ fn cmdWindowMaterial(state: *anyopaque, args: WindowMaterial.Args, call: *sdk.Co fn stepWindowMaterial(editor: *Editor) fizzy.core.material.Kind { const Kind = fizzy.core.material.Kind; - const next: Kind = @fromBackingInt(@intCast((@backingInt(editor.app.settings.window_material) + 1) % std.enums.values(Kind).len)); + const n = std.enums.values(Kind).len; + // From the kind drawn, which the row shows, not a saved one this platform cannot draw. + var next = fizzy.core.material.get().kind; + // `none` is drawn everywhere, so the walk ends within one round. + for (0..n) |_| { + next = @fromBackingInt(@intCast((@backingInt(next) + 1) % n)); + if (fizzy.Entry.materialMissing(next) == null) break; + } setWindowMaterial(editor, next); return next; } diff --git a/src/editor/explorer/settings.zig b/src/editor/explorer/settings.zig index 7308514bf..49ee22a1d 100644 --- a/src/editor/explorer/settings.zig +++ b/src/editor/explorer/settings.zig @@ -299,9 +299,10 @@ const window_items = [_]Item{ .label = "Window material", .key = "window_material", .description = "What the window is made of: opaque, clear, blurred, frosted or Liquid " ++ - "Glass, and every dialog, menu and float with it. Each platform shows the best it " ++ - "has at or below the one chosen: Liquid Glass on macOS 26 and on the web, frost on " ++ - "Windows, blur where a Linux compositor blurs.", + "Glass, and every dialog, menu and float with it. A material this platform cannot " ++ + "draw is listed but not offered, and says why. A saved one it cannot draw (settings " ++ + "shared with another machine) is drawn as the richest glass below it here, else " ++ + "opaque, and stays saved.", .keywords = "glass material liquid frost blur transparent opaque translucent window background", .draw = drawWindowMaterial, }, @@ -800,25 +801,40 @@ fn drawWindowMaterial() void { dvui.label(@src(), "{s}, set by FIZZY_GLASS", .{materialName(kind)}, .{ .gravity_x = 1.0 }); return; } + const Kind = fizzy.core.material.Kind; const asked = editor.app.settings.window_material; + // What is drawn: the saved choice where this platform can draw it, else its fallback + // (`Material.effective`). The saved choice stays, for a machine that can draw it. const shown = fizzy.core.material.get().kind; var dropdown: core.widgets.DropdownWidget = undefined; - dropdown.init(@src(), .{}, .{ .expand = .horizontal, .corners = dvui.CornerRect.all(1000) }); + dropdown.init(@src(), .{ .selected_index = @backingInt(shown) }, .{ .expand = .horizontal, .corners = dvui.CornerRect.all(1000) }); defer dropdown.deinit(); { var hbox = dvui.box(@src(), .{ .dir = .horizontal }, .{ .expand = .vertical, .gravity_x = 1.0 }); defer hbox.deinit(); - if (shown == asked) { - dvui.labelNoFmt(@src(), materialName(asked), .{}, .{ .margin = .all(0), .padding = .all(0) }); - } else { - // What this platform draws instead: the best it has at or below the one asked. - dvui.label(@src(), "{s}, shown as {s}", .{ materialName(asked), materialName(shown) }, .{ .margin = .all(0), .padding = .all(0) }); - } + dvui.labelNoFmt(@src(), materialName(shown), .{}, .{ .margin = .all(0), .padding = .all(0) }); core.icon.icon(@src(), "dropdown_triangle", dvui.entypo.triangle_down, .{}, .{ .gravity_y = 0.5 }); } + if (shown != asked) { + const why = fizzy.Entry.materialMissing(asked) orelse "Not drawn here"; + core.widgets.tooltip(@src(), .{ .active_rect = dropdown.data().borderRectScale().r, .position = .vertical }, "{s} is saved. {s}.", .{ materialName(asked), why }, .{}); + } if (dropdown.dropped()) { - for (std.enums.values(fizzy.core.material.Kind)) |kind| { - if (dropdown.addChoiceLabel(materialName(kind))) { + for (std.enums.values(Kind), 0..) |kind, i| { + const missing = fizzy.Entry.materialMissing(kind); + var mi = dropdown.addChoice(); + defer mi.deinit(); + const text = dvui.themeGet().color(.window, .text); + dvui.labelNoFmt(@src(), materialName(kind), .{}, mi.data().options.strip().override(mi.style()).override(.{ + .color_text = if (missing == null) null else .{ .color = text.opacity(0.4) }, + })); + // A kind this platform cannot draw is listed, not chosen: it says why instead. + if (missing) |why| { + core.widgets.tooltip(@src(), .{ .active_rect = mi.data().borderRectScale().r }, "{s}", .{why}, .{ .id_extra = i }); + continue; + } + if (mi.activeRect() != null) { + dropdown.close(); editor.app.settings.window_material = kind; editor.markSettingsDirty(); dvui.refresh(null, @src(), null);