diff --git a/backend/src/SDLBackend.zig b/backend/src/SDLBackend.zig index 8bb90bc4..c9d447ce 100644 --- a/backend/src/SDLBackend.zig +++ b/backend/src/SDLBackend.zig @@ -18,6 +18,8 @@ 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"); +/// `dvui.osWindow` as a window of its own from the one frame (`osWindowFloating`). +pub const os_windows = @import("os_windows.zig"); /// What this backend keeps count of as it runs (`health`). pub const Health = @import("Health.zig"); @@ -97,7 +99,7 @@ clock_ahead_ns: i128 = 0, /// Last known window rect while not maximized/fullscreen/minimized, tracked /// from move/resize events window_geometry: WindowGeometry = .{}, -// Set by `initWindow` and `initWindowSecondary` for use by eventual child window. +// Set by `initWindow`. init_opts_save: ?InitOptions = null, /// The OS windows besides this one that show part of its frame (`Viewport`). viewport_slots: [max_viewports]?Viewport = @splat(null), @@ -490,43 +492,6 @@ pub fn initWindow(init_options: InitOptions) !SDLBackend { return back; } -pub fn initWindowSecondary(parent: *SDLBackend, child_win_opts: dvui.OsWindowWidget.InitOptions) !SDLBackend { - const parent_opts = parent.init_opts_save orelse { - log.err("initWindowSecondary expects parent instance with `init_opts_save` field set (typically by `initWindow`)", .{}); - return dvui.Backend.GenericError.BackendError; - }; - const new_init_opts: SDLBackend.InitOptions = .{ - .io = parent.io, - .environ_map = parent_opts.environ_map, - - .size = child_win_opts.size orelse parent_opts.size, - .min_size = child_win_opts.min_size orelse parent_opts.min_size, - .max_size = child_win_opts.max_size orelse parent_opts.max_size, - .vsync = parent_opts.vsync, - .title = child_win_opts.title orelse parent_opts.title, - .org = parent_opts.org, - .icon = child_win_opts.icon orelse parent_opts.icon, - .hidden = child_win_opts.hidden, - .fullscreen = child_win_opts.fullscreen, - .transparent = parent_opts.transparent, - .sdl_init = false, - // only the primary window persists its geometry - .persist_window_geometry = false, - }; - // Secondary windows share the primary's GPU device: textures are per dvui window, but one - // device can drive any number of claimed windows. - const new = try createWindowRenderer(new_init_opts, parent.gpu); - - var back = init(dvui.io, new.win, new.gpu); - back.init_opts_save = new_init_opts; - back.window_geometry = new.saved_geometry orelse .{}; - back.sdl_quit = false; - back.log_events = parent.log_events; - - try configureBackend(&back, new_init_opts); - - return back; -} fn createWindowRenderer(options: InitOptions, share_device_of: ?*GpuRenderer) !struct { win: *c.SDL_Window, @@ -775,7 +740,7 @@ pub const WindowGeometry = struct { } }; -// Common configuration part for both `initWindow` and `initWindowSecondary` +// The configuration `initWindow` applies. fn configureBackend(back: *SDLBackend, options: InitOptions) !void { var hidden = options.hidden; var show_window_in_begin = false; @@ -1080,6 +1045,17 @@ fn addEventWinRecursive(self: *SDLBackend, event: *c.SDL_Event, win: *dvui.Windo return false; } +// --------------------------------------------------------------------------------------------- +// dvui's OS windows + +/// A `dvui.osWindow`, which dvui draws as a floating window here: this backend makes no +/// `dvui.Window` of its own for one (no `initWindowSecondary`), so it is a floating window in the +/// one frame, and `os_windows` shows it in an OS window of its own. dvui calls this each frame it is +/// drawn, with the header it is moved by (physical). +pub fn osWindowFloating(_: *SDLBackend, id: dvui.Id, header: dvui.Rect.Physical, opts: dvui.OsWindowWidget.InitOptions) void { + os_windows.named(id, header, opts.title); +} + // --------------------------------------------------------------------------------------------- // Viewports (fizzy's own; see `Viewport`) @@ -3596,10 +3572,12 @@ fn appIterate(_: ?*anyopaque) callconv(.c) c.SDL_AppResult { }; const app = dvui.App.get() orelse unreachable; + os_windows.beforeFrame(); var res = app.frameFn() catch |err| { log.err("dvui.App.frameFn failed: {any}", .{err}); return c.SDL_APP_FAILURE; }; + os_windows.afterFrame(); live_resize_trace.overlay(); diff --git a/backend/src/os_windows.zig b/backend/src/os_windows.zig new file mode 100644 index 00000000..b491d301 --- /dev/null +++ b/backend/src/os_windows.zig @@ -0,0 +1,185 @@ +//! `dvui.osWindow` on this backend: each is a floating window in the one frame (dvui's fallback, +//! as this backend makes no `dvui.Window` of its own for one), shown in an OS window of its own +//! (`viewports`) that looks just as it does inside the main window — dvui's header and close +//! button, no OS title bar (`Frame.app`). The app writes plain dvui; on a backend without OS +//! windows the same floating window stays in the main window. +//! +//! dvui says which floating windows are OS windows as they draw (`named`, from +//! `SDLBackend.osWindowFloating`). Around the app's frame (`SDLBackend.appIterate`, or an app's +//! own loop): +//! +//! - `beforeFrame`: a window the OS moved takes its floating window with it (`osPlaced`), and each +//! window's band is a screen of dvui's (`dvui.screensSet`), so dvui keeps the floating window in +//! it; one being resized holds the pointer in its band (`pinPointer`); +//! - `afterFrame`: dvui's dialogs and toasts made (`Window.drawRetained`); a window whose floating +//! window was not drawn — closed — goes; each one named and not yet in a window gets one over +//! where it is (`viewports.open`), drawn in that window's band from the next frame; and each +//! window is put where its floating window is (`viewports.place`), told its header is where the +//! OS moves it from (`hints`), and handed its picture: every subwindow in its band, taken out of +//! the frame (`viewports.Picture`). +//! +//! dvui keeps where each floating window is (`_rect` in its data, by its id): moved into the +//! window's band, and read back after the frame. +const dvui = @import("dvui"); +const viewports = @import("viewports.zig"); + +/// As many as there are viewports. Past that, a floating window stays in the main window. +const max_outs = 8; + +/// A floating window in an OS window of its own, by its dvui id. +const Out = struct { + id: dvui.Id, + viewport: *viewports.Viewport, + target: ?dvui.Texture.Target = null, + /// Opened this frame: drawn in the main window still, in its band from the next. + fresh: bool = true, +}; +var outs: [max_outs]?Out = @splat(null); + +/// The floating windows dvui named as OS windows this frame (`named`), with their headers +/// (physical) and titles. +const Named = struct { id: dvui.Id, header: dvui.Rect.Physical, title: [64:0]u8 }; +var named_list: [max_outs]Named = undefined; +var named_n: usize = 0; + +/// The pointer is pinned to a window's band by `beforeFrame`, to be let go once nothing holds it. +var pinned = false; + +/// dvui drew floating window `id` for an OS window this frame (`dvui.osWindow`), moved by `header`. +pub fn named(id: dvui.Id, header: dvui.Rect.Physical, title: ?[:0]const u8) void { + if (named_n == named_list.len) return; + var n: Named = .{ .id = id, .header = header, .title = @splat(0) }; + const t = title orelse ""; + const len = @min(t.len, n.title.len); + @memcpy(n.title[0..len], t[0..len]); + named_list[named_n] = n; + named_n += 1; +} + +fn natural(r: viewports.Rect, s: f32) dvui.Rect { + return .{ .x = r.x / s, .y = r.y / s, .w = r.w / s, .h = r.h / s }; +} + +fn physical(r: dvui.Rect, s: f32) viewports.Rect { + return .{ .x = r.x * s, .y = r.y * s, .w = r.w * s, .h = r.h * s }; +} + +/// After `dvui.Window.begin`, before the app's frame. +pub fn beforeFrame() void { + named_n = 0; + const s = dvui.windowNaturalScale(); + var screens: [max_outs]dvui.Rect.Natural = undefined; + var n: usize = 0; + var pin: ?*viewports.Viewport = null; + for (&outs) |*slot| { + const o = if (slot.*) |*o| o else continue; + if (viewports.osPlaced(o.viewport)) |fr| dvui.dataSet(null, o.id, "_rect", natural(fr, s)); + _ = viewports.osMoveEnded(o.viewport); + const r = dvui.dataGet(null, o.id, "_rect", dvui.Rect) orelse continue; + screens[n] = .{ .x = r.x, .y = r.y, .w = r.w, .h = r.h }; + n += 1; + // Resized from its edges, it holds the pointer: read in its band till it lets go, even + // past its window's edge. + if (dvui.captured(o.id)) pin = o.viewport; + } + // Only with windows of its own: an app keeping screens of its own (fizzy's floats) sets them. + if (n > 0) dvui.screensSet(screens[0..n]); + if (pin) |vp| { + viewports.pinPointer(.{ .viewport = vp }); + pinned = true; + } else if (pinned) { + viewports.pinPointer(.none); + pinned = false; + } +} + +/// After the app's frame, before `dvui.Window.end`. +pub fn afterFrame() void { + if (named_n == 0 and for (outs) |slot| { + if (slot != null) break false; + } else true) return; + const cw = dvui.currentWindow(); + // Dialogs and toasts are subwindows from here, so each lands in the window it is over. + cw.drawRetained(.{}); + const s = dvui.windowNaturalScale(); + + for (&outs) |*slot| { + const o = if (slot.*) |*o| o else continue; + const drawn = for (cw.subwindows.stack.items) |sw| { + if (sw.id == o.id) break sw.used; + } else false; + if (drawn) { + // Its close button cleared what showed it: it goes next frame, which comes now + // rather than at the next event. + if (namedOf(o.id) == null) dvui.refresh(null, @src(), null); + continue; + } + viewports.close(o.viewport); + if (o.target) |t| t.destroyLater(); + slot.* = null; + } + + for (named_list[0..named_n]) |*w| { + if (outOf(w.id) != null) continue; + if (!viewports.available()) break; + const slot = for (&outs) |*slot| { + if (slot.* == null) break slot; + } else break; + const r = dvui.dataGet(null, w.id, "_rect", dvui.Rect) orelse continue; + // Not before dvui has sized and placed it: a floating window spends its first frame + // measuring itself, hidden, and centers itself once (`_auto_pos`) — over the main window, + // which would take it out of its window's band again. + if (r.w < 1 or r.h < 1) continue; + if (dvui.dataGet(null, w.id, "_auto_pos", bool) orelse true) continue; + const vp = viewports.open(physical(r, s), &w.title, .app) orelse break; + slot.* = .{ .id = w.id, .viewport = vp }; + dvui.dataSet(null, w.id, "_rect", natural(viewports.frameOf(vp), s)); + dvui.refresh(null, @src(), null); + } + + for (&outs) |*slot| { + const o = if (slot.*) |*o| o else continue; + if (o.fresh) { + o.fresh = false; + continue; + } + const r = dvui.dataGet(null, o.id, "_rect", dvui.Rect) orelse continue; + const shown = viewports.place(o.viewport, physical(r, s)); + // The OS moves its window by its header, but for the close button — square at the + // header's height, at its right end where buttons go OK then Cancel, else its left + // (`dvui.windowHeader`) — and not from the floating window's own resize zones round its + // edge (`app_side`). + if (namedOf(o.id)) |w| { + const hd = w.header; + const keep_x = if (cw.button_order == .ok_cancel) hd.x + hd.w - hd.h else hd.x; + viewports.hints(o.viewport, .{ + .drag = .{ .x = hd.x, .y = hd.y, .w = hd.w, .h = hd.h }, + .keep = .{ .x = keep_x, .y = hd.y, .w = hd.h, .h = hd.h }, + .glass = shown, + .edge = 0, + .app_side = 6 * s, + .app_corner = 15 * s, + }); + } + const w: u32 = @intFromFloat(@max(1, @round(shown.w))); + const h: u32 = @intFromFloat(@max(1, @round(shown.h))); + const target = viewports.sizedTarget(&o.target, w, h) orelse continue; + const area: dvui.Rect.Physical = .{ .x = shown.x, .y = shown.y, .w = shown.w, .h = shown.h }; + const picture: viewports.Picture = .begin(target, shown); + for (cw.subwindows.stack.items) |*sw| { + if (area.contains(sw.rect_pixels.center())) picture.subwindow(sw, true); + } + picture.end(); + viewports.present(o.viewport, target); + } +} + +fn outOf(id: dvui.Id) ?*Out { + for (&outs) |*slot| if (slot.*) |*o| if (o.id == id) return o; + return null; +} + +fn namedOf(id: dvui.Id) ?*const Named { + for (named_list[0..named_n]) |*w| if (w.id == id) return w; + return null; +} diff --git a/docs/DEPENDENCIES.md b/docs/DEPENDENCIES.md index 7c026a80..3a41e8d2 100644 --- a/docs/DEPENDENCIES.md +++ b/docs/DEPENDENCIES.md @@ -69,18 +69,31 @@ backend and never link SDL, so an SDL bump is never an SDK release and never mov 6. **screensSet: floating widgets can be kept on screens besides the window** (`a90bc70`). `dvui.screensSet(rects)` lists areas besides the window that are screens of their own; floating windows, menus and tooltips place, clip and dim against `dvui.screenFor` (the one - holding their middle, else `windowRect`). Not yet used by fizzy, whose `core.screens` and - widget forks do the same. Upstream: branch `screens-set`, to propose. Test: "a floating window - on another screen (dvui.screensSet) stays on it". + holding their middle, else `windowRect`). Popout sets each float window's band as one (#325), + and the backend's `os_windows` each `dvui.osWindow`'s. Upstream: branch `screens-set`, to + propose. Test: "a floating window on another screen (dvui.screensSet) stays on it". +7. **OsWindowWidget: a child window is deinit before its backend** (`f0f498a`). `ChildOsWindow.deinit` + freed the backend first, then the `dvui.Window`, whose deinit gives its textures (the font + atlases) back to that backend: closing any `dvui.osWindow` on a backend with secondary windows + crashed. Upstream: worth proposing on its own. +8. **osWindow: the fallback floating window is the window asked for, and its backend hears of it** + (`f646ec0`). Where a backend makes no OS window of its own, `dvui.osWindow` falls back to a + floating window, which now takes the window's size, least and most size, `id_extra`, a close + button on its `open_flag` (the next frame asked for at once), and steps past one already there; + and the backend hears of it each frame (`Backend.osWindowFloating`: its id, the header it is + moved by, the options). Fizzy's backend makes no secondary `dvui.Window`, so every + `dvui.osWindow` takes this path, and `backend/src/os_windows.zig` shows the floating window in + an OS window of its own. Upstream: to propose with `run-dvui` (fizzyedit/example-app) as the + demonstration. Each of 5 and 6 is also a branch of its own on upstream `main` (`window-draw-retained`, `screens-set`), from which the upstream PR is opened. Tags: `fizzy-sdk-0.2.13` → `712f7f7` (the stack before this rebase, still what SDK ≤ 0.2.15 builds); `fizzy-sdk-0.2.16` → `4428108` (SDK 0.2.16 to 0.2.18); `fizzy-sdk-0.2.19` → -`f8b32db`; `fizzy-sdk-0.2.19-2` → `a90bc70`, what `sdk/build.zig.zon` pins now (0.2.19 was not -released yet, and a tag never moves, so the second pin of one version takes a suffix, as the SDL -fork's tags do). +`f8b32db`; `fizzy-sdk-0.2.19-2` → `a90bc70` (0.2.19 was not released yet, and a tag never +moves, so the second pin of one version takes a suffix, as the SDL fork's tags do; SDK 0.2.19 to +0.2.22); `fizzy-sdk-0.2.23` → `f646ec0`, what `sdk/build.zig.zon` pins now. Fizzy builds dvui in its `custom` backend mode on every target and links its own backends (`backend/`, `src/backend/WebBackend.zig` + `web/web.js`), so dvui's `sdl3` and `web` diff --git a/sdk/build.zig.zon b/sdk/build.zig.zon index 44f42f1c..b48e2e3b 100644 --- a/sdk/build.zig.zon +++ b/sdk/build.zig.zon @@ -39,9 +39,9 @@ // `fizzy-sdk-` tag in the fork before the pin lands. `fizzy-dev` itself stays a // working branch and can be rebased freely — the tag, never. .dvui = .{ - // fizzy-sdk-0.2.19-2 - .url = "https://github.com/foxnne/dvui-dev/archive/a90bc701c5b9.tar.gz", - .hash = "dvui-0.5.0-dev-AQFJmfUrBAEB1joV1vjYrG_e_jx27JfNi9Bdr9ldPPGn", + // fizzy-sdk-0.2.23 + .url = "https://github.com/foxnne/dvui-dev/archive/f646ec0b17899a2c2f222ecce834c82e235e0486.tar.gz", + .hash = "dvui-0.5.0-dev-AQFJmTQyBAHtzJRJ2BzYtusSfj5mYzF4owYGcDHfyNY_", //.path = "../../dvui-dev", }, .zf = .{