xbintsc 0.5.51 → 0.5.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/help.d.ts +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/src/extensions/gui/features.d.ts +9 -6
- package/dist/src/extensions/gui/features.js +14 -9
- package/dist/src/extensions/gui/features.js.map +1 -1
- package/dist/tests/e2e/gui-css-coverage.test.d.ts +9 -0
- package/dist/tests/e2e/gui-css-coverage.test.js +333 -0
- package/dist/tests/e2e/gui-css-coverage.test.js.map +1 -0
- package/dist/tests/e2e/gui-css-features.test.d.ts +6 -0
- package/dist/tests/e2e/gui-css-features.test.js +270 -0
- package/dist/tests/e2e/gui-css-features.test.js.map +1 -0
- package/dist/tests/e2e/gui-effects.test.js +92 -11
- package/dist/tests/e2e/gui-effects.test.js.map +1 -1
- package/dist/tests/e2e/gui-font-face.test.d.ts +10 -0
- package/dist/tests/e2e/gui-font-face.test.js +142 -0
- package/dist/tests/e2e/gui-font-face.test.js.map +1 -0
- package/dist/tests/e2e/gui-fonts.test.d.ts +10 -0
- package/dist/tests/e2e/gui-fonts.test.js +150 -0
- package/dist/tests/e2e/gui-fonts.test.js.map +1 -0
- package/dist/tests/e2e/gui-helpers.d.ts +5 -0
- package/dist/tests/e2e/gui-helpers.js +4 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-layout.test.js +3 -2
- package/dist/tests/e2e/gui-layout.test.js.map +1 -1
- package/dist/tests/e2e/gui-track-sizing.test.d.ts +6 -0
- package/dist/tests/e2e/gui-track-sizing.test.js +173 -0
- package/dist/tests/e2e/gui-track-sizing.test.js.map +1 -0
- package/dist/tests/e2e/gui-transform.test.js +3 -2
- package/dist/tests/e2e/gui-transform.test.js.map +1 -1
- package/dist/tests/extensions/gui-features.test.js +8 -1
- package/dist/tests/extensions/gui-features.test.js.map +1 -1
- package/doc/ai/extensions.md +10 -5
- package/doc/ai/zh-CN/extensions.md +9 -4
- package/doc/gui/animation.md +39 -9
- package/doc/gui/build.md +6 -5
- package/doc/gui/dom.md +20 -4
- package/doc/gui/known-issues.md +31 -23
- package/doc/gui/layout.md +30 -7
- package/doc/gui/rendering.md +84 -26
- package/doc/gui.md +62 -14
- package/doc/zh-CN/gui/animation.md +33 -10
- package/doc/zh-CN/gui/build.md +5 -5
- package/doc/zh-CN/gui/dom.md +16 -3
- package/doc/zh-CN/gui/known-issues.md +21 -12
- package/doc/zh-CN/gui/layout.md +22 -5
- package/doc/zh-CN/gui/rendering.md +61 -20
- package/doc/zh-CN/gui.md +53 -15
- package/package.json +7 -7
- package/runtime/ext_gui/animation_values.cpp +425 -0
- package/runtime/ext_gui/animation_values.h +64 -0
- package/runtime/ext_gui/css.cpp +172 -21
- package/runtime/ext_gui/css.h +75 -4
- package/runtime/ext_gui/css_match.cpp +4 -0
- package/runtime/ext_gui/css_sheet.cpp +218 -2
- package/runtime/ext_gui/document.cpp +42 -192
- package/runtime/ext_gui/document.h +25 -12
- package/runtime/ext_gui/document_animation.cpp +428 -0
- package/runtime/ext_gui/effect_values.cpp +245 -21
- package/runtime/ext_gui/gradient_values.cpp +24 -0
- package/runtime/ext_gui/grid_values.cpp +23 -0
- package/runtime/ext_gui/gui_cursor.cpp +50 -0
- package/runtime/ext_gui/gui_events.cpp +16 -4
- package/runtime/ext_gui/gui_internal.h +3 -0
- package/runtime/ext_gui/image_api.h +3 -1
- package/runtime/ext_gui/image_feature.cpp +4 -2
- package/runtime/ext_gui/inline_svg.cpp +95 -8
- package/runtime/ext_gui/inline_svg.h +5 -1
- package/runtime/ext_gui/layout.cpp +3 -2
- package/runtime/ext_gui/layout.h +6 -0
- package/runtime/ext_gui/layout_flex.cpp +134 -29
- package/runtime/ext_gui/layout_flow.cpp +58 -7
- package/runtime/ext_gui/layout_flow_internal.h +39 -4
- package/runtime/ext_gui/layout_grid.cpp +31 -0
- package/runtime/ext_gui/layout_inline.cpp +26 -2
- package/runtime/ext_gui/layout_vertical.cpp +196 -0
- package/runtime/ext_gui/paint.cpp +67 -9
- package/runtime/ext_gui/paint.h +61 -8
- package/runtime/ext_gui/paint_backdrop.cpp +211 -0
- package/runtime/ext_gui/paint_background.cpp +175 -97
- package/runtime/ext_gui/paint_effects.cpp +12 -6
- package/runtime/ext_gui/paint_internal.h +27 -0
- package/runtime/ext_gui/paint_outline.cpp +44 -0
- package/runtime/ext_gui/paint_shadow.cpp +368 -0
- package/runtime/ext_gui/renderer.cpp +15 -7
- package/runtime/ext_gui/renderer.h +10 -1
- package/runtime/ext_gui/renderer_backdrop.cpp +374 -0
- package/runtime/ext_gui/renderer_bitmap.cpp +82 -0
- package/runtime/ext_gui/renderer_gradient.cpp +51 -17
- package/runtime/ext_gui/renderer_image.cpp +6 -1
- package/runtime/ext_gui/renderer_internal.h +21 -0
- package/runtime/ext_gui/renderer_layer.cpp +27 -1
- package/runtime/ext_gui/renderer_text.cpp +2 -1
- package/runtime/ext_gui/style.cpp +62 -8
- package/runtime/ext_gui/style.h +111 -4
- package/runtime/ext_gui/style_cascade.cpp +83 -0
- package/runtime/ext_gui/style_internal.h +13 -1
- package/runtime/ext_gui/style_length.cpp +92 -0
- package/runtime/ext_gui/style_print.cpp +83 -2
- package/runtime/ext_gui/style_svg.cpp +40 -0
- package/runtime/ext_gui/style_text.cpp +302 -0
- package/runtime/ext_gui/style_ui.cpp +150 -0
- package/runtime/ext_gui/style_values.cpp +76 -62
- package/runtime/ext_gui/style_values_internal.h +23 -0
- package/runtime/ext_gui/text.cpp +4 -2
- package/runtime/ext_gui/text.h +11 -4
- package/runtime/ext_gui/text_cache.cpp +11 -2
- package/runtime/ext_gui/text_font_db.cpp +427 -0
- package/runtime/ext_gui/text_font_paths.cpp +250 -0
- package/runtime/ext_gui/text_font_paths.h +39 -0
- package/runtime/ext_gui/text_font_sfnt.cpp +396 -0
- package/runtime/ext_gui/text_font_sfnt.h +44 -0
- package/runtime/ext_gui/text_fonts.cpp +348 -0
- package/runtime/ext_gui/text_fonts.h +78 -0
- package/runtime/ext_gui/text_fonts_internal.h +77 -0
- package/runtime/ext_gui/text_hb.cpp +63 -179
- package/runtime/ext_gui/text_stb.cpp +49 -193
- package/runtime/ext_gui/transform.cpp +107 -9
- package/runtime/ext_gui/transform.h +5 -0
- package/runtime/ext_gui/window.cpp +1 -1
- package/src/driver/version.ts +1 -1
- package/src/extensions/gui/features.ts +13 -9
package/runtime/ext_gui/css.cpp
CHANGED
|
@@ -112,6 +112,163 @@ static std::vector<std::string> splitFunctionArgs(const std::string &text) {
|
|
|
112
112
|
return args;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/** A scalar token (`12px`, `50%`, `1.5`) as a `Length`. Absolute lengths are
|
|
116
|
+
* converted to pixels here; relative units stay symbolic so layout can resolve
|
|
117
|
+
* them against the element that uses them. */
|
|
118
|
+
static Length makeScalarLength(double number, const std::string &rawUnit) {
|
|
119
|
+
Length length;
|
|
120
|
+
std::string unit = xt_css_lower(xt_css_trim(rawUnit));
|
|
121
|
+
length.value = (float)number;
|
|
122
|
+
if (unit.empty() || unit == "px") length.unit = Unit::Px;
|
|
123
|
+
else if (unit == "%") length.unit = Unit::Percent;
|
|
124
|
+
else if (unit == "em") length.unit = Unit::Em;
|
|
125
|
+
else if (unit == "rem") length.unit = Unit::Rem;
|
|
126
|
+
else if (unit == "vw") length.unit = Unit::Vw;
|
|
127
|
+
else if (unit == "vh") length.unit = Unit::Vh;
|
|
128
|
+
else if (unit == "pt") { length.value = (float)number * (96.0f / 72.0f); length.unit = Unit::Px; }
|
|
129
|
+
else if (unit == "pc") { length.value = (float)number * 16.0f; length.unit = Unit::Px; }
|
|
130
|
+
else if (unit == "in") { length.value = (float)number * 96.0f; length.unit = Unit::Px; }
|
|
131
|
+
else if (unit == "cm") { length.value = (float)number * (96.0f / 2.54f); length.unit = Unit::Px; }
|
|
132
|
+
else if (unit == "mm") { length.value = (float)number * (96.0f / 25.4f); length.unit = Unit::Px; }
|
|
133
|
+
else if (unit == "q") { length.value = (float)number * (96.0f / 101.6f); length.unit = Unit::Px; }
|
|
134
|
+
else length.unit = Unit::Invalid;
|
|
135
|
+
return length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static Length parseFunctionLength(const std::string &name, const std::string &inner);
|
|
139
|
+
static bool parseMathExpression(const std::string &text, Length *out);
|
|
140
|
+
|
|
141
|
+
/** Recursive-descent parser for `calc()` arithmetic (`+ - * /`, parentheses,
|
|
142
|
+
* nested `calc`/`min`/`max`/`clamp` and unit-bearing numbers). The expression
|
|
143
|
+
* tree stays unresolved: `xt_style_resolve_absolute` walks it once the
|
|
144
|
+
* containing block and font size are known. */
|
|
145
|
+
struct MathParser {
|
|
146
|
+
explicit MathParser(const std::string &text) : text_(text) {}
|
|
147
|
+
|
|
148
|
+
bool parse(Length *out) {
|
|
149
|
+
if (!parseSum(out)) return false;
|
|
150
|
+
skip();
|
|
151
|
+
return index_ == text_.size();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private:
|
|
155
|
+
void skip() {
|
|
156
|
+
while (index_ < text_.size() && xt_is_css_space(text_[index_])) index_++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
bool parseSum(Length *out) {
|
|
160
|
+
Length left;
|
|
161
|
+
if (!parseProduct(&left)) return false;
|
|
162
|
+
for (;;) {
|
|
163
|
+
skip();
|
|
164
|
+
if (index_ >= text_.size() || (text_[index_] != '+' && text_[index_] != '-')) break;
|
|
165
|
+
char op = text_[index_++];
|
|
166
|
+
Length right;
|
|
167
|
+
if (!parseProduct(&right)) return false;
|
|
168
|
+
Length node;
|
|
169
|
+
node.unit = Unit::Calc;
|
|
170
|
+
node.op = op;
|
|
171
|
+
node.args.push_back(left);
|
|
172
|
+
node.args.push_back(right);
|
|
173
|
+
left = node;
|
|
174
|
+
}
|
|
175
|
+
*out = left;
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
bool parseProduct(Length *out) {
|
|
180
|
+
Length left;
|
|
181
|
+
if (!parseOperand(&left)) return false;
|
|
182
|
+
for (;;) {
|
|
183
|
+
skip();
|
|
184
|
+
if (index_ >= text_.size() || (text_[index_] != '*' && text_[index_] != '/')) break;
|
|
185
|
+
char op = text_[index_++];
|
|
186
|
+
Length right;
|
|
187
|
+
if (!parseOperand(&right)) return false;
|
|
188
|
+
Length node;
|
|
189
|
+
node.unit = Unit::Calc;
|
|
190
|
+
node.op = op;
|
|
191
|
+
node.args.push_back(left);
|
|
192
|
+
node.args.push_back(right);
|
|
193
|
+
left = node;
|
|
194
|
+
}
|
|
195
|
+
*out = left;
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
bool parseOperand(Length *out) {
|
|
200
|
+
skip();
|
|
201
|
+
if (index_ >= text_.size()) return false;
|
|
202
|
+
if (text_[index_] == '(') {
|
|
203
|
+
index_++;
|
|
204
|
+
if (!parseSum(out)) return false;
|
|
205
|
+
skip();
|
|
206
|
+
if (index_ >= text_.size() || text_[index_] != ')') return false;
|
|
207
|
+
index_++;
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
if (std::isalpha((unsigned char)text_[index_])) return parseFunction(out);
|
|
211
|
+
return parseNumber(out);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
bool parseFunction(Length *out) {
|
|
215
|
+
size_t nameStart = index_;
|
|
216
|
+
while (index_ < text_.size() &&
|
|
217
|
+
(std::isalpha((unsigned char)text_[index_]) || text_[index_] == '-'))
|
|
218
|
+
index_++;
|
|
219
|
+
std::string name = xt_css_lower(text_.substr(nameStart, index_ - nameStart));
|
|
220
|
+
skip();
|
|
221
|
+
if (index_ >= text_.size() || text_[index_] != '(') return false;
|
|
222
|
+
size_t open = index_;
|
|
223
|
+
int depth = 0;
|
|
224
|
+
size_t close = std::string::npos;
|
|
225
|
+
for (; index_ < text_.size(); index_++) {
|
|
226
|
+
if (text_[index_] == '(') depth++;
|
|
227
|
+
else if (text_[index_] == ')') {
|
|
228
|
+
depth--;
|
|
229
|
+
if (depth == 0) {
|
|
230
|
+
close = index_;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (close == std::string::npos) return false;
|
|
236
|
+
std::string inner = text_.substr(open + 1, close - open - 1);
|
|
237
|
+
index_ = close + 1;
|
|
238
|
+
if (name == "calc") return parseMathExpression(inner, out);
|
|
239
|
+
if (name == "min" || name == "max" || name == "clamp" || name == "env") {
|
|
240
|
+
*out = parseFunctionLength(name, inner);
|
|
241
|
+
return out->unit != Unit::Invalid;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
bool parseNumber(Length *out) {
|
|
247
|
+
skip();
|
|
248
|
+
const char *begin = text_.c_str() + index_;
|
|
249
|
+
char *end = nullptr;
|
|
250
|
+
double number = std::strtod(begin, &end);
|
|
251
|
+
if (end == begin || end == nullptr) return false;
|
|
252
|
+
index_ = (size_t)(end - text_.c_str());
|
|
253
|
+
size_t unitStart = index_;
|
|
254
|
+
while (index_ < text_.size() &&
|
|
255
|
+
(std::isalpha((unsigned char)text_[index_]) || text_[index_] == '%'))
|
|
256
|
+
index_++;
|
|
257
|
+
std::string unit = text_.substr(unitStart, index_ - unitStart);
|
|
258
|
+
*out = makeScalarLength(number, unit);
|
|
259
|
+
if (unit.empty()) out->unit = Unit::Number;
|
|
260
|
+
return out->unit != Unit::Invalid;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const std::string &text_;
|
|
264
|
+
size_t index_ = 0;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
static bool parseMathExpression(const std::string &text, Length *out) {
|
|
268
|
+
MathParser parser(text);
|
|
269
|
+
return parser.parse(out);
|
|
270
|
+
}
|
|
271
|
+
|
|
115
272
|
/** `clamp(min, value, max)`, `min(...)` and `max(...)` over lengths. The
|
|
116
273
|
* arguments stay unresolved; layout resolves them against the element's em and
|
|
117
274
|
* the viewport. `env(safe-area-inset-*)` has no notch information in a desktop
|
|
@@ -135,8 +292,8 @@ static Length parseFunctionLength(const std::string &name, const std::string &in
|
|
|
135
292
|
}
|
|
136
293
|
result.unit = name == "clamp" ? Unit::Clamp : (name == "min" ? Unit::Min : Unit::Max);
|
|
137
294
|
for (const std::string &arg : args) {
|
|
138
|
-
Length value
|
|
139
|
-
if (value
|
|
295
|
+
Length value;
|
|
296
|
+
if (!parseMathExpression(arg, &value)) {
|
|
140
297
|
result.unit = Unit::Invalid;
|
|
141
298
|
result.args.clear();
|
|
142
299
|
return result;
|
|
@@ -163,34 +320,28 @@ Length xt_css_parse_length(const std::string &value) {
|
|
|
163
320
|
}
|
|
164
321
|
size_t open = text.find('(');
|
|
165
322
|
if (open != std::string::npos && !text.empty() && text.back() == ')') {
|
|
166
|
-
std::string name = text.substr(0, open);
|
|
323
|
+
std::string name = xt_css_trim(text.substr(0, open));
|
|
324
|
+
std::string inner = text.substr(open + 1, text.size() - open - 2);
|
|
325
|
+
if (name == "calc") {
|
|
326
|
+
Length parsed;
|
|
327
|
+
if (!parseMathExpression(inner, &parsed)) parsed.unit = Unit::Invalid;
|
|
328
|
+
return parsed;
|
|
329
|
+
}
|
|
167
330
|
if (name == "clamp" || name == "min" || name == "max" || name == "env") {
|
|
168
|
-
return parseFunctionLength(name,
|
|
331
|
+
return parseFunctionLength(name, inner);
|
|
169
332
|
}
|
|
170
333
|
}
|
|
334
|
+
/* A scalar keeps the historical `px` default for a unitless token; only
|
|
335
|
+
* arithmetic treats `2` as a number. */
|
|
171
336
|
char *end = nullptr;
|
|
172
|
-
|
|
173
|
-
if (end == text.c_str()) {
|
|
337
|
+
double number = std::strtod(text.c_str(), &end);
|
|
338
|
+
if (end == text.c_str() || end == nullptr) {
|
|
174
339
|
length.unit = Unit::Invalid;
|
|
175
340
|
return length;
|
|
176
341
|
}
|
|
177
342
|
std::string unit = end;
|
|
178
343
|
while (!unit.empty() && xt_is_css_space(unit[0])) unit.erase(unit.begin());
|
|
179
|
-
|
|
180
|
-
if (unit.empty() || unit == "px") length.unit = Unit::Px;
|
|
181
|
-
else if (unit == "%") length.unit = Unit::Percent;
|
|
182
|
-
else if (unit == "em") length.unit = Unit::Em;
|
|
183
|
-
else if (unit == "rem") length.unit = Unit::Rem;
|
|
184
|
-
else if (unit == "vw") length.unit = Unit::Vw;
|
|
185
|
-
else if (unit == "vh") length.unit = Unit::Vh;
|
|
186
|
-
else if (unit == "pt") { length.value = number * (96.0f / 72.0f); length.unit = Unit::Px; }
|
|
187
|
-
else if (unit == "pc") { length.value = number * 16.0f; length.unit = Unit::Px; }
|
|
188
|
-
else if (unit == "in") { length.value = number * 96.0f; length.unit = Unit::Px; }
|
|
189
|
-
else if (unit == "cm") { length.value = number * (96.0f / 2.54f); length.unit = Unit::Px; }
|
|
190
|
-
else if (unit == "mm") { length.value = number * (96.0f / 25.4f); length.unit = Unit::Px; }
|
|
191
|
-
else if (unit == "q") { length.value = number * (96.0f / 101.6f); length.unit = Unit::Px; }
|
|
192
|
-
else length.unit = Unit::Invalid;
|
|
193
|
-
return length;
|
|
344
|
+
return makeScalarLength(number, unit);
|
|
194
345
|
}
|
|
195
346
|
|
|
196
347
|
namespace {
|
package/runtime/ext_gui/css.h
CHANGED
|
@@ -17,14 +17,35 @@
|
|
|
17
17
|
|
|
18
18
|
namespace xtgui {
|
|
19
19
|
|
|
20
|
-
enum class Unit {
|
|
20
|
+
enum class Unit {
|
|
21
|
+
Auto,
|
|
22
|
+
None,
|
|
23
|
+
Px,
|
|
24
|
+
Percent,
|
|
25
|
+
Em,
|
|
26
|
+
Rem,
|
|
27
|
+
Vw,
|
|
28
|
+
Vh,
|
|
29
|
+
Number,
|
|
30
|
+
Clamp,
|
|
31
|
+
Min,
|
|
32
|
+
Max,
|
|
33
|
+
/** A `calc()` sub-expression: `args[0] <op> args[1]`. Nested arithmetic keeps
|
|
34
|
+
* its operands unresolved, so only the units actually present are resolved
|
|
35
|
+
* against the element (a `calc(100% - 2em)` depends on both the containing
|
|
36
|
+
* block and the font size). */
|
|
37
|
+
Calc,
|
|
38
|
+
Invalid
|
|
39
|
+
};
|
|
21
40
|
|
|
22
41
|
struct Length {
|
|
23
42
|
float value = 0.0f;
|
|
24
43
|
Unit unit = Unit::Auto;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
/** `+`, `-`, `*` or `/` for `Unit::Calc`; zero otherwise. */
|
|
45
|
+
char op = 0;
|
|
46
|
+
/* Arguments of `clamp(...)`, `min(...)`, `max(...)` and `calc(...)`; empty for
|
|
47
|
+
* a scalar length. Kept unresolved so vw/vh/em are resolved against the
|
|
48
|
+
* element that uses the length, not the viewport at parse time. */
|
|
28
49
|
std::vector<Length> args = {};
|
|
29
50
|
};
|
|
30
51
|
|
|
@@ -79,21 +100,71 @@ struct Rule {
|
|
|
79
100
|
std::string media;
|
|
80
101
|
};
|
|
81
102
|
|
|
103
|
+
/** One step of an `@keyframes` rule: a normalised offset (0..1) and the
|
|
104
|
+
* declarations that apply there. */
|
|
105
|
+
struct Keyframe {
|
|
106
|
+
float offset = 0.0f;
|
|
107
|
+
std::vector<Declaration> declarations;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** One `@keyframes <name>` block. Offsets are kept sorted so the animation
|
|
111
|
+
* engine can bracket a progress with two adjacent steps. */
|
|
112
|
+
struct KeyframesRule {
|
|
113
|
+
std::string name;
|
|
114
|
+
std::vector<Keyframe> keyframes;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** One `@font-face` source: a local file URL plus the optional `format(...)`
|
|
118
|
+
* hint. `local(...)` sources are dropped at parse time (the runtime has no
|
|
119
|
+
* name-to-file font lookup for them). */
|
|
120
|
+
struct FontFaceSource {
|
|
121
|
+
std::string url;
|
|
122
|
+
std::string format;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** One author `@font-face` rule. The runtime resolves the sources to a real
|
|
126
|
+
* font file when the document loads; the declared family name is what CSS
|
|
127
|
+
* matches, even when it differs from the font's internal name. */
|
|
128
|
+
struct FontFaceRule {
|
|
129
|
+
std::string family;
|
|
130
|
+
std::vector<FontFaceSource> sources;
|
|
131
|
+
/** `font-weight` descriptor. A range (`100 900`) is kept as min/max; when the
|
|
132
|
+
* descriptor is absent the font's OS/2 weight is used. */
|
|
133
|
+
bool has_weight = false;
|
|
134
|
+
int weight_min = 400;
|
|
135
|
+
int weight_max = 400;
|
|
136
|
+
/** `font-style` descriptor; absent means the font's own italic flag. */
|
|
137
|
+
bool has_italic = false;
|
|
138
|
+
bool italic = false;
|
|
139
|
+
};
|
|
140
|
+
|
|
82
141
|
struct StyleSheet {
|
|
83
142
|
std::vector<Rule> rules;
|
|
143
|
+
/** `@keyframes` blocks, in declaration order. */
|
|
144
|
+
std::vector<KeyframesRule> keyframes;
|
|
145
|
+
/** `@font-face` blocks, in declaration order. */
|
|
146
|
+
std::vector<FontFaceRule> font_faces;
|
|
84
147
|
/* Facts the cascade can act on once per sheet instead of once per element (a
|
|
85
148
|
* `@media` block, a `::before` rule or a `transition` declaration used to be
|
|
86
149
|
* discovered by scanning every rule for every node). */
|
|
87
150
|
bool has_pseudo_elements = false;
|
|
88
151
|
bool has_hover = false;
|
|
89
152
|
bool has_focus = false;
|
|
153
|
+
bool has_active = false;
|
|
90
154
|
bool has_transitions = false;
|
|
155
|
+
bool has_animations = false;
|
|
91
156
|
};
|
|
92
157
|
|
|
158
|
+
/** Look up the last `@keyframes` rule with `name` (later blocks replace
|
|
159
|
+
* earlier ones), or null when the sheet has no such animation. */
|
|
160
|
+
const KeyframesRule *xt_css_find_keyframes(const StyleSheet &sheet, const std::string &name);
|
|
161
|
+
|
|
93
162
|
/** Dynamic element state consulted by `:hover` / `:focus` while matching. */
|
|
94
163
|
struct MatchState {
|
|
95
164
|
const Node *hover = nullptr;
|
|
96
165
|
const Node *focus = nullptr;
|
|
166
|
+
/** The element currently held down by the primary pointer (`:active`). */
|
|
167
|
+
const Node *active = nullptr;
|
|
97
168
|
};
|
|
98
169
|
|
|
99
170
|
StyleSheet xt_css_parse_stylesheet(const std::string &text);
|
|
@@ -93,6 +93,10 @@ bool matchPseudo(const Node *node, const std::string &pseudo, const MatchState &
|
|
|
93
93
|
if (pseudo == "root") return node->parent != nullptr && node->parent->isDocument();
|
|
94
94
|
if (pseudo == "hover") return isHoverTarget(node, state.hover);
|
|
95
95
|
if (pseudo == "focus") return node == state.focus;
|
|
96
|
+
/* The engine has no keyboard modality tracking, so focus-visible is the same
|
|
97
|
+
* as focus (a mouse click also shows the focus ring). */
|
|
98
|
+
if (pseudo == "focus-visible") return node == state.focus;
|
|
99
|
+
if (pseudo == "active") return isHoverTarget(node, state.active);
|
|
96
100
|
if (pseudo == "empty") {
|
|
97
101
|
for (const std::unique_ptr<Node> &child : node->children) {
|
|
98
102
|
if (child->isElement()) return false;
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
#include "css_internal.h"
|
|
10
10
|
|
|
11
|
+
#include <algorithm>
|
|
12
|
+
#include <cctype>
|
|
13
|
+
#include <cstring>
|
|
14
|
+
#include <utility>
|
|
15
|
+
|
|
11
16
|
namespace xtgui {
|
|
12
17
|
|
|
13
18
|
namespace {
|
|
@@ -92,6 +97,202 @@ std::string stripImportant(std::string value, bool *important) {
|
|
|
92
97
|
return value;
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
/** Strip matching quotes from an at-rule prelude name. */
|
|
101
|
+
std::string unquote(std::string text) {
|
|
102
|
+
text = xt_css_trim(text);
|
|
103
|
+
if (text.size() >= 2 && (text.front() == '"' || text.front() == '\'') &&
|
|
104
|
+
text.back() == text.front()) {
|
|
105
|
+
text = text.substr(1, text.size() - 2);
|
|
106
|
+
}
|
|
107
|
+
return text;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
bool parseKeyframeOffset(const std::string &text, float *offset) {
|
|
111
|
+
std::string token = xt_css_lower(xt_css_trim(text));
|
|
112
|
+
if (token == "from") {
|
|
113
|
+
*offset = 0.0f;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (token == "to") {
|
|
117
|
+
*offset = 1.0f;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (!token.empty() && token.back() == '%') {
|
|
121
|
+
float value = 0.0f;
|
|
122
|
+
if (!xt_css_parse_number(token.substr(0, token.size() - 1), &value)) return false;
|
|
123
|
+
*offset = value / 100.0f;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Parse the body of an `@keyframes` block into one rule. A selector list such
|
|
130
|
+
* as `0%, 50%` creates one step per offset with the same declarations. */
|
|
131
|
+
void parseKeyframesBlock(const std::string &block, const std::string &prelude,
|
|
132
|
+
StyleSheet *sheet) {
|
|
133
|
+
KeyframesRule rule;
|
|
134
|
+
rule.name = unquote(prelude);
|
|
135
|
+
if (rule.name.empty()) return;
|
|
136
|
+
size_t index = 0;
|
|
137
|
+
while (index < block.size()) {
|
|
138
|
+
while (index < block.size() && xt_is_css_space(block[index])) index++;
|
|
139
|
+
if (index >= block.size()) break;
|
|
140
|
+
size_t brace = block.find('{', index);
|
|
141
|
+
if (brace == std::string::npos) break;
|
|
142
|
+
size_t close = findBlockEnd(block, brace);
|
|
143
|
+
if (close == std::string::npos) break;
|
|
144
|
+
std::string selectors = block.substr(index, brace - index);
|
|
145
|
+
std::vector<Declaration> declarations =
|
|
146
|
+
xt_css_parse_declarations(block.substr(brace + 1, close - brace - 1));
|
|
147
|
+
for (const std::string &part : xt_css_split_commas(selectors)) {
|
|
148
|
+
float offset = 0.0f;
|
|
149
|
+
if (!parseKeyframeOffset(part, &offset)) continue;
|
|
150
|
+
Keyframe keyframe;
|
|
151
|
+
keyframe.offset = offset;
|
|
152
|
+
keyframe.declarations = declarations;
|
|
153
|
+
rule.keyframes.push_back(std::move(keyframe));
|
|
154
|
+
}
|
|
155
|
+
index = close + 1;
|
|
156
|
+
}
|
|
157
|
+
if (rule.keyframes.empty()) return;
|
|
158
|
+
std::stable_sort(rule.keyframes.begin(), rule.keyframes.end(),
|
|
159
|
+
[](const Keyframe &a, const Keyframe &b) { return a.offset < b.offset; });
|
|
160
|
+
sheet->keyframes.push_back(std::move(rule));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Locate `name(` in a lower-cased value, returning the index of `(`. A match
|
|
164
|
+
* inside another identifier (`myurl(`) is rejected. */
|
|
165
|
+
size_t findFunction(const std::string &lower, const char *name, size_t from) {
|
|
166
|
+
const size_t length = std::strlen(name);
|
|
167
|
+
size_t pos = lower.find(name, from);
|
|
168
|
+
while (pos != std::string::npos) {
|
|
169
|
+
const bool boundary =
|
|
170
|
+
pos == 0 || !(std::isalnum((unsigned char)lower[pos - 1]) || lower[pos - 1] == '-' ||
|
|
171
|
+
lower[pos - 1] == '_');
|
|
172
|
+
size_t open = pos + length;
|
|
173
|
+
while (open < lower.size() && std::isspace((unsigned char)lower[open])) open++;
|
|
174
|
+
if (boundary && open < lower.size() && lower[open] == '(') return open;
|
|
175
|
+
pos = lower.find(name, pos + length);
|
|
176
|
+
}
|
|
177
|
+
return std::string::npos;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Extract the argument of a function whose `(` is at `open_paren`. Handles
|
|
181
|
+
* quotes and nested parentheses; trims the result. */
|
|
182
|
+
bool functionArgument(const std::string &text, size_t open_paren, std::string *out,
|
|
183
|
+
size_t *close_paren) {
|
|
184
|
+
int depth = 0;
|
|
185
|
+
char quote = 0;
|
|
186
|
+
for (size_t i = open_paren; i < text.size(); i++) {
|
|
187
|
+
const char c = text[i];
|
|
188
|
+
if (quote != 0) {
|
|
189
|
+
if (c == quote && (i == 0 || text[i - 1] != '\\')) quote = 0;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (c == '"' || c == '\'') {
|
|
193
|
+
quote = c;
|
|
194
|
+
} else if (c == '(') {
|
|
195
|
+
depth++;
|
|
196
|
+
} else if (c == ')') {
|
|
197
|
+
depth--;
|
|
198
|
+
if (depth == 0) {
|
|
199
|
+
*out = xt_css_trim(text.substr(open_paren + 1, i - open_paren - 1));
|
|
200
|
+
*close_paren = i;
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Parse the `src` descriptor: keep every `url(...)` source in order and drop
|
|
209
|
+
* `local(...)`. The `format(...)` hint is retained for diagnostics/future
|
|
210
|
+
* format selection; the runtime simply tries the sources in order. */
|
|
211
|
+
void parseFontFaceSources(const std::string &value, FontFaceRule *rule) {
|
|
212
|
+
rule->sources.clear();
|
|
213
|
+
for (const std::string &part : xt_css_split_commas(value)) {
|
|
214
|
+
const std::string lower = xt_css_lower(part);
|
|
215
|
+
const size_t open = findFunction(lower, "url", 0);
|
|
216
|
+
if (open == std::string::npos) continue;
|
|
217
|
+
std::string url;
|
|
218
|
+
size_t close = 0;
|
|
219
|
+
if (!functionArgument(part, open, &url, &close)) continue;
|
|
220
|
+
url = unquote(url);
|
|
221
|
+
if (url.empty()) continue;
|
|
222
|
+
FontFaceSource source;
|
|
223
|
+
source.url = url;
|
|
224
|
+
const size_t format_open = findFunction(lower, "format", close + 1);
|
|
225
|
+
if (format_open != std::string::npos) {
|
|
226
|
+
std::string format;
|
|
227
|
+
size_t format_close = 0;
|
|
228
|
+
if (functionArgument(part, format_open, &format, &format_close)) {
|
|
229
|
+
source.format = unquote(format);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
rule->sources.push_back(std::move(source));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** `font-weight: normal | bold | <number> [<number>]`, with the range clamped. */
|
|
237
|
+
bool parseFontWeightDescriptor(const std::string &value, int *minimum, int *maximum) {
|
|
238
|
+
const std::string text = xt_css_lower(xt_css_trim(value));
|
|
239
|
+
if (text.empty()) return false;
|
|
240
|
+
if (text == "normal") {
|
|
241
|
+
*minimum = 400;
|
|
242
|
+
*maximum = 400;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
if (text == "bold") {
|
|
246
|
+
*minimum = 700;
|
|
247
|
+
*maximum = 700;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
std::vector<int> weights;
|
|
251
|
+
for (const std::string &token : xt_css_split_whitespace(text)) {
|
|
252
|
+
float number = 0.0f;
|
|
253
|
+
if (!xt_css_parse_number(token, &number)) continue;
|
|
254
|
+
int weight = (int)(number + 0.5f);
|
|
255
|
+
weights.push_back(std::min(1000, std::max(1, weight)));
|
|
256
|
+
}
|
|
257
|
+
if (weights.empty()) return false;
|
|
258
|
+
*minimum = weights.front();
|
|
259
|
+
*maximum = weights.back();
|
|
260
|
+
if (*minimum > *maximum) std::swap(*minimum, *maximum);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Parse one `@font-face` block. A rule without a family name or a local source
|
|
265
|
+
* is ignored. */
|
|
266
|
+
void parseFontFaceBlock(const std::string &block, StyleSheet *sheet) {
|
|
267
|
+
FontFaceRule rule;
|
|
268
|
+
for (const Declaration &declaration : xt_css_parse_declarations(block)) {
|
|
269
|
+
if (declaration.property == "font-family") {
|
|
270
|
+
rule.family = unquote(xt_css_trim(declaration.value));
|
|
271
|
+
} else if (declaration.property == "src") {
|
|
272
|
+
parseFontFaceSources(declaration.value, &rule);
|
|
273
|
+
} else if (declaration.property == "font-weight") {
|
|
274
|
+
int minimum = 400;
|
|
275
|
+
int maximum = 400;
|
|
276
|
+
if (parseFontWeightDescriptor(declaration.value, &minimum, &maximum)) {
|
|
277
|
+
rule.has_weight = true;
|
|
278
|
+
rule.weight_min = minimum;
|
|
279
|
+
rule.weight_max = maximum;
|
|
280
|
+
}
|
|
281
|
+
} else if (declaration.property == "font-style") {
|
|
282
|
+
const std::string text = xt_css_lower(xt_css_trim(declaration.value));
|
|
283
|
+
if (text == "normal") {
|
|
284
|
+
rule.has_italic = true;
|
|
285
|
+
rule.italic = false;
|
|
286
|
+
} else if (text == "italic" || text.rfind("oblique", 0) == 0) {
|
|
287
|
+
rule.has_italic = true;
|
|
288
|
+
rule.italic = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (rule.family.empty() || rule.sources.empty()) return;
|
|
293
|
+
sheet->font_faces.push_back(std::move(rule));
|
|
294
|
+
}
|
|
295
|
+
|
|
95
296
|
/** Parse one rule list, carrying the enclosing `@media` prelude. */
|
|
96
297
|
void parseRules(const std::string &text, const std::string &media, int *order,
|
|
97
298
|
StyleSheet *sheet) {
|
|
@@ -116,9 +317,14 @@ void parseRules(const std::string &text, const std::string &media, int *order,
|
|
|
116
317
|
std::string query = xt_css_trim(text.substr(nameEnd, brace - nameEnd));
|
|
117
318
|
std::string nested = combineMedia(media, query);
|
|
118
319
|
parseRules(text.substr(brace + 1, close - brace - 1), nested, order, sheet);
|
|
320
|
+
} else if (name == "font-face") {
|
|
321
|
+
parseFontFaceBlock(text.substr(brace + 1, close - brace - 1), sheet);
|
|
322
|
+
} else if (name.size() >= 9 && name.compare(name.size() - 9, 9, "keyframes") == 0) {
|
|
323
|
+
parseKeyframesBlock(text.substr(brace + 1, close - brace - 1),
|
|
324
|
+
text.substr(nameEnd, brace - nameEnd), sheet);
|
|
119
325
|
}
|
|
120
|
-
/* Other block at-rules (`@supports`,
|
|
121
|
-
*
|
|
326
|
+
/* Other block at-rules (`@supports`, …) are not part of the engine's
|
|
327
|
+
* subset: ignore the whole block. */
|
|
122
328
|
index = close + 1;
|
|
123
329
|
} else {
|
|
124
330
|
index = (semi == std::string::npos) ? text.size() : semi + 1;
|
|
@@ -152,11 +358,13 @@ void computeSheetFlags(StyleSheet *sheet) {
|
|
|
152
358
|
for (const std::string &pseudo : compound.pseudoClasses) {
|
|
153
359
|
if (pseudo.find("hover") != std::string::npos) sheet->has_hover = true;
|
|
154
360
|
if (pseudo.find("focus") != std::string::npos) sheet->has_focus = true;
|
|
361
|
+
if (pseudo == "active") sheet->has_active = true;
|
|
155
362
|
}
|
|
156
363
|
}
|
|
157
364
|
}
|
|
158
365
|
for (const Declaration &declaration : rule.declarations) {
|
|
159
366
|
if (declaration.property.compare(0, 10, "transition") == 0) sheet->has_transitions = true;
|
|
367
|
+
if (declaration.property.compare(0, 9, "animation") == 0) sheet->has_animations = true;
|
|
160
368
|
}
|
|
161
369
|
}
|
|
162
370
|
}
|
|
@@ -183,6 +391,14 @@ std::vector<Declaration> xt_css_parse_declarations(const std::string &text) {
|
|
|
183
391
|
return declarations;
|
|
184
392
|
}
|
|
185
393
|
|
|
394
|
+
const KeyframesRule *xt_css_find_keyframes(const StyleSheet &sheet, const std::string &name) {
|
|
395
|
+
/* Later `@keyframes` blocks with the same name replace earlier ones. */
|
|
396
|
+
for (size_t i = sheet.keyframes.size(); i-- > 0;) {
|
|
397
|
+
if (sheet.keyframes[i].name == name) return &sheet.keyframes[i];
|
|
398
|
+
}
|
|
399
|
+
return nullptr;
|
|
400
|
+
}
|
|
401
|
+
|
|
186
402
|
StyleSheet xt_css_parse_stylesheet(const std::string &text) {
|
|
187
403
|
StyleSheet sheet;
|
|
188
404
|
std::string clean = stripComments(text);
|