From a544c0b1af65427f2186baaa5dff176bcd310915 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 08:59:20 -0300 Subject: T1: ChipShadow, a terminal's notice chips cast a cell's shadow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/. --- src/config.zig | 8 ++++- src/tty/tty.zig | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) (limited to 'src') 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); +} -- cgit v1.3