use chrono::{DateTime, Utc};
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use std::time::Instant;
use crate::app::mode::Mode;
use crate::app::state::{ActiveOverlay, AppState};
use crate::widgets::conversation::ConversationPane;
use crate::widgets::live_stream::LiveStreamPane;
use crate::widgets::overlay::{FrictionPauseOverlay, RiskOverlay, StateOverlay, VerdictOverlay};
use crate::widgets::pane::{CockpitPane, DecisionsPane, HeatPane, PositionsPane};
use crate::widgets::picker::{PickerWidget, picker_rows};
use crate::widgets::prompt::PromptWidget;
use crate::widgets::statusbar::StatusBar;
pub const LIVE_STREAM_ROWS: u16 = 8;
const MIN_MODE_ROWS_WITH_STREAM: u16 = 6;
pub fn render(frame: &mut Frame<'_>, state: &AppState) {
render_at(frame, state, Utc::now());
}
const MAX_VISIBLE_PROMPT_ROWS: u16 = 6;
pub fn render_at(frame: &mut Frame<'_>, state: &AppState, now: DateTime<Utc>) {
let size = frame.area();
let prompt_rows = u16::try_from(state.prompt.height())
.unwrap_or(u16::MAX)
.clamp(1, MAX_VISIBLE_PROMPT_ROWS);
let picker_rows_needed = state
.picker
.as_ref()
.map_or(0, picker_rows)
.min(MAX_VISIBLE_PICKER_ROWS);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1), Constraint::Length(picker_rows_needed), Constraint::Length(prompt_rows), Constraint::Length(1), ])
.split(size);
let mode_area = chunks[0];
let (mode_rect, live_stream_rect) = if state.live_stream_visible
&& mode_area.height.saturating_sub(LIVE_STREAM_ROWS) >= MIN_MODE_ROWS_WITH_STREAM
{
let split = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(MIN_MODE_ROWS_WITH_STREAM),
Constraint::Length(LIVE_STREAM_ROWS),
])
.split(mode_area);
(split[0], Some(split[1]))
} else {
(mode_area, None)
};
render_mode(frame, state, mode_rect);
if let Some(area) = live_stream_rect {
frame.render_widget(
LiveStreamPane {
ring: &state.event_ring,
theme: state.theme,
},
area,
);
}
if let Some(ov) = state.overlay.as_ref() {
render_overlay(frame, state, ov, mode_area, now);
}
if picker_rows_needed > 0
&& let Some(picker) = state.picker.as_ref()
{
frame.render_widget(
PickerWidget {
picker,
theme: state.theme,
},
chunks[1],
);
}
let prompt = PromptWidget {
prompt: &state.prompt,
theme: state.theme,
};
let (cursor_col, cursor_row) = prompt.cursor_position();
frame.render_widget(prompt, chunks[2]);
let visible_cursor_row = cursor_row.min(prompt_rows.saturating_sub(1));
frame.set_cursor_position((chunks[2].x + cursor_col, chunks[2].y + visible_cursor_row));
let engine_snapshot = state.engine.read().clone();
let rate_budget = state
.rate_budget
.as_ref()
.map(zero_engine_client::RateBudget::snapshot);
let status = StatusBar {
mode: state.mode,
engine: &engine_snapshot,
theme: state.theme,
now,
rate_budget,
};
frame.render_widget(status, chunks[3]);
}
const MAX_VISIBLE_PICKER_ROWS: u16 = 6;
fn render_overlay(
frame: &mut Frame<'_>,
state: &AppState,
ov: &ActiveOverlay,
area: Rect,
now: DateTime<Utc>,
) {
match ov {
ActiveOverlay::State => {
let snap = state.engine.read().clone();
frame.render_widget(
StateOverlay {
engine: &snap,
theme: state.theme,
now,
},
area,
);
}
ActiveOverlay::FrictionPause(fp) => {
frame.render_widget(
FrictionPauseOverlay {
pause: fp,
theme: state.theme,
now: Instant::now(),
},
area,
);
}
ActiveOverlay::Verdict(eval) => {
frame.render_widget(
VerdictOverlay {
evaluation: eval.as_ref(),
theme: state.theme,
},
area,
);
}
ActiveOverlay::Risk { trigger, .. } => {
let snap = state.engine.read().clone();
frame.render_widget(
RiskOverlay {
engine: &snap,
trigger: *trigger,
theme: state.theme,
now,
},
area,
);
}
}
}
fn render_mode(frame: &mut Frame<'_>, state: &AppState, area: Rect) {
match state.mode {
Mode::Conversation => {
let pane = ConversationPane {
log: &state.log,
theme: state.theme,
scroll: state.log_scroll,
screen_reader: state.screen_reader,
verbose: state.verbose,
};
frame.render_widget(pane, area);
}
Mode::Positions => {
let snap = state.engine.read().clone();
frame.render_widget(
PositionsPane {
engine: &snap,
theme: state.theme,
},
area,
);
}
Mode::Decisions => {
frame.render_widget(DecisionsPane { theme: state.theme }, area);
}
Mode::Heat => {
let snap = state.engine.read().clone();
frame.render_widget(
HeatPane {
engine: &snap,
theme: state.theme,
},
area,
);
}
Mode::Cockpit => {
let snap = state.engine.read().clone();
frame.render_widget(
CockpitPane {
engine: &snap,
theme: state.theme,
},
area,
);
}
}
}