120 lines
4.1 KiB
Rust
120 lines
4.1 KiB
Rust
|
|
use ratatui::widgets::BorderType::Rounded;
|
||
|
|
use ratatui::widgets::{Cell, Padding, Row, Table};
|
||
|
|
use ratatui::{prelude::*, symbols::border, widgets::Block};
|
||
|
|
|
||
|
|
use crate::model::{CellData, FocusState, Model};
|
||
|
|
|
||
|
|
/// Elm render function.
|
||
|
|
pub(crate) fn view(model: &mut Model, frame: &mut Frame) {
|
||
|
|
let title = Line::from(" Team Time Zones ".bold());
|
||
|
|
let instructions = Line::from(match model.focus_state {
|
||
|
|
FocusState::Table => vec![
|
||
|
|
" Filter ".into(),
|
||
|
|
"/".blue().bold(),
|
||
|
|
" Quit ".into(),
|
||
|
|
"Q ".blue().bold(),
|
||
|
|
],
|
||
|
|
FocusState::Filter => vec![" Reset ".into(), "<Esc>".blue().bold(), " ".into()],
|
||
|
|
});
|
||
|
|
let main_block = Block::bordered()
|
||
|
|
.title(title.centered().fg(Color::Reset))
|
||
|
|
.title_bottom(instructions.centered().fg(Color::Reset))
|
||
|
|
.border_set(border::PLAIN)
|
||
|
|
.border_style(Style::new().fg(Color::DarkGray))
|
||
|
|
.border_type(Rounded);
|
||
|
|
frame.render_widget(&main_block, frame.area());
|
||
|
|
|
||
|
|
let visible_locations = model.get_visible_locations();
|
||
|
|
let header = Row::new(
|
||
|
|
visible_locations
|
||
|
|
.iter()
|
||
|
|
.map(|location| location.name.as_str())
|
||
|
|
.collect::<Vec<&str>>(),
|
||
|
|
)
|
||
|
|
.cyan()
|
||
|
|
.bold();
|
||
|
|
let col_widths: Vec<Constraint> = visible_locations
|
||
|
|
.iter()
|
||
|
|
.map(|location| Constraint::Length(clamp_u16(location.name.len(), 5, 32)))
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
// TODO: Figure out a good way to refactor this into a distinct view
|
||
|
|
// function without offending the borrow checker.
|
||
|
|
let table = Table::new(
|
||
|
|
model
|
||
|
|
.table_rows
|
||
|
|
.iter()
|
||
|
|
.map(|row| {
|
||
|
|
Row::new(
|
||
|
|
row.iter()
|
||
|
|
.map(
|
||
|
|
|&CellData {
|
||
|
|
is_current_time,
|
||
|
|
is_daylight,
|
||
|
|
datetime,
|
||
|
|
}| {
|
||
|
|
Cell::new(datetime.format("%H:%M").to_string()).style(
|
||
|
|
if is_current_time {
|
||
|
|
Style::new().red()
|
||
|
|
} else if !is_daylight {
|
||
|
|
Style::new().fg(Color::DarkGray)
|
||
|
|
} else {
|
||
|
|
Style::new()
|
||
|
|
},
|
||
|
|
)
|
||
|
|
},
|
||
|
|
)
|
||
|
|
.collect::<Vec<Cell>>(),
|
||
|
|
)
|
||
|
|
})
|
||
|
|
.collect::<Vec<Row>>(),
|
||
|
|
col_widths,
|
||
|
|
)
|
||
|
|
.header(header)
|
||
|
|
.column_spacing(4)
|
||
|
|
.block(Block::new().padding(Padding::horizontal(2)));
|
||
|
|
|
||
|
|
let [table_area, filter_area] = Layout::vertical([
|
||
|
|
Constraint::Fill(1),
|
||
|
|
Constraint::Length(if model.focus_state == FocusState::Filter {
|
||
|
|
1
|
||
|
|
} else {
|
||
|
|
0
|
||
|
|
}),
|
||
|
|
])
|
||
|
|
.areas(main_block.inner(frame.area()));
|
||
|
|
if model.focus_state == FocusState::Filter {
|
||
|
|
const FILTER_LABEL_TEXT: &str = "Filter: ";
|
||
|
|
let [filter_label_area, filter_input_area] = Layout::horizontal([
|
||
|
|
Constraint::Length(
|
||
|
|
FILTER_LABEL_TEXT
|
||
|
|
.len()
|
||
|
|
.try_into()
|
||
|
|
.expect("filter label is of static, reasonable length"),
|
||
|
|
),
|
||
|
|
Constraint::Fill(1),
|
||
|
|
])
|
||
|
|
.areas(filter_area);
|
||
|
|
frame.render_widget(FILTER_LABEL_TEXT.dim(), filter_label_area);
|
||
|
|
frame.render_widget(&model.filter_textarea, filter_input_area);
|
||
|
|
}
|
||
|
|
frame.render_stateful_widget(table, table_area, &mut model.table_state);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Clip a [`usize`]-like value to between two [`u16`]s (inclusive).
|
||
|
|
fn clamp_u16<T>(value: T, min: u16, max: u16) -> u16
|
||
|
|
where
|
||
|
|
T: Into<usize>,
|
||
|
|
{
|
||
|
|
let value: usize = value.into();
|
||
|
|
if value <= min.into() {
|
||
|
|
min
|
||
|
|
} else if value >= max.into() {
|
||
|
|
max
|
||
|
|
} else {
|
||
|
|
value
|
||
|
|
.try_into()
|
||
|
|
.expect("value is known to be unsigned and less than some valid u16")
|
||
|
|
}
|
||
|
|
}
|