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 set_modal: model.settings.is_modal,
114 add_css_class: "relm4-alert",
115
116 #[wrap(Some)]
117 set_titlebar = >k::Box {
118 set_visible: false,
119 },
120
121 gtk::Box {
122 set_orientation: gtk::Orientation::Vertical,
123
124 #[name(message_area)]
125 gtk::Box {
126 set_orientation: gtk::Orientation::Vertical,
127 set_spacing: 8,
128 set_vexpand: true,
129 add_css_class: MESSAGE_AREA_CSS,
130
131 gtk::Label {
132 #[watch]
133 set_text: model.settings.text.as_deref().unwrap_or_default(),
134 #[watch]
135 set_visible: model.settings.text.is_some(),
136 set_valign: gtk::Align::Start,
137 set_justify: gtk::Justification::Center,
138 add_css_class: relm4::css::TITLE_2,
139 set_wrap: true,
140 set_max_width_chars: 20,
141 },
142
143 gtk::Label {
144 #[watch]
145 set_text: model.settings.secondary_text.as_deref().unwrap_or_default(),
146 set_vexpand: true,
147 set_valign: gtk::Align::Fill,
148 set_justify: gtk::Justification::Center,
149 set_wrap: true,
150 set_max_width_chars: 40,
151 },
152 },
153
154 gtk::Box {
155 add_css_class: RESPONSE_BUTTONS_CSS,
156 set_orientation: gtk::Orientation::Vertical,
157 set_vexpand_set: true,
158 set_valign: gtk::Align::End,
159 gtk::Separator {},
160
161 gtk::Box {
162 set_homogeneous: true,
163 set_vexpand: true,
164 set_valign: gtk::Align::End,
165
166 #[name(confirm_label)]
171 gtk::Button {
172 #[watch]
173 set_visible: model.settings.confirm_label.is_some(),
174 #[watch]
175 set_class_active: (relm4::css::DESTRUCTIVE_ACTION, !LIBADWAITA_ENABLED && model.settings.destructive_accept),
176 #[watch]
177 set_class_active: (relm4::css::FLAT, LIBADWAITA_ENABLED || !model.settings.destructive_accept),
178 set_hexpand: true,
179 connect_clicked => AlertMsg::Response(AlertResponse::Confirm),
180
181 gtk::Label {
182 #[watch]
183 set_label: model.settings.confirm_label.as_deref().unwrap_or_default(),
184 #[watch]
185 set_class_active: (relm4::css::ERROR, LIBADWAITA_ENABLED && model.settings.destructive_accept),
186 }
187 },
188
189 gtk::Box {
190 #[watch]
191 set_visible: model.settings.cancel_label.is_some(),
192
193 gtk::Separator {},
194
195 #[name(cancel_label)]
196 gtk::Button {
197 #[watch]
198 set_label: model.settings.cancel_label.as_deref().unwrap_or_default(),
199 add_css_class: relm4::css::FLAT,
200 set_hexpand: true,
201 connect_clicked => AlertMsg::Response(AlertResponse::Cancel)
202 }
203 },
204
205 gtk::Box {
206 #[watch]
207 set_visible: model.settings.option_label.is_some(),
208
209 gtk::Separator {},
210
211 #[name(option_label)]
212 gtk::Button {
213 #[watch]
214 set_label: model.settings.option_label.as_deref().unwrap_or_default(),
215 add_css_class: relm4::css::FLAT,
216 set_hexpand: true,
217 connect_clicked => AlertMsg::Response(AlertResponse::Option)
218 }
219 }
220 }
221 }
222 }
223 }
224 }
225
226 fn init(
227 settings: AlertSettings,
228 root: Self::Root,
229 sender: ComponentSender<Self>,
230 ) -> ComponentParts<Self> {
231 #[allow(clippy::no_effect)] *INITIALIZE_CSS;
234
235 let current_child = settings.extra_child.clone();
236
237 let model = Alert {
238 settings,
239 is_active: false,
240 current_child,
241 };
242
243 let widgets = view_output!();
244
245 ComponentParts { model, widgets }
246 }
247
248 fn update_with_view(
249 &mut self,
250 widgets: &mut Self::Widgets,
251 input: AlertMsg,
252 sender: ComponentSender<Self>,
253 _root: &Self::Root,
254 ) {
255 if let Some(widget) = self.current_child.take() {
257 widgets.message_area.remove(&widget);
258 }
259
260 if let Some(extra_child) = self.settings.extra_child.clone() {
261 widgets.message_area.append(&extra_child);
262 self.current_child = Some(extra_child);
263 }
264
265 match input {
266 AlertMsg::Show => self.is_active = true,
267 AlertMsg::Hide => self.is_active = false,
268 AlertMsg::Response(resp) => {
269 self.is_active = false;
270 sender.output(resp).unwrap();
271 }
272 }
273
274 self.update_view(widgets, sender);
275 }
276}