forked from kent/consciousness
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,
|
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)]
|
#[derive(PartialEq)]
|
||||||
enum PaneTarget {
|
enum PaneTarget {
|
||||||
Conversation,
|
Conversation,
|
||||||
|
|
@ -1050,7 +1076,7 @@ fn draw_conversation_pane(
|
||||||
pane: &mut PaneState,
|
pane: &mut PaneState,
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
) {
|
) {
|
||||||
use super::scroll_pane::visible_range;
|
use super::scroll_pane::ScrollPane;
|
||||||
|
|
||||||
let border_style = if is_active {
|
let border_style = if is_active {
|
||||||
Style::default().fg(Color::Cyan)
|
Style::default().fg(Color::Cyan)
|
||||||
|
|
@ -1063,36 +1089,13 @@ fn draw_conversation_pane(
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(border_style);
|
.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();
|
let (lines, markers) = pane.all_lines_with_markers();
|
||||||
pane.scroll.ensure_heights_for_lines(&lines, text_width);
|
|
||||||
|
|
||||||
if !pane.scroll.pinned {
|
// Apply selection highlighting
|
||||||
pane.scroll.offset = pane.scroll.total_visual.saturating_sub(inner.height);
|
let items: Vec<MarkedLine> = if let Some(ref sel) = pane.selection {
|
||||||
}
|
|
||||||
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 {
|
|
||||||
let (sl, sc, el, ec) = sel.range();
|
let (sl, sc, el, ec) = sel.range();
|
||||||
(first..last).map(|i| {
|
lines.into_iter().zip(markers).enumerate().map(|(i, (line, marker))| {
|
||||||
let line = &lines[i];
|
let line = if i >= sl && i <= el {
|
||||||
if i >= sl && i <= el {
|
|
||||||
let line_text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
|
let line_text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
|
||||||
let start_col = if i == sl { sc } else { 0 };
|
let start_col = if i == sl { sc } else { 0 };
|
||||||
let end_col = if i == el { ec } else { line_text.len() };
|
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())); }
|
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))
|
Line::from(spans).style(line.style).alignment(line.alignment.unwrap_or(ratatui::layout::Alignment::Left))
|
||||||
} else {
|
} else {
|
||||||
line.clone()
|
line
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
line.clone()
|
line
|
||||||
}
|
};
|
||||||
|
MarkedLine { line, marker }
|
||||||
}).collect()
|
}).collect()
|
||||||
} else {
|
} 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)
|
let widget = ScrollPane::new(&items)
|
||||||
.wrap(Wrap { trim: false })
|
.block(block)
|
||||||
.scroll((sub_scroll, 0));
|
.gutter_width(2)
|
||||||
frame.render_widget(text_para, text_area);
|
.pin_to_bottom(true);
|
||||||
|
frame.render_stateful_widget(widget, area, &mut pane.scroll);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a scrollable text pane (free function to avoid borrow issues).
|
/// Draw a scrollable text pane (free function to avoid borrow issues).
|
||||||
|
|
@ -1147,7 +1136,7 @@ fn draw_pane(
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
left_title: Option<&str>,
|
left_title: Option<&str>,
|
||||||
) {
|
) {
|
||||||
use super::scroll_pane::visible_range;
|
use super::scroll_pane::ScrollPane;
|
||||||
|
|
||||||
let border_style = if is_active {
|
let border_style = if is_active {
|
||||||
Style::default().fg(Color::Cyan)
|
Style::default().fg(Color::Cyan)
|
||||||
|
|
@ -1166,22 +1155,9 @@ fn draw_pane(
|
||||||
block = block.title(format!(" {} ", title));
|
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();
|
let lines = pane.all_lines();
|
||||||
pane.scroll.ensure_heights_for_lines(&lines, text_width);
|
let widget = ScrollPane::new(&lines)
|
||||||
|
.block(block)
|
||||||
if !pane.scroll.pinned {
|
.pin_to_bottom(true);
|
||||||
pane.scroll.offset = pane.scroll.total_visual.saturating_sub(inner_height);
|
frame.render_stateful_widget(widget, area, &mut pane.scroll);
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ──────────────────────────────────────────────────────
|
// ── State ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct ScrollPaneState {
|
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 {
|
fn max_offset(&self) -> u16 {
|
||||||
self.total_visual.saturating_sub(self.viewport_height)
|
self.total_visual.saturating_sub(self.viewport_height)
|
||||||
}
|
}
|
||||||
|
|
@ -126,11 +104,6 @@ impl ScrollPaneState {
|
||||||
self.heights.truncate(index);
|
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
|
/// Convert a screen row (relative to viewport) to an item index and
|
||||||
/// column, given the content items for text extraction.
|
/// 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)> {
|
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) {
|
fn ensure_heights_inner(&mut self, count: usize, text_width: u16, height_fn: impl Fn(usize) -> u16) {
|
||||||
if text_width == 0 {
|
if text_width == 0 {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue