@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,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
|