#import #import #import #include "yasl/yasl.h" #include "yasl/timing.h" #include "cocoa/keycodes.h" #include #include #include #include #include #define LINK_IDLE_FRAMES 8 #define SCROLL_POINTS_PER_LINE 10.0 enum { DEVICE_LCTL = 0x0001, DEVICE_LSHIFT = 0x0002, DEVICE_RSHIFT = 0x0004, DEVICE_LCMD = 0x0008, DEVICE_RCMD = 0x0010, DEVICE_LALT = 0x0020, DEVICE_RALT = 0x0040, DEVICE_RCTL = 0x2000, }; @interface YaslView : NSView @property (nonatomic, assign) struct yasl_surface *surface; @end @interface YaslAppDelegate : NSObject @end @interface YaslWindow : NSWindow @property (nonatomic, assign) struct yasl_surface *surface; @end @interface YaslWindowController : NSWindowController @property (nonatomic, assign) struct yasl_surface *surface; @property (nonatomic, assign) BOOL fullscreenTransition; @end #define YASL_QUEUE_CAP 256 struct input_queue { struct yasl_input_event events[YASL_QUEUE_CAP]; int head; int tail; int count; }; static inline void input_queue_push(struct input_queue *q, const struct yasl_input_event *ev) { if (q->count >= YASL_QUEUE_CAP) return; q->events[q->tail] = *ev; q->tail = (q->tail + 1) % YASL_QUEUE_CAP; q->count++; } static inline bool input_queue_pop(struct input_queue *q, struct yasl_input_event *ev) { if (q->count <= 0) return false; *ev = q->events[q->head]; q->head = (q->head + 1) % YASL_QUEUE_CAP; q->count--; return true; } static inline void input_queue_clear(struct input_queue *q) { q->head = q->tail = q->count = 0; } struct yasl_surface { YaslWindow *window; YaslWindowController *controller; YaslView *view; CAMetalLayer *layer; CADisplayLink *link; const struct yasl_surface_cbs *cbs; void *ud; uint64_t frames_skipped; char *preedit; struct yasl_rect2i ime_caret; char *key_text; size_t key_text_capacity; size_t key_text_len; bool in_key_down; struct yasl_vector2i size; uint32_t scale_120; uint32_t bg_color; uint32_t *pixels; size_t pixel_capacity; struct yasl_frame frame; struct yasl_damage_list damage; bool did_present; unsigned idle_frames; bool want_draw; bool frame_due; bool unpaced; bool rendering; bool closed; bool destroyed; struct yasl_surface *dead_next; struct input_queue input; struct yasl_vector2i pointer; double scroll_x; uint32_t state; uint32_t mods; enum yasl_cursor cursor; char title[160]; struct yasl_frame_clock clock; }; struct yasl_display { struct yasl_surface **windows; int count; int capacity; const struct yasl_display_cbs *cbs; void *ud; char *app_id; struct yasl_loop *loop; id key_up_monitor; YaslAppDelegate *delegate; NSPoint cascade; struct yasl_surface *dead; int depth; bool open; bool launched; bool pumping; }; static struct yasl_display darwin_ctx; static void render_and_commit(struct yasl_surface *w); static void reap_dead(struct yasl_display *d); static uint64_t media_to_loop_ns(CFTimeInterval t) { double delta = (t - CACurrentMediaTime()) * 1e9; uint64_t now = yasl_loop_now_ns(); if (delta < 0.0 && -delta >= (double)now) return 0; return (uint64_t)((double)now + delta); } static void set_state(struct yasl_surface *w, uint32_t bit, bool on) { if (!w || w->closed) return; uint32_t next = on ? (w->state | bit) : (w->state & ~bit); if (next == w->state) return; w->state = next; darwin_ctx.depth++; if (w->cbs->state) w->cbs->state(w, w->state, w->ud); darwin_ctx.depth--; } static void push_focus(struct yasl_surface *w, bool focused) { if (!w || w->closed) return; struct yasl_input_event ev = { .kind = YASL_INPUT_FOCUS, .surface = w, .focus = { .focused = focused }, }; input_queue_push(&w->input, &ev); darwin_ctx.depth++; set_state(w, YASL_STATE_ACTIVATED, focused); yasl_surface_schedule_redraw(w); darwin_ctx.depth--; } static void sync_size(struct yasl_surface *w, bool force) { if (!w || !w->layer || w->closed) return; uint64_t start = yasl_loop_now_ns(); NSRect bounds = w->view.bounds; CGFloat scale = w->window.backingScaleFactor; if (scale < 1.0) scale = 1.0; struct yasl_vector2i size = { .x = (int)lround(bounds.size.width * scale), .y = (int)lround(bounds.size.height * scale), }; uint32_t scale_120 = (uint32_t)lround(scale * YASL_SCALE_UNIT); bool rescaled = force || scale_120 != w->scale_120; bool resized = force || (size.x != w->size.x || size.y != w->size.y); if (!rescaled && !resized) return; darwin_ctx.depth++; w->size = size; w->scale_120 = scale_120; w->layer.contentsScale = scale; w->layer.drawableSize = CGSizeMake(size.x, size.y); if (w->cbs->configure) w->cbs->configure(w, (struct yasl_geometry){ .size = size, .scale_120 = scale_120, }, w->ud); yasl_surface_schedule_redraw(w); if (w->view.inLiveResize) render_and_commit(w); yasl_clock_note_resize(&w->clock, yasl_loop_now_ns() - start); darwin_ctx.depth--; } static unsigned cocoa_mods(NSEventModifierFlags m) { unsigned mods = 0; if (m & NSEventModifierFlagShift) mods |= YASL_MOD_SHIFT; if (m & NSEventModifierFlagOption) mods |= YASL_MOD_ALT; if (m & NSEventModifierFlagControl) mods |= YASL_MOD_CTRL; if (m & NSEventModifierFlagCommand) mods |= YASL_MOD_SUPER; if (m & NSEventModifierFlagCapsLock) mods |= YASL_MOD_CAPS; return mods; } static struct yasl_key_event cocoa_key_event(NSEvent *ev, enum yasl_key_action action) { unsigned short vk = ev.keyCode; struct yasl_key_event k = { .key = vk, .keypad = (vk >= kVK_ANSI_Keypad0 && vk <= kVK_ANSI_Keypad9) || vk == kVK_ANSI_KeypadDecimal || vk == kVK_ANSI_KeypadMultiply || vk == kVK_ANSI_KeypadPlus || vk == kVK_ANSI_KeypadClear || vk == kVK_ANSI_KeypadDivide || vk == kVK_ANSI_KeypadEnter || vk == kVK_ANSI_KeypadMinus || vk == kVK_ANSI_KeypadEquals, .action = action, .mods = cocoa_mods(ev.modifierFlags), .base = vk, }; NSString *chars = ev.characters; if (chars && chars.length > 0) k.text = [chars characterAtIndex:0]; return k; } static void on_key_event(struct yasl_surface *w, NSEvent *ev, enum yasl_key_action action) { w->mods = cocoa_mods(ev.modifierFlags); struct yasl_key_event k = cocoa_key_event(ev, action); struct yasl_input_event input_ev = { .kind = YASL_INPUT_KEY, .surface = w, .key = k, }; input_queue_push(&w->input, &input_ev); } static void on_text_input(struct yasl_surface *w, const char *text, size_t len) { if (len) { struct yasl_input_event ev = { .kind = YASL_INPUT_TEXT, .surface = w, .text = { .text = text, .len = len }, }; input_queue_push(&w->input, &ev); } } static bool modifier_down(NSEvent *ev) { NSEventModifierFlags f = ev.modifierFlags; switch (ev.keyCode) { case kVK_Shift: return f & DEVICE_LSHIFT; case kVK_RightShift: return f & DEVICE_RSHIFT; case kVK_Control: return f & DEVICE_LCTL; case kVK_RightControl: return f & DEVICE_RCTL; case kVK_Option: return f & DEVICE_LALT; case kVK_RightOption: return f & DEVICE_RALT; case kVK_Command: return f & DEVICE_LCMD; case kVK_RightCommand: return f & DEVICE_RCMD; case kVK_CapsLock: return f & NSEventModifierFlagCapsLock; default: return false; } } static int cocoa_button(NSInteger number) { switch (number) { case 0: return 0; case 1: return 2; case 2: return 1; default: return -1; } } static struct yasl_vector2i event_pos(struct yasl_surface *w, NSEvent *ev) { NSPoint p = [w->view convertPoint:ev.locationInWindow fromView:nil]; CGFloat scale = w->window.backingScaleFactor; if (scale < 1.0) scale = 1.0; return (struct yasl_vector2i){ .x = (int)floor(p.x * scale), .y = (int)floor((w->view.bounds.size.height - p.y) * scale), }; } static void push_pointer(struct yasl_surface *w, NSEvent *ev, enum yasl_pointer_kind kind, int button) { if (!w || w->closed || button < 0) return; w->mods = cocoa_mods(ev.modifierFlags); if (kind != YASL_PTR_LEAVE) w->pointer = event_pos(w, ev); enum yasl_pointer_device device = YASL_PTR_DEV_MOUSE; struct yasl_pointer_pen pen = {0}; float press = (float)ev.pressure; if (ev.subtype == NSEventSubtypeTabletPoint || press > 0.0f) { if (ev.pointingDeviceType == NSPointingDeviceTypeEraser) device = YASL_PTR_DEV_ERASER; else if (ev.pointingDeviceType == NSPointingDeviceTypePen) device = YASL_PTR_DEV_PEN; if (press > 1.0f) press = 1.0f; if (press > 0.0f) pen.pressure = (uint16_t)(press * 65535.0f); NSPoint tilt = ev.tilt; pen.tilt_x = (int8_t)(tilt.x * 90.0); pen.tilt_y = (int8_t)(tilt.y * 90.0); } struct yasl_input_event input_ev = { .surface = w, .kind = YASL_INPUT_MOUSE, .mouse = { .pos = w->pointer, .button = (int16_t)button, .kind = kind, .device = device, .mods = w->mods, .pen = pen, }, }; input_queue_push(&w->input, &input_ev); } static NSCursor *cocoa_cursor(enum yasl_cursor cursor) { switch (cursor) { case YASL_CURSOR_TEXT: return [NSCursor IBeamCursor]; case YASL_CURSOR_LINK: return [NSCursor pointingHandCursor]; case YASL_CURSOR_CROSSHAIR: return [NSCursor crosshairCursor]; case YASL_CURSOR_ARROW: default: return [NSCursor arrowCursor]; } } @implementation YaslAppDelegate - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { (void)sender; if (darwin_ctx.cbs && darwin_ctx.cbs->quit) darwin_ctx.cbs->quit(darwin_ctx.ud); return NSTerminateCancel; } - (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)visible { (void)sender; if (!visible && darwin_ctx.cbs && darwin_ctx.cbs->reopen) darwin_ctx.cbs->reopen(darwin_ctx.ud); return YES; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { (void)sender; return NO; } @end @implementation YaslView - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { self.wantsLayer = YES; self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawNever; self.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft; [self registerForDraggedTypes:@[NSPasteboardTypeFileURL]]; } return self; } - (NSDragOperation)draggingEntered:(id)sender { NSPasteboard *pb = [sender draggingPasteboard]; if ([pb canReadItemWithDataConformingToTypes:@[NSPasteboardTypeFileURL]]) return NSDragOperationCopy; return NSDragOperationNone; } - (NSDragOperation)draggingUpdated:(id)sender { return [self draggingEntered:sender]; } - (BOOL)performDragOperation:(id)sender { struct yasl_surface *w = self.surface; if (!w) return NO; NSPasteboard *pb = [sender draggingPasteboard]; NSArray *urls = [pb readObjectsForClasses:@[[NSURL class]] options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}]; if (!urls.count) return NO; NSMutableString *result = [NSMutableString string]; for (NSURL *url in urls) { [result appendString:url.absoluteString]; [result appendString:@"\r\n"]; } const char *utf8 = result.UTF8String; if (utf8 && strlen(utf8)) { struct yasl_input_event ev = { .kind = YASL_INPUT_DROP_FILES, .surface = w, .drop_files = { .files = utf8, .len = strlen(utf8) }, }; input_queue_push(&w->input, &ev); yasl_surface_schedule_redraw(w); return YES; } return NO; } - (CALayer *)makeBackingLayer { CAMetalLayer *layer = [CAMetalLayer layer]; layer.pixelFormat = MTLPixelFormatBGRA8Unorm; layer.framebufferOnly = YES; layer.allowsNextDrawableTimeout = YES; layer.opaque = YES; layer.contentsGravity = kCAGravityTopLeft; CGColorSpaceRef srgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); layer.colorspace = srgb; CGColorSpaceRelease(srgb); return layer; } - (BOOL)isOpaque { return YES; } - (BOOL)acceptsFirstResponder { return YES; } - (BOOL)becomeFirstResponder { return YES; } - (BOOL)canBecomeKeyView { return YES; } - (BOOL)wantsUpdateLayer { return YES; } - (void)setFrameSize:(NSSize)size { [super setFrameSize:size]; sync_size(self.surface, false); } - (void)viewDidChangeBackingProperties { [super viewDidChangeBackingProperties]; sync_size(self.surface, false); } - (void)viewWillStartLiveResize { [super viewWillStartLiveResize]; struct yasl_surface *w = self.surface; if (w) w->layer.presentsWithTransaction = YES; } - (void)viewDidEndLiveResize { [super viewDidEndLiveResize]; struct yasl_surface *w = self.surface; if (!w) return; w->layer.presentsWithTransaction = NO; yasl_surface_schedule_redraw(w); } - (void)displayLinkFired:(CADisplayLink *)link { struct yasl_surface *w = self.surface; if (!w || w->closed) return; CFTimeInterval period = link.targetTimestamp - link.timestamp; if (period > 0.0 && period < 0.25) w->clock.refresh_ns = period * 1e9; w->clock.last_present_ns = media_to_loop_ns(link.timestamp); w->frame_due = true; if (!w->want_draw || (w->state & YASL_STATE_SUSPENDED)) { if (++w->idle_frames >= LINK_IDLE_FRAMES) link.paused = YES; return; } w->idle_frames = 0; if (darwin_ctx.loop) yasl_loop_kick(darwin_ctx.loop); } - (void)updateTrackingAreas { [super updateTrackingAreas]; if (self.trackingAreas.count > 0) return; NSTrackingArea *area = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingCursorUpdate | NSTrackingActiveAlways | NSTrackingInVisibleRect owner:self userInfo:nil]; [self addTrackingArea:area]; [area release]; } - (void)cursorUpdate:(NSEvent *)event { (void)event; struct yasl_surface *w = self.surface; [cocoa_cursor(w ? w->cursor : YASL_CURSOR_ARROW) set]; } - (void)keyDown:(NSEvent *)event { struct yasl_surface *w = self.surface; if (!w) return; on_key_event(w, event, event.isARepeat ? YASL_KEY_REPEAT : YASL_KEY_PRESS); NSString *chars = event.characters; if (chars && chars.length > 0) on_text_input(w, chars.UTF8String, strlen(chars.UTF8String)); } - (void)keyUp:(NSEvent *)event { struct yasl_surface *w = self.surface; if (w) on_key_event(w, event, YASL_KEY_RELEASE); } - (void)flagsChanged:(NSEvent *)event { struct yasl_surface *w = self.surface; if (!w) return; w->mods = cocoa_mods(event.modifierFlags); on_key_event(w, event, modifier_down(event) ? YASL_KEY_PRESS : YASL_KEY_RELEASE); } - (void)mouseDown:(NSEvent *)event { [self.window makeFirstResponder:self]; push_pointer(self.surface, event, YASL_PTR_PRESS, cocoa_button(event.buttonNumber)); } - (void)rightMouseDown:(NSEvent *)event { [self mouseDown:event]; } - (void)otherMouseDown:(NSEvent *)event { [self mouseDown:event]; } - (void)mouseUp:(NSEvent *)event { push_pointer(self.surface, event, YASL_PTR_RELEASE, cocoa_button(event.buttonNumber)); } - (void)rightMouseUp:(NSEvent *)event { [self mouseUp:event]; } - (void)otherMouseUp:(NSEvent *)event { [self mouseUp:event]; } - (void)mouseMoved:(NSEvent *)event { push_pointer(self.surface, event, YASL_PTR_MOTION, 3); } - (void)mouseDragged:(NSEvent *)event { push_pointer(self.surface, event, YASL_PTR_MOTION, 0); } - (void)rightMouseDragged:(NSEvent *)event { push_pointer(self.surface, event, YASL_PTR_MOTION, 2); } - (void)otherMouseDragged:(NSEvent *)event { push_pointer(self.surface, event, YASL_PTR_MOTION, cocoa_button(event.buttonNumber)); } - (void)mouseExited:(NSEvent *)event { push_pointer(self.surface, event, YASL_PTR_LEAVE, 3); } - (void)scrollWheel:(NSEvent *)event { struct yasl_surface *w = self.surface; if (!w) return; double dx = event.scrollingDeltaX, dy = event.scrollingDeltaY; if (event.hasPreciseScrollingDeltas) { dx /= SCROLL_POINTS_PER_LINE; dy /= SCROLL_POINTS_PER_LINE; } w->mods = cocoa_mods(event.modifierFlags); w->pointer = event_pos(w, event); if (dy != 0.0) { struct yasl_input_event input_ev = { .kind = YASL_INPUT_MOUSE, .surface = w, .mouse = { .pos = w->pointer, .button = dy > 0.0 ? 4 : 5, .kind = YASL_PTR_SCROLL, .mods = w->mods, .scroll_delta = (float)dy, .scroll_lines = (int)dy, }, }; input_queue_push(&w->input, &input_ev); } } static NSString *plain_string(id string) { return [string isKindOfClass:[NSAttributedString class]] ? [(NSAttributedString *)string string] : (NSString *)string; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { (void)replacementRange; struct yasl_surface *w = self.surface; if (!w) return; const char *utf8 = plain_string(string).UTF8String; size_t len = utf8 ? strlen(utf8) : 0; free(w->preedit); w->preedit = NULL; on_text_input(w, utf8, len); yasl_surface_schedule_redraw(w); } - (void)doCommandBySelector:(SEL)selector { (void)selector; } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { (void)selectedRange; (void)replacementRange; struct yasl_surface *w = self.surface; if (!w) return; NSString *str = plain_string(string); const char *utf8 = str.length ? str.UTF8String : NULL; free(w->preedit); w->preedit = utf8 ? strdup(utf8) : NULL; yasl_surface_schedule_redraw(w); } - (void)unmarkText { struct yasl_surface *w = self.surface; if (!w || !w->preedit) return; free(w->preedit); w->preedit = NULL; yasl_surface_schedule_redraw(w); } - (NSRange)selectedRange { return NSMakeRange(NSNotFound, 0); } - (NSRange)markedRange { struct yasl_surface *w = self.surface; if (!w || !w->preedit) return NSMakeRange(NSNotFound, 0); return NSMakeRange(0, [NSString stringWithUTF8String:w->preedit].length); } - (BOOL)hasMarkedText { return self.surface && self.surface->preedit != NULL; } - (nullable NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange { (void)range; (void)actualRange; return nil; } - (NSArray *)validAttributesForMarkedText { return @[]; } - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(nullable NSRangePointer)actualRange { (void)range; (void)actualRange; struct yasl_surface *w = self.surface; if (!w || !w->window) return NSZeroRect; CGFloat scale = w->window.backingScaleFactor; if (scale < 1.0) scale = 1.0; NSRect r = NSMakeRect(w->ime_caret.x / scale, self.bounds.size.height - (w->ime_caret.y + w->ime_caret.height) / scale, w->ime_caret.width / scale, w->ime_caret.height / scale); return [w->window convertRectToScreen:[self convertRect:r toView:nil]]; } - (NSUInteger)characterIndexForPoint:(NSPoint)point { (void)point; return 0; } @end @implementation YaslWindow - (void)sendEvent:(NSEvent *)event { darwin_ctx.depth++; @try { [super sendEvent:event]; } @finally { darwin_ctx.depth--; } } @end @implementation YaslWindowController - (void)windowDidBecomeKey:(NSNotification *)notification { (void)notification; if (self.surface) push_focus(self.surface, true); } - (void)windowDidResignKey:(NSNotification *)notification { (void)notification; if (self.surface) push_focus(self.surface, false); } - (void)windowDidChangeOcclusionState:(NSNotification *)notification { (void)notification; struct yasl_surface *w = self.surface; if (!w) return; bool visible = (self.window.occlusionState & NSWindowOcclusionStateVisible) != 0; darwin_ctx.depth++; set_state(w, YASL_STATE_SUSPENDED, !visible); if (visible) yasl_surface_schedule_redraw(w); else w->link.paused = YES; darwin_ctx.depth--; } - (void)windowWillEnterFullScreen:(NSNotification *)notification { (void)notification; self.fullscreenTransition = YES; } - (void)windowWillExitFullScreen:(NSNotification *)notification { (void)notification; self.fullscreenTransition = YES; } - (void)syncWindowState { self.fullscreenTransition = NO; struct yasl_surface *w = self.surface; if (!w || w->closed) return; darwin_ctx.depth++; bool fullscreen = (self.window.styleMask & NSWindowStyleMaskFullScreen) != 0; set_state(w, YASL_STATE_FULLSCREEN, fullscreen); set_state(w, YASL_STATE_MAXIMIZED, !fullscreen && self.window.isZoomed); sync_size(w, false); darwin_ctx.depth--; } - (void)windowDidEnterFullScreen:(NSNotification *)notification { [self syncWindowState]; } - (void)windowDidExitFullScreen:(NSNotification *)notification { [self syncWindowState]; } - (void)windowDidFailToEnterFullScreen:(NSWindow *)window { (void)window; [self syncWindowState]; } - (void)windowDidFailToExitFullScreen:(NSWindow *)window { (void)window; [self syncWindowState]; } - (void)windowDidResize:(NSNotification *)notification { (void)notification; if (!self.fullscreenTransition) [self syncWindowState]; } - (void)windowDidChangeScreen:(NSNotification *)notification { (void)notification; sync_size(self.surface, false); } - (void)windowWillClose:(NSNotification *)notification { (void)notification; struct yasl_surface *w = self.surface; if (!w || w->closed) return; darwin_ctx.depth++; w->closed = true; [w->link invalidate]; if (w->cbs->close) w->cbs->close(w, w->ud); darwin_ctx.depth--; } @end bool yasl_surface_present(struct yasl_surface *w, const struct yasl_framebuffer *fb, const struct yasl_damage_list *damage) { (void)damage; if (!w || w->closed || !w->rendering || w->did_present || !fb || !fb->native_drawable || fb->native_drawable != w->frame.target.native_drawable) return false; w->did_present = true; return true; } static void render_and_commit(struct yasl_surface *w) { if (w->closed || !w->cbs->draw || w->rendering || w->size.x <= 0 || w->size.y <= 0 || (w->state & YASL_STATE_SUSPENDED)) return; size_t count = (size_t)w->size.x * (size_t)w->size.y; if (count > w->pixel_capacity) { uint32_t *pixels = realloc(w->pixels, count * sizeof(*pixels)); if (!pixels) return; w->pixels = pixels; w->pixel_capacity = count; } darwin_ctx.depth++; w->rendering = true; w->want_draw = false; w->frame_due = false; w->did_present = false; id drawable = [w->layer nextDrawable]; w->damage = (struct yasl_damage_list){0}; w->frame = (struct yasl_frame){ .surface = w, .target = { .pixels = w->pixels, .size = w->size, .damage = &w->damage, .native_drawable = drawable, .transaction = w->layer.presentsWithTransaction, }, }; if (drawable) w->cbs->draw(w, &w->frame, w->ud); if (!w->closed && !w->did_present) { w->want_draw = true; w->frames_skipped++; } w->frame.target.native_drawable = NULL; w->rendering = false; darwin_ctx.depth--; } static void pump_window(struct yasl_surface *w) { if (!w || w->closed || !w->want_draw || !w->cbs->draw) return; if (w->link && !w->frame_due && !w->unpaced) return; @autoreleasepool { render_and_commit(w); } } int yasl_display_dispatch_timeout(struct yasl_display *d, int timeout_ms) { assert([NSThread isMainThread]); if (!d || d->pumping) return -1; d->pumping = true; d->depth++; int count = 0; @try { NSDate *limitDate; if (timeout_ms == 0) limitDate = [NSDate distantPast]; else if (timeout_ms < 0) limitDate = [NSDate distantFuture]; else limitDate = [NSDate dateWithTimeIntervalSinceNow:(double)timeout_ms / 1000.0]; @autoreleasepool { NSEvent *ev = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:limitDate inMode:NSDefaultRunLoopMode dequeue:YES]; if (ev) { count++; [NSApp sendEvent:ev]; [NSApp updateWindows]; while ((ev = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES])) { count++; [NSApp sendEvent:ev]; [NSApp updateWindows]; } } } } @finally { d->depth--; d->pumping = false; } return count; } void yasl_display_dispatch(struct yasl_display *d) { if (!d) return; bool immediate = false; for (int i = 0; i < d->count; i++) { struct yasl_surface *w = d->windows[i]; if (w->input.count > 0 || (w->want_draw && (!w->link || w->frame_due || w->unpaced))) { immediate = true; break; } } yasl_display_dispatch_timeout(d, immediate ? 0 : -1); } void yasl_display_flush(struct yasl_display *d) { assert([NSThread isMainThread]); if (!d) return; d->depth++; for (int i = d->count - 1; i >= 0; i--) { if (i < d->count) pump_window(d->windows[i]); } @autoreleasepool { [NSApp updateWindows]; } if (--d->depth == 0) reap_dead(d); } void yasl_surface_schedule_redraw(struct yasl_surface *w) { if (!w || w->closed) return; w->want_draw = true; w->idle_frames = 0; if (w->link.paused) { w->frame_due = true; w->link.paused = NO; } if (darwin_ctx.loop) yasl_loop_kick(darwin_ctx.loop); } static void app_init(const char *app_name) { if (darwin_ctx.launched) return; [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; NSWindow.allowsAutomaticWindowTabbing = NO; darwin_ctx.delegate = [[YaslAppDelegate alloc] init]; NSApp.delegate = darwin_ctx.delegate; NSString *ns_name = app_name ? [NSString stringWithUTF8String:app_name] : @"YASL"; NSMenu *menu = [[NSMenu alloc] init]; NSMenuItem *app_item = [[NSMenuItem alloc] init]; NSMenu *app_menu = [[NSMenu alloc] initWithTitle:ns_name]; [app_menu addItemWithTitle:[NSString stringWithFormat:@"About %@", ns_name] action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""]; [app_menu addItem:[NSMenuItem separatorItem]]; [app_menu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", ns_name] action:@selector(terminate:) keyEquivalent:@"q"]; app_item.submenu = app_menu; [menu addItem:app_item]; [app_menu release]; [app_item release]; NSApp.mainMenu = menu; [menu release]; darwin_ctx.key_up_monitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp handler:^NSEvent *(NSEvent *event) { if (!(event.modifierFlags & NSEventModifierFlagCommand) || !event.window) return event; [event.window sendEvent:event]; return nil; }]; [NSApp finishLaunching]; darwin_ctx.launched = true; } static void place_window(NSWindow *window) { if (!darwin_ctx.count) { [window center]; NSRect frame = window.frame; darwin_ctx.cascade = NSMakePoint(frame.origin.x, frame.origin.y + frame.size.height); return; } darwin_ctx.cascade = [window cascadeTopLeftFromPoint:darwin_ctx.cascade]; } struct yasl_display *yasl_display_open(struct yasl_loop *loop, const struct yasl_display_spec *spec) { assert([NSThread isMainThread]); if (darwin_ctx.open) return NULL; const char *name = spec && spec->app_id ? spec->app_id : "YASL"; @autoreleasepool { app_init(name); } darwin_ctx.open = true; darwin_ctx.loop = loop; if (spec) { darwin_ctx.cbs = spec->cbs; darwin_ctx.ud = spec->ud; if (spec->app_id) darwin_ctx.app_id = strdup(spec->app_id); } return &darwin_ctx; } void yasl_display_close(struct yasl_display *d) { if (!d) return; assert(!d->count && "destroy every surface before its display"); reap_dead(d); if (d->key_up_monitor) { [NSEvent removeMonitor:d->key_up_monitor]; d->key_up_monitor = nil; } NSApp.delegate = nil; [d->delegate release]; d->delegate = nil; free(d->app_id); free(d->windows); *d = (struct yasl_display){0}; } uint32_t yasl_display_caps(const struct yasl_display *d) { return d ? (YASL_CAP_PLACE_WINDOW | YASL_CAP_CSD) : 0; } struct yasl_surface *yasl_surface_create_toplevel(struct yasl_display *d, const struct yasl_surface_spec *spec) { assert([NSThread isMainThread]); if (!d || !d->open || !spec || !spec->graphics_device) return NULL; @autoreleasepool { if (darwin_ctx.count >= darwin_ctx.capacity) { int cap = darwin_ctx.capacity ? darwin_ctx.capacity * 2 : 16; struct yasl_surface **wins = realloc(darwin_ctx.windows, (size_t)cap * sizeof(*wins)); if (!wins) return NULL; darwin_ctx.windows = wins; darwin_ctx.capacity = cap; } struct yasl_surface *w = calloc(1, sizeof(*w)); if (!w) return NULL; yasl_surface_set_callbacks(w, NULL, NULL); w->clock.refresh_ns = YASL_REFRESH_DEFAULT_NS; const char *t = spec->title ? spec->title : "YASL"; strncpy(w->title, t, sizeof(w->title) - 1); int win_w = spec->size.x > 0 ? spec->size.x : 800; int win_h = spec->size.y > 0 ? spec->size.y : 480; NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; w->window = [[YaslWindow alloc] initWithContentRect:NSMakeRect(0, 0, win_w, win_h) styleMask:style backing:NSBackingStoreBuffered defer:NO]; w->window.releasedWhenClosed = NO; w->controller = [[YaslWindowController alloc] initWithWindow:w->window]; w->controller.surface = w; w->window.delegate = w->controller; w->window.acceptsMouseMovedEvents = YES; w->window.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary; w->window.titlebarAppearsTransparent = YES; w->window.title = [NSString stringWithUTF8String:w->title] ?: @"YASL"; w->window.colorSpace = [NSColorSpace sRGBColorSpace]; w->view = [[YaslView alloc] initWithFrame:w->window.contentView.bounds]; w->window.contentView = w->view; w->layer = (CAMetalLayer *)w->view.layer; w->layer.device = (id)spec->graphics_device; w->link = [[w->view displayLinkWithTarget:w->view selector:@selector(displayLinkFired:)] retain]; [w->link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; place_window(w->window); w->window.surface = w; w->view.surface = w; darwin_ctx.windows[darwin_ctx.count++] = w; [w->window makeKeyAndOrderFront:nil]; [w->window makeFirstResponder:w->view]; [NSApp activate]; sync_size(w, true); if (spec->tearing) { w->layer.displaySyncEnabled = NO; w->unpaced = true; } if (spec->fullscreen) [w->window toggleFullScreen:nil]; return w; } } static void surface_free(struct yasl_surface *w) { [w->link release]; [w->view release]; [w->controller release]; [w->window release]; free(w->key_text); free(w->preedit); free(w->pixels); free(w); } static void reap_dead(struct yasl_display *d) { assert(d->depth == 0); while (d->dead) { struct yasl_surface *w = d->dead; d->dead = w->dead_next; surface_free(w); } } void yasl_surface_destroy(struct yasl_surface *w) { assert([NSThread isMainThread]); if (!w || w->destroyed) return; darwin_ctx.depth++; bool native_closed = w->closed; static const struct yasl_surface_cbs no_callbacks = {0}; w->destroyed = true; w->closed = true; w->cbs = &no_callbacks; w->ud = NULL; [w->link invalidate]; w->window.delegate = nil; w->controller.surface = NULL; w->window.surface = NULL; w->view.surface = NULL; if (!native_closed) [w->window close]; input_queue_clear(&w->input); w->dead_next = darwin_ctx.dead; darwin_ctx.dead = w; for (int i = 0; i < darwin_ctx.count; i++) { if (darwin_ctx.windows[i] == w) { darwin_ctx.windows[i] = darwin_ctx.windows[--darwin_ctx.count]; break; } } if (!darwin_ctx.count) { free(darwin_ctx.windows); darwin_ctx.windows = NULL; darwin_ctx.capacity = 0; darwin_ctx.cascade = NSZeroPoint; } darwin_ctx.depth--; if (darwin_ctx.loop) yasl_loop_kick(darwin_ctx.loop); } struct yasl_display *yasl_surface_display(const struct yasl_surface *w) { return w ? &darwin_ctx : NULL; } void yasl_surface_set_title(struct yasl_surface *w, const char *title) { if (!w || w->closed || !title) return; strncpy(w->title, title, sizeof(w->title) - 1); w->window.title = [NSString stringWithUTF8String:w->title] ?: @"YASL"; } void yasl_surface_set_background_color(struct yasl_surface *w, uint32_t bg) { if (!w || w->closed) return; w->bg_color = bg; double r = ((bg >> 16) & 0xff) / 255.0; double g = ((bg >> 8) & 0xff) / 255.0; double b = (bg & 0xff) / 255.0; w->window.backgroundColor = [NSColor colorWithSRGBRed:r green:g blue:b alpha:1.0]; bool dark = 0.299 * r + 0.587 * g + 0.114 * b < 0.5; w->window.appearance = [NSAppearance appearanceNamed: dark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua]; } void yasl_surface_set_cursor(struct yasl_surface *w, enum yasl_cursor cursor) { if (!w || w->closed || cursor == w->cursor) return; w->cursor = cursor; if (cursor == YASL_CURSOR_NONE) { [NSCursor setHiddenUntilMouseMoves:YES]; return; } NSPoint p = [w->view convertPoint:w->window.mouseLocationOutsideOfEventStream fromView:nil]; if (w->window.isKeyWindow && NSPointInRect(p, w->view.bounds)) [cocoa_cursor(cursor) set]; } bool yasl_surface_place_under_pointer(struct yasl_surface *w, struct yasl_vector2i at) { if (!w || w->closed || !w->window) return false; CGFloat scale = w->window.backingScaleFactor; if (scale < 1.0) scale = 1.0; NSPoint mouse = [NSEvent mouseLocation]; NSRect content = [w->window contentRectForFrameRect:w->window.frame]; NSPoint origin = NSMakePoint(mouse.x - at.x / scale, mouse.y - content.size.height + at.y / scale); [w->window setFrameOrigin:[w->window frameRectForContentRect: (NSRect){ .origin = origin, .size = content.size }].origin]; return true; } void yasl_surface_set_ime_caret(struct yasl_surface *w, struct yasl_rect2i caret) { if (w) w->ime_caret = caret; } const char *yasl_surface_ime_preedit(const struct yasl_surface *w) { return w ? w->preedit : NULL; } bool yasl_surface_ime_preedit_cursor(const struct yasl_surface *w, int *begin, int *end) { (void)w; (void)begin; (void)end; return false; } void yasl_display_copy(struct yasl_display *d, enum yasl_selection sel, const char *text) { (void)d; (void)sel; if (!text) return; NSString *s = [NSString stringWithUTF8String:text]; if (!s) return; NSPasteboard *pb = [NSPasteboard generalPasteboard]; [pb clearContents]; [pb setString:s forType:NSPasteboardTypeString]; } void yasl_display_paste(struct yasl_display *d, enum yasl_selection sel, struct yasl_surface *target) { (void)d; (void)sel; if (!target) return; NSString *s = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]; const char *utf8 = s.length ? s.UTF8String : NULL; if (utf8) { struct yasl_input_event ev = { .kind = YASL_INPUT_PASTE, .surface = target, .paste = { .text = utf8, .len = strlen(utf8) }, }; input_queue_push(&target->input, &ev); } } int yasl_display_modifiers(const struct yasl_display *d) { (void)d; return (int)cocoa_mods(NSEvent.modifierFlags); } struct yasl_vector2i yasl_surface_size(const struct yasl_surface *w) { return w ? w->size : (struct yasl_vector2i){0}; } uint32_t yasl_surface_scale_120(const struct yasl_surface *w) { return w ? w->scale_120 : YASL_SCALE_UNIT; } struct yasl_surface *yasl_surface_create_popup(struct yasl_surface *parent, const struct yasl_popup_spec *spec) { (void)parent; (void)spec; return NULL; } bool yasl_surface_popup_reposition(struct yasl_surface *popup, const struct yasl_popup_spec *spec) { (void)popup; (void)spec; return false; } bool yasl_display_drag_start(struct yasl_display *d, struct yasl_surface *origin, const struct yasl_drag_image *image, const struct yasl_drag_listener *l, void *ud) { (void)d; (void)origin; (void)image; (void)l; (void)ud; return false; } void yasl_display_drag_cancel(struct yasl_display *d) { (void)d; } bool yasl_surface_drag_carry(struct yasl_surface *w, struct yasl_vector2i offset) { (void)w; (void)offset; return false; } bool yasl_surface_start_interactive_move(struct yasl_surface *s) { if (!s || !s->window) return false; [s->window performWindowDragWithEvent:[NSApp currentEvent]]; return true; } bool yasl_surface_start_interactive_resize(struct yasl_surface *s, enum yasl_resize_edge edge) { (void)s; (void)edge; return false; } bool yasl_surface_show_window_menu(struct yasl_surface *s, struct yasl_vector2i pos) { (void)s; (void)pos; return false; } void *yasl_display_get_native_handle(struct yasl_display *d) { return d ? (void *)d : NULL; } void *yasl_surface_get_native_handle(struct yasl_surface *s) { return s ? (void *)s->view : NULL; }