//! Centered modal listing keybindings for the current screen. use ftui::core::geometry::Rect; use ftui::render::cell::{Cell, PackedRgba}; use ftui::render::drawing::Draw; use ftui::render::frame::Frame; use crate::commands::CommandRegistry; use crate::message::Screen; /// Render a centered help overlay listing keybindings for the current screen. /// /// The overlay is a bordered modal that lists all commands from the /// registry that are available on the current screen. #[allow(clippy::too_many_arguments)] pub fn render_help_overlay( frame: &mut Frame<'_>, area: Rect, registry: &CommandRegistry, screen: &Screen, border_color: PackedRgba, text_color: PackedRgba, muted_color: PackedRgba, scroll_offset: usize, ) { if area.height < 5 || area.width < 20 { return; } // Overlay dimensions: 60% of screen, capped. let overlay_width = (area.width * 3 / 5).clamp(30, 70); let overlay_height = (area.height * 3 / 5).clamp(8, 30); let overlay_x = area.x + (area.width.saturating_sub(overlay_width)) / 2; let overlay_y = area.y + (area.height.saturating_sub(overlay_height)) / 2; let overlay_rect = Rect::new(overlay_x, overlay_y, overlay_width, overlay_height); // Draw border. let border_cell = Cell { fg: border_color, ..Cell::default() }; frame.draw_border( overlay_rect, ftui::render::drawing::BorderChars::ROUNDED, border_cell, ); // Title. let title = " Help (? to close) "; let title_x = overlay_x + (overlay_width.saturating_sub(title.len() as u16)) / 2; let title_cell = Cell { fg: border_color, ..Cell::default() }; frame.print_text_clipped(title_x, overlay_y, title, title_cell, overlay_rect.right()); // Inner content area (inside border). let inner = Rect::new( overlay_x + 2, overlay_y + 1, overlay_width.saturating_sub(4), overlay_height.saturating_sub(2), ); // Get commands for this screen. let commands = registry.help_entries(screen); let visible_lines = inner.height as usize; let key_cell = Cell { fg: text_color, ..Cell::default() }; let desc_cell = Cell { fg: muted_color, ..Cell::default() }; for (i, cmd) in commands.iter().skip(scroll_offset).enumerate() { if i >= visible_lines { break; } let y = inner.y + i as u16; // Key binding label (left). let key_label = cmd .keybinding .as_ref() .map_or_else(String::new, |kb| kb.display()); let label_end = frame.print_text_clipped(inner.x, y, &key_label, key_cell, inner.right()); // Spacer + description (right). let desc_x = label_end.saturating_add(2); if desc_x < inner.right() { frame.print_text_clipped(desc_x, y, cmd.help_text, desc_cell, inner.right()); } } // Scroll indicator if needed. if commands.len() > visible_lines + scroll_offset { let indicator = format!("({}/{})", scroll_offset + visible_lines, commands.len()); let ind_x = inner.right().saturating_sub(indicator.len() as u16); let ind_y = overlay_rect.bottom().saturating_sub(1); frame.print_text_clipped(ind_x, ind_y, &indicator, desc_cell, overlay_rect.right()); } } #[cfg(test)] mod tests { use super::*; use crate::commands::build_registry; use crate::message::Screen; use ftui::render::grapheme_pool::GraphemePool; macro_rules! with_frame { ($width:expr, $height:expr, |$frame:ident| $body:block) => {{ let mut pool = GraphemePool::new(); let mut $frame = Frame::new($width, $height, &mut pool); $body }}; } fn white() -> PackedRgba { PackedRgba::rgb(0xFF, 0xFF, 0xFF) } fn gray() -> PackedRgba { PackedRgba::rgb(0x80, 0x80, 0x80) } #[test] fn test_help_overlay_renders_border() { with_frame!(80, 24, |frame| { let registry = build_registry(); render_help_overlay( &mut frame, Rect::new(0, 0, 80, 24), ®istry, &Screen::Dashboard, gray(), white(), gray(), 0, ); // The overlay should have non-empty cells in the center area. let has_content = (20..60u16).any(|x| { (8..16u16).any(|y| { let cell = frame.buffer.get(x, y).unwrap(); !cell.is_empty() }) }); assert!(has_content, "Expected help overlay in center area"); }); } #[test] fn test_help_overlay_tiny_terminal_noop() { with_frame!(15, 4, |frame| { let registry = build_registry(); render_help_overlay( &mut frame, Rect::new(0, 0, 15, 4), ®istry, &Screen::Dashboard, gray(), white(), gray(), 0, ); }); } }