winio_ui_app_kit/dialogs/
msgbox.rs1use 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
132unsafe 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}