use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::Frame;
use crate::app::state::AppState;
use crate::engine::models::TestPhase;
use crate::tui::layout::{breakpoint, padded, Breakpoint};
use crate::tui::renderer::Motion;
use crate::tui::theme::Theme;
use crate::tui::widgets::dial_gauge::{self, DialData};
use crate::tui::widgets::speed_gauge::{self, SpeedGaugeData};
use crate::tui::widgets::{ping_card, progress_bar, status_bar, transfer_card};
use crate::utils::format::SpeedUnit;
const DIAL_MIN_HEIGHT: u16 = 13;
pub fn render(frame: &mut Frame, area: Rect, theme: &Theme, state: &AppState, motion: &Motion) {
let colors = &theme.colors;
let bp = breakpoint(area);
let (gauge_height, card_height, pad) = match bp {
Breakpoint::Compact => (7, 6, 1),
Breakpoint::Normal => (13, 7, 2),
Breakpoint::Wide => (18, 7, 4),
};
let body = padded(area, pad * 2, 0);
let [status_area, _, gauge_area, bar_area, _, cards_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(gauge_height),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(card_height),
])
.areas(body);
let phase = state.phase;
let (eta, ratio) = match phase {
Some(TestPhase::Download) => {
let eta = (state.download.bytes > 0).then_some(state.download.eta);
(eta, motion.download_ratio)
}
Some(TestPhase::Upload) => {
let eta = (state.upload.bytes > 0).then_some(state.upload.eta);
(eta, motion.upload_ratio)
}
_ => (None, 0.0),
};
status_bar::render(
frame,
status_area,
theme,
&status_bar::StatusInfo {
phase,
server: state.server_name(),
tick: state.tick,
elapsed: state.elapsed,
eta,
},
);
render_gauge(frame, gauge_area, theme, state, motion, bp);
let bar = padded(bar_area, body.width / 6, 0);
let (from, to) = match phase {
Some(TestPhase::Upload) => (colors.upload, colors.accent_alt),
_ => (colors.download, colors.accent),
};
progress_bar::render(frame, bar, theme, ratio, from, to);
let [ping_area, download_area, upload_area] = Layout::horizontal([
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
])
.spacing(pad)
.areas(cards_area);
ping_card::render(
frame,
ping_area,
theme,
&state.ping,
phase == Some(TestPhase::Ping),
);
transfer_card::render_download(
frame,
download_area,
theme,
&state.download,
phase == Some(TestPhase::Download),
);
transfer_card::render_upload(
frame,
upload_area,
theme,
&state.upload,
phase == Some(TestPhase::Upload),
);
}
fn render_gauge(
frame: &mut Frame,
area: Rect,
theme: &Theme,
state: &AppState,
motion: &Motion,
bp: Breakpoint,
) {
let colors = &theme.colors;
let phase = state.phase;
let dial = area.height >= DIAL_MIN_HEIGHT && crate::tui::glyphs::current().fancy;
let ping_ms = state
.ping
.stats
.map(|stats| stats.average_ms)
.or(state.ping.last_ms);
if dial && bp == Breakpoint::Wide {
let [left, right] = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
.spacing(2)
.areas(area);
let download_active = !matches!(phase, Some(TestPhase::Upload));
dial_gauge::render(
frame,
left,
theme,
&DialData {
label: "Download",
bps: motion.download_bps,
color: colors.download,
peak_bps: state.download.peak_bps,
trail: &motion.download_trail,
ping_ms,
override_ratio: sweep_for(motion, phase, TestPhase::Download),
dimmed: !download_active,
speed_unit: state.speed_unit,
},
);
dial_gauge::render(
frame,
right,
theme,
&DialData {
label: "Upload",
bps: motion.upload_bps,
color: colors.upload,
peak_bps: state.upload.peak_bps,
trail: &motion.upload_trail,
ping_ms,
override_ratio: sweep_for(motion, phase, TestPhase::Upload),
dimmed: download_active,
speed_unit: state.speed_unit,
},
);
return;
}
if dial && phase != Some(TestPhase::Ping) {
let upload = phase == Some(TestPhase::Upload);
let (label, bps, color, peak, trail, active_phase) = if upload {
(
"Upload",
motion.upload_bps,
colors.upload,
state.upload.peak_bps,
motion.upload_trail.as_slice(),
TestPhase::Upload,
)
} else {
(
"Download",
motion.download_bps,
colors.download,
state.download.peak_bps,
motion.download_trail.as_slice(),
TestPhase::Download,
)
};
dial_gauge::render(
frame,
area,
theme,
&DialData {
label,
bps,
color,
peak_bps: peak,
trail,
ping_ms,
override_ratio: sweep_for(motion, phase, active_phase),
dimmed: false,
speed_unit: state.speed_unit,
},
);
return;
}
match phase {
Some(TestPhase::Upload) => speed_gauge::render(
frame,
area,
theme,
&SpeedGaugeData {
label: "Upload",
bps: motion.upload_bps,
color: colors.upload,
history: &state.upload.history,
speed_unit: state.speed_unit,
},
),
Some(TestPhase::Ping) => speed_gauge::render(
frame,
area,
theme,
&SpeedGaugeData {
label: "Ping",
bps: 0.0,
color: colors.latency,
history: &state.ping.history,
speed_unit: SpeedUnit::Bits,
},
),
_ => speed_gauge::render(
frame,
area,
theme,
&SpeedGaugeData {
label: "Download",
bps: motion.download_bps,
color: colors.download,
history: &state.download.history,
speed_unit: state.speed_unit,
},
),
}
}
fn sweep_for(motion: &Motion, current: Option<TestPhase>, dial_phase: TestPhase) -> Option<f64> {
if current == Some(dial_phase) {
motion.sweep_ratio
} else {
None
}
}