feat(ui): simplify AppMode minibuffer handling and panel rendering

Refactor AppMode to use MinibufferConfig for all text-entry modes. Update
command implementations to use new mode constructors. Introduce
PanelContent trait and replace panel structs with content types. Adjust
rendering to use Panel::new and minibuffer configuration. Update imports
and add MinibufferConfig struct. No functional changes; all behavior
preserved.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
This commit is contained in:
Edward Langley
2026-04-07 02:04:46 -07:00
parent de047ddf1a
commit 4b11b6e321
9 changed files with 376 additions and 320 deletions

View File

@ -373,17 +373,9 @@ impl Cmd for EnterMode {
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> { fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let mut effects: Vec<Box<dyn Effect>> = Vec::new(); let mut effects: Vec<Box<dyn Effect>> = Vec::new();
// Clear the corresponding buffer when entering a text-entry mode // Clear the corresponding buffer when entering a text-entry mode
let buffer_name = match &self.0 { if let Some(mb) = self.0.minibuffer() {
AppMode::CommandMode { .. } => Some("command"),
AppMode::Editing { .. } => Some("edit"),
AppMode::FormulaEdit { .. } => Some("formula"),
AppMode::CategoryAdd { .. } => Some("category"),
AppMode::ExportPrompt { .. } => Some("export"),
_ => None,
};
if let Some(name) = buffer_name {
effects.push(Box::new(effect::SetBuffer { effects.push(Box::new(effect::SetBuffer {
name: name.to_string(), name: mb.buffer_key.to_string(),
value: String::new(), value: String::new(),
})); }));
} }
@ -623,9 +615,7 @@ impl Cmd for EnterEditMode {
name: "edit".to_string(), name: "edit".to_string(),
value: self.initial_value.clone(), value: self.initial_value.clone(),
}), }),
effect::change_mode(AppMode::Editing { effect::change_mode(AppMode::editing()),
buffer: String::new(),
}),
] ]
} }
} }
@ -764,9 +754,7 @@ impl Cmd for EnterExportPrompt {
"enter-export-prompt" "enter-export-prompt"
} }
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> { fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
vec![effect::change_mode(AppMode::ExportPrompt { vec![effect::change_mode(AppMode::export_prompt())]
buffer: String::new(),
})]
} }
} }
@ -869,9 +857,7 @@ impl Cmd for SearchOrCategoryAdd {
panel: Panel::Category, panel: Panel::Category,
open: true, open: true,
}), }),
effect::change_mode(AppMode::CategoryAdd { effect::change_mode(AppMode::category_add()),
buffer: String::new(),
}),
] ]
} }
} }
@ -1237,9 +1223,7 @@ impl Cmd for EnterFormulaEdit {
"enter-formula-edit" "enter-formula-edit"
} }
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> { fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
vec![effect::change_mode(AppMode::FormulaEdit { vec![effect::change_mode(AppMode::formula_edit())]
buffer: String::new(),
})]
} }
} }
@ -1302,10 +1286,7 @@ impl Cmd for OpenItemAddAtCursor {
} }
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> { fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
if let Some(cat_name) = ctx.cat_at_cursor() { if let Some(cat_name) = ctx.cat_at_cursor() {
vec![effect::change_mode(AppMode::ItemAdd { vec![effect::change_mode(AppMode::item_add(cat_name))]
category: cat_name,
buffer: String::new(),
})]
} else { } else {
vec![effect::set_status( vec![effect::set_status(
"No category selected. Press n to add a category first.", "No category selected. Press n to add a category first.",
@ -2543,21 +2524,11 @@ pub fn default_registry() -> CmdRegistry {
"category-panel" => AppMode::CategoryPanel, "category-panel" => AppMode::CategoryPanel,
"view-panel" => AppMode::ViewPanel, "view-panel" => AppMode::ViewPanel,
"tile-select" => AppMode::TileSelect, "tile-select" => AppMode::TileSelect,
"command" => AppMode::CommandMode { "command" => AppMode::command_mode(),
buffer: String::new(), "category-add" => AppMode::category_add(),
}, "editing" => AppMode::editing(),
"category-add" => AppMode::CategoryAdd { "formula-edit" => AppMode::formula_edit(),
buffer: String::new(), "export-prompt" => AppMode::export_prompt(),
},
"editing" => AppMode::Editing {
buffer: String::new(),
},
"formula-edit" => AppMode::FormulaEdit {
buffer: String::new(),
},
"export-prompt" => AppMode::ExportPrompt {
buffer: String::new(),
},
other => return Err(format!("Unknown mode: {other}")), other => return Err(format!("Unknown mode: {other}")),
}; };
Ok(Box::new(EnterMode(mode))) Ok(Box::new(EnterMode(mode)))

View File

@ -18,13 +18,14 @@ use ratatui::{
use crate::model::Model; use crate::model::Model;
use crate::ui::app::{App, AppMode}; use crate::ui::app::{App, AppMode};
use crate::ui::category_panel::CategoryPanel; use crate::ui::category_panel::CategoryContent;
use crate::ui::formula_panel::FormulaPanel; use crate::ui::formula_panel::FormulaContent;
use crate::ui::grid::GridWidget; use crate::ui::grid::GridWidget;
use crate::ui::help::HelpWidget; use crate::ui::help::HelpWidget;
use crate::ui::import_wizard_ui::ImportWizardWidget; use crate::ui::import_wizard_ui::ImportWizardWidget;
use crate::ui::panel::Panel;
use crate::ui::tile_bar::TileBar; use crate::ui::tile_bar::TileBar;
use crate::ui::view_panel::ViewPanel; use crate::ui::view_panel::ViewContent;
struct TuiContext<'a> { struct TuiContext<'a> {
terminal: Terminal<CrosstermBackend<&'a mut Stdout>>, terminal: Terminal<CrosstermBackend<&'a mut Stdout>>,
@ -224,31 +225,20 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
if app.formula_panel_open { if app.formula_panel_open {
let a = Rect::new(side.x, y, side.width, ph); let a = Rect::new(side.x, y, side.width, ph);
f.render_widget( let content = FormulaContent::new(&app.model, &app.mode);
FormulaPanel::new(&app.model, &app.mode, app.formula_cursor), f.render_widget(Panel::new(content, &app.mode, app.formula_cursor), a);
a,
);
y += ph; y += ph;
} }
if app.category_panel_open { if app.category_panel_open {
let a = Rect::new(side.x, y, side.width, ph); let a = Rect::new(side.x, y, side.width, ph);
f.render_widget( let content = CategoryContent::new(&app.model, &app.expanded_cats);
CategoryPanel::new( f.render_widget(Panel::new(content, &app.mode, app.cat_panel_cursor), a);
&app.model,
&app.mode,
app.cat_panel_cursor,
&app.expanded_cats,
),
a,
);
y += ph; y += ph;
} }
if app.view_panel_open { if app.view_panel_open {
let a = Rect::new(side.x, y, side.width, ph); let a = Rect::new(side.x, y, side.width, ph);
f.render_widget( let content = ViewContent::new(&app.model);
ViewPanel::new(&app.model, &app.mode, app.view_panel_cursor), f.render_widget(Panel::new(content, &app.mode, app.view_panel_cursor), a);
a,
);
} }
} else { } else {
grid_area = area; grid_area = area;
@ -272,44 +262,17 @@ fn draw_tile_bar(f: &mut Frame, area: Rect, app: &App) {
} }
fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) { fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
// All text-entry modes use the bottom bar as a minibuffer. if let Some(mb) = app.mode.minibuffer() {
let minibuf = match &app.mode {
AppMode::CommandMode { .. } => {
let buf = app.buffers.get("command").map(|s| s.as_str()).unwrap_or("");
Some((format!(":{buf}"), Color::Yellow))
}
AppMode::Editing { .. } => {
let buf = app.buffers.get("edit").map(|s| s.as_str()).unwrap_or("");
Some((format!("edit: {buf}"), Color::Green))
}
AppMode::FormulaEdit { .. } => {
let buf = app.buffers.get("formula").map(|s| s.as_str()).unwrap_or("");
Some((format!("formula: {buf}"), Color::Cyan))
}
AppMode::CategoryAdd { .. } => {
let buf = app let buf = app
.buffers .buffers
.get("category") .get(mb.buffer_key)
.map(|s| s.as_str()) .map(|s| s.as_str())
.unwrap_or(""); .unwrap_or("");
Some((format!("new category: {buf}"), Color::Yellow)) let text = format!("{}{}", mb.prompt, buf);
}
AppMode::ItemAdd { category, .. } => {
let buf = app.buffers.get("item").map(|s| s.as_str()).unwrap_or("");
Some((format!("add item to {category}: {buf}"), Color::Green))
}
AppMode::ExportPrompt { .. } => {
let buf = app.buffers.get("export").map(|s| s.as_str()).unwrap_or("");
Some((format!("export path: {buf}"), Color::Yellow))
}
_ => None,
};
if let Some((text, color)) = minibuf {
f.render_widget( f.render_widget(
Paragraph::new(text).style( Paragraph::new(text).style(
Style::default() Style::default()
.fg(color) .fg(mb.color)
.bg(Color::Indexed(235)) .bg(Color::Indexed(235))
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
), ),

View File

@ -7,6 +7,8 @@ use std::time::{Duration, Instant};
use std::rc::Rc; use std::rc::Rc;
use ratatui::style::Color;
use crate::command::cmd::CmdContext; use crate::command::cmd::CmdContext;
use crate::command::keymap::{Keymap, KeymapSet}; use crate::command::keymap::{Keymap, KeymapSet};
use crate::import::wizard::ImportWizard; use crate::import::wizard::ImportWizard;
@ -29,40 +31,126 @@ pub struct DrillState {
pub pending_edits: std::collections::HashMap<(usize, String), String>, pub pending_edits: std::collections::HashMap<(usize, String), String>,
} }
/// Display configuration for the bottom-bar minibuffer.
/// Carried structurally by text-entry `AppMode` variants.
#[derive(Debug, Clone, PartialEq)]
pub struct MinibufferConfig {
pub buffer_key: &'static str,
pub prompt: String,
pub color: Color,
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum AppMode { pub enum AppMode {
Normal, Normal,
Editing { Editing {
buffer: String, minibuf: MinibufferConfig,
}, },
FormulaEdit { FormulaEdit {
buffer: String, minibuf: MinibufferConfig,
}, },
FormulaPanel, FormulaPanel,
CategoryPanel, CategoryPanel,
/// Quick-add a new category: Enter adds and stays open, Esc closes. /// Quick-add a new category: Enter adds and stays open, Esc closes.
CategoryAdd { CategoryAdd {
buffer: String, minibuf: MinibufferConfig,
}, },
/// Quick-add items to `category`: Enter adds and stays open, Esc closes. /// Quick-add items to `category`: Enter adds and stays open, Esc closes.
ItemAdd { ItemAdd {
category: String, category: String,
buffer: String, minibuf: MinibufferConfig,
}, },
ViewPanel, ViewPanel,
TileSelect, TileSelect,
ImportWizard, ImportWizard,
ExportPrompt { ExportPrompt {
buffer: String, minibuf: MinibufferConfig,
}, },
/// Vim-style `:` command line /// Vim-style `:` command line
CommandMode { CommandMode {
buffer: String, minibuf: MinibufferConfig,
}, },
Help, Help,
Quit, Quit,
} }
impl AppMode {
/// Extract the minibuffer config from text-entry modes, if present.
pub fn minibuffer(&self) -> Option<&MinibufferConfig> {
match self {
Self::Editing { minibuf, .. }
| Self::FormulaEdit { minibuf, .. }
| Self::CommandMode { minibuf, .. }
| Self::CategoryAdd { minibuf, .. }
| Self::ItemAdd { minibuf, .. }
| Self::ExportPrompt { minibuf, .. } => Some(minibuf),
_ => None,
}
}
pub fn editing() -> Self {
Self::Editing {
minibuf: MinibufferConfig {
buffer_key: "edit",
prompt: "edit: ".into(),
color: Color::Green,
},
}
}
pub fn formula_edit() -> Self {
Self::FormulaEdit {
minibuf: MinibufferConfig {
buffer_key: "formula",
prompt: "formula: ".into(),
color: Color::Cyan,
},
}
}
pub fn command_mode() -> Self {
Self::CommandMode {
minibuf: MinibufferConfig {
buffer_key: "command",
prompt: ":".into(),
color: Color::Yellow,
},
}
}
pub fn category_add() -> Self {
Self::CategoryAdd {
minibuf: MinibufferConfig {
buffer_key: "category",
prompt: "new category: ".into(),
color: Color::Yellow,
},
}
}
pub fn item_add(category: String) -> Self {
let prompt = format!("add item to {category}: ");
Self::ItemAdd {
category,
minibuf: MinibufferConfig {
buffer_key: "item",
prompt,
color: Color::Green,
},
}
}
pub fn export_prompt() -> Self {
Self::ExportPrompt {
minibuf: MinibufferConfig {
buffer_key: "export",
prompt: "export path: ".into(),
color: Color::Yellow,
},
}
}
}
pub struct App { pub struct App {
pub model: Model, pub model: Model,
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,

View File

@ -2,12 +2,12 @@ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, Widget},
}; };
use crate::model::Model; use crate::model::Model;
use crate::ui::app::AppMode; use crate::ui::app::AppMode;
use crate::ui::cat_tree::{build_cat_tree, CatTreeEntry}; use crate::ui::cat_tree::{build_cat_tree, CatTreeEntry};
use crate::ui::panel::PanelContent;
use crate::view::Axis; use crate::view::Axis;
fn axis_display(axis: Axis) -> (&'static str, Color) { fn axis_display(axis: Axis) -> (&'static str, Color) {
@ -19,67 +19,45 @@ fn axis_display(axis: Axis) -> (&'static str, Color) {
} }
} }
pub struct CategoryPanel<'a> { pub struct CategoryContent<'a> {
pub model: &'a Model,
pub mode: &'a AppMode,
pub cursor: usize,
pub expanded: &'a std::collections::HashSet<String>,
}
impl<'a> CategoryPanel<'a> {
pub fn new(
model: &'a Model, model: &'a Model,
mode: &'a AppMode, tree: Vec<CatTreeEntry>,
cursor: usize, }
expanded: &'a std::collections::HashSet<String>,
) -> Self { impl<'a> CategoryContent<'a> {
Self { pub fn new(model: &'a Model, expanded: &'a std::collections::HashSet<String>) -> Self {
model, let tree = build_cat_tree(model, expanded);
Self { model, tree }
}
}
impl PanelContent for CategoryContent<'_> {
fn is_active(&self, mode: &AppMode) -> bool {
matches!(
mode, mode,
cursor, AppMode::CategoryPanel | AppMode::ItemAdd { .. } | AppMode::CategoryAdd { .. }
expanded, )
}
}
} }
impl<'a> Widget for CategoryPanel<'a> { fn active_color(&self) -> Color {
fn render(self, area: Rect, buf: &mut Buffer) { Color::Cyan
let is_item_add = matches!(self.mode, AppMode::ItemAdd { .. }); }
let is_cat_add = matches!(self.mode, AppMode::CategoryAdd { .. });
let is_active = matches!(self.mode, AppMode::CategoryPanel) || is_item_add || is_cat_add;
let (border_color, title) = if is_active { fn title(&self) -> &str {
(Color::Cyan, " Categories ") " Categories "
} else { }
(Color::DarkGray, " Categories ")
};
let block = Block::default() fn item_count(&self) -> usize {
.borders(Borders::ALL) self.tree.len()
.border_style(Style::default().fg(border_color)) }
.title(title);
let inner = block.inner(area);
block.render(area, buf);
fn empty_message(&self) -> &str {
"(no categories — use :add-cat <name>)"
}
fn render_item(&self, index: usize, is_selected: bool, inner: Rect, buf: &mut Buffer) {
let y = inner.y + index as u16;
let view = self.model.active_view(); let view = self.model.active_view();
let tree = build_cat_tree(self.model, self.expanded);
if tree.is_empty() {
buf.set_string(
inner.x,
inner.y,
"(no categories — use :add-cat <name>)",
Style::default().fg(Color::DarkGray),
);
return;
}
for (i, entry) in tree.iter().enumerate() {
if i as u16 >= inner.height {
break;
}
let y = inner.y + i as u16;
let is_selected = i == self.cursor && is_active;
let base_style = if is_selected { let base_style = if is_selected {
Style::default() Style::default()
@ -95,7 +73,7 @@ impl<'a> Widget for CategoryPanel<'a> {
buf.set_string(inner.x, y, &fill, base_style); buf.set_string(inner.x, y, &fill, base_style);
} }
match entry { match &self.tree[index] {
CatTreeEntry::Category { CatTreeEntry::Category {
name, name,
item_count, item_count,
@ -127,4 +105,3 @@ impl<'a> Widget for CategoryPanel<'a> {
} }
} }
} }
}

View File

@ -108,9 +108,7 @@ impl Effect for EnterEditAtCursor {
let value = ctx.display_value.clone(); let value = ctx.display_value.clone();
drop(ctx); drop(ctx);
app.buffers.insert("edit".to_string(), value); app.buffers.insert("edit".to_string(), value);
app.mode = AppMode::Editing { app.mode = AppMode::editing();
buffer: String::new(),
};
} }
} }

View File

@ -2,64 +2,46 @@ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, Widget},
}; };
use crate::model::Model; use crate::model::Model;
use crate::ui::app::AppMode; use crate::ui::app::AppMode;
use crate::ui::panel::PanelContent;
pub struct FormulaPanel<'a> { pub struct FormulaContent<'a> {
pub model: &'a Model, pub model: &'a Model,
pub mode: &'a AppMode, pub mode: &'a AppMode,
pub cursor: usize,
} }
impl<'a> FormulaPanel<'a> { impl<'a> FormulaContent<'a> {
pub fn new(model: &'a Model, mode: &'a AppMode, cursor: usize) -> Self { pub fn new(model: &'a Model, mode: &'a AppMode) -> Self {
Self { Self { model, mode }
model,
mode,
cursor,
}
} }
} }
impl<'a> Widget for FormulaPanel<'a> { impl PanelContent for FormulaContent<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn is_active(&self, mode: &AppMode) -> bool {
let is_active = matches!( matches!(mode, AppMode::FormulaPanel | AppMode::FormulaEdit { .. })
self.mode,
AppMode::FormulaPanel | AppMode::FormulaEdit { .. }
);
let border_style = if is_active {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::DarkGray)
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(" Formulas [n]ew [d]elete ");
let inner = block.inner(area);
block.render(area, buf);
let formulas = self.model.formulas();
if formulas.is_empty() {
buf.set_string(
inner.x,
inner.y,
"(no formulas — press 'n' to add)",
Style::default().fg(Color::DarkGray),
);
return;
} }
for (i, formula) in formulas.iter().enumerate() { fn active_color(&self) -> Color {
if inner.y + i as u16 >= inner.y + inner.height { Color::Yellow
break;
} }
let is_selected = i == self.cursor && is_active;
fn title(&self) -> &str {
" Formulas [n]ew [d]elete "
}
fn item_count(&self) -> usize {
self.model.formulas().len()
}
fn empty_message(&self) -> &str {
"(no formulas — press 'n' to add)"
}
fn render_item(&self, index: usize, is_selected: bool, inner: Rect, buf: &mut Buffer) {
let formula = &self.model.formulas()[index];
let style = if is_selected { let style = if is_selected {
Style::default() Style::default()
.fg(Color::Black) .fg(Color::Black)
@ -74,21 +56,18 @@ impl<'a> Widget for FormulaPanel<'a> {
} else { } else {
text text
}; };
buf.set_string(inner.x, inner.y + i as u16, &truncated, style); buf.set_string(inner.x, inner.y + index as u16, &truncated, style);
} }
// Formula edit mode fn render_footer(&self, inner: Rect, buf: &mut Buffer) {
if let AppMode::FormulaEdit { buffer } = self.mode { if matches!(self.mode, AppMode::FormulaEdit { .. }) {
let y = inner.y + inner.height.saturating_sub(2); let y = inner.y + inner.height.saturating_sub(1);
buf.set_string( buf.set_string(
inner.x, inner.x,
y, y,
"┄ Enter formula (Name = expr): ", "┄ Enter formula (Name = expr)",
Style::default().fg(Color::Yellow), Style::default().fg(Color::Yellow),
); );
let y = y + 1;
let prompt = format!("> {buffer}");
buf.set_string(inner.x, y, &prompt, Style::default().fg(Color::Green));
} }
} }
} }

View File

@ -6,5 +6,6 @@ pub mod formula_panel;
pub mod grid; pub mod grid;
pub mod help; pub mod help;
pub mod import_wizard_ui; pub mod import_wizard_ui;
pub mod panel;
pub mod tile_bar; pub mod tile_bar;
pub mod view_panel; pub mod view_panel;

82
src/ui/panel.rs Normal file
View File

@ -0,0 +1,82 @@
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);
/// Optional footer rendered below the items (e.g. inline hints).
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;
}
for i in 0..self.content.item_count() {
if i as u16 >= inner.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);
}
}

View File

@ -2,54 +2,52 @@ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, Borders, Widget},
}; };
use crate::model::Model; use crate::model::Model;
use crate::ui::app::AppMode; use crate::ui::app::AppMode;
use crate::ui::panel::PanelContent;
pub struct ViewPanel<'a> { pub struct ViewContent {
pub model: &'a Model, view_names: Vec<String>,
pub mode: &'a AppMode, active_view: String,
pub cursor: usize,
} }
impl<'a> ViewPanel<'a> { impl ViewContent {
pub fn new(model: &'a Model, mode: &'a AppMode, cursor: usize) -> Self { pub fn new(model: &Model) -> Self {
let view_names: Vec<String> = model.views.keys().cloned().collect();
let active_view = model.active_view.clone();
Self { Self {
model, view_names,
mode, active_view,
cursor,
} }
} }
} }
impl<'a> Widget for ViewPanel<'a> { impl PanelContent for ViewContent {
fn render(self, area: Rect, buf: &mut Buffer) { fn is_active(&self, mode: &AppMode) -> bool {
let is_active = matches!(self.mode, AppMode::ViewPanel); matches!(mode, AppMode::ViewPanel)
let border_style = if is_active {
Style::default().fg(Color::Blue)
} else {
Style::default().fg(Color::DarkGray)
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style)
.title(" Views ");
let inner = block.inner(area);
block.render(area, buf);
let view_names: Vec<&str> = self.model.views.keys().map(|s| s.as_str()).collect();
let active = &self.model.active_view;
for (i, view_name) in view_names.iter().enumerate() {
if inner.y + i as u16 >= inner.y + inner.height {
break;
} }
let is_selected = i == self.cursor && is_active; fn active_color(&self) -> Color {
let is_active_view = *view_name == active.as_str(); Color::Blue
}
fn title(&self) -> &str {
" Views "
}
fn item_count(&self) -> usize {
self.view_names.len()
}
fn empty_message(&self) -> &str {
"(no views)"
}
fn render_item(&self, index: usize, is_selected: bool, inner: Rect, buf: &mut Buffer) {
let view_name = &self.view_names[index];
let is_active_view = view_name == &self.active_view;
let style = if is_selected { let style = if is_selected {
Style::default() Style::default()
@ -67,10 +65,9 @@ impl<'a> Widget for ViewPanel<'a> {
let prefix = if is_active_view { "" } else { " " }; let prefix = if is_active_view { "" } else { " " };
buf.set_string( buf.set_string(
inner.x, inner.x,
inner.y + i as u16, inner.y + index as u16,
format!("{prefix}{view_name}"), format!("{prefix}{view_name}"),
style, style,
); );
} }
} }
}