codetether_agent/tui/ui/chat_view/scroll.rs
1//! Scroll-offset clamping for the messages panel.
2//!
3//! The chat [`Paragraph`] is rendered without `Wrap`, so ratatui draws
4//! exactly one terminal row per [`Line`]. Total height is therefore just
5//! `lines.len()` — no per-line width arithmetic required.
6//!
7//! [`Paragraph`]: ratatui::widgets::Paragraph
8
9use ratatui::layout::Rect;
10use ratatui::text::Line;
11
12use crate::tui::app::state::App;
13
14/// Sentinel meaning "follow latest content" (auto-scroll).
15///
16/// ```rust
17/// use codetether_agent::tui::ui::chat_view::scroll::SCROLL_BOTTOM;
18/// assert!(SCROLL_BOTTOM > 999_999);
19/// ```
20pub const SCROLL_BOTTOM: usize = 1_000_000;
21
22/// No-op retained for API compatibility with an earlier memo-based
23/// revision of the scroll-height calculator.
24///
25/// The current implementation recomputes the total in O(1) from
26/// `lines.len()`, so there is no memo to reset. Callers that still invoke
27/// this function continue to compile and behave identically.
28///
29/// # Examples
30///
31/// ```rust
32/// use codetether_agent::tui::ui::chat_view::scroll::reset_height_memo;
33/// reset_height_memo(); // always safe, never fails
34/// ```
35pub fn reset_height_memo() {}
36
37/// Clamp scroll offset so the visible slice of `lines` stays within
38/// `messages_rect` and honor the `SCROLL_BOTTOM` follow-tail sentinel.
39///
40/// The chat `Paragraph` is rendered without wrapping (see module docs),
41/// so the total rendered height equals `lines.len()` and no per-line
42/// width arithmetic is needed.
43///
44/// # Arguments
45///
46/// * `app` — mutable app state; `chat_scroll` is read and the computed
47/// `max_scroll` is written via `AppState::set_chat_max_scroll`.
48/// * `messages_rect` — the panel rect including borders; interior height
49/// is `height - 2`.
50/// * `lines` — the rendered chat lines for this frame.
51///
52/// # Returns
53///
54/// The clamped scroll offset suitable for `Paragraph::scroll((offset, 0))`.
55/// Saturates at `u16::MAX` for safety on very tall buffers.
56///
57/// # Examples
58///
59/// ```rust,no_run
60/// # use codetether_agent::tui::ui::chat_view::scroll::clamp_scroll;
61/// # fn d(a:&mut codetether_agent::tui::app::state::App){
62/// let r = clamp_scroll(a, ratatui::layout::Rect::new(0, 0, 80, 24), &vec![]);
63/// assert_eq!(r, 0);
64/// # }
65/// ```
66pub fn clamp_scroll(app: &mut App, messages_rect: Rect, lines: &[Line<'_>]) -> u16 {
67 // Paragraph is rendered without Wrap → 1 line == 1 terminal row.
68 let total = lines.len();
69 let visible = messages_rect.height.saturating_sub(2) as usize;
70 let max_scroll = total.saturating_sub(visible);
71 app.state.set_chat_max_scroll(max_scroll);
72 let scroll = if app.state.chat_scroll >= SCROLL_BOTTOM {
73 max_scroll
74 } else {
75 app.state.chat_scroll.min(max_scroll)
76 };
77 scroll.min(u16::MAX as usize) as u16
78}