From 0ebf14fd44a829c7edd7791cb92b3c029b0df127 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2024 10:39:18 -0700 Subject: [PATCH] terminal/kitty: working on moving placement math here for testing --- src/renderer/Metal.zig | 44 ++++- src/terminal/kitty/graphics.zig | 2 + src/terminal/kitty/graphics_unicode.zig | 227 ++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 7 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e5d1a5a77..e22faaf4c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1632,8 +1632,6 @@ fn prepKittyGraphics( var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( t, - &top, - &bot, &virtual_p, ); } @@ -1675,13 +1673,8 @@ fn prepKittyGraphics( fn prepKittyVirtualPlacement( self: *Metal, t: *terminal.Terminal, - top: *const terminal.Pin, - bot: *const terminal.Pin, p: *const terminal.kitty.graphics.unicode.Placement, ) !void { - _ = top; - _ = bot; - const storage = &t.screen.kitty_images; const image = storage.imageById(p.image_id) orelse { log.warn( @@ -1691,6 +1684,43 @@ fn prepKittyVirtualPlacement( return; }; + if (true) { + const rp = p.renderPlacement( + &t.screen.kitty_images, + &image, + self.grid_metrics.cell_width, + self.grid_metrics.cell_height, + ) catch |err| { + log.warn("error rendering virtual placement err={}", .{err}); + return; + }; + + // Send our image to the GPU + try self.prepKittyImage(&image); + + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( + .viewport, + rp.top_left, + ) orelse @panic("TODO: unreachable?"); + + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rp.top_left.x), + .y = @intCast(viewport.viewport.y), + .z = -1, + .width = rp.dest_width, + .height = rp.dest_height, + .cell_offset_x = rp.offset_x, + .cell_offset_y = rp.offset_y, + .source_x = rp.source_x, + .source_y = rp.source_y, + .source_width = rp.source_width, + .source_height = rp.source_height, + }); + + return; + } + // Get the placement. If an ID is specified we look for the exact one. // If no ID, then we find the first virtual placement for this image. const placement = if (p.placement_id > 0) storage.placements.get(.{ diff --git a/src/terminal/kitty/graphics.zig b/src/terminal/kitty/graphics.zig index 22d102f53..0beb4901e 100644 --- a/src/terminal/kitty/graphics.zig +++ b/src/terminal/kitty/graphics.zig @@ -16,11 +16,13 @@ //! aim to ship a v1 of this implementation came at some cost. I learned a lot //! though and I think we can go back through and fix this up. +const render = @import("graphics_render.zig"); pub usingnamespace @import("graphics_command.zig"); pub usingnamespace @import("graphics_exec.zig"); pub usingnamespace @import("graphics_image.zig"); pub usingnamespace @import("graphics_storage.zig"); pub const unicode = @import("graphics_unicode.zig"); +pub const RenderPlacement = render.Placement; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index a908ac200..23371d180 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -5,6 +5,8 @@ const std = @import("std"); const assert = std.debug.assert; const testing = std.testing; const terminal = @import("../main.zig"); +const kitty_gfx = terminal.kitty.graphics; +const RenderPlacement = kitty_gfx.RenderPlacement; const log = std.log.scoped(.kitty_gfx); @@ -117,6 +119,231 @@ pub const Placement = struct { /// The width/height in cells of this placement. width: u32, height: u32, + + pub const Error = error{ + PlacementGridOutOfBounds, + PlacementMissingPlacement, + }; + + /// Take this virtual placement and convert it to a render placement. + pub fn renderPlacement( + self: *const Placement, + storage: *const kitty_gfx.ImageStorage, + img: *const kitty_gfx.Image, + cell_width: u32, + cell_height: u32, + ) Error!RenderPlacement { + // In this function, there is a variable naming convention to try + // to make it slightly less confusing. The prefix will tell you what + // coordinate/size space a variable lives in: + // - img_* is for the original image + // - p_* is for the final placement + // - vp_* is for the virtual placement + + // Determine the grid size that this virtual placement fits into. + const p_grid = try self.grid(storage, img, cell_width, cell_height); + + // From here on out we switch to floating point math. These are + // constants that we'll reference repeatedly. + const img_width_f64: f64 = @floatFromInt(img.width); + const img_height_f64: f64 = @floatFromInt(img.height); + + // Next we have to fit the source image into the grid size while preserving + // aspect ratio. We will center the image horizontally/vertically if + // necessary. + const p_scale: struct { + /// The offsets are pixels from the top-left of the placement-sized + /// image in order to center the image as necessary. + x_offset: f64 = 0, + y_offset: f64 = 0, + + /// The multipliers to apply to the width/height of the original + /// image size in order to reach the placement size. + x_scale: f64 = 0, + y_scale: f64 = 0, + } = scale: { + const p_rows_px: f64 = @floatFromInt(p_grid.rows * cell_height); + const p_cols_px: f64 = @floatFromInt(p_grid.columns * cell_width); + if (img_width_f64 * p_rows_px > img_height_f64 * p_cols_px) { + // Image is wider than the grid, fit width and center height + const x_scale = p_cols_px / @max(img_width_f64, 1); + const y_scale = x_scale; + const y_offset = (p_rows_px - img_height_f64 * y_scale) / 2; + break :scale .{ + .x_scale = x_scale, + .y_scale = y_scale, + .y_offset = y_offset, + }; + } else { + // Image is taller than the grid, fit height and center width + const y_scale = p_rows_px / @max(img_height_f64, 1); + const x_scale = y_scale; + const x_offset = (p_cols_px - img_width_f64 * x_scale) / 2; + break :scale .{ + .x_scale = x_scale, + .y_scale = y_scale, + .x_offset = x_offset, + }; + } + }; + + // Scale our original image according to the aspect ratio + // and padding calculated for p_scale. + const img_scaled: struct { + x_offset: f64, + y_offset: f64, + width: f64, + height: f64, + } = scale: { + const x_offset: f64 = p_scale.x_offset / p_scale.x_scale; + const y_offset: f64 = p_scale.y_offset / p_scale.y_scale; + const width: f64 = img_width_f64 + (x_offset * 2); + const height: f64 = img_height_f64 + (y_offset * 2); + break :scale .{ + .x_offset = x_offset, + .y_offset = y_offset, + .width = width, + .height = height, + }; + }; + + // Calculate the source rectangle for the scaled image. These + // coordinates are in the scaled image space. + var img_scale_source: struct { + x: f64, + y: f64, + width: f64, + height: f64, + } = source: { + // Float-converted values we already have + const vp_width: f64 = @floatFromInt(self.width); + const vp_height: f64 = @floatFromInt(self.height); + const vp_col: f64 = @floatFromInt(self.col); + const vp_row: f64 = @floatFromInt(self.row); + const p_grid_cols: f64 = @floatFromInt(p_grid.columns); + const p_grid_rows: f64 = @floatFromInt(p_grid.rows); + + // Calculate the scaled source rectangle for the image, undoing + // the aspect ratio scaling as necessary. + const width: f64 = img_scaled.width * (vp_width / p_grid_cols); + const height: f64 = img_scaled.height * (vp_height / p_grid_rows); + const x: f64 = img_scaled.width * (vp_col / p_grid_cols); + const y: f64 = img_scaled.height * (vp_row / p_grid_rows); + + break :source .{ + .width = width, + .height = height, + .x = x, + .y = y, + }; + }; + + // The destination rectangle. The x/y is specified by offsets from + // the top-left since that's how our RenderPlacement works. + const p_dest: struct { + x_offset: f64, + y_offset: f64, + width: f64, + height: f64, + } = dest: { + const x_offset: f64 = 0; + var y_offset: f64 = 0; + const width: f64 = @floatFromInt(self.width * cell_width); + var height: f64 = @floatFromInt(self.height * cell_height); + + if (img_scale_source.y < img_scaled.y_offset) { + // If our source rect y is within the offset area, we need to + // adjust our source rect and destination since the source texture + // doesnt actually have the offset area blank. + const offset: f64 = img_scaled.y_offset - img_scale_source.y; + img_scale_source.height -= offset; + y_offset = offset; + height -= offset * p_scale.y_scale; + img_scale_source.y = 0; + } + + if (img_scale_source.y + img_scale_source.height > + img_scaled.height - img_scaled.y_offset) + { + // if our y is in our bottom offset area, we need to shorten the + // source to fit in the cell. + img_scale_source.y -= img_scaled.y_offset; + img_scale_source.height = img_scaled.height - img_scaled.y_offset - img_scale_source.y; + img_scale_source.height -= img_scaled.y_offset; + height = img_scale_source.height * p_scale.y_scale; + } + + break :dest .{ + .x_offset = x_offset, + .y_offset = y_offset, + .width = width, + .height = height, + }; + }; + + return .{ + .top_left = self.pin, + .offset_x = @intFromFloat(@round(p_dest.x_offset)), + .offset_y = @intFromFloat(@round(p_dest.y_offset)), + .source_x = @intFromFloat(@round(img_scale_source.x)), + .source_y = @intFromFloat(@round(img_scale_source.y)), + .source_width = @intFromFloat(@round(img_scale_source.width)), + .source_height = @intFromFloat(@round(img_scale_source.height)), + .dest_width = @intFromFloat(@round(p_dest.width)), + .dest_height = @intFromFloat(@round(p_dest.height)), + }; + } + + // Calculate the grid size for the placement. For virtual placements, + // we use the requested row/cols. If either isn't specified, we choose + // the best size based on the image size to fit the entire image in its + // original size. + // + // This part of the code does NOT do preserve any aspect ratios. Its + // dumbly fitting the image into the grid size -- possibly user specified. + fn grid( + self: *const Placement, + storage: *const kitty_gfx.ImageStorage, + image: *const kitty_gfx.Image, + cell_width: u32, + cell_height: u32, + ) !struct { + rows: u32, + columns: u32, + } { + // Get the placement. If an ID is specified we look for the exact one. + // If no ID, then we find the first virtual placement for this image. + const placement = if (self.placement_id > 0) storage.placements.get(.{ + .image_id = self.image_id, + .placement_id = .{ .tag = .external, .id = self.placement_id }, + }) orelse { + return Error.PlacementMissingPlacement; + } else placement: { + var it = storage.placements.iterator(); + while (it.next()) |entry| { + if (entry.key_ptr.image_id == self.image_id and + entry.value_ptr.location == .virtual) + { + break :placement entry.value_ptr.*; + } + } + + return Error.PlacementMissingPlacement; + }; + + // Use requested rows/columns if specified + // For unspecified rows/columns, calculate based on the image size. + var rows = placement.rows; + var columns = placement.columns; + if (rows == 0) rows = (image.height + cell_height - 1) / cell_height; + if (columns == 0) columns = (image.width + cell_width - 1) / cell_width; + return .{ + .rows = std.math.cast(terminal.size.CellCountInt, rows) orelse + return Error.PlacementGridOutOfBounds, + .columns = std.math.cast(terminal.size.CellCountInt, columns) orelse + return Error.PlacementGridOutOfBounds, + }; + } }; /// IncompletePlacement is the placement information present in a single