diff options
author | Jiayi Zhao <jeff.no.zhao@gmail.com> | 2020-02-22 19:54:41 -0500 |
---|---|---|
committer | Jiayi Zhao <jeff.no.zhao@gmail.com> | 2020-02-22 19:54:41 -0500 |
commit | d38dcdbbee44187bdb605dbf9bbf9c6c6d3e4f35 (patch) | |
tree | 91b059b99793fe250c67f66be3930a298117e0b3 /src/ui | |
parent | 03594099dafb4cda04e50f087df61cf76e2034d0 (diff) |
implement textfield widget
- for asking users for long input strings
- implement prompt widget
- for prompting users for a single key response
Diffstat (limited to 'src/ui')
-rw-r--r-- | src/ui/widgets/mod.rs | 2 | ||||
-rw-r--r-- | src/ui/widgets/tui_prompt.rs | 60 | ||||
-rw-r--r-- | src/ui/widgets/tui_textfield.rs | 176 | ||||
-rw-r--r-- | src/ui/widgets/tui_view.rs | 40 |
4 files changed, 258 insertions, 20 deletions
diff --git a/src/ui/widgets/mod.rs b/src/ui/widgets/mod.rs index e36c904..49fbfc5 100644 --- a/src/ui/widgets/mod.rs +++ b/src/ui/widgets/mod.rs @@ -2,6 +2,7 @@ pub mod tui_dirlist; pub mod tui_dirlist_detailed; pub mod tui_footer; pub mod tui_menu; +pub mod tui_prompt; pub mod tui_textfield; pub mod tui_topbar; pub mod tui_view; @@ -10,6 +11,7 @@ pub use self::tui_dirlist::TuiDirList; pub use self::tui_dirlist_detailed::TuiDirListDetailed; pub use self::tui_footer::TuiFooter; pub use self::tui_menu::{TuiCommandMenu, TuiMenu}; +pub use self::tui_prompt::TuiPrompt; pub use self::tui_textfield::TuiTextField; pub use self::tui_topbar::TuiTopBar; pub use self::tui_view::TuiView; diff --git a/src/ui/widgets/tui_prompt.rs b/src/ui/widgets/tui_prompt.rs new file mode 100644 index 0000000..21526f3 --- /dev/null +++ b/src/ui/widgets/tui_prompt.rs @@ -0,0 +1,60 @@ +use termion::event::Key; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget}; +use unicode_width::UnicodeWidthStr; + +use crate::context::JoshutoContext; +use crate::ui::TuiBackend; +use crate::util::event::{Event, Events}; + +use super::TuiView; + +pub struct TuiPrompt<'a> { + prompt: &'a str, +} + +impl<'a> TuiPrompt<'a> { + pub fn new(prompt: &'a str) -> Self { + Self { prompt } + } + + pub fn get_key(&mut self, backend: &mut TuiBackend, context: &JoshutoContext) -> Key { + loop { + backend.terminal.draw(|mut frame| { + let f_size = frame.size(); + if f_size.height == 0 { + return; + } + + { + let mut view = TuiView::new(&context); + view.show_bottom_status = false; + view.render(&mut frame, f_size); + } + + let prompt_style = Style::default().fg(Color::LightYellow); + + let text = [Text::styled(self.prompt, prompt_style)]; + + let textfield_rect = Rect { + x: 0, + y: f_size.height - 1, + width: f_size.width, + height: 1, + }; + + Paragraph::new(text.iter()) + .wrap(true) + .render(&mut frame, textfield_rect); + }); + + if let Ok(event) = context.events.next() { + match event { + Event::Input(key) => return key, + _ => {} + }; + } + } + } +} diff --git a/src/ui/widgets/tui_textfield.rs b/src/ui/widgets/tui_textfield.rs index c0f54a8..9dbbad3 100644 --- a/src/ui/widgets/tui_textfield.rs +++ b/src/ui/widgets/tui_textfield.rs @@ -1,9 +1,11 @@ +use std::io::Write; + use rustyline::completion::{Candidate, Completer, FilenameCompleter, Pair}; use rustyline::line_buffer; -use termion::clear; use termion::cursor::Goto; use termion::event::Key; +use tui::backend::Backend; use tui::layout::Rect; use tui::style::{Color, Style}; use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget}; @@ -13,7 +15,7 @@ use crate::context::JoshutoContext; use crate::ui::TuiBackend; use crate::util::event::{Event, Events}; -use super::TuiMenu; +use super::{TuiMenu, TuiView}; struct CompletionTracker { pub index: usize, @@ -34,12 +36,31 @@ impl CompletionTracker { } pub struct TuiTextField<'a> { - menu: Option<&'a mut TuiMenu<'a>>, + _prompt: &'a str, + _prefix: &'a str, + _suffix: &'a str, + _menu: Option<&'a mut TuiMenu<'a>>, } impl<'a> TuiTextField<'a> { - pub fn new(menu: &'a mut TuiMenu<'a>) -> Self { - Self { menu: Some(menu) } + pub fn menu(mut self, menu: &'a mut TuiMenu<'a>) -> Self { + self._menu = Some(menu); + self + } + + pub fn prompt(mut self, prompt: &'a str) -> Self { + self._prompt = prompt; + self + } + + pub fn prefix(mut self, prefix: &'a str) -> Self { + self._prefix = prefix; + self + } + + pub fn suffix(mut self, suffix: &'a str) -> Self { + self._suffix = suffix; + self } pub fn get_input( @@ -47,11 +68,44 @@ impl<'a> TuiTextField<'a> { backend: &mut TuiBackend, context: &JoshutoContext, ) -> Option<String> { - let mut input_string = String::with_capacity(64); + let mut line_buffer = line_buffer::LineBuffer::with_capacity(255); + let completer = FilenameCompleter::new(); + + let mut completion_tracker: Option<CompletionTracker> = None; + + let mut char_idx = self + ._prefix + .char_indices() + .last() + .map(|(i, c)| i) + .unwrap_or(0); + + line_buffer.insert_str(0, self._prefix); + line_buffer.insert_str(line_buffer.len(), self._suffix); + line_buffer.set_pos(char_idx); + + backend.terminal.show_cursor(); + let mut cursor_xpos = line_buffer.pos() + 1; + { + let frame = backend.terminal.get_frame(); + let f_size = frame.size(); + backend + .terminal + .set_cursor(cursor_xpos as u16, f_size.height - 1); + } loop { backend.terminal.draw(|mut frame| { let f_size = frame.size(); + if f_size.height == 0 { + return; + } + + { + let mut view = TuiView::new(&context); + view.show_bottom_status = false; + view.render(&mut frame, f_size); + } let top_rect = Rect { x: 0, @@ -60,27 +114,129 @@ impl<'a> TuiTextField<'a> { height: 1, }; - if let Some(menu) = self.menu.as_mut() { + if let Some(menu) = self._menu.as_mut() { menu.render(&mut frame, top_rect); } + + let cmd_prompt_style = Style::default().fg(Color::LightGreen); + + let text = [ + Text::styled(self._prompt, cmd_prompt_style), + Text::raw(line_buffer.as_str()), + ]; + + let textfield_rect = Rect { + x: 0, + y: f_size.height - 1, + width: f_size.width, + height: 1, + }; + + Paragraph::new(text.iter()) + .wrap(true) + .render(&mut frame, textfield_rect); }); if let Ok(event) = context.events.next() { match event { + Event::Input(Key::Backspace) => { + if line_buffer.backspace(1) { + completion_tracker.take(); + } + } + Event::Input(Key::Left) => { + if line_buffer.move_backward(1) { + completion_tracker.take(); + } + } + Event::Input(Key::Right) => { + if line_buffer.move_forward(1) { + completion_tracker.take(); + } + } + Event::Input(Key::Delete) => { + if line_buffer.delete(1).is_some() { + completion_tracker.take(); + } + } + Event::Input(Key::Home) => { + line_buffer.move_end(); + completion_tracker.take(); + } + Event::Input(Key::End) => { + line_buffer.move_end(); + completion_tracker.take(); + } + Event::Input(Key::Up) => {} + Event::Input(Key::Down) => {} Event::Input(Key::Esc) => { + backend.terminal.hide_cursor(); return None; } + Event::Input(Key::Char('\t')) => { + if completion_tracker.is_none() { + let res = + completer.complete_path(line_buffer.as_str(), line_buffer.pos()); + if let Ok((pos, mut candidates)) = res { + candidates.sort_by(|x, y| { + x.display() + .partial_cmp(y.display()) + .unwrap_or(std::cmp::Ordering::Less) + }); + let ct = CompletionTracker::new( + pos, + candidates, + String::from(line_buffer.as_str()), + ); + completion_tracker = Some(ct); + } + } + + if let Some(ref mut s) = completion_tracker { + if s.index < s.candidates.len() { + let candidate = &s.candidates[s.index]; + completer.update(&mut line_buffer, s.pos, candidate.replacement()); + s.index += 1; + } + } + } Event::Input(Key::Char('\n')) => { break; } Event::Input(Key::Char(c)) => { - input_string.push(c); + if line_buffer.insert(c, 1).is_some() { + completion_tracker.take(); + } } _ => {} }; } + cursor_xpos = line_buffer.pos() + 1; + { + let frame = backend.terminal.get_frame(); + let f_size = frame.size(); + backend + .terminal + .set_cursor(cursor_xpos as u16, f_size.height - 1); + } + } + backend.terminal.hide_cursor(); + if line_buffer.as_str().is_empty() { + None + } else { + let strin = line_buffer.to_string(); + Some(strin) + } + } +} + +impl<'a> std::default::Default for TuiTextField<'a> { + fn default() -> Self { + Self { + _prompt: "", + _prefix: "", + _suffix: "", + _menu: None, } - eprintln!("You typed: {}", input_string); - Some(input_string) } } diff --git a/src/ui/widgets/tui_view.rs b/src/ui/widgets/tui_view.rs index 57f644c..0f2d64d 100644 --- a/src/ui/widgets/tui_view.rs +++ b/src/ui/widgets/tui_view.rs @@ -1,6 +1,7 @@ use tui::buffer::Buffer; use tui::layout::{Direction, Layout, Rect}; -use tui::widgets::Widget; +use tui::style::{Color, Modifier, Style}; +use tui::widgets::{Paragraph, Text, Widget}; use unicode_width::UnicodeWidthStr; use super::{TuiDirList, TuiDirListDetailed, TuiFooter, TuiTopBar}; @@ -8,13 +9,17 @@ use crate::context::JoshutoContext; pub struct TuiView<'a> { pub context: &'a JoshutoContext, + pub show_bottom_status: bool, } use super::super::{DEFAULT_LAYOUT, NO_PREVIEW_LAYOUT}; impl<'a> TuiView<'a> { pub fn new(context: &'a JoshutoContext) -> Self { - Self { context } + Self { + context, + show_bottom_status: true, + } } } @@ -55,15 +60,30 @@ impl<'a> Widget for TuiView<'a> { if let Some(curr_list) = curr_list.as_ref() { TuiDirListDetailed::new(&curr_list).draw(layout_rect[1], buf); + let rect = Rect { + x: 0, + y: f_size.height - 1, + width: f_size.width, + height: 1, + }; + + let message_style = Style::default() + .fg(Color::LightCyan) + .modifier(Modifier::BOLD); + + if self.show_bottom_status { + /* draw the bottom status bar */ + if let Some(msg) = self.context.worker_msg.as_ref() { + let text = [Text::styled(msg, message_style)]; + + Paragraph::new(text.iter()).wrap(true).draw(rect, buf); + } else if !self.context.message_queue.is_empty() { + let text = [Text::styled(&self.context.message_queue[0], message_style)]; - if let Some(entry) = curr_list.get_curr_ref() { - let rect = Rect { - x: 0, - y: f_size.height - 1, - width: f_size.width, - height: 1, - }; - TuiFooter::new(entry).draw(rect, buf); + Paragraph::new(text.iter()).wrap(true).draw(rect, buf); + } else if let Some(entry) = curr_list.get_curr_ref() { + TuiFooter::new(entry).draw(rect, buf); + } } }; |