use crate::cli::tui::state::TuiState;
use crate::cli::tui::traits::{DisplayName, IsActive, KeyBindings};
use crate::cli::tui::widgets::utils::block_ext::RoundedBlockExt;
use ratatui::layout::{Constraint, Layout, Margin, Rect};
use ratatui::prelude::{Color, Line, Style, Stylize};
use ratatui::style::Styled;
use ratatui::widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState};
use ratatui::Frame;
use std::cmp::PartialEq;
pub fn ui(frame: &mut Frame, state: &mut TuiState) {
update_focus(state);
let (header_area, middle_area, footer_area) = setup_layout(frame);
let (main_area, log_area) = arrange_middle_area(state, middle_area);
let [filter_area, start_stop_toggle_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Max(8)]).areas(header_area);
render_start_stop_toggle(frame, state, filter_area, start_stop_toggle_area);
render_sections(frame, state, main_area);
frame.render_widget(&mut state.logs_widget, log_area);
render_keybindings(frame, state, footer_area);
}
#[derive(PartialEq)]
pub enum LayoutSection {
Filter,
Main,
Logging,
}
fn update_focus(state: &mut TuiState) {
if state.filter_widget.inputting {
state.focused = LayoutSection::Filter;
} else if state.logs_widget.focused {
state.focused = LayoutSection::Logging;
} else {
state.focused = LayoutSection::Main;
}
}
fn setup_layout(frame: &mut Frame) -> (Rect, Rect, Rect) {
let [header_area, middle_area, footer_area] = Layout::vertical([
Constraint::Max(3),
Constraint::Min(0),
Constraint::Length(1),
])
.areas(frame.area());
(header_area, middle_area, footer_area)
}
fn arrange_middle_area(state: &mut TuiState, middle_area: Rect) -> (Rect, Rect) {
let [main_area, log_area] =
if middle_area.height + 60 >= middle_area.width || !state.logs_widget.open {
Layout::vertical([
Constraint::Max(500),
Constraint::Max(if state.logs_widget.open { 10 } else { 1 }),
])
.areas(middle_area)
} else {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Fill(if state.logs_widget.open { 1 } else { 0 }),
])
.areas(middle_area)
};
(main_area, log_area)
}
fn render_start_stop_toggle(
frame: &mut Frame,
state: &mut TuiState,
filter_area: Rect,
start_stop_toggle_area: Rect,
) {
frame.render_widget(&mut state.filter_widget, filter_area);
frame.render_widget(
Paragraph::new(if state.processing {
"Stop".to_string()
} else {
"Start".to_string()
})
.block(Block::roundedt("[P]").set_style(if state.processing {
Style::new().fg(Color::LightRed)
} else {
Style::new().fg(Color::LightGreen)
})),
start_stop_toggle_area,
);
}
fn render_sections(frame: &mut Frame, state: &mut TuiState, main_area: Rect) {
let total_sections = state.sections.len();
let default_height = 5;
let available_rect = main_area.inner(Margin {
horizontal: 1,
vertical: 1,
});
let available_height = available_rect.height as usize;
let max_visible_sections = available_height / default_height;
let half_visible = max_visible_sections / 2;
let max_visible_sections = max_visible_sections.max(1);
let (start_index, end_index) = if max_visible_sections >= total_sections {
(0, total_sections - 1)
} else {
let start = state.selected.saturating_sub(half_visible);
let end = (start + max_visible_sections - 1).min(total_sections - 1);
let start = if end == total_sections - 1 {
end.saturating_sub(max_visible_sections - 1)
} else {
start
};
(start, end)
};
let constraints: Vec<Constraint> = (0..total_sections)
.map(|i| {
if i >= start_index && i <= end_index {
Constraint::Length(default_height as u16)
} else {
Constraint::Length(0)
}
})
.collect();
let section_areas: [Rect; 7] = Layout::vertical(constraints).areas(available_rect);
let mut main_block =
Block::roundedt("Main").title_bottom(Line::from("This is the main area").right_aligned());
main_block = main_block.highlight_if(state.focused == LayoutSection::Main);
frame.render_widget(main_block, main_area);
for (i, option) in state.sections.iter_mut().enumerate() {
let mut area_block = Block::rounded().title(format!("[{}]-{}", i + 1, option.name()));
if !option.is_active() {
area_block = area_block.fg(Color::DarkGray);
}
if state.selected == i {
area_block = area_block.border_style(Style::default().fg(Color::Cyan));
}
area_block = area_block.highlight_if(state.interacting == Some(i));
frame.render_widget(area_block, section_areas[i]);
frame.render_widget(option, section_areas[i]);
}
if total_sections > max_visible_sections {
let scroll_position = state.selected;
let mut scrollbar_state = ScrollbarState::new(total_sections)
.viewport_content_length(max_visible_sections)
.position(scroll_position);
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.thumb_symbol("█")
.thumb_style(Style::default().fg(Color::DarkGray));
frame.render_stateful_widget(scrollbar, available_rect, &mut scrollbar_state);
}
}
fn render_keybindings(frame: &mut Frame, state: &mut TuiState, key_bind_area: Rect) {
let mut keybinds = "Quit: q | Toggle: Space | Navigation: Up and Down".to_string();
match state.focused {
LayoutSection::Filter => {
keybinds = state.filter_widget.key_bindings();
}
LayoutSection::Main => {
if let Some(index) = state.interacting {
keybinds = state.sections[index].key_bindings();
}
}
LayoutSection::Logging => {
keybinds = state.logs_widget.key_bindings();
}
}
frame.render_widget(Paragraph::new(keybinds).style(Color::Cyan), key_bind_area)
}