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.
Files changed (53) hide show
  1. package/README.md +1 -3
  2. package/README.zh-CN.md +1 -2
  3. package/dist/src/driver/compiler.js +6 -1
  4. package/dist/src/driver/compiler.js.map +1 -1
  5. package/dist/src/extensions/gui/index.js +10 -2
  6. package/dist/src/extensions/gui/index.js.map +1 -1
  7. package/dist/tests/e2e/gui-example-svg.test.d.ts +21 -0
  8. package/dist/tests/e2e/gui-example-svg.test.js +105 -0
  9. package/dist/tests/e2e/gui-example-svg.test.js.map +1 -0
  10. package/dist/tests/e2e/gui-transform.test.d.ts +7 -0
  11. package/dist/tests/e2e/gui-transform.test.js +198 -0
  12. package/dist/tests/e2e/gui-transform.test.js.map +1 -0
  13. package/doc/ai/cli.md +5 -0
  14. package/doc/ai/extensions.md +5 -1
  15. package/doc/ai/zh-CN/extensions.md +5 -1
  16. package/doc/gui/animation.md +53 -2
  17. package/doc/gui/api.md +1 -1
  18. package/doc/gui/dom.md +6 -1
  19. package/doc/gui/input.md +4 -1
  20. package/doc/gui/known-issues.md +16 -4
  21. package/doc/gui/layout.md +36 -0
  22. package/doc/gui/rendering.md +15 -0
  23. package/doc/gui.md +32 -3
  24. package/doc/implemented.md +1 -1
  25. package/doc/zh-CN/gui/animation.md +49 -2
  26. package/doc/zh-CN/gui/api.md +1 -1
  27. package/doc/zh-CN/gui/dom.md +6 -1
  28. package/doc/zh-CN/gui/input.md +2 -0
  29. package/doc/zh-CN/gui/known-issues.md +12 -2
  30. package/doc/zh-CN/gui/layout.md +31 -0
  31. package/doc/zh-CN/gui/rendering.md +11 -0
  32. package/doc/zh-CN/gui.md +28 -3
  33. package/doc/zh-CN/implemented.md +1 -1
  34. package/package.json +1 -1
  35. package/runtime/ext_gui/document.cpp +21 -3
  36. package/runtime/ext_gui/document.h +4 -1
  37. package/runtime/ext_gui/dom_api_node.cpp +6 -4
  38. package/runtime/ext_gui/layout.cpp +78 -1
  39. package/runtime/ext_gui/layout.h +21 -0
  40. package/runtime/ext_gui/paint.cpp +48 -15
  41. package/runtime/ext_gui/paint.h +8 -2
  42. package/runtime/ext_gui/renderer.cpp +4 -2
  43. package/runtime/ext_gui/renderer_image.cpp +2 -1
  44. package/runtime/ext_gui/renderer_internal.h +6 -1
  45. package/runtime/ext_gui/renderer_text.cpp +37 -15
  46. package/runtime/ext_gui/style.h +10 -0
  47. package/runtime/ext_gui/style_print.cpp +6 -0
  48. package/runtime/ext_gui/style_values.cpp +43 -0
  49. package/runtime/ext_gui/transform.cpp +412 -0
  50. package/runtime/ext_gui/transform.h +112 -0
  51. package/runtime/ext_gui/window.cpp +6 -4
  52. package/src/driver/compiler.ts +6 -1
  53. 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
- if (x >= box->x && x < box->x + box->width && y >= box->y && y < box->y + box->height) {
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
@@ -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
- addRect(out, box->x, box->y + top, left, inner_height, 0.0f, bc, clip);
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[224];
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\n", rect.x, rect.y,
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\"\n", run.x, run.baseline,
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\n",
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\n",
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\n",
220
- image.x, image.y, image.width, image.height, image.src.c_str());
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
  }
@@ -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 (viewport-relative logical
62
- * pixels). `width <= 0` means "no clip". */
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
- corners[i].x = cx + local_x[i];
54
- corners[i].y = cy + local_y[i];
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
- void pushGlyph(std::vector<TextVertex> &verts, float x, float y, float width, float height,
21
- const GlyphEntry &entry, const xtgui::Color &color) {
22
- const float corners_x[4] = {x, x + width, x + width, x};
23
- const float corners_y[4] = {y, y, y + height, y + height};
24
- const float u[4] = {entry.u0, entry.u1, entry.u1, entry.u0};
25
- const float v[4] = {entry.v0, entry.v0, entry.v1, entry.v1};
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 = corners_x[i];
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 (scale != 1.0f) {
313
- xtgui::Font *scaled = xtgui::xt_text_scale(face, run.font.pixel_size * scale);
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
- float quad_x = pen_x + glyph.x_offset + (float)entry.left / scale;
319
- float quad_y = run.baseline + glyph.y_offset - (float)entry.top / scale;
320
- float quad_w = (float)entry.width / scale;
321
- float quad_h = (float)entry.height / scale;
322
- pushGlyph(verts, quad_x, quad_y, quad_w, quad_h, entry, run.color);
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;
@@ -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);