@geastack/apple 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +54 -0
  3. package/dist/index.d.ts +127 -0
  4. package/dist/index.js +3076 -0
  5. package/generated/AVFoundation.d.ts +112 -0
  6. package/generated/ActivityKit.d.ts +7 -0
  7. package/generated/AppKit.d.ts +268 -0
  8. package/generated/CoreGraphics.d.ts +18 -0
  9. package/generated/CoreLocation.d.ts +28 -0
  10. package/generated/CoreMedia.d.ts +8 -0
  11. package/generated/Dispatch.d.ts +2 -0
  12. package/generated/Foundation.d.ts +13 -0
  13. package/generated/MapKit.d.ts +28 -0
  14. package/generated/Metal.d.ts +91 -0
  15. package/generated/MetalKit.d.ts +22 -0
  16. package/generated/Photos.d.ts +5 -0
  17. package/generated/QuartzCore.d.ts +12 -0
  18. package/generated/SwiftData.d.ts +11 -0
  19. package/generated/UIKit.d.ts +187 -0
  20. package/package.json +123 -0
  21. package/runtime/AVFoundation.js +136 -0
  22. package/runtime/ActivityKit.js +18 -0
  23. package/runtime/AppKit.js +313 -0
  24. package/runtime/CoreGraphics.js +15 -0
  25. package/runtime/CoreLocation.js +30 -0
  26. package/runtime/CoreMedia.js +7 -0
  27. package/runtime/Dispatch.js +11 -0
  28. package/runtime/Foundation.js +21 -0
  29. package/runtime/MapKit.js +22 -0
  30. package/runtime/Metal.js +126 -0
  31. package/runtime/MetalKit.js +15 -0
  32. package/runtime/Photos.js +9 -0
  33. package/runtime/QuartzCore.js +9 -0
  34. package/runtime/SwiftData.js +29 -0
  35. package/runtime/UIKit.js +242 -0
  36. package/runtime/empty.js +1 -0
  37. package/targets/ios/Info.plist.in +25 -0
  38. package/targets/ios/README.md +46 -0
  39. package/targets/ios/build-ios.sh +395 -0
  40. package/targets/ios/detect-development-team.mjs +109 -0
  41. package/targets/ios/generate-resident-entry.mjs +67 -0
  42. package/targets/ios/generate-resident-registry.mjs +162 -0
  43. package/targets/ios/generate-xcode-project.mjs +520 -0
  44. package/targets/ios/main/canvas_view.h +7 -0
  45. package/targets/ios/main/canvas_view.mm +94 -0
  46. package/targets/ios/main/font_registry.h +17 -0
  47. package/targets/ios/main/font_registry.mm +129 -0
  48. package/targets/ios/main/image_bridge.h +13 -0
  49. package/targets/ios/main/image_bridge.mm +53 -0
  50. package/targets/ios/main/ios_apps.c +28 -0
  51. package/targets/ios/main/ios_camera.mm +446 -0
  52. package/targets/ios/main/ios_display.mm +344 -0
  53. package/targets/ios/main/ios_main.mm +811 -0
  54. package/targets/ios/main/ios_memory.cpp +55 -0
  55. package/targets/ios/main/ios_renderer.h +20 -0
  56. package/targets/ios/main/ios_renderer.mm +27 -0
  57. package/targets/ios/main/ios_root_background.cpp +20 -0
  58. package/targets/ios/main/ios_root_background.h +11 -0
  59. package/targets/ios/main/ios_sensors.mm +244 -0
  60. package/targets/ios/main/ios_timers.mm +53 -0
  61. package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
  62. package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
  63. package/targets/ios/main/renderer/native_button.mm +179 -0
  64. package/targets/ios/main/renderer/native_label.mm +161 -0
  65. package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
  66. package/targets/ios/main/renderer/view_reconciler.mm +274 -0
  67. package/targets/macos/Info.plist.in +26 -0
  68. package/targets/macos/README.md +131 -0
  69. package/targets/macos/build-child-lifecycle.sh +119 -0
  70. package/targets/macos/build-macos.sh +2156 -0
  71. package/targets/macos/check-module-graph-freshness.mjs +82 -0
  72. package/targets/macos/compiler-input-fingerprint.mjs +32 -0
  73. package/targets/macos/finalize-apple-native-output.mjs +77 -0
  74. package/targets/macos/generate-resident-entry.mjs +80 -0
  75. package/targets/macos/generate-resident-registry.mjs +141 -0
  76. package/targets/macos/heavy-build-lock.sh +176 -0
  77. package/targets/macos/main/canvas_view.h +9 -0
  78. package/targets/macos/main/canvas_view.mm +176 -0
  79. package/targets/macos/main/color_convert.h +18 -0
  80. package/targets/macos/main/color_convert.mm +39 -0
  81. package/targets/macos/main/font_registry.h +35 -0
  82. package/targets/macos/main/font_registry.mm +350 -0
  83. package/targets/macos/main/image_bridge.h +16 -0
  84. package/targets/macos/main/image_bridge.mm +52 -0
  85. package/targets/macos/main/macos_app_platform.mm +95 -0
  86. package/targets/macos/main/macos_apps.c +31 -0
  87. package/targets/macos/main/macos_device_control.mm +56 -0
  88. package/targets/macos/main/macos_display.mm +484 -0
  89. package/targets/macos/main/macos_host_device_control.mm +95 -0
  90. package/targets/macos/main/macos_main.mm +839 -0
  91. package/targets/macos/main/macos_memory.cpp +31 -0
  92. package/targets/macos/main/macos_native_shell.h +31 -0
  93. package/targets/macos/main/macos_native_shell.mm +396 -0
  94. package/targets/macos/main/macos_network.mm +253 -0
  95. package/targets/macos/main/macos_renderer.h +39 -0
  96. package/targets/macos/main/macos_renderer.mm +1969 -0
  97. package/targets/macos/main/macos_sensors.cpp +148 -0
  98. package/targets/macos/main/macos_smoke_app.cpp +65 -0
  99. package/targets/macos/main/macos_timers.mm +52 -0
  100. package/targets/macos/main/press_bridge.h +21 -0
  101. package/targets/macos/main/press_bridge.mm +72 -0
  102. package/targets/macos/main/thermalright_hid_display.h +14 -0
  103. package/targets/macos/main/thermalright_hid_display.mm +995 -0
  104. package/targets/macos/native-artifact-provenance.mjs +421 -0
  105. package/targets/macos/order-compile-edges.mjs +64 -0
  106. package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
  107. package/targets/macos/prune-generated-modules.mjs +147 -0
  108. package/targets/macos/sync-resources.mjs +153 -0
  109. package/targets/shared/analyze-audio-capture.mjs +131 -0
  110. package/targets/shared/apple_audio.mm +1194 -0
  111. package/targets/shared/apple_audio_selftest.mm +602 -0
  112. package/targets/shared/apple_audio_testing.h +34 -0
  113. package/targets/shared/build-audio-selftest.sh +50 -0
@@ -0,0 +1,839 @@
1
+ #import <Cocoa/Cocoa.h>
2
+ #import <QuartzCore/CADisplayLink.h>
3
+
4
+ #include "font_registry.h"
5
+ #include "macos_native_shell.h"
6
+ #include "macos_renderer.h"
7
+ #include "press_bridge.h"
8
+ #include "thermalright_hid_display.h"
9
+
10
+ namespace gea::macos { void installAppLauncherPlatform(const char *currentAppId); }
11
+ namespace gea::macos { void installWifiDriver(); }
12
+
13
+ #include "app.h"
14
+ #include "css/declarative.h"
15
+ #include "css/engine.h"
16
+ #include "display.h"
17
+ #if __has_include("resident_apps.h")
18
+ #include "resident_apps.h"
19
+ #define GEA_MACOS_HAS_RESIDENT_APPS 1
20
+ #else
21
+ #define GEA_MACOS_HAS_RESIDENT_APPS 0
22
+ #endif
23
+ #include "ui/document.h"
24
+ #include "ui/node.h"
25
+ #include "ui/node_model.h"
26
+ #include "ui/style.h"
27
+ #include "ui/tree_internal.h"
28
+
29
+ #include <algorithm>
30
+ #include <chrono>
31
+ #include <cstdint>
32
+ #include <cstdio>
33
+ #include <cstdlib>
34
+ #include <cstring>
35
+
36
+ extern "C" int gea_embedded_now_ms(void);
37
+ // Headless text-input driver used by GEA_MACOS_SYNTH_INPUT (press_bridge.mm).
38
+ extern "C" int gea_macos_fire_input_for_node(int nodeId, const char *text);
39
+
40
+ static void gea_macos_smoke_log(const char *message)
41
+ {
42
+ const char *path = std::getenv("GEA_MACOS_MAIN_SMOKE_LOG");
43
+ if (!path || path[0] == '\0') return;
44
+ if (FILE *file = std::fopen(path, "a")) {
45
+ std::fprintf(file, "%s\n", message);
46
+ std::fclose(file);
47
+ }
48
+ }
49
+
50
+ // Apple-native apps (those importing @geajs/apple/*) make geatsc emit this
51
+ // handle-table bridge header plus Objective-C++ app modules. When present, this
52
+ // file is built in apple-native mode: __gea_top_level (run from Application::init)
53
+ // builds a real AppKit object graph and calls AppKit::installRootView, which we
54
+ // implement at the bottom of this file to swap the window's content view.
55
+ #if __has_include("gea/apple/native_bridge.h")
56
+ #include "gea/apple/native_bridge.h"
57
+ #define GEA_MACOS_HAS_APPLE_NATIVE_BRIDGE 1
58
+ #endif
59
+
60
+ @interface GeaContentView : NSView
61
+ @end
62
+
63
+ @implementation GeaContentView
64
+ // Override to keep the default AppKit y-flip semantics (origin bottom-left).
65
+ // MacosRenderer::applyViewStyle does its own coordinate flipping.
66
+ - (BOOL)isFlipped { return NO; }
67
+ @end
68
+
69
+ @interface AppDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate>
70
+ @property(nonatomic, strong) NSWindow *window;
71
+ @property(nonatomic, strong) GeaContentView *rootView;
72
+ @property(nonatomic, strong) NSTimer *frameTimer;
73
+ @property(nonatomic, strong) CADisplayLink *frameDisplayLink API_AVAILABLE(macos(14.0));
74
+ @property(nonatomic, strong) GeaSplitShell *splitShell;
75
+ @property(nonatomic, strong) id geaToolbarDelegate; // NSToolbar.delegate is weak; retain it here
76
+ @property(nonatomic, assign) BOOL geaDidFinishBoot;
77
+ @property(nonatomic, assign) double geaProfileApplicationMs;
78
+ - (void)installNativeRootView:(NSView *)view;
79
+ - (void)startFrameTimer;
80
+ - (void)updateDisplayLinkRate API_AVAILABLE(macos(14.0));
81
+ @end
82
+
83
+ // Set once the delegate exists so the apple-native AppKit::installRootView entry
84
+ // (called from inside Application::init → __gea_top_level) can reach the window.
85
+ static __weak AppDelegate *gGeaAppDelegate = nil;
86
+
87
+ @implementation AppDelegate
88
+
89
+ - (void)installMainMenu
90
+ {
91
+ NSMenu *mainMenu = [[NSMenu alloc] init];
92
+
93
+ NSMenuItem *appItem = [[NSMenuItem alloc] init];
94
+ [mainMenu addItem:appItem];
95
+ NSMenu *appMenu = [[NSMenu alloc] init];
96
+ NSString *appName = [[NSProcessInfo processInfo] processName];
97
+ [appMenu addItemWithTitle:[@"About " stringByAppendingString:appName]
98
+ action:@selector(orderFrontStandardAboutPanel:)
99
+ keyEquivalent:@""];
100
+ [appMenu addItem:[NSMenuItem separatorItem]];
101
+ NSMenuItem *hide = [appMenu addItemWithTitle:[@"Hide " stringByAppendingString:appName]
102
+ action:@selector(hide:)
103
+ keyEquivalent:@"h"];
104
+ hide.keyEquivalentModifierMask = NSEventModifierFlagCommand;
105
+ NSMenuItem *hideOthers = [appMenu addItemWithTitle:@"Hide Others"
106
+ action:@selector(hideOtherApplications:)
107
+ keyEquivalent:@"h"];
108
+ hideOthers.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagOption;
109
+ [appMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""];
110
+ [appMenu addItem:[NSMenuItem separatorItem]];
111
+ NSMenuItem *quit = [appMenu addItemWithTitle:[@"Quit " stringByAppendingString:appName]
112
+ action:@selector(terminate:)
113
+ keyEquivalent:@"q"];
114
+ quit.keyEquivalentModifierMask = NSEventModifierFlagCommand;
115
+ appItem.submenu = appMenu;
116
+
117
+ NSMenuItem *editItem = [[NSMenuItem alloc] init];
118
+ [mainMenu addItem:editItem];
119
+ NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"];
120
+ [editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"];
121
+ NSMenuItem *redo = [editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"z"];
122
+ redo.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagShift;
123
+ [editMenu addItem:[NSMenuItem separatorItem]];
124
+ [editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"];
125
+ [editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"];
126
+ [editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"];
127
+ [editMenu addItemWithTitle:@"Select All" action:@selector(selectAll:) keyEquivalent:@"a"];
128
+ editItem.submenu = editMenu;
129
+
130
+ NSMenuItem *windowItem = [[NSMenuItem alloc] init];
131
+ [mainMenu addItem:windowItem];
132
+ NSMenu *windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
133
+ [windowMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"];
134
+ [windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
135
+ [windowMenu addItem:[NSMenuItem separatorItem]];
136
+ [windowMenu addItemWithTitle:@"Close" action:@selector(performClose:) keyEquivalent:@"w"];
137
+ windowItem.submenu = windowMenu;
138
+ [NSApp setWindowsMenu:windowMenu];
139
+
140
+ [NSApp setMainMenu:mainMenu];
141
+ }
142
+
143
+ - (void)applicationDidFinishLaunching:(NSNotification *)note
144
+ {
145
+ (void)note;
146
+ if (self.geaDidFinishBoot) return;
147
+ self.geaDidFinishBoot = YES;
148
+ gea_macos_smoke_log("[gea-macos] applicationDidFinishLaunching");
149
+ gGeaAppDelegate = self;
150
+
151
+ // Report the Mac's real link state through the WiFi facade before
152
+ // Application::init — apps gate remote fetches on wifi().connected()
153
+ // (e.g. maps skips tile downloads while it reads false).
154
+ gea::macos::installWifiDriver();
155
+ const bool thermalrightTarget = gea::macos::thermalright::enabled();
156
+ if (!thermalrightTarget) [self installMainMenu];
157
+
158
+ // Register any .ttf / .otf the build script copied next to the binary.
159
+ // Done before Application::init so @font-face declarations in the app's
160
+ // CSS resolve to the registered family on first lookup.
161
+ NSString *fontsDir = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Fonts"];
162
+ gea::macos::registerCustomTtfDirectory(fontsDir);
163
+
164
+ // Wire the AppManager platform so JSX-level Apps.launch('foo') opens
165
+ // dist/foo/Foo.app and terminates this process. Derive the current app
166
+ // id from CFBundleIdentifier (set by Info.plist.in to com.gea.<id>).
167
+ NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier] ?: @"";
168
+ NSString *currentId = [bundleId hasPrefix:@"com.gea."] ? [bundleId substringFromIndex:8] : @"";
169
+ gea::macos::installAppLauncherPlatform(currentId.UTF8String);
170
+
171
+ if (thermalrightTarget) {
172
+ NSLog(@"[gea-thermalright] headless display target %dx%d",
173
+ gea::platform::display::kWidth,
174
+ gea::platform::display::kHeight);
175
+ gea::framework::app::Application::init(gea::platform::display::kWidth,
176
+ gea::platform::display::kHeight);
177
+ [self tick];
178
+ [self startFrameTimer];
179
+ return;
180
+ }
181
+
182
+ // Default window size — apps designed for the 410×502 hardware display
183
+ // fit exactly. A per-app `macos.json` (copied into Resources/window.json
184
+ // by the build script) opts into a larger desktop-class layout (e.g.
185
+ // the notes-jsx example uses 1000×640 + transparent title bar so its
186
+ // sidebar vibrancy reaches the top of the window). Env vars still
187
+ // override for layout-debugging without touching the bundle.
188
+ int winW = 410, winH = 502;
189
+ int minW = 200, minH = 200;
190
+ NSString *windowTitle = @"gea";
191
+ BOOL fullSizeContentView = NO;
192
+ BOOL transparentTitleBar = NO;
193
+ BOOL titleHidden = NO;
194
+ NSString *appearanceName = nil;
195
+
196
+ NSString *cfgPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"window.json"];
197
+ NSData *cfgData = [NSData dataWithContentsOfFile:cfgPath];
198
+ if (cfgData) {
199
+ NSError *err = nil;
200
+ id parsed = [NSJSONSerialization JSONObjectWithData:cfgData options:0 error:&err];
201
+ if ([parsed isKindOfClass:[NSDictionary class]]) {
202
+ NSDictionary *cfg = parsed;
203
+ NSDictionary *win = cfg[@"window"] ?: cfg;
204
+ id w = win[@"width"]; if ([w respondsToSelector:@selector(intValue)]) winW = [w intValue];
205
+ id h = win[@"height"]; if ([h respondsToSelector:@selector(intValue)]) winH = [h intValue];
206
+ id mw = win[@"minWidth"]; if ([mw respondsToSelector:@selector(intValue)]) minW = [mw intValue];
207
+ id mh = win[@"minHeight"]; if ([mh respondsToSelector:@selector(intValue)]) minH = [mh intValue];
208
+ id t = win[@"title"]; if ([t isKindOfClass:[NSString class]] && [(NSString *)t length] > 0) windowTitle = t;
209
+ id fsc = win[@"fullSizeContentView"]; if ([fsc respondsToSelector:@selector(boolValue)]) fullSizeContentView = [fsc boolValue];
210
+ id ttb = win[@"titleBarTransparent"]; if ([ttb respondsToSelector:@selector(boolValue)]) transparentTitleBar = [ttb boolValue];
211
+ id tv = win[@"titleVisibility"];
212
+ if ([tv isKindOfClass:[NSString class]] && [(NSString *)tv isEqualToString:@"hidden"]) titleHidden = YES;
213
+ id ap = win[@"appearance"];
214
+ if ([ap isKindOfClass:[NSString class]]) appearanceName = ap;
215
+ }
216
+ }
217
+
218
+ if (const char *e = getenv("GEA_MACOS_WIN_W")) winW = atoi(e);
219
+ if (const char *e = getenv("GEA_MACOS_WIN_H")) winH = atoi(e);
220
+ NSRect frame = NSMakeRect(0, 0, winW, winH);
221
+ NSUInteger styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
222
+ NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
223
+ if (fullSizeContentView) styleMask |= NSWindowStyleMaskFullSizeContentView;
224
+ self.window = [[NSWindow alloc] initWithContentRect:frame
225
+ styleMask:styleMask
226
+ backing:NSBackingStoreBuffered
227
+ defer:NO];
228
+ [self.window setTitle:windowTitle];
229
+ self.window.contentMinSize = NSMakeSize(minW, minH);
230
+ self.window.delegate = self;
231
+ if (transparentTitleBar) self.window.titlebarAppearsTransparent = YES;
232
+ if (titleHidden) self.window.titleVisibility = NSWindowTitleHidden;
233
+ if ([appearanceName isEqualToString:@"dark"]) {
234
+ self.window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
235
+ } else if ([appearanceName isEqualToString:@"light"]) {
236
+ self.window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua];
237
+ }
238
+ [self.window center];
239
+
240
+ self.rootView = [[GeaContentView alloc] initWithFrame:[self.window.contentView bounds]];
241
+ self.rootView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
242
+ [self.rootView setWantsLayer:YES];
243
+ [self.window setContentView:self.rootView];
244
+ [self.window makeKeyAndOrderFront:nil];
245
+ [NSApp activateIgnoringOtherApps:YES];
246
+
247
+ NSSize size = self.rootView.bounds.size;
248
+ if (gea::macos::thermalright::enabled()) {
249
+ size = NSMakeSize(gea::platform::display::kWidth, gea::platform::display::kHeight);
250
+ }
251
+ gea_macos_smoke_log("[gea-macos] before Application::init");
252
+ gea::framework::app::Application::init(static_cast<int>(size.width), static_cast<int>(size.height));
253
+ gea_macos_smoke_log("[gea-macos] after Application::init");
254
+
255
+ // If the app's mounted root is a <glass-split>, swap the flat content view
256
+ // for a native NSSplitViewController + NSToolbar shell. The shell installs
257
+ // itself as window.contentViewController (replacing rootView) and renders
258
+ // each pane's gea subtree into a real, resizable AppKit pane.
259
+ if (!gea::macos::thermalright::enabled()) {
260
+ int mountedRoot = gea::embedded::ui::Tree::instance().mountedRoot();
261
+ if (mountedRoot >= 0) {
262
+ self.splitShell = [GeaSplitShell shellForRootNode:mountedRoot window:self.window];
263
+ }
264
+ }
265
+
266
+ [self tick];
267
+ [self startFrameTimer];
268
+ }
269
+
270
+ - (void)startFrameTimer
271
+ {
272
+ if (self.frameTimer) return;
273
+ const BOOL thermalrightTarget = gea::macos::thermalright::enabled();
274
+ if (!thermalrightTarget) {
275
+ if (@available(macOS 14.0, *)) {
276
+ if (self.frameDisplayLink) return;
277
+ // A timer cannot express the desired display refresh rate. The
278
+ // window's display link requests the screen's native cadence and
279
+ // follows it when the window moves between displays.
280
+ self.frameDisplayLink = [self.window displayLinkWithTarget:self
281
+ selector:@selector(displayLinkDidFire:)];
282
+ [self updateDisplayLinkRate];
283
+ [self.frameDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
284
+ return;
285
+ }
286
+ }
287
+ // Older macOS versions rely on the blocking presentation swap for pacing.
288
+ // A fast timer avoids losing a whole refresh when a swap spans a timer fire.
289
+ const NSTimeInterval frameInterval = thermalrightTarget
290
+ ? (1.0 / static_cast<double>(gea::macos::thermalright::fps()))
291
+ : (1.0 / 960.0);
292
+ AppDelegate * __weak weakSelf = self;
293
+ // Schedule on NSRunLoopCommonModes so the timer keeps firing during
294
+ // NSEventTrackingRunLoopMode (live window-resize drag, menu tracking,
295
+ // etc.) instead of pausing until the user releases the mouse.
296
+ self.frameTimer = [NSTimer timerWithTimeInterval:frameInterval
297
+ repeats:YES
298
+ block:^(NSTimer *t) {
299
+ (void)t;
300
+ [weakSelf tick];
301
+ }];
302
+ self.frameTimer.tolerance = thermalrightTarget ? 0.001 : 0.0;
303
+ [[NSRunLoop currentRunLoop] addTimer:self.frameTimer forMode:NSRunLoopCommonModes];
304
+ }
305
+
306
+ - (void)updateDisplayLinkRate
307
+ {
308
+ NSScreen *screen = self.window.screen ?: [NSScreen mainScreen];
309
+ const NSInteger maximumFPS = screen.maximumFramesPerSecond;
310
+ const float fps = maximumFPS > 0 ? static_cast<float>(maximumFPS) : 60.0f;
311
+ self.frameDisplayLink.preferredFrameRateRange = CAFrameRateRangeMake(fps, fps, fps);
312
+ }
313
+
314
+ - (void)displayLinkDidFire:(CADisplayLink *)displayLink
315
+ {
316
+ static const bool profile = std::getenv("GEA_NATIVE_FRAME_PROFILE") != nullptr;
317
+ if (!profile) {
318
+ [self tick];
319
+ return;
320
+ }
321
+ using Clock = std::chrono::steady_clock;
322
+ const auto begin = Clock::now();
323
+ [self tick];
324
+ const auto end = Clock::now();
325
+ static auto windowStart = begin;
326
+ static double totalMs = 0.0;
327
+ static double applicationMs = 0.0;
328
+ static double maximumMs = 0.0;
329
+ static unsigned frames = 0;
330
+ const double elapsedMs = std::chrono::duration<double, std::milli>(end - begin).count();
331
+ totalMs += elapsedMs;
332
+ applicationMs += self.geaProfileApplicationMs;
333
+ maximumMs = std::max(maximumMs, elapsedMs);
334
+ ++frames;
335
+ const double windowMs = std::chrono::duration<double, std::milli>(end - windowStart).count();
336
+ if (windowMs >= 1000.0) {
337
+ NSScreen *screen = self.window.screen;
338
+ std::fprintf(stderr,
339
+ "[gea.frame-profile] callbacks_hz=%.1f callback_mean_ms=%.3f application_mean_ms=%.3f callback_max_ms=%.3f "
340
+ "link_period_ms=%.3f screen_max_hz=%ld screen=%s\n",
341
+ frames * 1000.0 / windowMs, totalMs / frames, applicationMs / frames, maximumMs,
342
+ (displayLink.targetTimestamp - displayLink.timestamp) * 1000.0,
343
+ static_cast<long>(screen.maximumFramesPerSecond), screen.localizedName.UTF8String);
344
+ frames = 0;
345
+ totalMs = applicationMs = maximumMs = 0.0;
346
+ windowStart = end;
347
+ }
348
+ }
349
+
350
+ - (void)windowDidChangeScreen:(NSNotification *)notification
351
+ {
352
+ (void)notification;
353
+ if (@available(macOS 14.0, *)) {
354
+ if (self.frameDisplayLink) [self updateDisplayLinkRate];
355
+ }
356
+ }
357
+
358
+ - (void)applicationWillTerminate:(NSNotification *)notification
359
+ {
360
+ (void)notification;
361
+ [self.frameTimer invalidate];
362
+ self.frameTimer = nil;
363
+ if (@available(macOS 14.0, *)) {
364
+ [self.frameDisplayLink invalidate];
365
+ self.frameDisplayLink = nil;
366
+ }
367
+ }
368
+
369
+ - (void)tick
370
+ {
371
+ if (getenv("GEA_MACOS_TICK_DEBUG")) {
372
+ static int tickCount = 0;
373
+ tickCount++;
374
+ if (tickCount <= 10 || tickCount % 60 == 0) {
375
+ std::fprintf(stderr, "[gea-macos] tick %d now=%d\n", tickCount, gea_embedded_now_ms());
376
+ }
377
+ }
378
+ // Drain any pending resident-app launch from the previous frame BEFORE
379
+ // running app::frame for this tick. When the launcher's onPress called
380
+ // Apps.launch('foo'), the registry pushed 'foo' onto its pending queue;
381
+ // here we pop it, select it as the new active resident, and re-run
382
+ // Application::init so its __gea_top_level mounts the new UI. After this
383
+ // switch, Application::frame below targets the new resident.
384
+ #if GEA_MACOS_HAS_RESIDENT_APPS
385
+ {
386
+ char pendingId[64] = "";
387
+ if (gea::framework::apps::ResidentApps::consumeLaunch(pendingId, sizeof(pendingId))) {
388
+ if (gea::framework::apps::ResidentApps::select(pendingId)) {
389
+ NSSize sz = gea::macos::thermalright::enabled()
390
+ ? NSMakeSize(gea::platform::display::kWidth, gea::platform::display::kHeight)
391
+ : self.rootView.bounds.size;
392
+ gea::macos::MacosRenderer::instance().teardown();
393
+ // Node ids are recycled by the next mount, so animations still
394
+ // running against the outgoing app's nodes must be dropped
395
+ // before it is torn down (runtime.cpp does this via
396
+ // LaunchSurface). The scan below restarts them for the new app.
397
+ gea::css::AnimationEngine::instance().clear();
398
+ gea::framework::app::Application::init(static_cast<int>(sz.width),
399
+ static_cast<int>(sz.height));
400
+ }
401
+ }
402
+ }
403
+ #endif
404
+ static const bool profile = std::getenv("GEA_NATIVE_FRAME_PROFILE") != nullptr;
405
+ using Clock = std::chrono::steady_clock;
406
+ const auto applicationBegin = profile ? Clock::now() : Clock::time_point{};
407
+ gea::framework::app::Application::frame(gea_embedded_now_ms());
408
+ if (profile) self.geaProfileApplicationMs = std::chrono::duration<double, std::milli>(Clock::now() - applicationBegin).count();
409
+ auto &tree = gea::embedded::ui::Tree::instance();
410
+ int root = tree.mountedRoot();
411
+ if (root < 0) return;
412
+
413
+ // Drive the CSS animation clock. The embedded runtime loop does this in
414
+ // core/packages/core/runtime.cpp (frame()); macOS has its own AppDelegate
415
+ // loop and never compiled that file, so `data-anim` declarative animations
416
+ // and @keyframes rules were registered and then never started or advanced —
417
+ // every animated app rendered its first frame and froze. Mirrors
418
+ // runtime.cpp: scan once per mounted app (activeId change, or exactly once
419
+ // in a single-app build where activeId() is always null), then tick.
420
+ {
421
+ const char *active = nullptr;
422
+ #if GEA_MACOS_HAS_RESIDENT_APPS
423
+ active = gea::framework::apps::ResidentApps::activeId();
424
+ #endif
425
+ static char lastScannedAnim[64] = {0};
426
+ static bool singleAppAnimScanned = false;
427
+ const bool shouldScan = active ? std::strcmp(active, lastScannedAnim) != 0 : !singleAppAnimScanned;
428
+ if (shouldScan) {
429
+ if (active)
430
+ std::strncpy(lastScannedAnim, active, sizeof(lastScannedAnim) - 1);
431
+ else
432
+ singleAppAnimScanned = true;
433
+ gea::css::DeclarativeAnimations::scanAndStart(gea_embedded_now_ms());
434
+ gea::embedded::ui::StyleSheet::instance().startCssAnimations(gea_embedded_now_ms());
435
+ }
436
+ }
437
+ gea::css::AnimationEngine::instance().tick(
438
+ static_cast<std::uint32_t>(gea_embedded_now_ms()));
439
+
440
+ if (gea::macos::thermalright::enabled()) return;
441
+
442
+ // Type into the first <input>/<textarea> without a keyboard. Set
443
+ // GEA_MACOS_SYNTH_INPUT=<text> to run the app's own onInput path (value
444
+ // attribute write + PointerEventType::Input dispatch) once, a few frames in.
445
+ // Unlike GEA_MACOS_SYNTH_CLICK this does NOT exit, so the resulting frame can
446
+ // be screenshotted. Posting real key events needs Accessibility permission,
447
+ // which a scripted run does not have — this is how the text-input path stays
448
+ // regression-testable from a shell. Placed ahead of the split-shell branch so
449
+ // it runs for both shell and flat apps.
450
+ if (const char *inputEnv = getenv("GEA_MACOS_SYNTH_INPUT")) {
451
+ static int inputFrameCount = 0;
452
+ ++inputFrameCount;
453
+ if (inputFrameCount == 4) {
454
+ const int node = gea_macos_fire_input_for_node(-1, inputEnv);
455
+ NSLog(@"[gea-synth-input] node=%d text=%s", node, inputEnv);
456
+ }
457
+ }
458
+
459
+ // Native split-shell apps: each pane lays out + syncs against its own
460
+ // resizable AppKit container. Skip the flat single-view path entirely.
461
+ if (self.splitShell) {
462
+ [self.splitShell layoutAndSync];
463
+ return;
464
+ }
465
+
466
+ NSSize size = self.rootView.bounds.size;
467
+ const int w = static_cast<int>(size.width);
468
+ const int h = static_cast<int>(size.height);
469
+ // macOS target convention: the mounted root view fills the window. This
470
+ // gives apps a viewport that tracks resize without per-app code; if an
471
+ // app wants different sizing it puts a sized child inside the root.
472
+ gea::embedded::ui::NodeHandle(root).style().width(w);
473
+ gea::embedded::ui::NodeHandle(root).style().height(h);
474
+ tree.computeLayout(root, w, h);
475
+ gea::macos::MacosRenderer::instance().sync(self.rootView, root);
476
+
477
+ if (const char *clickEnv = getenv("GEA_MACOS_SYNTH_CLICK")) {
478
+ static int frameCount = 0;
479
+ ++frameCount;
480
+ if (frameCount == 3) {
481
+ extern void gea_macos_fire_press_for_node(int);
482
+ gea_macos_fire_press_for_node(atoi(clickEnv));
483
+ } else if (frameCount == 8) {
484
+ NSLog(@"[gea-synth] rootView frame=%@ bounds=%@",
485
+ NSStringFromRect(self.rootView.frame),
486
+ NSStringFromRect(self.rootView.bounds));
487
+ [self walkAndLogSubviews:self.rootView indent:@" "];
488
+ exit(0);
489
+ }
490
+ }
491
+ // Dump tree layout + NSView hierarchy on the first frame to stderr. Set
492
+ // GEA_MACOS_LAYOUT_DUMP=1 to enable; lets you see root size, child positions
493
+ // and how they map to NSView frames so layout vs. y-flip bugs are diagnosable.
494
+ if (getenv("GEA_MACOS_LAYOUT_DUMP")) {
495
+ static int dumpFrame = 0;
496
+ ++dumpFrame;
497
+ // First few frames after launch, then every 60th frame so we can catch
498
+ // "renders correctly on frame 1, breaks on frame 2" patterns.
499
+ if (dumpFrame <= 5 || dumpFrame % 60 == 0) {
500
+ NSLog(@"[gea-layout] frame=%d rootView=%@ window=%dx%d",
501
+ dumpFrame, NSStringFromRect(self.rootView.frame), w, h);
502
+ using gea::embedded::ui::NodeType;
503
+ for (int i = 0; i < tree.nodeCount(); i++) {
504
+ const auto &n = tree.node(i);
505
+ NSLog(@" node[%d] t=%d xy=(%d,%d) wh=(%dx%d)%s",
506
+ i, (int)n.type,
507
+ (int)n.layout.x, (int)n.layout.y,
508
+ (int)n.layout.width, (int)n.layout.height,
509
+ n.type == NodeType::Text && !n.text.empty() ? n.text.c_str() : "");
510
+ }
511
+ [self walkAndLogSubviews:self.rootView indent:@" "];
512
+ }
513
+ }
514
+ if (getenv("GEA_MACOS_VERIFY_ONCE")) {
515
+ static BOOL logged = NO;
516
+ if (!logged) {
517
+ logged = YES;
518
+ using gea::embedded::ui::NodeType;
519
+ NSLog(@"[gea-verify] mountedRoot=%d nodeCount=%d subviews=%lu",
520
+ root, tree.nodeCount(), (unsigned long)self.rootView.subviews.count);
521
+ int buttonNode = -1;
522
+ for (int i = 0; i < tree.nodeCount(); i++) {
523
+ const auto &n = tree.node(i);
524
+ NSLog(@"[gea-verify] node[%d] type=%d frame=(%d,%d,%dx%d)",
525
+ i, (int)n.type, (int)n.layout.x, (int)n.layout.y,
526
+ (int)n.layout.width, (int)n.layout.height);
527
+ if (n.type == NodeType::Button) buttonNode = i;
528
+ }
529
+ [self walkAndLogSubviews:self.rootView indent:@" "];
530
+ if (buttonNode >= 0) {
531
+ NSLog(@"[gea-verify] firing press for buttonNode=%d twice", buttonNode);
532
+ gea_macos_fire_press_for_node(buttonNode);
533
+ gea_macos_fire_press_for_node(buttonNode);
534
+ for (int i = 0; i < tree.nodeCount(); i++) {
535
+ const auto &n = tree.node(i);
536
+ if (n.type == NodeType::Text && !n.text.empty()) {
537
+ NSLog(@"[gea-verify] text node[%d] = \"%s\"", i, n.text.c_str());
538
+ }
539
+ }
540
+ }
541
+ exit(0);
542
+ }
543
+ }
544
+ }
545
+
546
+ - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
547
+ {
548
+ (void)sender;
549
+ if (gea::macos::thermalright::enabled()) return NO;
550
+ return YES;
551
+ }
552
+
553
+ // Apple-native entry point: replace the window's flat content view with the
554
+ // AppKit object graph the TS program built. The view fills the content area and
555
+ // tracks window resizes; the app's own view controllers manage internal layout.
556
+ - (void)installNativeRootView:(NSView *)view
557
+ {
558
+ if (gea::macos::thermalright::enabled()) return;
559
+ if (!view) return;
560
+ self.rootView = nil; // drop the gea flat content view; unused in this mode
561
+ view.frame = [self.window.contentView bounds];
562
+ view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
563
+ [self.window setContentView:view];
564
+ [self.window makeKeyAndOrderFront:nil];
565
+ [NSApp activateIgnoringOtherApps:YES];
566
+ }
567
+
568
+ - (void)windowDidResize:(NSNotification *)notification
569
+ {
570
+ (void)notification;
571
+ if (gea::macos::thermalright::enabled()) return;
572
+ // Sync immediately on resize end too; the live-mode timer covers the
573
+ // drag, this handles the final snap.
574
+ [self tick];
575
+ }
576
+
577
+ - (void)walkAndLogSubviews:(NSView *)view indent:(NSString *)indent
578
+ {
579
+ for (NSView *sv in view.subviews) {
580
+ NSString *extra = @"";
581
+ if ([sv isKindOfClass:[NSTextField class]]) {
582
+ NSTextField *tf = (NSTextField *)sv;
583
+ extra = [NSString stringWithFormat:@" text=\"%@\" font=%@/%g",
584
+ tf.stringValue, tf.font.familyName, tf.font.pointSize];
585
+ } else if ([sv isKindOfClass:[NSButton class]]) {
586
+ extra = [NSString stringWithFormat:@" title=\"%@\"", ((NSButton *)sv).title];
587
+ }
588
+ CALayer *L = sv.layer;
589
+ NSString *layerInfo = L ? [NSString stringWithFormat:
590
+ @" L.frame=%@ L.pos=(%g,%g) L.anchor=(%g,%g) L.bg=%@",
591
+ NSStringFromRect(NSRectFromCGRect(L.frame)),
592
+ L.position.x, L.position.y,
593
+ L.anchorPoint.x, L.anchorPoint.y,
594
+ L.backgroundColor ? @"set" : @"nil"] : @"";
595
+ NSLog(@"[gea-verify]%@ %@ frame=%@%@%@", indent,
596
+ NSStringFromClass([sv class]),
597
+ NSStringFromRect(sv.frame),
598
+ layerInfo,
599
+ extra);
600
+ [self walkAndLogSubviews:sv indent:[indent stringByAppendingString:@" "]];
601
+ }
602
+ }
603
+
604
+ @end
605
+
606
+ #if GEA_MACOS_HAS_APPLE_NATIVE_BRIDGE
607
+ // Implements the @geajs/apple/AppKit `installRootView(view)` entry. geatsc lowers
608
+ // the TS call to `gea::apple::AppKit::installRootView(NSView)`; we unwrap the
609
+ // handle-table object back to a real ::NSView and hand it to the delegate.
610
+ void gea::apple::AppKit::installRootView(gea::apple::AppKit::NSView view)
611
+ {
612
+ ::NSView *nativeView = (__bridge ::NSView *)gea::apple::objc::object(view.handle);
613
+ if (!nativeView) return;
614
+ AppDelegate *delegate = gGeaAppDelegate;
615
+ if (!delegate) return;
616
+ if ([NSThread isMainThread]) {
617
+ [delegate installNativeRootView:nativeView];
618
+ } else {
619
+ dispatch_async(dispatch_get_main_queue(), ^{
620
+ [delegate installNativeRootView:nativeView];
621
+ });
622
+ }
623
+ }
624
+
625
+ // Implements @geajs/apple/AppKit installRootViewController(vc). Sets the window's
626
+ // contentViewController, so an NSSplitViewController (with sidebar/content/detail
627
+ // items) drives the whole window — Liquid Glass sidebar, resizable dividers, and
628
+ // the unified-toolbar safe area all come for free, exactly like real Notes.
629
+ void gea::apple::AppKit::installRootViewController(gea::apple::AppKit::NSViewController viewController)
630
+ {
631
+ ::NSViewController *vc = (__bridge ::NSViewController *)gea::apple::objc::object(viewController.handle);
632
+ if (!vc) return;
633
+ AppDelegate *delegate = gGeaAppDelegate;
634
+ if (!delegate) return;
635
+ auto install = ^{
636
+ // Setting contentViewController makes AppKit resize the window to the split
637
+ // view's fitting (minimum-thickness) size, discarding the configured
638
+ // macos.json size. Capture the configured content size first and restore it
639
+ // after the swap so the window opens at the size the app asked for.
640
+ NSSize wantedContent = delegate.window.contentView.bounds.size;
641
+ delegate.window.contentViewController = vc;
642
+ if (wantedContent.width > 1.0 && wantedContent.height > 1.0) {
643
+ [delegate.window setContentSize:wantedContent];
644
+ [delegate.window center];
645
+ }
646
+ [delegate.window makeKeyAndOrderFront:nil];
647
+ [NSApp activateIgnoringOtherApps:YES];
648
+ // Clear the initial first responder so an editable NSTextField/NSTextView
649
+ // in the content doesn't grab focus (drawing a blue focus ring + selecting
650
+ // its text) the instant the window opens — matching a freshly launched
651
+ // document app, which shows no focused field until the user clicks one.
652
+ [delegate.window makeFirstResponder:nil];
653
+ };
654
+ if ([NSThread isMainThread]) install();
655
+ else dispatch_async(dispatch_get_main_queue(), install);
656
+ }
657
+
658
+ // Vends the toolbar items declared by AppKit::installToolbar's spec string,
659
+ // matching the reference's GeaSplitShell toolbar: bordered image buttons with a
660
+ // label/tooltip and a target/action (so they render enabled, not dimmed). The
661
+ // sidebar toggle rides the responder chain to NSSplitViewController.toggleSidebar:,
662
+ // the compose item invokes the New Note ObjCTarget, and the rest are inert (a
663
+ // no-op action keeps them enabled and bordered, exactly like the reference's
664
+ // data-action-less items). Held strongly by AppDelegate.geaToolbarDelegate
665
+ // because NSToolbar.delegate is weak.
666
+ @interface GeaAppleToolbarDelegate : NSObject <NSToolbarDelegate>
667
+ @property(nonatomic, strong) NSArray<NSToolbarItemIdentifier> *ids;
668
+ @property(nonatomic, strong) NSDictionary<NSToolbarItemIdentifier, NSString *> *symbols;
669
+ @property(nonatomic, copy) NSString *searchId;
670
+ @property(nonatomic, strong) id composeTarget;
671
+ @end
672
+
673
+ @implementation GeaAppleToolbarDelegate
674
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar { return self.ids; }
675
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar { return self.ids; }
676
+ - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
677
+ itemForItemIdentifier:(NSToolbarItemIdentifier)identifier
678
+ willBeInsertedIntoToolbar:(BOOL)flag
679
+ {
680
+ (void)toolbar;
681
+ (void)flag;
682
+ if (self.searchId && [identifier isEqualToString:self.searchId]) {
683
+ if (@available(macOS 11.0, *)) {
684
+ NSSearchToolbarItem *search = [[NSSearchToolbarItem alloc] initWithItemIdentifier:identifier];
685
+ search.resignsFirstResponderWithCancel = YES;
686
+ return search;
687
+ }
688
+ NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:identifier];
689
+ item.view = [[NSSearchField alloc] initWithFrame:NSMakeRect(0, 0, 180, 22)];
690
+ return item;
691
+ }
692
+ NSString *symbol = self.symbols[identifier];
693
+ if (!symbol) return nil;
694
+ NSDictionary<NSString *, NSString *> *labels = @{
695
+ @"sidebar.left" : @"Toggle Sidebar",
696
+ @"square.and.pencil" : @"New Note",
697
+ @"textformat" : @"Format",
698
+ @"checklist" : @"Checklist",
699
+ @"tablecells" : @"Table",
700
+ @"square.and.arrow.up" : @"Share",
701
+ };
702
+ NSString *label = labels[symbol] ?: symbol;
703
+ NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:identifier];
704
+ item.label = label;
705
+ item.toolTip = label;
706
+ if (@available(macOS 11.0, *)) {
707
+ item.image = [NSImage imageWithSystemSymbolName:symbol accessibilityDescription:label];
708
+ }
709
+ item.bordered = YES;
710
+ if ([symbol isEqualToString:@"sidebar.left"]) {
711
+ item.action = @selector(toggleSidebar:);
712
+ item.target = nil; // responder chain → NSSplitViewController
713
+ } else if ([symbol isEqualToString:@"square.and.pencil"] && self.composeTarget) {
714
+ item.target = self.composeTarget;
715
+ item.action = @selector(invoke:);
716
+ } else {
717
+ item.target = self;
718
+ item.action = @selector(toolbarNoop:);
719
+ }
720
+ return item;
721
+ }
722
+ - (void)toolbarNoop:(id)sender { (void)sender; }
723
+ @end
724
+
725
+ // Implements @geajs/apple/AppKit installToolbar(spec, composeTarget). `spec` is a
726
+ // comma-separated list: an SF Symbol name => bordered image button, `space` =>
727
+ // flexible space, `search` => search field. Builds a real unified-title-bar
728
+ // NSToolbar and attaches it. `composeTarget` is invoked by the compose item.
729
+ void gea::apple::AppKit::installToolbar(std::string spec, gea::apple::Foundation::NSObject composeTargetWrapper)
730
+ {
731
+ AppDelegate *delegate = gGeaAppDelegate;
732
+ if (!delegate) return;
733
+ NSString *specStr = [NSString stringWithUTF8String:spec.c_str()];
734
+ id composeTarget = (__bridge id)gea::apple::objc::object(composeTargetWrapper.handle);
735
+ auto install = ^{
736
+ NSMutableArray<NSToolbarItemIdentifier> *ids = [NSMutableArray array];
737
+ NSMutableDictionary<NSToolbarItemIdentifier, NSString *> *symbols = [NSMutableDictionary dictionary];
738
+ NSString *searchId = nil;
739
+ int index = 0;
740
+ for (NSString *raw in [specStr componentsSeparatedByString:@","]) {
741
+ NSString *tok = [raw stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
742
+ if (tok.length == 0) continue;
743
+ if ([tok isEqualToString:@"space"]) {
744
+ [ids addObject:NSToolbarFlexibleSpaceItemIdentifier];
745
+ } else if ([tok isEqualToString:@"search"]) {
746
+ searchId = @"gea.toolbar.search";
747
+ [ids addObject:searchId];
748
+ } else {
749
+ NSString *iid = [NSString stringWithFormat:@"gea.toolbar.item.%d", index++];
750
+ symbols[iid] = tok;
751
+ [ids addObject:iid];
752
+ }
753
+ }
754
+ GeaAppleToolbarDelegate *d = [[GeaAppleToolbarDelegate alloc] init];
755
+ d.ids = ids;
756
+ d.symbols = symbols;
757
+ d.searchId = searchId;
758
+ d.composeTarget = composeTarget;
759
+ delegate.geaToolbarDelegate = d;
760
+ NSToolbar *tb = [[NSToolbar alloc] initWithIdentifier:@"gea.apple.toolbar"];
761
+ tb.delegate = d;
762
+ tb.displayMode = NSToolbarDisplayModeIconOnly;
763
+ tb.allowsUserCustomization = NO;
764
+ delegate.window.toolbar = tb;
765
+ if (@available(macOS 11.0, *)) delegate.window.toolbarStyle = NSWindowToolbarStyleUnified;
766
+ };
767
+ if ([NSThread isMainThread]) install();
768
+ else dispatch_async(dispatch_get_main_queue(), install);
769
+ }
770
+
771
+ // A top-left-origin container. NSScrollView document views default to NSView's
772
+ // bottom-left origin, so a tall vertical list opens scrolled to the bottom. A
773
+ // flipped document view lays content out top-down and opens at the top.
774
+ @interface GeaFlippedView : NSView
775
+ @end
776
+ @implementation GeaFlippedView
777
+ - (BOOL)isFlipped { return YES; }
778
+ @end
779
+
780
+ // Implements @geajs/apple/AppKit setScrollDocumentTopAligned(scrollView, content).
781
+ void gea::apple::AppKit::setScrollDocumentTopAligned(gea::apple::AppKit::NSScrollView scrollWrapper,
782
+ gea::apple::AppKit::NSView contentWrapper)
783
+ {
784
+ ::NSScrollView *sv = (__bridge ::NSScrollView *)gea::apple::objc::object(scrollWrapper.handle);
785
+ ::NSView *content = (__bridge ::NSView *)gea::apple::objc::object(contentWrapper.handle);
786
+ if (!sv || !content) return;
787
+ auto install = ^{
788
+ GeaFlippedView *flip = [[GeaFlippedView alloc] initWithFrame:NSZeroRect];
789
+ flip.translatesAutoresizingMaskIntoConstraints = NO;
790
+ content.translatesAutoresizingMaskIntoConstraints = NO;
791
+ [flip addSubview:content];
792
+ [::NSLayoutConstraint activateConstraints:@[
793
+ [content.topAnchor constraintEqualToAnchor:flip.topAnchor],
794
+ [content.leadingAnchor constraintEqualToAnchor:flip.leadingAnchor],
795
+ [content.trailingAnchor constraintEqualToAnchor:flip.trailingAnchor],
796
+ [content.bottomAnchor constraintEqualToAnchor:flip.bottomAnchor],
797
+ ]];
798
+ sv.documentView = flip;
799
+ // Pin the flipped document to the scroll's clip view: it tracks the clip
800
+ // width (no horizontal scroll) while its height grows with the content
801
+ // (vertical scroll), and its top aligns with the clip's top.
802
+ [::NSLayoutConstraint activateConstraints:@[
803
+ [flip.topAnchor constraintEqualToAnchor:sv.contentView.topAnchor],
804
+ [flip.leadingAnchor constraintEqualToAnchor:sv.contentView.leadingAnchor],
805
+ [flip.trailingAnchor constraintEqualToAnchor:sv.contentView.trailingAnchor],
806
+ [flip.widthAnchor constraintEqualToAnchor:sv.contentView.widthAnchor],
807
+ ]];
808
+ };
809
+ if ([NSThread isMainThread]) install();
810
+ else dispatch_async(dispatch_get_main_queue(), install);
811
+ }
812
+ #endif
813
+
814
+ int main(int argc, const char *argv[])
815
+ {
816
+ (void)argc;
817
+ (void)argv;
818
+ @autoreleasepool {
819
+ gea_macos_smoke_log("[gea-macos] main entered");
820
+ NSApplication *app = [NSApplication sharedApplication];
821
+ AppDelegate *delegate = [[AppDelegate alloc] init];
822
+ [app setDelegate:delegate];
823
+ [app setActivationPolicy:gea::macos::thermalright::enabled()
824
+ ? NSApplicationActivationPolicyProhibited
825
+ : NSApplicationActivationPolicyRegular];
826
+ gea_macos_smoke_log("[gea-macos] before finishLaunching");
827
+ [app finishLaunching];
828
+ gea_macos_smoke_log("[gea-macos] after finishLaunching");
829
+ gea_macos_smoke_log("[gea-macos] before explicit delegate launch");
830
+ NSNotification *launchNotification =
831
+ [NSNotification notificationWithName:NSApplicationDidFinishLaunchingNotification object:app];
832
+ [delegate applicationDidFinishLaunching:launchNotification];
833
+ gea_macos_smoke_log("[gea-macos] after explicit delegate launch");
834
+ gea_macos_smoke_log("[gea-macos] before NSApplication run");
835
+ [app run];
836
+ gea_macos_smoke_log("[gea-macos] after NSApplication run");
837
+ }
838
+ return 0;
839
+ }