@geastack/core 0.1.23

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 (73) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +36 -0
  3. package/app_frame_perf.cpp +77 -0
  4. package/css.d.ts +1 -0
  5. package/events.cpp +67 -0
  6. package/gea_app_entry.cpp +317 -0
  7. package/gea_sources.mjs +126 -0
  8. package/gea_sources.sh +36 -0
  9. package/include/app.h +60 -0
  10. package/include/apps.h +46 -0
  11. package/include/audio.h +90 -0
  12. package/include/ble.h +212 -0
  13. package/include/bluetooth/hid_profile.h +92 -0
  14. package/include/camera.h +107 -0
  15. package/include/canvas.h +290 -0
  16. package/include/display.h +390 -0
  17. package/include/display_present.h +869 -0
  18. package/include/event.h +32 -0
  19. package/include/events.h +116 -0
  20. package/include/gea/embedded.h +13 -0
  21. package/include/gea/reactive_runtime.h +32 -0
  22. package/include/gea.h +14 -0
  23. package/include/gea_perf_config.h +96 -0
  24. package/include/geaos/abi.h +165 -0
  25. package/include/geolocation.h +126 -0
  26. package/include/gpio.h +61 -0
  27. package/include/graphics/font.h +122 -0
  28. package/include/i2c.h +20 -0
  29. package/include/image.h +156 -0
  30. package/include/imu.h +23 -0
  31. package/include/input.h +33 -0
  32. package/include/memory.h +52 -0
  33. package/include/pixel.h +907 -0
  34. package/include/platform/file_cache.h +18 -0
  35. package/include/power.h +11 -0
  36. package/include/runtime.h +83 -0
  37. package/include/services/app_runner.h +10 -0
  38. package/include/services/app_state.h +14 -0
  39. package/include/services/battery_service.h +12 -0
  40. package/include/services/bluetooth_service.h +13 -0
  41. package/include/services/diagnostics.h +76 -0
  42. package/include/services/diagnostics_platform.h +54 -0
  43. package/include/services/frame_scheduler.h +71 -0
  44. package/include/services/mirror.h +49 -0
  45. package/include/services/network_services.h +25 -0
  46. package/include/services/ota.h +32 -0
  47. package/include/services/runtime_log.h +11 -0
  48. package/include/services/storage_service.h +26 -0
  49. package/include/touch.h +38 -0
  50. package/include/ui/signal.h +149 -0
  51. package/include/wifi.h +67 -0
  52. package/index.d.ts +1940 -0
  53. package/jsx.d.ts +1 -0
  54. package/package.json +70 -0
  55. package/runtime/ble.ts +49 -0
  56. package/runtime/color.ts +21 -0
  57. package/runtime/compiler.ts +43 -0
  58. package/runtime/host.ts +1083 -0
  59. package/runtime/images.ts +129 -0
  60. package/runtime/primitives.ts +49 -0
  61. package/runtime-surface.ts +180 -0
  62. package/runtime.cpp +431 -0
  63. package/runtime.ts +34 -0
  64. package/scripts/build-gea-vite-geatsc.mjs +4514 -0
  65. package/scripts/dotenv-defines.mjs +98 -0
  66. package/scripts/gea-bundle-type-hints.mjs +546 -0
  67. package/scripts/gea-embedded-compat-staging.mjs +17 -0
  68. package/scripts/gea-embedded-compat-transform.mjs +427 -0
  69. package/scripts/gea-vite-module-graph-plugin.mjs +578 -0
  70. package/scripts/generate-gea-embedded-assets.mjs +296 -0
  71. package/scripts/generate-gea-embedded-fonts.mjs +1252 -0
  72. package/scripts/generate-installed-app-plan.mjs +55 -0
  73. package/scripts/native-object-cache.sh +244 -0
package/runtime.cpp ADDED
@@ -0,0 +1,431 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ #include "runtime.h"
3
+
4
+ #include "app.h"
5
+ #include "apps.h"
6
+ #include "display.h"
7
+ #include "input.h"
8
+ #include "memory.h"
9
+ #include "events.h"
10
+ #include "power.h"
11
+ #include "services/app_runner.h"
12
+ #include "services/app_state.h"
13
+ #include "services/battery_service.h"
14
+ #include "services/bluetooth_service.h"
15
+ #include "services/diagnostics.h"
16
+ #include "services/frame_scheduler.h"
17
+ #include "services/network_services.h"
18
+ #include "services/runtime_log.h"
19
+ #include "services/settings_surface.h"
20
+ #include "services/storage_service.h"
21
+
22
+ #include "host/storage.h"
23
+ #include "wifi.h"
24
+
25
+ #if __has_include("wifi_config.h")
26
+ #include "wifi_config.h"
27
+ #endif
28
+ #if __has_include("gea_embedded_app_config.h")
29
+ #include "gea_embedded_app_config.h"
30
+ #endif
31
+ #ifndef GEA_EMBEDDED_WIFI_SSID
32
+ #define GEA_EMBEDDED_WIFI_SSID ""
33
+ #endif
34
+ #ifndef GEA_EMBEDDED_WIFI_EARLY_CONNECT
35
+ #define GEA_EMBEDDED_WIFI_EARLY_CONNECT 0
36
+ #endif
37
+
38
+ #include "css/declarative.h"
39
+ #include "css/engine.h"
40
+ #include "ui/style.h"
41
+ #include "ui/document.h"
42
+ #include "ui/tree_internal.h"
43
+
44
+ namespace gea::framework {
45
+
46
+ namespace {
47
+
48
+ #if defined(GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT) && GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT
49
+
50
+ void run_app_frame(int timestampMs, void *context)
51
+ {
52
+ (void)context;
53
+ events::TouchRuntime::poll(timestampMs);
54
+ services::BatteryService::poll(timestampMs);
55
+ app::Application::frame(timestampMs);
56
+ }
57
+
58
+ void dispatch_event(const events::Event &event, const RuntimeOptions &)
59
+ {
60
+ if (event.type != events::EventType::Frame) return;
61
+ services::FrameScheduler::FrameCallbacks frameCallbacks{};
62
+ frameCallbacks.frame = run_app_frame;
63
+ services::FrameScheduler::runFrame(frameCallbacks);
64
+ }
65
+
66
+ #else
67
+
68
+ void dispatch_rotary_input()
69
+ {
70
+ const int delta = input::consumeRotaryDelta();
71
+ if (delta == 0) return;
72
+ // Rotary is a document-level event — no element target, no tree walk.
73
+ gea::embedded::ui::dispatchDocumentRotary(delta);
74
+ }
75
+
76
+ // Hardware-button key presses (boards without touch — e.g. the e-paper's
77
+ // BOOT/PWR buttons) queued via input::queueKeyDown. A focused <input> gets the
78
+ // key first through its per-node keydown listener; then the document-level
79
+ // keydown handler runs (hardware buttons / app-level shortcuts bound on a
80
+ // non-input element), unless the input consumed it.
81
+ void dispatch_key_input()
82
+ {
83
+ auto &tree = gea::embedded::ui::Tree::instance();
84
+ for (int keyCode = input::consumeKeyDown(); keyCode != 0; keyCode = input::consumeKeyDown()) {
85
+ events::PointerEvent event{};
86
+ event.type = events::PointerEventType::KeyDown;
87
+ event.keyCode = keyCode;
88
+ event.bubbles = true;
89
+ event.cancelable = true;
90
+
91
+ const int inputId = tree.activeInputId();
92
+ bool stopped = false;
93
+ if (inputId >= 0) {
94
+ event.targetId = inputId;
95
+ event.currentTargetId = inputId;
96
+ tree.dispatchEvent(event);
97
+ stopped = event.propagationStopped;
98
+ }
99
+ if (!stopped) {
100
+ event.propagationStopped = false;
101
+ gea::embedded::ui::dispatchDocumentKeyDown(event);
102
+ }
103
+ }
104
+ }
105
+
106
+ void run_app_frame(int timestampMs, void *context)
107
+ {
108
+ (void)context;
109
+ const uint32_t nowMs = static_cast<uint32_t>(timestampMs);
110
+ // Low-rate platform work shares the runtime event loop. This keeps input and
111
+ // battery observation deterministic without dedicating several mostly-idle
112
+ // FreeRTOS tasks (and their stacks) to polling.
113
+ events::TouchRuntime::poll(static_cast<int>(nowMs));
114
+ services::BatteryService::poll(static_cast<int>(nowMs));
115
+ // A firmware image contains one application, mounted once before the frame
116
+ // loop. Start its declarative animations on the first frame.
117
+ static bool animationsScanned = false;
118
+ if (!animationsScanned) {
119
+ animationsScanned = true;
120
+ gea::css::DeclarativeAnimations::scanAndStart(nowMs);
121
+ gea::embedded::ui::StyleSheet::instance().startCssAnimations(nowMs);
122
+ }
123
+
124
+ gea::css::AnimationEngine::instance().tick(nowMs);
125
+ // Render-only while settings overlays the frozen application.
126
+ const bool refreshOnly = SettingsSurface::instance().active();
127
+ if (refreshOnly)
128
+ gea::embedded::ui::Document::instance().refreshMountedIfDirty();
129
+ else {
130
+ dispatch_rotary_input();
131
+ dispatch_key_input();
132
+ app::Application::frame(timestampMs);
133
+ }
134
+ }
135
+
136
+ void dispatch_event(const events::Event &event, const RuntimeOptions &options)
137
+ {
138
+ switch (event.type) {
139
+ case events::EventType::Touch: {
140
+ if (SettingsSurface::instance().active()) { // settings is modal: taps go to it
141
+ if (event.touchPhase == events::TouchPhase::Up)
142
+ SettingsSurface::instance().handleTap(event.x, event.y);
143
+ break;
144
+ }
145
+ services::AppState::lock();
146
+ const bool touchChangedScene = events::TouchRuntime::dispatchEvent(event);
147
+ services::AppState::unlock();
148
+ // Input-triggered render: a touch that actually ran handlers (down / real
149
+ // move / up) likely changed the scene, so post a Frame now instead of
150
+ // letting the render wait for the timer's next tick. Without this, a
151
+ // finger-down drag updates state here but only paints on the timer cadence
152
+ // — a multi-ms idle gap per frame that drops a full-screen pan from ~60 to
153
+ // ~42fps (finger-up momentum has no touch events, so it already renders
154
+ // back-to-back via the overrun catch-up). Redundant moves (finger held
155
+ // still) return false and post nothing — no busy-render on a static hold —
156
+ // and the frame loop's drainFramesAndCheckInput discards duplicate posts.
157
+ // When the panel TE is the single frame clock, do NOT post a touch-driven frame:
158
+ // it would render off-TE (tearing, since the present no longer waits for VBlank)
159
+ // and add a second producer. The touch handler already updated pan state; the next
160
+ // TE edge (≤16.6ms) renders it. Only post the extra frame in the non-TE path.
161
+ if (touchChangedScene && !services::FrameScheduler::vsyncDriven()) {
162
+ events::Event frameEvent{};
163
+ frameEvent.type = events::EventType::Frame;
164
+ services::FrameScheduler::sendEvent(frameEvent, 0);
165
+ }
166
+ break;
167
+ }
168
+
169
+ case events::EventType::Frame: {
170
+ services::FrameScheduler::FrameCallbacks frameCallbacks{};
171
+ frameCallbacks.frame = run_app_frame;
172
+ services::FrameScheduler::runFrame(frameCallbacks);
173
+ // Graceful overrun: if the frame blew the 16ms budget, run its successor
174
+ // back-to-back instead of waiting for the next timer tick (which would snap
175
+ // the overrun to a 30fps cliff). drainFramesAndCheckInput() discards the
176
+ // timer's redundant frame posts and reports the instant real input (touch)
177
+ // is queued.
178
+ // Keep rendering back-to-back while EITHER the last frame overran (momentum
179
+ // / heavy animation, as before) OR a finger is down and moved recently. The
180
+ // second case makes a finger-down drag self-drive at the render rate the way
181
+ // momentum does: without it the loop renders one frame then blocks for the
182
+ // next touch/timer event (~6ms idle per frame), which pinned a full-screen
183
+ // finger-down pan to ~42fps while finger-up momentum ran ~60. The touch
184
+ // driver only emits a move event when the finger position changes and only
185
+ // re-arms after the previous one is consumed, so after each frame the next
186
+ // move can be up to a poll-interval away — rendering on that cadence stalls.
187
+ // Driving the loop ourselves reads the latest pan every frame instead.
188
+ // A finger held still posts no moves, so gestureActiveWithin() lapses to
189
+ // false after the window and the loop quiesces — no busy-render on a hold.
190
+ for (;;) {
191
+ const bool catchUp = services::FrameScheduler::takeCatchUpRequest();
192
+ const bool activeDrag =
193
+ events::TouchRuntime::gestureActiveWithin(services::FrameScheduler::nowMs(), 50);
194
+ // TE single-clock: never run a frame back-to-back here — the panel TE posts the
195
+ // next frame exactly one VBlank later, which is the whole point. Back-to-back
196
+ // would render off-TE (tearing) and re-introduce a second cadence.
197
+ if (services::FrameScheduler::vsyncDriven() || (!catchUp && !activeDrag)) break;
198
+ // Service any queued input INLINE (touch dispatch is microseconds) and
199
+ // keep going, instead of returning to the timer-paced outer loop.
200
+ // drainFramesAndCheckInput() leaves a non-Frame event at the queue
201
+ // front, so receiveEvent() returns it immediately without blocking.
202
+ while (services::FrameScheduler::drainFramesAndCheckInput()) {
203
+ events::Event pending{};
204
+ if (!services::FrameScheduler::receiveEvent(&pending)) break;
205
+ dispatch_event(pending, options);
206
+ }
207
+ services::FrameScheduler::runFrame(frameCallbacks);
208
+ }
209
+ // Persist any localStorage writes made this frame (or from a store's
210
+ // init() on the PSRAM-stack task since the last frame). This is the only
211
+ // place flash is touched for localStorage — always the main frame task.
212
+ gea::host::Storage.flushPending();
213
+ break;
214
+ }
215
+
216
+ case events::EventType::Timeout:
217
+ break;
218
+
219
+ case events::EventType::SettingsToggle:
220
+ services::AppState::lock();
221
+ SettingsSurface::instance().toggle();
222
+ services::AppState::unlock();
223
+ break;
224
+ }
225
+ }
226
+
227
+ #endif
228
+
229
+ } // namespace
230
+
231
+ void Runtime::runNativeBoot()
232
+ {
233
+ // An app with native hardware of its own (audio, USB host, sensors) brings
234
+ // it up here, before the display framebuffer and the radios take the
235
+ // contiguous internal RAM they need. Idempotent, so a target may run it on
236
+ // its bring-up task and run() below then finds nothing left to do.
237
+ static bool done = false;
238
+ if (done) return;
239
+ done = true;
240
+ if (gea_app_native_boot) gea_app_native_boot();
241
+ }
242
+
243
+ // Everything that has to happen before the first frame, split out of run() so a
244
+ // target can run it on a DIFFERENT task than the event loop. It reads and writes
245
+ // flash -- the SPIFFS mount, the localStorage restore, NVS from the radios --
246
+ // and a flash operation disables the cache, which trips
247
+ // esp_task_stack_is_sane_cache_disabled() for any task whose stack lives in
248
+ // external RAM. So a board that puts the loop on a PSRAM stack (the only way to
249
+ // keep 20 KB of internal DRAM free on a board whose app already owns most of it)
250
+ // calls boot() from its bring-up task first, and the loop task's run() then
251
+ // finds the work done. Idempotent, and single-shot in both outcomes: a target
252
+ // that only ever calls run() behaves exactly as before.
253
+ namespace {
254
+ // The app's own reserve, taken from inside gea_app_native_boot at a moment only
255
+ // the app knows. File-scope because boot() takes it before the hook runs and
256
+ // releases it after, and the hook has no runtime handle to pass it back through.
257
+ void *g_appDisplayReserve = nullptr;
258
+ } // namespace
259
+
260
+ void Runtime::holdDisplayReserve(void *reserve)
261
+ {
262
+ if (g_appDisplayReserve == reserve) return;
263
+ gea::platform::memory::Memory::releaseInternalDma(g_appDisplayReserve);
264
+ g_appDisplayReserve = reserve;
265
+ }
266
+
267
+ namespace {
268
+ void releaseDisplayReserves(void *runtimeReserve)
269
+ {
270
+ gea::platform::memory::Memory::releaseInternalDma(runtimeReserve);
271
+ gea::platform::memory::Memory::releaseInternalDma(g_appDisplayReserve);
272
+ g_appDisplayReserve = nullptr;
273
+ }
274
+ } // namespace
275
+
276
+ bool Runtime::boot(const RuntimeOptions &options)
277
+ {
278
+ static bool done = false;
279
+ static bool ok = false;
280
+ if (done) return ok;
281
+ done = true;
282
+ // Held across the app's hook, released just before Display::init() below.
283
+ // See GEA_EMBEDDED_DISPLAY_INTERNAL_RESERVE_BYTES in runtime.h.
284
+ void *displayReserve =
285
+ gea::platform::memory::Memory::reserveInternalDma(GEA_EMBEDDED_DISPLAY_INTERNAL_RESERVE_BYTES);
286
+ runNativeBoot();
287
+ #if !defined(GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT) || !GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT
288
+ services::HeapProbe::log("runtime:start");
289
+ services::StackProbe::logCurrentTask("runtime:start");
290
+ #endif
291
+ #if defined(GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT) && GEA_EMBEDDED_DIRECT_CANVAS_CONTEXT
292
+ if (!services::AppState::init()) return false;
293
+ auto eventQueue = services::FrameScheduler::createEventQueue();
294
+ if (!eventQueue) return false;
295
+ releaseDisplayReserves(displayReserve);
296
+ if (!gea::platform::display::Display::init()) return false;
297
+ // The layout ratio, not the board's font ratio -- see the comment on
298
+ // GEA_EMBEDDED_CSS_LAYOUT_DEVICE_PIXEL_RATIO in app.h. It defaults to 1.0, so
299
+ // this is the same call the hardcoded literal made; an app that declares
300
+ // `gea.cssDevicePixelRatio` gets its own value here and in the font atlas.
301
+ app::Application::init(options.width, options.height, GEA_EMBEDDED_CSS_LAYOUT_DEVICE_PIXEL_RATIO);
302
+ if (!gea::platform::display::Display::start()) return false;
303
+ services::FrameScheduler::start(eventQueue);
304
+ #else
305
+ if (!services::StorageService::init()) return false;
306
+ // Restore persisted localStorage into its RAM mirror once, here on the
307
+ // bring-up task, whose stack can touch flash — so app code can read
308
+ // localStorage from anywhere, including a store's init().
309
+ gea::host::Storage.load();
310
+ services::HeapProbe::log("runtime:after_storage");
311
+ if (!services::AppState::init()) return false;
312
+ services::HeapProbe::log("runtime:after_app_state");
313
+
314
+ #if !GEA_EMBEDDED_NO_DISPLAY
315
+ auto eventQueue = services::FrameScheduler::createEventQueue();
316
+ if (!eventQueue) return false;
317
+ services::HeapProbe::log("runtime:after_frame_queue");
318
+ #endif
319
+
320
+ // BLE is opt-in but must be preinited EARLY for apps that use it. The BT
321
+ // controller needs a large (~24-41 KB) contiguous block of internal DRAM, and
322
+ // on a PSRAM display board the only moment that block is reliably available is
323
+ // here at boot — before the app's JSX mount + display framebuffers fragment
324
+ // internal RAM. If we wait for the lazy first-use path (HidServer::init's
325
+ // preinit()), the controller's allocation fails (OOM) and BLE is silently
326
+ // skipped → the device never advertises. preinitForApp() is gated on
327
+ // GEA_EMBEDDED_APP_USES_BLE is derived from the application. Non-BLE images omit the controller source and component
328
+ // entirely, so they pay neither its static pools nor its boot allocation.
329
+ services::BluetoothService::preinitForApp();
330
+ services::HeapProbe::log("runtime:after_preinit");
331
+
332
+ gea::platform::power::Power::init();
333
+ services::HeapProbe::log("runtime:after_power");
334
+ releaseDisplayReserves(displayReserve);
335
+ // A board that declares no panel and no canvas has no surface to render to,
336
+ // so nothing here is constructed: no framebuffers, no app tree, no frame
337
+ // loop. This is not a display that is switched off -- it is a board whose
338
+ // hardware the firmware takes at its word, the same way an absent PMIC means
339
+ // no PMIC driver. Everything the app starts from its native boot hook, which
340
+ // ran above, is untouched.
341
+ #if !GEA_EMBEDDED_NO_DISPLAY
342
+ if (!gea::platform::display::Display::init()) return false;
343
+ services::HeapProbe::log("runtime:after_display_init");
344
+ #endif
345
+
346
+ #if GEA_EMBEDDED_WIFI_EARLY_CONNECT
347
+ if (GEA_EMBEDDED_WIFI_SSID[0] != '\0') {
348
+ // Both radios need large contiguous internal allocations, and BLE has the
349
+ // stricter one-shot controller allocation, so it is claimed first above.
350
+ // WiFi comes up AFTER Display::init(), not before it. The elastic-RAM
351
+ // arbiter is one-directional: the display yields staging to the radios
352
+ // (reserveInternal) and the radios never give any back. Starting WiFi first
353
+ // therefore left the panel with a few hundred bytes of DMA-capable RAM, no
354
+ // flush staging, and a dark screen — while the framebuffer rendered
355
+ // correctly in PSRAM the whole time. Display::init() now claims its floor
356
+ // before this line, and this bring-up shrinks it the designed way.
357
+ services::DiagnosticsServer::print("Starting WiFi after display init\n");
358
+ network::wifi().init();
359
+ }
360
+ #endif
361
+
362
+ if (!events::TouchRuntime::start()) return false;
363
+ services::BatteryService::update();
364
+ services::BatteryService::start();
365
+ services::HeapProbe::log("runtime:after_input_battery");
366
+
367
+ services::NetworkServicesOptions networkOptions{};
368
+ #ifdef GEA_EMBEDDED_DIAGNOSTICS_ENABLED
369
+ // Opt in to the TCP diagnostics server (port 8081, channel 1 = log stream).
370
+ // It defaults off because it costs a ~6 KiB task, and the default assumed
371
+ // logs and screenshots come over USB. A board that has the DRAM to spare
372
+ // gets device logs with no cable attached -- which is the only way to see a
373
+ // boot log at all once the USB console is gone.
374
+ networkOptions.diagnosticsEnabled = true;
375
+ #endif
376
+ #ifdef GEA_EMBEDDED_DIAGNOSTICS_DISABLED
377
+ // Production lockdown: don't start the diagnostics server (diag_srv TCP
378
+ // task + the esp_log_set_vprintf log-ring sink). OTA below stays enabled.
379
+ networkOptions.diagnosticsEnabled = false;
380
+ #endif
381
+ bool networkReady = services::NetworkServices::start(networkOptions);
382
+ services::HeapProbe::log("runtime:after_network");
383
+
384
+ services::RuntimeLog::printRuntimeBanner();
385
+ #if GEA_EMBEDDED_NO_DISPLAY
386
+ (void)options;
387
+ services::RuntimeLog::appStarted(networkReady);
388
+ #else
389
+ if (!services::AppRunner::initApplication(options.width, options.height)) return false;
390
+ services::HeapProbe::log("runtime:after_app_init");
391
+ services::StackProbe::logCurrentTask("runtime:after_app_init");
392
+
393
+ if (!gea::platform::display::Display::start()) return false;
394
+ services::HeapProbe::log("runtime:after_display_start");
395
+ services::StackProbe::logCurrentTask("runtime:after_display_start");
396
+
397
+ services::FrameScheduler::start(eventQueue);
398
+ services::HeapProbe::log("runtime:after_frame_start");
399
+ services::StackProbe::logCurrentTask("runtime:after_frame_start");
400
+ services::HeapTaskSummary::log("runtime:after_frame_start");
401
+ services::RuntimeLog::appStarted(networkReady);
402
+ #endif
403
+
404
+ #endif
405
+ ok = true;
406
+ return true;
407
+ }
408
+
409
+ void Runtime::run(const RuntimeOptions &options)
410
+ {
411
+ // A target that runs the loop on its own task calls boot() first, from a task
412
+ // whose stack can touch flash; this call then returns that recorded outcome
413
+ // without repeating any of the work.
414
+ if (!boot(options)) return;
415
+ #if GEA_EMBEDDED_NO_DISPLAY
416
+ // No surface means no frame queue, so there is no event to wait on and
417
+ // nothing to dispatch. The loop below would find receiveEvent() failing
418
+ // immediately against a queue that was never created and spin the core,
419
+ // which starves whatever else shares it. boot() was the work; return.
420
+ (void)options;
421
+ #else
422
+ while (true) {
423
+ events::Event event{};
424
+ if (services::FrameScheduler::receiveEvent(&event)) {
425
+ dispatch_event(event, options);
426
+ }
427
+ }
428
+ #endif
429
+ }
430
+
431
+ } // namespace gea::framework
package/runtime.ts ADDED
@@ -0,0 +1,34 @@
1
+ export * from './runtime-surface'
2
+
3
+ /**
4
+ * The JSX tag set, restated on the *runtime* entrypoint so an app that
5
+ * augments `@geastack/core` reaches it.
6
+ *
7
+ * The package answers `"."` with two files -- `types: ./index.d.ts` for an
8
+ * editor, `default: ./runtime.ts` for anything that actually loads the
9
+ * library, this file. A build pipeline that compiles the framework from
10
+ * source (geatsc, and the vite plugin) resolves the specifier to THIS module,
11
+ * so an app's `declare module '@geastack/core' { interface
12
+ * GeaIntrinsicElements { 'glass-pane': ... } }` augments this module -- while
13
+ * `JSX.IntrinsicElements` was wired, in `index.d.ts`, to *that* module's
14
+ * interface. One package, two module identities, and the app's own tags
15
+ * landing in the one the JSX namespace never reads: every use of a custom tag
16
+ * is then `Property 'glass-pane' does not exist on type
17
+ * 'JSX.IntrinsicElements'`, which is a fact about which file the specifier
18
+ * resolved to and not about the app.
19
+ *
20
+ * Extending the declaration file's own interface rather than re-listing its
21
+ * tags keeps `index.d.ts` the single authority for what the framework itself
22
+ * ships; this adds only the seam an augmentation needs. The two global
23
+ * `JSX.IntrinsicElements` declarations merge, so a program that resolved the
24
+ * package the other way loses nothing.
25
+ */
26
+ import type { GeaIntrinsicElements as FrameworkIntrinsicElements } from './index'
27
+
28
+ export interface GeaIntrinsicElements extends FrameworkIntrinsicElements {}
29
+
30
+ declare global {
31
+ namespace JSX {
32
+ interface IntrinsicElements extends GeaIntrinsicElements {}
33
+ }
34
+ }