mod anchor;
mod modal;
mod nav;
mod parts;
pub mod submenu;
pub use anchor::*;
pub use modal::*;
pub use nav::*;
pub use parts::*;
pub use submenu::Chain;
use crate::{icons, stack};
use gpui::{
Anchor, AnyElement, ElementId, IntoElement, Pixels, Point, SharedString, div, prelude::*, px,
};
use icons::Icon;
use motion::{self as motion, AnimationExt as _, Fade, PULSE, Painter};
use theme::{TextStyle, Theme, Typeset, ink};
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Loadable<T> {
#[default]
Idle,
Loading,
Ready(T),
Error(String),
}
impl<T> Loadable<T> {
pub fn ready(&self) -> Option<&T> {
match self {
Loadable::Ready(value) => Some(value),
_ => None,
}
}
pub fn is_loading(&self) -> bool {
matches!(self, Loadable::Loading)
}
pub fn error(&self) -> Option<&str> {
match self {
Loadable::Error(message) => Some(message),
_ => None,
}
}
}
pub struct Popup<T> {
inner: Option<(T, Option<web_time::Instant>)>,
pressed_while_open: bool,
generation: u64,
}
impl<T> Default for Popup<T> {
fn default() -> Self {
Self {
inner: None,
pressed_while_open: false,
generation: 0,
}
}
}
impl<T> Popup<T> {
pub fn open(&mut self, value: T) {
self.generation = self.generation.wrapping_add(1);
self.inner = Some((value, None));
}
pub fn is_open(&self) -> bool {
matches!(self.inner, Some((_, None)))
}
pub fn is_closing(&self) -> bool {
matches!(self.inner, Some((_, Some(_))))
}
pub fn closing_since(&self) -> Option<web_time::Instant> {
match &self.inner {
Some((_, Some(since))) => Some(*since),
_ => None,
}
}
pub fn get(&self) -> Option<&T> {
self.inner.as_ref().map(|(value, _)| value)
}
pub fn as_open(&self) -> Option<&T> {
match &self.inner {
Some((value, None)) => Some(value),
_ => None,
}
}
pub fn open_mut(&mut self) -> Option<&mut T> {
match &mut self.inner {
Some((value, None)) => Some(value),
_ => None,
}
}
pub fn close(&mut self) {
self.inner = None;
}
pub fn begin_close(&mut self) -> bool {
match &mut self.inner {
Some((_, closing @ None)) => {
self.generation = self.generation.wrapping_add(1);
*closing = Some(web_time::Instant::now());
true
}
_ => false,
}
}
pub fn note_trigger_press(&mut self) {
self.note_trigger_press_matching(|_| true);
}
pub fn note_trigger_press_matching(&mut self, owns: impl FnOnce(&T) -> bool) {
self.pressed_while_open = self.inner.as_ref().is_some_and(|(value, _)| owns(value));
}
pub fn take_press_was_open(&mut self) -> bool {
std::mem::take(&mut self.pressed_while_open)
}
pub fn finish_close(&mut self) {
if matches!(&self.inner, Some((_, Some(_)))) {
self.inner = None;
}
}
}
pub fn reap_popup<V: 'static, T: 'static>(
view: &mut V,
cx: &mut gpui::Context<V>,
popup: impl Fn(&mut V) -> &mut Popup<T> + 'static,
) {
let generation = popup(view).generation;
cx.spawn(async move |view, cx| {
cx.background_executor()
.timer(
motion::MENU_OUT
.total()
.mul_f32(motion::speed_scale())
.saturating_add(std::time::Duration::from_millis(20)),
)
.await;
view.update(cx, |view, cx| {
let popup = popup(view);
if popup.generation == generation && popup.is_closing() {
popup.finish_close();
cx.notify();
}
})
.ok();
})
.detach();
}
pub fn close_popup<V: 'static, T: 'static>(
view: &mut V,
cx: &mut gpui::Context<V>,
popup: impl Fn(&mut V) -> &mut Popup<T> + Copy + 'static,
) {
if popup(view).begin_close() {
reap_popup(view, cx, popup);
cx.notify();
}
}
pub fn dismiss_on_out<V: 'static, T: 'static, E: gpui::InteractiveElement>(
el: E,
popup: impl Fn(&mut V) -> &mut Popup<T> + Copy + 'static,
cx: &gpui::Context<V>,
) -> E {
el.on_mouse_down_out(cx.listener(move |view, _: &gpui::MouseDownEvent, _, cx| {
close_popup(view, cx, popup);
cx.stop_propagation();
}))
}
pub fn menu_trigger<V: 'static, T: 'static, E: gpui::StatefulInteractiveElement>(
el: E,
popup: impl Fn(&mut V) -> &mut Popup<T> + Copy + 'static,
value: impl Fn(&gpui::ClickEvent) -> T + 'static,
cx: &gpui::Context<V>,
) -> E {
menu_trigger_matching(el, popup, |_| true, value, cx)
}
pub fn menu_trigger_matching<V: 'static, T: 'static, E: gpui::StatefulInteractiveElement>(
el: E,
popup: impl Fn(&mut V) -> &mut Popup<T> + Copy + 'static,
owns: impl Fn(&T) -> bool + 'static,
value: impl Fn(&gpui::ClickEvent) -> T + 'static,
cx: &gpui::Context<V>,
) -> E {
trigger_press_matching(el, popup, owns, cx).on_click(cx.listener(
move |view, event: &gpui::ClickEvent, _, cx| {
if popup(view).take_press_was_open() {
close_popup(view, cx, popup);
} else {
popup(view).open(value(event));
}
cx.notify();
},
))
}
pub fn trigger_press<V: 'static, T: 'static, E: gpui::InteractiveElement>(
el: E,
popup: impl Fn(&mut V) -> &mut Popup<T> + Copy + 'static,
cx: &gpui::Context<V>,
) -> E {
trigger_press_matching(el, popup, |_| true, cx)
}
pub fn trigger_press_matching<V: 'static, T: 'static, E: gpui::InteractiveElement>(
el: E,
popup: impl Fn(&mut V) -> &mut Popup<T> + Copy + 'static,
owns: impl Fn(&T) -> bool + 'static,
cx: &gpui::Context<V>,
) -> E {
el.capture_any_mouse_down(cx.listener(move |view, _: &gpui::MouseDownEvent, _, _| {
popup(view).note_trigger_press_matching(&owns);
}))
}
pub(crate) const MENU_PAD: f32 = 4.0;
pub(crate) const SNAP: f32 = 8.0;
pub const MENU_ROW_INSET: f32 = 8.0;
pub(crate) const MENU_ROW_PAD_Y: f32 = 6.0;
pub(crate) fn menu_row_height() -> f32 {
TextStyle::Body.painted_line_height() + 2.0 * MENU_ROW_PAD_Y
}
pub fn popover_card(theme: &Theme) -> gpui::Div {
let card = div()
.rounded(px(Theme::surface_radius()))
.p(px(MENU_PAD))
.overflow_hidden()
.text_style(TextStyle::Body)
.text_color(theme.text);
if theme.glass {
card
} else {
card.bg(theme.surface_overlay)
.border_1()
.border_color(theme.border)
.shadow_lg()
}
}
pub fn popover_card_flush(theme: &Theme) -> gpui::Div {
popover_card(theme).p(px(0.0))
}