meli/ui/src/components/contacts/contact_list.rs

783 lines
26 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

use super::*;
use melib::CardId;
use std::cmp;
const MAX_COLS: usize = 500;
#[derive(Debug, PartialEq)]
enum ViewMode {
List,
View(ComponentId),
}
#[derive(Debug)]
struct AccountMenuEntry {
name: String,
// Index in the config account vector.
index: usize,
}
#[derive(Debug)]
pub struct ContactList {
accounts: Vec<AccountMenuEntry>,
cursor_pos: usize,
new_cursor_pos: usize,
account_pos: usize,
length: usize,
data_columns: DataColumns,
initialized: bool,
id_positions: Vec<CardId>,
mode: ViewMode,
dirty: bool,
show_divider: bool,
menu_visibility: bool,
movement: Option<PageMovement>,
cmd_buf: String,
view: Option<ContactManager>,
ratio: usize, // right/(container width) * 100
id: ComponentId,
}
impl fmt::Display for ContactList {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", ContactList::DESCRIPTION)
}
}
impl ContactList {
const DESCRIPTION: &'static str = "contact list";
pub fn new(context: &Context) -> Self {
let accounts = context
.accounts
.iter()
.enumerate()
.map(|(i, a)| AccountMenuEntry {
name: a.name().to_string(),
index: i,
})
.collect();
ContactList {
accounts,
cursor_pos: 0,
new_cursor_pos: 0,
length: 0,
account_pos: 0,
id_positions: Vec::new(),
mode: ViewMode::List,
data_columns: DataColumns::default(),
initialized: false,
dirty: true,
movement: None,
cmd_buf: String::with_capacity(8),
view: None,
ratio: 90,
show_divider: false,
menu_visibility: true,
id: ComponentId::new_v4(),
}
}
pub fn for_account(pos: usize, context: &Context) -> Self {
ContactList {
account_pos: pos,
..Self::new(context)
}
}
fn initialize(&mut self, context: &mut Context) {
let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book;
self.length = book.len();
self.id_positions.clear();
if self.id_positions.capacity() < book.len() {
self.id_positions.reserve(book.len());
}
self.dirty = true;
let mut min_width = ("Name".len(), "E-mail".len(), 0, 0, 0);
for c in book.values() {
self.id_positions.push(*c.id());
min_width.0 = cmp::max(min_width.0, c.name().split_graphemes().len()); /* name */
min_width.1 = cmp::max(min_width.1, c.email().split_graphemes().len()); /* email */
min_width.2 = cmp::max(min_width.2, c.url().split_graphemes().len());
/* url */
}
/* name column */
self.data_columns.columns[0] = CellBuffer::new_with_context(
min_width.0,
self.length + 1,
Cell::with_char(' '),
context,
);
/* email column */
self.data_columns.columns[1] = CellBuffer::new_with_context(
min_width.1,
self.length + 1,
Cell::with_char(' '),
context,
);
/* url column */
self.data_columns.columns[2] = CellBuffer::new_with_context(
min_width.2,
self.length + 1,
Cell::with_char(' '),
context,
);
write_string_to_grid(
"NAME",
&mut self.data_columns.columns[0],
Color::Black,
Color::White,
Attr::Bold,
((0, 0), (MAX_COLS - 1, self.length)),
false,
);
write_string_to_grid(
"E-MAIL",
&mut self.data_columns.columns[1],
Color::Black,
Color::White,
Attr::Bold,
((0, 0), (MAX_COLS - 1, self.length)),
false,
);
write_string_to_grid(
"URL",
&mut self.data_columns.columns[2],
Color::Black,
Color::White,
Attr::Bold,
((0, 0), (MAX_COLS - 1, self.length)),
false,
);
let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book;
for (idx, c) in book.values().enumerate() {
self.id_positions.push(*c.id());
write_string_to_grid(
c.name(),
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
((0, idx + 1), (min_width.0, idx + 1)),
false,
);
write_string_to_grid(
c.email(),
&mut self.data_columns.columns[1],
Color::Default,
Color::Default,
Attr::Default,
((0, idx + 1), (min_width.1, idx + 1)),
false,
);
write_string_to_grid(
c.url(),
&mut self.data_columns.columns[2],
Color::Default,
Color::Default,
Attr::Default,
((0, idx + 1), (min_width.2, idx + 1)),
false,
);
}
if self.length == 0 {
let message = "Address book is empty.".to_string();
self.data_columns.columns[0] = CellBuffer::new_with_context(
message.len(),
self.length + 1,
Cell::with_char(' '),
context,
);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
((0, 0), (MAX_COLS - 1, 0)),
false,
);
return;
}
}
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize) {
/* Reset previously highlighted line */
let fg_color = Color::Default;
let bg_color = if idx == self.new_cursor_pos {
Color::Byte(246)
} else {
Color::Default
};
change_colors(grid, area, fg_color, bg_color);
}
fn draw_menu(&mut self, grid: &mut CellBuffer, mut area: Area, context: &mut Context) {
if !self.is_dirty() {
return;
}
clear_area(grid, area);
/* visually divide menu and listing */
area = (area.0, pos_dec(area.1, (1, 0)));
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 {
self.print_account(grid, (set_y(upper_left, y), bottom_right), &a, context);
y += 1;
}
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,
) {
if !is_valid_area!(area) {
debug!("BUG: invalid area in print_account");
}
let width = width!(area);
let must_highlight_account: bool = self.account_pos == a.index;
let (fg_color, bg_color) = if must_highlight_account {
if self.account_pos == a.index {
(Color::Byte(233), Color::Byte(15))
} else {
(Color::Byte(15), Color::Byte(233))
}
} else {
(Color::Default, Color::Default)
};
let s = format!(" [{}]", context.accounts[a.index].address_book.len());
if a.name.grapheme_len() + s.len() > width + 1 {
/* Print account name */
write_string_to_grid(&a.name, grid, fg_color, bg_color, Attr::Bold, area, false);
write_string_to_grid(
&s,
grid,
fg_color,
bg_color,
Attr::Bold,
(
pos_dec(
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
(s.len() - 1, 0),
),
bottom_right!(area),
),
false,
);
write_string_to_grid(
"…",
grid,
fg_color,
bg_color,
Attr::Bold,
(
pos_dec(
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
(s.len() - 1, 0),
),
bottom_right!(area),
),
false,
);
} else {
/* Print account name */
write_string_to_grid(&a.name, grid, fg_color, bg_color, Attr::Bold, area, false);
write_string_to_grid(
&s,
grid,
fg_color,
bg_color,
Attr::Bold,
(
pos_dec(
(get_x(bottom_right!(area)), get_y(upper_left!(area))),
(s.len() - 1, 0),
),
bottom_right!(area),
),
false,
);
}
}
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.length == 0 {
clear_area(grid, area);
copy_area(
grid,
&self.data_columns.columns[0],
area,
((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))),
);
context.dirty_areas.push_back(area);
return;
}
let rows = get_y(bottom_right) - get_y(upper_left);
if let Some(mvm) = self.movement.take() {
match mvm {
PageMovement::Up(amount) => {
self.new_cursor_pos = self.new_cursor_pos.saturating_sub(amount);
}
PageMovement::PageUp(multiplier) => {
self.new_cursor_pos = self.new_cursor_pos.saturating_sub(rows * multiplier);
}
PageMovement::Down(amount) => {
if self.new_cursor_pos + amount + 1 < self.length {
self.new_cursor_pos += amount;
} else {
self.new_cursor_pos = self.length - 1;
}
}
PageMovement::PageDown(multiplier) => {
if self.new_cursor_pos + rows * multiplier + 1 < self.length {
self.new_cursor_pos += rows * multiplier;
} else if self.new_cursor_pos + rows * multiplier > self.length {
self.new_cursor_pos = self.length - 1;
} else {
self.new_cursor_pos = (self.length / rows) * rows;
}
}
PageMovement::Right(_) | PageMovement::Left(_) => {}
PageMovement::Home => {
self.new_cursor_pos = 0;
}
PageMovement::End => {
if self.new_cursor_pos + rows > self.length {
self.new_cursor_pos = self.length - 1;
} else {
self.new_cursor_pos = (self.length / rows) * rows;
}
}
}
}
let prev_page_no = (self.cursor_pos).wrapping_div(rows);
let page_no = (self.new_cursor_pos).wrapping_div(rows);
let top_idx = page_no * rows;
/* If cursor position has changed, remove the highlight from the previous position and
* apply it in the new one. */
if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no {
let old_cursor_pos = self.cursor_pos;
self.cursor_pos = self.new_cursor_pos;
for idx in &[old_cursor_pos, self.new_cursor_pos] {
if *idx >= self.length {
continue; //bounds check
}
let new_area = (
set_y(upper_left, get_y(upper_left) + (*idx % rows) + 1),
set_y(bottom_right, get_y(upper_left) + (*idx % rows) + 1),
);
self.highlight_line(grid, new_area, *idx);
context.dirty_areas.push_back(new_area);
}
return;
} else if self.cursor_pos != self.new_cursor_pos {
self.cursor_pos = self.new_cursor_pos;
}
if self.new_cursor_pos >= self.length {
self.new_cursor_pos = self.length - 1;
self.cursor_pos = self.new_cursor_pos;
}
let width = width!(area);
self.data_columns.widths = Default::default();
self.data_columns.widths[0] = self.data_columns.columns[0].size().0; /* name */
self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* email*/
self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* url */
let min_col_width = std::cmp::min(
15,
std::cmp::min(self.data_columns.widths[0], self.data_columns.widths[1]),
);
if self.data_columns.widths[0] + self.data_columns.widths[1] + 3 * min_col_width + 8 > width
{
let remainder =
width.saturating_sub(self.data_columns.widths[0] + self.data_columns.widths[1] + 4);
self.data_columns.widths[2] = remainder / 6;
}
clear_area(grid, area);
/* Page_no has changed, so draw new page */
let mut x = get_x(upper_left);
for i in 0..self.data_columns.columns.len() {
let (column_width, column_height) = self.data_columns.columns[i].size();
if self.data_columns.widths[i] == 0 {
continue;
}
copy_area(
grid,
&self.data_columns.columns[i],
(
set_x(upper_left, x),
set_x(
bottom_right,
std::cmp::min(get_x(bottom_right), x + (self.data_columns.widths[i])),
),
),
(
(0, top_idx),
(
column_width.saturating_sub(1),
column_height.saturating_sub(1),
),
),
);
x += self.data_columns.widths[i] + 2; // + SEPARATOR
if x > get_x(bottom_right) {
break;
}
}
change_colors(
grid,
(upper_left, set_y(bottom_right, get_y(upper_left))),
Color::Black,
Color::White,
);
if top_idx + rows + 1 > self.length {
clear_area(
grid,
(
pos_inc(upper_left, (0, self.length - top_idx + 2)),
bottom_right,
),
);
}
self.highlight_line(
grid,
(
set_y(upper_left, get_y(upper_left) + (self.cursor_pos % rows) + 1),
set_y(
bottom_right,
get_y(upper_left) + (self.cursor_pos % rows) + 1,
),
),
self.cursor_pos,
);
context.dirty_areas.push_back(area);
}
}
impl Component for ContactList {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if let Some(mgr) = self.view.as_mut() {
mgr.draw(grid, area, context);
return;
}
if !self.dirty {
return;
}
if !self.initialized {
self.initialize(context);
}
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 = if self.menu_visibility {
(self.ratio * total_cols) / 100
} else {
total_cols
};
let mid = get_x(bottom_right) - right_component_width;
if self.dirty && mid != get_x(upper_left) {
if self.show_divider {
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);
}
} else {
for i in get_y(upper_left)..=get_y(bottom_right) {
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 {
self.draw_list(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);
self.draw_list(grid, (set_x(upper_left, mid + 1), bottom_right), context);
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if let Some(ref mut v) = self.view {
if v.process_event(event, context) {
return true;
}
}
let shortcuts = &self.get_shortcuts(context)[Self::DESCRIPTION];
match *event {
UIEvent::Input(ref key)
if *key == shortcuts["create_contact"] && self.view.is_none() =>
{
let mut manager = ContactManager::default();
manager.set_parent_id(self.id);
manager.account_pos = self.account_pos;
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
return true;
}
UIEvent::Input(ref key)
if *key == shortcuts["edit_contact"] && self.length > 0 && self.view.is_none() =>
{
let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book;
let card = book[&self.id_positions[self.cursor_pos]].clone();
let mut manager = ContactManager::default();
manager.set_parent_id(self.id);
manager.card = card;
manager.account_pos = self.account_pos;
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
return true;
}
UIEvent::Input(ref key) if *key == shortcuts["next_account"] && self.view.is_none() => {
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
if self.accounts.is_empty() {
return true;
}
if self.account_pos + amount < self.accounts.len() {
self.account_pos += amount;
self.set_dirty();
self.initialized = false;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context).unwrap(),
)));
}
return true;
}
UIEvent::Input(ref key) if *key == shortcuts["prev_account"] && self.view.is_none() => {
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
if self.accounts.is_empty() {
return true;
}
if self.account_pos >= amount {
self.account_pos -= amount;
self.set_dirty();
self.initialized = false;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context).unwrap(),
)));
}
return true;
}
UIEvent::Input(ref k)
if k == shortcuts["toggle_menu_visibility"] && self.view.is_none() =>
{
self.menu_visibility = !self.menu_visibility;
self.set_dirty();
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) => {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
}
UIEvent::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
self.cmd_buf.push(c);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufSet(
self.cmd_buf.clone(),
)));
return true;
}
UIEvent::Input(Key::Up) if self.view.is_none() => {
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
self.movement = Some(PageMovement::Up(amount));
self.set_dirty();
return true;
}
UIEvent::Input(Key::Down)
if self.cursor_pos < self.length.saturating_sub(1) && self.view.is_none() =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
self.set_dirty();
self.movement = Some(PageMovement::Down(amount));
return true;
}
UIEvent::ComponentKill(ref kill_id) if self.mode == ViewMode::View(*kill_id) => {
self.mode = ViewMode::List;
self.view.take();
self.set_dirty();
return true;
}
UIEvent::ChangeMode(UIMode::Normal) => {
self.set_dirty();
}
UIEvent::Resize => {
self.set_dirty();
}
_ => {}
}
false
}
fn is_dirty(&self) -> bool {
self.dirty || self.view.as_ref().map(|v| v.is_dirty()).unwrap_or(false)
}
fn set_dirty(&mut self) {
if let Some(p) = self.view.as_mut() {
p.set_dirty();
};
self.dirty = true;
}
fn kill(&mut self, uuid: Uuid, context: &mut Context) {
debug_assert!(uuid == self.id);
context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = self
.view
.as_ref()
.map(|p| p.get_shortcuts(context))
.unwrap_or_default();
let config_map = context.settings.shortcuts.contact_list.key_values();
map.insert(
self.to_string(),
config_map
.into_iter()
.map(|(k, v)| (k, v.clone()))
.collect(),
);
map
}
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
self.view
.as_mut()
.map(|p| p.can_quit_cleanly(context))
.unwrap_or(true)
}
fn get_status(&self, context: &Context) -> Option<String> {
Some(format!(
"{} entries",
context.accounts[self.account_pos].address_book.len()
))
}
}