1#![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 menu_target: RefCell<Option<Retained<MenuTarget>>>,
49}
50
51impl MacBackend {
52 #[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 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 let style = NSWindowStyleMask::Titled
102 | NSWindowStyleMask::Closable
103 | NSWindowStyleMask::Miniaturizable
104 | NSWindowStyleMask::Resizable
105 | NSWindowStyleMask::FullSizeContentView;
106 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 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 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 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
397fn 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
407fn host_view(parent: Parent<'_, MacBackend>) -> &NSView {
409 match parent {
410 Parent::Window(window) => &window.content,
411 Parent::Scroll(scroll) => scroll.content_view(),
412 }
413}