chat: use ScrollPane widget for both draw functions
draw_conversation_pane and draw_pane now delegate all scroll bookkeeping and rendering to the ScrollPane widget. The conversation pane builds MarkedLine items (line + gutter marker), applies selection highlighting, and passes them to the widget. The simpler panes just pass lines directly. Removed dead code from scroll_pane: BorrowedItem, scroll_to_bottom, heights(), ensure_heights_for_lines — all superseded by the widget doing the work internally through the ScrollItem trait. Co-Authored-By: Kent Overstreet <kent.overstreet@gmail.com>
This commit is contained in:
parent
ceaa66e30d
commit
2d6a68048c
2 changed files with 46 additions and 106 deletions
116
src/user/chat.rs
116
src/user/chat.rs
|
|
@ -141,6 +141,32 @@ enum Marker {
|
|||
Assistant,
|
||||
}
|
||||
|
||||
impl Marker {
|
||||
fn gutter_span(self) -> Option<Span<'static>> {
|
||||
match self {
|
||||
Marker::User => Some(Span::styled("● ", Style::default().fg(Color::Cyan))),
|
||||
Marker::Assistant => Some(Span::styled("● ", Style::default().fg(Color::Magenta))),
|
||||
Marker::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A line paired with a gutter marker, for use with ScrollPane.
|
||||
struct MarkedLine {
|
||||
line: Line<'static>,
|
||||
marker: Marker,
|
||||
}
|
||||
|
||||
impl super::scroll_pane::ScrollItem for MarkedLine {
|
||||
fn content(&self) -> ratatui::text::Text<'_> {
|
||||
ratatui::text::Text::from(self.line.clone())
|
||||
}
|
||||
|
||||
fn gutter(&self) -> Option<Span<'_>> {
|
||||
self.marker.gutter_span()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum PaneTarget {
|
||||
Conversation,
|
||||
|
|
@ -1050,7 +1076,7 @@ fn draw_conversation_pane(
|
|||
pane: &mut PaneState,
|
||||
is_active: bool,
|
||||
) {
|
||||
use super::scroll_pane::visible_range;
|
||||
use super::scroll_pane::ScrollPane;
|
||||
|
||||
let border_style = if is_active {
|
||||
Style::default().fg(Color::Cyan)
|
||||
|
|
@ -1063,36 +1089,13 @@ fn draw_conversation_pane(
|
|||
.borders(Borders::ALL)
|
||||
.border_style(border_style);
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
if inner.width < 5 || inner.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let cols = Layout::horizontal([Constraint::Length(2), Constraint::Min(1)]).split(inner);
|
||||
let gutter_area = cols[0];
|
||||
let text_area = cols[1];
|
||||
let text_width = text_area.width;
|
||||
|
||||
let (lines, markers) = pane.all_lines_with_markers();
|
||||
pane.scroll.ensure_heights_for_lines(&lines, text_width);
|
||||
|
||||
if !pane.scroll.pinned {
|
||||
pane.scroll.offset = pane.scroll.total_visual.saturating_sub(inner.height);
|
||||
}
|
||||
pane.scroll.viewport_height = inner.height;
|
||||
pane.scroll.offset = pane.scroll.offset.min(pane.scroll.total_visual.saturating_sub(inner.height));
|
||||
|
||||
let heights = pane.scroll.heights();
|
||||
let (first, sub_scroll, last) = visible_range(heights, pane.scroll.offset, inner.height);
|
||||
|
||||
// Apply selection highlighting to visible lines
|
||||
let visible_lines: Vec<Line<'static>> = if let Some(ref sel) = pane.selection {
|
||||
// Apply selection highlighting
|
||||
let items: Vec<MarkedLine> = if let Some(ref sel) = pane.selection {
|
||||
let (sl, sc, el, ec) = sel.range();
|
||||
(first..last).map(|i| {
|
||||
let line = &lines[i];
|
||||
if i >= sl && i <= el {
|
||||
lines.into_iter().zip(markers).enumerate().map(|(i, (line, marker))| {
|
||||
let line = if i >= sl && i <= el {
|
||||
let line_text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
|
||||
let start_col = if i == sl { sc } else { 0 };
|
||||
let end_col = if i == el { ec } else { line_text.len() };
|
||||
|
|
@ -1106,36 +1109,22 @@ fn draw_conversation_pane(
|
|||
if !after.is_empty() { spans.push(Span::raw(after.to_string())); }
|
||||
Line::from(spans).style(line.style).alignment(line.alignment.unwrap_or(ratatui::layout::Alignment::Left))
|
||||
} else {
|
||||
line.clone()
|
||||
line
|
||||
}
|
||||
} else {
|
||||
line.clone()
|
||||
}
|
||||
line
|
||||
};
|
||||
MarkedLine { line, marker }
|
||||
}).collect()
|
||||
} else {
|
||||
lines[first..last].to_vec()
|
||||
lines.into_iter().zip(markers).map(|(line, marker)| MarkedLine { line, marker }).collect()
|
||||
};
|
||||
|
||||
let text_para = Paragraph::new(visible_lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((sub_scroll, 0));
|
||||
frame.render_widget(text_para, text_area);
|
||||
|
||||
// Build gutter for the visible slice
|
||||
let mut gutter_lines: Vec<Line<'static>> = Vec::new();
|
||||
for i in first..last {
|
||||
gutter_lines.push(match markers[i] {
|
||||
Marker::User => Line::styled("● ", Style::default().fg(Color::Cyan)),
|
||||
Marker::Assistant => Line::styled("● ", Style::default().fg(Color::Magenta)),
|
||||
Marker::None => Line::raw(""),
|
||||
});
|
||||
for _ in 1..heights[i] {
|
||||
gutter_lines.push(Line::raw(""));
|
||||
}
|
||||
}
|
||||
|
||||
let gutter_para = Paragraph::new(gutter_lines).scroll((sub_scroll, 0));
|
||||
frame.render_widget(gutter_para, gutter_area);
|
||||
let widget = ScrollPane::new(&items)
|
||||
.block(block)
|
||||
.gutter_width(2)
|
||||
.pin_to_bottom(true);
|
||||
frame.render_stateful_widget(widget, area, &mut pane.scroll);
|
||||
}
|
||||
|
||||
/// Draw a scrollable text pane (free function to avoid borrow issues).
|
||||
|
|
@ -1147,7 +1136,7 @@ fn draw_pane(
|
|||
is_active: bool,
|
||||
left_title: Option<&str>,
|
||||
) {
|
||||
use super::scroll_pane::visible_range;
|
||||
use super::scroll_pane::ScrollPane;
|
||||
|
||||
let border_style = if is_active {
|
||||
Style::default().fg(Color::Cyan)
|
||||
|
|
@ -1166,22 +1155,9 @@ fn draw_pane(
|
|||
block = block.title(format!(" {} ", title));
|
||||
}
|
||||
|
||||
let text_width = area.width.saturating_sub(2);
|
||||
let inner_height = area.height.saturating_sub(2);
|
||||
let lines = pane.all_lines();
|
||||
pane.scroll.ensure_heights_for_lines(&lines, text_width);
|
||||
|
||||
if !pane.scroll.pinned {
|
||||
pane.scroll.offset = pane.scroll.total_visual.saturating_sub(inner_height);
|
||||
}
|
||||
pane.scroll.viewport_height = inner_height;
|
||||
pane.scroll.offset = pane.scroll.offset.min(pane.scroll.total_visual.saturating_sub(inner_height));
|
||||
|
||||
let (first, sub_scroll, last) = visible_range(pane.scroll.heights(), pane.scroll.offset, inner_height);
|
||||
|
||||
let paragraph = Paragraph::new(lines[first..last].to_vec())
|
||||
.block(block.clone())
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((sub_scroll, 0));
|
||||
frame.render_widget(paragraph, area);
|
||||
let widget = ScrollPane::new(&lines)
|
||||
.block(block)
|
||||
.pin_to_bottom(true);
|
||||
frame.render_stateful_widget(widget, area, &mut pane.scroll);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,23 +40,6 @@ impl ScrollItem for Text<'static> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A borrowed item: a line with an optional gutter span.
|
||||
/// Useful for rendering from parallel slices (lines + markers).
|
||||
pub struct BorrowedItem<'a> {
|
||||
pub line: &'a Line<'a>,
|
||||
pub gutter_span: Option<Span<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> ScrollItem for BorrowedItem<'a> {
|
||||
fn content(&self) -> Text<'_> {
|
||||
Text::from(self.line.clone())
|
||||
}
|
||||
|
||||
fn gutter(&self) -> Option<Span<'_>> {
|
||||
self.gutter_span.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────
|
||||
|
||||
pub struct ScrollPaneState {
|
||||
|
|
@ -106,11 +89,6 @@ impl ScrollPaneState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn scroll_to_bottom(&mut self) {
|
||||
self.offset = self.max_offset();
|
||||
self.pinned = false;
|
||||
}
|
||||
|
||||
fn max_offset(&self) -> u16 {
|
||||
self.total_visual.saturating_sub(self.viewport_height)
|
||||
}
|
||||
|
|
@ -126,11 +104,6 @@ impl ScrollPaneState {
|
|||
self.heights.truncate(index);
|
||||
}
|
||||
|
||||
/// Access cached heights (for mouse coordinate mapping).
|
||||
pub fn heights(&self) -> &[u16] {
|
||||
&self.heights
|
||||
}
|
||||
|
||||
/// Convert a screen row (relative to viewport) to an item index and
|
||||
/// column, given the content items for text extraction.
|
||||
pub fn screen_to_item(&self, mouse_x: u16, mouse_y: u16, lines: &[Line<'_>]) -> Option<(usize, usize)> {
|
||||
|
|
@ -162,15 +135,6 @@ impl ScrollPaneState {
|
|||
});
|
||||
}
|
||||
|
||||
/// Compute or update cached heights from raw Lines.
|
||||
pub fn ensure_heights_for_lines(&mut self, lines: &[Line<'_>], text_width: u16) {
|
||||
self.ensure_heights_inner(lines.len(), text_width, |i| {
|
||||
Paragraph::new(lines[i].clone())
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(text_width) as u16
|
||||
});
|
||||
}
|
||||
|
||||
fn ensure_heights_inner(&mut self, count: usize, text_width: u16, height_fn: impl Fn(usize) -> u16) {
|
||||
if text_width == 0 {
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue