summaryrefslogtreecommitdiff
path: root/src/tty/tty.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tty/tty.zig')
-rw-r--r--src/tty/tty.zig104
1 files changed, 103 insertions, 1 deletions
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);
+}