summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/effects.md7
-rw-r--r--src/animation.zig2
-rw-r--r--src/draw.zig231
-rw-r--r--src/gui/gui.zig54
-rw-r--r--src/pardes.zig4
-rw-r--r--test/gui-goldens.txt2
6 files changed, 258 insertions, 42 deletions
diff --git a/docs/effects.md b/docs/effects.md
index 8564b756..a0edc2ab 100644
--- a/docs/effects.md
+++ b/docs/effects.md
@@ -96,6 +96,13 @@ Which principles each motion uses:
the flavour's arc), anticipation (playful's wind-up). A step of a cell or
less, or any move in insert mode, lands at once, so typing never trails;
a scroll carries the cursor with the text. Only the focused cursor moves.
+ A pane that moves carries it too: the springs keep its place in the pane
+ and the quad is drawn through the pane's presented box, so through a
+ slide, a zoom or a drag it rides with the text, and a layout change with
+ no transition lands it with the text. A focus change (to another pane, or
+ into a column's or the workspace's tag) glides from where the cursor is
+ drawn to the new one, free of the old pane's edges while it crosses
+ (.scratch/render/cursor/move/: ride-*, cross-*).
Notes on G1: the lift and the dim run on one focus spring per pane, at the
diff --git a/src/animation.zig b/src/animation.zig
index 840412f3..19f7ce0b 100644
--- a/src/animation.zig
+++ b/src/animation.zig
@@ -1002,7 +1002,7 @@ pub const CursorGlide = struct {
}
}
- fn centreAt(glide: *const CursorGlide, now_ns: u64) [2]f32 {
+ pub fn centreAt(glide: *const CursorGlide, now_ns: u64) [2]f32 {
var c: [2]f32 = .{ 0, 0 };
for (glide.corners) |corner| for (&c, corner) |*sum, axis| {
sum.* += axis.value(now_ns) / 4;
diff --git a/src/draw.zig b/src/draw.zig
index b0b16e12..15411770 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -212,6 +212,14 @@ fn focusedCursorBox(p: *const Pardes, s: *const Surface) ?animation.Box {
/// at the Motion flavour's pace, a jump gliding and a step or a keystroke in
/// insert mode landing at once; blinking unless typing. A pixel shell's: a
/// grid's cursor is the terminal's own.
+///
+/// The springs hold the cursor where the pane's final layout puts it, and
+/// the quad is drawn through the pane's frame (paneFrame): while the pane
+/// slides, zooms or is dragged, the cursor rides with its text. A pane that
+/// moved in the layout carries the springs with it (they keep their place
+/// in the pane), so with no transition the cursor lands with the text. A
+/// focus change glides from where the cursor is drawn to the new pane's
+/// cursor, wherever that pane is drawn now.
fn glideCursor(p: *Pardes, s: *Surface) void {
s.chrome.cursor_glide = 0;
s.chrome.cursor_alpha = 1;
@@ -220,26 +228,59 @@ fn glideCursor(p: *Pardes, s: *Surface) void {
const box = focusedCursorBox(p, s) orelse return;
const motion = animation.Motion.of(p.settings.motion);
var snap = if (s.cursor) |cursor| cursor.bar else false;
+ var frame: PaneFrame = .{};
+ // In a column's or the workspace's tag the cursor is not the pane's,
+ // and does not move with it.
+ var inside = false;
if (p.panes[p.active]) |pane| {
- // A focus change lands at once: a glide between panes would be
- // squashed against the new one's edge.
- if (p.cursor_view.serial != pane.serial) snap = true;
- // Typing never trails: insert mode lands every move.
- if (pane.body.mode == .insert) snap = true;
- // Scrolled (a file's view, a terminal's output): the quad moves with
- // the text and lands, never gliding against text that jumped (every
- // Enter at a prompt scrolls one and moves the other).
+ const rect = p.rects[p.active];
+ const cx = box.x + box.w / 2;
+ const cy = box.y + box.h / 2;
+ inside = cx >= @as(f32, @floatFromInt(rect.x)) and cx <= @as(f32, @floatFromInt(rect.x + rect.w)) and
+ cy >= @as(f32, @floatFromInt(rect.y)) and cy <= @as(f32, @floatFromInt(rect.y + rect.h));
+ if (inside) frame = paneFrame(s, pane.serial, rect);
+ const origin: [2]f32 = .{ @floatFromInt(rect.x), @floatFromInt(rect.y) };
+ const view = p.cursor_view;
const line: i64 = if (pane.file) |f| @intCast(f.scroll) else if (pane.isTerminal()) panes.Terminal.gridOffset(pane) else 0;
- if (p.cursor_view.serial == pane.serial and line != p.cursor_view.line) {
- p.cursor_glide.shift(0, @floatFromInt(p.cursor_view.line - line));
- snap = true;
+ if (view.serial == pane.serial and view.inside == inside) {
+ // The pane moved in the layout: the springs move with it (its
+ // own cursor's; one in a column's tag stays).
+ if (inside) p.cursor_glide.shift(origin[0] - view.origin[0], origin[1] - view.origin[1]);
+ // Typing never trails: insert mode lands every move.
+ if (pane.body.mode == .insert) snap = true;
+ // Scrolled (a file's view, a terminal's output): the quad moves
+ // with the text and lands, never gliding against text that
+ // jumped (every Enter at a prompt scrolls one and moves the
+ // other).
+ if (inside and line != view.line) {
+ p.cursor_glide.shift(0, @floatFromInt(view.line - line));
+ snap = true;
+ }
+ } else if (view.serial != 0 and p.cursor_glide.placed) {
+ // Focus moved (to another pane, or between a pane and a
+ // column's or the workspace's tag): the glide starts where the
+ // cursor is drawn, in the old place's frame, and heads for the
+ // new cursor through the new one's.
+ const old: PaneFrame = if (!view.inside) .{} else for (p.panes, 0..) |slot, id| {
+ const other = slot orelse continue;
+ if (other.serial != view.serial) continue;
+ const r = p.rects[id];
+ p.cursor_glide.shift(@as(f32, @floatFromInt(r.x)) - view.origin[0], @as(f32, @floatFromInt(r.y)) - view.origin[1]);
+ break paneFrame(s, view.serial, r);
+ } else .{};
+ const at = p.cursor_glide.centreAt(p.now_ns);
+ const seen = frame.unplace(old.place(at));
+ p.cursor_glide.shift(seen[0] - at[0], seen[1] - at[1]);
+ p.cursor_crossing = true;
}
- p.cursor_view = .{ .serial = pane.serial, .line = line };
+ p.cursor_view = .{ .serial = pane.serial, .line = line, .origin = origin, .inside = inside };
}
p.cursor_glide.aim(box, p.now_ns, motion, snap);
_ = p.cursor_glide.step(p.now_ns);
const moving = p.cursor_glide.moving();
- s.chrome.cursor_glide = 2 | @as(u8, @intFromBool(moving));
+ if (!moving) p.cursor_crossing = false;
+ // Drawn as the quad while it glides and while its pane moves.
+ s.chrome.cursor_glide = 2 | @as(u8, @intFromBool(moving or frame.moving()));
// The shell blinks it, from how long ago the last key or click was.
if (p.settings.cursor_blink)
s.chrome.cursor_idle_ms = @intCast(@min(std.math.maxInt(u32) - 1, (p.now_ns -| p.input_ns) / std.time.ns_per_ms));
@@ -249,13 +290,51 @@ fn glideCursor(p: *Pardes, s: *Surface) void {
const see_through: f32 = if (motion.zeta < 1) 0.7 else 0.85;
s.chrome.cursor_alpha = if (moving) see_through + (1 - see_through) * std.math.clamp(1 - remaining / 0.5, 0, 1) else 1;
const quad = p.cursor_glide.sample(p.now_ns, motion);
- // Never outside the focused pane, however far a flavour overshoots or
- // a scroll carried it: it squashes against the pane's edges.
- const r = p.rects[p.active];
- for (quad, 0..) |point, i| {
- s.chrome.cursor_quad[i * 2] = std.math.clamp(point[0], @as(f32, @floatFromInt(r.x)), @as(f32, @floatFromInt(r.x + r.w)));
- s.chrome.cursor_quad[i * 2 + 1] = std.math.clamp(point[1], @as(f32, @floatFromInt(r.y)), @as(f32, @floatFromInt(r.y + r.h)));
+ // Never outside the focused pane as drawn, however far a flavour
+ // overshoots or a scroll carried it: it squashes against the pane's
+ // edges. Crossing to another pane, it may pass over any of the grid.
+ const bounds: animation.Box = if (p.cursor_crossing or !inside)
+ .{ .w = @floatFromInt(s.cols), .h = @floatFromInt(s.rows) }
+ else
+ frame.at;
+ for (quad, 0..) |corner, i| {
+ const point = frame.place(corner);
+ s.chrome.cursor_quad[i * 2] = std.math.clamp(point[0], bounds.x, bounds.x + bounds.w);
+ s.chrome.cursor_quad[i * 2 + 1] = std.math.clamp(point[1], bounds.y, bounds.y + bounds.h);
+ }
+}
+
+/// Where a pane is drawn against where the layout puts it: its transition
+/// track's presented box against the track's target, or the layout's box
+/// both ways when it is still. Maps grid points between the two.
+const PaneFrame = struct {
+ to: animation.Box = .{ .w = 1, .h = 1 },
+ at: animation.Box = .{ .w = 1, .h = 1 },
+
+ fn moving(f: PaneFrame) bool {
+ return !std.meta.eql(f.to, f.at);
+ }
+
+ fn place(f: PaneFrame, point: [2]f32) [2]f32 {
+ return .{ f.at.x + (point[0] - f.to.x) * scale(f.at.w, f.to.w), f.at.y + (point[1] - f.to.y) * scale(f.at.h, f.to.h) };
+ }
+
+ fn unplace(f: PaneFrame, point: [2]f32) [2]f32 {
+ return .{ f.to.x + (point[0] - f.at.x) * scale(f.to.w, f.at.w), f.to.y + (point[1] - f.at.y) * scale(f.to.h, f.at.h) };
+ }
+
+ fn scale(num: f32, den: f32) f32 {
+ return if (den > 0.001) num / den else 1;
+ }
+};
+
+fn paneFrame(s: *const Surface, serial: u32, rect: Rect) PaneFrame {
+ const still = layout.panelBox(rect);
+ for (s.panelTracks()) |track| {
+ if (track.serial != serial or track.phase == .closing or !track.active()) continue;
+ return .{ .to = track.to, .at = track.visualBox() };
}
+ return .{ .to = still, .at = still };
}
/// The Lift style as drawn: `auto` is a shadow on a light page, and on a
@@ -667,8 +746,10 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
}
}
};
- glideCursor(p, s);
+ // After the tracks are submitted: the cursor is drawn through its
+ // pane's.
try p.presentation.submit(p, s);
+ glideCursor(p, s);
return composeAsciiTransitions(p, arena, s);
}
@@ -1706,11 +1787,13 @@ test "a dim leaves a pair already under 4.5 alone, and reads a theme's own sixte
try std.testing.expect(reverse.bg.rgb[0] < 255);
}
-test "with no lift and no dim a focus switch asks for no frames" {
+test "with no lift, no dim and no motion a focus switch asks for no frames" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 });
defer p.deinit();
_ = try p.setTestFile("text\n");
p.newScratchBelow(0);
+ // With motion the cursor glides to the other pane (glideCursor).
+ p.settings.motion = .off;
var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena.deinit();
p.advance(std.time.ns_per_s);
@@ -2059,3 +2142,109 @@ test "a body with context rows is painted once, and the grid's rows are what a p
try std.testing.expect(layer.rows > body.rect.h);
}
}
+
+/// Two columns, the file in the left one and a shell in the right, the
+/// right one focused with its cursor settled.
+fn twoColumns(p: *Pardes, arena: *std.heap.ArenaAllocator) !void {
+ _ = try p.setTestFile("some text on a line\n" ** 6);
+ _ = try p.newShell(1, "");
+ try std.testing.expect(layout.splitColumn(p, 0, 1, false));
+ p.sync();
+ p.advance(std.time.ns_per_s);
+ const boot = try p.render(arena.allocator());
+ p.acknowledgePanelPresentation(boot.panelTracks());
+ p.advance(2 * std.time.ns_per_s);
+ _ = arena.reset(.retain_capacity);
+ const settled = try p.render(arena.allocator());
+ p.acknowledgePanelPresentation(settled.panelTracks());
+ try std.testing.expect(!p.cursor_glide.moving());
+}
+
+test "while its pane slides the cursor rides with it, at the same cell of the pane every frame" {
+ if (!builtins.capabilities.lift) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 120, .rows = 20 });
+ defer p.deinit();
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ p.settings.motion = .bouncy;
+ try twoColumns(p, &arena);
+ p.active = 1;
+ p.advance(3 * std.time.ns_per_s);
+ _ = arena.reset(.retain_capacity);
+ p.acknowledgePanelPresentation((try p.render(arena.allocator())).panelTracks());
+ p.advance(4 * std.time.ns_per_s);
+ p.settings.panel_transition = .slide;
+ // The right column's pane moves right.
+ p.col_weight[0] = layout.column_weight_unit * 2;
+ p.col_weight[1] = layout.column_weight_unit;
+ p.sync();
+ var now: u64 = 4 * std.time.ns_per_s;
+ var riding: usize = 0;
+ for (0..40) |_| {
+ now += 16 * std.time.ns_per_ms;
+ p.advance(now);
+ _ = arena.reset(.retain_capacity);
+ const s = try p.render(arena.allocator());
+ p.acknowledgePanelPresentation(s.panelTracks());
+ const frame = paneFrame(s, p.panes[1].?.serial, p.rects[1]);
+ const box = focusedCursorBox(p, s).?;
+ const want = frame.place(.{ box.x, box.y });
+ try std.testing.expectApproxEqAbs(want[0], s.chrome.cursor_quad[0], 0.01);
+ try std.testing.expectApproxEqAbs(want[1], s.chrome.cursor_quad[1], 0.01);
+ if (frame.moving()) {
+ riding += 1;
+ // Drawn as the quad while the pane moves.
+ try std.testing.expect(s.chrome.cursor_glide & 1 != 0);
+ }
+ }
+ try std.testing.expect(riding >= 3);
+
+ // With no transition, the cursor lands with the text at once.
+ p.settings.panel_transition = .off;
+ p.col_weight[0] = layout.column_weight_unit;
+ p.col_weight[1] = layout.column_weight_unit * 2;
+ p.sync();
+ now += 16 * std.time.ns_per_ms;
+ p.advance(now);
+ _ = arena.reset(.retain_capacity);
+ const landed = try p.render(arena.allocator());
+ const box = focusedCursorBox(p, landed).?;
+ try std.testing.expectApproxEqAbs(box.x, landed.chrome.cursor_quad[0], 0.01);
+ try std.testing.expect(!p.cursor_glide.moving());
+}
+
+test "focus moved to another column glides from where the cursor was drawn" {
+ if (!builtins.capabilities.lift) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 120, .rows = 20 });
+ defer p.deinit();
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ p.settings.motion = .smooth;
+ try twoColumns(p, &arena);
+ p.active = 0;
+ p.advance(3 * std.time.ns_per_s);
+ _ = arena.reset(.retain_capacity);
+ const before = try p.render(arena.allocator());
+ try std.testing.expect(!p.cursor_glide.moving());
+ const was: [2]f32 = .{ before.chrome.cursor_quad[0], before.chrome.cursor_quad[1] };
+
+ p.active = 1;
+ _ = arena.reset(.retain_capacity);
+ const start = try p.render(arena.allocator());
+ // It sets off from where it was drawn, in the left column...
+ try std.testing.expectApproxEqAbs(was[0], start.chrome.cursor_quad[0], 0.01);
+ try std.testing.expectApproxEqAbs(was[1], start.chrome.cursor_quad[1], 0.01);
+ try std.testing.expect(p.cursor_glide.moving() and start.chrome.cursor_glide & 1 != 0);
+ // ...crosses the grid, not squashed against the right one's edge...
+ p.advance(3 * std.time.ns_per_s + 40 * std.time.ns_per_ms);
+ _ = arena.reset(.retain_capacity);
+ const mid = try p.render(arena.allocator());
+ try std.testing.expect(mid.chrome.cursor_quad[0] > was[0] and mid.chrome.cursor_quad[0] < @as(f32, @floatFromInt(p.rects[1].x)) + 1);
+ // ...and lands on the right one's cursor.
+ p.advance(5 * std.time.ns_per_s);
+ _ = arena.reset(.retain_capacity);
+ const landed = try p.render(arena.allocator());
+ const box = focusedCursorBox(p, landed).?;
+ try std.testing.expectApproxEqAbs(box.x, landed.chrome.cursor_quad[0], 0.01);
+ try std.testing.expect(!p.cursor_glide.moving() and !p.cursor_crossing);
+}
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 2d31416b..18a1c8cb 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -5882,9 +5882,10 @@ fn renderFrame(
}
}
var groups = makeGroups(surface.panelTracks(), surface.hasPanelDiff());
- // Cursors stand still while panes move, as they always have; while the
- // focused one glides or blinks, it is the overlay's quad instead. One
- // or the other every frame (none only at a blink's off half).
+ // While panes move the cells' cursors are not drawn; the focused one is
+ // the overlay's quad, riding with its pane (draw.glideCursor), as it is
+ // while it glides or blinks. One or the other every frame (none only at
+ // a blink's off half).
const blink_alpha: f32 = if (blinkNow(g, chrome)) |b| b.alpha else 1;
const quad_mode = chrome.cursor_glide & 2 != 0 and (chrome.cursor_glide & 1 != 0 or blink_alpha < 1);
g.cursor_quad_alpha = if (quad_mode) chrome.cursor_alpha * blink_alpha else 0;
@@ -6706,9 +6707,12 @@ fn emitSurfaceCell(
// this underlay at tagline height leaves page-colored gutters between
// adjacent workspace, column and pane tags at TaglineSize < 100.
emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .body, (&band_base)[0..1], 0, null, old_layer, false, page);
- // Over a theme's plaque (not a grip's button) the underlay lets it
- // show (tagPlaques).
- if (cell_decor.nplaques != 0 and !here.grip and cell_decor.inPlaque((@as(f32, @floatFromInt(col)) + 0.5) * body_layout.w + body_layout.x_off, @as(f32, @floatFromInt(row)) * body_layout.h + body_layout.y_off + 0.5))
+ // Over a theme's plaque the underlay lets it show (tagPlaques),
+ // in a grip's cell too where the plaque reaches into it past the
+ // button (which is drawn over the cells).
+ const cell_x = @as(f32, @floatFromInt(col)) * body_layout.w + body_layout.x_off;
+ const cell_y = @as(f32, @floatFromInt(row)) * body_layout.h + body_layout.y_off + 0.5;
+ if (cell_decor.nplaques != 0 and cell_decor.inPlaque(if (here.grip) cell_x + body_layout.w - 0.5 else cell_x + 0.5, cell_y))
instances[next.*].effect |= clear_bg_bit;
next.* += 1;
// Tag glyphs keep their font size inside a full physical-width grip.
@@ -7145,10 +7149,13 @@ test "lapis plaques stay in their tag's band, clear of every grip; only the focu
const band = plaqueBox(&g, &s.chrome, s.regionList(), region, layout, 1280, 768) orelse continue;
if (r[0] >= band.band[0] - 0.01 and r[1] >= band.band[1] - band.border - 0.01 and r[2] <= band.band[2] + 0.01 and r[3] <= band.band[3] + 0.01) break region;
} else return error.PlaqueOutsideItsBand;
+ // Clear of every grip's button: a plaque starts flush against it,
+ // in the gap between it and the text.
for (s.regionList()) |grip| {
if (grip.kind != .grip and grip.kind != .column_grip) continue;
- const gx0 = @as(f32, @floatFromInt(grip.rect.x)) * 16;
- const gx1 = @as(f32, @floatFromInt(grip.rect.x + grip.rect.w)) * 16;
+ const column = scrollColumn(&g, &s.chrome, grip.rect.x, layout);
+ const gx0 = column[0];
+ const gx1 = column[1];
const gy0 = @as(f32, @floatFromInt(grip.rect.y)) * 32;
const gy1 = @as(f32, @floatFromInt(grip.rect.y + grip.rect.h)) * 32;
try std.testing.expect(r[2] <= gx0 + 0.01 or r[0] >= gx1 - 0.01 or r[3] <= gy0 + 0.01 or r[1] >= gy1 - 0.01);
@@ -7173,6 +7180,11 @@ test "lapis plaques stay in their tag's band, clear of every grip; only the focu
if (tag.kind != .tag or tag.rect.y != column.rect.y + column.rect.h or tag.rect.x + tag.rect.w != column.rect.x + column.rect.w) continue;
const lower = plaqueBox(&g, &s.chrome, s.regionList(), tag, layout, 1280, 768).?;
try std.testing.expectEqual(upper.box[0], lower.box[0]);
+ // Flush against the grips' buttons, which line up too.
+ const grip = for (s.regionList()) |part| {
+ if (part.kind == .grip and part.owner == tag.owner) break part;
+ } else unreachable;
+ try std.testing.expectEqual(scrollColumn(&g, &s.chrome, grip.rect.x, layout)[1], lower.box[0]);
try std.testing.expectEqual(upper.box[2], lower.box[2]);
try std.testing.expectEqual(upper.band[3], upper.box[3]);
try std.testing.expectEqual(lower.band[1] - lower.border, lower.box[1]);
@@ -9382,14 +9394,15 @@ fn plaqueBox(g: *const Gui, chrome: *const pardes.Chrome, regions: []const parde
const pane = tag.kind == .tag;
const first = tag.rect.y;
const last = tag.rect.y + tag.rect.h - 1;
- // From its start, right of its grip: the gap cell before its text is
- // the plaque's margin, so its frame is never under the first letter. A
- // column's text sits a cell further in than a pane's; its plaque starts
- // where the pane's under it does, at its grip's end.
- const start = if (tag.kind == .column_tag) for (regions) |grip| {
- if (grip.kind == .column_grip and grip.owner == tag.owner) break grip.rect.x + grip.rect.w;
- } else tag.rect.x else tag.rect.x;
- const x0 = @as(f32, @floatFromInt(@min(start, tag.rect.x))) * layout.w;
+ // Flush against its grip's button, so the two read as one piece: the
+ // gap between button and text (the scroll gap) is the plaque's, and so
+ // is the cell before its text, so its frame is never under the first
+ // letter. A column's plaque starts where the pane's under it does.
+ const grip_kind: pardes.Region.Kind = if (tag.kind == .column_tag) .column_grip else .grip;
+ const x0 = for (regions) |grip| {
+ if (tag.kind != .workspace_tag and grip.kind == grip_kind and grip.owner == tag.owner)
+ break scrollColumn(g, chrome, grip.rect.x, layout)[1];
+ } else @as(f32, @floatFromInt(tag.rect.x)) * layout.w;
const x1_cells = @as(f32, @floatFromInt(tag.rect.x + tag.rect.w)) * layout.w;
const window_edge = win_w - x1_cells < layout.w;
const x1 = if (window_edge) @max(x1_cells, win_w) else x1_cells;
@@ -9454,7 +9467,13 @@ fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, ce
const left = @as(f32, @floatFromInt(grip.rect.x)) * layout.w;
const right = @as(f32, @floatFromInt(grip.rect.x + grip.rect.w)) * layout.w;
try addDecor(g, gpa, left, button[1], button[0], button[3], band, 1, track, win_w, win_h);
- try addDecor(g, gpa, button[2], button[1], right, button[3], band, 1, track, win_w, win_h);
+ // Right of it, a theme's plaque starts flush against it (plaqueBox).
+ const plaqued = chrome.decor_box_border != null and !cell_decor.plaques_off;
+ if (!plaqued) try addDecor(g, gpa, button[2], button[1], right, button[3], band, 1, track, win_w, win_h);
+ // Its cells over the plaque's reach are let through (emitInstance):
+ // under the button's row, the band goes on as far as the plaque.
+ if (plaqued and grip.rect.h > 1)
+ try addDecor(g, gpa, left, button[3], button[2], @as(f32, @floatFromInt(grip.rect.y + grip.rect.h)) * layout.h, band, 1, track, win_w, win_h);
if (grip.kind != .grip) continue;
// acme's button ring, two pixels (ButtonBorder) inside the button:
// the focused one's in its own fill. An unfocused dirty one keeps a
@@ -10160,7 +10179,6 @@ fn buildOverlay(
fn appendCursorGlide(g: *Gui, surface: *const pardes.Surface, builder: *OverlayBuilder) void {
const chrome = &surface.chrome;
if (g.cursor_quad_alpha <= 0.001) return;
- if (surface.panelTracks().len != 0) return;
const layout = fixedCellLayout(g);
var px: [4][2]f32 = undefined;
var centre: [2]f32 = .{ 0, 0 };
diff --git a/src/pardes.zig b/src/pardes.zig
index d6385e6f..2ee4e4ce 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4347,7 +4347,9 @@ pub const Pardes = struct {
/// scroll, G6): the core then keeps the continuous path running, so the
/// shell draws every loop and a virtual clock moves on, as for a lift.
shell_continuous: bool = false,
- cursor_view: struct { serial: u32 = 0, line: i64 = 0 } = .{},
+ cursor_view: struct { serial: u32 = 0, line: i64 = 0, origin: [2]f32 = .{ 0, 0 }, inside: bool = true } = .{},
+ /// The cursor glides from one pane to another: it may cross the grid.
+ cursor_crossing: bool = false,
input_ns: u64 = 0,
animate_theme_changes: bool = false,
native_images: bool = false,
diff --git a/test/gui-goldens.txt b/test/gui-goldens.txt
index 01bdfc7c..473e50a7 100644
--- a/test/gui-goldens.txt
+++ b/test/gui-goldens.txt
@@ -14,4 +14,4 @@
15-theme-acme-light ca5252f5117ac8d6c679cef917c9f316c9ca311fc7a8dcbac2796b4fc04d3b64
16-debug 591bb7c45b4b9c46cd2458330b4fa3962afc6dac827fa6f5e20e927a72c586e6
17-terminal 38299427891e31de0b5597159501c161474620eb26fd7708a6396f0fd685209e
-18-mid-transition 80cf350f8736fa4b348225a3872572a799ce57c763939796b68e8c839418f8ee
+18-mid-transition 5d3e37d874988b460297e18df9508a77b602d5f675e28983c2c793871d4301f6