summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 08:59:20 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commita544c0b1af65427f2186baaa5dff176bcd310915 (patch)
treed6851d8d3623200a7b28e75fa6d470600c8aec01 /src
parent137e784bb617438bec5079ec5fef4b16ce8b75fe (diff)
downloadpardes-a544c0b1af65427f2186baaa5dff176bcd310915.tar.gz
pardes-a544c0b1af65427f2186baaa5dff176bcd310915.zip
T1: ChipShadow, a terminal's notice chips cast a cell's shadow
ChipShadow on|off (off; the tty shell's, §9.2 T1): each notice chip casts a shadow one cell right and one down inside its own pane's body: on a blank cell a half block (▌ right, ▀ below, ▘ at the corner) in a colour 35% darker than the ground (OKLab), so the shadow is half a cell thick; under text the ground itself darkened. Never the cursor's cell, a selection, a tag or another chip; truecolor terminals only. Tests: the shadow lies right of or below the chip, never in it; nothing with it off or without truecolor. Snapshots unchanged. Feel review: .scratch/render/tty/shadow/.
Diffstat (limited to 'src')
-rw-r--r--src/config.zig8
-rw-r--r--src/tty/tty.zig104
2 files changed, 110 insertions, 2 deletions
diff --git a/src/config.zig b/src/config.zig
index c0266e40..316ed3a0 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -128,6 +128,7 @@ pub const leader_path = paths: {
if (builtins.capabilities.lift) table.set(.Parallax, null);
if (builtins.capabilities.lift) table.set(.Afterglow, null);
table.set(.JumpTrail, null);
+ table.set(.ChipShadow, null);
if (builtins.capabilities.lift) table.set(.Lift, null);
table.set(.Motion, null);
table.set(.InactiveDim, null);
@@ -690,6 +691,9 @@ pub const Runtime = struct {
/// 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,
+ /// T1: in a terminal, a notice chip casts a cell's shadow right and
+ /// down (truecolor terminals).
+ chip_shadow: 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,
@@ -913,7 +917,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 };
+ 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 };
/// 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 };
@@ -978,6 +982,7 @@ pub const Runtime = struct {
.{ .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 = "ChipShadow", .action = .{ .toggle = .chip_shadow }, .doc = "In a terminal, a notice chip casts a cell's shadow right and down, 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." },
@@ -1717,6 +1722,7 @@ pub const Runtime = struct {
.parallax => "Parallax on\n",
.afterglow => "Afterglow on\n",
.jump_trail => "JumpTrail on\n",
+ .chip_shadow => "ChipShadow 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 8f07d7fc..69934dd3 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1163,7 +1163,9 @@ const Shell = struct {
) catch return;
const tz_cells = tracy.zone(@src(), "surface->vaxis");
const win = vx.window();
- const trailed = s.trail.apply(s.frame.allocator(), surface, s.core, vx.caps.rgb or colortermTruecolor()) catch surface;
+ const truecolor = vx.caps.rgb or colortermTruecolor();
+ 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;
paintCells(win, trailed.cells, trailed.cols, trailed.rows);
tz_cells.end();
for (surface.images[0..surface.nimages]) |maybe| {
@@ -1807,6 +1809,70 @@ fn paintCells(win: vaxis.Window, cells: []const pardes.Cell, cols: u16, rows: u1
}
}
+/// T1 (§9.2): each notice chip casts a shadow one cell right and one down,
+/// inside its own pane's body: its ground 35% toward black in OKLab on a
+/// cell with text; on a blank one a half block (▌ on the right, ▀ below,
+/// ▘ at the corner) in the darker colour, so the shadow is half a cell
+/// thick and reads as one. Never the cursor's cell, a selection, a tag or
+/// another chip. Truecolor terminals only.
+fn chipShadows(arena: std.mem.Allocator, surface: *pardes.Surface, core: *pardes.Pardes, truecolor: bool) !*pardes.Surface {
+ if (!core.settings.chip_shadow or !truecolor) return surface;
+ const regions = surface.regionList();
+ const any = for (regions) |region| {
+ if (region.kind == .notice) break true;
+ } else false;
+ if (!any) 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 page = theme.bg orelse pardes.colors.null_page;
+ for (regions) |chip| {
+ if (chip.kind != .notice or chip.rect.w == 0) continue;
+ const body = for (regions) |region| {
+ if (region.kind == .body and region.owner == chip.owner) break region.rect;
+ } else continue;
+ const right = chip.rect.x + chip.rect.w;
+ const below = chip.rect.y + chip.rect.h;
+ // The column right of it (from its second row), the row below (from
+ // its second column), and the corner.
+ var y = chip.rect.y;
+ while (y <= below) : (y += 1) {
+ var x = chip.rect.x;
+ while (x <= right) : (x += 1) {
+ const side = x == right and y > chip.rect.y and y < below;
+ const foot = y == below and x > chip.rect.x and x < right;
+ const corner = x == right and y == below;
+ if (!side and !foot and !corner) continue;
+ if (x < body.x or y < body.y or x >= body.x + body.w or y >= body.y + body.h or x >= out.cols or y >= out.rows) continue;
+ if (surface.cursor) |cu| if (cu.x == x and cu.y == y) continue;
+ const covered = for (regions) |region| {
+ if ((region.kind == .notice or region.kind == .tag or region.kind == .grip) and x >= region.rect.x and x < region.rect.x + region.rect.w and y >= region.rect.y and y < region.rect.y + region.rect.h) break true;
+ } else false;
+ if (covered) continue;
+ const cell = &out.cells[@as(usize, y) * out.cols + 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 dark = pardes.colors.mixOklab(ground, .{ 0, 0, 0 }, 0.35);
+ const blank = cell.default or cell.len == 0 or (cell.len == 1 and cell.text[0] == ' ');
+ if (blank) {
+ const glyph: []const u8 = if (corner) "▘" else if (side) "▌" else "▀";
+ cell.* = .{ .default = false, .style = .{ .fg = .{ .rgb = dark }, .bg = .{ .rgb = ground } } };
+ @memcpy(cell.text[0..glyph.len], glyph);
+ cell.len = @intCast(glyph.len);
+ } else {
+ cell.default = false;
+ cell.style.bg = .{ .rgb = dark };
+ }
+ }
+ }
+ }
+ return out;
+}
+
/// 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 {
@@ -2200,3 +2266,39 @@ test "a jump leaves a trail of a few cells behind the cursor that fades in 120 m
s.cursor = .{ .x = body.x + 2, .y = body.y + 1 };
try std.testing.expect(try trail.apply(arena.allocator(), s, core, false) == s);
}
+
+test "a notice chip casts a cell's shadow right and down, inside its body, off the cursor" {
+ 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.chip_shadow = true;
+ try std.testing.expect(core.executeBuiltinLine(0, "Msg a notice"));
+ for (0..60) |_| core.update(.tick);
+ core.sync();
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ const s = try core.render(arena.allocator());
+ const chip = for (s.regionList()) |region| {
+ if (region.kind == .notice) break region.rect;
+ } else return error.NoNotice;
+ s.cursor = null;
+ const out = try chipShadows(arena.allocator(), s, core, true);
+ try std.testing.expect(out != s);
+ var changed: usize = 0;
+ for (out.cells, s.cells, 0..) |a, b, i| {
+ if (std.meta.eql(a, b)) continue;
+ changed += 1;
+ const x = i % s.cols;
+ const y = i / s.cols;
+ // Right of the chip or below it, never inside it.
+ try std.testing.expect(x == chip.x + chip.w or y == chip.y + chip.h);
+ try std.testing.expect(!(x >= chip.x and x < chip.x + chip.w and y >= chip.y and y < chip.y + chip.h));
+ }
+ try std.testing.expect(changed >= chip.w - 1);
+ // Off, or without truecolor: the frame as it was.
+ try std.testing.expect(try chipShadows(arena.allocator(), s, core, false) == s);
+ core.settings.chip_shadow = false;
+ try std.testing.expect(try chipShadows(arena.allocator(), s, core, true) == s);
+}