Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8844748
core: glass that merges with its place's edge, and stays inside it
foxnne Oct 18, 2026
d0703ad
Merge main into core/glass-edge
foxnne Oct 18, 2026
07d45fb
core: glass that is the window's own material
foxnne Oct 18, 2026
b6e5a31
core: the glass program compiled for the window's own material
foxnne Oct 18, 2026
741290a
core: orb geometry
foxnne Oct 19, 2026
0c99c18
merge main into core/glass-edge
foxnne Oct 19, 2026
a55d15c
core: glass cut by its place with an edge of its own, and a rim lit f…
foxnne Oct 18, 2026
b3c2536
core: the orb — one material and one shadow for a control of glass, g…
foxnne Oct 19, 2026
9b0051f
app: the history grip is a vial in the card's corner, its liquid alive
foxnne Oct 18, 2026
507bc67
merge main into app/history-vial
foxnne Oct 19, 2026
fc0fcd0
app: the history bar goes back into its corner vial without a pop
foxnne Oct 19, 2026
f592308
automation: a playing tape rests in a glass pill under the title bar
foxnne Oct 19, 2026
1a0e123
Merge app/history-vial (main, the play button) into app/history-vial-…
foxnne Oct 19, 2026
1e93238
Merge app/history-vial-return (main, the play button) into automation…
foxnne Oct 19, 2026
51b3d95
automation: the tape rests in the window's own corner, marked with a …
foxnne Oct 19, 2026
870dcd0
core: orb geometry
foxnne Oct 19, 2026
9043580
automation: the tape bar stacks above a history bar it would cover
foxnne Oct 19, 2026
ae9b61b
app: the history grip is a vial in the card's corner, its liquid alive
foxnne Oct 18, 2026
8dcf15b
core: glass cut by its place with an edge of its own, and a rim lit f…
foxnne Oct 18, 2026
e231777
merge main into app/history-vial
foxnne Oct 19, 2026
29ab06e
core: the orb — one material and one shadow for a control of glass, g…
foxnne Oct 19, 2026
5a57b15
app: the vial takes only its glass, its liquid settles on slow frames…
foxnne Oct 19, 2026
cd733c0
Merge app/history-vial (the reviewed vial) into app/history-vial-return
foxnne Oct 19, 2026
12b4a3a
automation: merge the reviewed vial; centred controls whose icons rid…
foxnne Oct 19, 2026
b83f2d5
app: review fixes for the history bar's return into its vial
foxnne Oct 19, 2026
2762394
Merge app/history-vial-return (review fixes) into automation/tape-pill
foxnne Oct 19, 2026
3835529
automation: review fixes for the tape's corner and its stacking
foxnne Oct 19, 2026
468a7c2
merge main into core/orb
foxnne Oct 19, 2026
87dc2e3
merge core/orb (with main) into app/history-vial
foxnne Oct 19, 2026
327bd39
merge main into app/history-vial
foxnne Oct 19, 2026
86fda4b
Merge app/history-vial (main) into app/history-vial-return
foxnne Oct 19, 2026
fcb4205
Merge app/history-vial-return (main) into automation/tape-pill
foxnne Oct 19, 2026
0cd3239
Merge main (#635, #663, #660) into app/history-vial-return
foxnne Oct 19, 2026
5c3c7df
Merge app/history-vial-return (main) into automation/tape-pill
foxnne Oct 19, 2026
a7bd7a9
Merge main (#636, #664) into automation/tape-pill
foxnne Oct 19, 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
6 changes: 5 additions & 1 deletion app/HistoryTransport.zig
Original file line number Diff line number Diff line change
Expand Up @@ -316,12 +316,16 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
.liquid = if (opts.corner != null) &self.liquid else null,
};
if (p <= 0) if (opts.corner) |c| {
// Where it is, for a bar that would cover it to stand clear (`dome.barsOut`).
dome.publishBar(c.box(dvui.windowRectScale().s));
if (dome.drawCorner(c, id_extra, grip_look, "history.grip:{s}", .{doc.owner.documentPath(doc)})) self.stir(now);
} else if (gripRect(area, dome.radius, opts.edge)) |g| if (dome.draw(g, id_extra, grip_look, "history.grip:{s}", .{doc.owner.documentPath(doc)})) self.stir(now);
if (self.dwell.since != null or self.stepped_at != null or !self.slosh.settled(now) or self.presence.moving(in_use) or
(opts.corner != null and !self.liquid.settled())) dvui.refresh(null, @src(), null);
}
if (p <= 0) return pressed;
// Out: where, for a bar that would cover it to stand clear (`dome.barsOut`).
if (self.place(area, now)) |pl| dome.publishBar(pl);
const arena = dvui.currentWindow().arena();
const line = doc.owner.historyLine(doc, arena) orelse return pressed;
const n = line.entries.len;
Expand Down Expand Up @@ -555,7 +559,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
// (the tab into the bar). The one soft shadow of the merged shape is the orb's
// (`plans/LIQUID_EDGE_PLAN.md`); the rim and lens are what set the glass off meanwhile.
// Out of a corner vial, cut by the card's outline, curve and all, as the vial is at rest.
core.dialogs.glassBubbles(fid, shapes[0..count], s, .{ .merge = if (opts.corner != null) dome.cornerMerge(p, 14) else dome.merge(p, 14), .shadow = 0, .edge = if (opts.corner) |c| c.place() else null });
core.dialogs.glassBubbles(fid, shapes[0..count], s, .{ .merge = if (opts.corner != null) dome.homeMerge(p, 14) else dome.merge(p, 14), .shadow = 0, .edge = if (opts.corner) |c| c.place() else null });
}
// A corner vial's contents change with it rather than fade: its liquid drains as the stream
// pours out into the line and fills again as it pours back, and its icon shrinks into the
Expand Down
281 changes: 187 additions & 94 deletions app/automation/overlay.zig

Large diffs are not rendered by default.

65 changes: 57 additions & 8 deletions app/dome.zig
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,14 @@ pub fn merge(p: f32, settled: f32) f32 {
return std.math.lerp(28, settled, smooth((p - 0.3) / 0.6));
}

/// `merge` for a bar growing out of a corner vial: none at all at the very start and end, so the
/// glass the bar goes back into is the vial's own outline, not the vial swollen by the drops
/// sinking into it, and the vial at rest takes over from it without a step.
pub fn cornerMerge(p: f32, settled: f32) f32 {
/// `merge` for a bar growing out of a home its bubbles change back into (a corner vial: a card's,
/// or the window's): none at all at the very start and end, so the glass the bar goes back into is
/// the home's own outline, not the home swollen by the bubbles arriving on it, and the home at rest
/// takes over without a step.
pub fn homeMerge(p: f32, settled: f32) f32 {
return merge(p, settled) * smooth(p / corner_merge_in);
}
/// How far along the growing a corner vial's glass reaches out to the drops leaving it in full.
/// How far along the growing a home's glass reaches out to the bubbles leaving it in full.
const corner_merge_in: f32 = 0.25;

/// The clip that keeps the glass growing out of the dome at `from` from reaching past the edge it
Expand All @@ -158,6 +159,7 @@ pub fn tabRect(whole: dvui.Rect) dvui.Rect {
/// The tab's chevron, in the middle of its bubble `r` (window-natural; `bounds` the layer's).
pub fn chevron(bounds: dvui.Rect, r: dvui.Rect, color: dvui.Color) void {
const d = r.w * 0.44;
if (d < 2) return;
core.icon.icon(@src(), "dome_collapse", icons.tvg.lucide.@"chevron-down", .{
.stroke_color = .{ .color = color },
.fill_color = .{ .color = color },
Expand Down Expand Up @@ -337,6 +339,10 @@ const corner_icon: f32 = 13;
pub const Corner = struct {
card: dvui.Rect.Physical,
radii: [4]f32 = @splat(0),
/// The icon over its liquid, and the name it is cached by: the history's, or what else stands
/// in a corner (a playing tape's, in the window's own).
icon: []const u8 = icons.tvg.lucide.history,
icon_name: []const u8 = "history_grip",

fn curveIn(self: Corner) f32 {
return self.radii[2] * (std.math.sqrt2 - 1);
Expand Down Expand Up @@ -436,7 +442,7 @@ pub fn cornerIcon(c: Corner, t: f32, look: Look, bounds: dvui.Rect.Physical) voi
const full = c.iconCenter(s);
const ic: [2]f32 = .{ std.math.lerp(p[0], full[0], k), std.math.lerp(p[1], full[1], k) };
const d = corner_icon * k * s;
core.icon.icon(@src(), "history_grip", icons.tvg.lucide.history, .{
core.icon.icon(@src(), c.icon_name, c.icon, .{
.stroke_color = .white,
.fill_color = .white,
}, .{
Expand Down Expand Up @@ -570,6 +576,49 @@ fn pourStream(lip: [2]f32, end: [2]f32, tail: f32, head: f32, th: f32, color: dv
r.fill(.all(hw), .{ .color = .{ .color = color }, .fade = 1 });
}

// ── Bars out this frame ──────────────────────────────────────────────────────────────────────
//
// Each transport's bar that is out, and each corner vial at rest, says where it is (`publishBar`),
// so a bar that would cover it can stand clear (`barsOut`): the tape's bar, out along the bottom
// of the window, stacks above a pane's history vial or history bar under it. Per frame, kept in
// dvui's data, and only while something reads them (a tape is loaded): with no reader the last
// frame, publishing does nothing, so an app with no tape playing does no work for it.

const max_bars = 32;
const Bars = struct {
/// The frame they were published in (`dvui.frameTimeNS`): an earlier frame's are gone.
frame_ns: i128 = 0,
n: usize = 0,
rects: [max_bars]dvui.Rect.Physical = undefined,
/// Something read them this frame (`barsOut`), and the last frame: only then are they kept.
read: bool = false,
wanted: bool = false,
};

fn bars() *Bars {
const b = dvui.dataGetPtrDefault(null, dvui.Id.update(.zero, "app.dome.bars"), "_bars", Bars, .{});
const now = dvui.frameTimeNS();
if (b.frame_ns != now) b.* = .{ .frame_ns = now, .wanted = b.read };
return b;
}

/// A bar, or a vial at rest, is at `r` (physical, a bar with its tab) this frame. Nothing is kept
/// unless something read the last frame's (`barsOut`).
pub fn publishBar(r: dvui.Rect.Physical) void {
const b = bars();
if (!b.wanted or b.n == max_bars) return;
b.rects[b.n] = r;
b.n += 1;
}

/// The bars out this frame so far: those drawn before the caller, as the panes' are before the
/// demo's overlay.
pub fn barsOut() []const dvui.Rect.Physical {
const b = bars();
b.read = true;
return b.rects[0..b.n];
}

test "a bar going back into its corner vial ends as the vial, with no glass between" {
const c: Corner = .{ .card = .{ .x = 10, .y = 20, .w = 800, .h = 600 }, .radii = .{ 12, 12, 12, 12 } };
const s: f32 = 2;
Expand All @@ -580,6 +629,6 @@ test "a bar going back into its corner vial ends as the vial, with no glass betw
// Its home is the vial whole: the bubbles arriving are the vial's own outline.
try std.testing.expectEqual(home, core.widgets.orb.rectOf(c.shape(s, 1)));
// And no glass runs between them and the vial at the end, which would bulge it.
try std.testing.expectEqual(@as(f32, 0), cornerMerge(0, 14));
try std.testing.expectApproxEqAbs(@as(f32, 14), cornerMerge(1, 14), 0.001);
try std.testing.expectEqual(@as(f32, 0), homeMerge(0, 14));
try std.testing.expectApproxEqAbs(@as(f32, 14), homeMerge(1, 14), 0.001);
}
12 changes: 12 additions & 0 deletions core/screens.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ pub fn publishMainTop(top: f32) void {
dvui.dataSet(null, key_id, "_main_top", @max(0, top));
}

/// The main window's corner radius, natural: 0 maximized or full screen, else its frame's. The
/// app's, each frame, for what stands in the window's own corner (a playing tape's orb). 0 when it
/// publishes none.
pub fn publishMainRadius(radius: f32) void {
dvui.dataSet(null, key_id, "_main_radius", @max(0, radius));
}

/// The main window's corner radius (`publishMainRadius`).
pub fn mainRadius() f32 {
return dvui.dataGet(null, key_id, "_main_radius", f32) orelse 0;
}

/// The main window's screen: the window, below its title strip (`publishMainTop`). A dialog drawn
/// in the window and dragged up over the strip took no press there, and could be neither clicked nor
/// dragged back (the user); kept on this, it, a menu or a tooltip stays where the app takes presses.
Expand Down
8 changes: 5 additions & 3 deletions docs/AUTOMATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ While a demo plays:
behind.
- **The transport bar** (bottom) has chapter back / play-pause / chapter forward, the time, a
scrubber with chapter ticks, and close. Scrubbing backwards rewinds; forwards fast-forwards.
While the demo plays it hides into a dome on the window's bottom edge (`app/dome.zig`, the
same one a document's history transport hides into), filling with the demo's progress. The
pointer held on the dome (or a press on it) grows the bar back out; it goes back soon after
While the demo plays it rests in the window's own bottom-right corner, in the corner glass a
document's history rests in (`app/dome.zig`, `Corner`), cut by the window's outline and marked
with a tape, filling with liquid as the demo plays: a new chapter splashes it, a seek knocks it.
The pointer held on the corner (or a press on it) grows the bar out of it to the middle of the
window's bottom, above any pane's history vial or bar it would cover; it goes back soon after
the pointer leaves it. Paused or ended it stays out, unless its chevron tab sent it in.
- **Nothing is persisted.** The stage sets the user's session aside when a demo loads (project
folder, open documents, layout as on disk, plugin settings) and gives it back when it is
Expand Down
23 changes: 12 additions & 11 deletions sdk/replay/Player.zig
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,13 @@ pub const Transport = struct {
next: dvui.Rect.Physical = .{},
track: dvui.Rect.Physical = .{},
close: dvui.Rect.Physical = .{},
/// The tab over the bar that sends it into its dome (`tucked`).
/// The tab on the bar that sends it into where it rests (`tucked`).
tab: dvui.Rect.Physical = .{},
/// Where the bar's dome was drawn last frame, physical, as far round it as the pointer is on
/// it: what the bar hides into, and a press there brings it back. Null when it was not drawn.
dome: ?dvui.Rect.Physical = null,
/// Sent into its dome (its tab): the bar stays there, playing or not, until the dome brings it
/// Where the bar rests was drawn last frame (fizzy: corner glass in the window's own corner),
/// physical, as far round it as the pointer is on it: what the bar hides into, and a press
/// there brings it back. Null when it was not drawn.
rest: ?dvui.Rect.Physical = null,
/// Sent into where it rests (its tab): the bar stays there, playing or not, until that brings it
/// back (a press here, the pointer held on it in the overlay).
tucked: bool = false,
/// The speed on the track (`rate`), which a press steps through `speeds`.
Expand All @@ -227,8 +228,8 @@ pub const Transport = struct {
/// bar is drawn under them, so a press there is theirs, not the bar's.
covered: [8]dvui.Rect.Physical = undefined,
covered_n: u8 = 0,
/// When the real pointer was last on the bar, or the dome brought it (ns, `wallNs`): while
/// playing, the bar shows for a while after, then goes back into its dome.
/// When the real pointer was last on the bar, or its rest brought it (ns, `wallNs`): while
/// playing, the bar shows for a while after, then goes back to rest.
stirred_ns: ?i128 = null,
/// The bar opening (`wanted`) or closing, since `since_ns` (`wallNs`), and how open it was
/// last frame — kept by the overlay, which opens and closes it as a floating surface does.
Expand Down Expand Up @@ -889,14 +890,14 @@ fn transportTakes(self: *Player, e: *dvui.Event, me: dvui.Event.Mouse) bool {
tr.pointer = null;
return false;
};
// A press on the dome brings the bar out of it at once, and is not the app's.
if (tr.dome) |d| if (d.contains(me.p)) {
// A press where the bar rests brings it out at once, and is not the app's.
if (tr.rest) |d| if (d.contains(me.p)) {
if (me.action == .press and me.button.pointer() and !tr.closing) {
tr.tucked = false;
tr.stirred_ns = self.wallNs();
dvui.refresh(null, @src(), null);
}
// Its motion goes on to the app (the dome only sits on it); the rest is the dome's.
// Its motion goes on to the app (the rest only sits on it); the rest is the bar's.
if (me.action == .motion) return false;
e.handle(@src(), wd);
return true;
Expand Down Expand Up @@ -946,7 +947,7 @@ fn claimCursor(self: *Player) void {
if (tr.scrub != null) return dvui.cursorSet(.hand);
if (tr.pointer) |p| {
if (tr.tab.contains(p)) return dvui.cursorSet(.hand);
if (tr.dome) |d| if (d.contains(p)) return dvui.cursorSet(.hand);
if (tr.rest) |d| if (d.contains(p)) return dvui.cursorSet(.hand);
if (tr.bar) |bar| {
if (bar.contains(p)) {
for ([_]dvui.Rect.Physical{ tr.play, tr.prev, tr.next, tr.track, tr.close }) |r| {
Expand Down
7 changes: 7 additions & 0 deletions src/editor/Editor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4337,6 +4337,13 @@ pub fn tick(editor: *Editor) !dvui.App.Result {
// Where the main window's title bar takes presses, not the app (macOS): floating windows,
// menus and tooltips are kept below it (`core.screens.mainScreen`).
fizzy.core.screens.publishMainTop(if (builtin.target.os.tag == .macos) fizzy.backend.titlebarStripHeight(dvui.currentWindow()) else 0);
// Its corner's curve, for what stands in the window's own corner (a playing tape's orb).
{
const win = dvui.currentWindow();
// Square in full screen; maximized, too, but on macOS, where a zoomed window keeps its curve.
const square = fizzy.backend.isFullscreenChromeHidden(win) or (builtin.target.os.tag != .macos and fizzy.backend.isMaximized(win));
fizzy.core.screens.publishMainRadius(if (square) 0 else if (builtin.target.os.tag == .linux) Constants.linux_window_radius else fizzy.backend.viewports.windowRadius());
}
fizzy.core.LiquidField.publishEnabled(editor.app.settings.glass_shader and fizzy.core.material.get().kind == .liquid);
if (comptime builtin.target.cpu.arch == .wasm32) {
// Plugins the page has finished linking since last frame register now.
Expand Down
45 changes: 37 additions & 8 deletions tests/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5461,6 +5461,9 @@ var demo_player: automation.Player = undefined;
/// its own, and its next frame's time comes from the last one's (`dvui.testing.step`) anyway.
var demo_clock: i128 = 0;
var demo_clock_on: bool = true;
/// A bar out under the demo's (a document's history bar, as `HistoryTransport` publishes it), for
/// the demo's to stack above; physical. Null: none.
var demo_other_bar: ?dvui.Rect.Physical = null;

/// The app's frame function, as an app runs it: through the player, which repeats it unseen
/// while a seek catches up.
Expand All @@ -5484,6 +5487,7 @@ fn demoRun() !dvui.App.Result {
te.draw();
te.deinit();
}
if (demo_other_bar) |b| @import("app").dome.publishBar(b);
automation.overlay.draw(&demo_player);
return .ok;
}
Expand Down Expand Up @@ -5771,7 +5775,7 @@ test "demo: closed from its bar, the bar runs back together before the demo goes
try std.testing.expect(frames > 1);
}

test "demo: the bar goes into its dome and grows back out of it" {
test "demo: the bar goes into its corner and grows back out of it" {
var t = try demoCtx();
defer deinitDemo(&t);

Expand All @@ -5781,7 +5785,7 @@ test "demo: the bar goes into its dome and grows back out of it" {
const tr = &demo_player.transport;
try std.testing.expect(tr.openness > 0.99);

// Its tab sends it into the dome, ended as it is, and the dome full: the tape has played out.
// Its tab sends it into the corner, ended as it is, and the corner full: the tape has played out.
try std.testing.expect(tr.tab.w > 0);
_ = try dvui.currentWindow().addEventMouseMotion(.{ .pt = tr.tab.center() });
_ = try dvui.currentWindow().addEventMouseButton(.left, .press);
Expand All @@ -5791,26 +5795,26 @@ test "demo: the bar goes into its dome and grows back out of it" {
var frames: usize = 0;
while (tr.openness > 0 and frames < 60) : (frames += 1) _ = try dvui.testing.step(demoFrame);
try std.testing.expectEqual(@as(f32, 0), tr.openness);
const dome = tr.dome orelse return error.TestUnexpectedResult;
try std.testing.expect(dvui.tagGet("demo.dome") != null);
const corner = tr.rest orelse return error.TestUnexpectedResult;
try std.testing.expect(dvui.tagGet("demo.corner") != null);

// Passing over the dome brings nothing; held on it, the bar grows back out.
_ = try dvui.currentWindow().addEventMouseMotion(.{ .pt = dome.center() });
// Passing over the corner brings nothing; held on it, the bar grows back out.
_ = try dvui.currentWindow().addEventMouseMotion(.{ .pt = corner.center() });
for (0..3) |_| _ = try dvui.testing.step(demoFrame);
try std.testing.expectEqual(@as(f32, 0), tr.openness);
frames = 0;
while (tr.openness < 0.99 and frames < 200) : (frames += 1) _ = try dvui.testing.step(demoFrame);
try std.testing.expect(tr.openness > 0.99);
try std.testing.expect(!tr.tucked);

// In again, and a press on the dome brings it at once.
// In again, and a press on the corner brings it at once.
_ = try dvui.currentWindow().addEventMouseMotion(.{ .pt = tr.tab.center() });
_ = try dvui.currentWindow().addEventMouseButton(.left, .press);
_ = try dvui.testing.step(demoFrame);
_ = try dvui.currentWindow().addEventMouseButton(.left, .release);
frames = 0;
while (tr.openness > 0 and frames < 60) : (frames += 1) _ = try dvui.testing.step(demoFrame);
_ = try dvui.currentWindow().addEventMouseMotion(.{ .pt = tr.dome.?.center() });
_ = try dvui.currentWindow().addEventMouseMotion(.{ .pt = tr.rest.?.center() });
_ = try dvui.currentWindow().addEventMouseButton(.left, .press);
_ = try dvui.testing.step(demoFrame);
_ = try dvui.currentWindow().addEventMouseButton(.left, .release);
Expand All @@ -5819,6 +5823,31 @@ test "demo: the bar goes into its dome and grows back out of it" {
try std.testing.expect(!demo_player.diverged);
}

test "demo: the bar stacks above a history bar out under it, and comes back down" {
var t = try demoCtx();
defer deinitDemo(&t);
defer demo_other_bar = null;

demo_player.load(try demoTape(), .{});
try stepDemoUntil(.ended, 400);
for (0..20) |_| _ = try dvui.testing.step(demoFrame);
const tr = &demo_player.transport;
const rest = tr.bar orelse return error.TestUnexpectedResult;

// A bar comes out where the tape's is: the tape's eases up until it is clear above it.
demo_other_bar = .{ .x = rest.x + 40, .y = rest.y - 4, .w = 300, .h = rest.h + 8 };
for (0..60) |_| _ = try dvui.testing.step(demoFrame);
const up = tr.bar orelse return error.TestUnexpectedResult;
try std.testing.expect(up.y + up.h <= demo_other_bar.?.y);
try std.testing.expectEqual(rest.x, up.x);

// It goes: the tape's comes back down to its place.
demo_other_bar = null;
for (0..60) |_| _ = try dvui.testing.step(demoFrame);
const down = tr.bar orelse return error.TestUnexpectedResult;
try std.testing.expectApproxEqAbs(rest.y, down.y, 0.5);
}

test "demo: a replay that does not reach what playing did is caught" {
var t = try demoCtx();
defer deinitDemo(&t);
Expand Down
Loading