From 927ae621c9a3d46dbc401b8e9ea7bbed1bf2e97b Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Sun, 11 Oct 2026 21:53:42 +0300 Subject: 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. --- src/cocoa/keycodes.h | 130 +++++ src/cocoa/window.m | 1280 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1410 insertions(+) create mode 100644 src/cocoa/keycodes.h create mode 100644 src/cocoa/window.m (limited to 'src/cocoa') 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 +#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); + 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)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); } +} + +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)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; +} -- cgit v1.2.3