diff --git a/build.zig b/build.zig index 33c7b7f..e60c15a 100644 --- a/build.zig +++ b/build.zig @@ -12,6 +12,12 @@ pub fn build(b: *std.Build) void { }); vaxis.addImport("ziglyph", ziglyph.module("ziglyph")); + const zigimg = b.dependency("zigimg", .{ + .optimize = optimize, + .target = target, + }); + vaxis.addImport("zigimg", zigimg.module("zigimg")); + const exe = b.addExecutable(.{ .name = "vaxis", .root_source_file = .{ .path = "examples/text_input.zig" }, @@ -39,6 +45,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); lib_unit_tests.root_module.addImport("ziglyph", ziglyph.module("ziglyph")); + lib_unit_tests.root_module.addImport("zigimg", zigimg.module("zigimg")); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); diff --git a/build.zig.zon b/build.zig.zon index db07220..172aafd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,6 +14,10 @@ .url = "https://codeberg.org/dude_the_builder/ziglyph/archive/main.tar.gz", .hash = "12208553f3f47e51494e187f4c0e6f6b3844e3993436cad4a0e8c4db4e99645967b5", }, + .zigimg = .{ + .url = "https://github.com/zigimg/zigimg/archive/f6998808f283f8d3c2ef34e8b4af423bc1786f32.tar.gz", + .hash = "12202ee5d22ade0c300e9e7eae4c1951bda3d5f236fe1a139eb3613b43e2f12a88db", + } }, .paths = .{ diff --git a/src/Image.zig b/src/Image.zig new file mode 100644 index 0000000..638641b --- /dev/null +++ b/src/Image.zig @@ -0,0 +1,83 @@ +const std = @import("std"); +const math = std.math; +const testing = std.testing; +const zigimg = @import("zigimg"); + +const Window = @import("Window.zig"); +const Winsize = @import("Tty.zig").Winsize; + +const Image = @This(); + +pub const Source = union(enum) { + /// loads an image from a path. path can be relative to cwd, or absolute + path: []const u8, + /// loads an image from raw bytes + mem: []const u8, +}; + +pub const Protocol = enum { + kitty, + // TODO: sixel, full block, half block, quad block +}; + +/// the decoded image +img: zigimg.Image, + +/// unique identifier for this image +id: u32, + +/// width of the image, in cells +cell_width: usize, +/// height of the image, in cells +cell_height: usize, + +/// initialize a new image +pub fn init( + alloc: std.mem.Allocator, + winsize: Winsize, + src: Source, + id: u32, +) !Image { + const img = switch (src) { + .path => |path| try zigimg.Image.fromFilePath(alloc, path), + .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), + }; + // cell geometry + const pix_per_col = try math.divCeil(usize, winsize.x_pixel, winsize.cols); + const pix_per_row = try math.divCeil(usize, winsize.y_pixel, winsize.rows); + + const cell_width = math.divCeil(usize, img.width, pix_per_col) catch 0; + const cell_height = math.divCeil(usize, img.height, pix_per_row) catch 0; + + return Image{ + .img = img, + .cell_width = cell_width, + .cell_height = cell_height, + .id = id, + }; +} + +pub fn deinit(self: *Image) void { + self.img.deinit(); +} + +pub fn draw(self: *Image, win: Window, placement_id: u32) !void { + try win.writeImage(win.x_off, win.y_off, self, placement_id); +} + +test "image" { + const alloc = testing.allocator; + var img = try init( + alloc, + .{ + .rows = 1, + .cols = 1, + .x_pixel = 1, + .y_pixel = 1, + }, + .{ .path = "vaxis.png" }, + ); + defer img.deinit(); + try testing.expectEqual(1, img.cell_width); + try testing.expectEqual(1, img.cell_height); +} diff --git a/src/InternalScreen.zig b/src/InternalScreen.zig index 7bdda2d..abd3d85 100644 --- a/src/InternalScreen.zig +++ b/src/InternalScreen.zig @@ -3,6 +3,8 @@ const assert = std.debug.assert; const Style = @import("cell.zig").Style; const Cell = @import("cell.zig").Cell; const Shape = @import("Mouse.zig").Shape; +const Image = @import("Image.zig").Placement; +const Placement = @import("Screen.zig").Placement; const log = std.log.scoped(.internal_screen); @@ -35,10 +37,14 @@ cursor_vis: bool = false, mouse_shape: Shape = .default, +images: std.ArrayList(Placement) = undefined, + /// sets each cell to the default cell pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen { - var screen = InternalScreen{}; - screen.buf = try alloc.alloc(InternalCell, w * h); + var screen = InternalScreen{ + .buf = try alloc.alloc(InternalCell, w * h), + .images = std.ArrayList(Placement).init(alloc), + }; for (screen.buf, 0..) |_, i| { screen.buf[i] = .{ .char = try std.ArrayList(u8).initCapacity(alloc, 1), @@ -52,6 +58,7 @@ pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen { } pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void { + self.images.deinit(); for (self.buf, 0..) |_, i| { self.buf[i].char.deinit(); self.buf[i].uri.deinit(); diff --git a/src/Screen.zig b/src/Screen.zig index d87b6b5..4be5884 100644 --- a/src/Screen.zig +++ b/src/Screen.zig @@ -3,11 +3,19 @@ const assert = std.debug.assert; const Cell = @import("cell.zig").Cell; const Shape = @import("Mouse.zig").Shape; +const Image = @import("Image.zig"); const log = std.log.scoped(.screen); const Screen = @This(); +pub const Placement = struct { + img: *Image, + placement_id: u32, + col: usize, + row: usize, +}; + width: usize = 0, height: usize = 0, @@ -21,11 +29,14 @@ unicode: bool = false, mouse_shape: Shape = .default, +images: std.ArrayList(Placement) = undefined, + pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen { var self = Screen{ .buf = try alloc.alloc(Cell, w * h), .width = w, .height = h, + .images = std.ArrayList(Placement).init(alloc), }; for (self.buf, 0..) |_, i| { self.buf[i] = .{}; @@ -34,6 +45,7 @@ pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen { } pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void { alloc.free(self.buf); + self.images.deinit(); } /// writes a cell to a location. 0 indexed @@ -50,3 +62,19 @@ pub fn writeCell(self: *Screen, col: usize, row: usize, cell: Cell) void { assert(i < self.buf.len); self.buf[i] = cell; } + +pub fn writeImage( + self: *Screen, + col: usize, + row: usize, + img: *Image, + placement_id: u32, +) !void { + const p = Placement{ + .img = img, + .placement_id = placement_id, + .col = col, + .row = row, + }; + try self.images.append(p); +} diff --git a/src/Window.zig b/src/Window.zig index cd5bc12..425ed03 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Screen = @import("Screen.zig"); const Cell = @import("cell.zig").Cell; +const Image = @import("Image.zig"); const gw = @import("gwidth.zig"); const log = std.log.scoped(.window); @@ -68,9 +69,30 @@ pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void { self.screen.writeCell(col + self.x_off, row + self.y_off, cell); } +/// writes a cell to the location in the window +pub fn writeImage( + self: Window, + col: usize, + row: usize, + img: *Image, + placement_id: u32, +) !void { + if (self.height == 0 or self.width == 0) return; + if (self.height <= row or self.width <= col) return; + self.screen.writeImage(col, row, img, placement_id); +} + /// fills the window with the default cell pub fn clear(self: Window) void { self.fill(.{}); + // we clear any image with it's first cell within this window + for (self.screen.images.items, 0..) |p, i| { + if (p.col >= self.x_off and p.col < self.width and + p.row >= self.y_off and p.row < self.height) + { + _ = self.screen.images.swapRemove(i); + } + } } /// returns the width of the grapheme. This depends on the terminal capabilities diff --git a/src/main.zig b/src/main.zig index 1e760f7..6fd2407 100644 --- a/src/main.zig +++ b/src/main.zig @@ -17,6 +17,7 @@ pub fn init(comptime EventType: type, opts: Options) !Vaxis(EventType) { test { _ = @import("GraphemeCache.zig"); + _ = @import("Image.zig"); _ = @import("Key.zig"); _ = @import("Mouse.zig"); _ = @import("Options.zig"); diff --git a/src/vaxis.zig b/src/vaxis.zig index 01fc846..b1a2b09 100644 --- a/src/vaxis.zig +++ b/src/vaxis.zig @@ -14,6 +14,7 @@ const Style = @import("cell.zig").Style; const Hyperlink = @import("cell.zig").Hyperlink; const gwidth = @import("gwidth.zig"); const Shape = @import("Mouse.zig").Shape; +const Placement = Screen.Placement; /// Vaxis is the entrypoint for a Vaxis application. The provided type T should /// be a tagged union which contains all of the events the application will @@ -242,6 +243,7 @@ pub fn Vaxis(comptime T: type) type { // the next render call will refresh the entire screen pub fn queueRefresh(self: *Self) void { self.refresh = true; + self.screen_last.images.clearRetainingCapacity(); } /// draws the screen to the terminal @@ -278,6 +280,15 @@ pub fn Vaxis(comptime T: type) type { var cursor: Style = .{}; var link: Hyperlink = .{}; + // delete remove images from the screen by looping through the + // current state and comparing to the next state + for (self.screen_last.images.items) |last_img| { + const keep: bool = for (self.screen.images.items) |next_img| { + if (std.meta.eql(last_img, next_img)) break true; + } else false; + if (keep) continue; + } + var i: usize = 0; while (i < self.screen.buf.len) { const cell = self.screen.buf[i]; diff --git a/vaxis.png b/vaxis.png new file mode 100644 index 0000000..cee9f63 Binary files /dev/null and b/vaxis.png differ