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.
Files changed (99) hide show
  1. package/README.md +59 -4
  2. package/README.zh-CN.md +52 -2
  3. package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
  4. package/dist/tests/e2e/gui-advanced.test.js +261 -0
  5. package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
  6. package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
  7. package/dist/tests/e2e/gui-canvas.test.js +233 -0
  8. package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
  9. package/dist/tests/e2e/gui-example.test.d.ts +1 -1
  10. package/dist/tests/e2e/gui-example.test.js +2 -2
  11. package/dist/tests/e2e/gui-helpers.d.ts +3 -0
  12. package/dist/tests/e2e/gui-helpers.js +5 -0
  13. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  14. package/dist/tests/e2e/gui-layout.test.js +73 -0
  15. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  16. package/dist/tests/e2e/gui-media.test.d.ts +8 -0
  17. package/dist/tests/e2e/gui-media.test.js +119 -0
  18. package/dist/tests/e2e/gui-media.test.js.map +1 -0
  19. package/dist/tests/e2e/gui-paint.test.js +40 -0
  20. package/dist/tests/e2e/gui-paint.test.js.map +1 -1
  21. package/doc/ai/extensions.md +16 -3
  22. package/doc/ai/zh-CN/extensions.md +10 -3
  23. package/doc/gui/animation.md +46 -0
  24. package/doc/gui/api.md +92 -0
  25. package/doc/gui/build.md +91 -0
  26. package/doc/gui/canvas.md +98 -0
  27. package/doc/gui/dom.md +123 -0
  28. package/doc/gui/input.md +22 -0
  29. package/doc/gui/known-issues.md +66 -0
  30. package/doc/gui/layout.md +72 -0
  31. package/doc/gui/media.md +87 -0
  32. package/doc/gui/rendering.md +98 -0
  33. package/doc/gui/runtime.md +88 -0
  34. package/doc/gui-scripts.md +7 -3
  35. package/doc/gui.md +80 -612
  36. package/doc/implemented.md +1 -0
  37. package/doc/zh-CN/gui/animation.md +46 -0
  38. package/doc/zh-CN/gui/api.md +88 -0
  39. package/doc/zh-CN/gui/build.md +84 -0
  40. package/doc/zh-CN/gui/canvas.md +80 -0
  41. package/doc/zh-CN/gui/dom.md +114 -0
  42. package/doc/zh-CN/gui/input.md +21 -0
  43. package/doc/zh-CN/gui/known-issues.md +50 -0
  44. package/doc/zh-CN/gui/layout.md +60 -0
  45. package/doc/zh-CN/gui/media.md +71 -0
  46. package/doc/zh-CN/gui/rendering.md +81 -0
  47. package/doc/zh-CN/gui/runtime.md +76 -0
  48. package/doc/zh-CN/gui-scripts.md +6 -2
  49. package/doc/zh-CN/gui.md +64 -532
  50. package/doc/zh-CN/implemented.md +1 -0
  51. package/llms.txt +15 -2
  52. package/package.json +1 -1
  53. package/runtime/ext_gui/canvas.cpp +580 -0
  54. package/runtime/ext_gui/canvas.h +170 -0
  55. package/runtime/ext_gui/canvas_internal.h +59 -0
  56. package/runtime/ext_gui/canvas_raster.cpp +254 -0
  57. package/runtime/ext_gui/document.cpp +24 -1
  58. package/runtime/ext_gui/document.h +11 -0
  59. package/runtime/ext_gui/dom_api.cpp +3 -0
  60. package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
  61. package/runtime/ext_gui/dom_api_internal.h +73 -0
  62. package/runtime/ext_gui/dom_api_media.cpp +274 -0
  63. package/runtime/ext_gui/dom_api_proto.cpp +80 -0
  64. package/runtime/ext_gui/gui.cpp +6 -0
  65. package/runtime/ext_gui/gui_canvas.cpp +95 -0
  66. package/runtime/ext_gui/gui_engine.h +49 -0
  67. package/runtime/ext_gui/gui_events.cpp +21 -5
  68. package/runtime/ext_gui/gui_media.cpp +193 -0
  69. package/runtime/ext_gui/image.cpp +31 -12
  70. package/runtime/ext_gui/image.h +20 -7
  71. package/runtime/ext_gui/layout.cpp +52 -5
  72. package/runtime/ext_gui/layout.h +33 -1
  73. package/runtime/ext_gui/layout_flex.cpp +263 -99
  74. package/runtime/ext_gui/layout_flow.cpp +85 -9
  75. package/runtime/ext_gui/layout_flow_internal.h +84 -5
  76. package/runtime/ext_gui/layout_inline.cpp +54 -37
  77. package/runtime/ext_gui/media.cpp +514 -0
  78. package/runtime/ext_gui/media.h +104 -0
  79. package/runtime/ext_gui/nanosvg.h +3278 -0
  80. package/runtime/ext_gui/nanosvgrast.h +1472 -0
  81. package/runtime/ext_gui/paint.cpp +123 -38
  82. package/runtime/ext_gui/paint.h +36 -6
  83. package/runtime/ext_gui/renderer.cpp +113 -10
  84. package/runtime/ext_gui/renderer.h +49 -9
  85. package/runtime/ext_gui/renderer_image.cpp +203 -77
  86. package/runtime/ext_gui/renderer_internal.h +6 -8
  87. package/runtime/ext_gui/renderer_text.cpp +11 -26
  88. package/runtime/ext_gui/style.h +11 -0
  89. package/runtime/ext_gui/style_print.cpp +11 -0
  90. package/runtime/ext_gui/style_ua.cpp +7 -0
  91. package/runtime/ext_gui/style_values.cpp +40 -1
  92. package/runtime/ext_gui/svg.cpp +182 -0
  93. package/runtime/ext_gui/svg.h +46 -0
  94. package/runtime/ext_gui/text.cpp +156 -25
  95. package/runtime/ext_gui/text.h +11 -2
  96. package/runtime/ext_gui/window.cpp +4 -1
  97. package/scripts/build-gui.ts +148 -6
  98. package/scripts/check-file-length.ts +5 -1
  99. package/scripts/install-gui-deps.sh +28 -0
@@ -1,8 +1,9 @@
1
1
  /*
2
2
  * xbintsc GUI engine — flex formatting context.
3
3
  *
4
- * Sizes flex items along the main axis (grow/shrink/basis), resolves the cross
5
- * axis (align-items) and places them (justify-content), reusing the block and
4
+ * Sizes flex items along the main axis (grow/shrink/basis), wraps them into
5
+ * lines when `flex-wrap` allows it, resolves the cross axis (align-items /
6
+ * align-content) and places them (justify-content), reusing the block and
6
7
  * inline helpers from `layout_flow.cpp` / `layout_inline.cpp`.
7
8
  */
8
9
 
@@ -11,151 +12,314 @@
11
12
  namespace xtgui {
12
13
  namespace layout_flow_detail {
13
14
 
15
+ /** Partition `basis` (outer main sizes) into lines, breaking before an item
16
+ * that no longer fits on the current line. A single line is produced when
17
+ * `wrap` is false. */
18
+ static std::vector<std::vector<size_t>> breakIntoLines(const std::vector<float> &basis,
19
+ float mainSize, float gap, bool wrap) {
20
+ std::vector<std::vector<size_t>> lines;
21
+ std::vector<size_t> current;
22
+ float used = 0;
23
+ for (size_t i = 0; i < basis.size(); i++) {
24
+ float add = basis[i] + (current.empty() ? 0.0f : gap);
25
+ if (wrap && !current.empty() && used + add > mainSize + 0.001f) {
26
+ lines.push_back(current);
27
+ current.clear();
28
+ used = 0;
29
+ add = basis[i];
30
+ }
31
+ current.push_back(i);
32
+ used += add;
33
+ }
34
+ if (!current.empty()) lines.push_back(current);
35
+ return lines;
36
+ }
37
+
14
38
  float Layouter::layoutFlex(LayoutBox *container, float contentWidth, float definiteHeight) const {
15
39
  const XtStyle &containerStyle = *container->style;
16
40
  bool isRow = containerStyle.flex_direction == FlexDirection::Row ||
17
41
  containerStyle.flex_direction == FlexDirection::RowReverse;
18
- bool reverse = containerStyle.flex_direction == FlexDirection::RowReverse ||
19
- containerStyle.flex_direction == FlexDirection::ColumnReverse;
20
- float gap = px(containerStyle.gap, contentWidth, containerStyle.font_size, 0.0f);
21
42
 
22
43
  /* An absolutely positioned child is out of flow: it takes no part in the flex
23
44
  * algorithm and is placed against the container's padding box instead. */
24
45
  std::vector<LayoutBox *> items;
25
46
  for (const std::unique_ptr<LayoutBox> &child : container->children) {
26
47
  if (child->display == Display::None || isAbsoluteChild(child.get())) continue;
48
+ /* Whitespace-only text (source indentation) does not become a flex item. */
49
+ if (isCollapsibleText(child.get())) continue;
27
50
  items.push_back(child.get());
28
51
  }
29
52
  if (items.empty()) return 0;
53
+
54
+ float mainGap =
55
+ px(isRow ? containerStyle.column_gap : containerStyle.row_gap, contentWidth,
56
+ containerStyle.font_size, 0.0f);
57
+ float crossGap =
58
+ px(isRow ? containerStyle.row_gap : containerStyle.column_gap, contentWidth,
59
+ containerStyle.font_size, 0.0f);
60
+ if (isRow)
61
+ return layoutFlexRow(container, items, contentWidth, definiteHeight, mainGap, crossGap);
62
+ return layoutFlexColumn(container, items, contentWidth, definiteHeight, mainGap, crossGap);
63
+ }
64
+
65
+ float Layouter::layoutFlexRow(LayoutBox *container, const std::vector<LayoutBox *> &items,
66
+ float contentWidth, float definiteHeight, float mainGap,
67
+ float crossGap) const {
68
+ const XtStyle &containerStyle = *container->style;
69
+ bool reverse = containerStyle.flex_direction == FlexDirection::RowReverse;
70
+ bool wrap = containerStyle.flex_wrap != FlexWrap::NoWrap;
71
+ bool wrapReverse = containerStyle.flex_wrap == FlexWrap::WrapReverse;
30
72
  size_t count = items.size();
73
+ float mainSize = contentWidth;
31
74
 
32
- if (isRow) {
33
- float mainSize = contentWidth;
34
- std::vector<float> outer(count, 0.0f);
35
- std::vector<float> mainSizes(count, 0.0f);
36
- float sumBase = 0;
37
- for (size_t i = 0; i < count; i++) {
38
- const XtStyle &style = *items[i]->style;
39
- float basis;
40
- if (isAuto(style.flex_basis)) {
41
- if (!isAuto(style.width)) basis = px(style.width, mainSize, style.font_size, 0.0f);
42
- else basis = preferredContentWidth(items[i], mainSize);
43
- } else {
44
- basis = px(style.flex_basis, mainSize, style.font_size, 0.0f);
45
- }
46
- resolveEdges(items[i], mainSize);
47
- float edges = horizontalEdges(items[i]);
48
- outer[i] = basis + edges;
49
- mainSizes[i] = outer[i];
50
- sumBase += outer[i];
75
+ /* Main-axis basis: flex-basis, then width, then preferred content width. */
76
+ std::vector<float> basis(count, 0.0f);
77
+ for (size_t i = 0; i < count; i++) {
78
+ const XtStyle &style = *items[i]->style;
79
+ float base;
80
+ if (isAuto(style.flex_basis)) {
81
+ if (!isAuto(style.width)) base = px(style.width, mainSize, style.font_size, 0.0f);
82
+ else base = preferredContentWidth(items[i], mainSize);
83
+ } else {
84
+ base = px(style.flex_basis, mainSize, style.font_size, 0.0f);
51
85
  }
52
- float free = mainSize - sumBase - gap * (float)(count - 1);
86
+ resolveEdges(items[i], mainSize);
87
+ basis[i] = base + horizontalEdges(items[i]);
88
+ }
89
+
90
+ std::vector<std::vector<size_t>> lines = breakIntoLines(basis, mainSize, mainGap, wrap);
91
+ std::vector<float> mainSizes(count, 0.0f);
92
+ std::vector<float> cross(count, 0.0f);
93
+ std::vector<float> lineCross(lines.size(), 0.0f);
94
+
95
+ for (size_t li = 0; li < lines.size(); li++) {
96
+ const std::vector<size_t> &line = lines[li];
97
+ float sumBase = 0;
98
+ for (size_t i : line) sumBase += basis[i];
99
+ for (size_t i : line) mainSizes[i] = basis[i];
100
+ float free = mainSize - sumBase - mainGap * (float)(line.size() - 1);
53
101
  if (free > 0) {
54
102
  float totalGrow = 0;
55
- for (LayoutBox *item : items) totalGrow += item->style->flex_grow;
56
- if (totalGrow > 0) {
57
- for (size_t i = 0; i < count; i++)
58
- mainSizes[i] += free * items[i]->style->flex_grow / totalGrow;
59
- }
103
+ for (size_t i : line) totalGrow += items[i]->style->flex_grow;
104
+ if (totalGrow > 0)
105
+ for (size_t i : line) mainSizes[i] += free * items[i]->style->flex_grow / totalGrow;
60
106
  } else if (free < 0) {
61
107
  float totalShrink = 0;
62
- for (size_t i = 0; i < count; i++) totalShrink += items[i]->style->flex_shrink * outer[i];
63
- if (totalShrink > 0) {
64
- for (size_t i = 0; i < count; i++) {
65
- mainSizes[i] += free * (items[i]->style->flex_shrink * outer[i]) / totalShrink;
66
- mainSizes[i] = std::max(0.0f, mainSizes[i]);
108
+ for (size_t i : line) totalShrink += items[i]->style->flex_shrink * basis[i];
109
+ if (totalShrink > 0)
110
+ for (size_t i : line) {
111
+ mainSizes[i] += free * (items[i]->style->flex_shrink * basis[i]) / totalShrink;
112
+ if (mainSizes[i] < 0) mainSizes[i] = 0;
67
113
  }
68
- }
69
114
  }
70
-
71
- /* Cross sizes: lay out once to measure, then apply cross alignment. */
72
- float crossSize = definiteHeight >= 0 ? definiteHeight : 0;
73
- std::vector<float> cross(count, 0.0f);
74
- for (size_t i = 0; i < count; i++) {
115
+ /* Measure each item's cross size by laying it out at its used main size. */
116
+ for (size_t i : line) {
75
117
  float contentW = mainSizes[i] - horizontalEdges(items[i]);
76
118
  if (contentW < 0) contentW = 0;
77
119
  setContainingBlock(items[i], containingBlockAncestor(items[i]));
78
- layoutBlock(items[i], container->content_x, container->content_y, contentWidth,
79
- definiteHeight, contentW);
120
+ layoutBlock(items[i], container->content_x, container->content_y, contentWidth, definiteHeight,
121
+ contentW);
80
122
  cross[i] = verticalEdges(items[i]) + items[i]->height;
81
- if (definiteHeight < 0) crossSize = std::max(crossSize, cross[i]);
123
+ if (cross[i] > lineCross[li]) lineCross[li] = cross[i];
82
124
  }
83
- if (containerStyle.align_items == AlignItems::Stretch && definiteHeight >= 0) {
84
- for (size_t i = 0; i < count; i++) {
85
- if (isAuto(items[i]->style->height)) {
86
- float contentH = crossSize - verticalEdges(items[i]);
87
- if (contentH < 0) contentH = 0;
88
- float contentW = mainSizes[i] - horizontalEdges(items[i]);
89
- layoutBlock(items[i], container->content_x, container->content_y, contentWidth,
90
- definiteHeight, contentW, contentH);
91
- }
92
- }
125
+ }
126
+
127
+ /* Cross-axis sizing and align-content. A single line takes the container's
128
+ * definite cross size (or its content size); multiple lines share any extra
129
+ * space according to align-content. */
130
+ float crossContainer;
131
+ float lineOffset = 0.0f;
132
+ float lineStep = crossGap;
133
+ if (lines.size() == 1) {
134
+ crossContainer = definiteHeight >= 0 ? definiteHeight : lineCross[0];
135
+ lineCross[0] = crossContainer;
136
+ } else {
137
+ float total = crossGap * (float)(lines.size() - 1);
138
+ for (float value : lineCross) total += value;
139
+ crossContainer = definiteHeight >= 0 ? definiteHeight : total;
140
+ float remaining = crossContainer - total;
141
+ if (remaining < 0) remaining = 0;
142
+ switch (containerStyle.align_content) {
143
+ case AlignContent::Stretch:
144
+ for (float &value : lineCross) value += remaining / (float)lines.size();
145
+ break;
146
+ case AlignContent::FlexEnd:
147
+ lineOffset = remaining;
148
+ break;
149
+ case AlignContent::Center:
150
+ lineOffset = remaining / 2.0f;
151
+ break;
152
+ case AlignContent::SpaceBetween:
153
+ if (lines.size() > 1) lineStep += remaining / (float)(lines.size() - 1);
154
+ break;
155
+ case AlignContent::SpaceAround:
156
+ lineStep += remaining / (float)lines.size();
157
+ lineOffset = remaining / (2.0f * (float)lines.size());
158
+ break;
159
+ default:
160
+ break;
93
161
  }
162
+ }
94
163
 
95
- /* justify-content: place the main axis. */
96
- float freeSpace = mainSize - (sumOf(mainSizes) + gap * (float)(count - 1));
164
+ for (size_t li = 0; li < lines.size(); li++) {
165
+ const std::vector<size_t> &line = lines[li];
166
+ float lineStart = wrapReverse ? crossContainer - lineOffset - lineCross[li] : lineOffset;
167
+ float sumMain = 0;
168
+ for (size_t i : line) sumMain += mainSizes[i];
169
+ float freeSpace = mainSize - (sumMain + mainGap * (float)(line.size() - 1));
97
170
  float offset = 0;
98
- float between = gap;
99
- applyJustify(containerStyle.justify_content, freeSpace, count, &offset, &between);
100
-
171
+ float between = mainGap;
172
+ applyJustify(containerStyle.justify_content, freeSpace, line.size(), &offset, &between);
101
173
  float cursor = container->content_x + offset;
102
- for (size_t k = 0; k < count; k++) {
103
- size_t i = reverse ? (count - 1 - k) : k;
104
- float crossOffset = crossOffsetFor(containerStyle.align_items, crossSize, cross[i]);
174
+ for (size_t k = 0; k < line.size(); k++) {
175
+ size_t i = reverse ? line[line.size() - 1 - k] : line[k];
176
+ bool stretch = containerStyle.align_items == AlignItems::Stretch &&
177
+ isAuto(items[i]->style->height) &&
178
+ (definiteHeight >= 0 || lines.size() > 1);
179
+ if (stretch) {
180
+ float contentH = lineCross[li] - verticalEdges(items[i]);
181
+ if (contentH < 0) contentH = 0;
182
+ float contentW = mainSizes[i] - horizontalEdges(items[i]);
183
+ layoutBlock(items[i], container->content_x, container->content_y, contentWidth,
184
+ definiteHeight, contentW, contentH);
185
+ cross[i] = lineCross[li];
186
+ }
187
+ float crossOffset = crossOffsetFor(containerStyle.align_items, lineCross[li], cross[i]);
105
188
  float dx = cursor + items[i]->margin_left - items[i]->x;
106
- float dy = container->content_y + crossOffset + items[i]->margin_top - items[i]->y;
189
+ float dy =
190
+ container->content_y + lineStart + crossOffset + items[i]->margin_top - items[i]->y;
107
191
  translate(items[i], dx, dy);
108
192
  cursor += mainSizes[i] + between;
109
193
  }
110
- return crossSize;
194
+ lineOffset += lineCross[li] + lineStep;
111
195
  }
196
+ return crossContainer;
197
+ }
112
198
 
113
- /* Column flex. */
114
- float mainSize = definiteHeight >= 0 ? definiteHeight : -1;
199
+ float Layouter::layoutFlexColumn(LayoutBox *container, const std::vector<LayoutBox *> &items,
200
+ float contentWidth, float definiteHeight, float mainGap,
201
+ float crossGap) const {
202
+ const XtStyle &containerStyle = *container->style;
203
+ bool reverse = containerStyle.flex_direction == FlexDirection::ColumnReverse;
204
+ bool wrap = containerStyle.flex_wrap != FlexWrap::NoWrap && definiteHeight >= 0;
205
+ bool wrapReverse = containerStyle.flex_wrap == FlexWrap::WrapReverse;
206
+ size_t count = items.size();
207
+
208
+ /* Column items have to be laid out to learn their main (height) size. */
115
209
  std::vector<float> outer(count, 0.0f);
116
- float sumBase = 0;
117
210
  for (size_t i = 0; i < count; i++) {
118
211
  setContainingBlock(items[i], containingBlockAncestor(items[i]));
119
212
  layoutBlock(items[i], container->content_x, container->content_y, contentWidth, definiteHeight);
120
213
  outer[i] = verticalEdges(items[i]) + items[i]->height;
121
- sumBase += outer[i];
122
214
  }
123
- std::vector<float> mainSizes = outer;
124
- if (mainSize >= 0) {
125
- float free = mainSize - sumBase - gap * (float)(count - 1);
126
- if (free > 0) {
127
- float totalGrow = 0;
128
- for (LayoutBox *item : items) totalGrow += item->style->flex_grow;
129
- if (totalGrow > 0) {
130
- for (size_t i = 0; i < count; i++) {
131
- mainSizes[i] += free * items[i]->style->flex_grow / totalGrow;
132
- float contentH = mainSizes[i] - verticalEdges(items[i]);
133
- if (contentH < 0) contentH = 0;
134
- layoutBlock(items[i], container->content_x, container->content_y, contentWidth,
135
- definiteHeight, -1, contentH);
215
+
216
+ if (!wrap) {
217
+ float mainSize = definiteHeight >= 0 ? definiteHeight : -1;
218
+ std::vector<float> mainSizes = outer;
219
+ if (mainSize >= 0) {
220
+ float free = mainSize - sumOf(outer) - mainGap * (float)(count - 1);
221
+ if (free > 0) {
222
+ float totalGrow = 0;
223
+ for (LayoutBox *item : items) totalGrow += item->style->flex_grow;
224
+ if (totalGrow > 0) {
225
+ for (size_t i = 0; i < count; i++) {
226
+ mainSizes[i] += free * items[i]->style->flex_grow / totalGrow;
227
+ float contentH = mainSizes[i] - verticalEdges(items[i]);
228
+ if (contentH < 0) contentH = 0;
229
+ layoutBlock(items[i], container->content_x, container->content_y, contentWidth,
230
+ definiteHeight, -1, contentH);
231
+ }
136
232
  }
137
233
  }
234
+ } else {
235
+ mainSize = sumOf(outer) + mainGap * (float)(count - 1);
138
236
  }
139
- } else {
140
- mainSize = sumBase + gap * (float)(count - 1);
237
+ float freeSpace = mainSize - (sumOf(mainSizes) + mainGap * (float)(count - 1));
238
+ float offset = 0;
239
+ float between = mainGap;
240
+ applyJustify(containerStyle.justify_content, freeSpace, count, &offset, &between);
241
+ float cursor = container->content_y + offset;
242
+ for (size_t k = 0; k < count; k++) {
243
+ size_t i = reverse ? (count - 1 - k) : k;
244
+ float dx = container->content_x + items[i]->margin_left - items[i]->x;
245
+ float dy = cursor + items[i]->margin_top - items[i]->y;
246
+ translate(items[i], dx, dy);
247
+ cursor += mainSizes[i] + between;
248
+ }
249
+ return mainSize;
250
+ }
251
+
252
+ /* Wrapped columns: lines run top-to-bottom and are placed side by side. */
253
+ std::vector<std::vector<size_t>> lines = breakIntoLines(outer, definiteHeight, mainGap, true);
254
+ std::vector<float> mainSizes(count, 0.0f);
255
+ std::vector<float> lineCross(lines.size(), 0.0f);
256
+ for (size_t li = 0; li < lines.size(); li++) {
257
+ const std::vector<size_t> &line = lines[li];
258
+ float sumBase = 0;
259
+ for (size_t i : line) sumBase += outer[i];
260
+ for (size_t i : line) mainSizes[i] = outer[i];
261
+ float free = definiteHeight - sumBase - mainGap * (float)(line.size() - 1);
262
+ if (free > 0) {
263
+ float totalGrow = 0;
264
+ for (size_t i : line) totalGrow += items[i]->style->flex_grow;
265
+ if (totalGrow > 0)
266
+ for (size_t i : line) mainSizes[i] += free * items[i]->style->flex_grow / totalGrow;
267
+ }
268
+ for (size_t i : line) {
269
+ float contentH = mainSizes[i] - verticalEdges(items[i]);
270
+ if (contentH < 0) contentH = 0;
271
+ layoutBlock(items[i], container->content_x, container->content_y, contentWidth,
272
+ definiteHeight, -1, contentH);
273
+ float crossSize = horizontalEdges(items[i]) + items[i]->width;
274
+ if (crossSize > lineCross[li]) lineCross[li] = crossSize;
275
+ }
276
+ }
277
+ float total = crossGap * (float)(lines.size() - 1);
278
+ for (float value : lineCross) total += value;
279
+ float lineOffset = 0.0f;
280
+ float lineStep = crossGap;
281
+ float remaining = contentWidth - total;
282
+ if (remaining < 0) remaining = 0;
283
+ switch (containerStyle.align_content) {
284
+ case AlignContent::Stretch:
285
+ for (float &value : lineCross) value += remaining / (float)lines.size();
286
+ break;
287
+ case AlignContent::FlexEnd:
288
+ lineOffset = remaining;
289
+ break;
290
+ case AlignContent::Center:
291
+ lineOffset = remaining / 2.0f;
292
+ break;
293
+ case AlignContent::SpaceBetween:
294
+ if (lines.size() > 1) lineStep += remaining / (float)(lines.size() - 1);
295
+ break;
296
+ case AlignContent::SpaceAround:
297
+ lineStep += remaining / (float)lines.size();
298
+ lineOffset = remaining / (2.0f * (float)lines.size());
299
+ break;
300
+ default:
301
+ break;
141
302
  }
142
- float freeSpace = mainSize - (sumOf(mainSizes) + gap * (float)(count - 1));
143
- float offset = 0;
144
- float between = gap;
145
- applyJustify(containerStyle.justify_content, freeSpace, count, &offset, &between);
146
-
147
- float cursor = container->content_y + offset;
148
- for (size_t k = 0; k < count; k++) {
149
- size_t i = reverse ? (count - 1 - k) : k;
150
- float dx = container->content_x + items[i]->margin_left - items[i]->x;
151
- /* Margins keep the item's outer box on the main axis, exactly like block
152
- * flow — a negative `margin-top` pulls the item up instead of being
153
- * silently dropped. */
154
- float dy = cursor + items[i]->margin_top - items[i]->y;
155
- translate(items[i], dx, dy);
156
- cursor += mainSizes[i] + between;
303
+ for (size_t li = 0; li < lines.size(); li++) {
304
+ const std::vector<size_t> &line = lines[li];
305
+ float lineStart = wrapReverse ? contentWidth - lineOffset - lineCross[li] : lineOffset;
306
+ float sumMain = 0;
307
+ for (size_t i : line) sumMain += mainSizes[i];
308
+ float freeSpace = definiteHeight - (sumMain + mainGap * (float)(line.size() - 1));
309
+ float offset = 0;
310
+ float between = mainGap;
311
+ applyJustify(containerStyle.justify_content, freeSpace, line.size(), &offset, &between);
312
+ float cursor = container->content_y + offset;
313
+ for (size_t k = 0; k < line.size(); k++) {
314
+ size_t i = reverse ? line[line.size() - 1 - k] : line[k];
315
+ float dx = container->content_x + lineStart + items[i]->margin_left - items[i]->x;
316
+ float dy = cursor + items[i]->margin_top - items[i]->y;
317
+ translate(items[i], dx, dy);
318
+ cursor += mainSizes[i] + between;
319
+ }
320
+ lineOffset += lineCross[li] + lineStep;
157
321
  }
158
- return mainSize;
322
+ return contentWidth;
159
323
  }
160
324
 
161
325
  float Layouter::sumOf(const std::vector<float> &values) {
@@ -44,7 +44,7 @@ float Layouter::verticalEdges(const LayoutBox *box) const {
44
44
  float Layouter::preferredContentWidth(const LayoutBox *box, float available) const {
45
45
  const XtStyle &style = *box->style;
46
46
  if (!isAuto(style.width)) return px(style.width, available, style.font_size, 0.0f);
47
- if (box->is_image && box->intrinsic_width > 0) return box->intrinsic_width;
47
+ if ((box->is_image || box->is_canvas) && box->intrinsic_width > 0) return box->intrinsic_width;
48
48
  float best = 0;
49
49
  for (const std::unique_ptr<LayoutBox> &child : box->children) {
50
50
  if (child->display == Display::None) continue;
@@ -201,7 +201,7 @@ float Layouter::layoutBlock(LayoutBox *box, float x, float y, float availableWid
201
201
  if (contentWidth < 0) contentWidth = 0;
202
202
 
203
203
  box->x = x + box->margin_left;
204
- box->y = y + box->margin_top;
204
+ box->y = y;
205
205
  box->content_x = box->x + box->border_left + box->padding_left;
206
206
  box->content_y = box->y + box->border_top + box->padding_top;
207
207
  box->content_width = contentWidth;
@@ -225,8 +225,8 @@ float Layouter::layoutBlock(LayoutBox *box, float x, float y, float availableWid
225
225
  if (contentHeight < 0) contentHeight = 0;
226
226
  }
227
227
  /* Replaced elements keep their aspect ratio when only one axis is set. */
228
- if (box->is_image && forcedContentHeight < 0 && isAuto(style.height) && box->intrinsic_width > 0 &&
229
- box->intrinsic_height > 0) {
228
+ if ((box->is_image || box->is_canvas) && forcedContentHeight < 0 && isAuto(style.height) &&
229
+ box->intrinsic_width > 0 && box->intrinsic_height > 0) {
230
230
  contentHeight = contentWidth * box->intrinsic_height / box->intrinsic_width;
231
231
  }
232
232
  box->content_height = contentHeight;
@@ -239,9 +239,10 @@ float Layouter::layoutBlock(LayoutBox *box, float x, float y, float availableWid
239
239
  publishContainingBlock(box);
240
240
  layoutAbsoluteChildren(box, contentWidth, availableHeight);
241
241
  applyPosition(box);
242
- /* An absolutely positioned box consumes no space in its parent's flow. */
243
- if (!inFlow) return 0;
244
- return box->margin_top + box->height + box->margin_bottom;
242
+ /* The caller owns the vertical margins: it collapses them and advances the
243
+ * flow itself, so the border-box height is what comes back. */
244
+ (void)inFlow;
245
+ return box->height;
245
246
  }
246
247
 
247
248
  bool Layouter::isAbsoluteChild(const LayoutBox *box) {
@@ -266,11 +267,17 @@ float Layouter::layoutChildren(LayoutBox *block, float contentWidth, float avail
266
267
 
267
268
  float y = block->content_y;
268
269
  float startY = y;
270
+ /* The bottom margin of the last in-flow block, waiting to collapse with the
271
+ * next block's top margin. */
272
+ float pending = 0.0f;
273
+ bool hasBlock = false;
269
274
  std::vector<LayoutBox *> inlineRun;
275
+ FloatContext floats;
270
276
  auto flush = [&]() {
271
277
  if (inlineRun.empty()) return;
272
278
  /* Source whitespace between block boxes collapses away (HTML's model), so a
273
- * run that carries no visible content produces no line at all. */
279
+ * run that carries no visible content produces no line at all — and,
280
+ * crucially, does not break margin adjacency with the next block. */
274
281
  bool visible = false;
275
282
  for (LayoutBox *box : inlineRun) {
276
283
  if (!isCollapsibleText(box)) {
@@ -278,8 +285,20 @@ float Layouter::layoutChildren(LayoutBox *block, float contentWidth, float avail
278
285
  break;
279
286
  }
280
287
  }
281
- if (visible) y += layoutInlineRun(block, inlineRun, contentWidth, y);
288
+ if (!visible) {
289
+ /* Source whitespace only: no line, and it does not break margin
290
+ * adjacency with the next block. */
291
+ inlineRun.clear();
292
+ return;
293
+ }
294
+ /* Inline content breaks adjacency: apply the pending margin here instead of
295
+ * letting it collapse into the next block. */
296
+ y += pending;
297
+ pending = 0.0f;
298
+ floats.prune(y);
299
+ y += layoutInlineRun(block, inlineRun, contentWidth, y, &floats);
282
300
  inlineRun.clear();
301
+ hasBlock = true;
283
302
  };
284
303
  for (const std::unique_ptr<LayoutBox> &child : block->children) {
285
304
  Display display = child->display;
@@ -289,16 +308,73 @@ float Layouter::layoutChildren(LayoutBox *block, float contentWidth, float avail
289
308
  * its own size. */
290
309
  continue;
291
310
  }
311
+ const XtStyle *childStyle = child->style;
312
+ if (childStyle != nullptr && childStyle->float_side != FloatSide::None) {
313
+ /* A float interrupts the inline run (content before it is placed first,
314
+ * then the float affects the lines that follow), and takes no flow space
315
+ * of its own. */
316
+ flush();
317
+ resolveEdges(child.get(), contentWidth);
318
+ float edges = horizontalEdges(child.get());
319
+ float available = contentWidth - edges;
320
+ if (available < 0) available = 0;
321
+ float outerWidth;
322
+ if (!isAuto(childStyle->width)) {
323
+ outerWidth = px(childStyle->width, contentWidth, childStyle->font_size, 0.0f) + edges;
324
+ } else {
325
+ outerWidth = preferredContentWidth(child.get(), contentWidth);
326
+ if (outerWidth > available) outerWidth = available;
327
+ outerWidth += edges;
328
+ }
329
+ float contentW = outerWidth - edges;
330
+ if (contentW < 0) contentW = 0;
331
+ float floatX = childStyle->float_side == FloatSide::Left
332
+ ? block->content_x
333
+ : block->content_x + contentWidth - outerWidth;
334
+ setContainingBlock(child.get(), containingBlockAncestor(block));
335
+ float height =
336
+ layoutBlock(child.get(), floatX, y, contentWidth, availableHeight, contentW);
337
+ ActiveFloat entry;
338
+ entry.left = childStyle->float_side == FloatSide::Left;
339
+ entry.x = child->x;
340
+ entry.width = child->width + child->margin_left + child->margin_right;
341
+ entry.top = y;
342
+ entry.height = height + child->margin_top + child->margin_bottom;
343
+ floats.floats.push_back(entry);
344
+ continue;
345
+ }
292
346
  if (display == Display::Block || display == Display::ListItem || display == Display::Flex ||
293
347
  child->style == nullptr) {
294
348
  flush();
349
+ resolveEdges(child.get(), contentWidth);
350
+ /* Adjacent vertical margins collapse: the gap is the collapsed value of
351
+ * the waiting bottom margin and this child's top margin. */
352
+ float top = child->margin_top;
353
+ float gap = hasBlock ? collapseMargins(pending, top) : top;
354
+ float desired = y + gap;
355
+ if (childStyle != nullptr && childStyle->clear != Clear::None) {
356
+ bool clearLeft = childStyle->clear == Clear::Left || childStyle->clear == Clear::Both;
357
+ bool clearRight = childStyle->clear == Clear::Right || childStyle->clear == Clear::Both;
358
+ float clearY = floats.clearY(clearLeft, clearRight, y) + top;
359
+ if (desired < clearY) desired = clearY;
360
+ }
361
+ y = desired;
295
362
  setContainingBlock(child.get(), containingBlockAncestor(block));
296
363
  y += layoutBlock(child.get(), block->content_x, y, contentWidth, availableHeight);
364
+ pending = child->margin_bottom;
365
+ hasBlock = true;
297
366
  } else {
298
367
  inlineRun.push_back(child.get());
299
368
  }
300
369
  }
301
370
  flush();
371
+ /* The last bottom margin is part of the content box: it escapes only if the
372
+ * parent has no bottom padding/border (handled by the parent's caller). */
373
+ y += pending;
374
+ /* Grow to contain the floats: a block with only floated children would
375
+ * otherwise collapse to zero height. */
376
+ float floatBottom = floats.maxBottom();
377
+ if (floatBottom > y) y = floatBottom;
302
378
  return y - startY;
303
379
  }
304
380