From 78d1297452763ab513f41c0435be502a0794b9b8 Mon Sep 17 00:00:00 2001 From: Takayuki Maeda <41065217+TaKO8Ki@users.noreply.github.com> Date: Fri, 17 Sep 2021 15:16:31 +0900 Subject: [PATCH] Refactor filter components (#114) * prevent cursor from sticking out of paragraph * define StatefulDrawableComponent * use database filter component * fix event order --- src/app.rs | 12 +-- src/components/connections.rs | 4 +- src/components/database_filter.rs | 132 ++++++++++++++++++++++++++++++ src/components/databases.rs | 122 ++++++--------------------- src/components/debug.rs | 2 +- src/components/error.rs | 2 +- src/components/help.rs | 2 +- src/components/mod.rs | 6 ++ src/components/record_table.rs | 4 +- src/components/tab.rs | 2 +- src/components/table.rs | 4 +- src/components/table_filter.rs | 9 +- src/components/table_status.rs | 2 +- src/components/table_value.rs | 2 +- 14 files changed, 187 insertions(+), 118 deletions(-) create mode 100644 src/components/database_filter.rs diff --git a/src/app.rs b/src/app.rs index cfb5031..d08deb7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,7 @@ use crate::clipboard::copy_to_clipboard; -use crate::components::{CommandInfo, Component as _, DrawableComponent as _, EventState}; +use crate::components::{ + CommandInfo, Component as _, DrawableComponent as _, EventState, StatefulDrawableComponent, +}; use crate::database::{MySqlPool, Pool, PostgresPool, SqlitePool, RECORDS_LIMIT_PER_PAGE}; use crate::event::Key; use crate::{ @@ -330,14 +332,12 @@ impl App { } } Focus::DabataseList => { - let state = self.databases.event(key)?; - - if key == self.config.key_config.enter && self.databases.tree_focused() { - self.update_table().await?; + if self.databases.event(key)?.is_consumed() { return Ok(EventState::Consumed); } - if state.is_consumed() { + if key == self.config.key_config.enter && self.databases.tree_focused() { + self.update_table().await?; return Ok(EventState::Consumed); } } diff --git a/src/components/connections.rs b/src/components/connections.rs index f05665d..5f5c57e 100644 --- a/src/components/connections.rs +++ b/src/components/connections.rs @@ -1,4 +1,4 @@ -use super::{Component, DrawableComponent, EventState}; +use super::{Component, EventState, StatefulDrawableComponent}; use crate::components::command::CommandInfo; use crate::config::{Connection, KeyConfig}; use crate::event::Key; @@ -81,7 +81,7 @@ impl ConnectionsComponent { } } -impl DrawableComponent for ConnectionsComponent { +impl StatefulDrawableComponent for ConnectionsComponent { fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { let width = 80; let height = 20; diff --git a/src/components/database_filter.rs b/src/components/database_filter.rs new file mode 100644 index 0000000..6aeea78 --- /dev/null +++ b/src/components/database_filter.rs @@ -0,0 +1,132 @@ +use super::{compute_character_width, Component, DrawableComponent, EventState}; +use crate::components::command::CommandInfo; +use crate::event::Key; +use anyhow::Result; +use database_tree::Table; +use tui::{ + backend::Backend, + layout::Rect, + style::{Color, Style}, + text::Spans, + widgets::{Block, Borders, Paragraph}, + Frame, +}; +use unicode_width::UnicodeWidthStr; + +pub struct DatabaseFilterComponent { + pub table: Option, + input: Vec, + input_idx: usize, + input_cursor_position: u16, +} + +impl DatabaseFilterComponent { + pub fn new() -> Self { + Self { + table: None, + input: Vec::new(), + input_idx: 0, + input_cursor_position: 0, + } + } + + pub fn input_str(&self) -> String { + self.input.iter().collect() + } + + pub fn reset(&mut self) { + self.table = None; + self.input = Vec::new(); + self.input_idx = 0; + self.input_cursor_position = 0; + } +} + +impl DrawableComponent for DatabaseFilterComponent { + fn draw(&self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + let query = Paragraph::new(Spans::from(format!( + "{:w$}", + if self.input.is_empty() && !focused { + "Filter tables".to_string() + } else { + self.input_str() + }, + w = area.width as usize + ))) + .style(if focused { + Style::default() + } else { + Style::default().fg(Color::DarkGray) + }) + .block(Block::default().borders(Borders::BOTTOM)); + f.render_widget(query, area); + + if focused { + f.set_cursor( + (area.x + self.input_cursor_position).min(area.right().saturating_sub(1)), + area.y, + ) + } + Ok(()) + } +} + +impl Component for DatabaseFilterComponent { + fn commands(&self, _out: &mut Vec) {} + + fn event(&mut self, key: Key) -> Result { + let input_str: String = self.input.iter().collect(); + + match key { + Key::Char(c) => { + self.input.insert(self.input_idx, c); + self.input_idx += 1; + self.input_cursor_position += compute_character_width(c); + + return Ok(EventState::Consumed); + } + Key::Delete | Key::Backspace => { + if input_str.width() > 0 && !self.input.is_empty() && self.input_idx > 0 { + let last_c = self.input.remove(self.input_idx - 1); + self.input_idx -= 1; + self.input_cursor_position -= compute_character_width(last_c); + } + return Ok(EventState::Consumed); + } + Key::Left => { + if !self.input.is_empty() && self.input_idx > 0 { + self.input_idx -= 1; + self.input_cursor_position = self + .input_cursor_position + .saturating_sub(compute_character_width(self.input[self.input_idx])); + } + return Ok(EventState::Consumed); + } + Key::Ctrl('a') => { + if !self.input.is_empty() && self.input_idx > 0 { + self.input_idx = 0; + self.input_cursor_position = 0 + } + return Ok(EventState::Consumed); + } + Key::Right => { + if self.input_idx < self.input.len() { + let next_c = self.input[self.input_idx]; + self.input_idx += 1; + self.input_cursor_position += compute_character_width(next_c); + } + return Ok(EventState::Consumed); + } + Key::Ctrl('e') => { + if self.input_idx < self.input.len() { + self.input_idx = self.input.len(); + self.input_cursor_position = self.input_str().width() as u16; + } + return Ok(EventState::Consumed); + } + _ => (), + } + + Ok(EventState::NotConsumed) + } +} diff --git a/src/components/databases.rs b/src/components/databases.rs index 84b4d57..472f607 100644 --- a/src/components/databases.rs +++ b/src/components/databases.rs @@ -1,5 +1,5 @@ use super::{ - compute_character_width, utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, + utils::scroll_vertical::VerticalScroll, Component, DatabaseFilterComponent, DrawableComponent, EventState, }; use crate::components::command::{self, CommandInfo}; @@ -16,10 +16,9 @@ use tui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, text::{Span, Spans}, - widgets::{Block, Borders, Paragraph}, + widgets::{Block, Borders}, Frame, }; -use unicode_width::UnicodeWidthStr; // ▸ const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}"; @@ -35,11 +34,9 @@ pub enum Focus { pub struct DatabasesComponent { tree: DatabaseTree, + filter: DatabaseFilterComponent, filterd_tree: Option, scroll: VerticalScroll, - input: Vec, - input_idx: usize, - input_cursor_position: u16, focus: Focus, key_config: KeyConfig, } @@ -48,26 +45,18 @@ impl DatabasesComponent { pub fn new(key_config: KeyConfig) -> Self { Self { tree: DatabaseTree::default(), + filter: DatabaseFilterComponent::new(), filterd_tree: None, scroll: VerticalScroll::new(false, false), - input: Vec::new(), - input_idx: 0, - input_cursor_position: 0, focus: Focus::Tree, key_config, } } - fn input_str(&self) -> String { - self.input.iter().collect() - } - pub fn update(&mut self, list: &[Database]) -> Result<()> { self.tree = DatabaseTree::new(list, &BTreeSet::new())?; self.filterd_tree = None; - self.input = Vec::new(); - self.input_idx = 0; - self.input_cursor_position = 0; + self.filter.reset(); Ok(()) } @@ -147,7 +136,7 @@ impl DatabasesComponent { )) } - fn draw_tree(&self, f: &mut Frame, area: Rect, focused: bool) { + fn draw_tree(&self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { f.render_widget( Block::default() .title("Databases") @@ -167,24 +156,8 @@ impl DatabasesComponent { .constraints([Constraint::Length(2), Constraint::Min(1)].as_ref()) .split(area); - let filter = Paragraph::new(Span::styled( - format!( - "{}{:w$}", - if self.input.is_empty() && matches!(self.focus, Focus::Tree) { - "Filter tables".to_string() - } else { - self.input_str() - }, - w = area.width as usize - ), - if let Focus::Filter = self.focus { - Style::default() - } else { - Style::default().fg(Color::DarkGray) - }, - )) - .block(Block::default().borders(Borders::BOTTOM)); - f.render_widget(filter, chunks[0]); + self.filter + .draw(f, chunks[0], matches!(self.focus, Focus::Filter))?; let tree_height = chunks[1].height as usize; let tree = if let Some(tree) = self.filterd_tree.as_ref() { @@ -209,10 +182,10 @@ impl DatabasesComponent { item.clone(), selected, area.width, - if self.input.is_empty() { + if self.filter.input_str().is_empty() { None } else { - Some(self.input_str()) + Some(self.filter.input_str()) }, ) }); @@ -220,20 +193,18 @@ impl DatabasesComponent { draw_list_block(f, chunks[1], Block::default().borders(Borders::NONE), items); self.scroll.draw(f, chunks[1]); - if let Focus::Filter = self.focus { - f.set_cursor(area.x + self.input_cursor_position + 1, area.y + 1) - } + Ok(()) } } impl DrawableComponent for DatabasesComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + fn draw(&self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(100)].as_ref()) .split(area); - self.draw_tree(f, chunks[0], focused); + self.draw_tree(f, chunks[0], focused)?; Ok(()) } } @@ -244,70 +215,29 @@ impl Component for DatabasesComponent { } fn event(&mut self, key: Key) -> Result { - let input_str: String = self.input.iter().collect(); if key == self.key_config.filter && self.focus == Focus::Tree { self.focus = Focus::Filter; return Ok(EventState::Consumed); } + + if matches!(self.focus, Focus::Filter) { + self.filterd_tree = if self.filter.input_str().is_empty() { + None + } else { + Some(self.tree.filter(self.filter.input_str())) + }; + } + match key { - Key::Char(c) if self.focus == Focus::Filter => { - self.input.insert(self.input_idx, c); - self.input_idx += 1; - self.input_cursor_position += compute_character_width(c); - self.filterd_tree = Some(self.tree.filter(self.input_str())); + Key::Enter if matches!(self.focus, Focus::Filter) => { + self.focus = Focus::Tree; return Ok(EventState::Consumed); } - Key::Delete | Key::Backspace if matches!(self.focus, Focus::Filter) => { - if input_str.width() > 0 { - if !self.input.is_empty() && self.input_idx > 0 { - let last_c = self.input.remove(self.input_idx - 1); - self.input_idx -= 1; - self.input_cursor_position -= compute_character_width(last_c); - } - - self.filterd_tree = if self.input.is_empty() { - None - } else { - Some(self.tree.filter(self.input_str())) - }; + key if matches!(self.focus, Focus::Filter) => { + if self.filter.event(key)?.is_consumed() { return Ok(EventState::Consumed); } } - Key::Left if matches!(self.focus, Focus::Filter) => { - if !self.input.is_empty() && self.input_idx > 0 { - self.input_idx -= 1; - self.input_cursor_position = self - .input_cursor_position - .saturating_sub(compute_character_width(self.input[self.input_idx])); - } - return Ok(EventState::Consumed); - } - Key::Ctrl('a') => { - if !self.input.is_empty() && self.input_idx > 0 { - self.input_idx = 0; - self.input_cursor_position = 0 - } - return Ok(EventState::Consumed); - } - Key::Right if matches!(self.focus, Focus::Filter) => { - if self.input_idx < self.input.len() { - let next_c = self.input[self.input_idx]; - self.input_idx += 1; - self.input_cursor_position += compute_character_width(next_c); - } - return Ok(EventState::Consumed); - } - Key::Ctrl('e') => { - if self.input_idx < self.input.len() { - self.input_idx = self.input.len(); - self.input_cursor_position = self.input_str().width() as u16; - } - return Ok(EventState::Consumed); - } - Key::Enter if matches!(self.focus, Focus::Filter) => { - self.focus = Focus::Tree; - return Ok(EventState::Consumed); - } key => { if tree_nav( if let Some(tree) = self.filterd_tree.as_mut() { diff --git a/src/components/debug.rs b/src/components/debug.rs index b5f9628..5098b75 100644 --- a/src/components/debug.rs +++ b/src/components/debug.rs @@ -28,7 +28,7 @@ impl DebugComponent { } impl DrawableComponent for DebugComponent { - fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { + fn draw(&self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { if true { let width = 65; let height = 10; diff --git a/src/components/error.rs b/src/components/error.rs index 9836d00..a9d123e 100644 --- a/src/components/error.rs +++ b/src/components/error.rs @@ -35,7 +35,7 @@ impl ErrorComponent { } impl DrawableComponent for ErrorComponent { - fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { + fn draw(&self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { if self.visible { let width = 65; let height = 10; diff --git a/src/components/help.rs b/src/components/help.rs index f1977ea..90a54fb 100644 --- a/src/components/help.rs +++ b/src/components/help.rs @@ -23,7 +23,7 @@ pub struct HelpComponent { } impl DrawableComponent for HelpComponent { - fn draw(&mut self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { + fn draw(&self, f: &mut Frame, _area: Rect, _focused: bool) -> Result<()> { if self.visible { const SIZE: (u16, u16) = (65, 24); let scroll_threshold = SIZE.1 / 3; diff --git a/src/components/mod.rs b/src/components/mod.rs index 79440ce..44c6e6d 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,6 +1,7 @@ pub mod command; pub mod completion; pub mod connections; +pub mod database_filter; pub mod databases; pub mod error; pub mod help; @@ -18,6 +19,7 @@ pub mod debug; pub use command::{CommandInfo, CommandText}; pub use completion::CompletionComponent; pub use connections::ConnectionsComponent; +pub use database_filter::DatabaseFilterComponent; pub use databases::DatabasesComponent; pub use error::ErrorComponent; pub use help::HelpComponent; @@ -60,6 +62,10 @@ impl From for EventState { } pub trait DrawableComponent { + fn draw(&self, f: &mut Frame, rect: Rect, focused: bool) -> Result<()>; +} + +pub trait StatefulDrawableComponent { fn draw(&mut self, f: &mut Frame, rect: Rect, focused: bool) -> Result<()>; } diff --git a/src/components/record_table.rs b/src/components/record_table.rs index dea2800..d4857aa 100644 --- a/src/components/record_table.rs +++ b/src/components/record_table.rs @@ -1,4 +1,4 @@ -use super::{Component, DrawableComponent, EventState}; +use super::{Component, EventState, StatefulDrawableComponent}; use crate::components::command::CommandInfo; use crate::components::{TableComponent, TableFilterComponent}; use crate::config::KeyConfig; @@ -54,7 +54,7 @@ impl RecordTableComponent { } } -impl DrawableComponent for RecordTableComponent { +impl StatefulDrawableComponent for RecordTableComponent { fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { let layout = Layout::default() .direction(Direction::Vertical) diff --git a/src/components/tab.rs b/src/components/tab.rs index f825499..dc276f1 100644 --- a/src/components/tab.rs +++ b/src/components/tab.rs @@ -57,7 +57,7 @@ impl TabComponent { } impl DrawableComponent for TabComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { + fn draw(&self, f: &mut Frame, area: Rect, _focused: bool) -> Result<()> { let titles = self.names().iter().cloned().map(Spans::from).collect(); let tabs = Tabs::new(titles) .block(Block::default().borders(Borders::ALL)) diff --git a/src/components/table.rs b/src/components/table.rs index 1da7a6d..0c5da62 100644 --- a/src/components/table.rs +++ b/src/components/table.rs @@ -1,6 +1,6 @@ use super::{ utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState, - TableStatusComponent, TableValueComponent, + StatefulDrawableComponent, TableStatusComponent, TableValueComponent, }; use crate::components::command::{self, CommandInfo}; use crate::config::KeyConfig; @@ -400,7 +400,7 @@ impl TableComponent { } } -impl DrawableComponent for TableComponent { +impl StatefulDrawableComponent for TableComponent { fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { let chunks = Layout::default() .vertical_margin(1) diff --git a/src/components/table_filter.rs b/src/components/table_filter.rs index 1361cea..45352df 100644 --- a/src/components/table_filter.rs +++ b/src/components/table_filter.rs @@ -1,6 +1,6 @@ use super::{ - compute_character_width, CompletionComponent, Component, DrawableComponent, EventState, - MovableComponent, + compute_character_width, CompletionComponent, Component, EventState, MovableComponent, + StatefulDrawableComponent, }; use crate::components::command::CommandInfo; use crate::config::KeyConfig; @@ -129,7 +129,7 @@ impl TableFilterComponent { } } -impl DrawableComponent for TableFilterComponent { +impl StatefulDrawableComponent for TableFilterComponent { fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { let query = Paragraph::new(Spans::from(vec![ Span::styled( @@ -181,7 +181,8 @@ impl DrawableComponent for TableFilterComponent { .map_or(String::new(), |table| table.name.to_string()) .width() + 1) as u16) - .saturating_add(self.input_cursor_position), + .saturating_add(self.input_cursor_position) + .min(area.right().saturating_sub(2)), area.y + 1, ) } diff --git a/src/components/table_status.rs b/src/components/table_status.rs index 38ce428..1b49342 100644 --- a/src/components/table_status.rs +++ b/src/components/table_status.rs @@ -43,7 +43,7 @@ impl TableStatusComponent { } impl DrawableComponent for TableStatusComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + fn draw(&self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { let status = Paragraph::new(Spans::from(vec![ Span::from(format!( "rows: {}, ", diff --git a/src/components/table_value.rs b/src/components/table_value.rs index 649bff4..82c3cc2 100644 --- a/src/components/table_value.rs +++ b/src/components/table_value.rs @@ -21,7 +21,7 @@ impl TableValueComponent { } impl DrawableComponent for TableValueComponent { - fn draw(&mut self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { + fn draw(&self, f: &mut Frame, area: Rect, focused: bool) -> Result<()> { let paragraph = Paragraph::new(self.value.clone()) .block(Block::default().borders(Borders::BOTTOM)) .style(if focused {