Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
13 kB · 294 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295// Host contract:// - fields: entries, transcript, layout, cursor_row, cursor_col, viewport_top_row,// owned_top_row, pending_scroll_compact,// min_visible_viewport_rows, reflow_clear_guard_rows, viewport_clear_pending,// resize_history_row_delta,// has_painted_transcript,// last_viewport_selection, last_visible_transcript_*, last_paint_bottom_reserved_rows,// replaceable_row, extra_input_rows, footer_reserved_base_rows// - methods: invalidateTranscriptAnchor, recordFrameInvalidation// This module owns viewport anchoring and footer-gap coordination helpers.
const std = @import("std");const debug_trace = @import("../../core/shared/debug_trace.zig");const types = @import("../../core/shared/types.zig");const render_engine = @import("../render_engine.zig");
const Allocator = std.mem.Allocator;const Metrics = types.Metrics;const transcript_blocks = render_engine.transcript_blocks;const viewport_selection = render_engine.viewport_selection;
const TranscriptBlockKind = transcript_blocks.TranscriptBlockKind;const ViewportSelection = viewport_selection.ViewportSelection;const blockKindForRawClass = transcript_blocks.blockKindForRawClass;const compactTailVisibleBlockKind = transcript_blocks.compactTailVisibleBlockKind;const default_block_gap_policy = transcript_blocks.default_block_gap_policy;const rowForLineIndex = viewport_selection.rowForLineIndex;const trimBlockTail = transcript_blocks.trimBlockTail;
fn requestTranscriptPaint(self: anytype) void { const Runtime = @TypeOf(self.*); if (comptime @hasDecl(Runtime, "markTranscriptDirty")) { self.markTranscriptDirty(); } else { self.transcript_band_dirty = true; }}
fn idleFooterGapAppliesTo(kind: ?TranscriptBlockKind) bool { return transcript_blocks.footerBoundaryGapRowsForTail(kind) > 0;}
pub fn initViewport(self: anytype, metrics: *Metrics, requested_start_row: u16) !void { try self.initViewportWithReservedRows(metrics, requested_start_row, 0);}
pub fn initViewportWithReservedRows(self: anytype, metrics: *Metrics, requested_start_row: u16, min_body_rows: u16) !void { _ = metrics; self.min_visible_viewport_rows = min_body_rows;
var start_row = requested_start_row; if (start_row < 1) start_row = 1; if (start_row > self.layout.rows) start_row = self.layout.rows;
const reserve_rows = @min(min_body_rows, self.layout.content_bottom); const max_start_row = @max(@as(u16, 1), if (reserve_rows > 0) self.layout.content_bottom - reserve_rows + 1 else self.layout.content_bottom); debug_trace.logf("render", "initViewport requested={d} clamped={d} reserve_rows={d} target_start={d} layout={d}x{d} content_bottom={d} has_painted={s}", .{ requested_start_row, start_row, reserve_rows, max_start_row, self.layout.cols, self.layout.rows, self.layout.content_bottom, if (self.has_painted_transcript) "true" else "false" }); if (start_row > max_start_row) { debug_trace.logf("render", "initViewport clamp_to_frame start={d} target={d}", .{ start_row, max_start_row }); start_row = max_start_row; }
self.cursor_row = start_row; self.cursor_col = 1; self.viewport_top_row = start_row; self.owned_top_row = start_row; self.pending_scroll_compact = false; self.invalidateTranscriptAnchor("init_viewport_frame_reanchor"); requestTranscriptPaint(self); recordViewportInvalidation(self, .viewport_reanchor, start_row, self.layout.rows); debug_trace.logf("frame_plan", "init_viewport_frame_deferred start={d} rows={d}", .{ start_row, self.layout.rows }); debug_trace.logf("render", "initViewport exit viewport_top={d} cursor={d},{d} has_painted={s}", .{ self.viewport_top_row, self.cursor_row, self.cursor_col, if (self.has_painted_transcript) "true" else "false" });}
pub fn reserveVisibleViewportRows(self: anytype, min_body_rows: u16) void { if (min_body_rows == 0 or self.layout.content_bottom == 0) return;
const reserve_rows = @min(min_body_rows, self.layout.content_bottom); if (reserve_rows == 0) return;
const desired_top = self.layout.content_bottom - reserve_rows + 1; const target_top = @max(desired_top, self.owned_top_row); if (self.viewport_top_row <= target_top) return;
const scroll_rows = self.viewport_top_row - target_top; self.reflow_clear_guard_rows = @max(self.reflow_clear_guard_rows, 1); self.viewport_clear_pending = true; debug_trace.logf("render", "viewport reserve_resize_reanchor rows={d} from={d} to={d} reserve_rows={d} owned_top={d} layout={d}x{d} content_bottom={d}", .{ scroll_rows, self.viewport_top_row, target_top, reserve_rows, self.owned_top_row, self.layout.cols, self.layout.rows, self.layout.content_bottom }); self.viewport_top_row = target_top; self.recordFrameInvalidation(.{ .reason = .viewport_reanchor, .top = target_top, .bottom = self.layout.rows, }) catch |err| { debug_trace.logf( "frame_plan", "viewport_reserve_invalidation_drop top={d} bottom={d} err={s}", .{ target_top, self.layout.rows, @errorName(err) }, ); };}
pub fn reanchorTop(self: anytype, alloc: Allocator, metrics: *Metrics) !void { _ = alloc; debug_trace.logf("render", "reanchorTop layout={d}x{d} content_bottom={d} viewport_top={d} transcript_bytes={d}", .{ self.layout.cols, self.layout.rows, self.layout.content_bottom, self.viewport_top_row, self.transcript.items.len }); metrics.full_redraws += 1; self.cursor_row = self.owned_top_row; self.cursor_col = 1; self.viewport_top_row = self.owned_top_row; self.pending_scroll_compact = false; self.invalidateTranscriptAnchor("reanchor_top_frame"); requestTranscriptPaint(self); recordViewportInvalidation(self, .viewport_reanchor, self.owned_top_row, self.layout.rows); debug_trace.logf("frame_plan", "reanchor_top_runtime_frame_deferred owned_top={d} rows={d}", .{ self.owned_top_row, self.layout.rows });}
fn recordViewportInvalidation( self: anytype, reason: render_engine.paint_plan.FrameInvalidationReason, top: u16, bottom: u16,) void { if (top == 0 or bottom == 0 or bottom < top) return; const Shell = @TypeOf(self.*); if (comptime @hasDecl(Shell, "recordFrameInvalidation")) { self.recordFrameInvalidation(.{ .reason = reason, .top = top, .bottom = bottom }) catch |err| { debug_trace.logf( "frame_plan", "viewport_runtime_invalidation_drop reason={s} top={d} bottom={d} err={s}", .{ @tagName(reason), top, bottom, @errorName(err) }, ); }; }}
pub fn rowForEntryId(self: anytype, entry_id: u32) ?u16 { const selection = self.last_viewport_selection orelse ViewportSelection{ .top_row = self.last_visible_transcript_top_row, .bottom_row = self.layout.content_bottom, .start_line = self.last_visible_transcript_start_line, .partial_skip_rows = self.last_visible_transcript_partial_skip_rows, .line_count = 0, .last_visible_row = self.last_visible_transcript_last_row, .last_visible_row_blank = self.last_visible_transcript_last_row_blank, .tail_kind = self.last_visible_transcript_tail_kind, .replaceable_start_row = self.replaceable_row, .split_active = self.last_visible_transcript_split_active, .split_prefix_lines = self.last_visible_transcript_split_prefix_lines, .split_suffix_start_line = self.last_visible_transcript_split_suffix_start_line, }; return rowForEntryIdInSelection( self, std.heap.c_allocator, entry_id, selection, ) catch null;}
pub fn rowForEntryIdInSelection( self: anytype, alloc: Allocator, entry_id: u32, selection: ViewportSelection,) !?u16 { const entry_line = try transcript_blocks.renderedEntryStartLine( alloc, self.entries.items, entry_id, self.layout.cols, self.command_output_render.styles, ) orelse return null; return rowForLineIndex(.{ .selection = selection, .bytes = self.transcript.items, .line_index = entry_line, .cols = self.layout.cols, .max_row = selection.bottom_row, });}
pub fn transientAssistantGapRows(self: anytype) u16 { var index = self.entries.items.len; while (index > 0) { index -= 1; const entry = self.entries.items[index]; if (!transcript_blocks.isEntryVisibleInCompactPresentation(entry)) continue; switch (entry) { .raw_bytes => |e| { if (trimBlockTail(e.bytes).len == 0) continue; const kind = blockKindForRawClass(e.class); if (kind == .tool_status) return default_block_gap_policy.default_gap_rows; return default_block_gap_policy.gapBetween(kind, .assistant_turn); }, .user_turn => return default_block_gap_policy.gapBetween(.user_turn, .assistant_turn), .assistant_turn => |e| { if (e.segments.text.items.len == 0) continue; return default_block_gap_policy.default_gap_rows; }, .assistant_table => return default_block_gap_policy.default_gap_rows, .assistant_code_block => return default_block_gap_policy.default_gap_rows, .assistant_thematic_rule => return default_block_gap_policy.default_gap_rows, .semantic_notice => |e| return default_block_gap_policy.gapBetween( transcript_blocks.blockKindForNoticeTone(e.tone), .assistant_turn, ), } } return 0;}
pub fn needsIdleFooterBottomReservation(self: anytype) bool { if (self.pending_scroll_compact) return false; if (self.last_paint_bottom_reserved_rows > 0) return false; if (self.last_visible_transcript_last_row == 0) return false; if (self.last_visible_transcript_last_row_blank) return false; if (!idleFooterGapAppliesTo(self.last_visible_transcript_tail_kind)) return false; return self.last_visible_transcript_last_row >= self.layout.content_bottom;}
pub fn idleFooterGapOffset(self: anytype) u16 { if (self.last_visible_transcript_last_row == 0) return 0; if (!idleFooterGapAppliesTo(self.last_visible_transcript_tail_kind)) return 0; return footerGapOffsetForVisibleTail(self);}
pub fn bannerFooterGapOffset(self: anytype) u16 { if (self.last_visible_transcript_last_row == 0) return 0; if (self.last_visible_transcript_tail_kind != .tool_status and !idleFooterGapAppliesTo(self.last_visible_transcript_tail_kind)) { return 0; } return footerGapOffsetForVisibleTail(self);}
fn footerGapOffsetForVisibleTail(self: anytype) u16 { const base_top = self.footerTopRowForExtra(self.extra_input_rows); if (self.last_visible_transcript_last_row_blank) { return if (@as(u32, base_top) <= @as(u32, self.last_visible_transcript_last_row)) 1 else 0; } const adjacent_top: u32 = @as(u32, self.last_visible_transcript_last_row) + 1; return if (@as(u32, base_top) <= adjacent_top) 1 else 0;}
pub fn preservedBottomReservationRows(self: anytype) u16 { if (self.last_paint_bottom_reserved_rows == 0) return 0; if (self.pending_scroll_compact) return 0; if (!idleFooterGapAppliesTo(compactTailVisibleBlockKind(self.entries.items))) return 0; return self.last_paint_bottom_reserved_rows;}
pub fn footerTopRowForExtra(self: anytype, extra: u16) u16 { const max_top: u16 = if (@as(u32, self.layout.rows) > @as(u32, self.footer_reserved_base_rows) + extra) @intCast(@as(u32, self.layout.rows) - @as(u32, self.footer_reserved_base_rows) - extra) else 1; const after_content: u16 = if (self.cursor_col > 1) @min(self.cursor_row +| 1, max_top) else self.cursor_row; return @min(after_content, max_top);}
test "footer top row uses committed reserved base rows when present" { const layout = types.Layout{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }; const Host = struct { layout: types.Layout, cursor_row: u16, cursor_col: u16, footer_reserved_base_rows: u16, };
var tint_host = Host{ .layout = layout, .cursor_row = 100, .cursor_col = 1, .footer_reserved_base_rows = 2, }; try std.testing.expectEqual(@as(u16, 22), footerTopRowForExtra(tint_host, 0));
tint_host.footer_reserved_base_rows = 3; try std.testing.expectEqual(@as(u16, 21), footerTopRowForExtra(tint_host, 0));}