1use std::{cell::RefCell, rc::Rc};
2
3use futures_util::FutureExt;
4use send_wrapper::SendWrapper;
5use windows::{
6 Foundation::PropertyValue,
7 UI::Text::FontWeight,
8 Win32::Foundation::E_POINTER,
9 core::{HSTRING, Interface, h},
10};
11use windows_sys::Win32::Foundation::HWND;
12use winio_handle::AsWindow;
13use winio_primitive::{MessageBoxButton, MessageBoxResponse, MessageBoxStyle};
14use winui3::Microsoft::UI::{
15 WindowId,
16 Windowing::{AppWindow, OverlappedPresenter},
17 Xaml::{
18 Application,
19 Controls::{
20 BackgroundSizing, Button, ColumnDefinition, ContentDialog, ContentDialogButton, Grid,
21 RowDefinition, StackPanel, TextBlock,
22 },
23 GridLength, GridUnitType, HorizontalAlignment,
24 Media::Brush,
25 RoutedEventHandler, Style, TextWrapping, Thickness, XamlRoot,
26 },
27};
28
29use crate::{Error, ROOT_WINDOWS, Result};
30
31struct ButtonMeta {
32 flag: MessageBoxButton,
33 label: &'static HSTRING,
34 response: MessageBoxResponse,
35}
36
37const BUTTON_META: [ButtonMeta; 6] = [
38 ButtonMeta {
39 flag: MessageBoxButton::Ok,
40 label: h!("OK"),
41 response: MessageBoxResponse::Ok,
42 },
43 ButtonMeta {
44 flag: MessageBoxButton::Yes,
45 label: h!("Yes"),
46 response: MessageBoxResponse::Yes,
47 },
48 ButtonMeta {
49 flag: MessageBoxButton::No,
50 label: h!("No"),
51 response: MessageBoxResponse::No,
52 },
53 ButtonMeta {
54 flag: MessageBoxButton::Cancel,
55 label: h!("Cancel"),
56 response: MessageBoxResponse::Cancel,
57 },
58 ButtonMeta {
59 flag: MessageBoxButton::Retry,
60 label: h!("Retry"),
61 response: MessageBoxResponse::Retry,
62 },
63 ButtonMeta {
64 flag: MessageBoxButton::Close,
65 label: h!("Close"),
66 response: MessageBoxResponse::Close,
67 },
68];
69
70#[derive(Debug, Clone, Default)]
71pub struct MessageBox {
72 msg: HSTRING,
73 title: HSTRING,
74 instr: HSTRING,
75 btns: MessageBoxButton,
76 cbtns: Vec<CustomButton>,
77}
78
79impl MessageBox {
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn show(
85 self,
86 parent: Option<impl AsWindow>,
87 ) -> Result<impl Future<Output = Result<MessageBoxResponse>> + 'static> {
88 let (hwnd, xaml_root) = if let Some(parent) = parent {
89 let window = parent.as_window();
90 let hwnd = window.handle()?;
91 (hwnd, window.as_winui().Content()?.XamlRoot()?)
92 } else {
93 let xaml_root = ROOT_WINDOWS
94 .with_borrow(|windows| windows.first().cloned())
95 .ok_or_else(|| Error::from_hresult(E_POINTER))?
96 .Content()?
97 .XamlRoot()?;
98 (std::ptr::null_mut(), xaml_root)
99 };
100
101 msgbox(
102 hwnd, xaml_root, self.msg, self.title, self.instr, self.btns, self.cbtns,
103 )
104 }
105
106 pub fn message(&mut self, msg: &str) {
107 self.msg = HSTRING::from(msg);
108 }
109
110 pub fn title(&mut self, title: &str) {
111 self.title = HSTRING::from(title);
112 }
113
114 pub fn instruction(&mut self, instr: &str) {
115 self.instr = HSTRING::from(instr);
116 }
117
118 pub fn style(&mut self, _style: MessageBoxStyle) {}
119
120 pub fn buttons(&mut self, btns: MessageBoxButton) {
121 self.btns = btns;
122 }
123
124 pub fn custom_button(&mut self, btn: CustomButton) {
125 self.cbtns.push(btn);
126 }
127
128 pub fn custom_buttons(&mut self, btn: impl IntoIterator<Item = CustomButton>) {
129 self.cbtns.extend(btn);
130 }
131}
132
133fn collect_buttons(
134 mut btns: MessageBoxButton,
135 cbtns: &[CustomButton],
136) -> Vec<(&HSTRING, MessageBoxResponse)> {
137 if cbtns.is_empty() && btns.is_empty() {
138 btns = MessageBoxButton::Ok;
139 }
140
141 let n = BUTTON_META.iter().filter(|m| btns.contains(m.flag)).count();
142 let mut out = Vec::with_capacity(n + cbtns.len());
143 out.extend(
144 cbtns
145 .iter()
146 .map(|btn| (&btn.text, MessageBoxResponse::Custom(btn.result))),
147 );
148
149 out.extend(
150 BUTTON_META
151 .iter()
152 .filter(|m| btns.contains(m.flag))
153 .map(|m| (m.label, m.response)),
154 );
155
156 out
157}
158
159fn lookup<T: Interface>(key: &HSTRING) -> Result<T> {
160 let resources = Application::Current()?.Resources()?;
161 let key_obj = PropertyValue::CreateString(key)?;
162 resources.Lookup(&key_obj)?.cast()
163}
164
165fn build_button_grid(
166 buttons: &[(&HSTRING, MessageBoxResponse)],
167 dialog: &ContentDialog,
168 result: &SendWrapper<Rc<RefCell<Option<MessageBoxResponse>>>>,
169) -> Result<Grid> {
170 let grid = Grid::new()?;
171 grid.SetColumnSpacing(8.0)?;
172 let cols = grid.ColumnDefinitions()?;
173 let children = grid.Children()?;
174 let n = buttons.len();
175 let mut accent_style = (!buttons.is_empty())
176 .then(|| lookup::<Style>(h!("AccentButtonStyle")).ok())
177 .flatten();
178
179 for _ in 0..n.max(2) {
180 let btn_col = ColumnDefinition::new()?;
181 btn_col.SetWidth(GridLength {
182 Value: 1.0,
183 GridUnitType: GridUnitType::Star,
184 })?;
185 cols.Append(&btn_col)?;
186 }
187
188 for (i, (label, response)) in buttons.iter().enumerate() {
189 let col = if n == 1 { 1 } else { i as i32 };
190 let btn = Button::new()?;
191 let tb = TextBlock::new()?;
192 tb.SetText(label)?;
193 btn.SetContent(&tb)?;
194 btn.SetHorizontalAlignment(HorizontalAlignment::Stretch)?;
195 Grid::SetColumn(&btn, col)?;
196
197 if matches!(response, MessageBoxResponse::Ok | MessageBoxResponse::Yes)
198 && let Some(style) = accent_style.take()
199 {
200 btn.SetStyle(&style)?;
201 }
202
203 let result = result.clone();
204 let dialog = dialog.clone();
205 let resp = *response;
206 btn.Click(&RoutedEventHandler::new(move |_, _| {
207 *result.borrow_mut() = Some(resp);
208 dialog.Hide()?;
209 Ok(())
210 }))?;
211
212 children.Append(&btn)?;
213 }
214
215 Ok(grid)
216}
217
218fn build_content(
219 instr: &HSTRING,
220 msg: &HSTRING,
221 buttons: &[(&HSTRING, MessageBoxResponse)],
222 dialog: &ContentDialog,
223 result: &SendWrapper<Rc<RefCell<Option<MessageBoxResponse>>>>,
224) -> Result<Grid> {
225 let content = Grid::new()?;
226 let content_rows = content.RowDefinitions()?;
227 let content_children = content.Children()?;
228
229 let row0 = RowDefinition::new()?;
230 row0.SetHeight(GridLength {
231 Value: 1.0,
232 GridUnitType: GridUnitType::Star,
233 })?;
234 content_rows.Append(&row0)?;
235
236 let text_panel = StackPanel::new()?;
237 text_panel.SetPadding(Thickness {
238 Left: 0.0,
239 Top: 0.0,
240 Right: 0.0,
241 Bottom: 24.0,
242 })?;
243 let text_children = text_panel.Children()?;
244
245 if !instr.is_empty() {
246 let block = TextBlock::new()?;
247 block.SetText(instr)?;
248 block.SetFontSize(14.0)?;
249 block.SetFontWeight(FontWeight { Weight: 600 })?;
250 text_children.Append(&block)?;
251 }
252
253 if !msg.is_empty() {
254 let block = TextBlock::new()?;
255 block.SetText(msg)?;
256 block.SetTextWrapping(TextWrapping::Wrap)?;
257 text_children.Append(&block)?;
258 }
259
260 Grid::SetRow(&text_panel, 0)?;
261 content_children.Append(&text_panel)?;
262
263 if !buttons.is_empty() {
264 let row1 = RowDefinition::new()?;
265 row1.SetHeight(GridLength {
266 Value: 0.0,
267 GridUnitType: GridUnitType::Auto,
268 })?;
269 content_rows.Append(&row1)?;
270
271 let bar = Grid::new()?;
272 bar.SetMargin(Thickness {
273 Left: -24.0,
274 Top: 0.0,
275 Right: -24.0,
276 Bottom: -24.0,
277 })?;
278 bar.SetBackgroundSizing(BackgroundSizing::OuterBorderEdge)?;
279
280 bar.SetBackground(&lookup::<Brush>(h!("SolidBackgroundFillColorBaseBrush"))?)?;
281
282 bar.SetBorderBrush(&lookup::<Brush>(h!("CardStrokeColorDefaultBrush"))?)?;
283 bar.SetBorderThickness(Thickness {
284 Left: 0.0,
285 Top: 1.0,
286 Right: 0.0,
287 Bottom: 0.0,
288 })?;
289
290 let btn_grid = build_button_grid(buttons, dialog, result)?;
291 btn_grid.SetMargin(Thickness {
292 Left: 24.0,
293 Top: 24.0,
294 Right: 24.0,
295 Bottom: 24.0,
296 })?;
297 bar.Children()?.Append(&btn_grid)?;
298
299 Grid::SetRow(&bar, 1)?;
300 content_children.Append(&bar)?;
301 }
302
303 Ok(content)
304}
305
306fn msgbox(
307 hwnd: HWND,
308 xaml_root: XamlRoot,
309 msg: HSTRING,
310 title: HSTRING,
311 instr: HSTRING,
312 btns: MessageBoxButton,
313 cbtns: Vec<CustomButton>,
314) -> Result<impl Future<Output = Result<MessageBoxResponse>> + 'static> {
315 let all_buttons = collect_buttons(btns, &cbtns);
316
317 let dialog = ContentDialog::new()?;
318 dialog.SetXamlRoot(&xaml_root)?;
319 dialog.SetTitle(&PropertyValue::CreateString(&title)?)?;
320 dialog.SetDefaultButton(ContentDialogButton::None)?;
321
322 let result = SendWrapper::new(Rc::new(RefCell::new(None)));
323 let content = build_content(&instr, &msg, &all_buttons, &dialog, &result)?;
324 dialog.SetContent(&content)?;
325
326 struct EnableGuard {
327 presenter: OverlappedPresenter,
328 restored: bool,
329 }
330
331 impl EnableGuard {
332 fn new(presenter: OverlappedPresenter) -> Self {
333 Self {
334 presenter,
335 restored: false,
336 }
337 }
338
339 fn restore_impl(&mut self) -> Result<()> {
340 self.presenter.SetIsMaximizable(true)?;
341 self.presenter.SetIsResizable(true)?;
342 self.restored = true;
343 Ok(())
344 }
345
346 fn restore(mut self) -> Result<()> {
347 self.restore_impl()
348 }
349 }
350
351 impl Drop for EnableGuard {
352 fn drop(&mut self) {
353 if !self.restored {
354 self.restore_impl().ok();
355 }
356 }
357 }
358
359 let guard = if !hwnd.is_null() {
360 let window = AppWindow::GetFromWindowId(WindowId { Value: hwnd as _ })?;
361 let presenter = window.Presenter()?.cast::<OverlappedPresenter>()?;
362 presenter.SetIsMaximizable(false)?;
363 presenter.SetIsResizable(false)?;
364 Some(EnableGuard::new(presenter))
365 } else {
366 None
367 };
368
369 let action = dialog.ShowAsync()?;
370
371 Ok(action.into_future().map(move |res| {
372 res?;
373
374 if let Some(guard) = guard {
375 guard.restore()?;
376 }
377
378 Ok(result
379 .borrow_mut()
380 .take()
381 .unwrap_or(MessageBoxResponse::Cancel))
382 }))
383}
384
385#[derive(Debug, PartialEq, Eq, Clone)]
386pub struct CustomButton {
387 pub result: u16,
388 pub text: HSTRING,
389}
390
391impl CustomButton {
392 pub fn new(result: u16, text: &str) -> Self {
393 Self {
394 result,
395 text: HSTRING::from(text),
396 }
397 }
398}