Skip to main content

winio_ui_app_kit/widgets/
window.rs

1use inherit_methods_macro::inherit_methods;
2use objc2::{
3    DeclaredClass, MainThreadOnly, define_class, msg_send,
4    rc::{Allocated, Retained, Weak},
5    runtime::ProtocolObject,
6    sel,
7};
8use objc2_app_kit::{
9    NSAppKitVersionNumber, NSAppKitVersionNumber10_10, NSAppKitVersionNumber10_11,
10    NSAppKitVersionNumber10_14, NSAutoresizingMaskOptions, NSBackingStoreType, NSControl, NSScreen,
11    NSView, NSVisualEffectBlendingMode, NSVisualEffectMaterial, NSVisualEffectState,
12    NSVisualEffectView, NSWindow, NSWindowDelegate, NSWindowOrderingMode, NSWindowStyleMask,
13};
14use objc2_foundation::{
15    MainThreadMarker, NSDistributedNotificationCenter, NSNotification, NSObject, NSObjectProtocol,
16    NSPoint, NSRect, NSSize, NSString, ns_string,
17};
18use winio_callback::Callback;
19use winio_handle::{
20    AsContainer, AsWidget, AsWindow, BorrowedContainer, BorrowedWidget, BorrowedWindow,
21};
22use winio_primitive::{Point, Rect, Size};
23
24use crate::{
25    Error, GlobalRuntime, Result, catch, from_cgsize, from_nsstring, to_cgsize, transform_cgrect,
26    transform_rect,
27};
28
29#[derive(Debug)]
30pub struct Window {
31    wnd: Retained<NSWindow>,
32    content_view: Retained<NSView>,
33    delegate: Retained<WindowDelegate>,
34    vibrancy: Option<Vibrancy>,
35    vibrancy_view: Option<Retained<NSVisualEffectView>>,
36}
37
38impl Window {
39    pub fn new() -> Result<Self> {
40        unsafe {
41            let mtm = MainThreadMarker::new().ok_or(Error::NotMainThread)?;
42
43            let frame = NSRect::new(NSPoint::ZERO, NSSize::new(100.0, 100.0));
44
45            let mut this = catch(|| {
46                let wnd = {
47                    NSWindow::initWithContentRect_styleMask_backing_defer(
48                        mtm.alloc(),
49                        frame,
50                        NSWindowStyleMask::Titled
51                            | NSWindowStyleMask::Closable
52                            | NSWindowStyleMask::Resizable
53                            | NSWindowStyleMask::Miniaturizable,
54                        NSBackingStoreType::Buffered,
55                        false,
56                    )
57                };
58
59                let delegate = WindowDelegate::new(mtm);
60                let del_obj = ProtocolObject::from_ref(&*delegate);
61                wnd.setDelegate(Some(del_obj));
62                wnd.setAcceptsMouseMovedEvents(true);
63                wnd.makeKeyWindow();
64
65                NSDistributedNotificationCenter::defaultCenter().addObserver_selector_name_object(
66                    &delegate,
67                    sel!(userDefaultsDidChange),
68                    Some(ns_string!("AppleInterfaceThemeChangedNotification")),
69                    None,
70                );
71
72                let content_view = wnd.contentView().ok_or(Error::NullPointer)?.clone();
73
74                Ok(Self {
75                    wnd,
76                    content_view,
77                    delegate,
78                    vibrancy: None,
79                    vibrancy_view: None,
80                })
81            })
82            .flatten()?;
83            this.set_loc(Point::zero())?;
84            Ok(this)
85        }
86    }
87
88    fn screen(&self) -> Result<Retained<NSScreen>> {
89        catch(|| self.wnd.screen())?.ok_or(Error::NullPointer)
90    }
91
92    pub fn is_visible(&self) -> Result<bool> {
93        catch(|| self.wnd.isVisible())
94    }
95
96    pub fn set_visible(&mut self, v: bool) -> Result<()> {
97        catch(|| self.wnd.setIsVisible(v))
98    }
99
100    pub fn loc(&self) -> Result<Point> {
101        catch(|| {
102            let frame = self.wnd.frame();
103            let screen_frame = self.screen()?.frame();
104            Ok(transform_cgrect(from_cgsize(screen_frame.size), frame).origin)
105        })
106        .flatten()
107    }
108
109    pub fn set_loc(&mut self, p: Point) -> Result<()> {
110        catch(|| {
111            let frame = self.wnd.frame();
112            let screen_frame = self.screen()?.frame();
113            let frame = transform_rect(
114                from_cgsize(screen_frame.size),
115                Rect::new(p, from_cgsize(frame.size)),
116            );
117            self.wnd.setFrame_display(frame, true);
118            Ok(())
119        })
120        .flatten()
121    }
122
123    pub fn size(&self) -> Result<Size> {
124        catch(|| from_cgsize(self.wnd.frame().size))
125    }
126
127    pub fn set_size(&mut self, v: Size) -> Result<()> {
128        catch(|| {
129            let mut frame = self.wnd.frame();
130            let ydiff = v.height - frame.size.height;
131            frame.size = to_cgsize(v);
132            frame.origin.y -= ydiff;
133            self.wnd.setFrame_display(frame, true);
134        })
135    }
136
137    pub fn client_size(&self) -> Result<Size> {
138        catch(|| from_cgsize(self.content_view.frame().size))
139    }
140
141    pub fn text(&self) -> Result<String> {
142        catch(|| from_nsstring(&self.wnd.title()))
143    }
144
145    pub fn set_text(&mut self, s: impl AsRef<str>) -> Result<()> {
146        catch(|| self.wnd.setTitle(&NSString::from_str(s.as_ref())))
147    }
148
149    pub fn vibrancy(&self) -> Result<Option<Vibrancy>> {
150        Ok(self.vibrancy)
151    }
152
153    pub fn set_vibrancy(&mut self, v: Option<Vibrancy>) -> Result<()> {
154        unsafe {
155            if self.vibrancy == v {
156                return Ok(());
157            }
158            if NSAppKitVersionNumber < NSAppKitVersionNumber10_10 {
159                return Err(Error::NotSupported);
160            }
161            self.vibrancy = v;
162
163            catch(|| {
164                if let Some(v) = v {
165                    let view = &self.content_view;
166                    let bounds = view.bounds();
167                    let vev: Retained<NSVisualEffectView> = NSVisualEffectView::initWithFrame(
168                        self.wnd.mtm().alloc::<NSVisualEffectView>(),
169                        bounds,
170                    );
171                    #[allow(deprecated)]
172                    let m = if (v as u32 > 9 && NSAppKitVersionNumber < NSAppKitVersionNumber10_14)
173                        || (v as u32 > 4 && NSAppKitVersionNumber < NSAppKitVersionNumber10_11)
174                    {
175                        NSVisualEffectMaterial::AppearanceBased
176                    } else {
177                        NSVisualEffectMaterial(v as u64 as _)
178                    };
179                    vev.setMaterial(m);
180                    vev.setBlendingMode(NSVisualEffectBlendingMode::BehindWindow);
181                    vev.setState(NSVisualEffectState::FollowsWindowActiveState);
182                    vev.setAutoresizingMask(
183                        NSAutoresizingMaskOptions::ViewWidthSizable
184                            | NSAutoresizingMaskOptions::ViewHeightSizable,
185                    );
186                    view.addSubview_positioned_relativeTo(&vev, NSWindowOrderingMode::Below, None);
187                    if let Some(vv) = self.vibrancy_view.replace(vev) {
188                        vv.removeFromSuperview();
189                    }
190                } else if let Some(vv) = self.vibrancy_view.take() {
191                    vv.removeFromSuperview();
192                }
193                Ok(())
194            })
195            .flatten()
196        }
197    }
198
199    pub async fn wait_size(&self) {
200        self.delegate.ivars().did_resize.wait().await
201    }
202
203    pub async fn wait_move(&self) {
204        self.delegate.ivars().did_move.wait().await
205    }
206
207    pub async fn wait_close(&self) {
208        self.delegate.ivars().should_close.wait().await
209    }
210
211    pub async fn wait_theme_changed(&self) {
212        self.delegate.ivars().defaults_change.wait().await
213    }
214}
215
216impl AsWindow for Window {
217    fn as_window(&self) -> BorrowedWindow<'_> {
218        BorrowedWindow::app_kit(&self.wnd)
219    }
220}
221
222impl AsContainer for Window {
223    fn as_container(&self) -> BorrowedContainer<'_> {
224        BorrowedContainer::app_kit(&self.content_view)
225    }
226}
227
228impl Drop for Window {
229    fn drop(&mut self) {
230        unsafe {
231            NSDistributedNotificationCenter::defaultCenter().removeObserver(&self.delegate);
232        }
233    }
234}
235
236#[derive(Debug, Default)]
237struct WindowDelegateIvars {
238    did_resize: Callback,
239    did_move: Callback,
240    should_close: Callback,
241    defaults_change: Callback,
242}
243
244define_class! {
245    #[unsafe(super(NSObject))]
246    #[name = "WinioWindowDelegate"]
247    #[ivars = WindowDelegateIvars]
248    #[thread_kind = MainThreadOnly]
249    #[derive(Debug)]
250    struct WindowDelegate;
251
252    #[allow(non_snake_case)]
253    impl WindowDelegate {
254        #[unsafe(method_id(init))]
255        fn init(this: Allocated<Self>) -> Option<Retained<Self>> {
256            let this = this.set_ivars(WindowDelegateIvars::default());
257            unsafe { msg_send![super(this), init] }
258        }
259
260        #[unsafe(method(userDefaultsDidChange))]
261        unsafe fn userDefaultsDidChange(&self) {
262            self.ivars().defaults_change.signal::<GlobalRuntime>(());
263        }
264    }
265
266    unsafe impl NSObjectProtocol for WindowDelegate {}
267
268    #[allow(non_snake_case)]
269    unsafe impl NSWindowDelegate for WindowDelegate {
270        #[unsafe(method(windowDidResize:))]
271        unsafe fn windowDidResize(&self, _notification: &NSNotification) {
272            self.ivars().did_resize.signal::<GlobalRuntime>(());
273        }
274
275        #[unsafe(method(windowDidMove:))]
276        unsafe fn windowDidMove(&self, _notification: &NSNotification) {
277            self.ivars().did_move.signal::<GlobalRuntime>(());
278        }
279
280        #[unsafe(method(windowShouldClose:))]
281        unsafe fn windowShouldClose(&self, _sender: &NSWindow) -> bool {
282            self.ivars().should_close.signal::<GlobalRuntime>(())
283        }
284    }
285}
286
287impl WindowDelegate {
288    pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
289        unsafe { msg_send![mtm.alloc::<Self>(), init] }
290    }
291}
292
293/// <https://developer.apple.com/documentation/appkit/nsvisualeffectview/material>
294#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
295#[non_exhaustive]
296pub enum Vibrancy {
297    #[deprecated(
298        note = "A default material appropriate for the view's effectiveAppearance.  You should \
299                instead choose an appropriate semantic material."
300    )]
301    AppearanceBased     = 0,
302    #[deprecated(note = "Use a semantic material instead.")]
303    Light               = 1,
304    #[deprecated(note = "Use a semantic material instead.")]
305    Dark                = 2,
306    #[deprecated(note = "Use a semantic material instead.")]
307    MediumLight         = 8,
308    #[deprecated(note = "Use a semantic material instead.")]
309    UltraDark           = 9,
310
311    /// macOS 10.10+
312    Titlebar            = 3,
313    /// macOS 10.10+
314    Selection           = 4,
315
316    /// macOS 10.11+
317    Menu                = 5,
318    /// macOS 10.11+
319    Popover             = 6,
320    /// macOS 10.11+
321    Sidebar             = 7,
322
323    /// macOS 10.14+
324    HeaderView          = 10,
325    /// macOS 10.14+
326    Sheet               = 11,
327    /// macOS 10.14+
328    WindowBackground    = 12,
329    /// macOS 10.14+
330    HudWindow           = 13,
331    /// macOS 10.14+
332    FullScreenUI        = 15,
333    /// macOS 10.14+
334    Tooltip             = 17,
335    /// macOS 10.14+
336    ContentBackground   = 18,
337    /// macOS 10.14+
338    UnderWindowBackground = 21,
339    /// macOS 10.14+
340    UnderPageBackground = 22,
341}
342
343#[derive(Debug)]
344pub(crate) struct Widget {
345    parent: Weak<NSView>,
346    view: Retained<NSView>,
347}
348
349impl Widget {
350    pub fn from_nsview(parent: impl AsContainer, view: Retained<NSView>) -> Result<Self> {
351        let mut this = catch(|| {
352            let parent = parent.as_container().as_app_kit();
353            parent.addSubview(&view);
354            Self {
355                parent: Weak::from_retained(parent),
356                view,
357            }
358        })?;
359        this.set_loc(Point::zero())?;
360        Ok(this)
361    }
362
363    pub fn parent(&self) -> Result<Retained<NSView>> {
364        catch(|| self.parent.load())?.ok_or(Error::NullPointer)
365    }
366
367    pub fn is_visible(&self) -> Result<bool> {
368        catch(|| !self.view.isHidden())
369    }
370
371    pub fn set_visible(&mut self, v: bool) -> Result<()> {
372        catch(|| self.view.setHidden(!v))
373    }
374
375    pub fn is_enabled(&self) -> Result<bool> {
376        catch(|| {
377            self.view
378                .downcast_ref::<NSControl>()
379                .map(|c| c.isEnabled())
380                .unwrap_or(true)
381        })
382    }
383
384    pub fn set_enabled(&mut self, v: bool) -> Result<()> {
385        catch(|| {
386            if let Some(c) = self.view.downcast_ref::<NSControl>() {
387                c.setEnabled(v);
388            }
389        })
390    }
391
392    pub fn preferred_size(&self) -> Result<Size> {
393        catch(|| {
394            let s = self.view.fittingSize();
395            if s != NSSize::ZERO {
396                return from_cgsize(s);
397            }
398            self.view
399                .downcast_ref::<NSControl>()
400                .map(|c| from_cgsize(c.sizeThatFits(NSSize::ZERO)))
401                .unwrap_or_default()
402        })
403    }
404
405    pub fn loc(&self) -> Result<Point> {
406        catch(|| {
407            let frame = self.view.frame();
408            let screen_frame = self.parent()?.frame();
409            Ok(transform_cgrect(from_cgsize(screen_frame.size), frame).origin)
410        })
411        .flatten()
412    }
413
414    pub fn set_loc(&mut self, p: Point) -> Result<()> {
415        catch(|| {
416            let frame = self.view.frame();
417            let screen_frame = self.parent()?.frame();
418            let frame = transform_rect(
419                from_cgsize(screen_frame.size),
420                Rect::new(p, from_cgsize(frame.size)),
421            );
422            self.view.setFrame(frame);
423            Ok(())
424        })
425        .flatten()
426    }
427
428    pub fn size(&self) -> Result<Size> {
429        catch(|| from_cgsize(self.view.frame().size))
430    }
431
432    pub fn set_size(&mut self, v: Size) -> Result<()> {
433        catch(|| {
434            let mut frame = self.view.frame();
435            let ydiff = v.height - frame.size.height;
436            frame.size = to_cgsize(v);
437            frame.origin.y -= ydiff;
438            self.view.setFrame(frame);
439        })
440    }
441
442    pub fn text(&self) -> Result<String> {
443        catch(|| {
444            self.view
445                .downcast_ref::<NSControl>()
446                .map(|c| from_nsstring(&c.stringValue()))
447                .unwrap_or_default()
448        })
449    }
450
451    pub fn set_text(&mut self, s: impl AsRef<str>) -> Result<()> {
452        catch(|| {
453            if let Some(c) = self.view.downcast_ref::<NSControl>() {
454                c.setStringValue(&NSString::from_str(s.as_ref()));
455            }
456        })
457    }
458
459    pub fn tooltip(&self) -> Result<String> {
460        catch(|| {
461            self.view
462                .toolTip()
463                .map(|s| from_nsstring(&s))
464                .unwrap_or_default()
465        })
466    }
467
468    pub fn set_tooltip(&mut self, s: impl AsRef<str>) -> Result<()> {
469        catch(|| {
470            let s = s.as_ref();
471            let s = if s.is_empty() {
472                None
473            } else {
474                Some(NSString::from_str(s))
475            };
476            self.view.setToolTip(s.as_deref());
477        })
478    }
479}
480
481impl Drop for Widget {
482    fn drop(&mut self) {
483        self.view.removeFromSuperview();
484    }
485}
486
487impl AsWidget for Widget {
488    fn as_widget(&self) -> BorrowedWidget<'_> {
489        BorrowedWidget::app_kit(&self.view)
490    }
491}
492
493impl AsContainer for Widget {
494    fn as_container(&self) -> BorrowedContainer<'_> {
495        BorrowedContainer::app_kit(&self.view)
496    }
497}
498
499#[derive(Debug)]
500pub struct View {
501    handle: Widget,
502}
503
504#[inherit_methods(from = "self.handle")]
505impl View {
506    pub fn new(parent: impl AsContainer) -> Result<Self> {
507        unsafe {
508            catch(|| {
509                let parent = parent.as_container();
510                let mtm = parent.as_app_kit().mtm();
511
512                let view = NSView::new(mtm);
513                let handle = Widget::from_nsview(parent, Retained::cast_unchecked(view.clone()))?;
514
515                Ok(Self { handle })
516            })
517            .flatten()
518        }
519    }
520
521    pub fn is_visible(&self) -> Result<bool>;
522
523    pub fn set_visible(&mut self, v: bool) -> Result<()>;
524
525    pub fn loc(&self) -> Result<Point>;
526
527    pub fn set_loc(&mut self, p: Point) -> Result<()>;
528
529    pub fn size(&self) -> Result<Size>;
530
531    pub fn set_size(&mut self, v: Size) -> Result<()>;
532}
533
534winio_handle::impl_as_widget!(View, handle);
535winio_handle::impl_as_container!(View, handle);