use std::collections::VecDeque;
use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use crate::app::App;
use crate::theme::hex_to_color;
const MAX_WRAPPED_LINES_PER_MSG: usize = 16;
fn compute_render_budget(buffer_len: usize, visible_height: usize, scroll_offset: usize) -> usize {
let cap = buffer_len
.saturating_mul(MAX_WRAPPED_LINES_PER_MSG)
.max(visible_height);
visible_height.saturating_add(scroll_offset).min(cap)
}
pub fn render(frame: &mut Frame, area: Rect, app: &App) {
if app
.state
.active_buffer()
.is_some_and(|b| b.buffer_type == crate::state::buffer::BufferType::Shell)
{
super::shell_view::render(frame, area, app);
return;
}
let colors = &app.theme.colors;
let bg = hex_to_color(&colors.bg).unwrap_or(Color::Reset);
let fg_muted = hex_to_color(&colors.fg_muted).unwrap_or(Color::DarkGray);
if let Some(buf) = app.state.active_buffer() {
let current_nick = app
.state
.connections
.get(&buf.connection_id)
.map_or("", |c| c.nick.as_str());
let total_width = area.width as usize;
let visible_height = area.height as usize;
if total_width == 0 || visible_height == 0 {
return;
}
let indent = app.wrap_indent;
let needed = compute_render_budget(buf.messages.len(), visible_height, app.scroll_offset);
let mut visual_lines: VecDeque<Line<'_>> = VecDeque::new();
for msg in buf.messages.iter().rev() {
let is_own = msg.nick.as_deref() == Some(current_nick);
let nick_fg = if app.config.display.nick_colors && !is_own && !msg.highlight {
msg.nick.as_deref().map(|n| {
crate::nick_color::nick_color(
n,
app.color_support,
app.config.display.nick_color_saturation,
app.config.display.nick_color_lightness,
)
})
} else {
None
};
let line =
super::message_line::render_message(msg, is_own, &app.theme, &app.config, nick_fg);
let wrapped = super::wrap_line(line, total_width, indent);
for wl in wrapped.into_iter().rev() {
visual_lines.push_front(wl);
}
if visual_lines.len() > needed {
break;
}
}
let total = visual_lines.len();
let max_scroll = total.saturating_sub(visible_height);
let scroll = app.scroll_offset.min(max_scroll);
let skip = total.saturating_sub(visible_height + scroll);
let visible_lines: Vec<Line<'_>> = visual_lines
.into_iter()
.skip(skip)
.take(visible_height)
.collect();
let paragraph = Paragraph::new(visible_lines).style(Style::default().bg(bg));
frame.render_widget(paragraph, area);
} else {
let paragraph = Paragraph::new("No active buffer")
.style(Style::default().fg(fg_muted).bg(bg))
.alignment(Alignment::Center);
frame.render_widget(paragraph, area);
}
}
#[cfg(test)]
mod tests {
mod compute_render_budget {
use super::super::{MAX_WRAPPED_LINES_PER_MSG, compute_render_budget};
#[test]
fn returns_visible_plus_offset_for_normal_scroll() {
let got = compute_render_budget(2000, 78, 50);
assert_eq!(
got, 128,
"2000-msg buffer with scroll_offset=50 should return visible_height+offset (78+50=128), got {got}"
);
}
#[test]
fn returns_visible_height_when_scroll_is_zero() {
let got = compute_render_budget(2000, 78, 0);
assert_eq!(
got, 78,
"zero scroll_offset should return exactly visible_height, got {got}"
);
}
#[test]
fn caps_at_buffer_times_max_wraps_for_pathological_scroll() {
let buffer_len = 2000;
let got = compute_render_budget(buffer_len, 78, usize::MAX / 2);
let expected = buffer_len * MAX_WRAPPED_LINES_PER_MSG;
assert_eq!(
got,
expected,
"pathological scroll_offset={} with buffer_len={buffer_len} must cap at buffer_len*MAX_WRAPPED_LINES_PER_MSG={expected}, got {got}",
usize::MAX / 2
);
}
#[test]
fn returns_visible_height_for_empty_buffer() {
let got = compute_render_budget(0, 78, 1000);
assert_eq!(
got, 78,
"empty buffer with any scroll_offset should fall back to visible_height, got {got}"
);
}
#[test]
fn caps_at_buffer_cap_for_small_buffer_with_large_scroll() {
let got = compute_render_budget(10, 78, 1_000_000);
let expected = 10 * MAX_WRAPPED_LINES_PER_MSG;
assert_eq!(
got, expected,
"10-msg buffer with scroll_offset=1M must cap at 10*MAX_WRAPPED_LINES_PER_MSG={expected}, got {got}"
);
}
#[test]
fn is_overflow_safe_for_usize_max_scroll() {
let got = compute_render_budget(100, 78, usize::MAX);
let expected = 100 * MAX_WRAPPED_LINES_PER_MSG;
assert_eq!(
got, expected,
"usize::MAX scroll_offset must not overflow and must cap at 100*MAX_WRAPPED_LINES_PER_MSG={expected}, got {got}"
);
}
}
}