Skip to main content

nifty_macos/
lib.rs

1//! `AppKit` backend: every Nifty widget is a real `NSView` subclass from the system.
2#![cfg(target_os = "macos")]
3
4mod areas;
5mod classes;
6mod context_menu;
7mod convert;
8mod dialog;
9mod drop;
10mod icons;
11mod menus;
12mod nav_item;
13mod switch;
14mod waker;
15mod widgets;
16
17use std::cell::RefCell;
18use std::path::Path;
19
20use objc2::rc::Retained;
21use objc2::runtime::ProtocolObject;
22use objc2::{MainThreadMarker, MainThreadOnly};
23use objc2_app_kit::{
24    NSApplication, NSApplicationActivationPolicy, NSAutoresizingMaskOptions, NSBackingStoreType,
25    NSView, NSVisualEffectMaterial, NSVisualEffectView, NSWindow, NSWindowOrderingMode,
26    NSWindowStyleMask, NSWindowToolbarStyle,
27};
28use objc2_foundation::{NSPoint, NSRect, NSString};
29
30use nifty_runtime::{
31    Backdrop, Backend, ButtonStyle, ClickHandler, CloseHandler, DialogHandler, DropHandler,
32    FileDialog, Frame, Icon, ImageSize, LabelStyle, Menu, MenuEntry, MenuHandler, NumberHandler,
33    Parent, ResizeHandler, Size, Surface, TextHandler, ToggleHandler, ToggleStyle, WakeHandler,
34};
35
36use classes::{AppDelegate, FlippedView, MenuTarget, WindowDelegate};
37use convert::{to_ns_size, to_points, to_size};
38pub use waker::MacWaker;
39pub use widgets::MacWidget;
40use widgets::behind_window;
41
42#[derive(Debug)]
43pub struct MacBackend {
44    mtm: MainThreadMarker,
45    app: Retained<NSApplication>,
46    _delegate: Retained<AppDelegate>,
47    /// What the app's own menu items tell when chosen; menu items hold it weakly.
48    menu_target: RefCell<Option<Retained<MenuTarget>>>,
49}
50
51impl MacBackend {
52    /// # Panics
53    /// When called off the main thread, which `AppKit` forbids.
54    #[must_use]
55    pub fn new() -> Self {
56        let mtm = MainThreadMarker::new().expect("AppKit apps must start on the main thread");
57        let app = NSApplication::sharedApplication(mtm);
58        app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
59        menus::install(&app, mtm, &[], None);
60
61        let delegate = AppDelegate::new(mtm);
62        app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
63        Self {
64            mtm,
65            app,
66            _delegate: delegate,
67            menu_target: RefCell::new(None),
68        }
69    }
70}
71
72impl Default for MacBackend {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78#[derive(Debug)]
79pub struct MacWindow {
80    window: Retained<NSWindow>,
81    content: Retained<FlippedView>,
82    _delegate: Retained<WindowDelegate>,
83    /// Behind all the content of a translucent window; made the first time one asks.
84    translucency: RefCell<Option<Retained<NSVisualEffectView>>>,
85}
86
87impl Backend for MacBackend {
88    type Window = MacWindow;
89    type Widget = MacWidget;
90    type Waker = MacWaker;
91
92    fn open_window(
93        &self,
94        title: &str,
95        size: Size,
96        on_resize: ResizeHandler,
97        on_close: CloseHandler,
98    ) -> MacWindow {
99        // The content runs under a transparent title bar, so surfaces such as a sidebar can
100        // reach the top of the window the way Finder's does.
101        let style = NSWindowStyleMask::Titled
102            | NSWindowStyleMask::Closable
103            | NSWindowStyleMask::Miniaturizable
104            | NSWindowStyleMask::Resizable
105            | NSWindowStyleMask::FullSizeContentView;
106        // SAFETY: `setReleasedWhenClosed(false)` below; `MacWindow` owns the window instead.
107        let window = unsafe {
108            NSWindow::initWithContentRect_styleMask_backing_defer(
109                NSWindow::alloc(self.mtm),
110                NSRect::new(NSPoint::ZERO, to_ns_size(size)),
111                style,
112                NSBackingStoreType::Buffered,
113                false,
114            )
115        };
116        // SAFETY: the window is retained by `MacWindow` for as long as it is used.
117        unsafe { window.setReleasedWhenClosed(false) };
118        window.setTitle(&NSString::from_str(title));
119        window.setTitlebarAppearsTransparent(true);
120        window.setToolbarStyle(NSWindowToolbarStyle::Unified);
121
122        let content = FlippedView::new(self.mtm);
123        window.setContentView(Some(&content));
124        // `size` is the area below the title bar, which the content view also covers. The
125        // window grows before it has a delegate, so the resize isn't reported while the
126        // caller is still creating it.
127        let inset = inset_under_title_bar(&window, &content);
128        window.setContentSize(to_ns_size(Size::new(size.width, size.height + inset)));
129        let delegate =
130            WindowDelegate::new(self.mtm, Retained::clone(&content), on_resize, on_close);
131        window.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
132
133        MacWindow {
134            window,
135            content,
136            _delegate: delegate,
137            translucency: RefCell::new(None),
138        }
139    }
140
141    fn content_size(&self, window: &MacWindow) -> Size {
142        to_size(window.content.frame().size)
143    }
144
145    fn show_window(&self, window: &MacWindow) {
146        window.window.center();
147        window.window.makeKeyAndOrderFront(None);
148    }
149
150    fn focus_window(&self, window: &MacWindow) {
151        window.window.makeKeyAndOrderFront(None);
152    }
153
154    fn set_title(&self, window: &MacWindow, title: &str) {
155        window.window.setTitle(&NSString::from_str(title));
156    }
157
158    /// A Mica-like material doesn't exist on macOS, where a standard window is already the
159    /// platform's own look, so only `Translucent` changes anything.
160    fn set_backdrop(&self, window: &MacWindow, backdrop: Backdrop) {
161        let translucent = backdrop == Backdrop::Translucent;
162        let mut translucency = window.translucency.borrow_mut();
163        if let Some(view) = &*translucency {
164            view.setHidden(!translucent);
165        } else if translucent {
166            let view = behind_window(self.mtm, NSVisualEffectMaterial::UnderWindowBackground);
167            view.setFrame(window.content.bounds());
168            view.setAutoresizingMask(
169                NSAutoresizingMaskOptions::ViewWidthSizable
170                    | NSAutoresizingMaskOptions::ViewHeightSizable,
171            );
172            window.content.addSubview_positioned_relativeTo(
173                &view,
174                NSWindowOrderingMode::Below,
175                None,
176            );
177            *translucency = Some(view);
178        }
179    }
180
181    fn set_content_size(&self, window: &MacWindow, size: Size) {
182        window.window.setContentSize(to_ns_size(size));
183    }
184
185    fn close_window(&self, window: MacWindow) {
186        window.window.close();
187    }
188
189    fn title_bar_inset(&self, window: &MacWindow) -> f32 {
190        inset_under_title_bar(&window.window, &window.content)
191    }
192
193    fn set_min_content_width(&self, window: &MacWindow, width: f32) {
194        window
195            .window
196            .setContentMinSize(to_ns_size(Size::new(width, 0.0)));
197    }
198
199    fn create_surface(&self, parent: Parent<'_, Self>, surface: Surface) -> MacWidget {
200        add_to(parent, MacWidget::surface(self.mtm, surface))
201    }
202
203    fn create_scroll(&self, parent: Parent<'_, Self>) -> MacWidget {
204        add_to(parent, MacWidget::scroll(self.mtm))
205    }
206
207    fn scroll_bar_width(&self, _window: &MacWindow) -> f32 {
208        MacWidget::scroll_bar_width(self.mtm)
209    }
210
211    fn set_scroll_content(&self, scroll: &MacWidget, size: Size) {
212        scroll.set_scroll_content(size);
213    }
214
215    fn create_label(&self, parent: Parent<'_, Self>, text: &str, style: LabelStyle) -> MacWidget {
216        add_to(parent, MacWidget::label(self.mtm, text, style))
217    }
218
219    fn create_button(
220        &self,
221        parent: Parent<'_, Self>,
222        title: &str,
223        style: ButtonStyle,
224        icon: Icon,
225        selected: Option<bool>,
226        on_click: ClickHandler,
227    ) -> MacWidget {
228        add_to(
229            parent,
230            MacWidget::button(self.mtm, title, (style, icon), selected, on_click),
231        )
232    }
233
234    fn create_nav_item(
235        &self,
236        parent: Parent<'_, Self>,
237        title: &str,
238        icon: Icon,
239        badge: &str,
240        selected: bool,
241        on_click: ClickHandler,
242    ) -> MacWidget {
243        add_to(
244            parent,
245            MacWidget::nav_item(self.mtm, title, icon, badge, selected, on_click),
246        )
247    }
248
249    fn create_text_field(
250        &self,
251        parent: Parent<'_, Self>,
252        text: &str,
253        placeholder: &str,
254        lines: u32,
255        on_change: TextHandler,
256        on_submit: Option<ClickHandler>,
257    ) -> MacWidget {
258        let field = if lines > 1 {
259            MacWidget::text_area(self.mtm, text, lines, on_change)
260        } else {
261            MacWidget::text_field(self.mtm, text, placeholder, on_change, on_submit)
262        };
263        add_to(parent, field)
264    }
265
266    fn create_checkbox(
267        &self,
268        parent: Parent<'_, Self>,
269        title: &str,
270        checked: bool,
271        style: ToggleStyle,
272        on_toggle: ToggleHandler,
273    ) -> MacWidget {
274        let widget = match style {
275            ToggleStyle::Checkbox => MacWidget::checkbox(self.mtm, title, checked, on_toggle),
276            ToggleStyle::Switch => MacWidget::switch(self.mtm, title, checked, on_toggle),
277        };
278        add_to(parent, widget)
279    }
280
281    fn create_slider(
282        &self,
283        parent: Parent<'_, Self>,
284        value: i64,
285        min: i64,
286        max: i64,
287        on_change: NumberHandler,
288    ) -> MacWidget {
289        add_to(
290            parent,
291            MacWidget::slider(self.mtm, value, min, max, on_change),
292        )
293    }
294
295    fn create_picker(
296        &self,
297        parent: Parent<'_, Self>,
298        options: &[String],
299        selected: &str,
300        on_pick: TextHandler,
301    ) -> MacWidget {
302        add_to(
303            parent,
304            MacWidget::picker(self.mtm, options, selected, on_pick),
305        )
306    }
307
308    fn create_divider(&self, parent: Parent<'_, Self>) -> MacWidget {
309        add_to(parent, MacWidget::divider(self.mtm))
310    }
311
312    fn create_drop_target(&self, parent: Parent<'_, Self>, on_drop: DropHandler) -> MacWidget {
313        drop::accept_drops(host_view(parent));
314        add_to(parent, MacWidget::drop_target(self.mtm, on_drop))
315    }
316
317    fn create_context_menu(
318        &self,
319        parent: Parent<'_, Self>,
320        entries: &[MenuEntry<usize>],
321        on_select: MenuHandler,
322    ) -> MacWidget {
323        add_to(
324            parent,
325            MacWidget::context_menu(self.mtm, entries, on_select),
326        )
327    }
328
329    fn create_image(
330        &self,
331        parent: Parent<'_, Self>,
332        path: Option<&Path>,
333        size: ImageSize,
334    ) -> MacWidget {
335        add_to(parent, MacWidget::image(self.mtm, path, size))
336    }
337
338    fn set_image(&self, widget: &MacWidget, path: Option<&Path>) {
339        widget.set_image(path);
340    }
341
342    fn create_progress(&self, parent: Parent<'_, Self>, percent: Option<i64>) -> MacWidget {
343        add_to(parent, MacWidget::progress(self.mtm, percent))
344    }
345
346    fn set_text(&self, widget: &MacWidget, text: &str) {
347        widget.set_text(text);
348    }
349
350    fn set_checked(&self, widget: &MacWidget, checked: bool) {
351        widget.set_checked(checked);
352    }
353
354    fn set_badge(&self, widget: &MacWidget, badge: &str) {
355        widget.set_badge(badge);
356    }
357
358    fn set_value(&self, widget: &MacWidget, value: i64) {
359        widget.set_value(value);
360    }
361
362    fn set_options(&self, widget: &MacWidget, options: &[String]) {
363        widget.set_options(options);
364    }
365
366    fn measure(&self, widget: &MacWidget) -> Size {
367        widget.measure()
368    }
369
370    fn set_frame(&self, widget: &MacWidget, frame: Frame) {
371        widget.set_frame(frame);
372    }
373
374    fn remove_widget(&self, widget: MacWidget) {
375        widget.remove();
376    }
377
378    fn set_menu_bar(&self, menus: &[Menu<usize>], on_select: MenuHandler) {
379        let target = MenuTarget::new(self.mtm, on_select);
380        menus::install(&self.app, self.mtm, menus, Some(&target));
381        *self.menu_target.borrow_mut() = Some(target);
382    }
383
384    fn waker(&self, on_wake: WakeHandler) -> MacWaker {
385        MacWaker::new(self.mtm, on_wake)
386    }
387
388    fn show_file_dialog(&self, dialog: &FileDialog, on_close: DialogHandler) {
389        dialog::show(self.mtm, &self.app, dialog, on_close);
390    }
391
392    fn run(&self) {
393        self.app.run();
394    }
395}
396
397/// How much of `content` the title bar covers: the part outside the window's layout rect.
398fn inset_under_title_bar(window: &NSWindow, content: &NSView) -> f32 {
399    to_points(content.frame().size.height - window.contentLayoutRect().size.height)
400}
401
402fn add_to(parent: Parent<'_, MacBackend>, widget: MacWidget) -> MacWidget {
403    host_view(parent).addSubview(widget.view());
404    widget
405}
406
407/// The view widgets created in `parent` sit in.
408fn host_view(parent: Parent<'_, MacBackend>) -> &NSView {
409    match parent {
410        Parent::Window(window) => &window.content,
411        Parent::Scroll(scroll) => scroll.content_view(),
412    }
413}