summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 23:34:20 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitfaa5ee1d81e2b9c4be6925b7178db2d2dc4577f8 (patch)
tree14799bbd5748743b985ac617ba134ad12bdc9269 /src/gui
parentd8d770454e77fbeeb0dd9c5e965a7eda6a5558ba (diff)
downloadpardes-faa5ee1d81e2b9c4be6925b7178db2d2dc4577f8.tar.gz
pardes-faa5ee1d81e2b9c4be6925b7178db2d2dc4577f8.zip
G3: the focused cursor glides a jump on the Motion flavour's springs, and blinks
The core owns the motion, as Lift: animation.CursorGlide keeps the focused cursor's four corners on springs (leading corners at the flavour's pace x2.4, trailing ones x follow, so a jump smears and collapses; a long jump bows by the flavour's arc), sampled at each frame's own time. A step of a cell or less, insert mode, a focus change and any scroll (a file's view, a terminal's output) land at once; a snap lands a glide under way too. The quad is clamped to the focused pane when drawn (never its springs). Chrome carries it on the wire (cursor_glide: bit 1 a cursor, bit 0 gliding; cursor_alpha; cursor_quad, grid cells; cursor_idle_ms): the GUI draws the quad as an overlay in the cursor's colour, at 85% (70% on the long flavours) rising to solid as it lands, and hides the cells' cursors while it does: one or the other every frame. Only the focused cursor glides. Blink (CursorBlink, on by default in pixel shells) is the shell's: from cursor_idle_ms and when the frame arrived, solid while typing and 500 ms after, 530 ms halves with 80 ms eased edges, solid after 10 s idle, and never while the window is unfocused or minimized or on a virtual clock. An edge redraws the last frame with no core render (level B, blink_redraws); an attached GUI blinks from the same field without a frame per edge. Tests: snaps a cell, glides a jump with smear and bow, arrives under a pixel by 150 ms on smooth and lands exactly, shifts with a scroll; blink holds, eases, stops; the core glides a jump (ge) frame by frame (gliding exactly while it moves, alpha rising, exact at rest), snaps a step (j) and a switch to insert mid-glide; a terminal's scrolling output lands its cursor; wire round-trip of the cursor record. Shared files: src/animation.zig, src/pardes.zig, src/config.zig (toggle CursorBlink), src/surface.zig, src/detached/wire.zig.
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.zig84
1 files changed, 81 insertions, 3 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 5dfacee9..7d097e83 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -1857,6 +1857,12 @@ const Gui = struct {
scroll_edge: []pardes.Cell = &.{}, // the row that just left the pane
scroll_edge_len: u16 = 0,
+ /// The cursor's blink (blinkNow): when the frame drawn last arrived,
+ /// a redraw owed for an edge, and this frame's cursor mode and alpha.
+ frame_arrival_ns: u64 = 0,
+ blink_due: bool = false,
+ no_blink: bool = false,
+ cursor_quad_alpha: f32 = 0,
scene_tex: ?*c.SDL_GPUTexture = null,
scene_tex_w: u32 = 0,
scene_tex_h: u32 = 0,
@@ -2648,6 +2654,8 @@ fn localSession(
.test_hz = if (std.c.getenv("PARDES_TEST_HZ")) |hz| @max(1, std.fmt.parseInt(u64, std.mem.span(hz), 10) catch 60) else 60,
};
defer shell.shutdownPtys();
+ // A virtual clock never blinks: goldens and feel series hold still.
+ g.no_blink = shell.test_clock != null;
const host = shell.host();
core.host = host;
@@ -4179,7 +4187,14 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void {
@intCast(@max(1, refreshNs(g) / std.time.ns_per_ms))
else
16;
- const ms: c_int = if (latency_fd >= 0) 0 else if (minimized) -1 else if (timeout_ms != 0) @min(@as(c_int, @intCast(@min(timeout_ms, std.math.maxInt(c_int)))), if (polls) poll_ms else std.math.maxInt(c_int)) else if (polls) poll_ms else -1;
+ const waited: c_int = if (latency_fd >= 0) 0 else if (minimized) -1 else if (timeout_ms != 0) @min(@as(c_int, @intCast(@min(timeout_ms, std.math.maxInt(c_int)))), if (polls) poll_ms else std.math.maxInt(c_int)) else if (polls) poll_ms else -1;
+ // The cursor's blink is the shell's to time (Chrome.cursor_idle_ms):
+ // a frame a loop through an edge, else a wake at the next one; none
+ // while the window is minimized or unfocused. A virtual clock never
+ // blinks, so goldens hold still.
+ const blink = if (s.test_clock != null or minimized) null else blinkNow(g, if (s.surface) |frame| &frame.chrome else null);
+ const blink_ms: ?c_int = if (blink) |b| (if (b.edge) 1 else if (b.next_ns) |next| @as(c_int, @intCast(@min(std.math.maxInt(c_int), next / std.time.ns_per_ms + 1))) else null) else null;
+ const ms: c_int = if (blink_ms) |b| (if (waited < 0) b else @min(waited, b)) else waited;
// The wait is the 9P connections' turn with the core.
pardes.turn.rest();
const got = c.SDL_WaitEventTimeout(&sev, ms);
@@ -4189,6 +4204,9 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void {
// (PARDES_TEST_HZ), events or not, as a real display's present
// would; otherwise a wait that ran out moves it exactly to the
// core's wake.
+ // Through an edge, or woken for the next: the shell redraws the last
+ // frame with the new alpha, no core render (ยง5.5 level B).
+ if (blink_ms != null and (blink.?.edge or !got)) g.blink_due = true;
if (s.test_clock) |*virtual| {
if (core.continuous()) {
const display = virtual.* + std.time.ns_per_s / s.test_hz;
@@ -4273,6 +4291,27 @@ fn pollFrame(ctx: ?*anyopaque) void {
// early is on time: the wait that paces it is in whole milliseconds).
if (g.post.animating(core.settings.shader_animation) and !core.needs_frame and s.presented and
c.SDL_GetTicksNS() -| g.post.last_ns + std.time.ns_per_ms >= refreshNs(g)) redrawPost(g, s.gpa);
+ // A blink edge: the last frame again, the cursor's alpha moved on.
+ if (g.blink_due and !core.needs_frame and s.presented) if (s.surface) |frame| {
+ g.blink_due = false;
+ blink_redraws += 1;
+ s.presented = renderFrame(g, s.gpa, core, frame, core.settings.debug) catch false;
+ };
+}
+
+/// Frames drawn for a blink alone, with no core render (tests, tracing).
+var blink_redraws: u64 = 0;
+
+/// The blink now, from the frame's time since input and how long ago it
+/// arrived; null where nothing blinks (off, idle, an unfocused window).
+fn blinkNow(g: *const Gui, chrome: ?*const pardes.Chrome) ?pardes.animation.Blink {
+ const frame = chrome orelse return null;
+ if (g.no_blink or frame.cursor_glide & 2 == 0 or frame.cursor_idle_ms == std.math.maxInt(u32)) return null;
+ if (c.SDL_GetWindowFlags(g.window) & c.SDL_WINDOW_INPUT_FOCUS == 0) return null;
+ const since = @as(u64, frame.cursor_idle_ms) * std.time.ns_per_ms + (monoNs() -| g.frame_arrival_ns);
+ // Relative to now: `next_ns` is how long until the next edge.
+ const b = pardes.animation.Blink.at(since, 0);
+ return .{ .alpha = b.alpha, .edge = b.edge, .next_ns = if (b.next_ns) |next| next -| since else null };
}
/// One refresh of the display the window is on, in ns; a frame of the
@@ -4340,6 +4379,7 @@ fn present(ctx: ?*anyopaque, surface: *const pardes.Surface) void {
frame_started_ns = monoNs();
const frame = @constCast(surface);
s.surface = frame;
+ g.frame_arrival_ns = monoNs();
const scene_requested = g.post.ready();
if (!scene_requested) g.scene_failures = 0;
s.presented = renderFrame(
@@ -5359,8 +5399,13 @@ fn renderFrame(
const layout = fixedCellLayout(g);
const chrome = &surface.chrome;
var groups = makeGroups(surface.panelTracks(), surface.hasPanelDiff());
- // Cursors stand still while panes move, as they always have.
- const cursors_shown = groups.tracks == 0;
+ // 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).
+ 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;
+ const cursors_shown = groups.tracks == 0 and !quad_mode;
var overlay_count = buildOverlay(g, core, surface, sw, sh, debug_on);
if (overlay_count != 0 and !uploadOverlayGpu(g, cmd, overlay_count))
overlay_count = 0;
@@ -8498,10 +8543,43 @@ fn buildOverlay(
if (g.overlay_vertices.len == 0 or sw == 0 or sh == 0) return 0;
var builder = OverlayBuilder{ .vertices = g.overlay_vertices, .win_w = @floatFromInt(sw), .win_h = @floatFromInt(sh) };
appendWorkspacePet(g, core, surface, &builder);
+ appendCursorGlide(g, surface, &builder);
appendTouchOverlay(g, &builder, debug_on);
return @intCast(builder.len);
}
+/// G3: the focused cursor while it glides or blinks, its quad from the core
+/// (Chrome.cursor_quad, grid cells) in its cursor colour; a little less than
+/// solid, so the glyphs it passes over stay legible.
+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 };
+ for (&px, 0..) |*point, i| {
+ point.* = .{ chrome.cursor_quad[i * 2] * layout.w, chrome.cursor_quad[i * 2 + 1] * layout.h };
+ centre[0] += point[0] / 4;
+ centre[1] += point[1] / 4;
+ }
+ // Its colour: the cursor the cells would draw nearest the quad.
+ var boxes: [max_cursors]CursorBox = undefined;
+ var rgb: [3]u8 = chrome.fg orelse fg_default;
+ var nearest: f32 = std.math.inf(f32);
+ for (boxes[0..cursorBoxes(g, surface, layout, builder.win_h, &boxes)]) |box| {
+ const dx = (box.x0 + box.x1) / 2 - centre[0];
+ const dy = (box.y0 + box.y1) / 2 - centre[1];
+ if (dx * dx + dy * dy < nearest) {
+ nearest = dx * dx + dy * dy;
+ rgb = box.rgb;
+ }
+ }
+ const color: OverlayColor = .{ .r = @as(f32, @floatFromInt(rgb[0])) / 255, .g = @as(f32, @floatFromInt(rgb[1])) / 255, .b = @as(f32, @floatFromInt(rgb[2])) / 255, .a = g.cursor_quad_alpha };
+ builder.addTri(px[0][0], px[0][1], px[1][0], px[1][1], px[2][0], px[2][1], color);
+ builder.addTri(px[0][0], px[0][1], px[2][0], px[2][1], px[3][0], px[3][1], color);
+}
+
fn appendWorkspacePet(g: *Gui, core: ?*const pardes.Pardes, surface: *const pardes.Surface, builder: *OverlayBuilder) void {
const p = core orelse return;
const kind: pet.Kind = @enumFromInt(@intFromEnum(p.settings.pet));