@geastack/core 0.1.42 → 0.1.43
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/README.md +36 -9
- package/events.cpp +11 -0
- package/gea_app_entry.cpp +4 -0
- package/gea_sources.mjs +12 -2
- package/gea_sources.sh +6 -1
- package/include/canvas.h +1 -0
- package/include/display.h +1 -0
- package/include/event_listener_options.h +26 -0
- package/include/events.h +109 -5
- package/include/gea/embedded.h +1 -0
- package/include/graphics/font.h +11 -0
- package/include/native_identity.h +65 -0
- package/include/platform/asset_bundle.h +71 -0
- package/include/platform/esp_now.h +113 -0
- package/include/platform/magnetometer.h +60 -0
- package/include/runtime.h +14 -0
- package/index.d.ts +156 -8
- package/package.json +5 -2
- package/runtime/host.ts +61 -1
- package/runtime-surface.ts +5 -0
- package/runtime.cpp +145 -16
- package/scripts/build-gea-vite-geatsc.mjs +27 -3
- package/scripts/gea-file-asset-bundle.mjs +82 -0
- package/scripts/gea-native-style-plugin.mjs +3 -1
- package/scripts/gea-vite-plugin-resolution.mjs +49 -0
- package/scripts/generate-gea-embedded-assets.mjs +20 -3
- package/scripts/generate-gea-embedded-fonts.mjs +25 -4
- package/scripts/tests/gea-vite-plugin-resolution.test.mjs +46 -0
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ npm install @geastack/core
|
|
|
13
13
|
Apps import the types and the runtime modules:
|
|
14
14
|
|
|
15
15
|
```tsx
|
|
16
|
-
import type { Style } from
|
|
17
|
-
import { View, Text } from
|
|
16
|
+
import type { Style } from "@geastack/core";
|
|
17
|
+
import { View, Text } from "@geastack/engine";
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Everything else in the package is consumed by tooling, not by app code. The
|
|
@@ -37,15 +37,15 @@ before this hook runs. Field initializers and constructors initialize state;
|
|
|
37
37
|
code that needs a rendered canvas belongs in the hook.
|
|
38
38
|
|
|
39
39
|
```tsx
|
|
40
|
-
import { ReactiveComponent } from
|
|
40
|
+
import { ReactiveComponent } from "@geastack/core";
|
|
41
41
|
|
|
42
42
|
class Counter extends ReactiveComponent {
|
|
43
|
-
count = 0
|
|
43
|
+
count = 0;
|
|
44
44
|
onAfterRender(): void {
|
|
45
|
-
this.count = 1
|
|
45
|
+
this.count = 1;
|
|
46
46
|
}
|
|
47
47
|
template(): JSX.Element {
|
|
48
|
-
return <div>{this.count}</div
|
|
48
|
+
return <div>{this.count}</div>;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
```
|
|
@@ -63,15 +63,42 @@ changes need to preserve the shared component and native-type contracts.
|
|
|
63
63
|
WebSocket constructor argument for connection headers on ESP32 native hosts:
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
|
-
const socket = new WebSocket(
|
|
67
|
-
headers: [
|
|
68
|
-
}
|
|
66
|
+
const socket = new WebSocket("wss://api.openai.com/v1/live/sessions", "", {
|
|
67
|
+
headers: [
|
|
68
|
+
{ name: "Authorization", value: `Bearer ${process.env.OPENAI_API_KEY}` },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
69
71
|
```
|
|
70
72
|
|
|
71
73
|
Use these paired versions or newer when a service requires bearer authentication
|
|
72
74
|
in the WebSocket handshake. Header names and values are validated before the
|
|
73
75
|
connection starts. This native option is unavailable in browser WebSockets.
|
|
74
76
|
|
|
77
|
+
## File-backed native assets
|
|
78
|
+
|
|
79
|
+
Native apps can keep images and baked font atlases on a mounted volume instead
|
|
80
|
+
of in the executable. Opt in through the app manifest:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"gea": {
|
|
85
|
+
"embeddedAssets": {
|
|
86
|
+
"storage": { "type": "file", "directory": "/nand" }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The generators emit content-addressed `.bin` payloads and a `.assets.json`
|
|
93
|
+
deployment manifest alongside each generated C++ source. Copy each `local` file
|
|
94
|
+
to its `remote` path, for example with `gea devctl push`, before starting the app.
|
|
95
|
+
The target must register a mount provider; ESP32 targets also need external BSS
|
|
96
|
+
enabled. The runtime reads and validates the entire bundle once into permanent
|
|
97
|
+
RAM buffers. Render-time image and font lookups use those buffers, with no file
|
|
98
|
+
I/O. Missing, truncated or corrupt bundles are rejected until the next process
|
|
99
|
+
start. Retain older content-addressed files when firmware rollback is required.
|
|
100
|
+
Apps without this option continue embedding their assets in the executable.
|
|
101
|
+
|
|
75
102
|
## Related packages
|
|
76
103
|
|
|
77
104
|
- `@geastack/engine` renders the element tree, `@geastack/host` supplies the
|
package/events.cpp
CHANGED
|
@@ -35,6 +35,7 @@ int EventTarget::pressValue() const
|
|
|
35
35
|
|
|
36
36
|
const char *PointerEvent::typeName() const
|
|
37
37
|
{
|
|
38
|
+
if (!subscriberTypeName.empty()) return subscriberTypeName.c_str();
|
|
38
39
|
switch (type) {
|
|
39
40
|
case PointerEventType::TouchStart:
|
|
40
41
|
return "touchstart";
|
|
@@ -46,12 +47,22 @@ const char *PointerEvent::typeName() const
|
|
|
46
47
|
return "click";
|
|
47
48
|
case PointerEventType::Input:
|
|
48
49
|
return "input";
|
|
50
|
+
case PointerEventType::FocusIn: return "focusin";
|
|
51
|
+
case PointerEventType::FocusOut: return "focusout";
|
|
52
|
+
case PointerEventType::Change:
|
|
53
|
+
return "change";
|
|
54
|
+
case PointerEventType::PointerCancel:
|
|
55
|
+
return "pointercancel";
|
|
49
56
|
case PointerEventType::KeyUp:
|
|
50
57
|
return "keyup";
|
|
51
58
|
case PointerEventType::KeyDown:
|
|
52
59
|
return "keydown";
|
|
53
60
|
case PointerEventType::Rotary:
|
|
54
61
|
return "rotary";
|
|
62
|
+
case PointerEventType::WindowBlur:
|
|
63
|
+
return "blur";
|
|
64
|
+
case PointerEventType::WindowResize:
|
|
65
|
+
return "resize";
|
|
55
66
|
case PointerEventType::Scroll:
|
|
56
67
|
return "scroll";
|
|
57
68
|
}
|
package/gea_app_entry.cpp
CHANGED
|
@@ -31,6 +31,7 @@ void drainMicrotasks();
|
|
|
31
31
|
|
|
32
32
|
extern "C" void gea_cycle_collection_defer_begin() __attribute__((weak));
|
|
33
33
|
extern "C" void gea_cycle_collection_defer_end() __attribute__((weak));
|
|
34
|
+
extern "C" bool gea_embedded_assets_prepare() __attribute__((weak));
|
|
34
35
|
|
|
35
36
|
namespace gea::framework::app {
|
|
36
37
|
|
|
@@ -91,6 +92,9 @@ private:
|
|
|
91
92
|
|
|
92
93
|
void Application::init(int width, int height, double devicePixelRatio)
|
|
93
94
|
{
|
|
95
|
+
// File-backed static symbols must be populated before generated code can
|
|
96
|
+
// construct an image directly from them. Embedded assets need no hook.
|
|
97
|
+
if (gea_embedded_assets_prepare) gea_embedded_assets_prepare();
|
|
94
98
|
// Top-level construction allocates the entire native object graph. Keep the
|
|
95
99
|
// generated collector at a host safepoint until initialization has unwound,
|
|
96
100
|
// just as Application::frame does for animation callbacks.
|
package/gea_sources.mjs
CHANGED
|
@@ -78,7 +78,7 @@ export function cxxSources(env = process.env) {
|
|
|
78
78
|
'tree_state', 'node_lifecycle', 'display_invalidation', 'layout_snapshot',
|
|
79
79
|
'absolute_leaf_refresh', 'viewport_region', 'dirty_regions', 'root_scroll_refresh',
|
|
80
80
|
'node', 'view_element', 'text_element', 'image_element', 'style', 'css_atom',
|
|
81
|
-
'document', 'core', 'tree_nodes', 'tree_style', 'tree_scroll_mirror', 'tree_render',
|
|
81
|
+
'document', 'selector', 'core', 'tree_nodes', 'tree_style', 'tree_scroll_mirror', 'tree_render',
|
|
82
82
|
'view', 'text', 'image_node', 'layout', 'render', 'input', 'input_render',
|
|
83
83
|
'virtual_keyboard', 'canvas_element', 'canvas_store', 'tree_events'
|
|
84
84
|
]),
|
|
@@ -87,6 +87,8 @@ export function cxxSources(env = process.env) {
|
|
|
87
87
|
...cpp(join(elements, 'ui'), ['camera_element', 'virtual_list']),
|
|
88
88
|
// @geastack/host — platform layer + services
|
|
89
89
|
...cpp(host, ['input', 'wifi', 'bluetooth', 'geolocation']),
|
|
90
|
+
// Only native Linux targets opt into their existing libcurl dependency.
|
|
91
|
+
...(env.GEA_DESKTOP_WEBSOCKET_CURL === '1' ? [join(host, 'host/websocket_curl.cpp')] : []),
|
|
90
92
|
...cpp(join(host, 'host'), [
|
|
91
93
|
'timers', 'worker', 'input', 'device', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
|
|
92
94
|
'ble', 'apps', 'audio', 'audio_worklet', 'audio_buffer_runtime', 'video', 'mjpeg', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
|
|
@@ -105,10 +107,18 @@ export function cxxSources(env = process.env) {
|
|
|
105
107
|
]
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// Apple targets compile Objective-C++ separately with ARC/Foundation. Keep the
|
|
111
|
+
// framework transport provider at the same source authority as C/C++ inputs.
|
|
112
|
+
export function objcxxSources(env = process.env) {
|
|
113
|
+
const { host } = packageDirs(env)
|
|
114
|
+
return [join(host, 'host/websocket_apple.mm')]
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
const queries = {
|
|
109
118
|
'include-flags': includeFlags,
|
|
110
119
|
'c-sources': cSources,
|
|
111
|
-
'cxx-sources': cxxSources
|
|
120
|
+
'cxx-sources': cxxSources,
|
|
121
|
+
'objcxx-sources': objcxxSources
|
|
112
122
|
}
|
|
113
123
|
|
|
114
124
|
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
package/gea_sources.sh
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# no bash, so `execute_process(COMMAND bash -c ...)` failed and no ESP32 target
|
|
5
5
|
# could configure. Node is already a hard requirement of every path that gets
|
|
6
6
|
# here (the CLI and geatsc are Node), so reading it from Node costs no new
|
|
7
|
-
# dependency, and keeping
|
|
7
|
+
# dependency, and keeping the existing function names means shell callers that
|
|
8
8
|
# `source` this file (simulator build-web.sh, core's own test) do not change.
|
|
9
9
|
#
|
|
10
10
|
# Caller must export GEA_CORE = the @geastack/core package dir, plus
|
|
@@ -34,3 +34,8 @@ gea_fw_c_sources() {
|
|
|
34
34
|
gea_fw_cxx_sources() {
|
|
35
35
|
__gea_sources_run cxx-sources
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
# Apple-only framework sources (ARC/Foundation); ordinary C++ targets do not call this.
|
|
39
|
+
gea_fw_objcxx_sources() {
|
|
40
|
+
__gea_sources_run objcxx-sources
|
|
41
|
+
}
|
package/include/canvas.h
CHANGED
|
@@ -213,6 +213,7 @@ public:
|
|
|
213
213
|
void drawText(const char *text, int x, int y, pixel::native_t color, float scale);
|
|
214
214
|
void drawTextFont(const char *text, int x, int y, pixel::native_t color, int font_id);
|
|
215
215
|
void drawTextFontFamily(const char *text, int x, int y, pixel::native_t color, int family_id, int size_px);
|
|
216
|
+
void drawTextFontFamily(const char *text, int x, int y, pixel::native_t color, int family_id, int size_px,int weight,int stretch);
|
|
216
217
|
// Rotated 90deg for panel-native landscape rendering: the anchor and the
|
|
217
218
|
// text advance are in LANDSCAPE coordinates; every glyph pixel writes to
|
|
218
219
|
// the portrait panel address (panel_x = ly, panel_y = height-1-lx). The
|
package/include/display.h
CHANGED
|
@@ -356,6 +356,7 @@ public:
|
|
|
356
356
|
static void drawText(const char *text, int x, int y, gea::framework::graphics::pixel::native_t color, float scale);
|
|
357
357
|
static void drawTextFont(const char *text, int x, int y, gea::framework::graphics::pixel::native_t color, int font_id);
|
|
358
358
|
static void drawTextFontFamily(const char *text, int x, int y, gea::framework::graphics::pixel::native_t color, int family_id, int size_px);
|
|
359
|
+
static void drawTextFontFamily(const char *text,int x,int y,gea::framework::graphics::pixel::native_t color,int family_id,int size_px,int weight,int stretch);
|
|
359
360
|
static void setPixel(int x, int y, gea::framework::graphics::pixel::native_t color);
|
|
360
361
|
static void fillRoundedRect(int x, int y, int w, int h, int tl, int tr, int br, int bl, gea::framework::graphics::pixel::native_t color);
|
|
361
362
|
static void fillRoundedRectBoxesRgb565(const int16_t *xs, const int16_t *ys, int count,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#pragma once
|
|
3
|
+
#include <cstddef>
|
|
4
|
+
#include <cstdlib>
|
|
5
|
+
#include <type_traits>
|
|
6
|
+
namespace gea { template <typename... Arms> class TaggedUnion; }
|
|
7
|
+
namespace gea::framework::events {
|
|
8
|
+
struct NativeListenerOptions { bool capture=false; bool once=false; };
|
|
9
|
+
inline NativeListenerOptions listenerOptions(bool capture) { return {capture,false}; }
|
|
10
|
+
template <typename Boolean> bool listenerOptionBoolean(const Boolean &value) {
|
|
11
|
+
if constexpr (std::is_same_v<Boolean,bool>) return value;
|
|
12
|
+
else { static_assert(std::is_same_v<std::remove_cvref_t<decltype(*value)>,bool>); return value.has_value() && *value; }
|
|
13
|
+
}
|
|
14
|
+
// Exact generated record fields remain native Optional<bool>; never Value/boxing.
|
|
15
|
+
template <typename Record> requires requires(const Record &value) { value->capture; value->once; }
|
|
16
|
+
NativeListenerOptions listenerOptions(const Record &value) {
|
|
17
|
+
if (!value) return {};
|
|
18
|
+
return {listenerOptionBoolean(value->capture),listenerOptionBoolean(value->once)};
|
|
19
|
+
}
|
|
20
|
+
template <std::size_t Index=0, typename... Arms>
|
|
21
|
+
NativeListenerOptions listenerOptions(const gea::TaggedUnion<Arms...> &value) {
|
|
22
|
+
if (value.template is<Index>()) return listenerOptions(value.template get<Index>());
|
|
23
|
+
if constexpr (Index+1<sizeof...(Arms)) return listenerOptions<Index+1>(value);
|
|
24
|
+
else std::abort();
|
|
25
|
+
}
|
|
26
|
+
}
|
package/include/events.h
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include "event.h"
|
|
4
|
+
#include "native_identity.h"
|
|
4
5
|
|
|
5
6
|
#include <cstdint>
|
|
6
7
|
#include <functional>
|
|
7
8
|
#include <memory>
|
|
9
|
+
#include <string>
|
|
10
|
+
#include <tuple>
|
|
11
|
+
#include <type_traits>
|
|
12
|
+
#include <utility>
|
|
13
|
+
|
|
14
|
+
namespace gea::detail { struct RefVisitor; template<class T> struct TraceEdges; }
|
|
8
15
|
|
|
9
16
|
namespace gea::framework::events {
|
|
10
17
|
|
|
@@ -17,7 +24,13 @@ enum class PointerEventType {
|
|
|
17
24
|
KeyDown,
|
|
18
25
|
Rotary,
|
|
19
26
|
Scroll,
|
|
20
|
-
KeyUp
|
|
27
|
+
KeyUp,
|
|
28
|
+
PointerCancel,
|
|
29
|
+
WindowBlur,
|
|
30
|
+
WindowResize,
|
|
31
|
+
Change,
|
|
32
|
+
FocusIn,
|
|
33
|
+
FocusOut
|
|
21
34
|
};
|
|
22
35
|
|
|
23
36
|
enum class EventPhase {
|
|
@@ -29,10 +42,13 @@ enum class EventPhase {
|
|
|
29
42
|
|
|
30
43
|
class EventTarget {
|
|
31
44
|
public:
|
|
32
|
-
explicit EventTarget(int nodeId = -1) : nodeId_(nodeId) {}
|
|
45
|
+
explicit EventTarget(int nodeId = -1) : nodeId_(nodeId), identity_(identity::node(nodeId)) {}
|
|
46
|
+
const void *identity() const { return identity_.get(); }
|
|
47
|
+
std::shared_ptr<const void> weakIdentityOwner() const { return identity_; }
|
|
48
|
+
const identity::Owner &identityOwner() const { return identity_; }
|
|
33
49
|
|
|
34
|
-
int id() const { return nodeId_; }
|
|
35
|
-
bool valid() const { return nodeId_
|
|
50
|
+
int id() const { return valid() ? nodeId_ : -1; }
|
|
51
|
+
bool valid() const { return identity::current(nodeId_, identity_); }
|
|
36
52
|
const char *getAttribute(const char *name) const;
|
|
37
53
|
const char *dataset(const char *name) const;
|
|
38
54
|
int pressId() const;
|
|
@@ -40,6 +56,7 @@ public:
|
|
|
40
56
|
|
|
41
57
|
private:
|
|
42
58
|
int nodeId_;
|
|
59
|
+
identity::Owner identity_;
|
|
43
60
|
};
|
|
44
61
|
|
|
45
62
|
struct TouchPoint {
|
|
@@ -86,6 +103,9 @@ struct PointerEvent {
|
|
|
86
103
|
int targetId = -1;
|
|
87
104
|
int currentTargetId = -1;
|
|
88
105
|
int pointerId = 0;
|
|
106
|
+
// Unknown/non-pointer producers never impersonate a primary button edge.
|
|
107
|
+
int button = -1;
|
|
108
|
+
std::uint32_t buttons = 0;
|
|
89
109
|
int x = 0;
|
|
90
110
|
int y = 0;
|
|
91
111
|
int clientX = 0;
|
|
@@ -97,12 +117,25 @@ struct PointerEvent {
|
|
|
97
117
|
int pressId = -1;
|
|
98
118
|
int pressValue = -1;
|
|
99
119
|
int keyCode = 0;
|
|
120
|
+
// Native keyboard metadata. Non-keyboard producers retain explicit empty
|
|
121
|
+
// strings/false flags; no browser record or boxed event is involved.
|
|
122
|
+
std::string key;
|
|
123
|
+
std::string code;
|
|
124
|
+
bool altKey = false;
|
|
125
|
+
bool ctrlKey = false;
|
|
126
|
+
bool metaKey = false;
|
|
127
|
+
bool shiftKey = false;
|
|
128
|
+
bool repeat = false;
|
|
129
|
+
bool isComposing = false;
|
|
100
130
|
int delta = 0;
|
|
101
131
|
bool primary = true;
|
|
102
132
|
bool bubbles = true;
|
|
103
133
|
bool cancelable = true;
|
|
104
134
|
EventControlFlag defaultPrevented;
|
|
105
135
|
EventControlFlag propagationStopped;
|
|
136
|
+
identity::Occurrence occurrenceIdentity;
|
|
137
|
+
const void *identity() const { return occurrenceIdentity.identity(); }
|
|
138
|
+
std::shared_ptr<const void> weakIdentityOwner() const { return occurrenceIdentity.owner(); }
|
|
106
139
|
EventPhase eventPhase = EventPhase::None;
|
|
107
140
|
TouchPoint touches[1]{};
|
|
108
141
|
TouchPoint targetTouches[1]{};
|
|
@@ -111,12 +144,83 @@ struct PointerEvent {
|
|
|
111
144
|
int targetTouchesLength = 0;
|
|
112
145
|
int changedTouchesLength = 0;
|
|
113
146
|
|
|
147
|
+
// Scoped subscriber spelling; physical phase/type and occurrence stay unchanged.
|
|
148
|
+
std::string subscriberTypeName;
|
|
114
149
|
const char *typeName() const;
|
|
115
150
|
void preventDefault();
|
|
116
151
|
void stopPropagation();
|
|
117
152
|
};
|
|
118
153
|
|
|
119
|
-
|
|
154
|
+
// Ordinary native callbacks keep the platform std::function's own small-object
|
|
155
|
+
// storage. Explicit ADL-traceable callbacks instead own a concrete copy: a
|
|
156
|
+
// shared erased model would report one physical capture once for every copy.
|
|
157
|
+
class EventListener {
|
|
158
|
+
struct Model {
|
|
159
|
+
virtual ~Model() = default;
|
|
160
|
+
virtual std::unique_ptr<Model> clone() const = 0;
|
|
161
|
+
virtual void invoke(PointerEvent&) const = 0;
|
|
162
|
+
virtual void trace(gea::detail::RefVisitor&) const = 0;
|
|
163
|
+
};
|
|
164
|
+
template<class F> struct Concrete final : Model {
|
|
165
|
+
mutable F callable;
|
|
166
|
+
explicit Concrete(F value) : callable(std::move(value)) {}
|
|
167
|
+
std::unique_ptr<Model> clone() const override { return std::make_unique<Concrete>(callable); }
|
|
168
|
+
void invoke(PointerEvent& event) const override { callable(event); }
|
|
169
|
+
void trace(gea::detail::RefVisitor& visitor) const override { geaTraceRefs(callable, visitor); }
|
|
170
|
+
};
|
|
171
|
+
std::function<void(PointerEvent&)> ordinary_;
|
|
172
|
+
std::unique_ptr<Model> traced_;
|
|
173
|
+
public:
|
|
174
|
+
EventListener() = default;
|
|
175
|
+
EventListener(std::nullptr_t) noexcept {}
|
|
176
|
+
template<class F> requires (!std::is_same_v<std::decay_t<F>,EventListener> &&
|
|
177
|
+
std::is_copy_constructible_v<std::decay_t<F>> &&
|
|
178
|
+
std::is_invocable_r_v<void,std::decay_t<F>&,PointerEvent&>)
|
|
179
|
+
EventListener(F&& callable) {
|
|
180
|
+
using Held = std::decay_t<F>;
|
|
181
|
+
if constexpr (requires(const Held& held, gea::detail::RefVisitor& visitor) { geaTraceRefs(held,visitor); })
|
|
182
|
+
traced_ = std::make_unique<Concrete<Held>>(std::forward<F>(callable));
|
|
183
|
+
else ordinary_ = std::forward<F>(callable);
|
|
184
|
+
}
|
|
185
|
+
EventListener(const EventListener& other)
|
|
186
|
+
: ordinary_(other.ordinary_), traced_(other.traced_ ? other.traced_->clone() : nullptr) {}
|
|
187
|
+
EventListener(EventListener&&) noexcept = default;
|
|
188
|
+
EventListener& operator=(const EventListener& other) { EventListener copy(other); swap(copy); return *this; }
|
|
189
|
+
EventListener& operator=(EventListener&&) noexcept = default;
|
|
190
|
+
EventListener& operator=(std::nullptr_t) noexcept { ordinary_=nullptr; traced_.reset(); return *this; }
|
|
191
|
+
void swap(EventListener& other) noexcept { ordinary_.swap(other.ordinary_); traced_.swap(other.traced_); }
|
|
192
|
+
explicit operator bool() const noexcept { return bool(traced_) || bool(ordinary_); }
|
|
193
|
+
void operator()(PointerEvent& event) const { if(traced_) traced_->invoke(event); else ordinary_(event); }
|
|
194
|
+
friend void geaTraceRefs(const EventListener& value, gea::detail::RefVisitor& visitor) {
|
|
195
|
+
if(value.traced_) value.traced_->trace(visitor);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Invocation has no hidden captures. Each owned capture occurs exactly once,
|
|
200
|
+
// in this tuple, and nested adapters preserve that same ADL trace authority.
|
|
201
|
+
template<class Invoke,class... Captures>
|
|
202
|
+
struct ExplicitEventCallback {
|
|
203
|
+
static_assert(std::is_empty_v<Invoke>, "event invocation must be capture-free");
|
|
204
|
+
Invoke invoke;
|
|
205
|
+
std::tuple<Captures...> captures;
|
|
206
|
+
void operator()(PointerEvent& event) {
|
|
207
|
+
std::apply([&](auto&... values){invoke(values...,event);},captures);
|
|
208
|
+
}
|
|
209
|
+
friend void geaTraceRefs(const ExplicitEventCallback& value,gea::detail::RefVisitor& visitor) {
|
|
210
|
+
std::apply([&](const auto&... values){
|
|
211
|
+
auto trace=[&]<class T>(const T& capture){
|
|
212
|
+
if constexpr(requires { gea::detail::TraceEdges<T>::supported; })
|
|
213
|
+
gea::detail::TraceEdges<T>::visit(capture,visitor);
|
|
214
|
+
else if constexpr(requires { geaTraceRefs(capture,visitor); }) geaTraceRefs(capture,visitor);
|
|
215
|
+
};
|
|
216
|
+
(trace(values),...);
|
|
217
|
+
},value.captures);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
template<class Invoke,class... Captures>
|
|
221
|
+
auto explicitEventCallback(Invoke invoke,Captures&&... captures) {
|
|
222
|
+
return ExplicitEventCallback<Invoke,std::decay_t<Captures>...>{std::move(invoke),{std::forward<Captures>(captures)...}};
|
|
223
|
+
}
|
|
120
224
|
using EventListenerId = std::uint64_t;
|
|
121
225
|
inline constexpr EventListenerId kInvalidEventListenerId = 0;
|
|
122
226
|
|
package/include/gea/embedded.h
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// includes gea/embedded-host.h instead (no engine/reactive dependency).
|
|
11
11
|
#include "gea/embedded-host.h"
|
|
12
12
|
#include "ui/document.h"
|
|
13
|
+
#include "ui/child_node_list.h"
|
|
13
14
|
#include "ui/signal.h"
|
|
14
15
|
// Debug builds: attributes JSX listeners to nodes for DevTools.
|
|
15
16
|
#include "ui/debugger_listeners.h"
|
package/include/graphics/font.h
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
#include <cstdint>
|
|
4
4
|
|
|
5
|
+
#ifndef GEA_EMBEDDED_TTF_FREETYPE
|
|
6
|
+
#define GEA_EMBEDDED_TTF_FREETYPE 0
|
|
7
|
+
#endif
|
|
8
|
+
|
|
5
9
|
namespace gea::framework::graphics {
|
|
6
10
|
|
|
7
11
|
struct Glyph {
|
|
@@ -98,6 +102,12 @@ struct RasterizedFontData {
|
|
|
98
102
|
const FontKerningPair *kerning = nullptr;
|
|
99
103
|
bool strictGlyphLookup = false;
|
|
100
104
|
int fallbackCodepoint = -1;
|
|
105
|
+
#if GEA_EMBEDDED_TTF_FREETYPE
|
|
106
|
+
// Runtime slot reuse must not reuse Canvas sprite/glyph-cache identity.
|
|
107
|
+
std::uint64_t generation = 0;
|
|
108
|
+
#else
|
|
109
|
+
static constexpr std::uint64_t generation = 0;
|
|
110
|
+
#endif
|
|
101
111
|
};
|
|
102
112
|
|
|
103
113
|
class RasterizedFont final : public Font {
|
|
@@ -122,6 +132,7 @@ class FontRegistry {
|
|
|
122
132
|
public:
|
|
123
133
|
static RasterizedFont rasterized(int fontId);
|
|
124
134
|
static RasterizedFont rasterizedFamily(int familyId, int sizePx);
|
|
135
|
+
static RasterizedFont rasterizedFamily(int familyId, int sizePx, int weight, int stretch);
|
|
125
136
|
static const BitmapFont8x16 &bitmap8x16();
|
|
126
137
|
static int familyId(const char *family);
|
|
127
138
|
static const char *familyName(int familyId);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <memory>
|
|
3
|
+
#include <mutex>
|
|
4
|
+
#include <unordered_map>
|
|
5
|
+
|
|
6
|
+
namespace gea::framework::identity {
|
|
7
|
+
struct Token {};
|
|
8
|
+
using Owner = std::shared_ptr<const Token>;
|
|
9
|
+
// A copied carrier retains the same occurrence, including copies made before
|
|
10
|
+
// the first identity observation. Fresh event carriers have distinct tokens.
|
|
11
|
+
class Occurrence {
|
|
12
|
+
public:
|
|
13
|
+
Occurrence() = default;
|
|
14
|
+
Occurrence(const Occurrence &other) : owner_(other.owner()) {}
|
|
15
|
+
Occurrence &operator=(const Occurrence &other) {
|
|
16
|
+
if (this != &other) owner_ = other.owner();
|
|
17
|
+
return *this;
|
|
18
|
+
}
|
|
19
|
+
Owner owner() const {
|
|
20
|
+
if (!owner_) owner_ = std::make_shared<Token>();
|
|
21
|
+
return owner_;
|
|
22
|
+
}
|
|
23
|
+
const void *identity() const { return owner().get(); }
|
|
24
|
+
private:
|
|
25
|
+
mutable Owner owner_;
|
|
26
|
+
};
|
|
27
|
+
// Tree ids are reused. Registry entries cover only the current allocation;
|
|
28
|
+
// the tree retains live-node tokens until removal/clear. Retained handles
|
|
29
|
+
// own retired tokens independently; weak collections never extend that life.
|
|
30
|
+
struct NodeRegistry {
|
|
31
|
+
std::mutex mutex;
|
|
32
|
+
std::unordered_map<int, Owner> nodes;
|
|
33
|
+
};
|
|
34
|
+
inline NodeRegistry ®istry() { static NodeRegistry value; return value; }
|
|
35
|
+
inline Owner node(int id) {
|
|
36
|
+
if (id < 0) return {};
|
|
37
|
+
auto &r = registry();
|
|
38
|
+
std::lock_guard<std::mutex> lock(r.mutex);
|
|
39
|
+
const auto found = r.nodes.find(id);
|
|
40
|
+
return found == r.nodes.end() ? Owner{} : found->second;
|
|
41
|
+
}
|
|
42
|
+
inline bool current(int id, const Owner &owner) {
|
|
43
|
+
if (id < 0 || !owner) return false;
|
|
44
|
+
auto &r = registry();
|
|
45
|
+
std::lock_guard<std::mutex> lock(r.mutex);
|
|
46
|
+
const auto found = r.nodes.find(id);
|
|
47
|
+
return found != r.nodes.end() && found->second == owner;
|
|
48
|
+
}
|
|
49
|
+
inline void activate(int id) {
|
|
50
|
+
if (id < 0) return;
|
|
51
|
+
auto &r = registry();
|
|
52
|
+
std::lock_guard<std::mutex> lock(r.mutex);
|
|
53
|
+
r.nodes[id] = std::make_shared<Token>();
|
|
54
|
+
}
|
|
55
|
+
inline void retire(int id) {
|
|
56
|
+
auto &r = registry();
|
|
57
|
+
std::lock_guard<std::mutex> lock(r.mutex);
|
|
58
|
+
r.nodes.erase(id);
|
|
59
|
+
}
|
|
60
|
+
inline void clear() {
|
|
61
|
+
auto &r = registry();
|
|
62
|
+
std::lock_guard<std::mutex> lock(r.mutex);
|
|
63
|
+
r.nodes.clear();
|
|
64
|
+
}
|
|
65
|
+
} // namespace gea::framework::identity
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "platform/file_cache.h"
|
|
4
|
+
#include <cstddef>
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <cstdio>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
|
|
9
|
+
#ifdef ESP_PLATFORM
|
|
10
|
+
#include "esp_attr.h"
|
|
11
|
+
#if !CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY
|
|
12
|
+
#error "File-backed assets require external BSS storage on ESP32"
|
|
13
|
+
#endif
|
|
14
|
+
#define GEA_FILE_ASSET_BUFFER EXT_RAM_BSS_ATTR
|
|
15
|
+
#else
|
|
16
|
+
#define GEA_FILE_ASSET_BUFFER
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
namespace gea::platform::storage {
|
|
20
|
+
struct AssetSegment {
|
|
21
|
+
unsigned char *data;
|
|
22
|
+
std::size_t size;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// The generator names immutable bundles by content. Validate the complete
|
|
26
|
+
// payload before exposing any font/image, then retain it in RAM for rendering.
|
|
27
|
+
inline bool loadAssetBundle(const char *path, const AssetSegment *segments,
|
|
28
|
+
std::size_t count, std::size_t bytes,
|
|
29
|
+
std::uint32_t expectedCrc) {
|
|
30
|
+
if (!ensureMounted())
|
|
31
|
+
return false;
|
|
32
|
+
auto *file = std::fopen(path, "rb");
|
|
33
|
+
if (!file) {
|
|
34
|
+
std::fprintf(stderr, "Gea asset bundle missing: %s\n", path);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
std::uint32_t crc = 0xffffffffU;
|
|
38
|
+
constexpr std::uint32_t table[] = {
|
|
39
|
+
0x00000000U, 0x1db71064U, 0x3b6e20c8U, 0x26d930acU,
|
|
40
|
+
0x76dc4190U, 0x6b6b51f4U, 0x4db26158U, 0x5005713cU,
|
|
41
|
+
0xedb88320U, 0xf00f9344U, 0xd6d6a3e8U, 0xcb61b38cU,
|
|
42
|
+
0x9b64c2b0U, 0x86d3d2d4U, 0xa00ae278U, 0xbdbdf21cU};
|
|
43
|
+
std::size_t read = 0;
|
|
44
|
+
bool ok = true;
|
|
45
|
+
for (std::size_t index = 0; index < count; ++index) {
|
|
46
|
+
const auto &part = segments[index];
|
|
47
|
+
if (part.size > bytes - read ||
|
|
48
|
+
std::fread(part.data, 1, part.size, file) != part.size) {
|
|
49
|
+
ok = false;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
read += part.size;
|
|
53
|
+
for (std::size_t i = 0; i < part.size; ++i) {
|
|
54
|
+
crc ^= part.data[i];
|
|
55
|
+
crc = (crc >> 4) ^ table[crc & 15];
|
|
56
|
+
crc = (crc >> 4) ^ table[crc & 15];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
ok = ok && read == bytes && (crc ^ 0xffffffffU) == expectedCrc &&
|
|
60
|
+
std::fgetc(file) == EOF && !std::ferror(file);
|
|
61
|
+
std::fclose(file);
|
|
62
|
+
if (!ok) {
|
|
63
|
+
for (std::size_t index = 0; index < count; ++index)
|
|
64
|
+
std::memset(segments[index].data, 0, segments[index].size);
|
|
65
|
+
std::fprintf(stderr, "Gea asset bundle failed validation: %s\n", path);
|
|
66
|
+
} else
|
|
67
|
+
std::fprintf(stderr, "Gea asset bundle loaded: %s (%lu bytes)\n", path,
|
|
68
|
+
static_cast<unsigned long>(bytes));
|
|
69
|
+
return ok;
|
|
70
|
+
}
|
|
71
|
+
} // namespace gea::platform::storage
|