1use 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
14static INITIALIZE_CSS: Lazy<()> = Lazy::new(|| {
16 relm4::set_global_css_with_priority(COMPONENT_CSS, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
17});
18
19#[derive(Debug)]
32pub struct AlertSettings {
33 pub text: Option<String>,
35 pub secondary_text: Option<String>,
37 pub is_modal: bool,
39 pub destructive_accept: bool,
41 pub confirm_label: Option<String>,
43 pub cancel_label: Option<String>,
45 pub option_label: Option<String>,
47 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#[derive(Debug)]
68pub struct Alert {
69 pub settings: AlertSettings,
71 is_active: bool,
72 current_child: Option<gtk::Widget>,
73}
74
75#[derive(Debug)]
77pub enum AlertMsg {
78 Show,
80
81 Hide,
83
84 #[doc(hidden)]
85 Response(AlertResponse),
86}
87
88#[derive(Debug)]
90pub enum AlertResponse {
91 Confirm,
93
94 Cancel,
96
97 Option,
99}
100
101#[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 = >k::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 #[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 #[allow(clippy::no_effect)] *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 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}