xbintsc 0.3.69 → 0.3.79
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 +59 -4
- package/README.zh-CN.md +52 -2
- 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.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/doc/ai/extensions.md +16 -3
- package/doc/ai/zh-CN/extensions.md +10 -3
- package/doc/gui/animation.md +46 -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 +123 -0
- package/doc/gui/input.md +22 -0
- package/doc/gui/known-issues.md +66 -0
- package/doc/gui/layout.md +72 -0
- package/doc/gui/media.md +87 -0
- package/doc/gui/rendering.md +98 -0
- package/doc/gui/runtime.md +88 -0
- package/doc/gui-scripts.md +7 -3
- package/doc/gui.md +80 -612
- package/doc/implemented.md +1 -0
- package/doc/zh-CN/gui/animation.md +46 -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 +114 -0
- package/doc/zh-CN/gui/input.md +21 -0
- package/doc/zh-CN/gui/known-issues.md +50 -0
- package/doc/zh-CN/gui/layout.md +60 -0
- package/doc/zh-CN/gui/media.md +71 -0
- package/doc/zh-CN/gui/rendering.md +81 -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 +64 -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 +24 -1
- package/runtime/ext_gui/document.h +11 -0
- 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_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 +52 -5
- package/runtime/ext_gui/layout.h +33 -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 +123 -38
- package/runtime/ext_gui/paint.h +36 -6
- package/runtime/ext_gui/renderer.cpp +113 -10
- package/runtime/ext_gui/renderer.h +49 -9
- package/runtime/ext_gui/renderer_image.cpp +203 -77
- package/runtime/ext_gui/renderer_internal.h +6 -8
- package/runtime/ext_gui/renderer_text.cpp +11 -26
- package/runtime/ext_gui/style.h +11 -0
- package/runtime/ext_gui/style_print.cpp +11 -0
- package/runtime/ext_gui/style_ua.cpp +7 -0
- package/runtime/ext_gui/style_values.cpp +40 -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/window.cpp +4 -1
- package/scripts/build-gui.ts +148 -6
- package/scripts/check-file-length.ts +5 -1
- package/scripts/install-gui-deps.sh +28 -0
|
@@ -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
|
|
@@ -25,7 +25,8 @@ namespace {
|
|
|
25
25
|
|
|
26
26
|
std::unique_ptr<LayoutBox> buildBoxTree(const Node *node,
|
|
27
27
|
const std::unordered_map<const Node *, XtStyle> &styles,
|
|
28
|
-
const XtStyle *inherited
|
|
28
|
+
const XtStyle *inherited, const MediaSizeMap *media,
|
|
29
|
+
const CanvasInfoMap *canvases) {
|
|
29
30
|
if (node->isText()) {
|
|
30
31
|
auto box = std::make_unique<LayoutBox>();
|
|
31
32
|
box->node = node;
|
|
@@ -55,9 +56,53 @@ std::unique_ptr<LayoutBox> buildBoxTree(const Node *node,
|
|
|
55
56
|
box->intrinsic_height = (float)height;
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
} else if (node->isElement() && node->isTag("video")) {
|
|
60
|
+
/* Default the video box to the HTML 300x150 until the decoder reports the
|
|
61
|
+
* real pixel size (fetched as soon as the element is associated with a
|
|
62
|
+
* player, before the next layout pass). */
|
|
63
|
+
box->is_image = true;
|
|
64
|
+
box->is_media = true;
|
|
65
|
+
box->intrinsic_width = 300;
|
|
66
|
+
box->intrinsic_height = 150;
|
|
67
|
+
if (media != nullptr) {
|
|
68
|
+
auto found = media->find(node);
|
|
69
|
+
if (found != media->end()) {
|
|
70
|
+
box->media_id = found->second.id;
|
|
71
|
+
if (found->second.width > 0 && found->second.height > 0) {
|
|
72
|
+
box->intrinsic_width = found->second.width;
|
|
73
|
+
box->intrinsic_height = found->second.height;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else if (node->isElement() && node->isTag("canvas")) {
|
|
78
|
+
/* A replaced inline-block whose pixels are a script-owned bitmap. The
|
|
79
|
+
* intrinsic size is the HTML 300x150 default, overridden by the
|
|
80
|
+
* `width`/`height` attributes (or a script assignment to `canvas.width`),
|
|
81
|
+
* published by the window as `CanvasInfo`. */
|
|
82
|
+
box->is_canvas = true;
|
|
83
|
+
box->intrinsic_width = 300;
|
|
84
|
+
box->intrinsic_height = 150;
|
|
85
|
+
float parsed = 0.0f;
|
|
86
|
+
const std::string *attr_width = node->attr("width");
|
|
87
|
+
const std::string *attr_height = node->attr("height");
|
|
88
|
+
if (attr_width != nullptr && xt_css_parse_number(*attr_width, &parsed) && parsed > 0.0f)
|
|
89
|
+
box->intrinsic_width = parsed;
|
|
90
|
+
if (attr_height != nullptr && xt_css_parse_number(*attr_height, &parsed) && parsed > 0.0f)
|
|
91
|
+
box->intrinsic_height = parsed;
|
|
92
|
+
if (canvases != nullptr) {
|
|
93
|
+
auto found = canvases->find(node);
|
|
94
|
+
if (found != canvases->end()) {
|
|
95
|
+
box->canvas_id = found->second.id;
|
|
96
|
+
if (found->second.width > 0 && found->second.height > 0) {
|
|
97
|
+
box->intrinsic_width = found->second.width;
|
|
98
|
+
box->intrinsic_height = found->second.height;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
58
102
|
}
|
|
59
103
|
for (const std::unique_ptr<Node> &child : node->children) {
|
|
60
|
-
std::unique_ptr<LayoutBox> childBox =
|
|
104
|
+
std::unique_ptr<LayoutBox> childBox =
|
|
105
|
+
buildBoxTree(child.get(), styles, style, media, canvases);
|
|
61
106
|
if (childBox != nullptr) {
|
|
62
107
|
childBox->parent = box.get();
|
|
63
108
|
box->children.push_back(std::move(childBox));
|
|
@@ -86,7 +131,8 @@ std::string dumpBox(const LayoutBox *box, int depth) {
|
|
|
86
131
|
} // namespace
|
|
87
132
|
|
|
88
133
|
void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *, XtStyle> &styles,
|
|
89
|
-
float viewportWidth, float viewportHeight
|
|
134
|
+
float viewportWidth, float viewportHeight, const MediaSizeMap *media,
|
|
135
|
+
const CanvasInfoMap *canvases) {
|
|
90
136
|
root_.reset();
|
|
91
137
|
index_.clear();
|
|
92
138
|
root_style_ = xt_style_initial();
|
|
@@ -105,14 +151,15 @@ void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *
|
|
|
105
151
|
if (root != nullptr) {
|
|
106
152
|
if (root->type == NodeType::Document) {
|
|
107
153
|
for (const std::unique_ptr<Node> &child : root->children) {
|
|
108
|
-
std::unique_ptr<LayoutBox> box =
|
|
154
|
+
std::unique_ptr<LayoutBox> box =
|
|
155
|
+
buildBoxTree(child.get(), styles, &root_style_, media, canvases);
|
|
109
156
|
if (box != nullptr) {
|
|
110
157
|
box->parent = root_.get();
|
|
111
158
|
root_->children.push_back(std::move(box));
|
|
112
159
|
}
|
|
113
160
|
}
|
|
114
161
|
} else {
|
|
115
|
-
std::unique_ptr<LayoutBox> box = buildBoxTree(root, styles, &root_style_);
|
|
162
|
+
std::unique_ptr<LayoutBox> box = buildBoxTree(root, styles, &root_style_, media, canvases);
|
|
116
163
|
if (box != nullptr) {
|
|
117
164
|
box->parent = root_.get();
|
|
118
165
|
root_->children.push_back(std::move(box));
|
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 {
|
|
@@ -47,6 +67,17 @@ struct LayoutBox {
|
|
|
47
67
|
float intrinsic_width = 0;
|
|
48
68
|
float intrinsic_height = 0;
|
|
49
69
|
|
|
70
|
+
/* `<video>`: a replaced element whose pixels come from a live decoder
|
|
71
|
+
* (`media_id`) instead of a file. `is_image` is set too so the
|
|
72
|
+
* replaced-element sizing in `layout_flow.cpp` applies unchanged. */
|
|
73
|
+
bool is_media = false;
|
|
74
|
+
uint64_t media_id = 0;
|
|
75
|
+
|
|
76
|
+
/* `<canvas>`: a replaced element whose pixels are a script-owned `Canvas2D`
|
|
77
|
+
* (`canvas_id`). `is_image`-style intrinsic sizing applies. */
|
|
78
|
+
bool is_canvas = false;
|
|
79
|
+
uint64_t canvas_id = 0;
|
|
80
|
+
|
|
50
81
|
Display display = Display::Inline;
|
|
51
82
|
|
|
52
83
|
/* Resolved (px) box edges. */
|
|
@@ -70,7 +101,8 @@ struct LayoutBox {
|
|
|
70
101
|
class LayoutTree {
|
|
71
102
|
public:
|
|
72
103
|
void compute(const Node *root, const std::unordered_map<const Node *, XtStyle> &styles,
|
|
73
|
-
float viewportWidth, float viewportHeight
|
|
104
|
+
float viewportWidth, float viewportHeight, const MediaSizeMap *media = nullptr,
|
|
105
|
+
const CanvasInfoMap *canvases = nullptr);
|
|
74
106
|
void clear() {
|
|
75
107
|
root_.reset();
|
|
76
108
|
index_.clear();
|