Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/platform/mac/objc/host_view.m
1#import "host_view.h"
2 
3#import <Metal/Metal.h>
4 
5void warp_view_did_change_backing_properties(WarpHostView *, BOOL);
6void warp_view_set_frame_size(WarpHostView *, NSSize, BOOL);
7void warp_update_layer(WarpHostView *);
8BOOL warp_handle_view_event(WarpHostView *, NSEvent *, BOOL);
9BOOL warp_handle_first_mouse_event(WarpHostView *, NSEvent *);
10void warp_handle_insert_text(WarpHostView *, id);
11void warp_update_ime_state(WarpHostView *, BOOL);
12void warp_handle_drag_and_drop(WarpHostView *, NSArray *, NSPoint);
13void warp_handle_file_drag(WarpHostView *, NSPoint);
14void warp_handle_file_drag_exit(WarpHostView *);
15NSRect warp_ime_position(WarpHostView *, NSRect *);
16id warp_get_accessibility_contents(WarpHostView *);
17void warp_marked_text_updated(WarpHostView *, NSString *, NSRange);
18void warp_marked_text_cleared(WarpHostView *);
19 
20@implementation NSPasteboard (Warp)
21 
22- (NSArray *)getFilePaths {
23 NSMutableArray *paths = [NSMutableArray array];
24 NSArray<NSURL *> *urls = [self readObjectsForClasses:@[ [NSURL class] ] options:0];
25 for (NSURL *url in urls) {
26 NSString *path = url.path;
27 if (path) {
28 [paths addObject:path];
29 }
30 }
31 return paths;
32}
33 
34@end
35 
36@implementation WarpHostView {
37 // The windowState is managed on the Rust side.
38 // Note Rust expects this name even though we are not a window.
39 void *windowState;
40 
41 // Whether we start a window drag on an unhandled mouseDown event inside the title bar
42 BOOL titlebarDragEnabled;
43 
44 // Whether we are in test mode, which suppresses drawing.
45 BOOL testMode;
46 
47 // The metal device for our layer.
48 id metalDevice;
49 
50 NSMutableAttributedString *markedText;
51 NSMutableString *textToInsert;
52 
53 // Whether to have resize event callback called asynchronously.
54 BOOL asyncCallback;
55 
56 // Whether we're in the middle of a call to interpretKeyEvents.
57 BOOL interpretingKeyEvents;
58}
59 
60- (BOOL)acceptsFirstResponder {
61 return YES;
62}
63 
64- (BOOL)mouseDownCanMoveWindow {
65 return !titlebarDragEnabled;
66}
67 
68- (BOOL)readyForWarp {
69 return windowState != NULL;
70}
71 
72/// Returns the height of the titlebar.
73- (CGFloat)titlebarHeight {
74 NSButton *closeButton = [self.window standardWindowButton:NSWindowCloseButton];
75 NSView *titlebar = [closeButton superview];
76 return titlebar.frame.size.height;
77}
78 
79- (BOOL)mouseInTitleBar:(NSEvent *)event {
80 NSPoint windowLoc = [self convertPoint:event.locationInWindow fromView:nil];
81 // windowLoc.y is the distance from the bottom of the window to the cursor
82 // NSHeight(window.frame) will be the height of the whole window, so
83 // NSHeight - titlebarHeight will be the bottom border of the titlebar
84 return NSHeight(self.window.frame) - [self titlebarHeight] <= windowLoc.y;
85}
86 
87// See if the user double clicked in the titlebar. If so, do whatever
88// action is given by preferences.
89// \return true if handled, false otherwise.
90- (BOOL)handleTitleBarDoubleClick:(NSEvent *)event {
91 NSWindow *window = self.window;
92 NSWindowStyleMask styleMask = window.styleMask;
93 // Was this a double click in a full-sized content view, not in full screen?
94 if (event.clickCount != 2) return NO;
95 if (!(styleMask & NSWindowStyleMaskFullSizeContentView)) return NO;
96 if (styleMask & NSWindowStyleMaskFullScreen) return NO;
97 
98 // See if our point is in the titlebar of the window.
99 if (![self mouseInTitleBar:event]) return NO;
100 
101 // Ok, do the action.
102 NSString *action =
103 [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleActionOnDoubleClick"];
104 
105 // When user has not explicitly ticked or unticked the `Double-click the window's
106 // title bar to` option in system preferences, the NSUserDefaults will not have the key
107 // "AppleActionOnDoubleClick", despite in system preferences the default is to "Zoom".
108 // To make the behavior consistent, when the key is nil, we set performZoom as the
109 // default behavior here.
110 if ([action isEqualToString:@"Minimize"]) {
111 [window performMiniaturize:nil];
112 return YES;
113 } else if (action == nil || [action isEqualToString:@"Maximize"]) {
114 [window performZoom:nil];
115 return YES;
116 }
117 return NO;
118}
119 
120- (void)viewDidChangeBackingProperties {
121 if (self.readyForWarp) warp_view_did_change_backing_properties(self, asyncCallback);
122 [super viewDidChangeBackingProperties];
123}
124 
125- (void)setFrameSize:(NSSize)size {
126 BOOL changed = !NSEqualSizes(size, self.frame.size);
127 // We could receive invalid frame sizes when the window is moved offscreen.
128 // Validate the size against the minimum drawable size of the window before
129 // passing to the rust side.
130 if (size.height >= self.window.minSize.height && size.width >= self.window.minSize.width) {
131 [super setFrameSize:size];
132 // It's an important optimization to only invoke this if the size changed.
133 if (self.readyForWarp && changed) {
134 warp_view_set_frame_size(self, size, asyncCallback);
135 }
136 }
137}
138 
139- (void)displayLayer:(CALayer *)layer {
140 if (!testMode && self.readyForWarp) {
141 warp_update_layer(self);
142 }
143}
144 
145- (void)setAsyncCallback:(BOOL)shouldAsync {
146 asyncCallback = shouldAsync;
147}
148 
149- (void)keyDown:(NSEvent *)event {
150 [self keyDownImpl:event];
151}
152 
153- (BOOL)keyDownImpl:(NSEvent *)event {
154 BOOL wasComposing = [self hasMarkedText];
155 [textToInsert setString:@""];
156 
157 // Interpret the key events here so we could check whether user is composing
158 // text within the IME and pass the state down to the KeyDown events.
159 interpretingKeyEvents = YES;
160 [self interpretKeyEvents:[NSArray arrayWithObject:event]];
161 interpretingKeyEvents = NO;
162 
163 BOOL handled = NO;
164 if (self.readyForWarp) {
165 handled = warp_handle_view_event(self, event, wasComposing || [self hasMarkedText]);
166 }
167 
168 // It's possible to have keybinding conflicts between terminal apps which use the meta key and
169 // MacOS "dead keys". Dead keys are used to add diacritical marks to other characters, and they
170 // start composing marked text. To detect if a keybinding was triggered in the app, `handled`
171 // will be true. If that is the case, we don't want MacOS to also start composing because we
172 // already handled that keydown elsewhere. So, if `justStartedComposing` is also true, clear
173 // out the marked text.
174 // https://support.apple.com/guide/mac-help/enter-characters-with-accent-marks-on-mac-mh27474/mac#mchl45cdda7f
175 BOOL justStartedComposing = !wasComposing && [self hasMarkedText];
176 if (handled && justStartedComposing) {
177 NSTextInputContext *inputContext = [self inputContext];
178 [inputContext discardMarkedText];
179 [self unmarkText];
180 }
181 
182 // Dispatch TypedCharacter event after KeyDown has been dispatched.
183 if ([textToInsert length] > 0 && !handled) {
184 warp_handle_insert_text(self, (NSString *)textToInsert);
185 [self unmarkText];
186 }
187 
188 return handled;
189}
190 
191- (BOOL)acceptsFirstMouse:(NSEvent *)event {
192 // We want to receive mouseDown events even if the window is not key
193 // and we explicity fire the event here so that Warp can handle it.
194 if (self.readyForWarp) warp_handle_first_mouse_event(self, event);
195 
196 // We return NO though so that the event is not fired twice (returning YES
197 // would result in the event being passed to the mouseDown handler).
198 return NO;
199}
200 
201- (void)mouseDown:(NSEvent *)event {
202 if (self.readyForWarp) {
203 BOOL eventHandled = warp_handle_view_event(self, event, NO);
204 if (self->titlebarDragEnabled && !eventHandled && [self mouseInTitleBar:event]) {
205 // If Warp doesn't do anything with the event, indicated by returning `false`, and
206 // if the drag starts in the titlebar, begin dragging the window
207 [self.window performWindowDragWithEvent:event];
208 }
209 }
210}
211 
212- (void)mouseUp:(NSEvent *)event {
213 // Our content view is full-size so we don't get the default behavior
214 // on titlebar clicks. Implement it manually.
215 BOOL warp_handled = NO;
216 if (self.readyForWarp) {
217 warp_handled = warp_handle_view_event(self, event, NO);
218 }
219 if (!warp_handled) {
220 [self handleTitleBarDoubleClick:event];
221 }
222}
223 
224- (void)otherMouseDown:(NSEvent *)event {
225 if (self.readyForWarp) warp_handle_view_event(self, event, NO);
226}
227 
228- (void)rightMouseDown:(NSEvent *)event {
229 if (self.readyForWarp) warp_handle_view_event(self, event, NO);
230}
231 
232- (void)mouseDragged:(NSEvent *)event {
233 if (self.readyForWarp) warp_handle_view_event(self, event, NO);
234}
235 
236- (void)scrollWheel:(NSEvent *)event {
237 if (self.readyForWarp) warp_handle_view_event(self, event, NO);
238}
239 
240- (void)mouseMoved:(NSEvent *)event {
241 if (self.readyForWarp) warp_handle_view_event(self, event, NO);
242}
243 
244- (void)flagsChanged:(NSEvent *)event {
245 if (self.readyForWarp) warp_handle_view_event(self, event, NO);
246}
247 
248- (void)dealloc {
249 [markedText release];
250 [textToInsert release];
251 [metalDevice release];
252 [super dealloc];
253}
254 
255- (CALayer *)makeBackingLayer {
256 CAMetalLayer *layer = [CAMetalLayer layer];
257 layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
258 layer.device = metalDevice;
259 layer.allowsNextDrawableTimeout = NO;
260 layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
261 layer.needsDisplayOnBoundsChange = YES;
262 layer.presentsWithTransaction = YES;
263 layer.delegate = self;
264 layer.opaque = NO;
265 return layer;
266}
267 
268- (WarpHostView *)initWithFrame:(NSRect)frame
269 metalDevice:(id)device
270 enableTitlebarDrag:(BOOL)enableTitlebarDrag
271 testMode:(BOOL)testModeFlag {
272 NSAssert(testModeFlag || device, @"Nil metal device not in test mode");
273 [super initWithFrame:frame];
274 
275 // Register here so we could receive drag and drop events.
276 [self registerForDraggedTypes:@[
277 NSPasteboardTypeFileURL,
278 ]];
279 self->testMode = testModeFlag;
280 self->titlebarDragEnabled = enableTitlebarDrag;
281 self->metalDevice = [device retain];
282 self->markedText = [[NSMutableAttributedString alloc] init];
283 self->textToInsert = [[NSMutableString alloc] init];
284 self->asyncCallback = YES;
285 self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
286 self.wantsLayer = YES;
287 self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
288 return self;
289}
290 
291// Entry point for drag & drop. Check whether the source is an acceptable type and if so
292// pass it down to performDragOperaion.
293- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
294 NSDragOperation sourceMask = [sender draggingSourceOperationMask];
295 
296 BOOL pasteOK =
297 !![[sender draggingPasteboard] availableTypeFromArray:@[ NSPasteboardTypeFileURL ]];
298 if (pasteOK && (sourceMask & NSDragOperationCopy)) {
299 return NSDragOperationCopy;
300 }
301 return NSDragOperationNone;
302}
303 
304// Called continuously while the drag operation is occurring within the view
305- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
306 NSPoint dragPoint = [sender draggingLocation];
307 NSPoint localPoint = [self convertPoint:dragPoint fromView:nil];
308 
309 NSPasteboard *pasteboard = [sender draggingPasteboard];
310 if (self.readyForWarp) {
311 NSArray *types = [pasteboard types];
312 if ([types containsObject:NSPasteboardTypeFileURL]) {
313 warp_handle_file_drag(self, localPoint);
314 return YES;
315 }
316 }
317 return NSDragOperationNone;
318}
319 
320- (void)draggingExited:(id<NSDraggingInfo>)sender {
321 if (self.readyForWarp) {
322 warp_handle_file_drag_exit(self);
323 }
324}
325 
326- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
327 NSPasteboard *pasteboard = [sender draggingPasteboard];
328 NSDragOperation dragOperation = [sender draggingSourceOperationMask];
329 
330 NSPoint dragPoint = [sender draggingLocation];
331 NSPoint localPoint = [self convertPoint:dragPoint fromView:nil];
332 
333 if (self.readyForWarp && (dragOperation & NSDragOperationCopy)) {
334 NSArray *types = [pasteboard types];
335 if ([types containsObject:NSPasteboardTypeFileURL]) {
336 warp_handle_drag_and_drop(self, [pasteboard getFilePaths], localPoint);
337 return YES;
338 }
339 }
340 return NO;
341}
342 
343- (void)closeIMEAsync {
344 dispatch_async(dispatch_get_main_queue(), ^{
345 NSTextInputContext *inputContext = [self inputContext];
346 [inputContext discardMarkedText];
347 
348 [self unmarkText];
349 });
350}
351 
352#pragma mark - Accessibility
353- (BOOL)isAccessibilityElement {
354 return YES;
355}
356 
357- (NSAccessibilityRole)accessibilityRole {
358 return NSAccessibilityTextAreaRole;
359}
360 
361- (NSString *)accessibilityRoleDescription {
362 return NSAccessibilityRoleDescriptionForUIElement(self);
363}
364 
365- (BOOL)isAccessibilityFocused {
366 return YES;
367}
368 
369- (id)accessibilityValue {
370 return warp_get_accessibility_contents(self);
371}
372 
373- (NSInteger)accessibilityNumberOfCharacters {
374 return 0;
375}
376 
377- (NSInteger)accessibilityInsertionPointLineNumber {
378 return 0;
379}
380 
381- (NSString *)accessibilityDocument {
382 return nil;
383}
384 
385////////////////////////////////////////////////////////////////////////////////
386// NSTextInputClient protocol implementation
387////////////////////////////////////////////////////////////////////////////////
388 
389- (nullable NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
390 actualRange:
391 (nullable NSRangePointer)actualRange {
392 return nil;
393}
394 
395- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint {
396 return (NSUInteger)0;
397}
398 
399// This is a no-op as we will be handling control characters in KeyDown events.
400- (void)doCommandBySelector:(SEL)selector {
401}
402 
403- (NSRect)firstRectForCharacterRange:(NSRange)range
404 actualRange:(nullable NSRangePointer)actualRange {
405 NSWindow *window = self.window;
406 if (self.readyForWarp) {
407 NSRect contentRect = [window contentRectForFrameRect:[window frame]];
408 NSRect rect = warp_ime_position(self, &contentRect);
409 return rect;
410 } else {
411 return NSZeroRect;
412 }
413}
414 
415- (BOOL)hasMarkedText {
416 return [markedText length] > 0;
417}
418 
419// Referenced glfw for this implementation.
420// https://github.com/glfw/glfw/blob/7ef34eb06de54dd9186d3d21a401b2ef819b59e7/src/cocoa_window.m#L814
421- (void)insertText:(id)string replacementRange:(NSRange)replacementRange {
422 if (self.readyForWarp) {
423 NSMutableString *characters = [[NSMutableString alloc] init];
424 
425 if ([string isKindOfClass:[NSAttributedString class]]) {
426 // We are appending rather than replacing here because sometimes insertText
427 // could be fired multiple times in a row. For example, when user types
428 // Option-E followed by g, insertText will fire ´ first and then g.
429 [characters appendString:[string string]];
430 } else {
431 [characters appendString:(NSString *)string];
432 }
433 
434 // If we're in the middle of a call to interpretKeyEvents, batch up all
435 // inserted text, as we may handle the event during `keyDown`. If this
436 // call to `insertText` is not in a call stack underneath `keyDown`
437 // (e.g.: when inserting an emoji from the emoji composer), just insert
438 // the text directly.
439 if (interpretingKeyEvents) {
440 [textToInsert appendString:characters];
441 } else {
442 warp_handle_insert_text(self, (NSString *)characters);
443 }
444 
445 [characters release];
446 }
447 // When handling the key down Enter, we might need to rely on the IME being open
448 // to accept the marked text as-is and so can't call unmarkText.
449 if (!interpretingKeyEvents) {
450 [self unmarkText];
451 }
452}
453 
454- (NSRange)markedRange {
455 if ([markedText length] > 0)
456 return NSMakeRange(0, [markedText length]);
457 else
458 return NSMakeRange(NSNotFound, 0);
459}
460 
461- (NSRange)selectedRange {
462 return NSMakeRange(0, 0);
463}
464 
465- (void)setMarkedText:(id)string
466 selectedRange:(NSRange)selectedRange
467 replacementRange:(NSRange)replacementRange {
468 [markedText release];
469 if ([string isKindOfClass:[NSAttributedString class]])
470 markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
471 else
472 markedText = [[NSMutableAttributedString alloc] initWithString:string];
473 
474 if (self.readyForWarp) {
475 warp_marked_text_updated(self, markedText.string, selectedRange);
476 if ([markedText length] > 0) {
477 warp_update_ime_state(self, YES);
478 } else {
479 warp_update_ime_state(self, NO);
480 }
481 }
482}
483 
484- (void)unmarkText {
485 [[markedText mutableString] setString:@""];
486 if (self.readyForWarp) {
487 warp_update_ime_state(self, NO);
488 warp_marked_text_cleared(self);
489 }
490}
491 
492- (NSArray<NSString *> *)validAttributesForMarkedText {
493 return [NSArray array];
494}
495 
496@end
497