ttz/src/view.rs

120 lines
4.1 KiB
Rust
Raw Normal View History

2026-06-08 05:53:41 +00:00
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")
}
}