summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig46
1 files changed, 46 insertions, 0 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 6bd2c715..983be262 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -111,6 +111,39 @@ const overlay_circle_vertices: usize = 16 * 3;
const touch_click_flash_max_frames: u8 = 14;
const touch_click_flash_vertices: usize = 1400;
const touch_scroll_tick: f32 = 0.02;
+
+/// SDL input can wake the loop faster than display cadence, so ticking once
+/// per pass would make animation duration depend on pty traffic or mouse
+/// motion. This monotonic gate keeps it near 60 Hz without sleeping the event
+/// loop; the first frame after activation displays the exact source palette.
+const AnimationClock = struct {
+ next_ns: u64 = 0,
+
+ fn due(clock: *AnimationClock, active: bool, now_ns: u64) bool {
+ if (!active) {
+ clock.next_ns = 0;
+ return false;
+ }
+ if (clock.next_ns == 0) {
+ clock.next_ns = now_ns +| pardes.animation.frame_ns;
+ return false;
+ }
+ if (now_ns < clock.next_ns) return false;
+ clock.next_ns = now_ns +| pardes.animation.frame_ns;
+ return true;
+ }
+};
+
+test "GUI animation clock is active-only and cadence gated" {
+ var clock: AnimationClock = .{};
+ try std.testing.expect(!clock.due(false, 100));
+ try std.testing.expect(!clock.due(true, 100));
+ try std.testing.expect(!clock.due(true, 100 + pardes.animation.frame_ns - 1));
+ try std.testing.expect(clock.due(true, 100 + pardes.animation.frame_ns));
+ try std.testing.expect(!clock.due(false, 100 + 2 * pardes.animation.frame_ns));
+ try std.testing.expectEqual(@as(u64, 0), clock.next_ns);
+}
+
/// Web tap-vs-scroll heuristic, measured after normalized coordinates have
/// been mapped into backing pixels (12px = 8 CSS px at web_render_scale).
const touch_tap_slop_px: f32 = 12.0;
@@ -1273,6 +1306,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
var frame_arena: std.heap.ArenaAllocator = .init(gpa);
defer frame_arena.deinit();
+ var animation_clock: AnimationClock = .{};
while (!core.quit) {
// 1. SDL input: block briefly for the first event, then drain the rest
@@ -1389,6 +1423,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options) !void {
// 7. render — apply the wheel batch LAST before it, while
// core.surface still holds the frame the last pass drew
stepScroll(&g, core, gpa);
+ if (animation_clock.due(core.themeAnimationActive(), c.SDL_GetTicksNS()))
+ core.update(.tick);
_ = frame_arena.reset(.retain_capacity);
const surface = try core.render(frame_arena.allocator());
renderFrame(&g, gpa, surface, core.crt_on, core.show_debug) catch |err| log.err("render: {t}", .{err});
@@ -1410,6 +1446,7 @@ const WebState = struct {
core: *pardes.Pardes,
gpa: std.mem.Allocator,
frame_arena: std.heap.ArenaAllocator,
+ animation_clock: AnimationClock = .{},
};
var live_web_state: ?*WebState = null;
@@ -1672,6 +1709,8 @@ fn webFrame(arg: ?*anyopaque) callconv(.c) void {
if (updateCoreResize(core, cols, rows, g.cell_w, g.cell_h)) resetScroll(g);
// 4. render — apply the wheel batch last, see runNative
stepScroll(g, core, st.gpa);
+ if (st.animation_clock.due(core.themeAnimationActive(), c.SDL_GetTicksNS()))
+ core.update(.tick);
_ = st.frame_arena.reset(.retain_capacity);
const surface = core.render(st.frame_arena.allocator()) catch return;
renderFrameGl(g, st.gpa, surface, core.show_debug) catch return;
@@ -1805,6 +1844,13 @@ fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
msgs.deinit(gpa);
drainEffects(core, &ptys, &gens, io, gpa, &queue, &pipe_tasks, null, -1, &watches, true);
pollCwds(core, &ptys);
+ // The grid harness polls stdin at the same 16 ms cadence as native
+ // SDL. Advancing here lets `stable` wait for exact endpoint colors;
+ // once inactive it resumes the old event-only frame contract.
+ if (core.themeAnimationActive()) {
+ core.update(.tick);
+ n_events += 1;
+ }
if (n_events == 0) continue; // idle tick: nothing changed, no frame
_ = frame_arena.reset(.retain_capacity);
const surface = try core.render(frame_arena.allocator());