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(), "".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::>(), ) .cyan() .bold(); let col_widths: Vec = 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::>(), ) }) .collect::>(), 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(value: T, min: u16, max: u16) -> u16 where T: Into, { 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") } }