Files
Kigi-CLI/crates/codegen/kigi-tui/src/tips/render.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

175 lines
5.8 KiB
Rust

//! Tip renderer.
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Paragraph, Widget, Wrap},
};
use crate::render::SafeBuf;
use crate::theme::Theme;
/// Compute the number of rows a tip needs when rendered at the given `width`.
pub fn tip_height(width: u16, tip: &str) -> u16 {
if width == 0 {
return 0;
}
let line = tip_line(tip);
let line_width = line.width() as u16;
if line_width <= width {
1
} else {
// Ceiling division — word wrapping may use slightly more rows than
// a naive character split, but this is a close-enough upper bound.
(line_width as u32)
.div_ceil(width as u32)
.min(u16::MAX as u32) as u16
}
}
fn tip_line(tip: &str) -> Line<'_> {
let theme = Theme::current();
Line::from(vec![
Span::styled(
"Tip: ",
Style::default().fg(theme.gray).add_modifier(Modifier::BOLD),
),
Span::styled(tip, Style::default().fg(theme.gray)),
])
}
/// Render a tip into the provided area, word-wrapping if it exceeds the width.
pub fn render_tip(area: Rect, buf: &mut Buffer, tip: &str) {
if area.height == 0 {
return;
}
let theme = Theme::current();
Paragraph::new(tip_line(tip))
.style(Style::default().bg(theme.bg_base))
.wrap(Wrap { trim: false })
.render(area, buf);
}
/// Blank every cell of `area` (chars, colors, and modifiers) in `color`.
///
/// Modifiers MUST be reset here: ratatui's `Cell::set_style` only *merges*
/// modifiers (`insert(add)` / `remove(sub)`), so a later paint whose style
/// carries no `sub_modifier` inherits whatever BOLD/ITALIC/… an earlier
/// same-frame paint left behind (e.g. the welcome tip's bold `Tip: ` prefix
/// bleeding into the ephemeral tip as "**Queue**d · Enter to send now").
fn clear_rect(buf: &mut Buffer, area: Rect, color: Color) {
for row in 0..area.height {
for col in 0..area.width {
if let Some(cell) = buf.cell_mut((area.x + col, area.y + row)) {
cell.set_char(' ');
cell.fg = color;
cell.bg = color;
cell.modifier = Modifier::empty();
}
}
}
}
/// Render a pre-styled tip line into the banner rect. The whole rect is
/// cleared first (it can be taller than one row when a wrapped session tip
/// reserved it) and the line paints on the first row, truncated at width.
pub fn render_ephemeral_tip(area: Rect, buf: &mut Buffer, line: &Line<'static>) {
if area.height == 0 || area.width == 0 {
return;
}
let theme = Theme::current();
clear_rect(buf, area, theme.bg_base);
buf.set_line_safe(area.x, area.y, line, area.width);
}
#[cfg(test)]
mod tests {
use super::*;
fn row_text(buf: &Buffer, area: Rect, y: u16) -> String {
(0..area.width)
.map(|x| buf.cell((area.x + x, y)).expect("cell in area").symbol())
.collect()
}
#[test]
fn clears_full_rect_and_truncates_to_width() {
let area = Rect::new(0, 0, 8, 2);
let mut buf = Buffer::empty(area);
// Pre-dirty both rows to simulate stale banner content underneath.
buf.set_string(0, 0, "XXXXXXXX", Style::default());
buf.set_string(0, 1, "XXXXXXXX", Style::default());
// Wider than the rect.
let line = Line::from("0123456789");
render_ephemeral_tip(area, &mut buf, &line);
assert_eq!(row_text(&buf, area, 0), "01234567", "truncated at width");
assert_eq!(
row_text(&buf, area, 1),
" ",
"stale rows below the line are cleared"
);
}
#[test]
fn zero_sized_area_is_a_noop() {
let area = Rect::new(0, 0, 8, 1);
let mut buf = Buffer::empty(area);
buf.set_string(0, 0, "XXXXXXXX", Style::default());
render_ephemeral_tip(Rect::new(0, 0, 8, 0), &mut buf, &Line::from("tip"));
assert_eq!(row_text(&buf, area, 0), "XXXXXXXX", "untouched");
}
/// Regression: a bold underpaint in the banner rect (e.g. the welcome
/// tip's `Tip: ` prefix painted the same frame) must not bleed BOLD into
/// the ephemeral tip. `Cell::set_style` merges modifiers, so the clear
/// pass has to reset them explicitly — otherwise `Queued · Enter …`
/// rendered as bold `Queue` + regular `d` (5 leaked bold cells).
#[test]
fn clears_leaked_modifiers_from_underpaint() {
let area = Rect::new(0, 0, 40, 1);
let mut buf = Buffer::empty(area);
// Simulate the phantom session-tip underpaint: 5 bold cells ("Tip: ").
buf.set_string(
0,
0,
"Tip: never gonna give you up",
Style::default().add_modifier(Modifier::BOLD),
);
// The send-now tip shape: dim text with a single bold key chord.
let dim = Style::default();
let bold = Style::default().add_modifier(Modifier::BOLD);
let line = Line::from(vec![
Span::styled("Queued · ", dim),
Span::styled("Enter", bold),
Span::styled(" to send now", dim),
]);
render_ephemeral_tip(area, &mut buf, &line);
assert_eq!(
row_text(&buf, area, 0).trim_end(),
"Queued · Enter to send now"
);
let bold_cols: Vec<u16> = (0..area.width)
.filter(|&x| {
buf.cell((x, 0))
.expect("cell in area")
.modifier
.contains(Modifier::BOLD)
})
.collect();
// "Queued · " occupies cols 0..9, "Enter" cols 9..14.
assert_eq!(
bold_cols,
(9..14).collect::<Vec<u16>>(),
"only the Enter chord may be bold — no leak from the underpaint"
);
}
}