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).
175 lines
5.8 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|