concord 2.5.0

A terminal user interface client for Discord
use super::*;

const OPTIONS_GAUGE_X_OFFSET: u16 = 12;

pub(in crate::tui::ui) fn render_options_popup(
    frame: &mut Frame,
    area: Rect,
    state: &DashboardState,
) {
    if !state.is_active_modal_popup(ActiveModalPopupKind::Options) {
        return;
    }

    let items = state.display_option_items();
    let selected = state.selected_option_index().unwrap_or(0);
    let popup = options_popup_area(area, state);
    let inner = render_modal_frame(frame, popup, state.options_popup_title());
    let visible_items = usize::from(inner.height).max(1);
    let inner_width = usize::from(inner.width).max(1);
    let scroll = state.options_popup_scroll();
    frame.render_widget(
        Paragraph::new(options_popup_lines(
            &items,
            selected,
            visible_items,
            scroll,
            inner_width,
        )),
        inner,
    );
    render_option_gauges(frame, inner, &items, visible_items, scroll);
}

pub(in crate::tui::ui) fn options_popup_visible_items(area: Rect, state: &DashboardState) -> usize {
    let popup = options_popup_area(area, state);
    let inner = panel_block(state.options_popup_title(), true).inner(popup);
    usize::from(inner.height).max(1)
}

pub(in crate::tui::ui) fn options_popup_area(area: Rect, state: &DashboardState) -> Rect {
    let items = state.display_option_items();
    let detail_lines = items.iter().filter(|item| item.gauge.is_some()).count() as u16;
    centered_rect(
        area,
        66,
        (items.len() as u16)
            .saturating_add(detail_lines)
            .saturating_add(2),
    )
}

pub(in crate::tui::ui) fn options_popup_lines(
    items: &[DisplayOptionItem],
    selected: usize,
    visible_items: usize,
    scroll: usize,
    width: usize,
) -> Vec<Line<'static>> {
    let visible_items = visible_items.max(1);
    let width = width.max(1);
    let selected = selected.min(items.len().saturating_sub(1));
    let start = scroll.min(items.len().saturating_sub(visible_items));
    let lines: Vec<Line<'static>> = items
        .iter()
        .enumerate()
        .skip(start)
        .take(visible_items)
        .flat_map(|(index, item)| {
            let selected = index == selected;
            let control = item.value.as_ref().map_or_else(
                || {
                    if item.enabled {
                        "[x]".to_owned()
                    } else {
                        "[ ]".to_owned()
                    }
                },
                |value| format!("[{value}]"),
            );
            let style = selectable_popup_label_style(selected, item.effective || index == 0);
            let row = selected_row_line(
                Line::from(vec![
                    selectable_popup_marker(selected),
                    Span::styled(format!("{control} "), style),
                    Span::styled(item.label, style),
                    Span::styled(
                        " - ",
                        theme::current().style(theme::HighlightGroup::Description),
                    ),
                    Span::styled(
                        item.description,
                        theme::current().style(theme::HighlightGroup::Description),
                    ),
                ]),
                selected,
            );
            let gauge_line = item.gauge.map(|gauge| {
                let (min_label, max_label) = if item
                    .value
                    .as_deref()
                    .is_some_and(|value| value.ends_with('%'))
                {
                    ("0%".to_owned(), format!("{}%", gauge.maximum()))
                } else {
                    ("-100 dB".to_owned(), "0 dB".to_owned())
                };
                popup_gauge_line(
                    OPTIONS_GAUGE_X_OFFSET,
                    &min_label,
                    max_label,
                    theme::current().style(theme::HighlightGroup::Description),
                )
            });
            std::iter::once(row).chain(gauge_line)
        })
        .map(|line| truncate_line_to_display_width(line, width))
        .collect();
    lines
}

fn render_option_gauges(
    frame: &mut Frame,
    inner: Rect,
    items: &[DisplayOptionItem],
    visible_items: usize,
    scroll: usize,
) {
    let visible_items = visible_items.max(1);
    let start = scroll.min(items.len().saturating_sub(visible_items));
    let mut y = inner.y;
    for item in items.iter().skip(start).take(visible_items) {
        y = y.saturating_add(1);
        let Some(gauge) = item.gauge else {
            continue;
        };
        if y >= inner.y.saturating_add(inner.height) {
            break;
        }
        render_popup_gauge(
            frame,
            inner,
            PopupGauge {
                x_offset: OPTIONS_GAUGE_X_OFFSET,
                width_margin: 19,
                y,
                value: gauge.value(),
                maximum: gauge.maximum(),
                style: theme::current().apply(
                    theme::HighlightGroup::GaugeFill,
                    theme::current().style(theme::HighlightGroup::Normal),
                ),
            },
        );
        y = y.saturating_add(1);
    }
}