diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/fs.zig | 1 | ||||
| -rw-r--r-- | src/image.zig | 235 | ||||
| -rw-r--r-- | src/panes.zig | 231 |
3 files changed, 237 insertions, 230 deletions
@@ -37,6 +37,7 @@ const source_files = [_]Source{ .{ .path = "src/pardes.zig", .contents = @embedFile("pardes.zig") }, .{ .path = "src/panes.zig", .contents = @embedFile("panes.zig") }, .{ .path = "src/Mini.zig", .contents = @embedFile("Mini.zig") }, + .{ .path = "src/image.zig", .contents = @embedFile("image.zig") }, .{ .path = "src/layout.zig", .contents = @embedFile("layout.zig") }, .{ .path = "src/fs.zig", .contents = @embedFile("fs.zig") }, .{ .path = "src/look.zig", .contents = @embedFile("look.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 }); + }; +} diff --git a/src/panes.zig b/src/panes.zig index d494cfe4..bc579a92 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -3176,236 +3176,7 @@ pub const Output = struct { pub const Mini = @import("Mini.zig"); -pub const Image = struct { - const GridKey = struct { - cols: u16 = 0, - rows: u16 = 0, - palette: image.PaletteMode = .commodore, - ascii: bool = true, - }; - - pub const State = struct { - path: []u8, - glyph_art: bool = false, - pmode: image.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: image.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 (image.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 => image.GlyphArt.commodore, - .terminal => image.terminal_palette, - }; - state.grid = image.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 = image.GlyphArt.commodore[cell.fg] }, - .terminal => .{ .index = cell.fg }, - }; - const bg: pardes.Color = switch (state.pmode) { - .commodore => .{ .rgb = image.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 }); - }; - } -}; +pub const Image = @import("image.zig"); pub const Pdf = struct { pub const enabled = @import("pardes_config").mupdf; |
