diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 13:15:41 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | e0b5564f04aa0ae6666f29ef29c768d800fbabce (patch) | |
| tree | 3477e8e37ab752e883a8442e97df0701a93af836 /src/Messages.zig | |
| parent | 30c2c8a154e770b3920bcfd7d553825fe9182bf4 (diff) | |
| download | pardes-e0b5564f04aa0ae6666f29ef29c768d800fbabce.tar.gz pardes-e0b5564f04aa0ae6666f29ef29c768d800fbabce.zip | |
Step core animation by the shell's clock and sleep to the next wake
A shell now answers Host.now, its monotonic clock in ns, and the pump
advances core animation to it: one .tick per whole 16 ms frame since core
time last stood still. The same animation therefore takes the same time at
60, 120 and 144 Hz, over ssh and after a slow frame (the GUI's re-armed
clock ran them ~25% slow at 144 Hz; the tty's post-frame sleep drifted).
nextWake() lists every core animation in one place: a frame from now while
anything moves, the end of the wait while something only waits (a message
lingering, the look-hover delay), null when idle. Shells sleep exactly that
long, and a wait is jumped to its end in one step, so an 800 ms linger
costs one frame instead of fifty. An overshoot under 1.5 ms after a step is
let go: at 60 and 120 Hz every display frame takes exactly one step (a 16 ms
frame against a 16.67 ms vsync would otherwise double-step every 24th).
The six tick drivers are gone: tty's timer thread only times the wait,
interruptibly (a newer, shorter request cuts short a sleep still timing a
longer one); the GUI's
AnimationClock, web's JS tick bank (pardes_tick now takes rAF's timestamp),
the detached server's and the board's ticks; grid mode steps a virtual
clock straight to each wake. PARDES_TEST_CLOCK, set by the snapshot
harness, gives tty and the detached server the same virtual clock.
Every stepped frame is drawn. The old pump never drew the last frame of a
fade (a .tick asks for no frame, and the fade was over by the check), so a
theme switch stopped at 9/10 of the way until the next input: theme.golden,
themesel.golden and the GUI's acme-light scene move to the theme's true
colours, and nothing else changes. The frozen previous grid is captured only
while a panel transition is chosen, not on every idle frame.
Diffstat (limited to 'src/Messages.zig')
| -rw-r--r-- | src/Messages.zig | 81 |
1 files changed, 75 insertions, 6 deletions
diff --git a/src/Messages.zig b/src/Messages.zig index 97a2f939..7334de95 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -272,11 +272,35 @@ fn advanceLine(p: *const Pardes, life: *Pane.MessageLife) bool { return false; } -/// Is any message line moving (and so owed ticks)? -pub fn messagesAnimating(pane: *const Pane) bool { - if (pane.msg_len > 0 and pane.msg_life.phase != .shown) return true; - for (pane.msg_older[0..pane.msg_older_len]) |line| if (line.life.phase != .shown) return true; - return false; +/// Move every lingering message line `frames` along its linger at once: +/// how `advance` jumps a wait instead of stepping it frame by frame. +pub fn holdMessages(p: *Pardes, frames: u64) void { + const n: u16 = @intCast(@min(frames, std.math.maxInt(u16))); + for (p.panes) |slot| { + const pane = slot orelse continue; + if (pane.msg_len > 0 and pane.msg_life.phase == .lingering) pane.msg_life.frame +|= n; + for (pane.msg_older[0..pane.msg_older_len]) |*line| { + if (line.life.phase == .lingering) line.life.frame +|= n; + } + } +} + +/// Frames until this pane's message lines next change: 1 while a line +/// falls in or dissolves, the rest of its linger while one only waits, null +/// when every line just shows. +pub fn messageWake(p: *const Pardes, pane: *const Pane) ?u64 { + var soonest: ?u64 = null; + const linger = messageFrames(p.settings.message_linger_ms); + for (0..@as(usize, pane.msg_older_len) + 1) |i| { + const life = if (i < pane.msg_older_len) pane.msg_older[i].life else if (pane.msg_len > 0) pane.msg_life else continue; + const frames: u64 = switch (life.phase) { + .shown => continue, + .entering, .leaving => 1, + .lingering => @max(1, linger -| life.frame), + }; + soonest = if (soonest) |n| @min(n, frames) else frames; + } + return soonest; } /// How present a message is (1 = fully) and how far above its row it @@ -286,7 +310,7 @@ pub const MessageMotion = struct { alpha: f32 = 1, slide: f32 = 0 }; /// Animation frames a message timing lasts; at least one. pub fn messageFrames(ms: u16) u16 { - return @intCast(@max(1, (@as(u32, ms) + animation.Animation.frame_ms - 1) / animation.Animation.frame_ms)); + return @intCast(@max(1, (@as(u32, ms) + animation.frame_ms - 1) / animation.frame_ms)); } pub fn messageMotion(p: *const Pardes, life: Pane.MessageLife) MessageMotion { @@ -792,3 +816,48 @@ test "notices stack over the body's top rows without moving them" { for (p.surface.tagLayers()) |*layer| try std.testing.expect(layer.cols == 0 or layer.kind != .notice); } + +test "an idle core wants no wake and a lingering message sleeps to its linger's end" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const frame = animation.frame_ns; + p.advance(1_000_000); + try std.testing.expectEqual(@as(?u64, null), p.nextWake()); + const pane = p.panes[p.active].?; + try std.testing.expect(p.executeBuiltinLine(p.active, "Msg hello")); + // Falling in: a frame from now. + try std.testing.expectEqual(@as(?u64, p.stepped_ns + frame), p.nextWake()); + const fall = messageFrames(p.settings.message_fall_ms); + p.advance(p.stepped_ns + fall * frame); + try std.testing.expectEqual(Pane.MessagePhase.shown, pane.msg_life.phase); + try std.testing.expectEqual(@as(?u64, null), p.nextWake()); + // A key starts the 800 ms linger: the core sleeps to its end, not a + // frame at a time, and the frames it jumps draw nothing. + p.update(.{ .key = .{ .cp = 'x' } }); + try std.testing.expectEqual(Pane.MessagePhase.lingering, pane.msg_life.phase); + const linger = messageFrames(p.settings.message_linger_ms); + const due = p.stepped_ns + linger * frame; + try std.testing.expectEqual(@as(?u64, due), p.nextWake()); + p.needs_frame = false; + p.advance(due - frame); + try std.testing.expectEqual(@as(?u64, due), p.nextWake()); + try std.testing.expectEqual(Pane.MessagePhase.lingering, pane.msg_life.phase); + p.advance(due); + try std.testing.expectEqual(Pane.MessagePhase.leaving, pane.msg_life.phase); + try std.testing.expectEqual(@as(?u64, due + frame), p.nextWake()); + p.advance(due + 10 * std.time.ns_per_s); + try std.testing.expectEqual(@as(usize, 0), pane.msg_len); + try std.testing.expectEqual(@as(?u64, null), p.nextWake()); + + // A long linger ends when it says, jumped in one step, not stretched by + // the cap on catching up with moving frames. + try std.testing.expect(p.executeBuiltinLine(p.active, "MessageLinger 5000")); + try std.testing.expect(p.executeBuiltinLine(p.active, "Msg again")); + p.advance(p.stepped_ns + fall * frame); + p.update(.{ .key = .{ .cp = 'x' } }); + const long_due = p.stepped_ns + messageFrames(5000) * frame; + try std.testing.expectEqual(@as(?u64, long_due), p.nextWake()); + p.advance(long_due); + try std.testing.expectEqual(Pane.MessagePhase.leaving, pane.msg_life.phase); +} |
