Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
338ac08
popout: the drag's glass as one overlay of Liquid Glass, wherever the…
foxnne Oct 5, 2026
75648a0
popout: floats are windows and the drag's glass is the OS's by defaul…
foxnne Oct 5, 2026
77a2b0d
native glass: one slider from the clear lens to the window's colour, …
foxnne Oct 5, 2026
c3d340e
native glass: the frost fades out toward the glass's edges, its rim t…
foxnne Oct 5, 2026
72beca7
glass: one mapping from the slider to every glass, and the app's glas…
foxnne Oct 5, 2026
efc21e0
native glass: the carried drop is its head alone
foxnne Oct 5, 2026
e819236
native glass: the carried drop's tail back, the window's colour drawn…
foxnne Oct 5, 2026
fad2f5b
macOS 26: the main window and float windows are Liquid Glass, with a …
foxnne Oct 5, 2026
22ad0e3
macOS 26 window glass: a blurred body fading into a clear lens rim, i…
foxnne Oct 5, 2026
2547e29
glass: frosted glass with a clearing bevel everywhere — the colour ov…
foxnne Oct 5, 2026
fb4366f
macOS 26 glass: the bevel stays clear lens until the top, frost under…
foxnne Oct 5, 2026
734a222
glass: the slider is roughness — frost first, the window's colour aft…
foxnne Oct 5, 2026
96beb49
drop zones: the bubbles rest joined by a full neck, and glass joins f…
foxnne Oct 5, 2026
bd5b36d
macOS 26 window glass: the OS reads a window's shadow again when its …
foxnne Oct 5, 2026
b4dbcf0
drop glass: the aimed bubble swells and glows, the drop holds still o…
foxnne Oct 5, 2026
56935bf
windows: floats stay over the main window as it comes forward, full s…
foxnne Oct 5, 2026
a597aa3
drop: smooth again on a bubble, and a middle bubble a quarter bigger
foxnne Oct 5, 2026
1f0abc5
macOS 26: a float's glass window dressed as the main window — its sha…
foxnne Oct 5, 2026
65f5c7a
drop zones: apart at rest, the aimed bubble swelling until it joins
foxnne Oct 5, 2026
70da723
full screen: no toolbar lingering on the way in, the fade out not wai…
foxnne Oct 6, 2026
47a266c
drop zones: further apart at rest, a bigger swell to join
foxnne Oct 6, 2026
64a349a
drop glass: water again — a light frost over the lens at most
foxnne Oct 6, 2026
0bf7e6b
menus: context menus in native popup windows, fizzy drawing them (macOS)
foxnne Oct 6, 2026
8d8eb0b
settings: floats as windows, native glass and native menus; one resta…
foxnne Oct 6, 2026
5cbeb8f
menus: follow the window they opened in, as child windows (macOS)
foxnne Oct 6, 2026
8e36c04
floats: closing one moves what it holds back into the main window, lo…
foxnne Oct 6, 2026
69aedee
dialogs: in native windows of their own, as menus are (macOS)
foxnne Oct 6, 2026
f55aa66
context menus: a right-click opens one in a window that is not the fo…
foxnne Oct 6, 2026
2860463
dialogs: a dialog belongs to the window it opened in, and rides on a …
foxnne Oct 6, 2026
03ab656
drag glass: the OS handed no change under half a point, so joins hold…
foxnne Oct 6, 2026
78e8880
drop glass: frosted enough to read an icon over a busy background
foxnne Oct 6, 2026
05b003d
drag glass: drops trail the carried view, parting when it moves fast …
foxnne Oct 6, 2026
4066f76
floats: the window grows out of drops, the trail running into it (mac…
foxnne Oct 6, 2026
402333a
drag glass: the trail laid by the hand's pace, tight to the drop, not…
foxnne Oct 6, 2026
a7b947d
drag glass: the drop gives — stretched as it is set going, wobbling a…
foxnne Oct 6, 2026
c503eea
drag glass: one drop that sloshes, in place of the eight-point give
foxnne Oct 6, 2026
cd47046
floats: a window growing out of the drag's glass is never under its lens
foxnne Oct 6, 2026
7904dcc
drag glass: no springs — the drop moves only as the hand does
foxnne Oct 6, 2026
3b4ee5d
window: the fade out of full screen runs without waiting for an event
foxnne Oct 6, 2026
158c6b5
floats: a float's window stacks as any window does, not kept over the…
foxnne Oct 6, 2026
c2725d3
drop glass: bubbles blurred through, the carried drop a clear lens ov…
foxnne Oct 6, 2026
cc60d66
explorer: a file is the view drag from the moment it is lifted, narro…
foxnne Oct 7, 2026
2cfaac7
drag glass: the drop held still under the hand, its photograph shown,…
foxnne Oct 7, 2026
25d4932
drag glass: the drop's snapshot under its lens, fitted and frosted; t…
foxnne Oct 7, 2026
cf3139a
drag glass: the snapshot still in its lens, under every bubble; the d…
foxnne Oct 7, 2026
787b05e
drag glass: the drop pours into the bubble it joins; its name as the …
foxnne Oct 7, 2026
1e60139
drag glass: the carried drop frosted as the bubbles are, its picture …
foxnne Oct 7, 2026
4386904
floats: the drop's snapshot grows with the window out of it
foxnne Oct 7, 2026
8a2bee4
drag glass: the drop's snapshot closer, on what the view shows; a flo…
foxnne Oct 7, 2026
8cac74f
window: the way into and out of full screen
foxnne Oct 7, 2026
8f63eb7
window: full screen grows the window itself
foxnne Oct 7, 2026
d9610a2
explorer: no scrollbar beside a pane that scrolls itself as the windo…
foxnne Oct 7, 2026
53b5701
window: the same size out of full screen as it went in
foxnne Oct 7, 2026
f68f714
window: full screen transition frames
foxnne Oct 7, 2026
c4a89ac
dialogs: the button row whole
foxnne Oct 7, 2026
dd4aa9c
drag glass: the carried view's picture a touch frosted
foxnne Oct 7, 2026
236b32b
floats: one let go from a drop comes down over the main window
foxnne Oct 7, 2026
dc7bd20
window: no pop as it lands out of full screen
foxnne Oct 7, 2026
a9af78a
web: the glass as it was, clear liquid glass with no slider's frost o…
foxnne Oct 7, 2026
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
7 changes: 7 additions & 0 deletions app/layout/Floats.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub const Landing = struct {
photo: ?dvui.Texture = null,
/// The size it was taken at, physical, for its proportions.
photo_size: dvui.Size.Physical = .{},
/// Where the whole of it lay in the drop it was carried in, physical, where the app showed it
/// on the drag's glass (`ViewDrag.photo_last`): it grows from there to where the view lies in
/// the window (`Popout.growFrame`). Null where it was not shown so.
photo_from: ?dvui.Rect.Physical = null,
};

pub const Float = struct {
Expand Down Expand Up @@ -119,6 +123,9 @@ pub const Viewport = struct {
/// window's frame, this frame (`Popout`) — for a drag to read the window where it lies over the
/// main window's places (`ViewDrag.mapOccluders`).
main_delta: dvui.Point.Physical = .{},
/// Its window lies under the main window in the OS's stacking (`Popout`): a float a window like
/// any other, clicked behind the main one. Over the main window it covers nothing there.
under_main: bool = false,
};


Expand Down
12 changes: 8 additions & 4 deletions app/layout/SPLITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,14 @@ by again, the drop zones, splits, the picker. A place of several views keeps the
- **Moving and stacking.** A float moves by its header and resizes from its edges, no smaller than
160×96. A press anywhere in it brings it to the front, as on an OS window.
- **Back again.** Dragged out by its corner button, a view lands like any other, and a float its
last view leaves closes behind it. Closed from its header (or the picker's Remove), a float
sends each view home — into the list of the place it floated out of when the user had arranged
that place, otherwise let go for its keywords to place, which never freezes a place its keywords
fill. Every place a split of it made goes with it.
last view leaves closes behind it. Closed from its header, its window's close button (or the
picker's Remove), a float moves everything it holds back into the main window and loses
nothing (`float_rules.goHome`): each view into the list of the place it floated out of when the
user had arranged that place; let go where its keywords show it, which never freezes a place
its keywords fill; and a view they would show nowhere — one merged in from elsewhere, a
plugin's tools beside Files — into that place's list all the same, as a drop there writes it,
or the first place of the main window's that shows several where that place is gone or holds
something else. Every place a split of it made goes with it.
- **Remembered.** `layout.zon` keeps each float's window, its place in the stack and its home. One
with nothing left to show is not brought back, and one saved on a bigger window comes back onto
the window it opens in. A float remembers only where the user put it: a window that shrinks shows
Expand Down
644 changes: 537 additions & 107 deletions app/layout/ViewDrag.zig

Large diffs are not rendered by default.

77 changes: 68 additions & 9 deletions app/layout/float_rules.zig
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ pub fn initialRect(source: Rect, window: Rect) Rect {
return inside(.{ .x = source.centerX() - w / 2, .y = source.centerY() - h / 2, .w = w, .h = h }, window);
}

/// Where a float opens for a view carried out of `source` and let go as a drop with its picture:
/// the place itself, its size and where it was, kept inside the window and no smaller than a float
/// may be resized to — so the picture it grows with lands on the view as the float shows it (the
/// user: the size the popped out region was, aligned with its snapshot).
pub fn asTaken(source: Rect, window: Rect) Rect {
const cap_w = @max(0, window.w - 2 * margin);
const cap_h = @max(0, window.h - 2 * margin);
return inside(.{
.x = source.x,
.y = source.y,
.w = @min(@max(source.w, resize_min_w), cap_w),
.h = @min(@max(source.h, resize_min_h), cap_h),
}, window);
}

/// A float opened from the float `from`: the same size, a step down and right, kept inside.
pub fn nudged(from: Rect, window: Rect) Rect {
return inside(.{ .x = from.x + nudge, .y = from.y + nudge, .w = from.w, .h = from.h }, window);
Expand Down Expand Up @@ -171,9 +186,13 @@ pub const Home = struct {
shows_many: bool,
/// It shows nothing now.
empty: bool,
/// Let go, the view's keywords would show it in a place of the main window's of their own
/// accord — a view merged into the float from elsewhere may have keywords no place answers.
keywords_place: bool,
};

/// What closing a float does with one of its views.
/// What closing a float does with one of its views. Nothing a float holds is lost: every view
/// comes back into the main window.
pub const GoHome = enum {
/// Added to its home's list, beside what is there.
add,
Expand All @@ -183,12 +202,25 @@ pub const GoHome = enum {
/// its keywords fill. Writing it into such a place would freeze the place's list against
/// every view a plugin registers from then on.
keywords,
/// Into another place of the main window's that shows several: its home is gone or holds
/// something else, and its keywords would show it nowhere.
elsewhere,
};

pub fn goHome(home: Home) GoHome {
if (!home.declared or !home.assigned) return .keywords;
if (home.shows_many) return .add;
return if (home.empty) .put else .keywords;
// A home the user arranged takes it back as it left: beside what is there, or alone where
// nothing is.
if (home.declared and home.assigned) {
if (home.shows_many) return .add;
if (home.empty) return .put;
}
// Brought back by its keywords: let go, so no place's list is written down for it.
if (home.keywords_place) return .keywords;
// Let go, it would be shown nowhere: written into its home all the same, as a drop there
// writes it; where the home is gone or full, into another place.
if (home.declared and home.shows_many) return .add;
if (home.declared and home.empty) return .put;
return .elsewhere;
}

// ── Tests ──────────────────────────────────────────────────────────────────────────────────────
Expand All @@ -203,6 +235,22 @@ test "a float opens at a share of its place, centred on it" {
try std.testing.expectApproxEqAbs(@as(f32, 500), r.y + r.h / 2, 0.01);
}

test "a float let go with its picture opens as its place was, kept on the window" {
const r = asTaken(.{ .x = 400, .y = 100, .w = 1000, .h = 800 }, test_window);
try std.testing.expectEqual(@as(f32, 400), r.x);
try std.testing.expectEqual(@as(f32, 100), r.y);
try std.testing.expectEqual(@as(f32, 1000), r.w);
try std.testing.expectEqual(@as(f32, 800), r.h);
// A sidebar's narrow place keeps its width, above what a float may be resized to.
const side = asTaken(.{ .x = 0, .y = 40, .w = 240, .h = 900 }, test_window);
try std.testing.expectEqual(@as(f32, 240), side.w);
try std.testing.expect(side.x >= margin and side.y + side.h <= test_window.h - margin);
// Nothing smaller than that.
const tiny = asTaken(.{ .x = 300, .y = 300, .w = 50, .h = 40 }, test_window);
try std.testing.expectEqual(resize_min_w, tiny.w);
try std.testing.expectEqual(resize_min_h, tiny.h);
}

test "a float out of a small place opens at the minimum" {
const r = initialRect(.{ .x = 0, .y = 0, .w = 200, .h = 120 }, test_window);
try std.testing.expectEqual(open_min_w, r.w);
Expand Down Expand Up @@ -296,13 +344,24 @@ test "a view floats out of its own place's middle, unless it has no place or is

test "closing a float sends a view home without freezing a place its keywords fill" {
// The sidebar, filled by keywords: let go, and the keywords bring it back.
try std.testing.expectEqual(GoHome.keywords, goHome(.{ .declared = true, .assigned = false, .shows_many = true, .empty = false }));
try std.testing.expectEqual(GoHome.keywords, goHome(.{ .declared = true, .assigned = false, .shows_many = true, .empty = false, .keywords_place = true }));
// A panel the user arranged: added beside what is there.
try std.testing.expectEqual(GoHome.add, goHome(.{ .declared = true, .assigned = true, .shows_many = true, .empty = false }));
try std.testing.expectEqual(GoHome.add, goHome(.{ .declared = true, .assigned = true, .shows_many = true, .empty = false, .keywords_place = true }));
// A slot the user arranged, emptied by the float: the view goes back in.
try std.testing.expectEqual(GoHome.put, goHome(.{ .declared = true, .assigned = true, .shows_many = false, .empty = true }));
try std.testing.expectEqual(GoHome.put, goHome(.{ .declared = true, .assigned = true, .shows_many = false, .empty = true, .keywords_place = true }));
// Something else went there since: the view does not evict it.
try std.testing.expectEqual(GoHome.keywords, goHome(.{ .declared = true, .assigned = true, .shows_many = false, .empty = false }));
try std.testing.expectEqual(GoHome.keywords, goHome(.{ .declared = true, .assigned = true, .shows_many = false, .empty = false, .keywords_place = true }));
// The place is gone (a split since closed).
try std.testing.expectEqual(GoHome.keywords, goHome(.{ .declared = false, .assigned = true, .shows_many = true, .empty = true }));
try std.testing.expectEqual(GoHome.keywords, goHome(.{ .declared = false, .assigned = true, .shows_many = true, .empty = true, .keywords_place = true }));
}

test "closing a float loses no view its keywords would show nowhere" {
// Merged in from elsewhere, into a float out of the sidebar its keywords fill: the sidebar
// takes it, beside what its keywords show there.
try std.testing.expectEqual(GoHome.add, goHome(.{ .declared = true, .assigned = false, .shows_many = true, .empty = false, .keywords_place = false }));
// A slot something else went into since, or a home gone: another place of the main window's.
try std.testing.expectEqual(GoHome.elsewhere, goHome(.{ .declared = true, .assigned = true, .shows_many = false, .empty = false, .keywords_place = false }));
try std.testing.expectEqual(GoHome.elsewhere, goHome(.{ .declared = false, .assigned = false, .shows_many = false, .empty = true, .keywords_place = false }));
// An empty slot takes it back in.
try std.testing.expectEqual(GoHome.put, goHome(.{ .declared = true, .assigned = false, .shows_many = false, .empty = true, .keywords_place = false }));
}
83 changes: 83 additions & 0 deletions app/restart.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! Restarting the application: quitting the way it always quits, then starting again.
//!
//! One path for every reason to restart — a setting the app takes only at launch, an update
//! downloaded and waiting to be swapped in. `request` posts the app's ordinary quit, so unsaved
//! documents are asked about first and a quit the user calls off calls the restart off with it.
//! Once the app has torn down, `relaunch` starts it again: through Velopack when an update is
//! waiting (it swaps the install, then starts the new version), as this executable otherwise.

const std = @import("std");
const builtin = @import("builtin");
const dvui = @import("dvui");
const core = @import("core");
const update_install = @import("update/update_install.zig");

/// A browser tab has nothing to start again.
pub const supported = builtin.target.cpu.arch != .wasm32;

var requested_: bool = false;
/// Frames since the restart's quit was posted (`tick`).
var quit_frames: u8 = 0;

/// Quit and start again. From any handler on the GUI thread.
pub fn request() void {
if (comptime !supported) return;
requested_ = true;
quit_frames = 0;
dvui.refresh(null, @src(), null);
}

/// Whether a restart is under way: its quit posted, the app not yet called back from it.
pub fn requested() bool {
return requested_;
}

/// Once a frame, before the app reads the frame's quit events. `quitting`: the app is still in
/// the middle of a quit — a question about unsaved documents open, saves running.
///
/// The restart's quit goes in ahead of the frame's own, so it is answered like any other: with
/// nothing unsaved the frame ends closing, and `relaunch` runs at teardown. Still here two frames
/// on and no longer quitting, the user kept the app open — and called the restart off with it.
pub fn tick(quitting: bool) void {
if (comptime !supported) return;
// An update the user asked for has finished downloading: installing it is a restart.
if (update_install.takeDownloaded()) request();
if (!requested_) return;
if (quit_frames == 0) {
dvui.currentWindow().addEventApp(.{ .action = .quit }) catch {};
} else if (quit_frames > 1 and !quitting) {
requested_ = false;
}
quit_frames +|= 1;
}

/// At the very end of teardown — after the app's windows, its documents and its single-instance
/// listener are gone, so the new instance owns the lock rather than handing its launch to this
/// one. No-op unless a restart was asked for.
pub fn relaunch(io: std.Io, gpa: std.mem.Allocator) void {
if (comptime !supported) return;
if (!requested_) return;
// Velopack waits for this process to exit, swaps the install and starts the new version.
if (update_install.applyAtExit()) return;
relaunchSelf(io, gpa);
}

/// This executable again, as a process of its own that nothing waits on. On macOS through
/// `core.darwin_spawn` — std's spawn crashes after any `unsetenv`.
fn relaunchSelf(io: std.Io, gpa: std.mem.Allocator) void {
var buf: [std.fs.max_path_bytes]u8 = undefined;
const n = std.process.executablePath(io, &buf) catch |err| {
dvui.log.err("restart: could not find this executable: {s}", .{@errorName(err)});
return;
};
const argv: []const []const u8 = &.{buf[0..n]};
if (comptime builtin.os.tag.isDarwin()) {
_ = core.darwin_spawn.spawn(gpa, .{ .argv = argv, .stdin = .discard, .stdout = .discard, .stderr = .discard }, null) catch |err| {
dvui.log.err("restart: could not launch again: {s}", .{@errorName(err)});
};
return;
}
_ = std.process.spawn(io, .{ .argv = argv, .stdin = .ignore, .stdout = .ignore, .stderr = .ignore }) catch |err| {
dvui.log.err("restart: could not launch again: {s}", .{@errorName(err)});
};
}
5 changes: 5 additions & 0 deletions app/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ pub const update = struct {
pub const update_notify = @import("update/update_notify.zig");
};

/// Quitting the way the app always quits — unsaved documents asked about first — then starting
/// again: for a setting the app takes only at launch, and for installing a downloaded `update`.
/// The app asks with `request`, runs `tick` each frame and `relaunch` at the end of teardown.
pub const restart = @import("restart.zig");

/// Demos that drive the real app — a tape of timed input played into dvui, which a viewer can
/// pause, take over, rewind and resume. `Tape` is the format, `Sequencer` the deterministic engine
/// that replays it, `Script` the high-level way to write one, `Player` + `overlay` the dvui half
Expand Down
20 changes: 20 additions & 0 deletions app/settings/Settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ motion_speed: f32 = core.motion.default_speed,
/// compare the two.
glass_shader: bool = true,

/// Whether a float can leave the main window into an OS window of its own, and a view be carried
/// past the main window to open one there. Taken at launch: a change waits for a restart, which the
/// app says (`Editor.restartPending`). On by default on macOS; `FIZZY_POPOUT` overrides it.
float_windows: bool = builtin.os.tag == .macos,

/// Whether a view drag's glass — the carried view and the drop zones — is the OS's own (Liquid Glass,
/// macOS 26) rather than the app's, where floats are windows. Applied at once; `FIZZY_NATIVE_GLASS`
/// overrides it.
native_glass: bool = true,

/// Whether menus open in OS windows of their own — the OS's material, shadow and corners, over every
/// window, past the window's edge — rather than drawn inside the window they open from, where floats
/// are windows. Applied at once; `FIZZY_NATIVE_MENUS` overrides it.
native_menus: bool = true,

/// Whether dialogs open in OS windows of their own — the OS's material, shadow and corners, riding
/// on the main window — rather than drawn inside it, where floats are windows. Applied at once;
/// `FIZZY_NATIVE_DIALOGS` overrides it.
native_dialogs: bool = true,

/// Canvas zoom/pan control scheme shared by the image viewer, pixi, and any other
/// `CanvasWidget` consumer. `auto` picks mouse vs trackpad from `dvui.mouseType()`.
input_scheme: InputScheme = .auto,
Expand Down
4 changes: 0 additions & 4 deletions app/update/auto_update.zig
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,5 @@ pub const UpdateInstallError = error{
InstallLayoutUnsupported,
CheckFailed,
DownloadFailed,
/// `vpkc_wait_exit_then_apply_updates` returned false — the helper rejected
/// the asset (commonly: code-signing mismatch, channel mismatch, or the helper
/// binary couldn't be spawned). See `vpkc_get_last_error` in the logs.
ApplyFailed,
OutOfMemory,
};
Loading
Loading