@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.
- package/LICENSE +202 -0
- package/README.md +54 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +3076 -0
- package/generated/AVFoundation.d.ts +112 -0
- package/generated/ActivityKit.d.ts +7 -0
- package/generated/AppKit.d.ts +268 -0
- package/generated/CoreGraphics.d.ts +18 -0
- package/generated/CoreLocation.d.ts +28 -0
- package/generated/CoreMedia.d.ts +8 -0
- package/generated/Dispatch.d.ts +2 -0
- package/generated/Foundation.d.ts +13 -0
- package/generated/MapKit.d.ts +28 -0
- package/generated/Metal.d.ts +91 -0
- package/generated/MetalKit.d.ts +22 -0
- package/generated/Photos.d.ts +5 -0
- package/generated/QuartzCore.d.ts +12 -0
- package/generated/SwiftData.d.ts +11 -0
- package/generated/UIKit.d.ts +187 -0
- package/package.json +123 -0
- package/runtime/AVFoundation.js +136 -0
- package/runtime/ActivityKit.js +18 -0
- package/runtime/AppKit.js +313 -0
- package/runtime/CoreGraphics.js +15 -0
- package/runtime/CoreLocation.js +30 -0
- package/runtime/CoreMedia.js +7 -0
- package/runtime/Dispatch.js +11 -0
- package/runtime/Foundation.js +21 -0
- package/runtime/MapKit.js +22 -0
- package/runtime/Metal.js +126 -0
- package/runtime/MetalKit.js +15 -0
- package/runtime/Photos.js +9 -0
- package/runtime/QuartzCore.js +9 -0
- package/runtime/SwiftData.js +29 -0
- package/runtime/UIKit.js +242 -0
- package/runtime/empty.js +1 -0
- package/targets/ios/Info.plist.in +25 -0
- package/targets/ios/README.md +46 -0
- package/targets/ios/build-ios.sh +395 -0
- package/targets/ios/detect-development-team.mjs +109 -0
- package/targets/ios/generate-resident-entry.mjs +67 -0
- package/targets/ios/generate-resident-registry.mjs +162 -0
- package/targets/ios/generate-xcode-project.mjs +520 -0
- package/targets/ios/main/canvas_view.h +7 -0
- package/targets/ios/main/canvas_view.mm +94 -0
- package/targets/ios/main/font_registry.h +17 -0
- package/targets/ios/main/font_registry.mm +129 -0
- package/targets/ios/main/image_bridge.h +13 -0
- package/targets/ios/main/image_bridge.mm +53 -0
- package/targets/ios/main/ios_apps.c +28 -0
- package/targets/ios/main/ios_camera.mm +446 -0
- package/targets/ios/main/ios_display.mm +344 -0
- package/targets/ios/main/ios_main.mm +811 -0
- package/targets/ios/main/ios_memory.cpp +55 -0
- package/targets/ios/main/ios_renderer.h +20 -0
- package/targets/ios/main/ios_renderer.mm +27 -0
- package/targets/ios/main/ios_root_background.cpp +20 -0
- package/targets/ios/main/ios_root_background.h +11 -0
- package/targets/ios/main/ios_sensors.mm +244 -0
- package/targets/ios/main/ios_timers.mm +53 -0
- package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
- package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
- package/targets/ios/main/renderer/native_button.mm +179 -0
- package/targets/ios/main/renderer/native_label.mm +161 -0
- package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
- package/targets/ios/main/renderer/view_reconciler.mm +274 -0
- package/targets/macos/Info.plist.in +26 -0
- package/targets/macos/README.md +131 -0
- package/targets/macos/build-child-lifecycle.sh +119 -0
- package/targets/macos/build-macos.sh +2156 -0
- package/targets/macos/check-module-graph-freshness.mjs +82 -0
- package/targets/macos/compiler-input-fingerprint.mjs +32 -0
- package/targets/macos/finalize-apple-native-output.mjs +77 -0
- package/targets/macos/generate-resident-entry.mjs +80 -0
- package/targets/macos/generate-resident-registry.mjs +141 -0
- package/targets/macos/heavy-build-lock.sh +176 -0
- package/targets/macos/main/canvas_view.h +9 -0
- package/targets/macos/main/canvas_view.mm +176 -0
- package/targets/macos/main/color_convert.h +18 -0
- package/targets/macos/main/color_convert.mm +39 -0
- package/targets/macos/main/font_registry.h +35 -0
- package/targets/macos/main/font_registry.mm +350 -0
- package/targets/macos/main/image_bridge.h +16 -0
- package/targets/macos/main/image_bridge.mm +52 -0
- package/targets/macos/main/macos_app_platform.mm +95 -0
- package/targets/macos/main/macos_apps.c +31 -0
- package/targets/macos/main/macos_device_control.mm +56 -0
- package/targets/macos/main/macos_display.mm +484 -0
- package/targets/macos/main/macos_host_device_control.mm +95 -0
- package/targets/macos/main/macos_main.mm +839 -0
- package/targets/macos/main/macos_memory.cpp +31 -0
- package/targets/macos/main/macos_native_shell.h +31 -0
- package/targets/macos/main/macos_native_shell.mm +396 -0
- package/targets/macos/main/macos_network.mm +253 -0
- package/targets/macos/main/macos_renderer.h +39 -0
- package/targets/macos/main/macos_renderer.mm +1969 -0
- package/targets/macos/main/macos_sensors.cpp +148 -0
- package/targets/macos/main/macos_smoke_app.cpp +65 -0
- package/targets/macos/main/macos_timers.mm +52 -0
- package/targets/macos/main/press_bridge.h +21 -0
- package/targets/macos/main/press_bridge.mm +72 -0
- package/targets/macos/main/thermalright_hid_display.h +14 -0
- package/targets/macos/main/thermalright_hid_display.mm +995 -0
- package/targets/macos/native-artifact-provenance.mjs +421 -0
- package/targets/macos/order-compile-edges.mjs +64 -0
- package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
- package/targets/macos/prune-generated-modules.mjs +147 -0
- package/targets/macos/sync-resources.mjs +153 -0
- package/targets/shared/analyze-audio-capture.mjs +131 -0
- package/targets/shared/apple_audio.mm +1194 -0
- package/targets/shared/apple_audio_selftest.mm +602 -0
- package/targets/shared/apple_audio_testing.h +34 -0
- 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
|
+
}
|