terminal: render state contains cursor state

pull/9662/head
Mitchell Hashimoto 2025-11-18 15:05:39 -10:00
parent 2d94cd6bbd
commit 9162e71bcc
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 50 additions and 1 deletions

View File

@ -88,7 +88,7 @@ pub const Dirty = packed struct {
/// The cursor position and style.
pub const Cursor = struct {
// The x/y position within the viewport.
// The x/y position within the active area.
x: size.CellCountInt = 0,
y: size.CellCountInt = 0,

View File

@ -3,6 +3,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const fastmem = @import("../fastmem.zig");
const point = @import("point.zig");
const size = @import("size.zig");
const page = @import("page.zig");
const Screen = @import("Screen.zig");
@ -10,6 +11,9 @@ const ScreenSet = @import("ScreenSet.zig");
const Style = @import("style.zig").Style;
const Terminal = @import("Terminal.zig");
// TODO:
// - tests for cursor state
// Developer note: this is in src/terminal and not src/renderer because
// the goal is that this remains generic to multiple renderers. This can
// aid specifically with libghostty-vt with converting terminal state to
@ -43,6 +47,9 @@ pub const RenderState = struct {
/// area and scrolling with new output.
viewport_is_bottom: bool,
/// Cursor state within the viewport.
cursor: Cursor,
/// The rows (y=0 is top) of the viewport. Guaranteed to be `rows` length.
///
/// This is a MultiArrayList because only the update cares about
@ -66,11 +73,33 @@ pub const RenderState = struct {
.rows = 0,
.cols = 0,
.viewport_is_bottom = false,
.cursor = .{
.active = .{ .x = 0, .y = 0 },
.viewport = null,
.cell = .{},
.style = undefined,
},
.row_data = .empty,
.redraw = false,
.screen = .primary,
};
pub const Cursor = struct {
/// The x/y position of the cursor within the active area.
active: point.Coordinate,
/// 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,
/// The cell data for the cursor position. Managed memory is not
/// safe to access from this.
cell: page.Cell,
/// The style, always valid even if the cell is default style.
style: Style,
};
/// A row within the viewport.
pub const Row = struct {
/// Arena used for any heap allocations for cell contents
@ -174,6 +203,14 @@ pub const RenderState = struct {
self.rows = s.pages.rows;
self.cols = s.pages.cols;
self.viewport_is_bottom = s.viewportIsBottom();
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);
// Always reset the cursor viewport position. In the future we can
// probably cache this by comparing the cursor pin and viewport pin
// but may not be worth it.
self.cursor.viewport = null;
// Ensure our row length is exactly our height, freeing or allocating
// data as necessary. In most cases we'll have a perfectly matching
@ -226,6 +263,18 @@ pub const RenderState = struct {
);
var y: size.CellCountInt = 0;
while (row_it.next()) |row_pin| : (y = y + 1) {
// Find our cursor if we haven't found it yet. We do this even
// if the row is not dirty because the cursor is unrelated.
if (self.cursor.viewport == null and
row_pin.node == s.cursor.page_pin.node and
row_pin.y == s.cursor.page_pin.y)
{
self.cursor.viewport = .{
.y = y,
.x = s.cursor.x,
};
}
// If the row isn't dirty then we assume it is unchanged.
if (!redraw and !row_pin.isDirty()) continue;