Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 17 additions & 39 deletions backend/src/SDLBackend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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`)

Expand Down Expand Up @@ -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();

Expand Down
185 changes: 185 additions & 0 deletions backend/src/os_windows.zig
Original file line number Diff line number Diff line change
@@ -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;
}
25 changes: 19 additions & 6 deletions docs/DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
6 changes: 3 additions & 3 deletions sdk/build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@
// `fizzy-sdk-<version>` 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 = .{
Expand Down
Loading