/* * meli * * Copyright 2017-2020 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::*; use std::borrow::Cow; use std::collections::HashMap; use std::time::Duration; type AutoCompleteFn = Box Vec + Send + Sync>; #[derive(Debug, PartialEq)] enum FormFocus { Fields, Buttons, TextInput, } type Cursor = usize; impl Default for FormFocus { fn default() -> FormFocus { FormFocus::Fields } } pub enum Field { Text(UText, Option<(AutoCompleteFn, AutoComplete)>), Choice(Vec>, Cursor), } impl Debug for Field { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Text(s, _) => fmt::Debug::fmt(s, f), k => fmt::Debug::fmt(k, f), } } } use crate::Field::*; impl Default for Field { fn default() -> Field { Field::Text(UText::new(String::with_capacity(256)), None) } } impl Field { pub fn as_str(&self) -> &str { match self { Text(ref s, _) => s.as_str(), Choice(ref v, cursor) => { if v.is_empty() { "" } else { v[*cursor].as_ref() } } } } pub fn cursor(&self) -> usize { match self { Text(ref s, _) => s.grapheme_pos(), Choice(_, ref cursor) => *cursor, } } pub fn is_empty(&self) -> bool { self.as_str().is_empty() } pub fn into_string(self) -> String { match self { Text(s, _) => s.into_string(), Choice(mut v, cursor) => v.remove(cursor).to_string(), } } pub fn clear(&mut self) { match self { Text(s, _) => s.clear(), Choice(_, _) => {} } } pub fn draw_cursor( &mut self, grid: &mut CellBuffer, area: Area, secondary_area: Area, context: &mut Context, ) { let upper_left = upper_left!(area); match self { Text(ref term, auto_complete_fn) => { let width = width!(area); let pos = if width < term.grapheme_pos() { width } else { term.grapheme_pos() }; change_colors( grid, (pos_inc(upper_left, (pos, 0)), pos_inc(upper_left, (pos, 0))), Color::Default, Color::Byte(248), ); if term.grapheme_len() <= 2 { return; } if let Some((auto_complete_fn, auto_complete)) = auto_complete_fn { let entries = auto_complete_fn(context, term.as_str()); auto_complete.set_suggestions(entries); auto_complete.draw(grid, secondary_area, context); } } Choice(_, _cursor) => {} } } } impl Component for Field { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let theme_attr = crate::conf::value(context, "widgets.form.field"); let width = width!(area); let str = self.as_str(); match self { Text(ref term, _) => { /* Calculate which part of the str is visible * ########################################## * * Example: * For the string "The quick brown fox jumps over the lazy dog" with visible width * of field of 10 columns * * * Cursor <= width * ================= * Cursor at: * ⇩ * The quick brown fox jumps over the lazy dog * * cursor * ⇩ * ┌──────────┐ * │The quick │ brown fox jumps over the lazy dog * └──────────┘ * * No skip. * * Cursor at the end * ================= * Cursor at: * ⇩ * The quick brown fox jumps over the lazy dog * * remainder cursor * ⇩⇩⇩⇩⇩ ⇩ * +╌╌╌+╭┅┅┅┅┅┅┅┅┅┅╮╭┅┅┅┅┅┅┅┅┅┅╮╭┅┅┅┅┅┅┅┅┅┅╮┌──────────┐ * |The|┊ quick bro┊┊wn fox jum┊┊ps over th┊│e lazy dog│ * +╌╌╌+╰┅┅┅┅┅┅┅┅┅┅╯╰┅┅┅┅┅┅┅┅┅┅╯╰┅┅┅┅┅┅┅┅┅┅╯└──────────┘ * ⇧⇧⇧++⇧⇧⇧⇧⇧⇧⇧⇧⇧⇧++⇧⇧⇧⇧⇧⇧⇧⇧⇧⇧++⇧⇧⇧⇧⇧⇧⇧⇧⇧⇧ * skip offset * * Intermediate cursor * =================== * Cursor at: * ⇩ * The quick brown fox jumps over the lazy dog * * remainder cursor * ⇩ ⇩ * +╭┅┅┅┅┅┅┅┅┅┅╮╭┅┅┅┅┅┅┅┅┅┅╮┌──────────┐ * T|he quick b┊┊rown fox j┊│umps over │ the lazy dog * +╰┅┅┅┅┅┅┅┅┅┅╯╰┅┅┅┅┅┅┅┅┅┅╯└──────────┘ * ⇧+⇧⇧⇧⇧⇧⇧⇧⇧⇧⇧++⇧⇧⇧⇧⇧⇧⇧⇧⇧⇧ * skip offset */ write_string_to_grid( if width < term.grapheme_pos() { str.trim_left_at_boundary( width * term.grapheme_pos().wrapping_div(width).saturating_sub(1) + term.grapheme_pos().wrapping_rem(width), ) } else { str }, grid, theme_attr.fg, theme_attr.bg, theme_attr.attrs, area, None, ); } Choice(_, _) => { write_string_to_grid( str, grid, theme_attr.fg, theme_attr.bg, theme_attr.attrs, area, None, ); } } } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { match *event { UIEvent::InsertInput(Key::Char('\t')) => { if let Text(ref mut s, Some((_, auto_complete))) = self { if let Some(suggestion) = auto_complete.get_suggestion() { *s = UText::new(suggestion); let len = s.as_str().len(); s.set_cursor(len); return true; } } if let Text(ref mut s, _) = self { s.insert_char(' '); } return true; } UIEvent::InsertInput(Key::Char('\n')) => { if let Text(ref mut s, Some((_, auto_complete))) = self { if let Some(suggestion) = auto_complete.get_suggestion() { *s = UText::new(suggestion); let len = s.as_str().len(); s.set_cursor(len); } } context .replies .push_back(UIEvent::ChangeMode(UIMode::Normal)); return true; } UIEvent::InsertInput(Key::Up) => { if let Text(_, Some((_, auto_complete))) = self { auto_complete.dec_cursor(); } else { return false; } } UIEvent::InsertInput(Key::Down) => { if let Text(_, Some((_, auto_complete))) = self { auto_complete.inc_cursor(); } else { return false; } } UIEvent::InsertInput(Key::Right) => match self { Text(ref mut s, _) => { s.cursor_inc(); } Choice(ref vec, ref mut cursor) => { *cursor = if *cursor == vec.len().saturating_sub(1) { 0 } else { *cursor + 1 }; } }, UIEvent::InsertInput(Key::Left) => match self { Text(ref mut s, _) => { s.cursor_dec(); } Choice(_, ref mut cursor) => { if *cursor == 0 { return false; } else { *cursor -= 1; } } }, UIEvent::InsertInput(Key::Char(k)) => { if let Text(ref mut s, _) = self { s.insert_char(k); } } UIEvent::InsertInput(Key::Paste(ref p)) => { if let Text(ref mut s, _) = self { for c in p.chars() { s.insert_char(c); } } } UIEvent::InsertInput(Key::Backspace) | UIEvent::InsertInput(Key::Ctrl('h')) => { if let Text(ref mut s, auto_complete) = self { s.backspace(); if let Some(ac) = auto_complete.as_mut() { ac.1.set_suggestions(Vec::new()); } } } UIEvent::InsertInput(Key::Ctrl('a')) => { if let Text(ref mut s, _) = self { s.set_cursor(0); } } UIEvent::InsertInput(Key::Ctrl('b')) => { /* Backward one character */ if let Text(ref mut s, _) = self { s.cursor_dec(); } } UIEvent::InsertInput(Key::Ctrl('f')) => { /* Forward one character */ if let Text(ref mut s, _) = self { s.cursor_inc(); } } UIEvent::InsertInput(Key::Ctrl('w')) => { /* Cut previous word */ if let Text(ref mut s, _) = self { while s.as_str()[..s.cursor_pos()] .last_grapheme() .map(|(_, graph)| !graph.is_empty() && graph.trim().is_empty()) .unwrap_or(false) { s.backspace(); } while s.as_str()[..s.cursor_pos()] .last_grapheme() .map(|(_, graph)| !graph.is_empty() && !graph.trim().is_empty()) .unwrap_or(false) { s.backspace(); } } } UIEvent::InsertInput(Key::Ctrl('u')) => { if let Text(ref mut s, _) = self { s.cut_left() } } UIEvent::InsertInput(Key::Ctrl('e')) => { if let Text(ref mut s, _) = self { s.set_cursor(s.as_str().len()); } } /* TODO: add rest of readline shortcuts */ _ => { return false; } } self.set_dirty(true); true } fn is_dirty(&self) -> bool { false } fn set_dirty(&mut self, _value: bool) {} fn id(&self) -> ComponentId { ComponentId::nil() } fn set_id(&mut self, _id: ComponentId) {} } impl fmt::Display for Field { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, "{}", match self { Text(ref s, _) => s.as_str(), Choice(ref v, ref cursor) => v[*cursor].as_ref(), } ) } } #[derive(Debug, Copy, Clone, PartialEq)] pub enum FormButtonActions { Accept, Reset, Cancel, Other(&'static str), } impl Default for FormButtonActions { fn default() -> Self { FormButtonActions::Cancel } } #[derive(Debug, Default)] pub struct FormWidget where T: 'static + std::fmt::Debug + Copy + Default + Send + Sync, { fields: HashMap, Field>, layout: Vec>, buttons: ButtonWidget, field_name_max_length: usize, cursor: usize, focus: FormFocus, hide_buttons: bool, dirty: bool, id: ComponentId, } impl fmt::Display for FormWidget { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt("", f) } } impl FormWidget { pub fn new(action: (Cow<'static, str>, T)) -> FormWidget { FormWidget { buttons: ButtonWidget::new(action), focus: FormFocus::Fields, hide_buttons: false, id: ComponentId::new_v4(), dirty: true, ..Default::default() } } pub fn cursor(&self) -> usize { self.cursor } pub fn set_cursor(&mut self, new_cursor: usize) { self.cursor = new_cursor; } pub fn hide_buttons(&mut self) { self.hide_buttons = true; self.buttons.set_dirty(false); } pub fn len(&self) -> usize { self.layout.len() } pub fn add_button(&mut self, val: (Cow<'static, str>, T)) { self.buttons.push(val); } pub fn push_choices(&mut self, value: (Cow<'static, str>, Vec>)) { self.field_name_max_length = std::cmp::max(self.field_name_max_length, value.0.len()); self.layout.push(value.0.clone()); self.fields.insert(value.0, Choice(value.1, 0)); } pub fn push_cl(&mut self, value: (Cow<'static, str>, String, AutoCompleteFn)) { self.field_name_max_length = std::cmp::max(self.field_name_max_length, value.0.len()); self.layout.push(value.0.clone()); self.fields.insert( value.0, Text( UText::new(value.1), Some((value.2, AutoComplete::new(Vec::new()))), ), ); } pub fn push(&mut self, value: (Cow<'static, str>, String)) { self.field_name_max_length = std::cmp::max(self.field_name_max_length, value.0.len()); self.layout.push(value.0.clone()); self.fields.insert(value.0, Text(UText::new(value.1), None)); } pub fn insert(&mut self, index: usize, value: (Cow<'static, str>, Field)) { self.layout.insert(index, value.0.clone()); self.fields.insert(value.0, value.1); } pub fn values(&self) -> &HashMap, Field> { &self.fields } pub fn values_mut(&mut self) -> &mut HashMap, Field> { &mut self.fields } pub fn collect(self) -> Option, Field>> { if self.buttons_result().is_some() { Some(self.fields) } else { None } } pub fn buttons_result(&self) -> Option { self.buttons.result } } impl Component for FormWidget { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { let upper_left = upper_left!(area); let bottom_right = bottom_right!(area); if self.is_dirty() { let theme_default = crate::conf::value(context, "theme_default"); clear_area( grid, ( upper_left, set_y(bottom_right, get_y(upper_left) + self.layout.len()), ), theme_default, ); let label_attrs = crate::conf::value(context, "widgets.form.label"); for (i, k) in self.layout.iter().enumerate().rev() { let v = self.fields.get_mut(k).unwrap(); /* Write field label */ write_string_to_grid( k.as_ref(), grid, label_attrs.fg, label_attrs.bg, label_attrs.attrs, ( pos_inc(upper_left, (1, i)), set_y(bottom_right, i + get_y(upper_left)), ), None, ); /* draw field */ v.draw( grid, ( pos_inc(upper_left, (self.field_name_max_length + 3, i)), set_y(bottom_right, i + get_y(upper_left)), ), context, ); /* Highlight if necessary */ if i == self.cursor { if self.focus == FormFocus::Fields { let mut field_attrs = crate::conf::value(context, "widgets.form.highlighted"); if !context.settings.terminal.use_color() { field_attrs.attrs |= Attr::REVERSE; } for row in grid.bounds_iter(( pos_inc(upper_left, (0, i)), (get_x(bottom_right).saturating_sub(1), i + get_y(upper_left)), )) { for c in row { grid[c] .set_fg(field_attrs.fg) .set_bg(field_attrs.bg) .set_attrs(field_attrs.attrs); } } } if self.focus == FormFocus::TextInput { v.draw_cursor( grid, ( pos_inc(upper_left, (self.field_name_max_length + 3, i)), (get_x(bottom_right), i + get_y(upper_left)), ), ( pos_inc(upper_left, (self.field_name_max_length + 3, i + 1)), bottom_right, ), context, ); } } } let length = self.layout.len(); clear_area( grid, ( pos_inc(upper_left, (0, length)), set_y(bottom_right, length + 2 + get_y(upper_left)), ), theme_default, ); if !self.hide_buttons { self.buttons.draw( grid, ( pos_inc(upper_left, (1, length + 3)), set_y(bottom_right, length + 3 + get_y(upper_left)), ), context, ); } if length + 4 < height!(area) { clear_area( grid, (pos_inc(upper_left, (0, length + 4)), bottom_right), theme_default, ); } self.set_dirty(false); context.dirty_areas.push_back(area); } } fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { if self.focus == FormFocus::Buttons && self.buttons.process_event(event, context) { return true; } match *event { UIEvent::Input(Key::Up) if self.focus == FormFocus::Buttons => { self.focus = FormFocus::Fields; self.buttons.set_focus(false); self.set_dirty(true); return true; } UIEvent::InsertInput(Key::Up) if self.focus == FormFocus::TextInput => { let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); field.process_event(event, context); self.set_dirty(true); return true; } UIEvent::Input(Key::Up) => { self.cursor = self.cursor.saturating_sub(1); self.set_dirty(true); return true; } UIEvent::InsertInput(Key::Down) if self.focus == FormFocus::TextInput => { let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); field.process_event(event, context); self.set_dirty(true); return true; } UIEvent::Input(Key::Down) if self.cursor < self.layout.len().saturating_sub(1) => { self.cursor += 1; self.set_dirty(true); return true; } UIEvent::Input(Key::Down) if self.focus == FormFocus::Fields => { self.focus = FormFocus::Buttons; self.buttons.set_focus(true); self.set_dirty(true); if self.hide_buttons { return false; } return true; } UIEvent::InsertInput(Key::Char('\t')) if self.focus == FormFocus::TextInput => { let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); field.process_event(event, context); self.set_dirty(true); return true; } UIEvent::Input(Key::Char('\n')) if self.focus == FormFocus::Fields => { self.focus = FormFocus::TextInput; context .replies .push_back(UIEvent::ChangeMode(UIMode::Insert)); self.set_dirty(true); return true; } UIEvent::InsertInput(Key::Right) if self.focus == FormFocus::TextInput => { let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); field.process_event(event, context); self.set_dirty(true); return true; } UIEvent::InsertInput(Key::Left) if self.focus == FormFocus::TextInput => { let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); if !field.process_event(event, context) { self.focus = FormFocus::Fields; context .replies .push_back(UIEvent::ChangeMode(UIMode::Normal)); } self.set_dirty(true); return true; } UIEvent::ChangeMode(UIMode::Normal) if self.focus == FormFocus::TextInput => { self.focus = FormFocus::Fields; return false; } UIEvent::InsertInput(Key::Backspace) if self.focus == FormFocus::TextInput => { let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); field.process_event(event, context); self.set_dirty(true); return true; } UIEvent::InsertInput(_) if self.focus == FormFocus::TextInput => { let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap(); field.process_event(event, context); self.set_dirty(true); return true; } _ => {} } false } fn is_dirty(&self) -> bool { self.dirty || self.buttons.is_dirty() } fn set_dirty(&mut self, value: bool) { self.dirty = value; self.buttons.set_dirty(value); } fn id(&self) -> ComponentId { self.id } fn set_id(&mut self, id: ComponentId) { self.id = id; } } #[derive(Debug, Default)] pub struct ButtonWidget where T: 'static + std::fmt::Debug + Copy + Default + Send + Sync, { buttons: HashMap, T>, layout: Vec>, result: Option, cursor: usize, /// Is the button widget focused, i.e do we need to draw the highlighting? focus: bool, dirty: bool, id: ComponentId, } impl fmt::Display for ButtonWidget where T: 'static + std::fmt::Debug + Copy + Default + Send + Sync, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt("", f) } } impl ButtonWidget where T: 'static + std::fmt::Debug + Copy + Default + Send + Sync, { pub fn new(init_val: (Cow<'static, str>, T)) -> ButtonWidget { ButtonWidget { layout: vec![init_val.0.clone()], buttons: vec![init_val].into_iter().collect(), result: None, cursor: 0, focus: false, dirty: true, id: ComponentId::new_v4(), } } pub fn push(&mut self, value: (Cow<'static, str>, T)) { self.layout.push(value.0.clone()); self.buttons.insert(value.0, value.1); } pub fn is_resolved(&self) -> bool { self.result.is_some() } pub fn result(&self) -> Option { self.result } pub fn set_focus(&mut self, new_val: bool) { self.focus = new_val; } pub fn set_cursor(&mut self, new_val: usize) { if self.buttons.is_empty() { return; } self.cursor = new_val % self.buttons.len(); } } impl Component for ButtonWidget where T: 'static + std::fmt::Debug + Copy + Default + Send + Sync, { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if self.dirty { let theme_default = crate::conf::value(context, "theme_default"); clear_area(grid, area, theme_default); let upper_left = upper_left!(area); let mut len = 0; for (i, k) in self.layout.iter().enumerate() { let cur_len = k.len(); write_string_to_grid( k.as_ref(), grid, theme_default.fg, if i == self.cursor && self.focus { Color::Byte(246) } else { theme_default.bg }, Attr::BOLD, ( pos_inc(upper_left, (len, 0)), pos_inc(upper_left, (cur_len + len, 0)), ), None, ); len += cur_len + 3; } self.dirty = false; } } fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool { match *event { UIEvent::Input(Key::Char('\n')) => { self.result = Some( self.buttons .remove(&self.layout[self.cursor]) .unwrap_or_default(), ); self.set_dirty(true); return true; } UIEvent::Input(Key::Left) => { self.cursor = self.cursor.saturating_sub(1); self.set_dirty(true); return true; } UIEvent::Input(Key::Right) if self.cursor < self.layout.len().saturating_sub(1) => { self.cursor += 1; self.set_dirty(true); return true; } _ => {} } false } fn is_dirty(&self) -> bool { self.dirty } fn set_dirty(&mut self, value: bool) { self.dirty = value; } fn id(&self) -> ComponentId { self.id } fn set_id(&mut self, id: ComponentId) { self.id = id; } } #[derive(Debug, PartialEq, Clone)] pub struct AutoCompleteEntry { pub entry: String, pub description: String, } impl AutoCompleteEntry { pub fn as_str(&self) -> &str { self.entry.as_str() } } impl From for AutoCompleteEntry { fn from(val: String) -> Self { AutoCompleteEntry { entry: val, description: String::new(), } } } impl From<&(&str, &str, TokenStream)> for AutoCompleteEntry { fn from(val: &(&str, &str, TokenStream)) -> Self { let (a, b, _) = val; AutoCompleteEntry { entry: a.to_string(), description: b.to_string(), } } } impl From<(String, String)> for AutoCompleteEntry { fn from(val: (String, String)) -> Self { let (a, b) = val; AutoCompleteEntry { entry: a, description: b, } } } #[derive(Debug, PartialEq, Clone)] pub struct AutoComplete { entries: Vec, content: CellBuffer, cursor: usize, dirty: bool, id: ComponentId, } impl fmt::Display for AutoComplete { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt("AutoComplete", f) } } impl Component for AutoComplete { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if self.entries.is_empty() { return; }; self.dirty = false; let (upper_left, bottom_right) = area; let rows = get_y(bottom_right) - get_y(upper_left); if rows == 0 { return; } let page_no = (self.cursor.saturating_sub(1)).wrapping_div(rows); let top_idx = page_no * rows; let x_offset = if rows < self.entries.len() { 1 } else { 0 }; let (width, height) = self.content.size(); clear_area(grid, area, crate::conf::value(context, "theme_default")); copy_area( grid, &self.content, (upper_left, pos_dec(bottom_right, (x_offset, 0))), ( (0, top_idx), (width.saturating_sub(1), height.saturating_sub(1)), ), ); /* Highlight cursor */ if self.cursor > 0 { change_colors( grid, ( pos_inc(upper_left, (0, (self.cursor - 1) % rows)), ( std::cmp::min( get_x(upper_left) + width.saturating_sub(1), get_x(bottom_right), ) .saturating_sub(x_offset), get_y(pos_inc(upper_left, (0, (self.cursor - 1) % rows))), ), ), Color::Default, Color::Byte(246), ); } if rows < self.entries.len() { ScrollBar { show_arrows: false }.draw( grid, ( set_y(pos_dec(bottom_right, (x_offset, 0)), get_y(upper_left)), pos_dec(bottom_right, (x_offset, 0)), ), context, self.cursor.saturating_sub(1), rows, self.entries.len(), ); } context.dirty_areas.push_back(area); } fn process_event(&mut self, _event: &mut UIEvent, _context: &mut Context) -> bool { false } fn is_dirty(&self) -> bool { self.dirty } fn set_dirty(&mut self, value: bool) { self.dirty = value; } fn id(&self) -> ComponentId { self.id } fn set_id(&mut self, id: ComponentId) { self.id = id; } } impl AutoComplete { pub fn new(entries: Vec) -> Self { let mut ret = AutoComplete { entries: Vec::new(), content: CellBuffer::default(), cursor: 0, dirty: true, id: ComponentId::new_v4(), }; ret.set_suggestions(entries); ret } pub fn set_suggestions(&mut self, entries: Vec) -> bool { if entries.len() == self.entries.len() && entries == self.entries { return false; } let mut content = CellBuffer::new( entries .iter() .map(|a| a.entry.grapheme_len() + a.description.grapheme_len() + 2) .max() .unwrap_or(0) + 1, entries.len(), Cell::with_style(Color::Byte(23), Color::Byte(7), Attr::DEFAULT), ); let width = content.cols(); for (i, e) in entries.iter().enumerate() { let (x, _) = write_string_to_grid( &e.entry, &mut content, Color::Byte(23), Color::Byte(7), Attr::DEFAULT, ((0, i), (width - 1, i)), None, ); write_string_to_grid( &e.description, &mut content, Color::Byte(23), Color::Byte(7), Attr::ITALICS, ((x + 2, i), (width - 1, i)), None, ); write_string_to_grid( "▒", &mut content, Color::Byte(23), Color::Byte(7), Attr::DEFAULT, ((width - 1, i), (width - 1, i)), None, ); } self.content = content; self.entries = entries; self.cursor = 0; true } pub fn inc_cursor(&mut self) { if self.cursor < self.entries.len() { self.cursor += 1; self.set_dirty(true); } } pub fn dec_cursor(&mut self) { self.cursor = self.cursor.saturating_sub(1); self.set_dirty(true); } pub fn cursor(&self) -> usize { self.cursor } pub fn set_cursor(&mut self, val: usize) { debug_assert!(val <= self.entries.len()); self.cursor = val; } pub fn get_suggestion(&mut self) -> Option { if self.entries.is_empty() || self.cursor == 0 { return None; } let ret = self.entries.remove(self.cursor - 1); self.entries.clear(); self.cursor = 0; self.content.empty(); Some(ret.entry) } pub fn suggestions(&self) -> &Vec { &self.entries } } #[derive(Default, Copy, Clone)] pub struct ScrollBar { pub show_arrows: bool, } impl ScrollBar { pub fn set_show_arrows(&mut self, new_val: bool) -> &mut Self { self.show_arrows = new_val; self } pub fn draw( &mut self, grid: &mut CellBuffer, area: Area, context: &Context, pos: usize, visible_rows: usize, length: usize, ) { if length == 0 { return; } let height = height!(area); if height < 3 { return; } let theme_default = crate::conf::value(context, "theme_default"); clear_area(grid, area, theme_default); let visible_rows = std::cmp::min(visible_rows, length); let ascii_drawing = grid.ascii_drawing; let ratio: f64 = (height as f64) / (length as f64); let scrollbar_height = std::cmp::max((ratio * (visible_rows as f64)) as usize, 1); let scrollbar_offset = (ratio * (pos as f64)) as usize; let (mut upper_left, bottom_right) = area; if self.show_arrows { grid[upper_left] .set_ch(if ascii_drawing { '^' } else { '▀' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg); upper_left = pos_inc(upper_left, (0, 1)); } upper_left = pos_inc(upper_left, (0, scrollbar_offset)); for _ in 0..scrollbar_height { if get_y(upper_left) >= get_y(bottom_right) { break; } grid[upper_left] .set_ch(if ascii_drawing { '#' } else { '█' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg) .set_attrs(if !context.settings.terminal.use_color() { theme_default.attrs | Attr::REVERSE } else { theme_default.attrs }); upper_left = pos_inc(upper_left, (0, 1)); } if self.show_arrows { grid[bottom_right] .set_ch(if ascii_drawing { 'v' } else { '▄' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg) .set_bg(crate::conf::value(context, "theme_default").bg); } } pub fn draw_horizontal( &mut self, grid: &mut CellBuffer, area: Area, context: &Context, pos: usize, visible_cols: usize, length: usize, ) { if length == 0 { return; } let width = width!(area); if width < 3 { return; } let theme_default = crate::conf::value(context, "theme_default"); clear_area(grid, area, theme_default); let visible_cols = std::cmp::min(visible_cols, length); let ascii_drawing = grid.ascii_drawing; let ratio: f64 = (width as f64) / (length as f64); let scrollbar_width = std::cmp::min((ratio * (visible_cols as f64)) as usize, 1); let scrollbar_offset = (ratio * (pos as f64)) as usize; let (mut upper_left, bottom_right) = area; if self.show_arrows { grid[upper_left] .set_ch(if ascii_drawing { '<' } else { '▐' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg); upper_left = pos_inc(upper_left, (1, 0)); } upper_left = pos_inc(upper_left, (scrollbar_offset, 0)); for _ in 0..scrollbar_width { if get_x(upper_left) >= get_x(bottom_right) { break; } grid[upper_left] .set_ch(if ascii_drawing { '#' } else { '█' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg) .set_attrs(if !context.settings.terminal.use_color() { theme_default.attrs | Attr::REVERSE } else { theme_default.attrs }); upper_left = pos_inc(upper_left, (1, 0)); } if self.show_arrows { grid[bottom_right] .set_ch(if ascii_drawing { '>' } else { '▌' }) .set_fg(crate::conf::value(context, "widgets.options.highlighted").bg) .set_bg(crate::conf::value(context, "theme_default").bg); } } } #[derive(Debug)] pub struct ProgressSpinner { timer: crate::jobs::Timer, stage: usize, pub kind: std::result::Result>, pub width: usize, theme_attr: ThemeAttribute, active: bool, dirty: bool, id: ComponentId, } impl ProgressSpinner { pub const KINDS: [(Duration, &'static [&'static str]); 37] = [ (Duration::from_millis(130), &["-", "\\", "|", "/"]), (Self::INTERVAL, &["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]), (Self::INTERVAL, &["⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"]), (Self::INTERVAL, &["⣀", "⣄", "⣆", "⣇", "⣧", "⣷", "⣿"]), (Self::INTERVAL, &["○", "◔", "◐", "◕", "⬤"]), (Self::INTERVAL, &["□", "◱", "◧", "▣", "■"]), (Self::INTERVAL, &["□", "◱", "▨", "▩", "■"]), (Self::INTERVAL, &["□", "◱", "▥", "▦", "■"]), (Self::INTERVAL, &["░", "▒", "▓", "█"]), (Self::INTERVAL, &["░", "█"]), (Self::INTERVAL, &["⬜", "⬛"]), (Self::INTERVAL, &["▱", "▰"]), (Self::INTERVAL, &["▭", "◼"]), (Self::INTERVAL, &["▯", "▮"]), (Self::INTERVAL, &["◯", "⬤"]), (Self::INTERVAL, &["⚪", "⚫"]), ( Self::INTERVAL, &["▖", "▗", "▘", "▝", "▞", "▚", "▙", "▟", "▜", "▛"], ), (Self::INTERVAL, &["|", "/", "-", "\\"]), (Self::INTERVAL, &[".", "o", "O", "@", "*"]), (Self::INTERVAL, &["◡◡", "⊙⊙", "◠◠", "⊙⊙"]), (Self::INTERVAL, &["◜ ", " ◝", " ◞", "◟ "]), (Self::INTERVAL, &["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"]), ( Self::INTERVAL, &["▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃"], ), ( Self::INTERVAL, &[ "▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉", ], ), (Self::INTERVAL, &["▖", "▘", "▝", "▗"]), (Self::INTERVAL, &["▌", "▀", "▐", "▄"]), (Self::INTERVAL, &["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"]), (Self::INTERVAL, &["◢", "◣", "◤", "◥"]), (Self::INTERVAL, &["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]), ( Self::INTERVAL, &["⢎⡰", "⢎⡡", "⢎⡑", "⢎⠱", "⠎⡱", "⢊⡱", "⢌⡱", "⢆⡱"], ), (Self::INTERVAL, &[".", "o", "O", "°", "O", "o", "."]), (Duration::from_millis(100), &["㊂", "㊀", "㊁"]), ( Duration::from_millis(100), &["💛 ", "💙 ", "💜 ", "💚 ", "❤️ "], ), ( Duration::from_millis(100), &[ "🕛 ", "🕐 ", "🕑 ", "🕒 ", "🕓 ", "🕔 ", "🕕 ", "🕖 ", "🕗 ", "🕘 ", "🕙 ", "🕚 ", ], ), (Duration::from_millis(100), &["🌍 ", "🌎 ", "🌏 "]), ( Duration::from_millis(80), &[ "[ ]", "[= ]", "[== ]", "[=== ]", "[ ===]", "[ ==]", "[ =]", "[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]", ], ), ( Duration::from_millis(80), &["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "], ), ]; pub const INTERVAL_MS: u64 = 50; const INTERVAL: std::time::Duration = std::time::Duration::from_millis(Self::INTERVAL_MS); pub fn new(kind: usize, context: &Context) -> Self { let kind = kind % Self::KINDS.len(); let width = Self::KINDS[kind] .1 .iter() .map(|f| f.grapheme_len()) .max() .unwrap_or(0); let interval = Self::KINDS[kind].0; let timer = context .job_executor .clone() .create_timer(interval, interval); let mut theme_attr = crate::conf::value(context, "status.bar"); if !context.settings.terminal.use_color() { theme_attr.attrs |= Attr::REVERSE; } theme_attr.attrs |= Attr::BOLD; ProgressSpinner { timer, stage: 0, kind: Ok(kind), width, theme_attr, dirty: true, active: false, id: ComponentId::new_v4(), } } pub fn is_active(&self) -> bool { self.active } pub fn set_kind(&mut self, kind: usize) { self.stage = 0; self.width = Self::KINDS[kind % Self::KINDS.len()] .1 .iter() .map(|f| f.grapheme_len()) .max() .unwrap_or(0); self.kind = Ok(kind % Self::KINDS.len()); let interval = Self::KINDS[kind % Self::KINDS.len()].0; self.timer.set_interval(interval); self.dirty = true; } pub fn set_custom_kind(&mut self, frames: Vec, interval: u64) { self.stage = 0; self.width = frames.iter().map(|f| f.grapheme_len()).max().unwrap_or(0); if self.width == 0 { self.stop(); } self.kind = Err(frames); self.timer.set_interval(Duration::from_millis(interval)); self.dirty = true; } pub fn start(&mut self) { if self.width == 0 { return; } self.active = true; self.timer.rearm(); } pub fn stop(&mut self) { self.active = false; self.stage = 0; self.timer.disable(); } } impl fmt::Display for ProgressSpinner { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "progress bar") } } impl Component for ProgressSpinner { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { if self.dirty { clear_area(grid, area, self.theme_attr); if self.active { write_string_to_grid( match self.kind.as_ref() { Ok(kind) => (Self::KINDS[*kind].1)[self.stage].as_ref(), Err(custom) => custom[self.stage].as_ref(), }, grid, self.theme_attr.fg, self.theme_attr.bg, self.theme_attr.attrs, area, None, ); } context.dirty_areas.push_back(area); self.dirty = false; } } fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool { match event { UIEvent::Timer(id) if *id == self.timer.id() => { match self.kind.as_ref() { Ok(kind) => { self.stage = (self.stage + 1).wrapping_rem(Self::KINDS[*kind].1.len()); } Err(custom) => { self.stage = (self.stage + 1).wrapping_rem(custom.len()); } } self.dirty = true; true } _ => false, } } fn set_dirty(&mut self, new_val: bool) { self.dirty = new_val; } fn is_dirty(&self) -> bool { self.dirty } fn id(&self) -> ComponentId { self.id } fn set_id(&mut self, id: ComponentId) { self.id = id; } }