use std::sync::RwLock;
use std::sync::{mpsc::Sender, Arc};
use termwiz::input::{KeyCode, KeyEvent, MouseButtons, MouseEvent};
use termwiz::surface::{Change, Position, Surface};
use crate::event::Event;
use crate::layout::Rect;
use crate::widget::{RenderCtx, UpdateCtx};
use crate::{event::UserEvent, Widget};
use termwiz::{
cell::AttributeChange,
color::{AnsiColor, ColorAttribute},
};
pub trait MenuAction<U>: Fn(usize, &mut Menu<U>, Arc<Sender<UserEvent<U>>>) {}
impl<C, U> MenuAction<U> for C where C: Fn(usize, &mut Menu<U>, Arc<Sender<UserEvent<U>>>) {}
pub struct Menu<U> {
title: String,
items: Vec<(String, String, Box<dyn MenuAction<U>>)>,
active: usize,
}
impl<U> Menu<U> {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
items: vec![],
active: 0,
}
}
pub fn add_item(
&mut self,
title: impl Into<String>,
tag: impl Into<String>,
action: impl MenuAction<U> + 'static,
) {
self.items
.push((title.into(), tag.into(), Box::new(action)));
}
pub fn next(&mut self) {
self.active = (self.active + 1) % self.items.len().max(1);
}
pub fn prev(&mut self) {
self.active = (self.active + self.items.len() - 1) % self.items.len().max(1);
}
pub fn select(&mut self, event_tx: Arc<Sender<UserEvent<U>>>) {
if let Some((_, _, action)) = self.items.get(self.active) {
let func = action as *const dyn MenuAction<U>;
unsafe { (*func)(self.active, self, event_tx.clone()) };
}
}
pub fn item(&self, index: usize) -> Option<&(String, String, Box<dyn MenuAction<U>>)> {
self.items.get(index)
}
pub fn tag(&self, index: usize) -> Option<&str> {
self.items.get(index).map(|(_, tag, _)| tag.as_str())
}
pub fn update_tag(&mut self, index: usize, f: impl Fn(&str) -> String) {
if let Some((_, t, _)) = self.items.get_mut(index) {
*t = f(t);
}
}
pub fn entry(&self, index: usize) -> Option<&str> {
self.items.get(index).map(|(title, _, _)| title.as_str())
}
pub fn update_entry(&mut self, index: usize, f: impl Fn(&str) -> String) {
if let Some((t, _, _)) = self.items.get_mut(index) {
*t = f(t);
}
}
pub fn clear(&mut self) {
self.items.clear();
}
pub fn remove(&mut self, index: usize) {
self.items.remove(index);
}
pub fn update_menu_title(&mut self, f: impl Fn(&str) -> String) {
self.title = f(&self.title);
}
}
impl<U, S> Widget<U, S> for Menu<U> {
fn render<'r>(
&self,
_cx: &RenderCtx<'r, U, S>,
surface: &mut Surface,
) -> Option<Vec<(Rect, Arc<RwLock<dyn Widget<U, S>>>)>> {
let dims = surface.dimensions();
surface.add_changes(vec![Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(0),
}]);
let line = format!("{:^width$}", self.title, width = dims.0);
surface.add_changes(vec![
Change::Attribute(AttributeChange::Foreground(AnsiColor::Black.into())),
Change::Attribute(AttributeChange::Background(AnsiColor::White.into())),
Change::Text(line),
Change::Attribute(AttributeChange::Foreground(Default::default())),
Change::Attribute(AttributeChange::Background(Default::default())),
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(2),
},
]);
surface.add_changes(vec![]);
for (i, (item, tag, _)) in self.items.iter().enumerate() {
if i == self.active {
surface.add_changes(vec![
Change::Attribute(AttributeChange::Foreground(AnsiColor::Black.into())),
Change::Attribute(AttributeChange::Background(AnsiColor::White.into())),
]);
}
let line = format!("{item} {tag}");
surface.add_changes(vec![
Change::Text(format!("{:^width$}", line, width = dims.0)),
Change::CursorPosition {
x: Position::Relative(dims.0 as isize),
y: Position::Relative(0),
},
Change::Attribute(AttributeChange::Foreground(ColorAttribute::Default)),
Change::Attribute(AttributeChange::Background(ColorAttribute::Default)),
Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(1),
},
]);
}
None
}
fn update<'u>(
&mut self,
cx: &mut UpdateCtx<'u, U, S>,
event: Event<U>,
) -> crate::error::Result<()> {
match event {
Event::Key(KeyEvent { key, .. }) => match key {
KeyCode::UpArrow => self.prev(),
KeyCode::DownArrow => self.next(),
KeyCode::Enter => self.select(cx.tx.clone()),
_ => {}
},
Event::Mouse(MouseEvent {
y, mouse_buttons, ..
}) => {
if mouse_buttons == MouseButtons::LEFT {
if (y as usize) <= self.items.len() + 1 && y >= 2 {
self.active = y as usize - 2;
self.select(cx.tx.clone());
}
} else if mouse_buttons == MouseButtons::NONE {
if (y as usize) <= self.items.len() + 1 && y >= 2 {
self.active = y as usize - 2;
}
}
}
_ => {}
}
Ok(())
}
}