renderer: dedicated size struct, defined coordinate spaces

This commit is contained in:
Mitchell Hashimoto
2024-11-11 19:17:09 -08:00
parent 4853597cd9
commit c1c9aac0fe
2 changed files with 171 additions and 1 deletions

View File

@ -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;

View File

@ -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);
}
}