@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,31 @@
1
+ #include "memory.h"
2
+
3
+ #include <cstdlib>
4
+ #include <new>
5
+
6
+ namespace gea::framework::memory {
7
+
8
+ void *Allocator::allocatePreferSpiram(std::size_t size, std::size_t alignment)
9
+ {
10
+ if (size == 0) return nullptr;
11
+ if (alignment <= alignof(std::max_align_t)) return std::malloc(size);
12
+ void *ptr = nullptr;
13
+ if (posix_memalign(&ptr, alignment, size) != 0) return nullptr;
14
+ return ptr;
15
+ }
16
+
17
+ void *Allocator::reallocatePreferSpiram(void *ptr, std::size_t size)
18
+ {
19
+ if (size == 0) {
20
+ std::free(ptr);
21
+ return nullptr;
22
+ }
23
+ return std::realloc(ptr, size);
24
+ }
25
+
26
+ void Allocator::free(void *ptr) noexcept
27
+ {
28
+ std::free(ptr);
29
+ }
30
+
31
+ } // namespace gea::framework::memory
@@ -0,0 +1,31 @@
1
+ #pragma once
2
+
3
+ #ifdef __OBJC__
4
+ #import <Cocoa/Cocoa.h>
5
+
6
+ // Native AppKit shell for apps whose mounted root is a `<glass-split>`.
7
+ //
8
+ // Instead of rendering the whole gea tree into one flat NSView, the shell
9
+ // maps the declared structure onto real AppKit containers:
10
+ // <glass-split> → NSSplitViewController (resizable dividers)
11
+ // <toolbar> → window NSToolbar (unified title bar, SF Symbols)
12
+ // <glass-pane sidebar> → NSSplitViewItem sidebar behavior (Liquid Glass)
13
+ // <glass-pane list> → NSSplitViewItem content-list behavior
14
+ // <glass-pane detail> → NSSplitViewItem regular
15
+ // Each pane's gea subtree is laid out against the pane's live bounds and
16
+ // rendered into the pane's container view, so resizing a divider re-flows
17
+ // that pane's content natively.
18
+ @interface GeaSplitShell : NSObject
19
+
20
+ // Builds the shell if `rootNodeId` is a `<glass-split>`; installs the split
21
+ // controller as the window's contentViewController and the toolbar on the
22
+ // window. Returns nil when the root isn't a glass-split (caller falls back to
23
+ // the legacy single-content-view path).
24
+ + (instancetype)shellForRootNode:(int)rootNodeId window:(NSWindow *)window;
25
+
26
+ // Per-frame: size each pane node to its container's current bounds, run the
27
+ // flex layout for that pane, and sync the gea subtrees into the pane views.
28
+ - (void)layoutAndSync;
29
+
30
+ @end
31
+ #endif
@@ -0,0 +1,396 @@
1
+ #import "macos_native_shell.h"
2
+
3
+ #include "macos_renderer.h"
4
+
5
+ #include "events.h"
6
+ #include "ui/document.h"
7
+ #include "ui/node.h"
8
+ #include "ui/node_model.h"
9
+ #include "ui/tree_internal.h"
10
+
11
+ #include <string>
12
+ #include <vector>
13
+
14
+ using gea::embedded::ui::NodeHandle;
15
+ using gea::embedded::ui::Tree;
16
+
17
+ namespace {
18
+
19
+ const char *nodeTag(int nodeId)
20
+ {
21
+ Tree &tree = Tree::instance();
22
+ if (nodeId < 0 || nodeId >= tree.nodeCount()) return "";
23
+ const char *t = tree.tagName(nodeId);
24
+ return t ? t : "";
25
+ }
26
+
27
+ bool tagIs(int nodeId, const char *tag)
28
+ {
29
+ return std::strcmp(nodeTag(nodeId), tag) == 0;
30
+ }
31
+
32
+ // First ELEMENT child (skips nothing special here — gea children are all
33
+ // nodes). Returns -1 if none.
34
+ int firstChild(int nodeId)
35
+ {
36
+ Tree &tree = Tree::instance();
37
+ if (nodeId < 0 || nodeId >= tree.nodeCount()) return -1;
38
+ return tree.node(nodeId).first_child;
39
+ }
40
+
41
+ int nextSibling(int nodeId)
42
+ {
43
+ Tree &tree = Tree::instance();
44
+ if (nodeId < 0 || nodeId >= tree.nodeCount()) return -1;
45
+ return tree.node(nodeId).next_sibling;
46
+ }
47
+
48
+ std::string attr(int nodeId, const char *name)
49
+ {
50
+ Tree &tree = Tree::instance();
51
+ if (nodeId < 0 || nodeId >= tree.nodeCount()) return "";
52
+ const char *v = tree.getAttribute(nodeId, name);
53
+ return v ? std::string(v) : std::string();
54
+ }
55
+
56
+ // Fire Click + Press at the node carrying attribute id == elementId. Used by
57
+ // toolbar items to drive JSX onPress handlers without the item being part of
58
+ // the visual tree.
59
+ void firePressForElementId(const std::string &elementId)
60
+ {
61
+ if (elementId.empty()) return;
62
+ Tree &tree = Tree::instance();
63
+ int targetId = -1;
64
+ for (int i = 0; i < tree.nodeCount(); i++) {
65
+ if (attr(i, "id") == elementId) { targetId = i; break; }
66
+ }
67
+ if (targetId < 0) return;
68
+ using gea::framework::events::PointerEvent;
69
+ using gea::framework::events::PointerEventType;
70
+ auto fire = [&](PointerEventType type) {
71
+ PointerEvent ev;
72
+ ev.type = type;
73
+ ev.targetId = targetId;
74
+ tree.dispatchEvent(ev);
75
+ };
76
+ fire(PointerEventType::Click);
77
+ }
78
+
79
+ // The `<glass-split>` at or below `nodeId`, or -1. Bounded: the shell element is
80
+ // an app's OUTERMOST element, so it sits directly under the `#app` mount root
81
+ // (or under a wrapper or two a component added). Walking a few levels of the
82
+ // tree's head is enough and cannot wander into a deep list body.
83
+ int findGlassSplit(int nodeId, int depth = 0)
84
+ {
85
+ if (nodeId < 0 || depth > 4) return -1;
86
+ if (tagIs(nodeId, "glass-split")) return nodeId;
87
+ for (int child = firstChild(nodeId); child >= 0; child = nextSibling(child)) {
88
+ const int found = findGlassSplit(child, depth + 1);
89
+ if (found >= 0) return found;
90
+ }
91
+ return -1;
92
+ }
93
+
94
+ struct PaneInfo {
95
+ int contentNodeId; // the gea subtree root rendered into this pane
96
+ NSView *container; // the split item's view (full height, behind toolbar)
97
+ NSView *content; // inner view inset to the safe area (below the toolbar)
98
+ NSRect lastFrame = NSZeroRect; // last content frame synced (resize detection)
99
+ };
100
+
101
+ struct ToolbarItemInfo {
102
+ std::string identifier;
103
+ std::string symbol; // SF Symbol name
104
+ std::string label;
105
+ std::string actionId; // element id to press on click ("" = inert)
106
+ bool isSearch = false;
107
+ bool isSpace = false; // flexible space
108
+ bool isSidebarToggle = false;
109
+ };
110
+
111
+ } // namespace
112
+
113
+ // Lightweight NSViewController whose view is supplied directly. Using a bare
114
+ // NSViewController subclass avoids loadView nib lookups.
115
+ @interface GeaPaneViewController : NSViewController
116
+ @end
117
+ @implementation GeaPaneViewController
118
+ - (void)loadView
119
+ {
120
+ if (!self.view) self.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)];
121
+ }
122
+ @end
123
+
124
+ @interface GeaSplitShell () <NSToolbarDelegate>
125
+ @property(nonatomic, strong) NSSplitViewController *splitController;
126
+ @property(nonatomic, strong) NSToolbar *toolbar;
127
+ @property(nonatomic, assign) std::vector<PaneInfo> *panes;
128
+ @property(nonatomic, assign) std::vector<ToolbarItemInfo> *toolbarItems;
129
+ @property(nonatomic, assign) uint64_t lastSerial; // Tree::refreshSerial last synced
130
+ @end
131
+
132
+ @implementation GeaSplitShell
133
+
134
+ + (instancetype)shellForRootNode:(int)rootNodeId window:(NSWindow *)window
135
+ {
136
+ // The MOUNTED root is never the app's own element: `mount(App)` resolves
137
+ // `document.getElementById('app')` and renders the component INTO it
138
+ // (core/packages/core/runtime/primitives.ts), and `Document::ensureAppRoot`
139
+ // is what mounts that `#app` view (engine/ui/document.cpp). So a
140
+ // `<glass-split>` template lands one (or more) levels BELOW the node
141
+ // `Tree::mountedRoot()` names, and matching only the root itself silently
142
+ // declined every app that uses the shell — notes-jsx fell back to flat gea
143
+ // layout with no toolbar and no split.
144
+ rootNodeId = findGlassSplit(rootNodeId);
145
+ if (rootNodeId < 0) return nil;
146
+ GeaSplitShell *shell = [[GeaSplitShell alloc] init];
147
+ shell.panes = new std::vector<PaneInfo>();
148
+ shell.toolbarItems = new std::vector<ToolbarItemInfo>();
149
+
150
+ NSSplitViewController *svc = [[NSSplitViewController alloc] init];
151
+ svc.view.frame = [window.contentView bounds];
152
+
153
+ // Walk glass-split children: panes + an optional <toolbar>.
154
+ for (int child = firstChild(rootNodeId); child >= 0; child = nextSibling(child)) {
155
+ if (tagIs(child, "toolbar")) {
156
+ [shell parseToolbar:child];
157
+ continue;
158
+ }
159
+ if (!tagIs(child, "glass-pane")) continue;
160
+ const std::string role = attr(child, "data-pane");
161
+ const int contentNode = firstChild(child);
162
+ if (contentNode < 0) continue;
163
+
164
+ GeaPaneViewController *vc = [[GeaPaneViewController alloc] init];
165
+ NSView *container = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 240, 400)];
166
+ container.wantsLayer = YES;
167
+ vc.view = container;
168
+ // Inner content view, re-framed each layout to the window's below-toolbar
169
+ // region (contentLayoutRect) converted into THIS container's coordinate
170
+ // space. Converting the one shared window rect per container makes every
171
+ // pane's content top land at the exact same screen y (the toolbar
172
+ // bottom), regardless of each split item's own frame/height — the
173
+ // sidebar item is shorter and offset vs. the content panes, so its own
174
+ // safeAreaInsets under-inset its first row under the toolbar. The split
175
+ // item's glass/background still fills the full pane height because only
176
+ // this inner view is inset.
177
+ NSView *contentView = [[NSView alloc] initWithFrame:container.bounds];
178
+ contentView.wantsLayer = YES;
179
+ contentView.autoresizingMask = NSViewNotSizable;
180
+ [container addSubview:contentView];
181
+
182
+ NSSplitViewItem *item;
183
+ if (role == "sidebar") {
184
+ // Sidebar behavior gives the system-managed Liquid Glass material
185
+ // + collapse/show animation. We must NOT paint an opaque
186
+ // background on the container or it hides the vibrancy.
187
+ item = [NSSplitViewItem sidebarWithViewController:vc];
188
+ item.minimumThickness = 180;
189
+ item.maximumThickness = 320;
190
+ item.canCollapse = YES;
191
+ } else if (role == "list") {
192
+ item = [NSSplitViewItem contentListWithViewController:vc];
193
+ item.minimumThickness = 240;
194
+ item.maximumThickness = 460;
195
+ } else {
196
+ item = [NSSplitViewItem splitViewItemWithViewController:vc];
197
+ item.minimumThickness = 320;
198
+ container.layer.backgroundColor = [[NSColor colorWithSRGBRed:0.114 green:0.114 blue:0.122 alpha:1.0] CGColor];
199
+ }
200
+ [svc addSplitViewItem:item];
201
+ shell.panes->push_back({contentNode, container, contentView});
202
+ }
203
+
204
+ if (shell.panes->empty()) {
205
+ delete shell.panes;
206
+ delete shell.toolbarItems;
207
+ return nil;
208
+ }
209
+
210
+ shell.splitController = svc;
211
+ window.contentViewController = svc;
212
+
213
+ if (!shell.toolbarItems->empty()) {
214
+ NSToolbar *tb = [[NSToolbar alloc] initWithIdentifier:@"gea.toolbar"];
215
+ tb.delegate = shell;
216
+ tb.displayMode = NSToolbarDisplayModeIconOnly;
217
+ tb.allowsUserCustomization = NO;
218
+ shell.toolbar = tb;
219
+ window.toolbar = tb;
220
+ if (@available(macOS 11.0, *)) window.toolbarStyle = NSWindowToolbarStyleUnified;
221
+ }
222
+
223
+ return shell;
224
+ }
225
+
226
+ - (void)parseToolbar:(int)toolbarNode
227
+ {
228
+ for (int item = firstChild(toolbarNode); item >= 0; item = nextSibling(item)) {
229
+ ToolbarItemInfo info;
230
+ if (tagIs(item, "toolbar-search")) {
231
+ info.isSearch = true;
232
+ info.identifier = "gea.search";
233
+ } else if (tagIs(item, "toolbar-space")) {
234
+ info.isSpace = true;
235
+ info.identifier = NSToolbarFlexibleSpaceItemIdentifier.UTF8String;
236
+ } else if (tagIs(item, "toolbar-item")) {
237
+ info.symbol = attr(item, "data-symbol");
238
+ info.label = attr(item, "data-label");
239
+ info.actionId = attr(item, "data-action");
240
+ info.isSidebarToggle = attr(item, "data-role") == "sidebar-toggle";
241
+ info.identifier = "gea.item." + std::to_string(_toolbarItems->size());
242
+ } else {
243
+ continue;
244
+ }
245
+ _toolbarItems->push_back(info);
246
+ }
247
+ }
248
+
249
+ - (void)layoutAndSync
250
+ {
251
+ if (!_panes || _panes->empty()) return;
252
+ Tree &tree = Tree::instance();
253
+ const bool dbg = getenv("GEA_DEBUG_SHELL") != nullptr;
254
+ const std::size_t count = _panes->size();
255
+
256
+ NSWindow *window = nil;
257
+ for (const PaneInfo &pane : *_panes) {
258
+ if (pane.container.window) { window = pane.container.window; break; }
259
+ }
260
+ NSView *windowContent = window.contentView;
261
+ const NSRect belowToolbar = window ? window.contentLayoutRect : NSZeroRect;
262
+
263
+ // Cheap pass: compute each pane's content frame (a rect convert + clip) and
264
+ // detect whether anything moved. This is the only work done every frame.
265
+ std::vector<NSRect> frames(count);
266
+ bool anyFrameChanged = false;
267
+ for (std::size_t i = 0; i < count; i++) {
268
+ const PaneInfo &pane = (*_panes)[i];
269
+ NSView *container = pane.container;
270
+ // Convert the window's below-toolbar rect into this container's space and
271
+ // clip to the container, so the content view's TOP edge lands exactly at
272
+ // the toolbar bottom in screen space — identical y across all panes.
273
+ NSRect inContainer = windowContent
274
+ ? [container convertRect:belowToolbar fromView:windowContent]
275
+ : container.bounds;
276
+ NSRect contentFrame = NSIntersectionRect(inContainer, container.bounds);
277
+ if (NSIsEmptyRect(contentFrame)) contentFrame = container.bounds;
278
+ frames[i] = contentFrame;
279
+ if (!NSEqualRects(contentFrame, pane.lastFrame)) anyFrameChanged = true;
280
+ }
281
+
282
+ // Skip the expensive layout + full view diff unless the reactive tree changed
283
+ // (Tree::refreshSerial bumps on mount/refresh) or a pane resized. This is what
284
+ // keeps an idle Notes window off the CPU instead of re-syncing 60×/sec.
285
+ const uint64_t serial = tree.refreshSerial();
286
+ if (serial == _lastSerial && !anyFrameChanged) return;
287
+
288
+ NSMutableArray *paneViews = [NSMutableArray arrayWithCapacity:count];
289
+ std::vector<int> rootIds;
290
+ rootIds.reserve(count);
291
+ for (std::size_t i = 0; i < count; i++) {
292
+ PaneInfo &pane = (*_panes)[i];
293
+ const NSRect contentFrame = frames[i];
294
+ pane.content.frame = contentFrame;
295
+ pane.lastFrame = contentFrame;
296
+ const int w = static_cast<int>(contentFrame.size.width);
297
+ const int h = static_cast<int>(contentFrame.size.height);
298
+ if (dbg) {
299
+ fprintf(stderr, "[shell] pane node=%d container=%.0fx%.0f -> content=%dx%d (serial=%llu)\n",
300
+ pane.contentNodeId, pane.container.bounds.size.width, pane.container.bounds.size.height,
301
+ w, h, (unsigned long long)serial);
302
+ }
303
+ if (w <= 0 || h <= 0) continue;
304
+ if (pane.contentNodeId < 0 || pane.contentNodeId >= tree.nodeCount()) continue;
305
+ NodeHandle(pane.contentNodeId).style().width(w);
306
+ NodeHandle(pane.contentNodeId).style().height(h);
307
+ tree.computeLayout(pane.contentNodeId, w, h);
308
+ [paneViews addObject:pane.content];
309
+ rootIds.push_back(pane.contentNodeId);
310
+ }
311
+ _lastSerial = serial;
312
+ if (paneViews.count == 0) return;
313
+ gea::macos::MacosRenderer::instance().syncPanes(paneViews, rootIds.data());
314
+ }
315
+
316
+ #pragma mark NSToolbarDelegate
317
+
318
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarItemIdentifiers
319
+ {
320
+ NSMutableArray *ids = [NSMutableArray array];
321
+ for (const ToolbarItemInfo &info : *_toolbarItems) {
322
+ [ids addObject:[NSString stringWithUTF8String:info.identifier.c_str()]];
323
+ }
324
+ return ids;
325
+ }
326
+
327
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
328
+ {
329
+ return [self toolbarItemIdentifiers];
330
+ }
331
+
332
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
333
+ {
334
+ return [self toolbarItemIdentifiers];
335
+ }
336
+
337
+ - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
338
+ itemForItemIdentifier:(NSToolbarItemIdentifier)itemIdentifier
339
+ willBeInsertedIntoToolbar:(BOOL)flag
340
+ {
341
+ const std::string ident = itemIdentifier.UTF8String;
342
+ for (const ToolbarItemInfo &info : *_toolbarItems) {
343
+ if (info.identifier != ident) continue;
344
+ if (info.isSpace) return nil; // standard identifier, AppKit provides it
345
+ if (info.isSearch) {
346
+ NSSearchToolbarItem *search = [[NSSearchToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
347
+ search.resignsFirstResponderWithCancel = YES;
348
+ return search;
349
+ }
350
+ NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
351
+ NSString *label = [NSString stringWithUTF8String:info.label.c_str()];
352
+ toolbarItem.label = label;
353
+ toolbarItem.toolTip = label;
354
+ NSImage *image = nil;
355
+ if (!info.symbol.empty()) {
356
+ NSString *sym = [NSString stringWithUTF8String:info.symbol.c_str()];
357
+ image = [NSImage imageWithSystemSymbolName:sym accessibilityDescription:label];
358
+ }
359
+ toolbarItem.image = image;
360
+ toolbarItem.bordered = YES;
361
+ if (info.isSidebarToggle) {
362
+ toolbarItem.target = self.splitController;
363
+ toolbarItem.action = @selector(toggleSidebar:);
364
+ } else {
365
+ toolbarItem.target = self;
366
+ toolbarItem.action = @selector(toolbarItemClicked:);
367
+ // Stash the action element id on the item via its identifier map;
368
+ // we resolve it back in the action by scanning _toolbarItems.
369
+ }
370
+ return toolbarItem;
371
+ }
372
+ return nil;
373
+ }
374
+
375
+ - (void)toolbarItemClicked:(NSToolbarItem *)sender
376
+ {
377
+ // Drop focus from any in-flight text field or textarea before firing the
378
+ // action. Without this the reactive `value`-update guard in applyTextProps /
379
+ // applyTextAreaProps sees firstResponder != nil and skips the write, so the
380
+ // editor title/body never clears when the user clicks e.g. "New Note" while
381
+ // the body textarea is still focused. (Same pattern as the gesture
382
+ // recognizer's NSGestureRecognizerStateBegan handler.)
383
+ NSWindow *window = [NSApp keyWindow];
384
+ if (window && window.firstResponder != window) {
385
+ [window makeFirstResponder:nil];
386
+ }
387
+ const std::string ident = sender.itemIdentifier.UTF8String;
388
+ for (const ToolbarItemInfo &info : *_toolbarItems) {
389
+ if (info.identifier == ident) {
390
+ firePressForElementId(info.actionId);
391
+ return;
392
+ }
393
+ }
394
+ }
395
+
396
+ @end