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.zig96
2 files changed, 101 insertions, 3 deletions
diff --git a/src/config.zig b/src/config.zig
index 316ed3a0..1b432415 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -129,6 +129,7 @@ pub const leader_path = paths: {
if (builtins.capabilities.lift) table.set(.Afterglow, null);
table.set(.JumpTrail, null);
table.set(.ChipShadow, null);
+ table.set(.ThumbFlash, null);
if (builtins.capabilities.lift) table.set(.Lift, null);
table.set(.Motion, null);
table.set(.InactiveDim, null);
@@ -694,6 +695,9 @@ pub const Runtime = struct {
/// T1: in a terminal, a notice chip casts a cell's shadow right and
/// down (truecolor terminals).
chip_shadow: bool = false,
+ /// T7: in a terminal, a pane's scroll thumb brightens as it scrolls and
+ /// fades back (truecolor terminals).
+ thumb_flash: 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,
@@ -917,7 +921,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, jump_trail, chip_shadow };
+ 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, chip_shadow, thumb_flash };
/// 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 };
@@ -983,6 +987,7 @@ pub const Runtime = struct {
.{ .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 = "ChipShadow", .action = .{ .toggle = .chip_shadow }, .doc = "In a terminal, a notice chip casts a cell's shadow right and down, on or off." },
+ .{ .word = "ThumbFlash", .action = .{ .toggle = .thumb_flash }, .doc = "In a terminal, a pane's scroll thumb brightens as it scrolls and fades back, 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." },
@@ -1723,6 +1728,7 @@ pub const Runtime = struct {
.afterglow => "Afterglow on\n",
.jump_trail => "JumpTrail on\n",
.chip_shadow => "ChipShadow on\n",
+ .thumb_flash => "ThumbFlash 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 69934dd3..c4096faf 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -916,6 +916,8 @@ const Shell = struct {
tracks: []const pardes.animation.Track = &.{},
/// T2: the cursor's jump trail (JumpTrail).
trail: JumpTrail = .{},
+ /// T7: when each pane's view last moved (ThumbFlash).
+ flash: ThumbFlash = .{},
/// 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,
@@ -1164,8 +1166,11 @@ const Shell = struct {
const tz_cells = tracy.zone(@src(), "surface->vaxis");
const win = vx.window();
const truecolor = vx.caps.rgb or colortermTruecolor();
+ // Each of the shell's own motions below asks for frames while it runs.
+ s.core.shell_continuous = false;
const shadowed = chipShadows(s.frame.allocator(), surface, s.core, truecolor) catch surface;
- const trailed = s.trail.apply(s.frame.allocator(), shadowed, s.core, truecolor) catch shadowed;
+ const flashed = s.flash.apply(s.frame.allocator(), shadowed, s.core, truecolor) catch shadowed;
+ const trailed = s.trail.apply(s.frame.allocator(), flashed, s.core, truecolor) catch flashed;
paintCells(win, trailed.cells, trailed.cols, trailed.rows);
tz_cells.end();
for (surface.images[0..surface.nimages]) |maybe| {
@@ -1873,6 +1878,60 @@ fn chipShadows(arena: std.mem.Allocator, surface: *pardes.Surface, core: *pardes
return out;
}
+/// T7 (§9.2): a pane whose view moved (Region.line) has its scroll thumb
+/// brighten, drawn most of the way to the text's colour, and fade back
+/// over 250 ms with an ease-out: where you are in the file, as you move.
+/// The rail is chrome, not a focus indicator. Truecolor terminals only.
+const ThumbFlash = struct {
+ lines: [pardes.MAX_PANES]i32 = @splat(std.math.minInt(i32)),
+ since: [pardes.MAX_PANES]u64 = @splat(0),
+
+ const life_ns: u64 = 250 * std.time.ns_per_ms;
+
+ fn apply(flash: *ThumbFlash, arena: std.mem.Allocator, surface: *pardes.Surface, core: *pardes.Pardes, truecolor: bool) !*pardes.Surface {
+ const on = core.settings.thumb_flash and truecolor and core.settings.motion != .off;
+ var out: ?*pardes.Surface = null;
+ const theme = core.theme();
+ const ink = theme.fg orelse .{ 0xcc, 0xcc, 0xcc };
+ for (surface.regionList()) |body| {
+ if (body.kind != .body or body.owner >= flash.lines.len) continue;
+ const id = body.owner;
+ if (flash.lines[id] != std.math.minInt(i32) and flash.lines[id] != body.line) flash.since[id] = core.now_ns;
+ flash.lines[id] = body.line;
+ const age = core.now_ns -| flash.since[id];
+ if (!on or flash.since[id] == 0 or age >= life_ns) continue;
+ const rail = for (surface.regionList()) |region| {
+ if (region.kind == .rail and region.owner == id) break region;
+ } else continue;
+ if (rail.thumb_h == 0) continue;
+ const t = @as(f32, @floatFromInt(age)) / @as(f32, @floatFromInt(life_ns));
+ const strength = 0.6 * (1 - t) * (1 - t);
+ if (out == null) {
+ const copy = try arena.create(pardes.Surface);
+ copy.* = surface.*;
+ copy.cells = try arena.dupe(pardes.Cell, surface.cells);
+ out = copy;
+ }
+ const o = out.?;
+ var y = rail.rect.y + rail.thumb_y;
+ while (y < rail.rect.y + rail.thumb_y + rail.thumb_h and y < o.rows) : (y += 1) {
+ var x = rail.rect.x;
+ while (x < rail.rect.x + rail.rect.w and x < o.cols) : (x += 1) {
+ const cell = &o.cells[@as(usize, y) * o.cols + x];
+ const ground: [3]u8 = switch (cell.style.bg) {
+ .rgb => |rgb| rgb,
+ else => continue,
+ };
+ if (!std.mem.eql(u8, &ground, &core.chromeTheme().scroll_thumb)) continue;
+ cell.style.bg = .{ .rgb = pardes.colors.mixOklab(ground, ink, strength) };
+ }
+ }
+ core.shell_continuous = true;
+ }
+ return out orelse surface;
+ }
+};
+
/// 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 {
@@ -1896,7 +1955,7 @@ const JumpTrail = struct {
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;
+ defer core.shell_continuous = trail.active or core.shell_continuous;
const on = core.settings.jump_trail and truecolor and core.settings.motion != .off;
const cursor = surface.cursor orelse {
trail.active = false;
@@ -2259,6 +2318,7 @@ test "a jump leaves a trail of a few cells behind the cursor that fades in 120 m
// 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;
+ core.shell_continuous = false;
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 };
@@ -2302,3 +2362,35 @@ test "a notice chip casts a cell's shadow right and down, inside its body, off t
core.settings.chip_shadow = false;
try std.testing.expect(try chipShadows(arena.allocator(), s, core, true) == s);
}
+
+test "a scrolled pane's thumb brightens and fades back in 250 ms" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer core.deinit();
+ const pane = try core.setTestFile("some text on a line\n" ** 200);
+ try std.testing.expect(core.executeBuiltinLine(0, "Theme forge"));
+ core.settings.thumb_flash = true;
+ for (0..40) |_| core.update(.tick);
+ core.sync();
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ var flash: ThumbFlash = .{};
+ core.now_ns = std.time.ns_per_s;
+ const first = try core.render(arena.allocator());
+ try std.testing.expect(try flash.apply(arena.allocator(), first, core, true) == first);
+ pane.scrollBy(20);
+ core.now_ns += 16 * std.time.ns_per_ms;
+ const moved = try core.render(arena.allocator());
+ const lit = try flash.apply(arena.allocator(), moved, core, true);
+ try std.testing.expect(lit != moved and core.shell_continuous);
+ var brighter: usize = 0;
+ for (lit.cells, moved.cells) |a, b| {
+ if (!std.meta.eql(a.style.bg, b.style.bg)) brighter += 1;
+ }
+ try std.testing.expect(brighter >= 1);
+ core.now_ns += 300 * std.time.ns_per_ms;
+ core.shell_continuous = false;
+ const later = try core.render(arena.allocator());
+ try std.testing.expect(try flash.apply(arena.allocator(), later, core, true) == later);
+ try std.testing.expect(!core.shell_continuous);
+}