renderer: switch to using render state

pull/9662/head
Mitchell Hashimoto 2025-11-19 05:07:07 -10:00
parent 9162e71bcc
commit ebc8bff8f1
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 334 additions and 385 deletions

View File

@ -255,8 +255,12 @@ pub fn isSymbol(cp: u21) bool {
/// Returns the appropriate `constraint_width` for
/// the provided cell when rendering its glyph(s).
pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
const cell = cell_pin.rowAndCell().cell;
pub fn constraintWidth(
raw_slice: []const terminal.page.Cell,
x: usize,
cols: usize,
) u2 {
const cell = raw_slice[x];
const cp = cell.codepoint();
const grid_width = cell.gridWidth();
@ -271,20 +275,14 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
if (!isSymbol(cp)) return grid_width;
// If we are at the end of the screen it must be constrained to one cell.
if (cell_pin.x == cell_pin.node.data.size.cols - 1) return 1;
if (x == cols - 1) return 1;
// If we have a previous cell and it was a symbol then we need
// to also constrain. This is so that multiple PUA glyphs align.
// This does not apply if the previous symbol is a graphics
// element such as a block element or Powerline glyph.
if (cell_pin.x > 0) {
const prev_cp = prev_cp: {
var copy = cell_pin;
copy.x -= 1;
const prev_cell = copy.rowAndCell().cell;
break :prev_cp prev_cell.codepoint();
};
if (x > 0) {
const prev_cp = raw_slice[x - 1].codepoint();
if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) {
return 1;
}
@ -292,15 +290,8 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
// If the next cell is whitespace, then we
// allow the glyph to be up to two cells wide.
const next_cp = next_cp: {
var copy = cell_pin;
copy.x += 1;
const next_cell = copy.rowAndCell().cell;
break :next_cp next_cell.codepoint();
};
if (next_cp == 0 or isSpace(next_cp)) {
return 2;
}
const next_cp = raw_slice[x + 1].codepoint();
if (next_cp == 0 or isSpace(next_cp)) return 2;
// Otherwise, this has to be 1 cell wide.
return 1;
@ -524,108 +515,171 @@ test "Cell constraint widths" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try terminal.Screen.init(alloc, .{ .cols = 4, .rows = 1, .max_scrollback = 0 });
var t: terminal.Terminal = try .init(alloc, .{
.cols = 4,
.rows = 1,
});
defer t.deinit(alloc);
var s = t.vtStream();
defer s.deinit();
var state: terminal.RenderState = .empty;
defer state.deinit(alloc);
// for each case, the numbers in the comment denote expected
// constraint widths for the symbol-containing cells
// symbol->nothing: 2
{
try s.testWriteString("");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expectEqual(2, constraintWidth(p0));
s.reset();
t.fullReset();
try s.nextSlice("");
try state.update(alloc, &t);
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
}
// symbol->character: 1
{
try s.testWriteString("z");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expectEqual(1, constraintWidth(p0));
s.reset();
t.fullReset();
try s.nextSlice("z");
try state.update(alloc, &t);
try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
}
// symbol->space: 2
{
try s.testWriteString(" z");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expectEqual(2, constraintWidth(p0));
s.reset();
t.fullReset();
try s.nextSlice(" z");
try state.update(alloc, &t);
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
}
// symbol->no-break space: 1
{
try s.testWriteString("\u{00a0}z");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expectEqual(1, constraintWidth(p0));
s.reset();
t.fullReset();
try s.nextSlice("\u{00a0}z");
try state.update(alloc, &t);
try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
}
// symbol->end of row: 1
{
try s.testWriteString("");
const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?;
try testing.expectEqual(1, constraintWidth(p3));
s.reset();
t.fullReset();
try s.nextSlice("");
try state.update(alloc, &t);
try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
3,
state.cols,
));
}
// character->symbol: 2
{
try s.testWriteString("z");
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?;
try testing.expectEqual(2, constraintWidth(p1));
s.reset();
t.fullReset();
try s.nextSlice("z");
try state.update(alloc, &t);
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
1,
state.cols,
));
}
// symbol->symbol: 1,1
{
try s.testWriteString("");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?;
try testing.expectEqual(1, constraintWidth(p0));
try testing.expectEqual(1, constraintWidth(p1));
s.reset();
t.fullReset();
try s.nextSlice("");
try state.update(alloc, &t);
try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
1,
state.cols,
));
}
// symbol->space->symbol: 2,2
{
try s.testWriteString(" ");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?;
try testing.expectEqual(2, constraintWidth(p0));
try testing.expectEqual(2, constraintWidth(p2));
s.reset();
t.fullReset();
try s.nextSlice(" ");
try state.update(alloc, &t);
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
2,
state.cols,
));
}
// symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg)
{
try s.testWriteString("");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expectEqual(1, constraintWidth(p0));
s.reset();
t.fullReset();
try s.nextSlice("");
try state.update(alloc, &t);
try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
}
// powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg)
{
try s.testWriteString("");
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?;
try testing.expectEqual(2, constraintWidth(p1));
s.reset();
t.fullReset();
try s.nextSlice("");
try state.update(alloc, &t);
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
1,
state.cols,
));
}
// powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg)
{
try s.testWriteString("");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expectEqual(2, constraintWidth(p0));
s.reset();
t.fullReset();
try s.nextSlice("");
try state.update(alloc, &t);
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
}
// powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg)
{
try s.testWriteString(" z");
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
try testing.expectEqual(2, constraintWidth(p0));
s.reset();
t.fullReset();
try s.nextSlice(" z");
try state.update(alloc, &t);
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
}
}

View File

@ -125,12 +125,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// cells goes into a separate shader.
cells: cellpkg.Contents,
/// 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,
/// Set to true after rebuildCells is called. This can be used
/// to determine if any possible changes have been made to the
/// cells for the draw call.
@ -940,8 +934,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
/// Mark the full screen as dirty so that we redraw everything.
pub fn markDirty(self: *Self) void {
self.cells_viewport = null;
pub inline fn markDirty(self: *Self) void {
self.terminal_state.redraw = true;
}
/// Called when we get an updated display ID for our display link.
@ -1047,7 +1041,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Force a full rebuild, because cached rows may still reference
// an outdated atlas from the old grid and this can cause garbage
// to be rendered.
self.cells_viewport = null;
self.markDirty();
}
/// Update uniforms that are based on the font grid.
@ -1070,17 +1064,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const Critical = struct {
bg: terminal.color.RGB,
fg: terminal.color.RGB,
screen: terminal.Screen,
screen_type: terminal.ScreenSet.Key,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_color: ?terminal.color.RGB,
cursor_style: ?renderer.CursorStyle,
color_palette: terminal.color.Palette,
scrollbar: terminal.Scrollbar,
/// If true, rebuild the full screen.
full_rebuild: bool,
};
// Update all our data as tightly as possible within the mutex.
@ -1122,19 +1111,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.{ bg, fg };
};
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screens.active.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 while rebuilding GPU cells.
var screen_copy = try state.terminal.screens.active.clone(
self.alloc,
.{ .viewport = .{} },
null,
);
errdefer screen_copy.deinit();
// Whether to draw our cursor or not.
const cursor_style = if (state.terminal.flags.password_input)
.lock
@ -1166,77 +1142,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
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.screens.active.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.screens.active.dirty = .{};
{
var it = state.terminal.screens.active.pages.pageIterator(
.right_down,
.{ .viewport = .{} },
null,
);
while (it.next()) |chunk| {
chunk.node.data.dirty = false;
for (chunk.rows()) |*row| {
row.dirty = false;
}
}
}
// Update our viewport pin
self.cells_viewport = viewport_pin;
break :critical .{
.bg = bg,
.fg = fg,
.screen = screen_copy,
.screen_type = state.terminal.screens.active_key,
.mouse = state.mouse,
.preedit = preedit,
.cursor_color = state.terminal.colors.cursor.get(),
.cursor_style = cursor_style,
.color_palette = state.terminal.colors.palette.current,
.scrollbar = scrollbar,
.full_rebuild = full_rebuild,
};
};
defer {
critical.screen.deinit();
if (critical.preedit) |p| p.deinit(self.alloc);
}
// Build our GPU cells
try self.rebuildCells(
critical.full_rebuild,
&critical.screen,
critical.screen_type,
critical.mouse,
critical.preedit,
critical.cursor_style,
&critical.color_palette,
@ -2098,7 +2020,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (bg_image_config_changed) self.updateBgImageBuffer();
// Reset our viewport to force a rebuild, in case of a font change.
self.cells_viewport = null;
self.markDirty();
const blending_changed = old_blending != config.blending;
@ -2319,95 +2241,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
}
fn rebuildCells2(
self: *Self,
) !void {
const state: *terminal.RenderState = &self.terminal_state;
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
// Handle the case that our grid size doesn't match the terminal
// state grid size. It's possible our backing views for renderers
// have a mismatch temporarily since view resize is handled async
// to terminal state resize and is mostly dependent on GUI
// frameworks.
const grid_size_diff =
self.cells.size.rows != state.rows or
self.cells.size.columns != state.cols;
if (grid_size_diff) {
var new_size = self.cells.size;
new_size.rows = state.rows;
new_size.columns = state.cols;
try self.cells.resize(self.alloc, new_size);
// Update our uniforms accordingly, otherwise
// our background cells will be out of place.
self.uniforms.grid_size = .{ new_size.columns, new_size.rows };
}
// Redraw means we are redrawing the full grid, regardless of
// individual row dirtiness.
const redraw = state.redraw or grid_size_diff;
if (redraw) {
// 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
switch (self.config.padding_color) {
.background => {},
// For extension, assume we are extending in all directions.
// For "extend" this may be disabled due to heuristics below.
.extend, .@"extend-always" => {
self.uniforms.padding_extend = .{
.up = true,
.down = true,
.left = true,
.right = true,
};
},
}
}
// Go through all the rows and rebuild as necessary. If we have
// a size mismatch on the state and our grid we just fill what
// we can from the BOTTOM of the viewport.
const start_idx = state.rows - @min(
state.rows,
self.cells.size.rows,
);
const row_data = state.row_data.slice();
for (
0..,
row_data.items(.cells)[start_idx..],
row_data.items(.dirty)[start_idx..],
) |y, *cell, dirty| {
if (!redraw) {
// Only rebuild if we are doing a full rebuild or
// this row is dirty.
if (!dirty) continue;
// Clear the cells if the row is dirty
self.cells.clear(y);
}
_ = cell;
}
}
/// Convert the terminal state to GPU cells stored in CPU memory. These
/// are then synced to the GPU in the next frame. This only updates CPU
/// memory and doesn't touch the GPU.
fn rebuildCells(
self: *Self,
wants_rebuild: bool,
screen: *terminal.Screen,
screen_type: terminal.ScreenSet.Key,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle,
color_palette: *const terminal.color.Palette,
@ -2415,6 +2253,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
foreground: terminal.color.RGB,
terminal_cursor_color: ?terminal.color.RGB,
) !void {
const state: *terminal.RenderState = &self.terminal_state;
defer state.redraw = false;
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
@ -2426,20 +2267,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
// }
_ = screen_type; // we might use this again later so not deleting it yet
// Create an arena for all our temporary allocations while rebuilding
var arena = ArenaAllocator.init(self.alloc);
defer arena.deinit();
const arena_alloc = arena.allocator();
// TODO: renderstate
// Create our match set for the links.
var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
arena_alloc,
screen,
mouse_pt,
mouse.mods,
) else .{};
// var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
// arena_alloc,
// screen,
// mouse_pt,
// mouse.mods,
// ) else .{};
// Determine our x/y range for preedit. We don't want to render anything
// here because we will render the preedit separately.
@ -2448,22 +2283,31 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
x: [2]terminal.size.CellCountInt,
cp_offset: usize,
} = if (preedit) |preedit_v| preedit: {
const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1);
// We base the preedit on the position of the cursor in the
// viewport. If the cursor isn't visible in the viewport we
// don't show it.
const cursor_vp = state.cursor.viewport orelse
break :preedit null;
const range = preedit_v.range(
cursor_vp.x,
state.cols - 1,
);
break :preedit .{
.y = screen.cursor.y,
.y = @intCast(cursor_vp.y),
.x = .{ range.start, range.end },
.cp_offset = range.cp_offset,
};
} else null;
const grid_size_diff =
self.cells.size.rows != screen.pages.rows or
self.cells.size.columns != screen.pages.cols;
self.cells.size.rows != state.rows or
self.cells.size.columns != state.cols;
if (grid_size_diff) {
var new_size = self.cells.size;
new_size.rows = screen.pages.rows;
new_size.columns = screen.pages.cols;
new_size.rows = state.rows;
new_size.columns = state.cols;
try self.cells.resize(self.alloc, new_size);
// Update our uniforms accordingly, otherwise
@ -2471,8 +2315,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.uniforms.grid_size = .{ new_size.columns, new_size.rows };
}
const rebuild = wants_rebuild or grid_size_diff;
const rebuild = state.redraw or grid_size_diff;
if (rebuild) {
// If we are doing a full rebuild, then we clear the entire cell buffer.
self.cells.reset();
@ -2494,76 +2337,82 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
}
// We rebuild the cells row-by-row because we
// do font shaping and dirty tracking by row.
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
// Get our row data from our state
const row_data = state.row_data.slice();
const row_cells = row_data.items(.cells);
const row_dirty = row_data.items(.dirty);
const row_selection = row_data.items(.selection);
// If our cell contents buffer is shorter than the screen viewport,
// we render the rows that fit, starting from the bottom. If instead
// the viewport is shorter than the cell contents buffer, we align
// the top of the viewport with the top of the contents buffer.
var y: terminal.size.CellCountInt = @min(
screen.pages.rows,
const row_len: usize = @min(
state.rows,
self.cells.size.rows,
);
while (row_it.next()) |row| {
// The viewport may have more rows than our cell contents,
// so we need to break from the loop early if we hit y = 0.
if (y == 0) break;
y -= 1;
for (
0..,
row_cells[0..row_len],
row_dirty[0..row_len],
row_selection[0..row_len],
) |y_usize, *cells, *dirty, selection| {
const y: terminal.size.CellCountInt = @intCast(y_usize);
if (!rebuild) {
// Only rebuild if we are doing a full rebuild or this row is dirty.
if (!row.isDirty()) continue;
if (!dirty.*) continue;
// Clear the cells if the row is dirty
self.cells.clear(y);
}
// True if we want to do font shaping around the cursor.
// We want to do font shaping as long as the cursor is enabled.
const shape_cursor = screen.viewportIsBottom() and
y == screen.cursor.y;
// We need to get this row's selection, if
// there is one, for proper run splitting.
const row_selection = sel: {
const sel = screen.selection orelse break :sel null;
const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
break :sel null;
break :sel sel.containedRow(screen, pin) orelse null;
};
// Unmark the dirty state in our render state.
dirty.* = false;
// TODO: renderstate
// 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.
switch (self.config.padding_color) {
// These already have the correct values set above.
.background, .@"extend-always" => {},
// Apply heuristics for padding extension.
.extend => if (y == 0) {
self.uniforms.padding_extend.up = !row.neverExtendBg(
color_palette,
background,
);
} else if (y == self.cells.size.rows - 1) {
self.uniforms.padding_extend.down = !row.neverExtendBg(
color_palette,
background,
);
},
}
// switch (self.config.padding_color) {
// // These already have the correct values set above.
// .background, .@"extend-always" => {},
//
// // Apply heuristics for padding extension.
// .extend => if (y == 0) {
// self.uniforms.padding_extend.up = !row.neverExtendBg(
// color_palette,
// background,
// );
// } else if (y == self.cells.size.rows - 1) {
// self.uniforms.padding_extend.down = !row.neverExtendBg(
// color_palette,
// background,
// );
// },
// }
// Iterator of runs for shaping.
const cells_slice = cells.slice();
var run_iter_opts: font.shape.RunOptions = .{
.grid = self.font_grid,
.screen = screen,
.row = row,
.selection = row_selection,
.cursor_x = if (shape_cursor) screen.cursor.x else null,
.cells = cells_slice,
.selection2 = if (selection) |s| s else null,
// We want to do font shaping as long as the cursor is
// visible on this viewport.
.cursor_x = cursor_x: {
const vp = state.cursor.viewport orelse break :cursor_x null;
if (vp.y != y) break :cursor_x null;
break :cursor_x vp.x;
},
// Old stuff
.screen = undefined,
.row = undefined,
.selection = null,
};
run_iter_opts.applyBreakConfig(self.config.font_shaping_break);
var run_iter = self.font_shaper.runIterator(run_iter_opts);
@ -2571,13 +2420,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
var shaper_cells: ?[]const font.shape.Cell = null;
var shaper_cells_i: usize = 0;
const row_cells_all = row.cells(.all);
// If our viewport is wider than our cell contents buffer,
// we still only process cells up to the width of the buffer.
const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)];
for (row_cells, 0..) |*cell, x| {
const cells_len = @min(cells_slice.len, self.cells.size.columns);
const cells_raw = cells_slice.items(.raw);
const cells_style = cells_slice.items(.style);
for (
0..,
cells_raw[0..cells_len],
cells_style[0..cells_len],
) |x, *cell, *managed_style| {
// If this cell falls within our preedit range then we
// skip this because preedits are setup separately.
if (preedit_range) |range| preedit: {
@ -2610,7 +2462,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.font_shaper_cache.get(run) orelse
cache: {
// Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run);
const new_cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization,
@ -2618,7 +2470,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.font_shaper_cache.put(
self.alloc,
run,
cells,
new_cells,
) catch |err| {
log.warn(
"error caching font shaping results err={}",
@ -2629,49 +2481,42 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so
// we can safely use them.
break :cache cells;
break :cache new_cells;
};
const cells = shaper_cells.?;
// Advance our index until we reach or pass
// our current x position in the shaper cells.
while (run.offset + cells[shaper_cells_i].x < x) {
const shaper_cells_unwrapped = shaper_cells.?;
while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) {
shaper_cells_i += 1;
}
}
const wide = cell.wide;
const style = row.style(cell);
const cell_pin: terminal.Pin = cell: {
var copy = row;
copy.x = @intCast(x);
break :cell copy;
};
const style: terminal.Style = if (cell.hasStyling())
managed_style.*
else
.{};
// True if this cell is selected
const selected: bool = if (screen.selection) |sel|
sel.contains(screen, .{
.node = row.node,
.y = row.y,
.x = @intCast(
// Spacer tails should show the selection
// state of the wide cell they belong to.
if (wide == .spacer_tail)
x -| 1
else
x,
),
})
else
false;
const selected: bool = selected: {
const sel = selection orelse break :selected false;
const x_compare = if (wide == .spacer_tail)
x -| 1
else
x;
break :selected x_compare >= sel[0] and
x_compare <= sel[1];
};
// The `_style` suffixed values are the colors based on
// the cell style (SGR), before applying any additional
// configuration, inversions, selections, etc.
const bg_style = style.bg(cell, color_palette);
const bg_style = style.bg(
cell,
color_palette,
);
const fg_style = style.fg(.{
.default = foreground,
.palette = color_palette,
@ -2793,16 +2638,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
continue;
}
// TODO: renderstate
// Give links a single underline, unless they already have
// an underline, in which case use a double underline to
// distinguish them.
const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
if (style.flags.underline == .single)
.double
else
.single
else
style.flags.underline;
// const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
// if (style.flags.underline == .single)
// .double
// else
// .single
// else
// style.flags.underline;
const underline = style.flags.underline;
// We draw underlines first so that they layer underneath text.
// This improves readability when a colored underline is used
@ -2842,7 +2689,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.font_shaper_cache.get(run) orelse
cache: {
// Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run);
const new_cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization,
@ -2850,7 +2697,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.font_shaper_cache.put(
self.alloc,
run,
cells,
new_cells,
) catch |err| {
log.warn(
"error caching font shaping results err={}",
@ -2861,32 +2708,34 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so
// we can safely use them.
break :cache cells;
break :cache new_cells;
};
const cells = shaper_cells orelse break :glyphs;
const shaped_cells = shaper_cells orelse break :glyphs;
// If there are no shaper cells for this run, ignore it.
// This can occur for runs of empty cells, and is fine.
if (cells.len == 0) break :glyphs;
if (shaped_cells.len == 0) break :glyphs;
// If we encounter a shaper cell to the left of the current
// cell then we have some problems. This logic relies on x
// position monotonically increasing.
assert(run.offset + cells[shaper_cells_i].x >= x);
assert(run.offset + shaped_cells[shaper_cells_i].x >= x);
// NOTE: An assumption is made here that a single cell will never
// be present in more than one shaper run. If that assumption is
// violated, this logic breaks.
while (shaper_cells_i < cells.len and run.offset + cells[shaper_cells_i].x == x) : ({
while (shaper_cells_i < shaped_cells.len and
run.offset + shaped_cells[shaper_cells_i].x == x) : ({
shaper_cells_i += 1;
}) {
self.addGlyph(
@intCast(x),
@intCast(y),
cell_pin,
cells[shaper_cells_i],
state.cols,
cells_raw,
shaped_cells[shaper_cells_i],
shaper_run.?,
fg,
alpha,
@ -2938,14 +2787,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
inline .@"cell-foreground",
.@"cell-background",
=> |_, tag| {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
const sty: terminal.Style = state.cursor.style;
const fg_style = sty.fg(.{
.default = foreground,
.palette = color_palette,
.bold = self.config.bold_color,
});
const bg_style = sty.bg(
screen.cursor.page_cell,
&state.cursor.cell,
color_palette,
) orelse background;
@ -2960,21 +2809,27 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
break :cursor_color foreground;
};
self.addCursor(screen, style, cursor_color);
self.addCursor(
&state.cursor,
style,
cursor_color,
);
// If the cursor is visible then we set our uniforms.
if (style == .block and screen.viewportIsBottom()) {
const wide = screen.cursor.page_cell.wide;
if (style == .block) cursor_uniforms: {
const cursor_vp = state.cursor.viewport orelse
break :cursor_uniforms;
const wide = state.cursor.cell.wide;
self.uniforms.cursor_pos = .{
// If we are a spacer tail of a wide cell, our cursor needs
// to move back one cell. The saturate is to ensure we don't
// overflow but this shouldn't happen with well-formed input.
switch (wide) {
.narrow, .spacer_head, .wide => screen.cursor.x,
.spacer_tail => screen.cursor.x -| 1,
.narrow, .spacer_head, .wide => cursor_vp.x,
.spacer_tail => cursor_vp.x -| 1,
},
screen.cursor.y,
@intCast(cursor_vp.y),
};
self.uniforms.bools.cursor_wide = switch (wide) {
@ -2990,14 +2845,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
break :blk txt.color.toTerminalRGB();
}
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
const sty = state.cursor.style;
const fg_style = sty.fg(.{
.default = foreground,
.palette = color_palette,
.bold = self.config.bold_color,
});
const bg_style = sty.bg(
screen.cursor.page_cell,
&state.cursor.cell,
color_palette,
) orelse background;
@ -3157,15 +3012,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self: *Self,
x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt,
cell_pin: terminal.Pin,
cols: usize,
cell_raws: []const terminal.page.Cell,
shaper_cell: font.shape.Cell,
shaper_run: font.shape.TextRun,
color: terminal.color.RGB,
alpha: u8,
) !void {
const rac = cell_pin.rowAndCell();
const cell = rac.cell;
const cell = cell_raws[x];
const cp = cell.codepoint();
// Render
@ -3185,7 +3039,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (cellpkg.isSymbol(cp)) .{
.size = .fit,
} else .none,
.constraint_width = constraintWidth(cell_pin),
.constraint_width = constraintWidth(
cell_raws,
x,
cols,
),
},
);
@ -3214,22 +3072,24 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
fn addCursor(
self: *Self,
screen: *terminal.Screen,
cursor_state: *const terminal.RenderState.Cursor,
cursor_style: renderer.CursorStyle,
cursor_color: terminal.color.RGB,
) void {
const cursor_vp = cursor_state.viewport orelse return;
// Add the cursor. We render the cursor over the wide character if
// we're on the wide character tail.
const wide, const x = cell: {
// The cursor goes over the screen cursor position.
const cell = screen.cursor.page_cell;
if (cell.wide != .spacer_tail or screen.cursor.x == 0)
break :cell .{ cell.wide == .wide, screen.cursor.x };
if (!cursor_vp.wide_tail) break :cell .{
cursor_state.cell.wide == .wide,
cursor_vp.x,
};
// If we're part of a wide character, we move the cursor back to
// the actual character.
const prev_cell = screen.cursorCellLeft(1);
break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
// If we're part of a wide character, we move the cursor back
// to the actual character.
break :cell .{ true, cursor_vp.x - 1 };
};
const alpha: u8 = if (!self.focused) 255 else alpha: {
@ -3288,7 +3148,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.cells.setCursor(.{
.atlas = .grayscale,
.bools = .{ .is_cursor_glyph = true },
.grid_pos = .{ x, screen.cursor.y },
.grid_pos = .{ x, cursor_vp.y },
.color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height },

View File

@ -6,6 +6,7 @@ const fastmem = @import("../fastmem.zig");
const point = @import("point.zig");
const size = @import("size.zig");
const page = @import("page.zig");
const Pin = @import("PageList.zig").Pin;
const Screen = @import("Screen.zig");
const ScreenSet = @import("ScreenSet.zig");
const Style = @import("style.zig").Style;
@ -68,6 +69,11 @@ pub const RenderState = struct {
/// to detect changes.
screen: ScreenSet.Key,
/// The last viewport pin used to generate this state. This is NOT
/// a tracked pin and is generally NOT safe to read other than the direct
/// values for comparison.
viewport_pin: ?Pin = null,
/// Initial state.
pub const empty: RenderState = .{
.rows = 0,
@ -90,7 +96,7 @@ pub const RenderState = struct {
/// The x/y position of the cursor within the viewport. This
/// may be null if the cursor is not visible within the viewport.
viewport: ?point.Coordinate,
viewport: ?Viewport,
/// The cell data for the cursor position. Managed memory is not
/// safe to access from this.
@ -98,6 +104,17 @@ pub const RenderState = struct {
/// The style, always valid even if the cell is default style.
style: Style,
pub const Viewport = struct {
/// The x/y position of the cursor within the viewport.
x: size.CellCountInt,
y: size.CellCountInt,
/// Whether the cursor is part of a wide character and
/// on the tail of it. If so, some renderers may use this
/// to move the cursor back one.
wide_tail: bool,
};
};
/// A row within the viewport.
@ -118,7 +135,7 @@ pub const RenderState = struct {
dirty: bool,
/// The x range of the selection within this row.
selection: [2]size.CellCountInt,
selection: ?[2]size.CellCountInt,
};
pub const Cell = struct {
@ -159,6 +176,7 @@ pub const RenderState = struct {
t: *Terminal,
) Allocator.Error!void {
const s: *Screen = t.screens.active;
const viewport_pin = s.pages.getTopLeft(.viewport);
const redraw = redraw: {
// If our screen key changed, we need to do a full rebuild
// because our render state is viewport-specific.
@ -187,6 +205,11 @@ pub const RenderState = struct {
break :redraw true;
}
// If our viewport pin changed, we do a full rebuild.
if (self.viewport_pin) |old| {
if (!old.eql(viewport_pin)) break :redraw true;
}
break :redraw false;
};
@ -203,6 +226,7 @@ pub const RenderState = struct {
self.rows = s.pages.rows;
self.cols = s.pages.cols;
self.viewport_is_bottom = s.viewportIsBottom();
self.viewport_pin = viewport_pin;
self.cursor.active = .{ .x = s.cursor.x, .y = s.cursor.y };
self.cursor.cell = s.cursor.page_cell.*;
self.cursor.style = s.cursor.page_pin.style(s.cursor.page_cell);
@ -232,7 +256,7 @@ pub const RenderState = struct {
.arena = .{},
.cells = .empty,
.dirty = true,
.selection = .{ 0, 0 },
.selection = null,
});
}
} else {
@ -272,11 +296,22 @@ pub const RenderState = struct {
self.cursor.viewport = .{
.y = y,
.x = s.cursor.x,
// Future: we should use our own state here to look this
// up rather than calling this.
.wide_tail = if (s.cursor.x > 0)
s.cursorCellLeft(1).wide == .wide
else
false,
};
}
// If the row isn't dirty then we assume it is unchanged.
if (!redraw and !row_pin.isDirty()) continue;
var dirty_set = row_pin.node.data.dirtyBitSet();
if (!redraw and !dirty_set.isSet(row_pin.y)) continue;
// Clear the dirty flag on the row
dirty_set.unset(row_pin.y);
// Promote our arena. State is copied by value so we need to
// restore it on all exit paths so we don't leak memory.