diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 09:04:44 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 99149638c2b89fd134131d081bfd5fc8f967c53e (patch) | |
| tree | bc1de1e1b2f96cd1da79f7d0b6bff40b619d5ae3 /src | |
| parent | a544c0b1af65427f2186baaa5dff176bcd310915 (diff) | |
| download | pardes-99149638c2b89fd134131d081bfd5fc8f967c53e.tar.gz pardes-99149638c2b89fd134131d081bfd5fc8f967c53e.zip | |
T7: the scroll thumb flashes as a terminal pane scrolls
ThumbFlash (off by default) brightens a body's scroll thumb toward the ink
when its view moves and fades it back over 250 ms, truecolor only. The tty
present path now clears shell_continuous once before its effects and each
effect only raises it, so the trail and the flash no longer mask each other.
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.zig | 8 | ||||
| -rw-r--r-- | src/tty/tty.zig | 96 |
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); +} |
