refactor: split common/mod.rs into per-widget modules

This commit is contained in:
teernisse
2026-02-12 16:25:22 -05:00
parent eb5b464d03
commit 28ce63f818
8 changed files with 1152 additions and 1109 deletions

View File

@@ -0,0 +1,173 @@
//! 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),
&registry,
&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),
&registry,
&Screen::Dashboard,
gray(),
white(),
gray(),
0,
);
});
}
}