summaryrefslogtreecommitdiff
path: root/src/cocoa
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-11 21:53:42 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-11 21:53:42 +0300
commit927ae621c9a3d46dbc401b8e9ea7bbed1bf2e97b (patch)
treeaeb00c16770dcf5a36d28c1167cc39c6759059fb /src/cocoa
initial: Yet Another Surface Library (YASL) 0.1.0
A minimal, modern desktop window surface library in C. Supports Wayland on Linux and Cocoa/Metal on macOS with presentation timing, fractional scaling, IME, and CSD.
Diffstat (limited to 'src/cocoa')
-rw-r--r--src/cocoa/keycodes.h130
-rw-r--r--src/cocoa/window.m1280
2 files changed, 1410 insertions, 0 deletions
diff --git a/src/cocoa/keycodes.h b/src/cocoa/keycodes.h
new file mode 100644
index 0000000..f3c06be
--- /dev/null
+++ b/src/cocoa/keycodes.h
@@ -0,0 +1,130 @@
+#ifndef SURFACE_DARWIN_KEYCODES_H
+#define SURFACE_DARWIN_KEYCODES_H
+
+/* Virtual keycodes for standard physical keys on macOS. */
+enum {
+ kVK_ANSI_A = 0x00,
+ kVK_ANSI_S = 0x01,
+ kVK_ANSI_D = 0x02,
+ kVK_ANSI_F = 0x03,
+ kVK_ANSI_H = 0x04,
+ kVK_ANSI_G = 0x05,
+ kVK_ANSI_Z = 0x06,
+ kVK_ANSI_X = 0x07,
+ kVK_ANSI_C = 0x08,
+ kVK_ANSI_V = 0x09,
+ kVK_ANSI_B = 0x0B,
+ kVK_ANSI_Q = 0x0C,
+ kVK_ANSI_W = 0x0D,
+ kVK_ANSI_E = 0x0E,
+ kVK_ANSI_R = 0x0F,
+ kVK_ANSI_Y = 0x10,
+ kVK_ANSI_T = 0x11,
+ kVK_ANSI_1 = 0x12,
+ kVK_ANSI_2 = 0x13,
+ kVK_ANSI_3 = 0x14,
+ kVK_ANSI_4 = 0x15,
+ kVK_ANSI_6 = 0x16,
+ kVK_ANSI_5 = 0x17,
+ kVK_ANSI_Equal = 0x18,
+ kVK_ANSI_9 = 0x19,
+ kVK_ANSI_7 = 0x1A,
+ kVK_ANSI_Minus = 0x1B,
+ kVK_ANSI_8 = 0x1C,
+ kVK_ANSI_0 = 0x1D,
+ kVK_ANSI_RightBracket = 0x1E,
+ kVK_ANSI_O = 0x1F,
+ kVK_ANSI_U = 0x20,
+ kVK_ANSI_LeftBracket = 0x21,
+ kVK_ANSI_I = 0x22,
+ kVK_ANSI_P = 0x23,
+ kVK_ANSI_L = 0x25,
+ kVK_ANSI_J = 0x26,
+ kVK_ANSI_Quote = 0x27,
+ kVK_ANSI_K = 0x28,
+ kVK_ANSI_Semicolon = 0x29,
+ kVK_ANSI_Backslash = 0x2A,
+ kVK_ANSI_Comma = 0x2B,
+ kVK_ANSI_Slash = 0x2C,
+ kVK_ANSI_N = 0x2D,
+ kVK_ANSI_M = 0x2E,
+ kVK_ANSI_Period = 0x2F,
+ kVK_ANSI_Grave = 0x32,
+ kVK_ANSI_KeypadDecimal = 0x41,
+ kVK_ANSI_KeypadMultiply = 0x43,
+ kVK_ANSI_KeypadPlus = 0x45,
+ kVK_ANSI_KeypadClear = 0x47,
+ kVK_ANSI_KeypadDivide = 0x4B,
+ kVK_ANSI_KeypadEnter = 0x4C,
+ kVK_ANSI_KeypadMinus = 0x4E,
+ kVK_ANSI_KeypadEquals = 0x51,
+ kVK_ANSI_Keypad0 = 0x52,
+ kVK_ANSI_Keypad1 = 0x53,
+ kVK_ANSI_Keypad2 = 0x54,
+ kVK_ANSI_Keypad3 = 0x55,
+ kVK_ANSI_Keypad4 = 0x56,
+ kVK_ANSI_Keypad5 = 0x57,
+ kVK_ANSI_Keypad6 = 0x58,
+ kVK_ANSI_Keypad7 = 0x59,
+ kVK_ANSI_Keypad8 = 0x5B,
+ kVK_ANSI_Keypad9 = 0x5C,
+
+ kVK_Return = 0x24,
+ kVK_Tab = 0x30,
+ kVK_Space = 0x31,
+ kVK_Delete = 0x33,
+ kVK_Escape = 0x35,
+ kVK_Command = 0x37,
+ kVK_Shift = 0x38,
+ kVK_CapsLock = 0x39,
+ kVK_Option = 0x3A,
+ kVK_Control = 0x3B,
+ kVK_RightCommand = 0x36,
+ kVK_RightShift = 0x3C,
+ kVK_RightOption = 0x3D,
+ kVK_RightControl = 0x3E,
+ kVK_Function = 0x3F,
+ kVK_F17 = 0x40,
+ kVK_VolumeUp = 0x48,
+ kVK_VolumeDown = 0x49,
+ kVK_Mute = 0x4A,
+ kVK_F18 = 0x4F,
+ kVK_F19 = 0x50,
+ kVK_F20 = 0x5A,
+ kVK_F5 = 0x60,
+ kVK_F6 = 0x61,
+ kVK_F7 = 0x62,
+ kVK_F3 = 0x63,
+ kVK_F8 = 0x64,
+ kVK_F9 = 0x65,
+ kVK_F11 = 0x67,
+ kVK_F13 = 0x69,
+ kVK_F16 = 0x6A,
+ kVK_F14 = 0x6B,
+ kVK_F10 = 0x6D,
+ kVK_ContextualMenu = 0x6E,
+ kVK_F12 = 0x6F,
+ kVK_F15 = 0x71,
+ kVK_Help = 0x72,
+ kVK_Home = 0x73,
+ kVK_PageUp = 0x74,
+ kVK_ForwardDelete = 0x75,
+ kVK_F4 = 0x76,
+ kVK_End = 0x77,
+ kVK_F2 = 0x78,
+ kVK_PageDown = 0x79,
+ kVK_F1 = 0x7A,
+ kVK_LeftArrow = 0x7B,
+ kVK_RightArrow = 0x7C,
+ kVK_DownArrow = 0x7D,
+ kVK_UpArrow = 0x7E,
+
+ kVK_ISO_Section = 0x0A,
+ kVK_JIS_Yen = 0x5D,
+ kVK_JIS_Underscore = 0x5E,
+ kVK_JIS_KeypadComma = 0x5F,
+ kVK_JIS_Eisu = 0x66,
+ kVK_JIS_Kana = 0x68,
+};
+
+#endif
diff --git a/src/cocoa/window.m b/src/cocoa/window.m
new file mode 100644
index 0000000..0edf694
--- /dev/null
+++ b/src/cocoa/window.m
@@ -0,0 +1,1280 @@
+#import <AppKit/AppKit.h>
+#import <Metal/Metal.h>
+#import <QuartzCore/QuartzCore.h>
+
+#include "yasl/yasl.h"
+#include "yasl/timing.h"
+#include "cocoa/keycodes.h"
+
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 <NSTextInputClient>
+@property (nonatomic, assign) struct yasl_surface *surface;
+@end
+
+@interface YaslAppDelegate : NSObject <NSApplicationDelegate>
+@end
+
+@interface YaslWindow : NSWindow
+@property (nonatomic, assign) struct yasl_surface *surface;
+@end
+
+@interface YaslWindowController : NSWindowController <NSWindowDelegate>
+@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);
+ struct yasl_input_event input_ev = {
+ .kind = YASL_INPUT_MOUSE,
+ .surface = w,
+ .mouse = {
+ .pos = w->pointer,
+ .button = button,
+ .kind = kind,
+ .mods = w->mods,
+ },
+ };
+ 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<NSDraggingInfo>)sender
+{
+ NSPasteboard *pb = [sender draggingPasteboard];
+ if ([pb canReadItemWithDataConformingToTypes:@[NSPasteboardTypeFileURL]])
+ return NSDragOperationCopy;
+ return NSDragOperationNone;
+}
+
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
+{
+ return [self draggingEntered:sender];
+}
+
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
+{
+ struct yasl_surface *w = self.surface;
+ if (!w) return NO;
+ NSPasteboard *pb = [sender draggingPasteboard];
+ NSArray<NSURL *> *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<NSAttributedStringKey> *)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<CAMetalDrawable> 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); }
+}
+
+void yasl_display_dispatch(struct yasl_display *d)
+{
+ assert([NSThread isMainThread]);
+ if (!d || d->pumping)
+ return;
+ d->pumping = true;
+ d->depth++;
+ @try {
+ for (;;) {
+ @autoreleasepool {
+ NSEvent *ev = [NSApp nextEventMatchingMask:NSEventMaskAny
+ untilDate:[NSDate distantPast]
+ inMode:NSDefaultRunLoopMode
+ dequeue:YES];
+ if (!ev) break;
+ [NSApp sendEvent:ev];
+ [NSApp updateWindows];
+ }
+ }
+ } @finally {
+ d->depth--;
+ d->pumping = false;
+ }
+}
+
+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<MTLDevice>)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;
+}