use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style},
widgets::{Clear, Paragraph, StatefulWidget, Widget},
};
use throbber_widgets_tui::{Throbber, ThrobberState, WhichUse};
use super::{PopupSizing, SizeHint};
use crate::tui::theme::{self, BlockVariant};
pub struct IssueLoadingPopup<'a> {
issue_number: u32,
message: &'a str,
}
impl<'a> IssueLoadingPopup<'a> {
#[must_use]
pub fn new(issue_number: u32, message: &'a str) -> Self {
Self {
issue_number,
message,
}
}
}
impl PopupSizing for IssueLoadingPopup<'_> {
fn size_hint(&self) -> SizeHint {
SizeHint::percent(0, 0)
.with_min_width(30)
.with_min_height(6)
}
}
impl StatefulWidget for IssueLoadingPopup<'_> {
type State = ThrobberState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Clear.render(area, buf);
let title = format!(" Issue #{} ", self.issue_number);
let block = theme::block(&title, BlockVariant::Focused);
let inner = block.inner(area);
block.render(area, buf);
let chunks = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(inner);
let throbber = Throbber::default()
.label(self.message)
.style(Style::default().fg(Color::Cyan))
.throbber_style(Style::default().fg(Color::Yellow))
.use_type(WhichUse::Spin);
let label_len = u16::try_from(self.message.len()).unwrap_or(u16::MAX);
let content_width = 2_u16.saturating_add(label_len).min(chunks[1].width);
let centered_area = Rect {
x: chunks[1].x + chunks[1].width.saturating_sub(content_width) / 2,
y: chunks[1].y,
width: content_width,
height: 1,
};
StatefulWidget::render(throbber, centered_area, buf, state);
Paragraph::new("[Esc: cancel]")
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center)
.render(chunks[3], buf);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tui::test_utils::buffer_to_text;
#[test]
fn issue_loading_popup_new() {
let popup = IssueLoadingPopup::new(42, "Loading...");
assert_eq!(popup.issue_number, 42);
assert_eq!(popup.message, "Loading...");
}
#[test]
fn issue_loading_popup_size_hint() {
let popup = IssueLoadingPopup::new(1, "test");
let hint = popup.size_hint();
assert_eq!(hint.percent_x, 0);
assert_eq!(hint.percent_y, 0);
assert_eq!(hint.min_width, Some(30));
assert_eq!(hint.min_height, Some(6));
}
#[test]
fn issue_loading_popup_renders() {
let popup = IssueLoadingPopup::new(42, "Fetching issue...");
let area = Rect::new(0, 0, 50, 8);
let mut buf = Buffer::empty(area);
let mut state = ThrobberState::default();
popup.render(area, &mut buf, &mut state);
let output = buffer_to_text(&buf);
assert!(output.contains("Issue #42"));
assert!(output.contains("Fetching issue"));
assert!(output.contains("[Esc: cancel]"));
}
}