diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e843ba30d..5fe98a73d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,6 +159,17 @@ jobs: shell: bash run: bash scripts/ci-retry.sh zig build check-web + # Fizzy on dvui's own SDL3 backend (`-Dnative-backend=sdl3`), where any dvui app on the stock + # backend stands: the platform pieces compile without fizzy's backend and lose what needs it + # (floats' windows, the window skin). It rotted unnoticed while nothing built it. macOS + # only: the platform reaches furthest into the backend there, and that is where it broke. + - name: Fetch dependencies for dvui's SDL3 backend (with retries) + if: matrix.name == 'macOS' + run: bash scripts/ci-retry.sh zig build --fetch -Dnative-backend=sdl3 + - name: Compile on dvui's SDL3 backend + if: matrix.name == 'macOS' + run: bash scripts/ci-retry.sh zig build -Dnative-backend=sdl3 -Dno-emit --summary all + # Fizzy consumed from outside: fizzyedit/example-app (fizzy as a library, in every shape) # and fizzyedit/example-plugin (the plugin template), built against this tree fetched as a # URL dependency would be. The only checks that catch a path resolving against the diff --git a/backend/src/SDLBackend.zig b/backend/src/SDLBackend.zig index 690bf88af..3d4908c55 100644 --- a/backend/src/SDLBackend.zig +++ b/backend/src/SDLBackend.zig @@ -24,6 +24,10 @@ pub const Health = @import("Health.zig"); /// What an app gets from its window beyond drawing into it: native dialogs, files the OS hands /// over, trackpad gestures, the window's chrome and state (`platform/`). pub const platform = @import("platform"); +// The platform knows this backend by that declaration (`platform.own_backend`). +comptime { + std.debug.assert(platform.own_backend); +} /// SDL3 only. Kept as a constant so the event code below stays a verbatim copy of dvui's /// `sdl.zig` (its `if (sdl3)` branches are the ones compiled). diff --git a/backend/src/platform/macos/window_monitor.m b/backend/src/platform/macos/window_monitor.m index b89db6a95..da4f48d8e 100644 --- a/backend/src/platform/macos/window_monitor.m +++ b/backend/src/platform/macos/window_monitor.m @@ -1206,11 +1206,14 @@ static void install_constrain_override(NSWindow *window) { * AppKit keeps the content rect across each change, so the window grew by its title bar and shrank * back: kept at will-enter it came back from full screen a title bar taller each time, and at * did-exit it jumped up a title bar and back down in a frame. A window the monitor follows keeps its - * content under the title bar through any style it is given. */ + * content under the title bar through any style it is given, and so does one that asked for that + * alone (`fizzy_macos_window_keep_full_size_content`). */ static IMP g_nswindow_style_mask_imp = NULL; +static char g_keeps_full_size_content; static void fizzy_set_style_mask(id self, SEL _cmd, NSWindowStyleMask mask) { - if (monitor_of((__bridge void *)self)) mask |= NSWindowStyleMaskFullSizeContentView; + if (monitor_of((__bridge void *)self) || objc_getAssociatedObject(self, &g_keeps_full_size_content)) + mask |= NSWindowStyleMaskFullSizeContentView; ((void (*)(id, SEL, NSWindowStyleMask))g_nswindow_style_mask_imp)(self, _cmd, mask); } @@ -1231,6 +1234,16 @@ static void install_style_mask_override(NSWindow *window) { } } +/* `nswindow` keeps its content under the title bar through every style SDL gives it, as a followed + * window does, without the rest of the monitor: on a backend whose frames the monitor cannot step + * (`macos_monitor.zig`'s `install`), the window's chrome is still fizzy's. */ +void fizzy_macos_window_keep_full_size_content(void *nswindow) { + if (!nswindow) return; + NSWindow *window = (__bridge NSWindow *)nswindow; + objc_setAssociatedObject(window, &g_keeps_full_size_content, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + install_style_mask_override(window); +} + /* Keep `token`, an observer `m` added, to remove when the window is forgotten. */ static void keep_observer(WindowMonitor *m, id token) { if (!m || !token || m->observer_count >= (int)(sizeof(m->observers) / sizeof(m->observers[0]))) return; diff --git a/backend/src/platform/macos_monitor.zig b/backend/src/platform/macos_monitor.zig index 29649fc2d..3a479939e 100644 --- a/backend/src/platform/macos_monitor.zig +++ b/backend/src/platform/macos_monitor.zig @@ -18,6 +18,7 @@ const dvui = @import("dvui"); const Backend = @import("backend"); const c = Backend.c; const window_layout = @import("window_layout.zig"); +const own_backend = @import("root.zig").own_backend; const log = std.log.scoped(.macos_monitor); @@ -45,6 +46,7 @@ extern fn fizzy_macos_window_set_frame(cocoa_window: ?*anyopaque, x: f64, y: f64 extern fn fizzy_macos_copy_screen_frames(out: [*]f64, max: c_int) c_int; extern fn fizzy_macos_window_sync_content_views(cocoa_window: ?*anyopaque) void; extern fn fizzy_macos_window_install_resize_observer(cocoa_window: ?*anyopaque) void; +extern fn fizzy_macos_window_keep_full_size_content(cocoa_window: ?*anyopaque) void; extern fn fizzy_macos_window_sdl_draws_live_resize(cocoa_window: ?*anyopaque) c_int; extern fn fizzy_macos_window_space_step(cocoa_window: ?*anyopaque) void; extern fn fizzy_macos_window_space_moving(cocoa_window: ?*anyopaque) c_int; @@ -369,10 +371,18 @@ export fn fizzy_macos_origin_nudged(cap_x: f64, cap_y: f64, cur_x: f64, cur_y: f /// Follow `win` through Spaces, zooms and live resizes: from here on AppKit's live sizes reach SDL /// before each frame (`Backend.SDLBackend.begin_hook`) and through every animation. Call once the /// window's chrome is in place, before it is shown. +/// +/// This package's backend only. The monitor takes full screen and zooms over from AppKit and draws +/// their steps itself, which needs that backend's frame hooks; on dvui's its steps were drawn out of +/// time with the window. There the window goes in and out as AppKit and SDL take it, every query +/// here answers from AppKit's own state, and the window only keeps its content under the title bar +/// through SDL's styles, for fizzy's chrome (`styleTitled`): without that it came back from each +/// round trip a title bar shorter. pub fn install(win: *dvui.Window) void { if (comptime builtin.os.tag != .macos) return; const back = win.backend.impl; const cocoa = cocoaWindowOf(back.window) orelse return; + if (comptime !own_backend) return fizzy_macos_window_keep_full_size_content(cocoa); macos_monitor_window = back.window; back.begin_hook = macosAppPreBeginSync; back.present_hook = macosAppPresented; diff --git a/backend/src/platform/root.zig b/backend/src/platform/root.zig index 8fadd88fd..8b41fd44a 100644 --- a/backend/src/platform/root.zig +++ b/backend/src/platform/root.zig @@ -17,6 +17,12 @@ pub const macos_monitor = @import("macos_monitor.zig"); pub const window_layout = @import("window_layout.zig"); pub const menu = @import("menu.zig"); +/// Whether the app is on this package's backend (`SDLBackend`, which imports these pieces) rather +/// than dvui's own SDL3 backend. What they reach into beyond SDL's window — floats' windows, the +/// window skin, the present hook — is there only; on dvui's, those parts do nothing. One flag, not +/// a `@hasField` per field: a field renamed on this backend then fails its build, not goes quiet. +pub const own_backend = @hasDecl(@import("backend"), "platform"); + 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 diff --git a/backend/src/platform/window.zig b/backend/src/platform/window.zig index 9ea461e73..888d433d7 100644 --- a/backend/src/platform/window.zig +++ b/backend/src/platform/window.zig @@ -9,6 +9,7 @@ const objc = @import("objc"); const titlebar = @import("titlebar.zig"); const win32_titlebar = @import("win32_titlebar.zig"); const linux_titlebar = @import("linux_titlebar.zig"); +const own_backend = @import("root.zig").own_backend; const NSPoint = extern struct { x: f64, y: f64 }; const NSSize = extern struct { width: f64, height: f64 }; @@ -299,12 +300,12 @@ 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.viewport_slots) |*slot| { + if (comptime own_backend) 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; styleTitled(ns); - } + }; } else if (builtin.os.tag == .windows) { win32_titlebar.applyChrome(win); } else if (builtin.os.tag == .linux) { @@ -422,12 +423,14 @@ pub fn setBackground(win: *dvui.Window, color: dvui.Color) void { skin(raw_ptr.?, color, dvui.themeGet().dark); // 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.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; - skin(ns, color, dvui.themeGet().dark); + if (comptime own_backend) { + win.backend.impl.window_skin = color; + 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; + skin(ns, color, dvui.themeGet().dark); + } } // SDL3 currently removes the shadow when the transparency flag for the window is set. This brings it back.