Ports the terminal-independent half of ZIG/src/renderer (OpenGL only): - opengl/*: buffers, frames, pipelines, render passes, samplers, targets, textures and the OpenGL GraphicsAPI backend on top of glow; the GTK GLArea glue is replaced by a get_proc_address loader closure - the GLSL shaders, copied verbatim and embedded; uniform/vertex structs as bytemuck Pod types with std140 offsets asserted in tests - shadertoy custom shaders converted with naga (GLSL in -> validate -> GLSL 430 out) instead of glslang + spirv-cross - size (screen/grid/cell/padding math), cursor style logic, backend health, options and message types The crate opts into unsafe_code = "deny"; unsafe glow calls live only in opengl/gl.rs. A headless EGL integration test builds every pipeline and the converted shadertoy shaders on Mesa and reads back rendered pixels. Note: Zig's shadertoy Uniforms order the selection background/foreground colors opposite to the GLSL block (likely an upstream bug); kept as in Zig for parity and recorded in the notes. Deferred to the next renderer pass: generic, cell, image, link, row, State, Thread, Overlay (need terminal render state). Test Plan: - cargo test -p ghostty-renderer (33 tests incl. tests/egl_headless.rs)
499 lines
16 KiB
Rust
499 lines
16 KiB
Rust
//! Screen/grid/cell/padding size math (`renderer/size.zig`).
|
|
|
|
/// Terminal cell count integer (`terminal.size.CellCountInt`).
|
|
// TODO(integration): re-export from ghostty-terminal once it is a dependency.
|
|
pub type CellCountInt = u16;
|
|
|
|
/// 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.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct Size {
|
|
pub screen: ScreenSize,
|
|
pub cell: CellSize,
|
|
pub padding: Padding,
|
|
}
|
|
|
|
impl Size {
|
|
/// 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) -> GridSize {
|
|
GridSize::init(self.screen.sub_padding(self.padding), self.cell)
|
|
}
|
|
|
|
/// The size of the terminal. This is the same as the screen without
|
|
/// padding.
|
|
pub fn terminal(&self) -> ScreenSize {
|
|
self.screen.sub_padding(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 balance_padding(&mut self, explicit: Padding) {
|
|
// 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);
|
|
|
|
// 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 maximum left padding,
|
|
// which is the balanced explicit horizontal padding plus half a cell width.
|
|
let max_padding_left = (explicit.left + explicit.right + self.cell.width) / 2;
|
|
let vshift = self.padding.top.saturating_sub(max_padding_left);
|
|
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.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum Coordinate {
|
|
Surface(SurfaceCoord),
|
|
Terminal(TerminalCoord),
|
|
Grid(GridCoord),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum CoordinateTag {
|
|
Surface,
|
|
Terminal,
|
|
Grid,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct SurfaceCoord {
|
|
pub x: f64,
|
|
pub y: f64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct TerminalCoord {
|
|
pub x: f64,
|
|
pub y: f64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct GridCoord {
|
|
pub x: CellCountInt,
|
|
pub y: CellCountInt,
|
|
}
|
|
|
|
impl Coordinate {
|
|
pub fn tag(&self) -> CoordinateTag {
|
|
match self {
|
|
Coordinate::Surface(_) => CoordinateTag::Surface,
|
|
Coordinate::Terminal(_) => CoordinateTag::Terminal,
|
|
Coordinate::Grid(_) => CoordinateTag::Grid,
|
|
}
|
|
}
|
|
|
|
/// Convert a coordinate to a different space within the same Size.
|
|
pub fn convert(self, to: CoordinateTag, size: Size) -> Coordinate {
|
|
// Unlikely fast-path but avoid work.
|
|
if self.tag() == to {
|
|
return self;
|
|
}
|
|
|
|
// To avoid the combinatorial explosion of conversion functions, we
|
|
// convert to the surface system first and then reconvert from there.
|
|
let surface = self.convert_to_surface(size);
|
|
|
|
match to {
|
|
CoordinateTag::Surface => Coordinate::Surface(surface),
|
|
CoordinateTag::Terminal => Coordinate::Terminal(TerminalCoord {
|
|
x: surface.x - f64::from(size.padding.left),
|
|
y: surface.y - f64::from(size.padding.top),
|
|
}),
|
|
CoordinateTag::Grid => {
|
|
// Get rid of the padding.
|
|
let Coordinate::Terminal(term) =
|
|
Coordinate::Surface(surface).convert(CoordinateTag::Terminal, size)
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
// We need our grid to clamp
|
|
let grid = size.grid();
|
|
|
|
// Calculate the grid position.
|
|
let cell_width = f64::from(size.cell.width);
|
|
let cell_height = f64::from(size.cell.height);
|
|
let clamped_x = term.x.max(0.0);
|
|
let clamped_y = term.y.max(0.0);
|
|
let col = (clamped_x / cell_width) as CellCountInt;
|
|
let row = (clamped_y / cell_height) as CellCountInt;
|
|
let clamped_col = col.min(grid.columns - 1);
|
|
let clamped_row = row.min(grid.rows - 1);
|
|
Coordinate::Grid(GridCoord {
|
|
x: clamped_col,
|
|
y: clamped_row,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convert a coordinate to the surface coordinate system.
|
|
fn convert_to_surface(self, size: Size) -> SurfaceCoord {
|
|
match self {
|
|
Coordinate::Surface(v) => v,
|
|
Coordinate::Terminal(v) => SurfaceCoord {
|
|
x: v.x + f64::from(size.padding.left),
|
|
y: v.y + f64::from(size.padding.top),
|
|
},
|
|
Coordinate::Grid(v) => {
|
|
let col = f64::from(v.x);
|
|
let row = f64::from(v.y);
|
|
let cell_width = f64::from(size.cell.width);
|
|
let cell_height = f64::from(size.cell.height);
|
|
let padding_left = f64::from(size.padding.left);
|
|
let padding_top = f64::from(size.padding.top);
|
|
SurfaceCoord {
|
|
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.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct CellSize {
|
|
pub width: u32,
|
|
pub 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.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct ScreenSize {
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
impl ScreenSize {
|
|
/// Subtract padding from the screen size.
|
|
pub fn sub_padding(self, padding: Padding) -> ScreenSize {
|
|
ScreenSize {
|
|
width: self.width.saturating_sub(padding.left + padding.right),
|
|
height: self.height.saturating_sub(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 blank_padding(self, padding: Padding, grid: GridSize, cell: CellSize) -> Padding {
|
|
let grid_width = u32::from(grid.columns) * cell.width;
|
|
let grid_height = u32::from(grid.rows) * cell.height;
|
|
let padded_width = grid_width + (padding.left + padding.right);
|
|
let 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.
|
|
let leftover_width = self.width.saturating_sub(padded_width);
|
|
let leftover_height = self.height.saturating_sub(padded_height);
|
|
|
|
Padding {
|
|
top: 0,
|
|
bottom: leftover_height,
|
|
right: leftover_width,
|
|
left: 0,
|
|
}
|
|
}
|
|
|
|
/// Returns true if two sizes are equal.
|
|
pub fn equals(self, other: ScreenSize) -> bool {
|
|
self == other
|
|
}
|
|
}
|
|
|
|
/// The dimensions of the grid itself, in rows/columns units.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct GridSize {
|
|
pub columns: CellCountInt,
|
|
pub rows: CellCountInt,
|
|
}
|
|
|
|
impl GridSize {
|
|
/// Initialize a grid size based on a screen and cell size.
|
|
pub fn init(screen: ScreenSize, cell: CellSize) -> GridSize {
|
|
let mut result = GridSize::default();
|
|
result.update(screen, cell);
|
|
result
|
|
}
|
|
|
|
/// Update the columns/rows for the grid based on the given screen and
|
|
/// cell size.
|
|
pub fn update(&mut self, screen: ScreenSize, cell: CellSize) {
|
|
let cell_width = cell.width as f32;
|
|
let cell_height = cell.height as f32;
|
|
let screen_width = screen.width as f32;
|
|
let screen_height = screen.height as f32;
|
|
let calc_cols = (screen_width / cell_width) as CellCountInt;
|
|
let calc_rows = (screen_height / cell_height) as CellCountInt;
|
|
self.columns = calc_cols.max(1);
|
|
self.rows = calc_rows.max(1);
|
|
}
|
|
|
|
/// Returns true if two sizes are equal.
|
|
pub fn equals(self, other: GridSize) -> bool {
|
|
self == other
|
|
}
|
|
}
|
|
|
|
/// The padding to add to a screen.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct Padding {
|
|
pub top: u32,
|
|
pub bottom: u32,
|
|
pub right: u32,
|
|
pub left: u32,
|
|
}
|
|
|
|
impl Padding {
|
|
/// 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
|
|
let cell_width = cell.width as f32;
|
|
let cell_height = cell.height as f32;
|
|
|
|
// The size of our full grid
|
|
let grid_width = f32::from(grid.columns) * cell_width;
|
|
let grid_height = f32::from(grid.rows) * cell_height;
|
|
|
|
// The empty space to the right of a line and bottom of the last row
|
|
let space_right = screen.width as f32 - grid_width;
|
|
let space_bot = screen.height as f32 - grid_height;
|
|
|
|
// The padding is split equally along both axes.
|
|
let padding_right = (space_right / 2.0).floor();
|
|
let padding_left = padding_right;
|
|
|
|
let padding_bot = (space_bot / 2.0).floor();
|
|
let padding_top = padding_bot;
|
|
|
|
let zero = 0.0f32;
|
|
Padding {
|
|
top: padding_top.max(zero) as u32,
|
|
bottom: padding_bot.max(zero) as u32,
|
|
right: padding_right.max(zero) as u32,
|
|
left: padding_left.max(zero) as u32,
|
|
}
|
|
}
|
|
|
|
/// Equality test between two paddings.
|
|
pub fn eql(self, other: Padding) -> bool {
|
|
self == other
|
|
}
|
|
}
|
|
|
|
/// Add another padding to this one (`Padding.add`).
|
|
impl std::ops::Add for Padding {
|
|
type Output = Padding;
|
|
|
|
fn add(self, other: Padding) -> Padding {
|
|
Padding {
|
|
top: self.top + other.top,
|
|
bottom: self.bottom + other.bottom,
|
|
right: self.right + other.right,
|
|
left: self.left + other.left,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn 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.
|
|
let grid = GridSize {
|
|
columns: 100,
|
|
rows: 37,
|
|
};
|
|
let cell = CellSize {
|
|
width: 10,
|
|
height: 20,
|
|
};
|
|
let screen = ScreenSize {
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
let padding = Padding::balanced(screen, grid, cell);
|
|
assert_eq!(Padding::default(), padding);
|
|
}
|
|
|
|
#[test]
|
|
fn grid_size_update_exact() {
|
|
let mut grid = GridSize::default();
|
|
grid.update(
|
|
ScreenSize {
|
|
width: 100,
|
|
height: 40,
|
|
},
|
|
CellSize {
|
|
width: 5,
|
|
height: 10,
|
|
},
|
|
);
|
|
assert_eq!(20, grid.columns);
|
|
assert_eq!(4, grid.rows);
|
|
}
|
|
|
|
#[test]
|
|
fn grid_size_update_rounding() {
|
|
let mut grid = GridSize::default();
|
|
grid.update(
|
|
ScreenSize {
|
|
width: 20,
|
|
height: 40,
|
|
},
|
|
CellSize {
|
|
width: 6,
|
|
height: 15,
|
|
},
|
|
);
|
|
assert_eq!(3, grid.columns);
|
|
assert_eq!(2, grid.rows);
|
|
}
|
|
|
|
#[test]
|
|
fn coordinate_conversion() {
|
|
// A size for testing purposes. Purposely easy to calculate numbers.
|
|
let test_size = Size {
|
|
screen: ScreenSize {
|
|
width: 100,
|
|
height: 100,
|
|
},
|
|
cell: CellSize {
|
|
width: 5,
|
|
height: 10,
|
|
},
|
|
padding: Padding::default(),
|
|
};
|
|
|
|
let g = |x, y| Coordinate::Grid(GridCoord { x, y });
|
|
let s = |x, y| Coordinate::Surface(SurfaceCoord { x, y });
|
|
let table = [
|
|
(g(0, 0), s(0.0, 0.0)),
|
|
(g(1, 0), s(6.0, 0.0)),
|
|
(g(1, 1), s(6.0, 10.0)),
|
|
(g(0, 0), s(-10.0, -10.0)),
|
|
(
|
|
g(test_size.grid().columns - 1, test_size.grid().rows - 1),
|
|
s(100_000.0, 100_000.0),
|
|
),
|
|
];
|
|
|
|
for (expected, input) in table {
|
|
let actual = input.convert(expected.tag(), test_size);
|
|
assert_eq!(expected, actual);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn balance_padding_keeps_top_at_most_max_left() {
|
|
let mut size = Size {
|
|
screen: ScreenSize {
|
|
width: 105,
|
|
height: 200,
|
|
},
|
|
cell: CellSize {
|
|
width: 10,
|
|
height: 20,
|
|
},
|
|
padding: Padding::default(),
|
|
};
|
|
size.balance_padding(Padding::default());
|
|
// grid is 10x10 -> 100x200; leftover width 5 => left 2, right 2.
|
|
assert_eq!(size.padding.left, 2);
|
|
assert_eq!(size.padding.top, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn padding_add() {
|
|
let a = Padding {
|
|
top: 1,
|
|
bottom: 2,
|
|
right: 3,
|
|
left: 4,
|
|
};
|
|
let sum = a + a;
|
|
assert_eq!((sum.top, sum.bottom, sum.right, sum.left), (2, 4, 6, 8));
|
|
assert!(sum.eql(a + a));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_and_blank_padding() {
|
|
let size = Size {
|
|
screen: ScreenSize {
|
|
width: 110,
|
|
height: 70,
|
|
},
|
|
cell: CellSize {
|
|
width: 10,
|
|
height: 20,
|
|
},
|
|
padding: Padding {
|
|
top: 5,
|
|
bottom: 5,
|
|
left: 5,
|
|
right: 5,
|
|
},
|
|
};
|
|
let term = size.terminal();
|
|
assert_eq!((term.width, term.height), (100, 60));
|
|
let grid = size.grid();
|
|
assert_eq!((grid.columns, grid.rows), (10, 3));
|
|
let blank = term.blank_padding(size.padding, grid, size.cell);
|
|
// 100 - (100 + 10) saturates to 0; 60 - (60 + 10) saturates to 0.
|
|
assert_eq!(blank, Padding::default());
|
|
}
|
|
}
|