relm4_components/alert/
mod.rs

1//! Reusable and easily configurable alert component.
2//!
3//! **[Example implementation](https://github.com/AaronErhardt/relm4/blob/main/relm4-examples/examples/alert.rs)**
4
5use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt, WidgetExt};
6use once_cell::sync::Lazy;
7use relm4::{Component, ComponentParts, ComponentSender, RelmWidgetExt, gtk};
8
9const LIBADWAITA_ENABLED: bool = cfg!(feature = "libadwaita");
10const COMPONENT_CSS: &str = include_str!("style.css");
11const MESSAGE_AREA_CSS: &str = "message-area";
12const RESPONSE_BUTTONS_CSS: &str = "response-buttons";
13
14/// The initializer for the CSS, ensuring it only happens once.
15static INITIALIZE_CSS: Lazy<()> = Lazy::new(|| {
16    relm4::set_global_css_with_priority(COMPONENT_CSS, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
17});
18
19/// Configuration for the alert dialog component
20///
21/// The configuration object provides a [`Default`] implementation for any fields you don't want to manually specify, which is configured as such:
22///
23/// - `text` is set to "Alert".
24/// - `secondary_text` is set to [`None`].
25/// - `is_modal` is set to [`true`].
26/// - `destructive_accept` is set to [`false`].
27/// - `confirm_label` is set to [`None`].
28/// - `cancel_label` is set to [`None`].
29/// - `option_label` is set to [`None`].
30/// - `extra_child` is set to [`None`].
31#[derive(Debug)]
32pub struct AlertSettings {
33    /// Large text
34    pub text: Option<String>,
35    /// Optional secondary, smaller text
36    pub secondary_text: Option<String>,
37    /// Modal dialogs freeze other windows as long they are visible
38    pub is_modal: bool,
39    /// Sets color of the accept button to red if the theme supports it
40    pub destructive_accept: bool,
41    /// Text for confirm button. If [`None`] the button won't be shown.
42    pub confirm_label: Option<String>,
43    /// Text for cancel button. If [`None`] the button won't be shown.
44    pub cancel_label: Option<String>,
45    /// Text for third option button. If [`None`] the button won't be shown.
46    pub option_label: Option<String>,
47    /// An optional, extra widget to display below the secondary text.
48    pub extra_child: Option<gtk::Widget>,
49}
50
51impl Default for AlertSettings {
52    fn default() -> Self {
53        Self {
54            text: Some("Alert".into()),
55            secondary_text: None,
56            is_modal: true,
57            destructive_accept: false,
58            confirm_label: None,
59            cancel_label: None,
60            option_label: None,
61            extra_child: None,
62        }
63    }
64}
65
66/// Alert dialog component.
67#[derive(Debug)]
68pub struct Alert {
69    /// The settings used by the alert component.
70    pub settings: AlertSettings,
71    is_active: bool,
72    current_child: Option<gtk::Widget>,
73}
74
75/// Messages that can be sent to the alert dialog component
76#[derive(Debug)]
77pub enum AlertMsg {
78    /// Message sent by the parent to view the dialog
79    Show,
80
81    /// Message sent by the parent to hide the dialog
82    Hide,
83
84    #[doc(hidden)]
85    Response(AlertResponse),
86}
87
88/// User action performed on the alert dialog.
89#[derive(Debug)]
90pub enum AlertResponse {
91    /// User clicked confirm button.
92    Confirm,
93
94    /// User clicked cancel button.
95    Cancel,
96
97    /// User clicked user-supplied option.
98    Option,
99}
100
101/// Widgets of the alert dialog component.
102#[relm4::component(pub)]
103impl Component for Alert {
104    type Init = AlertSettings;
105    type Input = AlertMsg;
106    type Output = AlertResponse;
107    type CommandOutput = ();
108
109    view! {
110        gtk::Window {
111            #[watch]
112            set_visible: model.is_active,
113            add_css_class: "relm4-alert",
114
115            #[wrap(Some)]
116            set_titlebar = &gtk::Box {
117                set_visible: false,
118            },
119
120            gtk::Box {
121                set_orientation: gtk::Orientation::Vertical,
122
123                #[name(message_area)]
124                gtk::Box {
125                    set_orientation: gtk::Orientation::Vertical,
126                    set_spacing: 8,
127                    set_vexpand: true,
128                    add_css_class: MESSAGE_AREA_CSS,
129
130                    gtk::Label {
131                        #[watch]
132                        set_text: model.settings.text.as_deref().unwrap_or_default(),
133                        #[watch]
134                        set_visible: model.settings.text.is_some(),
135                        set_valign: gtk::Align::Start,
136                        set_justify: gtk::Justification::Center,
137                        add_css_class: relm4::css::TITLE_2,
138                        set_wrap: true,
139                        set_max_width_chars: 20,
140                    },
141
142                    gtk::Label {
143                        #[watch]
144                        set_text: model.settings.secondary_text.as_deref().unwrap_or_default(),
145                        set_vexpand: true,
146                        set_valign: gtk::Align::Fill,
147                        set_justify: gtk::Justification::Center,
148                        set_wrap: true,
149                        set_max_width_chars: 40,
150                    },
151                },
152
153                gtk::Box {
154                    add_css_class: RESPONSE_BUTTONS_CSS,
155                    set_orientation: gtk::Orientation::Vertical,
156                    set_vexpand_set: true,
157                    set_valign: gtk::Align::End,
158                    gtk::Separator {},
159
160                    gtk::Box {
161                        set_homogeneous: true,
162                        set_vexpand: true,
163                        set_valign: gtk::Align::End,
164
165                        // The confirm widget is a bit more complicated than the rest, since we have destructive coloring on it sometimes.
166                        //
167                        // - On GTK, we want the *background* of the button to be red.
168                        // - On Adwaita, we want the *text* of the button to be red.
169                        #[name(confirm_label)]
170                        gtk::Button {
171                            #[watch]
172                            set_visible: model.settings.confirm_label.is_some(),
173                            #[watch]
174                            set_class_active: (relm4::css::DESTRUCTIVE_ACTION, !LIBADWAITA_ENABLED && model.settings.destructive_accept),
175                            #[watch]
176                            set_class_active: (relm4::css::FLAT, LIBADWAITA_ENABLED || !model.settings.destructive_accept),
177                            set_hexpand: true,
178                            connect_clicked => AlertMsg::Response(AlertResponse::Confirm),
179
180                            gtk::Label {
181                                #[watch]
182                                set_label: model.settings.confirm_label.as_deref().unwrap_or_default(),
183                                #[watch]
184                                set_class_active: (relm4::css::ERROR, LIBADWAITA_ENABLED && model.settings.destructive_accept),
185                            }
186                        },
187
188                        gtk::Box {
189                            #[watch]
190                            set_visible: model.settings.cancel_label.is_some(),
191
192                            gtk::Separator {},
193
194                            #[name(cancel_label)]
195                            gtk::Button {
196                                #[watch]
197                                set_label: model.settings.cancel_label.as_deref().unwrap_or_default(),
198                                add_css_class: relm4::css::FLAT,
199                                set_hexpand: true,
200                                connect_clicked => AlertMsg::Response(AlertResponse::Cancel)
201                            }
202                        },
203
204                        gtk::Box {
205                            #[watch]
206                            set_visible: model.settings.option_label.is_some(),
207
208                            gtk::Separator {},
209
210                            #[name(option_label)]
211                            gtk::Button {
212                                #[watch]
213                                set_label: model.settings.option_label.as_deref().unwrap_or_default(),
214                                add_css_class: relm4::css::FLAT,
215                                set_hexpand: true,
216                                connect_clicked => AlertMsg::Response(AlertResponse::Option)
217                            }
218                        }
219                    }
220                }
221            }
222        }
223    }
224
225    fn init(
226        settings: AlertSettings,
227        root: Self::Root,
228        sender: ComponentSender<Self>,
229    ) -> ComponentParts<Self> {
230        // Initialize the CSS.
231        #[allow(clippy::no_effect)] // Fixes a false positive in Rust < 1.78
232        *INITIALIZE_CSS;
233
234        let current_child = settings.extra_child.clone();
235
236        let model = Alert {
237            settings,
238            is_active: false,
239            current_child,
240        };
241
242        let widgets = view_output!();
243
244        ComponentParts { model, widgets }
245    }
246
247    fn update_with_view(
248        &mut self,
249        widgets: &mut Self::Widgets,
250        input: AlertMsg,
251        sender: ComponentSender<Self>,
252        _root: &Self::Root,
253    ) {
254        // Update the view to contain the extra component, by removing whatever's present in the UI and then adding what the caller's current widget is.
255        if let Some(widget) = self.current_child.take() {
256            widgets.message_area.remove(&widget);
257        }
258
259        if let Some(extra_child) = self.settings.extra_child.clone() {
260            widgets.message_area.append(&extra_child);
261            self.current_child = Some(extra_child);
262        }
263
264        match input {
265            AlertMsg::Show => self.is_active = true,
266            AlertMsg::Hide => self.is_active = false,
267            AlertMsg::Response(resp) => {
268                self.is_active = false;
269                sender.output(resp).unwrap();
270            }
271        }
272
273        self.update_view(widgets, sender);
274    }
275}