Skip to main content

app/platform/apple/
macos.rs

1#![allow(unused_imports, unused_doc_comments, clippy::tabs_in_doc_comments)]
2use log::debug;
3use crate::{void, String};
4
5use objc2::{
6	rc::{Retained, Allocated},
7	runtime::ProtocolObject,
8	define_class,
9	msg_send,
10	DefinedClass,
11	MainThreadOnly,
12	Message,
13	ClassType,
14	sel
15};
16
17use objc2_app_kit::{
18	NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate,
19	NSBackingStoreType, NSColor, NSFont, NSTextAlignment, NSTextField, NSWindow, NSWindowDelegate,
20	NSWindowStyleMask, NSView, NSWindowTitleVisibility, NSVisualEffectBlendingMode,
21	NSVisualEffectView, NSVisualEffectMaterial, NSVisualEffectState, NSAutoresizingMaskOptions,
22	NSMenu, NSMenuItem
23};
24
25use objc2_foundation::{
26	MainThreadMarker, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect,
27	NSSize, NSString,
28};
29
30use crate::{DecorationMode, Decoration, WResponse, Color, ThemeDefault, NativeDecoration};
31
32/// Wrapper struct
33#[derive(PartialEq, Debug, Clone)]
34pub struct Wrapper {
35	pub ns_view: *mut void,		// NSView
36	pub rect:  *const void,		// NSRect
37}
38
39impl NativeDecoration for Decoration
40{
41	/// Creates the native window frame decoration for macOS
42	fn new(title: String, width: f64, height: f64, theme: ThemeDefault) -> Result<Self, WResponse>
43	{
44		let Some(mtm) = MainThreadMarker::new() else { return Err(WResponse::UnexpectedError) };
45
46		let origin = NSPoint::new(10.0, -2.3);
47		let size = NSSize::new(width, height);
48		let rect = NSRect::new(origin, size);
49
50		let window = unsafe { NSWindow::initWithContentRect_styleMask_backing_defer(
51			NSWindow::alloc(mtm),
52			rect,
53			NSWindowStyleMask::Titled
54				| NSWindowStyleMask::Closable
55				| NSWindowStyleMask::Miniaturizable
56				| NSWindowStyleMask::Resizable
57				| NSWindowStyleMask::FullSizeContentView,
58			NSBackingStoreType::Buffered,
59			false,
60		)};
61
62		window.setTitle(&NSString::from_str(title.as_str()));
63
64		if !theme.has_title {
65			window.setTitlebarAppearsTransparent(true);
66			window.setTitleVisibility(NSWindowTitleVisibility(1));
67		}
68
69		// TODO: this should be handled by the graphical backend if used with `blur`
70		let (r, g, b, a) = theme.background_color.to_default();
71		window.setBackgroundColor(
72			Some(&NSColor::colorWithSRGBRed_green_blue_alpha(r, g, b, a)
73		));
74
75		window.makeKeyAndOrderFront(None);
76		unsafe { window.setReleasedWhenClosed(false) };
77
78		let Some(view) = window.contentView() else { return Err(WResponse::UnexpectedError) };
79
80		window.center();
81		window.setContentMinSize(NSSize::new(width, height));
82
83		let Some(delegate) =
84			Delegate::new(window.clone()) else { return Err(WResponse::UnexpectedError) };
85
86		window.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
87		window.makeKeyAndOrderFront(None);
88
89		let app = NSApplication::sharedApplication(mtm);
90		let _ = app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
91		app.activate();
92
93		let backend = Wrapper {
94			ns_view: void::to_handle(Retained::<NSView>::as_ptr(&view).cast_mut()),
95			rect: void::to_handle(&rect),
96		};
97
98		debug!("Creating NativeDecoration object");
99
100		Ok(Decoration {
101			mode: DecorationMode::ServerSide,
102			frame: void::to_handle(Retained::<NSWindow>::as_ptr(&window).cast_mut()),
103			backend,
104		})
105	}
106
107	/// Apply blur effect on the window
108	fn apply_blur(&mut self) -> Result<(), WResponse>
109	{
110		let Some(mtm) = MainThreadMarker::new() else { return Err(WResponse::UnexpectedError) };
111
112		let backend = self.backend.clone();
113		let rect: NSRect = void::from_handle(backend.rect);
114		/**
115		 * Blur view configs
116		 * Not using liquid glass for this part in specific
117		 * Mostly, other effects will be managed trought renderer/shaders on vulkan and not macOS
118		 */
119		let alloc: Allocated<NSVisualEffectView> = NSVisualEffectView::alloc(mtm);
120		let blur_view_ptr = NSVisualEffectView::initWithFrame(alloc, rect);
121		let window: &NSWindow = void::from_handle(self.frame);
122
123		let Some(content) = window.contentView() else {
124			log::warn!("couldn't set blur");
125			return Err(WResponse::UnexpectedError);
126		};
127
128		let blur_view = blur_view_ptr.retain();
129		content.addSubview(&blur_view);
130
131		blur_view.setBlendingMode(NSVisualEffectBlendingMode(0));
132		blur_view.setMaterial(NSVisualEffectMaterial::HUDWindow);
133		blur_view.setState(NSVisualEffectState::Active);
134		blur_view.setFrame(content.bounds());
135		blur_view.setTranslatesAutoresizingMaskIntoConstraints(false);
136		blur_view.setAutoresizingMask(
137			NSAutoresizingMaskOptions::ViewWidthSizable
138				| NSAutoresizingMaskOptions::ViewHeightSizable
139		);
140
141		debug!("applying blur on NativeDecoration");
142		Ok(())
143	}
144
145	// this is way easier in swift...
146	fn create_app_menu(&self, app_name: String) -> Result<(), WResponse>
147	{
148		let Some(mtm) = MainThreadMarker::new() else { return Err(WResponse::UnexpectedError) };
149		let app = NSApplication::sharedApplication(mtm);
150
151		let item_menu = NSMenuItem::alloc(mtm);
152		let quit_item = unsafe { NSMenuItem::initWithTitle_action_keyEquivalent(
153			item_menu,
154			&NSString::from_str(dirty::format!("Quit {}", app_name).as_str()),
155			Some(sel!(terminate:)),
156			&NSString::from_str("q")
157		) };
158		unsafe { quit_item.setTarget(Some(&app)) };
159
160		let app_menu = NSMenu::new(mtm);
161			app_menu.addItem(&quit_item);
162
163		let app_menu_item = NSMenuItem::new(mtm);
164		let menubar = NSMenu::new(mtm);
165			menubar.addItem(&app_menu_item);
166
167		app_menu_item.setSubmenu(Some(&app_menu));
168		app.setMainMenu(Some(&menubar));
169
170		debug!("creating app menu");
171		Ok(())
172	}
173
174	/// The default function to run the program, since it's required on macOS
175	fn run(&self)
176	{
177		use objc2::{class, runtime::AnyObject};
178		let raw: *mut NSApplication = unsafe {
179			msg_send![class!(NSApplication), sharedApplication]
180		};
181
182		unsafe { let _: *mut AnyObject = msg_send![raw, retain]; }
183		let Some(app) = (unsafe { Retained::from_raw(raw) }) else {
184			log::error!("no NSApplication found");
185			return
186		};
187
188		unsafe { msg_send![&*app, run] }
189	}
190
191	#[inline]
192	fn exit(&self) -> Result<(), WResponse>
193	{
194		use objc2::{class, runtime::AnyObject};
195		let raw: *mut NSApplication = unsafe {
196			msg_send![class!(NSApplication), sharedApplication]
197		};
198
199		unsafe { let _: *mut AnyObject = msg_send![raw, retain]; }
200		let Some(app) = (unsafe { Retained::from_raw(raw) }) else {
201			 return Err(WResponse::UnexpectedError);
202		};
203
204		app.terminate(None);
205		Ok(())
206	}
207
208	/*fn set_title(&self, title: &str) {
209		let ns = NSString::from_str(title);
210		self.window.setTitle(&ns);
211	}*/
212}
213
214#[derive(Debug)]
215#[allow(dead_code)]
216struct AppDelegateIvars {
217	window: Retained<NSWindow>,
218}
219
220define_class!(
221	#[unsafe(super = NSObject)]
222	#[thread_kind = MainThreadOnly]
223	#[ivars = AppDelegateIvars]
224	struct Delegate;
225
226	unsafe impl NSObjectProtocol for Delegate {}
227
228	unsafe impl NSApplicationDelegate for Delegate {
229		#[unsafe(method(applicationDidFinishLaunching:))]
230		fn did_finish_launching(&self, _notification: &NSNotification) {}
231	}
232
233	unsafe impl NSWindowDelegate for Delegate {
234		#[unsafe(method(windowWillClose:))]
235		fn window_will_close(&self, _notification: &NSNotification)
236			{ NSApplication::sharedApplication(self.mtm()).terminate(None); }
237	}
238);
239
240impl Delegate {
241	fn new(window: Retained<NSWindow>) -> Option<Retained<Self>>
242	{
243		let mtm = MainThreadMarker::new()?;
244		let this = Self::alloc(mtm).set_ivars(AppDelegateIvars { window });
245		Some(unsafe { msg_send![super(this), init] })
246	}
247}