summaryrefslogtreecommitdiffstats
path: root/src/app/widgets/base
diff options
context:
space:
mode:
authorClementTsang <cjhtsang@uwaterloo.ca>2021-08-30 23:48:11 -0400
committerClementTsang <cjhtsang@uwaterloo.ca>2021-09-05 19:09:11 -0400
commitb1889b09344fea9eef98e5c2b9ce3b269dc8234b (patch)
tree2f0650e02a5256249fededd249603f5dda539aea /src/app/widgets/base
parent27736b7fc048a9fbc8621d98ee99a4eb521a8c24 (diff)
refactor: add text input
Diffstat (limited to 'src/app/widgets/base')
-rw-r--r--src/app/widgets/base/scrollable.rs27
-rw-r--r--src/app/widgets/base/sort_menu.rs2
-rw-r--r--src/app/widgets/base/sort_text_table.rs10
-rw-r--r--src/app/widgets/base/text_input.rs344
-rw-r--r--src/app/widgets/base/text_table.rs25
-rw-r--r--src/app/widgets/base/time_graph.rs2
6 files changed, 321 insertions, 89 deletions
diff --git a/src/app/widgets/base/scrollable.rs b/src/app/widgets/base/scrollable.rs
index dc55a717..2d708ece 100644
--- a/src/app/widgets/base/scrollable.rs
+++ b/src/app/widgets/base/scrollable.rs
@@ -1,3 +1,5 @@
+use std::cmp::Ordering;
+
use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use tui::{layout::Rect, widgets::TableState};
@@ -83,7 +85,6 @@ impl Scrollable {
} else if self.current_index >= num_visible_rows {
// Else if the current position past the last element visible in the list, omit
// until we can see that element. The +1 is of how indexes start at 0.
-
self.window_index.index = self.current_index - num_visible_rows + 1;
self.window_index.index
} else {
@@ -96,6 +97,8 @@ impl Scrollable {
// If it's past the first element, then show from that element downwards
self.window_index.index = self.current_index;
} else if self.current_index >= self.window_index.index + num_visible_rows {
+ // Else, if the current index is off screen (sometimes caused by a sudden size change),
+ // just put it so that the selected index is the last entry,
self.window_index.index = self.current_index - num_visible_rows + 1;
}
// Else, don't change what our start position is from whatever it is set to!
@@ -111,8 +114,6 @@ impl Scrollable {
/// Update the index with this! This will automatically update the scroll direction as well!
pub fn set_index(&mut self, new_index: usize) {
- use std::cmp::Ordering;
-
match new_index.cmp(&self.current_index) {
Ordering::Greater => {
self.current_index = new_index;
@@ -156,9 +157,7 @@ impl Scrollable {
}
let new_index = self.current_index + change_by;
- if new_index >= self.num_items {
- WidgetEventResult::NoRedraw
- } else if self.current_index == new_index {
+ if new_index >= self.num_items || self.current_index == new_index {
WidgetEventResult::NoRedraw
} else {
self.set_index(new_index);
@@ -234,12 +233,16 @@ impl Component for Scrollable {
let y = usize::from(event.row - self.bounds.top());
if let Some(selected) = self.tui_state.selected() {
- if y > selected {
- let offset = y - selected;
- return self.move_down(offset);
- } else if y < selected {
- let offset = selected - y;
- return self.move_up(offset);
+ match y.cmp(&selected) {
+ Ordering::Less => {
+ let offset = selected - y;
+ return self.move_up(offset);
+ }
+ Ordering::Equal => {}
+ Ordering::Greater => {
+ let offset = y - selected;
+ return self.move_down(offset);
+ }
}
}
}
diff --git a/src/app/widgets/base/sort_menu.rs b/src/app/widgets/base/sort_menu.rs
index fc85d0bb..3265cb59 100644
--- a/src/app/widgets/base/sort_menu.rs
+++ b/src/app/widgets/base/sort_menu.rs
@@ -49,7 +49,7 @@ impl SortMenu {
let data = columns
.iter()
- .map(|c| vec![(c.original_name().clone().into(), None, None)])
+ .map(|c| vec![(c.original_name().clone(), None, None)])
.collect::<Vec<_>>();
self.table
diff --git a/src/app/widgets/base/sort_text_table.rs b/src/app/widgets/base/sort_text_table.rs
index e8f0ff18..2b59f948 100644
--- a/src/app/widgets/base/sort_text_table.rs
+++ b/src/app/widgets/base/sort_text_table.rs
@@ -11,7 +11,7 @@ use crate::{
canvas::Painter,
};
-use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn, TextTableData};
+use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn, TextTableDataRef};
fn get_shortcut_name(e: &KeyEvent) -> String {
let modifier = if e.modifiers.is_empty() {
@@ -48,7 +48,7 @@ fn get_shortcut_name(e: &KeyEvent) -> String {
KeyCode::Esc => "Esc".into(),
};
- format!("({}{})", modifier, key).into()
+ format!("({}{})", modifier, key)
}
#[derive(Copy, Clone, Debug)]
@@ -346,8 +346,8 @@ where
/// Note if the number of columns don't match in the [`SortableTextTable`] and data,
/// it will only create as many columns as it can grab data from both sources from.
pub fn draw_tui_table<B: Backend>(
- &mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>,
- block_area: Rect, show_selected_entry: bool,
+ &mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef,
+ block: Block<'_>, block_area: Rect, show_selected_entry: bool,
) {
self.table
.draw_tui_table(painter, f, data, block, block_area, show_selected_entry);
@@ -360,7 +360,7 @@ where
{
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
for (index, column) in self.table.columns.iter().enumerate() {
- if let &Some((shortcut, _)) = column.shortcut() {
+ if let Some((shortcut, _)) = *column.shortcut() {
if shortcut == event {
self.set_sort_index(index);
return WidgetEventResult::Signal(ReturnSignal::Update);
diff --git a/src/app/widgets/base/text_input.rs b/src/app/widgets/base/text_input.rs
index 597dff6e..a6119c79 100644
--- a/src/app/widgets/base/text_input.rs
+++ b/src/app/widgets/base/text_input.rs
@@ -1,47 +1,87 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
-use tui::layout::Rect;
+use itertools::Itertools;
+use tui::{
+ backend::Backend,
+ layout::{Alignment, Rect},
+ text::{Span, Spans},
+ widgets::Paragraph,
+ Frame,
+};
+use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
+use unicode_width::UnicodeWidthStr;
-use crate::app::{
- event::WidgetEventResult::{self},
- Component,
+use crate::{
+ app::{
+ event::{
+ ReturnSignal,
+ WidgetEventResult::{self},
+ },
+ Component,
+ },
+ canvas::Painter,
};
+enum CursorDirection {
+ Left,
+ Right,
+}
+
+/// We save the previous window index for future reference, but we must invalidate if the area changes.
#[derive(Default)]
+struct WindowIndex {
+ start_index: usize,
+ cached_area: Rect,
+}
+
/// A single-line component for taking text inputs.
pub struct TextInput {
text: String,
- cursor_index: usize,
bounds: Rect,
- border_bounds: Rect,
+ cursor: GraphemeCursor,
+
+ cursor_direction: CursorDirection,
+ window_index: WindowIndex,
}
-impl TextInput {
- /// Creates a new [`TextInput`].
- pub fn new() -> Self {
+impl Default for TextInput {
+ fn default() -> Self {
Self {
- ..Default::default()
+ text: Default::default(),
+ bounds: Default::default(),
+ cursor: GraphemeCursor::new(0, 0, true),
+ cursor_direction: CursorDirection::Right,
+ window_index: Default::default(),
}
}
+}
- fn set_cursor(&mut self, new_cursor_index: usize) -> WidgetEventResult {
- if self.cursor_index == new_cursor_index {
- WidgetEventResult::NoRedraw
- } else {
- self.cursor_index = new_cursor_index;
- WidgetEventResult::Redraw
- }
+impl TextInput {
+ /// Returns a reference to the current query.
+ pub fn query(&self) -> &str {
+ &self.text
}
- fn move_back(&mut self, amount_to_subtract: usize) -> WidgetEventResult {
- self.set_cursor(self.cursor_index.saturating_sub(amount_to_subtract))
+ fn move_back(&mut self) -> usize {
+ let current_position = self.cursor.cur_cursor();
+ if let Ok(Some(new_position)) = self.cursor.prev_boundary(&self.text[..current_position], 0)
+ {
+ self.cursor_direction = CursorDirection::Left;
+ new_position
+ } else {
+ current_position
+ }
}
- fn move_forward(&mut self, amount_to_add: usize) -> WidgetEventResult {
- let new_cursor = self.cursor_index + amount_to_add;
- if new_cursor >= self.text.len() {
- self.set_cursor(self.text.len() - 1)
+ fn move_forward(&mut self) -> usize {
+ let current_position = self.cursor.cur_cursor();
+ if let Ok(Some(new_position)) = self
+ .cursor
+ .next_boundary(&self.text[current_position..], current_position)
+ {
+ self.cursor_direction = CursorDirection::Right;
+ new_position
} else {
- self.set_cursor(new_cursor)
+ current_position
}
}
@@ -50,38 +90,199 @@ impl TextInput {
WidgetEventResult::NoRedraw
} else {
self.text = String::default();
- self.cursor_index = 0;
- WidgetEventResult::Redraw
+ self.cursor = GraphemeCursor::new(0, 0, true);
+ self.window_index = Default::default();
+ self.cursor_direction = CursorDirection::Left;
+ WidgetEventResult::Signal(ReturnSignal::Update)
}
}
fn move_word_forward(&mut self) -> WidgetEventResult {
- // TODO: Implement this
- WidgetEventResult::NoRedraw
+ let current_index = self.cursor.cur_cursor();
+
+ for (index, _word) in self.text[current_index..].unicode_word_indices() {
+ if index > current_index {
+ self.cursor.set_cursor(index);
+ self.cursor_direction = CursorDirection::Right;
+ return WidgetEventResult::Redraw;
+ }
+ }
+
+ self.cursor.set_cursor(self.text.len());
+ WidgetEventResult::Redraw
}
fn move_word_back(&mut self) -> WidgetEventResult {
- // TODO: Implement this
+ let current_index = self.cursor.cur_cursor();
+
+ for (index, _word) in self.text[..current_index].unicode_word_indices().rev() {
+ if index < current_index {
+ self.cursor.set_cursor(index);
+ self.cursor_direction = CursorDirection::Left;
+ return WidgetEventResult::Redraw;
+ }
+ }
+
WidgetEventResult::NoRedraw
}
- fn clear_previous_word(&mut self) -> WidgetEventResult {
- // TODO: Implement this
- WidgetEventResult::NoRedraw
+ fn clear_word_from_cursor(&mut self) -> WidgetEventResult {
+ // Fairly simple logic - create the word index iterator, skip the word that intersects with the current
+ // cursor location, draw the rest, update the string.
+ let current_index = self.cursor.cur_cursor();
+ let mut start_delete_index = 0;
+ let mut saw_non_whitespace = false;
+ for (index, word) in self.text[..current_index].split_word_bound_indices().rev() {
+ if word.trim().is_empty() {
+ if saw_non_whitespace {
+ // It's whitespace! Stop!
+ break;
+ }
+ } else {
+ saw_non_whitespace = true;
+ start_delete_index = index;
+ }
+ }
+
+ if start_delete_index == current_index {
+ WidgetEventResult::NoRedraw
+ } else {
+ self.text.drain(start_delete_index..current_index);
+ self.cursor = GraphemeCursor::new(start_delete_index, self.text.len(), true);
+ self.cursor_direction = CursorDirection::Left;
+ WidgetEventResult::Signal(ReturnSignal::Update)
+ }
}
fn clear_previous_grapheme(&mut self) -> WidgetEventResult {
- // TODO: Implement this
- WidgetEventResult::NoRedraw
+ let current_index = self.cursor.cur_cursor();
+
+ if current_index > 0 {
+ let new_index = self.move_back();
+ self.text.drain(new_index..current_index);
+
+ self.cursor = GraphemeCursor::new(new_index, self.text.len(), true);
+ self.cursor_direction = CursorDirection::Left;
+
+ WidgetEventResult::Signal(ReturnSignal::Update)
+ } else {
+ WidgetEventResult::NoRedraw
+ }
}
- pub fn update(&mut self, new_text: String) {
- self.text = new_text;
+ fn clear_current_grapheme(&mut self) -> WidgetEventResult {
+ let current_index = self.cursor.cur_cursor();
+
+ if current_index < self.text.len() {
+ let current_index_bound = self.move_forward();
+ self.text.drain(current_index..current_index_bound);
- if self.cursor_index >= self.text.len() {
- self.cursor_index = self.text.len() - 1;
+ self.cursor = GraphemeCursor::new(current_index, self.text.len(), true);
+ self.cursor_direction = CursorDirection::Left;
+
+ WidgetEventResult::Signal(ReturnSignal::Update)
+ } else {
+ WidgetEventResult::NoRedraw
}
}
+
+ fn insert_character(&mut self, c: char) -> WidgetEventResult {
+ let current_index = self.cursor.cur_cursor();
+ self.text.insert(current_index, c);
+ self.cursor = GraphemeCursor::new(current_index, self.text.len(), true);
+ self.move_forward();
+
+ WidgetEventResult::Signal(ReturnSignal::Update)
+ }
+
+ /// Updates the window indexes and returns the start index.
+ pub fn update_window_index(&mut self, num_visible_columns: usize) -> usize {
+ if self.window_index.cached_area != self.bounds {
+ self.window_index.start_index = 0;
+ self.window_index.cached_area = self.bounds;
+ }
+
+ let current_index = self.cursor.cur_cursor();
+
+ match self.cursor_direction {
+ CursorDirection::Right => {
+ if current_index < self.window_index.start_index + num_visible_columns {
+ self.window_index.start_index
+ } else if current_index >= num_visible_columns {
+ self.window_index.start_index = current_index - num_visible_columns + 1;
+ self.window_index.start_index
+ } else {
+ 0
+ }
+ }
+ CursorDirection::Left => {
+ if current_index <= self.window_index.start_index {
+ self.window_index.start_index = current_index;
+ } else if current_index >= self.window_index.start_index + num_visible_columns {
+ self.window_index.start_index = current_index - num_visible_columns + 1;
+ }
+ self.window_index.start_index
+ }
+ }
+ }
+
+ /// Draws the [`TextInput`] on screen.
+ pub fn draw_text_input<B: Backend>(
+ &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
+ ) {
+ self.set_bounds(area);
+
+ const SEARCH_PROMPT: &str = "> ";
+ let prompt = if area.width > 5 { SEARCH_PROMPT } else { "" };
+
+ let num_visible_columns = area.width as usize - prompt.len();
+ let start_position = self.update_window_index(num_visible_columns);
+ let cursor_start = self.cursor.cur_cursor();
+
+ let mut graphemes = self.text.grapheme_indices(true).peekable();
+ let mut current_grapheme_posn = 0;
+
+ graphemes
+ .peeking_take_while(|(index, _)| *index < start_position)
+ .for_each(|(_, s)| {
+ current_grapheme_posn += UnicodeWidthStr::width(s);
+ });
+
+ let before_cursor = graphemes
+ .peeking_take_while(|(index, _)| *index < cursor_start)
+ .map(|(_, grapheme)| grapheme)
+ .collect::<String>();
+
+ let cursor = graphemes
+ .next()
+ .map(|(_, grapheme)| grapheme)
+ .unwrap_or(" ");
+
+ let after_cursor = graphemes.map(|(_, grapheme)| grapheme).collect::<String>();
+
+ // FIXME: This is NOT done! This is an incomplete (but kinda working) implementation, for now.
+
+ let search_text = vec![Spans::from(vec![
+ Span::styled(
+ prompt,
+ if selected {
+ painter.colours.highlighted_border_style
+ } else {
+ painter.colours.text_style
+ },
+ ),
+ Span::styled(before_cursor, painter.colours.text_style),
+ Span::styled(cursor, painter.colours.currently_selected_text_style),
+ Span::styled(after_cursor, painter.colours.text_style),
+ ])];
+
+ f.render_widget(
+ Paragraph::new(search_text)
+ .style(painter.colours.text_style)
+ .alignment(Alignment::Left),
+ area,
+ );
+ }
}
impl Component for TextInput {
@@ -93,35 +294,59 @@ impl Component for TextInput {
self.bounds = new_bounds;
}
- fn border_bounds(&self) -> Rect {
- self.border_bounds
- }
-
- fn set_border_bounds(&mut self, new_bounds: Rect) {
- self.border_bounds = new_bounds;
- }
-
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
if event.modifiers.is_empty() {
match event.code {
- KeyCode::Left => self.move_back(1),
- KeyCode::Right => self.move_forward(1),
+ KeyCode::Left => {
+ let original_cursor = self.cursor.cur_cursor();
+ if self.move_back() == original_cursor {
+ WidgetEventResult::NoRedraw
+ } else {
+ WidgetEventResult::Redraw
+ }
+ }
+ KeyCode::Right => {
+ let original_cursor = self.cursor.cur_cursor();
+ if self.move_forward() == original_cursor {
+ WidgetEventResult::NoRedraw
+ } else {
+ WidgetEventResult::Redraw
+ }
+ }
KeyCode::Backspace => self.clear_previous_grapheme(),
+ KeyCode::Delete => self.clear_current_grapheme(),
+ KeyCode::Char(c) => self.insert_character(c),
_ => WidgetEventResult::NoRedraw,
}
} else if let KeyModifiers::CONTROL = event.modifiers {
match event.code {
- KeyCode::Char('a') => self.set_cursor(0),
- KeyCode::Char('e') => self.set_cursor(self.text.len()),
+ KeyCode::Char('a') => {
+ let prev_index = self.cursor.cur_cursor();
+ self.cursor.set_cursor(0);
+ if self.cursor.cur_cursor() == prev_index {
+ WidgetEventResult::NoRedraw
+ } else {
+ WidgetEventResult::Redraw
+ }
+ }
+ KeyCode::Char('e') => {
+ let prev_index = self.cursor.cur_cursor();
+ self.cursor.set_cursor(self.text.len());
+ if self.cursor.cur_cursor() == prev_index {
+ WidgetEventResult::NoRedraw
+ } else {
+ WidgetEventResult::Redraw
+ }
+ }
KeyCode::Char('u') => self.clear_text(),
- KeyCode::Char('w') => self.clear_previous_word(),
+ KeyCode::Char('w') => self.clear_word_from_cursor(),
KeyCode::Char('h') => self.clear_previous_grapheme(),
_ => WidgetEventResult::NoRedraw,
}
} else if let KeyModifiers::ALT = event.modifiers {
match event.code {
- KeyCode::Char('b') => self.move_word_forward(),
- KeyCode::Char('f') => self.move_word_back(),
+ KeyCode::Char('b') => self.move_word_back(),
+ KeyCode::Char('f') => self.move_word_forward(),
_ => WidgetEventResult::NoRedraw,
}
} else {
@@ -133,15 +358,12 @@ impl Component for TextInput {
// We are assuming this is within bounds...
let x = event.column;
- let widget_x = self.bounds.x;
- let new_cursor_index = usize::from(x.saturating_sub(widget_x));
-
- if new_cursor_index >= self.text.len() {
- self.cursor_index = self.text.len() - 1;
+ let widget_x = self.bounds.x + 2;
+ if x >= widget_x {
+ // TODO: do this
+ WidgetEventResult::Redraw
} else {
- self.cursor_index = new_cursor_index;
+ WidgetEventResult::NoRedraw
}
-
- WidgetEventResult::Redraw
}
}
diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs
index 3ce56886..44f6d488 100644
--- a/src/app/widgets/base/text_table.rs
+++ b/src/app/widgets/base/text_table.rs
@@ -40,6 +40,7 @@ pub trait TableColumn {
}
pub type TextTableData = Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>, Option<Style>)>>;
+pub type TextTableDataRef = [Vec<(Cow<'static, str>, Option<Cow<'static, str>>, Option<Style>)>];
/// A [`SimpleColumn`] represents some column in a [`TextTable`].
#[derive(Debug)]
@@ -199,7 +200,7 @@ where
}
pub fn get_desired_column_widths(
- columns: &[C], data: &TextTableData,
+ columns: &[C], data: &TextTableDataRef,
) -> Vec<DesiredColumnWidth> {
columns
.iter()
@@ -237,7 +238,7 @@ where
.collect::<Vec<_>>()
}
- fn get_cache(&mut self, area: Rect, data: &TextTableData) -> Vec<u16> {
+ fn get_cache(&mut self, area: Rect, data: &TextTableDataRef) -> Vec<u16> {
fn calculate_column_widths(
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
) -> Vec<u16> {
@@ -296,9 +297,18 @@ where
column_widths
}
- // If empty, do NOT save the cache! We have to get it again when it updates.
+ // If empty, get the cached values if they exist; if they don't, do not cache!
if data.is_empty() {
- vec![0; self.columns.len()]
+ match &self.cached_column_widths {
+ CachedColumnWidths::Uncached => {
+ let desired_widths = TextTable::get_desired_column_widths(&self.columns, data);
+ calculate_column_widths(self.left_to_right, desired_widths, area.width)
+ }
+ CachedColumnWidths::Cached {
+ cached_area: _,
+ cached_data,
+ } => cached_data.clone(),
+ }
} else {
let was_cached: bool;
let column_widths = match &mut self.cached_column_widths {
@@ -351,12 +361,9 @@ where
}
/// Draws a [`Table`] on screen corresponding to the [`TextTable`].
- ///
- /// Note if the number of columns don't match in the [`TextTable`] and data,
- /// it will only create as many columns as it can grab data from both sources from.
pub fn draw_tui_table<B: Backend>(
- &mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>,
- block_area: Rect, show_selected_entry: bool,
+ &mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef,
+ block: Block<'_>, block_area: Rect, show_selected_entry: bool,
) {
use tui::widgets::Row;
diff --git a/src/app/widgets/base/time_graph.rs b/src/app/widgets/base/time_graph.rs
index 1bfd4a8b..3c915361 100644
--- a/src/app/widgets/base/time_graph.rs
+++ b/src/app/widgets/base/time_graph.rs
@@ -257,7 +257,7 @@ impl TimeGraph {
.style(painter.colours.graph_style)
.labels(
y_bound_labels
- .into_iter()
+ .iter()
.map(|label| Span::styled(label.clone(), painter.colours.graph_style))
.collect(),
);