Browse Source

contacts: add support for externally managed contacts

Adds support for contacts (Cards) marked as `external_resource` which
prevents modifications from happening. No way to import external
contacts is added yet.
tags/pre-alpha-0.4.0
Manos Pitsidianakis 2 years ago
parent
commit
5beed91df2
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 33
      melib/src/addressbook.rs
  2. 2
      melib/src/addressbook/vcard.rs
  3. 227
      ui/src/components/contacts.rs
  4. 354
      ui/src/components/contacts/contact_list.rs
  5. 8
      ui/src/components/mail/listing.rs
  6. 151
      ui/src/components/utilities/widgets.rs

33
melib/src/addressbook.rs

@ -81,6 +81,9 @@ pub struct Card {
color: u8,
last_edited: DateTime<Local>,
extra_properties: FnvHashMap<String, String>,
/// If true, we can't make any changes because we do not manage this resource.
external_resource: bool,
}
impl AddressBook {
@ -134,6 +137,7 @@ impl Card {
key: String::new(),
last_edited: Local::now(),
external_resource: false,
extra_properties: FnvHashMap::default(),
color: 0,
}
@ -202,37 +206,50 @@ impl Card {
pub fn set_extra_property(&mut self, key: &str, value: String) {
self.extra_properties.insert(key.to_string(), value);
}
pub fn extra_property(&self, key: &str) -> Option<&str> {
self.extra_properties.get(key).map(String::as_str)
}
pub fn extra_properties(&self) -> &FnvHashMap<String, String> {
&self.extra_properties
}
pub fn set_external_resource(&mut self, new_val: bool) {
self.external_resource = new_val;
}
pub fn external_resource(&self) -> bool {
self.external_resource
}
}
impl From<FnvHashMap<String, String>> for Card {
fn from(mut map: FnvHashMap<String, String>) -> Card {
let mut card = Card::new();
if let Some(val) = map.remove("Title") {
if let Some(val) = map.remove("TITLE") {
card.title = val;
}
if let Some(val) = map.remove("Name") {
if let Some(val) = map.remove("NAME") {
card.name = val;
}
if let Some(val) = map.remove("Additional Name") {
if let Some(val) = map.remove("ADDITIONAL NAME") {
card.additionalname = val;
}
if let Some(val) = map.remove("Name Prefix") {
if let Some(val) = map.remove("NAME PREFIX") {
card.name_prefix = val;
}
if let Some(val) = map.remove("Name Suffix") {
if let Some(val) = map.remove("NAME SUFFIX") {
card.name_suffix = val;
}
if let Some(val) = map.remove("E-mail") {
if let Some(val) = map.remove("E-MAIL") {
card.email = val;
}
if let Some(val) = map.remove("url") {
if let Some(val) = map.remove("URL") {
card.url = val;
}
if let Some(val) = map.remove("key") {
if let Some(val) = map.remove("KEY") {
card.key = val;
}
card.extra_properties = map;

2
melib/src/addressbook/vcard.rs

@ -65,7 +65,7 @@ pub struct ContentLine {
impl CardDeserializer {
pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
input = if !input.starts_with(HEADER) || !input.ends_with(FOOTER) {
return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{}", input)));
return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{:?}", input)));
} else {
&input[HEADER.len()..input.len() - FOOTER.len()]
};

227
ui/src/components/contacts.rs

@ -28,21 +28,24 @@ pub use self::contact_list::*;
#[derive(Debug)]
enum ViewMode {
//ReadOnly,
Read,
//Edit,
ReadOnly,
Discard(Selector<char>),
Edit,
//New,
}
#[derive(Debug)]
pub struct ContactManager {
id: ComponentId,
parent_id: ComponentId,
pub card: Card,
mode: ViewMode,
form: FormWidget,
account_pos: usize,
content: CellBuffer,
dirty: bool,
has_changes: bool,
initialized: bool,
}
@ -50,12 +53,14 @@ impl Default for ContactManager {
fn default() -> Self {
ContactManager {
id: Uuid::nil(),
parent_id: Uuid::nil(),
card: Card::new(),
mode: ViewMode::Read,
mode: ViewMode::Edit,
form: FormWidget::default(),
account_pos: 0,
content: CellBuffer::new(200, 100, Cell::with_char(' ')),
content: CellBuffer::new(100, 1, Cell::with_char(' ')),
dirty: true,
has_changes: false,
initialized: false,
}
}
@ -77,20 +82,36 @@ impl ContactManager {
Color::Byte(250),
Color::Default,
Attr::Default,
((0, 0), (width, 0)),
((0, 0), (width - 1, 0)),
false,
);
write_string_to_grid(
let (x, y) = write_string_to_grid(
&self.card.last_edited(),
&mut self.content,
Color::Byte(250),
Color::Default,
Attr::Default,
((x, 0), (width, 0)),
((x, 0), (width - 1, 0)),
false,
);
if self.card.external_resource() {
self.mode = ViewMode::ReadOnly;
self.content
.resize(self.content.size().0, 2, Cell::default());
let (x, y) = write_string_to_grid(
"This contact's origin is external and cannot be edited within meli.",
&mut self.content,
Color::Byte(250),
Color::Default,
Attr::Default,
((x, y), (width - 1, y)),
false,
);
}
self.form = FormWidget::new("Save".into());
self.form.add_button(("Cancel".into(), false));
self.form.add_button(("Cancel(Esc)".into(), false));
self.form
.push(("NAME".into(), self.card.name().to_string()));
self.form.push((
@ -105,6 +126,13 @@ impl ContactManager {
.push(("E-MAIL".into(), self.card.email().to_string()));
self.form.push(("URL".into(), self.card.url().to_string()));
self.form.push(("KEY".into(), self.card.key().to_string()));
for (k, v) in self.card.extra_properties() {
self.form.push((k.into(), v.to_string()));
}
}
pub fn set_parent_id(&mut self, new_val: ComponentId) {
self.parent_id = new_val;
}
}
@ -114,87 +142,166 @@ impl Component for ContactManager {
self.initialize();
self.initialized = true;
}
clear_area(grid, area);
let (width, _height) = self.content.size();
copy_area(grid, &self.content, area, ((0, 0), (width - 1, 0)));
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.dirty {
let (width, _height) = self.content.size();
clear_area(
grid,
(upper_left, set_y(bottom_right, get_y(upper_left) + 1)),
);
copy_area_with_break(grid, &self.content, area, ((0, 0), (width - 1, 0)));
self.dirty = false;
}
self.form.draw(
grid,
(set_y(upper_left, get_y(upper_left) + 1), bottom_right),
(set_y(upper_left, get_y(upper_left) + 2), bottom_right),
context,
);
match self.mode {
ViewMode::Discard(ref mut selector) => {
/* Let user choose whether to quit with/without saving or cancel */
selector.draw(grid, center_area(area, selector.content.size()), context);
}
_ => {}
}
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if self.form.process_event(event, context) {
match self.form.buttons_result() {
None => {}
Some(true) => {
let fields = std::mem::replace(&mut self.form, FormWidget::default())
.collect()
.unwrap();
let fields: FnvHashMap<String, String> = fields
.into_iter()
.map(|(s, v)| {
(
s,
match v {
Field::Text(v, _) => v.as_str().to_string(),
Field::Choice(mut v, c) => v.remove(c),
},
)
})
.collect();
let mut new_card = Card::from(fields);
new_card.set_id(*self.card.id());
context.accounts[self.account_pos]
.address_book
.add_card(new_card);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
"Saved.".into(),
)));
context.replies.push_back(UIEvent::ComponentKill(self.id));
match self.mode {
ViewMode::Discard(ref mut selector) => {
if selector.process_event(event, context) {
if selector.is_done() {
let s = match std::mem::replace(&mut self.mode, ViewMode::Edit) {
ViewMode::Discard(s) => s,
_ => unreachable!(),
};
let key = s.collect()[0] as char;
match key {
'x' => {
context
.replies
.push_back(UIEvent::Action(Tab(Kill(self.parent_id))));
return true;
}
'n' => {}
'y' => {}
_ => {}
}
}
self.set_dirty();
return true;
}
}
ViewMode::Edit => {
if let &mut UIEvent::Input(Key::Esc) = event {
if self.can_quit_cleanly(context) {
context
.replies
.push_back(UIEvent::Action(Tab(Kill(self.parent_id))));
}
return true;
}
Some(false) => {
context.replies.push_back(UIEvent::ComponentKill(self.id));
if self.form.process_event(event, context) {
match self.form.buttons_result() {
None => {}
Some(true) => {
let fields = std::mem::replace(&mut self.form, FormWidget::default())
.collect()
.unwrap();
let fields: FnvHashMap<String, String> = fields
.into_iter()
.map(|(s, v)| {
(
s,
match v {
Field::Text(v, _) => v.as_str().to_string(),
Field::Choice(mut v, c) => v.remove(c),
},
)
})
.collect();
let mut new_card = Card::from(fields);
new_card.set_id(*self.card.id());
context.accounts[self.account_pos]
.address_book
.add_card(new_card);
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage("Saved.".into()),
));
context.replies.push_back(UIEvent::ComponentKill(self.id));
}
Some(false) => {
context.replies.push_back(UIEvent::ComponentKill(self.id));
}
}
self.set_dirty();
if let UIEvent::InsertInput(_) = event {
self.has_changes = true;
}
return true;
}
}
ViewMode::ReadOnly => {
if let &mut UIEvent::Input(Key::Esc) = event {
if self.can_quit_cleanly(context) {
context.replies.push_back(UIEvent::ComponentKill(self.id));
}
return true;
}
}
return true;
}
/*
match *event {
UIEvent::Input(Key::Char('\n')) => {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEvent::ComponentKill(self.id),
});
return true;
},
_ => {},
}
*/
false
}
fn is_dirty(&self) -> bool {
self.dirty | self.form.is_dirty()
self.dirty
|| self.form.is_dirty()
|| if let ViewMode::Discard(ref selector) = self.mode {
selector.is_dirty()
} else {
false
}
}
fn set_dirty(&mut self) {
self.dirty = true;
self.initialized = false;
self.form.set_dirty();
if let ViewMode::Discard(ref mut selector) = self.mode {
selector.set_dirty();
}
}
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 {
if !self.has_changes {
return true;
}
/* Play it safe and ask user for confirmation */
self.mode = ViewMode::Discard(Selector::new(
"this contact has unsaved changes",
vec![
('x', "quit without saving".to_string()),
('y', "save draft and quit".to_string()),
('n', "cancel".to_string()),
],
true,
context,
));
self.set_dirty();
false
}
}

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

@ -1,6 +1,7 @@
use super::*;
use melib::CardId;
use std::cmp;
const MAX_COLS: usize = 500;
@ -16,13 +17,15 @@ pub struct ContactList {
new_cursor_pos: usize,
account_pos: usize,
length: usize,
content: CellBuffer,
data_columns: DataColumns,
initialized: bool,
id_positions: Vec<CardId>,
mode: ViewMode,
dirty: bool,
view: Option<Box<dyn Component>>,
movement: Option<PageMovement>,
view: Option<ContactManager>,
id: ComponentId,
}
@ -41,7 +44,6 @@ impl fmt::Display for ContactList {
impl ContactList {
const DESCRIPTION: &'static str = "contact list";
pub fn new() -> Self {
let content = CellBuffer::new(0, 0, Cell::with_char(' '));
ContactList {
cursor_pos: 0,
new_cursor_pos: 0,
@ -49,8 +51,10 @@ impl ContactList {
account_pos: 0,
id_positions: Vec::new(),
mode: ViewMode::List,
content,
data_columns: DataColumns::default(),
initialized: false,
dirty: true,
movement: None,
view: None,
id: ComponentId::new_v4(),
}
@ -67,92 +71,142 @@ impl ContactList {
let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book;
self.length = book.len();
self.content
.resize(MAX_COLS, book.len() + 1, Cell::with_char(' '));
clear_area(&mut self.content, ((0, 0), (MAX_COLS - 1, self.length)));
self.id_positions.clear();
if self.id_positions.capacity() < book.len() {
self.id_positions.reserve(book.len());
}
let mut maxima = ("Name".len(), "E-mail".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());
maxima.0 = std::cmp::max(maxima.0, c.name().split_graphemes().len());
maxima.1 = std::cmp::max(maxima.1, c.email().split_graphemes().len());
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 */
}
maxima.0 += 5;
maxima.1 += maxima.0 + 5;
/* 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,
);
let (x, _) = write_string_to_grid(
"NAME",
&mut self.content,
&mut self.data_columns.columns[0],
Color::Black,
Color::White,
Attr::Default,
Attr::Bold,
((0, 0), (MAX_COLS - 1, self.length)),
false,
);
for x in x..maxima.0 {
self.content[(x, 0)].set_bg(Color::White);
}
write_string_to_grid(
"E-MAIL",
&mut self.content,
&mut self.data_columns.columns[1],
Color::Black,
Color::White,
Attr::Default,
((maxima.0, 0), (MAX_COLS - 1, self.length)),
Attr::Bold,
((0, 0), (MAX_COLS - 1, self.length)),
false,
);
for x in x..maxima.1 {
self.content[(x, 0)].set_bg(Color::White);
}
write_string_to_grid(
"URL",
&mut self.content,
&mut self.data_columns.columns[2],
Color::Black,
Color::White,
Attr::Default,
((maxima.1, 0), (MAX_COLS - 1, self.length)),
Attr::Bold,
((0, 0), (MAX_COLS - 1, self.length)),
false,
);
for x in x..(MAX_COLS - 1) {
self.content[(x, 0)].set_bg(Color::White);
}
for (i, c) in book.values().enumerate() {
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(
let (x, _) = write_string_to_grid(
c.name(),
&mut self.content,
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
((0, i + 1), (MAX_COLS - 1, self.length)),
((0, idx + 1), (min_width.0, idx + 1)),
false,
);
write_string_to_grid(
let (x, _) = write_string_to_grid(
c.email(),
&mut self.content,
&mut self.data_columns.columns[1],
Color::Default,
Color::Default,
Attr::Default,
((maxima.0, i + 1), (MAX_COLS - 1, self.length)),
((0, idx + 1), (min_width.1, idx + 1)),
false,
);
write_string_to_grid(
let (x, _) = write_string_to_grid(
c.url(),
&mut self.content,
&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,
((maxima.1, i + 1), (MAX_COLS - 1, self.length)),
((0, 0), (MAX_COLS - 1, 0)),
false,
);
return;
}
}
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize) {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
/* 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);
}
}
impl Component for ContactList {
@ -162,49 +216,163 @@ impl Component for ContactList {
return;
}
if self.dirty {
if !self.dirty {
return;
}
self.dirty = false;
if !self.initialized {
self.initialize(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.content,
&self.data_columns.columns[0],
area,
(
(0, 0),
(MAX_COLS - 1, self.content.size().1.saturating_sub(1)),
),
((0, 0), pos_dec(self.data_columns.columns[0].size(), (1, 1))),
);
context.dirty_areas.push_back(area);
self.dirty = false;
return;
}
let rows = get_y(bottom_right) - get_y(upper_left);
if let Some(mvm) = self.movement.take() {
match mvm {
PageMovement::PageUp => {
self.new_cursor_pos = self.new_cursor_pos.saturating_sub(rows);
}
PageMovement::PageDown => {
if self.new_cursor_pos + rows + 1 < self.length {
self.new_cursor_pos += rows;
} else if self.new_cursor_pos + rows > self.length {
self.new_cursor_pos = self.length - 1;
} else {
self.new_cursor_pos = (self.length / rows) * rows;
}
}
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 upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
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;
}
}
/* Reset previously highlighted line */
let fg_color = Color::Default;
let bg_color = Color::Default;
change_colors(
grid,
(
pos_inc(upper_left, (0, self.cursor_pos + 1)),
set_y(bottom_right, get_y(upper_left) + self.cursor_pos + 1),
),
fg_color,
bg_color,
(upper_left, set_y(bottom_right, get_y(upper_left))),
Color::Black,
Color::White,
);
/* Highlight current line */
let bg_color = Color::Byte(246);
change_colors(
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,
(
pos_inc(upper_left, (0, self.new_cursor_pos + 1)),
set_y(bottom_right, get_y(upper_left) + self.new_cursor_pos + 1),
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,
),
),
fg_color,
bg_color,
self.cursor_pos,
);
self.cursor_pos = self.new_cursor_pos;
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
@ -215,39 +383,44 @@ impl Component for ContactList {
}
let shortcuts = &self.get_shortcuts(context)[Self::DESCRIPTION];
match *event {
UIEvent::Input(ref key) if *key == shortcuts["create_contact"] => {
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;
let component = Box::new(manager);
self.mode = ViewMode::View(component.id());
self.view = Some(component);
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
return true;
}
UIEvent::Input(ref key) if *key == shortcuts["edit_contact"] && self.length > 0 => {
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;
let component = Box::new(manager);
self.mode = ViewMode::View(component.id());
self.view = Some(component);
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
return true;
}
UIEvent::Input(Key::Char('n')) => {
UIEvent::Input(Key::Char('n')) if self.view.is_none() => {
let card = Card::new();
let mut manager = ContactManager::default();
manager.set_parent_id(self.id);
manager.card = card;
manager.account_pos = self.account_pos;
let component = Box::new(manager);
self.mode = ViewMode::View(component.id());
self.view = Some(component);
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
return true;
}
@ -267,6 +440,12 @@ impl Component for ContactList {
self.set_dirty();
return true;
}
UIEvent::ChangeMode(UIMode::Normal) => {
self.set_dirty();
}
UIEvent::Resize => {
self.set_dirty();
}
_ => {}
}
false
@ -297,13 +476,10 @@ impl Component for ContactList {
let config_map = context.settings.shortcuts.contact_list.key_values();
map.insert(
self.to_string(),
[
("create_contact", (*config_map["create_contact"]).clone()),
("edit_contact", (*config_map["edit_contact"]).clone()),
]
.iter()
.cloned()
.collect(),
config_map
.into_iter()
.map(|(k, v)| (k, v.clone()))
.collect(),
);
map
@ -312,7 +488,15 @@ impl Component for ContactList {
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)
}
}

8
ui/src/components/mail/listing.rs

@ -34,9 +34,9 @@ mod plain;
pub use self::plain::*;
#[derive(Debug, Default, Clone)]
pub(in crate::listing) struct DataColumns {
columns: [CellBuffer; 12],
widths: [usize; 12], // widths of columns calculated in first draw and after size changes
pub struct DataColumns {
pub columns: [CellBuffer; 12],
pub widths: [usize; 12], // widths of columns calculated in first draw and after size changes
}
#[derive(Debug)]
@ -52,7 +52,7 @@ pub(in crate::listing) struct CachedSearchStrings {
body: String,
}
trait ListingTrait {
pub trait ListingTrait {
fn coordinates(&self) -> (usize, usize, Option<EnvelopeHash>);
fn set_coordinates(&mut self, _: (usize, usize, Option<EnvelopeHash>));
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);

151
ui/src/components/utilities/widgets.rs

@ -238,6 +238,7 @@ impl FormWidget {
focus: FormFocus::Fields,
hide_buttons: false,
id: ComponentId::new_v4(),
dirty: true,
..Default::default()
}
}
@ -310,75 +311,100 @@ impl Component for FormWidget {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
for (i, k) in self.layout.iter().enumerate() {
let v = self.fields.get_mut(k).unwrap();
/* Write field label */
write_string_to_grid(
k.as_str(),
grid,
Color::Default,
Color::Default,
Attr::Default,
(
pos_inc(upper_left, (1, i)),
set_y(bottom_right, i + get_y(upper_left)),
),
false,
);
/* draw field */
v.draw(
if self.dirty {
clear_area(
grid,
(
pos_inc(upper_left, (self.field_name_max_length + 3, i)),
set_y(bottom_right, i + get_y(upper_left)),
upper_left,
set_y(bottom_right, get_y(upper_left) + self.layout.len()),
),
context,
);
/* Highlight if necessary */
if i == self.cursor {
if self.focus == FormFocus::Fields {
change_colors(
grid,
(
pos_inc(upper_left, (0, i)),
set_y(bottom_right, i + get_y(upper_left)),
),
Color::Default,
Color::Byte(246),
);
}
if self.focus == FormFocus::TextInput {
v.draw_cursor(
grid,
(
pos_inc(upper_left, (self.field_name_max_length + 3, i)),
for (i, k) in self.layout.iter().enumerate() {
let v = self.fields.get_mut(k).unwrap();
/* Write field label */
write_string_to_grid(
k.as_str(),
grid,
Color::Default,
Color::Default,
Attr::Bold,
(
pos_inc(upper_left, (1, i)),
set_y(bottom_right, i + get_y(upper_left)),
),
false,
);
/* 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 {
change_colors(
grid,
(
pos_inc(upper_left, (0, i)),
set_y(bottom_right, i + get_y(upper_left)),
),
Color::Default,
Color::Byte(246),
);
}
if self.focus == FormFocus::TextInput {
v.draw_cursor(
grid,
(
pos_inc(upper_left, (self.field_name_max_length + 3, i)),
(
get_x(upper_left) + self.field_name_max_length + 3,
i + get_y(upper_left),
),
),
(
get_x(upper_left) + self.field_name_max_length + 3,
i + get_y(upper_left),
pos_inc(upper_left, (self.field_name_max_length + 3, i + 1)),
bottom_right,
),
),
(
pos_inc(upper_left, (self.field_name_max_length + 3, i + 1)),
bottom_right,
),
context,
);
context,
);
}
}
}
}
if !self.hide_buttons {
let length = self.layout.len();
self.buttons.draw(
clear_area(
grid,
(
pos_inc(upper_left, (1, length * 2 + 3)),
set_y(bottom_right, length * 2 + 3 + get_y(upper_left)),
pos_inc(upper_left, (0, length)),
set_y(bottom_right, length + 2 + get_y(upper_left)),
),
context,
);
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,
);
}
clear_area(
grid,
(
set_y(upper_left, length + 4 + get_y(upper_left)),
bottom_right,
),
);
self.dirty = false;
}
self.dirty = false;
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
@ -389,6 +415,7 @@ impl Component for FormWidget {
match *event {
UIEvent::Input(Key::Up) if self.focus == FormFocus::Buttons => {
self.focus = FormFocus::Fields;
self.buttons.set_focus(false);
}
UIEvent::InsertInput(Key::Up) if self.focus == FormFocus::TextInput => {
let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap();
@ -406,6 +433,7 @@ impl Component for FormWidget {
}
UIEvent::Input(Key::Down) if self.focus == FormFocus::Fields => {
self.focus = FormFocus::Buttons;
self.buttons.set_focus(true);
if self.hide_buttons {
self.set_dirty();
return false;
@ -453,10 +481,11 @@ impl Component for FormWidget {
true
}
fn is_dirty(&self) -> bool {
self.dirty
self.dirty || self.buttons.is_dirty()
}
fn set_dirty(&mut self) {
self.dirty = true;
self.buttons.set_dirty();
}
fn id(&self) -> ComponentId {
@ -477,6 +506,8 @@ where
result: Option<T>,
cursor: usize,
/// Is the button widget focused, i.e do we need to draw the highlighting?
focus: bool,
dirty: bool,
id: ComponentId,
}
@ -500,6 +531,7 @@ where
buttons: vec![init_val].into_iter().collect(),
result: None,
cursor: 0,
focus: false,
dirty: true,
id: ComponentId::new_v4(),
}
@ -513,6 +545,10 @@ where
pub fn is_resolved(&self) -> bool {
self.result.is_some()
}
pub fn set_focus(&mut self, new_val: bool) {
self.focus = new_val;
}
}
impl<T> Component for ButtonWidget<T>
@ -521,6 +557,7 @@ where
{
fn draw(&mut self, grid: &mut CellBuffer, area: Area, _context: &mut Context) {
if self.dirty {
clear_area(grid, area);
let upper_left = upper_left!(area);
let mut len = 0;
@ -530,12 +567,12 @@ where
k.as_str(),
grid,
Color::Default,
if i == self.cursor {
if i == self.cursor && self.focus {
Color::Byte(246)
} else {
Color::Default
},
Attr::Default,
Attr::Bold,
(
pos_inc(upper_left, (len, 0)),
pos_inc(upper_left, (cur_len + len, 0)),

Loading…
Cancel
Save