use defaults::*;
use prototty_input::inputs::*;
use prototty_input::*;
use prototty_render::*;
use std::cmp;
use text_info::*;
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct MenuEntry<T: Copy> {
pub name: String,
pub value: T,
}
impl<T: Copy, S: Into<String>> From<(S, T)> for MenuEntry<T> {
fn from((s, t): (S, T)) -> Self {
Self {
name: s.into(),
value: t,
}
}
}
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Menu<T: Copy> {
pub entries: Vec<MenuEntry<T>>,
pub size: Size,
pub selected_info: TextInfo,
pub normal_info: TextInfo,
}
fn selected_info() -> TextInfo {
TextInfo::default()
.bold()
.foreground_colour(DEFAULT_BG)
.background_colour(DEFAULT_FG)
}
impl<T: Copy> Menu<T> {
pub fn new<S, V>(mut e: Vec<(S, T)>, size: V) -> Self
where
S: Into<String>,
V: Into<Size>,
{
Self {
entries: e.drain(..).map(Into::into).collect(),
size: size.into(),
normal_info: Default::default(),
selected_info: selected_info(),
}
}
pub fn smallest<S>(mut e: Vec<(S, T)>) -> Self
where
S: Into<String>,
{
let entries: Vec<MenuEntry<T>> = e.drain(..).map(Into::into).collect();
let width = entries.iter().fold(0, |acc, e| cmp::max(acc, e.name.len()));
let height = entries.len();
Self {
entries,
size: Size::new(width as u32, height as u32),
normal_info: Default::default(),
selected_info: selected_info(),
}
}
}
pub enum MenuOutput<T> {
Quit,
Cancel,
Finalise(T),
}
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct MenuInstance<T: Copy> {
menu: Menu<T>,
index: usize,
}
impl<T: Copy> MenuInstance<T> {
pub fn new(menu: Menu<T>) -> Option<Self> {
Self::with_index(menu, 0)
}
pub fn with_index(menu: Menu<T>, index: usize) -> Option<Self> {
if index < menu.entries.len() {
Some(Self { menu, index })
} else {
None
}
}
pub fn menu(&self) -> &Menu<T> {
&self.menu
}
pub fn into_menu(self) -> Menu<T> {
self.menu
}
pub fn index(&self) -> usize {
self.index
}
pub fn set_index(&mut self, index: usize) {
if index < self.menu.entries.len() {
self.index = index;
}
}
pub fn up(&mut self) {
match self.index.checked_sub(1) {
Some(index) => self.index = index,
None => self.index = self.menu.entries.len() - 1,
}
}
pub fn down(&mut self) {
if self.index < self.menu.entries.len() - 1 {
self.index += 1;
} else {
self.index = 0;
}
}
pub fn selected(&self) -> T {
self.menu.entries[self.index].value
}
pub fn tick<I>(&mut self, inputs: I) -> Option<MenuOutput<T>>
where
I: IntoIterator<Item = Input>,
{
for input in inputs {
match input {
ETX => return Some(MenuOutput::Quit),
ESCAPE => return Some(MenuOutput::Cancel),
RETURN => {
return Some(MenuOutput::Finalise(self.selected()));
}
Input::Up => self.up(),
Input::Down => self.down(),
_ => (),
}
}
None
}
pub fn tick_with_mouse<'a, I, M>(&mut self, inputs: I, view: &'a M) -> Option<MenuOutput<T>>
where
I: IntoIterator<Item = Input>,
M: MenuIndexFromScreenCoord,
{
for input in inputs {
match input {
ETX => return Some(MenuOutput::Quit),
ESCAPE => return Some(MenuOutput::Cancel),
RETURN => {
return Some(MenuOutput::Finalise(self.selected()));
}
Input::Up
| Input::MouseScroll {
direction: ScrollDirection::Up,
..
} => self.up(),
Input::Down
| Input::MouseScroll {
direction: ScrollDirection::Down,
..
} => self.down(),
Input::MouseMove { coord, .. } => {
if let Some(index) = view.menu_index_from_screen_coord(&self.menu, coord) {
self.set_index(index);
}
}
Input::MousePress { coord, .. } => {
if let Some(index) = view.menu_index_from_screen_coord(&self.menu, coord) {
self.set_index(index);
return Some(MenuOutput::Finalise(self.selected()));
}
}
_ => (),
}
}
None
}
}
pub trait MenuIndexFromScreenCoord {
fn menu_index_from_screen_coord<'a, T: Copy>(
&self,
menu: &'a Menu<T>,
coord: Coord,
) -> Option<usize>;
}
impl MenuIndexFromScreenCoord for () {
fn menu_index_from_screen_coord<'a, T: Copy>(
&self,
_menu: &'a Menu<T>,
_coord: Coord,
) -> Option<usize> {
None
}
}
pub struct DefaultMenuInstanceView {
last_coord: Coord,
}
impl DefaultMenuInstanceView {
pub fn new() -> Self {
Self {
last_coord: Coord::new(0, 0),
}
}
}
impl MenuIndexFromScreenCoord for DefaultMenuInstanceView {
fn menu_index_from_screen_coord<'a, T: Copy>(
&self,
menu: &'a Menu<T>,
coord: Coord,
) -> Option<usize> {
let rel_coord = coord - self.last_coord;
if rel_coord.x < 0
|| rel_coord.y < 0
|| rel_coord.x >= menu.size.x() as i32
|| rel_coord.y >= menu.entries.len() as i32
{
None
} else {
Some(rel_coord.y as usize)
}
}
}
impl<T: Copy> View<MenuInstance<T>> for DefaultMenuInstanceView {
fn view<G: ViewGrid>(
&mut self,
value: &MenuInstance<T>,
offset: Coord,
depth: i32,
grid: &mut G,
) {
self.last_coord = offset;
for (i, entry) in value.menu.entries.iter().enumerate() {
if i == value.menu.size.y() as usize {
break;
}
let info = if i == value.index {
&value.menu.selected_info
} else {
&value.menu.normal_info
};
for (j, ch) in entry.name.chars().enumerate() {
if j == value.menu.size.x() as usize {
break;
}
let coord = offset + Coord::new(j as i32, i as i32);
grid.set_cell(coord, depth, info.view_cell_info(ch));
}
}
}
}
impl<T: Copy> ViewSize<MenuInstance<T>> for DefaultMenuInstanceView {
fn size(&mut self, data: &MenuInstance<T>) -> Size {
data.menu.size
}
}