alert_modal.rs

  1use crate::component_prelude::*;
  2use crate::prelude::*;
  3use crate::{Checkbox, ListBulletItem, ToggleState};
  4use gpui::Action;
  5use gpui::FocusHandle;
  6use gpui::IntoElement;
  7use gpui::Stateful;
  8use smallvec::{SmallVec, smallvec};
  9use theme::ActiveTheme;
 10
 11type ActionHandler = Box<dyn FnOnce(Stateful<Div>) -> Stateful<Div>>;
 12
 13#[derive(IntoElement, RegisterComponent)]
 14pub struct AlertModal {
 15    id: ElementId,
 16    header: Option<AnyElement>,
 17    children: SmallVec<[AnyElement; 2]>,
 18    footer: Option<AnyElement>,
 19    title: Option<SharedString>,
 20    primary_action: Option<SharedString>,
 21    dismiss_label: Option<SharedString>,
 22    width: Option<DefiniteLength>,
 23    key_context: Option<String>,
 24    action_handlers: Vec<ActionHandler>,
 25    focus_handle: Option<FocusHandle>,
 26}
 27
 28impl AlertModal {
 29    pub fn new(id: impl Into<ElementId>) -> Self {
 30        Self {
 31            id: id.into(),
 32            header: None,
 33            children: smallvec![],
 34            footer: None,
 35            title: None,
 36            primary_action: None,
 37            dismiss_label: None,
 38            width: None,
 39            key_context: None,
 40            action_handlers: Vec::new(),
 41            focus_handle: None,
 42        }
 43    }
 44
 45    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
 46        self.title = Some(title.into());
 47        self
 48    }
 49
 50    pub fn header(mut self, header: impl IntoElement) -> Self {
 51        self.header = Some(header.into_any_element());
 52        self
 53    }
 54
 55    pub fn footer(mut self, footer: impl IntoElement) -> Self {
 56        self.footer = Some(footer.into_any_element());
 57        self
 58    }
 59
 60    pub fn primary_action(mut self, primary_action: impl Into<SharedString>) -> Self {
 61        self.primary_action = Some(primary_action.into());
 62        self
 63    }
 64
 65    pub fn dismiss_label(mut self, dismiss_label: impl Into<SharedString>) -> Self {
 66        self.dismiss_label = Some(dismiss_label.into());
 67        self
 68    }
 69
 70    pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
 71        self.width = Some(width.into());
 72        self
 73    }
 74
 75    pub fn key_context(mut self, key_context: impl Into<String>) -> Self {
 76        self.key_context = Some(key_context.into());
 77        self
 78    }
 79
 80    pub fn on_action<A: Action>(
 81        mut self,
 82        listener: impl Fn(&A, &mut Window, &mut App) + 'static,
 83    ) -> Self {
 84        self.action_handlers
 85            .push(Box::new(move |div| div.on_action(listener)));
 86        self
 87    }
 88
 89    pub fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
 90        self.focus_handle = Some(focus_handle.clone());
 91        self
 92    }
 93}
 94
 95impl RenderOnce for AlertModal {
 96    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 97        let width = self.width.unwrap_or_else(|| px(440.).into());
 98        let has_default_footer = self.primary_action.is_some() || self.dismiss_label.is_some();
 99
100        let mut modal = v_flex()
101            .when_some(self.key_context, |this, key_context| {
102                this.key_context(key_context.as_str())
103            })
104            .when_some(self.focus_handle, |this, focus_handle| {
105                this.track_focus(&focus_handle)
106            })
107            .id(self.id)
108            .elevation_3(cx)
109            .w(width)
110            .bg(cx.theme().colors().elevated_surface_background)
111            .overflow_hidden();
112
113        for handler in self.action_handlers {
114            modal = handler(modal);
115        }
116
117        if let Some(header) = self.header {
118            modal = modal.child(header);
119        } else if let Some(title) = self.title {
120            modal = modal.child(
121                v_flex()
122                    .pt_3()
123                    .pr_3()
124                    .pl_3()
125                    .pb_1()
126                    .child(Headline::new(title).size(HeadlineSize::Small)),
127            );
128        }
129
130        if !self.children.is_empty() {
131            modal = modal.child(
132                v_flex()
133                    .p_3()
134                    .text_ui(cx)
135                    .text_color(Color::Muted.color(cx))
136                    .gap_1()
137                    .children(self.children),
138            );
139        }
140
141        if let Some(footer) = self.footer {
142            modal = modal.child(footer);
143        } else if has_default_footer {
144            let primary_action = self.primary_action.unwrap_or_else(|| "Ok".into());
145            let dismiss_label = self.dismiss_label.unwrap_or_else(|| "Cancel".into());
146
147            modal = modal.child(
148                h_flex()
149                    .p_3()
150                    .items_center()
151                    .justify_end()
152                    .gap_1()
153                    .child(Button::new(dismiss_label.clone(), dismiss_label).color(Color::Muted))
154                    .child(Button::new(primary_action.clone(), primary_action)),
155            );
156        }
157
158        modal
159    }
160}
161
162impl ParentElement for AlertModal {
163    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
164        self.children.extend(elements)
165    }
166}
167
168impl Component for AlertModal {
169    fn scope() -> ComponentScope {
170        ComponentScope::Notification
171    }
172
173    fn status() -> ComponentStatus {
174        ComponentStatus::WorkInProgress
175    }
176
177    fn description() -> Option<&'static str> {
178        Some("A modal dialog that presents an alert message with primary and dismiss actions.")
179    }
180
181    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
182        Some(
183            v_flex()
184                .gap_6()
185                .p_4()
186                .children(vec![
187                    example_group(vec![single_example(
188                        "Basic Alert",
189                        AlertModal::new("simple-modal")
190                            .title("Do you want to leave the current call?")
191                            .child(
192                                "The current window will be closed, and connections to any shared projects will be terminated."
193                            )
194                            .primary_action("Leave Call")
195                            .dismiss_label("Cancel")
196                            .into_any_element(),
197                    )]),
198                    example_group(vec![single_example(
199                        "Custom Header",
200                        AlertModal::new("custom-header-modal")
201                            .header(
202                                v_flex()
203                                    .p_3()
204                                    .bg(cx.theme().colors().background)
205                                    .gap_1()
206                                    .child(
207                                        h_flex()
208                                            .gap_1()
209                                            .child(Icon::new(IconName::Warning).color(Color::Warning))
210                                            .child(Headline::new("Unrecognized Workspace").size(HeadlineSize::Small))
211                                    )
212                                    .child(
213                                        h_flex()
214                                            .pl(IconSize::default().rems() + rems(0.5))
215                                            .child(Label::new("~/projects/my-project").color(Color::Muted))
216                                    )
217                            )
218                            .child(
219                                "Untrusted workspaces are opened in Restricted Mode to protect your system.
220Review .zed/settings.json for any extensions or commands configured by this project.",
221                            )
222                            .child(
223                                v_flex()
224                                    .mt_1()
225                                    .child(Label::new("Restricted mode prevents:").color(Color::Muted))
226                                    .child(ListBulletItem::new("Project settings from being applied"))
227                                    .child(ListBulletItem::new("Language servers from running"))
228                                    .child(ListBulletItem::new("MCP integrations from installing"))
229                            )
230                            .footer(
231                                h_flex()
232                                    .p_3()
233                                    .justify_between()
234                                    .child(
235                                        Checkbox::new("trust-parent", ToggleState::Unselected)
236                                            .label("Trust all projects in parent directory")
237                                    )
238                                    .child(
239                                        h_flex()
240                                            .gap_1()
241                                            .child(Button::new("restricted", "Stay in Restricted Mode").color(Color::Muted))
242                                            .child(Button::new("trust", "Trust and Continue").style(ButtonStyle::Filled))
243                                    )
244                            )
245                            .width(rems(40.))
246                            .into_any_element(),
247                    )]),
248                ])
249                .into_any_element(),
250        )
251    }
252}