aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/size.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/size.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/size.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/size.zig458
1 files changed, 458 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/size.zig b/vendor/libghostty-vt/src/renderer/size.zig
new file mode 100644
index 00000000..6cf05f58
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/size.zig
@@ -0,0 +1,458 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const terminal_size = @import("../terminal/size.zig");
+
+const log = std.log.scoped(.renderer_size);
+
+/// Controls how extra whitespace around the terminal grid is distributed.
+pub const PaddingBalance = enum {
+ /// No balancing; padding is applied as specified explicitly.
+ false,
+ /// Balances padding but caps the top padding so the first row doesn't
+ /// drift too far from the top of the window. Excess vertical space is
+ /// shifted to the bottom.
+ true,
+ /// Distributes leftover space equally on all sides so the grid is
+ /// centered within the screen.
+ equal,
+};
+
+/// 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 .init(self.screen.subPadding(self.padding), self.cell);
+ }
+
+ /// The size of the terminal. This is the same as the screen without
+ /// padding.
+ pub fn terminal(self: Size) ScreenSize {
+ return self.screen.subPadding(self.padding);
+ }
+
+ /// Set the padding to be balanced around the grid. The balanced
+ /// padding is calculated AFTER the explicit padding is taken
+ /// into account.
+ pub fn balancePadding(
+ self: *Size,
+ explicit: Padding,
+ mode: PaddingBalance,
+ ) void {
+ // This ensure grid() does the right thing
+ self.padding = explicit;
+
+ // Now we can calculate the balanced padding
+ self.padding = .balanced(
+ self.screen,
+ self.grid(),
+ self.cell,
+ );
+
+ switch (mode) {
+ .false => unreachable,
+ .equal => {},
+ .true => {
+ // Cap the top padding to avoid excessive space above the
+ // first row. The maximum is the balanced explicit horizontal
+ // padding plus half a cell width. Any excess is shifted to
+ // the bottom.
+ const max_top = (explicit.left + explicit.right + self.cell.width) / 2;
+ const vshift = self.padding.top -| max_top;
+ self.padding.top -= vshift;
+ self.padding.bottom += vshift;
+ },
+ }
+ }
+};
+
+/// 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.
+/// We calculate the width based on the widest character and the height based
+/// on the height requirement for an underscore (the "lowest" -- visually --
+/// character).
+///
+/// The units for the width and height are in world space. They have to
+/// be normalized for any renderer implementation.
+pub const CellSize = extern struct {
+ width: u32,
+ height: u32,
+};
+
+/// The dimensions of the screen that the grid is rendered to. This is the
+/// terminal screen, so it is likely a subset of the window size. The dimensions
+/// should be in pixels.
+pub const ScreenSize = extern struct {
+ width: u32,
+ height: u32,
+
+ /// Subtract padding from the screen size.
+ pub fn subPadding(self: ScreenSize, padding: Padding) ScreenSize {
+ return .{
+ .width = self.width -| (padding.left + padding.right),
+ .height = self.height -| (padding.top + padding.bottom),
+ };
+ }
+
+ /// Calculates the amount of blank space around the grid. This is possible
+ /// when padding isn't balanced.
+ ///
+ /// The "self" screen size here should be the unpadded screen.
+ pub fn blankPadding(self: ScreenSize, padding: Padding, grid: GridSize, cell: CellSize) Padding {
+ const grid_width = grid.columns * cell.width;
+ const grid_height = grid.rows * cell.height;
+ const padded_width = grid_width + (padding.left + padding.right);
+ const padded_height = grid_height + (padding.top + padding.bottom);
+
+ // Note these have to use a saturating subtraction to avoid underflow
+ // because our padding can cause the padded sizes to be larger than
+ // our real screen if the screen is shrunk to a minimal size such
+ // as 1x1.
+ const leftover_width = self.width -| padded_width;
+ const leftover_height = self.height -| padded_height;
+
+ return .{
+ .top = 0,
+ .bottom = leftover_height,
+ .right = leftover_width,
+ .left = 0,
+ };
+ }
+
+ /// Returns true if two sizes are equal.
+ pub fn equals(self: ScreenSize, other: ScreenSize) bool {
+ return self.width == other.width and self.height == other.height;
+ }
+};
+
+/// The dimensions of the grid itself, in rows/columns units.
+pub const GridSize = extern struct {
+ pub const Unit = terminal_size.CellCountInt;
+
+ columns: Unit = 0,
+ rows: Unit = 0,
+
+ /// Initialize a grid size based on a screen and cell size.
+ pub fn init(screen: ScreenSize, cell: CellSize) GridSize {
+ var result: GridSize = undefined;
+ result.update(screen, cell);
+ return result;
+ }
+
+ /// Update the columns/rows for the grid based on the given screen and
+ /// cell size.
+ pub fn update(self: *GridSize, screen: ScreenSize, cell: CellSize) void {
+ const cell_width: f32 = @floatFromInt(cell.width);
+ const cell_height: f32 = @floatFromInt(cell.height);
+ const screen_width: f32 = @floatFromInt(screen.width);
+ const screen_height: f32 = @floatFromInt(screen.height);
+ const calc_cols: Unit = @intFromFloat(screen_width / cell_width);
+ const calc_rows: Unit = @intFromFloat(screen_height / cell_height);
+ self.columns = @max(1, calc_cols);
+ self.rows = @max(1, calc_rows);
+ }
+
+ /// Returns true if two sizes are equal.
+ pub fn equals(self: GridSize, other: GridSize) bool {
+ return self.columns == other.columns and self.rows == other.rows;
+ }
+};
+
+/// The padding to add to a screen.
+pub const Padding = extern struct {
+ top: u32 = 0,
+ bottom: u32 = 0,
+ right: u32 = 0,
+ left: u32 = 0,
+
+ /// Returns padding that balances the whitespace around the screen
+ /// for the given grid and cell sizes.
+ pub fn balanced(screen: ScreenSize, grid: GridSize, cell: CellSize) Padding {
+ // Turn our cell sizes into floats for the math
+ const cell_width: f32 = @floatFromInt(cell.width);
+ const cell_height: f32 = @floatFromInt(cell.height);
+
+ // The size of our full grid
+ const grid_width = @as(f32, @floatFromInt(grid.columns)) * cell_width;
+ const grid_height = @as(f32, @floatFromInt(grid.rows)) * cell_height;
+
+ // The empty space to the right of a line and bottom of the last row
+ const space_right = @as(f32, @floatFromInt(screen.width)) - grid_width;
+ const space_bot = @as(f32, @floatFromInt(screen.height)) - grid_height;
+
+ // The padding is split equally along both axes.
+ const padding_right = @floor(space_right / 2);
+ const padding_left = padding_right;
+
+ const padding_bot = @floor(space_bot / 2);
+ const padding_top = padding_bot;
+
+ const zero = @as(f32, 0);
+ return .{
+ .top = @intFromFloat(@max(zero, padding_top)),
+ .bottom = @intFromFloat(@max(zero, padding_bot)),
+ .right = @intFromFloat(@max(zero, padding_right)),
+ .left = @intFromFloat(@max(zero, padding_left)),
+ };
+ }
+
+ /// Add another padding to this one
+ pub fn add(self: Padding, other: Padding) Padding {
+ return .{
+ .top = self.top + other.top,
+ .bottom = self.bottom + other.bottom,
+ .right = self.right + other.right,
+ .left = self.left + other.left,
+ };
+ }
+
+ /// Equality test between two paddings.
+ pub fn eql(self: Padding, other: Padding) bool {
+ return self.top == other.top and
+ self.bottom == other.bottom and
+ self.right == other.right and
+ self.left == other.left;
+ }
+};
+
+test "Size.balancePadding equal distributes whitespace equally" {
+ const testing = std.testing;
+
+ // screen=1050x850, cell=10x20, explicit=4 each side
+ // grid: (1050-8)/10=104 cols, (850-8)/20=42 rows
+ // leftover: 1050-1040=10 horizontal, 850-840=10 vertical
+ // balanced: left=right=5, top=bottom=5
+ var size: Size = .{
+ .screen = .{ .width = 1050, .height = 850 },
+ .cell = .{ .width = 10, .height = 20 },
+ .padding = .{},
+ };
+ size.balancePadding(.{ .top = 4, .bottom = 4, .left = 4, .right = 4 }, .equal);
+ try testing.expectEqual(size.padding.left, size.padding.right);
+ try testing.expectEqual(size.padding.top, size.padding.bottom);
+ try testing.expect(size.padding.top > 0);
+}
+
+test "Size.balancePadding true shifts excess top to bottom" {
+ const testing = std.testing;
+
+ // screen=1090x1070, cell=20x40, explicit=0
+ // grid: 1090/20=54 cols, 1070/40=26 rows
+ // leftover: 1090-1080=10, 1070-1040=30
+ // balanced: left=right=5, top=bottom=15
+ // vshift cap: (0+0+20)/2=10, vshift=15-10=5
+ // result: top=10, bottom=20
+ var size: Size = .{
+ .screen = .{ .width = 1090, .height = 1070 },
+ .cell = .{ .width = 20, .height = 40 },
+ .padding = .{},
+ };
+ size.balancePadding(.{}, .true);
+ try testing.expectEqual(size.padding.left, size.padding.right);
+ try testing.expect(size.padding.top < size.padding.bottom);
+ try testing.expectEqual(@as(u32, 10), size.padding.top);
+ try testing.expectEqual(@as(u32, 20), size.padding.bottom);
+}
+
+test "Padding balanced on zero" {
+ // On some systems, our screen can be zero-sized for a bit, and we
+ // don't want to end up with negative padding.
+ const testing = std.testing;
+ const grid: GridSize = .{ .columns = 100, .rows = 37 };
+ const cell: CellSize = .{ .width = 10, .height = 20 };
+ const screen: ScreenSize = .{ .width = 0, .height = 0 };
+ const padding = Padding.balanced(screen, grid, cell);
+ try testing.expectEqual(Padding{}, padding);
+}
+
+test "GridSize update exact" {
+ const testing = std.testing;
+
+ var grid: GridSize = .{};
+ grid.update(.{
+ .width = 100,
+ .height = 40,
+ }, .{
+ .width = 5,
+ .height = 10,
+ });
+
+ try testing.expectEqual(@as(GridSize.Unit, 20), grid.columns);
+ try testing.expectEqual(@as(GridSize.Unit, 4), grid.rows);
+}
+
+test "GridSize update rounding" {
+ const testing = std.testing;
+
+ var grid: GridSize = .{};
+ grid.update(.{
+ .width = 20,
+ .height = 40,
+ }, .{
+ .width = 6,
+ .height = 15,
+ });
+
+ 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);
+ }
+}