xbintsc 0.3.79 → 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 +1 -3
- package/README.zh-CN.md +1 -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-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-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 +5 -1
- package/doc/ai/zh-CN/extensions.md +5 -1
- package/doc/gui/animation.md +53 -2
- package/doc/gui/api.md +1 -1
- package/doc/gui/dom.md +6 -1
- package/doc/gui/input.md +4 -1
- package/doc/gui/known-issues.md +16 -4
- package/doc/gui/layout.md +36 -0
- package/doc/gui/rendering.md +15 -0
- package/doc/gui.md +32 -3
- package/doc/implemented.md +1 -1
- package/doc/zh-CN/gui/animation.md +49 -2
- package/doc/zh-CN/gui/api.md +1 -1
- package/doc/zh-CN/gui/dom.md +6 -1
- package/doc/zh-CN/gui/input.md +2 -0
- package/doc/zh-CN/gui/known-issues.md +12 -2
- package/doc/zh-CN/gui/layout.md +31 -0
- package/doc/zh-CN/gui/rendering.md +11 -0
- package/doc/zh-CN/gui.md +28 -3
- package/doc/zh-CN/implemented.md +1 -1
- package/package.json +1 -1
- package/runtime/ext_gui/document.cpp +21 -3
- package/runtime/ext_gui/document.h +4 -1
- package/runtime/ext_gui/dom_api_node.cpp +6 -4
- package/runtime/ext_gui/layout.cpp +78 -1
- package/runtime/ext_gui/layout.h +21 -0
- package/runtime/ext_gui/paint.cpp +48 -15
- package/runtime/ext_gui/paint.h +8 -2
- package/runtime/ext_gui/renderer.cpp +4 -2
- package/runtime/ext_gui/renderer_image.cpp +2 -1
- package/runtime/ext_gui/renderer_internal.h +6 -1
- package/runtime/ext_gui/renderer_text.cpp +37 -15
- package/runtime/ext_gui/style.h +10 -0
- package/runtime/ext_gui/style_print.cpp +6 -0
- package/runtime/ext_gui/style_values.cpp +43 -0
- package/runtime/ext_gui/transform.cpp +412 -0
- package/runtime/ext_gui/transform.h +112 -0
- package/runtime/ext_gui/window.cpp +6 -4
- package/src/driver/compiler.ts +6 -1
- package/src/extensions/gui/index.ts +10 -2
|
@@ -19,10 +19,15 @@
|
|
|
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
33
|
const XtStyle *inherited, const MediaSizeMap *media,
|
|
@@ -167,6 +172,7 @@ void LayoutTree::compute(const Node *root, const std::unordered_map<const Node *
|
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
xt_layout_run(root_.get(), styles, viewportWidth, viewportHeight);
|
|
175
|
+
applyTransforms(root_.get(), Matrix2D(), false, viewportWidth, viewportHeight);
|
|
170
176
|
|
|
171
177
|
std::vector<const LayoutBox *> stack;
|
|
172
178
|
stack.push_back(root_.get());
|
|
@@ -191,7 +197,11 @@ const LayoutBox *hitTestBox(const LayoutBox *box, float x, float y) {
|
|
|
191
197
|
for (auto it = box->children.rbegin(); it != box->children.rend(); ++it) {
|
|
192
198
|
if (const LayoutBox *hit = hitTestBox(it->get(), x, y)) return hit;
|
|
193
199
|
}
|
|
194
|
-
|
|
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) {
|
|
195
205
|
return box;
|
|
196
206
|
}
|
|
197
207
|
return nullptr;
|
|
@@ -207,4 +217,71 @@ std::string LayoutTree::dump() const {
|
|
|
207
217
|
return root_ == nullptr ? std::string() : dumpBox(root_.get(), 0);
|
|
208
218
|
}
|
|
209
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
|
+
|
|
210
287
|
} // namespace xtgui
|
package/runtime/ext_gui/layout.h
CHANGED
|
@@ -53,6 +53,11 @@ struct TextFragment {
|
|
|
53
53
|
std::string text;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
/** A viewport-relative rectangle (`getBoundingClientRect` result). */
|
|
57
|
+
struct LayoutRect {
|
|
58
|
+
float x = 0, y = 0, width = 0, height = 0;
|
|
59
|
+
};
|
|
60
|
+
|
|
56
61
|
struct LayoutBox {
|
|
57
62
|
const Node *node = nullptr;
|
|
58
63
|
const XtStyle *style = nullptr;
|
|
@@ -94,6 +99,13 @@ struct LayoutBox {
|
|
|
94
99
|
* is none): the containing block `position: absolute` resolves against. */
|
|
95
100
|
float cb_x = 0, cb_y = 0, cb_width = 0, cb_height = 0;
|
|
96
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
|
+
|
|
97
109
|
LayoutBox *parent = nullptr;
|
|
98
110
|
std::vector<std::unique_ptr<LayoutBox>> children;
|
|
99
111
|
};
|
|
@@ -124,6 +136,15 @@ class LayoutTree {
|
|
|
124
136
|
/** Text width under `style` (shaped with the HarfBuzz/FreeType stack). */
|
|
125
137
|
float xt_layout_text_width(const std::string &text, const XtStyle &style);
|
|
126
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
|
+
|
|
127
148
|
} // namespace xtgui
|
|
128
149
|
|
|
129
150
|
#endif /* XT_GUI_LAYOUT_H */
|
|
@@ -22,6 +22,18 @@ std::string formatColor(const Color &color) {
|
|
|
22
22
|
return buffer;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** ` transform=matrix(...)` for a non-identity matrix, so the dump (and the
|
|
26
|
+
* tests that read it) can see where a box was transformed to. */
|
|
27
|
+
std::string transformSuffix(const Matrix2D &matrix) {
|
|
28
|
+
if (matrix.identity()) return "";
|
|
29
|
+
char buffer[192];
|
|
30
|
+
std::snprintf(buffer, sizeof(buffer),
|
|
31
|
+
" transform=matrix(%.4g,%.4g,%.4g,%.4g,%.4g,%.4g)", (double)matrix.a,
|
|
32
|
+
(double)matrix.b, (double)matrix.c, (double)matrix.d, (double)matrix.e,
|
|
33
|
+
(double)matrix.f);
|
|
34
|
+
return buffer;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
FontSpec specOf(const XtStyle &style) {
|
|
26
38
|
FontSpec spec;
|
|
27
39
|
spec.family = style.font_family;
|
|
@@ -32,7 +44,7 @@ FontSpec specOf(const XtStyle &style) {
|
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
void addRect(DisplayList &out, float x, float y, float width, float height, float radius,
|
|
35
|
-
const Color &color, const PaintClip &clip) {
|
|
47
|
+
const Color &color, const PaintClip &clip, const Matrix2D &transform) {
|
|
36
48
|
if (width <= 0.0f || height <= 0.0f || color.a <= 0.0f) return;
|
|
37
49
|
PaintRect rect;
|
|
38
50
|
rect.x = x;
|
|
@@ -41,6 +53,7 @@ void addRect(DisplayList &out, float x, float y, float width, float height, floa
|
|
|
41
53
|
rect.height = height;
|
|
42
54
|
rect.radius = radius;
|
|
43
55
|
rect.color = color;
|
|
56
|
+
rect.transform = transform;
|
|
44
57
|
out.rects.push_back(rect);
|
|
45
58
|
out.ops.push_back(PaintOp{PaintKind::Rect, (uint32_t)(out.rects.size() - 1), clip});
|
|
46
59
|
}
|
|
@@ -62,6 +75,7 @@ void addTextRuns(const LayoutBox *box, const XtStyle &style, DisplayList &out,
|
|
|
62
75
|
run.color = style.color;
|
|
63
76
|
run.text = fragment.text;
|
|
64
77
|
run.font = spec;
|
|
78
|
+
run.transform = box->world;
|
|
65
79
|
out.texts.push_back(std::move(run));
|
|
66
80
|
out.ops.push_back(PaintOp{PaintKind::Text, (uint32_t)(out.texts.size() - 1), clip});
|
|
67
81
|
}
|
|
@@ -75,7 +89,8 @@ void addImage(const PaintImage &image, DisplayList &out, const PaintClip &clip)
|
|
|
75
89
|
void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip);
|
|
76
90
|
|
|
77
91
|
/** The clip a box imposes on its content: its padding box, intersected with the
|
|
78
|
-
* clip already in effect. Only `overflow: hidden/scroll/auto` clips.
|
|
92
|
+
* clip already in effect. Only `overflow: hidden/scroll/auto` clips. A
|
|
93
|
+
* transformed box clips to the bounding box of its transformed padding box. */
|
|
79
94
|
PaintClip clipForChildren(const LayoutBox *box, const PaintClip &parent) {
|
|
80
95
|
if (box->style == nullptr || box->style->overflow == Overflow::Visible) return parent;
|
|
81
96
|
float x = box->x + box->border_left;
|
|
@@ -84,6 +99,13 @@ PaintClip clipForChildren(const LayoutBox *box, const PaintClip &parent) {
|
|
|
84
99
|
float height = box->height - box->border_top - box->border_bottom;
|
|
85
100
|
if (width < 0) width = 0;
|
|
86
101
|
if (height < 0) height = 0;
|
|
102
|
+
if (box->has_transform) {
|
|
103
|
+
LayoutRect bounds = xt_layout_transform_rect(box->world, x, y, width, height);
|
|
104
|
+
x = bounds.x;
|
|
105
|
+
y = bounds.y;
|
|
106
|
+
width = bounds.width;
|
|
107
|
+
height = bounds.height;
|
|
108
|
+
}
|
|
87
109
|
if (!parent.active) return PaintClip{true, x, y, width, height};
|
|
88
110
|
float x0 = std::max(x, parent.x);
|
|
89
111
|
float y0 = std::max(y, parent.y);
|
|
@@ -124,10 +146,12 @@ void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip) {
|
|
|
124
146
|
addTextRuns(box, *style, out, clip);
|
|
125
147
|
} else {
|
|
126
148
|
/* Background first, then the border ring, then replaced content. Children
|
|
127
|
-
* are clipped by this box's padding box when it establishes one.
|
|
149
|
+
* are clipped by this box's padding box when it establishes one. Every
|
|
150
|
+
* shape carries the box's `transform` matrix for the renderer. */
|
|
151
|
+
const Matrix2D &matrix = box->world;
|
|
128
152
|
if (style->has_background) {
|
|
129
153
|
addRect(out, box->x, box->y, box->width, box->height, style->border_radius,
|
|
130
|
-
style->background, clip);
|
|
154
|
+
style->background, clip, matrix);
|
|
131
155
|
}
|
|
132
156
|
if (style->border_style != BorderStyle::None && style->has_border_color) {
|
|
133
157
|
const Color bc = style->border_color;
|
|
@@ -136,11 +160,12 @@ void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip) {
|
|
|
136
160
|
float bottom = box->border_bottom;
|
|
137
161
|
float left = box->border_left;
|
|
138
162
|
float inner_height = box->height - top - bottom;
|
|
139
|
-
addRect(out, box->x, box->y, box->width, top, 0.0f, bc, clip);
|
|
140
|
-
addRect(out, box->x, box->y + box->height - bottom, box->width, bottom, 0.0f, bc, clip
|
|
141
|
-
|
|
163
|
+
addRect(out, box->x, box->y, box->width, top, 0.0f, bc, clip, matrix);
|
|
164
|
+
addRect(out, box->x, box->y + box->height - bottom, box->width, bottom, 0.0f, bc, clip,
|
|
165
|
+
matrix);
|
|
166
|
+
addRect(out, box->x, box->y + top, left, inner_height, 0.0f, bc, clip, matrix);
|
|
142
167
|
addRect(out, box->x + box->width - right, box->y + top, right, inner_height, 0.0f, bc,
|
|
143
|
-
clip);
|
|
168
|
+
clip, matrix);
|
|
144
169
|
}
|
|
145
170
|
if (box->is_canvas && box->canvas_id != 0 && box->content_width > 0.0f &&
|
|
146
171
|
box->content_height > 0.0f) {
|
|
@@ -150,6 +175,7 @@ void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip) {
|
|
|
150
175
|
image.width = box->content_width;
|
|
151
176
|
image.height = box->content_height;
|
|
152
177
|
image.canvas_id = box->canvas_id;
|
|
178
|
+
image.transform = matrix;
|
|
153
179
|
addImage(image, out, clip);
|
|
154
180
|
} else if (box->is_media && box->media_id != 0 && box->content_width > 0.0f &&
|
|
155
181
|
box->content_height > 0.0f) {
|
|
@@ -159,6 +185,7 @@ void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip) {
|
|
|
159
185
|
image.width = box->content_width;
|
|
160
186
|
image.height = box->content_height;
|
|
161
187
|
image.media_id = box->media_id;
|
|
188
|
+
image.transform = matrix;
|
|
162
189
|
addImage(image, out, clip);
|
|
163
190
|
} else if (box->is_image && !box->image_src.empty() && box->content_width > 0.0f &&
|
|
164
191
|
box->content_height > 0.0f) {
|
|
@@ -168,6 +195,7 @@ void paintBox(const LayoutBox *box, DisplayList &out, const PaintClip &clip) {
|
|
|
168
195
|
image.width = box->content_width;
|
|
169
196
|
image.height = box->content_height;
|
|
170
197
|
image.src = box->image_src;
|
|
198
|
+
image.transform = matrix;
|
|
171
199
|
addImage(image, out, clip);
|
|
172
200
|
}
|
|
173
201
|
}
|
|
@@ -185,7 +213,7 @@ void xt_paint_build(const LayoutBox *root, DisplayList &out) {
|
|
|
185
213
|
|
|
186
214
|
std::string DisplayList::dump() const {
|
|
187
215
|
std::string text;
|
|
188
|
-
char buffer[
|
|
216
|
+
char buffer[288];
|
|
189
217
|
for (const PaintOp &op : ops) {
|
|
190
218
|
std::string prefix;
|
|
191
219
|
if (op.clip.active) {
|
|
@@ -193,34 +221,39 @@ std::string DisplayList::dump() const {
|
|
|
193
221
|
op.clip.width, op.clip.height);
|
|
194
222
|
prefix = buffer;
|
|
195
223
|
}
|
|
224
|
+
std::string suffix;
|
|
196
225
|
if (op.kind == PaintKind::Rect) {
|
|
226
|
+
suffix = transformSuffix(rects[op.index].transform);
|
|
197
227
|
const PaintRect &rect = rects[op.index];
|
|
198
228
|
std::snprintf(buffer, sizeof(buffer),
|
|
199
|
-
"rect x=%.1f y=%.1f w=%.1f h=%.1f r=%.1f color=%s
|
|
229
|
+
"rect x=%.1f y=%.1f w=%.1f h=%.1f r=%.1f color=%s", rect.x, rect.y,
|
|
200
230
|
rect.width, rect.height, rect.radius, formatColor(rect.color).c_str());
|
|
201
231
|
text += prefix + buffer;
|
|
202
232
|
} else if (op.kind == PaintKind::Text) {
|
|
233
|
+
suffix = transformSuffix(texts[op.index].transform);
|
|
203
234
|
const PaintText &run = texts[op.index];
|
|
204
235
|
std::snprintf(buffer, sizeof(buffer),
|
|
205
|
-
"text x=%.1f baseline=%.1f size=%.1f color=%s \"%s\"
|
|
236
|
+
"text x=%.1f baseline=%.1f size=%.1f color=%s \"%s\"", run.x, run.baseline,
|
|
206
237
|
run.font.pixel_size, formatColor(run.color).c_str(), run.text.c_str());
|
|
207
238
|
text += prefix + buffer;
|
|
208
239
|
} else {
|
|
209
240
|
const PaintImage &image = images[op.index];
|
|
241
|
+
suffix = transformSuffix(image.transform);
|
|
210
242
|
if (image.media_id != 0) {
|
|
211
|
-
std::snprintf(buffer, sizeof(buffer), "image x=%.1f y=%.1f w=%.1f h=%.1f media=%llu
|
|
243
|
+
std::snprintf(buffer, sizeof(buffer), "image x=%.1f y=%.1f w=%.1f h=%.1f media=%llu",
|
|
212
244
|
image.x, image.y, image.width, image.height,
|
|
213
245
|
(unsigned long long)image.media_id);
|
|
214
246
|
} else if (image.canvas_id != 0) {
|
|
215
|
-
std::snprintf(buffer, sizeof(buffer), "image x=%.1f y=%.1f w=%.1f h=%.1f canvas=%llu
|
|
247
|
+
std::snprintf(buffer, sizeof(buffer), "image x=%.1f y=%.1f w=%.1f h=%.1f canvas=%llu",
|
|
216
248
|
image.x, image.y, image.width, image.height,
|
|
217
249
|
(unsigned long long)image.canvas_id);
|
|
218
250
|
} else {
|
|
219
|
-
std::snprintf(buffer, sizeof(buffer), "image x=%.1f y=%.1f w=%.1f h=%.1f src=%s
|
|
220
|
-
image.
|
|
251
|
+
std::snprintf(buffer, sizeof(buffer), "image x=%.1f y=%.1f w=%.1f h=%.1f src=%s", image.x,
|
|
252
|
+
image.y, image.width, image.height, image.src.c_str());
|
|
221
253
|
}
|
|
222
254
|
text += prefix + buffer;
|
|
223
255
|
}
|
|
256
|
+
text += suffix + "\n";
|
|
224
257
|
}
|
|
225
258
|
return text;
|
|
226
259
|
}
|
package/runtime/ext_gui/paint.h
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
#include "css.h"
|
|
21
21
|
#include "layout.h"
|
|
22
22
|
#include "text.h"
|
|
23
|
+
#include "transform.h"
|
|
23
24
|
|
|
24
25
|
namespace xtgui {
|
|
25
26
|
|
|
@@ -31,6 +32,8 @@ struct PaintRect {
|
|
|
31
32
|
float height = 0;
|
|
32
33
|
float radius = 0;
|
|
33
34
|
Color color;
|
|
35
|
+
/** `transform` of the box this shape belongs to (layout -> device space). */
|
|
36
|
+
Matrix2D transform;
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
/** A run of text to draw at `x`/`baseline` (viewport-relative). */
|
|
@@ -42,6 +45,7 @@ struct PaintText {
|
|
|
42
45
|
Color color;
|
|
43
46
|
std::string text;
|
|
44
47
|
FontSpec font;
|
|
48
|
+
Matrix2D transform;
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
/** An image to draw, scaled into the given rectangle (viewport-relative).
|
|
@@ -56,10 +60,12 @@ struct PaintImage {
|
|
|
56
60
|
std::string src;
|
|
57
61
|
uint64_t media_id = 0;
|
|
58
62
|
uint64_t canvas_id = 0;
|
|
63
|
+
Matrix2D transform;
|
|
59
64
|
};
|
|
60
65
|
|
|
61
|
-
/** The clip rectangle in effect for a painted shape (
|
|
62
|
-
* pixels
|
|
66
|
+
/** The clip rectangle in effect for a painted shape (device-space logical
|
|
67
|
+
* pixels — a transformed subtree contributes the bounding box of its clip).
|
|
68
|
+
* `width <= 0` means "no clip". */
|
|
63
69
|
struct PaintClip {
|
|
64
70
|
bool active = false;
|
|
65
71
|
float x = 0;
|
|
@@ -50,8 +50,10 @@ void pushRect(std::vector<Vertex> &verts, const xtgui::PaintRect &rect) {
|
|
|
50
50
|
const float local_y[4] = {-hh, -hh, hh, hh};
|
|
51
51
|
Vertex corners[4];
|
|
52
52
|
for (int i = 0; i < 4; i++) {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
/* `local` keeps the distance-field coordinates in the box's own frame, so
|
|
54
|
+
* a rotated/scaled shape still gets a rounded rectangle (the radius scales
|
|
55
|
+
* with the matrix instead of staying circular). */
|
|
56
|
+
rect.transform.apply(cx + local_x[i], cy + local_y[i], &corners[i].x, &corners[i].y);
|
|
55
57
|
corners[i].lx = local_x[i];
|
|
56
58
|
corners[i].ly = local_y[i];
|
|
57
59
|
corners[i].hx = hw;
|
|
@@ -313,7 +313,8 @@ void xt_gui_image_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
|
|
|
313
313
|
texture = ensureImageTexture(device, image.src, pixel_width, pixel_height);
|
|
314
314
|
}
|
|
315
315
|
if (texture == nullptr) continue;
|
|
316
|
-
pushGlyph(verts, image.x, image.y, image.width, image.height, full, white
|
|
316
|
+
pushGlyph(verts, image.x, image.y, image.width, image.height, full, white,
|
|
317
|
+
image.transform);
|
|
317
318
|
geometry->op_counts[index] = 6;
|
|
318
319
|
geometry->op_textures[index] = texture;
|
|
319
320
|
}
|
|
@@ -81,7 +81,12 @@ extern bool g_atlas_full_logged;
|
|
|
81
81
|
|
|
82
82
|
void pushRect(std::vector<Vertex> &verts, const xtgui::PaintRect &rect);
|
|
83
83
|
void pushGlyph(std::vector<TextVertex> &verts, float x, float y, float width, float height,
|
|
84
|
-
const GlyphEntry &entry, const xtgui::Color &color
|
|
84
|
+
const GlyphEntry &entry, const xtgui::Color &color,
|
|
85
|
+
const xtgui::Matrix2D &transform);
|
|
86
|
+
/** Push a textured quad, applying a `transform` matrix to its four corners. */
|
|
87
|
+
void pushQuad(std::vector<TextVertex> &verts, const float corners_x[4], const float corners_y[4],
|
|
88
|
+
const float u[4], const float v[4], const xtgui::Color &color,
|
|
89
|
+
const xtgui::Matrix2D &transform);
|
|
85
90
|
void uploadVertices(SDL_GPUDevice *device, XtGuiGeometry *geometry, const void *data,
|
|
86
91
|
uint32_t count, uint32_t stride);
|
|
87
92
|
|
|
@@ -17,16 +17,17 @@
|
|
|
17
17
|
namespace xtgui {
|
|
18
18
|
namespace renderer_detail {
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
/** Bounds for the atlas raster of a transformed glyph: below 1 the text is
|
|
21
|
+
* already shrinking, above 4 the atlas cost outweighs the sharpness. */
|
|
22
|
+
constexpr float kMinGlyphScale = 0.5f;
|
|
23
|
+
constexpr float kMaxGlyphScale = 4.0f;
|
|
24
|
+
|
|
25
|
+
void pushQuad(std::vector<TextVertex> &verts, const float corners_x[4], const float corners_y[4],
|
|
26
|
+
const float u[4], const float v[4], const xtgui::Color &color,
|
|
27
|
+
const xtgui::Matrix2D &transform) {
|
|
26
28
|
TextVertex corners[4];
|
|
27
29
|
for (int i = 0; i < 4; i++) {
|
|
28
|
-
corners[i].x
|
|
29
|
-
corners[i].y = corners_y[i];
|
|
30
|
+
transform.apply(corners_x[i], corners_y[i], &corners[i].x, &corners[i].y);
|
|
30
31
|
corners[i].u = u[i];
|
|
31
32
|
corners[i].v = v[i];
|
|
32
33
|
corners[i].r = color.r;
|
|
@@ -38,6 +39,16 @@ void pushGlyph(std::vector<TextVertex> &verts, float x, float y, float width, fl
|
|
|
38
39
|
for (int i = 0; i < 6; i++) verts.push_back(corners[order[i]]);
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
void pushGlyph(std::vector<TextVertex> &verts, float x, float y, float width, float height,
|
|
43
|
+
const GlyphEntry &entry, const xtgui::Color &color,
|
|
44
|
+
const xtgui::Matrix2D &transform) {
|
|
45
|
+
const float corners_x[4] = {x, x + width, x + width, x};
|
|
46
|
+
const float corners_y[4] = {y, y, y + height, y + height};
|
|
47
|
+
const float u[4] = {entry.u0, entry.u1, entry.u1, entry.u0};
|
|
48
|
+
const float v[4] = {entry.v0, entry.v0, entry.v1, entry.v1};
|
|
49
|
+
pushQuad(verts, corners_x, corners_y, u, v, color, transform);
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
bool ensureAtlasTexture(SDL_GPUDevice *device) {
|
|
42
53
|
if (g_atlas_texture != nullptr) return true;
|
|
43
54
|
SDL_GPUTextureCreateInfo info;
|
|
@@ -302,6 +313,15 @@ void xt_gui_text_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
|
|
|
302
313
|
xtgui::Font *logical = xtgui::xt_text_resolve(run.font);
|
|
303
314
|
if (logical == nullptr) continue;
|
|
304
315
|
|
|
316
|
+
/* A scaling `transform` magnifies the glyph quads on the GPU, so the atlas
|
|
317
|
+
has to hold a raster of that size or the text looks like a scaled bitmap:
|
|
318
|
+
rasterise at the device scale times the transform's magnification, and
|
|
319
|
+
clamp it — the atlas is finite, and a 40x transform would evict every
|
|
320
|
+
other glyph (a magnified bitmap is the right trade at that point). */
|
|
321
|
+
float glyph_scale = scale * run.transform.magnification();
|
|
322
|
+
if (glyph_scale < kMinGlyphScale) glyph_scale = kMinGlyphScale;
|
|
323
|
+
if (glyph_scale > kMaxGlyphScale) glyph_scale = kMaxGlyphScale;
|
|
324
|
+
|
|
305
325
|
std::vector<xtgui::ShapedGlyph> glyphs;
|
|
306
326
|
xtgui::xt_text_shape_run(run.text, logical, glyphs);
|
|
307
327
|
float pen_x = run.x;
|
|
@@ -309,17 +329,19 @@ void xt_gui_text_upload(SDL_GPUDevice *device, XtGuiGeometry *geometry,
|
|
|
309
329
|
for (const xtgui::ShapedGlyph &glyph : glyphs) {
|
|
310
330
|
if (glyph.glyph != 0) {
|
|
311
331
|
xtgui::Font *face = glyph.font != nullptr ? glyph.font : logical;
|
|
312
|
-
if (
|
|
313
|
-
xtgui::Font *scaled = xtgui::xt_text_scale(face, run.font.pixel_size *
|
|
332
|
+
if (glyph_scale != 1.0f) {
|
|
333
|
+
xtgui::Font *scaled = xtgui::xt_text_scale(face, run.font.pixel_size * glyph_scale);
|
|
314
334
|
if (scaled != nullptr) face = scaled;
|
|
315
335
|
}
|
|
316
336
|
GlyphEntry entry;
|
|
317
337
|
if (ensureGlyph(device, face, glyph.glyph, entry)) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
float
|
|
321
|
-
float
|
|
322
|
-
|
|
338
|
+
/* Entry metrics are in raster pixels; divide them back into logical
|
|
339
|
+
units — the transform (not the atlas) does the final scaling. */
|
|
340
|
+
float quad_x = pen_x + glyph.x_offset + (float)entry.left / glyph_scale;
|
|
341
|
+
float quad_y = run.baseline + glyph.y_offset - (float)entry.top / glyph_scale;
|
|
342
|
+
float quad_w = (float)entry.width / glyph_scale;
|
|
343
|
+
float quad_h = (float)entry.height / glyph_scale;
|
|
344
|
+
pushGlyph(verts, quad_x, quad_y, quad_w, quad_h, entry, run.color, run.transform);
|
|
323
345
|
}
|
|
324
346
|
}
|
|
325
347
|
pen_x += glyph.x_advance;
|
package/runtime/ext_gui/style.h
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
#include "css.h"
|
|
18
18
|
#include "dom.h"
|
|
19
|
+
#include "transform.h"
|
|
19
20
|
|
|
20
21
|
namespace xtgui {
|
|
21
22
|
|
|
@@ -107,6 +108,12 @@ struct XtStyle {
|
|
|
107
108
|
FloatSide float_side = FloatSide::None;
|
|
108
109
|
Clear clear = Clear::None;
|
|
109
110
|
|
|
111
|
+
/** `transform` (not inherited) and `transform-origin` (defaults to the centre
|
|
112
|
+
* of the border box). Layout resolves the list into one matrix per box. */
|
|
113
|
+
std::vector<TransformOp> transform;
|
|
114
|
+
Length transform_origin_x{50.0f, Unit::Percent};
|
|
115
|
+
Length transform_origin_y{50.0f, Unit::Percent};
|
|
116
|
+
|
|
110
117
|
/** `transition` specs; not inherited. Applied by `XtDocument`. */
|
|
111
118
|
std::vector<TransitionSpec> transitions;
|
|
112
119
|
};
|
|
@@ -123,6 +130,9 @@ void xt_style_compute(const Node *root, const StyleSheet &sheet,
|
|
|
123
130
|
/** Format a computed property for diagnostics/tests (`computedStyle(sel, prop)`). */
|
|
124
131
|
std::string xt_style_property_to_string(const XtStyle &style, const std::string &property);
|
|
125
132
|
|
|
133
|
+
/** Format a length the way `computedStyle` does (`4px`, `50%`, `1.5`). */
|
|
134
|
+
std::string xt_style_length_to_string(const Length &length);
|
|
135
|
+
|
|
126
136
|
} // namespace xtgui
|
|
127
137
|
|
|
128
138
|
#endif /* XT_GUI_STYLE_H */
|
|
@@ -118,7 +118,13 @@ std::string xt_style_property_to_string(const XtStyle &style, const std::string
|
|
|
118
118
|
if (property == "border-radius") return formatNumber(style.border_radius) + "px";
|
|
119
119
|
if (property == "opacity") return formatNumber(style.opacity);
|
|
120
120
|
if (property == "z-index") return style.has_z_index ? formatNumber((float)style.z_index) : "auto";
|
|
121
|
+
if (property == "transform") return xt_transform_to_string(style.transform);
|
|
122
|
+
if (property == "transform-origin") {
|
|
123
|
+
return formatLength(style.transform_origin_x) + " " + formatLength(style.transform_origin_y);
|
|
124
|
+
}
|
|
121
125
|
return "";
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
std::string xt_style_length_to_string(const Length &length) { return formatLength(length); }
|
|
129
|
+
|
|
124
130
|
} // namespace xtgui
|
|
@@ -441,6 +441,49 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
|
|
|
441
441
|
return;
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
if (property == "transform") {
|
|
445
|
+
std::vector<TransformOp> ops;
|
|
446
|
+
if (xt_transform_parse(value, &ops)) style->transform = ops;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (property == "transform-origin") {
|
|
450
|
+
/* One or two values plus an optional z offset (ignored: the engine is 2D).
|
|
451
|
+
* The keywords carry an axis, so `top` alone means `center top` and
|
|
452
|
+
* `top center` is the same as `center top`. */
|
|
453
|
+
std::vector<std::string> parts = xt_css_split_whitespace(value);
|
|
454
|
+
Length values[2] = {Length{50.0f, Unit::Percent}, Length{50.0f, Unit::Percent}};
|
|
455
|
+
bool vertical[2] = {false, false};
|
|
456
|
+
size_t used = 0;
|
|
457
|
+
for (const std::string &part : parts) {
|
|
458
|
+
if (used >= 2) break;
|
|
459
|
+
std::string keyword = xt_css_lower(part);
|
|
460
|
+
Length length = xt_css_parse_length(keyword);
|
|
461
|
+
if (length.unit == Unit::Invalid || length.unit == Unit::Auto || length.unit == Unit::None) {
|
|
462
|
+
if (keyword == "left" || keyword == "top") length = {0.0f, Unit::Percent};
|
|
463
|
+
else if (keyword == "center") length = {50.0f, Unit::Percent};
|
|
464
|
+
else if (keyword == "right" || keyword == "bottom") length = {100.0f, Unit::Percent};
|
|
465
|
+
else return;
|
|
466
|
+
vertical[used] = keyword == "top" || keyword == "bottom";
|
|
467
|
+
}
|
|
468
|
+
values[used++] = length;
|
|
469
|
+
}
|
|
470
|
+
if (used == 0) return;
|
|
471
|
+
Length x = values[0];
|
|
472
|
+
Length y = used > 1 ? values[1] : Length{50.0f, Unit::Percent};
|
|
473
|
+
if (used > 1) {
|
|
474
|
+
if (vertical[0] && !vertical[1]) {
|
|
475
|
+
x = values[1];
|
|
476
|
+
y = values[0];
|
|
477
|
+
}
|
|
478
|
+
} else if (vertical[0]) {
|
|
479
|
+
/* `top`/`bottom` on their own keep x centred. */
|
|
480
|
+
x = Length{50.0f, Unit::Percent};
|
|
481
|
+
y = values[0];
|
|
482
|
+
}
|
|
483
|
+
style->transform_origin_x = x;
|
|
484
|
+
style->transform_origin_y = y;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
444
487
|
if (property == "opacity") {
|
|
445
488
|
float number = 0;
|
|
446
489
|
if (xt_css_parse_number(value, &number)) style->opacity = number < 0 ? 0 : (number > 1 ? 1 : number);
|