From b0491bd2f886c1f63ab1d8a195d85636c5027ac0 Mon Sep 17 00:00:00 2001 From: foxnne Date: Fri, 9 Oct 2026 22:03:34 -0500 Subject: [PATCH 1/4] build: pin fizzy at main with its backend package, SDK 0.2.20 --- build.zig.zon | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 7b2d321..b974c21 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ // by commit; fizzy's CI builds this repo against each of its pull requests, so a change // there that breaks an outside app fails before it lands. .fizzy = .{ - .url = "https://github.com/fizzyedit/fizzy/archive/4649893e4d27.tar.gz", - .hash = "fizzy-0.2.0-c46aVrUuiwDNap4KtjKj7wmmReYkOlSa5z4p7hDBUCgu", + .url = "https://github.com/fizzyedit/fizzy/archive/d4404912112e.tar.gz", + .hash = "fizzy-0.2.0-c46aVmO0jAAHJIMhRNSgO_yGX6bnqTfRAvpqT-kE28t8", }, // The plugins this app bundles beyond fizzy's own: fizzyedit/example-plugin, as any // plugin from its own repo, and this app's own, which also builds alone as a dylib @@ -22,8 +22,8 @@ // The plugin SDK alone, for the replay app: its pinned dvui and `tape` / `replay`, with // nothing of fizzy the app. .fizzy_sdk = .{ - .url = "https://github.com/fizzyedit/fizzy/releases/download/sdk-v0.2.19/fizzy-sdk-v0.2.19.tar.gz", - .hash = "fizzy-0.2.19-SGBwY8MiIQDGX5QO1AMQHemD99HG9DNnjTLQ4Pu1bpf5", + .url = "https://github.com/fizzyedit/fizzy/releases/download/sdk-v0.2.20/fizzy-sdk-v0.2.20.tar.gz", + .hash = "fizzy-0.2.20-SGBwY7BcIQAqHKHzaj0vwADkUQNdpiap19SmKZ29Rp0W", }, }, .paths = .{ "build.zig", "build.zig.zon", "shapes", "plugins", "replay", "README.md", "LICENSE" }, From 5d437fbe4e6944e862c2feb141c7c17b2c49d650 Mon Sep 17 00:00:00 2001 From: foxnne Date: Sat, 10 Oct 2026 07:02:47 -0500 Subject: [PATCH 2/4] dvui: run-dvui, a plain dvui app whose floating windows are OS windows `dvui/main.zig` on fizzy's backend through `fizzy.addDvui`; `-Ddvui-backend=sdl3` builds it on dvui's own backend, the floating windows in the main window. Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +-- build.zig | 29 ++++++--- build.zig.zon | 6 +- dvui/main.zig | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 dvui/main.zig diff --git a/README.md b/README.md index 3f8e307..af58ceb 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,12 @@ typing, a command), draws the tape's pointer, stops if you click or type, and wh prints what is on screen as text: the names a test or an automation client reads instead of pixels. It depends only on the SDK package. -`zig build run-dvui` is where a plain dvui app on fizzy's backend goes: dvui's own widgets, with -floating windows, menus and dialogs as OS windows of their own (Liquid Glass windows on macOS). -It needs fizzy's backend as a package an app can depend on without the rest of fizzy, which is -not done yet; until then the step says so. +`zig build run-dvui` is a plain dvui app on fizzy's backend (`dvui/main.zig`): dvui's own +widgets, and each `dvui.floatingWindow` an OS window of its own, moved, resized and closed by the +OS like any window. `fizzy.addDvui` gives it dvui, the backend and the backend's `viewports`. +`-Ddvui-backend=sdl3` builds the same app on dvui's own SDL3 backend, where there are no OS +windows besides the main one and the floating windows stay in it, as in any dvui app. Menus and +dialogs as OS windows of their own come later. ## Shapes diff --git a/build.zig b/build.zig index c921dfe..e124512 100644 --- a/build.zig +++ b/build.zig @@ -6,9 +6,9 @@ //! `zig build run-replay` builds a plain dvui app, nothing of fizzy, with tape playback from the //! plugin SDK's `replay`: it plays a tape into its own window and prints what is on screen. //! -//! `zig build run-dvui` will build a plain dvui app on fizzy's backend: dvui's widgets, with -//! floating windows, menus and dialogs as OS windows of their own. It needs the backend as a -//! package of its own first. +//! `zig build run-dvui` builds a plain dvui app on fizzy's backend: dvui's own widgets, each +//! floating window an OS window of its own. `-Ddvui-backend=sdl3` builds the same app on dvui's own +//! SDL3 backend, where the floating windows stay in the main window. const std = @import("std"); const fizzy = @import("fizzy"); const fizzy_sdk = @import("fizzy_sdk"); @@ -95,9 +95,22 @@ pub fn build(b: *std.Build) !void { if (b.args) |args| run_replay.addArgs(args); b.step("run-replay", "Run the replay app: a plain dvui app with tape playback").dependOn(&run_replay.step); - // Where the dvui app goes: dvui's own widgets on fizzy's backend. Waits on the backend - // becoming a package an app depends on without the rest of fizzy. - b.step("run-dvui", "Run a plain dvui app on fizzy's backend (not yet)").dependOn( - &b.addFail("run-dvui needs fizzy's backend as a package of its own; see this repo's README").step, - ); + // A plain dvui app on fizzy's backend (`dvui/main.zig`): dvui and the backend from fizzy, the + // backend's `viewports` for its floating windows' OS windows. Not part of the default install. + const dvui_backend = b.option(fizzy.NativeBackend, "dvui-backend", "run-dvui: fizzy (OS windows) or sdl3 (dvui's own, the main window only)") orelse .fizzy; + const dvui_exe = b.addExecutable(.{ + .name = "dvui-app", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("dvui/main.zig"), + }), + }); + try fizzy.addDvui(fizzy_dep, dvui_exe.root_module, dvui_backend); + const install_dvui = b.addInstallArtifact(dvui_exe, .{}); + b.step("dvui", "Build the dvui app").dependOn(&install_dvui.step); + const run_dvui = b.addRunArtifact(dvui_exe); + run_dvui.step.dependOn(&install_dvui.step); + if (b.args) |args| run_dvui.addArgs(args); + b.step("run-dvui", "Run a plain dvui app on fizzy's backend, its floating windows OS windows").dependOn(&run_dvui.step); } diff --git a/build.zig.zon b/build.zig.zon index b974c21..b26189c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ // by commit; fizzy's CI builds this repo against each of its pull requests, so a change // there that breaks an outside app fails before it lands. .fizzy = .{ - .url = "https://github.com/fizzyedit/fizzy/archive/d4404912112e.tar.gz", - .hash = "fizzy-0.2.0-c46aVmO0jAAHJIMhRNSgO_yGX6bnqTfRAvpqT-kE28t8", + .url = "https://github.com/fizzyedit/fizzy/archive/1bbc9e08cf1e.tar.gz", + .hash = "fizzy-0.2.0-c46aVtrIjAANbDpJBKvMUKJDGnxxr8dazzwdG6twFGHg", }, // The plugins this app bundles beyond fizzy's own: fizzyedit/example-plugin, as any // plugin from its own repo, and this app's own, which also builds alone as a dylib @@ -26,5 +26,5 @@ .hash = "fizzy-0.2.20-SGBwY7BcIQAqHKHzaj0vwADkUQNdpiap19SmKZ29Rp0W", }, }, - .paths = .{ "build.zig", "build.zig.zon", "shapes", "plugins", "replay", "README.md", "LICENSE" }, + .paths = .{ "build.zig", "build.zig.zon", "shapes", "plugins", "replay", "dvui", "README.md", "LICENSE" }, } diff --git a/dvui/main.zig b/dvui/main.zig new file mode 100644 index 0000000..38667a6 --- /dev/null +++ b/dvui/main.zig @@ -0,0 +1,174 @@ +//! A plain dvui app on fizzy's backend: dvui's own widgets, nothing of fizzy the editor, and each +//! floating window an OS window of its own where the backend has them (`viewports`). On a backend +//! without them — dvui's own SDL3 backend (`-Ddvui-backend=sdl3`), or Wayland, where a window +//! cannot be placed — the same floating windows stay in the main window, as in any dvui app. +//! +//! There stays one `dvui.Window` and one frame. A floating window that is out is drawn in its +//! viewport's band of the frame, far past the main window's edge, and its OS window shows that +//! band; the pointer over the OS window comes back to dvui as a pointer in the band. What it takes, +//! each frame: +//! +//! - before drawing (`beforeFloats`): each floating window not out yet goes out into a viewport +//! (`viewports.open`), one the OS moved or resized follows its window (`osPlaced`), and each +//! band is a screen of dvui's (`dvui.screensSet`), so dvui keeps the floating window in it; +//! - the floating windows, drawn as dvui draws them anywhere (`drawFloat`); +//! - after drawing (`afterFloats`): dvui's dialogs and toasts made (`Window.drawRetained`), then +//! each OS window put where its floating window is (`viewports.place`), told where its header is +//! for the OS to move it by (`hints`), and handed its picture: every subwindow in its band, +//! taken out of the frame (`viewports.Picture`), so the main window draws none of it. +const std = @import("std"); +const dvui = @import("dvui"); +const backend = @import("backend"); +const viewports = if (@hasDecl(backend, "viewports")) backend.viewports else @import("viewports_none"); + +pub const dvui_app: dvui.App = .{ + .config = .{ .options = .{ + .size = .{ .w = 720, .h = 480 }, + .title = "dvui on fizzy's backend", + } }, + .frameFn = frame, +}; +pub const main = dvui.App.main; +pub const panic = dvui.App.panic; +pub const std_options: std.Options = .{ .logFn = dvui.App.logFn }; + +/// The most floating windows at once: as many as the backend has viewports. Past what it can +/// open, a floating window stays in the main window. +const max_floats = 8; + +const Float = struct { + /// Natural units of the frame: in the main window, or in its viewport's band once out. + rect: dvui.Rect, + open: bool = true, + title_buf: [32:0]u8 = @splat(0), + title_len: usize = 0, + count: u32 = 0, + viewport: ?*viewports.Viewport = null, + target: ?dvui.Texture.Target = null, + /// Its header this frame, physical (`dvui.windowHeader`): where the OS moves its window from. + header: dvui.Rect.Physical = .{}, + + fn title(self: *const Float) [:0]const u8 { + return self.title_buf[0..self.title_len :0]; + } +}; + +var floats: [max_floats]?Float = @splat(null); +var made: u32 = 0; +var started = false; + +fn frame() !dvui.App.Result { + // Two floating windows to begin with. + if (!started) { + started = true; + newFloat(); + newFloat(); + } + beforeFloats(); + mainWindow(); + for (&floats, 0..) |*slot, i| if (slot.*) |*f| drawFloat(f, i); + afterFloats(); + return .ok; +} + +fn mainWindow() void { + var box = dvui.box(@src(), .{}, .{ .expand = .both, .padding = .all(16) }); + defer box.deinit(); + dvui.labelNoFmt(@src(), "dvui's own widgets on fizzy's backend.", .{}, .{ .font = .theme(.title) }); + const how = if (viewports.supported and viewports.available()) + "Each floating window is an OS window of its own: move it, resize it, put it on another display." + else + "This backend has no OS windows besides this one, so floating windows stay in it."; + dvui.labelNoFmt(@src(), how, .{}, .{}); + if (dvui.button(@src(), "New floating window", .{}, .{ .margin = .{ .y = 12 } })) newFloat(); +} + +fn newFloat() void { + const slot = for (&floats) |*slot| { + if (slot.* == null) break slot; + } else return; + made += 1; + const step: f32 = @floatFromInt(made % 6); + slot.* = .{ .rect = .{ .x = 80 + 32 * step, .y = 120 + 24 * step, .w = 280, .h = 180 } }; + const f = &slot.*.?; + const t: []const u8 = std.fmt.bufPrint(&f.title_buf, "Floating window {d}", .{made}) catch ""; + f.title_len = t.len; +} + +fn release(f: *Float) void { + if (f.viewport) |vp| viewports.close(vp); + if (f.target) |t| t.destroyLater(); +} + +fn beforeFloats() void { + const s = dvui.windowNaturalScale(); + var screens: [max_floats]dvui.Rect.Natural = undefined; + var n: usize = 0; + for (&floats) |*slot| { + const f = if (slot.*) |*f| f else continue; + if (!f.open) { + release(f); + slot.* = null; + continue; + } + if (f.viewport == null and viewports.available()) { + // Out into an OS window over where it is in the main window; drawn in the band from now. + const at: viewports.Rect = .{ .x = f.rect.x * s, .y = f.rect.y * s, .w = f.rect.w * s, .h = f.rect.h * s }; + if (viewports.open(at, f.title())) |vp| { + f.viewport = vp; + const fr = viewports.frameOf(vp); + f.rect = .{ .x = fr.x / s, .y = fr.y / s, .w = fr.w / s, .h = fr.h / s }; + } + } + const vp = f.viewport orelse continue; + // Its close button, or the OS's (⌘W): gone next frame. + if (viewports.closeRequested(vp)) f.open = false; + // The OS moved or resized its window: the floating window follows it. + if (viewports.osPlaced(vp)) |fr| f.rect = .{ .x = fr.x / s, .y = fr.y / s, .w = fr.w / s, .h = fr.h / s }; + _ = viewports.osMoveEnded(vp); + screens[n] = .{ .x = f.rect.x, .y = f.rect.y, .w = f.rect.w, .h = f.rect.h }; + n += 1; + } + dvui.screensSet(screens[0..n]); +} + +fn drawFloat(f: *Float, i: usize) void { + var fw = dvui.floatingWindow(@src(), .{ .rect = &f.rect, .open_flag = &f.open }, .{ .id_extra = i, .min_size_content = .{ .w = 220, .h = 120 } }); + defer fw.deinit(); + f.header = dvui.windowHeader(f.title(), "", &f.open); + var box = dvui.box(@src(), .{}, .{ .expand = .both, .padding = .all(12) }); + defer box.deinit(); + dvui.label(@src(), "Clicked {d} times", .{f.count}, .{}); + if (dvui.button(@src(), "Count", .{}, .{})) f.count += 1; +} + +fn afterFloats() void { + if (comptime !viewports.supported) 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 (&floats) |*slot| { + const f = if (slot.*) |*f| f else continue; + const vp = f.viewport orelse continue; + const shown = viewports.place(vp, .{ .x = f.rect.x * s, .y = f.rect.y * s, .w = f.rect.w * s, .h = f.rect.h * s }); + // Its header moves its window, but for the close button at its right end; its edges resize it. + const keep_w = f.header.h; + viewports.hints(vp, .{ + .drag = .{ .x = f.header.x, .y = f.header.y, .w = f.header.w, .h = f.header.h }, + .keep = .{ .x = f.header.x + f.header.w - keep_w, .y = f.header.y, .w = keep_w, .h = f.header.h }, + .glass = shown, + .edge = 6 * s, + }); + const w: u32 = @intFromFloat(@max(1, @round(shown.w))); + const h: u32 = @intFromFloat(@max(1, @round(shown.h))); + const target = viewports.sizedTarget(&f.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(vp, target); + } +} From c400ca0024928fc3295284377d9643670b46e6ec Mon Sep 17 00:00:00 2001 From: foxnne Date: Sat, 10 Oct 2026 11:06:28 -0500 Subject: [PATCH 3/4] build: pin fizzy at main, now with fizzy.addDvui Co-Authored-By: Claude Opus 5.5 --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b26189c..0e080c8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ // by commit; fizzy's CI builds this repo against each of its pull requests, so a change // there that breaks an outside app fails before it lands. .fizzy = .{ - .url = "https://github.com/fizzyedit/fizzy/archive/1bbc9e08cf1e.tar.gz", - .hash = "fizzy-0.2.0-c46aVtrIjAANbDpJBKvMUKJDGnxxr8dazzwdG6twFGHg", + .url = "https://github.com/fizzyedit/fizzy/archive/940a13d8b193.tar.gz", + .hash = "fizzy-0.2.0-c46aVpcsjQDBg7se5va-ZA2K67GYFxnosMJCw10ZzJhA", }, // The plugins this app bundles beyond fizzy's own: fizzyedit/example-plugin, as any // plugin from its own repo, and this app's own, which also builds alone as a dylib From f30fdf1daedd1a068d284e71277651dad97f7650 Mon Sep 17 00:00:00 2001 From: foxnne Date: Sat, 10 Oct 2026 11:26:53 -0500 Subject: [PATCH 4/4] dvui: floating windows as dvui draws them, dvui's demo among them Each floating window that goes out opens framed by the app (`viewports.Frame.app`): its own header and close button, no OS title bar. Any floating window can go out, by its dvui id, dvui's demo included; fizzy pinned at main with `viewports.Frame` and the overlapping-window press fix. Co-Authored-By: Claude Opus 5.5 --- build.zig.zon | 4 +- dvui/main.zig | 262 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 188 insertions(+), 78 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 0e080c8..89e1808 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ // by commit; fizzy's CI builds this repo against each of its pull requests, so a change // there that breaks an outside app fails before it lands. .fizzy = .{ - .url = "https://github.com/fizzyedit/fizzy/archive/940a13d8b193.tar.gz", - .hash = "fizzy-0.2.0-c46aVpcsjQDBg7se5va-ZA2K67GYFxnosMJCw10ZzJhA", + .url = "https://github.com/fizzyedit/fizzy/archive/189c7d5dac1b.tar.gz", + .hash = "fizzy-0.2.0-c46aVg6TjQCmiypW-eerXKRnlDgof9ss5ErBTUtH0aHO", }, // The plugins this app bundles beyond fizzy's own: fizzyedit/example-plugin, as any // plugin from its own repo, and this app's own, which also builds alone as a dylib diff --git a/dvui/main.zig b/dvui/main.zig index 38667a6..047f5a7 100644 --- a/dvui/main.zig +++ b/dvui/main.zig @@ -1,21 +1,30 @@ -//! A plain dvui app on fizzy's backend: dvui's own widgets, nothing of fizzy the editor, and each -//! floating window an OS window of its own where the backend has them (`viewports`). On a backend -//! without them — dvui's own SDL3 backend (`-Ddvui-backend=sdl3`), or Wayland, where a window -//! cannot be placed — the same floating windows stay in the main window, as in any dvui app. +//! A plain dvui app on fizzy's backend: dvui's own widgets and its demo, nothing of fizzy the +//! editor, and each floating window an OS window of its own where the backend has them +//! (`viewports`), looking just as it does in the main window: its own header and close button, no +//! OS title bar (`.app`). The main window is an ordinary OS window. On a backend without them — +//! dvui's own SDL3 backend (`-Ddvui-backend=sdl3`), or Wayland, where a window cannot be placed — +//! the same floating windows stay in the main window, as in any dvui app. //! //! There stays one `dvui.Window` and one frame. A floating window that is out is drawn in its //! viewport's band of the frame, far past the main window's edge, and its OS window shows that -//! band; the pointer over the OS window comes back to dvui as a pointer in the band. What it takes, -//! each frame: +//! band; the pointer over the OS window comes back to dvui as a pointer in the band. Any floating +//! window can go out, the app's own or dvui's demo: dvui keeps where each is (`_rect` in its data, +//! by its id), and the app moves that into the band and reads it back. What it takes, each frame: //! -//! - before drawing (`beforeFloats`): each floating window not out yet goes out into a viewport -//! (`viewports.open`), one the OS moved or resized follows its window (`osPlaced`), and each -//! band is a screen of dvui's (`dvui.screensSet`), so dvui keeps the floating window in it; -//! - the floating windows, drawn as dvui draws them anywhere (`drawFloat`); -//! - after drawing (`afterFloats`): dvui's dialogs and toasts made (`Window.drawRetained`), then -//! each OS window put where its floating window is (`viewports.place`), told where its header is -//! for the OS to move it by (`hints`), and handed its picture: every subwindow in its band, -//! taken out of the frame (`viewports.Picture`), so the main window draws none of it. +//! - before drawing (`beforeFloats`): a window the OS moved takes its floating window with it +//! (`osPlaced`), and each band is a screen of dvui's (`dvui.screensSet`), so dvui keeps the +//! floating window in it; +//! - drawing, as dvui draws anywhere, naming the floating windows to put out as they draw (`out`); +//! - after drawing (`afterFloats`): dvui's dialogs and toasts made (`Window.drawRetained`); a +//! window whose floating window was not drawn — closed — goes; each one named and not yet out +//! goes into a viewport over where it is (`viewports.open`); and each window is put where its +//! floating window is (`viewports.place`), told where its header is for the OS to move it by +//! (`hints`), and handed its picture: every subwindow in its band, taken out of the frame +//! (`viewports.Picture`), so the main window draws none of it. +//! +//! A floating window resizes itself from its edges, as dvui's does anywhere, and its OS window +//! follows (`place`); while it holds the pointer the pointer is read in its band, wherever it goes +//! (`pinPointer`). const std = @import("std"); const dvui = @import("dvui"); const backend = @import("backend"); @@ -23,7 +32,7 @@ const viewports = if (@hasDecl(backend, "viewports")) backend.viewports else @im pub const dvui_app: dvui.App = .{ .config = .{ .options = .{ - .size = .{ .w = 720, .h = 480 }, + .size = .{ .w = 960, .h = 640 }, .title = "dvui on fizzy's backend", } }, .frameFn = frame, @@ -32,41 +41,59 @@ pub const main = dvui.App.main; pub const panic = dvui.App.panic; pub const std_options: std.Options = .{ .logFn = dvui.App.logFn }; -/// The most floating windows at once: as many as the backend has viewports. Past what it can +/// The most floating windows out at once: as many as the backend has viewports. Past what it can /// open, a floating window stays in the main window. -const max_floats = 8; +const max_outs = 8; + +/// A floating window out 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 to put out, named as they draw this frame (`out`). +const Wanted = struct { id: dvui.Id, title: [:0]const u8 }; +var wanted: [max_outs]Wanted = undefined; +var wanted_n: usize = 0; + +/// How tall `dvui.windowHeader` is, natural: the strip the OS moves a window by. Measured on the +/// app's own floating windows; dvui's demo draws the same header. +var header_h: f32 = 32; +/// A floating window of the app's own. dvui keeps where it is, as for any floating window. const Float = struct { - /// Natural units of the frame: in the main window, or in its viewport's band once out. - rect: dvui.Rect, open: bool = true, + /// Where it first opens, natural, in the main window. + at: dvui.Rect, title_buf: [32:0]u8 = @splat(0), title_len: usize = 0, count: u32 = 0, - viewport: ?*viewports.Viewport = null, - target: ?dvui.Texture.Target = null, - /// Its header this frame, physical (`dvui.windowHeader`): where the OS moves its window from. - header: dvui.Rect.Physical = .{}, fn title(self: *const Float) [:0]const u8 { return self.title_buf[0..self.title_len :0]; } }; - -var floats: [max_floats]?Float = @splat(null); +var floats: [4]?Float = @splat(null); var made: u32 = 0; var started = false; fn frame() !dvui.App.Result { - // Two floating windows to begin with. + // dvui's demo and two floating windows of the app's own to begin with. if (!started) { started = true; + dvui.Examples.show_demo_window = true; newFloat(); newFloat(); } beforeFloats(); mainWindow(); for (&floats, 0..) |*slot, i| if (slot.*) |*f| drawFloat(f, i); + dvui.Examples.demo(.lite); + if (dvui.Examples.show_demo_window) if (demoWindow()) |id| out(id, "DVUI Demo"); afterFloats(); return .ok; } @@ -81,6 +108,9 @@ fn mainWindow() void { "This backend has no OS windows besides this one, so floating windows stay in it."; dvui.labelNoFmt(@src(), how, .{}, .{}); if (dvui.button(@src(), "New floating window", .{}, .{ .margin = .{ .y = 12 } })) newFloat(); + if (!dvui.Examples.show_demo_window) { + if (dvui.button(@src(), "Show the dvui demo", .{}, .{})) dvui.Examples.show_demo_window = true; + } } fn newFloat() void { @@ -89,57 +119,90 @@ fn newFloat() void { } else return; made += 1; const step: f32 = @floatFromInt(made % 6); - slot.* = .{ .rect = .{ .x = 80 + 32 * step, .y = 120 + 24 * step, .w = 280, .h = 180 } }; + slot.* = .{ .at = .{ .x = 80 + 32 * step, .y = 140 + 24 * step, .w = 280, .h = 180 } }; const f = &slot.*.?; const t: []const u8 = std.fmt.bufPrint(&f.title_buf, "Floating window {d}", .{made}) catch ""; f.title_len = t.len; } -fn release(f: *Float) void { - if (f.viewport) |vp| viewports.close(vp); - if (f.target) |t| t.destroyLater(); +fn drawFloat(f: *Float, i: usize) void { + var fw = dvui.floatingWindow(@src(), .{ .open_flag = &f.open }, .{ .id_extra = i, .rect = f.at, .min_size_content = .{ .w = 220, .h = 120 } }); + defer fw.deinit(); + const header = dvui.windowHeader(f.title(), "", &f.open); + // Moved by its header only, as dvui's demos do: elsewhere a press is its content's. + fw.dragAreaSet(header); + header_h = header.h / dvui.windowNaturalScale(); + out(fw.data().id, f.title()); + // Closed by its X: its window goes next frame, which comes now rather than at the next event. + if (!f.open) dvui.refresh(null, @src(), null); + var box = dvui.box(@src(), .{}, .{ .expand = .both, .padding = .all(12) }); + defer box.deinit(); + dvui.label(@src(), "Clicked {d} times", .{f.count}, .{}); + if (dvui.button(@src(), "Count", .{}, .{})) f.count += 1; +} + +/// dvui's demo's floating window, by its id: the subwindow its tag's rect is. dvui gives a floating +/// window's `tag` to the box it draws in, which fills it, not to the window itself. +fn demoWindow() ?dvui.Id { + const t = dvui.tagGet(dvui.Examples.demo_window_tag) orelse return null; + if (t.rect.w < 1 or t.rect.h < 1) return null; + for (dvui.currentWindow().subwindows.stack.items) |sw| { + const r = sw.rect_pixels; + if (@abs(r.x - t.rect.x) < 1 and @abs(r.y - t.rect.y) < 1 and @abs(r.w - t.rect.w) < 1 and @abs(r.h - t.rect.h) < 1) return sw.id; + } + return null; +} + +/// Put floating window `id` out in an OS window of its own, called `title` there: named each frame +/// it draws. +fn out(id: dvui.Id, title: [:0]const u8) void { + if (wanted_n == wanted.len) return; + wanted[wanted_n] = .{ .id = id, .title = title }; + wanted_n += 1; +} + +fn outOf(id: dvui.Id) ?*Out { + for (&outs) |*slot| if (slot.*) |*o| if (o.id == id) return o; + return null; +} + +fn release(o: *Out) void { + viewports.close(o.viewport); + if (o.target) |t| t.destroyLater(); +} + +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 }; } fn beforeFloats() void { + wanted_n = 0; + for (&floats) |*slot| if (slot.*) |f| if (!f.open) { + slot.* = null; + }; + if (comptime !viewports.supported) return; const s = dvui.windowNaturalScale(); - var screens: [max_floats]dvui.Rect.Natural = undefined; + var screens: [max_outs]dvui.Rect.Natural = undefined; var n: usize = 0; - for (&floats) |*slot| { - const f = if (slot.*) |*f| f else continue; - if (!f.open) { - release(f); - slot.* = null; - continue; - } - if (f.viewport == null and viewports.available()) { - // Out into an OS window over where it is in the main window; drawn in the band from now. - const at: viewports.Rect = .{ .x = f.rect.x * s, .y = f.rect.y * s, .w = f.rect.w * s, .h = f.rect.h * s }; - if (viewports.open(at, f.title())) |vp| { - f.viewport = vp; - const fr = viewports.frameOf(vp); - f.rect = .{ .x = fr.x / s, .y = fr.y / s, .w = fr.w / s, .h = fr.h / s }; - } - } - const vp = f.viewport orelse continue; - // Its close button, or the OS's (⌘W): gone next frame. - if (viewports.closeRequested(vp)) f.open = false; - // The OS moved or resized its window: the floating window follows it. - if (viewports.osPlaced(vp)) |fr| f.rect = .{ .x = fr.x / s, .y = fr.y / s, .w = fr.w / s, .h = fr.h / s }; - _ = viewports.osMoveEnded(vp); - screens[n] = .{ .x = f.rect.x, .y = f.rect.y, .w = f.rect.w, .h = f.rect.h }; + var pin: viewports.Pin = .none; + for (&outs) |*slot| { + const o = if (slot.*) |*o| o else continue; + // The OS moved its window: the floating window goes with it. + 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 = .{ .viewport = o.viewport }; } dvui.screensSet(screens[0..n]); -} - -fn drawFloat(f: *Float, i: usize) void { - var fw = dvui.floatingWindow(@src(), .{ .rect = &f.rect, .open_flag = &f.open }, .{ .id_extra = i, .min_size_content = .{ .w = 220, .h = 120 } }); - defer fw.deinit(); - f.header = dvui.windowHeader(f.title(), "", &f.open); - var box = dvui.box(@src(), .{}, .{ .expand = .both, .padding = .all(12) }); - defer box.deinit(); - dvui.label(@src(), "Clicked {d} times", .{f.count}, .{}); - if (dvui.button(@src(), "Count", .{}, .{})) f.count += 1; + viewports.pinPointer(pin); } fn afterFloats() void { @@ -148,27 +211,74 @@ fn afterFloats() void { // Dialogs and toasts are subwindows from here, so each lands in the window it is over. cw.drawRetained(.{}); const s = dvui.windowNaturalScale(); - for (&floats) |*slot| { - const f = if (slot.*) |*f| f else continue; - const vp = f.viewport orelse continue; - const shown = viewports.place(vp, .{ .x = f.rect.x * s, .y = f.rect.y * s, .w = f.rect.w * s, .h = f.rect.h * s }); - // Its header moves its window, but for the close button at its right end; its edges resize it. - const keep_w = f.header.h; - viewports.hints(vp, .{ - .drag = .{ .x = f.header.x, .y = f.header.y, .w = f.header.w, .h = f.header.h }, - .keep = .{ .x = f.header.x + f.header.w - keep_w, .y = f.header.y, .w = keep_w, .h = f.header.h }, + + // One no longer named — its X cleared what showed it, as dvui's demo's does — goes next frame, + // which comes now rather than at the next event. + for (outs) |slot| if (slot) |o| { + const named = for (wanted[0..wanted_n]) |w| { + if (w.id == o.id) break true; + } else false; + if (!named) dvui.refresh(null, @src(), null); + }; + // A window whose floating window was not drawn this frame — closed — goes. + 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) continue; + release(o); + slot.* = null; + } + + // Each floating window named and not out yet goes into a window of its own, opening over + // where it is in the main window; it is drawn in that window's band from the next frame. + for (wanted[0..wanted_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 on its first frame, which dvui spends measuring it, hidden, with no size yet. + if (r.w < 1 or r.h < 1) 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 — the top `header_h` — but for the close button, + // square at the header's height: at its right end where buttons go OK then Cancel + // (Windows), else at its left (`dvui.windowHeader`); and not from the floating window's + // own resize zones round its edge (`app_side`). + const hh = header_h * s; + const keep_x = if (cw.button_order == .ok_cancel) shown.x + shown.w - hh else shown.x; + viewports.hints(o.viewport, .{ + .drag = .{ .x = shown.x, .y = shown.y, .w = shown.w, .h = hh }, + .keep = .{ .x = keep_x, .y = shown.y, .w = hh, .h = hh }, .glass = shown, - .edge = 6 * s, + .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(&f.target, w, h) orelse continue; + 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(vp, target); + viewports.present(o.viewport, target); } }