use super::types::MenuItem;
use crate::event::Key;
use crate::render::Cell;
use crate::style::Color;
use crate::widget::theme::DARK_BG;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
pub struct ContextMenu {
pub(crate) items: Vec<MenuItem>,
x: u16,
y: u16,
pub(crate) selected: usize,
visible: bool,
pub(crate) bg: Color,
pub(crate) fg: Color,
selected_bg: Color,
selected_fg: Color,
props: WidgetProps,
}
impl Default for ContextMenu {
fn default() -> Self {
Self::new()
}
}
impl ContextMenu {
pub fn new() -> Self {
Self {
items: Vec::new(),
x: 0,
y: 0,
selected: 0,
visible: false,
bg: DARK_BG,
fg: Color::WHITE,
selected_bg: Color::rgb(60, 100, 180),
selected_fg: Color::WHITE,
props: WidgetProps::new(),
}
}
pub fn item(mut self, item: MenuItem) -> Self {
self.items.push(item);
self
}
pub fn items(mut self, items: Vec<MenuItem>) -> Self {
self.items.extend(items);
self
}
pub fn show(&mut self, x: u16, y: u16) {
self.x = x;
self.y = y;
self.visible = true;
self.selected = 0;
}
pub fn hide(&mut self) {
self.visible = false;
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn handle_key(&mut self, key: &Key) -> bool {
if !self.visible {
return false;
}
match key {
Key::Up | Key::Char('k') => {
if self.selected > 0 {
self.selected -= 1;
}
true
}
Key::Down | Key::Char('j') => {
if self.selected < self.items.len().saturating_sub(1) {
self.selected += 1;
}
true
}
Key::Enter | Key::Char(' ') => {
if let Some(item) = self.items.get(self.selected) {
item.execute();
}
self.hide();
true
}
Key::Escape => {
self.hide();
true
}
_ => false,
}
}
}
impl View for ContextMenu {
crate::impl_view_meta!("ContextMenu");
fn render(&self, ctx: &mut RenderContext) {
if !self.visible || self.items.is_empty() {
return;
}
let width = self.items.iter().map(|i| i.label.len()).max().unwrap_or(10) as u16 + 4;
let height = self.items.len() as u16 + 2;
let x = self.x.min(ctx.area.width.saturating_sub(width));
let y = self.y.min(ctx.area.height.saturating_sub(height));
for dy in 0..height {
for dx in 0..width {
let ch = if dy == 0 && dx == 0 {
'┌'
} else if dy == 0 && dx == width - 1 {
'┐'
} else if dy == height - 1 && dx == 0 {
'└'
} else if dy == height - 1 && dx == width - 1 {
'┘'
} else if dy == 0 || dy == height - 1 {
'─'
} else if dx == 0 || dx == width - 1 {
'│'
} else {
' '
};
let mut cell = Cell::new(ch);
cell.bg = Some(self.bg);
cell.fg = Some(self.fg);
ctx.set(x + dx, y + dy, cell);
}
}
for (i, item) in self.items.iter().enumerate() {
let item_y = y + 1 + i as u16;
let is_selected = i == self.selected;
let bg = if is_selected {
self.selected_bg
} else {
self.bg
};
let fg = if is_selected {
self.selected_fg
} else {
self.fg
};
for dx in 1..width - 1 {
let mut cell = Cell::new(' ');
cell.bg = Some(bg);
ctx.set(x + dx, item_y, cell);
}
let mut dx: u16 = 0;
for ch in item.label.chars() {
let cw = crate::utils::char_width(ch) as u16;
if dx + cw + 2 >= width - 1 {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
cell.bg = Some(bg);
ctx.set(x + 2 + dx, item_y, cell);
dx += cw;
}
}
}
}
impl_styled_view!(ContextMenu);
impl_props_builders!(ContextMenu);