Skip to main content

winio_ui_winui/dialogs/
msgbox.rs

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}