use super::*;
use crate::app::{AgentMenu, AgentMenuItem, PaneMenuItem, WsMenuItem};
use crate::i18n::Catalog;
use ratatui::widgets::{Borders, Clear};
struct MenuRow {
text: String,
divider: bool,
destructive: bool,
}
fn render_popup(
f: &mut RenderTarget,
area: Rect,
anchor: (u16, u16),
rows: &[MenuRow],
hover: Option<(u16, u16)>,
t: &Theme,
) -> Vec<Rect> {
let (ax, ay) = anchor;
let label_w = rows
.iter()
.map(|r| super::display_width(&r.text))
.max()
.unwrap_or(6) as u16;
let w = (label_w + 3).clamp(12, area.width.max(1));
let h = (rows.len() as u16 + 2).min(area.height.max(1));
let x = ax.min(area.right().saturating_sub(w)).max(area.x);
let y = ay.min(area.bottom().saturating_sub(h)).max(area.y);
let popup = Rect::new(x, y, w, h);
f.render_widget(Clear, popup);
let block = Block::new()
.borders(Borders::ALL)
.border_style(Style::new().fg(t.border_focus).bg(t.surface0))
.style(Style::new().bg(t.surface0));
let inner = block.inner(popup);
f.render_widget(block, popup);
let mut rects = Vec::with_capacity(rows.len());
for (i, r) in rows.iter().enumerate() {
let row = Rect::new(inner.x, inner.y + i as u16, inner.width, 1);
if r.divider {
let line = "─".repeat(inner.width as usize);
f.render_widget(
Paragraph::new(Span::styled(
line,
Style::new().fg(t.surface1).bg(t.surface0),
)),
row,
);
rects.push(row);
continue;
}
let hot = hover.is_some_and(|(c, hr)| c >= row.x && c < row.right() && hr == row.y);
let fg = if hot {
t.crust
} else if r.destructive {
t.coral } else {
t.text
};
let bg = if hot { t.accent } else { t.surface0 };
f.render_widget(
Paragraph::new(Span::styled(
format!(" {}", r.text),
Style::new().fg(fg).bg(bg),
)),
row,
);
rects.push(row);
}
rects
}
pub(super) fn draw_ws_menu(
f: &mut RenderTarget,
area: Rect,
app: &mut App,
cat: &Catalog,
t: &Theme,
) {
let Some(menu) = app.ws_menu.as_ref() else {
return;
};
let anchor = menu.anchor;
let items = app.ws_menu_items(menu.index);
let rows: Vec<MenuRow> = items
.iter()
.map(|it| MenuRow {
text: ws_label(*it, cat),
divider: matches!(it, WsMenuItem::Divider),
destructive: matches!(it, WsMenuItem::Close),
})
.collect();
let rects = render_popup(f, area, anchor, &rows, app.hover, t);
if let Some(menu) = app.ws_menu.as_mut() {
menu.items = items.into_iter().zip(rects).collect();
}
}
pub(super) fn draw_pane_menu(
f: &mut RenderTarget,
area: Rect,
app: &mut App,
cat: &Catalog,
t: &Theme,
) {
let Some(menu) = app.pane_menu.as_ref() else {
return;
};
let anchor = menu.anchor;
let items = PaneMenuItem::ALL.to_vec();
let rows: Vec<MenuRow> = items
.iter()
.map(|it| MenuRow {
text: pane_label(*it, cat),
divider: matches!(it, PaneMenuItem::Divider),
destructive: matches!(it, PaneMenuItem::Close),
})
.collect();
let rects = render_popup(f, area, anchor, &rows, app.hover, t);
if let Some(menu) = app.pane_menu.as_mut() {
menu.items = items.into_iter().zip(rects).collect();
}
}
pub(super) fn draw_agent_menu(
f: &mut RenderTarget,
area: Rect,
app: &mut App,
cat: &Catalog,
t: &Theme,
) {
let Some(menu) = app.agent_menu.as_ref() else {
return;
};
let anchor = menu.anchor;
let items = AgentMenu::items_for(menu.target);
let rows: Vec<MenuRow> = items
.iter()
.map(|it| MenuRow {
text: agent_label(*it, cat),
divider: false,
destructive: matches!(it, AgentMenuItem::Close),
})
.collect();
let rects = render_popup(f, area, anchor, &rows, app.hover, t);
if let Some(menu) = app.agent_menu.as_mut() {
menu.items = items.into_iter().zip(rects).collect();
}
}
fn agent_label(it: AgentMenuItem, cat: &Catalog) -> String {
match it {
AgentMenuItem::Resume => cat.menu_resume.to_string(),
AgentMenuItem::Close => cap_first(cat.act_close),
}
}
fn ws_label(it: WsMenuItem, cat: &Catalog) -> String {
match it {
WsMenuItem::Close => cap_first(cat.act_close),
WsMenuItem::Rename => cat.menu_rename.to_string(),
WsMenuItem::NewWorktree => cat.new_git_worktree.to_string(),
WsMenuItem::OpenWorktree => cat.menu_open_worktree.to_string(),
WsMenuItem::Divider => String::new(),
WsMenuItem::OpenGit => cat.cmd_open_git.to_string(),
WsMenuItem::OpenOrch => cat.cmd_open_board.to_string(),
}
}
fn pane_label(it: PaneMenuItem, cat: &Catalog) -> String {
match it {
PaneMenuItem::SplitVertical => cat.menu_split_vertical.to_string(),
PaneMenuItem::SplitHorizontal => cat.menu_split_horizontal.to_string(),
PaneMenuItem::RunningCmd => cat.menu_running_cmd.to_string(),
PaneMenuItem::Divider => String::new(),
PaneMenuItem::Close => cap_first(cat.act_close),
}
}
fn cap_first(s: &str) -> String {
let mut c = s.chars();
match c.next() {
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
None => String::new(),
}
}