summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.zig8
-rw-r--r--src/tty/tty.zig138
2 files changed, 144 insertions, 2 deletions
diff --git a/src/config.zig b/src/config.zig
index 2a764118..c0266e40 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -127,6 +127,7 @@ pub const leader_path = paths: {
if (builtins.capabilities.lift) table.set(.Occlusion, null);
if (builtins.capabilities.lift) table.set(.Parallax, null);
if (builtins.capabilities.lift) table.set(.Afterglow, null);
+ table.set(.JumpTrail, null);
if (builtins.capabilities.lift) table.set(.Lift, null);
table.set(.Motion, null);
table.set(.InactiveDim, null);
@@ -686,6 +687,9 @@ pub const Runtime = struct {
/// G14: text that changes glows for a moment in the theme's accent and
/// fades, a pixel shell's.
afterglow: bool = false,
+ /// T2: in a terminal, a jump of the cursor leaves a short trail that
+ /// fades in 120 ms (a truecolor terminal's; a pixel shell glides).
+ jump_trail: bool = false,
/// How long a message stays after the key or click that dismisses it.
message_linger_ms: u16 = 800,
/// How long its fall into the row takes, easing out (§8.1: arriving,
@@ -909,7 +913,7 @@ pub const Runtime = struct {
/// The message timings a setting word sets, each in milliseconds.
pub const MessageTime = enum { linger, fall, dissolve };
- pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures, cursor_blink, selection_glow, hover_glow, occlusion, parallax, afterglow };
+ pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures, cursor_blink, selection_glow, hover_glow, occlusion, parallax, afterglow, jump_trail };
/// A bundled post pass, a Shadertoy file in shaders/post/: the CRT, and
/// G4's bloom, vignette and grain (docs/effects.md).
pub const Scene = enum { crt, bloom, vignette, grain };
@@ -973,6 +977,7 @@ pub const Runtime = struct {
.{ .word = "Occlusion", .action = .{ .toggle = .occlusion }, .availability = .lift, .doc = "Pane bodies darken faintly toward their edges, on or off." },
.{ .word = "Parallax", .action = .{ .toggle = .parallax }, .availability = .lift, .doc = "A theme's page pattern moves with the text at a quarter of its speed, on or off." },
.{ .word = "Afterglow", .action = .{ .toggle = .afterglow }, .availability = .lift, .doc = "Text that changes glows for a moment and fades, on or off." },
+ .{ .word = "JumpTrail", .action = .{ .toggle = .jump_trail }, .doc = "In a terminal, a jump of the cursor leaves a short trail that fades, on or off." },
.{ .word = "Lift", .action = .lift, .availability = .lift, .doc = "How the focused pane stands off the page: off, shadow, rim or auto." },
.{ .word = "Motion", .action = .motion, .doc = "How animations move: off, crisp, smooth, bouncy or playful." },
.{ .word = "InactiveDim", .action = .inactive_dim, .doc = "Fade the unfocused panes' text by a percentage, 0 through 100, 0 off." },
@@ -1711,6 +1716,7 @@ pub const Runtime = struct {
.occlusion => "Occlusion on\n",
.parallax => "Parallax on\n",
.afterglow => "Afterglow on\n",
+ .jump_trail => "JumpTrail on\n",
},
.shell => "Shell requested (new panes) fish\n",
.tagline_size => "TaglineSize 73\n",
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index e7eddec4..8f07d7fc 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -914,6 +914,8 @@ const Shell = struct {
check_files: bool = false,
in_paste: bool = false,
tracks: []const pardes.animation.Track = &.{},
+ /// T2: the cursor's jump trail (JumpTrail).
+ trail: JumpTrail = .{},
/// PARDES_TEST_CLOCK's virtual time (host_io.testClock), and where the
/// running-out wait moves it: the core's next wake.
test_clock: ?u64 = null,
@@ -1161,7 +1163,8 @@ const Shell = struct {
) catch return;
const tz_cells = tracy.zone(@src(), "surface->vaxis");
const win = vx.window();
- paintCells(win, surface.cells, surface.cols, surface.rows);
+ const trailed = s.trail.apply(s.frame.allocator(), surface, s.core, vx.caps.rgb or colortermTruecolor()) catch surface;
+ paintCells(win, trailed.cells, trailed.cols, trailed.rows);
tz_cells.end();
for (surface.images[0..surface.nimages]) |maybe| {
const place = maybe orelse continue;
@@ -1804,6 +1807,95 @@ fn paintCells(win: vaxis.Window, cells: []const pardes.Cell, cols: u16, rows: u1
}
}
+/// Truecolor by COLORTERM (truecolor or 24bit), for a terminal that does
+/// not answer vaxis's query (§9.2: vaxis caps or COLORTERM).
+fn colortermTruecolor() bool {
+ const value = std.mem.span(std.c.getenv("COLORTERM") orelse return false);
+ return std.mem.eql(u8, value, "truecolor") or std.mem.eql(u8, value, "24bit");
+}
+
+/// T2 (docs/render-pipeline.md §9.2): the cursor jumping three cells or
+/// more leaves a trail of three to five cells along its path, just behind
+/// it, their ground drawn toward the text's colour, most beside the
+/// cursor, all fading out over 120 ms. Truecolor terminals only; a cell a
+/// frame changes, a few bytes. Never the cursor's own cell, a selection or
+/// anything outside the focused pane's body; off with Motion off.
+const JumpTrail = struct {
+ last: ?[2]u16 = null,
+ from: [2]u16 = .{ 0, 0 },
+ to: [2]u16 = .{ 0, 0 },
+ start_ns: u64 = 0,
+ active: bool = false,
+
+ const life_ns: u64 = 120 * std.time.ns_per_ms;
+
+ fn apply(trail: *JumpTrail, arena: std.mem.Allocator, surface: *pardes.Surface, core: *pardes.Pardes, truecolor: bool) !*pardes.Surface {
+ defer core.shell_continuous = trail.active;
+ const on = core.settings.jump_trail and truecolor and core.settings.motion != .off;
+ const cursor = surface.cursor orelse {
+ trail.active = false;
+ return surface;
+ };
+ const here: [2]u16 = .{ cursor.x, cursor.y };
+ if (trail.last) |was| {
+ const dx = @as(i32, here[0]) - was[0];
+ const dy = @as(i32, here[1]) - was[1];
+ if (on and @max(@abs(dx), @abs(dy)) >= 3) {
+ trail.* = .{ .last = here, .from = was, .to = here, .start_ns = core.now_ns, .active = true };
+ }
+ }
+ trail.last = here;
+ if (!on or !trail.active) {
+ trail.active = false;
+ return surface;
+ }
+ const age = core.now_ns -| trail.start_ns;
+ if (age >= life_ns or !std.meta.eql(trail.to, here)) {
+ trail.active = false;
+ return surface;
+ }
+ const out = try arena.create(pardes.Surface);
+ out.* = surface.*;
+ out.cells = try arena.dupe(pardes.Cell, surface.cells);
+ const theme = core.theme();
+ const ink = theme.fg orelse .{ 0xcc, 0xcc, 0xcc };
+ const page = theme.bg orelse pardes.colors.null_page;
+ const body = for (surface.regionList()) |region| {
+ if (region.kind == .body and region.owner == core.active) break region.rect;
+ } else return out;
+ const t = @as(f32, @floatFromInt(age)) / @as(f32, @floatFromInt(life_ns));
+ const fade = (1 - t) * (1 - t);
+ const dx = @as(f32, @floatFromInt(@as(i32, trail.from[0]) - trail.to[0]));
+ const dy = @as(f32, @floatFromInt(@as(i32, trail.from[1]) - trail.to[1]));
+ const span = @max(@abs(dx), @abs(dy));
+ const n: usize = @intFromFloat(std.math.clamp(@round(span / 4), 3, 5));
+ var k: usize = 1;
+ var last_cell: ?[2]i32 = null;
+ var step: f32 = 1;
+ while (k <= n and step <= span) : (step += 1) {
+ const x: i32 = @intFromFloat(@round(@as(f32, @floatFromInt(trail.to[0])) + dx * step / span));
+ const y: i32 = @intFromFloat(@round(@as(f32, @floatFromInt(trail.to[1])) + dy * step / span));
+ if (last_cell) |seen| if (seen[0] == x and seen[1] == y) continue;
+ last_cell = .{ x, y };
+ defer k += 1;
+ if (x < body.x or y < body.y or x >= @as(i32, body.x) + body.w or y >= @as(i32, body.y) + body.h) continue;
+ const cell = &out.cells[@as(usize, @intCast(y)) * out.cols + @as(usize, @intCast(x))];
+ const ground: [3]u8 = if (cell.default) page else switch (cell.style.bg) {
+ .rgb => |rgb| rgb,
+ else => page,
+ };
+ if (std.mem.eql(u8, &ground, &theme.sel_bg)) continue;
+ const strength = 0.55 * fade * (1 - @as(f32, @floatFromInt(k - 1)) / @as(f32, @floatFromInt(n)));
+ if (strength < 0.02) continue;
+ const was_default = cell.default;
+ cell.default = false;
+ if (was_default) cell.style = .{};
+ cell.style.bg = .{ .rgb = pardes.colors.mixOklab(ground, ink, strength) };
+ }
+ return out;
+ }
+};
+
fn paintCursor(win: vaxis.Window, x: u16, y: u16, bar: bool) void {
win.showCursor(x, y);
win.setCursorShape(if (bar) .beam else .default);
@@ -2064,3 +2156,47 @@ fn attachSession(init: std.process.Init, name: []const u8, tty: *vaxis.Tty, vx:
return attachLoop(&a);
}
+
+test "a jump leaves a trail of a few cells behind the cursor that fades in 120 ms" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.setTestFile("some text on a line\n" ** 20);
+ try std.testing.expect(core.executeBuiltinLine(0, "Theme forge"));
+ core.settings.jump_trail = true;
+ for (0..40) |_| core.update(.tick);
+ core.sync();
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ const s = try core.render(arena.allocator());
+ const body = for (s.regionList()) |region| {
+ if (region.kind == .body) break region.rect;
+ } else return error.NoBody;
+ var trail: JumpTrail = .{};
+ core.now_ns = std.time.ns_per_s;
+ s.cursor = .{ .x = body.x + 2, .y = body.y + 1 };
+ _ = try trail.apply(arena.allocator(), s, core, true);
+ // Ten cells right: a trail back toward where it was.
+ s.cursor = .{ .x = body.x + 12, .y = body.y + 1 };
+ core.now_ns += 16 * std.time.ns_per_ms;
+ const lit = try trail.apply(arena.allocator(), s, core, true);
+ try std.testing.expect(trail.active and core.shell_continuous);
+ var changed: usize = 0;
+ for (lit.cells, s.cells, 0..) |a, b, i| {
+ if (std.meta.eql(a.style.bg, b.style.bg)) continue;
+ changed += 1;
+ // On the cursor's row, behind it, never its own cell.
+ try std.testing.expectEqual(@as(usize, body.y + 1), i / s.cols);
+ try std.testing.expect(i % s.cols < body.x + 12 and i % s.cols > body.x + 2);
+ }
+ try std.testing.expect(changed >= 3 and changed <= 5);
+ // Gone by 120 ms; a step of a cell leaves none; nor a terminal without
+ // truecolor.
+ core.now_ns += 130 * std.time.ns_per_ms;
+ const faded = try trail.apply(arena.allocator(), s, core, true);
+ try std.testing.expect(faded == s and !trail.active and !core.shell_continuous);
+ s.cursor = .{ .x = body.x + 13, .y = body.y + 1 };
+ try std.testing.expect(try trail.apply(arena.allocator(), s, core, true) == s);
+ s.cursor = .{ .x = body.x + 2, .y = body.y + 1 };
+ try std.testing.expect(try trail.apply(arena.allocator(), s, core, false) == s);
+}