Skip to main content

ui/components/
action_panel.rs

1use gpui::{AnyElement, FontWeight, IntoElement, ParentElement, Styled};
2use std::rc::Rc;
3
4use crate::prelude::*;
5
6/// A bordered, `semantic::elevated_surface`-backed section with a fieldset
7/// area (arbitrary child fields, e.g. `FormField`s) followed by a `border-t`
8/// footer with right-aligned Save/Cancel `Button`s.
9#[derive(IntoElement, RegisterComponent)]
10pub struct ActionPanel {
11    title: Option<SharedString>,
12    description: Option<SharedString>,
13    fields: Vec<AnyElement>,
14    save_label: SharedString,
15    cancel_label: SharedString,
16    on_save: Option<Rc<dyn Fn(&mut Window, &mut App) + 'static>>,
17    on_cancel: Option<Rc<dyn Fn(&mut Window, &mut App) + 'static>>,
18}
19
20impl ActionPanel {
21    pub fn new() -> Self {
22        Self {
23            title: None,
24            description: None,
25            fields: Vec::new(),
26            save_label: "Save".into(),
27            cancel_label: "Cancel".into(),
28            on_save: None,
29            on_cancel: None,
30        }
31    }
32
33    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
34        self.title = Some(title.into());
35        self
36    }
37
38    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
39        self.description = Some(description.into());
40        self
41    }
42
43    /// Adds a field element to the fieldset area (e.g. a `FormField`).
44    pub fn field(mut self, field: impl IntoElement) -> Self {
45        self.fields.push(field.into_any_element());
46        self
47    }
48
49    pub fn save_label(mut self, label: impl Into<SharedString>) -> Self {
50        self.save_label = label.into();
51        self
52    }
53
54    pub fn cancel_label(mut self, label: impl Into<SharedString>) -> Self {
55        self.cancel_label = label.into();
56        self
57    }
58
59    pub fn on_save(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
60        self.on_save = Some(Rc::new(handler));
61        self
62    }
63
64    pub fn on_cancel(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
65        self.on_cancel = Some(Rc::new(handler));
66        self
67    }
68}
69
70impl Default for ActionPanel {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl RenderOnce for ActionPanel {
77    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
78        let title = self.title;
79        let description = self.description;
80        let fields = self.fields;
81        let save_label = self.save_label;
82        let cancel_label = self.cancel_label;
83        let on_save = self.on_save;
84        let on_cancel = self.on_cancel;
85
86        let mut fieldset = v_flex().gap_4().p_4();
87        for field in fields {
88            fieldset = fieldset.child(field);
89        }
90
91        v_flex()
92            .w_full()
93            .rounded_lg()
94            .border_1()
95            .border_color(semantic::border(cx))
96            .bg(semantic::elevated_surface(cx))
97            .overflow_hidden()
98            .when(title.is_some() || description.is_some(), |this| {
99                this.child(
100                    v_flex()
101                        .gap_1()
102                        .px_4()
103                        .pt_4()
104                        .when_some(title, |this, title| {
105                            this.child(
106                                Label::new(title)
107                                    .size(LabelSize::Default)
108                                    .weight(FontWeight::MEDIUM),
109                            )
110                        })
111                        .when_some(description, |this, description| {
112                            this.child(
113                                Label::new(description)
114                                    .size(LabelSize::Small)
115                                    .color(Color::Muted),
116                            )
117                        }),
118                )
119            })
120            .child(fieldset)
121            .child(
122                h_flex()
123                    .justify_end()
124                    .gap_2()
125                    .px_4()
126                    .py_3()
127                    .border_t_1()
128                    .border_color(semantic::border(cx))
129                    .when_some(on_cancel, |this, handler| {
130                        this.child(
131                            // Wrapping `div` only exists so integration tests can
132                            // locate the Cancel button's real rendered pixel bounds
133                            // via `VisualTestContext::debug_bounds` (test-only,
134                            // no-op in release builds — mirrors the `Tab`/
135                            // `ContextMenu` `debug_selector` precedent) and drive a
136                            // genuine `simulate_click`, instead of invoking
137                            // `on_cancel` directly. It does not intercept the click:
138                            // the inner `Button` still owns the only click handler.
139                            div()
140                                .id("action-panel-cancel-wrap")
141                                .debug_selector(|| "ACTION_PANEL-cancel".into())
142                                .child(
143                                    Button::new("action-panel-cancel", cancel_label)
144                                        .on_click(move |_, window, cx| handler(window, cx)),
145                                ),
146                        )
147                    })
148                    .when_some(on_save, |this, handler| {
149                        this.child(
150                            // See the Cancel wrapper's comment above; same rationale.
151                            div()
152                                .id("action-panel-save-wrap")
153                                .debug_selector(|| "ACTION_PANEL-save".into())
154                                .child(
155                                    Button::new("action-panel-save", save_label)
156                                        .primary()
157                                        .on_click(move |_, window, cx| handler(window, cx)),
158                                ),
159                        )
160                    }),
161            )
162    }
163}
164
165impl Component for ActionPanel {
166    fn scope() -> ComponentScope {
167        ComponentScope::Input
168    }
169
170    fn description() -> Option<&'static str> {
171        Some("A fieldset section with a footer of right-aligned Save/Cancel buttons.")
172    }
173
174    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
175        Some(
176            ActionPanel::new()
177                .title("Profile")
178                .description("Update your personal details.")
179                .field(
180                    FormField::new(Label::new("Ada Lovelace").color(Color::Default)).label("Name"),
181                )
182                .field(
183                    FormField::new(Label::new("ada@example.com").color(Color::Default))
184                        .label("Email"),
185                )
186                .on_save(|_, _| {})
187                .on_cancel(|_, _| {})
188                .into_any_element(),
189        )
190    }
191}