xbintsc 0.5.31 → 0.5.32

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 (181) hide show
  1. package/README.md +16 -10
  2. package/dist/src/binder/references.js +14 -1
  3. package/dist/src/binder/references.js.map +1 -1
  4. package/dist/src/cli/help.d.ts +1 -1
  5. package/dist/src/codegen/generator/module.d.ts +1 -0
  6. package/dist/src/codegen/generator/module.js +54 -0
  7. package/dist/src/codegen/generator/module.js.map +1 -1
  8. package/dist/src/codegen/generator/statements/variables.js +6 -4
  9. package/dist/src/codegen/generator/statements/variables.js.map +1 -1
  10. package/dist/src/driver/version.d.ts +1 -1
  11. package/dist/src/driver/version.js +1 -1
  12. package/dist/src/extensions/gui/css.d.ts +26 -0
  13. package/dist/src/extensions/gui/css.js +100 -0
  14. package/dist/src/extensions/gui/css.js.map +1 -0
  15. package/dist/src/extensions/gui/html.js +9 -2
  16. package/dist/src/extensions/gui/html.js.map +1 -1
  17. package/dist/src/extensions/gui/index.js +16 -2
  18. package/dist/src/extensions/gui/index.js.map +1 -1
  19. package/dist/src/extensions/gui/three.d.ts +16 -0
  20. package/dist/src/extensions/gui/three.js +22 -0
  21. package/dist/src/extensions/gui/three.js.map +1 -0
  22. package/dist/src/extensions/media/index.d.ts +6 -5
  23. package/dist/src/extensions/media/index.js +12 -14
  24. package/dist/src/extensions/media/index.js.map +1 -1
  25. package/dist/tests/e2e/compile-builtins.test.js +24 -0
  26. package/dist/tests/e2e/compile-builtins.test.js.map +1 -1
  27. package/dist/tests/e2e/compile-objects-classes.test.js +23 -0
  28. package/dist/tests/e2e/compile-objects-classes.test.js.map +1 -1
  29. package/dist/tests/e2e/gui-browser.test.js +1 -1
  30. package/dist/tests/e2e/gui-browser.test.js.map +1 -1
  31. package/dist/tests/e2e/gui-effects.test.d.ts +6 -0
  32. package/dist/tests/e2e/gui-effects.test.js +262 -0
  33. package/dist/tests/e2e/gui-effects.test.js.map +1 -0
  34. package/dist/tests/e2e/gui-example-webgl.test.d.ts +10 -0
  35. package/dist/tests/e2e/gui-example-webgl.test.js +75 -0
  36. package/dist/tests/e2e/gui-example-webgl.test.js.map +1 -0
  37. package/dist/tests/e2e/gui-example-webgl2.test.d.ts +8 -0
  38. package/dist/tests/e2e/gui-example-webgl2.test.js +72 -0
  39. package/dist/tests/e2e/gui-example-webgl2.test.js.map +1 -0
  40. package/dist/tests/e2e/gui-layout.test.js +48 -0
  41. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  42. package/dist/tests/e2e/gui-media.test.js +5 -4
  43. package/dist/tests/e2e/gui-media.test.js.map +1 -1
  44. package/dist/tests/e2e/gui-webgl.test.d.ts +11 -0
  45. package/dist/tests/e2e/gui-webgl.test.js +179 -0
  46. package/dist/tests/e2e/gui-webgl.test.js.map +1 -0
  47. package/dist/tests/e2e/gui-webgl2.test.d.ts +10 -0
  48. package/dist/tests/e2e/gui-webgl2.test.js +238 -0
  49. package/dist/tests/e2e/gui-webgl2.test.js.map +1 -0
  50. package/dist/tests/extensions/gui.test.js +41 -0
  51. package/dist/tests/extensions/gui.test.js.map +1 -1
  52. package/dist/tests/extensions/media.test.js +7 -3
  53. package/dist/tests/extensions/media.test.js.map +1 -1
  54. package/doc/ai/extensions.md +29 -9
  55. package/doc/ai/troubleshooting.md +12 -8
  56. package/doc/ai/zh-CN/extensions.md +26 -8
  57. package/doc/ai/zh-CN/troubleshooting.md +10 -6
  58. package/doc/gui/api.md +3 -2
  59. package/doc/gui/build.md +72 -16
  60. package/doc/gui/canvas.md +11 -3
  61. package/doc/gui/dom.md +33 -18
  62. package/doc/gui/known-issues.md +57 -24
  63. package/doc/gui/layout.md +18 -0
  64. package/doc/gui/media.md +17 -13
  65. package/doc/gui/rendering.md +50 -3
  66. package/doc/gui/webgl.md +256 -0
  67. package/doc/gui.md +108 -17
  68. package/doc/npm-package.md +6 -4
  69. package/doc/requirements.md +18 -9
  70. package/doc/zh-CN/gui/api.md +3 -2
  71. package/doc/zh-CN/gui/build.md +64 -15
  72. package/doc/zh-CN/gui/canvas.md +9 -2
  73. package/doc/zh-CN/gui/dom.md +20 -7
  74. package/doc/zh-CN/gui/known-issues.md +45 -19
  75. package/doc/zh-CN/gui/layout.md +14 -0
  76. package/doc/zh-CN/gui/media.md +15 -11
  77. package/doc/zh-CN/gui/rendering.md +41 -4
  78. package/doc/zh-CN/gui/webgl.md +115 -0
  79. package/doc/zh-CN/gui.md +89 -14
  80. package/doc/zh-CN/npm-package.md +9 -6
  81. package/doc/zh-CN/requirements.md +12 -6
  82. package/llms.txt +2 -0
  83. package/package.json +1 -1
  84. package/runtime/ext_gui/browser.cpp +39 -13
  85. package/runtime/ext_gui/canvas.cpp +3 -18
  86. package/runtime/ext_gui/canvas_internal.h +10 -0
  87. package/runtime/ext_gui/canvas_raster.cpp +60 -0
  88. package/runtime/ext_gui/clip_values.cpp +246 -0
  89. package/runtime/ext_gui/css.cpp +94 -0
  90. package/runtime/ext_gui/css.h +22 -1
  91. package/runtime/ext_gui/css_match.cpp +3 -0
  92. package/runtime/ext_gui/css_selector.cpp +9 -0
  93. package/runtime/ext_gui/css_sheet.cpp +89 -45
  94. package/runtime/ext_gui/document.cpp +4 -1
  95. package/runtime/ext_gui/document.h +2 -0
  96. package/runtime/ext_gui/dom.h +16 -0
  97. package/runtime/ext_gui/dom_api.cpp +1 -0
  98. package/runtime/ext_gui/dom_api_canvas.cpp +17 -80
  99. package/runtime/ext_gui/dom_api_canvas_extra.cpp +130 -1
  100. package/runtime/ext_gui/dom_api_canvas_pending.cpp +41 -0
  101. package/runtime/ext_gui/dom_api_internal.h +259 -0
  102. package/runtime/ext_gui/dom_api_webgl.cpp +376 -0
  103. package/runtime/ext_gui/dom_api_webgl2.cpp +434 -0
  104. package/runtime/ext_gui/dom_api_webgl2_draw.cpp +269 -0
  105. package/runtime/ext_gui/dom_api_webgl_buffer.cpp +212 -0
  106. package/runtime/ext_gui/dom_api_webgl_rt.h +30 -0
  107. package/runtime/ext_gui/dom_api_webgl_shader.cpp +443 -0
  108. package/runtime/ext_gui/dom_api_webgl_state.cpp +444 -0
  109. package/runtime/ext_gui/dom_api_webgl_texture.cpp +445 -0
  110. package/runtime/ext_gui/effect_values.cpp +312 -0
  111. package/runtime/ext_gui/gradient_values.cpp +476 -0
  112. package/runtime/ext_gui/grid_values.cpp +230 -0
  113. package/runtime/ext_gui/gui_canvas.cpp +50 -2
  114. package/runtime/ext_gui/gui_engine.h +27 -0
  115. package/runtime/ext_gui/gui_events.cpp +4 -1
  116. package/runtime/ext_gui/gui_media.cpp +1 -1
  117. package/runtime/ext_gui/gui_webgl.cpp +83 -0
  118. package/runtime/ext_gui/layout.cpp +21 -0
  119. package/runtime/ext_gui/layout_flow.cpp +81 -4
  120. package/runtime/ext_gui/layout_flow_internal.h +31 -0
  121. package/runtime/ext_gui/layout_grid.cpp +374 -0
  122. package/runtime/ext_gui/media.h +1 -1
  123. package/runtime/ext_gui/media_api.h +2 -2
  124. package/runtime/ext_gui/media_query.cpp +211 -0
  125. package/runtime/ext_gui/paint.cpp +111 -77
  126. package/runtime/ext_gui/paint.h +33 -3
  127. package/runtime/ext_gui/paint_background.cpp +229 -0
  128. package/runtime/ext_gui/paint_clip.cpp +268 -0
  129. package/runtime/ext_gui/paint_effects.cpp +324 -0
  130. package/runtime/ext_gui/paint_internal.h +85 -0
  131. package/runtime/ext_gui/renderer.cpp +8 -31
  132. package/runtime/ext_gui/renderer_clip.cpp +196 -0
  133. package/runtime/ext_gui/renderer_image.cpp +13 -8
  134. package/runtime/ext_gui/renderer_internal.h +15 -8
  135. package/runtime/ext_gui/renderer_text.cpp +6 -31
  136. package/runtime/ext_gui/style.cpp +343 -20
  137. package/runtime/ext_gui/style.h +144 -3
  138. package/runtime/ext_gui/style_print.cpp +134 -1
  139. package/runtime/ext_gui/style_values.cpp +152 -126
  140. package/runtime/ext_gui/style_values_internal.h +35 -0
  141. package/runtime/ext_gui/webgl.h +466 -0
  142. package/runtime/ext_gui/webgl2_constants.h +274 -0
  143. package/runtime/ext_gui/webgl_buffer.cpp +565 -0
  144. package/runtime/ext_gui/webgl_constants.h +317 -0
  145. package/runtime/ext_gui/webgl_context.cpp +466 -0
  146. package/runtime/ext_gui/webgl_draw.cpp +495 -0
  147. package/runtime/ext_gui/webgl_glsl.h +125 -0
  148. package/runtime/ext_gui/webgl_glsl_compile.cpp +431 -0
  149. package/runtime/ext_gui/webgl_glsl_rewrite.cpp +349 -0
  150. package/runtime/ext_gui/webgl_glsl_scan.cpp +581 -0
  151. package/runtime/ext_gui/webgl_msl.cpp +62 -0
  152. package/runtime/ext_gui/webgl_msl.h +33 -0
  153. package/runtime/ext_gui/webgl_pipeline.cpp +274 -0
  154. package/runtime/ext_gui/webgl_program_internal.h +18 -0
  155. package/runtime/ext_gui/webgl_shader.cpp +582 -0
  156. package/runtime/ext_gui/webgl_texture.cpp +293 -0
  157. package/runtime/ext_gui/webgl_uniform.cpp +167 -0
  158. package/runtime/ext_gui/webgl_webgl2.cpp +224 -0
  159. package/runtime/ext_media/media.cpp +1 -1
  160. package/runtime/ext_media/media_bridge.c +9 -3
  161. package/runtime/rt_internal.h +1 -0
  162. package/runtime/xt_containers/access.inc +8 -0
  163. package/runtime/xt_regex/api.inc +2 -0
  164. package/runtime/xt_regex/matcher.inc +16 -3
  165. package/runtime/xt_regex/parser.inc +8 -2
  166. package/runtime/xt_regex/types.inc +3 -0
  167. package/runtime/xt_stdlib2/regexp-match.inc +60 -21
  168. package/runtime/xt_stdlib2/regexp.inc +1 -0
  169. package/runtime/xt_typed_array.c +1 -1
  170. package/scripts/build-gui.ts +90 -3
  171. package/scripts/build-media.ts +323 -58
  172. package/scripts/setup-vs-toolchain.ps1 +152 -0
  173. package/src/binder/references.ts +13 -1
  174. package/src/codegen/generator/module.ts +50 -0
  175. package/src/codegen/generator/statements/variables.ts +6 -3
  176. package/src/driver/version.ts +1 -1
  177. package/src/extensions/gui/css.ts +113 -0
  178. package/src/extensions/gui/html.ts +9 -2
  179. package/src/extensions/gui/index.ts +16 -2
  180. package/src/extensions/gui/three.ts +25 -0
  181. package/src/extensions/media/index.ts +13 -16
@@ -0,0 +1,246 @@
1
+ /*
2
+ * xbintsc GUI engine — `clip-path` value parsing.
3
+ *
4
+ * The geometry is resolved in `paint_clip.cpp`, where the element's border box
5
+ * and transform are known. Values here keep CSS lengths unresolved so `%`, em
6
+ * and viewport units survive the cascade.
7
+ */
8
+ #include <cctype>
9
+ #include <string>
10
+ #include <utility>
11
+ #include <vector>
12
+
13
+ #include "css_internal.h"
14
+ #include "style_internal.h"
15
+ #include "style_values_internal.h"
16
+
17
+ namespace xtgui {
18
+
19
+ namespace {
20
+
21
+ std::vector<std::string> splitTokens(const std::string &text) {
22
+ std::vector<std::string> parts;
23
+ std::string current;
24
+ int depth = 0;
25
+ for (char c : text) {
26
+ if (c == '(') depth++;
27
+ if (c == ')') depth--;
28
+ if ((c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == ',') && depth == 0) {
29
+ if (!current.empty()) {
30
+ parts.push_back(current);
31
+ current.clear();
32
+ }
33
+ } else {
34
+ current.push_back(c);
35
+ }
36
+ }
37
+ if (!current.empty()) parts.push_back(current);
38
+ return parts;
39
+ }
40
+
41
+ /** Split `text` on a top-level keyword (`at` / `round`), keeping both sides. */
42
+ bool splitKeyword(const std::string &text, const std::string &keyword, std::string *left,
43
+ std::string *right) {
44
+ std::string lower = xt_css_lower(text);
45
+ int depth = 0;
46
+ for (size_t i = 0; i + keyword.size() <= text.size(); i++) {
47
+ char c = text[i];
48
+ if (c == '(') depth++;
49
+ else if (c == ')') depth--;
50
+ if (depth != 0) continue;
51
+ if (lower.compare(i, keyword.size(), keyword) != 0) continue;
52
+ bool before = i == 0 || xt_is_css_space(text[i - 1]) || text[i - 1] == ',';
53
+ bool after = i + keyword.size() >= text.size() ||
54
+ xt_is_css_space(text[i + keyword.size()]) || text[i + keyword.size()] == ',';
55
+ if (!before || !after) continue;
56
+ *left = xt_css_trim(text.substr(0, i));
57
+ *right = xt_css_trim(text.substr(i + keyword.size()));
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ bool parsePositionToken(const std::string &raw, Length *out, bool *vertical) {
64
+ std::string text = xt_css_lower(xt_css_trim(raw));
65
+ if (text == "left") { *out = {0.0f, Unit::Percent}; *vertical = false; return true; }
66
+ if (text == "right") { *out = {100.0f, Unit::Percent}; *vertical = false; return true; }
67
+ if (text == "top") { *out = {0.0f, Unit::Percent}; *vertical = true; return true; }
68
+ if (text == "bottom") { *out = {100.0f, Unit::Percent}; *vertical = true; return true; }
69
+ if (text == "center") { *out = {50.0f, Unit::Percent}; *vertical = false; return true; }
70
+ Length length = xt_css_parse_length(text);
71
+ if (length.unit == Unit::Invalid || length.unit == Unit::Auto || length.unit == Unit::None)
72
+ return false;
73
+ *out = length;
74
+ *vertical = false;
75
+ return true;
76
+ }
77
+
78
+ bool parseCenter(const std::string &text, Length *x, Length *y) {
79
+ std::vector<std::string> tokens = splitTokens(text);
80
+ if (tokens.empty() || tokens.size() > 2) return false;
81
+ Length values[2];
82
+ bool vertical[2] = {false, false};
83
+ for (size_t i = 0; i < tokens.size(); i++) {
84
+ if (!parsePositionToken(tokens[i], &values[i], &vertical[i])) return false;
85
+ }
86
+ if (tokens.size() == 1) {
87
+ if (vertical[0]) {
88
+ *x = {50.0f, Unit::Percent};
89
+ *y = values[0];
90
+ } else {
91
+ *x = values[0];
92
+ *y = {50.0f, Unit::Percent};
93
+ }
94
+ return true;
95
+ }
96
+ if (vertical[0] && !vertical[1]) {
97
+ *x = values[1];
98
+ *y = values[0];
99
+ } else {
100
+ *x = values[0];
101
+ *y = values[1];
102
+ }
103
+ return true;
104
+ }
105
+
106
+ /** Parse the 1–4 side lengths of `inset(...)` and an optional round radius. */
107
+ bool parseInset(const std::string &body, ClipPathSpec *spec) {
108
+ std::string sides = body;
109
+ std::string round;
110
+ if (splitKeyword(body, "round", &sides, &round)) {
111
+ std::vector<std::string> radiusTokens = splitTokens(round);
112
+ if (!radiusTokens.empty()) {
113
+ Length radius = xt_css_parse_length(radiusTokens[0]);
114
+ if (radius.unit != Unit::Invalid && radius.unit != Unit::Auto &&
115
+ radius.unit != Unit::None)
116
+ spec->inset_radius = radius;
117
+ }
118
+ }
119
+ std::vector<std::string> tokens = splitTokens(sides);
120
+ if (tokens.empty() || tokens.size() > 4) return false;
121
+ Length values[4];
122
+ for (size_t i = 0; i < tokens.size(); i++) {
123
+ values[i] = xt_css_parse_length(tokens[i]);
124
+ if (values[i].unit == Unit::Invalid) return false;
125
+ }
126
+ switch (tokens.size()) {
127
+ case 1:
128
+ spec->inset_top = spec->inset_right = spec->inset_bottom = spec->inset_left = values[0];
129
+ break;
130
+ case 2:
131
+ spec->inset_top = spec->inset_bottom = values[0];
132
+ spec->inset_right = spec->inset_left = values[1];
133
+ break;
134
+ case 3:
135
+ spec->inset_top = values[0];
136
+ spec->inset_right = spec->inset_left = values[1];
137
+ spec->inset_bottom = values[2];
138
+ break;
139
+ default:
140
+ spec->inset_top = values[0];
141
+ spec->inset_right = values[1];
142
+ spec->inset_bottom = values[2];
143
+ spec->inset_left = values[3];
144
+ break;
145
+ }
146
+ spec->type = ClipPathSpec::Type::Inset;
147
+ return true;
148
+ }
149
+
150
+ bool parseCircle(const std::string &body, ClipPathSpec *spec) {
151
+ std::string radius;
152
+ std::string center;
153
+ if (splitKeyword(body, "at", &radius, &center)) {
154
+ if (!parseCenter(center, &spec->center_x, &spec->center_y)) return false;
155
+ } else {
156
+ radius = body;
157
+ }
158
+ std::vector<std::string> tokens = splitTokens(radius);
159
+ if (!tokens.empty()) {
160
+ Length length = xt_css_parse_length(tokens[0]);
161
+ if (length.unit == Unit::Invalid) return false;
162
+ spec->radius_x = length;
163
+ spec->radius_y = length;
164
+ spec->radius_explicit = true;
165
+ }
166
+ spec->type = ClipPathSpec::Type::Circle;
167
+ return true;
168
+ }
169
+
170
+ bool parseEllipse(const std::string &body, ClipPathSpec *spec) {
171
+ std::string radii = body;
172
+ std::string center;
173
+ splitKeyword(body, "at", &radii, &center);
174
+ if (!center.empty() && !parseCenter(center, &spec->center_x, &spec->center_y)) return false;
175
+ std::vector<std::string> tokens = splitTokens(radii);
176
+ if (!tokens.empty()) {
177
+ Length rx = xt_css_parse_length(tokens[0]);
178
+ Length ry = tokens.size() > 1 ? xt_css_parse_length(tokens[1]) : rx;
179
+ if (rx.unit == Unit::Invalid || ry.unit == Unit::Invalid) return false;
180
+ spec->radius_x = rx;
181
+ spec->radius_y = ry;
182
+ spec->radius_explicit = true;
183
+ }
184
+ spec->type = ClipPathSpec::Type::Ellipse;
185
+ return true;
186
+ }
187
+
188
+ bool parsePolygon(const std::string &body, ClipPathSpec *spec) {
189
+ std::string text = xt_css_trim(body);
190
+ std::string lower = xt_css_lower(text);
191
+ if (lower.rfind("nonzero", 0) == 0) text = xt_css_trim(text.substr(7));
192
+ else if (lower.rfind("evenodd", 0) == 0) text = xt_css_trim(text.substr(7));
193
+ /* Pairs are separated by commas in CSS, but whitespace is accepted too. */
194
+ std::vector<std::string> tokens = splitTokens(text);
195
+ if (tokens.size() < 3) return false;
196
+ std::vector<Length> points;
197
+ for (const std::string &token : tokens) {
198
+ Length length = xt_css_parse_length(token);
199
+ if (length.unit == Unit::Invalid) return false;
200
+ points.push_back(length);
201
+ }
202
+ if (points.size() < 3) return false;
203
+ if (points.size() % 2 != 0) points.pop_back();
204
+ spec->polygon = std::move(points);
205
+ spec->type = ClipPathSpec::Type::Polygon;
206
+ return true;
207
+ }
208
+
209
+ } // namespace
210
+
211
+ bool xt_style_apply_clip_path(XtStyle *style, const std::string &property,
212
+ const std::string &value) {
213
+ if (property != "clip-path") return false;
214
+ std::string text = xt_css_trim(value);
215
+ std::string lower = xt_css_lower(text);
216
+ ClipPathSpec spec;
217
+ if (lower == "none" || text.empty()) {
218
+ style->clip_path = spec;
219
+ return true;
220
+ }
221
+ if (text.size() < 8 || text.back() != ')') {
222
+ style->clip_path = spec;
223
+ return true;
224
+ }
225
+ if (lower.rfind("inset(", 0) == 0) {
226
+ if (parseInset(text.substr(6, text.size() - 7), &spec)) style->clip_path = spec;
227
+ return true;
228
+ }
229
+ if (lower.rfind("circle(", 0) == 0) {
230
+ if (parseCircle(text.substr(7, text.size() - 8), &spec)) style->clip_path = spec;
231
+ return true;
232
+ }
233
+ if (lower.rfind("ellipse(", 0) == 0) {
234
+ if (parseEllipse(text.substr(8, text.size() - 9), &spec)) style->clip_path = spec;
235
+ return true;
236
+ }
237
+ if (lower.rfind("polygon(", 0) == 0) {
238
+ if (parsePolygon(text.substr(8, text.size() - 9), &spec)) style->clip_path = spec;
239
+ return true;
240
+ }
241
+ /* `url(#…)`, `path()` and other shapes are not part of the subset. */
242
+ style->clip_path = spec;
243
+ return true;
244
+ }
245
+
246
+ } // namespace xtgui
@@ -45,6 +45,39 @@ std::vector<std::string> xt_css_split_whitespace(const std::string &text) {
45
45
  return parts;
46
46
  }
47
47
 
48
+ std::vector<std::string> xt_css_split_commas(const std::string &text) {
49
+ std::vector<std::string> parts;
50
+ std::string current;
51
+ int depth = 0;
52
+ char quote = 0;
53
+ for (char c : text) {
54
+ if (quote != 0) {
55
+ current.push_back(c);
56
+ if (c == quote) quote = 0;
57
+ continue;
58
+ }
59
+ if (c == '"' || c == '\'') {
60
+ quote = c;
61
+ current.push_back(c);
62
+ } else if (c == '(' || c == '[') {
63
+ depth++;
64
+ current.push_back(c);
65
+ } else if (c == ')' || c == ']') {
66
+ if (depth > 0) depth--;
67
+ current.push_back(c);
68
+ } else if (c == ',' && depth == 0) {
69
+ std::string part = xt_css_trim(current);
70
+ if (!part.empty()) parts.push_back(part);
71
+ current.clear();
72
+ } else {
73
+ current.push_back(c);
74
+ }
75
+ }
76
+ std::string part = xt_css_trim(current);
77
+ if (!part.empty()) parts.push_back(part);
78
+ return parts;
79
+ }
80
+
48
81
  /* -- numbers / lengths / colors ------------------------------------------- */
49
82
 
50
83
  bool xt_css_parse_number(const std::string &value, float *out) {
@@ -59,6 +92,60 @@ bool xt_css_parse_number(const std::string &value, float *out) {
59
92
  return true;
60
93
  }
61
94
 
95
+
96
+ /** Split a CSS function's argument list at top-level commas. */
97
+ static std::vector<std::string> splitFunctionArgs(const std::string &text) {
98
+ std::vector<std::string> args;
99
+ std::string current;
100
+ int depth = 0;
101
+ for (char c : text) {
102
+ if (c == '(') depth++;
103
+ if (c == ')') depth--;
104
+ if (c == ',' && depth == 0) {
105
+ args.push_back(xt_css_trim(current));
106
+ current.clear();
107
+ } else {
108
+ current.push_back(c);
109
+ }
110
+ }
111
+ if (!current.empty() || !args.empty()) args.push_back(xt_css_trim(current));
112
+ return args;
113
+ }
114
+
115
+ /** `clamp(min, value, max)`, `min(...)` and `max(...)` over lengths. The
116
+ * arguments stay unresolved; layout resolves them against the element's em and
117
+ * the viewport. `env(safe-area-inset-*)` has no notch information in a desktop
118
+ * window, so it resolves to zero (its fallback is ignored, matching the
119
+ * browser's value without insets). */
120
+ static Length parseFunctionLength(const std::string &name, const std::string &inner) {
121
+ Length result;
122
+ if (name == "env") {
123
+ result.unit = Unit::Px;
124
+ return result;
125
+ }
126
+ if (name != "clamp" && name != "min" && name != "max") {
127
+ result.unit = Unit::Invalid;
128
+ return result;
129
+ }
130
+ std::vector<std::string> args = splitFunctionArgs(inner);
131
+ size_t expected = name == "clamp" ? 3 : 1;
132
+ if (args.size() < expected) {
133
+ result.unit = Unit::Invalid;
134
+ return result;
135
+ }
136
+ result.unit = name == "clamp" ? Unit::Clamp : (name == "min" ? Unit::Min : Unit::Max);
137
+ for (const std::string &arg : args) {
138
+ Length value = xt_css_parse_length(arg);
139
+ if (value.unit == Unit::Invalid) {
140
+ result.unit = Unit::Invalid;
141
+ result.args.clear();
142
+ return result;
143
+ }
144
+ result.args.push_back(value);
145
+ }
146
+ return result;
147
+ }
148
+
62
149
  Length xt_css_parse_length(const std::string &value) {
63
150
  Length length;
64
151
  std::string text = xt_css_lower(xt_css_trim(value));
@@ -74,6 +161,13 @@ Length xt_css_parse_length(const std::string &value) {
74
161
  length.unit = Unit::None;
75
162
  return length;
76
163
  }
164
+ size_t open = text.find('(');
165
+ if (open != std::string::npos && !text.empty() && text.back() == ')') {
166
+ std::string name = text.substr(0, open);
167
+ if (name == "clamp" || name == "min" || name == "max" || name == "env") {
168
+ return parseFunctionLength(name, text.substr(open + 1, text.size() - open - 2));
169
+ }
170
+ }
77
171
  char *end = nullptr;
78
172
  float number = strtof(text.c_str(), &end);
79
173
  if (end == text.c_str()) {
@@ -17,11 +17,15 @@
17
17
 
18
18
  namespace xtgui {
19
19
 
20
- enum class Unit { Auto, None, Px, Percent, Em, Rem, Vw, Vh, Number, Invalid };
20
+ enum class Unit { Auto, None, Px, Percent, Em, Rem, Vw, Vh, Number, Clamp, Min, Max, Invalid };
21
21
 
22
22
  struct Length {
23
23
  float value = 0.0f;
24
24
  Unit unit = Unit::Auto;
25
+ /* Arguments of `clamp(...)`, `min(...)` and `max(...)`; empty for a scalar
26
+ * length. Kept unresolved so vw/vh/em are resolved against the element that
27
+ * uses the length, not the viewport at parse time. */
28
+ std::vector<Length> args = {};
25
29
  };
26
30
 
27
31
  struct Color {
@@ -52,6 +56,9 @@ struct CompoundSelector {
52
56
  std::vector<std::string> classes;
53
57
  std::vector<AttrSelector> attrs;
54
58
  std::vector<std::string> pseudoClasses;
59
+ /** `before`/`after` for a `::before`/`::after` (or legacy `:before`)
60
+ * pseudo-element; empty for an ordinary selector. */
61
+ std::string pseudo_element;
55
62
  };
56
63
 
57
64
  enum class Combinator { Descendant, Child, Adjacent, Sibling };
@@ -66,6 +73,10 @@ struct Rule {
66
73
  std::vector<ComplexSelector> selectors;
67
74
  std::vector<Declaration> declarations;
68
75
  int order = 0;
76
+ /** Raw `@media` prelude this rule came from (empty = unconditional). The
77
+ * query is evaluated against the current viewport during the cascade, so a
78
+ * resize restyles with the right rules. */
79
+ std::string media;
69
80
  };
70
81
 
71
82
  struct StyleSheet {
@@ -85,6 +96,16 @@ std::vector<Declaration> xt_css_parse_declarations(const std::string &text);
85
96
  bool xt_css_match(const Node *node, const ComplexSelector &selector);
86
97
  bool xt_css_match(const Node *node, const ComplexSelector &selector, const MatchState &state);
87
98
 
99
+ /** True when a raw `@media` prelude matches a viewport. Supports media types
100
+ * (`all`/`screen`), comma-separated query lists, `not`/`only`, `and`, and the
101
+ * width/height/orientation/hover/pointer/colour-scheme features (with
102
+ * `min-`/`max-` ranges). Unknown features never match, as in a browser. */
103
+ bool xt_css_media_matches(const std::string &query, float viewport_width, float viewport_height);
104
+
105
+ /** Split `text` at top-level commas (outside `(...)`, `[...]` and quotes),
106
+ * trimming each part and dropping empty ones. */
107
+ std::vector<std::string> xt_css_split_commas(const std::string &text);
108
+
88
109
  Length xt_css_parse_length(const std::string &value);
89
110
  Color xt_css_parse_color(const std::string &value);
90
111
  bool xt_css_parse_number(const std::string &value, float *out);
@@ -132,6 +132,9 @@ bool matchPseudo(const Node *node, const std::string &pseudo, const MatchState &
132
132
 
133
133
  bool matchCompound(const Node *node, const CompoundSelector &compound, const MatchState &state) {
134
134
  if (!node->isElement()) return false;
135
+ /* A pseudo-element selector matches a generated node, never the origin
136
+ * element; the cascade matches it separately in `style.cpp`. */
137
+ if (!compound.pseudo_element.empty()) return false;
135
138
  if (!compound.tag.empty() && node->tag != compound.tag) return false;
136
139
  if (!compound.id.empty()) {
137
140
  const std::string *id = node->attr("id");
@@ -72,6 +72,13 @@ class SelectorParser {
72
72
  }
73
73
  std::string name = xt_css_lower(readIdent(text, index));
74
74
  if (name.empty()) return false;
75
+ /* `::before`/`::after` are generated-content elements, not states. The
76
+ * single-colon spellings (`:before`) are the legacy equivalent. */
77
+ if (pseudoElement || name == "before" || name == "after") {
78
+ compound->pseudo_element = name;
79
+ any = true;
80
+ continue;
81
+ }
75
82
  if (!pseudoElement && *index < text.size() && text[*index] == '(') {
76
83
  int depth = 0;
77
84
  size_t start = *index;
@@ -192,6 +199,8 @@ class SelectorParser {
192
199
  if (!compound.id.empty()) ids++;
193
200
  classes += computeCompoundSpecificity(compound);
194
201
  if (!compound.tag.empty()) types++;
202
+ /* A pseudo-element counts like a type selector (CSS specificity). */
203
+ if (!compound.pseudo_element.empty()) types++;
195
204
  }
196
205
  return (ids << 20) | (classes << 10) | types;
197
206
  }
@@ -1,5 +1,9 @@
1
1
  /*
2
2
  * xbintsc GUI engine — CSS declaration and stylesheet parsing.
3
+ *
4
+ * At-rules are no longer all skipped: `@media` blocks are parsed recursively
5
+ * and each rule keeps its (possibly nested) query, which the cascade evaluates
6
+ * against the viewport. Every other at-rule is ignored with its block.
3
7
  */
4
8
 
5
9
  #include "css_internal.h"
@@ -41,6 +45,42 @@ size_t findTopLevel(const std::string &text, char needle, size_t from) {
41
45
  return std::string::npos;
42
46
  }
43
47
 
48
+ /** The `}` matching the `{` at `open`, respecting strings and nested blocks. */
49
+ size_t findBlockEnd(const std::string &text, size_t open) {
50
+ int depth = 0;
51
+ char quote = 0;
52
+ for (size_t i = open; i < text.size(); i++) {
53
+ char c = text[i];
54
+ if (quote != 0) {
55
+ if (c == quote) quote = 0;
56
+ continue;
57
+ }
58
+ if (c == '"' || c == '\'') quote = c;
59
+ else if (c == '{') depth++;
60
+ else if (c == '}') {
61
+ depth--;
62
+ if (depth == 0) return i;
63
+ }
64
+ }
65
+ return std::string::npos;
66
+ }
67
+
68
+ /** Join a nested `@media` prelude: distribute `inner` over an outer comma list. */
69
+ std::string combineMedia(const std::string &outer, const std::string &inner) {
70
+ if (outer.empty()) return inner;
71
+ if (inner.empty()) return outer;
72
+ std::vector<std::string> outerParts = xt_css_split_commas(outer);
73
+ std::vector<std::string> innerParts = xt_css_split_commas(inner);
74
+ std::string combined;
75
+ for (const std::string &o : outerParts) {
76
+ for (const std::string &i : innerParts) {
77
+ if (!combined.empty()) combined += ", ";
78
+ combined += o + " and " + i;
79
+ }
80
+ }
81
+ return combined;
82
+ }
83
+
44
84
  std::string stripImportant(std::string value, bool *important) {
45
85
  size_t pos = value.rfind('!');
46
86
  if (pos == std::string::npos) return value;
@@ -52,6 +92,54 @@ std::string stripImportant(std::string value, bool *important) {
52
92
  return value;
53
93
  }
54
94
 
95
+ /** Parse one rule list, carrying the enclosing `@media` prelude. */
96
+ void parseRules(const std::string &text, const std::string &media, int *order,
97
+ StyleSheet *sheet) {
98
+ size_t index = 0;
99
+ while (index < text.size()) {
100
+ while (index < text.size() && xt_is_css_space(text[index])) index++;
101
+ if (index >= text.size()) break;
102
+
103
+ if (text[index] == '@') {
104
+ size_t nameStart = index + 1;
105
+ size_t nameEnd = nameStart;
106
+ while (nameEnd < text.size() &&
107
+ (std::isalnum((unsigned char)text[nameEnd]) || text[nameEnd] == '-'))
108
+ nameEnd++;
109
+ std::string name = xt_css_lower(text.substr(nameStart, nameEnd - nameStart));
110
+ size_t brace = text.find('{', nameEnd);
111
+ size_t semi = text.find(';', nameEnd);
112
+ if (brace != std::string::npos && (semi == std::string::npos || brace < semi)) {
113
+ size_t close = findBlockEnd(text, brace);
114
+ if (close == std::string::npos) break;
115
+ if (name == "media") {
116
+ std::string query = xt_css_trim(text.substr(nameEnd, brace - nameEnd));
117
+ std::string nested = combineMedia(media, query);
118
+ parseRules(text.substr(brace + 1, close - brace - 1), nested, order, sheet);
119
+ }
120
+ /* Other block at-rules (`@supports`, `@keyframes`, `@font-face`, …) are
121
+ * not part of the engine's subset: ignore the whole block. */
122
+ index = close + 1;
123
+ } else {
124
+ index = (semi == std::string::npos) ? text.size() : semi + 1;
125
+ }
126
+ continue;
127
+ }
128
+
129
+ size_t brace = text.find('{', index);
130
+ if (brace == std::string::npos) break;
131
+ size_t close = findBlockEnd(text, brace);
132
+ if (close == std::string::npos) break;
133
+ Rule rule;
134
+ rule.selectors = xt_css_parse_selector_list(text.substr(index, brace - index));
135
+ rule.declarations = xt_css_parse_declarations(text.substr(brace + 1, close - brace - 1));
136
+ rule.order = (*order)++;
137
+ rule.media = media;
138
+ if (!rule.selectors.empty() && !rule.declarations.empty()) sheet->rules.push_back(std::move(rule));
139
+ index = close + 1;
140
+ }
141
+ }
142
+
55
143
  } // namespace
56
144
 
57
145
  std::vector<Declaration> xt_css_parse_declarations(const std::string &text) {
@@ -77,52 +165,8 @@ std::vector<Declaration> xt_css_parse_declarations(const std::string &text) {
77
165
  StyleSheet xt_css_parse_stylesheet(const std::string &text) {
78
166
  StyleSheet sheet;
79
167
  std::string clean = stripComments(text);
80
- size_t index = 0;
81
168
  int order = 0;
82
- while (index < clean.size()) {
83
- while (index < clean.size() && xt_is_css_space(clean[index])) index++;
84
- if (index >= clean.size()) break;
85
- if (clean[index] == '@') {
86
- size_t brace = clean.find('{', index);
87
- size_t semi = clean.find(';', index);
88
- if (brace != std::string::npos && (semi == std::string::npos || brace < semi)) {
89
- int depth = 0;
90
- size_t i = brace;
91
- for (; i < clean.size(); i++) {
92
- if (clean[i] == '{') depth++;
93
- else if (clean[i] == '}') {
94
- depth--;
95
- if (depth == 0) break;
96
- }
97
- }
98
- index = (i < clean.size()) ? i + 1 : clean.size();
99
- } else {
100
- index = (semi == std::string::npos) ? clean.size() : semi + 1;
101
- }
102
- continue;
103
- }
104
- size_t brace = clean.find('{', index);
105
- if (brace == std::string::npos) break;
106
- int depth = 0;
107
- size_t close = std::string::npos;
108
- for (size_t i = brace; i < clean.size(); i++) {
109
- if (clean[i] == '{') depth++;
110
- else if (clean[i] == '}') {
111
- depth--;
112
- if (depth == 0) {
113
- close = i;
114
- break;
115
- }
116
- }
117
- }
118
- if (close == std::string::npos) break;
119
- Rule rule;
120
- rule.selectors = xt_css_parse_selector_list(clean.substr(index, brace - index));
121
- rule.declarations = xt_css_parse_declarations(clean.substr(brace + 1, close - brace - 1));
122
- rule.order = order++;
123
- if (!rule.selectors.empty() && !rule.declarations.empty()) sheet.rules.push_back(std::move(rule));
124
- index = close + 1;
125
- }
169
+ parseRules(clean, "", &order, &sheet);
126
170
  return sheet;
127
171
  }
128
172
 
@@ -47,7 +47,7 @@ void XtDocument::restyle(float viewportWidth, float viewportHeight) {
47
47
  MatchState state;
48
48
  state.hover = hover_;
49
49
  state.focus = focus_;
50
- xt_style_compute(root_.get(), sheet_, styles_, viewportWidth, viewportHeight, state);
50
+ xt_style_compute(root_.get(), sheet_, styles_, viewportWidth, viewportHeight, state, &pseudos_);
51
51
  startTransitions();
52
52
  updateDisplay();
53
53
  }
@@ -124,6 +124,9 @@ void XtDocument::startTransitions() {
124
124
  for (auto &entry : styles_) {
125
125
  const Node *node = entry.first;
126
126
  const XtStyle &target = entry.second;
127
+ /* Generated boxes are rebuilt on every restyle; a transition keyed by one
128
+ * would address a node that no longer exists. */
129
+ if (node->is_pseudo) continue;
127
130
  if (target.transitions.empty()) continue;
128
131
  auto prev_it = display_.find(node);
129
132
  if (prev_it == display_.end()) continue;
@@ -118,6 +118,8 @@ class XtDocument {
118
118
  std::unique_ptr<Node> root_;
119
119
  /** Detached nodes (created or removed) stay alive here so handles remain valid. */
120
120
  std::unordered_map<const Node *, std::unique_ptr<Node>> detached_;
121
+ /** Synthetic `::before`/`::after` nodes for the current style generation. */
122
+ PseudoNodeSet pseudos_;
121
123
  bool dirty_ = false;
122
124
  StyleSheet sheet_;
123
125
  std::unordered_map<const Node *, XtStyle> styles_; // target (cascade)
@@ -18,6 +18,9 @@ namespace xtgui {
18
18
 
19
19
  enum class NodeType { Document, Element, Text };
20
20
 
21
+ /** Which generated-content box a synthetic pseudo-element node represents. */
22
+ enum class PseudoElement { None, Before, After };
23
+
21
24
  struct Attr {
22
25
  std::string name;
23
26
  std::string value;
@@ -32,6 +35,19 @@ struct Node {
32
35
  Node *parent = nullptr;
33
36
  std::vector<std::unique_ptr<Node>> children;
34
37
 
38
+ /* -- generated content (`::before` / `::after`) -------------------------
39
+ * A pseudo-element is a synthetic node owned by the document's per-restyle
40
+ * arena, not by the DOM tree: it is never in `children`, so traversal,
41
+ * queries, serialization and `elementIndex()` do not see it. `pseudo_origin`
42
+ * is the element the selector matched and whose computed style it inherits.
43
+ * `pseudo_before`/`pseudo_after` are transient pointers set by the cascade
44
+ * for the layout pass (the arena is rebuilt on every restyle). */
45
+ bool is_pseudo = false;
46
+ PseudoElement pseudo = PseudoElement::None;
47
+ const Node *pseudo_origin = nullptr;
48
+ Node *pseudo_before = nullptr;
49
+ Node *pseudo_after = nullptr;
50
+
35
51
  Node() = default;
36
52
  explicit Node(NodeType nodeType) : type(nodeType) {}
37
53
 
@@ -65,6 +65,7 @@ static xtgui::Node *bind_pending(XtGuiWindow *win, xt_value self, xtgui::Node *n
65
65
  xt_object_set(self, xt_string_from_cstr("__xt_gui_gen"),
66
66
  xt_object_get_cstr(canonical, "__xt_gui_gen"));
67
67
  xt_object_set(self, xt_string_from_cstr("__xt_gui_pending"), XT_UNDEFINED);
68
+ xt_gui_canvas_bind_pending(win, node, self);
68
69
  return node;
69
70
  }
70
71