use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ Animation, AnimationExt as _, App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, Window, deferred, div, ease_out_quint, px, }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::{ActiveTheme as _, Size}; use crate::common::DismissHandler; use crate::common::v_flex; /// How alarming an [`Alert`] is; picks the icon and the accent color. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum AlertSeverity { /// The default; uses the info role. #[default] Info, /// Uses the success role. Success, /// Uses the warning role. Warning, /// Uses the destructive role. Danger, } /// A static callout banner. #[derive(Clone, IntoElement)] pub struct Alert { id: ElementId, title: Option, message: Option, severity: AlertSeverity, icon: Option, show_icon: bool, on_dismiss: Option, } impl Alert { pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), title: None, message: None, severity: AlertSeverity::Info, icon: None, show_icon: true, on_dismiss: None, } } /// Sets the primary line. pub fn title(mut self, title: L) -> Self where L: Into, { self.title = Some(title.into()); self } /// Sets a secondary line under the title. pub fn message(mut self, message: L) -> Self where L: Into, { self.message = Some(message.into()); self } /// Sets how alarming the alert is. pub fn severity(mut self, severity: AlertSeverity) -> Self { self.severity = severity; self } /// Overrides the severity-derived icon with this glyph. pub fn icon(mut self, role: IconRole) -> Self { self.icon = Some(role); self } /// Hides the icon entirely. pub fn no_icon(mut self) -> Self { self.show_icon = false; self } /// Runs when the user dismisses the alert. pub fn on_dismiss(mut self, handler: F) -> Self where F: Fn(&mut App) + 'static, { self.on_dismiss = Some(Rc::new(handler)); self } fn rekey(mut self, id: usize) -> Self { self.id = ElementId::from(id); self } } impl RenderOnce for Alert { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let ramp = match self.severity { AlertSeverity::Info => theme.ramps.info, AlertSeverity::Success => theme.ramps.success, AlertSeverity::Warning => theme.ramps.warning, AlertSeverity::Danger => theme.ramps.destructive, }; let icon_role = self.icon.unwrap_or(match self.severity { AlertSeverity::Info => IconRole::Info, AlertSeverity::Success => IconRole::Check, AlertSeverity::Warning | AlertSeverity::Danger => IconRole::Warning, }); let foreground = theme.colors.foreground; let muted_foreground = theme.colors.muted_foreground; let muted = theme.colors.muted; let surface = theme.colors.surface; let border = theme.colors.border; let shadows = theme.elevations.popover.shadows(); let metrics = theme.sizes.resolve(Size::Medium); let icon_size = metrics.icon_size; let title_size = metrics.text_size; let caption_size = metrics.caption_size; div() .w_full() .flex() .items_start() .gap_2() .px_3() .py_2() .rounded(theme.radius.overlay) .bg(surface) .border_1() .border_color(border) .shadow(shadows) .when(self.show_icon, |alert| { alert.child( div() .flex_none() .flex() .items_center() .h(window.line_height()) .child(icon(icon_role).size(icon_size).color(ramp.base)), ) }) .child( v_flex() .flex_1() .gap_0p5() .when_some(self.title.clone(), |alert, title| { alert.child( div() .text_size(title_size) .text_color(foreground) .when(self.message.is_some(), |title| { title.font_weight(gpui::FontWeight::SEMIBOLD) }) .child(title), ) }) .when_some(self.message.clone(), |alert, message| { let color = if self.title.is_some() { muted_foreground } else { foreground }; alert.child( div() .text_size(caption_size) .text_color(color) .child(message), ) }), ) .when_some(self.on_dismiss, |alert, on_dismiss| { alert.child( div() .id((self.id, "alert-close")) .flex_none() .cursor_pointer() .p_0p5() .rounded(theme.radius.sm) .hover(|style| style.bg(muted)) .child( icon(IconRole::Close) .size(theme.sizes.small.dense_icon_size) .color(theme.colors.muted_foreground), ) .on_click(move |_, _window, cx| on_dismiss(cx)), ) }) } } /// One pushed alert, with its identity in the stack. #[derive(Clone)] pub struct AlertEntry { pub id: usize, pub alert: Alert, on_dismiss: Option, } /// The stack of pushed alerts. #[derive(Default)] pub struct Alerts { entries: Vec, next_id: usize, } impl Alerts { /// Pushes an alert onto the stack. pub fn push(&mut self, mut alert: Alert, cx: &mut Context) -> usize { let id = self.next_id; self.next_id += 1; let on_dismiss = alert.on_dismiss.take(); let stack = cx.entity().downgrade(); alert.on_dismiss = Some(Rc::new(move |cx: &mut App| { stack.update(cx, |alerts, cx| alerts.dismiss(id, cx)).ok(); })); self.entries.push(AlertEntry { id, alert: alert.rekey(id), on_dismiss, }); cx.notify(); id } /// Dismisses one alert by id, running its dismiss handler. pub fn dismiss(&mut self, id: usize, cx: &mut Context) { if let Some(position) = self.entries.iter().position(|entry| entry.id == id) { let entry = self.entries.remove(position); cx.notify(); if let Some(on_dismiss) = &entry.on_dismiss { (on_dismiss.clone())(cx); } } } /// Dismisses every alert. pub fn clear(&mut self, cx: &mut Context) { if !self.entries.is_empty() { self.entries.clear(); cx.notify(); } } /// The stacked alerts, oldest first. pub fn entries(&self) -> &[AlertEntry] { &self.entries } /// How many alerts are stacked. pub fn count(&self) -> usize { self.entries.len() } } /// The floating stack of pushed alerts, topmost right. #[derive(IntoElement)] pub struct AlertsView { alerts: Entity, id: ElementId, } impl AlertsView { /// Binds the view to an alert stack. pub fn new(id: I, alerts: Entity) -> Self where I: Into, { Self { id: id.into(), alerts, } } } impl RenderOnce for AlertsView { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let enter_duration = theme.motion.overlay; let entries: Vec = self.alerts.read(cx).entries().to_vec(); deferred( div() .id(self.id) .absolute() .top_4() .right_4() .w_80() .flex() .flex_col() .items_end() .gap_2() .children(entries.iter().rev().map(|entry| { let id = entry.id; let ease_slide = ease_out_quint(); div() .relative() .w_full() .child(entry.alert.clone()) .with_animation( ("alert-enter", id), Animation::new(enter_duration), move |layer, delta| layer.left(px(24. * (1. - ease_slide(delta)))), ) })), ) } }