Files
improvise/src/ui/panel.rs
Edward Langley 5566d7349b feat(ui): add footer support to panels
Add footer_height method to PanelContent trait with default 0. Implement
footer_height in FormulaContent to return 1 when in FormulaEdit mode.
Update Panel::render to subtract footer height from item height. This
enables optional footers in panels.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
2026-04-11 00:06:48 -07:00

88 lines
2.8 KiB
Rust

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<C: PanelContent> 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);
}
}