Merge pull request #2037 from ghostty-org/extend

renderer: disable window-padding-color=extend in certain scenarios
This commit is contained in:
Mitchell Hashimoto
2024-08-03 22:01:36 -07:00
committed by GitHub
8 changed files with 189 additions and 11 deletions

View File

@ -104,6 +104,7 @@ pub fn setUniform(
// Perform the correct call depending on the type of the value.
switch (@TypeOf(value)) {
bool => glad.context.Uniform1i.?(loc, if (value) 1 else 0),
comptime_int => glad.context.Uniform1i.?(loc, value),
f32 => glad.context.Uniform1f.?(loc, value),
@Vector(2, f32) => glad.context.Uniform2f.?(loc, value[0], value[1]),

View File

@ -674,6 +674,17 @@ keybind: Keybinds = .{},
/// * `background` - The background color specified in `background`.
/// * `extend` - Extend the background color of the nearest grid cell.
///
/// The "extend" value will be disabled in certain scenarios. On primary
/// screen applications (i.e. not something like Neovim), the color will not
/// be extended vertically if any of the following are true:
///
/// * The nearest row has any cells that have the default backgroudn color.
/// The thinking is that in this case, the default background color looks
/// fine as a padding color.
/// * The nearest row is a prompt row (requires shell integration). The
/// thinking here is that prompts often contain powerline glyphs that
/// do not look good extended.
///
/// The default value is "extend". This allows for smooth resizing of a
/// terminal grid without having visible empty areas around the edge. The edge
/// cells may appear slightly larger due to the extension.

View File

@ -625,6 +625,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.cell_size = undefined,
.grid_size = undefined,
.grid_padding = undefined,
.padding_extend_top = true,
.padding_extend_bottom = true,
.min_contrast = options.config.min_contrast,
.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
.cursor_color = undefined,
@ -872,6 +874,7 @@ pub fn updateFrame(
const Critical = struct {
bg: terminal.color.RGB,
screen: terminal.Screen,
screen_type: terminal.ScreenType,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle,
@ -1001,6 +1004,7 @@ pub fn updateFrame(
break :critical .{
.bg = self.background_color,
.screen = screen_copy,
.screen_type = state.terminal.active_screen,
.mouse = state.mouse,
.preedit = preedit,
.cursor_style = cursor_style,
@ -1018,6 +1022,7 @@ pub fn updateFrame(
try self.rebuildCells(
critical.full_rebuild,
&critical.screen,
critical.screen_type,
critical.mouse,
critical.preedit,
critical.cursor_style,
@ -1988,6 +1993,8 @@ pub fn setScreenSize(
@floatFromInt(blank.bottom),
@floatFromInt(blank.left),
},
.padding_extend_top = old.padding_extend_top,
.padding_extend_bottom = old.padding_extend_bottom,
.min_contrast = old.min_contrast,
.cursor_pos = old.cursor_pos,
.cursor_color = old.cursor_color,
@ -2085,6 +2092,7 @@ fn rebuildCells(
self: *Metal,
rebuild: bool,
screen: *terminal.Screen,
screen_type: terminal.ScreenType,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
@ -2126,8 +2134,14 @@ fn rebuildCells(
};
} else null;
// If we are doing a full rebuild, then we clear the entire cell buffer.
if (rebuild) self.cells.reset();
if (rebuild) {
// If we are doing a full rebuild, then we clear the entire cell buffer.
self.cells.reset();
// We also reset our padding extension depending on the screen type
self.uniforms.padding_extend_top = screen_type == .alternate;
self.uniforms.padding_extend_bottom = screen_type == .alternate;
}
// Go row-by-row to build the cells. We go row by row because we do
// font shaping by row. In the future, we will also do dirty tracking
@ -2159,6 +2173,16 @@ fn rebuildCells(
break :sel sel.containedRow(screen, pin) orelse null;
};
// On primary screen, we still apply vertical padding extension
// under certain conditions we feel are safe. This helps make some
// scenarios look better while avoiding scenarios we know do NOT look
// good.
if (y == 0 and screen_type == .primary) {
self.uniforms.padding_extend_top = !row.neverExtendBg();
} else if (y == self.cells.size.rows - 1 and screen_type == .primary) {
self.uniforms.padding_extend_bottom = !row.neverExtendBg();
}
// Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(
self.font_grid,

View File

@ -53,12 +53,19 @@ grid_metrics: font.face.Metrics,
/// Current screen size dimensions for this grid. This is set on the first
/// resize event, and is not immediately available.
screen_size: ?renderer.ScreenSize,
grid_size: renderer.GridSize,
/// The current set of cells to render. Each set of cells goes into
/// a separate shader call.
cells_bg: std.ArrayListUnmanaged(CellProgram.Cell),
cells: std.ArrayListUnmanaged(CellProgram.Cell),
/// The last viewport that we based our rebuild off of. If this changes,
/// then we do a full rebuild of the cells. The pointer values in this pin
/// are NOT SAFE to read because they may be modified, freed, etc from the
/// termio thread. We treat the pointers as integers for comparison only.
cells_viewport: ?terminal.Pin = null,
/// The size of the cells list that was sent to the GPU. This is used
/// to detect when the cells array was reallocated/resized and handle that
/// accordingly.
@ -120,6 +127,10 @@ draw_mutex: DrawMutex = drawMutexZero,
/// terminal is in reversed mode.
draw_background: terminal.color.RGB,
/// Whether we're doing padding extension for vertical sides.
padding_extend_top: bool = true,
padding_extend_bottom: bool = true,
/// The images that we may render.
images: ImageMap = .{},
image_placements: ImagePlacementList = .{},
@ -394,6 +405,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.cells = .{},
.grid_metrics = grid.metrics,
.screen_size = null,
.grid_size = .{},
.gl_state = gl_state,
.font_grid = grid,
.font_shaper = shaper,
@ -669,6 +681,12 @@ pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void {
self.font_shaper_cache.deinit(self.alloc);
self.font_shaper_cache = font_shaper_cache;
// Update our grid size if we have a screen size. If we don't, its okay
// because this will get set when we get the screen size set.
if (self.screen_size) |size| {
self.grid_size = self.gridSize(size);
}
// Defer our GPU updates
self.deferred_font_size = .{ .metrics = grid.metrics };
}
@ -684,8 +702,10 @@ pub fn updateFrame(
// Data we extract out of the critical area.
const Critical = struct {
full_rebuild: bool,
gl_bg: terminal.color.RGB,
screen: terminal.Screen,
screen_type: terminal.ScreenType,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle,
@ -715,6 +735,9 @@ pub fn updateFrame(
self.foreground_color = bg;
}
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
// We used to share terminal state, but we've since learned through
// analysis that it is faster to copy the terminal state than to
// hold the lock wile rebuilding GPU cells.
@ -758,9 +781,55 @@ pub fn updateFrame(
try self.prepKittyGraphics(state.terminal);
}
// If we have any terminal dirty flags set then we need to rebuild
// the entire screen. This can be optimized in the future.
const full_rebuild: bool = rebuild: {
{
const Int = @typeInfo(terminal.Terminal.Dirty).Struct.backing_integer.?;
const v: Int = @bitCast(state.terminal.flags.dirty);
if (v > 0) break :rebuild true;
}
{
const Int = @typeInfo(terminal.Screen.Dirty).Struct.backing_integer.?;
const v: Int = @bitCast(state.terminal.screen.dirty);
if (v > 0) break :rebuild true;
}
// If our viewport changed then we need to rebuild the entire
// screen because it means we scrolled. If we have no previous
// viewport then we must rebuild.
const prev_viewport = self.cells_viewport orelse break :rebuild true;
if (!prev_viewport.eql(viewport_pin)) break :rebuild true;
break :rebuild false;
};
// Reset the dirty flags in the terminal and screen. We assume
// that our rebuild will be successful since so we optimize for
// success and reset while we hold the lock. This is much easier
// than coordinating row by row or as changes are persisted.
state.terminal.flags.dirty = .{};
state.terminal.screen.dirty = .{};
{
var it = state.terminal.screen.pages.pageIterator(
.right_down,
.{ .screen = .{} },
null,
);
while (it.next()) |chunk| {
var dirty_set = chunk.page.data.dirtyBitSet();
dirty_set.unsetAll();
}
}
// Update our viewport pin for dirty tracking
self.cells_viewport = viewport_pin;
break :critical .{
.full_rebuild = full_rebuild,
.gl_bg = self.background_color,
.screen = screen_copy,
.screen_type = state.terminal.active_screen,
.mouse = state.mouse,
.preedit = preedit,
.cursor_style = cursor_style,
@ -782,7 +851,9 @@ pub fn updateFrame(
// Build our GPU cells
try self.rebuildCells(
critical.full_rebuild,
&critical.screen,
critical.screen_type,
critical.mouse,
critical.preedit,
critical.cursor_style,
@ -1089,7 +1160,9 @@ fn prepKittyImage(
/// the renderer will do this when it needs more memory space.
pub fn rebuildCells(
self: *OpenGL,
rebuild: bool,
screen: *terminal.Screen,
screen_type: terminal.ScreenType,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
@ -1135,6 +1208,12 @@ pub fn rebuildCells(
// remains visible.
var cursor_cell: ?CellProgram.Cell = null;
if (rebuild) {
// We also reset our padding extension depending on the screen type
self.padding_extend_top = screen_type == .alternate;
self.padding_extend_bottom = screen_type == .alternate;
}
// Build each cell
var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null);
var y: terminal.size.CellCountInt = 0;
@ -1185,6 +1264,16 @@ pub fn rebuildCells(
}
};
// On primary screen, we still apply vertical padding extension
// under certain conditions we feel are safe. This helps make some
// scenarios look better while avoiding scenarios we know do NOT look
// good.
if (y == 0 and screen_type == .primary) {
self.padding_extend_top = !row.neverExtendBg();
} else if (y == self.grid_size.rows - 1 and screen_type == .primary) {
self.padding_extend_bottom = !row.neverExtendBg();
}
// Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(
self.font_grid,
@ -1787,13 +1876,11 @@ pub fn setScreenSize(
// Store our screen size
self.screen_size = dim;
self.padding.explicit = pad;
// Recalculate the rows/columns.
const grid_size = self.gridSize(dim);
self.grid_size = self.gridSize(dim);
log.debug("screen size screen={} grid={} cell={} padding={}", .{
dim,
grid_size,
self.grid_size,
renderer.CellSize{
.width = self.grid_metrics.cell_width,
.height = self.grid_metrics.cell_height,
@ -1996,6 +2083,21 @@ fn drawCellProgram(
self.deferred_config = null;
}
// Apply our padding extension fields
{
const program = gl_state.cell_program;
const bind = try program.program.use();
defer bind.unbind();
try program.program.setUniform(
"padding_vertical_top",
self.padding_extend_top,
);
try program.program.setUniform(
"padding_vertical_bottom",
self.padding_extend_bottom,
);
}
// Draw background images first
try self.drawImages(
gl_state,

View File

@ -124,6 +124,10 @@ pub const Uniforms = extern struct {
/// top, right, bottom, left.
grid_padding: [4]f32 align(16),
/// True if vertical padding gets the extended color of the nearest row.
padding_extend_top: bool align(1),
padding_extend_bottom: bool align(1),
/// The minimum contrast ratio for text. The contrast ratio is calculated
/// according to the WCAG 2.0 spec.
min_contrast: f32 align(4),

View File

@ -5,6 +5,8 @@ struct Uniforms {
float2 cell_size;
ushort2 grid_size;
float4 grid_padding;
bool padding_extend_top;
bool padding_extend_bottom;
float min_contrast;
ushort2 cursor_pos;
uchar4 cursor_color;
@ -101,11 +103,14 @@ vertex CellBgVertexOut cell_bg_vertex(unsigned int vid [[vertex_id]],
cell_size_scaled.x = cell_size_scaled.x * input.cell_width;
// If we're at the edge of the grid, we add our padding to the background
// to extend it. Note: grid_padding is top/right/bottom/left.
if (input.grid_pos.y == 0) {
// to extend it. Note: grid_padding is top/right/bottom/left. We always
// extend horiziontally because there is no downside but there are various
// heuristics to disable vertical extension.
if (input.grid_pos.y == 0 && uniforms.padding_extend_top) {
cell_pos.y -= uniforms.grid_padding.r;
cell_size_scaled.y += uniforms.grid_padding.r;
} else if (input.grid_pos.y == uniforms.grid_size.y - 1) {
} else if (input.grid_pos.y == uniforms.grid_size.y - 1 &&
uniforms.padding_extend_bottom) {
cell_size_scaled.y += uniforms.grid_padding.b;
}
if (input.grid_pos.x == 0) {

View File

@ -57,6 +57,8 @@ uniform sampler2D text_color;
uniform vec2 cell_size;
uniform vec2 grid_size;
uniform vec4 grid_padding;
uniform bool padding_vertical_top;
uniform bool padding_vertical_bottom;
uniform mat4 projection;
uniform float min_contrast;
@ -171,10 +173,10 @@ void main() {
case MODE_BG:
// If we're at the edge of the grid, we add our padding to the background
// to extend it. Note: grid_padding is top/right/bottom/left.
if (grid_coord.y == 0) {
if (grid_coord.y == 0 && padding_vertical_top) {
cell_pos.y -= grid_padding.r;
cell_size_scaled.y += grid_padding.r;
} else if (grid_coord.y == grid_size.y - 1) {
} else if (grid_coord.y == grid_size.y - 1 && padding_vertical_bottom) {
cell_size_scaled.y += grid_padding.b;
}
if (grid_coord.x == 0) {

View File

@ -3254,6 +3254,35 @@ pub const Pin = struct {
set.set(self.y);
}
/// Returns true if the row of this pin should never have its background
/// color extended for filling padding space in the renderer. This is
/// a set of heuristics that help making our padding look better.
pub fn neverExtendBg(self: Pin) bool {
// Any semantic prompts should not have their background extended
// because prompts often contain special formatting (such as
// powerline) that looks bad when extended.
const rac = self.rowAndCell();
switch (rac.row.semantic_prompt) {
.prompt, .prompt_continuation, .input => return true,
.unknown, .command => {},
}
for (self.cells(.all)) |*cell| {
// If any cell has a default background color then we don't
// extend because the default background color probably looks
// good enough as an extension.
switch (cell.content_tag) {
.bg_color_palette, .bg_color_rgb => {},
.codepoint, .codepoint_grapheme => {
const s = self.style(cell);
if (s.bg_color == .none) return true;
},
}
}
return false;
}
/// Iterators. These are the same as PageList iterator funcs but operate
/// on pins rather than points. This is MUCH more efficient than calling
/// pointFromPin and building up the iterator from points.