@geastack/core 0.1.39 → 0.1.40

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/events.cpp CHANGED
@@ -46,6 +46,8 @@ const char *PointerEvent::typeName() const
46
46
  return "click";
47
47
  case PointerEventType::Input:
48
48
  return "input";
49
+ case PointerEventType::KeyUp:
50
+ return "keyup";
49
51
  case PointerEventType::KeyDown:
50
52
  return "keydown";
51
53
  case PointerEventType::Rotary:
package/gea_app_entry.cpp CHANGED
@@ -268,7 +268,7 @@ void Application::frame(int timestampMs)
268
268
  applicationFramePhaseSet(ApplicationFramePhase::DrainMicrotasks);
269
269
  generated::drainMicrotasks();
270
270
  applicationFramePhaseSet(ApplicationFramePhase::AnimationFrameCallbacks);
271
- gea::host::runAnimationFrameCallbacks(static_cast<gea::host::AnimationFrameTimestamp>(timestampMs));
271
+ gea::host::runAnimationFrameCallbacks(timestampMs);
272
272
  generated::drainMicrotasks();
273
273
  applicationFramePhaseSet(ApplicationFramePhase::Idle);
274
274
  #else
@@ -294,7 +294,7 @@ void Application::frame(int timestampMs)
294
294
 
295
295
  startUs = phaseClockUs();
296
296
  applicationFramePhaseSet(ApplicationFramePhase::AnimationFrameCallbacks);
297
- gea::host::runAnimationFrameCallbacks(static_cast<double>(timestampMs));
297
+ gea::host::runAnimationFrameCallbacks(timestampMs);
298
298
  applicationFramePhaseSet(ApplicationFramePhase::CallbackMicrotasks);
299
299
  generated::drainMicrotasks();
300
300
  recordFramePhase(ApplicationFramePhase::AnimationFrameCallbacks, phaseClockUs() - startUs);
package/gea_sources.mjs CHANGED
@@ -59,7 +59,8 @@ export function includeFlags(env = process.env) {
59
59
  join(elements, 'ui'),
60
60
  geaos,
61
61
  join(engine, 'vendor/stb'),
62
- join(engine, 'vendor/AnimatedGIF')
62
+ join(engine, 'vendor/AnimatedGIF'),
63
+ ...(env.GEA_COMPILER_RUNTIME ? [env.GEA_COMPILER_RUNTIME] : [])
63
64
  ].map((dir) => `-I${dir}`)
64
65
  }
65
66
 
@@ -87,8 +88,8 @@ export function cxxSources(env = process.env) {
87
88
  // @geastack/host — platform layer + services
88
89
  ...cpp(host, ['input', 'wifi', 'bluetooth', 'geolocation']),
89
90
  ...cpp(join(host, 'host'), [
90
- 'timers', 'worker', 'input', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
91
- 'ble', 'apps', 'audio', 'audio_worklet', 'video', 'mjpeg', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
91
+ 'timers', 'worker', 'input', 'device', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
92
+ 'ble', 'apps', 'audio', 'audio_worklet', 'audio_buffer_runtime', 'video', 'mjpeg', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
92
93
  'tile_loader'
93
94
  ]),
94
95
  // framework services (built on the platform abstraction — no target-specific deps)
package/include/audio.h CHANGED
@@ -7,8 +7,18 @@
7
7
  #include <string>
8
8
  #include <functional>
9
9
 
10
+ // Physical capture/mixer clock shared by the board driver and native hosts.
11
+ // AudioContext and network PCM may use other rates through their resamplers.
12
+ #ifndef GEA_AUDIO_DEVICE_SAMPLE_RATE
13
+ #define GEA_AUDIO_DEVICE_SAMPLE_RATE 16000
14
+ #endif
15
+
10
16
  namespace gea::platform::audio {
11
17
 
18
+ inline constexpr int deviceSampleRate = GEA_AUDIO_DEVICE_SAMPLE_RATE;
19
+ static_assert(deviceSampleRate >= 8000 && deviceSampleRate <= 48000,
20
+ "Audio device sample rate must be between 8000 and 48000 Hz");
21
+
12
22
  enum class OscillatorType {
13
23
  Sine = 0,
14
24
  Square = 1,
@@ -74,7 +84,7 @@ public:
74
84
  static bool playPcm(const std::int16_t *samples, std::size_t sample_count, int sample_rate, int channels);
75
85
  static void stopPlayback();
76
86
  static void flushPlayback();
77
- // Continuous 16 kHz mono source; zero means the platform could not attach it.
87
+ // Continuous mono source at deviceSampleRate; zero means the platform could not attach it.
78
88
  static std::uint64_t playPcmStream(std::function<std::size_t(std::int16_t*, std::size_t)> pull);
79
89
  static void stopPcmStream(std::uint64_t stream);
80
90
  static bool pcmStreamSettled(std::uint64_t stream);
@@ -0,0 +1,52 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ #pragma once
3
+
4
+ #include <algorithm>
5
+ #include <optional>
6
+
7
+ namespace gea::platform::audio {
8
+
9
+ // Worklet render time and the physical output scheduler can have different
10
+ // origins. Translate an explicit context deadline at the host boundary.
11
+ inline double physicalAudioDeadline(double when, double contextNow,
12
+ double physicalNow) {
13
+ return when > 0 ? physicalNow + when - contextNow : when;
14
+ }
15
+
16
+ struct FiniteToneTiming {
17
+ int durationMs;
18
+ int delayMs;
19
+ };
20
+
21
+ // The embedded oscillator backend schedules one finite tone when stop() is
22
+ // first called. It does not support replacing/cancelling an already queued
23
+ // individual voice. Further stops must never submit that voice a second time.
24
+ class FiniteOscillatorSchedule {
25
+ public:
26
+ void start(double when, double now) {
27
+ startTime_ = when > 0 ? when : now;
28
+ started_ = true;
29
+ submitted_ = false;
30
+ }
31
+
32
+ std::optional<FiniteToneTiming> stop(double when, double now) {
33
+ if (submitted_)
34
+ return std::nullopt;
35
+ submitted_ = true;
36
+ const double start = started_ ? startTime_ : now;
37
+ const double end = std::max(when > 0 ? when : now, start);
38
+ const int duration =
39
+ static_cast<int>(std::min((end - start) * 1000.0 + 0.5, 2500.0));
40
+ if (duration <= 0)
41
+ return std::nullopt;
42
+ return FiniteToneTiming{
43
+ duration, static_cast<int>(std::max(start - now, 0.0) * 1000.0 + 0.5)};
44
+ }
45
+
46
+ private:
47
+ double startTime_ = 0;
48
+ bool started_ = false;
49
+ bool submitted_ = false;
50
+ };
51
+
52
+ } // namespace gea::platform::audio
package/include/camera.h CHANGED
@@ -3,6 +3,7 @@
3
3
  #include "pixel.h"
4
4
 
5
5
  #include <cstdint>
6
+ #include <atomic>
6
7
  #include <string>
7
8
 
8
9
  namespace gea::platform::camera {
@@ -20,6 +21,14 @@ struct DeviceInfo {
20
21
  int sensorHeight = 0;
21
22
  };
22
23
 
24
+ // Optional encoded-frame producer. The device owns capture/encoding on its
25
+ // worker; reading a frame never waits for a sensor or codec on the UI thread.
26
+ class CameraFrameProvider {
27
+ public:
28
+ virtual ~CameraFrameProvider() = default;
29
+ virtual std::string takeFrameDataUrl() = 0;
30
+ };
31
+
23
32
  // Static facade — one active camera at a time on the device. The platform
24
33
  // layer maintains the live preview buffer and exposes a small surface that
25
34
  // the host bridge forwards into.
@@ -36,6 +45,12 @@ public:
36
45
  static void close();
37
46
  static bool isOpen();
38
47
 
48
+ static void setFrameProvider(CameraFrameProvider *provider) { frameProvider().store(provider); }
49
+ static std::string captureFrame() {
50
+ auto *provider = frameProvider().load();
51
+ return provider ? provider->takeFrameDataUrl() : std::string();
52
+ }
53
+
39
54
  static int width();
40
55
  static int height();
41
56
  static int orientation();
@@ -102,6 +117,12 @@ public:
102
117
  static void setWhiteBalance(const std::string &mode, double temperatureK, double tint);
103
118
  static void setFocus(const std::string &mode, double pointX, double pointY);
104
119
  static void setTorch(const std::string &mode, double level);
120
+
121
+ private:
122
+ static std::atomic<CameraFrameProvider *> &frameProvider() {
123
+ static std::atomic<CameraFrameProvider *> provider{nullptr};
124
+ return provider;
125
+ }
105
126
  };
106
127
 
107
128
  } // namespace gea::platform::camera
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ #pragma once
3
+
4
+ #include <cstddef>
5
+ #include <cstdint>
6
+ #include <cstring>
7
+
8
+ namespace gea::framework::network {
9
+
10
+ // A bounded DNS responder for AP captive portals. Rebuild the question name
11
+ // to avoid retaining compression pointers into discarded additional records.
12
+ inline std::size_t captiveDnsReply(const std::uint8_t *query,
13
+ std::size_t length,
14
+ const std::uint8_t address[4],
15
+ std::uint8_t *reply, std::size_t capacity) {
16
+ if (length < 12 || length > 512 || capacity < 12 || (query[2] & 0xf8) ||
17
+ query[4] != 0 || query[5] != 1)
18
+ return 0;
19
+ std::memcpy(reply, query, 12);
20
+ reply[2] = 0x80 | (query[2] & 1);
21
+ reply[3] = 0x80;
22
+ std::memset(reply + 6, 0, 6);
23
+ std::size_t source = 12, target = 12, questionEnd = 0, jumps = 0,
24
+ nameBytes = 0;
25
+ for (;;) {
26
+ if (source >= length)
27
+ return 0;
28
+ const auto count = query[source++];
29
+ if ((count & 0xc0) == 0xc0) {
30
+ if (source >= length || ++jumps > 8)
31
+ return 0;
32
+ const auto offset = ((count & 0x3f) << 8) | query[source++];
33
+ if (!questionEnd)
34
+ questionEnd = source;
35
+ if (offset >= length)
36
+ return 0;
37
+ source = offset;
38
+ continue;
39
+ }
40
+ if (count > 63 || source + count > length ||
41
+ target + count + 1 > capacity || (nameBytes += count + 1) > 255)
42
+ return 0;
43
+ reply[target++] = count;
44
+ if (!count)
45
+ break;
46
+ std::memcpy(reply + target, query + source, count);
47
+ target += count;
48
+ source += count;
49
+ }
50
+ if (!questionEnd)
51
+ questionEnd = source;
52
+ if (questionEnd + 4 > length || target + 4 > capacity)
53
+ return 0;
54
+ const auto type = (query[questionEnd] << 8) | query[questionEnd + 1];
55
+ const auto klass = (query[questionEnd + 2] << 8) | query[questionEnd + 3];
56
+ std::memcpy(reply + target, query + questionEnd, 4);
57
+ target += 4;
58
+ // AAAA and other record types receive an empty successful answer so clients
59
+ // can issue their IPv4 query; this portal only owns its AP IPv4 address.
60
+ if (klass != 1 || (type != 1 && type != 255))
61
+ return target;
62
+ if (target + 16 > capacity)
63
+ return 0;
64
+ constexpr std::uint8_t answer[] = {0xc0, 0x0c, 0, 1, 0, 1, 0, 0, 0, 30, 0, 4};
65
+ std::memcpy(reply + target, answer, sizeof(answer));
66
+ std::memcpy(reply + target + sizeof(answer), address, 4);
67
+ reply[7] = 1;
68
+ return target + 16;
69
+ }
70
+
71
+ } // namespace gea::framework::network
@@ -97,6 +97,7 @@ struct HeapCapsFree {
97
97
  static constexpr int kInlineCircleEntryCapacity = 32;
98
98
 
99
99
  struct Command {
100
+ gea::framework::graphics::ImageReference imageReference;
100
101
  using CircleEntry = gea::framework::graphics::CircleEntry;
101
102
 
102
103
  gea::platform::display::DisplayPresentCommandType type =
@@ -149,6 +150,9 @@ struct Command {
149
150
 
150
151
  struct Frame {
151
152
  std::vector<Command> commands;
153
+ // Embedded canvas batches preserve bitmap drawing semantics: circle painter
154
+ // order and the established triangle edge rasterization remain observable.
155
+ bool bitmapSemantics = false;
152
156
 
153
157
  void clear()
154
158
  {
@@ -308,29 +312,44 @@ inline bool commandsEqual(const Command &a, const Command &b)
308
312
  case Type::FillCircleRgb565:
309
313
  case Type::StrokeCircleRgb565:
310
314
  return a.x == b.x && a.y == b.y && a.radius == b.radius && a.color == b.color;
311
- case Type::FillCirclesRgb565: {
312
- if (a.radius != b.radius || a.circlesCount != b.circlesCount) return false;
313
- if (a.circlesCount == 0) return true;
315
+ case Type::FillCirclesRgb565:
316
+ {
317
+ if (a.radius != b.radius || a.circlesCount != b.circlesCount)
318
+ return false;
319
+ if (a.circlesCount == 0)
320
+ return true;
314
321
  const auto *ap = a.circleEntries();
315
322
  const auto *bp = b.circleEntries();
316
- if (!ap || !bp) return ap == bp;
317
- return std::memcmp(ap, bp,
318
- static_cast<std::size_t>(a.circlesCount) *
319
- sizeof(gea::framework::graphics::CircleEntry)) == 0;
323
+ if (!ap || !bp)
324
+ return ap == bp;
325
+ for (int i = 0; i < a.circlesCount; ++i)
326
+ if (ap[i].x != bp[i].x || ap[i].y != bp[i].y || ap[i].color != bp[i].color)
327
+ return false;
328
+ return true;
320
329
  }
321
- case Type::FillTrianglesRgb565: {
322
- if (a.trianglesCount != b.trianglesCount) return false;
323
- if (a.trianglesCount == 0) return true;
330
+ case Type::FillTrianglesRgb565:
331
+ {
332
+ if (a.trianglesCount != b.trianglesCount)
333
+ return false;
334
+ if (a.trianglesCount == 0)
335
+ return true;
324
336
  const auto *ap = a.trianglesBuffer.get();
325
337
  const auto *bp = b.trianglesBuffer.get();
326
- if (!ap || !bp) return ap == bp;
327
- return std::memcmp(ap, bp,
328
- static_cast<std::size_t>(a.trianglesCount) *
329
- sizeof(gea::framework::graphics::TriangleEntry)) == 0;
338
+ if (!ap || !bp)
339
+ return ap == bp;
340
+ for (int i = 0; i < a.trianglesCount; ++i)
341
+ {
342
+ const auto &x = ap[i];
343
+ const auto &y = bp[i];
344
+ if (x.x0 != y.x0 || x.y0 != y.y0 || x.x1 != y.x1 || x.y1 != y.y1 || x.x2 != y.x2 ||
345
+ x.y2 != y.y2 || x.color != y.color || x.rowY0 != y.rowY0 || x.rowY1 != y.rowY1)
346
+ return false;
347
+ }
348
+ return true;
330
349
  }
331
350
  case Type::DrawImage:
332
351
  return a.pixels == b.pixels && a.alphaPixels == b.alphaPixels && imagePixelsStable(a) &&
333
- a.srcWidth == b.srcWidth && a.srcHeight == b.srcHeight && a.x == b.x && a.y == b.y;
352
+ a.srcWidth == b.srcWidth && a.srcHeight == b.srcHeight && a.x == b.x && a.y == b.y;
334
353
  case Type::DrawImageScaled:
335
354
  case Type::DrawImageRotated90CW:
336
355
  return a.pixels == b.pixels && a.alphaPixels == b.alphaPixels && imagePixelsStable(a) &&
@@ -347,8 +366,9 @@ inline bool commandsEqual(const Command &a, const Command &b)
347
366
  return false;
348
367
  }
349
368
 
350
- inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *commands, int commandCount, Frame &frame)
369
+ inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *commands, int commandCount, Frame &frame, bool bitmapSemantics = false)
351
370
  {
371
+ frame.bitmapSemantics = bitmapSemantics;
352
372
  frame.clear();
353
373
  if (!commands || commandCount <= 0) return false;
354
374
  frame.commands.reserve(static_cast<std::size_t>(commandCount));
@@ -435,7 +455,7 @@ inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *co
435
455
  }
436
456
  bool sortedByBucket = false;
437
457
  constexpr int kCircleBucketSortMaxYRange = 2048;
438
- if (count <= 65535) {
458
+ if (!bitmapSemantics && count <= 65535) {
439
459
  int minY = static_cast<int>(src.ys[0]);
440
460
  int maxY = minY;
441
461
  for (int i = 1; i < count; i++) {
@@ -466,8 +486,9 @@ inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *co
466
486
  for (int i = 0; i < count; i++) {
467
487
  buffer[i] = CircleEntry{src.ys[i], src.xs[i], src.colors[i]};
468
488
  }
469
- std::sort(buffer, buffer + count,
470
- [](const CircleEntry &a, const CircleEntry &b) { return a.y < b.y; });
489
+ if (!bitmapSemantics)
490
+ std::sort(buffer, buffer + count,
491
+ [](const CircleEntry &a, const CircleEntry &b) { return a.y < b.y; });
471
492
  }
472
493
  if (!out.circlesInline) out.circlesBuffer.reset(buffer);
473
494
  }
@@ -571,6 +592,8 @@ inline bool extractFrame(const gea::platform::display::DisplayPresentCommand *co
571
592
  out.alpha = command.fillText.alpha;
572
593
  break;
573
594
  }
595
+ out.imageReference = gea::framework::graphics::ImageReference(
596
+ gea::framework::graphics::ImageStore::instance().idForPixels(out.pixels));
574
597
  frame.commands.push_back(std::move(out));
575
598
  }
576
599
  return true;
@@ -696,7 +719,8 @@ inline void rasterCommandRows(gea::framework::graphics::Canvas &canvas,
696
719
  int row,
697
720
  int chunkRows,
698
721
  int displayHeight,
699
- const Command &command)
722
+ const Command &command,
723
+ bool bitmapSemantics = false)
700
724
  {
701
725
  if (!buffer) return;
702
726
  using Type = gea::platform::display::DisplayPresentCommandType;
@@ -765,6 +789,13 @@ inline void rasterCommandRows(gea::framework::graphics::Canvas &canvas,
765
789
  canvas.strokeCircle(command.x - ox, command.y - oy, command.radius, command.color);
766
790
  break;
767
791
  case Type::FillCirclesRgb565: {
792
+ if (bitmapSemantics) {
793
+ const auto *circles = command.circleEntries();
794
+ for (int i = 0; circles && i < command.circlesCount; ++i)
795
+ canvas.fillCircle(int(circles[i].x) - ox, int(circles[i].y) - oy,
796
+ command.radius, circles[i].color);
797
+ break;
798
+ }
768
799
  // Hand the whole sorted batch to Canvas's optimized batch entrypoint.
769
800
  // It inlines fillCircleNoDirty's hot path and uses [chunkY0, chunkY1]
770
801
  // as a world-Y band to skip leading + break trailing in one loop.
@@ -777,21 +808,28 @@ inline void rasterCommandRows(gea::framework::graphics::Canvas &canvas,
777
808
  break;
778
809
  }
779
810
  case Type::FillTrianglesRgb565: {
780
- // Depth-ordered batch (farthest-first). OPAQUE batches take the span-
781
- // occlusion path (front-to-back, each pixel written once); it band-rejects
782
- // per chunk internally. TRANSLUCENT must stay painter's back-to-front.
811
+ // Full-display opaque 3D batches retain their front-to-back occlusion
812
+ // optimization. Bitmap canvas batches and translucent geometry replay
813
+ // in producer order, rejecting triangles outside this strip first.
783
814
  const gea::framework::graphics::TriangleEntry *tris = command.trianglesBuffer.get();
784
815
  const int count = command.trianglesCount;
785
816
  if (!tris) break;
786
- if (command.alpha == 255) {
817
+ if (command.alpha == 255 && !bitmapSemantics) {
787
818
  canvas.fillTrianglesOpaqueOccluded(tris, count, ox, oy);
788
819
  } else {
820
+ // Retained canvas batches preserve producer order. Sparse numeric
821
+ // geometry needs no coverage bitset; opaque spans avoid per-pixel
822
+ // comparisons while sharing the public triangle edge definition.
789
823
  const int chunkY1 = row + chunkRows - 1;
790
824
  for (int i = 0; i < count; i++) {
791
825
  const auto &t = tris[i];
792
826
  if (t.rowY1 < row || t.rowY0 > chunkY1) continue;
793
- canvas.fillTriangle(t.x0 - ox, t.y0 - oy, t.x1 - ox, t.y1 - oy,
794
- t.x2 - ox, t.y2 - oy, t.color);
827
+ if (command.alpha == 255)
828
+ canvas.fillTriangleOpaque(t.x0 - ox, t.y0 - oy, t.x1 - ox, t.y1 - oy,
829
+ t.x2 - ox, t.y2 - oy, t.color);
830
+ else
831
+ canvas.fillTriangle(t.x0 - ox, t.y0 - oy, t.x1 - ox, t.y1 - oy,
832
+ t.x2 - ox, t.y2 - oy, t.color);
795
833
  }
796
834
  }
797
835
  break;
@@ -851,7 +889,7 @@ inline void rasterFrameRows(gea::framework::graphics::pixel::native_t *buffer,
851
889
  gea::framework::graphics::Canvas canvas;
852
890
  canvas.bindPixels(buffer, regionWidth, chunkRows);
853
891
  for (const Command &command : frame.commands) {
854
- rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command);
892
+ rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command, frame.bitmapSemantics);
855
893
  }
856
894
  }
857
895
 
@@ -879,7 +917,7 @@ inline void rasterFrameRowsStrided(gea::framework::graphics::pixel::native_t *bu
879
917
  }
880
918
  continue;
881
919
  }
882
- rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command);
920
+ rasterCommandRows(canvas, buffer, regionX0, regionWidth, row, chunkRows, displayHeight, command, frame.bitmapSemantics);
883
921
  }
884
922
  }
885
923
 
package/include/events.h CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  #include <cstdint>
6
6
  #include <functional>
7
+ #include <memory>
7
8
 
8
9
  namespace gea::framework::events {
9
10
 
@@ -15,7 +16,8 @@ enum class PointerEventType {
15
16
  Input,
16
17
  KeyDown,
17
18
  Rotary,
18
- Scroll
19
+ Scroll,
20
+ KeyUp
19
21
  };
20
22
 
21
23
  enum class EventPhase {
@@ -51,6 +53,32 @@ struct TouchPoint {
51
53
  int pageY = 0;
52
54
  };
53
55
 
56
+ // Native callback lowering may copy the event carrier. Its cancellation flags
57
+ // still belong to the same dispatched event, including copies retained later.
58
+ // Allocate shared storage only when a carrier is actually copied.
59
+ class EventControlFlag {
60
+ public:
61
+ EventControlFlag(bool value = false) : initial_(value) {}
62
+ EventControlFlag(const EventControlFlag &other) : shared_(other.share()) {}
63
+ EventControlFlag &operator=(const EventControlFlag &other) {
64
+ if (this != &other) shared_ = other.share();
65
+ return *this;
66
+ }
67
+ EventControlFlag &operator=(bool value) {
68
+ if (shared_) *shared_ = value;
69
+ else initial_ = value;
70
+ return *this;
71
+ }
72
+ operator bool() const { return shared_ ? *shared_ : initial_; }
73
+ private:
74
+ std::shared_ptr<bool> share() const {
75
+ if (!shared_) shared_ = std::make_shared<bool>(initial_);
76
+ return shared_;
77
+ }
78
+ bool initial_ = false;
79
+ mutable std::shared_ptr<bool> shared_;
80
+ };
81
+
54
82
  struct PointerEvent {
55
83
  PointerEventType type;
56
84
  EventTarget target{};
@@ -73,8 +101,8 @@ struct PointerEvent {
73
101
  bool primary = true;
74
102
  bool bubbles = true;
75
103
  bool cancelable = true;
76
- bool defaultPrevented = false;
77
- bool propagationStopped = false;
104
+ EventControlFlag defaultPrevented;
105
+ EventControlFlag propagationStopped;
78
106
  EventPhase eventPhase = EventPhase::None;
79
107
  TouchPoint touches[1]{};
80
108
  TouchPoint targetTouches[1]{};
@@ -11,3 +11,5 @@
11
11
  #include "gea/embedded-host.h"
12
12
  #include "ui/document.h"
13
13
  #include "ui/signal.h"
14
+ // Debug builds: attributes JSX listeners to nodes for DevTools.
15
+ #include "ui/debugger_listeners.h"
@@ -13,6 +13,7 @@ struct Glyph {
13
13
  int advance = 0;
14
14
  int bearingX = 0;
15
15
  int bearingY = 0;
16
+ int advance16 = -1;
16
17
  };
17
18
 
18
19
  class Font {
@@ -25,6 +26,12 @@ public:
25
26
  virtual int ascender() const = 0;
26
27
  virtual bool glyph(int codepoint, Glyph *out) const = 0;
27
28
  virtual std::uint8_t coverage(const Glyph &glyph, int row, int col) const = 0;
29
+ virtual int advance(int codepoint, int nextCodepoint = 0) const
30
+ {
31
+ (void)nextCodepoint;
32
+ Glyph value{};
33
+ return glyph(codepoint, &value) ? value.advance : sizePx() / 2;
34
+ }
28
35
  };
29
36
 
30
37
  class BitmapFont : public Font {
@@ -63,6 +70,13 @@ private:
63
70
  static const std::uint8_t glyphs[kGlyphCount][kHeight];
64
71
  };
65
72
 
73
+ struct FontKerningPair
74
+ {
75
+ int left = 0;
76
+ int right = 0;
77
+ int adjustment16 = 0;
78
+ };
79
+
66
80
  struct RasterizedFontData {
67
81
  int id = 0;
68
82
  int sizePx = 0;
@@ -80,6 +94,10 @@ struct RasterizedFontData {
80
94
  // byte-aligned (stride = (atlasWidth + 3) / 4) and the leftmost pixel of a
81
95
  // byte sits in the most significant bit pair.
82
96
  int atlasBits = 8;
97
+ int kerningCount = 0;
98
+ const FontKerningPair *kerning = nullptr;
99
+ bool strictGlyphLookup = false;
100
+ int fallbackCodepoint = -1;
83
101
  };
84
102
 
85
103
  class RasterizedFont final : public Font {
@@ -93,6 +111,7 @@ public:
93
111
  int ascender() const final;
94
112
  bool glyph(int codepoint, Glyph *out) const final;
95
113
  std::uint8_t coverage(const Glyph &glyph, int row, int col) const final;
114
+ int advance(int codepoint, int nextCodepoint = 0) const final;
96
115
  const RasterizedFontData *data() const { return data_; }
97
116
 
98
117
  private:
package/include/image.h CHANGED
@@ -3,6 +3,7 @@
3
3
  #include "pixel.h"
4
4
 
5
5
  #include <cstdint>
6
+ #include <utility>
6
7
 
7
8
  #if GEA_EMBEDDED_SINGLE_THREAD_IMAGE_STORE
8
9
  namespace gea::framework::graphics {
@@ -26,7 +27,10 @@ using ImageStoreMutex = std::mutex;
26
27
  // tile-grid app (maps) keep ~4 screens of decoded tiles resident in PSRAM for
27
28
  // instant pan-back, on top of its persistent SD cache. ~128 KiB/tile (RGB565
28
29
  // 256x256) when full → ~12 MiB; the P4 has ample PSRAM.
29
- inline constexpr int kImageMax = 96;
30
+ #ifndef GEA_EMBEDDED_MAX_IMAGES
31
+ #define GEA_EMBEDDED_MAX_IMAGES 96
32
+ #endif
33
+ inline constexpr int kImageMax = GEA_EMBEDDED_MAX_IMAGES;
30
34
 
31
35
  enum class ImageFit {
32
36
  Fill = 0,
@@ -119,6 +123,10 @@ public:
119
123
  const pixel::native_t *framePixels(int id, int frame) const;
120
124
  const pixel::native_t *currentPixels(int id) const;
121
125
  const std::uint8_t *currentAlpha(int id) const;
126
+ // Pins keep decoded pixels alive for nodes and retained render commands.
127
+ bool retain(int id);
128
+ void release(int id);
129
+ int idForPixels(const pixel::native_t *pixels) const;
122
130
  void dispose(int id);
123
131
  void disposeAll();
124
132
  ImageFormat detectFormat(const std::uint8_t *data, int length) const;
@@ -150,6 +158,8 @@ private:
150
158
 
151
159
  ImageSlot images_[kImageMax]{};
152
160
  bool used_[kImageMax]{};
161
+ unsigned references_[kImageMax]{};
162
+ bool retired_[kImageMax]{};
153
163
  // Guards slot ALLOCATION and DISPOSAL only (reservation-style): the async
154
164
  // tile loader decodes on the second core while the frame task blits and
155
165
  // evicts. A decode reserves its slot under the lock, then fills it
@@ -159,4 +169,32 @@ private:
159
169
  mutable ImageStoreMutex slotMutex_;
160
170
  };
161
171
 
172
+ // Copyable ownership for retained commands; raw pixel pointers alone do not
173
+ // keep a decoded slot alive.
174
+ class ImageReference {
175
+ public:
176
+ ImageReference() = default;
177
+ explicit ImageReference(int id) : id_(ImageStore::instance().retain(id) ? id : -1) {}
178
+ ImageReference(const ImageReference &other) : ImageReference(other.id_) {}
179
+ ImageReference(ImageReference &&other) noexcept : id_(other.id_) { other.id_ = -1; }
180
+ ~ImageReference() { ImageStore::instance().release(id_); }
181
+ ImageReference &operator=(const ImageReference &other) {
182
+ if (this != &other) {
183
+ ImageReference next(other);
184
+ std::swap(id_, next.id_);
185
+ }
186
+ return *this;
187
+ }
188
+ ImageReference &operator=(ImageReference &&other) noexcept {
189
+ if (this != &other) {
190
+ ImageStore::instance().release(id_);
191
+ id_ = other.id_;
192
+ other.id_ = -1;
193
+ }
194
+ return *this;
195
+ }
196
+ private:
197
+ int id_ = -1;
198
+ };
199
+
162
200
  } // namespace gea::framework::graphics
package/include/input.h CHANGED
@@ -27,7 +27,18 @@ int consumeRotaryDelta();
27
27
  // `keydown` event. keyCode 0 is ignored.
28
28
  void queueKeyDown(int keyCode);
29
29
 
30
+ // Queue a release in the same ordered stream as key presses.
31
+ void queueKeyUp(int keyCode);
32
+
33
+ struct HardwareKeyEvent {
34
+ int keyCode;
35
+ bool pressed;
36
+ };
37
+
38
+ // Consume the oldest press or release without changing their order.
39
+ bool consumeKeyEvent(HardwareKeyEvent &event);
40
+
30
41
  // Reads and clears the oldest pending key press. Returns 0 when none pending.
31
42
  int consumeKeyDown();
32
43
 
33
- } // namespace gea::framework::input
44
+ } // namespace gea::framework::input