Make table cells responsive (#23)

* make table cells responsive

* add workspace flag

* fix compile error

* scrolling responsive cells

* horizontal scrolling without numbers

* fix limit

* add row number

* implement TEXT type

* remove unused functions

* add keymap and update gobang.gif

* add a test for `calculate_widths`

* remove a debug line
pull/16/merge
Takayuki Maeda 3 years ago committed by GitHub
parent 98ca815c68
commit da06d0b557
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -89,4 +89,4 @@ jobs:
sudo apt-get install libxcb-xkb-dev
sudo apt install libxcb-composite0-dev
- name: Run Tests
run: cargo test -- --skip=e2e --color always
run: cargo test --workspace -- --skip=e2e --color always

@ -27,4 +27,13 @@ If you already have a Rust environment set up, you can use the `cargo install` c
```
$ cargo install --version 0.1.0-alpha.0 gobang
```
## Keymap
| Key | Description |
| ---- | ---- |
| <kbd>h</kbd> | move right |
| <kbd>j</kbd> | move down |
| <kbd>k</kbd> | move up |
| <kbd>l</kbd> | move left |
| <kbd>Ctrl</kbd> + <kbd>d</kbd> | scroll down multiple lines |
| <kbd>Ctrl</kbd> + <kbd>u</kbd> | scroll up multiple lines |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

After

Width:  |  Height:  |  Size: 9.0 MiB

@ -8,10 +8,11 @@ use std::convert::From;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Row, Table, TableState},
Frame,
};
use unicode_width::UnicodeWidthStr;
pub struct TableComponent {
pub state: TableState,
@ -19,6 +20,7 @@ pub struct TableComponent {
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,
@ -31,7 +33,8 @@ impl Default for TableComponent {
headers: vec![],
rows: vec![],
column_page: 0,
column_index: 1,
column_index: 0,
column_page_start: std::cell::Cell::new(0),
scroll: VerticalScroll::new(),
select_entire_row: false,
eod: false,
@ -107,11 +110,7 @@ impl TableComponent {
if self.rows.is_empty() {
return;
}
if self.column_index == self.headers.len() {
return;
}
if self.column_index == 9 {
self.next_column_page();
if self.column_index >= self.headers().len().saturating_sub(1) {
return;
}
self.select_entire_row = false;
@ -122,58 +121,161 @@ impl TableComponent {
if self.rows.is_empty() {
return;
}
if self.column_index == 1 {
self.previous_column_page();
if self.column_index == 0 {
return;
}
self.select_entire_row = false;
self.column_index -= 1;
}
pub fn next_column_page(&mut self) {
if self.headers.len() > 9 && self.column_page < self.headers.len() - 9 {
self.column_page += 1
}
}
pub fn previous_column_page(&mut self) {
if self.column_page > 0 {
self.column_page -= 1
}
}
pub fn is_selected_cell(&self, row_index: usize, column_index: usize) -> bool {
if column_index != self.column_index {
return false;
}
matches!(self.state.selected(), Some(selected_row_index) if row_index == selected_row_index)
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> {
self.rows
.get(self.state.selected()?)?
.get(self.column_index.saturating_sub(1) + self.column_page)
.get(self.column_index)
.map(|cell| cell.to_string())
}
pub fn headers(&self) -> Vec<String> {
let mut headers = self.headers[self.column_page..].to_vec();
self.headers.clone()
}
pub fn headers_with_number(&self, left: usize, right: usize) -> Vec<String> {
let mut headers = self.headers.clone()[left..right].to_vec();
headers.insert(0, "".to_string());
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
.iter()
.map(|row| row[self.column_page..].to_vec())
.map(|row| row.to_vec())
.collect::<Vec<Vec<String>>>();
let mut new_rows = rows;
let mut new_rows: Vec<Vec<String>> =
rows.iter().map(|row| row[left..right].to_vec()).collect();
for (index, row) in new_rows.iter_mut().enumerate() {
row.insert(0, (index + 1).to_string())
}
new_rows
}
pub fn calculate_widths(
&self,
area_width: u16,
) -> (usize, Vec<String>, Vec<Vec<String>>, Vec<Constraint>) {
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);
}
let right_column_index = self.column_index.clone();
let mut column_index = self.column_index;
let number_clomn_width = (self.rows.len() + 1).to_string().width() as u16;
let mut widths = vec![];
loop {
let length = self
.rows()
.iter()
.map(|row| {
row.get(column_index)
.map_or(String::new(), |cell| cell.to_string())
.width()
})
.collect::<Vec<usize>>()
.iter()
.max()
.map_or(3, |v| {
*v.max(
&self
.headers()
.get(column_index)
.map_or(3, |header| header.to_string().width()),
)
.clamp(&3, &20) as u16
});
if widths.iter().map(|(_, width)| width).sum::<u16>() + length
> area_width
.saturating_sub(7)
.saturating_sub(number_clomn_width)
{
column_index += 1;
break;
}
widths.push((self.headers[column_index].clone(), length));
if column_index == self.column_page_start.get() {
break;
}
column_index -= 1;
}
let left_column_index = column_index;
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)
{
let length = self
.rows()
.iter()
.map(|row| {
row.get(column_index)
.map_or(String::new(), |cell| cell.to_string())
.width()
})
.collect::<Vec<usize>>()
.iter()
.max()
.map_or(3, |v| {
*v.max(
self.headers()
.iter()
.map(|header| header.to_string().width())
.collect::<Vec<usize>>()
.get(column_index)
.unwrap_or(&3),
)
.clamp(&3, &20) as u16
});
match self.headers.get(column_index) {
Some(header) => {
widths.push((header.to_string(), length));
}
None => break,
}
column_index += 1
}
if self.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))
.collect::<Vec<Constraint>>();
if self.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.headers_with_number(left_column_index, right_column_index),
self.rows_with_number(left_column_index, right_column_index),
constraints,
)
}
}
impl DrawableComponent for TableComponent {
@ -199,12 +301,17 @@ impl DrawableComponent for TableComponent {
TableValueComponent::new(self.selected_cell().unwrap_or_default())
.draw(f, layout[0], focused)?;
let headers = self.headers();
let header_cells = headers
.iter()
.map(|h| Cell::from(h.to_string()).style(Style::default()));
let block = Block::default().borders(Borders::ALL).title("Records");
let (selected_column_index, headers, rows, constraints) =
self.calculate_widths(block.inner(layout[1]).width);
let header_cells = headers.iter().enumerate().map(|(column_index, h)| {
Cell::from(h.to_string()).style(if selected_column_index == column_index {
Style::default().add_modifier(Modifier::BOLD)
} else {
Style::default()
})
});
let header = Row::new(header_cells).height(1).bottom_margin(1);
let rows = self.rows();
let rows = rows.iter().enumerate().map(|(row_index, item)| {
let height = item
.iter()
@ -213,20 +320,20 @@ impl DrawableComponent for TableComponent {
.unwrap_or(0)
+ 1;
let cells = item.iter().enumerate().map(|(column_index, c)| {
Cell::from(c.to_string()).style(if self.is_selected_cell(row_index, column_index) {
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()
})
});
Row::new(cells).height(height as u16).bottom_margin(1)
});
let widths = (0..10)
.map(|_| Constraint::Percentage(10))
.collect::<Vec<Constraint>>();
let table = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Records"))
.block(block)
.highlight_style(if self.select_entire_row {
Style::default().bg(Color::Blue)
} else {
@ -237,7 +344,7 @@ impl DrawableComponent for TableComponent {
} else {
Style::default().fg(Color::DarkGray)
})
.widths(&widths);
.widths(&constraints);
f.render_stateful_widget(table, layout[1], &mut self.state);
self.scroll.draw(f, layout[1]);
@ -293,12 +400,13 @@ impl Component for TableComponent {
#[cfg(test)]
mod test {
use super::TableComponent;
use tui::layout::Constraint;
#[test]
fn test_headers() {
let mut component = TableComponent::default();
component.headers = vec!["a", "b", "c"].iter().map(|h| h.to_string()).collect();
assert_eq!(component.headers(), vec!["", "a", "b", "c"])
assert_eq!(component.headers_with_number(1, 2), vec!["", "b"])
}
#[test]
@ -309,8 +417,53 @@ mod test {
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
assert_eq!(
component.rows(),
vec![vec!["1", "a", "b", "c"], vec!["2", "d", "e", "f"]],
component.rows_with_number(1, 2),
vec![vec!["1", "b"], vec!["2", "e"]],
)
}
#[test]
fn test_calculate_widths() {
let mut component = TableComponent::default();
component.headers = vec!["1", "2", "3"].iter().map(|h| h.to_string()).collect();
component.rows = vec![
vec!["aaaaa", "bbbbb", "ccccc"]
.iter()
.map(|h| h.to_string())
.collect(),
vec!["d", "e", "f"].iter().map(|h| h.to_string()).collect(),
];
let (selected_column_index, headers, rows, constraints) = component.calculate_widths(17);
assert_eq!(selected_column_index, 1);
assert_eq!(headers, vec!["", "1", "2"]);
assert_eq!(rows, vec![vec!["1", "aaaaa", "bbbbb"], vec!["2", "d", "e"]]);
assert_eq!(
constraints,
vec![
Constraint::Length(1),
Constraint::Length(5),
Constraint::Min(10),
]
);
let (selected_column_index, headers, rows, constraints) = component.calculate_widths(27);
assert_eq!(selected_column_index, 1);
assert_eq!(headers, vec!["", "1", "2", "3"]);
assert_eq!(
rows,
vec![
vec!["1", "aaaaa", "bbbbb", "ccccc"],
vec!["2", "d", "e", "f"]
]
);
assert_eq!(
constraints,
vec![
Constraint::Length(1),
Constraint::Length(5),
Constraint::Length(5),
Constraint::Min(10),
]
);
}
}

@ -164,7 +164,9 @@ pub fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> S
}
Err(_) => "".to_string(),
},
"VARCHAR" | "CHAR" | "ENUM" => row.try_get(column_name).unwrap_or_else(|_| "".to_string()),
"VARCHAR" | "CHAR" | "ENUM" | "TEXT" => {
row.try_get(column_name).unwrap_or_else(|_| "".to_string())
}
"DATE" => match row.try_get(column_name) {
Ok(value) => {
let value: NaiveDate = value;
@ -186,6 +188,6 @@ pub fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> S
}
Err(_) => "".to_string(),
},
_ => "".to_string(),
column_type => unimplemented!("not implemented column type: {}", column_type),
}
}

Loading…
Cancel
Save