/* * meli - ui crate. * * Copyright 2017-2018 Manos Pitsidianakis * * This file is part of meli. * * meli is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * meli is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ use super::*; mod compact; pub use self::compact::*; mod thread; pub use self::thread::*; mod plain; pub use self::plain::*; #[derive(Debug)] struct AccountMenuEntry { name: String, // Index in the config account vector. index: usize, } trait ListingTrait { fn coordinates(&self) -> (usize, usize, Option); fn set_coordinates(&mut self, (usize, usize, Option)); } #[derive(Debug)] pub enum ListingComponent { Plain(PlainListing), Threaded(ThreadListing), Compact(CompactListing), } use ListingComponent::*; impl ListingTrait for ListingComponent { fn coordinates(&self) -> (usize, usize, Option) { match &self { Compact(ref l) => l.coordinates(), Plain(ref l) => l.coordinates(), Threaded(ref l) => l.coordinates(), } } fn set_coordinates(&mut self, c: (usize, usize, Option)) { match self { Compact(ref mut l) => l.set_coordinates(c), Plain(ref mut l) => l.set_coordinates(c), Threaded(ref mut l) => l.set_coordinates(c), } } } #[derive(Debug)] pub struct Listing { component: ListingComponent, accounts: Vec, dirty: bool, visible: bool, cursor_pos: (usize, usize), id: ComponentId, show_divider: bool, menu_visibility: bool, /// This is the width of the right container to the entire width. ratio: usize, // right/(container width) * 100 } impl fmt::Display for Listing { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self.component { Compact(ref l) => write!(f, "{}", l), Plain(ref l) => write!(f, "{}", l), Threaded(ref l) => write!(f, "{}", l), } } } impl Component for Listing { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if !self.is_dirty() { return; } if !is_valid_area!(area) { return; } let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); let total_cols = get_x(bottom_right) - get_x(upper_left); let right_component_width = match self.menu_visibility { true => (self.ratio * total_cols) / 100, false => total_cols, }; let mid = get_x(bottom_right) - right_component_width; if self.dirty && self.show_divider && mid != get_x(upper_left) { for i in get_y(upper_left)..=get_y(bottom_right) { grid[(mid, i)].set_ch(VERT_BOUNDARY); grid[(mid, i)].set_fg(Color::Default); grid[(mid, i)].set_bg(Color::Default); } context .dirty_areas .push_back(((mid, get_y(upper_left)), (mid, get_y(bottom_right)))); } if right_component_width == total_cols { match self.component { Compact(ref mut l) => l.draw(grid, area, context), Plain(ref mut l) => l.draw(grid, area, context), Threaded(ref mut l) => l.draw(grid, area, context), } } else if right_component_width == 0 { self.draw_menu(grid, area, context); } else { self.draw_menu(grid, (upper_left, (mid, get_y(bottom_right))), context); match self.component { Compact(ref mut l) => { l.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context) } Plain(ref mut l) => { l.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context) } Threaded(ref mut l) => { l.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context) } } } } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { if match self.component { Plain(ref mut l) => l.process_event(event, context), Compact(ref mut l) => l.process_event(event, context), Threaded(ref mut l) => l.process_event(event, context), } { return true; } let shortcuts = self.get_shortcuts(context); match *event { UIEvent::Input(ref k) if k == shortcuts["next_folder"] || k == shortcuts["prev_folder"] => { let folder_length = context.accounts[self.cursor_pos.0].len(); match k { k if k == shortcuts["next_folder"] && folder_length > 0 => { if self.cursor_pos.1 < folder_length - 1 { self.cursor_pos.1 += 1; self.component.set_coordinates(( self.cursor_pos.0, self.cursor_pos.1, None, )); self.set_dirty(); } else { return true; } } k if k == shortcuts["prev_folder"] => { if self.cursor_pos.1 > 0 { self.cursor_pos.1 -= 1; self.component.set_coordinates(( self.cursor_pos.0, self.cursor_pos.1, None, )); self.set_dirty(); } else { return true; } } _ => return false, } let folder_hash = context.accounts[self.cursor_pos.0].folders_order[self.cursor_pos.1]; // Inform State that we changed the current folder view. context .replies .push_back(UIEvent::RefreshMailbox((self.cursor_pos.0, folder_hash))); return true; } UIEvent::Input(ref k) if k == shortcuts["next_account"] || k == shortcuts["prev_account"] => { match k { k if k == shortcuts["next_account"] => { if self.cursor_pos.0 < self.accounts.len() - 1 { self.cursor_pos = (self.cursor_pos.0 + 1, 0); self.component.set_coordinates((self.cursor_pos.0, 0, None)); self.set_dirty(); } else { return true; } } k if k == shortcuts["prev_account"] => { if self.cursor_pos.0 > 0 { self.cursor_pos = (self.cursor_pos.0 - 1, 0); self.component.set_coordinates((self.cursor_pos.0, 0, None)); self.set_dirty(); } else { return true; } } _ => return false, } let folder_hash = context.accounts[self.cursor_pos.0].folders_order[self.cursor_pos.1]; // Inform State that we changed the current folder view. context .replies .push_back(UIEvent::RefreshMailbox((self.cursor_pos.0, folder_hash))); return true; } UIEvent::Action(ref action) => match action { Action::Listing(ListingAction::SetPlain) => { let new_l = match self.component { Plain(_) => { return true; } Threaded(ref mut l) => { let mut new_l = PlainListing::default(); new_l.set_coordinates(l.coordinates()); new_l } Compact(ref mut l) => { let mut new_l = PlainListing::default(); new_l.set_coordinates(l.coordinates()); new_l } }; self.component = Plain(new_l); return true; } Action::Listing(ListingAction::SetThreaded) => { let new_l = match self.component { Threaded(_) => { return true; } Plain(ref mut l) => { let mut new_l = ThreadListing::default(); new_l.set_coordinates(l.coordinates()); new_l } Compact(ref mut l) => { let mut new_l = ThreadListing::default(); new_l.set_coordinates(l.coordinates()); new_l } }; self.component = Threaded(new_l); return true; } Action::Listing(ListingAction::SetCompact) => { let new_l = match self.component { Compact(_) => { return true; } Threaded(ref mut l) => { let mut new_l = CompactListing::default(); new_l.set_coordinates(l.coordinates()); new_l } Plain(ref mut l) => { let mut new_l = CompactListing::default(); new_l.set_coordinates(l.coordinates()); new_l } }; self.component = Compact(new_l); return true; } _ => {} }, UIEvent::RefreshMailbox((idxa, folder_hash)) => { self.cursor_pos = ( idxa, context.accounts[idxa] .folders_order .iter() .position(|&h| h == folder_hash) .unwrap_or(0), ); self.dirty = true; } UIEvent::ChangeMode(UIMode::Normal) => { self.dirty = true; } UIEvent::Resize => { self.dirty = true; } UIEvent::Input(ref k) if k == shortcuts["toggle-menu-visibility"] => { self.menu_visibility = !self.menu_visibility; self.set_dirty(); } UIEvent::Input(ref k) if k == shortcuts["new_mail"] => { context .replies .push_back(UIEvent::Action(Tab(NewDraft(self.cursor_pos.0)))); return true; } UIEvent::StartupCheck(_) => { self.dirty = true; } UIEvent::MailboxUpdate(_) => { self.dirty = true; } _ => {} } false } fn is_dirty(&self) -> bool { self.dirty || match self.component { Compact(ref l) => l.is_dirty(), Plain(ref l) => l.is_dirty(), Threaded(ref l) => l.is_dirty(), } } fn set_dirty(&mut self) { self.dirty = true; match self.component { Compact(ref mut l) => l.set_dirty(), Plain(ref mut l) => l.set_dirty(), Threaded(ref mut l) => l.set_dirty(), } } fn get_shortcuts(&self, context: &Context) -> ShortcutMap { let mut map = match self.component { Compact(ref l) => l.get_shortcuts(context), Plain(ref l) => l.get_shortcuts(context), Threaded(ref l) => l.get_shortcuts(context), }; let config_map = context.settings.shortcuts.listing.key_values(); map.insert( "new_mail", if let Some(key) = config_map.get("new_mail") { (*key).clone() } else { Key::Char('m') }, ); map.insert( "prev_folder", if let Some(key) = config_map.get("prev_folder") { (*key).clone() } else { Key::Char('J') }, ); map.insert( "next_folder", if let Some(key) = config_map.get("next_folder") { (*key).clone() } else { Key::Char('K') }, ); map.insert( "prev_account", if let Some(key) = config_map.get("prev_account") { (*key).clone() } else { Key::Char('h') }, ); map.insert( "next_account", if let Some(key) = config_map.get("next_account") { (*key).clone() } else { Key::Char('l') }, ); map.insert("toggle-menu-visibility", Key::Char('`')); map } fn id(&self) -> ComponentId { match self.component { Compact(ref l) => l.id(), Plain(ref l) => l.id(), Threaded(ref l) => l.id(), } } fn set_id(&mut self, id: ComponentId) { match self.component { Compact(ref mut l) => l.set_id(id), Plain(ref mut l) => l.set_id(id), Threaded(ref mut l) => l.set_id(id), } } } impl From for ListingComponent { fn from(index_style: IndexStyle) -> Self { match index_style { IndexStyle::Plain => Plain(Default::default()), IndexStyle::Threaded => Threaded(Default::default()), IndexStyle::Compact => Compact(Default::default()), } } } impl Listing { pub fn new(accounts: &[Account]) -> Self { let accounts = accounts .iter() .enumerate() .map(|(i, a)| AccountMenuEntry { name: a.name().to_string(), index: i, }) .collect(); Listing { component: Compact(Default::default()), accounts, visible: true, dirty: true, cursor_pos: (0, 0), id: ComponentId::new_v4(), show_divider: false, menu_visibility: true, ratio: 90, } } fn draw_menu(&mut self, grid: &mut CellBuffer, mut area: Area, context: &mut Context) { if !self.is_dirty() { return; } if self.show_divider { area = (area.0, pos_dec(area.1, (1, 0))); } clear_area(grid, area); let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); self.dirty = false; let mut y = get_y(upper_left); for a in &self.accounts { y += 1; y += self.print_account(grid, (set_y(upper_left, y), bottom_right), &a, context); } context.dirty_areas.push_back(area); } /* * Print a single account in the menu area. */ fn print_account( &self, grid: &mut CellBuffer, area: Area, a: &AccountMenuEntry, context: &mut Context, ) -> usize { if cfg!(debug_assertions) && !is_valid_area!(area) { eprint!("{}:{}_{}: ", file!(), line!(), column!()); eprintln!("BUG: invalid area in print_account"); } // Each entry and its index in the account let entries: FnvHashMap = context.accounts[a.index] .list_folders() .into_iter() .map(|f| (f.hash(), f)) .collect(); let folders_order: FnvHashMap = context.accounts[a.index] .folders_order() .iter() .enumerate() .map(|(i, &fh)| (fh, i)) .collect(); let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); let highlight = self.cursor_pos.0 == a.index; let mut inc = 0; let mut depth = String::from(""); let mut s = format!("{}\n", a.name); fn pop(depth: &mut String) { depth.pop(); depth.pop(); } fn push(depth: &mut String, c: char) { depth.push(c); } fn print( folder_idx: FolderHash, depth: &mut String, inc: &mut usize, entries: &FnvHashMap, folders_order: &FnvHashMap, s: &mut String, index: usize, //account index context: &mut Context, ) { match context.accounts[index].status(entries[&folder_idx].hash()) { Ok(_) => { let count = context.accounts[index][entries[&folder_idx].hash()] .as_ref() .unwrap() .collection .values() .filter(|e| !e.is_seen()) .count(); let len = s.len(); s.insert_str( len, &format!("{} {} {}\n ", *inc, &entries[&folder_idx].name(), count), ); } Err(_) => { let len = s.len(); s.insert_str( len, &format!("{} {} ...\n ", *inc, &entries[&folder_idx].name()), ); } } *inc += 1; let mut children: Vec = entries[&folder_idx].children().to_vec(); children .sort_unstable_by(|a, b| folders_order[a].partial_cmp(&folders_order[b]).unwrap()); for child in entries[&folder_idx].children().iter() { let len = s.len(); s.insert_str(len, &format!("{} ", depth)); push(depth, ' '); print( *child, depth, inc, entries, folders_order, s, index, context, ); pop(depth); } } for f in entries.keys() { if entries[f].parent().is_none() { print( *f, &mut depth, &mut inc, &entries, &folders_order, &mut s, a.index, context, ); } } let lines: Vec<&str> = s.lines().collect(); let lines_len = lines.len(); if lines_len < 2 { return 0; } let mut idx = 0; for y in get_y(upper_left)..get_y(bottom_right) { if idx == lines_len { break; } let s = lines[idx].to_string(); let (color_fg, color_bg) = if highlight { if self.cursor_pos.1 + 1 == idx { (Color::Byte(233), Color::Byte(15)) } else { (Color::Byte(15), Color::Byte(233)) } } else { (Color::Default, Color::Default) }; let (x, _) = write_string_to_grid( &s, grid, color_fg, color_bg, (set_y(upper_left, y), bottom_right), false, ); { let mut x = get_x(upper_left); while let Some(cell) = grid.get_mut(x, y) { if x == get_x(bottom_right) { break; } match cell.ch() { c if c.is_numeric() => { cell.set_fg(Color::Byte(243)); x += 1; continue; } c if c.is_whitespace() => { x += 1; continue; } _ => { break; } } } } if highlight && idx > 1 && self.cursor_pos.1 == idx - 1 { change_colors(grid, ((x, y), (get_x(bottom_right), y)), color_fg, color_bg); } else { change_colors(grid, ((x, y), set_y(bottom_right, y)), color_fg, color_bg); } idx += 1; } if idx == 0 { 0 } else { idx - 1 } } }