use ratatui::{
Frame,
layout::{Constraint, Flex, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState},
};
use super::widgets::{initials, person_color, priority_glyph, state_glyph, user_name};
use crate::api::types::Priority;
use crate::app::{App, FilterKind, Popup};
use crate::config::Theme;
pub fn draw(f: &mut Frame, app: &mut App) {
match app.popup {
Popup::TeamSelect => draw_team_select(f, app),
Popup::Filter => draw_filter(f, app),
Popup::StatusChange => draw_status_change(f, app),
Popup::PriorityChange => draw_priority_change(f, app),
Popup::AssigneeChange => draw_assignee_change(f, app),
Popup::None => {}
}
}
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let vertical = Layout::vertical([Constraint::Length(height)])
.flex(Flex::Center)
.split(area);
let horizontal = Layout::horizontal([Constraint::Length(width)])
.flex(Flex::Center)
.split(vertical[0]);
horizontal[0]
}
fn render_popup_list(f: &mut Frame, app: &mut App, title: &str, items: Vec<ListItem>, width: u16) {
let th = app.theme;
let height = (items.len() as u16 + 2).min(22).min(f.area().height);
let width = width.min(f.area().width.saturating_sub(2));
let area = centered_rect(width, height, f.area());
f.render_widget(Clear, area);
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(th.border))
.title(Span::styled(
format!(" {title} "),
Style::default().fg(th.text).add_modifier(Modifier::BOLD),
))
.title_bottom(Line::from(Span::styled(
" 1-9 pick \u{00b7} Enter apply \u{00b7} Esc close ",
Style::default().fg(th.muted),
))),
)
.highlight_style(
Style::default()
.bg(th.selection_bg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("\u{258c}");
let mut state = ListState::default().with_offset(app.popup_offset);
state.select(Some(app.popup_index));
f.render_stateful_widget(list, area, &mut state);
app.popup_offset = state.offset();
app.popup_area = Rect {
x: area.x + 1,
y: area.y + 1,
width: area.width.saturating_sub(2),
height: area.height.saturating_sub(2),
};
}
fn numbered_item(
index: usize,
lead: Vec<Span<'static>>,
text: &str,
is_current: bool,
th: &Theme,
) -> ListItem<'static> {
let mut spans = vec![Span::styled(
if index < 9 {
format!("{} ", index + 1)
} else {
" ".to_string()
},
Style::default().fg(th.muted),
)];
spans.extend(lead);
spans.push(Span::styled(text.to_string(), Style::default().fg(th.text)));
if is_current {
spans.push(Span::styled(" \u{2713}", Style::default().fg(th.success)));
}
ListItem::new(Line::from(spans))
}
fn draw_team_select(f: &mut Frame, app: &mut App) {
let th = app.theme;
let items: Vec<ListItem> = app
.teams
.iter()
.enumerate()
.map(|(i, team)| {
let color = team
.color
.as_deref()
.and_then(crate::api::types::hex_color)
.unwrap_or(th.accent);
numbered_item(
i,
vec![Span::styled("\u{25cf} ", Style::default().fg(color))],
&format!("{} {}", team.name, team.key),
i == app.selected_team_index,
&th,
)
})
.collect();
render_popup_list(f, app, "Switch team", items, 44);
}
fn draw_filter(f: &mut Frame, app: &mut App) {
let th = app.theme;
let (title, items) = match app.filter_kind {
FilterKind::Status => {
let mut items = vec![numbered_item(
0,
vec![],
"Any status",
app.filters.status.is_none(),
&th,
)];
for (i, state) in app.workflow_states.iter().enumerate() {
let is_current = app
.filters
.status
.as_ref()
.is_some_and(|s| s == &state.name);
items.push(numbered_item(
i + 1,
vec![state_glyph(Some(state), &th), Span::raw(" ")],
&state.name,
is_current,
&th,
));
}
("Filter \u{2014} status", items)
}
FilterKind::Priority => {
let mut items = vec![numbered_item(
0,
vec![],
"Any priority",
app.filters.priority.is_none(),
&th,
)];
for i in 1..=5 {
let p = Priority::from_index(i);
items.push(numbered_item(
i,
vec![priority_glyph(p, &th), Span::raw(" ")],
p.label(),
app.filters.priority == Some(p),
&th,
));
}
("Filter \u{2014} priority", items)
}
};
render_popup_list(f, app, title, items, 38);
}
fn draw_status_change(f: &mut Frame, app: &mut App) {
let th = app.theme;
let current_state_id = app
.focused_issue()
.and_then(|i| i.state.as_ref())
.map(|s| s.id.clone());
let items: Vec<ListItem> = app
.workflow_states
.iter()
.enumerate()
.map(|(i, state)| {
numbered_item(
i,
vec![state_glyph(Some(state), &th), Span::raw(" ")],
&state.name,
current_state_id.as_deref() == Some(state.id.as_str()),
&th,
)
})
.collect();
render_popup_list(f, app, "Change status", items, 38);
}
fn draw_priority_change(f: &mut Frame, app: &mut App) {
let th = app.theme;
let current_pri = app.focused_issue().map(|i| i.priority);
let priorities = [
Priority::None,
Priority::Urgent,
Priority::High,
Priority::Medium,
Priority::Low,
];
let items: Vec<ListItem> = priorities
.iter()
.enumerate()
.map(|(i, pri)| {
numbered_item(
i,
vec![priority_glyph(*pri, &th), Span::raw(" ")],
pri.label(),
current_pri == Some(*pri),
&th,
)
})
.collect();
render_popup_list(f, app, "Change priority", items, 34);
}
fn draw_assignee_change(f: &mut Frame, app: &mut App) {
let th = app.theme;
let current_assignee_id = app
.focused_issue()
.and_then(|i| i.assignee.as_ref())
.map(|a| a.id.clone());
let mut items = vec![numbered_item(
0,
vec![Span::styled("\u{25cc} ", Style::default().fg(th.muted))],
"No assignee",
current_assignee_id.is_none(),
&th,
)];
for (i, member) in app.team_members.iter().enumerate() {
let name = user_name(member).to_string();
items.push(numbered_item(
i + 1,
vec![
Span::styled(
initials(&name),
Style::default()
.fg(ratatui::style::Color::Black)
.bg(person_color(&name))
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
],
&name,
current_assignee_id.as_deref() == Some(member.id.as_str()),
&th,
));
}
render_popup_list(f, app, "Assign to", items, 44);
}