Skip to main content

winio_ui_app_kit/widgets/
button.rs

1use compio_log::{error, info, warn};
2use inherit_methods_macro::inherit_methods;
3use objc2::{
4    DeclaredClass, MainThreadOnly, define_class, msg_send,
5    rc::{Allocated, Retained},
6    sel,
7};
8use objc2_app_kit::{
9    NSBezelStyle, NSButton, NSButtonType, NSControlStateValueOff, NSControlStateValueOn,
10    NSWorkspace,
11};
12use objc2_foundation::{MainThreadMarker, NSObject, NSString, NSURL};
13use winio_callback::Callback;
14use winio_handle::AsContainer;
15use winio_primitive::{Point, Size};
16
17use crate::{GlobalRuntime, Result, Widget, catch, from_nsstring};
18
19#[derive(Debug)]
20pub struct Button {
21    handle: Widget,
22    view: Retained<NSButton>,
23    delegate: Retained<ButtonDelegate>,
24}
25
26#[inherit_methods(from = "self.handle")]
27impl Button {
28    pub fn new(parent: impl AsContainer) -> Result<Self> {
29        let parent = parent.as_container();
30        let mtm = parent.as_app_kit().mtm();
31
32        catch(|| unsafe {
33            let view = NSButton::new(mtm);
34            let handle = Widget::from_nsview(parent, Retained::cast_unchecked(view.clone()))?;
35
36            let delegate = ButtonDelegate::new(mtm);
37            view.setTarget(Some(&delegate));
38            view.setAction(Some(sel!(onAction)));
39
40            view.setBezelStyle(NSBezelStyle::FlexiblePush);
41            Ok(Self {
42                handle,
43                view,
44                delegate,
45            })
46        })
47        .flatten()
48    }
49
50    pub fn is_visible(&self) -> Result<bool>;
51
52    pub fn set_visible(&mut self, v: bool) -> Result<()>;
53
54    pub fn is_enabled(&self) -> Result<bool>;
55
56    pub fn set_enabled(&mut self, v: bool) -> Result<()>;
57
58    pub fn preferred_size(&self) -> Result<Size>;
59
60    pub fn loc(&self) -> Result<Point>;
61
62    pub fn set_loc(&mut self, p: Point) -> Result<()>;
63
64    pub fn size(&self) -> Result<Size>;
65
66    pub fn set_size(&mut self, v: Size) -> Result<()>;
67
68    pub fn tooltip(&self) -> Result<String>;
69
70    pub fn set_tooltip(&mut self, s: impl AsRef<str>) -> Result<()>;
71
72    pub fn text(&self) -> Result<String> {
73        catch(|| from_nsstring(&self.view.title()))
74    }
75
76    pub fn set_text(&mut self, s: impl AsRef<str>) -> Result<()> {
77        catch(|| self.view.setTitle(&NSString::from_str(s.as_ref())))
78    }
79
80    pub async fn wait_click(&self) {
81        self.delegate.ivars().action.wait().await
82    }
83}
84
85winio_handle::impl_as_widget!(Button, handle);
86
87#[derive(Debug)]
88pub struct CheckBox {
89    handle: Button,
90}
91
92#[inherit_methods(from = "self.handle")]
93impl CheckBox {
94    pub fn new(parent: impl AsContainer) -> Result<Self> {
95        let handle = Button::new(parent)?;
96        catch(|| {
97            handle.view.setButtonType(NSButtonType::Switch);
98            handle.view.setAllowsMixedState(false);
99        })?;
100        Ok(Self { handle })
101    }
102
103    pub fn is_visible(&self) -> Result<bool>;
104
105    pub fn set_visible(&mut self, v: bool) -> Result<()>;
106
107    pub fn is_enabled(&self) -> Result<bool>;
108
109    pub fn set_enabled(&mut self, v: bool) -> Result<()>;
110
111    pub fn preferred_size(&self) -> Result<Size> {
112        let mut s = self.handle.preferred_size()?;
113        s.width += 4.0;
114        Ok(s)
115    }
116
117    pub fn loc(&self) -> Result<Point>;
118
119    pub fn set_loc(&mut self, p: Point) -> Result<()>;
120
121    pub fn size(&self) -> Result<Size>;
122
123    pub fn set_size(&mut self, v: Size) -> Result<()>;
124
125    pub fn tooltip(&self) -> Result<String>;
126
127    pub fn set_tooltip(&mut self, s: impl AsRef<str>) -> Result<()>;
128
129    pub fn text(&self) -> Result<String>;
130
131    pub fn set_text(&mut self, s: impl AsRef<str>) -> Result<()>;
132
133    pub fn is_checked(&self) -> Result<bool> {
134        catch(|| self.handle.view.state() == NSControlStateValueOn)
135    }
136
137    pub fn set_checked(&mut self, v: bool) -> Result<()> {
138        catch(|| {
139            self.handle.view.setState(if v {
140                NSControlStateValueOn
141            } else {
142                NSControlStateValueOff
143            })
144        })
145    }
146
147    pub async fn wait_click(&self) {
148        self.handle.wait_click().await
149    }
150}
151
152winio_handle::impl_as_widget!(CheckBox, handle);
153
154#[derive(Debug)]
155pub struct RadioButton {
156    handle: Button,
157}
158
159#[inherit_methods(from = "self.handle")]
160impl RadioButton {
161    pub fn new(parent: impl AsContainer) -> Result<Self> {
162        let handle = Button::new(parent)?;
163        catch(|| {
164            handle.view.setButtonType(NSButtonType::Radio);
165            handle.view.setAllowsMixedState(false);
166        })?;
167        Ok(Self { handle })
168    }
169
170    pub fn is_visible(&self) -> Result<bool>;
171
172    pub fn set_visible(&mut self, v: bool) -> Result<()>;
173
174    pub fn is_enabled(&self) -> Result<bool>;
175
176    pub fn set_enabled(&mut self, v: bool) -> Result<()>;
177
178    pub fn preferred_size(&self) -> Result<Size>;
179
180    pub fn loc(&self) -> Result<Point>;
181
182    pub fn set_loc(&mut self, p: Point) -> Result<()>;
183
184    pub fn size(&self) -> Result<Size>;
185
186    pub fn set_size(&mut self, v: Size) -> Result<()>;
187
188    pub fn tooltip(&self) -> Result<String>;
189
190    pub fn set_tooltip(&mut self, s: impl AsRef<str>) -> Result<()>;
191
192    pub fn text(&self) -> Result<String>;
193
194    pub fn set_text(&mut self, s: impl AsRef<str>) -> Result<()>;
195
196    pub fn is_checked(&self) -> Result<bool> {
197        catch(|| self.handle.view.state() == NSControlStateValueOn)
198    }
199
200    pub fn set_checked(&mut self, v: bool) -> Result<()> {
201        catch(|| {
202            self.handle.view.setState(if v {
203                NSControlStateValueOn
204            } else {
205                NSControlStateValueOff
206            })
207        })
208    }
209
210    pub async fn wait_click(&self) {
211        self.handle.wait_click().await
212    }
213}
214
215winio_handle::impl_as_widget!(RadioButton, handle);
216
217#[derive(Debug)]
218pub struct LinkLabel {
219    handle: Button,
220    uri: String,
221}
222
223#[inherit_methods(from = "self.handle")]
224impl LinkLabel {
225    pub fn new(parent: impl AsContainer) -> Result<Self> {
226        let handle = Button::new(parent)?;
227        catch(|| {
228            handle.view.setBordered(false);
229            handle.view.setBezelStyle(NSBezelStyle::Badge);
230        })?;
231        Ok(Self {
232            handle,
233            uri: String::new(),
234        })
235    }
236
237    pub fn is_visible(&self) -> Result<bool>;
238
239    pub fn set_visible(&mut self, v: bool) -> Result<()>;
240
241    pub fn is_enabled(&self) -> Result<bool>;
242
243    pub fn set_enabled(&mut self, v: bool) -> Result<()>;
244
245    pub fn preferred_size(&self) -> Result<Size>;
246
247    pub fn loc(&self) -> Result<Point>;
248
249    pub fn set_loc(&mut self, p: Point) -> Result<()>;
250
251    pub fn size(&self) -> Result<Size>;
252
253    pub fn set_size(&mut self, v: Size) -> Result<()>;
254
255    pub fn tooltip(&self) -> Result<String>;
256
257    pub fn set_tooltip(&mut self, s: impl AsRef<str>) -> Result<()>;
258
259    pub fn text(&self) -> Result<String>;
260
261    pub fn set_text(&mut self, s: impl AsRef<str>) -> Result<()>;
262
263    pub fn uri(&self) -> Result<String> {
264        Ok(self.uri.clone())
265    }
266
267    pub fn set_uri(&mut self, uri: impl AsRef<str>) -> Result<()> {
268        self.uri = uri.as_ref().to_string();
269        Ok(())
270    }
271
272    pub async fn wait_click(&self) {
273        loop {
274            self.handle.wait_click().await;
275            if self.uri.is_empty() {
276                break;
277            } else {
278                if let Some(url) = NSURL::URLWithString(&NSString::from_str(&self.uri)) {
279                    info!("Opening link: {}", self.uri);
280                    let opened = NSWorkspace::sharedWorkspace().openURL(&url);
281                    if !opened {
282                        error!("Failed to open link: {}", self.uri);
283                    }
284                } else {
285                    warn!("Invalid URL: {}", self.uri);
286                }
287            }
288        }
289    }
290}
291
292winio_handle::impl_as_widget!(LinkLabel, handle);
293
294#[derive(Debug, Default)]
295struct ButtonDelegateIvars {
296    action: Callback,
297}
298
299define_class! {
300    #[unsafe(super(NSObject))]
301    #[name = "WinioButtonDelegate"]
302    #[ivars = ButtonDelegateIvars]
303    #[thread_kind = MainThreadOnly]
304    #[derive(Debug)]
305    struct ButtonDelegate;
306
307    #[allow(non_snake_case)]
308    impl ButtonDelegate {
309        #[unsafe(method_id(init))]
310        fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
311            let this = this.set_ivars(ButtonDelegateIvars::default());
312            unsafe { msg_send![super(this), init] }
313        }
314
315        #[unsafe(method(onAction))]
316        unsafe fn onAction(&self) {
317            self.ivars().action.signal::<GlobalRuntime>(());
318        }
319    }
320}
321
322impl ButtonDelegate {
323    pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
324        unsafe { msg_send![mtm.alloc::<Self>(), init] }
325    }
326}