Select multiple cells (#30)

* select multiple cells

* define `expand_selected_area_x`, `expand_selected_area_y`

* make some table component fields private

* fix keymap

* fix a cell selection bug in page 2 and after

* add tests for `is_selected_cell` and `selected_cells`

* select cells while scrolling right

* calculate the number of cells considering cell spacing

* add tests for expand_selected_area

* add comments

* implement `reset`

* fix clippy warnings
pull/31/head
Takayuki Maeda 3 years ago committed by GitHub
parent da06d0b557
commit 7e8291be22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -31,9 +31,9 @@ $ cargo install --version 0.1.0-alpha.0 gobang
| Key | Description |
| ---- | ---- |
| <kbd>h</kbd> | move right |
| <kbd>h</kbd> | move left |
| <kbd>j</kbd> | move down |
| <kbd>k</kbd> | move up |
| <kbd>l</kbd> | move left |
| <kbd>l</kbd> | move right |
| <kbd>Ctrl</kbd> + <kbd>d</kbd> | scroll down multiple lines |
| <kbd>Ctrl</kbd> + <kbd>u</kbd> | scroll up multiple lines |

@ -191,7 +191,6 @@ impl App {
.get_columns(&database, &table.name)
.await?;
self.structure_table = TableComponent::new(records, headers);
self.table_status
.update(self.record_table.len() as u64, table);
}
@ -206,7 +205,7 @@ impl App {
};
if let Key::Char('y') = key {
if let Some(text) = self.record_table.table.selected_cell() {
if let Some(text) = self.record_table.table.selected_cells() {
self.clipboard.store(text)
}
}
@ -276,7 +275,7 @@ impl App {
};
if let Key::Char('y') = key {
if let Some(text) = self.structure_table.selected_cell() {
if let Some(text) = self.structure_table.selected_cells() {
self.clipboard.store(text)
}
};
@ -302,20 +301,18 @@ impl App {
return Ok(EventState::Consumed);
}
}
Focus::DabataseList => match key {
Key::Right if self.databases.tree_focused() => {
Focus::DabataseList => {
if matches!(key, Key::Right) && self.databases.tree_focused() {
self.focus = Focus::Table;
return Ok(EventState::Consumed);
}
_ => (),
},
Focus::Table => match key {
Key::Left => {
}
Focus::Table => {
if let Key::Left = key {
self.focus = Focus::DabataseList;
return Ok(EventState::Consumed);
}
_ => (),
},
}
}
Ok(EventState::NotConsumed)
}

@ -91,8 +91,8 @@ impl Component for RecordTableComponent {
self.focus = Focus::Filter;
return Ok(EventState::Consumed);
}
key if matches!(self.focus, Focus::Filter) => return Ok(self.filter.event(key)?),
key if matches!(self.focus, Focus::Table) => return Ok(self.table.event(key)?),
key if matches!(self.focus, Focus::Filter) => return self.filter.event(key),
key if matches!(self.focus, Focus::Table) => return self.table.event(key),
_ => (),
}
Ok(EventState::NotConsumed)

@ -18,22 +18,24 @@ pub struct TableComponent {
pub state: TableState,
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub column_index: usize,
pub column_page: usize,
pub column_page_start: std::cell::Cell<usize>,
pub scroll: VerticalScroll,
pub select_entire_row: bool,
pub eod: bool,
select_state: TableState,
selected_left_column_index: usize,
selected_right_cell: Option<(usize, usize)>,
column_page_start: std::cell::Cell<usize>,
scroll: VerticalScroll,
select_entire_row: bool,
}
impl Default for TableComponent {
fn default() -> Self {
Self {
state: TableState::default(),
select_state: TableState::default(),
headers: vec![],
rows: vec![],
column_page: 0,
column_index: 0,
selected_left_column_index: 0,
selected_right_cell: None,
column_page_start: std::cell::Cell::new(0),
scroll: VerticalScroll::new(),
select_entire_row: false,
@ -50,13 +52,19 @@ impl TableComponent {
state.select(Some(0))
}
Self {
rows,
headers,
state,
headers,
rows,
..Self::default()
}
}
pub fn reset(&mut self) {
self.select_state.select(None);
self.selected_right_cell = None;
self.select_entire_row = false;
}
pub fn end(&mut self) {
self.eod = true;
}
@ -72,10 +80,24 @@ impl TableComponent {
}
None => None,
};
self.select_entire_row = false;
self.reset();
self.state.select(i);
}
pub fn next_select(&mut self, lines: usize) {
let i = match self.select_state.selected() {
Some(i) => {
if i + lines >= self.rows.len() {
Some(self.rows.len() - 1)
} else {
Some(i + lines)
}
}
None => None,
};
self.select_state.select(i);
}
pub fn previous(&mut self, lines: usize) {
let i = match self.state.selected() {
Some(i) => {
@ -87,15 +109,29 @@ impl TableComponent {
}
None => None,
};
self.select_entire_row = false;
self.reset();
self.state.select(i);
}
pub fn previout_select(&mut self, lines: usize) {
let i = match self.select_state.selected() {
Some(i) => {
if i <= lines {
Some(0)
} else {
Some(i - lines)
}
}
None => None,
};
self.select_state.select(i);
}
pub fn scroll_top(&mut self) {
if self.rows.is_empty() {
return;
}
self.state.select(None);
self.reset();
self.state.select(Some(0));
}
@ -103,6 +139,7 @@ impl TableComponent {
if self.rows.is_empty() {
return;
}
self.reset();
self.state.select(Some(self.rows.len() - 1));
}
@ -110,37 +147,124 @@ impl TableComponent {
if self.rows.is_empty() {
return;
}
if self.column_index >= self.headers().len().saturating_sub(1) {
if self.selected_left_column_index >= self.headers.len().saturating_sub(1) {
return;
}
self.select_entire_row = false;
self.column_index += 1;
self.reset();
self.selected_left_column_index += 1;
}
pub fn previous_column(&mut self) {
if self.rows.is_empty() {
return;
}
if self.column_index == 0 {
if self.selected_left_column_index == 0 {
return;
}
self.select_entire_row = false;
self.column_index -= 1;
self.reset();
self.selected_left_column_index -= 1;
}
pub fn expand_selected_area_x(&mut self, positive: bool) {
if self.selected_right_cell.is_none() {
self.selected_right_cell = Some((
self.selected_left_column_index,
self.state.selected().unwrap_or(0),
));
}
if let Some((x, y)) = self.selected_right_cell {
self.selected_right_cell = Some((
if positive {
(x + 1).min(self.headers.len().saturating_sub(1))
} else {
x.saturating_sub(1)
},
y,
));
}
}
pub fn expand_selected_area_y(&mut self, positive: bool) {
if self.select_state.selected().is_none() {
self.select_state = self.state.clone();
}
if positive {
self.next_select(1);
} else {
self.previout_select(1);
}
if self.selected_right_cell.is_none() {
self.selected_right_cell = Some((
self.selected_left_column_index,
self.state.selected().unwrap_or(0),
));
}
if let Some((x, y)) = self.selected_right_cell {
self.selected_right_cell = Some((
x,
if positive {
(y + 1).min(self.rows.len().saturating_sub(1))
} else {
y.saturating_sub(1)
},
));
}
}
pub fn is_row_number_clumn(&self, row_index: usize, column_index: usize) -> bool {
matches!(self.state.selected(), Some(selected_row_index) if row_index == selected_row_index && 0 == column_index)
}
pub fn selected_cell(&self) -> Option<String> {
pub fn selected_cells(&self) -> Option<String> {
if let Some((x, y)) = self.selected_right_cell {
let selected_row_index = self.state.selected()?;
return Some(
self.rows[y.min(selected_row_index)..y.max(selected_row_index) + 1]
.iter()
.map(|row| {
row[x.min(self.selected_left_column_index)
..x.max(self.selected_left_column_index) + 1]
.join(",")
})
.collect::<Vec<String>>()
.join("\n"),
);
}
self.rows
.get(self.state.selected()?)?
.get(self.column_index)
.get(self.selected_left_column_index)
.map(|cell| cell.to_string())
}
pub fn headers(&self) -> Vec<String> {
self.headers.clone()
pub fn selected_column_index(&self) -> usize {
if let Some((x, _)) = self.selected_right_cell {
return x;
}
self.selected_left_column_index
}
pub fn is_selected_cell(
&self,
row_index: usize,
column_index: usize,
selected_column_index: usize,
) -> bool {
if let Some((x, y)) = self.selected_right_cell {
let x_in_page = x
.saturating_add(1)
.saturating_sub(self.column_page_start.get());
return matches!(
self.state.selected(),
Some(selected_row_index)
if (x_in_page.min(selected_column_index).max(1)..x_in_page.max(selected_column_index) + 1)
.contains(&column_index)
&& (y.min(selected_row_index)..y.max(selected_row_index) + 1)
.contains(&row_index)
);
}
matches!(self.state.selected(), Some(selected_row_index) if row_index == selected_row_index && column_index == selected_column_index)
}
pub fn headers_with_number(&self, left: usize, right: usize) -> Vec<String> {
@ -149,10 +273,6 @@ impl TableComponent {
headers
}
pub fn rows(&self) -> Vec<Vec<String>> {
self.rows.clone()
}
pub fn rows_with_number(&self, left: usize, right: usize) -> Vec<Vec<String>> {
let rows = self
.rows
@ -174,17 +294,17 @@ impl TableComponent {
if self.rows.is_empty() {
return (0, Vec::new(), Vec::new(), Vec::new());
}
if self.column_index < self.column_page_start.get() {
self.column_page_start.set(self.column_index);
if self.selected_column_index() < self.column_page_start.get() {
self.column_page_start.set(self.selected_column_index());
}
let right_column_index = self.column_index.clone();
let mut column_index = self.column_index;
let right_column_index = self.selected_column_index();
let mut column_index = self.selected_column_index();
let number_clomn_width = (self.rows.len() + 1).to_string().width() as u16;
let mut widths = vec![];
loop {
let length = self
.rows()
.rows
.iter()
.map(|row| {
row.get(column_index)
@ -197,16 +317,14 @@ impl TableComponent {
.map_or(3, |v| {
*v.max(
&self
.headers()
.headers
.get(column_index)
.map_or(3, |header| header.to_string().width()),
)
.clamp(&3, &20) as u16
.clamp(&3, &20)
});
if widths.iter().map(|(_, width)| width).sum::<u16>() + length
> area_width
.saturating_sub(7)
.saturating_sub(number_clomn_width)
if widths.iter().map(|(_, width)| width).sum::<usize>() + length + widths.len()
> area_width.saturating_sub(number_clomn_width) as usize
{
column_index += 1;
break;
@ -221,13 +339,11 @@ impl TableComponent {
widths.reverse();
let selected_column_index = widths.len().saturating_sub(1);
let mut column_index = right_column_index + 1;
while widths.iter().map(|(_, width)| width).sum::<u16>()
<= area_width
.saturating_sub(7)
.saturating_sub(number_clomn_width)
while widths.iter().map(|(_, width)| width).sum::<usize>() + widths.len()
<= area_width.saturating_sub(number_clomn_width) as usize
{
let length = self
.rows()
.rows
.iter()
.map(|row| {
row.get(column_index)
@ -239,14 +355,14 @@ impl TableComponent {
.max()
.map_or(3, |v| {
*v.max(
self.headers()
self.headers
.iter()
.map(|header| header.to_string().width())
.collect::<Vec<usize>>()
.get(column_index)
.unwrap_or(&3),
)
.clamp(&3, &20) as u16
.clamp(&3, &20)
});
match self.headers.get(column_index) {
Some(header) => {
@ -256,21 +372,30 @@ impl TableComponent {
}
column_index += 1
}
if self.column_index != self.headers.len().saturating_sub(1) {
if self.selected_column_index() != self.headers.len().saturating_sub(1) {
widths.pop();
}
let right_column_index = column_index;
let mut constraints = widths
.iter()
.map(|(_, width)| Constraint::Length(*width))
.map(|(_, width)| Constraint::Length(*width as u16))
.collect::<Vec<Constraint>>();
if self.column_index != self.headers.len().saturating_sub(1) {
if self.selected_column_index() != self.headers.len().saturating_sub(1) {
constraints.push(Constraint::Min(10));
}
constraints.insert(0, Constraint::Length(number_clomn_width));
self.column_page_start.set(left_column_index);
(
selected_column_index + 1,
self.selected_right_cell
.map_or(selected_column_index + 1, |(x, _)| {
if x > self.selected_left_column_index {
(selected_column_index + 1)
.saturating_sub(x.saturating_sub(self.selected_left_column_index))
} else {
(selected_column_index + 1)
.saturating_add(self.selected_left_column_index.saturating_sub(x))
}
}),
self.headers_with_number(left_column_index, right_column_index),
self.rows_with_number(left_column_index, right_column_index),
constraints,
@ -298,7 +423,7 @@ impl DrawableComponent for TableComponent {
},
);
TableValueComponent::new(self.selected_cell().unwrap_or_default())
TableValueComponent::new(self.selected_cells().unwrap_or_default())
.draw(f, layout[0], focused)?;
let block = Block::default().borders(Borders::ALL).title("Records");
@ -320,13 +445,15 @@ impl DrawableComponent for TableComponent {
.unwrap_or(0)
+ 1;
let cells = item.iter().enumerate().map(|(column_index, c)| {
Cell::from(c.to_string()).style(if matches!(self.state.selected(), Some(selected_row_index) if row_index == selected_row_index && selected_column_index == column_index) {
Style::default().bg(Color::Blue)
} else if self.is_row_number_clumn(row_index, column_index) {
Style::default().add_modifier(Modifier::BOLD)
} else {
Style::default()
})
Cell::from(c.to_string()).style(
if self.is_selected_cell(row_index, column_index, selected_column_index) {
Style::default().bg(Color::Blue)
} else if self.is_row_number_clumn(row_index, column_index) {
Style::default().add_modifier(Modifier::BOLD)
} else {
Style::default()
},
)
});
Row::new(cells).height(height as u16).bottom_margin(1)
});
@ -345,7 +472,15 @@ impl DrawableComponent for TableComponent {
Style::default().fg(Color::DarkGray)
})
.widths(&constraints);
f.render_stateful_widget(table, layout[1], &mut self.state);
f.render_stateful_widget(
table,
layout[1],
if self.select_state.selected().is_some() {
&mut self.select_state
} else {
&mut self.state
},
);
self.scroll.draw(f, layout[1]);
Ok(())
@ -391,6 +526,22 @@ impl Component for TableComponent {
self.next_column();
return Ok(EventState::Consumed);
}
Key::Char('H') => {
self.expand_selected_area_x(false);
return Ok(EventState::Consumed);
}
Key::Char('K') => {
self.expand_selected_area_y(false);
return Ok(EventState::Consumed);
}
Key::Char('J') => {
self.expand_selected_area_y(true);
return Ok(EventState::Consumed);
}
Key::Char('L') => {
self.expand_selected_area_x(true);
return Ok(EventState::Consumed);
}
_ => (),
}
Ok(EventState::NotConsumed)
@ -422,6 +573,184 @@ mod test {
)
}
#[test]
fn test_expand_selected_area_x_left() {
// before
// 1 2 3
// 1 a b c
// 2 d |e| f
// after
// 1 2 3
// 1 a b c
// 2 |d e| f
let mut component = TableComponent::default();
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
component.state.select(Some(1));
component.selected_left_column_index = 1;
component.expand_selected_area_x(false);
assert_eq!(component.selected_right_cell, Some((0, 1)));
assert_eq!(component.selected_cells(), Some("d,e".to_string()));
}
#[test]
fn test_expand_selected_area_x_right() {
// before
// 1 2 3
// 1 a b c
// 2 d |e| f
// after
// 1 2 3
// 1 a b c
// 2 d |e f|
let mut component = TableComponent::default();
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
component.state.select(Some(1));
component.selected_left_column_index = 1;
component.expand_selected_area_x(true);
assert_eq!(component.selected_right_cell, Some((2, 1)));
assert_eq!(component.selected_cells(), Some("e,f".to_string()));
}
#[test]
fn test_expand_selected_area_y_up() {
// before
// 1 2 3
// 1 a b c
// 2 d |e| f
// after
// 1 2 3
// 1 a |b| c
// 2 d |e| f
let mut component = TableComponent::default();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
component.state.select(Some(1));
component.selected_left_column_index = 1;
component.expand_selected_area_y(false);
assert_eq!(component.selected_right_cell, Some((1, 0)));
assert_eq!(component.selected_cells(), Some("b\ne".to_string()));
}
#[test]
fn test_expand_selected_area_y_down() {
// before
// 1 2 3
// 1 a |b| c
// 2 d e f
// after
// 1 2 3
// 1 a |b| c
// 2 d |e| f
let mut component = TableComponent::default();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
component.state.select(Some(0));
component.selected_left_column_index = 1;
component.expand_selected_area_y(true);
assert_eq!(component.selected_right_cell, Some((1, 1)));
assert_eq!(component.selected_cells(), Some("b\ne".to_string()));
}
#[test]
fn test_selected_cell_when_one_cell_selected() {
// 1 2 3
// 1 |a| b c
// 2 d e f
let mut component = TableComponent::default();
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
component.state.select(Some(0));
assert_eq!(component.selected_cells(), Some("a".to_string()));
}
#[test]
fn test_selected_cell_when_multiple_cells_selected() {
// 1 2 3
// 1 |a b| c
// 2 |d e| f
let mut component = TableComponent::default();
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
component.state.select(Some(0));
component.selected_right_cell = Some((1, 1));
assert_eq!(component.selected_cells(), Some("a,b\nd,e".to_string()));
}
#[test]
fn test_is_selected_cell_when_one_cell_selected() {
// 1 2 3
// 1 |a| b c
// 2 d e f
let mut component = TableComponent::default();
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
component.state.select(Some(0));
// a
assert!(component.is_selected_cell(0, 1, 1));
// d
assert!(!component.is_selected_cell(1, 1, 1));
// e
assert!(!component.is_selected_cell(1, 2, 1));
}
#[test]
fn test_is_selected_cell_when_multiple_cells_selected() {
// 1 2 3
// 1 |a b| c
// 2 |d e| f
let mut component = TableComponent::default();
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![
vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
component.state.select(Some(0));
component.selected_right_cell = Some((1, 1));
// a
assert!(component.is_selected_cell(0, 1, 1));
// b
assert!(component.is_selected_cell(0, 2, 1));
// d
assert!(component.is_selected_cell(1, 1, 1));
// e
assert!(component.is_selected_cell(1, 2, 1));
// f
assert!(!component.is_selected_cell(1, 3, 1));
}
#[test]
fn test_calculate_widths() {
let mut component = TableComponent::default();
@ -433,7 +762,7 @@ mod test {
.collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
let (selected_column_index, headers, rows, constraints) = component.calculate_widths(17);
let (selected_column_index, headers, rows, constraints) = component.calculate_widths(10);
assert_eq!(selected_column_index, 1);
assert_eq!(headers, vec!["", "1", "2"]);
assert_eq!(rows, vec![vec!["1", "aaaaa", "bbbbb"], vec!["2", "d", "e"]]);
@ -446,7 +775,7 @@ mod test {
]
);
let (selected_column_index, headers, rows, constraints) = component.calculate_widths(27);
let (selected_column_index, headers, rows, constraints) = component.calculate_widths(20);
assert_eq!(selected_column_index, 1);
assert_eq!(headers, vec!["", "1", "2", "3"]);
assert_eq!(

@ -89,12 +89,10 @@ impl Component for TableFilterComponent {
return Ok(EventState::Consumed);
}
Key::Delete | Key::Backspace => {
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);
}
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);
}

@ -13,15 +13,15 @@ pub trait Pool {
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Table>>;
async fn get_records(
&self,
database: &String,
table: &String,
database: &str,
table: &str,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn get_columns(
&self,
database: &String,
table: &String,
database: &str,
table: &str,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn close(&self);
}
@ -67,8 +67,8 @@ impl Pool for MySqlPool {
async fn get_records(
&self,
database: &String,
table: &String,
database: &str,
table: &str,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
@ -111,8 +111,8 @@ impl Pool for MySqlPool {
async fn get_columns(
&self,
database: &String,
table: &String,
database: &str,
table: &str,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = format!("SHOW FULL COLUMNS FROM `{}`.`{}`", database, table);
let mut rows = sqlx::query(query.as_str()).fetch(&self.pool);

Loading…
Cancel
Save