@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,176 @@
1
+ #import "canvas_view.h"
2
+
3
+ #include "ui/tree_internal.h"
4
+ #include "canvas.h"
5
+ #include "events.h"
6
+ #include "host/display_orientation.h"
7
+ #include "pixel.h"
8
+
9
+ #include <algorithm>
10
+ #include <cmath>
11
+
12
+ extern "C" void gea_macos_touch_set_state(int touching, int x, int y);
13
+
14
+ namespace {
15
+
16
+ // Single shared scratch buffer for RGB565→RGBA8888 expansion. Sized lazily to
17
+ // the largest canvas seen. -drawRect: is main-thread-only so no locking.
18
+ std::uint8_t *g_scratch = nullptr;
19
+ size_t g_scratchBytes = 0;
20
+
21
+ std::uint8_t *ensureScratch(size_t bytes)
22
+ {
23
+ if (g_scratchBytes < bytes) {
24
+ void *grown = std::realloc(g_scratch, bytes);
25
+ if (!grown) return nullptr;
26
+ g_scratch = static_cast<std::uint8_t *>(grown);
27
+ g_scratchBytes = bytes;
28
+ }
29
+ return g_scratch;
30
+ }
31
+
32
+ } // namespace
33
+
34
+ @implementation GeaCanvasView
35
+
36
+ - (instancetype)initWithFrame:(NSRect)frame
37
+ {
38
+ self = [super initWithFrame:frame];
39
+ if (self) {
40
+ _nodeId = -1;
41
+ }
42
+ return self;
43
+ }
44
+
45
+ // Stay UNFLIPPED. The canvas pixel buffer is stored top-down (row 0 = top),
46
+ // which is what CGContextDrawImage expects when the destination context uses
47
+ // default Quartz (bottom-up) coords. Setting isFlipped:YES would make AppKit
48
+ // apply an implicit Y-inversion and the image would render upside-down. The
49
+ // renderer that positions this view already converts CSS top-left coords to
50
+ // AppKit bottom-left when computing our frame, so unflipped is correct.
51
+ - (BOOL)isFlipped { return NO; }
52
+
53
+ // Mouse → framework touch events, mirroring the iOS renderer's dispatchTouch
54
+ // flow. Canvas apps (maps, games) read gesture coordinates off onPointerDown/
55
+ // Move/Up — the AppKit click recognizer only fires coordinate-less presses, so
56
+ // without this a drag never pans and every tap lands at 0,0. The recognizer's
57
+ // delegate declines canvas views, so events arrive here exactly once.
58
+ - (BOOL)acceptsFirstMouse:(NSEvent *)event
59
+ {
60
+ (void)event;
61
+ return YES;
62
+ }
63
+
64
+ - (void)dispatchMousePhase:(gea::framework::events::TouchPhase)phase
65
+ touching:(bool)touching
66
+ event:(NSEvent *)event
67
+ {
68
+ if (self.nodeId < 0) return;
69
+ auto &tree = gea::embedded::ui::Tree::instance();
70
+ if (self.nodeId >= tree.nodeCount()) return;
71
+ const auto *canvas = tree.canvas(self.nodeId);
72
+ const NSRect bounds = self.bounds;
73
+ if (!canvas || bounds.size.width <= 0 || bounds.size.height <= 0) return;
74
+
75
+ // View point (bottom-up) → top-down surface coords, scaled to the canvas
76
+ // pixel grid (drawRect stretches the canvas into the view bounds, so input
77
+ // must apply the inverse of that stretch to stay aligned).
78
+ const NSPoint p = [self convertPoint:event.locationInWindow fromView:nil];
79
+ const double scaleX = canvas->width() / bounds.size.width;
80
+ const double scaleY = canvas->height() / bounds.size.height;
81
+ const int lx = std::clamp((int)std::lround(p.x * scaleX), 0, canvas->width() - 1);
82
+ const int ly = std::clamp((int)std::lround((bounds.size.height - 1 - p.y) * scaleY), 0,
83
+ canvas->height() - 1);
84
+
85
+ // The latest-move cache holds PANEL-NATIVE coordinates (the hardware touch
86
+ // controller contract): the dispatcher re-reads Move coords from it through
87
+ // transformTouchToLogical, so store the inverse-transformed point.
88
+ namespace od = gea::framework::display::detail;
89
+ using gea::framework::display::DisplayOrientation;
90
+ int px = lx;
91
+ int py = ly;
92
+ switch (od::DisplayOrientationState::orientation()) {
93
+ case DisplayOrientation::LandscapePrimary:
94
+ px = ly;
95
+ py = od::DisplayOrientationState::nativeHeight() - 1 - lx;
96
+ break;
97
+ case DisplayOrientation::LandscapeSecondary:
98
+ px = od::DisplayOrientationState::nativeWidth() - 1 - ly;
99
+ py = lx;
100
+ break;
101
+ case DisplayOrientation::PortraitSecondary:
102
+ px = od::DisplayOrientationState::nativeWidth() - 1 - lx;
103
+ py = od::DisplayOrientationState::nativeHeight() - 1 - ly;
104
+ break;
105
+ case DisplayOrientation::PortraitPrimary:
106
+ default:
107
+ break;
108
+ }
109
+ gea_macos_touch_set_state(touching ? 1 : 0, px, py);
110
+
111
+ gea::framework::events::Event ev{};
112
+ ev.type = gea::framework::events::EventType::Touch;
113
+ ev.touchPhase = phase;
114
+ ev.touching = touching;
115
+ ev.x = lx;
116
+ ev.y = ly;
117
+ ev.pointerId = 0;
118
+ gea::framework::events::TouchRuntime::dispatchEvent(ev);
119
+ }
120
+
121
+ - (void)mouseDown:(NSEvent *)event
122
+ {
123
+ [self dispatchMousePhase:gea::framework::events::TouchPhase::Down touching:true event:event];
124
+ }
125
+
126
+ - (void)mouseDragged:(NSEvent *)event
127
+ {
128
+ [self dispatchMousePhase:gea::framework::events::TouchPhase::Move touching:true event:event];
129
+ }
130
+
131
+ - (void)mouseUp:(NSEvent *)event
132
+ {
133
+ [self dispatchMousePhase:gea::framework::events::TouchPhase::Up touching:false event:event];
134
+ }
135
+
136
+ - (void)drawRect:(NSRect)dirtyRect
137
+ {
138
+ (void)dirtyRect;
139
+ if (self.nodeId < 0) return;
140
+ auto &tree = gea::embedded::ui::Tree::instance();
141
+ if (self.nodeId >= tree.nodeCount()) return;
142
+ const auto *canvas = tree.canvas(self.nodeId);
143
+ if (!canvas) return;
144
+ const std::uint16_t *src = canvas->pixels();
145
+ const int w = canvas->width();
146
+ const int h = canvas->height();
147
+ if (!src || w <= 0 || h <= 0) return;
148
+
149
+ const size_t bytes = (size_t)w * (size_t)h * 4;
150
+ std::uint8_t *dst = ensureScratch(bytes);
151
+ if (!dst) return;
152
+ for (int i = 0; i < w * h; i++) {
153
+ int r, g, b;
154
+ gea::framework::graphics::pixel::unpackRgb565(src[i], &r, &g, &b);
155
+ dst[i * 4 + 0] = static_cast<std::uint8_t>((r * 255 + 15) / 31);
156
+ dst[i * 4 + 1] = static_cast<std::uint8_t>((g * 255 + 31) / 63);
157
+ dst[i * 4 + 2] = static_cast<std::uint8_t>((b * 255 + 15) / 31);
158
+ dst[i * 4 + 3] = 0xFF;
159
+ }
160
+
161
+ CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
162
+ CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, dst, bytes, NULL);
163
+ const uint32_t bitmapInfo = (uint32_t)kCGImageAlphaNoneSkipLast | (uint32_t)kCGBitmapByteOrder32Big;
164
+ CGImageRef img = CGImageCreate((size_t)w, (size_t)h, 8, 32, (size_t)w * 4,
165
+ cs, bitmapInfo,
166
+ provider, NULL, false, kCGRenderingIntentDefault);
167
+
168
+ CGContextRef ctx = [[NSGraphicsContext currentContext] CGContext];
169
+ CGContextDrawImage(ctx, NSRectToCGRect(self.bounds), img);
170
+
171
+ CGImageRelease(img);
172
+ CGDataProviderRelease(provider);
173
+ CGColorSpaceRelease(cs);
174
+ }
175
+
176
+ @end
@@ -0,0 +1,18 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+
5
+ #ifdef __OBJC__
6
+ @class NSColor;
7
+ #endif
8
+
9
+ namespace gea::macos {
10
+
11
+ #ifdef __OBJC__
12
+ NSColor *rgb565ToNSColor(std::uint16_t rgb565);
13
+ #endif
14
+
15
+ // Caller-owns the returned CGColorRef; release with CGColorRelease.
16
+ struct CGColor *rgb565ToCGColor(std::uint16_t rgb565);
17
+
18
+ } // namespace gea::macos
@@ -0,0 +1,39 @@
1
+ #import <AppKit/AppKit.h>
2
+
3
+ #include "color_convert.h"
4
+ #include "pixel.h"
5
+
6
+ namespace gea::macos {
7
+
8
+ namespace {
9
+
10
+ void unpack(std::uint16_t rgb565, CGFloat *r, CGFloat *g, CGFloat *b)
11
+ {
12
+ int ri, gi, bi;
13
+ gea::framework::graphics::pixel::unpackRgb565(rgb565, &ri, &gi, &bi);
14
+ *r = static_cast<CGFloat>(ri) / 31.0;
15
+ *g = static_cast<CGFloat>(gi) / 63.0;
16
+ *b = static_cast<CGFloat>(bi) / 31.0;
17
+ }
18
+
19
+ } // namespace
20
+
21
+ NSColor *rgb565ToNSColor(std::uint16_t rgb565)
22
+ {
23
+ CGFloat r, g, b;
24
+ unpack(rgb565, &r, &g, &b);
25
+ return [NSColor colorWithSRGBRed:r green:g blue:b alpha:1.0];
26
+ }
27
+
28
+ CGColor *rgb565ToCGColor(std::uint16_t rgb565)
29
+ {
30
+ CGFloat r, g, b;
31
+ unpack(rgb565, &r, &g, &b);
32
+ CGFloat comps[4] = {r, g, b, 1.0};
33
+ CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
34
+ CGColorRef color = CGColorCreate(cs, comps);
35
+ CGColorSpaceRelease(cs);
36
+ return color;
37
+ }
38
+
39
+ } // namespace gea::macos
@@ -0,0 +1,35 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+
5
+ #ifdef __OBJC__
6
+ #import <AppKit/AppKit.h>
7
+ @class NSFont;
8
+ @class NSString;
9
+ #endif
10
+
11
+ namespace gea::macos {
12
+
13
+ #ifdef __OBJC__
14
+ // Returns an NSFont for the given gea font id at the requested pixel size.
15
+ // fontId comes from the framework's lookupFontFamily which we override below
16
+ // to assign stable ids the first time each family is seen. Unknown ids fall
17
+ // back to the system font.
18
+ NSFont *fontForId(int fontId, int sizePx);
19
+
20
+ // Walks `directoryPath` for .ttf / .otf files and registers each with
21
+ // CTFontManager in the process scope. Returns the count of newly registered
22
+ // fonts; already-registered files are silently skipped. Call once at startup
23
+ // after the app bundle is known.
24
+ int registerCustomTtfDirectory(NSString *directoryPath);
25
+
26
+ // Picks the right NSLineBreakMode for `text` rendered in `font` within
27
+ // `maxWidth` points. Returns word-wrap when every space-separated chunk fits
28
+ // on a line by itself, otherwise char-wrap so that a long unbreakable run
29
+ // (like "TYPOGRAPHY") gets split mid-glyph instead of overflowing and being
30
+ // clipped at the field edge. Shared between the host measurement hook and
31
+ // the NSTextField renderer so both compute identical geometry.
32
+ NSLineBreakMode lineBreakModeForText(NSString *text, NSFont *font, CGFloat maxWidth);
33
+ #endif
34
+
35
+ } // namespace gea::macos
@@ -0,0 +1,350 @@
1
+ #import <AppKit/AppKit.h>
2
+ #import <CoreText/CoreText.h>
3
+
4
+ #include "font_registry.h"
5
+ #include "graphics/font.h"
6
+
7
+ #include <algorithm>
8
+ #include <cctype>
9
+ #include <cmath>
10
+ #include <cstdint>
11
+ #include <cstring>
12
+ #include <mutex>
13
+ #include <string>
14
+ #include <unordered_map>
15
+ #include <utility>
16
+ #include <vector>
17
+
18
+ namespace gea::macos {
19
+
20
+ namespace {
21
+
22
+ // Family name registry. Indexed by familyId; ids are handed out in the order
23
+ // the framework first asks for them. CSS family lookups all flow through
24
+ // generated::lookupFontFamily below.
25
+ //
26
+ // Construct-on-first-use, NOT namespace-scope globals: an app with a static
27
+ // `font-family` CSS rule (maps' device-fonts.css) registers it from a GLOBAL
28
+ // CONSTRUCTOR — the generated GeaPluginCppPreludeRegistration calls
29
+ // registerStaticFontFamilyRule → lookupFontFamily during dyld initializers,
30
+ // which may run BEFORE this translation unit's own initializers. With plain
31
+ // globals the unordered_map was still zeroed BSS at that point (max load
32
+ // factor 0 → __next_prime overflow_error → abort at launch), and locking the
33
+ // unconstructed mutex is UB. A function-local static is constructed on first
34
+ // use (thread-safe since C++11), so the registry is valid whenever reached.
35
+ struct FamilyRegistry {
36
+ std::mutex lock;
37
+ std::vector<std::string> names;
38
+ std::unordered_map<std::string, int> byName;
39
+ };
40
+
41
+ FamilyRegistry &familyRegistry()
42
+ {
43
+ static FamilyRegistry registry;
44
+ return registry;
45
+ }
46
+
47
+ int registerFamily(const std::string &name)
48
+ {
49
+ FamilyRegistry &registry = familyRegistry();
50
+ std::scoped_lock guard(registry.lock);
51
+ auto it = registry.byName.find(name);
52
+ if (it != registry.byName.end()) return it->second;
53
+ const int id = static_cast<int>(registry.names.size());
54
+ registry.names.push_back(name);
55
+ registry.byName.emplace(name, id);
56
+ return id;
57
+ }
58
+
59
+ NSString *familyForId(int id)
60
+ {
61
+ FamilyRegistry &registry = familyRegistry();
62
+ std::scoped_lock guard(registry.lock);
63
+ if (id < 0 || static_cast<size_t>(id) >= registry.names.size()) return nil;
64
+ return [NSString stringWithUTF8String:registry.names[id].c_str()];
65
+ }
66
+
67
+ // Raw TTF/OTF bytes of the bundle's custom fonts, keyed by lowercased family
68
+ // name. The engine's runtime TTF rasterizer (GEA_EMBEDDED_TTF_RUNTIME_FONTS)
69
+ // bakes canvas glyph atlases from these via lookupRuntimeTtfFontForFamily —
70
+ // without it, ctx.fillText fell back to the builtin bitmap font and canvas
71
+ // labels (maps' place pins) rendered garbled. The byte buffers must live for
72
+ // the process lifetime: the rasterizer keeps stbtt_fontinfo pointers into
73
+ // them across atlas rebuilds. Construct-on-first-use for the same dyld-
74
+ // initializer-ordering reason as FamilyRegistry above.
75
+ struct TtfByteRegistry {
76
+ std::mutex lock;
77
+ std::unordered_map<std::string, std::vector<std::uint8_t>> byFamily;
78
+ };
79
+
80
+ TtfByteRegistry &ttfByteRegistry()
81
+ {
82
+ static TtfByteRegistry registry;
83
+ return registry;
84
+ }
85
+
86
+ std::string lowercased(const std::string &value)
87
+ {
88
+ std::string out = value;
89
+ std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
90
+ return out;
91
+ }
92
+
93
+ void registerTtfBytesForFamily(NSString *path)
94
+ {
95
+ NSData *data = [NSData dataWithContentsOfFile:path];
96
+ if (data.length == 0) return;
97
+ CTFontDescriptorRef desc = CTFontManagerCreateFontDescriptorFromData((__bridge CFDataRef)data);
98
+ if (!desc) return;
99
+ CFStringRef family = (CFStringRef)CTFontDescriptorCopyAttribute(desc, kCTFontFamilyNameAttribute);
100
+ CFRelease(desc);
101
+ if (!family) return;
102
+ char buf[256] = {0};
103
+ const bool ok = CFStringGetCString(family, buf, sizeof(buf), kCFStringEncodingUTF8);
104
+ CFRelease(family);
105
+ if (!ok || !buf[0]) return;
106
+ const auto *bytes = static_cast<const std::uint8_t *>(data.bytes);
107
+ TtfByteRegistry &registry = ttfByteRegistry();
108
+ std::scoped_lock guard(registry.lock);
109
+ // First face wins (e.g. Inter-Regular before other weights of the family):
110
+ // the runtime rasterizer keys on the family alone.
111
+ registry.byFamily.emplace(lowercased(buf), std::vector<std::uint8_t>(bytes, bytes + data.length));
112
+ }
113
+
114
+ } // namespace
115
+
116
+ // Non-anonymous accessor for the generated-hook namespace below: resolve a
117
+ // framework familyId to the bundled TTF bytes feeding the runtime rasterizer.
118
+ const std::uint8_t *runtimeTtfBytesForFamilyId(int familyId, unsigned long *length)
119
+ {
120
+ if (length) *length = 0;
121
+ NSString *family = familyForId(familyId);
122
+ if (!family || family.length == 0) return nullptr;
123
+ TtfByteRegistry &registry = ttfByteRegistry();
124
+ std::scoped_lock guard(registry.lock);
125
+ const auto it = registry.byFamily.find(lowercased(family.UTF8String));
126
+ if (it == registry.byFamily.end()) return nullptr;
127
+ if (length) *length = it->second.size();
128
+ return it->second.data();
129
+ }
130
+
131
+ static NSFont *self_resolveFont(int fontId, CGFloat size);
132
+
133
+ NSFont *fontForId(int fontId, int sizePx)
134
+ {
135
+ CGFloat size = sizePx > 0 ? static_cast<CGFloat>(sizePx) : [NSFont systemFontSize];
136
+
137
+ // Cache resolved fonts by (fontId, size). `+[NSFont fontWithName:size:]` and
138
+ // the CTFontDescriptor fallback below both do a CoreText font-descriptor
139
+ // match, which is expensive — and this is called for EVERY text node on
140
+ // EVERY frame (renderer sync + layout measurement). Without this cache,
141
+ // CoreText font matching dominated the profile and pegged a CPU core. The
142
+ // set of (family, size) pairs an app uses is tiny, so the cache stays small.
143
+ static NSMutableDictionary<NSNumber *, NSFont *> *cache;
144
+ static dispatch_once_t once;
145
+ dispatch_once(&once, ^{ cache = [NSMutableDictionary dictionary]; });
146
+ NSNumber *key = @(static_cast<long long>(fontId) * 100000LL + llround(size));
147
+ @synchronized(cache) {
148
+ NSFont *hit = cache[key];
149
+ if (hit) return hit;
150
+ }
151
+ NSFont *resolved = self_resolveFont(fontId, size);
152
+ if (resolved) {
153
+ @synchronized(cache) { cache[key] = resolved; }
154
+ }
155
+ return resolved;
156
+ }
157
+
158
+ static NSFont *self_resolveFont(int fontId, CGFloat size)
159
+ {
160
+ NSString *family = familyForId(fontId);
161
+ if (!family || family.length == 0) return [NSFont systemFontOfSize:size];
162
+ // CSS system-font keywords map to the actual system UI font — San Francisco
163
+ // on macOS. There is no PostScript face literally named "-apple-system", so
164
+ // `+[NSFont fontWithName:@"-apple-system"]` returns nil and we'd otherwise
165
+ // fall through to a wrong family. systemFontOfSize: IS San Francisco.
166
+ NSString *lower = family.lowercaseString;
167
+ if ([lower isEqualToString:@"-apple-system"] || [lower isEqualToString:@"system-ui"] ||
168
+ [lower isEqualToString:@"blinkmacsystemfont"] || [lower hasPrefix:@".applesystemuifont"] ||
169
+ [lower hasPrefix:@".sf"] || [lower isEqualToString:@"san francisco"] ||
170
+ [lower isEqualToString:@"sf pro"] || [lower isEqualToString:@"sf pro text"]) {
171
+ return [NSFont systemFontOfSize:size];
172
+ }
173
+ NSFont *font = [NSFont fontWithName:family size:size];
174
+ if (font) return font;
175
+ // CTFont's family lookup is more forgiving than NSFont's PostScript-name
176
+ // lookup — fall back to building a CTFont with the family attribute.
177
+ NSDictionary *attrs = @{
178
+ (__bridge NSString *)kCTFontFamilyNameAttribute: family,
179
+ (__bridge NSString *)kCTFontSizeAttribute: @(size),
180
+ };
181
+ CTFontDescriptorRef desc = CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)attrs);
182
+ CTFontRef ct = CTFontCreateWithFontDescriptor(desc, size, NULL);
183
+ CFRelease(desc);
184
+ if (ct) {
185
+ NSFont *resolved = (__bridge_transfer NSFont *)ct;
186
+ return resolved;
187
+ }
188
+ return [NSFont systemFontOfSize:size];
189
+ }
190
+
191
+ int registerCustomTtfDirectory(NSString *directoryPath)
192
+ {
193
+ NSFileManager *fm = [NSFileManager defaultManager];
194
+ BOOL isDir = NO;
195
+ if (![fm fileExistsAtPath:directoryPath isDirectory:&isDir] || !isDir) return 0;
196
+ NSArray<NSString *> *entries = [fm contentsOfDirectoryAtPath:directoryPath error:nil];
197
+ int count = 0;
198
+ for (NSString *name in entries) {
199
+ NSString *lower = name.lowercaseString;
200
+ if (![lower hasSuffix:@".ttf"] && ![lower hasSuffix:@".otf"]) continue;
201
+ NSString *path = [directoryPath stringByAppendingPathComponent:name];
202
+ registerTtfBytesForFamily(path);
203
+ CFErrorRef err = NULL;
204
+ CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:path];
205
+ if (CTFontManagerRegisterFontsForURL(url, kCTFontManagerScopeProcess, &err)) {
206
+ count++;
207
+ } else if (err) {
208
+ NSError *e = (__bridge_transfer NSError *)err;
209
+ // kCTFontManagerErrorAlreadyRegistered == 105 — fine on rebuilds.
210
+ if (e.code != 105) {
211
+ NSLog(@"[gea] failed to register font %@: %@", name, e.localizedDescription);
212
+ }
213
+ }
214
+ }
215
+ return count;
216
+ }
217
+
218
+ } // namespace gea::macos
219
+
220
+ // Strong overrides of the framework's weak font-lookup hooks for the AppKit
221
+ // renderer. The Thermalright target uses Gea's generated raster font atlas
222
+ // instead, so these host-font overrides must stay out of that build.
223
+ #if !GEA_MACOS_THERMALRIGHT_DISPLAY_TARGET
224
+ namespace gea::framework::graphics::generated {
225
+
226
+ int lookupFontFamily(const char *family)
227
+ {
228
+ if (!family || !family[0]) return -1;
229
+ return gea::macos::registerFamily(std::string(family));
230
+ }
231
+
232
+ const RasterizedFontData *lookupFontForFamily(int, int) { return nullptr; }
233
+ const RasterizedFontData *lookupFont(int) { return nullptr; }
234
+ void ensureLinked() {}
235
+
236
+ // Feeds the engine's runtime TTF rasterizer (canvas ctx.fillText). AppKit
237
+ // text nodes never come through here — they resolve native NSFonts via
238
+ // fontForId above.
239
+ const std::uint8_t *lookupRuntimeTtfFontForFamily(int familyId, unsigned long *length)
240
+ {
241
+ return gea::macos::runtimeTtfBytesForFamilyId(familyId, length);
242
+ }
243
+
244
+ } // namespace gea::framework::graphics::generated
245
+ #endif
246
+
247
+ namespace gea::macos {
248
+
249
+ // Choose a line-break mode that breaks AT word boundaries when possible,
250
+ // falling back to mid-character when an unbreakable run is wider than the
251
+ // available width. The renderer and the measurement path share this logic
252
+ // so AppKit's render geometry always matches the framework's measurement.
253
+ NSLineBreakMode lineBreakModeForText(NSString *text, NSFont *font, CGFloat maxWidth)
254
+ {
255
+ if (maxWidth <= 0 || text.length == 0 || !font) return NSLineBreakByWordWrapping;
256
+ NSDictionary *probeAttrs = @{ NSFontAttributeName: font };
257
+ // Split on whitespace and check each chunk individually. If even one
258
+ // "word" wouldn't fit on a line by itself, word-wrap can't help us —
259
+ // we'd render that word overflowing the field width and the parent
260
+ // tile's overflow:hidden would clip it mid-glyph with no indication.
261
+ // Char-wrap breaks inside the word so all of it stays visible.
262
+ NSArray<NSString *> *chunks = [text componentsSeparatedByCharactersInSet:
263
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
264
+ for (NSString *chunk in chunks) {
265
+ if (chunk.length == 0) continue;
266
+ CGFloat chunkWidth = [chunk sizeWithAttributes:probeAttrs].width;
267
+ if (chunkWidth > maxWidth) return NSLineBreakByCharWrapping;
268
+ }
269
+ return NSLineBreakByWordWrapping;
270
+ }
271
+
272
+ } // namespace gea::macos
273
+
274
+ // Host text-measurement hook for the AppKit renderer. The Thermalright target
275
+ // has no AppKit text renderer; it wants the framework's generated-font metrics.
276
+ #if !GEA_MACOS_THERMALRIGHT_DISPLAY_TARGET
277
+ extern "C" bool gea_host_measure_text(const char *text,
278
+ int maxWidth,
279
+ int fontId,
280
+ int fontSize,
281
+ int *outWidth,
282
+ int *outHeight)
283
+ {
284
+ if (!outWidth || !outHeight) return false;
285
+ if (!text || !text[0]) {
286
+ *outWidth = 0;
287
+ *outHeight = 0;
288
+ return true;
289
+ }
290
+
291
+ // Memoize by (text, maxWidth, fontId, fontSize): the layout engine re-measures
292
+ // EVERY text node on EVERY layout pass, and a pass now runs on every
293
+ // interaction. NSTextFieldCell.cellSizeForBounds:/sizeWithAttributes:
294
+ // dominated the interaction profile; the inputs fully determine the result,
295
+ // so a cache turns repeat measurements (the stable note list) into O(1).
296
+ static std::mutex measureLock;
297
+ static std::unordered_map<std::string, std::pair<int, int>> measureCache;
298
+ std::string key;
299
+ key.reserve(std::strlen(text) + 24);
300
+ key.append(text);
301
+ key.push_back('\x1f');
302
+ key.append(std::to_string(maxWidth));
303
+ key.push_back('\x1f');
304
+ key.append(std::to_string(fontId));
305
+ key.push_back('\x1f');
306
+ key.append(std::to_string(fontSize));
307
+ {
308
+ std::scoped_lock guard(measureLock);
309
+ auto it = measureCache.find(key);
310
+ if (it != measureCache.end()) {
311
+ *outWidth = it->second.first;
312
+ *outHeight = it->second.second;
313
+ return true;
314
+ }
315
+ }
316
+
317
+ NSFont *font = gea::macos::fontForId(fontId, fontSize);
318
+ if (!font) return false;
319
+ NSString *str = [NSString stringWithUTF8String:text] ?: @"";
320
+ const CGFloat boundedMax = maxWidth > 0 ? maxWidth : CGFLOAT_MAX;
321
+ NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
322
+ paragraph.lineBreakMode = gea::macos::lineBreakModeForText(str, font, boundedMax);
323
+ NSDictionary *attrs = @{
324
+ NSFontAttributeName: font,
325
+ NSParagraphStyleAttributeName: paragraph,
326
+ };
327
+ NSAttributedString *attributed = [[NSAttributedString alloc] initWithString:str attributes:attrs];
328
+ // Use an actual NSTextFieldCell to measure: this is exactly what the
329
+ // renderer's NSTextField uses to lay text out, so the size we report
330
+ // here is the size AppKit will draw into — no off-by-side-bearing,
331
+ // no kerning surprise, no device-metrics vs typographic gap. Sizing
332
+ // against a fresh cell of size (maxWidth, large) returns the wrapped
333
+ // height + the actual line width AppKit needs.
334
+ NSTextFieldCell *cell = [[NSTextFieldCell alloc] init];
335
+ cell.bezeled = NO;
336
+ cell.bordered = NO;
337
+ cell.drawsBackground = NO;
338
+ cell.wraps = YES;
339
+ cell.attributedStringValue = attributed;
340
+ const NSSize cellSize = [cell cellSizeForBounds:NSMakeRect(0, 0, boundedMax, CGFLOAT_MAX)];
341
+ *outWidth = static_cast<int>(std::ceil(cellSize.width));
342
+ *outHeight = static_cast<int>(std::ceil(cellSize.height));
343
+ {
344
+ std::scoped_lock guard(measureLock);
345
+ if (measureCache.size() > 8192) measureCache.clear(); // bound unbounded growth
346
+ measureCache.emplace(std::move(key), std::make_pair(*outWidth, *outHeight));
347
+ }
348
+ return true;
349
+ }
350
+ #endif
@@ -0,0 +1,16 @@
1
+ #pragma once
2
+
3
+ #ifdef __OBJC__
4
+ @class NSImage;
5
+ #endif
6
+
7
+ namespace gea::macos {
8
+
9
+ #ifdef __OBJC__
10
+ // Returns an NSImage built from the ImageStore slot at imageId, or nil if
11
+ // the slot is empty / invalid. Pixels are converted from RGB565 (+ optional
12
+ // alpha channel) to RGBA8888. Caller owns the result via ARC.
13
+ NSImage *imageForId(int imageId);
14
+ #endif
15
+
16
+ } // namespace gea::macos
@@ -0,0 +1,52 @@
1
+ #import <AppKit/AppKit.h>
2
+
3
+ #include "image_bridge.h"
4
+ #include "image.h"
5
+ #include "pixel.h"
6
+
7
+ namespace gea::macos {
8
+
9
+ NSImage *imageForId(int imageId)
10
+ {
11
+ using namespace gea::framework::graphics;
12
+ if (imageId < 0) return nil;
13
+ auto &store = ImageStore::instance();
14
+ const int w = store.width(imageId);
15
+ const int h = store.height(imageId);
16
+ const std::uint16_t *src = store.currentPixels(imageId);
17
+ const std::uint8_t *alpha = store.currentAlpha(imageId);
18
+ if (w <= 0 || h <= 0 || !src) return nil;
19
+
20
+ const NSInteger pixelCount = w * h;
21
+ NSMutableData *rgba = [NSMutableData dataWithLength:(NSUInteger)(pixelCount * 4)];
22
+ std::uint8_t *dst = static_cast<std::uint8_t *>(rgba.mutableBytes);
23
+ for (NSInteger i = 0; i < pixelCount; i++) {
24
+ int r, g, b;
25
+ pixel::unpackRgb565(src[i], &r, &g, &b);
26
+ dst[i * 4 + 0] = static_cast<std::uint8_t>((r * 255 + 15) / 31);
27
+ dst[i * 4 + 1] = static_cast<std::uint8_t>((g * 255 + 31) / 63);
28
+ dst[i * 4 + 2] = static_cast<std::uint8_t>((b * 255 + 15) / 31);
29
+ dst[i * 4 + 3] = alpha ? alpha[i] : 0xFF;
30
+ }
31
+
32
+ NSBitmapImageRep *rep =
33
+ [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
34
+ pixelsWide:w
35
+ pixelsHigh:h
36
+ bitsPerSample:8
37
+ samplesPerPixel:4
38
+ hasAlpha:YES
39
+ isPlanar:NO
40
+ colorSpaceName:NSDeviceRGBColorSpace
41
+ bitmapFormat:NSBitmapFormatAlphaNonpremultiplied
42
+ bytesPerRow:w * 4
43
+ bitsPerPixel:32];
44
+ if (!rep) return nil;
45
+ memcpy([rep bitmapData], dst, (size_t)(pixelCount * 4));
46
+
47
+ NSImage *img = [[NSImage alloc] initWithSize:NSMakeSize(w, h)];
48
+ [img addRepresentation:rep];
49
+ return img;
50
+ }
51
+
52
+ } // namespace gea::macos