Refactor filter components (#114)

* prevent cursor from sticking out of paragraph

* define StatefulDrawableComponent

* use database filter component

* fix event order
pull/125/head
Takayuki Maeda 3 years ago committed by GitHub
parent 9c46512f70
commit 78d1297452
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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);
}
}

@ -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<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
let width = 80;
let height = 20;

@ -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<Table>,
input: Vec<char>,
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<B: Backend>(&self, f: &mut Frame<B>, 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<CommandInfo>) {}
fn event(&mut self, key: Key) -> Result<EventState> {
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)
}
}

@ -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<DatabaseTree>,
scroll: VerticalScroll,
input: Vec<char>,
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<B: Backend>(&self, f: &mut Frame<B>, area: Rect, focused: bool) {
fn draw_tree<B: Backend>(&self, f: &mut Frame<B>, 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<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
fn draw<B: Backend>(&self, f: &mut Frame<B>, 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<EventState> {
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() {

@ -28,7 +28,7 @@ impl DebugComponent {
}
impl DrawableComponent for DebugComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
fn draw<B: Backend>(&self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
if true {
let width = 65;
let height = 10;

@ -35,7 +35,7 @@ impl ErrorComponent {
}
impl DrawableComponent for ErrorComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
fn draw<B: Backend>(&self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
if self.visible {
let width = 65;
let height = 10;

@ -23,7 +23,7 @@ pub struct HelpComponent {
}
impl DrawableComponent for HelpComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
fn draw<B: Backend>(&self, f: &mut Frame<B>, _area: Rect, _focused: bool) -> Result<()> {
if self.visible {
const SIZE: (u16, u16) = (65, 24);
let scroll_threshold = SIZE.1 / 3;

@ -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<bool> for EventState {
}
pub trait DrawableComponent {
fn draw<B: Backend>(&self, f: &mut Frame<B>, rect: Rect, focused: bool) -> Result<()>;
}
pub trait StatefulDrawableComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, rect: Rect, focused: bool) -> Result<()>;
}

@ -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<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let layout = Layout::default()
.direction(Direction::Vertical)

@ -57,7 +57,7 @@ impl TabComponent {
}
impl DrawableComponent for TabComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, _focused: bool) -> Result<()> {
fn draw<B: Backend>(&self, f: &mut Frame<B>, 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))

@ -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<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let chunks = Layout::default()
.vertical_margin(1)

@ -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<B: Backend>(&mut self, f: &mut Frame<B>, 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,
)
}

@ -43,7 +43,7 @@ impl TableStatusComponent {
}
impl DrawableComponent for TableStatusComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
fn draw<B: Backend>(&self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let status = Paragraph::new(Spans::from(vec![
Span::from(format!(
"rows: {}, ",

@ -21,7 +21,7 @@ impl TableValueComponent {
}
impl DrawableComponent for TableValueComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
fn draw<B: Backend>(&self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
let paragraph = Paragraph::new(self.value.clone())
.block(Block::default().borders(Borders::BOTTOM))
.style(if focused {

Loading…
Cancel
Save