[tui] Add TitleBar popups for drop-down menus.

This commit is contained in:
Shaun Reed 2026-01-24 15:33:48 -05:00
parent 82ad3ab29f
commit 78c13f5766
2 changed files with 103 additions and 25 deletions

View File

@ -199,7 +199,6 @@ impl<'a> Widget for &mut App<'a> {
]) ])
.split(horizontal[1]); .split(horizontal[1]);
self.title_bar.render(vertical[0], buf);
self.draw_bottom_status(vertical[3], buf); self.draw_bottom_status(vertical[3], buf);
self.draw_tabs(editor_layout[0], buf); self.draw_tabs(editor_layout[0], buf);
let id = App::id().to_string(); let id = App::id().to_string();
@ -209,6 +208,9 @@ impl<'a> Widget for &mut App<'a> {
.context("Failed to render Explorer") .context("Failed to render Explorer")
.unwrap_or_else(|e| error!(target:id.as_str(), "{}", e)); .unwrap_or_else(|e| error!(target:id.as_str(), "{}", e));
self.logger.render(vertical[2], buf); self.logger.render(vertical[2], buf);
// The title bar is rendered last to overlay any popups created for drop-down menus.
self.title_bar.render(vertical[0], buf);
} }
} }

View File

@ -2,21 +2,22 @@ use crate::tui::component::{Action, Component, ComponentState};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{KeyCode, KeyEvent}; use ratatui::crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::style::{Color, Style}; use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Line; use ratatui::text::Line;
use ratatui::widgets::{Block, Borders, Tabs, Widget}; use ratatui::widgets::{
Block, Borders, Clear, List, ListItem, ListState, StatefulWidget, Tabs, Widget,
};
use strum::{EnumIter, FromRepr, IntoEnumIterator}; use strum::{EnumIter, FromRepr, IntoEnumIterator};
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)] #[derive(Debug, Clone, Copy, PartialEq, Eq, FromRepr, EnumIter)]
enum TitleBarItem { enum TitleBarItem {
File, File,
Edit,
View, View,
Help, Help,
} }
impl TitleBarItem { impl TitleBarItem {
pub fn next(mut self) -> Self { pub fn next(self) -> Self {
let cur = self as usize; let cur = self as usize;
let next = cur.saturating_add(1); let next = cur.saturating_add(1);
Self::from_repr(next).unwrap_or(self) Self::from_repr(next).unwrap_or(self)
@ -31,26 +32,35 @@ impl TitleBarItem {
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
match self { match self {
TitleBarItem::File => "File", TitleBarItem::File => "File",
TitleBarItem::Edit => "Edit",
TitleBarItem::View => "View", TitleBarItem::View => "View",
TitleBarItem::Help => "Help", TitleBarItem::Help => "Help",
} }
} }
pub fn options(&self) -> &[&str] {
match self {
TitleBarItem::File => &["Save", "Reload"],
TitleBarItem::View => &["Show/hide explorer", "Show/hide logger"],
TitleBarItem::Help => &["About"],
}
}
} }
pub struct TitleBar { pub struct TitleBar {
selected: TitleBarItem, selected: TitleBarItem,
opened: Option<TitleBarItem>, opened: Option<TitleBarItem>,
pub(crate) component_state: ComponentState, pub(crate) component_state: ComponentState,
list_state: ListState,
} }
impl TitleBar { impl TitleBar {
const DEFAULT_HELP: &str = "(←/h)/(→/l): Select option | Enter: Choose selection";
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
selected: TitleBarItem::File, selected: TitleBarItem::File,
opened: None, opened: None,
component_state: ComponentState::default() component_state: ComponentState::default().with_help_text(Self::DEFAULT_HELP),
.with_help_text(concat!("TODO: Title bar help text.").as_ref()), list_state: ListState::default().with_selected(Some(0)),
} }
} }
@ -71,10 +81,44 @@ impl TitleBar {
.select(self.selected as usize) .select(self.selected as usize)
.render(area, buf); .render(area, buf);
} }
}
impl Widget for &TitleBar { fn render_drop_down(
fn render(self, area: Rect, buf: &mut Buffer) &mut self,
title_bar_anchor: Rect,
area: Rect,
buf: &mut Buffer,
opened: TitleBarItem,
) {
let popup_area = Self::rect_under_option(title_bar_anchor, area, 40, 10);
Clear::default().render(popup_area, buf);
let options = opened.options().iter().map(|i| ListItem::new(*i));
StatefulWidget::render(
List::new(options)
.block(Block::bordered().title(self.selected.id()))
.highlight_style(
Style::default()
.bg(Color::Blue)
.fg(Color::White)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> "),
popup_area,
buf,
&mut self.list_state,
);
}
fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect {
// TODO: X offset for item option? It's fine as-is, but it might look nicer.
Rect {
x: anchor.x,
y: anchor.y + anchor.height,
width: width.min(area.width),
height,
}
}
pub fn render(&mut self, area: Rect, buf: &mut Buffer)
where where
Self: Sized, Self: Sized,
{ {
@ -85,14 +129,40 @@ impl Widget for &TitleBar {
height: 3, height: 3,
}; };
self.render_title_bar(title_bar_area, buf); self.render_title_bar(title_bar_area, buf);
if let Some(opened) = self.opened {
self.render_drop_down(title_bar_area, area, buf, opened);
}
} }
} }
impl Component for TitleBar { impl Component for TitleBar {
fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result<Action> { fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result<Action> {
if self.opened.is_some() {
// Keybinds for popup menu.
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
self.list_state.select_previous();
Ok(Action::Handled)
}
KeyCode::Down | KeyCode::Char('j') => {
self.list_state.select_next();
Ok(Action::Handled)
}
KeyCode::Enter => {
// TODO: Handle action for the item.
Ok(Action::Handled)
}
KeyCode::Esc | KeyCode::Char('q') => {
self.opened = None;
self.component_state.help_text = Self::DEFAULT_HELP.to_string();
self.list_state.select_first();
Ok(Action::Handled)
}
_ => Ok(Action::Noop),
}
} else {
// Keybinds for title bar.
match key.code { match key.code {
// KeyCode::Up | KeyCode::Char('k') => self.selected.key_up(),
// KeyCode::Down | KeyCode::Char('j') => self.selected.key_down(),
KeyCode::Left | KeyCode::Char('h') => { KeyCode::Left | KeyCode::Char('h') => {
self.selected = self.selected.prev(); self.selected = self.selected.prev();
Ok(Action::Handled) Ok(Action::Handled)
@ -103,9 +173,15 @@ impl Component for TitleBar {
} }
KeyCode::Enter => { KeyCode::Enter => {
self.opened = Some(self.selected); self.opened = Some(self.selected);
self.component_state.help_text = concat!(
"(↑/k)/(↓/j): Select option | Enter: Choose selection |",
" ESC/Q: Close drop-down menu"
)
.to_string();
Ok(Action::Handled) Ok(Action::Handled)
} }
_ => Ok(Action::Noop), _ => Ok(Action::Noop),
} }
} }
}
} }