From c1c9aac0fe4c85808049261a21bcea98b804dd71 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 11 Nov 2024 19:17:09 -0800 Subject: [PATCH] renderer: dedicated size struct, defined coordinate spaces --- src/renderer.zig | 2 + src/renderer/size.zig | 170 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/src/renderer.zig b/src/renderer.zig index 5cf316c70..d968ab4df 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -24,6 +24,8 @@ pub const Thread = @import("renderer/Thread.zig"); pub const State = @import("renderer/State.zig"); pub const CursorStyle = cursor.Style; pub const Message = message.Message; +pub const Size = size.Size; +pub const Coordinate = size.Coordinate; pub const CellSize = size.CellSize; pub const ScreenSize = size.ScreenSize; pub const GridSize = size.GridSize; diff --git a/src/renderer/size.zig b/src/renderer/size.zig index add3134ba..e7740d69f 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -5,6 +5,123 @@ const terminal = @import("../terminal/main.zig"); const log = std.log.scoped(.renderer_size); +/// All relevant sizes for a rendered terminal. These are all the sizes that +/// any functionality should need to know about the terminal in order to +/// convert between any coordinate systems. +/// +/// See the individual field type documentation for more information on each +/// field. One important note is that any pixel values should already be scaled +/// to the current DPI of the screen. If the DPI changes, the sizes should be +/// recalculated and we expect this to be done by the caller. +pub const Size = struct { + screen: ScreenSize, + cell: CellSize, + padding: Padding, + + /// Return the grid size for this size. The grid size is calculated by + /// taking the screen size, removing padding, and dividing by the cell + /// dimensions. + pub fn grid(self: Size) GridSize { + return GridSize.init(self.screen.subPadding(self.padding), self.cell); + } +}; + +/// A coordinate. This is defined as a tagged union to allow for different +/// coordinate systems to be represented. +/// +/// A coordinate is only valid within the context of a stable Size value. +/// If any of the sizes in the Size struct change, the coordinate is no +/// longer valid and must be recalculated. A conversion function is provided +/// to migrate to a new Size (which may result in failure). +/// +/// The coordinate systems are: +/// +/// * surface: (0, 0) is the top-left of the surface (with padding). Negative +/// values are allowed and are off the surface. Likewise, values greater +/// than the surface size are off the surface. Units are pixels. +/// +/// * terminal: (0, 0) is the top-left of the terminal grid. This is the +/// same as the surface but with the padding removed. Negative values and +/// values greater than the grid size are allowed and are off the terminal. +/// Units are pixels. +/// +/// * grid: (0, 0) is the top-left of the grid. Units are in cells. Negative +/// values are not allowed but values greater than the grid size are +/// possible and are off the grid. +/// +pub const Coordinate = union(enum) { + surface: Surface, + terminal: Terminal, + grid: Grid, + + pub const Tag = @typeInfo(Coordinate).Union.tag_type.?; + pub const Surface = struct { x: f64, y: f64 }; + pub const Terminal = struct { x: f64, y: f64 }; + pub const Grid = struct { x: GridSize.Unit, y: GridSize.Unit }; + + /// Convert a coordinate to a different space within the same Size. + pub fn convert(self: Coordinate, to: Tag, size: Size) Coordinate { + // Unlikely fast-path but avoid work. + if (@as(Tag, self) == to) return self; + + // To avoid the combinatorial explosion of conversion functions, we + // convert to the surface system first and then reconvert from there. + const surface = self.convertToSurface(size); + + return switch (to) { + .surface => .{ .surface = surface }, + .terminal => .{ .terminal = .{ + .x = surface.x - @as(f64, @floatFromInt(size.padding.left)), + .y = surface.y - @as(f64, @floatFromInt(size.padding.top)), + } }, + .grid => grid: { + // Get rid of the padding. + const term = (Coordinate{ .surface = surface }).convert( + .terminal, + size, + ).terminal; + + // We need our grid to clamp + const grid = size.grid(); + + // Calculate the grid position. + const cell_width: f64 = @as(f64, @floatFromInt(size.cell.width)); + const cell_height: f64 = @as(f64, @floatFromInt(size.cell.height)); + const clamped_x: f64 = @max(0, term.x); + const clamped_y: f64 = @max(0, term.y); + const col: GridSize.Unit = @intFromFloat(clamped_x / cell_width); + const row: GridSize.Unit = @intFromFloat(clamped_y / cell_height); + const clamped_col: GridSize.Unit = @min(col, grid.columns - 1); + const clamped_row: GridSize.Unit = @min(row, grid.rows - 1); + break :grid .{ .grid = .{ .x = clamped_col, .y = clamped_row } }; + }, + }; + } + + /// Convert a coordinate to the surface coordinate system. + fn convertToSurface(self: Coordinate, size: Size) Surface { + return switch (self) { + .surface => |v| v, + .terminal => |v| .{ + .x = v.x + @as(f64, @floatFromInt(size.padding.left)), + .y = v.y + @as(f64, @floatFromInt(size.padding.top)), + }, + .grid => |v| grid: { + const col: f64 = @floatFromInt(v.x); + const row: f64 = @floatFromInt(v.y); + const cell_width: f64 = @floatFromInt(size.cell.width); + const cell_height: f64 = @floatFromInt(size.cell.height); + const padding_left: f64 = @floatFromInt(size.padding.left); + const padding_top: f64 = @floatFromInt(size.padding.top); + break :grid .{ + .x = col * cell_width + padding_left, + .y = row * cell_height + padding_top, + }; + }, + }; + } +}; + /// The dimensions of a single "cell" in the terminal grid. /// /// The dimensions are dependent on the current loaded set of font glyphs. @@ -67,7 +184,7 @@ pub const ScreenSize = struct { /// The dimensions of the grid itself, in rows/columns units. pub const GridSize = struct { - const Unit = terminal.size.CellCountInt; + pub const Unit = terminal.size.CellCountInt; columns: Unit = 0, rows: Unit = 0, @@ -201,3 +318,54 @@ test "GridSize update rounding" { try testing.expectEqual(@as(GridSize.Unit, 3), grid.columns); try testing.expectEqual(@as(GridSize.Unit, 2), grid.rows); } + +test "coordinate conversion" { + const testing = std.testing; + + // A size for testing purposes. Purposely easy to calculate numbers. + const test_size: Size = .{ + .screen = .{ + .width = 100, + .height = 100, + }, + + .cell = .{ + .width = 5, + .height = 10, + }, + + .padding = .{}, + }; + + // Each pair is a test case of [expected, actual]. We only test + // one-way conversion because conversion can be lossy due to clamping + // and so on. + const table: []const [2]Coordinate = &.{ + .{ + .{ .grid = .{ .x = 0, .y = 0 } }, + .{ .surface = .{ .x = 0, .y = 0 } }, + }, + .{ + .{ .grid = .{ .x = 1, .y = 0 } }, + .{ .surface = .{ .x = 6, .y = 0 } }, + }, + .{ + .{ .grid = .{ .x = 1, .y = 1 } }, + .{ .surface = .{ .x = 6, .y = 10 } }, + }, + .{ + .{ .grid = .{ .x = 0, .y = 0 } }, + .{ .surface = .{ .x = -10, .y = -10 } }, + }, + .{ + .{ .grid = .{ .x = test_size.grid().columns - 1, .y = test_size.grid().rows - 1 } }, + .{ .surface = .{ .x = 100_000, .y = 100_000 } }, + }, + }; + + for (table) |pair| { + const expected = pair[0]; + const actual = pair[1].convert(@as(Coordinate.Tag, expected), test_size); + try testing.expectEqual(expected, actual); + } +}