Skip to main content

winio_ui_app_kit/dialogs/
msgbox.rs

1use std::{cell::Cell, rc::Rc};
2
3use arrayvec::ArrayVec;
4use block2::StackBlock;
5use futures_util::{FutureExt, future::Either};
6use objc2::{MainThreadOnly, rc::Retained};
7use objc2_app_kit::{
8    NSAlert, NSAlertFirstButtonReturn, NSAlertStyle, NSImage, NSImageNameCaution, NSImageNameInfo,
9};
10use objc2_foundation::{MainThreadMarker, NSString, ns_string};
11use winio_handle::AsWindow;
12use winio_primitive::{MessageBoxButton, MessageBoxResponse, MessageBoxStyle};
13
14use crate::{Error, Result, catch};
15
16fn msgbox_custom(
17    parent: Option<impl AsWindow>,
18    msg: Retained<NSString>,
19    title: Retained<NSString>,
20    instr: Retained<NSString>,
21    style: MessageBoxStyle,
22    btns: MessageBoxButton,
23    cbtns: Vec<CustomButton>,
24) -> Result<impl Future<Output = Result<MessageBoxResponse>> + 'static> {
25    let parent = parent.as_ref().map(|p| p.as_window().as_app_kit());
26    let mtm = parent
27        .as_ref()
28        .map(|w| w.mtm())
29        .or_else(MainThreadMarker::new)
30        .ok_or(Error::NotMainThread)?;
31
32    let (alert, responses) = catch(|| {
33        let alert = NSAlert::new(mtm);
34        if let Some(parent) = &parent {
35            unsafe {
36                alert.window().setParentWindow(Some(parent));
37            }
38        }
39        alert.setAlertStyle(match style {
40            MessageBoxStyle::Info => NSAlertStyle::Informational,
41            MessageBoxStyle::Warning | MessageBoxStyle::Error => NSAlertStyle::Critical,
42            _ => NSAlertStyle::Warning,
43        });
44        let image = match style {
45            MessageBoxStyle::Info => NSImage::imageNamed(unsafe { NSImageNameInfo }),
46            MessageBoxStyle::Warning | MessageBoxStyle::Error => {
47                NSImage::imageNamed(unsafe { NSImageNameCaution })
48            }
49            _ => None,
50        };
51        unsafe {
52            alert.setIcon(image.as_deref());
53        }
54
55        alert.window().setTitle(&title);
56        if instr.is_empty() {
57            alert.setMessageText(&msg);
58        } else {
59            alert.setMessageText(&instr);
60            alert.setInformativeText(&msg);
61        }
62
63        let mut responses = ArrayVec::<MessageBoxResponse, 6>::new();
64
65        if btns.contains(MessageBoxButton::Ok) {
66            alert.addButtonWithTitle(ns_string!("Ok"));
67            responses.push(MessageBoxResponse::Ok);
68        }
69        if btns.contains(MessageBoxButton::Yes) {
70            alert.addButtonWithTitle(ns_string!("Yes"));
71            responses.push(MessageBoxResponse::Yes);
72        }
73        if btns.contains(MessageBoxButton::No) {
74            alert.addButtonWithTitle(ns_string!("No"));
75            responses.push(MessageBoxResponse::No);
76        }
77        if btns.contains(MessageBoxButton::Cancel) {
78            alert.addButtonWithTitle(ns_string!("Cancel"));
79            responses.push(MessageBoxResponse::Cancel);
80        }
81        if btns.contains(MessageBoxButton::Retry) {
82            alert.addButtonWithTitle(ns_string!("Try again"));
83            responses.push(MessageBoxResponse::Retry);
84        }
85        if btns.contains(MessageBoxButton::Close) {
86            alert.addButtonWithTitle(ns_string!("Close"));
87            responses.push(MessageBoxResponse::Close);
88        }
89
90        for b in cbtns {
91            alert.addButtonWithTitle(&b.text);
92            responses.push(MessageBoxResponse::Custom(b.result));
93        }
94        Ok((alert, responses))
95    })
96    .flatten()?;
97
98    if let Some(parent) = &parent {
99        let (tx, rx) = local_sync::oneshot::channel();
100        let tx = Rc::new(Cell::new(Some(tx)));
101        let block = StackBlock::new(move |res| {
102            tx.take()
103                .expect("the handler should only be called once")
104                .send(responses[(res - NSAlertFirstButtonReturn) as usize])
105                .ok();
106        });
107        catch(|| alert.beginSheetModalForWindow_completionHandler(parent, Some(&block)))?;
108        let parent = (**parent).clone();
109        Ok(Either::Left(rx.map(move |res| {
110            let res = res?;
111            catch(|| parent.makeKeyWindow())?;
112            Ok(res)
113        })))
114    } else {
115        let res = catch(|| alert.runModal())?;
116        Ok(Either::Right(std::future::ready(Ok(
117            responses[res as usize - NSAlertFirstButtonReturn as usize]
118        ))))
119    }
120}
121
122#[derive(Debug, Default, Clone)]
123pub struct MessageBox {
124    msg: Retained<NSString>,
125    title: Retained<NSString>,
126    instr: Retained<NSString>,
127    style: MessageBoxStyle,
128    btns: MessageBoxButton,
129    cbtns: Vec<CustomButton>,
130}
131
132// SAFETY: NSString is thread-safe.
133unsafe impl Send for MessageBox {}
134unsafe impl Sync for MessageBox {}
135
136impl MessageBox {
137    pub fn new() -> Self {
138        Self::default()
139    }
140
141    pub fn show(
142        self,
143        parent: Option<impl AsWindow>,
144    ) -> Result<impl Future<Output = Result<MessageBoxResponse>> + 'static> {
145        msgbox_custom(
146            parent, self.msg, self.title, self.instr, self.style, self.btns, self.cbtns,
147        )
148    }
149
150    pub fn message(&mut self, msg: &str) {
151        self.msg = NSString::from_str(msg);
152    }
153
154    pub fn title(&mut self, title: &str) {
155        self.title = NSString::from_str(title);
156    }
157
158    pub fn instruction(&mut self, instr: &str) {
159        self.instr = NSString::from_str(instr);
160    }
161
162    pub fn style(&mut self, style: MessageBoxStyle) {
163        self.style = style;
164    }
165
166    pub fn buttons(&mut self, btns: MessageBoxButton) {
167        self.btns = btns;
168    }
169
170    pub fn custom_button(&mut self, btn: CustomButton) {
171        self.cbtns.push(btn);
172    }
173
174    pub fn custom_buttons(&mut self, btn: impl IntoIterator<Item = CustomButton>) {
175        self.cbtns.extend(btn);
176    }
177}
178
179#[derive(Debug, PartialEq, Eq, Clone)]
180pub struct CustomButton {
181    pub result: u16,
182    pub text: Retained<NSString>,
183}
184
185impl CustomButton {
186    pub fn new(result: u16, text: &str) -> Self {
187        Self {
188            result,
189            text: NSString::from_str(text),
190        }
191    }
192}