Files
Kigi-CLI/crates/codegen/kigi-tui/src/views/overlay_list.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

224 lines
6.8 KiB
Rust

//! Shared prompt-area list overlay: accent bar, bold title, and a
//! scrollable single-line row list with a cursor.
//!
//! One source of truth for the row geometry shared by `/rewind`'s picker
//! phase and `/jump` across their render, hit-test, and height paths. Row
//! *content* stays with the caller (a closure); this owns chrome, cursor
//! styling, and the scroll window.
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use crate::theme::Theme;
/// Rows shown before the list scrolls (matches the historical picker cap).
const MAX_ROWS: usize = 15;
/// List geometry: row count + cursor position. Construct per call; all
/// methods derive the same scroll window from these two fields, so the
/// render, hit-test, and height paths cannot drift.
pub struct ListOverlay {
pub len: usize,
pub selected: usize,
}
/// Per-row style context handed to the row-content closure.
pub struct RowCtx {
pub is_cursor: bool,
/// Resolved row background (cursor rows get the visual-selection bg).
pub row_bg: Color,
pub content_width: u16,
}
impl ListOverlay {
/// Overlay height: title + rows (≤ [`MAX_ROWS`]), capped at 60% of the
/// screen, plus one padding row.
pub fn height(&self, screen_h: u16) -> u16 {
let rows = self.len.min(MAX_ROWS) as u16;
let h = 2 + rows;
let cap = (screen_h as u32 * 60 / 100).max(6) as u16;
h.min(cap) + 1
}
/// Rows that fit in `area` (title + padding excluded).
fn visible_rows(area: Rect) -> usize {
area.height.saturating_sub(3) as usize
}
/// First visible row index (keeps the cursor inside the window).
fn scroll_offset(&self, visible_rows: usize) -> usize {
if visible_rows > 0 && self.selected >= visible_rows {
self.selected - visible_rows + 1
} else {
0
}
}
/// Row index under a screen position, or `None` off the rows.
pub fn row_at(&self, area: Rect, col: u16, row: u16) -> Option<usize> {
if area.height == 0 || area.width < 10 {
return None;
}
if col < area.x || col >= area.x + area.width {
return None;
}
if row < area.y || row >= area.y + area.height {
return None;
}
let first = area.y + 2;
if row < first {
return None;
}
let visible_rows = Self::visible_rows(area);
let rel = (row - first) as usize;
if rel >= visible_rows {
return None;
}
let idx = self.scroll_offset(visible_rows) + rel;
(idx < self.len).then_some(idx)
}
/// Render the overlay: bg fill, accent bar, title, then the visible
/// window of rows. `row_line(idx, ctx)` produces each row's content;
/// cursor/row backgrounds are painted here. Applies the standard
/// unfocus dim, so callers must not blend again.
pub fn render(
&self,
buf: &mut Buffer,
area: Rect,
title: &str,
focused: bool,
mut row_line: impl FnMut(usize, &RowCtx) -> Line<'static>,
) {
if area.height == 0 || area.width < 10 {
return;
}
let theme = Theme::current();
let bg = theme.bg_light;
buf.set_style(area, Style::default().bg(bg));
let accent_style = Style::default().fg(theme.accent_user);
for row in area.y..area.y + area.height {
if let Some(cell) = buf.cell_mut((area.x, row)) {
cell.set_symbol(crate::glyphs::accent_bar());
cell.set_style(accent_style);
}
}
let content_x = area.x + 3;
let content_w = area.width.saturating_sub(5);
let title_style = Style::default()
.fg(theme.accent_user)
.add_modifier(Modifier::BOLD);
let mut y = area.y + 1;
buf.set_line(
content_x,
y,
&Line::from(Span::styled(title.to_string(), title_style)),
content_w,
);
y += 1;
let visible_rows = Self::visible_rows(area);
let scroll_offset = self.scroll_offset(visible_rows);
for i in (scroll_offset..self.len).take(visible_rows) {
if y >= area.y + area.height {
break;
}
let is_cursor = i == self.selected;
let row_bg = if is_cursor && focused {
theme.bg_visual
} else {
bg
};
let row_rect = Rect {
x: content_x.saturating_sub(1),
y,
width: content_w + 2,
height: 1,
};
buf.set_style(row_rect, Style::default().bg(row_bg));
let ctx = RowCtx {
is_cursor,
row_bg,
content_width: content_w,
};
let line = row_line(i, &ctx);
buf.set_line(content_x, y, &line, content_w);
y += 1;
}
// Unfocus dim: blend foregrounds toward the panel bg so the overlay
// recedes when the prompt area is unfocused (prompt_widget pattern).
if !focused {
crate::render::color::blend_area(buf, area, Some((bg, 0.66)), None);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn area() -> Rect {
Rect {
x: 0,
y: 0,
width: 40,
height: 10,
}
}
#[test]
fn row_at_maps_rows_and_rejects_chrome() {
let list = ListOverlay {
len: 3,
selected: 0,
};
// Title at y+1; rows start at y+2.
assert_eq!(list.row_at(area(), 5, 1), None);
assert_eq!(list.row_at(area(), 5, 2), Some(0));
assert_eq!(list.row_at(area(), 5, 4), Some(2));
// Past the last row.
assert_eq!(list.row_at(area(), 5, 5), None);
// Outside horizontally.
assert_eq!(list.row_at(area(), 99, 2), None);
}
#[test]
fn row_at_respects_scroll_window() {
// 20 rows, 7 visible (height 10 - 3), cursor at the end: the window
// starts at 13 so the cursor stays visible.
let list = ListOverlay {
len: 20,
selected: 19,
};
assert_eq!(list.row_at(area(), 5, 2), Some(13));
assert_eq!(list.row_at(area(), 5, 8), Some(19));
}
#[test]
fn height_caps_at_max_rows_and_screen_fraction() {
let two = ListOverlay {
len: 2,
selected: 0,
};
// title + 2 rows + padding
assert_eq!(two.height(40), 5);
let many = ListOverlay {
len: 30,
selected: 0,
};
// 15-row cap
assert_eq!(many.height(40), 18);
// 60% screen cap
assert_eq!(many.height(12), 8);
}
}