xbintsc 0.3.69 → 0.3.81
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 +57 -4
- package/README.zh-CN.md +51 -2
- package/dist/src/driver/compiler.js +6 -1
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/extensions/gui/index.js +10 -2
- package/dist/src/extensions/gui/index.js.map +1 -1
- package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
- package/dist/tests/e2e/gui-advanced.test.js +261 -0
- package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
- package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
- package/dist/tests/e2e/gui-canvas.test.js +233 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
- package/dist/tests/e2e/gui-example-svg.test.d.ts +21 -0
- package/dist/tests/e2e/gui-example-svg.test.js +105 -0
- package/dist/tests/e2e/gui-example-svg.test.js.map +1 -0
- package/dist/tests/e2e/gui-example.test.d.ts +1 -1
- package/dist/tests/e2e/gui-example.test.js +2 -2
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +5 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-layout.test.js +73 -0
- package/dist/tests/e2e/gui-layout.test.js.map +1 -1
- package/dist/tests/e2e/gui-media.test.d.ts +8 -0
- package/dist/tests/e2e/gui-media.test.js +119 -0
- package/dist/tests/e2e/gui-media.test.js.map +1 -0
- package/dist/tests/e2e/gui-paint.test.js +40 -0
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/dist/tests/e2e/gui-transform.test.d.ts +7 -0
- package/dist/tests/e2e/gui-transform.test.js +198 -0
- package/dist/tests/e2e/gui-transform.test.js.map +1 -0
- package/doc/ai/cli.md +5 -0
- package/doc/ai/extensions.md +20 -3
- package/doc/ai/zh-CN/extensions.md +14 -3
- package/doc/gui/animation.md +97 -0
- package/doc/gui/api.md +92 -0
- package/doc/gui/build.md +91 -0
- package/doc/gui/canvas.md +98 -0
- package/doc/gui/dom.md +128 -0
- package/doc/gui/input.md +25 -0
- package/doc/gui/known-issues.md +78 -0
- package/doc/gui/layout.md +108 -0
- package/doc/gui/media.md +87 -0
- package/doc/gui/rendering.md +113 -0
- package/doc/gui/runtime.md +88 -0
- package/doc/gui-scripts.md +7 -3
- package/doc/gui.md +109 -612
- package/doc/implemented.md +1 -0
- package/doc/zh-CN/gui/animation.md +93 -0
- package/doc/zh-CN/gui/api.md +88 -0
- package/doc/zh-CN/gui/build.md +84 -0
- package/doc/zh-CN/gui/canvas.md +80 -0
- package/doc/zh-CN/gui/dom.md +119 -0
- package/doc/zh-CN/gui/input.md +23 -0
- package/doc/zh-CN/gui/known-issues.md +60 -0
- package/doc/zh-CN/gui/layout.md +91 -0
- package/doc/zh-CN/gui/media.md +71 -0
- package/doc/zh-CN/gui/rendering.md +92 -0
- package/doc/zh-CN/gui/runtime.md +76 -0
- package/doc/zh-CN/gui-scripts.md +6 -2
- package/doc/zh-CN/gui.md +89 -532
- package/doc/zh-CN/implemented.md +1 -0
- package/llms.txt +15 -2
- package/package.json +1 -1
- package/runtime/ext_gui/canvas.cpp +580 -0
- package/runtime/ext_gui/canvas.h +170 -0
- package/runtime/ext_gui/canvas_internal.h +59 -0
- package/runtime/ext_gui/canvas_raster.cpp +254 -0
- package/runtime/ext_gui/document.cpp +45 -4
- package/runtime/ext_gui/document.h +15 -1
- package/runtime/ext_gui/dom_api.cpp +3 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
- package/runtime/ext_gui/dom_api_internal.h +73 -0
- package/runtime/ext_gui/dom_api_media.cpp +274 -0
- package/runtime/ext_gui/dom_api_node.cpp +6 -4
- package/runtime/ext_gui/dom_api_proto.cpp +80 -0
- package/runtime/ext_gui/gui.cpp +6 -0
- package/runtime/ext_gui/gui_canvas.cpp +95 -0
- package/runtime/ext_gui/gui_engine.h +49 -0
- package/runtime/ext_gui/gui_events.cpp +21 -5
- package/runtime/ext_gui/gui_media.cpp +193 -0
- package/runtime/ext_gui/image.cpp +31 -12
- package/runtime/ext_gui/image.h +20 -7
- package/runtime/ext_gui/layout.cpp +130 -6
- package/runtime/ext_gui/layout.h +54 -1
- package/runtime/ext_gui/layout_flex.cpp +263 -99
- package/runtime/ext_gui/layout_flow.cpp +85 -9
- package/runtime/ext_gui/layout_flow_internal.h +84 -5
- package/runtime/ext_gui/layout_inline.cpp +54 -37
- package/runtime/ext_gui/media.cpp +514 -0
- package/runtime/ext_gui/media.h +104 -0
- package/runtime/ext_gui/nanosvg.h +3278 -0
- package/runtime/ext_gui/nanosvgrast.h +1472 -0
- package/runtime/ext_gui/paint.cpp +156 -38
- package/runtime/ext_gui/paint.h +42 -6
- package/runtime/ext_gui/renderer.cpp +117 -12
- package/runtime/ext_gui/renderer.h +49 -9
- package/runtime/ext_gui/renderer_image.cpp +205 -78
- package/runtime/ext_gui/renderer_internal.h +12 -9
- package/runtime/ext_gui/renderer_text.cpp +46 -39
- package/runtime/ext_gui/style.h +21 -0
- package/runtime/ext_gui/style_print.cpp +17 -0
- package/runtime/ext_gui/style_ua.cpp +7 -0
- package/runtime/ext_gui/style_values.cpp +83 -1
- package/runtime/ext_gui/svg.cpp +182 -0
- package/runtime/ext_gui/svg.h +46 -0
- package/runtime/ext_gui/text.cpp +156 -25
- package/runtime/ext_gui/text.h +11 -2
- package/runtime/ext_gui/transform.cpp +412 -0
- package/runtime/ext_gui/transform.h +112 -0
- package/runtime/ext_gui/window.cpp +10 -5
- package/scripts/build-gui.ts +148 -6
- package/scripts/check-file-length.ts +5 -1
- package/scripts/install-gui-deps.sh +28 -0
- package/src/driver/compiler.ts +6 -1
- package/src/extensions/gui/index.ts +10 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — `<video>` / `<audio>` lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* This unit bridges three pieces:
|
|
5
|
+
* - the DOM (`Node`) and the per-window media table on `XtGuiWindow`;
|
|
6
|
+
* - the decoders (`media.cpp`, backed by FFmpeg + SDL audio);
|
|
7
|
+
* - the renderer (`renderer_image.cpp`), which owns the GPU textures.
|
|
8
|
+
*
|
|
9
|
+
* The frame loop calls `xt_gui_media_sync` (open/close decoders so the next
|
|
10
|
+
* layout sees the current sources and intrinsic sizes) and `xt_gui_media_tick`
|
|
11
|
+
* (decode for the elapsed wall clock and fire the resulting DOM events) once
|
|
12
|
+
* per frame. No `xt_value` is stored here, so there is nothing for the
|
|
13
|
+
* collector to trace.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
#include "gui_internal.h"
|
|
17
|
+
|
|
18
|
+
#include <string>
|
|
19
|
+
#include <unordered_set>
|
|
20
|
+
#include <vector>
|
|
21
|
+
|
|
22
|
+
namespace {
|
|
23
|
+
|
|
24
|
+
bool isMediaElement(const xtgui::Node *node) {
|
|
25
|
+
return node != nullptr && node->isElement() && (node->isTag("video") || node->isTag("audio"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The element's `src`, or the `src` of its first child `<source>` (which is
|
|
29
|
+
* how a `<video>` with fallback formats is written). */
|
|
30
|
+
std::string sourceOf(const xtgui::Node *node) {
|
|
31
|
+
if (!isMediaElement(node)) return "";
|
|
32
|
+
const std::string *src = node->attr("src");
|
|
33
|
+
if (src != nullptr && !src->empty()) return *src;
|
|
34
|
+
for (const std::unique_ptr<xtgui::Node> &child : node->children) {
|
|
35
|
+
if (child->isElement() && child->isTag("source")) {
|
|
36
|
+
const std::string *child_src = child->attr("src");
|
|
37
|
+
if (child_src != nullptr && !child_src->empty()) return *child_src;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
bool booleanAttr(const xtgui::Node *node, const char *name) {
|
|
44
|
+
return node->attr(name) != nullptr;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void collectMedia(const xtgui::Node *node, std::vector<const xtgui::Node *> *out) {
|
|
48
|
+
if (node == nullptr) return;
|
|
49
|
+
if (isMediaElement(node)) {
|
|
50
|
+
out->push_back(node);
|
|
51
|
+
return; // a media element's children are `<source>`/`<track>`, not media
|
|
52
|
+
}
|
|
53
|
+
for (const std::unique_ptr<xtgui::Node> &child : node->children) collectMedia(child.get(), out);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
void releasePlayerTexture(const xtgui::MediaPlayer *player) {
|
|
57
|
+
if (player != nullptr && guidev::g_device != nullptr) {
|
|
58
|
+
xt_gui_media_texture_release(guidev::g_device, player->id());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Open a decoder for `node`/`src`, apply its boolean attributes, record it and
|
|
63
|
+
* return it (or NULL, after firing `error`). */
|
|
64
|
+
xtgui::MediaPlayer *openEntry(XtGuiWindow *win, const xtgui::Node *node, const std::string &src) {
|
|
65
|
+
XtGuiMediaEntry entry;
|
|
66
|
+
entry.src = src;
|
|
67
|
+
std::string error;
|
|
68
|
+
entry.player = xtgui::MediaPlayer::open(src, &error);
|
|
69
|
+
if (!entry.player) {
|
|
70
|
+
entry.failed = true;
|
|
71
|
+
win->media_players[node] = std::move(entry);
|
|
72
|
+
xt_gui_dispatch_to_node(win, node, "error");
|
|
73
|
+
return nullptr;
|
|
74
|
+
}
|
|
75
|
+
entry.player->set_loop(booleanAttr(node, "loop"));
|
|
76
|
+
entry.player->set_muted(booleanAttr(node, "muted"));
|
|
77
|
+
entry.player->set_autoplay(booleanAttr(node, "autoplay"));
|
|
78
|
+
xtgui::MediaPlayer *raw = entry.player.get();
|
|
79
|
+
win->media_players[node] = std::move(entry);
|
|
80
|
+
return raw;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void emitMediaEvents(XtGuiWindow *win, const xtgui::Node *node, unsigned events) {
|
|
84
|
+
if (events == 0) return;
|
|
85
|
+
if (events & xtgui::kMediaEventLoadedMetadata) {
|
|
86
|
+
xt_gui_dispatch_to_node(win, node, "loadedmetadata");
|
|
87
|
+
}
|
|
88
|
+
if (events & xtgui::kMediaEventPlay) xt_gui_dispatch_to_node(win, node, "play");
|
|
89
|
+
if (events & xtgui::kMediaEventPause) xt_gui_dispatch_to_node(win, node, "pause");
|
|
90
|
+
if (events & xtgui::kMediaEventEnded) xt_gui_dispatch_to_node(win, node, "ended");
|
|
91
|
+
if (events & xtgui::kMediaEventTimeUpdate) xt_gui_dispatch_to_node(win, node, "timeupdate");
|
|
92
|
+
if (events & xtgui::kMediaEventError) xt_gui_dispatch_to_node(win, node, "error");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
} // namespace
|
|
96
|
+
|
|
97
|
+
xtgui::MediaPlayer *xt_gui_media_player(XtGuiWindow *win, const xtgui::Node *node) {
|
|
98
|
+
if (win == nullptr || !isMediaElement(node)) return nullptr;
|
|
99
|
+
auto found = win->media_players.find(node);
|
|
100
|
+
if (found != win->media_players.end()) return found->second.player.get();
|
|
101
|
+
std::string src = sourceOf(node);
|
|
102
|
+
if (src.empty()) return nullptr;
|
|
103
|
+
return openEntry(win, node, src);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
void xt_gui_media_forget(XtGuiWindow *win, const xtgui::Node *node) {
|
|
107
|
+
if (win == nullptr) return;
|
|
108
|
+
auto found = win->media_players.find(node);
|
|
109
|
+
if (found == win->media_players.end()) return;
|
|
110
|
+
releasePlayerTexture(found->second.player.get());
|
|
111
|
+
win->media_players.erase(found);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
void xt_gui_media_sync(XtGuiWindow *win) {
|
|
115
|
+
if (win == nullptr || win->document == nullptr) return;
|
|
116
|
+
std::vector<const xtgui::Node *> nodes;
|
|
117
|
+
collectMedia(win->document->root(), &nodes);
|
|
118
|
+
std::unordered_set<const xtgui::Node *> live(nodes.begin(), nodes.end());
|
|
119
|
+
|
|
120
|
+
/* Drop decoders whose element left the tree. Comparing pointers only (never
|
|
121
|
+
* dereferencing) is safe for a node that was already destroyed. */
|
|
122
|
+
for (auto it = win->media_players.begin(); it != win->media_players.end();) {
|
|
123
|
+
if (live.count(it->first) == 0) {
|
|
124
|
+
releasePlayerTexture(it->second.player.get());
|
|
125
|
+
it = win->media_players.erase(it);
|
|
126
|
+
} else {
|
|
127
|
+
++it;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Open decoders for new sources and reopen when an element's `src` changes. */
|
|
132
|
+
for (const xtgui::Node *node : nodes) {
|
|
133
|
+
const std::string src = sourceOf(node);
|
|
134
|
+
auto found = win->media_players.find(node);
|
|
135
|
+
if (src.empty()) {
|
|
136
|
+
if (found != win->media_players.end()) {
|
|
137
|
+
releasePlayerTexture(found->second.player.get());
|
|
138
|
+
win->media_players.erase(found);
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (found != win->media_players.end() && found->second.src == src) continue;
|
|
143
|
+
if (found != win->media_players.end()) {
|
|
144
|
+
releasePlayerTexture(found->second.player.get());
|
|
145
|
+
win->media_players.erase(found);
|
|
146
|
+
}
|
|
147
|
+
openEntry(win, node, src);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Publish the intrinsic size of every `<video>` so layout can use it. */
|
|
151
|
+
for (const xtgui::Node *node : nodes) {
|
|
152
|
+
if (!node->isTag("video")) continue;
|
|
153
|
+
auto found = win->media_players.find(node);
|
|
154
|
+
xtgui::MediaPlayer *player = found == win->media_players.end() ? nullptr : found->second.player.get();
|
|
155
|
+
float width = 0.0f;
|
|
156
|
+
float height = 0.0f;
|
|
157
|
+
uint64_t id = 0;
|
|
158
|
+
if (player != nullptr) {
|
|
159
|
+
width = (float)player->frame_width();
|
|
160
|
+
height = (float)player->frame_height();
|
|
161
|
+
id = player->id();
|
|
162
|
+
}
|
|
163
|
+
win->document->setMediaSize(node, width, height, id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
void xt_gui_media_tick(XtGuiWindow *win, double now_ms) {
|
|
168
|
+
if (win == nullptr) return;
|
|
169
|
+
/* Snapshot the keys: a handler may change an element's `src`, which erases and
|
|
170
|
+
* reinserts into the table while we iterate. */
|
|
171
|
+
std::vector<const xtgui::Node *> nodes;
|
|
172
|
+
nodes.reserve(win->media_players.size());
|
|
173
|
+
for (const auto &entry : win->media_players) nodes.push_back(entry.first);
|
|
174
|
+
for (const xtgui::Node *node : nodes) {
|
|
175
|
+
auto found = win->media_players.find(node);
|
|
176
|
+
if (found == win->media_players.end() || !found->second.player) continue;
|
|
177
|
+
unsigned events = found->second.player->tick(now_ms);
|
|
178
|
+
/* A freshly decoded frame must reach the image upload. The geometry pass
|
|
179
|
+
* rebuilds the image batches, so mark it dirty once per new generation. */
|
|
180
|
+
if (found->second.player->has_video() &&
|
|
181
|
+
found->second.player->frame_generation() != found->second.rendered_generation) {
|
|
182
|
+
found->second.rendered_generation = found->second.player->frame_generation();
|
|
183
|
+
win->geometry.dirty = 1;
|
|
184
|
+
}
|
|
185
|
+
emitMediaEvents(win, node, events);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
void xt_gui_media_shutdown(XtGuiWindow *win) {
|
|
190
|
+
if (win == nullptr) return;
|
|
191
|
+
for (auto &entry : win->media_players) releasePlayerTexture(entry.second.player.get());
|
|
192
|
+
win->media_players.clear();
|
|
193
|
+
}
|
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
#include "stb_image.h"
|
|
14
14
|
|
|
15
15
|
#include <cctype>
|
|
16
|
+
#include <string>
|
|
16
17
|
#include <unordered_map>
|
|
17
18
|
|
|
19
|
+
#include "svg.h"
|
|
20
|
+
|
|
18
21
|
namespace xtgui {
|
|
19
22
|
namespace {
|
|
20
23
|
|
|
@@ -63,31 +66,44 @@ std::string normalizePath(const std::string &input) {
|
|
|
63
66
|
|
|
64
67
|
} // namespace
|
|
65
68
|
|
|
66
|
-
const Image *xt_image_load(const std::string &raw_path) {
|
|
69
|
+
const Image *xt_image_load(const std::string &raw_path, int target_width, int target_height) {
|
|
67
70
|
std::string path = normalizePath(raw_path);
|
|
71
|
+
bool svg = xt_svg_is_svg(path);
|
|
72
|
+
/* Raster formats always decode at their intrinsic resolution, so their cache
|
|
73
|
+
* key is the path. SVG is rasterised at the drawn pixel size, which is part
|
|
74
|
+
* of its identity. */
|
|
75
|
+
std::string key = path;
|
|
76
|
+
if (svg && (target_width > 0 || target_height > 0)) {
|
|
77
|
+
key += "#" + std::to_string(target_width) + "x" + std::to_string(target_height);
|
|
78
|
+
}
|
|
68
79
|
auto &cache = imageCache();
|
|
69
|
-
auto found = cache.find(
|
|
80
|
+
auto found = cache.find(key);
|
|
70
81
|
if (found != cache.end()) {
|
|
71
82
|
return found->second.width > 0 ? &found->second : nullptr;
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
Image image;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
if (svg) {
|
|
87
|
+
xt_svg_render(path, target_width, target_height, &image);
|
|
88
|
+
} else {
|
|
89
|
+
int width = 0;
|
|
90
|
+
int height = 0;
|
|
91
|
+
int channels = 0;
|
|
92
|
+
unsigned char *data = stbi_load(path.c_str(), &width, &height, &channels, 4);
|
|
93
|
+
if (data != nullptr) {
|
|
94
|
+
image.width = width;
|
|
95
|
+
image.height = height;
|
|
96
|
+
image.pixels.assign(data, data + (size_t)width * (size_t)height * 4);
|
|
97
|
+
stbi_image_free(data);
|
|
98
|
+
}
|
|
84
99
|
}
|
|
85
|
-
auto inserted = cache.emplace(
|
|
100
|
+
auto inserted = cache.emplace(key, std::move(image));
|
|
86
101
|
return inserted.first->second.width > 0 ? &inserted.first->second : nullptr;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
bool xt_image_size(const std::string &raw_path, int *width, int *height) {
|
|
90
105
|
std::string path = normalizePath(raw_path);
|
|
106
|
+
if (xt_svg_is_svg(path)) return xt_svg_size(path, width, height);
|
|
91
107
|
auto &cache = sizeCache();
|
|
92
108
|
auto found = cache.find(path);
|
|
93
109
|
if (found == cache.end()) {
|
|
@@ -117,9 +133,12 @@ bool xt_image_decode(const unsigned char *data, size_t size, Image *out) {
|
|
|
117
133
|
return true;
|
|
118
134
|
}
|
|
119
135
|
|
|
136
|
+
bool xt_image_is_vector(const std::string &path) { return xt_svg_is_svg(normalizePath(path)); }
|
|
137
|
+
|
|
120
138
|
void xt_image_shutdown() {
|
|
121
139
|
imageCache().clear();
|
|
122
140
|
sizeCache().clear();
|
|
141
|
+
xt_svg_shutdown();
|
|
123
142
|
}
|
|
124
143
|
|
|
125
144
|
} // namespace xtgui
|
package/runtime/ext_gui/image.h
CHANGED
|
@@ -23,18 +23,31 @@ struct Image {
|
|
|
23
23
|
std::vector<unsigned char> pixels;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
/** Decode an image file (PNG/JPEG/BMP/GIF/TGA) to RGBA8. Returns
|
|
27
|
-
* failure. Results (including failures) are cached by path. A
|
|
28
|
-
* and URL percent-encoding are handled.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*
|
|
26
|
+
/** Decode an image file (PNG/JPEG/BMP/GIF/TGA or SVG) to RGBA8. Returns
|
|
27
|
+
* nullptr on failure. Results (including failures) are cached by path. A
|
|
28
|
+
* `file://` prefix and URL percent-encoding are handled.
|
|
29
|
+
*
|
|
30
|
+
* `target_width`/`target_height` are the pixel size the image will be drawn at.
|
|
31
|
+
* Raster formats ignore them (they decode at their intrinsic resolution), but
|
|
32
|
+
* SVG is a vector format and is rasterised at (approximately) that size —
|
|
33
|
+
* preserving its aspect ratio — so a scaled `<img src="logo.svg">` stays crisp
|
|
34
|
+
* instead of being a magnified bitmap. Pass `<= 0` (the default) to use the intrinsic size. The cache is keyed by the
|
|
35
|
+
* resulting pixel size for SVG and by path for raster formats. */
|
|
36
|
+
const Image *xt_image_load(const std::string &path, int target_width = 0, int target_height = 0);
|
|
37
|
+
|
|
38
|
+
/** Read just the intrinsic size from the file header, without decoding. For an
|
|
39
|
+
* SVG this parses the document (the size comes from width/height or the
|
|
40
|
+
* viewBox). Returns false when the file cannot be read/parsed. Cached. */
|
|
33
41
|
bool xt_image_size(const std::string &path, int *width, int *height);
|
|
34
42
|
|
|
35
43
|
/** Decode an in-memory buffer to RGBA8 (e.g. a future `data:` URI). */
|
|
36
44
|
bool xt_image_decode(const unsigned char *data, size_t size, Image *out);
|
|
37
45
|
|
|
46
|
+
/** True when `path` is a vector source (SVG) that is rasterised on demand
|
|
47
|
+
* rather than decoded as a bitmap. The renderer uses this to key vector
|
|
48
|
+
* textures by pixel size. */
|
|
49
|
+
bool xt_image_is_vector(const std::string &path);
|
|
50
|
+
|
|
38
51
|
void xt_image_shutdown();
|
|
39
52
|
|
|
40
53
|
} // namespace xtgui
|
|
@@ -19,13 +19,19 @@
|
|
|
19
19
|
#include <cstring>
|
|
20
20
|
|
|
21
21
|
#include "image.h"
|
|
22
|
+
#include "style_internal.h"
|
|
22
23
|
|
|
23
24
|
namespace xtgui {
|
|
24
25
|
namespace {
|
|
25
26
|
|
|
27
|
+
/** Compose `transform`/`transform-origin` down the tree (defined below). */
|
|
28
|
+
void applyTransforms(LayoutBox *box, const Matrix2D &parent_world, bool parent_transformed,
|
|
29
|
+
float viewportWidth, float viewportHeight);
|
|
30
|
+
|
|
26
31
|
std::unique_ptr<LayoutBox> buildBoxTree(const Node *node,
|
|
27
32
|
const std::unordered_map<const Node *, XtStyle> &styles,
|
|
28
|
-
const XtStyle *inherited
|
|
33
|
+
const XtStyle *inherited, const MediaSizeMap *media,
|
|
34
|
+
const CanvasInfoMap *canvases) {
|
|
29
35
|
if (node->isText()) {
|
|
30
36
|
auto box = std::make_unique<LayoutBox>();
|
|
31
37
|
box->node = node;
|
|
@@ -55,9 +61,53 @@ std::unique_ptr<LayoutBox> buildBoxTree(const Node *node,
|
|
|
55
61
|
box->intrinsic_height = (float)height;
|
|
56
62
|
}
|
|
57
63
|
}
|
|
64
|
+
} else if (node->isElement() && node->isTag("video")) {
|
|
65
|
+
/* Default the video box to the HTML 300x150 until the decoder reports the
|
|
66
|
+
* real pixel size (fetched as soon as the element is associated with a
|
|
67
|
+
* player, before the next layout pass). */
|
|
68
|
+
box->is_image = true;
|
|
69
|
+
box->is_media = true;
|
|
70
|
+
box->intrinsic_width = 300;
|
|
71
|
+
box->intrinsic_height = 150;
|
|
72
|
+
if (media != nullptr) {
|
|
73
|
+
auto found = media->find(node);
|
|
74
|
+
if (found != media->end()) {
|
|
75
|
+
box->media_id = found->second.id;
|
|
76
|
+
if (found->second.width > 0 && found->second.height > 0) {
|
|
77
|
+
box->intrinsic_width = found->second.width;
|
|
78
|
+
box->intrinsic_height = found->second.height;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else if (node->isElement() && node->isTag("canvas")) {
|
|
83
|
+
/* A replaced inline-block whose pixels are a script-owned bitmap. The
|
|
84
|
+
* intrinsic size is the HTML 300x150 default, overridden by the
|
|
85
|
+
* `width`/`height` attributes (or a script assignment to `canvas.width`),
|
|
86
|
+
* published by the window as `CanvasInfo`. */
|
|
87
|
+
box->is_canvas = true;
|
|
88
|
+
box->intrinsic_width = 300;
|
|
89
|
+
box->intrinsic_height = 150;
|
|
90
|
+
float parsed = 0.0f;
|
|
91
|
+
const std::string *attr_width = node->attr("width");
|
|
92
|
+
const std::string *attr_height = node->attr("height");
|
|
93
|
+
if (attr_width != nullptr && xt_css_parse_number(*attr_width, &parsed) && parsed > 0.0f)
|
|
94
|
+
box->intrinsic_width = parsed;
|
|
95
|
+
if (attr_height != nullptr && xt_css_parse_number(*attr_height, &parsed) && parsed > 0.0f)
|
|
96
|
+
box->intrinsic_height = parsed;
|
|
97
|
+
if (canvases != nullptr) {
|
|
98
|
+
auto found = canvases->find(node);
|
|
99
|
+
if (found != canvases->end()) {
|
|
100
|
+
box->canvas_id = found->second.id;
|
|
101
|
+
if (found->second.width > 0 && found->second.height > 0) {
|
|
102
|
+
box->intrinsic_width = found->second.width;
|
|
103
|
+
box->intrinsic_height = found->second.height;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
58
107
|
}
|
|
59
108
|
for (const std::unique_ptr<Node> &child : node->children) {
|
|
60
|
-
std::unique_ptr<LayoutBox> childBox =
|
|
109
|
+
std::unique_ptr<LayoutBox> childBox =
|
|
110
|
+
buildBoxTree(child.get(), styles, style, media, canvases);
|
|
61
111
|
if (childBox != nullptr) {
|
|
62
112
|
childBox->parent = box.get();
|
|
63
113
|
box->children.push_back(std::move(childBox));
|
|
@@ -86,7 +136,8 @@ std::string dumpBox(const LayoutBox *box, int depth) {
|
|
|
86
136
|
} // namespace
|
|
87
137
|
|
|
88
138
|
void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *, XtStyle> &styles,
|
|
89
|
-
float viewportWidth, float viewportHeight
|
|
139
|
+
float viewportWidth, float viewportHeight, const MediaSizeMap *media,
|
|
140
|
+
const CanvasInfoMap *canvases) {
|
|
90
141
|
root_.reset();
|
|
91
142
|
index_.clear();
|
|
92
143
|
root_style_ = xt_style_initial();
|
|
@@ -105,14 +156,15 @@ void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *
|
|
|
105
156
|
if (root != nullptr) {
|
|
106
157
|
if (root->type == NodeType::Document) {
|
|
107
158
|
for (const std::unique_ptr<Node> &child : root->children) {
|
|
108
|
-
std::unique_ptr<LayoutBox> box =
|
|
159
|
+
std::unique_ptr<LayoutBox> box =
|
|
160
|
+
buildBoxTree(child.get(), styles, &root_style_, media, canvases);
|
|
109
161
|
if (box != nullptr) {
|
|
110
162
|
box->parent = root_.get();
|
|
111
163
|
root_->children.push_back(std::move(box));
|
|
112
164
|
}
|
|
113
165
|
}
|
|
114
166
|
} else {
|
|
115
|
-
std::unique_ptr<LayoutBox> box = buildBoxTree(root, styles, &root_style_);
|
|
167
|
+
std::unique_ptr<LayoutBox> box = buildBoxTree(root, styles, &root_style_, media, canvases);
|
|
116
168
|
if (box != nullptr) {
|
|
117
169
|
box->parent = root_.get();
|
|
118
170
|
root_->children.push_back(std::move(box));
|
|
@@ -120,6 +172,7 @@ void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *
|
|
|
120
172
|
}
|
|
121
173
|
}
|
|
122
174
|
xt_layout_run(root_.get(), styles, viewportWidth, viewportHeight);
|
|
175
|
+
applyTransforms(root_.get(), Matrix2D(), false, viewportWidth, viewportHeight);
|
|
123
176
|
|
|
124
177
|
std::vector<const LayoutBox *> stack;
|
|
125
178
|
stack.push_back(root_.get());
|
|
@@ -144,7 +197,11 @@ const LayoutBox *hitTestBox(const LayoutBox *box, float x, float y) {
|
|
|
144
197
|
for (auto it = box->children.rbegin(); it != box->children.rend(); ++it) {
|
|
145
198
|
if (const LayoutBox *hit = hitTestBox(it->get(), x, y)) return hit;
|
|
146
199
|
}
|
|
147
|
-
|
|
200
|
+
float local_x = x;
|
|
201
|
+
float local_y = y;
|
|
202
|
+
if (box->has_transform) box->inverse.apply(x, y, &local_x, &local_y);
|
|
203
|
+
if (local_x >= box->x && local_x < box->x + box->width && local_y >= box->y &&
|
|
204
|
+
local_y < box->y + box->height) {
|
|
148
205
|
return box;
|
|
149
206
|
}
|
|
150
207
|
return nullptr;
|
|
@@ -160,4 +217,71 @@ std::string LayoutTree::dump() const {
|
|
|
160
217
|
return root_ == nullptr ? std::string() : dumpBox(root_.get(), 0);
|
|
161
218
|
}
|
|
162
219
|
|
|
220
|
+
/* -- transforms ----------------------------------------------------------- */
|
|
221
|
+
|
|
222
|
+
namespace {
|
|
223
|
+
|
|
224
|
+
/** The transform a single box applies to its own geometry, relative to its
|
|
225
|
+
* `transform-origin`. Text boxes inherit their parent's style pointer, so only
|
|
226
|
+
* element boxes may contribute one (otherwise the style would be applied
|
|
227
|
+
* twice). */
|
|
228
|
+
Matrix2D boxLocalTransform(const LayoutBox *box, float viewportWidth, float viewportHeight) {
|
|
229
|
+
Matrix2D local;
|
|
230
|
+
if (box->style == nullptr || box->style->transform.empty()) return local;
|
|
231
|
+
if (box->node == nullptr || !box->node->isElement()) return local;
|
|
232
|
+
const XtStyle &style = *box->style;
|
|
233
|
+
Matrix2D body = xt_transform_matrix(style.transform, box->width, box->height, style.font_size,
|
|
234
|
+
viewportWidth, viewportHeight);
|
|
235
|
+
float origin_x = box->x + xt_style_resolve_absolute(style.transform_origin_x, box->width,
|
|
236
|
+
style.font_size, 16.0f, viewportWidth,
|
|
237
|
+
viewportHeight);
|
|
238
|
+
float origin_y = box->y + xt_style_resolve_absolute(style.transform_origin_y, box->height,
|
|
239
|
+
style.font_size, 16.0f, viewportWidth,
|
|
240
|
+
viewportHeight);
|
|
241
|
+
return Matrix2D::translate(origin_x, origin_y)
|
|
242
|
+
.multiply(body)
|
|
243
|
+
.multiply(Matrix2D::translate(-origin_x, -origin_y));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
void applyTransforms(LayoutBox *box, const Matrix2D &parent_world, bool parent_transformed,
|
|
247
|
+
float viewportWidth, float viewportHeight) {
|
|
248
|
+
Matrix2D local = boxLocalTransform(box, viewportWidth, viewportHeight);
|
|
249
|
+
box->world = parent_world.multiply(local);
|
|
250
|
+
box->has_transform = parent_transformed || !local.identity();
|
|
251
|
+
if (!box->world.invert(&box->inverse)) box->inverse = Matrix2D();
|
|
252
|
+
for (std::unique_ptr<LayoutBox> &child : box->children) {
|
|
253
|
+
applyTransforms(child.get(), box->world, box->has_transform, viewportWidth, viewportHeight);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
} // namespace
|
|
258
|
+
|
|
259
|
+
LayoutRect xt_layout_transform_rect(const Matrix2D &matrix, float x, float y, float width,
|
|
260
|
+
float height) {
|
|
261
|
+
LayoutRect out{x, y, width, height};
|
|
262
|
+
if (matrix.identity()) return out;
|
|
263
|
+
const float corners_x[4] = {x, x + width, x + width, x};
|
|
264
|
+
const float corners_y[4] = {y, y, y + height, y + height};
|
|
265
|
+
float min_x = 0, min_y = 0, max_x = 0, max_y = 0;
|
|
266
|
+
for (int i = 0; i < 4; i++) {
|
|
267
|
+
float corner_x = 0;
|
|
268
|
+
float corner_y = 0;
|
|
269
|
+
matrix.apply(corners_x[i], corners_y[i], &corner_x, &corner_y);
|
|
270
|
+
if (i == 0 || corner_x < min_x) min_x = corner_x;
|
|
271
|
+
if (i == 0 || corner_x > max_x) max_x = corner_x;
|
|
272
|
+
if (i == 0 || corner_y < min_y) min_y = corner_y;
|
|
273
|
+
if (i == 0 || corner_y > max_y) max_y = corner_y;
|
|
274
|
+
}
|
|
275
|
+
out.x = min_x;
|
|
276
|
+
out.y = min_y;
|
|
277
|
+
out.width = max_x - min_x;
|
|
278
|
+
out.height = max_y - min_y;
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
LayoutRect xt_layout_transformed_bounds(const LayoutBox *box) {
|
|
283
|
+
if (box == nullptr) return LayoutRect();
|
|
284
|
+
return xt_layout_transform_rect(box->world, box->x, box->y, box->width, box->height);
|
|
285
|
+
}
|
|
286
|
+
|
|
163
287
|
} // namespace xtgui
|
package/runtime/ext_gui/layout.h
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
#ifndef XT_GUI_LAYOUT_H
|
|
14
14
|
#define XT_GUI_LAYOUT_H
|
|
15
15
|
|
|
16
|
+
#include <cstdint>
|
|
16
17
|
#include <memory>
|
|
17
18
|
#include <string>
|
|
18
19
|
#include <unordered_map>
|
|
@@ -23,6 +24,25 @@
|
|
|
23
24
|
|
|
24
25
|
namespace xtgui {
|
|
25
26
|
|
|
27
|
+
/** Intrinsic size and decoder id of one `<video>` element, resolved by the
|
|
28
|
+
* engine and keyed by node. `width`/`height` are the decoded pixel size, or
|
|
29
|
+
* the 300x150 default before metadata is available. */
|
|
30
|
+
struct MediaSize {
|
|
31
|
+
float width = 0;
|
|
32
|
+
float height = 0;
|
|
33
|
+
uint64_t id = 0;
|
|
34
|
+
};
|
|
35
|
+
using MediaSizeMap = std::unordered_map<const Node *, MediaSize>;
|
|
36
|
+
|
|
37
|
+
/** Intrinsic size and render id of one `<canvas>` element, resolved by the
|
|
38
|
+
* engine and keyed by node. `width`/`height` are the bitmap's pixel size. */
|
|
39
|
+
struct CanvasInfo {
|
|
40
|
+
float width = 0;
|
|
41
|
+
float height = 0;
|
|
42
|
+
uint64_t id = 0;
|
|
43
|
+
};
|
|
44
|
+
using CanvasInfoMap = std::unordered_map<const Node *, CanvasInfo>;
|
|
45
|
+
|
|
26
46
|
/** A positioned text fragment (a wrapped text box can span several lines).
|
|
27
47
|
* `text` holds the run this fragment covers (a word or a run of spaces). */
|
|
28
48
|
struct TextFragment {
|
|
@@ -33,6 +53,11 @@ struct TextFragment {
|
|
|
33
53
|
std::string text;
|
|
34
54
|
};
|
|
35
55
|
|
|
56
|
+
/** A viewport-relative rectangle (`getBoundingClientRect` result). */
|
|
57
|
+
struct LayoutRect {
|
|
58
|
+
float x = 0, y = 0, width = 0, height = 0;
|
|
59
|
+
};
|
|
60
|
+
|
|
36
61
|
struct LayoutBox {
|
|
37
62
|
const Node *node = nullptr;
|
|
38
63
|
const XtStyle *style = nullptr;
|
|
@@ -47,6 +72,17 @@ struct LayoutBox {
|
|
|
47
72
|
float intrinsic_width = 0;
|
|
48
73
|
float intrinsic_height = 0;
|
|
49
74
|
|
|
75
|
+
/* `<video>`: a replaced element whose pixels come from a live decoder
|
|
76
|
+
* (`media_id`) instead of a file. `is_image` is set too so the
|
|
77
|
+
* replaced-element sizing in `layout_flow.cpp` applies unchanged. */
|
|
78
|
+
bool is_media = false;
|
|
79
|
+
uint64_t media_id = 0;
|
|
80
|
+
|
|
81
|
+
/* `<canvas>`: a replaced element whose pixels are a script-owned `Canvas2D`
|
|
82
|
+
* (`canvas_id`). `is_image`-style intrinsic sizing applies. */
|
|
83
|
+
bool is_canvas = false;
|
|
84
|
+
uint64_t canvas_id = 0;
|
|
85
|
+
|
|
50
86
|
Display display = Display::Inline;
|
|
51
87
|
|
|
52
88
|
/* Resolved (px) box edges. */
|
|
@@ -63,6 +99,13 @@ struct LayoutBox {
|
|
|
63
99
|
* is none): the containing block `position: absolute` resolves against. */
|
|
64
100
|
float cb_x = 0, cb_y = 0, cb_width = 0, cb_height = 0;
|
|
65
101
|
|
|
102
|
+
/* `transform`/`transform-origin` composed down the tree: `world` maps this
|
|
103
|
+
* box's layout coordinates to device space, `inverse` does the opposite (hit
|
|
104
|
+
* testing). `has_transform` is true for a box inside a transformed subtree. */
|
|
105
|
+
Matrix2D world;
|
|
106
|
+
Matrix2D inverse;
|
|
107
|
+
bool has_transform = false;
|
|
108
|
+
|
|
66
109
|
LayoutBox *parent = nullptr;
|
|
67
110
|
std::vector<std::unique_ptr<LayoutBox>> children;
|
|
68
111
|
};
|
|
@@ -70,7 +113,8 @@ struct LayoutBox {
|
|
|
70
113
|
class LayoutTree {
|
|
71
114
|
public:
|
|
72
115
|
void compute(const Node *root, const std::unordered_map<const Node *, XtStyle> &styles,
|
|
73
|
-
float viewportWidth, float viewportHeight
|
|
116
|
+
float viewportWidth, float viewportHeight, const MediaSizeMap *media = nullptr,
|
|
117
|
+
const CanvasInfoMap *canvases = nullptr);
|
|
74
118
|
void clear() {
|
|
75
119
|
root_.reset();
|
|
76
120
|
index_.clear();
|
|
@@ -92,6 +136,15 @@ class LayoutTree {
|
|
|
92
136
|
/** Text width under `style` (shaped with the HarfBuzz/FreeType stack). */
|
|
93
137
|
float xt_layout_text_width(const std::string &text, const XtStyle &style);
|
|
94
138
|
|
|
139
|
+
/** The box's border box in device space: its layout rectangle pushed through
|
|
140
|
+
* `box->world` and reduced to an axis-aligned bounding box. */
|
|
141
|
+
LayoutRect xt_layout_transformed_bounds(const LayoutBox *box);
|
|
142
|
+
|
|
143
|
+
/** Push an arbitrary layout rectangle through `matrix` and return its
|
|
144
|
+
* axis-aligned bounding box. */
|
|
145
|
+
LayoutRect xt_layout_transform_rect(const Matrix2D &matrix, float x, float y, float width,
|
|
146
|
+
float height);
|
|
147
|
+
|
|
95
148
|
} // namespace xtgui
|
|
96
149
|
|
|
97
150
|
#endif /* XT_GUI_LAYOUT_H */
|