use ratatui::{ buffer::Buffer, layout::Rect, style::{Color, Style}, widgets::{Block, Borders, Widget}, }; use crate::ui::app::AppMode; /// Trait for panel-specific content. Implement this to create a new side panel. pub trait PanelContent { /// Whether the panel should appear active given the current mode. fn is_active(&self, mode: &AppMode) -> bool; /// Color used for the active border AND the selection highlight background. fn active_color(&self) -> Color; /// Block title string (include surrounding spaces for padding). fn title(&self) -> &str; /// Number of renderable rows. fn item_count(&self) -> usize; /// Message shown when `item_count()` returns 0. fn empty_message(&self) -> &str; /// Render a single item at the given row index. /// `inner` is the full inner area of the panel; the item occupies row `index`. fn render_item(&self, index: usize, is_selected: bool, inner: Rect, buf: &mut Buffer); /// Number of lines the footer occupies (used to reserve space). fn footer_height(&self) -> u16 { 0 } /// Optional footer rendered in the reserved space at the bottom. fn render_footer(&self, _inner: Rect, _buf: &mut Buffer) {} } /// Generic side-panel widget that delegates content rendering to a `PanelContent` impl. pub struct Panel<'a, C: PanelContent> { content: C, mode: &'a AppMode, cursor: usize, } impl<'a, C: PanelContent> Panel<'a, C> { pub fn new(content: C, mode: &'a AppMode, cursor: usize) -> Self { Self { content, mode, cursor, } } } impl Widget for Panel<'_, C> { fn render(self, area: Rect, buf: &mut Buffer) { let is_active = self.content.is_active(self.mode); let border_style = if is_active { Style::default().fg(self.content.active_color()) } else { Style::default().fg(Color::DarkGray) }; let block = Block::default() .borders(Borders::ALL) .border_style(border_style) .title(self.content.title()); let inner = block.inner(area); block.render(area, buf); if self.content.item_count() == 0 { buf.set_string( inner.x, inner.y, self.content.empty_message(), Style::default().fg(Color::DarkGray), ); return; } let item_height = inner.height.saturating_sub(self.content.footer_height()); for i in 0..self.content.item_count() { if i as u16 >= item_height { break; } let is_selected = i == self.cursor && is_active; self.content.render_item(i, is_selected, inner, buf); } self.content.render_footer(inner, buf); } }