From c99dffaecf96d82502c5e8e7d1191e8507399e2f Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Thu, 25 Jan 2024 13:20:32 -0600 Subject: [PATCH] WIP: images Signed-off-by: Tim Culverhouse --- build.zig | 7 ++++ build.zig.zon | 4 ++ src/Image.zig | 83 +++++++++++++++++++++++++++++++++++++++++ src/InternalScreen.zig | 11 +++++- src/Screen.zig | 28 ++++++++++++++ src/Window.zig | 22 +++++++++++ src/main.zig | 1 + src/vaxis.zig | 11 ++++++ vaxis.png | Bin 0 -> 15987 bytes 9 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/Image.zig create mode 100644 vaxis.png 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 0000000000000000000000000000000000000000..cee9f63b67cbf44c346d5f4be4b423b3cacad3f4 GIT binary patch literal 15987 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 literal 0 HcmV?d00001