use crate::{icons, popover};
use gpui::{Context, SharedString, Window, div, prelude::*, px};
use motion::{Fade, Painter};
use std::rc::Rc;
use theme::{TextStyle, Theme, Typeset};
const GLYPH: f32 = 13.0;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Item {
Action {
label: SharedString,
icon: Option<SharedString>,
keystroke: Option<SharedString>,
checked: bool,
enabled: bool,
},
Separator,
}
impl Item {
pub fn action(label: impl Into<SharedString>) -> Self {
Item::Action {
label: label.into(),
icon: None,
keystroke: None,
checked: false,
enabled: true,
}
}
pub fn with_icon(self, icon: impl Into<SharedString>) -> Self {
match self {
Item::Action {
label,
keystroke,
checked,
enabled,
..
} => Item::Action {
label,
icon: Some(icon.into()),
keystroke,
checked,
enabled,
},
Item::Separator => Item::Separator,
}
}
pub fn with_keystroke(self, keystroke: impl Into<SharedString>) -> Self {
match self {
Item::Action {
label,
icon,
checked,
enabled,
..
} => Item::Action {
label,
icon,
keystroke: Some(keystroke.into()),
checked,
enabled,
},
Item::Separator => Item::Separator,
}
}
pub fn checked(self, checked: bool) -> Self {
match self {
Item::Action {
label,
icon,
keystroke,
enabled,
..
} => Item::Action {
label,
icon,
keystroke,
checked,
enabled,
},
Item::Separator => Item::Separator,
}
}
pub fn disabled(self) -> Self {
match self {
Item::Action {
label,
icon,
keystroke,
checked,
..
} => Item::Action {
label,
icon,
keystroke,
checked,
enabled: false,
},
Item::Separator => Item::Separator,
}
}
pub fn selectable(&self) -> bool {
matches!(self, Item::Action { enabled: true, .. })
}
fn has_icon(&self) -> bool {
matches!(self, Item::Action { icon: Some(_), .. })
}
}
pub fn next_selectable(items: &[Item], from: Option<usize>, delta: isize) -> Option<usize> {
let count = items.len();
if count == 0 {
return None;
}
let step = if delta >= 0 { 1 } else { -1 };
let wrap = |at: usize| (at as isize + step).rem_euclid(count as isize) as usize;
let mut at = match from {
None if step > 0 => 0,
None => count - 1,
Some(at) => wrap(at.min(count - 1)),
};
for _ in 0..count {
if items[at].selectable() {
return Some(at);
}
at = wrap(at);
}
None
}
pub fn card<V: 'static>(
theme: &Theme,
id: impl Into<SharedString>,
items: &[Item],
highlighted: Option<usize>,
cx: &mut Context<V>,
choose: impl Fn(&mut V, usize, &mut Window, &mut Context<V>) + 'static,
) -> gpui::Div {
let id = id.into();
let painter = Painter::of(cx);
let gutter = items.iter().any(Item::has_icon);
let choose = Rc::new(choose);
popover::popover_card(theme)
.min_w(px(180.0))
.children(items.iter().enumerate().map(|(index, item)| {
let Item::Action {
label,
icon,
keystroke,
checked,
enabled,
} = item
else {
return popover::divider().into_any_element();
};
let row = if *enabled {
let choose = choose.clone();
popover::menu_row(
theme,
highlighted == Some(index),
Some(Fade::new(painter, format!("{id}-{index}"))),
)
.id(SharedString::from(format!("{id}-{index}")))
.on_click(cx.listener(move |view, _, window, cx| choose(view, index, window, cx)))
} else {
disabled_row(theme).id(SharedString::from(format!("{id}-{index}")))
};
row.when(gutter, |row| {
row.child(glyph_slot(theme, icon.clone(), *enabled))
})
.child(div().flex_1().min_w_0().child(label.clone()))
.when(*checked, |row| {
row.child(
icons::icon(icons::status::CHECK)
.size(px(GLYPH))
.text_color(theme.text),
)
})
.when_some(keystroke.clone(), |row, keystroke| {
row.child(popover::kbd_hint(theme, &keystroke))
})
.into_any_element()
}))
}
fn glyph_slot(theme: &Theme, icon: Option<SharedString>, enabled: bool) -> gpui::Div {
div()
.flex_none()
.size(px(GLYPH))
.flex()
.items_center()
.justify_center()
.children(icon.map(|path| {
gpui::svg()
.path(path)
.size(px(GLYPH))
.text_color(if enabled {
theme.text_faint
} else {
theme.text_faint.opacity(0.5)
})
}))
}
fn disabled_row(theme: &Theme) -> gpui::Div {
div()
.flex()
.flex_row()
.items_center()
.gap(px(10.0))
.px(px(8.0))
.py(px(6.0))
.rounded(px(Theme::inset_radius(
Theme::surface_radius(),
popover::MENU_PAD,
)))
.text_style(TextStyle::Body)
.text_color(theme.text_faint)
}