mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-28 19:46:35 +00:00
Related to #111 This adds the necessary logic and data for the `PageList` data structure to keep track of **total length** of the screen, **offset** into the viewport, and **length** of the viewport. These three values are necessary to _render_ a scrollbar. This PR updates the renderer to grab this information but stops short of actually drawing a scrollbar (which we'll do with native UI), in the interest of having a PR that doesn't contain too many changes. **This doesn't yet draw a scrollbar, these are just the internal changes necessary to support it.** ## Background The `PageList` structure is very core to how we represent terminal state. It maintains a doubly linked list of "pages" (not literally virtual memory pages, but close). Each page stores cell information, styles, hyperlinks, etc fully self-contained in a contiguous sets of VM pages using offset addresses rather than full pointers. **Pages are not guaranteed to be equal sizes.** (This is where scrollbars get difficult) Because it is a linked list structure of non-equal sized nodes, it isn't amenable to typical scrollbar behavior. A scrollbar needs to know: full size, offset, and length in order to draw the scrollbar properly. Getting these values naively is `O(N)` within the data structure that is on the hottest IO performance path in all of Ghostty. ## Implementation ### PageList We now maintain two cached values for **total length** and **viewport offset**. The total length is relatively straightforward, we just have to be careful to update it in every operation that could add or remove rows. I've done this and ensured that every place we update it is covered with unit test coverage. The viewport offset is nasty, but I came up with what I believe is a good solution. The viewport when arbitrarily scrolled is defined as a direct pointer to the linked list node plus a row offset into that node. The only way to calculate offset from the top is `O(N)`. But we have a couple shortcuts: 1. If the viewport is at the bottom (most common) or top, calculating the offset is `O(1)`: bottom is `total_rows - active_rows`, both readily available. And top is `0` by definition. 2. Operations on the PageList typically add or remove rows. We don't do arbitrary linked list surgery. If we instrument those areas with delta updates to our cache, we can avoid the `O(N)` cost for most operations, including scrolling a scrollbar. The only expensive operation is a full, arbitrary jump (new node pointer). Point 1 was quick to implement, so I focused all the complexity on point 2. Whenever we have an operation that adds or removes rows (for example pruning the scroll back, adding more, erase rows within the active area, etc.) then I do the math to calculate the delta change required for the offset if we've already calculated it, and apply that directly. ### Renderer The other issue was how to notify the apprts of scrollbar state. Sending messages on any terminal change within the IO thread is a non-option because (1) sending messages is slow (2) the terminal changes a lot and (3) any slowness in the IO thread slows down overall terminal throughput. The solution was to **trigger scrollbar notifications with the renderer vsync**. We read the scrollbar information when we render a frame, compare it to renderer previous state, and if the scrollbar changed, send a message to the apprt _after the frame is GPU-renderer_. The renderer spends _most_ of its time sleeping compared to the IO thread, and has more opportunities for optimizing its awake time. Additionally, there's no reason to update the scrollbar information if the renderer hasn't rendered the new frames because the user can't even see the stuff the scrollbar wants to scroll to. We're talking about millisecond scale stuff here at worst but it adds up. ## Performance No noticeable performance impact for the additional metrics: <img width="1012" height="738" alt="image" src="https://github.com/user-attachments/assets/4ed0a3e8-6d76-40c1-b249-e34041c2f6fd" /> ## AI Usage I used Amp to help audit the codebase and write tests. I wrote all the main implementation code manually. I came up with the main design myself. Relevant threads: - https://ampcode.com/threads/T-95fff686-75bb-4553-a2fb-e41fe4cd4b77#message-0-block-0 - https://ampcode.com/threads/T-48e9a288-b280-4eec-83b7-ca73d029b4ef#message-91-block-0 ## Future This is just the internal changes necessary to _draw_ a scrollbar. There will be other changes we'll need to add to handle grabbing and actually jumping the scrollbar. I have a good idea of how to implement those performantly as well.
77 lines
2.8 KiB
Zig
77 lines
2.8 KiB
Zig
const builtin = @import("builtin");
|
|
|
|
const charsets = @import("charsets.zig");
|
|
const stream = @import("stream.zig");
|
|
const ansi = @import("ansi.zig");
|
|
const csi = @import("csi.zig");
|
|
const hyperlink = @import("hyperlink.zig");
|
|
const sgr = @import("sgr.zig");
|
|
const style = @import("style.zig");
|
|
pub const apc = @import("apc.zig");
|
|
pub const dcs = @import("dcs.zig");
|
|
pub const osc = @import("osc.zig");
|
|
pub const point = @import("point.zig");
|
|
pub const color = @import("color.zig");
|
|
pub const device_status = @import("device_status.zig");
|
|
pub const kitty = @import("kitty.zig");
|
|
pub const modes = @import("modes.zig");
|
|
pub const page = @import("page.zig");
|
|
pub const parse_table = @import("parse_table.zig");
|
|
pub const search = @import("search.zig");
|
|
pub const size = @import("size.zig");
|
|
pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {};
|
|
pub const x11_color = @import("x11_color.zig");
|
|
|
|
pub const Charset = charsets.Charset;
|
|
pub const CharsetSlot = charsets.Slots;
|
|
pub const CharsetActiveSlot = charsets.ActiveSlot;
|
|
pub const Cell = page.Cell;
|
|
pub const Coordinate = point.Coordinate;
|
|
pub const CSI = Parser.Action.CSI;
|
|
pub const DCS = Parser.Action.DCS;
|
|
pub const MouseShape = @import("mouse_shape.zig").MouseShape;
|
|
pub const Page = page.Page;
|
|
pub const PageList = @import("PageList.zig");
|
|
pub const Parser = @import("Parser.zig");
|
|
pub const Pin = PageList.Pin;
|
|
pub const Point = point.Point;
|
|
pub const Screen = @import("Screen.zig");
|
|
pub const ScreenType = Terminal.ScreenType;
|
|
pub const Scrollbar = PageList.Scrollbar;
|
|
pub const Selection = @import("Selection.zig");
|
|
pub const SizeReportStyle = csi.SizeReportStyle;
|
|
pub const StringMap = @import("StringMap.zig");
|
|
pub const Style = style.Style;
|
|
pub const Terminal = @import("Terminal.zig");
|
|
pub const Stream = stream.Stream;
|
|
pub const Cursor = Screen.Cursor;
|
|
pub const CursorStyle = Screen.CursorStyle;
|
|
pub const CursorStyleReq = ansi.CursorStyle;
|
|
pub const DeviceAttributeReq = ansi.DeviceAttributeReq;
|
|
pub const Mode = modes.Mode;
|
|
pub const ModePacked = modes.ModePacked;
|
|
pub const ModifyKeyFormat = ansi.ModifyKeyFormat;
|
|
pub const ProtectedMode = ansi.ProtectedMode;
|
|
pub const StatusLineType = ansi.StatusLineType;
|
|
pub const StatusDisplay = ansi.StatusDisplay;
|
|
pub const EraseDisplay = csi.EraseDisplay;
|
|
pub const EraseLine = csi.EraseLine;
|
|
pub const TabClear = csi.TabClear;
|
|
pub const Attribute = sgr.Attribute;
|
|
|
|
pub const Options = @import("build_options.zig").Options;
|
|
pub const options = @import("terminal_options");
|
|
|
|
/// This is set to true when we're building the C library.
|
|
pub const c_api = if (options.c_abi) @import("c/main.zig") else void;
|
|
|
|
test {
|
|
@import("std").testing.refAllDecls(@This());
|
|
|
|
// Internals
|
|
_ = @import("bitmap_allocator.zig");
|
|
_ = @import("hash_map.zig");
|
|
_ = @import("ref_counted_set.zig");
|
|
_ = @import("size.zig");
|
|
}
|