renderer: switch to using render state
parent
9162e71bcc
commit
ebc8bff8f1
|
|
@ -255,8 +255,12 @@ pub fn isSymbol(cp: u21) bool {
|
||||||
|
|
||||||
/// Returns the appropriate `constraint_width` for
|
/// Returns the appropriate `constraint_width` for
|
||||||
/// the provided cell when rendering its glyph(s).
|
/// the provided cell when rendering its glyph(s).
|
||||||
pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
|
pub fn constraintWidth(
|
||||||
const cell = cell_pin.rowAndCell().cell;
|
raw_slice: []const terminal.page.Cell,
|
||||||
|
x: usize,
|
||||||
|
cols: usize,
|
||||||
|
) u2 {
|
||||||
|
const cell = raw_slice[x];
|
||||||
const cp = cell.codepoint();
|
const cp = cell.codepoint();
|
||||||
|
|
||||||
const grid_width = cell.gridWidth();
|
const grid_width = cell.gridWidth();
|
||||||
|
|
@ -271,20 +275,14 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
|
||||||
if (!isSymbol(cp)) return grid_width;
|
if (!isSymbol(cp)) return grid_width;
|
||||||
|
|
||||||
// If we are at the end of the screen it must be constrained to one cell.
|
// 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
|
// 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.
|
// to also constrain. This is so that multiple PUA glyphs align.
|
||||||
// This does not apply if the previous symbol is a graphics
|
// This does not apply if the previous symbol is a graphics
|
||||||
// element such as a block element or Powerline glyph.
|
// element such as a block element or Powerline glyph.
|
||||||
if (cell_pin.x > 0) {
|
if (x > 0) {
|
||||||
const prev_cp = prev_cp: {
|
const prev_cp = raw_slice[x - 1].codepoint();
|
||||||
var copy = cell_pin;
|
|
||||||
copy.x -= 1;
|
|
||||||
const prev_cell = copy.rowAndCell().cell;
|
|
||||||
break :prev_cp prev_cell.codepoint();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) {
|
if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
@ -292,15 +290,8 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
|
||||||
|
|
||||||
// If the next cell is whitespace, then we
|
// If the next cell is whitespace, then we
|
||||||
// allow the glyph to be up to two cells wide.
|
// allow the glyph to be up to two cells wide.
|
||||||
const next_cp = next_cp: {
|
const next_cp = raw_slice[x + 1].codepoint();
|
||||||
var copy = cell_pin;
|
if (next_cp == 0 or isSpace(next_cp)) return 2;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, this has to be 1 cell wide.
|
// Otherwise, this has to be 1 cell wide.
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -524,108 +515,171 @@ test "Cell constraint widths" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
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();
|
defer s.deinit();
|
||||||
|
|
||||||
|
var state: terminal.RenderState = .empty;
|
||||||
|
defer state.deinit(alloc);
|
||||||
|
|
||||||
// for each case, the numbers in the comment denote expected
|
// for each case, the numbers in the comment denote expected
|
||||||
// constraint widths for the symbol-containing cells
|
// constraint widths for the symbol-containing cells
|
||||||
|
|
||||||
// symbol->nothing: 2
|
// symbol->nothing: 2
|
||||||
{
|
{
|
||||||
try s.testWriteString("");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice("");
|
||||||
try testing.expectEqual(2, constraintWidth(p0));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
try testing.expectEqual(2, constraintWidth(
|
||||||
|
state.row_data.get(0).cells.items(.raw),
|
||||||
|
0,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// symbol->character: 1
|
// symbol->character: 1
|
||||||
{
|
{
|
||||||
try s.testWriteString("z");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice("z");
|
||||||
try testing.expectEqual(1, constraintWidth(p0));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
try testing.expectEqual(1, constraintWidth(
|
||||||
|
state.row_data.get(0).cells.items(.raw),
|
||||||
|
0,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// symbol->space: 2
|
// symbol->space: 2
|
||||||
{
|
{
|
||||||
try s.testWriteString(" z");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice(" z");
|
||||||
try testing.expectEqual(2, constraintWidth(p0));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
try testing.expectEqual(2, constraintWidth(
|
||||||
|
state.row_data.get(0).cells.items(.raw),
|
||||||
|
0,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// symbol->no-break space: 1
|
// symbol->no-break space: 1
|
||||||
{
|
{
|
||||||
try s.testWriteString("\u{00a0}z");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice("\u{00a0}z");
|
||||||
try testing.expectEqual(1, constraintWidth(p0));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
try testing.expectEqual(1, constraintWidth(
|
||||||
|
state.row_data.get(0).cells.items(.raw),
|
||||||
|
0,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// symbol->end of row: 1
|
// symbol->end of row: 1
|
||||||
{
|
{
|
||||||
try s.testWriteString(" ");
|
t.fullReset();
|
||||||
const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?;
|
try s.nextSlice(" ");
|
||||||
try testing.expectEqual(1, constraintWidth(p3));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
try testing.expectEqual(1, constraintWidth(
|
||||||
|
state.row_data.get(0).cells.items(.raw),
|
||||||
|
3,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// character->symbol: 2
|
// character->symbol: 2
|
||||||
{
|
{
|
||||||
try s.testWriteString("z");
|
t.fullReset();
|
||||||
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
try s.nextSlice("z");
|
||||||
try testing.expectEqual(2, constraintWidth(p1));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
try testing.expectEqual(2, constraintWidth(
|
||||||
|
state.row_data.get(0).cells.items(.raw),
|
||||||
|
1,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// symbol->symbol: 1,1
|
// symbol->symbol: 1,1
|
||||||
{
|
{
|
||||||
try s.testWriteString("");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice("");
|
||||||
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
try state.update(alloc, &t);
|
||||||
try testing.expectEqual(1, constraintWidth(p0));
|
try testing.expectEqual(1, constraintWidth(
|
||||||
try testing.expectEqual(1, constraintWidth(p1));
|
state.row_data.get(0).cells.items(.raw),
|
||||||
s.reset();
|
0,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
|
try testing.expectEqual(1, constraintWidth(
|
||||||
|
state.row_data.get(0).cells.items(.raw),
|
||||||
|
1,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// symbol->space->symbol: 2,2
|
// symbol->space->symbol: 2,2
|
||||||
{
|
{
|
||||||
try s.testWriteString(" ");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice(" ");
|
||||||
const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?;
|
try state.update(alloc, &t);
|
||||||
try testing.expectEqual(2, constraintWidth(p0));
|
try testing.expectEqual(2, constraintWidth(
|
||||||
try testing.expectEqual(2, constraintWidth(p2));
|
state.row_data.get(0).cells.items(.raw),
|
||||||
s.reset();
|
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)
|
// symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg)
|
||||||
{
|
{
|
||||||
try s.testWriteString("");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice("");
|
||||||
try testing.expectEqual(1, constraintWidth(p0));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
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)
|
// powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg)
|
||||||
{
|
{
|
||||||
try s.testWriteString("");
|
t.fullReset();
|
||||||
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
try s.nextSlice("");
|
||||||
try testing.expectEqual(2, constraintWidth(p1));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
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)
|
// powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg)
|
||||||
{
|
{
|
||||||
try s.testWriteString("");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice("");
|
||||||
try testing.expectEqual(2, constraintWidth(p0));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
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)
|
// powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg)
|
||||||
{
|
{
|
||||||
try s.testWriteString(" z");
|
t.fullReset();
|
||||||
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
try s.nextSlice(" z");
|
||||||
try testing.expectEqual(2, constraintWidth(p0));
|
try state.update(alloc, &t);
|
||||||
s.reset();
|
try testing.expectEqual(2, constraintWidth(
|
||||||
|
state.row_data.get(0).cells.items(.raw),
|
||||||
|
0,
|
||||||
|
state.cols,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,12 +125,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
/// cells goes into a separate shader.
|
/// cells goes into a separate shader.
|
||||||
cells: cellpkg.Contents,
|
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
|
/// Set to true after rebuildCells is called. This can be used
|
||||||
/// to determine if any possible changes have been made to the
|
/// to determine if any possible changes have been made to the
|
||||||
/// cells for the draw call.
|
/// 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.
|
/// Mark the full screen as dirty so that we redraw everything.
|
||||||
pub fn markDirty(self: *Self) void {
|
pub inline fn markDirty(self: *Self) void {
|
||||||
self.cells_viewport = null;
|
self.terminal_state.redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when we get an updated display ID for our display link.
|
/// 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
|
// Force a full rebuild, because cached rows may still reference
|
||||||
// an outdated atlas from the old grid and this can cause garbage
|
// an outdated atlas from the old grid and this can cause garbage
|
||||||
// to be rendered.
|
// to be rendered.
|
||||||
self.cells_viewport = null;
|
self.markDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update uniforms that are based on the font grid.
|
/// Update uniforms that are based on the font grid.
|
||||||
|
|
@ -1070,17 +1064,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
const Critical = struct {
|
const Critical = struct {
|
||||||
bg: terminal.color.RGB,
|
bg: terminal.color.RGB,
|
||||||
fg: terminal.color.RGB,
|
fg: terminal.color.RGB,
|
||||||
screen: terminal.Screen,
|
|
||||||
screen_type: terminal.ScreenSet.Key,
|
|
||||||
mouse: renderer.State.Mouse,
|
mouse: renderer.State.Mouse,
|
||||||
preedit: ?renderer.State.Preedit,
|
preedit: ?renderer.State.Preedit,
|
||||||
cursor_color: ?terminal.color.RGB,
|
cursor_color: ?terminal.color.RGB,
|
||||||
cursor_style: ?renderer.CursorStyle,
|
cursor_style: ?renderer.CursorStyle,
|
||||||
color_palette: terminal.color.Palette,
|
color_palette: terminal.color.Palette,
|
||||||
scrollbar: terminal.Scrollbar,
|
scrollbar: terminal.Scrollbar,
|
||||||
|
|
||||||
/// If true, rebuild the full screen.
|
|
||||||
full_rebuild: bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update all our data as tightly as possible within the mutex.
|
// Update all our data as tightly as possible within the mutex.
|
||||||
|
|
@ -1122,19 +1111,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
.{ bg, fg };
|
.{ 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.
|
// Whether to draw our cursor or not.
|
||||||
const cursor_style = if (state.terminal.flags.password_input)
|
const cursor_style = if (state.terminal.flags.password_input)
|
||||||
.lock
|
.lock
|
||||||
|
|
@ -1166,77 +1142,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
try self.prepKittyGraphics(state.terminal);
|
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 .{
|
break :critical .{
|
||||||
.bg = bg,
|
.bg = bg,
|
||||||
.fg = fg,
|
.fg = fg,
|
||||||
.screen = screen_copy,
|
|
||||||
.screen_type = state.terminal.screens.active_key,
|
|
||||||
.mouse = state.mouse,
|
.mouse = state.mouse,
|
||||||
.preedit = preedit,
|
.preedit = preedit,
|
||||||
.cursor_color = state.terminal.colors.cursor.get(),
|
.cursor_color = state.terminal.colors.cursor.get(),
|
||||||
.cursor_style = cursor_style,
|
.cursor_style = cursor_style,
|
||||||
.color_palette = state.terminal.colors.palette.current,
|
.color_palette = state.terminal.colors.palette.current,
|
||||||
.scrollbar = scrollbar,
|
.scrollbar = scrollbar,
|
||||||
.full_rebuild = full_rebuild,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
defer {
|
defer {
|
||||||
critical.screen.deinit();
|
|
||||||
if (critical.preedit) |p| p.deinit(self.alloc);
|
if (critical.preedit) |p| p.deinit(self.alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build our GPU cells
|
// Build our GPU cells
|
||||||
try self.rebuildCells(
|
try self.rebuildCells(
|
||||||
critical.full_rebuild,
|
|
||||||
&critical.screen,
|
|
||||||
critical.screen_type,
|
|
||||||
critical.mouse,
|
|
||||||
critical.preedit,
|
critical.preedit,
|
||||||
critical.cursor_style,
|
critical.cursor_style,
|
||||||
&critical.color_palette,
|
&critical.color_palette,
|
||||||
|
|
@ -2098,7 +2020,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
if (bg_image_config_changed) self.updateBgImageBuffer();
|
if (bg_image_config_changed) self.updateBgImageBuffer();
|
||||||
|
|
||||||
// Reset our viewport to force a rebuild, in case of a font change.
|
// 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;
|
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
|
/// 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
|
/// are then synced to the GPU in the next frame. This only updates CPU
|
||||||
/// memory and doesn't touch the GPU.
|
/// memory and doesn't touch the GPU.
|
||||||
fn rebuildCells(
|
fn rebuildCells(
|
||||||
self: *Self,
|
self: *Self,
|
||||||
wants_rebuild: bool,
|
|
||||||
screen: *terminal.Screen,
|
|
||||||
screen_type: terminal.ScreenSet.Key,
|
|
||||||
mouse: renderer.State.Mouse,
|
|
||||||
preedit: ?renderer.State.Preedit,
|
preedit: ?renderer.State.Preedit,
|
||||||
cursor_style_: ?renderer.CursorStyle,
|
cursor_style_: ?renderer.CursorStyle,
|
||||||
color_palette: *const terminal.color.Palette,
|
color_palette: *const terminal.color.Palette,
|
||||||
|
|
@ -2415,6 +2253,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
foreground: terminal.color.RGB,
|
foreground: terminal.color.RGB,
|
||||||
terminal_cursor_color: ?terminal.color.RGB,
|
terminal_cursor_color: ?terminal.color.RGB,
|
||||||
) !void {
|
) !void {
|
||||||
|
const state: *terminal.RenderState = &self.terminal_state;
|
||||||
|
defer state.redraw = false;
|
||||||
|
|
||||||
self.draw_mutex.lock();
|
self.draw_mutex.lock();
|
||||||
defer self.draw_mutex.unlock();
|
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});
|
// 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
|
// TODO: renderstate
|
||||||
|
|
||||||
// Create an arena for all our temporary allocations while rebuilding
|
|
||||||
var arena = ArenaAllocator.init(self.alloc);
|
|
||||||
defer arena.deinit();
|
|
||||||
const arena_alloc = arena.allocator();
|
|
||||||
|
|
||||||
// Create our match set for the links.
|
// Create our match set for the links.
|
||||||
var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
|
// var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
|
||||||
arena_alloc,
|
// arena_alloc,
|
||||||
screen,
|
// screen,
|
||||||
mouse_pt,
|
// mouse_pt,
|
||||||
mouse.mods,
|
// mouse.mods,
|
||||||
) else .{};
|
// ) else .{};
|
||||||
|
|
||||||
// Determine our x/y range for preedit. We don't want to render anything
|
// Determine our x/y range for preedit. We don't want to render anything
|
||||||
// here because we will render the preedit separately.
|
// here because we will render the preedit separately.
|
||||||
|
|
@ -2448,22 +2283,31 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
x: [2]terminal.size.CellCountInt,
|
x: [2]terminal.size.CellCountInt,
|
||||||
cp_offset: usize,
|
cp_offset: usize,
|
||||||
} = if (preedit) |preedit_v| preedit: {
|
} = 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 .{
|
break :preedit .{
|
||||||
.y = screen.cursor.y,
|
.y = @intCast(cursor_vp.y),
|
||||||
.x = .{ range.start, range.end },
|
.x = .{ range.start, range.end },
|
||||||
.cp_offset = range.cp_offset,
|
.cp_offset = range.cp_offset,
|
||||||
};
|
};
|
||||||
} else null;
|
} else null;
|
||||||
|
|
||||||
const grid_size_diff =
|
const grid_size_diff =
|
||||||
self.cells.size.rows != screen.pages.rows or
|
self.cells.size.rows != state.rows or
|
||||||
self.cells.size.columns != screen.pages.cols;
|
self.cells.size.columns != state.cols;
|
||||||
|
|
||||||
if (grid_size_diff) {
|
if (grid_size_diff) {
|
||||||
var new_size = self.cells.size;
|
var new_size = self.cells.size;
|
||||||
new_size.rows = screen.pages.rows;
|
new_size.rows = state.rows;
|
||||||
new_size.columns = screen.pages.cols;
|
new_size.columns = state.cols;
|
||||||
try self.cells.resize(self.alloc, new_size);
|
try self.cells.resize(self.alloc, new_size);
|
||||||
|
|
||||||
// Update our uniforms accordingly, otherwise
|
// 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 };
|
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 (rebuild) {
|
||||||
// If we are doing a full rebuild, then we clear the entire cell buffer.
|
// If we are doing a full rebuild, then we clear the entire cell buffer.
|
||||||
self.cells.reset();
|
self.cells.reset();
|
||||||
|
|
@ -2494,76 +2337,82 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We rebuild the cells row-by-row because we
|
// Get our row data from our state
|
||||||
// do font shaping and dirty tracking by row.
|
const row_data = state.row_data.slice();
|
||||||
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
|
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,
|
// If our cell contents buffer is shorter than the screen viewport,
|
||||||
// we render the rows that fit, starting from the bottom. If instead
|
// we render the rows that fit, starting from the bottom. If instead
|
||||||
// the viewport is shorter than the cell contents buffer, we align
|
// the viewport is shorter than the cell contents buffer, we align
|
||||||
// the top of the viewport with the top of the contents buffer.
|
// the top of the viewport with the top of the contents buffer.
|
||||||
var y: terminal.size.CellCountInt = @min(
|
const row_len: usize = @min(
|
||||||
screen.pages.rows,
|
state.rows,
|
||||||
self.cells.size.rows,
|
self.cells.size.rows,
|
||||||
);
|
);
|
||||||
while (row_it.next()) |row| {
|
for (
|
||||||
// The viewport may have more rows than our cell contents,
|
0..,
|
||||||
// so we need to break from the loop early if we hit y = 0.
|
row_cells[0..row_len],
|
||||||
if (y == 0) break;
|
row_dirty[0..row_len],
|
||||||
|
row_selection[0..row_len],
|
||||||
y -= 1;
|
) |y_usize, *cells, *dirty, selection| {
|
||||||
|
const y: terminal.size.CellCountInt = @intCast(y_usize);
|
||||||
|
|
||||||
if (!rebuild) {
|
if (!rebuild) {
|
||||||
// Only rebuild if we are doing a full rebuild or this row is dirty.
|
// 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
|
// Clear the cells if the row is dirty
|
||||||
self.cells.clear(y);
|
self.cells.clear(y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// True if we want to do font shaping around the cursor.
|
// Unmark the dirty state in our render state.
|
||||||
// We want to do font shaping as long as the cursor is enabled.
|
dirty.* = false;
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// TODO: renderstate
|
||||||
// On primary screen, we still apply vertical padding
|
// On primary screen, we still apply vertical padding
|
||||||
// extension under certain conditions we feel are safe.
|
// extension under certain conditions we feel are safe.
|
||||||
//
|
//
|
||||||
// This helps make some scenarios look better while
|
// This helps make some scenarios look better while
|
||||||
// avoiding scenarios we know do NOT look good.
|
// avoiding scenarios we know do NOT look good.
|
||||||
switch (self.config.padding_color) {
|
// switch (self.config.padding_color) {
|
||||||
// These already have the correct values set above.
|
// // These already have the correct values set above.
|
||||||
.background, .@"extend-always" => {},
|
// .background, .@"extend-always" => {},
|
||||||
|
//
|
||||||
// Apply heuristics for padding extension.
|
// // Apply heuristics for padding extension.
|
||||||
.extend => if (y == 0) {
|
// .extend => if (y == 0) {
|
||||||
self.uniforms.padding_extend.up = !row.neverExtendBg(
|
// self.uniforms.padding_extend.up = !row.neverExtendBg(
|
||||||
color_palette,
|
// color_palette,
|
||||||
background,
|
// background,
|
||||||
);
|
// );
|
||||||
} else if (y == self.cells.size.rows - 1) {
|
// } else if (y == self.cells.size.rows - 1) {
|
||||||
self.uniforms.padding_extend.down = !row.neverExtendBg(
|
// self.uniforms.padding_extend.down = !row.neverExtendBg(
|
||||||
color_palette,
|
// color_palette,
|
||||||
background,
|
// background,
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Iterator of runs for shaping.
|
// Iterator of runs for shaping.
|
||||||
|
const cells_slice = cells.slice();
|
||||||
var run_iter_opts: font.shape.RunOptions = .{
|
var run_iter_opts: font.shape.RunOptions = .{
|
||||||
.grid = self.font_grid,
|
.grid = self.font_grid,
|
||||||
.screen = screen,
|
.cells = cells_slice,
|
||||||
.row = row,
|
.selection2 = if (selection) |s| s else null,
|
||||||
.selection = row_selection,
|
|
||||||
.cursor_x = if (shape_cursor) screen.cursor.x 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);
|
run_iter_opts.applyBreakConfig(self.config.font_shaping_break);
|
||||||
var run_iter = self.font_shaper.runIterator(run_iter_opts);
|
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: ?[]const font.shape.Cell = null;
|
||||||
var shaper_cells_i: usize = 0;
|
var shaper_cells_i: usize = 0;
|
||||||
|
|
||||||
const row_cells_all = row.cells(.all);
|
|
||||||
|
|
||||||
// If our viewport is wider than our cell contents buffer,
|
// If our viewport is wider than our cell contents buffer,
|
||||||
// we still only process cells up to the width of the 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)];
|
const cells_len = @min(cells_slice.len, self.cells.size.columns);
|
||||||
|
const cells_raw = cells_slice.items(.raw);
|
||||||
for (row_cells, 0..) |*cell, x| {
|
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
|
// If this cell falls within our preedit range then we
|
||||||
// skip this because preedits are setup separately.
|
// skip this because preedits are setup separately.
|
||||||
if (preedit_range) |range| preedit: {
|
if (preedit_range) |range| preedit: {
|
||||||
|
|
@ -2610,7 +2462,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
self.font_shaper_cache.get(run) orelse
|
self.font_shaper_cache.get(run) orelse
|
||||||
cache: {
|
cache: {
|
||||||
// Otherwise we have to shape them.
|
// 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
|
// Try to cache them. If caching fails for any reason we
|
||||||
// continue because it is just a performance optimization,
|
// 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.font_shaper_cache.put(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
run,
|
run,
|
||||||
cells,
|
new_cells,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.warn(
|
log.warn(
|
||||||
"error caching font shaping results err={}",
|
"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
|
// The cells we get from direct shaping are always owned
|
||||||
// by the shaper and valid until the next shaping call so
|
// by the shaper and valid until the next shaping call so
|
||||||
// we can safely use them.
|
// we can safely use them.
|
||||||
break :cache cells;
|
break :cache new_cells;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cells = shaper_cells.?;
|
|
||||||
|
|
||||||
// Advance our index until we reach or pass
|
// Advance our index until we reach or pass
|
||||||
// our current x position in the shaper cells.
|
// 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;
|
shaper_cells_i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wide = cell.wide;
|
const wide = cell.wide;
|
||||||
|
const style: terminal.Style = if (cell.hasStyling())
|
||||||
const style = row.style(cell);
|
managed_style.*
|
||||||
|
else
|
||||||
const cell_pin: terminal.Pin = cell: {
|
.{};
|
||||||
var copy = row;
|
|
||||||
copy.x = @intCast(x);
|
|
||||||
break :cell copy;
|
|
||||||
};
|
|
||||||
|
|
||||||
// True if this cell is selected
|
// True if this cell is selected
|
||||||
const selected: bool = if (screen.selection) |sel|
|
const selected: bool = selected: {
|
||||||
sel.contains(screen, .{
|
const sel = selection orelse break :selected false;
|
||||||
.node = row.node,
|
const x_compare = if (wide == .spacer_tail)
|
||||||
.y = row.y,
|
x -| 1
|
||||||
.x = @intCast(
|
else
|
||||||
// Spacer tails should show the selection
|
x;
|
||||||
// state of the wide cell they belong to.
|
|
||||||
if (wide == .spacer_tail)
|
break :selected x_compare >= sel[0] and
|
||||||
x -| 1
|
x_compare <= sel[1];
|
||||||
else
|
};
|
||||||
x,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
else
|
|
||||||
false;
|
|
||||||
|
|
||||||
// The `_style` suffixed values are the colors based on
|
// The `_style` suffixed values are the colors based on
|
||||||
// the cell style (SGR), before applying any additional
|
// the cell style (SGR), before applying any additional
|
||||||
// configuration, inversions, selections, etc.
|
// 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(.{
|
const fg_style = style.fg(.{
|
||||||
.default = foreground,
|
.default = foreground,
|
||||||
.palette = color_palette,
|
.palette = color_palette,
|
||||||
|
|
@ -2793,16 +2638,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: renderstate
|
||||||
// Give links a single underline, unless they already have
|
// Give links a single underline, unless they already have
|
||||||
// an underline, in which case use a double underline to
|
// an underline, in which case use a double underline to
|
||||||
// distinguish them.
|
// distinguish them.
|
||||||
const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
|
// const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
|
||||||
if (style.flags.underline == .single)
|
// if (style.flags.underline == .single)
|
||||||
.double
|
// .double
|
||||||
else
|
// else
|
||||||
.single
|
// .single
|
||||||
else
|
// else
|
||||||
style.flags.underline;
|
// style.flags.underline;
|
||||||
|
const underline = style.flags.underline;
|
||||||
|
|
||||||
// We draw underlines first so that they layer underneath text.
|
// We draw underlines first so that they layer underneath text.
|
||||||
// This improves readability when a colored underline is used
|
// 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
|
self.font_shaper_cache.get(run) orelse
|
||||||
cache: {
|
cache: {
|
||||||
// Otherwise we have to shape them.
|
// 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
|
// Try to cache them. If caching fails for any reason we
|
||||||
// continue because it is just a performance optimization,
|
// 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.font_shaper_cache.put(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
run,
|
run,
|
||||||
cells,
|
new_cells,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.warn(
|
log.warn(
|
||||||
"error caching font shaping results err={}",
|
"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
|
// The cells we get from direct shaping are always owned
|
||||||
// by the shaper and valid until the next shaping call so
|
// by the shaper and valid until the next shaping call so
|
||||||
// we can safely use them.
|
// 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.
|
// If there are no shaper cells for this run, ignore it.
|
||||||
// This can occur for runs of empty cells, and is fine.
|
// 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
|
// If we encounter a shaper cell to the left of the current
|
||||||
// cell then we have some problems. This logic relies on x
|
// cell then we have some problems. This logic relies on x
|
||||||
// position monotonically increasing.
|
// 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
|
// NOTE: An assumption is made here that a single cell will never
|
||||||
// be present in more than one shaper run. If that assumption is
|
// be present in more than one shaper run. If that assumption is
|
||||||
// violated, this logic breaks.
|
// 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;
|
shaper_cells_i += 1;
|
||||||
}) {
|
}) {
|
||||||
self.addGlyph(
|
self.addGlyph(
|
||||||
@intCast(x),
|
@intCast(x),
|
||||||
@intCast(y),
|
@intCast(y),
|
||||||
cell_pin,
|
state.cols,
|
||||||
cells[shaper_cells_i],
|
cells_raw,
|
||||||
|
shaped_cells[shaper_cells_i],
|
||||||
shaper_run.?,
|
shaper_run.?,
|
||||||
fg,
|
fg,
|
||||||
alpha,
|
alpha,
|
||||||
|
|
@ -2938,14 +2787,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
inline .@"cell-foreground",
|
inline .@"cell-foreground",
|
||||||
.@"cell-background",
|
.@"cell-background",
|
||||||
=> |_, tag| {
|
=> |_, tag| {
|
||||||
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
|
const sty: terminal.Style = state.cursor.style;
|
||||||
const fg_style = sty.fg(.{
|
const fg_style = sty.fg(.{
|
||||||
.default = foreground,
|
.default = foreground,
|
||||||
.palette = color_palette,
|
.palette = color_palette,
|
||||||
.bold = self.config.bold_color,
|
.bold = self.config.bold_color,
|
||||||
});
|
});
|
||||||
const bg_style = sty.bg(
|
const bg_style = sty.bg(
|
||||||
screen.cursor.page_cell,
|
&state.cursor.cell,
|
||||||
color_palette,
|
color_palette,
|
||||||
) orelse background;
|
) orelse background;
|
||||||
|
|
||||||
|
|
@ -2960,21 +2809,27 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
break :cursor_color foreground;
|
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 the cursor is visible then we set our uniforms.
|
||||||
if (style == .block and screen.viewportIsBottom()) {
|
if (style == .block) cursor_uniforms: {
|
||||||
const wide = screen.cursor.page_cell.wide;
|
const cursor_vp = state.cursor.viewport orelse
|
||||||
|
break :cursor_uniforms;
|
||||||
|
const wide = state.cursor.cell.wide;
|
||||||
|
|
||||||
self.uniforms.cursor_pos = .{
|
self.uniforms.cursor_pos = .{
|
||||||
// If we are a spacer tail of a wide cell, our cursor needs
|
// 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
|
// to move back one cell. The saturate is to ensure we don't
|
||||||
// overflow but this shouldn't happen with well-formed input.
|
// overflow but this shouldn't happen with well-formed input.
|
||||||
switch (wide) {
|
switch (wide) {
|
||||||
.narrow, .spacer_head, .wide => screen.cursor.x,
|
.narrow, .spacer_head, .wide => cursor_vp.x,
|
||||||
.spacer_tail => screen.cursor.x -| 1,
|
.spacer_tail => cursor_vp.x -| 1,
|
||||||
},
|
},
|
||||||
screen.cursor.y,
|
@intCast(cursor_vp.y),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.uniforms.bools.cursor_wide = switch (wide) {
|
self.uniforms.bools.cursor_wide = switch (wide) {
|
||||||
|
|
@ -2990,14 +2845,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
break :blk txt.color.toTerminalRGB();
|
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(.{
|
const fg_style = sty.fg(.{
|
||||||
.default = foreground,
|
.default = foreground,
|
||||||
.palette = color_palette,
|
.palette = color_palette,
|
||||||
.bold = self.config.bold_color,
|
.bold = self.config.bold_color,
|
||||||
});
|
});
|
||||||
const bg_style = sty.bg(
|
const bg_style = sty.bg(
|
||||||
screen.cursor.page_cell,
|
&state.cursor.cell,
|
||||||
color_palette,
|
color_palette,
|
||||||
) orelse background;
|
) orelse background;
|
||||||
|
|
||||||
|
|
@ -3157,15 +3012,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
self: *Self,
|
self: *Self,
|
||||||
x: terminal.size.CellCountInt,
|
x: terminal.size.CellCountInt,
|
||||||
y: 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_cell: font.shape.Cell,
|
||||||
shaper_run: font.shape.TextRun,
|
shaper_run: font.shape.TextRun,
|
||||||
color: terminal.color.RGB,
|
color: terminal.color.RGB,
|
||||||
alpha: u8,
|
alpha: u8,
|
||||||
) !void {
|
) !void {
|
||||||
const rac = cell_pin.rowAndCell();
|
const cell = cell_raws[x];
|
||||||
const cell = rac.cell;
|
|
||||||
|
|
||||||
const cp = cell.codepoint();
|
const cp = cell.codepoint();
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
|
|
@ -3185,7 +3039,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
if (cellpkg.isSymbol(cp)) .{
|
if (cellpkg.isSymbol(cp)) .{
|
||||||
.size = .fit,
|
.size = .fit,
|
||||||
} else .none,
|
} 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(
|
fn addCursor(
|
||||||
self: *Self,
|
self: *Self,
|
||||||
screen: *terminal.Screen,
|
cursor_state: *const terminal.RenderState.Cursor,
|
||||||
cursor_style: renderer.CursorStyle,
|
cursor_style: renderer.CursorStyle,
|
||||||
cursor_color: terminal.color.RGB,
|
cursor_color: terminal.color.RGB,
|
||||||
) void {
|
) void {
|
||||||
|
const cursor_vp = cursor_state.viewport orelse return;
|
||||||
|
|
||||||
// Add the cursor. We render the cursor over the wide character if
|
// Add the cursor. We render the cursor over the wide character if
|
||||||
// we're on the wide character tail.
|
// we're on the wide character tail.
|
||||||
const wide, const x = cell: {
|
const wide, const x = cell: {
|
||||||
// The cursor goes over the screen cursor position.
|
// The cursor goes over the screen cursor position.
|
||||||
const cell = screen.cursor.page_cell;
|
if (!cursor_vp.wide_tail) break :cell .{
|
||||||
if (cell.wide != .spacer_tail or screen.cursor.x == 0)
|
cursor_state.cell.wide == .wide,
|
||||||
break :cell .{ cell.wide == .wide, screen.cursor.x };
|
cursor_vp.x,
|
||||||
|
};
|
||||||
|
|
||||||
// If we're part of a wide character, we move the cursor back to
|
// If we're part of a wide character, we move the cursor back
|
||||||
// the actual character.
|
// to the actual character.
|
||||||
const prev_cell = screen.cursorCellLeft(1);
|
break :cell .{ true, cursor_vp.x - 1 };
|
||||||
break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const alpha: u8 = if (!self.focused) 255 else alpha: {
|
const alpha: u8 = if (!self.focused) 255 else alpha: {
|
||||||
|
|
@ -3288,7 +3148,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
self.cells.setCursor(.{
|
self.cells.setCursor(.{
|
||||||
.atlas = .grayscale,
|
.atlas = .grayscale,
|
||||||
.bools = .{ .is_cursor_glyph = true },
|
.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 },
|
.color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
|
||||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const fastmem = @import("../fastmem.zig");
|
||||||
const point = @import("point.zig");
|
const point = @import("point.zig");
|
||||||
const size = @import("size.zig");
|
const size = @import("size.zig");
|
||||||
const page = @import("page.zig");
|
const page = @import("page.zig");
|
||||||
|
const Pin = @import("PageList.zig").Pin;
|
||||||
const Screen = @import("Screen.zig");
|
const Screen = @import("Screen.zig");
|
||||||
const ScreenSet = @import("ScreenSet.zig");
|
const ScreenSet = @import("ScreenSet.zig");
|
||||||
const Style = @import("style.zig").Style;
|
const Style = @import("style.zig").Style;
|
||||||
|
|
@ -68,6 +69,11 @@ pub const RenderState = struct {
|
||||||
/// to detect changes.
|
/// to detect changes.
|
||||||
screen: ScreenSet.Key,
|
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.
|
/// Initial state.
|
||||||
pub const empty: RenderState = .{
|
pub const empty: RenderState = .{
|
||||||
.rows = 0,
|
.rows = 0,
|
||||||
|
|
@ -90,7 +96,7 @@ pub const RenderState = struct {
|
||||||
|
|
||||||
/// The x/y position of the cursor within the viewport. This
|
/// The x/y position of the cursor within the viewport. This
|
||||||
/// may be null if the cursor is not visible within the viewport.
|
/// 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
|
/// The cell data for the cursor position. Managed memory is not
|
||||||
/// safe to access from this.
|
/// safe to access from this.
|
||||||
|
|
@ -98,6 +104,17 @@ pub const RenderState = struct {
|
||||||
|
|
||||||
/// The style, always valid even if the cell is default style.
|
/// The style, always valid even if the cell is default style.
|
||||||
style: 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.
|
/// A row within the viewport.
|
||||||
|
|
@ -118,7 +135,7 @@ pub const RenderState = struct {
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
|
|
||||||
/// The x range of the selection within this row.
|
/// The x range of the selection within this row.
|
||||||
selection: [2]size.CellCountInt,
|
selection: ?[2]size.CellCountInt,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Cell = struct {
|
pub const Cell = struct {
|
||||||
|
|
@ -159,6 +176,7 @@ pub const RenderState = struct {
|
||||||
t: *Terminal,
|
t: *Terminal,
|
||||||
) Allocator.Error!void {
|
) Allocator.Error!void {
|
||||||
const s: *Screen = t.screens.active;
|
const s: *Screen = t.screens.active;
|
||||||
|
const viewport_pin = s.pages.getTopLeft(.viewport);
|
||||||
const redraw = redraw: {
|
const redraw = redraw: {
|
||||||
// If our screen key changed, we need to do a full rebuild
|
// If our screen key changed, we need to do a full rebuild
|
||||||
// because our render state is viewport-specific.
|
// because our render state is viewport-specific.
|
||||||
|
|
@ -187,6 +205,11 @@ pub const RenderState = struct {
|
||||||
break :redraw true;
|
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;
|
break :redraw false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -203,6 +226,7 @@ pub const RenderState = struct {
|
||||||
self.rows = s.pages.rows;
|
self.rows = s.pages.rows;
|
||||||
self.cols = s.pages.cols;
|
self.cols = s.pages.cols;
|
||||||
self.viewport_is_bottom = s.viewportIsBottom();
|
self.viewport_is_bottom = s.viewportIsBottom();
|
||||||
|
self.viewport_pin = viewport_pin;
|
||||||
self.cursor.active = .{ .x = s.cursor.x, .y = s.cursor.y };
|
self.cursor.active = .{ .x = s.cursor.x, .y = s.cursor.y };
|
||||||
self.cursor.cell = s.cursor.page_cell.*;
|
self.cursor.cell = s.cursor.page_cell.*;
|
||||||
self.cursor.style = s.cursor.page_pin.style(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 = .{},
|
.arena = .{},
|
||||||
.cells = .empty,
|
.cells = .empty,
|
||||||
.dirty = true,
|
.dirty = true,
|
||||||
.selection = .{ 0, 0 },
|
.selection = null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -272,11 +296,22 @@ pub const RenderState = struct {
|
||||||
self.cursor.viewport = .{
|
self.cursor.viewport = .{
|
||||||
.y = y,
|
.y = y,
|
||||||
.x = s.cursor.x,
|
.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 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
|
// Promote our arena. State is copied by value so we need to
|
||||||
// restore it on all exit paths so we don't leak memory.
|
// restore it on all exit paths so we don't leak memory.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue