use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
Frame,
};
use crate::shapes::ShapeType;
#[derive(Debug, Clone)]
pub struct ShapeMenu {
pub visible: bool,
selected_index: usize,
}
impl Default for ShapeMenu {
fn default() -> Self {
Self::new()
}
}
impl ShapeMenu {
pub fn new() -> Self {
Self {
visible: false,
selected_index: 0,
}
}
pub fn show(&mut self) {
self.visible = true;
}
pub fn hide(&mut self) {
self.visible = false;
}
pub fn toggle(&mut self) {
self.visible = !self.visible;
}
pub fn selected_shape_type(&self) -> ShapeType {
ShapeType::from_grid_index(self.selected_index).unwrap_or(ShapeType::Circle)
}
pub fn selected_index(&self) -> usize {
self.selected_index
}
pub fn select_up(&mut self) {
if self.selected_index >= 3 {
self.selected_index -= 3;
}
}
pub fn select_down(&mut self) {
if self.selected_index < 3 {
self.selected_index += 3;
}
}
pub fn select_left(&mut self) {
if !self.selected_index.is_multiple_of(3) {
self.selected_index -= 1;
}
}
pub fn select_right(&mut self) {
if self.selected_index % 3 < 2 {
self.selected_index += 1;
}
}
pub fn set_selected(&mut self, index: usize) {
if index < 6 {
self.selected_index = index;
}
}
pub fn handle_click(
&mut self,
local_x: u16,
local_y: u16,
menu_width: u16,
menu_height: u16,
) -> Option<ShapeType> {
if local_x < 1 || local_y < 2 {
return None;
}
let inner_x = local_x - 1;
let inner_y = local_y - 2;
let inner_width = menu_width.saturating_sub(2);
let inner_height = menu_height.saturating_sub(3);
if inner_width == 0 || inner_height == 0 {
return None;
}
let cell_width = inner_width / 3;
let cell_height = inner_height / 2;
if cell_width == 0 || cell_height == 0 {
return None;
}
let col = (inner_x / cell_width).min(2) as usize;
let row = (inner_y / cell_height).min(1) as usize;
let index = row * 3 + col;
if index < 6 {
self.selected_index = index;
ShapeType::from_grid_index(index)
} else {
None
}
}
pub fn render(&self, frame: &mut Frame, area: Rect) {
if !self.visible {
return;
}
let popup_area = centered_rect(60, 40, area);
frame.render_widget(Clear, popup_area);
let block = Block::default()
.title(" Select Shape (Arrows to navigate, Enter to place) ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Green))
.style(Style::default().bg(Color::Black));
let inner = block.inner(popup_area);
frame.render_widget(block, popup_area);
let cell_height = inner.height / 2;
let cell_width = inner.width / 3;
for row in 0..2 {
for col in 0..3 {
let index = row * 3 + col;
let shape_type = ShapeType::from_grid_index(index).unwrap_or(ShapeType::Circle);
let is_selected = index == self.selected_index;
let cell_x = inner.x + (col as u16 * cell_width);
let cell_y = inner.y + (row as u16 * cell_height);
let cell_area = Rect {
x: cell_x,
y: cell_y,
width: cell_width,
height: cell_height,
};
self.render_cell(frame, cell_area, shape_type, is_selected);
}
}
}
fn render_cell(&self, frame: &mut Frame, area: Rect, shape_type: ShapeType, is_selected: bool) {
let style = if is_selected {
Style::default()
.fg(Color::LightGreen)
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Green)
};
let name = shape_type.name();
let icon = shape_type.short_name();
let mut lines = Vec::new();
if area.height > 3 {
lines.push(Line::from(""));
}
let icon_line = format!(" {} ", icon);
lines.push(Line::from(Span::styled(
center_text(&icon_line, area.width as usize),
style,
)));
lines.push(Line::from(Span::styled(
center_text(name, area.width as usize),
style,
)));
if is_selected {
lines.push(Line::from(Span::styled(
center_text("[*]", area.width as usize),
style,
)));
}
let para = Paragraph::new(lines);
frame.render_widget(para, area);
}
}
fn center_text(text: &str, width: usize) -> String {
let text_len = text.chars().count();
if text_len >= width {
text.to_string()
} else {
let padding = (width - text_len) / 2;
format!("{}{}{}", " ".repeat(padding), text, " ".repeat(padding))
}
}
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let popup_width = (area.width * percent_x / 100).max(30);
let popup_height = (area.height * percent_y / 100).max(12);
let vertical = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length((area.height.saturating_sub(popup_height)) / 2),
Constraint::Length(popup_height),
Constraint::Min(0),
])
.split(area);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length((area.width.saturating_sub(popup_width)) / 2),
Constraint::Length(popup_width),
Constraint::Min(0),
])
.split(vertical[1])[1]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shape_menu_navigation() {
let mut menu = ShapeMenu::new();
assert_eq!(menu.selected_index(), 0);
menu.select_right();
assert_eq!(menu.selected_index(), 1);
menu.select_down();
assert_eq!(menu.selected_index(), 4);
menu.select_left();
assert_eq!(menu.selected_index(), 3);
menu.select_up();
assert_eq!(menu.selected_index(), 0);
}
#[test]
fn test_shape_menu_bounds() {
let mut menu = ShapeMenu::new();
menu.select_left();
assert_eq!(menu.selected_index(), 0);
menu.select_up();
assert_eq!(menu.selected_index(), 0);
menu.set_selected(5);
menu.select_right();
assert_eq!(menu.selected_index(), 5);
menu.select_down();
assert_eq!(menu.selected_index(), 5);
}
#[test]
fn test_selected_shape_type() {
let mut menu = ShapeMenu::new();
assert_eq!(menu.selected_shape_type(), ShapeType::Circle);
menu.set_selected(3); assert_eq!(menu.selected_shape_type(), ShapeType::Star);
menu.set_selected(5); assert_eq!(menu.selected_shape_type(), ShapeType::LineVertical);
}
}