Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
1a0e123
Merge app/history-vial (main, the play button) into app/history-vial-…
foxnne Oct 19, 2026
870dcd0
core: orb geometry
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
b83f2d5
app: review fixes for the history bar's return into its vial
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
0cd3239
Merge main (#635, #663, #660) into app/history-vial-return
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
37 changes: 32 additions & 5 deletions app/HistoryTransport.zig
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,10 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
};
const stepped = if (self.grip_steps) |was| opts.steps > was else false;
self.grip_steps = opts.steps;
self.liquid.setLevel(fill);
// Out of a corner vial, the liquid has poured into the line: the vial's own drains as the
// bar comes out and fills again as it goes back, the one liquid all the way, so the vial at
// rest takes over from the bar with no change of surface.
self.liquid.setLevel(if (opts.corner != null) fill * (1 - dome.smooth(p / vial_drain)) else fill);
if (opts.corner) |c| {
self.liquid.step(now, c.point());
// Into a vial, the new step splashes the surface, as a drop landing would: after the
Expand All @@ -298,7 +301,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
// splashes for when a vial came.
if (stepped) self.liquid.splashIn();
}
const glow: f32 = if (self.stepped_at) |t| (if (now - t < step_glow_ns) 1 - @as(f32, @floatCast(@as(f64, @floatFromInt(now - t)) / @as(f64, @floatFromInt(step_glow_ns)))) else 0) else 0;
const glow = self.stepGlow(now);
if (glow == 0) self.stepped_at = null;
const in_use = on_grip or self.stepped_at != null or !self.slosh.settled(now);
const alpha = self.presence.ease(in_use, now);
Expand Down Expand Up @@ -497,6 +500,9 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
// once they are most of the way out. Without a grip, it fades.
const from: ?dvui.Rect.Physical = if (!opts.history or p >= 1) null else if (opts.corner) |c| c.box(s) else if (gripRect(area, dome.radius, opts.edge)) |g| wrs.rectToPhysical(g) else null;
const out = dome.smooth(p);
// What the bubbles bud out of and change back into: a dome's bud, or a corner vial's own circle.
const home: ?dvui.Rect.Physical = if (from) |f| if (opts.corner) |c| c.home(s) else dome.bud(f) else null;
const travel: f32 = if (from) |f| dome.bud(f).h else 0;
// Out of a corner vial, which is whole at rest, the glass is whole throughout: eased toward a
// dome's quiet presence, it was glass at partial alpha — a hole — and the vial at full alpha
// the frame it went back in, a flash.
Expand All @@ -509,7 +515,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
// strip grows, so the pictures sit inside them.
const line_radius = std.math.lerp(phys[1].h / 2, core.corners.scaled(core.corners.card) * s, @min(1, grown * 1.5));
for (phys, bubbles[0..5], 0..) |r, *b, i| b.* = .{
.rect = if (from) |f| dome.pull(f, r, p, bud_delays[i], bud_arcs[i]) else r,
.rect = if (home) |h| dome.pullFrom(h, travel, r, p, bud_delays[i], bud_arcs[i]) else r,
.radius = if (i == 1 and grown > 0) line_radius else null,
// Lit under the pointer only once the bar is out: moving, a lit bubble was a lighter drop
// among the rest, its light running across the necks as they joined.
Expand All @@ -528,7 +534,7 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
if (self.tab) {
tab_index = count;
bubbles[count] = .{
.rect = if (from) |f| dome.pull(f, tab_p, p, bud_delays[5], bud_arcs[5]) else tab_p,
.rect = if (home) |h| dome.pullFrom(h, travel, tab_p, p, bud_delays[5], bud_arcs[5]) else tab_p,
.light = if (p >= 1 and tab_p.contains(pt)) core.dialogs.bubble_hover else 0,
};
count += 1;
Expand All @@ -549,8 +555,18 @@ 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 = 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.cornerMerge(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
// corner with the vial and grows back with it, so the vial the bar ends as is the vial at rest.
if (opts.corner) |c| if (from != null) {
const t = 1 - dome.smooth(p);
// The vial's own liquid (drained by `setLevel` above) and its icon lit as at rest.
const look: dome.Look = .{ .fill = fill, .tilt = tilt, .liquid = &self.liquid, .lit = on_grip, .glow = self.stepGlow(now) };
dome.paintCornerLiquid(c, t, look);
dome.cornerIcon(c, t, look, float.data().borderRectScale().r);
};
// What is on the bubbles comes once they are most of the way out; the liquid comes with them.
const shape_alpha = dvui.alpha(1);
if (from != null) {
Expand Down Expand Up @@ -627,6 +643,17 @@ pub fn draw(self: *HistoryTransport, doc: sdk.DocHandle, area: dvui.Rect.Physica
const bud_delays = [6]f32{ 0.3, 0, 0.16, 0.08, 0.23, 0.36 };
const bud_arcs = [6]f32{ 1.3, 0.6, 1.0, 0.8, 1.15, 1.6 };

/// How far out of a corner vial the bar is when the vial's liquid has all poured out into the line
/// (and, coming back, when it starts to fill again).
const vial_drain: f32 = 0.45;

/// How bright the grip is from the step it took last, 1 as it lands to 0 after `step_glow_ns`.
fn stepGlow(self: *const HistoryTransport, now: i128) f32 {
const t = self.stepped_at orelse return 0;
if (now - t >= step_glow_ns) return 0;
return 1 - @as(f32, @floatCast(@as(f64, @floatFromInt(now - t)) / @as(f64, @floatFromInt(step_glow_ns))));
}

/// macOS's trim yellow.
const yellow: dvui.Color = .{ .r = 0xF5, .g = 0xC0, .b = 0x2E };

Expand Down
93 changes: 77 additions & 16 deletions app/dome.zig
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,31 @@ pub fn emerge(p: f32, delay: f32) f32 {
return core.motion.enterFull(std.math.clamp((p - delay) / (1 - delay), 0, 1));
}

/// Where a bubble of the bar is, `p` of the way out (0 in its grip `from`, 1 at its place `to`),
/// budding at `delay` (a share of the growing): a drop pulled out of the grip rather than a button
/// sliding. Small while it travels, inflating as it nears its place; along an arc, up and then
/// across, rather than straight, `arc` times the usual height (so bubbles leaving together fan out
/// rather than travel stacked); on one smooth curve (`smooth`) both ways, so going back is coming
/// played backwards — in the reverse order, as the later a bubble buds, the sooner it is in.
pub fn pull(from: dvui.Rect.Physical, to: dvui.Rect.Physical, p: f32, delay: f32, arc: f32) dvui.Rect.Physical {
/// Where a bubble of the bar is, `p` of the way out (0 at its `home` in the grip, 1 at its place
/// `to`), budding at `delay` (a share of the growing): a drop pulled out of the grip rather than a
/// button sliding. It changes from `home`'s shape to a drop up to `size` across as it sets off,
/// travels small and inflates as it nears its place; along an arc, up and then across, rather than
/// straight, `arc` times the usual height (so bubbles leaving together fan out rather than travel
/// stacked); on one smooth curve (`smooth`) both ways, so going back is coming played backwards —
/// in the reverse order, as the later a bubble buds, the sooner it is in — and one arriving takes
/// `home`'s shape. A dome's home is its bud (`bud`); a corner vial's is the vial's own circle
/// (`Corner.home`), so the bubbles going back become the vial, and the glass the bar ends as is the
/// vial at rest. All physical.
pub fn pullFrom(home: dvui.Rect.Physical, size: f32, to: dvui.Rect.Physical, p: f32, delay: f32, arc: f32) dvui.Rect.Physical {
const k = std.math.clamp((p - delay) / (1 - delay), 0, 1);
const t = smooth(k);
if (t <= 0) return bud(from);
const a = bud(from);
if (t <= 0) return home;
const a = home;
const ac: [2]f32 = .{ a.x + a.w / 2, a.y + a.h / 2 };
const bc: [2]f32 = .{ to.x + to.w / 2, to.y + to.h / 2 };
// Up off the straight line, most of the way out at the middle of the way.
const dist = @sqrt((bc[0] - ac[0]) * (bc[0] - ac[0]) + (bc[1] - ac[1]) * (bc[1] - ac[1]));
const lift = dist * pull_arc * arc * @sin(std.math.pi * std.math.clamp(t, 0, 1));
const c: [2]f32 = .{ std.math.lerp(ac[0], bc[0], t), std.math.lerp(ac[1], bc[1], t) - lift };
// A drop while it travels, as big as the grip's bud at most; its place's size once near it.
const drop = @min(a.h, @min(to.w, to.h) * pull_drop);
// Leaving home it goes from home's size to the drop's, so one arriving takes home's.
const sink = std.math.clamp(t / pull_sink, 0, 1);
const drop = std.math.lerp(a.h, @min(size, @min(to.w, to.h) * pull_drop), sink * sink * (3 - 2 * sink));
const grow = std.math.clamp((t - pull_inflate) / (1 - pull_inflate), 0, 1);
const g = grow * grow * (3 - 2 * grow);
const w = std.math.lerp(drop, to.w, g);
Expand All @@ -103,6 +109,8 @@ const pull_arc: f32 = 0.14;
// smallest shape, and a tiny drop is all rim — a darker drop among the rest.
const pull_drop: f32 = 0.7;
const pull_inflate: f32 = 0.55;
/// How far along a bubble has gone from its home's size to its drop's.
const pull_sink: f32 = 0.3;

/// The dome at `from` itself in the bar's glass while the bar is `p` of the way out, shrinking into
/// its edge as the bubbles leave it. Clip the glass to the edge (`aboveEdge`).
Expand All @@ -118,6 +126,15 @@ 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 {
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.
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
/// stands on; set it round the glass, and put the previous one back after.
pub fn aboveEdge(from: dvui.Rect.Physical) dvui.Rect.Physical {
Expand Down Expand Up @@ -343,6 +360,14 @@ pub const Corner = struct {
return .{ .x = p[0] - r, .y = p[1] - r, .w = r, .h = r };
}

/// What the bar's bubbles bud out of and change back into (`pullFrom`): the vial's own circle
/// on the corner point, so a bubble arriving is the vial.
pub fn home(self: Corner, s: f32) dvui.Rect.Physical {
const r = self.radiusPx(s);
const p = self.point();
return .{ .x = p[0] - r, .y = p[1] - r, .w = 2 * r, .h = 2 * r };
}

/// The orb, `t` of its size (1 whole, 0 nothing), on the corner point.
pub fn shape(self: Corner, s: f32, t: f32) core.widgets.orb.Shape {
return .circle(self.point(), self.radiusPx(s) * @max(0, t));
Expand Down Expand Up @@ -392,21 +417,32 @@ pub fn drawCorner(c: Corner, id_extra: usize, look: Look, comptime anchor_fmt: [
}
}
paintCorner(float.data().id, c, 1, look);
// The history icon over the liquid: grey at rest, the text colour while it is in use.
cornerIcon(c, 1, look, float.data().borderRectScale().r);
return pressed;
}

/// The history icon over the vial's liquid at `c`, the vial `t` of its size (1 whole), in the
/// widget whose border rect is `bounds` (physical): grey at rest, the text colour while it is in
/// use. It shrinks into the corner with the vial as the bar grows out of it, and grows back with
/// it (`HistoryTransport`), rather than fading.
pub fn cornerIcon(c: Corner, t: f32, look: Look, bounds: dvui.Rect.Physical) void {
const s = dvui.windowRectScale().s;
const k = std.math.clamp(t, 0, 1);
if (k * corner_icon < 1) return;
const theme = dvui.themeGet();
const grey = theme.color(.window, .fill).lerp(if (theme.dark) dvui.Color.white else dvui.Color.black, 0.3).opacity(1);
const lit: f32 = if (look.lit) 1 else std.math.clamp(@max(look.glow, look.held), 0, 1);
const ic = c.iconCenter(s);
const d = corner_icon * s;
const bounds = float.data().borderRectScale().r;
const p = c.point();
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, .{
.stroke_color = .white,
.fill_color = .white,
}, .{
.rect = .{ .x = (ic[0] - d / 2 - bounds.x) / s, .y = (ic[1] - d / 2 - bounds.y) / s, .w = corner_icon, .h = corner_icon },
.rect = .{ .x = (ic[0] - d / 2 - bounds.x) / s, .y = (ic[1] - d / 2 - bounds.y) / s, .w = corner_icon * k, .h = corner_icon * k },
.color_text = .{ .color = grey.lerp(theme.color(.content, .text), lit) },
});
return pressed;
}

/// The vial's pixels at `c`, `t` of its size (1 whole; less while the bar grows out of it): its
Expand All @@ -422,6 +458,17 @@ pub fn paintCorner(id: dvui.Id, c: Corner, t: f32, look: Look) void {
// Lit under the pointer, lighter still as the wait before the bar runs.
.light = if (look.lit) std.math.lerp(core.dialogs.bubble_hover, core.dialogs.bubble_press, look.held) else 0,
});
paintCornerLiquid(c, t, look);
}

/// The liquid in the vial at `c`, `t` of its size, up to its level and tipped by its slosh, without
/// its glass: the vial's own, and the bar going back draws it filling again inside the bar's glass.
pub fn paintCornerLiquid(c: Corner, t: f32, look: Look) void {
const s = dvui.windowRectScale().s;
const prev = dvui.clip(c.card);
defer dvui.clipSet(prev);
const shape = c.shape(s, t);
if (shape.half[0] < 1) return;
if (look.fill <= 0) return;
// The vessel: the glass a little in from its rim, cut by the card a little in from its edge.
const inset = liquid_inset * s;
Expand Down Expand Up @@ -522,3 +569,17 @@ fn pourStream(lip: [2]f32, end: [2]f32, tail: f32, head: f32, th: f32, color: dv
const r: dvui.Rect.Physical = .{ .x = h[0] - hw, .y = h[1] - hw, .w = 2 * hw, .h = 2 * hw };
r.fill(.all(hw), .{ .color = .{ .color = color }, .fade = 1 });
}

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;
const home = c.home(s);
const to: dvui.Rect.Physical = .{ .x = 300, .y = 500, .w = 68, .h = 68 };
// Every bubble arrives as the home, whatever it budded at.
for ([_]f32{ 0, 0.16, 0.36 }) |delay| try std.testing.expectEqual(home, pullFrom(home, bud(c.box(s)).h, to, 0, delay, 1));
// 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);
}
Loading