summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-26 12:00:20 -0300
committerGabriel Schneider <[email protected]>2026-08-26 12:00:20 -0300
commit3b8ee10301e0b83013a38d93516a345622a93aaa (patch)
tree5e024ce79f1d470cddb63a4fc23ef72dbbc38328
parent18a8e39c551e08eca9ed26e2c13580ccdd36b955 (diff)
downloadpardes-3b8ee10301e0b83013a38d93516a345622a93aaa.tar.gz
pardes-3b8ee10301e0b83013a38d93516a345622a93aaa.zip
Make the chrome fade a build option, and compile it out for the board
A theme change moves the anchored chrome palette - taglines, boxes, line numbers, scroll bars - from the old colors to the new ones over ten display frames. On a screen that repaints in microseconds that is a short legible transition, and it is why the code exists: a palette that teleports reads as a glitch. On a 115200 serial line it is not a fade. Each of the ten steps recolors every anchored cell, so the diff finds the whole chrome dirty and spends a frame's worth of wire on it, ten times over, with nothing else on screen to look at. Measured on the die, one `NextColor`: fade on 12,593 bytes 1,097 ms of saturated wire fade off 2,425 bytes 215 ms A second of the editor talking to itself about a color, on the one transport where a second is noticeable, for a gradient nobody can watch arrive at 11.5 KB/s. ## Comptime, so the code is not there `ChromeAnimation` now selects between `animation.Transition` and a new `animation.Immediate` - the same interface with the animation taken out, a value that is only ever what it was last set to. That is what makes `ChromeTheme.interpolate` unreachable, and unreachable is what makes it absent: the flashed image drops 2,336 bytes, and the object 13,180. A bool tested at runtime would have kept every one of those bytes and still paid the branch. It also would have needed a second meaning bolted onto `animate_theme_changes`, whose job is the startup window and nothing else; that field is untouched here. The option is `-Dtheme-animation`, defaulting to off for `p4` and on everywhere else, and it is an option rather than a platform test because "is a frame expensive" is a property of the transport: a P4 driven over something faster than a UART would want the fade back, and `-Dtheme-animation=true` gives it to them. ## What was checked `Immediate` is new code with one contract worth pinning, and it is the one a caller could get wrong: it must arrive at the SAME palette a completed fade arrives at. An endpoint that differed by a rounding step would make the option a change of colors rather than a change of how long they take. Tested against a fully advanced `Transition` in `animation.zig`. Full suite: unit-test, snap 95/95, hxdiff 481/0, hxparity 561/0, image-harness, pdf-harness, mupdf-check. Builds: tty, p4, gui, and tty/gui with the fade forced off. On the die the canonical verifier reports the screen IDENTICAL across both arms - the workload contains no theme change, so this is the check that ordinary rendering was not perturbed - and `p4-bench --check` stays 4/4.
-rw-r--r--build.zig27
-rw-r--r--src/animation.zig65
-rw-r--r--src/pardes.zig23
3 files changed, 114 insertions, 1 deletions
diff --git a/build.zig b/build.zig
index ed3a2e6a..07c180d3 100644
--- a/build.zig
+++ b/build.zig
@@ -362,6 +362,33 @@ pub fn build(b: *std.Build) void {
// in region l2mem, overflowed by 76036 bytes`, that being the shell's shadow copy of the grid.
opts.addOption(u16, "p4_cols", b.option(u16, "p4-cols", "board grid width in cells (p4 only)") orelse 56);
opts.addOption(u16, "p4_rows", b.option(u16, "p4-rows", "board grid height in cells (p4 only)") orelse 14);
+ // ANIMATED THEME CHANGES, off on the board because there the animation is not an animation.
+ //
+ // A theme change moves the anchored chrome palette - taglines, boxes, line numbers, scroll bars -
+ // from the old colors to the new ones over `animation.transition_steps` display frames, which is
+ // ten. On a screen that repaints in microseconds that is a short legible fade, and it is the
+ // reason the transition exists: a palette that teleports reads as a glitch.
+ //
+ // On a 115200 serial line a frame is not free. Every one of those ten steps recolors every
+ // anchored cell, so the shell's diff finds the whole chrome dirty and spends a frame's worth of
+ // wire on it, ten times over, with nothing else to look at in between. Measured on the die, one
+ // `NextColor`:
+ //
+ // fade on 12,593 bytes 1,097 ms of saturated wire
+ // fade off 2,425 bytes 215 ms
+ //
+ // A second of the editor talking to itself about a color, on the one transport where a second is
+ // noticeable, for a gradient nobody can see arrive gradually at 11.5 KB/s.
+ //
+ // COMPTIME, not a runtime flag: `pardes.ChromeAnimation` selects `animation.Immediate` over
+ // `animation.Transition`, which leaves `ChromeTheme.interpolate` unreachable and out of the image
+ // (2,336 bytes of it). A bool tested at runtime would have kept every one of those bytes.
+ //
+ // A build option rather than a platform test, because "is a frame expensive" is a property of the
+ // transport and not of the target: a P4 driven over something faster than a UART would want it on,
+ // and it is off here only as the default that matches the wire this port actually has.
+ opts.addOption(bool, "theme_animation", b.option(bool, "theme-animation", "animate chrome colors across a theme change (default: off for p4)") orelse
+ (platform != .p4));
// Meaningful only for the SDL shell. Keeping the platform condition here
// prevents Config/EffectCode from describing tty/macOS/web as "prebuilt".
opts.addOption(bool, "gui_shader_sources_prebuilt", gui_shader_sources_prebuilt);
diff --git a/src/animation.zig b/src/animation.zig
index aef6fe6d..c22396b8 100644
--- a/src/animation.zig
+++ b/src/animation.zig
@@ -60,6 +60,44 @@ pub fn Transition(comptime Value: type) type {
};
}
+/// `Transition`'s interface with the animation taken OUT: a value that is only ever the one it was
+/// last set to.
+///
+/// This exists so that a build which never fades does not carry the machinery for fading. A runtime
+/// flag around the same `Transition` cannot achieve that - the endpoints stay in the struct and
+/// `Value.interpolate` stays in the binary, reachable and therefore emitted. Selecting a different
+/// type at comptime is what makes the interpolator genuinely unreachable, and on a target whose whole
+/// display is a 115200-baud serial line, absent code and unspent frames are the same saving twice.
+///
+/// Every method here is the trivial one, and `retarget` is deliberately `snap` rather than an error:
+/// callers ask for a new palette and get it, on the next frame, in one step. Nothing about the
+/// interface says how many frames the arrival takes.
+pub fn Immediate(comptime Value: type) type {
+ return struct {
+ const Self = @This();
+
+ displayed: Value,
+
+ pub fn init(value: Value) Self {
+ return .{ .displayed = value };
+ }
+
+ pub fn isActive(_: *const Self) bool {
+ return false;
+ }
+
+ pub fn retarget(a: *Self, target: Value) void {
+ a.displayed = target;
+ }
+
+ pub fn advance(_: *Self) void {}
+
+ pub fn snap(a: *Self, value: Value) void {
+ a.displayed = value;
+ }
+ };
+}
+
/// Linear RGB interpolation with nearest-integer rounding. The weighted-sum
/// form stays unsigned for both rising and falling channels.
pub fn interpolateRgb(from: [3]u8, to: [3]u8, step: u16, steps: u16) [3]u8 {
@@ -81,6 +119,33 @@ const TestColor = struct {
}
};
+// The substitute has to be interchangeable, and the property that matters is the one a caller could
+// otherwise get wrong: it must arrive at the SAME palette a completed fade arrives at. A fade whose
+// endpoint differed by a rounding step would make the build option a visible change of colors rather
+// than a change of how long they take.
+test "Immediate lands where a completed Transition lands" {
+ const from: TestColor = .{ .rgb = .{ 240, 3, 90 } };
+ const to: TestColor = .{ .rgb = .{ 5, 222, 90 } };
+
+ var faded = Transition(TestColor).init(from);
+ faded.retarget(to);
+ for (0..transition_steps) |_| faded.advance();
+
+ var instant = Immediate(TestColor).init(from);
+ try std.testing.expect(!instant.isActive());
+ instant.retarget(to);
+ try std.testing.expectEqual(faded.displayed, instant.displayed);
+
+ // Never active, so a frontend that renders only while something is animating stops immediately
+ // rather than spending ten frames discovering there is nothing to draw.
+ try std.testing.expect(!instant.isActive());
+ instant.advance();
+ try std.testing.expectEqual(to, instant.displayed);
+
+ instant.snap(from);
+ try std.testing.expectEqual(from, instant.displayed);
+}
+
test "fixed-step interpolation has exact monotonic endpoints" {
const Tween = Transition(TestColor);
const from: TestColor = .{ .rgb = .{ 240, 3, 90 } };
diff --git a/src/pardes.zig b/src/pardes.zig
index 691ee345..9f20e0a3 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -68,6 +68,17 @@ pub const frameMark = tracy.frameMark;
pub const Platform = enum { tty, gui, web, macos, p4 };
pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config").platform));
+/// Whether a theme change FADES the anchored chrome palette or replaces it. Ten display frames
+/// either way (`animation.transition_steps`), and on every screen but one that is a short legible
+/// transition rather than a glitch.
+///
+/// The exception is a screen reached through a UART. Each of the ten steps recolors every anchored
+/// cell, so the diff finds the whole chrome dirty and spends a frame's worth of wire on it, ten times
+/// over, for a fade nobody can see arrive gradually anyway. Off by default for `p4` and settable
+/// either way from the build, because the thing that makes it wrong is the transport rather than the
+/// target - see `build.zig`.
+pub const theme_animation = @import("pardes_config").theme_animation;
+
/// A build with no host but its display: the embedded source filesystem, the
/// in-process clipboard, silent ptys. Comptime, and its own option module
/// rather than a `pardes_config` field, because it is the one setting that
@@ -2436,7 +2447,13 @@ pub const ChromeTheme = struct {
}
};
-const ChromeAnimation = animation.Transition(ChromeTheme);
+/// The fade, or its absence, decided at comptime so that `-Dtheme-animation=false` leaves
+/// `ChromeTheme.interpolate` unreachable and therefore out of the binary entirely. Every call site
+/// below is written against the shared interface and needs no condition of its own.
+const ChromeAnimation = if (theme_animation)
+ animation.Transition(ChromeTheme)
+else
+ animation.Immediate(ChromeTheme);
const initial_chrome = ChromeTheme.fromTheme(&themes[0]);
// ---- the boundary types ----
@@ -5548,6 +5565,10 @@ pub const Pardes = struct {
chrome_animation: ChromeAnimation = ChromeAnimation.init(initial_chrome),
/// False only while startup configuration or a dump restore is selecting
/// its first theme. No frame is rendered in that interval.
+ ///
+ /// Nothing to do with `-Dtheme-animation`: that is decided by the type of
+ /// `chrome_animation`, so a build without the fade does not carry a flag
+ /// saying so.
animate_theme_changes: bool = false,
/// Native pixel attachments supported by the shell (Kitty graphics in a
/// terminal, GPU textures in SDL). Image panes dynamically fall back to