thd75_tui/ui/
band.rs

1use ratatui::Frame;
2use ratatui::layout::Rect;
3use ratatui::style::{Color, Modifier, Style};
4use ratatui::text::{Line, Span};
5use ratatui::widgets::{Block, Borders, Paragraph};
6
7use kenwood_thd75::types::SMeterReading;
8
9use crate::app::{App, BandState, InputMode, Pane};
10
11/// Render a band panel (A or B) into the given area.
12///
13/// Displays frequency, mode, power, squelch, busy/RX indicator,
14/// S-meter bar, step size, and attenuator state. When the pane is
15/// focused and in frequency input mode, an additional input prompt
16/// line is shown. Returns early for non-band panes.
17pub(crate) fn render(app: &App, frame: &mut Frame<'_>, area: Rect, pane: Pane) {
18    let (title, band) = match pane {
19        Pane::BandA => (" Band A ", &app.state.band_a),
20        Pane::BandB => (" Band B ", &app.state.band_b),
21        _ => return,
22    };
23
24    let block = Block::default()
25        .title(title)
26        .borders(Borders::ALL)
27        .border_style(super::border_style(app, pane));
28
29    let mut lines = band_lines(band);
30
31    // Show frequency input prompt when active on this pane
32    if app.focus == pane
33        && let InputMode::FreqInput(ref buf) = app.input_mode
34    {
35        lines.push(Line::from(vec![
36            Span::styled("  Freq: ", Style::default().fg(Color::Yellow)),
37            Span::styled(
38                format!("{buf}▎ MHz"),
39                Style::default()
40                    .fg(Color::White)
41                    .add_modifier(Modifier::BOLD),
42            ),
43        ]));
44    }
45
46    frame.render_widget(Paragraph::new(lines).block(block), area);
47}
48
49/// Build the 4-line display for a band panel:
50/// 1. Frequency (bold white, MHz with 6 decimal places)
51/// 2. Mode + Power + Squelch + RX indicator (green "RX" badge when busy)
52/// 3. S-meter bar (green S0-S3, yellow S5-S7, red S9)
53/// 4. Step size + ATT indicator (red when attenuator is on)
54fn band_lines(band: &BandState) -> Vec<Line<'static>> {
55    let freq = format!("{:.6} MHz", band.frequency.as_mhz());
56    let freq_line = Line::from(Span::styled(
57        format!("  {freq}"),
58        Style::default()
59            .fg(Color::White)
60            .add_modifier(Modifier::BOLD),
61    ));
62
63    let busy_span = if band.busy {
64        Span::styled(" RX ", Style::default().fg(Color::Black).bg(Color::Green))
65    } else {
66        Span::raw("    ")
67    };
68
69    let mode_line = Line::from(vec![
70        Span::styled(
71            format!("  {}  ", band.mode),
72            Style::default().fg(Color::Cyan),
73        ),
74        Span::styled("Pwr:", Style::default().fg(Color::DarkGray)),
75        Span::styled(
76            power_label(band.power_level).to_string(),
77            Style::default().fg(Color::Yellow),
78        ),
79        Span::raw("  "),
80        Span::styled("Sq:", Style::default().fg(Color::DarkGray)),
81        Span::styled(
82            format!("{}", band.squelch.as_u8()),
83            Style::default().fg(Color::Yellow),
84        ),
85        Span::raw("  "),
86        busy_span,
87    ]);
88
89    let s_meter_line = s_meter_line(band.s_meter);
90
91    let step_str = band
92        .step_size
93        .map_or_else(|| "N/A".into(), |s| format!("{s}"));
94    let mut extra = vec![
95        Span::styled("  Step:", Style::default().fg(Color::DarkGray)),
96        Span::styled(step_str, Style::default().fg(Color::Yellow)),
97    ];
98    if band.attenuator {
99        extra.push(Span::styled(" ATT", Style::default().fg(Color::Red)));
100    }
101    let extra_line = Line::from(extra);
102
103    vec![freq_line, mode_line, s_meter_line, extra_line]
104}
105
106/// Render S-meter bar from an `SMeterReading`.
107///
108/// Uses `as_u8()` for the raw reading (0-5) to compute bar width via
109/// `s_unit()` label mapping, and `s_unit()` for the display label.
110fn s_meter_line(reading: SMeterReading) -> Line<'static> {
111    let label = reading.s_unit(); // e.g. "S0", "S3", "S9"
112    // Use the raw-to-S-unit mapping for the bar width:
113    // raw 0→S0, 1→S1, 2→S3, 3→S5, 4→S7, 5→S9
114    let s_unit: u8 = match reading.as_u8() {
115        1 => 1,
116        2 => 3,
117        3 => 5,
118        4 => 7,
119        5 => 9,
120        _ => 0,
121    };
122
123    // Bar: 9 segments for S0-S9
124    let filled = s_unit.min(9) as usize;
125    let empty = 9 - filled;
126    let bar: String = "▓".repeat(filled) + &"░".repeat(empty);
127
128    let color = if s_unit >= 9 {
129        Color::Red
130    } else if s_unit >= 5 {
131        Color::Yellow
132    } else {
133        Color::Green
134    };
135
136    Line::from(vec![
137        Span::styled("  S ", Style::default().fg(Color::DarkGray)),
138        Span::styled(bar, Style::default().fg(color)),
139        Span::styled(format!(" {label}"), Style::default().fg(Color::White)),
140    ])
141}
142
143/// Map power level to a compact display label: H/M/L/EL.
144const fn power_label(level: kenwood_thd75::types::PowerLevel) -> &'static str {
145    use kenwood_thd75::types::PowerLevel;
146    match level {
147        PowerLevel::High => "H",
148        PowerLevel::Medium => "M",
149        PowerLevel::Low => "L",
150        PowerLevel::ExtraLow => "EL",
151    }
152}