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 State = @import("renderer/State.zig");
|
||||||
pub const CursorStyle = cursor.Style;
|
pub const CursorStyle = cursor.Style;
|
||||||
pub const Message = message.Message;
|
pub const Message = message.Message;
|
||||||
|
pub const Size = size.Size;
|
||||||
|
pub const Coordinate = size.Coordinate;
|
||||||
pub const CellSize = size.CellSize;
|
pub const CellSize = size.CellSize;
|
||||||
pub const ScreenSize = size.ScreenSize;
|
pub const ScreenSize = size.ScreenSize;
|
||||||
pub const GridSize = size.GridSize;
|
pub const GridSize = size.GridSize;
|
||||||
|
@ -5,6 +5,123 @@ const terminal = @import("../terminal/main.zig");
|
|||||||
|
|
||||||
const log = std.log.scoped(.renderer_size);
|
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 of a single "cell" in the terminal grid.
|
||||||
///
|
///
|
||||||
/// The dimensions are dependent on the current loaded set of font glyphs.
|
/// 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.
|
/// The dimensions of the grid itself, in rows/columns units.
|
||||||
pub const GridSize = struct {
|
pub const GridSize = struct {
|
||||||
const Unit = terminal.size.CellCountInt;
|
pub const Unit = terminal.size.CellCountInt;
|
||||||
|
|
||||||
columns: Unit = 0,
|
columns: Unit = 0,
|
||||||
rows: 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, 3), grid.columns);
|
||||||
try testing.expectEqual(@as(GridSize.Unit, 2), grid.rows);
|
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