mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
renderer: dedicated size struct, defined coordinate spaces
This commit is contained in:
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user