From df466f3c7310eadcfdcbc899d54aea9abec8f444 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 22 Nov 2025 14:19:25 -0800 Subject: [PATCH] renderer: make cursorStyle depend on RenderState This makes `cursorStyle` utilize `RenderState` to determine the appropriate cursor style. This moves the cursor style logic outside the critical area, although it was cheap to begin with. This always removes `viewport_is_bottom` which had no practical use. --- src/renderer/cursor.zig | 109 +++++++++++++++++++-------------------- src/renderer/generic.zig | 19 ++----- src/terminal/render.zig | 28 +++++++--- 3 files changed, 79 insertions(+), 77 deletions(-) diff --git a/src/renderer/cursor.zig b/src/renderer/cursor.zig index ee79ead29..bfa92f31d 100644 --- a/src/renderer/cursor.zig +++ b/src/renderer/cursor.zig @@ -1,6 +1,5 @@ const std = @import("std"); const terminal = @import("../terminal/main.zig"); -const State = @import("State.zig"); /// Available cursor styles for drawing that renderers must support. /// This is a superset of terminal cursor styles since the renderer supports @@ -26,64 +25,65 @@ pub const Style = enum { } }; +pub const StyleOptions = struct { + preedit: bool = false, + focused: bool = false, + blink_visible: bool = false, +}; + /// Returns the cursor style to use for the current render state or null /// if a cursor should not be rendered at all. pub fn style( - state: *State, - focused: bool, - blink_visible: bool, + state: *const terminal.RenderState, + opts: StyleOptions, ) ?Style { // Note the order of conditionals below is important. It represents // a priority system of how we determine what state overrides cursor // visibility and style. - // The cursor is only at the bottom of the viewport. If we aren't - // at the bottom, we never render the cursor. The cursor x/y is by - // viewport so if we are above the viewport, we'll end up rendering - // the cursor in some random part of the screen. - if (!state.terminal.screens.active.viewportIsBottom()) return null; + // The cursor must be visible in the viewport to be rendered. + if (state.cursor.viewport == null) return null; // If we are in preedit, then we always show the block cursor. We do // this even if the cursor is explicitly not visible because it shows // an important editing state to the user. - if (state.preedit != null) return .block; + if (opts.preedit) return .block; + + // If we're at a password input its always a lock. + if (state.cursor.password_input) return .lock; // If the cursor is explicitly not visible by terminal mode, we don't render. - if (!state.terminal.modes.get(.cursor_visible)) return null; + if (!state.cursor.visible) return null; // If we're not focused, our cursor is always visible so that // we can show the hollow box. - if (!focused) return .block_hollow; + if (!opts.focused) return .block_hollow; // If the cursor is blinking and our blink state is not visible, // then we don't show the cursor. - if (state.terminal.modes.get(.cursor_blinking) and !blink_visible) { - return null; - } + if (state.cursor.blinking and !opts.blink_visible) return null; // Otherwise, we use whatever style the terminal wants. - return .fromTerminal(state.terminal.screens.active.cursor.cursor_style); + return .fromTerminal(state.cursor.visual_style); } test "cursor: default uses configured style" { const testing = std.testing; const alloc = testing.allocator; - var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 }); + var term: terminal.Terminal = try .init(alloc, .{ .cols = 10, .rows = 10 }); defer term.deinit(alloc); term.screens.active.cursor.cursor_style = .bar; term.modes.set(.cursor_blinking, true); - var state: State = .{ - .mutex = undefined, - .terminal = &term, - .preedit = null, - }; + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &term); - try testing.expect(style(&state, true, true) == .bar); - try testing.expect(style(&state, false, true) == .block_hollow); - try testing.expect(style(&state, false, false) == .block_hollow); - try testing.expect(style(&state, true, false) == null); + try testing.expect(style(&state, .{ .preedit = false, .focused = true, .blink_visible = true }) == .bar); + try testing.expect(style(&state, .{ .preedit = false, .focused = false, .blink_visible = true }) == .block_hollow); + try testing.expect(style(&state, .{ .preedit = false, .focused = false, .blink_visible = false }) == .block_hollow); + try testing.expect(style(&state, .{ .preedit = false, .focused = true, .blink_visible = false }) == null); } test "cursor: blinking disabled" { @@ -95,16 +95,14 @@ test "cursor: blinking disabled" { term.screens.active.cursor.cursor_style = .bar; term.modes.set(.cursor_blinking, false); - var state: State = .{ - .mutex = undefined, - .terminal = &term, - .preedit = null, - }; + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &term); - try testing.expect(style(&state, true, true) == .bar); - try testing.expect(style(&state, true, false) == .bar); - try testing.expect(style(&state, false, true) == .block_hollow); - try testing.expect(style(&state, false, false) == .block_hollow); + try testing.expect(style(&state, .{ .focused = true, .blink_visible = true }) == .bar); + try testing.expect(style(&state, .{ .focused = true, .blink_visible = false }) == .bar); + try testing.expect(style(&state, .{ .focused = false, .blink_visible = true }) == .block_hollow); + try testing.expect(style(&state, .{ .focused = false, .blink_visible = false }) == .block_hollow); } test "cursor: explicitly not visible" { @@ -117,16 +115,14 @@ test "cursor: explicitly not visible" { term.modes.set(.cursor_visible, false); term.modes.set(.cursor_blinking, false); - var state: State = .{ - .mutex = undefined, - .terminal = &term, - .preedit = null, - }; + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &term); - try testing.expect(style(&state, true, true) == null); - try testing.expect(style(&state, true, false) == null); - try testing.expect(style(&state, false, true) == null); - try testing.expect(style(&state, false, false) == null); + try testing.expect(style(&state, .{ .focused = true, .blink_visible = true }) == null); + try testing.expect(style(&state, .{ .focused = true, .blink_visible = false }) == null); + try testing.expect(style(&state, .{ .focused = false, .blink_visible = true }) == null); + try testing.expect(style(&state, .{ .focused = false, .blink_visible = false }) == null); } test "cursor: always block with preedit" { @@ -135,25 +131,24 @@ test "cursor: always block with preedit" { var term = try terminal.Terminal.init(alloc, .{ .cols = 10, .rows = 10 }); defer term.deinit(alloc); - var state: State = .{ - .mutex = undefined, - .terminal = &term, - .preedit = .{}, - }; + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &term); // In any bool state - try testing.expect(style(&state, false, false) == .block); - try testing.expect(style(&state, true, false) == .block); - try testing.expect(style(&state, true, true) == .block); - try testing.expect(style(&state, false, true) == .block); + try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = false }) == .block); + try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = false }) == .block); + try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = true }) == .block); + try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = true }) == .block); // If we're scrolled though, then we don't show the cursor. for (0..100) |_| try term.index(); try term.scrollViewport(.{ .top = {} }); + try state.update(alloc, &term); // In any bool state - try testing.expect(style(&state, false, false) == null); - try testing.expect(style(&state, true, false) == null); - try testing.expect(style(&state, true, true) == null); - try testing.expect(style(&state, false, true) == null); + try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = false }) == null); + try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = false }) == null); + try testing.expect(style(&state, .{ .preedit = true, .focused = true, .blink_visible = true }) == null); + try testing.expect(style(&state, .{ .preedit = true, .focused = false, .blink_visible = true }) == null); } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 4478599a8..861625351 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1090,7 +1090,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { links: terminal.RenderState.CellSet, mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, - cursor_style: ?renderer.CursorStyle, scrollbar: terminal.Scrollbar, }; @@ -1122,19 +1121,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // cross-thread mailbox message within the IO path. const scrollbar = state.terminal.screens.active.pages.scrollbar(); - // Whether to draw our cursor or not. - const cursor_style = if (state.terminal.flags.password_input) - .lock - else - renderer.cursorStyle( - state, - self.focused, - cursor_blink_visible, - ); - // Get our preedit state const preedit: ?renderer.State.Preedit = preedit: { - if (cursor_style == null) break :preedit null; const p = state.preedit orelse break :preedit null; break :preedit try p.clone(arena_alloc); }; @@ -1175,7 +1163,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .links = links, .mouse = state.mouse, .preedit = preedit, - .cursor_style = cursor_style, .scrollbar = scrollbar, }; }; @@ -1195,7 +1182,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Build our GPU cells try self.rebuildCells( critical.preedit, - critical.cursor_style, + renderer.cursorStyle(&self.terminal_state, .{ + .preedit = critical.preedit != null, + .focused = self.focused, + .blink_visible = cursor_blink_visible, + }), &critical.links, ); diff --git a/src/terminal/render.zig b/src/terminal/render.zig index b19edf65d..86b299d72 100644 --- a/src/terminal/render.zig +++ b/src/terminal/render.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const fastmem = @import("../fastmem.zig"); const color = @import("color.zig"); +const cursor = @import("cursor.zig"); const point = @import("point.zig"); const size = @import("size.zig"); const page = @import("page.zig"); @@ -56,10 +57,6 @@ pub const RenderState = struct { rows: size.CellCountInt, cols: size.CellCountInt, - /// The viewport is at the bottom of the terminal, viewing the active - /// area and scrolling with new output. - viewport_is_bottom: bool, - /// The color state for the terminal. colors: Colors, @@ -96,7 +93,6 @@ pub const RenderState = struct { pub const empty: RenderState = .{ .rows = 0, .cols = 0, - .viewport_is_bottom = false, .colors = .{ .background = .{}, .foreground = .{}, @@ -108,6 +104,10 @@ pub const RenderState = struct { .viewport = null, .cell = .{}, .style = undefined, + .visual_style = .block, + .password_input = false, + .visible = true, + .blinking = false, }, .row_data = .empty, .dirty = .false, @@ -140,6 +140,19 @@ pub const RenderState = struct { /// The style, always valid even if the cell is default style. style: Style, + /// The visual style of the cursor itself, such as a block or + /// bar. + visual_style: cursor.Style, + + /// True if the cursor is detected to be at a password input field. + password_input: bool, + + /// Cursor visibility state determined by the terminal mode. + visible: bool, + + /// Cursor blink state determined by the terminal mode. + blinking: bool, + pub const Viewport = struct { /// The x/y position of the cursor within the viewport. x: size.CellCountInt, @@ -279,11 +292,14 @@ pub const RenderState = struct { // Always set our cheap fields, its more expensive to compare 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.style; + self.cursor.visual_style = s.cursor.cursor_style; + self.cursor.password_input = t.flags.password_input; + self.cursor.visible = t.modes.get(.cursor_visible); + self.cursor.blinking = t.modes.get(.cursor_blinking); // Always reset the cursor viewport position. In the future we can // probably cache this by comparing the cursor pin and viewport pin