add autocomplete suggestions in field
parent
5bffd8e966
commit
b1d25e6d8d
|
@ -73,6 +73,9 @@ impl AddressBook {
|
||||||
pub fn card_exists(&self, card_id: CardId) -> bool {
|
pub fn card_exists(&self, card_id: CardId) -> bool {
|
||||||
self.cards.contains_key(&card_id)
|
self.cards.contains_key(&card_id)
|
||||||
}
|
}
|
||||||
|
pub fn search(&self, term: &str) -> Vec<String> {
|
||||||
|
self.cards.values().filter(|c| c.email.contains(term)).map(|c| c.email.clone()).collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for AddressBook {
|
impl Deref for AddressBook {
|
||||||
|
|
|
@ -132,7 +132,7 @@ impl Component for ContactManager {
|
||||||
let mut fields = std::mem::replace(&mut self.form, FormWidget::default()).collect().unwrap();
|
let mut fields = std::mem::replace(&mut self.form, FormWidget::default()).collect().unwrap();
|
||||||
let fields: FnvHashMap<String, String> = fields.into_iter().map(|(s, v)| {
|
let fields: FnvHashMap<String, String> = fields.into_iter().map(|(s, v)| {
|
||||||
(s, match v {
|
(s, match v {
|
||||||
Field::Text(v, _) | Field::TextArea(v, _) => v,
|
Field::Text(v, _, _) | Field::TextArea(v, _) => v,
|
||||||
Field::Choice(mut v, c) => v.remove(c),
|
Field::Choice(mut v, c) => v.remove(c),
|
||||||
})}).collect();
|
})}).collect();
|
||||||
let mut new_card = Card::from(fields);
|
let mut new_card = Card::from(fields);
|
||||||
|
|
|
@ -170,8 +170,16 @@ impl Composer {
|
||||||
self.form.hide_buttons();
|
self.form.hide_buttons();
|
||||||
self.form.set_cursor(old_cursor);
|
self.form.set_cursor(old_cursor);
|
||||||
let headers = self.draft.headers();
|
let headers = self.draft.headers();
|
||||||
|
let account_cursor = self.account_cursor;
|
||||||
for &k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] {
|
for &k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] {
|
||||||
self.form.push((k.into(), headers[k].to_string()));
|
if k == "To" {
|
||||||
|
self.form.push_cl((k.into(), headers[k].to_string(), Box::new(move |c, term| {
|
||||||
|
let book: &AddressBook = &c.accounts[account_cursor].address_book;
|
||||||
|
let results: Vec<String> = book.search(term);
|
||||||
|
results})));
|
||||||
|
} else {
|
||||||
|
self.form.push((k.into(), headers[k].to_string()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,10 +219,10 @@ impl Composer {
|
||||||
impl Component for Composer {
|
impl Component for Composer {
|
||||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||||
if !self.initialized {
|
if !self.initialized {
|
||||||
clear_area(grid, area);
|
|
||||||
self.update_form();
|
self.update_form();
|
||||||
self.initialized = true;
|
self.initialized = true;
|
||||||
}
|
}
|
||||||
|
clear_area(grid, area);
|
||||||
|
|
||||||
let upper_left = upper_left!(area);
|
let upper_left = upper_left!(area);
|
||||||
let bottom_right = bottom_right!(area);
|
let bottom_right = bottom_right!(area);
|
||||||
|
|
|
@ -16,25 +16,33 @@ impl Default for FormFocus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, )]
|
|
||||||
pub enum Field {
|
pub enum Field {
|
||||||
Text(String, Cursor),
|
Text(String, Cursor, Option<(Box<Fn(&Context, &str) -> Vec<String> + Send>, AutoComplete)>),
|
||||||
Choice(Vec<String>, Cursor),
|
Choice(Vec<String>, Cursor),
|
||||||
TextArea(String, Cursor),
|
TextArea(String, Cursor),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for Field {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Text(s, c, _) => fmt::Debug::fmt(s, f),
|
||||||
|
k => fmt::Debug::fmt(k, f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use Field::*;
|
use Field::*;
|
||||||
|
|
||||||
impl Default for Field {
|
impl Default for Field {
|
||||||
fn default() -> Field {
|
fn default() -> Field {
|
||||||
Field::Text(String::new(), 0)
|
Field::Text(String::new(), 0, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Field {
|
impl Field {
|
||||||
fn as_str(&self) -> &str {
|
fn as_str(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Text(ref s, _) => {
|
Text(ref s, _, _) => {
|
||||||
s
|
s
|
||||||
},
|
},
|
||||||
TextArea(ref s, _) => {
|
TextArea(ref s, _) => {
|
||||||
|
@ -52,7 +60,7 @@ impl Field {
|
||||||
|
|
||||||
pub fn into_string(self) -> String {
|
pub fn into_string(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Text(s, _) => {
|
Text(s, _, _) => {
|
||||||
s
|
s
|
||||||
},
|
},
|
||||||
TextArea(s, _) => {
|
TextArea(s, _) => {
|
||||||
|
@ -64,11 +72,17 @@ impl Field {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_cursor(&self, grid: &mut CellBuffer, area: Area) {
|
fn draw_cursor(&mut self, grid: &mut CellBuffer, area: Area, secondary_area: Area, context: &mut Context) {
|
||||||
let upper_left = upper_left!(area);
|
let upper_left = upper_left!(area);
|
||||||
match self {
|
match self {
|
||||||
Text(_, cursor) => {
|
Text(ref term, cursor, auto_complete_fn) => {
|
||||||
change_colors(grid, (pos_inc(upper_left, (*cursor, 0)), (pos_inc(upper_left, (*cursor, 0)))), Color::Default, Color::Byte(248));
|
change_colors(grid, (pos_inc(upper_left, (*cursor, 0)), (pos_inc(upper_left, (*cursor, 0)))), Color::Default, Color::Byte(248));
|
||||||
|
if term.chars().count() <= 2 { return; }
|
||||||
|
if let Some((auto_complete_fn, auto_complete)) = auto_complete_fn {
|
||||||
|
let entries = auto_complete_fn(context, term);
|
||||||
|
auto_complete.set_suggestions(entries);
|
||||||
|
auto_complete.draw(grid, secondary_area, context);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
TextArea(_, _) => {
|
TextArea(_, _) => {
|
||||||
},
|
},
|
||||||
|
@ -90,10 +104,37 @@ impl Component for Field {
|
||||||
true);
|
true);
|
||||||
}
|
}
|
||||||
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
|
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
|
||||||
|
if let Text(ref mut s, ref mut cursor, Some((_, auto_complete))) = self {
|
||||||
|
match event.event_type {
|
||||||
|
UIEventType::InsertInput(Key::Char('\t')) => {
|
||||||
|
if let Some(suggestion) = auto_complete.get_suggestion() {
|
||||||
|
*s = suggestion;
|
||||||
|
*cursor = s.chars().count();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match event.event_type {
|
match event.event_type {
|
||||||
|
UIEventType::InsertInput(Key::Up) => {
|
||||||
|
if let Text(_, _, Some((_, auto_complete))) = self {
|
||||||
|
auto_complete.dec_cursor();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UIEventType::InsertInput(Key::Down) => {
|
||||||
|
if let Text(_, _, Some((_, auto_complete))) = self {
|
||||||
|
auto_complete.inc_cursor();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
UIEventType::InsertInput(Key::Right) => {
|
UIEventType::InsertInput(Key::Right) => {
|
||||||
match self {
|
match self {
|
||||||
TextArea(ref s, ref mut cursor) | Text(ref s, ref mut cursor) => {
|
TextArea(ref s, ref mut cursor) | Text(ref s, ref mut cursor, _) => {
|
||||||
if *cursor < s.len() {
|
if *cursor < s.len() {
|
||||||
*cursor += 1;
|
*cursor += 1;
|
||||||
}
|
}
|
||||||
|
@ -109,7 +150,7 @@ impl Component for Field {
|
||||||
},
|
},
|
||||||
UIEventType::InsertInput(Key::Left) => {
|
UIEventType::InsertInput(Key::Left) => {
|
||||||
match self {
|
match self {
|
||||||
TextArea(_, ref mut cursor) | Text(_, ref mut cursor) => {
|
TextArea(_, ref mut cursor) | Text(_, ref mut cursor, _) => {
|
||||||
if *cursor == 0 {
|
if *cursor == 0 {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -127,7 +168,7 @@ impl Component for Field {
|
||||||
},
|
},
|
||||||
UIEventType::InsertInput(Key::Char(k)) => {
|
UIEventType::InsertInput(Key::Char(k)) => {
|
||||||
match self {
|
match self {
|
||||||
Text(ref mut s, ref mut cursor) | TextArea(ref mut s, ref mut cursor) => {
|
Text(ref mut s, ref mut cursor, _) | TextArea(ref mut s, ref mut cursor) => {
|
||||||
s.insert(*cursor, k);
|
s.insert(*cursor, k);
|
||||||
*cursor += 1;
|
*cursor += 1;
|
||||||
},
|
},
|
||||||
|
@ -136,7 +177,14 @@ impl Component for Field {
|
||||||
},
|
},
|
||||||
UIEventType::InsertInput(Key::Backspace) => {
|
UIEventType::InsertInput(Key::Backspace) => {
|
||||||
match self {
|
match self {
|
||||||
Text(ref mut s, ref mut cursor) | TextArea(ref mut s, ref mut cursor) => {
|
Text(ref mut s, ref mut cursor, ref mut auto_complete) => {
|
||||||
|
if *cursor > 0 {
|
||||||
|
*cursor -= 1;
|
||||||
|
s.remove(*cursor);
|
||||||
|
}
|
||||||
|
auto_complete.as_mut().map(|ac| ac.1.set_suggestions(Vec::new()));
|
||||||
|
},
|
||||||
|
TextArea(ref mut s, ref mut cursor) => {
|
||||||
if *cursor > 0 {
|
if *cursor > 0 {
|
||||||
*cursor -= 1;
|
*cursor -= 1;
|
||||||
s.remove(*cursor);
|
s.remove(*cursor);
|
||||||
|
@ -224,10 +272,15 @@ impl FormWidget {
|
||||||
self.layout.push(value.0.clone());
|
self.layout.push(value.0.clone());
|
||||||
self.fields.insert(value.0, TextArea(value.1, 0));
|
self.fields.insert(value.0, TextArea(value.1, 0));
|
||||||
}
|
}
|
||||||
|
pub fn push_cl(&mut self, value: (String, String, Box<Fn(&Context, &str) -> Vec<String> + Send>)) {
|
||||||
|
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(value.1, 0, Some((value.2, AutoComplete::new(Vec::new())))));
|
||||||
|
}
|
||||||
pub fn push(&mut self, value: (String, String)) {
|
pub fn push(&mut self, value: (String, String)) {
|
||||||
self.field_name_max_length = std::cmp::max(self.field_name_max_length, value.0.len());
|
self.field_name_max_length = std::cmp::max(self.field_name_max_length, value.0.len());
|
||||||
self.layout.push(value.0.clone());
|
self.layout.push(value.0.clone());
|
||||||
self.fields.insert(value.0, Text(value.1, 0));
|
self.fields.insert(value.0, Text(value.1, 0, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, index: usize, value: (String, Field)) {
|
pub fn insert(&mut self, index: usize, value: (String, Field)) {
|
||||||
|
@ -274,12 +327,14 @@ impl Component for FormWidget {
|
||||||
/* Highlight if necessary */
|
/* Highlight if necessary */
|
||||||
if i == self.cursor {
|
if i == self.cursor {
|
||||||
if self.focus == FormFocus::Fields {
|
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));
|
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 {
|
if self.focus == FormFocus::TextInput {
|
||||||
v.draw_cursor(grid,
|
v.draw_cursor(grid,
|
||||||
(pos_inc(upper_left, (self.field_name_max_length + 3 , i)),
|
(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),
|
||||||
|
context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,9 +357,17 @@ impl Component for FormWidget {
|
||||||
UIEventType::Input(Key::Up) if self.focus == FormFocus::Buttons => {
|
UIEventType::Input(Key::Up) if self.focus == FormFocus::Buttons => {
|
||||||
self.focus = FormFocus::Fields;
|
self.focus = FormFocus::Fields;
|
||||||
},
|
},
|
||||||
|
UIEventType::InsertInput(Key::Up) if self.focus == FormFocus::TextInput => {
|
||||||
|
let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap();
|
||||||
|
field.process_event(event, context);
|
||||||
|
},
|
||||||
UIEventType::Input(Key::Up) => {
|
UIEventType::Input(Key::Up) => {
|
||||||
self.cursor = self.cursor.saturating_sub(1);
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
},
|
},
|
||||||
|
UIEventType::InsertInput(Key::Down) if self.focus == FormFocus::TextInput => {
|
||||||
|
let field = self.fields.get_mut(&self.layout[self.cursor]).unwrap();
|
||||||
|
field.process_event(event, context);
|
||||||
|
},
|
||||||
UIEventType::Input(Key::Down) if self.cursor < self.layout.len().saturating_sub(1) => {
|
UIEventType::Input(Key::Down) if self.cursor < self.layout.len().saturating_sub(1) => {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
},
|
},
|
||||||
|
@ -315,6 +378,10 @@ impl Component for FormWidget {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
UIEventType::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);
|
||||||
|
},
|
||||||
UIEventType::Input(Key::Char('\n')) if self.focus == FormFocus::Fields => {
|
UIEventType::Input(Key::Char('\n')) if self.focus == FormFocus::Fields => {
|
||||||
self.focus = FormFocus::TextInput;
|
self.focus = FormFocus::TextInput;
|
||||||
context.replies.push_back(UIEvent {
|
context.replies.push_back(UIEvent {
|
||||||
|
@ -443,3 +510,103 @@ impl<T> Component for ButtonWidget<T> where T: std::fmt::Debug + Default + Send
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct AutoComplete {
|
||||||
|
entries: Vec<String>,
|
||||||
|
content: CellBuffer,
|
||||||
|
cursor: usize,
|
||||||
|
|
||||||
|
dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
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; };
|
||||||
|
|
||||||
|
let upper_left = upper_left!(area);
|
||||||
|
self.dirty = false;
|
||||||
|
let (width, height) = self.content.size();
|
||||||
|
copy_area(
|
||||||
|
grid,
|
||||||
|
&self.content,
|
||||||
|
area,
|
||||||
|
((0, 0), (width.saturating_sub(1), height.saturating_sub(1))),
|
||||||
|
);
|
||||||
|
/* Highlight cursor */
|
||||||
|
change_colors(grid, (pos_inc(upper_left, (0, self.cursor)), pos_inc(upper_left, (width.saturating_sub(1), self.cursor))), Color::Default, Color::Byte(246));
|
||||||
|
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) {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AutoComplete {
|
||||||
|
pub fn new(entries: Vec<String>) -> Self {
|
||||||
|
let mut ret = AutoComplete {
|
||||||
|
entries: Vec::new(),
|
||||||
|
content: CellBuffer::default(),
|
||||||
|
cursor: 0,
|
||||||
|
dirty: true,
|
||||||
|
};
|
||||||
|
ret.set_suggestions(entries);
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_suggestions(&mut self, entries: Vec<String>) {
|
||||||
|
if entries.len() == self.entries.len() && entries == self.entries {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut content = CellBuffer::new(entries.iter().map(|e| e.len()).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,
|
||||||
|
&mut content,
|
||||||
|
Color::Byte(23),
|
||||||
|
Color::Byte(7),
|
||||||
|
((0, i), (width - 1, i)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
write_string_to_grid(
|
||||||
|
"▒",
|
||||||
|
&mut content,
|
||||||
|
Color::Byte(23),
|
||||||
|
Color::Byte(7),
|
||||||
|
((width - 1, i), (width - 1, i)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.content = content;
|
||||||
|
self.entries = entries;
|
||||||
|
self.cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inc_cursor(&mut self) { if self.cursor < self.entries.len().saturating_sub(1) { self.cursor += 1; self.set_dirty(); } }
|
||||||
|
pub fn dec_cursor(&mut self) { self.cursor = self.cursor.saturating_sub(1); self.set_dirty(); }
|
||||||
|
|
||||||
|
pub fn get_suggestion(&mut self) -> Option<String> {
|
||||||
|
if self.entries.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ret = self.entries.remove(self.cursor);
|
||||||
|
self.entries.clear();
|
||||||
|
self.cursor = 0;
|
||||||
|
self.content.empty();
|
||||||
|
Some(ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -177,6 +177,13 @@ impl CellBuffer {
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.buf.is_empty()
|
self.buf.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn empty(&mut self) {
|
||||||
|
self.buf.clear();
|
||||||
|
self.cols = 0;
|
||||||
|
self.rows = 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasSize for CellBuffer {
|
impl HasSize for CellBuffer {
|
||||||
|
|
|
@ -161,6 +161,12 @@ pub type Size = (usize, usize);
|
||||||
|
|
||||||
pub trait HasSize {
|
pub trait HasSize {
|
||||||
fn size(&self) -> Size;
|
fn size(&self) -> Size;
|
||||||
|
fn cols(&self) -> usize {
|
||||||
|
self.size().0
|
||||||
|
}
|
||||||
|
fn rows(&self) -> usize {
|
||||||
|
self.size().1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait HasPosition {
|
pub trait HasPosition {
|
||||||
|
|
Loading…
Reference in New Issue