use std::cmp::min;
use std::collections::hash_map::Entry;
use crossterm::style::{self, Stylize};
use super::{Panel, Scroll};
use crate::types::*;
#[derive(Debug)]
pub struct Menu<T>
where T: Clone + Menuable
{
pub panel: Panel,
pub header: Option<String>,
pub items: LockVec<T>,
pub start_row: u16, pub top_row: u16, pub selected: u16, pub active: bool,
}
impl<T: Clone + Menuable> Menu<T> {
pub fn new(panel: Panel, header: Option<String>, items: LockVec<T>) -> Self {
return Self {
panel: panel,
header: header,
items: items,
start_row: 0,
top_row: 0,
selected: 0,
active: false,
};
}
pub fn redraw(&mut self) {
self.panel.redraw();
self.update_items();
}
pub fn update_items(&mut self) {
self.start_row = self.print_header();
if self.selected < self.start_row {
self.selected = self.start_row;
}
let (map, _, order) = self.items.borrow();
if !order.is_empty() {
let current_selected = self.get_menu_idx(self.selected);
let list_len = order.len();
if current_selected >= list_len {
self.selected = (self.selected as usize - (current_selected - list_len) - 1) as u16;
}
for i in self.start_row..self.panel.get_rows() {
if let Some(elem_id) = order.get(self.get_menu_idx(i)) {
let elem = map.get(elem_id).expect("Could not retrieve menu item.");
if i == self.selected || !elem.is_played() {
let style = if !elem.is_played() {
style::ContentStyle::new()
.with(self.panel.colors.bold.0)
.on(self.panel.colors.bold.1)
.attribute(style::Attribute::Bold)
} else {
style::ContentStyle::new()
.with(self.panel.colors.normal.0)
.on(self.panel.colors.normal.1)
};
self.panel.write_line(
i,
elem.get_title(self.panel.get_cols() as usize),
Some(style),
);
} else {
self.panel.write_line(
i,
elem.get_title(self.panel.get_cols() as usize),
None,
);
}
} else {
break;
}
}
}
}
fn print_header(&mut self) -> u16 {
if let Some(header) = &self.header {
return self.panel.write_wrap_line(0, header, None) + 2;
} else {
return 0;
}
}
pub fn scroll(&mut self, lines: Scroll) {
let list_len = self.items.len(true) as u16;
if list_len == 0 {
return;
}
match lines {
Scroll::Up(v) => {
let selected_adj = self.selected - self.start_row;
if v <= selected_adj {
self.unhighlight_item(self.selected);
self.selected -= v;
} else {
let list_scroll_amount = v - selected_adj;
if let Some(top) = self.top_row.checked_sub(list_scroll_amount) {
self.top_row = top;
} else {
self.top_row = 0;
}
self.selected = self.start_row;
self.panel.clear_inner();
self.update_items();
}
self.highlight_item(self.selected, self.active);
}
Scroll::Down(v) => {
if self.get_menu_idx(self.selected) >= list_len as usize - 1 {
return;
}
let n_row = self.panel.get_rows();
let select_max = if list_len < n_row - self.start_row {
self.start_row + list_len - 1
} else {
n_row - 1
};
if v <= (select_max - self.selected) {
self.unhighlight_item(self.selected);
self.selected += v;
} else {
let list_scroll_amount = v - (n_row - self.selected - 1);
let visible_rows = n_row - self.start_row;
if list_len > visible_rows {
self.top_row =
min(self.top_row + list_scroll_amount, list_len - visible_rows);
}
self.selected = select_max;
self.panel.clear_inner();
self.update_items();
}
self.highlight_item(self.selected, self.active);
}
}
}
pub fn highlight_item(&mut self, item_y: u16, active: bool) {
let el_details = self
.items
.map_single_by_index(self.get_menu_idx(item_y), |el| {
(el.get_title(self.panel.get_cols() as usize), el.is_played())
});
if let Some((title, is_played)) = el_details {
let mut style = style::ContentStyle::new();
if active {
style = style.with(self.panel.colors.highlighted_active.0).on(self
.panel
.colors
.highlighted_active
.1);
} else {
style =
style
.with(self.panel.colors.highlighted.0)
.on(self.panel.colors.highlighted.1);
}
style = if is_played {
style.attribute(style::Attribute::NormalIntensity)
} else {
style.attribute(style::Attribute::Bold)
};
self.panel.write_line(item_y, title, Some(style));
}
}
pub fn unhighlight_item(&mut self, item_y: u16) {
let el_details = self
.items
.map_single_by_index(self.get_menu_idx(item_y), |el| {
(el.get_title(self.panel.get_cols() as usize), el.is_played())
});
if let Some((title, is_played)) = el_details {
let style = if is_played {
style::ContentStyle::new()
.with(self.panel.colors.normal.0)
.on(self.panel.colors.normal.1)
} else {
style::ContentStyle::new()
.with(self.panel.colors.bold.0)
.on(self.panel.colors.bold.1)
.attribute(style::Attribute::Bold)
};
self.panel.write_line(item_y, title, Some(style));
}
}
pub fn highlight_selected(&mut self) {
self.highlight_item(self.selected, self.active);
}
pub fn activate(&mut self) {
self.active = true;
self.highlight_selected();
}
pub fn resize(&mut self, n_row: u16, n_col: u16, start_x: u16) {
self.panel.resize(n_row, n_col, start_x);
let n_row = self.panel.get_rows();
if self.selected > (n_row - 1) {
self.top_row = self.top_row + self.selected - (n_row - 1);
self.selected = n_row - 1;
}
self.redraw();
}
pub fn get_menu_idx(&self, screen_y: u16) -> usize {
return (self.top_row + screen_y - self.start_row) as usize;
}
}
impl Menu<Podcast> {
pub fn get_episodes(&self) -> LockVec<Episode> {
let index = self.get_menu_idx(self.selected);
let (borrowed_map, _, borrowed_order) = self.items.borrow();
let pod_id = borrowed_order
.get(index)
.expect("Could not retrieve podcast.");
return borrowed_map
.get(pod_id)
.expect("Could not retrieve podcast info.")
.episodes
.clone();
}
pub fn deactivate(&mut self) {
self.active = false;
self.highlight_item(self.selected, false);
}
}
impl Menu<Episode> {
pub fn deactivate(&mut self, keep_highlighted: bool) {
self.active = false;
if keep_highlighted {
self.highlight_item(self.selected, false);
} else {
self.unhighlight_item(self.selected);
}
}
}
impl Menu<NewEpisode> {
pub fn select_item(&mut self) {
let changed = self.change_item_selections(vec![self.get_menu_idx(self.selected)], None);
if changed {
self.update_items();
self.highlight_selected();
}
}
pub fn select_all_items(&mut self) {
let all_selected = self.items.map(|ep| ep.selected, false).iter().all(|x| *x);
let changed =
self.change_item_selections((0..self.items.len(false)).collect(), Some(!all_selected));
if changed {
self.update_items();
self.highlight_selected();
}
}
fn change_item_selections(&mut self, indexes: Vec<usize>, selection: Option<bool>) -> bool {
let mut changed = false;
{
let (mut borrowed_map, borrowed_order, _) = self.items.borrow();
for idx in indexes {
if let Some(ep_id) = borrowed_order.get(idx) {
if let Entry::Occupied(mut ep) = borrowed_map.entry(*ep_id) {
let ep = ep.get_mut();
match selection {
Some(sel) => ep.selected = sel,
None => ep.selected = !ep.selected,
}
changed = true;
}
}
}
}
return changed;
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use std::rc::Rc;
fn create_menu(n_row: u16, n_col: u16, top_row: u16, selected: u16) -> Menu<Episode> {
let colors = Rc::new(crate::ui::AppColors::default());
let titles = vec![
"A Very Cool Episode",
"This is a very long episode title but we'll get through it together",
"An episode with le Unicodé",
"How does an episode with emoji sound? 😉",
"Here's another title",
"Un titre, c'est moi!",
"One more just for good measure",
];
let mut items = Vec::new();
for (i, t) in titles.iter().enumerate() {
let played = i % 2 == 0;
items.push(Episode {
id: i as _,
pod_id: 1,
title: t.to_string(),
url: String::new(),
guid: String::new(),
description: String::new(),
pubdate: Some(Utc::now()),
duration: Some(12345),
path: None,
played: played,
});
}
let panel = Panel::new(
"Episodes".to_string(),
1,
colors.clone(),
n_row,
n_col,
0,
(0, 0, 0, 0),
);
return Menu {
panel: panel,
header: None,
items: LockVec::new(items),
start_row: 0,
top_row: top_row,
selected: selected,
active: true,
};
}
#[test]
fn scroll_up() {
let real_rows = 5;
let real_cols = 65;
let mut menu = create_menu(real_rows + 2, real_cols + 3, 2, 0);
menu.update_items();
menu.scroll(Scroll::Up(1));
let expected_top = menu
.items
.map_single_by_index(1, |ep| ep.get_title(real_cols as usize))
.unwrap();
let expected_bot = menu
.items
.map_single_by_index(5, |ep| ep.get_title(real_cols as usize))
.unwrap();
assert_eq!(menu.panel.get_row(0), expected_top);
assert_eq!(menu.panel.get_row(4), expected_bot);
}
#[test]
fn scroll_down() {
let real_rows = 5;
let real_cols = 65;
let mut menu = create_menu(real_rows + 2, real_cols + 3, 0, 4);
menu.update_items();
menu.scroll(Scroll::Down(1));
let expected_top = menu
.items
.map_single_by_index(1, |ep| ep.get_title(real_cols as usize))
.unwrap();
let expected_bot = menu
.items
.map_single_by_index(5, |ep| ep.get_title(real_cols as usize))
.unwrap();
assert_eq!(menu.panel.get_row(0), expected_top);
assert_eq!(menu.panel.get_row(4), expected_bot);
}
#[test]
fn resize_bigger() {
let real_rows = 5;
let real_cols = 65;
let mut menu = create_menu(real_rows + 2, real_cols + 3, 0, 4);
menu.update_items();
menu.resize(real_rows + 2 + 5, real_cols + 3 + 5, 0);
menu.update_items();
assert_eq!(menu.top_row, 0);
assert_eq!(menu.selected, 4);
let non_empty: Vec<String> = menu
.panel
.buffer
.iter()
.filter_map(|x| if x.is_empty() { None } else { Some(x.clone()) })
.collect();
assert_eq!(non_empty.len(), menu.items.len(true));
}
#[test]
fn resize_smaller() {
let real_rows = 7;
let real_cols = 65;
let mut menu = create_menu(real_rows + 2, real_cols + 3, 0, 6);
menu.update_items();
menu.resize(real_rows + 2 - 2, real_cols + 3 - 5, 0);
menu.update_items();
assert_eq!(menu.top_row, 2);
assert_eq!(menu.selected, 4);
let non_empty: Vec<String> = menu
.panel
.buffer
.iter()
.filter_map(|x| if x.is_empty() { None } else { Some(x.clone()) })
.collect();
assert_eq!(non_empty.len(), (real_rows - 2) as usize);
}
#[test]
fn chop_accent() {
let real_rows = 5;
let real_cols = 25;
let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 0);
menu.update_items();
let expected = " An episode with le Unicod ".to_string();
assert_eq!(menu.panel.get_row(2), expected);
}
#[test]
fn chop_emoji() {
let real_rows = 5;
let real_cols = 38;
let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 0);
menu.update_items();
let expected = " How does an episode with emoji sound? ".to_string();
assert_eq!(menu.panel.get_row(3), expected);
}
}