concord 2.6.2

A terminal user interface client for Discord
use super::*;
use crate::tui::text::truncate_display_width_from;
use ratatui_image::{Image, protocol::Protocol};

pub(in crate::tui) fn gif_picker_popup_area(area: Rect) -> Rect {
    centered_rect(area, 84, 18)
}

pub(in crate::tui) fn gif_picker_search_area(area: Rect) -> Rect {
    let inner = panel_block("", false).inner(gif_picker_popup_area(area));
    Rect {
        x: inner.x.saturating_add(1),
        width: inner.width.saturating_sub(2),
        height: inner.height.min(1),
        ..inner
    }
}

fn gif_picker_body(area: Rect) -> Rect {
    let inner = panel_block("", false).inner(gif_picker_popup_area(area));
    Rect {
        y: inner.y.saturating_add(2),
        height: inner.height.saturating_sub(3),
        ..inner
    }
}

pub(in crate::tui) fn gif_picker_preview_area(area: Rect) -> Rect {
    let body = gif_picker_body(area);
    if body.width < 40 || body.height < 4 {
        return Rect::default();
    }
    Rect {
        x: body.x + body.width / 2 + 1,
        width: body.width - body.width / 2 - 1,
        ..body
    }
}

fn gif_picker_list_area(area: Rect) -> Rect {
    let body = gif_picker_body(area);
    let width = if gif_picker_preview_area(area).is_empty() {
        body.width
    } else {
        body.width / 2
    };
    Rect {
        x: body.x.saturating_add(1),
        width: width.saturating_sub(2),
        ..body
    }
}

pub(in crate::tui::ui) fn gif_picker_list_layout(
    area: Rect,
    snapshot: SelectablePopupSnapshot,
) -> SelectablePopupLayout {
    SelectablePopupLayout::new(
        snapshot.target,
        gif_picker_popup_area(area),
        gif_picker_list_area(area),
        snapshot,
        |start, max_rows| {
            (start..snapshot.item_count.min(start.saturating_add(max_rows)))
                .map(Some)
                .collect()
        },
    )
}

pub(in crate::tui::ui) fn render_gif_picker(
    frame: &mut Frame,
    area: Rect,
    state: &DashboardState,
    preview: Option<Result<&Protocol, &str>>,
) {
    let Some(picker) = state.gif_picker() else {
        return;
    };
    let inner = render_modal_frame(
        frame,
        gif_picker_popup_area(area),
        "GIFs · Powered by KLIPY",
    );
    if inner.is_empty() {
        return;
    }
    let query_area = gif_picker_search_area(area);
    let query_width = usize::from(query_area.width.saturating_sub(2));
    let (query, cursor) = visible_query(
        picker.query.value(),
        picker.query.cursor_byte_index(),
        query_width,
    );
    let placeholder = picker.query.value().is_empty();
    let search_style = if picker.query_selected {
        theme::current().style(theme::HighlightGroup::ActiveField)
    } else {
        theme::current().style(theme::HighlightGroup::Hint)
    };
    frame.render_widget(
        Paragraph::new(format!(
            "{}{}",
            if picker.query_selected { "› " } else { "  " },
            if placeholder { "Search KLIPY" } else { &query }
        ))
        .style(search_style),
        query_area,
    );
    if picker.query_editing {
        frame.set_cursor_position((query_area.x.saturating_add(2 + cursor as u16), query_area.y));
    }
    frame.render_widget(
        Paragraph::new("─".repeat(usize::from(inner.width)))
            .style(theme::current().style(theme::HighlightGroup::Decoration)),
        Rect {
            y: inner.y.saturating_add(1),
            height: 1,
            ..inner
        },
    );
    let preview_area = gif_picker_preview_area(area);
    let list = gif_picker_list_area(area);
    let selected = picker.selected_result_index();
    let scroll = state
        .active_selectable_popup_snapshot()
        .filter(|snapshot| snapshot.target == SelectablePopupTarget::GifResults)
        .map(|snapshot| gif_picker_list_layout(area, snapshot).scroll)
        .unwrap_or(0);
    let lines = if picker.results.is_empty() && picker.loading {
        vec![Line::from("Searching KLIPY…")]
    } else if picker.results.is_empty()
        && let Some(error) = &picker.error
    {
        vec![Line::from(error.clone()), Line::from("r: retry search")]
    } else if picker.results.is_empty() {
        vec![Line::from("No GIFs found")]
    } else {
        picker
            .results
            .iter()
            .enumerate()
            .skip(scroll)
            .take(usize::from(list.height))
            .map(|(i, gif)| {
                let title = gif
                    .title
                    .chars()
                    .filter(|c| !c.is_control())
                    .collect::<String>();
                let title_width = usize::from(list.width.saturating_sub(2));
                let title =
                    truncate_display_width_from(&title, picker.horizontal_scroll, title_width);
                Line::from(Span::styled(
                    truncate_display_width(
                        &format!("{} {}", if Some(i) == selected { "›" } else { " " }, title),
                        usize::from(list.width),
                    ),
                    selectable_popup_label_style(
                        Some(i) == selected,
                        gif.media_url(false).is_some(),
                    ),
                ))
            })
            .collect()
    };
    frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), list);
    if !preview_area.is_empty() {
        match preview {
            Some(Ok(protocol)) => frame.render_widget(Image::new(protocol), preview_area),
            Some(Err(error)) => frame.render_widget(
                Paragraph::new(error).wrap(Wrap { trim: false }),
                preview_area,
            ),
            None => frame.render_widget(
                Paragraph::new(if picker.selected_gif().is_none() {
                    "Select a GIF to preview"
                } else if picker
                    .selected_gif()
                    .and_then(|gif| gif.media_url(true))
                    .is_none()
                {
                    "Preview unavailable"
                } else if state.show_images() {
                    "Loading preview…"
                } else {
                    "Image previews disabled"
                }),
                preview_area,
            ),
        }
    }
    if picker.results.len() > usize::from(list.height) {
        render_vertical_scrollbar(
            frame,
            list,
            scroll,
            usize::from(list.height),
            picker.results.len(),
        );
    }
    let footer = picker
        .error
        .as_deref()
        .or_else(|| (picker.loading && !picker.results.is_empty()).then_some("Loading more…"));
    if inner.height >= 3
        && let Some(footer) = footer
    {
        frame.render_widget(
            Paragraph::new(footer),
            Rect {
                y: inner.y + inner.height - 1,
                height: 1,
                ..inner
            },
        );
    }
}

fn visible_query(query: &str, cursor: usize, width: usize) -> (String, usize) {
    let available = width.saturating_sub(1);
    let mut start = 0;
    while query[start..cursor].width() > available {
        start += query[start..]
            .chars()
            .next()
            .expect("nonempty prefix")
            .len_utf8();
    }
    (
        truncate_display_width(&query[start..], width),
        query[start..cursor].width().min(available),
    )
}