diff --git a/examples/image.zig b/examples/image.zig index acce241..f8008e0 100644 --- a/examples/image.zig +++ b/examples/image.zig @@ -39,6 +39,8 @@ pub fn main() !void { var n: usize = 0; + var clip_y: usize = 0; + while (true) { const event = loop.nextEvent(); switch (event) { @@ -47,7 +49,10 @@ pub fn main() !void { return; } else if (key.matches('l', .{ .ctrl = true })) { vx.queueRefresh(); - } + } else if (key.matches('j', .{})) + clip_y += 1 + else if (key.matches('k', .{})) + clip_y -|= 1; }, .winsize => |ws| try vx.resize(alloc, ws), } @@ -59,9 +64,9 @@ pub fn main() !void { const img = imgs[n]; const dims = try img.cellSize(win); const center = vaxis.widgets.alignment.center(win, dims.cols, dims.rows); - const scale = false; - const z_index = 0; - img.draw(center, scale, z_index); + try img.draw(center, .{ .scale = .contain, .clip_region = .{ + .y = clip_y, + } }); try vx.render(); } diff --git a/src/Image.zig b/src/Image.zig index ad45172..421421f 100644 --- a/src/Image.zig +++ b/src/Image.zig @@ -20,8 +20,7 @@ pub const Source = union(enum) { pub const Placement = struct { img_id: u32, - z_index: i32, - size: ?CellSize = null, + options: Image.DrawOptions, }; pub const CellSize = struct { @@ -29,29 +28,138 @@ pub const CellSize = struct { cols: usize, }; +pub const DrawOptions = struct { + /// an offset into the top left cell, in pixels, with where to place the + /// origin of the image. These must be less than the pixel size of a single + /// cell + pixel_offset: ?struct { + x: usize, + y: usize, + } = null, + /// the vertical stacking order + /// < 0: Drawn beneath text + /// < -1_073_741_824: Drawn beneath "default" background cells + z_index: ?i32 = null, + /// A clip region of the source image to draw. + clip_region: ?struct { + x: ?usize = null, + y: ?usize = null, + width: ?usize = null, + height: ?usize = null, + } = null, + /// Scaling to apply to the Image + scale: enum { + /// no scaling applied. the image may extend beyond the window + none, + /// Stretch / shrink the image to fill the window + fill, + /// Scale the image to fit the window, maintaining aspect ratio + fit, + /// Scale the image to fit the window, only if needed. + contain, + } = .none, + /// the size to render the image. Generally you will not need to use this + /// field, and should prefer to use scale. `draw` will fill in this field with + /// the correct values if a scale method is applied. + size: ?struct { + rows: ?usize = null, + cols: ?usize = null, + } = null, +}; + /// unique identifier for this image. This will be managed by the screen. id: u32, -// width in pixels +/// width in pixels width: usize, -// height in pixels +/// height in pixels height: usize, -pub fn draw(self: Image, win: Window, scale: bool, z_index: i32) void { - const p = Placement{ - .img_id = self.id, - .z_index = z_index, - .size = sz: { - if (!scale) break :sz null; - break :sz CellSize{ +pub fn draw(self: Image, win: Window, opts: DrawOptions) !void { + var p_opts = opts; + switch (opts.scale) { + .none => {}, + .fill => { + p_opts.size = .{ .rows = win.height, .cols = win.width, }; }, + .fit, + .contain, + => contain: { + // cell geometry + const x_pix = win.screen.width_pix; + const y_pix = win.screen.height_pix; + const w = win.screen.width; + const h = win.screen.height; + + const pix_per_col = try std.math.divCeil(usize, x_pix, w); + const pix_per_row = try std.math.divCeil(usize, y_pix, h); + + const win_width_pix = pix_per_col * win.width; + const win_height_pix = pix_per_row * win.height; + + const fit_x: bool = if (win_width_pix >= self.width) true else false; + const fit_y: bool = if (win_height_pix >= self.height) true else false; + + // Does the image fit with no scaling? + if (opts.scale == .contain and fit_x and fit_y) break :contain; + + // Does the image require vertical scaling? + if (fit_x and !fit_y) + p_opts.size = .{ + .rows = win.height, + } + + // Does the image require horizontal scaling? + else if (!fit_x and fit_y) + p_opts.size = .{ + .cols = win.height, + } + else if (!fit_x and !fit_y) { + const diff_x = self.width - win_width_pix; + const diff_y = self.height - win_height_pix; + // The width difference is larger than the height difference. + // Scale by width + if (diff_x > diff_y) + p_opts.size = .{ + .cols = win.width, + } + else + // The height difference is larger than the width difference. + // Scale by height + p_opts.size = .{ + .rows = win.height, + }; + } else { + std.debug.assert(opts.scale == .fit); + std.debug.assert(win_width_pix >= self.width); + std.debug.assert(win_height_pix >= self.height); + + // Fits in both directions. Find the closer direction + const diff_x = win_width_pix - self.width; + const diff_y = win_height_pix - self.height; + // The width is closer in dimension. Scale by that + if (diff_x < diff_y) + p_opts.size = .{ + .cols = win.width, + } + else + p_opts.size = .{ + .rows = win.height, + }; + } + }, + } + const p = Placement{ + .img_id = self.id, + .options = p_opts, }; win.writeCell(0, 0, .{ .image = p }); } +/// the size of the image, in cells pub fn cellSize(self: Image, win: Window) !CellSize { // cell geometry const x_pix = win.screen.width_pix; diff --git a/src/Vaxis.zig b/src/Vaxis.zig index 8ab70c9..6ead4e9 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -366,19 +366,57 @@ pub fn render(self: *Vaxis) !void { } if (cell.image) |img| { - if (img.size) |size| { - try std.fmt.format( - tty.buffered_writer.writer(), - ctlseqs.kitty_graphics_scale, - .{ img.img_id, img.z_index, size.cols, size.rows }, - ); - } else { - try std.fmt.format( - tty.buffered_writer.writer(), - ctlseqs.kitty_graphics_place, - .{ img.img_id, img.z_index }, + try tty.buffered_writer.writer().print( + ctlseqs.kitty_graphics_preamble, + .{img.img_id}, + ); + if (img.options.pixel_offset) |offset| { + try tty.buffered_writer.writer().print( + ",X={d},Y={d}", + .{ offset.x, offset.y }, ); } + if (img.options.clip_region) |clip| { + if (clip.x) |x| + try tty.buffered_writer.writer().print( + ",x={d}", + .{x}, + ); + if (clip.y) |y| + try tty.buffered_writer.writer().print( + ",y={d}", + .{y}, + ); + if (clip.width) |width| + try tty.buffered_writer.writer().print( + ",w={d}", + .{width}, + ); + if (clip.height) |height| + try tty.buffered_writer.writer().print( + ",h={d}", + .{height}, + ); + } + if (img.options.size) |size| { + if (size.rows) |rows| + try tty.buffered_writer.writer().print( + ",r={d}", + .{rows}, + ); + if (size.cols) |cols| + try tty.buffered_writer.writer().print( + ",c={d}", + .{cols}, + ); + } + if (img.options.z_index) |z| { + try tty.buffered_writer.writer().print( + ",z={d}", + .{z}, + ); + } + try tty.buffered_writer.writer().writeAll(ctlseqs.kitty_graphics_closing); } // something is different, so let's loop through everything and diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig index b663618..c0ecb43 100644 --- a/src/ctlseqs.zig +++ b/src/ctlseqs.zig @@ -107,5 +107,5 @@ pub const osc22_mouse_shape = "\x1b]22;{s}\x1b\\"; // Kitty graphics pub const kitty_graphics_clear = "\x1b_Ga=d\x1b\\"; -pub const kitty_graphics_place = "\x1b_Ga=p,i={d},z={d},C=1\x1b\\"; -pub const kitty_graphics_scale = "\x1b_Ga=p,i={d},z={d},c={d},r={d},C=1\x1b\\"; +pub const kitty_graphics_preamble = "\x1b_Ga=p,i={d}"; +pub const kitty_graphics_closing = ",C=1\x1b\\";