mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00

This fixes window resize not working properly when `window-padding-balance` is set to true.
393 lines
14 KiB
Zig
393 lines
14 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const font = @import("../font/main.zig");
|
|
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);
|
|
}
|
|
|
|
/// 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) void {
|
|
// This ensure grid() does the right thing
|
|
self.padding = explicit;
|
|
|
|
// Now we can calculate the balanced padding
|
|
self.padding = Padding.balanced(
|
|
self.screen,
|
|
self.grid(),
|
|
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.
|
|
/// 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 = 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 = 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 = 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 = 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 left/right padding is just an equal split.
|
|
const padding_right = @floor(space_right / 2);
|
|
const padding_left = padding_right;
|
|
|
|
// The top/bottom padding is interesting. Subjectively, lots of padding
|
|
// at the top looks bad. So instead of always being equal (like left/right),
|
|
// we force the top padding to be at most equal to the left, and the bottom
|
|
// padding is the difference thereafter.
|
|
const padding_top = @min(padding_left, @floor(space_bot / 2));
|
|
const padding_bot = space_bot - padding_top;
|
|
|
|
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 "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);
|
|
}
|
|
}
|