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
|
||||
/// 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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
const selected: bool = selected: {
|
||||
const sel = selection orelse break :selected false;
|
||||
const x_compare = if (wide == .spacer_tail)
|
||||
x -| 1
|
||||
else
|
||||
x,
|
||||
),
|
||||
})
|
||||
else
|
||||
false;
|
||||
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 },
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue