use crate::ui::core::menu::Menu;
use crate::ui::core::widget::{InputEvent, Widget};
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
Frame,
};
pub struct MenuWidget {
items: Vec<String>,
selected: usize,
title: Option<String>,
selected_style: Style,
normal_style: Style,
}
impl MenuWidget {
pub fn new(items: Vec<String>) -> Self {
Self {
items,
selected: 0,
title: None,
selected_style: Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
normal_style: Style::default(),
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_selected(mut self, selected: usize) -> Self {
self.selected = selected.min(self.items.len().saturating_sub(1));
self
}
pub fn render(&self, frame: &mut Frame, area: Rect) {
self.render_to_buffer(area, frame.buffer_mut());
}
pub fn render_to_buffer(&self, area: Rect, buf: &mut Buffer) {
let mut y = area.y;
if let Some(title) = &self.title {
buf.set_string(
area.x,
y,
title,
Style::default().add_modifier(Modifier::BOLD),
);
y += 2; }
for (i, item) in self.items.iter().enumerate() {
if y >= area.y + area.height {
break; }
let line = if i == self.selected {
Line::from(vec![
Span::styled("> ", self.selected_style),
Span::styled(item, self.selected_style),
])
} else {
Line::from(vec![Span::raw(" "), Span::styled(item, self.normal_style)])
};
buf.set_line(area.x, y, &line, area.width);
y += 1;
}
}
pub fn min_size(&self) -> (u16, u16) {
let height = self.items.len() as u16 + if self.title.is_some() { 2 } else { 0 };
let width = self.items.iter().map(|s| s.len()).max().unwrap_or(0) as u16 + 2; (width, height)
}
}
impl Widget for MenuWidget {
fn handle_input(&mut self, event: InputEvent) -> bool {
match event {
InputEvent::Up => {
self.move_up();
true
}
InputEvent::Down => {
self.move_down();
true
}
_ => false,
}
}
fn widget_type(&self) -> &'static str {
"MenuWidget"
}
}
impl Menu for MenuWidget {
fn selected(&self) -> usize {
self.selected
}
fn selected_item(&self) -> Option<&str> {
self.items.get(self.selected).map(|s| s.as_str())
}
fn move_up(&mut self) {
if self.selected > 0 {
self.selected -= 1;
}
}
fn move_down(&mut self) {
if self.selected < self.items.len().saturating_sub(1) {
self.selected += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_menu_creation() {
let menu = MenuWidget::new(vec!["Item 1".into(), "Item 2".into()]);
assert_eq!(menu.selected(), 0);
assert_eq!(menu.selected_item(), Some("Item 1"));
}
#[test]
fn test_menu_navigation() {
let mut menu = MenuWidget::new(vec!["Item 1".into(), "Item 2".into(), "Item 3".into()]);
assert_eq!(menu.selected(), 0);
menu.move_down();
assert_eq!(menu.selected(), 1);
menu.move_down();
assert_eq!(menu.selected(), 2);
menu.move_down(); assert_eq!(menu.selected(), 2);
menu.move_up();
assert_eq!(menu.selected(), 1);
}
#[test]
fn test_handle_input() {
let mut menu = MenuWidget::new(vec!["Item 1".into(), "Item 2".into()]);
assert!(menu.handle_input(InputEvent::Down));
assert_eq!(menu.selected(), 1);
assert!(menu.handle_input(InputEvent::Up));
assert_eq!(menu.selected(), 0);
assert!(!menu.handle_input(InputEvent::Select)); }
}