╭ changed ───────────╮ │src/embed/index.html│ ╰────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │30 #grid { │ │31 white-space: pre; │ │32 /* positioning context for the top-level .scroll divs */│ │33 + position: relative; │ │34 + } │ │35 + │ │36 + .ansi-art { │ │37 + position: absolute; │ │38 + top: 0; │ │39 + pointer-events: none; │ │40 + } │ │41 + │ │42 + .grid-content { │ │43 position: relative; │ │44 } │ │45 │ ╰────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │3128 .max_size = constraint.max_size, │ │3129 }, root_focus); │ │3130 │ │3131 - if (self.session.data.enable_ansi) { │ │3132 - self.art.content = self.session.data.ansi_art; │ │3133 - if (self.child.getGrid()) |fg| { │ │3134 - self.grid = try artBehind(allocator, fg, &self.art, .top_right, self.session.is_terminal, root_focus);│ │3135 - } │ │3136 - } │ │3131 + if (!self.session.data.enable_ansi) return; │ │3132 + const foreground = self.child.getGrid() orelse return; │ │3133 + self.art.content = self.session.data.ansi_art; │ │3134 + try self.buildArt(allocator, foreground.size, root_focus); │ │3135 + if (!self.session.is_terminal) return; │ │3136 + const art_grid = self.art.getGrid() orelse return; │ │3137 + self.grid = try artBehind(allocator, foreground, art_grid); │ │3138 } │ │3139 │ │3140 pub fn input(self: *AnsiBackground, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │3158 return self.child.getFocus(); │ │3159 } │ │3160 │ │3160 - // which top corner the art hugs. │ │3161 - const ArtAnchor = enum { top_left, top_right }; │ │3162 - │ │3161 // a foreground cell counts as blank if it's empty or a bare space without styling│ │3162 fn cellIsBlank(cell: Grid.Cell) bool { │ │3163 if (cell.rune) |rune| { │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │3180 }; │ │3181 } │ │3182 │ │3183 + fn buildArt(self: *AnsiBackground, allocator: std.mem.Allocator, size: layout.Size, root_focus: *Focus) !void {│ │3184 + try self.art.build(allocator, .{ │ │3185 + .min_size = .{ .width = null, .height = null }, │ │3186 + .max_size = .{ .width = size.width, .height = size.height }, │ │3187 + }, root_focus); │ │3188 + if (self.art.grid) |*grid| { │ │3189 + for (grid.cells.items) |*cell| { │ │3190 + cell.style.fg = dimColor(cell.style.fg); │ │3191 + cell.style.bg = dimColor(cell.style.bg); │ │3192 + } │ │3193 + } │ │3194 + } │ │3195 + │ │3196 // terminals that don't support truecolor misparse a "38;2;r;g;b"/"48;2;…" │ │3197 // SGR as a list of plain SGR codes, so any channel value in 1..9 turns on │ │3198 // a text attribute (5/6 = blink, 7 = inverse, 8 = conceal, …). │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │3210 } │ │3211 │ │3212 // composites ANSI art behind a foreground grid │ │3202 - fn artBehind(allocator: std.mem.Allocator, foreground: Grid, art: *AnsiArt, anchor: ArtAnchor, is_terminal: bool, root_focus: *Focus) !Grid {│ │3203 - art.clearGrid(); │ │3204 - try art.build(allocator, .{ │ │3205 - .min_size = .{ .width = null, .height = null }, │ │3206 - .max_size = .{ .width = foreground.size.width, .height = foreground.size.height }, │ │3207 - }, root_focus); │ │3208 - │ │3213 + fn artBehind(allocator: std.mem.Allocator, foreground: Grid, art_grid: Grid) !Grid { │ │3214 var out = try Grid.initFromGrid(allocator, foreground, foreground.size, 0, 0); │ │3215 errdefer out.deinit(); │ │3216 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │3212 - if (art.getGrid()) |art_grid| { │ │3213 - const anchor_x = switch (anchor) { │ │3214 - .top_left => 0, │ │3215 - .top_right => foreground.size.width -| art_grid.size.width, │ │3216 - }; │ │3217 - for (0..art_grid.size.height) |y| { │ │3218 - for (0..art_grid.size.width) |x| { │ │3219 - const src = art_grid.cells.items[try art_grid.cells.at(.{ y, x })]; │ │3220 - if (src.rune == null) continue; │ │3221 - const idx = out.cells.at(.{ y, anchor_x + x }) catch continue; │ │3222 - const dst = &out.cells.items[idx]; │ │3223 - // composite the dimmed art behind every cell so it shows │ │3224 - // through the whole UI: a blank cell becomes the art, while a │ │3225 - // cell with a glyph keeps the glyph but takes the dimmed art as │ │3226 - // its background (replacing whatever background it had, which │ │3227 - // would otherwise obscure the art). │ │3228 - if (cellIsBlank(dst.*)) { │ │3229 - dst.* = src; │ │3230 - dst.style.fg = sgrSafe(dimColor(dst.style.fg)); │ │3231 - dst.style.bg = sgrSafe(dimColor(dst.style.bg)); │ │3232 - } else { │ │3233 - const background_maybe = sgrSafe(dimColor(src.style.bg orelse src.style.fg));│ │3234 - dst.style.bg = background_maybe; │ │3235 - // change the color of text overlaying ANSI art to ensure │ │3236 - // it is readable. don't do this in the web UI, since we │ │3237 - // have direct control of the theme there (also, inner │ │3238 - // scroll panes allow text to move without a re-render │ │3239 - // by the TUI, which would break this). also don't do this │ │3240 - // if the text is explicitly styled. │ │3241 - if (is_terminal and dst.style.fg == null) { │ │3242 - if (background_maybe) |background| { │ │3243 - // use near-black and near-white, which is easier on the eyes │ │3244 - // while retaining high contrast │ │3245 - const luminance = (@as(u32, background.r) * 299 + │ │3246 - @as(u32, background.g) * 587 + │ │3247 - @as(u32, background.b) * 114) / 1000; │ │3248 - dst.style.fg = if (luminance >= 128) │ │3249 - .{ .r = 16, .g = 16, .b = 16 } │ │3250 - else │ │3251 - .{ .r = 240, .g = 240, .b = 240 }; │ │3252 - } │ │3217 + const anchor_x = foreground.size.width -| art_grid.size.width; │ │3218 + for (0..art_grid.size.height) |y| { │ │3219 + for (0..art_grid.size.width) |x| { │ │3220 + const src = art_grid.cells.items[try art_grid.cells.at(.{ y, x })]; │ │3221 + if (src.rune == null) continue; │ │3222 + const idx = try out.cells.at(.{ y, anchor_x + x }); │ │3223 + const dst = &out.cells.items[idx]; │ │3224 + // blank cells take the art; occupied cells take its background │ │3225 + if (cellIsBlank(dst.*)) { │ │3226 + dst.* = src; │ │3227 + dst.style.fg = sgrSafe(dst.style.fg); │ │3228 + dst.style.bg = sgrSafe(dst.style.bg); │ │3229 + } else { │ │3230 + const background_maybe = sgrSafe(src.style.bg orelse src.style.fg); │ │3231 + dst.style.bg = background_maybe; │ │3232 + // change unstyled text to contrast with the art behind it │ │3233 + if (dst.style.fg == null) { │ │3234 + if (background_maybe) |background| { │ │3235 + // use near-black and near-white, which is easier on the eyes │ │3236 + // while retaining high contrast │ │3237 + const luminance = (@as(u32, background.r) * 299 + │ │3238 + @as(u32, background.g) * 587 + │ │3239 + @as(u32, background.b) * 114) / 1000; │ │3240 + dst.style.fg = if (luminance >= 128) │ │3241 + .{ .r = 16, .g = 16, .b = 16 } │ │3242 + else │ │3243 + .{ .r = 240, .g = 240, .b = 240 }; │ │3244 } │ │3245 } │ │3246 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1559 var out: std.ArrayList(u8) = .empty; │ │1560 errdefer out.deinit(allocator); │ │1561 │ │1562 + switch (root.*) { │ │1563 + .background => |*background| { │ │1564 + if (background.art.getGrid()) |art| { │ │1565 + var position_buf: [64]u8 = undefined; │ │1566 + try out.appendSlice(allocator, try std.fmt.bufPrint(&position_buf, "<div class=\"ansi-art\" aria-hidden=\"true\" style=\"left:{d}ch\">", .{grid.size.width -| art.size.width}));│ │1567 + try renderPanel(allocator, &out, background.art.getFocus(), art); │ │1568 + try out.appendSlice(allocator, "</div>"); │ │1569 + } │ │1570 + }, │ │1571 + else => {}, │ │1572 + } │ │1573 + try out.appendSlice(allocator, "<div class=\"grid-content\">"); │ │1574 try renderPanel(allocator, &out, root.getFocus(), grid); │ │1575 + try out.appendSlice(allocator, "</div>"); │ │1576 return try out.toOwnedSlice(allocator); │ │1577 } │ │1578 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯