From a774cad033fdc371af9f89b9a787a30ea017481c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 3 Aug 2026 01:34:50 -0300 Subject: animate anchored theme colors on native and tty backends --- src/gui/gui.zig | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) (limited to 'src/gui') 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()); -- cgit v1.3