xbintsc 0.3.69 → 0.3.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -4
- package/README.zh-CN.md +51 -2
- package/dist/src/driver/compiler.js +6 -1
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/extensions/gui/index.js +10 -2
- package/dist/src/extensions/gui/index.js.map +1 -1
- package/dist/tests/e2e/gui-advanced.test.d.ts +6 -0
- package/dist/tests/e2e/gui-advanced.test.js +261 -0
- package/dist/tests/e2e/gui-advanced.test.js.map +1 -0
- package/dist/tests/e2e/gui-canvas.test.d.ts +9 -0
- package/dist/tests/e2e/gui-canvas.test.js +233 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -0
- package/dist/tests/e2e/gui-example-svg.test.d.ts +21 -0
- package/dist/tests/e2e/gui-example-svg.test.js +105 -0
- package/dist/tests/e2e/gui-example-svg.test.js.map +1 -0
- package/dist/tests/e2e/gui-example.test.d.ts +1 -1
- package/dist/tests/e2e/gui-example.test.js +2 -2
- package/dist/tests/e2e/gui-helpers.d.ts +3 -0
- package/dist/tests/e2e/gui-helpers.js +5 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-layout.test.js +73 -0
- package/dist/tests/e2e/gui-layout.test.js.map +1 -1
- package/dist/tests/e2e/gui-media.test.d.ts +8 -0
- package/dist/tests/e2e/gui-media.test.js +119 -0
- package/dist/tests/e2e/gui-media.test.js.map +1 -0
- package/dist/tests/e2e/gui-paint.test.js +40 -0
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/dist/tests/e2e/gui-transform.test.d.ts +7 -0
- package/dist/tests/e2e/gui-transform.test.js +198 -0
- package/dist/tests/e2e/gui-transform.test.js.map +1 -0
- package/doc/ai/cli.md +5 -0
- package/doc/ai/extensions.md +20 -3
- package/doc/ai/zh-CN/extensions.md +14 -3
- package/doc/gui/animation.md +97 -0
- package/doc/gui/api.md +92 -0
- package/doc/gui/build.md +91 -0
- package/doc/gui/canvas.md +98 -0
- package/doc/gui/dom.md +128 -0
- package/doc/gui/input.md +25 -0
- package/doc/gui/known-issues.md +78 -0
- package/doc/gui/layout.md +108 -0
- package/doc/gui/media.md +87 -0
- package/doc/gui/rendering.md +113 -0
- package/doc/gui/runtime.md +88 -0
- package/doc/gui-scripts.md +7 -3
- package/doc/gui.md +109 -612
- package/doc/implemented.md +1 -0
- package/doc/zh-CN/gui/animation.md +93 -0
- package/doc/zh-CN/gui/api.md +88 -0
- package/doc/zh-CN/gui/build.md +84 -0
- package/doc/zh-CN/gui/canvas.md +80 -0
- package/doc/zh-CN/gui/dom.md +119 -0
- package/doc/zh-CN/gui/input.md +23 -0
- package/doc/zh-CN/gui/known-issues.md +60 -0
- package/doc/zh-CN/gui/layout.md +91 -0
- package/doc/zh-CN/gui/media.md +71 -0
- package/doc/zh-CN/gui/rendering.md +92 -0
- package/doc/zh-CN/gui/runtime.md +76 -0
- package/doc/zh-CN/gui-scripts.md +6 -2
- package/doc/zh-CN/gui.md +89 -532
- package/doc/zh-CN/implemented.md +1 -0
- package/llms.txt +15 -2
- package/package.json +1 -1
- package/runtime/ext_gui/canvas.cpp +580 -0
- package/runtime/ext_gui/canvas.h +170 -0
- package/runtime/ext_gui/canvas_internal.h +59 -0
- package/runtime/ext_gui/canvas_raster.cpp +254 -0
- package/runtime/ext_gui/document.cpp +45 -4
- package/runtime/ext_gui/document.h +15 -1
- package/runtime/ext_gui/dom_api.cpp +3 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +548 -0
- package/runtime/ext_gui/dom_api_internal.h +73 -0
- package/runtime/ext_gui/dom_api_media.cpp +274 -0
- package/runtime/ext_gui/dom_api_node.cpp +6 -4
- package/runtime/ext_gui/dom_api_proto.cpp +80 -0
- package/runtime/ext_gui/gui.cpp +6 -0
- package/runtime/ext_gui/gui_canvas.cpp +95 -0
- package/runtime/ext_gui/gui_engine.h +49 -0
- package/runtime/ext_gui/gui_events.cpp +21 -5
- package/runtime/ext_gui/gui_media.cpp +193 -0
- package/runtime/ext_gui/image.cpp +31 -12
- package/runtime/ext_gui/image.h +20 -7
- package/runtime/ext_gui/layout.cpp +130 -6
- package/runtime/ext_gui/layout.h +54 -1
- package/runtime/ext_gui/layout_flex.cpp +263 -99
- package/runtime/ext_gui/layout_flow.cpp +85 -9
- package/runtime/ext_gui/layout_flow_internal.h +84 -5
- package/runtime/ext_gui/layout_inline.cpp +54 -37
- package/runtime/ext_gui/media.cpp +514 -0
- package/runtime/ext_gui/media.h +104 -0
- package/runtime/ext_gui/nanosvg.h +3278 -0
- package/runtime/ext_gui/nanosvgrast.h +1472 -0
- package/runtime/ext_gui/paint.cpp +156 -38
- package/runtime/ext_gui/paint.h +42 -6
- package/runtime/ext_gui/renderer.cpp +117 -12
- package/runtime/ext_gui/renderer.h +49 -9
- package/runtime/ext_gui/renderer_image.cpp +205 -78
- package/runtime/ext_gui/renderer_internal.h +12 -9
- package/runtime/ext_gui/renderer_text.cpp +46 -39
- package/runtime/ext_gui/style.h +21 -0
- package/runtime/ext_gui/style_print.cpp +17 -0
- package/runtime/ext_gui/style_ua.cpp +7 -0
- package/runtime/ext_gui/style_values.cpp +83 -1
- package/runtime/ext_gui/svg.cpp +182 -0
- package/runtime/ext_gui/svg.h +46 -0
- package/runtime/ext_gui/text.cpp +156 -25
- package/runtime/ext_gui/text.h +11 -2
- package/runtime/ext_gui/transform.cpp +412 -0
- package/runtime/ext_gui/transform.h +112 -0
- package/runtime/ext_gui/window.cpp +10 -5
- package/scripts/build-gui.ts +148 -6
- package/scripts/check-file-length.ts +5 -1
- package/scripts/install-gui-deps.sh +28 -0
- package/src/driver/compiler.ts +6 -1
- package/src/extensions/gui/index.ts +10 -2
|
@@ -347,6 +347,23 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
|
|
|
347
347
|
if (parseEnum(value, names, 4, &index)) style->flex_direction = (FlexDirection)index;
|
|
348
348
|
return;
|
|
349
349
|
}
|
|
350
|
+
if (property == "flex-wrap") {
|
|
351
|
+
static const char *const names[] = {"nowrap", "wrap", "wrap-reverse"};
|
|
352
|
+
int index = 0;
|
|
353
|
+
if (parseEnum(value, names, 3, &index)) style->flex_wrap = (FlexWrap)index;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (property == "flex-flow") {
|
|
357
|
+
/* `flex-flow: <direction> || <wrap>` in either order. */
|
|
358
|
+
for (const std::string &token : xt_css_split_whitespace(value)) {
|
|
359
|
+
static const char *const dirs[] = {"row", "row-reverse", "column", "column-reverse"};
|
|
360
|
+
static const char *const wraps[] = {"nowrap", "wrap", "wrap-reverse"};
|
|
361
|
+
int index = 0;
|
|
362
|
+
if (parseEnum(token, dirs, 4, &index)) style->flex_direction = (FlexDirection)index;
|
|
363
|
+
else if (parseEnum(token, wraps, 3, &index)) style->flex_wrap = (FlexWrap)index;
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
350
367
|
if (property == "justify-content") {
|
|
351
368
|
static const char *const names[] = {"flex-start", "flex-end", "center", "space-between",
|
|
352
369
|
"space-around", "space-evenly"};
|
|
@@ -360,10 +377,20 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
|
|
|
360
377
|
if (parseEnum(value, names, 5, &index)) style->align_items = (AlignItems)index;
|
|
361
378
|
return;
|
|
362
379
|
}
|
|
363
|
-
if (property == "
|
|
380
|
+
if (property == "align-content") {
|
|
381
|
+
static const char *const names[] = {"stretch", "flex-start", "flex-end", "center",
|
|
382
|
+
"space-between", "space-around"};
|
|
383
|
+
int index = 0;
|
|
384
|
+
if (parseEnum(value, names, 6, &index)) style->align_content = (AlignContent)index;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (property == "gap") {
|
|
364
388
|
assignLength(&style->gap, value);
|
|
389
|
+
style->row_gap = style->column_gap = style->gap;
|
|
365
390
|
return;
|
|
366
391
|
}
|
|
392
|
+
if (property == "row-gap") { assignLength(&style->row_gap, value); return; }
|
|
393
|
+
if (property == "column-gap") { assignLength(&style->column_gap, value); return; }
|
|
367
394
|
if (property == "flex-grow") {
|
|
368
395
|
float number = 0;
|
|
369
396
|
if (xt_css_parse_number(value, &number)) style->flex_grow = number;
|
|
@@ -414,6 +441,49 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
|
|
|
414
441
|
return;
|
|
415
442
|
}
|
|
416
443
|
|
|
444
|
+
if (property == "transform") {
|
|
445
|
+
std::vector<TransformOp> ops;
|
|
446
|
+
if (xt_transform_parse(value, &ops)) style->transform = ops;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (property == "transform-origin") {
|
|
450
|
+
/* One or two values plus an optional z offset (ignored: the engine is 2D).
|
|
451
|
+
* The keywords carry an axis, so `top` alone means `center top` and
|
|
452
|
+
* `top center` is the same as `center top`. */
|
|
453
|
+
std::vector<std::string> parts = xt_css_split_whitespace(value);
|
|
454
|
+
Length values[2] = {Length{50.0f, Unit::Percent}, Length{50.0f, Unit::Percent}};
|
|
455
|
+
bool vertical[2] = {false, false};
|
|
456
|
+
size_t used = 0;
|
|
457
|
+
for (const std::string &part : parts) {
|
|
458
|
+
if (used >= 2) break;
|
|
459
|
+
std::string keyword = xt_css_lower(part);
|
|
460
|
+
Length length = xt_css_parse_length(keyword);
|
|
461
|
+
if (length.unit == Unit::Invalid || length.unit == Unit::Auto || length.unit == Unit::None) {
|
|
462
|
+
if (keyword == "left" || keyword == "top") length = {0.0f, Unit::Percent};
|
|
463
|
+
else if (keyword == "center") length = {50.0f, Unit::Percent};
|
|
464
|
+
else if (keyword == "right" || keyword == "bottom") length = {100.0f, Unit::Percent};
|
|
465
|
+
else return;
|
|
466
|
+
vertical[used] = keyword == "top" || keyword == "bottom";
|
|
467
|
+
}
|
|
468
|
+
values[used++] = length;
|
|
469
|
+
}
|
|
470
|
+
if (used == 0) return;
|
|
471
|
+
Length x = values[0];
|
|
472
|
+
Length y = used > 1 ? values[1] : Length{50.0f, Unit::Percent};
|
|
473
|
+
if (used > 1) {
|
|
474
|
+
if (vertical[0] && !vertical[1]) {
|
|
475
|
+
x = values[1];
|
|
476
|
+
y = values[0];
|
|
477
|
+
}
|
|
478
|
+
} else if (vertical[0]) {
|
|
479
|
+
/* `top`/`bottom` on their own keep x centred. */
|
|
480
|
+
x = Length{50.0f, Unit::Percent};
|
|
481
|
+
y = values[0];
|
|
482
|
+
}
|
|
483
|
+
style->transform_origin_x = x;
|
|
484
|
+
style->transform_origin_y = y;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
417
487
|
if (property == "opacity") {
|
|
418
488
|
float number = 0;
|
|
419
489
|
if (xt_css_parse_number(value, &number)) style->opacity = number < 0 ? 0 : (number > 1 ? 1 : number);
|
|
@@ -433,6 +503,18 @@ void xt_style_apply_declaration(XtStyle *style, const std::string &property,
|
|
|
433
503
|
}
|
|
434
504
|
return;
|
|
435
505
|
}
|
|
506
|
+
if (property == "float") {
|
|
507
|
+
static const char *const names[] = {"none", "left", "right"};
|
|
508
|
+
int index = 0;
|
|
509
|
+
if (parseEnum(value, names, 3, &index)) style->float_side = (FloatSide)index;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (property == "clear") {
|
|
513
|
+
static const char *const names[] = {"none", "left", "right", "both"};
|
|
514
|
+
int index = 0;
|
|
515
|
+
if (parseEnum(value, names, 4, &index)) style->clear = (Clear)index;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
436
518
|
if (property == "transition") {
|
|
437
519
|
parseTransitionShorthand(value, &style->transitions);
|
|
438
520
|
return;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — SVG decoding. See `svg.h` for the contract.
|
|
3
|
+
*
|
|
4
|
+
* The heavy lifting is the vendored NanoSVG library (`nanosvg.h` +
|
|
5
|
+
* `nanosvgrast.h`, zlib licence, fetched verbatim). This file is only the glue:
|
|
6
|
+
* path detection, a parsed-document cache and the scale-to-fit call. NanoSVG
|
|
7
|
+
* emits straight (non-premultiplied) RGBA8, which is exactly the `Image`
|
|
8
|
+
* layout the rest of the engine expects.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
#include "svg.h"
|
|
12
|
+
|
|
13
|
+
#define NANOSVG_IMPLEMENTATION
|
|
14
|
+
#define NANOSVGRAST_IMPLEMENTATION
|
|
15
|
+
/* NanoSVG is vendored verbatim, so silence the one warning it raises under
|
|
16
|
+
* `-Wall` instead of patching the upstream sources. */
|
|
17
|
+
#if defined(__clang__)
|
|
18
|
+
#pragma clang diagnostic push
|
|
19
|
+
#pragma clang diagnostic ignored "-Wsign-compare"
|
|
20
|
+
#elif defined(__GNUC__)
|
|
21
|
+
#pragma GCC diagnostic push
|
|
22
|
+
#pragma GCC diagnostic ignored "-Wsign-compare"
|
|
23
|
+
#endif
|
|
24
|
+
#include "nanosvg.h"
|
|
25
|
+
#include "nanosvgrast.h"
|
|
26
|
+
#if defined(__clang__)
|
|
27
|
+
#pragma clang diagnostic pop
|
|
28
|
+
#elif defined(__GNUC__)
|
|
29
|
+
#pragma GCC diagnostic pop
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
#include <algorithm>
|
|
33
|
+
#include <cctype>
|
|
34
|
+
#include <cstdio>
|
|
35
|
+
#include <fstream>
|
|
36
|
+
#include <string>
|
|
37
|
+
#include <unordered_map>
|
|
38
|
+
|
|
39
|
+
namespace xtgui {
|
|
40
|
+
namespace {
|
|
41
|
+
|
|
42
|
+
/** NanoSVG's default when the SVG declares neither width/height nor a viewBox
|
|
43
|
+
* (SVG 1.1 says a replaced element falls back to 300x150). */
|
|
44
|
+
constexpr float kDefaultWidth = 300.0f;
|
|
45
|
+
constexpr float kDefaultHeight = 150.0f;
|
|
46
|
+
/** Refuse to allocate a monstrous texture if an `<img>` is scaled absurdly. */
|
|
47
|
+
constexpr int kMaxDimension = 8192;
|
|
48
|
+
|
|
49
|
+
std::string lowercase(std::string value) {
|
|
50
|
+
for (char &c : value) c = (char)std::tolower((unsigned char)c);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True when the file name ends in `.svg` (case-insensitive). */
|
|
55
|
+
bool hasSvgExtension(const std::string &path) {
|
|
56
|
+
if (path.size() < 4) return false;
|
|
57
|
+
return lowercase(path.substr(path.size() - 4)) == ".svg";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Cheap content sniff for a file whose extension is not enough (no extension,
|
|
61
|
+
* or a generated temporary). Looks for `<svg` in the first chunk, which is
|
|
62
|
+
* present after any XML declaration, doctype or leading comment of a sane
|
|
63
|
+
* size. */
|
|
64
|
+
bool prefixLooksLikeSvg(const std::string &path) {
|
|
65
|
+
std::ifstream in(path, std::ios::binary);
|
|
66
|
+
if (!in) return false;
|
|
67
|
+
char buffer[1024];
|
|
68
|
+
in.read(buffer, sizeof(buffer));
|
|
69
|
+
std::streamsize size = in.gcount();
|
|
70
|
+
if (size <= 0) return false;
|
|
71
|
+
std::string prefix(buffer, (size_t)size);
|
|
72
|
+
return prefix.find("<svg") != std::string::npos;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
std::unordered_map<std::string, NSVGimage *> &parsedCache() {
|
|
76
|
+
static std::unordered_map<std::string, NSVGimage *> cache;
|
|
77
|
+
return cache;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Remember the extension/sniff verdict so a raster image is not reopened on
|
|
81
|
+
* every frame (the loader asks for every `<img>` each time it paints). */
|
|
82
|
+
std::unordered_map<std::string, bool> &detectCache() {
|
|
83
|
+
static std::unordered_map<std::string, bool> cache;
|
|
84
|
+
return cache;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
NSVGrasterizer *&rasterizer() {
|
|
88
|
+
static NSVGrasterizer *value = nullptr;
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Parse `path` once and remember the result (including failures, so a missing
|
|
93
|
+
* file is not retried every frame). */
|
|
94
|
+
NSVGimage *parseCached(const std::string &path) {
|
|
95
|
+
auto &cache = parsedCache();
|
|
96
|
+
auto found = cache.find(path);
|
|
97
|
+
if (found != cache.end()) return found->second;
|
|
98
|
+
NSVGimage *image = nsvgParseFromFile(path.c_str(), "px", 96.0f);
|
|
99
|
+
cache.emplace(path, image);
|
|
100
|
+
return image;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
float intrinsicWidth(const NSVGimage *image) {
|
|
104
|
+
return image->width > 0.0f ? image->width : kDefaultWidth;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
float intrinsicHeight(const NSVGimage *image) {
|
|
108
|
+
return image->height > 0.0f ? image->height : kDefaultHeight;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} // namespace
|
|
112
|
+
|
|
113
|
+
bool xt_svg_is_svg(const std::string &path) {
|
|
114
|
+
auto &cache = detectCache();
|
|
115
|
+
auto found = cache.find(path);
|
|
116
|
+
if (found != cache.end()) return found->second;
|
|
117
|
+
bool result = hasSvgExtension(path) || prefixLooksLikeSvg(path);
|
|
118
|
+
cache.emplace(path, result);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
bool xt_svg_size(const std::string &path, int *width, int *height) {
|
|
123
|
+
NSVGimage *image = parseCached(path);
|
|
124
|
+
if (image == nullptr) return false;
|
|
125
|
+
float w = intrinsicWidth(image);
|
|
126
|
+
float h = intrinsicHeight(image);
|
|
127
|
+
if (w <= 0.0f || h <= 0.0f) return false;
|
|
128
|
+
if (width != nullptr) *width = (int)(w + 0.5f);
|
|
129
|
+
if (height != nullptr) *height = (int)(h + 0.5f);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
bool xt_svg_render(const std::string &path, int width, int height, Image *out) {
|
|
134
|
+
if (out == nullptr) return false;
|
|
135
|
+
NSVGimage *image = parseCached(path);
|
|
136
|
+
if (image == nullptr) return false;
|
|
137
|
+
|
|
138
|
+
float source_width = intrinsicWidth(image);
|
|
139
|
+
float source_height = intrinsicHeight(image);
|
|
140
|
+
int target_width = width > 0 ? width : (int)(source_width + 0.5f);
|
|
141
|
+
int target_height = height > 0 ? height : (int)(source_height + 0.5f);
|
|
142
|
+
if (target_width <= 0 || target_height <= 0) return false;
|
|
143
|
+
target_width = std::min(target_width, kMaxDimension);
|
|
144
|
+
target_height = std::min(target_height, kMaxDimension);
|
|
145
|
+
|
|
146
|
+
/* Rasterise at the source aspect ratio, at the larger of the two axis scales
|
|
147
|
+
* (so neither axis is under-sampled), then let the quad stretch the texture
|
|
148
|
+
* to the box — exactly how a bitmap would behave under CSS `object-fit:
|
|
149
|
+
* fill`. The common case (layout preserved the aspect ratio) makes this equal
|
|
150
|
+
* to the requested size. */
|
|
151
|
+
float scale = std::max((float)target_width / source_width, (float)target_height / source_height);
|
|
152
|
+
scale = std::min(scale, (float)kMaxDimension / source_width);
|
|
153
|
+
scale = std::min(scale, (float)kMaxDimension / source_height);
|
|
154
|
+
int raster_width = (int)(source_width * scale + 0.5f);
|
|
155
|
+
int raster_height = (int)(source_height * scale + 0.5f);
|
|
156
|
+
if (raster_width < 1 || raster_height < 1) return false;
|
|
157
|
+
|
|
158
|
+
NSVGrasterizer *&rast = rasterizer();
|
|
159
|
+
if (rast == nullptr) rast = nsvgCreateRasterizer();
|
|
160
|
+
if (rast == nullptr) return false;
|
|
161
|
+
|
|
162
|
+
out->width = raster_width;
|
|
163
|
+
out->height = raster_height;
|
|
164
|
+
out->pixels.assign((size_t)raster_width * (size_t)raster_height * 4u, 0);
|
|
165
|
+
nsvgRasterize(rast, image, 0.0f, 0.0f, scale, out->pixels.data(), raster_width, raster_height,
|
|
166
|
+
raster_width * 4);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
void xt_svg_shutdown() {
|
|
171
|
+
for (auto &entry : parsedCache()) {
|
|
172
|
+
if (entry.second != nullptr) nsvgDelete(entry.second);
|
|
173
|
+
}
|
|
174
|
+
parsedCache().clear();
|
|
175
|
+
detectCache().clear();
|
|
176
|
+
if (rasterizer() != nullptr) {
|
|
177
|
+
nsvgDeleteRasterizer(rasterizer());
|
|
178
|
+
rasterizer() = nullptr;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
} // namespace xtgui
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — SVG decoding (NanoSVG wrapper).
|
|
3
|
+
*
|
|
4
|
+
* An SVG file is not a raster, so `stb_image` cannot decode it. This module
|
|
5
|
+
* sits next to `image.{h,cpp}` and turns an SVG file into the same RGBA8
|
|
6
|
+
* `Image` the rest of the image pipeline already consumes: parse once (cached
|
|
7
|
+
* per path) and rasterise into a texture-sized buffer on demand. The parse and
|
|
8
|
+
* the anti-aliased scanline rasteriser are the vendored NanoSVG library
|
|
9
|
+
* (`nanosvg.h` + `nanosvgrast.h`) — the same class of low-level, third-party
|
|
10
|
+
* image library `stb_image` is (see doc/gui.md).
|
|
11
|
+
*
|
|
12
|
+
* Only *files* are handled: `<img src="logo.svg">`. Inline `<svg>` trees in a
|
|
13
|
+
* document are still ordinary unknown elements to the HTML parser and generate
|
|
14
|
+
* no boxes.
|
|
15
|
+
*/
|
|
16
|
+
#ifndef XT_GUI_SVG_H
|
|
17
|
+
#define XT_GUI_SVG_H
|
|
18
|
+
|
|
19
|
+
#include <string>
|
|
20
|
+
|
|
21
|
+
#include "image.h"
|
|
22
|
+
|
|
23
|
+
namespace xtgui {
|
|
24
|
+
|
|
25
|
+
/** True when `path` names an SVG file: a `.svg` extension (case-insensitive),
|
|
26
|
+
* or a small prefix of the contents that looks like an SVG root element. */
|
|
27
|
+
bool xt_svg_is_svg(const std::string &path);
|
|
28
|
+
|
|
29
|
+
/** Read the intrinsic size from an SVG file without rasterising it. Returns
|
|
30
|
+
* false when the file is missing or cannot be parsed. Cached per path. */
|
|
31
|
+
bool xt_svg_size(const std::string &path, int *width, int *height);
|
|
32
|
+
|
|
33
|
+
/** Rasterise `path` to RGBA8. `width`/`height` are the requested pixel size;
|
|
34
|
+
* when either is `<= 0` the intrinsic SVG size is used. The viewBox is scaled
|
|
35
|
+
* uniformly at the larger of the two axis scales (preserving the SVG's aspect
|
|
36
|
+
* ratio, so the caller's quad may stretch it exactly as it would a bitmap) and
|
|
37
|
+
* clamped so neither axis exceeds 8192px. Returns false on parse/rasteriser
|
|
38
|
+
* failure. The parse is cached per path; each rasterisation is fresh. */
|
|
39
|
+
bool xt_svg_render(const std::string &path, int width, int height, Image *out);
|
|
40
|
+
|
|
41
|
+
/** Drop the parsed-document cache (called on engine shutdown). */
|
|
42
|
+
void xt_svg_shutdown();
|
|
43
|
+
|
|
44
|
+
} // namespace xtgui
|
|
45
|
+
|
|
46
|
+
#endif /* XT_GUI_SVG_H */
|
package/runtime/ext_gui/text.cpp
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* HarfBuzz does shaping (ligatures/kerning/complex scripts), FreeType owns the
|
|
5
5
|
* glyph outlines and the vertical metrics. We create one HarfBuzz font per
|
|
6
6
|
* (file, pixel size) and scale it so that advances come back in 26.6 pixels.
|
|
7
|
+
*
|
|
8
|
+
* A run is shaped with a *single* face, but that face rarely covers every
|
|
9
|
+
* codepoint in it — a CJK face such as Microsoft YaHei / PingFang / Noto Sans
|
|
10
|
+
* CJK has no geometric shapes (U+25AE ▮, U+25AF ▯, …), and a Latin face has no
|
|
11
|
+
* CJK. Each codepoint therefore resolves to the first face in a small fallback
|
|
12
|
+
* chain that has it, the run is shaped in segments (one per face), and each
|
|
13
|
+
* shaped glyph carries the face it came from so the rasteriser draws it from
|
|
14
|
+
* the right atlas entry. Without this a missing glyph came back as `.notdef`
|
|
15
|
+
* (glyph 0) and the renderer silently dropped it.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
#include "text.h"
|
|
@@ -22,6 +31,7 @@
|
|
|
22
31
|
#include <memory>
|
|
23
32
|
#include <string>
|
|
24
33
|
#include <unordered_map>
|
|
34
|
+
#include <vector>
|
|
25
35
|
|
|
26
36
|
namespace xtgui {
|
|
27
37
|
|
|
@@ -30,6 +40,8 @@ namespace xtgui {
|
|
|
30
40
|
struct Font {
|
|
31
41
|
FT_Face ft_face = nullptr;
|
|
32
42
|
hb_font_t *hb_font = nullptr;
|
|
43
|
+
std::string path;
|
|
44
|
+
bool monospace = false;
|
|
33
45
|
float pixel_size = 16.0f;
|
|
34
46
|
float ascent = 0;
|
|
35
47
|
float descent = 0;
|
|
@@ -85,13 +97,33 @@ const char *const kMonoCandidates[] = {
|
|
|
85
97
|
"C:\\Windows\\Fonts\\cour.ttf",
|
|
86
98
|
};
|
|
87
99
|
|
|
100
|
+
/* Faces tried after the primary one for codepoints it does not cover. Symbol
|
|
101
|
+
* faces come first (they carry the geometric shapes, arrows, box drawing and
|
|
102
|
+
* dingbats the CJK/Latin primary often lacks), then the other family class so
|
|
103
|
+
* a monospace run still gets CJK and a sans run still gets monospace-only
|
|
104
|
+
* codepoints. The primary and duplicates are dropped when the chain is built. */
|
|
105
|
+
const char *const kSymbolCandidates[] = {
|
|
106
|
+
nullptr,
|
|
107
|
+
"C:\\Windows\\Fonts\\seguisym.ttf", /* Segoe UI Symbol */
|
|
108
|
+
"C:\\Windows\\Fonts\\arialuni.ttf", /* Arial Unicode MS */
|
|
109
|
+
"/System/Library/Fonts/Apple Symbols.ttf",
|
|
110
|
+
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
|
111
|
+
"/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf",
|
|
112
|
+
"/usr/share/fonts/truetype/noto/NotoSansSymbols-Regular.ttf",
|
|
113
|
+
"/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf",
|
|
114
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
115
|
+
};
|
|
116
|
+
|
|
88
117
|
struct Engine {
|
|
89
118
|
bool initialized = false;
|
|
90
119
|
FT_Library ft = nullptr;
|
|
91
120
|
bool ft_ok = false;
|
|
92
121
|
std::string sans_path;
|
|
93
122
|
std::string mono_path;
|
|
123
|
+
std::vector<std::string> sans_fallbacks;
|
|
124
|
+
std::vector<std::string> mono_fallbacks;
|
|
94
125
|
std::unordered_map<std::string, std::unique_ptr<Font>> cache; // key -> face (may be null)
|
|
126
|
+
std::unordered_map<std::string, std::vector<Font *>> fallbacks; // class+size -> faces
|
|
95
127
|
std::unordered_map<std::string, hb_face_t *> hb_faces; // path -> face
|
|
96
128
|
};
|
|
97
129
|
|
|
@@ -182,6 +214,25 @@ const char *firstExisting(const char *const *candidates, size_t count) {
|
|
|
182
214
|
return nullptr;
|
|
183
215
|
}
|
|
184
216
|
|
|
217
|
+
void addFallbackPath(std::vector<std::string> &paths, const char *path) {
|
|
218
|
+
if (path == nullptr || !fileExists(path)) return;
|
|
219
|
+
for (const std::string &entry : paths) {
|
|
220
|
+
if (entry == path) return;
|
|
221
|
+
}
|
|
222
|
+
paths.push_back(path);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* The chain for a primary face: every existing symbol face, then the other
|
|
226
|
+
* family class (CJK coverage for a monospace primary and vice versa). The
|
|
227
|
+
* primary itself is skipped because it is tried first, per codepoint. */
|
|
228
|
+
void buildFallbackChain(std::vector<std::string> &paths, const std::string &primary,
|
|
229
|
+
const std::string &otherClass) {
|
|
230
|
+
for (const char *candidate : kSymbolCandidates) {
|
|
231
|
+
if (candidate != nullptr && primary != candidate) addFallbackPath(paths, candidate);
|
|
232
|
+
}
|
|
233
|
+
if (!otherClass.empty() && otherClass != primary) addFallbackPath(paths, otherClass.c_str());
|
|
234
|
+
}
|
|
235
|
+
|
|
185
236
|
void ensureInitialized() {
|
|
186
237
|
Engine &e = engine();
|
|
187
238
|
if (e.initialized) return;
|
|
@@ -214,6 +265,9 @@ void ensureInitialized() {
|
|
|
214
265
|
}
|
|
215
266
|
/* No dedicated monospace face: reuse the sans face so metrics stay real. */
|
|
216
267
|
if (e.mono_path.empty()) e.mono_path = e.sans_path;
|
|
268
|
+
|
|
269
|
+
buildFallbackChain(e.sans_fallbacks, e.sans_path, e.mono_path);
|
|
270
|
+
buildFallbackChain(e.mono_fallbacks, e.mono_path, e.sans_path);
|
|
217
271
|
}
|
|
218
272
|
|
|
219
273
|
hb_face_t *hbFaceForPath(const std::string &path) {
|
|
@@ -230,7 +284,7 @@ hb_face_t *hbFaceForPath(const std::string &path) {
|
|
|
230
284
|
return face;
|
|
231
285
|
}
|
|
232
286
|
|
|
233
|
-
std::unique_ptr<Font> createFont(const std::string &path, float pixel_size) {
|
|
287
|
+
std::unique_ptr<Font> createFont(const std::string &path, float pixel_size, bool monospace) {
|
|
234
288
|
Engine &e = engine();
|
|
235
289
|
if (!e.ft_ok || path.empty()) return nullptr;
|
|
236
290
|
|
|
@@ -240,6 +294,8 @@ std::unique_ptr<Font> createFont(const std::string &path, float pixel_size) {
|
|
|
240
294
|
|
|
241
295
|
auto font = std::make_unique<Font>();
|
|
242
296
|
font->ft_face = face;
|
|
297
|
+
font->path = path;
|
|
298
|
+
font->monospace = monospace;
|
|
243
299
|
font->pixel_size = pixel_size;
|
|
244
300
|
|
|
245
301
|
FT_Size_Metrics metrics = face->size->metrics;
|
|
@@ -259,43 +315,63 @@ std::unique_ptr<Font> createFont(const std::string &path, float pixel_size) {
|
|
|
259
315
|
return font;
|
|
260
316
|
}
|
|
261
317
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
Font
|
|
265
|
-
|
|
318
|
+
/* Resolve one face by path, keyed by path+class+size so the primary resolved
|
|
319
|
+
* through a `FontSpec` and the same face resolved as a fallback share one
|
|
320
|
+
* `Font` (and one glyph-atlas key). */
|
|
321
|
+
Font *resolveFacePath(const std::string &path, float pixel_size, bool monospace) {
|
|
266
322
|
Engine &e = engine();
|
|
267
|
-
if (!e.ft_ok) return nullptr;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (path.empty()) return nullptr;
|
|
272
|
-
|
|
273
|
-
char key[80];
|
|
274
|
-
std::snprintf(key, sizeof(key), "%c|%.2f", mono ? 'm' : 's', (double)spec.pixel_size);
|
|
323
|
+
if (!e.ft_ok || path.empty()) return nullptr;
|
|
324
|
+
char key[512];
|
|
325
|
+
std::snprintf(key, sizeof(key), "p|%c|%.2f|%s", monospace ? 'm' : 's', (double)pixel_size,
|
|
326
|
+
path.c_str());
|
|
275
327
|
auto it = e.cache.find(key);
|
|
276
328
|
if (it != e.cache.end()) return it->second.get();
|
|
277
|
-
|
|
278
|
-
std::unique_ptr<Font> font = createFont(path, spec.pixel_size);
|
|
329
|
+
std::unique_ptr<Font> font = createFont(path, pixel_size, monospace);
|
|
279
330
|
Font *raw = font.get();
|
|
280
331
|
e.cache.emplace(key, std::move(font));
|
|
281
332
|
return raw;
|
|
282
333
|
}
|
|
283
334
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
335
|
+
/* The resolved fallback faces for a primary, in order, cached per class+size. */
|
|
336
|
+
const std::vector<Font *> &fallbackFaces(Font *font) {
|
|
337
|
+
static const std::vector<Font *> kEmpty;
|
|
338
|
+
if (font == nullptr) return kEmpty;
|
|
339
|
+
Engine &e = engine();
|
|
340
|
+
char key[80];
|
|
341
|
+
std::snprintf(key, sizeof(key), "f|%c|%.2f", font->monospace ? 'm' : 's',
|
|
342
|
+
(double)font->pixel_size);
|
|
343
|
+
auto it = e.fallbacks.find(key);
|
|
344
|
+
if (it != e.fallbacks.end()) return it->second;
|
|
345
|
+
|
|
346
|
+
const std::vector<std::string> &paths =
|
|
347
|
+
font->monospace ? e.mono_fallbacks : e.sans_fallbacks;
|
|
348
|
+
std::vector<Font *> faces;
|
|
349
|
+
for (const std::string &path : paths) {
|
|
350
|
+
Font *face = resolveFacePath(path, font->pixel_size, font->monospace);
|
|
351
|
+
if (face != nullptr) faces.push_back(face);
|
|
352
|
+
}
|
|
353
|
+
return e.fallbacks.emplace(key, std::move(faces)).first->second;
|
|
289
354
|
}
|
|
290
355
|
|
|
291
|
-
|
|
292
|
-
|
|
356
|
+
/* The face that draws `code_point`: the primary when it has the glyph, else the
|
|
357
|
+
* first fallback that does, else the primary (so `.notdef` is still skipped). */
|
|
358
|
+
Font *faceForCodePoint(Font *primary, uint32_t code_point) {
|
|
359
|
+
if (primary != nullptr && FT_Get_Char_Index(primary->ft_face, code_point) != 0) return primary;
|
|
360
|
+
for (Font *fallback : fallbackFaces(primary)) {
|
|
361
|
+
if (FT_Get_Char_Index(fallback->ft_face, code_point) != 0) return fallback;
|
|
362
|
+
}
|
|
363
|
+
return primary;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* Shape one face's segment of a run and append its glyphs. */
|
|
367
|
+
float shapeSegment(const std::string &segment, Font *font, std::vector<ShapedGlyph> &out) {
|
|
368
|
+
if (font == nullptr || segment.empty()) return 0;
|
|
293
369
|
float total = 0;
|
|
294
370
|
if (font->hb_font == nullptr) {
|
|
295
371
|
/* HarfBuzz unavailable: fall back to FreeType's per-codepoint advances. */
|
|
296
372
|
size_t index = 0;
|
|
297
|
-
while (index <
|
|
298
|
-
uint32_t code_point = decodeUtf8(
|
|
373
|
+
while (index < segment.size()) {
|
|
374
|
+
uint32_t code_point = decodeUtf8(segment, index);
|
|
299
375
|
FT_UInt glyph = FT_Get_Char_Index(font->ft_face, code_point);
|
|
300
376
|
float advance = 0;
|
|
301
377
|
if (FT_Load_Glyph(font->ft_face, glyph, FT_LOAD_DEFAULT) == 0) {
|
|
@@ -304,6 +380,7 @@ float xt_text_shape_run(const std::string &utf8, Font *font, std::vector<ShapedG
|
|
|
304
380
|
ShapedGlyph shaped;
|
|
305
381
|
shaped.glyph = glyph;
|
|
306
382
|
shaped.x_advance = advance;
|
|
383
|
+
shaped.font = font;
|
|
307
384
|
total += advance;
|
|
308
385
|
out.push_back(shaped);
|
|
309
386
|
}
|
|
@@ -311,7 +388,7 @@ float xt_text_shape_run(const std::string &utf8, Font *font, std::vector<ShapedG
|
|
|
311
388
|
}
|
|
312
389
|
|
|
313
390
|
hb_buffer_t *buffer = hb_buffer_create();
|
|
314
|
-
hb_buffer_add_utf8(buffer,
|
|
391
|
+
hb_buffer_add_utf8(buffer, segment.c_str(), (int)segment.size(), 0, -1);
|
|
315
392
|
hb_buffer_guess_segment_properties(buffer);
|
|
316
393
|
hb_shape(font->hb_font, buffer, nullptr, 0);
|
|
317
394
|
unsigned count = hb_buffer_get_length(buffer);
|
|
@@ -323,6 +400,7 @@ float xt_text_shape_run(const std::string &utf8, Font *font, std::vector<ShapedG
|
|
|
323
400
|
shaped.x_advance = positions[i].x_advance / 64.0f;
|
|
324
401
|
shaped.x_offset = positions[i].x_offset / 64.0f;
|
|
325
402
|
shaped.y_offset = positions[i].y_offset / 64.0f;
|
|
403
|
+
shaped.font = font;
|
|
326
404
|
total += shaped.x_advance;
|
|
327
405
|
out.push_back(shaped);
|
|
328
406
|
}
|
|
@@ -330,6 +408,56 @@ float xt_text_shape_run(const std::string &utf8, Font *font, std::vector<ShapedG
|
|
|
330
408
|
return total;
|
|
331
409
|
}
|
|
332
410
|
|
|
411
|
+
} // namespace
|
|
412
|
+
|
|
413
|
+
Font *xt_text_resolve(const FontSpec &spec) {
|
|
414
|
+
ensureInitialized();
|
|
415
|
+
Engine &e = engine();
|
|
416
|
+
if (!e.ft_ok) return nullptr;
|
|
417
|
+
|
|
418
|
+
bool mono = isMonospaceFamily(spec.family);
|
|
419
|
+
const std::string &path = mono ? e.mono_path : e.sans_path;
|
|
420
|
+
if (path.empty()) return nullptr;
|
|
421
|
+
return resolveFacePath(path, spec.pixel_size, mono);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
Font *xt_text_scale(Font *font, float pixel_size) {
|
|
425
|
+
if (font == nullptr) return nullptr;
|
|
426
|
+
return resolveFacePath(font->path, pixel_size, font->monospace);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
float xt_text_measure_width(const std::string &utf8, const FontSpec &spec) {
|
|
430
|
+
Font *font = xt_text_resolve(spec);
|
|
431
|
+
if (font == nullptr) return approximateWidth(utf8, spec.pixel_size, spec.family);
|
|
432
|
+
std::vector<ShapedGlyph> glyphs;
|
|
433
|
+
return xt_text_shape_run(utf8, font, glyphs);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
float xt_text_shape_run(const std::string &utf8, Font *font, std::vector<ShapedGlyph> &out) {
|
|
437
|
+
if (font == nullptr) return 0;
|
|
438
|
+
float total = 0;
|
|
439
|
+
size_t index = 0;
|
|
440
|
+
size_t segment_start = 0;
|
|
441
|
+
Font *segment_font = nullptr;
|
|
442
|
+
while (index < utf8.size()) {
|
|
443
|
+
size_t char_start = index;
|
|
444
|
+
uint32_t code_point = decodeUtf8(utf8, index);
|
|
445
|
+
Font *chosen = faceForCodePoint(font, code_point);
|
|
446
|
+
if (segment_font == nullptr) {
|
|
447
|
+
segment_font = chosen;
|
|
448
|
+
segment_start = char_start;
|
|
449
|
+
} else if (chosen != segment_font) {
|
|
450
|
+
total += shapeSegment(utf8.substr(segment_start, char_start - segment_start), segment_font, out);
|
|
451
|
+
segment_font = chosen;
|
|
452
|
+
segment_start = char_start;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (segment_font != nullptr) {
|
|
456
|
+
total += shapeSegment(utf8.substr(segment_start), segment_font, out);
|
|
457
|
+
}
|
|
458
|
+
return total;
|
|
459
|
+
}
|
|
460
|
+
|
|
333
461
|
bool xt_text_rasterize(Font *font, uint32_t glyph, GlyphImage &out) {
|
|
334
462
|
out.width = 0;
|
|
335
463
|
out.height = 0;
|
|
@@ -374,6 +502,7 @@ bool xt_text_ready() {
|
|
|
374
502
|
|
|
375
503
|
void xt_text_shutdown() {
|
|
376
504
|
Engine &e = engine();
|
|
505
|
+
e.fallbacks.clear();
|
|
377
506
|
e.cache.clear();
|
|
378
507
|
for (auto &entry : e.hb_faces) {
|
|
379
508
|
if (entry.second != nullptr) hb_face_destroy(entry.second);
|
|
@@ -387,6 +516,8 @@ void xt_text_shutdown() {
|
|
|
387
516
|
e.initialized = false;
|
|
388
517
|
e.sans_path.clear();
|
|
389
518
|
e.mono_path.clear();
|
|
519
|
+
e.sans_fallbacks.clear();
|
|
520
|
+
e.mono_fallbacks.clear();
|
|
390
521
|
}
|
|
391
522
|
|
|
392
523
|
} // namespace xtgui
|
package/runtime/ext_gui/text.h
CHANGED
|
@@ -29,12 +29,15 @@ struct FontSpec {
|
|
|
29
29
|
/** Opaque loaded face (defined in text.cpp). */
|
|
30
30
|
struct Font;
|
|
31
31
|
|
|
32
|
-
/** A positioned, shaped glyph (pixels).
|
|
32
|
+
/** A positioned, shaped glyph (pixels). `font` is the face the glyph id
|
|
33
|
+
* belongs to — the primary face or, for a codepoint it does not cover, a
|
|
34
|
+
* fallback face — so the rasteriser can load the right face. */
|
|
33
35
|
struct ShapedGlyph {
|
|
34
36
|
uint32_t glyph = 0;
|
|
35
37
|
float x_advance = 0;
|
|
36
38
|
float x_offset = 0;
|
|
37
39
|
float y_offset = 0;
|
|
40
|
+
Font *font = nullptr;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
/** An 8-bit grayscale glyph bitmap, as produced by FreeType. */
|
|
@@ -51,9 +54,15 @@ struct GlyphImage {
|
|
|
51
54
|
Font *xt_text_resolve(const FontSpec &spec);
|
|
52
55
|
|
|
53
56
|
/** Shape `utf8` with `font`, appending glyphs to `out`; returns the total
|
|
54
|
-
* advance width in pixels. `out` is not cleared.
|
|
57
|
+
* advance width in pixels. `out` is not cleared. Codepoints `font` does not
|
|
58
|
+
* cover are shaped with the first fallback face that has them, and each
|
|
59
|
+
* glyph's `font` says which face it belongs to. */
|
|
55
60
|
float xt_text_shape_run(const std::string &utf8, Font *font, std::vector<ShapedGlyph> &out);
|
|
56
61
|
|
|
62
|
+
/** The same face as `font` at a different pixel size (used to rasterise glyphs
|
|
63
|
+
* at the display's scale factor), or null when it cannot be loaded. */
|
|
64
|
+
Font *xt_text_scale(Font *font, float pixel_size);
|
|
65
|
+
|
|
57
66
|
/** Rasterise `glyph` from `font` into a grayscale bitmap. Returns false when
|
|
58
67
|
* the glyph has no outline (e.g. a space) or cannot be rendered. */
|
|
59
68
|
bool xt_text_rasterize(Font *font, uint32_t glyph, GlyphImage &out);
|