From 2352f0079404f31d458745e687d195d8f9c5b39f Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 27 Sep 2026 21:18:46 -0300 Subject: Move the Image pane namespace out of panes.zig into image.zig Pure move: the body of `pub const Image = struct { ... }` now sits at the end of image.zig, dedented, under a "the image pane" banner, so the image thing (decoding, glyph art, and the pane that shows it) is one file. Its nine `image.X` self-references lose the prefix; image.zig gains the four imports the pane code uses. panes.zig keeps `pub const Image = @import("image.zig");`, so `panes.Image.x` callers are unchanged. image.zig joins the embedded source list, since the pane code left panes.zig which was on it. Co-Authored-By: Claude Opus 5.5 --- src/image.zig | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) (limited to 'src/image.zig') diff --git a/src/image.zig b/src/image.zig index 592230c8..09f049ca 100644 --- a/src/image.zig +++ b/src/image.zig @@ -7,6 +7,10 @@ const zstbi = @import("zstbi"); const terminal_panes = @import("pardes.zig").terminal_panes; const ghostty_vt = if (terminal_panes) @import("ghostty-vt") else struct {}; const pdf_enabled = @import("pardes_config").mupdf; +const pardes = @import("pardes.zig"); +const config = @import("config.zig"); +const filesystem = @import("fs.zig"); +const dump = @import("dump.zig"); // Glyph matching adapted from caioluders/petsciinator. pub const GlyphArt = struct { pub const commodore = [16][3]u8{ @@ -751,3 +755,234 @@ pub fn decode(gpa: std.mem.Allocator, bytes: []const u8) ?struct { rgba: []u8, w const rgba = gpa.dupe(u8, src.data) catch return null; return .{ .rgba = rgba, .w = src.width, .h = src.height }; } + +// ---- the image pane ---- + +const GridKey = struct { + cols: u16 = 0, + rows: u16 = 0, + palette: PaletteMode = .commodore, + ascii: bool = true, +}; + +pub const State = struct { + path: []u8, + glyph_art: bool = false, + pmode: PaletteMode = .commodore, + ascii: bool = true, + tried: bool = false, + rgba: []u8 = &.{}, + iw: usize = 0, + ih: usize = 0, + /// Dump-loaded bytes, decoded lazily by the same path as a disk image. + raw: []u8 = &.{}, + grid: GlyphArt.Grid = .{ .cells = &.{}, .cols = 0, .rows = 0 }, + grid_key: GridKey = .{}, + + pub fn deinit(state: *State, gpa: std.mem.Allocator) void { + gpa.free(state.path); + if (state.rgba.len > 0) gpa.free(state.rgba); + if (state.raw.len > 0) gpa.free(state.raw); + if (state.grid.cells.len > 0) gpa.free(state.grid.cells); + state.* = undefined; + } +}; + +/// Construct an image pane from a path and optionally transferred dump bytes. +/// `raw` must be image_gpa-owned and ownership transfers only on success. +/// Without them the file is read here, not at the first draw: a draw +/// must not go out into the host (`pardes.turn`), and the path may be a +/// mount this editor serves. A file that cannot be read draws blank. +pub fn create(p: *pardes.Pardes, id: usize, path: []const u8, raw: []u8) !*pardes.Pane { + var bytes = raw; + var read_here = false; + if (bytes.len == 0) { + // The slot stays ours across the read: another request may + // make a pane meanwhile. + p.reserved_slots[id] = true; + defer p.reserved_slots[id] = false; + if (filesystem.read(p, path)) |from_disk| { + defer p.gpa.free(from_disk); + bytes = try p.image_gpa.dupe(u8, from_disk); + read_here = true; + } else |_| {} + } + errdefer if (read_here) p.image_gpa.free(bytes); + const path_copy = try p.image_gpa.dupe(u8, path); + errdefer p.image_gpa.free(path_copy); + const pane = try p.newDocPane(id); + pane.image = .{ .path = path_copy, .raw = bytes }; + return pane; +} + +/// Serialize the binary image record used by images and, for dump-schema +/// compatibility, PDFs. Common pane metadata is supplied by the core. +pub fn dumpPane( + p: *pardes.Pardes, + arena: std.mem.Allocator, + pane: *const pardes.Pane, + tag: []const u8, + body: []const u8, + scroll: usize, + path: []const u8, + raw: []const u8, +) !dump.Pane { + const bytes = if (raw.len > 0) raw else filesystem.read(p, path) catch &.{}; + defer if (raw.len == 0) p.gpa.free(bytes); + return .{ + .kind = .image, + .tag = tag, + .body = body, + .scroll = scroll, + .cols = pane.cols, + .rows = pane.rows, + .vweight = pane.vweight, + .collapsed = pane.collapsed, + .image = .{ + .path = path, + .bytes_b64 = if (bytes.len > 0) try dump.encodeBytes(arena, bytes) else "", + .petscii = if (pane.image) |state| state.glyph_art else false, + .palette = if (pane.image) |state| switch (state.pmode) { + .commodore => .commodore, + .terminal => .terminal, + } else .commodore, + .ascii = if (pane.image) |state| state.ascii else true, + }, + }; +} + +pub fn restore(p: *pardes.Pardes, id: usize, src: dump.Pane) !*pardes.Pane { + const saved = src.image.?; + var raw: []u8 = if (saved.bytes_b64.len > 0) + try dump.decodeBytes(p.image_gpa, saved.bytes_b64) + else + &.{}; + errdefer if (raw.len > 0) p.image_gpa.free(raw); + const pane = try create(p, id, saved.path, raw); + raw = &.{}; + pane.image.?.glyph_art = saved.petscii; + pane.image.?.pmode = switch (saved.palette) { + .commodore => .commodore, + .terminal => .terminal, + }; + pane.image.?.ascii = saved.ascii; + pane.cols = @max(1, src.cols); + pane.rows = @max(1, src.rows); + return pane; +} + +pub fn toggleGlyphArt(state: *State) void { + state.glyph_art = !state.glyph_art; +} + +pub fn togglePalette(state: *State) void { + state.pmode = if (state.pmode == .commodore) .terminal else .commodore; +} + +pub fn toggleAscii(state: *State) void { + state.ascii = !state.ascii; +} + +/// Image renderer choices are pane-local, not global Config values. Keep them +/// queryable where they apply: the live tag beside the image's path. +pub fn tagPrefix(arena: std.mem.Allocator, state: *const State) ![]u8 { + return std.fmt.allocPrint( + arena, + "{s} petscii:{s} palette:{s} ascii:{s} {s}", + .{ + config.tag_image, + if (state.glyph_art) "on" else "off", + @tagName(state.pmode), + if (state.ascii) "on" else "off", + state.path, + }, + ); +} + +pub fn legacySavedPrefix(state: *const State, saved_tag: []const u8) ?[]const u8 { + const lead = config.tag_image ++ " "; + if (!std.mem.startsWith(u8, saved_tag, lead)) return null; + const path_at = lead.len; + if (!std.mem.startsWith(u8, saved_tag[path_at..], state.path)) return null; + return saved_tag[0 .. path_at + state.path.len]; +} + +fn ensureDecoded(p: *pardes.Pardes, state: *State) void { + if (state.tried) return; + state.tried = true; + if (state.raw.len == 0) return; // nothing was readable at open + if (decode(p.image_gpa, state.raw)) |decoded| { + state.rgba = decoded.rgba; + state.iw = decoded.w; + state.ih = decoded.h; + } +} + +fn ensureGrid(p: *pardes.Pardes, state: *State, cols: u16, rows: u16) void { + const wanted = GridKey{ .cols = cols, .rows = rows, .palette = state.pmode, .ascii = state.ascii }; + if (state.grid.cells.len > 0 and std.meta.eql(state.grid_key, wanted)) return; + if (state.grid.cells.len > 0) p.image_gpa.free(state.grid.cells); + const palette = switch (state.pmode) { + .commodore => GlyphArt.commodore, + .terminal => terminal_palette, + }; + state.grid = GlyphArt.render( + p.image_gpa, + state.rgba, + state.iw, + state.ih, + cols, + rows, + palette, + state.ascii, + ) catch .{ .cells = &.{}, .cols = 0, .rows = 0 }; + state.grid_key = wanted; +} + +pub fn draw( + p: *pardes.Pardes, + state: *State, + pane_id: u8, + serial: u32, + x: u16, + y: u16, + cols: u16, + rows: u16, +) void { + ensureDecoded(p, state); + if (state.rgba.len == 0 or cols == 0 or rows == 0) return; + if (!state.glyph_art and p.native_images) { + _ = p.appendImagePlace(.{ + .pane = pane_id, + .serial = serial, + .x = x, + .y = y, + .w = cols, + .h = rows, + .rgba = state.rgba, + .iw = state.iw, + .ih = state.ih, + }); + return; + } + + ensureGrid(p, state, cols, rows); + if (state.grid.cells.len == 0) return; + const offx = if (cols > state.grid.cols) (@as(usize, cols) - state.grid.cols) / 2 else 0; + const offy = if (rows > state.grid.rows) (@as(usize, rows) - state.grid.rows) / 2 else 0; + for (0..state.grid.rows) |cy| for (0..state.grid.cols) |cx| { + const cell = &state.grid.cells[cy * state.grid.cols + cx]; + const fg: pardes.Color = switch (state.pmode) { + .commodore => .{ .rgb = GlyphArt.commodore[cell.fg] }, + .terminal => .{ .index = cell.fg }, + }; + const bg: pardes.Color = switch (state.pmode) { + .commodore => .{ .rgb = GlyphArt.commodore[cell.bg] }, + .terminal => .{ .index = cell.bg }, + }; + const sx = x + @as(u16, @intCast(offx + cx)); + const sy = y + @as(u16, @intCast(offy + cy)); + if (sx < p.surface.cols and sy < p.surface.rows) + p.surface.set(sx, sy, cell.glyph[0..cell.glyph_len], .{ .fg = fg, .bg = bg }); + }; +} -- cgit v1.3