From c23d6ede6bd66fbaf91ccfe94568f924e7afb10a Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 22:07:35 -0500 Subject: [PATCH 1/5] backend: the viewports API is the backend package's A dvui app on fizzy's backend opens its OS windows through `@import("backend").viewports`, the API fizzy's Popout used from `src/backend/backend_native.zig`, moved into `backend/src/viewports.zig` as it was. Fizzy re-exports it. Co-Authored-By: Claude Opus 5.5 --- backend/src/SDLBackend.zig | 44 ++-- backend/src/platform/window.zig | 44 +++- backend/src/viewports.zig | 378 ++++++++++++++++++++++++++ src/backend/backend_native.zig | 454 +------------------------------- 4 files changed, 447 insertions(+), 473 deletions(-) create mode 100644 backend/src/viewports.zig diff --git a/backend/src/SDLBackend.zig b/backend/src/SDLBackend.zig index e0387f0d0..690bf88af 100644 --- a/backend/src/SDLBackend.zig +++ b/backend/src/SDLBackend.zig @@ -16,6 +16,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"); /// What this backend keeps count of as it runs (`health`). pub const Health = @import("Health.zig"); @@ -94,7 +96,7 @@ window_geometry: WindowGeometry = .{}, // Set by `initWindow` and `initWindowSecondary` for use by eventual child window. init_opts_save: ?InitOptions = null, /// The OS windows besides this one that show part of its frame (`Viewport`). -viewports: [max_viewports]?Viewport = @splat(null), +viewport_slots: [max_viewports]?Viewport = @splat(null), /// Where a held pointer is read while one is held (`PointerPin`, `heldPoint`). pointer_pin: PointerPin = .none, /// macOS: the main window may have come in front of a viewport's window — it took focus, was @@ -915,7 +917,7 @@ fn noteMainForward(self: *SDLBackend, target: ?*c.SDL_Window, event_type: u32) v }; if (event_type == c.SDL_EVENT_WINDOW_FOCUS_GAINED) self.main_focused_once = true; if (!over) return; - for (&self.viewports) |*slot| if (slot.*) |*v| { + for (&self.viewport_slots) |*slot| if (slot.*) |*v| { v.over_main = true; }; } @@ -1172,7 +1174,7 @@ fn openViewport(self: *SDLBackend, at: viewport_map.Rect, title_text: [:0]const const carry = role != .window; const menu = role == .menu; if (!viewportsAvailable()) return null; - const slot = for (self.viewports, 0..) |v, i| { + const slot = for (self.viewport_slots, 0..) |v, i| { if (v == null) break i; } else return null; const b = viewport_map.band(slot); @@ -1220,8 +1222,8 @@ fn openViewport(self: *SDLBackend, at: viewport_map.Rect, title_text: [:0]const // where SDL makes it a child window that moves with its parent, and the windows that left // the main one stay where they are when it moves (`plans/POPOUT_WINDOWS_PLAN.md`, decision 2). if (comptime builtin.os.tag != .macos) _ = c.SDL_SetWindowParent(window, self.window); - self.viewports[slot] = .{ .window = window, .band = b, .anchor = anchor, .density = d, .screen = placed.screen, .frame = placed.frame, .passive = carry and !menu, .overlay = role == .overlay, .menu = menu }; - const vp = &self.viewports[slot].?; + self.viewport_slots[slot] = .{ .window = window, .band = b, .anchor = anchor, .density = d, .screen = placed.screen, .frame = placed.frame, .passive = carry and !menu, .overlay = role == .overlay, .menu = menu }; + const vp = &self.viewport_slots[slot].?; // Dressed by `viewportOpenMenu`, and no hit test: nothing in it moves or resizes it. if (menu) return vp; if (role == .overlay) { @@ -1325,13 +1327,13 @@ pub fn viewportClose(self: *SDLBackend, vp: *Viewport) void { // What rides on it lets go first (`Viewport.ride`): SDL destroys a window's children with it, // and the slot of each still holds its window. The app moves a dialog left behind back over // the main window (`Popout`). - for (&self.viewports) |*slot| { + for (&self.viewport_slots) |*slot| { if (slot.*) |*other| if (other != vp and other.ride != null and other.ride == vp.window) { _ = c.SDL_SetWindowParent(other.window, null); other.ride = null; }; } - for (&self.viewports) |*slot| { + for (&self.viewport_slots) |*slot| { if (slot.*) |*v| if (v == vp) { // Holding the keyboard as it goes — a float that merged back into the main window, or // came back on a command, while its window was the one in front — it hands it back to @@ -1352,7 +1354,7 @@ pub fn viewportClose(self: *SDLBackend, vp: *Viewport) void { break; }; } - for (self.viewports) |v| if (v != null) return; + for (self.viewport_slots) |v| if (v != null) return; _ = c.SDL_ResetHint(c.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH); } @@ -1623,7 +1625,7 @@ pub fn viewportPresent(_: *SDLBackend, vp: *Viewport, target: ?dvui.TextureTarge } fn viewportOf(self: *SDLBackend, window: *c.SDL_Window) ?*Viewport { - for (&self.viewports) |*slot| { + for (&self.viewport_slots) |*slot| { if (slot.*) |*v| if (v.window == window) return v; } return null; @@ -1666,7 +1668,7 @@ fn heldAcrossWindows(self: *SDLBackend, event: c.SDL_Event) bool { c.SDL_EVENT_MOUSE_BUTTON_DOWN, c.SDL_EVENT_MOUSE_BUTTON_UP => if (event.button.which == c.SDL_TOUCH_MOUSEID) return false, else => return false, } - const any = for (self.viewports) |v| { + const any = for (self.viewport_slots) |v| { if (v != null) break true; } else false; if (!any) return false; @@ -1700,7 +1702,7 @@ fn heldPoint(self: *SDLBackend) dvui.Point.Physical { var mh: c_int = 0; _ = c.SDL_GetWindowSize(self.window, &mw, &mh); const over_main = gx >= origin.x and gy >= origin.y and gx < origin.x + @as(f32, @floatFromInt(mw)) and gy < origin.y + @as(f32, @floatFromInt(mh)); - for (&self.viewports) |*slot| { + for (&self.viewport_slots) |*slot| { const vp = if (slot.*) |*v| v else continue; if (vp.passive or vp.see_through) continue; // In a fullscreen Space of its own, a window is where the OS put it, and under nothing: @@ -1964,7 +1966,7 @@ pub fn deinit(self: *SDLBackend) void { WindowGeometry.save(self); } // Viewports are claimed on this window's device: given back before it goes. - for (&self.viewports) |*slot| { + for (&self.viewport_slots) |*slot| { if (slot.*) |*v| self.viewportClose(v); } self.gpu.destroy(); @@ -1988,7 +1990,7 @@ pub fn renderPresent(self: *SDLBackend) void { var transacted: [max_viewports]bool = @splat(false); var any = false; if (comptime builtin.os.tag == .macos) { - for (&self.viewports, 0..) |*slot, i| { + for (&self.viewport_slots, 0..) |*slot, i| { const vp = if (slot.*) |*v| v else continue; const first = !vp.shown and vp.pending != null; const reshaped = vp.passive and !vp.overlay and (vp.carry_radius != vp.carry_radius_shown or vp.carry_alpha != vp.carry_alpha_shown or vp.carry_size_shown[0] != vp.screen.w or vp.carry_size_shown[1] != vp.screen.h); @@ -2018,7 +2020,7 @@ pub fn renderPresent(self: *SDLBackend) void { } } defer if (comptime builtin.os.tag == .macos) if (any) { - for (&self.viewports, transacted) |*slot, t| { + for (&self.viewport_slots, transacted) |*slot, t| { if (!t) continue; const vp = if (slot.*) |*v| v else continue; fizzy_native_viewport_presented(cocoaWindow(vp.window)); @@ -2033,7 +2035,7 @@ pub fn renderPresent(self: *SDLBackend) void { // drawables back slowly while the main window goes through a Space transition, and waiting on // them every frame halved the move's rate with one open. const main_moving = platform.window.windowMovingItself(self.window); - for (&self.viewports, 0..) |*slot, i| { + for (&self.viewport_slots, 0..) |*slot, i| { const vp = if (slot.*) |*v| v else continue; const target = vp.pending orelse continue; vp.pending = null; @@ -2058,7 +2060,7 @@ pub fn renderPresent(self: *SDLBackend) void { self.gpu.present(self.clear_window_on_begin, builtin.os.tag == .windows and self.inLiveResize()); // Shown once there is a frame in it (or empty, above). Not made key: the window it came out of // keeps the keyboard until the viewport is clicked. - for (&self.viewports, first_frame, map_empty) |*slot, show, empty| { + for (&self.viewport_slots, first_frame, map_empty) |*slot, show, empty| { if (!show and !empty) continue; const vp = if (slot.*) |*v| v else continue; if (!vp.mapped) { @@ -2081,11 +2083,11 @@ pub fn renderPresent(self: *SDLBackend) void { // activates, which brings the main window in front of them (`fizzy_macos_watch_first_activation`). if (comptime builtin.os.tag == .macos) { fizzy_macos_watch_first_activation(); - if (fizzy_macos_took_first_activation() != 0) for (&self.viewports) |*slot| if (slot.*) |*v| { + if (fizzy_macos_took_first_activation() != 0) for (&self.viewport_slots) |*slot| if (slot.*) |*v| { v.over_main = true; }; const main_ns = cocoaWindow(self.window); - for (&self.viewports) |*slot| { + for (&self.viewport_slots) |*slot| { const vp = if (slot.*) |*v| v else continue; if (!vp.over_main) continue; vp.over_main = false; @@ -2098,7 +2100,7 @@ pub fn renderPresent(self: *SDLBackend) void { // when the stacking may have changed. if (self.stack_stale) { self.stack_stale = false; - for (&self.viewports) |*slot| { + for (&self.viewport_slots) |*slot| { const vp = if (slot.*) |*v| v else continue; vp.under_main = vp.mapped and !vp.passive and !vp.menu and fizzy_macos_viewport_under_main(cocoaWindow(vp.window), main_ns) != 0; } @@ -2176,7 +2178,7 @@ pub fn begin(self: *SDLBackend, arena: std.mem.Allocator) !void { // A float's window standing still on its way into or out of full screen: its picture has // taken this frame's step (`begin_hook`), and the float follows it there — as it did the // window's moves — and back to the window as the way ends. - if (comptime builtin.os.tag == .macos) for (&self.viewports) |*slot| { + if (comptime builtin.os.tag == .macos) for (&self.viewport_slots) |*slot| { const vp = if (slot.*) |*v| v else continue; if (vp.picture_at[0] != 0 or vp.picture_at[1] != 0 or platform.window.windowSpacePicture(vp.window) != null) viewportFollowWindow(vp); @@ -2227,7 +2229,7 @@ fn mainHeld(self: *SDLBackend) bool { // Its own way into or out of full screen is drawn whatever AppKit says of it on the way. if (platform.window.windowMovingItself(self.window)) return false; if (c.SDL_GetWindowFlags(self.window) & c.SDL_WINDOW_OCCLUDED != 0) return true; - for (&self.viewports) |*slot| { + for (&self.viewport_slots) |*slot| { const vp = if (slot.*) |*v| v else continue; if (vp.shown and platform.window.windowMovingItself(vp.window)) return true; } diff --git a/backend/src/platform/window.zig b/backend/src/platform/window.zig index a2e845035..9ea461e73 100644 --- a/backend/src/platform/window.zig +++ b/backend/src/platform/window.zig @@ -53,6 +53,46 @@ pub const WindowGlass = extern struct { feather: f64, }; +/// A window's Liquid Glass this frame (`liquidGlassLook`, `viewports.windowGlass`), as an app +/// says it: what `core.glass_look.window` says at the window's opacity, the window's colour, and the clearing +/// bevel `core.glass_look.band` gives its size. +pub const WindowGlassLook = struct { + under_variant: i32, + under_style: i32, + over_variant: i32, + over_style: i32, + frost: f32, + blur: f32, + glass: f32, + fill: dvui.Color, + /// The colour's opacity in the body… + fill_opacity: f32, + /// …and over all of it, the bevel too. + top_fill: f32, + /// Points, each. + radius: f32, + clear: f32, + feather: f32, + + /// As `fizzy_macos_window_liquid_glass_look` reads it. + pub fn native(self: WindowGlassLook) WindowGlass { + return .{ + .under_variant = self.under_variant, + .under_style = self.under_style, + .over_variant = self.over_variant, + .over_style = self.over_style, + .frost = self.frost, + .blur = self.blur, + .glass = self.glass, + .fill = .{ @as(f64, @floatFromInt(self.fill.r)) / 255, @as(f64, @floatFromInt(self.fill.g)) / 255, @as(f64, @floatFromInt(self.fill.b)) / 255, self.fill_opacity }, + .top_fill = self.top_fill, + .radius = self.radius, + .clear = self.clear, + .feather = self.feather, + }; + } +}; + /// Make one of fizzy's titled windows (`raw_ptr`, its `NSWindow`) a window of Liquid Glass — over /// the clear lens a body of frost, the plain blur (the vibrancy it wore before, /// `ns_visual_effect_material`) and the window's colour, fading out across a clearing bevel along @@ -259,7 +299,7 @@ pub fn setStyle(win: *dvui.Window) void { // Not a menu's or a dialog's: dressed once as it opens (`fizzy_macos_viewport_menu`), it // never goes full screen, and this made it a full-screen primary window over the // auxiliary one it was opened as, every frame. - for (&win.backend.impl.viewports) |*slot| { + for (&win.backend.impl.viewport_slots) |*slot| { const vp = if (slot.*) |*v| v else continue; if (vp.passive or vp.menu) continue; const ns = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(vp.window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse continue; @@ -383,7 +423,7 @@ pub fn setBackground(win: *dvui.Window, color: dvui.Color) void { // Every float's own window too, and each opened from now on (`SDLBackend.viewportGlass`): // side by side, and each in a Space of its own, they are one app's windows. win.backend.impl.window_skin = color; - for (&win.backend.impl.viewports) |*slot| { + for (&win.backend.impl.viewport_slots) |*slot| { const vp = if (slot.*) |*v| v else continue; if (vp.passive) continue; const ns = c.SDL_GetPointerProperty(c.SDL_GetWindowProperties(vp.window), c.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, null) orelse continue; diff --git a/backend/src/viewports.zig b/backend/src/viewports.zig new file mode 100644 index 000000000..2a89da637 --- /dev/null +++ b/backend/src/viewports.zig @@ -0,0 +1,378 @@ +//! OS windows besides the main one, each showing a part of the one frame: a float, a menu or a +//! dialog popped out into a window of its own (`plans/POPOUT_WINDOWS_PLAN.md`, `Viewport`). The +//! API an app draws them with, over this backend's own (`SDLBackend.viewport*`); `viewport_map` +//! is where each one's part of the frame lies. +//! +//! Every call acts on the current dvui window's backend, so it is made between `dvui.Window.begin` +//! and `end`. On Wayland, where a window cannot be placed, there are none (`available`). +const builtin = @import("builtin"); +const dvui = @import("dvui"); +const SDLBackend = @import("SDLBackend.zig"); + +/// A window's Liquid Glass this frame, as the main window's (`platform.window.WindowGlassLook`). +pub const WindowGlassLook = SDLBackend.platform.window.WindowGlassLook; + +/// Always, on this backend: an app that also builds on one without OS windows (dvui's own, the +/// web) keeps a stand-in with this name false. +pub const supported = true; +pub const Viewport = SDLBackend.Viewport; +/// Physical pixels of the frame. +pub const Rect = SDLBackend.viewport_map.Rect; + +/// Whether this run can open viewports: not on Wayland, where a window cannot be placed. +pub fn available() bool { + return SDLBackend.viewportsAvailable(); +} + +/// A viewport over `at` (the frame as the main window shows it), its window opening over that +/// place on the desktop, hidden until a frame is presented into it. Its own part of the frame +/// is `frameOf`. +pub fn open(at: Rect, title: [:0]const u8) ?*Viewport { + return dvui.currentWindow().backend.impl.viewportOpen(at, title); +} + +/// A window to carry a view past every window of the app's, over the desktop: clear, the pointer +/// passing through it, above every window, placed and drawn as any viewport (`placeMain`, +/// `present`). Where it can be made (`carries`). +pub fn openCarry(at: Rect) ?*Viewport { + return dvui.currentWindow().backend.impl.viewportOpenCarry(at); +} + +/// Whether one of the app's windows has the keyboard: the app is the active one. +pub fn appActive() bool { + return SDLBackend.appActive(); +} + +/// Whether menus can be windows of their own (`openMenu`): macOS, and Windows (Acrylic popups), +/// where viewports are. +pub const menus = builtin.os.tag == .macos or builtin.os.tag == .windows; + +/// A window for a menu (`Popout`'s menus): borderless, above every window, never focused, the +/// OS's material in it — rounded by `radius` points — and the pointer over it read in the main +/// window's frame (`mainOffset`). Placed with `placeRiding`, drawn as any viewport. `ride`: the +/// window the OS moves it with — smoothly, through a drag. `dialog`: a dialog's window, the same +/// but in its window's stacking, not above every window. +pub fn openMenu(at: Rect, radius: f32, ride: Ride, dialog: bool) ?*Viewport { + return dvui.currentWindow().backend.impl.viewportOpenMenu(at, radius, ride, dialog); +} + +pub const Ride = SDLBackend.Ride; + +/// `placeMain` for a window riding on another (`openMenu`'s `ride`), put somewhere new only when +/// `key` — where it lies over that window — changes. +pub fn placeRiding(vp: *Viewport, frame: Rect, key: Rect) Rect { + return dvui.currentWindow().backend.impl.viewportPlaceRiding(vp, frame, key); +} + +/// Where menu `vp` is drawn in the frame from where its window lies over the main window, +/// physical pixels: none for a menu of the main window's. +pub fn mainOffset(vp: *Viewport, offset: dvui.Point.Physical) void { + dvui.currentWindow().backend.impl.viewportMainOffset(vp, .{ .x = offset.x, .y = offset.y }); +} + +/// A piece of the OS's glass in an overlay (`overlayGlass`): a rounded rect, points from the +/// overlay window's top left. +pub const GlassShape = SDLBackend.GlassShape; + +/// Whether the OS has Liquid Glass to draw a view drag's glass with (`openOverlay`): macOS 26. +pub fn liquidGlass() bool { + return SDLBackend.liquidGlassAvailable(); +} + +/// A window over the display the main window is on (`displayInMain`) holding the OS's glass +/// (`overlayGlass`) under the picture presented into it, the pointer passing through it. Where +/// `liquidGlass`. +pub fn openOverlay(at: Rect) ?*Viewport { + return dvui.currentWindow().backend.impl.viewportOpenOverlay(at); +} + +/// One of Liquid Glass's materials: its variant and style. +pub const GlassMaterial = struct { variant: i32, style: i32 }; +/// What an overlay's glass is: two layers of the same pieces, `under` and `over`, crossfaded by +/// `over_share` — each layer's pieces alike, for the OS to run them together. +pub const GlassLook = struct { + under: GlassMaterial, + over: GlassMaterial, + over_share: f32, + /// How much of the glass there is. + glass: f32 = 1, + /// The window's colour under the glass, each piece's shape, `fill_opacity` opaque; a lit + /// piece's lit by `lit_amount` toward `lit_toward`. + fill: dvui.Color = .black, + fill_opacity: f32 = 0, + lit_toward: dvui.Color = .white, + lit_amount: f32 = 0, + /// Each piece's clearing bevel (`core.glass_look.band`): a share of its shorter half, at + /// most `bevel_cap` points, clear for `bevel_clear` of it before the frost and the colour + /// come in. + bevel: f32 = 0, + bevel_cap: f32 = 0, + bevel_clear: f32 = 0, + /// Points of plain blur over the lens in each piece's body, in place of the `over` frost, + /// where the OS can blur what is behind a window without its materials' tint (macOS: Core + /// Animation's backdrop blur): 0, or none to draw it with, and the frost it is. + blur: f32 = 0, +}; + +/// Overlay `vp`'s glass this frame (`openOverlay`), run together within `spacing` points, as +/// `look`. +pub fn overlayGlass(vp: *Viewport, shapes: []const GlassShape, spacing: f32, look: GlassLook) void { + const f = look.fill; + const t = look.lit_toward; + dvui.currentWindow().backend.impl.viewportOverlayGlass(vp, shapes, spacing, .{ + .under_variant = look.under.variant, + .under_style = look.under.style, + .over_variant = look.over.variant, + .over_style = look.over.style, + .over_share = look.over_share, + .glass = look.glass, + .fill = .{ @as(f64, @floatFromInt(f.r)) / 255, @as(f64, @floatFromInt(f.g)) / 255, @as(f64, @floatFromInt(f.b)) / 255, look.fill_opacity }, + .lit_toward = .{ @as(f64, @floatFromInt(t.r)) / 255, @as(f64, @floatFromInt(t.g)) / 255, @as(f64, @floatFromInt(t.b)) / 255, look.lit_amount }, + .bevel = look.bevel, + .bevel_cap = look.bevel_cap, + .bevel_clear = look.bevel_clear, + .blur = look.blur, + }); +} + +/// The carried view's picture under an overlay's glass (`overlayPhoto`). +pub const OverlayPhoto = struct { + /// The rounded rect it shows in, points from the overlay window's top left. + rect: Rect, + radius: f32, + /// Where its image lies, points from `rect`'s top left: it may reach past it. + image: Rect, + fill: dvui.Color, + alpha: f32 = 1, + /// Points of plain blur on it. + blur: f32 = 0, +}; + +/// Overlay `vp`'s image of the carried view's picture: premultiplied RGBA rows, `w` by `h`, +/// copied; null, none. Once a drag (`overlayPhoto` places it). +pub fn overlayPhotoImage(vp: *Viewport, rgba: ?[]const u8, w: u32, h: u32) void { + dvui.currentWindow().backend.impl.viewportOverlayPhotoImage(vp, rgba, w, h); +} + +/// Where overlay `vp` shows the carried view's picture this frame, under its glass, so the +/// glass over it bends it; null, nowhere. Applied with the glass. +pub fn overlayPhoto(vp: *Viewport, photo: ?OverlayPhoto) void { + const p = photo orelse return dvui.currentWindow().backend.impl.viewportOverlayPhoto(vp, null); + const f = p.fill; + dvui.currentWindow().backend.impl.viewportOverlayPhoto(vp, .{ + .x = p.rect.x, + .y = p.rect.y, + .w = p.rect.w, + .h = p.rect.h, + .radius = p.radius, + .image = .{ p.image.x, p.image.y, p.image.w, p.image.h }, + .fill = .{ @as(f64, @floatFromInt(f.r)) / 255, @as(f64, @floatFromInt(f.g)) / 255, @as(f64, @floatFromInt(f.b)) / 255, @as(f64, @floatFromInt(f.a)) / 255 }, + .alpha = p.alpha, + .blur = p.blur, + }); +} + +/// The display the main window is on, in its frame (physical pixels from its top left). +pub fn displayInMain() Rect { + return dvui.currentWindow().backend.impl.viewportDisplayInMain(); +} + +/// A carry window's shape this frame (`openCarry`): what it carries fills it, rounded by +/// `radius` physical pixels — the OS's material and shadow in that shape — `alpha` opaque. +/// Null: hidden. +pub fn carryShape(vp: *Viewport, radius: ?f32, alpha: f32) void { + dvui.currentWindow().backend.impl.viewportCarryShape(vp, radius, alpha); +} + +/// `vp`'s window at `other`'s level and just above it — a float born of a drop, over the drag's +/// glass while that goes — until `settle` puts it back at its own. macOS. +pub fn lift(vp: *Viewport, other: *Viewport) void { + dvui.currentWindow().backend.impl.viewportLift(vp, other); +} + +pub fn settle(vp: *Viewport) void { + dvui.currentWindow().backend.impl.viewportSettle(vp); +} + +/// `vp`'s window put just above `other`'s in the stacking (a growing float's picture over the +/// glass it grows out of). macOS. +pub fn orderAbove(vp: *Viewport, other: *Viewport) void { + dvui.currentWindow().backend.impl.viewportOrderAbove(vp, other); +} + +/// A carry window's glass as the lens — the carried view, a drop of water — or as frost, as a +/// window's material is (a float's window growing out of it). Liquid Glass only. +pub fn carryLens(vp: *Viewport, lens: bool) void { + dvui.currentWindow().backend.impl.viewportCarryLens(vp, lens); +} + +/// Whether `vp`'s window lies under the main window in the OS's stacking — a float's, clicked +/// behind it — as last read (macOS; never elsewhere, where floats stay over it). +pub fn underMain(vp: *Viewport) bool { + return dvui.currentWindow().backend.impl.viewportUnderMain(vp); +} + +/// Points from `vp`'s window's left edge past the OS's own buttons in its title bar (macOS's +/// traffic lights, `os_buttons`): 0 without them. +pub fn buttonsWidth(vp: *Viewport) f32 { + return dvui.currentWindow().backend.impl.viewportButtonsWidth(vp); +} + +/// `vp`'s window's Liquid Glass this frame, as the main window's (`windowGlass`). Whether it +/// has any: its float then draws no base of its own. +pub fn windowGlass(vp: *Viewport, look: WindowGlassLook) bool { + return dvui.currentWindow().backend.impl.viewportLiquidGlass(vp, look.native()); +} + +/// A float's window's corner radius, points: the OS's for a titled window (`os_frame`). +pub fn windowRadius() f32 { + return dvui.currentWindow().backend.impl.windowCornerRadius(); +} + +/// A held pointer over `vp`'s window is read as over the main window beneath it while `on`: its +/// float gone to its ghost while a view is carried out of it, aimed at the places under it. +pub fn seeThrough(vp: *Viewport, on: bool) void { + dvui.currentWindow().backend.impl.viewportSeeThrough(vp, on); +} + +/// Whether `vp`'s window is maximized — zoomed, or in a fullscreen Space of its own — as the +/// main window's is asked: nothing of the desktop behind it shows. +pub fn maximized(vp: *const Viewport) bool { + return dvui.currentWindow().backend.impl.viewportMaximized(vp); +} + +/// Whether `vp`'s window covers the desktop, or will once the transition it is in ends, as the +/// main window's is asked (`coversDesktop`). +pub fn coversDesktop(vp: *const Viewport) bool { + return dvui.currentWindow().backend.impl.viewportCovers(vp); +} + +/// Whether `vp`'s window is on its way into a fullscreen Space (`enteringSpace`). +pub fn enteringSpace(vp: *const Viewport) bool { + return dvui.currentWindow().backend.impl.viewportEnteringSpace(vp); +} + +/// How far `vp`'s window is into full screen as it moves itself there or back (`spaceFullness`). +pub fn spaceFullness(vp: *const Viewport) ?f32 { + return dvui.currentWindow().backend.impl.viewportSpaceFullness(vp); +} + +/// `vp`'s window `alpha` opaque, all of it — its material too. +pub fn fade(vp: *Viewport, alpha: f32) void { + dvui.currentWindow().backend.impl.viewportFade(vp, alpha); +} + +/// Whether a carried view can be shown past every window (`openCarry`): macOS. +pub const carries = builtin.os.tag == .macos; + +pub fn close(vp: *Viewport) void { + dvui.currentWindow().backend.impl.viewportClose(vp); +} + +/// The part of the frame `vp` shows, physical pixels: in its band, past the main window. +pub fn frameOf(vp: *const Viewport) Rect { + return vp.frame; +} + +/// Put `vp`'s window where it shows `frame`; the part of the frame it then shows. +pub fn place(vp: *Viewport, frame: Rect) Rect { + return dvui.currentWindow().backend.impl.viewportPlace(vp, frame); +} + +/// Hand `vp` this frame's picture of its part of the frame, drawn into `target`, or nothing. +pub fn present(vp: *Viewport, target: ?dvui.TextureTarget) void { + dvui.currentWindow().backend.impl.viewportPresent(vp, target); +} + +/// Where `vp`'s window is now, in the main window's part of the frame (physical pixels from +/// its top left): where its float goes when it comes back. +pub fn inMain(vp: *const Viewport) Rect { + return dvui.currentWindow().backend.impl.viewportInMain(vp); +} + +/// Put `vp`'s window where it shows `frame` of the main window's frame (past its edge, for a +/// float split out under a drag); the part of the frame it then shows. +pub fn placeMain(vp: *Viewport, frame: Rect) Rect { + return dvui.currentWindow().backend.impl.viewportPlaceMain(vp, frame); +} + +/// `frame` of the main window's frame as the same desktop place in `vp`'s band. +pub fn bandFromMain(vp: *const Viewport, frame: Rect) Rect { + return dvui.currentWindow().backend.impl.viewportBandFromMain(vp, frame); +} + +/// Whether `vp`'s window has shown a frame yet. +pub fn shown(vp: *const Viewport) bool { + return dvui.currentWindow().backend.impl.viewportShown(vp); +} + +/// Where a held pointer is read: by the window it is over, or pinned to the main window's +/// frame or a viewport's band while a window is moved or resized. +pub const Pin = SDLBackend.PointerPin; +pub fn pinPointer(pin: Pin) void { + dvui.currentWindow().backend.impl.viewportPinPointer(pin); +} + +/// Whether the OS frames a viewport's window itself — its corners, its shadow, resizing from its +/// edges (Windows: DWM; macOS: a titled window, as the main window is) — so the window is +/// exactly the float's glass. Otherwise it is the glass with a clear +/// margin round it, which the float draws its own shadow in. +pub const os_frame = builtin.os.tag == .windows or builtin.os.tag == .macos; + +/// Whether a viewport's window has the OS's own buttons to close, minimize and zoom it — macOS's +/// traffic lights, a titled window's — so its float draws no close button of its own. +pub const os_buttons = builtin.os.tag == .macos; + +/// A material behind the float's glass in `vp`'s window — its rounded rect `inset` physical +/// pixels in from the window's edge, `radius` its corners — for the float's frost to read the +/// desktop through, in the app's light or dark (`dark`). False where the platform has none +/// (yet). +pub fn glass(vp: *Viewport, inset: f32, radius: f32, dark: bool) bool { + return dvui.currentWindow().backend.impl.viewportGlass(vp, inset, radius, dark); +} + + +/// The OS asked to close `vp`'s window. +pub fn closeRequested(vp: *const Viewport) bool { + return vp.close_requested; +} + +/// Where a press on `vp`'s window is the OS's, from its float this frame (physical pixels of +/// the frame): `drag` its header, less `keep` (its close button), moves the window; `edge` in +/// from `glass`'s sides resizes it. Null: all of it is the app's. +/// `app_side` / `app_corner`: the float's own resize zones, the app's over its header where +/// the OS resizes from no edge (macOS). +pub const Hints = struct { drag: Rect, keep: Rect, glass: Rect, edge: f32, app_side: f32 = 0, app_corner: f32 = 0 }; +pub fn hints(vp: *Viewport, h: ?Hints) void { + dvui.currentWindow().backend.impl.viewportHints(vp, if (h) |x| .{ .drag = x.drag, .keep = x.keep, .glass = x.glass, .edge = x.edge, .app_side = x.app_side, .app_corner = x.app_corner } else null); +} + +/// Where `vp`'s window shows in the frame now, when the OS has moved or resized it since the +/// last ask — where its float goes. +pub fn osPlaced(vp: *Viewport) ?Rect { + return dvui.currentWindow().backend.impl.viewportOsPlaced(vp); +} + +/// The press the OS took to move or resize `vp`'s window was let go; `resized` unless the +/// window was only moved. +pub const MoveEnd = struct { resized: bool }; +pub fn osMoveEnded(vp: *Viewport) ?MoveEnd { + const e = dvui.currentWindow().backend.impl.viewportOsMoveEnded(vp) orelse return null; + return .{ .resized = e.resized }; +} + +/// Hand the drag under way to the OS, which moves `vp`'s window from then on. False where it +/// is not done: the app goes on moving it. +pub fn dragMove(vp: *Viewport) bool { + return dvui.currentWindow().backend.impl.viewportDragMove(vp); +} + +/// What `vp`'s window is called: in the taskbar, the Window menu, the window switcher. +pub fn setTitle(vp: *Viewport, text: []const u8) void { + dvui.currentWindow().backend.impl.viewportTitle(vp, text); +} + +/// The least the OS may resize `vp`'s window to, physical pixels. +pub fn minSize(vp: *Viewport, w: f32, h: f32) void { + dvui.currentWindow().backend.impl.viewportMinSize(vp, w, h); +} diff --git a/src/backend/backend_native.zig b/src/backend/backend_native.zig index 3347f99c6..f849a7bd8 100644 --- a/src/backend/backend_native.zig +++ b/src/backend/backend_native.zig @@ -129,44 +129,8 @@ pub fn saveWindowGeometry(win: *dvui.Window) void { /// Called at the end of AppInit: the monitor may drive frames through window animations now. pub const macosLaunchComplete = platform.macos_monitor.launchComplete; -/// A window's Liquid Glass this frame (`windowGlass`, `viewports.windowGlass`): what -/// `core.glass_look.window` says at the window's opacity, the window's colour, and the clearing -/// bevel `core.glass_look.band` gives its size. -pub const WindowGlassLook = struct { - under_variant: i32, - under_style: i32, - over_variant: i32, - over_style: i32, - frost: f32, - blur: f32, - glass: f32, - fill: dvui.Color, - /// The colour's opacity in the body… - fill_opacity: f32, - /// …and over all of it, the bevel too. - top_fill: f32, - /// Points, each. - radius: f32, - clear: f32, - feather: f32, - - fn native(self: WindowGlassLook) platform.window.WindowGlass { - return .{ - .under_variant = self.under_variant, - .under_style = self.under_style, - .over_variant = self.over_variant, - .over_style = self.over_style, - .frost = self.frost, - .blur = self.blur, - .glass = self.glass, - .fill = .{ @as(f64, @floatFromInt(self.fill.r)) / 255, @as(f64, @floatFromInt(self.fill.g)) / 255, @as(f64, @floatFromInt(self.fill.b)) / 255, self.fill_opacity }, - .top_fill = self.top_fill, - .radius = self.radius, - .clear = self.clear, - .feather = self.feather, - }; - } -}; +/// A window's Liquid Glass this frame (`windowGlass`, `viewports.windowGlass`). +pub const WindowGlassLook = platform.window.WindowGlassLook; /// The main window's Liquid Glass this frame, where it is a window of Liquid Glass (macOS 26): /// whether it is — its colour is then under the glass, and the frame draws no base of its own. @@ -176,418 +140,8 @@ pub fn windowGlass(win: *dvui.Window, look: WindowGlassLook) bool { } /// OS windows besides the main one, each showing a part of the one frame — a float popped out -/// (`plans/POPOUT_WINDOWS_PLAN.md`, the backend's `Viewport`). Fizzy's own backend only: on dvui's -/// SDL3 backend (`-Dnative-backend=sdl3`) there are none, as on the web, and floats stay in. -pub const viewports = struct { - const Impl = @import("backend"); - pub const supported = @hasDecl(Impl, "viewportOpen"); - pub const Viewport = if (supported) Impl.Viewport else struct {}; - /// Physical pixels of the frame. - pub const Rect = if (supported) Impl.viewport_map.Rect else struct { x: f32 = 0, y: f32 = 0, w: f32 = 0, h: f32 = 0 }; - - /// Whether this run can open viewports: not on Wayland, where a window cannot be placed. - pub fn available() bool { - if (comptime !supported) return false; - return Impl.viewportsAvailable(); - } - - /// A viewport over `at` (the frame as the main window shows it), its window opening over that - /// place on the desktop, hidden until a frame is presented into it. Its own part of the frame - /// is `frameOf`. - pub fn open(at: Rect, title: [:0]const u8) ?*Viewport { - if (comptime !supported) return null; - return dvui.currentWindow().backend.impl.viewportOpen(at, title); - } - - /// A window to carry a view past every window of the app's, over the desktop: clear, the pointer - /// passing through it, above every window, placed and drawn as any viewport (`placeMain`, - /// `present`). Where it can be made (`carries`). - pub fn openCarry(at: Rect) ?*Viewport { - if (comptime !supported) return null; - return dvui.currentWindow().backend.impl.viewportOpenCarry(at); - } - - /// Whether one of the app's windows has the keyboard: the app is the active one. - pub fn appActive() bool { - if (comptime !supported) return true; - return Impl.appActive(); - } - - /// Whether menus can be windows of their own (`openMenu`): macOS, and Windows (Acrylic popups), - /// where viewports are. - pub const menus = supported and (builtin.os.tag == .macos or builtin.os.tag == .windows); - - /// A window for a menu (`Popout`'s menus): borderless, above every window, never focused, the - /// OS's material in it — rounded by `radius` points — and the pointer over it read in the main - /// window's frame (`mainOffset`). Placed with `placeRiding`, drawn as any viewport. `ride`: the - /// window the OS moves it with — smoothly, through a drag. `dialog`: a dialog's window, the same - /// but in its window's stacking, not above every window. - pub fn openMenu(at: Rect, radius: f32, ride: Ride, dialog: bool) ?*Viewport { - if (comptime !supported) return null; - return dvui.currentWindow().backend.impl.viewportOpenMenu(at, radius, ride, dialog); - } - - pub const Ride = if (supported) Impl.Ride else union(enum) { none, main, viewport: *Viewport }; - - /// `placeMain` for a window riding on another (`openMenu`'s `ride`), put somewhere new only when - /// `key` — where it lies over that window — changes. - pub fn placeRiding(vp: *Viewport, frame: Rect, key: Rect) Rect { - if (comptime !supported) return frame; - return dvui.currentWindow().backend.impl.viewportPlaceRiding(vp, frame, key); - } - - /// Where menu `vp` is drawn in the frame from where its window lies over the main window, - /// physical pixels: none for a menu of the main window's. - pub fn mainOffset(vp: *Viewport, offset: dvui.Point.Physical) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportMainOffset(vp, .{ .x = offset.x, .y = offset.y }); - } - - /// A piece of the OS's glass in an overlay (`overlayGlass`): a rounded rect, points from the - /// overlay window's top left. - pub const GlassShape = if (supported) Impl.GlassShape else extern struct { x: f64, y: f64, w: f64, h: f64, radius: f64, lit: f64, alpha: f64 }; - - /// Whether the OS has Liquid Glass to draw a view drag's glass with (`openOverlay`): macOS 26. - pub fn liquidGlass() bool { - if (comptime !supported) return false; - return Impl.liquidGlassAvailable(); - } - - /// A window over the display the main window is on (`displayInMain`) holding the OS's glass - /// (`overlayGlass`) under the picture presented into it, the pointer passing through it. Where - /// `liquidGlass`. - pub fn openOverlay(at: Rect) ?*Viewport { - if (comptime !supported) return null; - return dvui.currentWindow().backend.impl.viewportOpenOverlay(at); - } - - /// One of Liquid Glass's materials: its variant and style. - pub const GlassMaterial = struct { variant: i32, style: i32 }; - /// What an overlay's glass is: two layers of the same pieces, `under` and `over`, crossfaded by - /// `over_share` — each layer's pieces alike, for the OS to run them together. - pub const GlassLook = struct { - under: GlassMaterial, - over: GlassMaterial, - over_share: f32, - /// How much of the glass there is. - glass: f32 = 1, - /// The window's colour under the glass, each piece's shape, `fill_opacity` opaque; a lit - /// piece's lit by `lit_amount` toward `lit_toward`. - fill: dvui.Color = .black, - fill_opacity: f32 = 0, - lit_toward: dvui.Color = .white, - lit_amount: f32 = 0, - /// Each piece's clearing bevel (`core.glass_look.band`): a share of its shorter half, at - /// most `bevel_cap` points, clear for `bevel_clear` of it before the frost and the colour - /// come in. - bevel: f32 = 0, - bevel_cap: f32 = 0, - bevel_clear: f32 = 0, - /// Points of plain blur over the lens in each piece's body, in place of the `over` frost, - /// where the OS can blur what is behind a window without its materials' tint (macOS: Core - /// Animation's backdrop blur): 0, or none to draw it with, and the frost it is. - blur: f32 = 0, - }; - - /// Overlay `vp`'s glass this frame (`openOverlay`), run together within `spacing` points, as - /// `look`. - pub fn overlayGlass(vp: *Viewport, shapes: []const GlassShape, spacing: f32, look: GlassLook) void { - if (comptime !supported) return; - const f = look.fill; - const t = look.lit_toward; - dvui.currentWindow().backend.impl.viewportOverlayGlass(vp, shapes, spacing, .{ - .under_variant = look.under.variant, - .under_style = look.under.style, - .over_variant = look.over.variant, - .over_style = look.over.style, - .over_share = look.over_share, - .glass = look.glass, - .fill = .{ @as(f64, @floatFromInt(f.r)) / 255, @as(f64, @floatFromInt(f.g)) / 255, @as(f64, @floatFromInt(f.b)) / 255, look.fill_opacity }, - .lit_toward = .{ @as(f64, @floatFromInt(t.r)) / 255, @as(f64, @floatFromInt(t.g)) / 255, @as(f64, @floatFromInt(t.b)) / 255, look.lit_amount }, - .bevel = look.bevel, - .bevel_cap = look.bevel_cap, - .bevel_clear = look.bevel_clear, - .blur = look.blur, - }); - } - - /// The carried view's picture under an overlay's glass (`overlayPhoto`). - pub const OverlayPhoto = struct { - /// The rounded rect it shows in, points from the overlay window's top left. - rect: Rect, - radius: f32, - /// Where its image lies, points from `rect`'s top left: it may reach past it. - image: Rect, - fill: dvui.Color, - alpha: f32 = 1, - /// Points of plain blur on it. - blur: f32 = 0, - }; - - /// Overlay `vp`'s image of the carried view's picture: premultiplied RGBA rows, `w` by `h`, - /// copied; null, none. Once a drag (`overlayPhoto` places it). - pub fn overlayPhotoImage(vp: *Viewport, rgba: ?[]const u8, w: u32, h: u32) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportOverlayPhotoImage(vp, rgba, w, h); - } - - /// Where overlay `vp` shows the carried view's picture this frame, under its glass, so the - /// glass over it bends it; null, nowhere. Applied with the glass. - pub fn overlayPhoto(vp: *Viewport, photo: ?OverlayPhoto) void { - if (comptime !supported) return; - const p = photo orelse return dvui.currentWindow().backend.impl.viewportOverlayPhoto(vp, null); - const f = p.fill; - dvui.currentWindow().backend.impl.viewportOverlayPhoto(vp, .{ - .x = p.rect.x, - .y = p.rect.y, - .w = p.rect.w, - .h = p.rect.h, - .radius = p.radius, - .image = .{ p.image.x, p.image.y, p.image.w, p.image.h }, - .fill = .{ @as(f64, @floatFromInt(f.r)) / 255, @as(f64, @floatFromInt(f.g)) / 255, @as(f64, @floatFromInt(f.b)) / 255, @as(f64, @floatFromInt(f.a)) / 255 }, - .alpha = p.alpha, - .blur = p.blur, - }); - } - - /// The display the main window is on, in its frame (physical pixels from its top left). - pub fn displayInMain() Rect { - if (comptime !supported) return .{}; - return dvui.currentWindow().backend.impl.viewportDisplayInMain(); - } - - /// A carry window's shape this frame (`openCarry`): what it carries fills it, rounded by - /// `radius` physical pixels — the OS's material and shadow in that shape — `alpha` opaque. - /// Null: hidden. - pub fn carryShape(vp: *Viewport, radius: ?f32, alpha: f32) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportCarryShape(vp, radius, alpha); - } - - /// `vp`'s window at `other`'s level and just above it — a float born of a drop, over the drag's - /// glass while that goes — until `settle` puts it back at its own. macOS. - pub fn lift(vp: *Viewport, other: *Viewport) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportLift(vp, other); - } - - pub fn settle(vp: *Viewport) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportSettle(vp); - } - - /// `vp`'s window put just above `other`'s in the stacking (a growing float's picture over the - /// glass it grows out of). macOS. - pub fn orderAbove(vp: *Viewport, other: *Viewport) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportOrderAbove(vp, other); - } - - /// A carry window's glass as the lens — the carried view, a drop of water — or as frost, as a - /// window's material is (a float's window growing out of it). Liquid Glass only. - pub fn carryLens(vp: *Viewport, lens: bool) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportCarryLens(vp, lens); - } - - /// Whether `vp`'s window lies under the main window in the OS's stacking — a float's, clicked - /// behind it — as last read (macOS; never elsewhere, where floats stay over it). - pub fn underMain(vp: *Viewport) bool { - if (comptime !supported) return false; - return dvui.currentWindow().backend.impl.viewportUnderMain(vp); - } - - /// Points from `vp`'s window's left edge past the OS's own buttons in its title bar (macOS's - /// traffic lights, `os_buttons`): 0 without them. - pub fn buttonsWidth(vp: *Viewport) f32 { - if (comptime !supported) return 0; - return dvui.currentWindow().backend.impl.viewportButtonsWidth(vp); - } - - /// `vp`'s window's Liquid Glass this frame, as the main window's (`windowGlass`). Whether it - /// has any: its float then draws no base of its own. - pub fn windowGlass(vp: *Viewport, look: WindowGlassLook) bool { - if (comptime !supported) return false; - return dvui.currentWindow().backend.impl.viewportLiquidGlass(vp, look.native()); - } - - /// A float's window's corner radius, points: the OS's for a titled window (`os_frame`). - pub fn windowRadius() f32 { - if (comptime !supported) return 0; - return dvui.currentWindow().backend.impl.windowCornerRadius(); - } - - /// A held pointer over `vp`'s window is read as over the main window beneath it while `on`: its - /// float gone to its ghost while a view is carried out of it, aimed at the places under it. - pub fn seeThrough(vp: *Viewport, on: bool) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportSeeThrough(vp, on); - } - - /// Whether `vp`'s window is maximized — zoomed, or in a fullscreen Space of its own — as the - /// main window's is asked: nothing of the desktop behind it shows. - pub fn maximized(vp: *const Viewport) bool { - if (comptime !supported) return false; - return dvui.currentWindow().backend.impl.viewportMaximized(vp); - } - - /// Whether `vp`'s window covers the desktop, or will once the transition it is in ends, as the - /// main window's is asked (`coversDesktop`). - pub fn coversDesktop(vp: *const Viewport) bool { - if (comptime !supported) return false; - return dvui.currentWindow().backend.impl.viewportCovers(vp); - } - - /// Whether `vp`'s window is on its way into a fullscreen Space (`enteringSpace`). - pub fn enteringSpace(vp: *const Viewport) bool { - if (comptime !supported) return false; - return dvui.currentWindow().backend.impl.viewportEnteringSpace(vp); - } - - /// How far `vp`'s window is into full screen as it moves itself there or back (`spaceFullness`). - pub fn spaceFullness(vp: *const Viewport) ?f32 { - if (comptime !supported) return null; - return dvui.currentWindow().backend.impl.viewportSpaceFullness(vp); - } - - /// `vp`'s window `alpha` opaque, all of it — its material too. - pub fn fade(vp: *Viewport, alpha: f32) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportFade(vp, alpha); - } - - /// Whether a carried view can be shown past every window (`openCarry`): macOS. - pub const carries = supported and builtin.os.tag == .macos; - - pub fn close(vp: *Viewport) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportClose(vp); - } - - /// The part of the frame `vp` shows, physical pixels: in its band, past the main window. - pub fn frameOf(vp: *const Viewport) Rect { - if (comptime !supported) return .{}; - return vp.frame; - } - - /// Put `vp`'s window where it shows `frame`; the part of the frame it then shows. - pub fn place(vp: *Viewport, frame: Rect) Rect { - if (comptime !supported) return frame; - return dvui.currentWindow().backend.impl.viewportPlace(vp, frame); - } - - /// Hand `vp` this frame's picture of its part of the frame, drawn into `target`, or nothing. - pub fn present(vp: *Viewport, target: ?dvui.TextureTarget) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportPresent(vp, target); - } - - /// Where `vp`'s window is now, in the main window's part of the frame (physical pixels from - /// its top left): where its float goes when it comes back. - pub fn inMain(vp: *const Viewport) Rect { - if (comptime !supported) return .{}; - return dvui.currentWindow().backend.impl.viewportInMain(vp); - } - - /// Put `vp`'s window where it shows `frame` of the main window's frame (past its edge, for a - /// float split out under a drag); the part of the frame it then shows. - pub fn placeMain(vp: *Viewport, frame: Rect) Rect { - if (comptime !supported) return frame; - return dvui.currentWindow().backend.impl.viewportPlaceMain(vp, frame); - } - - /// `frame` of the main window's frame as the same desktop place in `vp`'s band. - pub fn bandFromMain(vp: *const Viewport, frame: Rect) Rect { - if (comptime !supported) return frame; - return dvui.currentWindow().backend.impl.viewportBandFromMain(vp, frame); - } - - /// Whether `vp`'s window has shown a frame yet. - pub fn shown(vp: *const Viewport) bool { - if (comptime !supported) return false; - return dvui.currentWindow().backend.impl.viewportShown(vp); - } - - /// Where a held pointer is read: by the window it is over, or pinned to the main window's - /// frame or a viewport's band while a window is moved or resized. - pub const Pin = if (supported) Impl.PointerPin else union(enum) { none, main, viewport: *Viewport }; - pub fn pinPointer(pin: Pin) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportPinPointer(pin); - } - - /// Whether the OS frames a viewport's window itself — its corners, its shadow, resizing from its - /// edges (Windows: DWM; macOS: a titled window, as the main window is) — so the window is - /// exactly the float's glass. Otherwise it is the glass with a clear - /// margin round it, which the float draws its own shadow in. - pub const os_frame = supported and (builtin.os.tag == .windows or builtin.os.tag == .macos); - - /// Whether a viewport's window has the OS's own buttons to close, minimize and zoom it — macOS's - /// traffic lights, a titled window's — so its float draws no close button of its own. - pub const os_buttons = supported and builtin.os.tag == .macos; - - /// A material behind the float's glass in `vp`'s window — its rounded rect `inset` physical - /// pixels in from the window's edge, `radius` its corners — for the float's frost to read the - /// desktop through, in the app's light or dark (`dark`). False where the platform has none - /// (yet). - pub fn glass(vp: *Viewport, inset: f32, radius: f32, dark: bool) bool { - if (comptime !supported) return false; - return dvui.currentWindow().backend.impl.viewportGlass(vp, inset, radius, dark); - } - - - /// The OS asked to close `vp`'s window. - pub fn closeRequested(vp: *const Viewport) bool { - if (comptime !supported) return false; - return vp.close_requested; - } - - /// Where a press on `vp`'s window is the OS's, from its float this frame (physical pixels of - /// the frame): `drag` its header, less `keep` (its close button), moves the window; `edge` in - /// from `glass`'s sides resizes it. Null: all of it is the app's. - /// `app_side` / `app_corner`: the float's own resize zones, the app's over its header where - /// the OS resizes from no edge (macOS). - pub const Hints = struct { drag: Rect, keep: Rect, glass: Rect, edge: f32, app_side: f32 = 0, app_corner: f32 = 0 }; - pub fn hints(vp: *Viewport, h: ?Hints) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportHints(vp, if (h) |x| .{ .drag = x.drag, .keep = x.keep, .glass = x.glass, .edge = x.edge, .app_side = x.app_side, .app_corner = x.app_corner } else null); - } - - /// Where `vp`'s window shows in the frame now, when the OS has moved or resized it since the - /// last ask — where its float goes. - pub fn osPlaced(vp: *Viewport) ?Rect { - if (comptime !supported) return null; - return dvui.currentWindow().backend.impl.viewportOsPlaced(vp); - } - - /// The press the OS took to move or resize `vp`'s window was let go; `resized` unless the - /// window was only moved. - pub const MoveEnd = struct { resized: bool }; - pub fn osMoveEnded(vp: *Viewport) ?MoveEnd { - if (comptime !supported) return null; - const e = dvui.currentWindow().backend.impl.viewportOsMoveEnded(vp) orelse return null; - return .{ .resized = e.resized }; - } - - /// Hand the drag under way to the OS, which moves `vp`'s window from then on. False where it - /// is not done: the app goes on moving it. - pub fn dragMove(vp: *Viewport) bool { - if (comptime !supported) return false; - return dvui.currentWindow().backend.impl.viewportDragMove(vp); - } - - /// What `vp`'s window is called: in the taskbar, the Window menu, the window switcher. - pub fn setTitle(vp: *Viewport, text: []const u8) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportTitle(vp, text); - } - - /// The least the OS may resize `vp`'s window to, physical pixels. - pub fn minSize(vp: *Viewport, w: f32, h: f32) void { - if (comptime !supported) return; - dvui.currentWindow().backend.impl.viewportMinSize(vp, w, h); - } -}; +/// (`plans/POPOUT_WINDOWS_PLAN.md`): the backend package's, for any dvui app on it. +pub const viewports = @import("backend").viewports; /// Fizzy keeps the window's geometry in `layout.zon`, beside its regions — one file for where the /// window and everything in it were left, and the file it has always kept the frame in. From 3b9a51c1cc528a7676d680247cec2bbecf0326ca Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 22:20:05 -0500 Subject: [PATCH 2/5] backend: one way to draw a viewport's picture (viewports.Picture) Popout's float, menu, carry and overlay windows each drew their picture with a copy of the same steps; `viewports.Picture` and `sizedTarget` in the backend package are those steps once, for any dvui app on the backend. Co-Authored-By: Claude Opus 5.5 --- backend/src/viewports.zig | 58 ++++++++++++++++++++ src/backend/backend_web.zig | 11 ++++ src/editor/Popout.zig | 102 +++++++----------------------------- 3 files changed, 89 insertions(+), 82 deletions(-) diff --git a/backend/src/viewports.zig b/backend/src/viewports.zig index 2a89da637..12f6b2169 100644 --- a/backend/src/viewports.zig +++ b/backend/src/viewports.zig @@ -284,6 +284,64 @@ pub fn present(vp: *Viewport, target: ?dvui.TextureTarget) void { dvui.currentWindow().backend.impl.viewportPresent(vp, target); } +/// A viewport's picture this frame, drawn as dvui draws the frame: into a target, from the top left +/// of the part of the frame the viewport shows, until `end`. In it go the subwindows the viewport shows (`subwindow`) — a float and +/// what opened in it, a menu, a carried view — and whatever the app draws under them. Begun after +/// `Window.drawRetained`, so the dialogs and toasts are subwindows by then; then `present`. +pub const Picture = struct { + /// The frame's own target, back at `end`. + prev: dvui.RenderTarget, + /// The picture's. + rt: dvui.RenderTarget, + + /// `target` cleared, and drawn into from now as `part` of the frame (physical). + pub fn begin(target: dvui.Texture.Target, part: Rect) Picture { + target.clear(); + var rt = dvui.currentWindow().render_target; + rt.texture = target; + rt.offset = .{ .x = part.x, .y = part.y }; + rt.rendering = true; + return .{ .prev = dvui.renderTarget(rt), .rt = rt }; + } + + /// Drawn into from now as the part of the frame from `at` (physical): the same drawing again, + /// from another part of the frame — a layer drawn across several windows' parts of it. + pub fn moveTo(self: *Picture, at: dvui.Point.Physical) void { + self.rt.offset = at; + _ = dvui.renderTarget(self.rt); + } + + /// Subwindow `sw`'s drawing this frame, in the picture. `take`: out of the frame too, so dvui's + /// replay into the main window — and any picture after this one — draws none of it. + pub fn subwindow(_: Picture, sw: *dvui.Subwindows.Subwindow, take: bool) void { + const cw = dvui.currentWindow(); + const cmds = sw.render_cmds; + const after = sw.render_cmds_after; + if (take) { + sw.render_cmds = .empty; + sw.render_cmds_after = .empty; + } + cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "drawing a subwindow into a viewport", .{}); + cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "drawing a subwindow into a viewport", .{}); + } + + /// Done: drawing goes to the frame's own target again. + pub fn end(self: Picture) void { + _ = dvui.renderTarget(self.prev); + } +}; + +/// `slot`'s target at `w` by `h` pixels, for a `Picture`: the one in it where it is that size, +/// else a new one, the old let go after the frame. Null where none can be made. +pub fn sizedTarget(slot: *?dvui.Texture.Target, w: u32, h: u32) ?dvui.Texture.Target { + if (slot.*) |t| if (t.width != w or t.height != h) { + t.destroyLater(); + slot.* = null; + }; + if (slot.* == null) slot.* = dvui.textureCreateTarget(.{ .width = w, .height = h, .interpolation = .nearest }) catch return null; + return slot.*.?; +} + /// Where `vp`'s window is now, in the main window's part of the frame (physical pixels from /// its top left): where its float goes when it comes back. pub fn inMain(vp: *const Viewport) Rect { diff --git a/src/backend/backend_web.zig b/src/backend/backend_web.zig index d426e819f..a314a323a 100644 --- a/src/backend/backend_web.zig +++ b/src/backend/backend_web.zig @@ -158,6 +158,17 @@ pub const viewports = struct { return frame; } pub fn present(_: *Viewport, _: ?dvui.TextureTarget) void {} + pub const Picture = struct { + pub fn begin(_: dvui.Texture.Target, _: Rect) Picture { + return .{}; + } + pub fn moveTo(_: *Picture, _: dvui.Point.Physical) void {} + pub fn subwindow(_: Picture, _: *dvui.Subwindows.Subwindow, _: bool) void {} + pub fn end(_: Picture) void {} + }; + pub fn sizedTarget(_: *?dvui.Texture.Target, _: u32, _: u32) ?dvui.Texture.Target { + return null; + } pub fn inMain(_: *const Viewport) Rect { return .{}; } diff --git a/src/editor/Popout.zig b/src/editor/Popout.zig index e7c55c270..53e60c231 100644 --- a/src/editor/Popout.zig +++ b/src/editor/Popout.zig @@ -419,13 +419,7 @@ fn carryFrame(state: *State) void { // The carried view's own layer (`core.screens.markCarried`), taken from it: dvui's replay into the main // window, and the float windows' (`windowFrame`), draw nothing of it. for (cw.subwindows.stack.items) |*sw| { - if (!fizzy.core.screens.isCarried(sw.id)) continue; - const cmds = sw.render_cmds; - const after = sw.render_cmds_after; - sw.render_cmds = .empty; - sw.render_cmds_after = .empty; - cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "replaying a carried view into its window", .{}); - cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "replaying a carried view into its window", .{}); + if (fizzy.core.screens.isCarried(sw.id)) drawing.picture.subwindow(sw, true); } } @@ -543,18 +537,8 @@ fn overlayFrame(state: *State) void { // them lands in the overlay, the rest outside it. const w: u32 = @intFromFloat(@max(1, @round(shown.w))); const h: u32 = @intFromFloat(@max(1, @round(shown.h))); - if (o.target) |t| if (t.width != w or t.height != h) { - t.destroyLater(); - o.target = null; - }; - if (o.target == null) o.target = dvui.textureCreateTarget(.{ .width = w, .height = h, .interpolation = .nearest }) catch return; - const target = o.target.?; - target.clear(); - var rt = cw.render_target; - rt.texture = target; - rt.offset = .{ .x = shown.x, .y = shown.y }; - rt.rendering = true; - const prev = dvui.renderTarget(rt); + const target = viewports.sizedTarget(&o.target, w, h) orelse return; + var picture: viewports.Picture = .begin(target, placed); glassBase(placed_shapes[0..n], shown, s, spacing, look.top_fill); var offsets: [max_out + 1]dvui.Point.Physical = undefined; offsets[0] = .{ .x = shown.x, .y = shown.y }; @@ -565,18 +549,12 @@ fn overlayFrame(state: *State) void { } for (cw.subwindows.stack.items) |*sw| { if (!fizzy.core.screens.isCarried(sw.id)) continue; - const cmds = sw.render_cmds; - const after = sw.render_cmds_after; - sw.render_cmds = .empty; - sw.render_cmds_after = .empty; - for (offsets[0..n_off]) |off| { - rt.offset = off; - _ = dvui.renderTarget(rt); - cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "replaying over the overlay's glass", .{}); - cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "replaying over the overlay's glass", .{}); + for (offsets[0..n_off], 0..) |off, i| { + picture.moveTo(off); + picture.subwindow(sw, i == n_off - 1); } } - _ = dvui.renderTarget(prev); + picture.end(); viewports.present(o.viewport, target); } @@ -682,10 +660,9 @@ fn inMainFrame(r: dvui.Rect.Physical, main_px: dvui.Rect.Physical) ?dvui.Rect.Ph return r.offsetPoint(.{ .x = cv.in_main.x - cv.band.x, .y = cv.in_main.y - cv.band.y }); } -/// A carry window's picture under way (`carryBegin`): the frame's own target to go back to, and -/// the part of the frame the window shows. +/// A carry window's picture under way (`carryBegin`), and the part of the frame the window shows. const CarryDrawing = struct { - prev: dvui.RenderTarget, + picture: viewports.Picture, shown: dvui.Rect.Physical, }; @@ -702,18 +679,8 @@ fn carryBegin(c: *Carry, place: dvui.Rect.Physical, shape: dvui.Rect.Physical, r viewports.carryShape(c.viewport, radius, alpha); const w: u32 = @intFromFloat(@max(1, @round(shown.w))); const h: u32 = @intFromFloat(@max(1, @round(shown.h))); - if (c.target) |t| if (t.width != w or t.height != h) { - t.destroyLater(); - c.target = null; - }; - if (c.target == null) c.target = dvui.textureCreateTarget(.{ .width = w, .height = h, .interpolation = .nearest }) catch return null; - const target = c.target.?; - target.clear(); - var rt = cw.render_target; - rt.texture = target; - rt.offset = .{ .x = shown.x, .y = shown.y }; - rt.rendering = true; - const prev = dvui.renderTarget(rt); + const target = viewports.sizedTarget(&c.target, w, h) orelse return null; + const picture: viewports.Picture = .begin(target, .{ .x = shown.x, .y = shown.y, .w = shown.w, .h = shown.h }); { const prev_clip = dvui.clipGet(); defer dvui.clipSet(prev_clip); @@ -727,12 +694,12 @@ fn carryBegin(c: *Carry, place: dvui.Rect.Physical, shape: dvui.Rect.Physical, r shape.fill(dvui.CornerRect.Physical.all(radius), .{ .color = .{ .color = color } }); } } - return .{ .prev = prev, .shown = shown }; + return .{ .picture = picture, .shown = shown }; } /// `carryBegin`'s picture done: handed to its window. fn carryEnd(c: *Carry, drawing: CarryDrawing) void { - _ = dvui.renderTarget(drawing.prev); + drawing.picture.end(); if (c.target) |t| viewports.present(c.viewport, t); } @@ -1112,13 +1079,8 @@ fn windowFrame(state: *State, o: *Out) void { const target = keepTarget(o, w, h) orelse return; // Transparent where the float is not: past its corners. - target.clear(); - var rt = cw.render_target; - rt.texture = target; - rt.offset = .{ .x = shown.x, .y = shown.y }; - rt.rendering = true; - const prev = dvui.renderTarget(rt); - defer _ = dvui.renderTarget(prev); + const picture: viewports.Picture = .begin(target, shown); + defer picture.end(); // Zoomed or full screen there is no desktop behind it: opaque, eased there and back with the // OS's transitions as the main window's base is. Where it has no material it is opaque // throughout. @@ -1140,15 +1102,7 @@ fn windowFrame(state: *State, o: *Out) void { if (fizzy.core.screens.isCarried(sw.id)) continue; if (fizzy.core.screens.isMenu(sw.id) and nativeMenus()) continue; const mine = area.contains(sw.rect_pixels.center()); - if (!mine and !fizzy.core.screens.isEverywhere(sw.id)) continue; - const cmds = sw.render_cmds; - const after = sw.render_cmds_after; - if (mine) { - sw.render_cmds = .empty; - sw.render_cmds_after = .empty; - } - cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "replaying a float into its window", .{}); - cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "replaying a float into its window", .{}); + if (mine or fizzy.core.screens.isEverywhere(sw.id)) picture.subwindow(sw, mine); } viewports.present(o.viewport, target); } @@ -1318,19 +1272,8 @@ fn menuFrame() void { m.glass = viewports.windowGlass(m.viewport, look); const w: u32 = @intFromFloat(@max(1, @round(shown.w))); const h: u32 = @intFromFloat(@max(1, @round(shown.h))); - if (m.target) |t| if (t.width != w or t.height != h) { - t.destroyLater(); - m.target = null; - }; - if (m.target == null) m.target = dvui.textureCreateTarget(.{ .width = w, .height = h, .interpolation = .nearest }) catch continue; - const target = m.target.?; - target.clear(); - var rt = cw.render_target; - rt.texture = target; - rt.offset = .{ .x = shown.x, .y = shown.y }; - rt.rendering = true; - const prev = dvui.renderTarget(rt); - defer _ = dvui.renderTarget(prev); + const target = viewports.sizedTarget(&m.target, w, h) orelse continue; + const picture: viewports.Picture = .begin(target, .{ .x = shown.x, .y = shown.y, .w = shown.w, .h = shown.h }); if (!m.glass) { const prev_clip = dvui.clipGet(); defer dvui.clipSet(prev_clip); @@ -1340,13 +1283,8 @@ fn menuFrame() void { defer dvui.alphaSet(prev_alpha); frame.fill(dvui.CornerRect.Physical.all(corner * s), .{ .color = .{ .color = Editor.windowBase(op) } }); } - const cmds = sw.render_cmds; - const after = sw.render_cmds_after; - sw.render_cmds = .empty; - sw.render_cmds_after = .empty; - cw.renderCommands(cmds.items) catch |err| dvui.logError(@src(), err, "replaying a menu into its window", .{}); - cw.renderCommands(after.items) catch |err| dvui.logError(@src(), err, "replaying a menu into its window", .{}); - _ = dvui.renderTarget(prev); + picture.subwindow(sw, true); + picture.end(); viewports.present(m.viewport, target); } } From 140780d82116196eb5c363bcfe53f5f536613b50 Mon Sep 17 00:00:00 2001 From: foxnne Date: Sat, 10 Oct 2026 06:50:03 -0500 Subject: [PATCH 3/5] backend: a stand-in viewports for backends without OS windows `viewports_none` (backend/src/viewports_none.zig) has every name `viewports` has, each saying there are no OS windows: the web's stand-in moved into the package, for fizzy on the web, on dvui's testing backend and on dvui's own SDL3 backend, and for any dvui app built on either kind. Co-Authored-By: Claude Opus 5.5 --- backend/build.zig | 10 ++ backend/src/viewports_none.zig | 161 +++++++++++++++++++++++++++++++++ build/app.zig | 2 + build/exe.zig | 2 + build/web.zig | 2 + src/backend/backend_native.zig | 5 +- src/backend/backend_web.zig | 149 +----------------------------- 7 files changed, 182 insertions(+), 149 deletions(-) create mode 100644 backend/src/viewports_none.zig diff --git a/backend/build.zig b/backend/build.zig index c0401a865..2cac7edd0 100644 --- a/backend/build.zig +++ b/backend/build.zig @@ -156,6 +156,16 @@ pub fn objcModule(dep: *std.Build.Dependency, opts: Options) ?*std.Build.Module return objc.module("objc"); } +/// `viewports` for a backend without OS windows besides the main one (`src/viewports_none.zig`): +/// dvui's own, its testing backend, the web. An app that builds on either kind imports it as +/// `viewports_none` and takes the backend's own `viewports` where it has one. Needs only dvui, +/// so nothing of SDL is fetched for it. +pub fn viewportsNoneModule(dep: *std.Build.Dependency, dvui_mod: *std.Build.Module) *std.Build.Module { + const m = dep.builder.createModule(.{ .root_source_file = dep.builder.path("src/viewports_none.zig") }); + m.addImport("dvui", dvui_mod); + return m; +} + /// zigwin32, from this package's pin, for an app's own Windows code. Null on the configure pass /// that fetches it. pub fn win32Module(dep: *std.Build.Dependency) ?*std.Build.Module { diff --git a/backend/src/viewports_none.zig b/backend/src/viewports_none.zig new file mode 100644 index 000000000..e7dc2061f --- /dev/null +++ b/backend/src/viewports_none.zig @@ -0,0 +1,161 @@ +//! `viewports` for a backend without OS windows besides the main one — dvui's own SDL3 backend, +//! its testing backend, the web (one canvas): the same names as the backend package's own +//! (`viewports.zig`), every call saying there are none. `available` is false and `open` gives +//! null, so an app's floats, menus and dialogs stay in the main window. +//! +//! An app that builds on either kind of backend imports this beside it and takes the backend's +//! own where it has one: +//! +//! const viewports = if (@hasDecl(backend, "viewports")) backend.viewports else @import("viewports_none"); +const dvui = @import("dvui"); + +/// A window's Liquid Glass this frame, as the backend package's (`platform.window.WindowGlassLook`): +/// there is none here, but an app still says what it would be. +pub const WindowGlassLook = struct { + under_variant: i32, + under_style: i32, + over_variant: i32, + over_style: i32, + frost: f32, + blur: f32, + glass: f32, + fill: dvui.Color, + fill_opacity: f32, + top_fill: f32, + radius: f32, + clear: f32, + feather: f32, +}; + +pub const supported = false; +pub fn available() bool { + return false; +} +pub const Viewport = struct {}; +pub const Rect = struct { x: f32 = 0, y: f32 = 0, w: f32 = 0, h: f32 = 0 }; + +pub fn open(_: Rect, _: [:0]const u8) ?*Viewport { + return null; +} +pub fn close(_: *Viewport) void {} +pub fn frameOf(_: *const Viewport) Rect { + return .{}; +} +pub fn place(_: *Viewport, frame: Rect) Rect { + return frame; +} +pub fn present(_: *Viewport, _: ?dvui.TextureTarget) void {} +pub fn inMain(_: *const Viewport) Rect { + return .{}; +} +pub const os_frame = false; +pub const os_buttons = false; +pub const carries = false; +pub fn seeThrough(_: *Viewport, _: bool) void {} +pub fn fade(_: *Viewport, _: f32) void {} +pub fn maximized(_: *const Viewport) bool { + return false; +} +pub fn coversDesktop(_: *const Viewport) bool { + return false; +} +pub fn enteringSpace(_: *const Viewport) bool { + return false; +} +pub fn spaceFullness(_: *const Viewport) ?f32 { + return null; +} +pub fn carryShape(_: *Viewport, _: ?f32, _: f32) void {} +pub fn carryLens(_: *Viewport, _: bool) void {} +/// Any look, `WindowGlassLook` or the backend package's (an app on dvui's own SDL3 backend still +/// dresses its main window with `platform`'s): there is no viewport to give it to. +pub fn windowGlass(_: *Viewport, _: anytype) bool { + return false; +} +pub fn underMain(_: *Viewport) bool { + return false; +} +pub fn buttonsWidth(_: *Viewport) f32 { + return 0; +} +pub fn windowRadius() f32 { + return 0; +} +pub fn openCarry(_: Rect) ?*Viewport { + return null; +} +pub const menus = false; +pub fn appActive() bool { + return true; +} +pub const Ride = union(enum) { none, main, viewport: *Viewport }; +pub fn orderAbove(_: *Viewport, _: *Viewport) void {} +pub fn lift(_: *Viewport, _: *Viewport) void {} +pub fn settle(_: *Viewport) void {} +pub fn placeRiding(_: *Viewport, frame: Rect, _: Rect) Rect { + return frame; +} +pub fn openMenu(_: Rect, _: f32, _: Ride, _: bool) ?*Viewport { + return null; +} +pub fn mainOffset(_: *Viewport, _: dvui.Point.Physical) void {} +pub const GlassShape = extern struct { x: f64, y: f64, w: f64, h: f64, radius: f64, lit: f64, alpha: f64, frost: f64 }; +pub fn liquidGlass() bool { + return false; +} +pub fn openOverlay(_: Rect) ?*Viewport { + return null; +} +pub const GlassMaterial = struct { variant: i32, style: i32 }; +pub const GlassLook = struct { + under: GlassMaterial, + over: GlassMaterial, + over_share: f32, + glass: f32 = 1, + fill: dvui.Color = .black, + fill_opacity: f32 = 0, + lit_toward: dvui.Color = .white, + lit_amount: f32 = 0, + bevel: f32 = 0, + bevel_cap: f32 = 0, + bevel_clear: f32 = 0, + blur: f32 = 0, +}; +pub fn overlayGlass(_: *Viewport, _: []const GlassShape, _: f32, _: GlassLook) void {} +pub const OverlayPhoto = struct { rect: Rect, radius: f32, image: Rect, fill: dvui.Color, alpha: f32 = 1, blur: f32 = 0 }; +pub fn overlayPhotoImage(_: *Viewport, _: ?[]const u8, _: u32, _: u32) void {} +pub fn overlayPhoto(_: *Viewport, _: ?OverlayPhoto) void {} +pub fn displayInMain() Rect { + return .{}; +} +pub const Hints = struct { drag: Rect, keep: Rect, glass: Rect, edge: f32, app_side: f32 = 0, app_corner: f32 = 0 }; +pub fn hints(_: *Viewport, _: ?Hints) void {} +pub fn osPlaced(_: *Viewport) ?Rect { + return null; +} +pub const MoveEnd = struct { resized: bool }; +pub fn osMoveEnded(_: *Viewport) ?MoveEnd { + return null; +} +pub fn dragMove(_: *Viewport) bool { + return false; +} +pub fn minSize(_: *Viewport, _: f32, _: f32) void {} +pub fn setTitle(_: *Viewport, _: []const u8) void {} +pub fn glass(_: *Viewport, _: f32, _: f32, _: bool) bool { + return false; +} +pub fn placeMain(_: *Viewport, frame: Rect) Rect { + return frame; +} +pub fn bandFromMain(_: *const Viewport, frame: Rect) Rect { + return frame; +} +pub fn shown(_: *const Viewport) bool { + return false; +} +pub const Pin = union(enum) { none, main, viewport: *Viewport }; +pub fn pinPointer(_: Pin) void {} +pub fn closeRequested(_: *const Viewport) bool { + return false; +} diff --git a/build/app.zig b/build/app.zig index 0a45cab63..a955a8b13 100644 --- a/build/app.zig +++ b/build/app.zig @@ -806,6 +806,8 @@ pub fn construct(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std. }); fizzy_test_module.addImport("dvui", dvui_testing_dep.module("dvui_testing")); fizzy_test_module.addImport("backend", dvui_testing_dep.module("testing")); + // The testing backend has no viewports (`backend_web.viewports`). + fizzy_test_module.addImport("viewports_none", @import("fizzy_backend").viewportsNoneModule(fizzy_exe.backendDependency(b), dvui_testing_dep.module("dvui_testing"))); fizzy_test_module.addImport("assets", assets_module); fizzy_test_module.addImport("build_opts", sdk.buildOptsModule(build_opts)); diff --git a/build/exe.zig b/build/exe.zig index df90cf5ae..28fed66e8 100644 --- a/build/exe.zig +++ b/build/exe.zig @@ -240,6 +240,8 @@ pub fn addFizzyExecutableForTarget( exe.root_module.addImport("dvui", dvui_mod); exe.root_module.addImport("backend", native.backend); exe.root_module.addImport("platform", platformModule(b, native, resolved_target, optimize, macos_sdl_paths)); + // 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)); // 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. diff --git a/build/web.zig b/build/web.zig index 51d269659..1a2303d15 100644 --- a/build/web.zig +++ b/build/web.zig @@ -72,6 +72,8 @@ pub fn addSteps( }); web_exe.entry = .disabled; web_exe.root_module.addImport("dvui", dvui_web); + // A page has one canvas: `backend_web.viewports`. + web_exe.root_module.addImport("viewports_none", @import("fizzy_backend").viewportsNoneModule(@import("exe.zig").backendDependency(b), dvui_web)); web_exe.root_module.addImport("web-backend", web_backend); // Extra wasm exports beyond dvui's own (`dvui_init`/`dvui_update`/etc.). The wasm diff --git a/src/backend/backend_native.zig b/src/backend/backend_native.zig index f849a7bd8..a4af6dd4a 100644 --- a/src/backend/backend_native.zig +++ b/src/backend/backend_native.zig @@ -140,8 +140,9 @@ pub fn windowGlass(win: *dvui.Window, look: WindowGlassLook) bool { } /// OS windows besides the main one, each showing a part of the one frame — a float popped out -/// (`plans/POPOUT_WINDOWS_PLAN.md`): the backend package's, for any dvui app on it. -pub const viewports = @import("backend").viewports; +/// (`plans/POPOUT_WINDOWS_PLAN.md`): the backend package's, for any dvui app on it. On dvui's own +/// SDL3 backend (`-Dnative-backend=sdl3`) there are none, as on the web, and floats stay in. +pub const viewports = if (@hasDecl(@import("backend"), "viewports")) @import("backend").viewports else @import("viewports_none"); /// Fizzy keeps the window's geometry in `layout.zon`, beside its regions — one file for where the /// window and everything in it were left, and the file it has always kept the frame in. diff --git a/src/backend/backend_web.zig b/src/backend/backend_web.zig index d426e819f..69a5a2cbc 100644 --- a/src/backend/backend_web.zig +++ b/src/backend/backend_web.zig @@ -139,154 +139,9 @@ pub const loadTree = layout_file.loadTree; pub fn macosLaunchComplete() void {} /// Symmetric with the native API: a page has one canvas, so a float never leaves it. -pub const viewports = struct { - pub const supported = false; - pub fn available() bool { - return false; - } - pub const Viewport = struct {}; - pub const Rect = struct { x: f32 = 0, y: f32 = 0, w: f32 = 0, h: f32 = 0 }; +pub const viewports = @import("viewports_none"); - pub fn open(_: Rect, _: [:0]const u8) ?*Viewport { - return null; - } - pub fn close(_: *Viewport) void {} - pub fn frameOf(_: *const Viewport) Rect { - return .{}; - } - pub fn place(_: *Viewport, frame: Rect) Rect { - return frame; - } - pub fn present(_: *Viewport, _: ?dvui.TextureTarget) void {} - pub fn inMain(_: *const Viewport) Rect { - return .{}; - } - pub const os_frame = false; - pub const os_buttons = false; - pub const carries = false; - pub fn seeThrough(_: *Viewport, _: bool) void {} - pub fn fade(_: *Viewport, _: f32) void {} - pub fn maximized(_: *const Viewport) bool { - return false; - } - pub fn coversDesktop(_: *const Viewport) bool { - return false; - } - pub fn enteringSpace(_: *const Viewport) bool { - return false; - } - pub fn spaceFullness(_: *const Viewport) ?f32 { - return null; - } - pub fn carryShape(_: *Viewport, _: ?f32, _: f32) void {} - pub fn carryLens(_: *Viewport, _: bool) void {} - pub fn windowGlass(_: *Viewport, _: WindowGlassLook) bool { - return false; - } - pub fn underMain(_: *Viewport) bool { - return false; - } - pub fn buttonsWidth(_: *Viewport) f32 { - return 0; - } - pub fn windowRadius() f32 { - return 0; - } - pub fn openCarry(_: Rect) ?*Viewport { - return null; - } - pub const menus = false; - pub fn appActive() bool { - return true; - } - pub const Ride = union(enum) { none, main, viewport: *Viewport }; - pub fn orderAbove(_: *Viewport, _: *Viewport) void {} - pub fn lift(_: *Viewport, _: *Viewport) void {} - pub fn settle(_: *Viewport) void {} - pub fn placeRiding(_: *Viewport, frame: Rect, _: Rect) Rect { - return frame; - } - pub fn openMenu(_: Rect, _: f32, _: Ride, _: bool) ?*Viewport { - return null; - } - pub fn mainOffset(_: *Viewport, _: dvui.Point.Physical) void {} - pub const GlassShape = extern struct { x: f64, y: f64, w: f64, h: f64, radius: f64, lit: f64, alpha: f64, frost: f64 }; - pub fn liquidGlass() bool { - return false; - } - pub fn openOverlay(_: Rect) ?*Viewport { - return null; - } - pub const GlassMaterial = struct { variant: i32, style: i32 }; - pub const GlassLook = struct { - under: GlassMaterial, - over: GlassMaterial, - over_share: f32, - glass: f32 = 1, - fill: dvui.Color = .black, - fill_opacity: f32 = 0, - lit_toward: dvui.Color = .white, - lit_amount: f32 = 0, - bevel: f32 = 0, - bevel_cap: f32 = 0, - bevel_clear: f32 = 0, - blur: f32 = 0, - }; - pub fn overlayGlass(_: *Viewport, _: []const GlassShape, _: f32, _: GlassLook) void {} - pub const OverlayPhoto = struct { rect: Rect, radius: f32, image: Rect, fill: dvui.Color, alpha: f32 = 1, blur: f32 = 0 }; - pub fn overlayPhotoImage(_: *Viewport, _: ?[]const u8, _: u32, _: u32) void {} - pub fn overlayPhoto(_: *Viewport, _: ?OverlayPhoto) void {} - pub fn displayInMain() Rect { - return .{}; - } - pub const Hints = struct { drag: Rect, keep: Rect, glass: Rect, edge: f32, app_side: f32 = 0, app_corner: f32 = 0 }; - pub fn hints(_: *Viewport, _: ?Hints) void {} - pub fn osPlaced(_: *Viewport) ?Rect { - return null; - } - pub const MoveEnd = struct { resized: bool }; - pub fn osMoveEnded(_: *Viewport) ?MoveEnd { - return null; - } - pub fn dragMove(_: *Viewport) bool { - return false; - } - pub fn minSize(_: *Viewport, _: f32, _: f32) void {} - pub fn setTitle(_: *Viewport, _: []const u8) void {} - pub fn glass(_: *Viewport, _: f32, _: f32, _: bool) bool { - return false; - } - pub fn placeMain(_: *Viewport, frame: Rect) Rect { - return frame; - } - pub fn bandFromMain(_: *const Viewport, frame: Rect) Rect { - return frame; - } - pub fn shown(_: *const Viewport) bool { - return false; - } - pub const Pin = union(enum) { none, main, viewport: *Viewport }; - pub fn pinPointer(_: Pin) void {} - pub fn closeRequested(_: *const Viewport) bool { - return false; - } -}; - -pub const WindowGlassLook = struct { - under_variant: i32, - under_style: i32, - over_variant: i32, - over_style: i32, - frost: f32, - blur: f32, - glass: f32, - fill: dvui.Color, - fill_opacity: f32, - top_fill: f32, - radius: f32, - clear: f32, - feather: f32, -}; +pub const WindowGlassLook = viewports.WindowGlassLook; pub fn windowGlass(_: *dvui.Window, _: WindowGlassLook) bool { return false; } From ef1ff786bd82862310caa8c766fb1c39bd94c463 Mon Sep 17 00:00:00 2001 From: Colton Franklin <49629865+foxnne@users.noreply.github.com> Date: Sat, 10 Oct 2026 08:04:33 -0500 Subject: [PATCH 4/5] build: a dvui app of its own on fizzy's backend (fizzy.addDvui) (#329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #281: what example-app's `run-dvui` builds on. Stacked on #326 (on #324); review those first. The example-app side is fizzyedit/example-app#4. **`fizzy.addDvui(fizzy_dep, root_module, backend)`** gives an outside app's module dvui on one of two backends: - `.fizzy`: fizzy's own, where each floating window can be an OS window of its own; - `.sdl3`: dvui's own SDL3 backend, which has the main window only. The module gets `dvui` (fizzy's pin), its `backend`, and `viewports_none` (#324), so the same app code builds on either: ```zig const backend = @import("backend"); const viewports = if (@hasDecl(backend, "viewports")) backend.viewports else @import("viewports_none"); ``` On fizzy's backend it also gets `platform`, and on macOS the backend's Objective-C compiled into it. `build/exe.zig`'s backend switch is now `nativeDvui`, shared by fizzy's own executable and `addDvui`. ## Two fixes a plain dvui app needed 1. **The main window's drawable was acquired with a copy pass still open** (`GpuRenderer.acquireSwapchain`). The frame's texture uploads had left it open, and SDL asserted on every frame ("Cannot acquire a swapchain texture during a pass"). Fizzy never hit this because it draws its frame into a target first. Any dvui app draws straight into the window. The fix ends the copy pass before acquiring, as `presentInto` and `clearPass` already do. 2. **`platform`'s menu-bar exports weren't emitted** (`FizzyMenu*`). `macos/menu_target.m` calls them and is compiled into every macOS app on the backend, but Zig emits an `export fn` only when analysis reaches its file. Fizzy reaches `platform/menu.zig` through its menu bar; a dvui app with none failed to link. Now `platform/root.zig` references them, and they're `pub`. ## Verification - **Fizzy:** `zig build`, `test` (514/515, 1 skipped), `test-integration` (358/358), `check-web`, `test-sdk-version`, the backend's tests (46/46), and the Windows and Linux cross-builds pass on macOS. The soak tape ends in a passing verdict (1,429 viewport presents, no leaks, nothing logged as an error). - **example-app's `run-dvui`, built against this branch, on macOS:** - **On fizzy's backend:** it opens its main window and two floating windows. Each is an OS window of its own, opened over its place in the main window (window list: main at 540,332; floats at 652,476 and 684,500, each 280×212). No assertions or warnings, and it quits cleanly on SIGTERM. - **With `-Ddvui-backend=sdl3`:** the same app shows only its main window, with the floating windows in it. No warnings. - **Not seen:** I couldn't screenshot, since this session has no screen-recording permission. Window positions and the logs are what I checked, not how they look. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- backend/src/GpuRenderer.zig | 4 +++ backend/src/platform/menu.zig | 10 +++---- backend/src/platform/root.zig | 13 +++++++++ build.zig | 32 +++++++++++++++++++++ build/exe.zig | 53 ++++++++++++++++++++++------------- 5 files changed, 87 insertions(+), 25 deletions(-) diff --git a/backend/src/GpuRenderer.zig b/backend/src/GpuRenderer.zig index cb11db39c..21aff8296 100644 --- a/backend/src/GpuRenderer.zig +++ b/backend/src/GpuRenderer.zig @@ -559,6 +559,10 @@ fn acquireSwapchain(self: *GpuRenderer) !bool { return false; } const cmd = try self.ensureCmd(); + // No pass may be open while a drawable is acquired, the uploads' copy pass included. An app + // drawing its frame straight into the window (any dvui app; fizzy draws into a target first) + // had one open from the frame's uploads, and SDL asserted on every frame. + self.endCopy(); var tex: ?*c.SDL_GPUTexture = null; var w: u32 = 0; var h: u32 = 0; diff --git a/backend/src/platform/menu.zig b/backend/src/platform/menu.zig index b8cc34ce9..56485ae67 100644 --- a/backend/src/platform/menu.zig +++ b/backend/src/platform/menu.zig @@ -105,31 +105,31 @@ fn selector(name: [*:0]const u8) ?*anyopaque { return FizzyGetSelector(name); } -export fn FizzyMenuActivated(section: c_int, tag: c_int, from_key: bool) void { +pub export fn FizzyMenuActivated(section: c_int, tag: c_int, from_key: bool) void { if (section < 0 or section > 2 or tag < 0) return; if (section == 0) pending_from_key.store(from_key, .release); pending_tag[@intCast(section)].store(tag, .release); } -export fn FizzyMenuEnabled(section: c_int, tag: c_int) callconv(.c) bool { +pub export fn FizzyMenuEnabled(section: c_int, tag: c_int) callconv(.c) bool { const h = hooks orelse return true; if (h.input_blocked(h.ctx)) return false; if (section < 0 or section > 2 or tag < 0) return true; return h.enabled(h.ctx, .{ .section = @enumFromInt(section), .tag = @intCast(tag) }); } -export fn FizzyMenuTitle(section: c_int, tag: c_int) callconv(.c) ?[*:0]const u8 { +pub export fn FizzyMenuTitle(section: c_int, tag: c_int) callconv(.c) ?[*:0]const u8 { const h = hooks orelse return null; if (section < 0 or section > 2 or tag < 0) return null; return h.title(h.ctx, .{ .section = @enumFromInt(section), .tag = @intCast(tag) }); } -export fn FizzyMenuInputBlocked() callconv(.c) bool { +pub export fn FizzyMenuInputBlocked() callconv(.c) bool { const h = hooks orelse return false; return h.input_blocked(h.ctx); } -export fn FizzyMenuAbout() callconv(.c) void { +pub export fn FizzyMenuAbout() callconv(.c) void { pending_about.store(true, .release); } diff --git a/backend/src/platform/root.zig b/backend/src/platform/root.zig index b456a1709..8fadd88fd 100644 --- a/backend/src/platform/root.zig +++ b/backend/src/platform/root.zig @@ -16,3 +16,16 @@ pub const geometry = @import("geometry.zig"); pub const macos_monitor = @import("macos_monitor.zig"); pub const window_layout = @import("window_layout.zig"); pub const menu = @import("menu.zig"); + +comptime { + // The menu bar's Objective-C (`macos/menu_target.m`) calls these back, and it is compiled into + // every app on macOS (`addPlatformObjC`), so they are kept whether or not the app ever reaches + // `menu` — a plain dvui app with no menu bar of its own failed to link without them. + if (@import("builtin").os.tag == .macos) { + _ = &menu.FizzyMenuActivated; + _ = &menu.FizzyMenuEnabled; + _ = &menu.FizzyMenuTitle; + _ = &menu.FizzyMenuInputBlocked; + _ = &menu.FizzyMenuAbout; + } +} diff --git a/build.zig b/build.zig index b4c140e69..1f072f17d 100644 --- a/build.zig +++ b/build.zig @@ -89,3 +89,35 @@ pub fn buildApp(fizzy_dep: *std.Build.Dependency, plugins: []const BundledPlugin opts.app_plugins = plugins; try @import("build/app.zig").construct(fizzy_dep.builder, d.target, d.optimize, opts, d.cfg); } + +/// What a dvui app of its own builds on (`addDvui`): fizzy's backend, with OS windows besides the +/// main one, or dvui's own SDL3 backend, with the main window only. +pub const NativeBackend = @import("build/exe.zig").NativeBackend; + +/// A plain dvui app — dvui's widgets and `dvui.App`, nothing of fizzy the editor — on `backend`: +/// `root_module` gets `dvui` (fizzy's pin), its `backend`, and `viewports_none`, so the same code +/// builds on either backend: +/// +/// ```zig +/// const backend = @import("backend"); +/// const viewports = if (@hasDecl(backend, "viewports")) backend.viewports else @import("viewports_none"); +/// ``` +/// +/// On fizzy's backend it also gets `platform` (the window's chrome and state), and on macOS the +/// backend's Objective-C, compiled into it. Give `root_module` its target and optimize mode first. +pub fn addDvui(fizzy_dep: *std.Build.Dependency, root_module: *std.Build.Module, backend: NativeBackend) !void { + const b = fizzy_dep.builder; + const exe = @import("build/exe.zig"); + const target = root_module.resolved_target orelse @panic("fizzy.addDvui: give the root module its target first"); + const optimize = root_module.optimize orelse .Debug; + const macos_sdl_paths = try @import("build/common.zig").macosSdlPathsForExplicitTarget(b, target); + const native = exe.nativeDvui(b, backend, target, optimize, .off, macos_sdl_paths); + root_module.addImport("dvui", native.dvui); + root_module.addImport("backend", native.backend); + const backend_dep = exe.backendDependency(b); + root_module.addImport("viewports_none", @import("fizzy_backend").viewportsNoneModule(backend_dep, native.dvui)); + if (backend == .fizzy) { + root_module.addImport("platform", exe.platformModule(b, native, target, optimize, macos_sdl_paths)); + @import("fizzy_backend").addPlatformObjC(backend_dep, root_module, exe.backendOptions(target, optimize, macos_sdl_paths)); + } +} diff --git a/build/exe.zig b/build/exe.zig index 28fed66e8..010402751 100644 --- a/build/exe.zig +++ b/build/exe.zig @@ -92,12 +92,42 @@ pub fn defaultNativeBackend(target: std.Build.ResolvedTarget) NativeBackend { } /// dvui as the executable links it, and the backend under it. -const NativeDvui = struct { +pub const NativeDvui = struct { dep: *std.Build.Dependency, dvui: *std.Build.Module, backend: *std.Build.Module, }; +/// dvui on the native backend asked for: fizzy's (`fizzyNativeDvui`), or dvui's own SDL3 backend. +/// The executable's, and a dvui app's of its own (`fizzy.addDvui`, in `build.zig`). +pub fn nativeDvui( + b: *std.Build, + native_backend: NativeBackend, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + accesskit: dvui.AccesskitOptions, + macos_sdl_paths: ?MacosSdlPaths, +) NativeDvui { + return switch (native_backend) { + .fizzy => fizzyNativeDvui(b, target, optimize, accesskit, macos_sdl_paths), + .sdl3 => blk: { + const dep = if (macos_sdl_paths) |p| + sdk.dvuiDependency(b, .{ + .target = target, + .optimize = optimize, + .backend = .sdl3, + .accesskit = accesskit, + .system_include_path = p.include, + .system_framework_path = p.framework, + .library_path = p.lib, + }) + else + sdk.dvuiDependency(b, .{ .target = target, .optimize = optimize, .backend = .sdl3, .accesskit = accesskit }); + break :blk .{ .dep = dep, .dvui = dep.module("dvui_sdl3"), .backend = dep.module("sdl3") }; + }, + }; +} + /// dvui in its `custom` mode with fizzy's backend linked under it — as `build/web.zig` does for /// the web. Every option is the one dvui's own `sdl3` mode sets, so dvui's shape, and with it /// the plugin ABI fingerprint, is the same either way. @@ -139,7 +169,7 @@ pub fn backendOptions(target: std.Build.ResolvedTarget, optimize: std.builtin.Op /// Window and platform pieces for apps on either SDL3 backend (the backend package's /// `platform`). Fizzy's own backend imports it as well. -fn platformModule( +pub fn platformModule( b: *std.Build, native: NativeDvui, target: std.Build.ResolvedTarget, @@ -175,24 +205,7 @@ pub fn addFizzyExecutableForTarget( /// `bundled_plugins` module under its plugin id. app_plugins: []const sdk.BundledPlugin, ) !FizzyExecutable { - const native: NativeDvui = switch (native_backend) { - .fizzy => fizzyNativeDvui(b, resolved_target, optimize, accesskit, macos_sdl_paths), - .sdl3 => blk: { - const dep = if (macos_sdl_paths) |p| - sdk.dvuiDependency(b, .{ - .target = resolved_target, - .optimize = optimize, - .backend = .sdl3, - .accesskit = accesskit, - .system_include_path = p.include, - .system_framework_path = p.framework, - .library_path = p.lib, - }) - else - sdk.dvuiDependency(b, .{ .target = resolved_target, .optimize = optimize, .backend = .sdl3, .accesskit = accesskit }); - break :blk .{ .dep = dep, .dvui = dep.module("dvui_sdl3"), .backend = dep.module("sdl3") }; - }, - }; + const native = nativeDvui(b, native_backend, resolved_target, optimize, accesskit, macos_sdl_paths); const dvui_dep = native.dep; const dvui_mod = native.dvui; From 40d9ffe5a7f974f96a74ac6b660905efc98b4ab1 Mon Sep 17 00:00:00 2001 From: Colton Franklin <49629865+foxnne@users.noreply.github.com> Date: Sat, 10 Oct 2026 08:04:55 -0500 Subject: [PATCH 5/5] popout: a demo's pointer shows over float, menu and dialog windows (#330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #326 (on #324); review those first. A demo or live tape's pointer was drawn only in the main window. Over a float's OS window, a menu or a dialog, it vanished, so a viewer couldn't see it reach a float's close button, a menu row or a dialog's buttons. Reported by @foxnne. ## Why - **The pointer's layer was the size of the main window.** `replay.overlay` made it a subwindow covering the whole window. Popout gives each OS window the subwindows whose middle is in its part of the frame, so the pointer layer always counted as the main window's. - **Even placed right, a float's window would have claimed it before a menu or dialog over the float could.** Floats are drawn first (`windowFrame` before `menuFrame`). Dialogs are made in `drawRetained`, after the pointer was raised, so they stack above it. ## Changes - **`sdk/replay/overlay.zig`:** the pointer's layer is a small rect round its tip, so its middle is where the pointer is. Drawing is still clipped to the screen the pointer is on (`dvui.screenFor`), not to that rect. Any dvui app that splits its frame across windows by subwindow middle gets the same behaviour. - **`Popout.overlaysFrame`, new, last in `endFrame`:** each subwindow that takes no input (`mouse_events` false: the pointer and its click ripple, a tooltip) is drawn into the topmost window over its middle. That's a menu's or dialog's window, else a float's. Over none of them, it's left to the main window. - `windowFrame` now leaves such subwindows for this pass, unless they are drawn everywhere (a drag's layer). - Carried layers stay the carry window's. - **`viewports.Picture.again`** (and its stand-in): draws more into a picture already handed over this frame, keeping what's there. `present` only names the target; the window copies it when the frame ends. `begin` is now `clear` plus `again`. ## Verification - `zig build`, `test` (514/515, 1 skipped), `test-integration` (358/358), `check-web`, `test-sdk-version` and the Windows cross-build pass on macOS. - **Sandbox, macOS:** - **Pointer in a float:** a tape floated the explorer out and moved the pointer onto a file in it. A temporary trace in `overlaysFrame` (removed before the commit) showed the pointer layer going into the float's window while over it (middle at `200354,388`, the float's band). The other input-less layer stayed with the main window. - **Soak:** the soak tape still ends in a passing verdict, with no leaks and nothing logged as an error. - **Not seen:** - The menu and dialog windows. The sandbox's menus close on the next frame because it isn't the active app, and this session has no screen-recording permission for screenshots. - The drawing itself. The routing is what's verified, not what it looks like. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 --- backend/src/viewports.zig | 7 ++++++ backend/src/viewports_none.zig | 3 +++ sdk/replay/overlay.zig | 13 ++++++++-- src/editor/Popout.zig | 46 +++++++++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/backend/src/viewports.zig b/backend/src/viewports.zig index 12f6b2169..112faa6fb 100644 --- a/backend/src/viewports.zig +++ b/backend/src/viewports.zig @@ -297,6 +297,13 @@ pub const Picture = struct { /// `target` cleared, and drawn into from now as `part` of the frame (physical). pub fn begin(target: dvui.Texture.Target, part: Rect) Picture { target.clear(); + return again(target, part); + } + + /// `target` drawn into again as `part` of the frame, what is in it kept: more for a picture + /// begun and ended earlier this frame. A window handed its picture (`present`) copies it only + /// when the frame ends, so what goes over it last — a demo's pointer — can still go in. + pub fn again(target: dvui.Texture.Target, part: Rect) Picture { var rt = dvui.currentWindow().render_target; rt.texture = target; rt.offset = .{ .x = part.x, .y = part.y }; diff --git a/backend/src/viewports_none.zig b/backend/src/viewports_none.zig index d7ebb2b45..e7c85a64c 100644 --- a/backend/src/viewports_none.zig +++ b/backend/src/viewports_none.zig @@ -49,6 +49,9 @@ pub const Picture = struct { pub fn begin(_: dvui.Texture.Target, _: Rect) Picture { return .{}; } + pub fn again(_: dvui.Texture.Target, _: Rect) Picture { + return .{}; + } pub fn moveTo(_: *Picture, _: dvui.Point.Physical) void {} pub fn subwindow(_: Picture, _: *dvui.Subwindows.Subwindow, _: bool) void {} pub fn end(_: Picture) void {} diff --git a/sdk/replay/overlay.zig b/sdk/replay/overlay.zig index 5459cc340..b92f61ec9 100644 --- a/sdk/replay/overlay.zig +++ b/sdk/replay/overlay.zig @@ -21,7 +21,7 @@ pub const Options = struct { /// (`LiveDriver.Hand`). pub fn drawLive(driver: *const LiveDriver, opts: Options) void { if (!driver.playing()) return; - const fw = layer(@src(), .{ .rect = .cast(dvui.windowRect()), .name = "LivePointer" }, .{}); + const fw = layer(@src(), .{ .rect = pointerRect(driver.seq.pointer), .name = "LivePointer" }, .{}); defer fw.deinit(); const now = dvui.frameTimeNS(); @@ -38,7 +38,7 @@ pub fn drawLive(driver: *const LiveDriver, opts: Options) void { /// A demo's pointer, while it drives, timed in demo time: a seek shows what playing showed. pub fn drawPlayer(player: *Player, opts: Options) void { if (!player.driving()) return; - const fw = layer(@src(), .{ .rect = .cast(dvui.windowRect()), .name = "DemoPointer" }, .{}); + const fw = layer(@src(), .{ .rect = pointerRect(player.seq.pointer), .name = "DemoPointer" }, .{}); defer fw.deinit(); const ripple: ?Ripple = if (player.last_press) |press| .{ .pt = press.pt, .age_ms = player.seq.now - press.at } else null; // Out of the way of the words while the tape types, as a desktop's pointer is. @@ -46,6 +46,15 @@ pub fn drawPlayer(player: *Player, opts: Options) void { paintPointer(player.seq.pointer, ripple, shown, player.input.held.count() > 0); } +/// The pointer's layer: round its tip, natural, so its middle is where the pointer is. An app that +/// shows parts of its frame in OS windows of their own (fizzy's floats, menus and dialogs) gives +/// each window the layers whose middle is in it, and the pointer is then drawn on whatever window +/// it is over. What it draws is clipped to the screen it is on (`dvui.screenFor`), not to this. +fn pointerRect(p: Sequencer.Point) dvui.Rect { + const s = dvui.windowNaturalScale(); + return .{ .x = p.x / s - 16, .y = p.y / s - 16, .w = 32, .h = 32 }; +} + /// A floating widget that takes no input, raised to the top. A floating widget otherwise stays /// just above the window it was made in, under anything opened after it; re-added as a subwindow /// of its own it can be raised, and each call puts the new one above the last. diff --git a/src/editor/Popout.zig b/src/editor/Popout.zig index 53e60c231..4b94a0a9c 100644 --- a/src/editor/Popout.zig +++ b/src/editor/Popout.zig @@ -52,6 +52,8 @@ const MenuOut = struct { id: dvui.Id, viewport: *viewports.Viewport, target: ?dvui.Texture.Target = null, + /// The part of the frame its picture shows this frame, once drawn (`overlaysFrame`). + part: ?viewports.Rect = null, /// Drawn this frame: a menu not drawn has closed, and its window goes. seen: bool = false, /// On Liquid Glass (`viewports.windowGlass`), else vibrancy with the menu's colour drawn over it. @@ -98,6 +100,8 @@ const Out = struct { /// the texture is kept larger through a fullscreen transition (`keepTarget`). target: ?dvui.Texture.Target = null, drawn: [2]u32 = .{ 0, 0 }, + /// The part of the frame its picture shows this frame, once drawn (`overlaysFrame`). + part: ?viewports.Rect = null, /// What its window is called now: the float's title, as its header says (`Floats.Float.titleText`). title_buf: [96]u8 = undefined, title_len: u8 = 0, @@ -327,6 +331,12 @@ fn release(o: *Out) void { pub fn endFrame(state: *State) void { if (!enabled()) return; cover_count = 0; + for (&outs) |*slot| if (slot.*) |*o| { + o.part = null; + }; + for (&menus) |*slot| if (slot.*) |*m| { + m.part = null; + }; for (&outs) |*slot| { if (slot.*) |*o| windowFrame(state, o); } @@ -340,6 +350,7 @@ pub fn endFrame(state: *State) void { } carryFrame(state); menuFrame(); + overlaysFrame(); // A drag's carry window no float grew out of in the frame after the drag: gone. if (spare) |*sp| { if (spare_kept) { @@ -1081,6 +1092,7 @@ fn windowFrame(state: *State, o: *Out) void { // Transparent where the float is not: past its corners. const picture: viewports.Picture = .begin(target, shown); defer picture.end(); + o.part = shown; // Zoomed or full screen there is no desktop behind it: opaque, eased there and back with the // OS's transitions as the main window's base is. Where it has no material it is opaque // throughout. @@ -1101,6 +1113,8 @@ fn windowFrame(state: *State, o: *Out) void { // A carried view is its carry window's (`carryFrame`); a menu, its own (`menuFrame`). if (fizzy.core.screens.isCarried(sw.id)) continue; if (fizzy.core.screens.isMenu(sw.id) and nativeMenus()) continue; + // What takes no input goes over everything, last (`overlaysFrame`). + if (!sw.mouse_events and !fizzy.core.screens.isEverywhere(sw.id)) continue; const mine = area.contains(sw.rect_pixels.center()); if (mine or fizzy.core.screens.isEverywhere(sw.id)) picture.subwindow(sw, mine); } @@ -1273,7 +1287,8 @@ fn menuFrame() void { const w: u32 = @intFromFloat(@max(1, @round(shown.w))); const h: u32 = @intFromFloat(@max(1, @round(shown.h))); const target = viewports.sizedTarget(&m.target, w, h) orelse continue; - const picture: viewports.Picture = .begin(target, .{ .x = shown.x, .y = shown.y, .w = shown.w, .h = shown.h }); + m.part = .{ .x = shown.x, .y = shown.y, .w = shown.w, .h = shown.h }; + const picture: viewports.Picture = .begin(target, m.part.?); if (!m.glass) { const prev_clip = dvui.clipGet(); defer dvui.clipSet(prev_clip); @@ -1321,6 +1336,35 @@ fn dialogsRide() void { }; } +/// What takes no input, drawn last: a demo's pointer and the ripple of its click +/// (`replay.overlay`, its layer round the pointer's tip), a tooltip. Each goes into the topmost of +/// the app's windows over its middle — a menu's or a dialog's, else a float's — over all that +/// window shows, so the pointer is seen on whatever it is over; over none of them it is left to +/// the main window. Those windows' pictures are handed over already (`windowFrame`, `menuFrame`): +/// a window copies its picture only when the frame ends, so this still goes in. +fn overlaysFrame() void { + const cw = dvui.currentWindow(); + for (cw.subwindows.stack.items) |*sw| { + if (sw.mouse_events) continue; + if (fizzy.core.screens.isCarried(sw.id) or fizzy.core.screens.isEverywhere(sw.id)) continue; + const mid = sw.rect_pixels.center(); + const Into = struct { target: dvui.Texture.Target, part: viewports.Rect }; + const into: ?Into = blk: { + for (menus) |slot| if (slot) |m| if (m.part) |p| if (m.target) |t| { + if ((dvui.Rect.Physical{ .x = p.x, .y = p.y, .w = p.w, .h = p.h }).contains(mid)) break :blk .{ .target = t, .part = p }; + }; + for (outs) |slot| if (slot) |o| if (o.part) |p| if (o.target) |t| { + if ((dvui.Rect.Physical{ .x = p.x, .y = p.y, .w = p.w, .h = p.h }).contains(mid)) break :blk .{ .target = t, .part = p }; + }; + break :blk null; + }; + const w = into orelse continue; + const picture: viewports.Picture = .again(w.target, w.part); + picture.subwindow(sw, true); + picture.end(); + } +} + /// Menu `id`'s window (`menuFrame`), opened at `place` (physical, in the main window's frame) where /// it has none yet — riding on `ride`; a dialog's (`dialog`) in that window's stacking. Null where /// no more windows can be opened: the menu is drawn in its window then.