xbintsc 0.5.45 → 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/AGENTS.md +1 -1
- package/README.md +5 -4
- package/README.zh-CN.md +4 -3
- package/dist/src/cli/extensions.d.ts +24 -0
- package/dist/src/cli/extensions.js +62 -0
- package/dist/src/cli/extensions.js.map +1 -0
- package/dist/src/cli/help.d.ts +1 -1
- package/dist/src/cli/help.js +3 -1
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hints.js +8 -3
- package/dist/src/cli/hints.js.map +1 -1
- package/dist/src/cli/main.js +21 -28
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/diagnostics/diagnostic.d.ts +3 -1
- package/dist/src/diagnostics/diagnostic.js +2 -0
- package/dist/src/diagnostics/diagnostic.js.map +1 -1
- package/dist/src/driver/build-key.d.ts +2 -0
- package/dist/src/driver/build-key.js +1 -0
- package/dist/src/driver/build-key.js.map +1 -1
- package/dist/src/driver/compiler.js +61 -8
- package/dist/src/driver/compiler.js.map +1 -1
- package/dist/src/driver/config.d.ts +4 -0
- package/dist/src/driver/config.js +2 -0
- package/dist/src/driver/config.js.map +1 -1
- package/dist/src/driver/link-features.d.ts +37 -0
- package/dist/src/driver/link-features.js +123 -0
- package/dist/src/driver/link-features.js.map +1 -0
- package/dist/src/driver/options.d.ts +10 -0
- package/dist/src/driver/runtime-objects.d.ts +5 -0
- package/dist/src/driver/runtime-objects.js +20 -1
- package/dist/src/driver/runtime-objects.js.map +1 -1
- package/dist/src/driver/usage.d.ts +23 -0
- package/dist/src/driver/usage.js +353 -0
- package/dist/src/driver/usage.js.map +1 -0
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/src/extensions/catalog.js +1 -2
- package/dist/src/extensions/catalog.js.map +1 -1
- package/dist/src/extensions/gui/features.d.ts +34 -0
- package/dist/src/extensions/gui/features.js +165 -0
- package/dist/src/extensions/gui/features.js.map +1 -0
- package/dist/src/extensions/gui/index.js +10 -4
- package/dist/src/extensions/gui/index.js.map +1 -1
- package/dist/src/extensions/link-feature.d.ts +70 -0
- package/dist/src/extensions/link-feature.js +18 -0
- package/dist/src/extensions/link-feature.js.map +1 -0
- package/dist/src/extensions/registry.d.ts +24 -4
- package/dist/src/extensions/registry.js +26 -3
- package/dist/src/extensions/registry.js.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +0 -1
- package/dist/src/index.js.map +1 -1
- package/dist/tests/cli/hints.test.js +5 -7
- package/dist/tests/cli/hints.test.js.map +1 -1
- package/dist/tests/driver/link-features.test.js +105 -0
- package/dist/tests/driver/link-features.test.js.map +1 -0
- package/dist/tests/driver/usage.test.d.ts +1 -0
- package/dist/tests/driver/usage.test.js +88 -0
- package/dist/tests/driver/usage.test.js.map +1 -0
- package/dist/tests/e2e/gui-audio.test.js +4 -4
- package/dist/tests/e2e/gui-audio.test.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-feature-linking.test.d.ts +9 -0
- package/dist/tests/e2e/gui-feature-linking.test.js +68 -0
- package/dist/tests/e2e/gui-feature-linking.test.js.map +1 -0
- 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 +10 -1
- package/dist/tests/e2e/gui-helpers.js +6 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-layout.test.js +7 -3
- package/dist/tests/e2e/gui-layout.test.js.map +1 -1
- package/dist/tests/e2e/gui-media-disabled.test.d.ts +5 -4
- package/dist/tests/e2e/gui-media-disabled.test.js +7 -6
- package/dist/tests/e2e/gui-media-disabled.test.js.map +1 -1
- package/dist/tests/e2e/gui-media.test.d.ts +5 -4
- package/dist/tests/e2e/gui-media.test.js +9 -9
- package/dist/tests/e2e/gui-media.test.js.map +1 -1
- package/dist/tests/e2e/gui-paint.test.js +5 -2
- package/dist/tests/e2e/gui-paint.test.js.map +1 -1
- package/dist/tests/e2e/gui-shaping.test.d.ts +5 -0
- package/dist/tests/e2e/gui-shaping.test.js +47 -0
- package/dist/tests/e2e/gui-shaping.test.js.map +1 -0
- 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/e2e/gui-webgl-pipeline-cache.test.d.ts +18 -0
- package/dist/tests/e2e/gui-webgl-pipeline-cache.test.js +120 -0
- package/dist/tests/e2e/gui-webgl-pipeline-cache.test.js.map +1 -0
- package/dist/tests/extensions/gui-features.test.d.ts +1 -0
- package/dist/tests/extensions/gui-features.test.js +175 -0
- package/dist/tests/extensions/gui-features.test.js.map +1 -0
- package/doc/ai/cli.md +5 -1
- package/doc/ai/extensions.md +56 -26
- package/doc/ai/troubleshooting.md +10 -7
- package/doc/ai/zh-CN/cli.md +5 -1
- package/doc/ai/zh-CN/extensions.md +48 -22
- package/doc/ai/zh-CN/troubleshooting.md +6 -5
- package/doc/gui/animation.md +39 -9
- package/doc/gui/api.md +2 -1
- package/doc/gui/build.md +137 -19
- package/doc/gui/canvas.md +1 -1
- package/doc/gui/dom.md +20 -4
- package/doc/gui/known-issues.md +63 -26
- package/doc/gui/layout.md +31 -8
- package/doc/gui/media.md +13 -7
- package/doc/gui/rendering.md +114 -39
- package/doc/gui/runtime.md +5 -0
- package/doc/gui/webgl.md +24 -6
- package/doc/gui.md +110 -47
- package/doc/implemented/cli.md +2 -0
- package/doc/implemented/extensions.md +9 -3
- package/doc/npm-package.md +4 -4
- package/doc/xbintsc.config.schema.json +10 -0
- package/doc/zh-CN/gui/animation.md +33 -10
- package/doc/zh-CN/gui/api.md +2 -1
- package/doc/zh-CN/gui/build.md +110 -16
- package/doc/zh-CN/gui/canvas.md +1 -1
- package/doc/zh-CN/gui/dom.md +16 -3
- package/doc/zh-CN/gui/known-issues.md +45 -15
- package/doc/zh-CN/gui/layout.md +23 -6
- package/doc/zh-CN/gui/media.md +11 -6
- package/doc/zh-CN/gui/rendering.md +84 -26
- package/doc/zh-CN/gui/runtime.md +3 -0
- package/doc/zh-CN/gui/webgl.md +3 -3
- package/doc/zh-CN/gui.md +92 -41
- package/doc/zh-CN/implemented/cli.md +2 -0
- package/doc/zh-CN/implemented/extensions.md +9 -3
- package/doc/zh-CN/npm-package.md +4 -3
- 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/browser.cpp +22 -15
- package/runtime/ext_gui/browser_loop.cpp +13 -1
- package/runtime/ext_gui/canvas.h +1 -1
- package/runtime/ext_gui/canvas_raster.cpp +1 -1
- package/runtime/ext_gui/canvas_text.cpp +1 -1
- 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/dom_api_canvas.cpp +15 -8
- package/runtime/ext_gui/dom_api_canvas_extra.cpp +44 -20
- package/runtime/ext_gui/dom_api_canvas_image.cpp +3 -1
- package/runtime/ext_gui/dom_api_node_read.cpp +10 -0
- package/runtime/ext_gui/dom_api_offscreen.cpp +3 -1
- package/runtime/ext_gui/dom_api_style.cpp +7 -0
- package/runtime/ext_gui/dom_api_webgl_internal.h +4 -0
- package/runtime/ext_gui/dom_api_webgl_source.cpp +5 -2
- package/runtime/ext_gui/effect_values.cpp +245 -21
- package/runtime/ext_gui/gpu_bridge.c +28 -0
- package/runtime/ext_gui/gpu_feature.cpp +30 -0
- package/runtime/ext_gui/gpu_shader.cpp +22 -0
- package/runtime/ext_gui/gpu_shader_api.h +50 -0
- package/runtime/ext_gui/gradient_values.cpp +24 -0
- package/runtime/ext_gui/grid_values.cpp +23 -0
- package/runtime/ext_gui/gui.cpp +6 -37
- package/runtime/ext_gui/gui_canvas.cpp +14 -6
- package/runtime/ext_gui/gui_cursor.cpp +50 -0
- package/runtime/ext_gui/gui_engine.h +1 -18
- package/runtime/ext_gui/gui_events.cpp +19 -6
- package/runtime/ext_gui/gui_internal.h +3 -0
- package/runtime/ext_gui/gui_webgl.cpp +25 -10
- package/runtime/ext_gui/image_api.cpp +18 -0
- package/runtime/ext_gui/image_api.h +43 -0
- package/runtime/ext_gui/image_bitmap_browser.cpp +20 -11
- package/runtime/ext_gui/image_bridge.c +22 -0
- package/runtime/ext_gui/image_feature.cpp +107 -0
- package/runtime/ext_gui/inline_svg.cpp +95 -8
- package/runtime/ext_gui/inline_svg.h +5 -1
- package/runtime/ext_gui/layout.cpp +15 -9
- package/runtime/ext_gui/layout.h +8 -2
- 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/media.cpp +4 -2
- package/runtime/ext_gui/media.h +3 -3
- package/runtime/ext_gui/media_api.h +7 -6
- 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 +11 -2
- 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 +19 -7
- package/runtime/ext_gui/renderer_internal.h +21 -0
- package/runtime/ext_gui/renderer_layer.cpp +37 -6
- package/runtime/ext_gui/renderer_text.cpp +2 -1
- package/runtime/ext_gui/stb_truetype.h +5077 -0
- 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 +27 -499
- package/runtime/ext_gui/text.h +18 -9
- package/runtime/ext_gui/text_api.h +45 -0
- package/runtime/ext_gui/text_bridge.c +23 -0
- package/runtime/ext_gui/text_cache.cpp +12 -3
- 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_glyph_cache.cpp +2 -2
- package/runtime/ext_gui/text_hb.cpp +438 -0
- package/runtime/ext_gui/text_stb.cpp +369 -0
- package/runtime/ext_gui/transform.cpp +107 -9
- package/runtime/ext_gui/transform.h +5 -0
- package/runtime/ext_gui/webgl_api.cpp +19 -0
- package/runtime/ext_gui/webgl_api.h +53 -0
- package/runtime/ext_gui/webgl_bridge.c +25 -0
- package/runtime/ext_gui/webgl_draw_setup.cpp +42 -1
- package/runtime/ext_gui/webgl_feature.cpp +82 -0
- package/runtime/ext_gui/webgl_feature.h +28 -0
- package/runtime/ext_gui/webgl_msl.cpp +41 -0
- package/runtime/ext_gui/webgl_msl.h +6 -0
- package/runtime/ext_gui/webgl_shader.cpp +40 -32
- package/runtime/ext_gui/window.cpp +7 -7
- package/runtime/ext_media/decode_audio.cpp +1 -1
- package/runtime/ext_media/media.cpp +1 -1
- package/runtime/ext_media/media_bridge.c +7 -7
- package/scripts/build-gui.ts +97 -98
- package/scripts/check-file-length.ts +1 -0
- package/scripts/gui-build-options.ts +122 -0
- package/src/cli/extensions.ts +72 -0
- package/src/cli/help.ts +3 -1
- package/src/cli/hints.ts +9 -3
- package/src/cli/main.ts +21 -27
- package/src/diagnostics/diagnostic.ts +2 -0
- package/src/driver/build-key.ts +3 -0
- package/src/driver/compiler.ts +63 -8
- package/src/driver/config.ts +6 -0
- package/src/driver/link-features.ts +163 -0
- package/src/driver/options.ts +10 -0
- package/src/driver/runtime-objects.ts +26 -1
- package/src/driver/usage.ts +376 -0
- package/src/driver/version.ts +1 -1
- package/src/extensions/catalog.ts +1 -2
- package/src/extensions/gui/features.ts +166 -0
- package/src/extensions/gui/index.ts +10 -4
- package/src/extensions/link-feature.ts +72 -0
- package/src/extensions/registry.ts +45 -4
- package/src/index.ts +3 -2
- package/dist/src/extensions/media/index.d.ts +0 -28
- package/dist/src/extensions/media/index.js +0 -59
- package/dist/src/extensions/media/index.js.map +0 -1
- package/dist/tests/extensions/media.test.js +0 -63
- package/dist/tests/extensions/media.test.js.map +0 -1
- package/src/extensions/media/index.ts +0 -65
- /package/dist/tests/{extensions/media.test.d.ts → driver/link-features.test.d.ts} +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — `shaping` link feature registration bridge.
|
|
3
|
+
*
|
|
4
|
+
* Compiled and linked only when the compiler selects the shaping feature (or
|
|
5
|
+
* the program is forced to). Its constructor installs the HarfBuzz + FreeType
|
|
6
|
+
* backend and references it, pulling both libraries out of `gui.a`. Without
|
|
7
|
+
* this object the core uses the stb_truetype default and neither library is
|
|
8
|
+
* linked.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
extern void xt_text_feature_register(void);
|
|
12
|
+
|
|
13
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
14
|
+
__attribute__((constructor)) static void xt_text_feature_boot(void) {
|
|
15
|
+
xt_text_feature_register();
|
|
16
|
+
}
|
|
17
|
+
#elif defined(_MSC_VER)
|
|
18
|
+
void xt_text_feature_boot(void);
|
|
19
|
+
#pragma section(".CRT$XCU", read)
|
|
20
|
+
__declspec(allocate(".CRT$XCU")) static void (*const xt_text_feature_ctor)(void) =
|
|
21
|
+
xt_text_feature_boot;
|
|
22
|
+
void xt_text_feature_boot(void) { xt_text_feature_register(); }
|
|
23
|
+
#endif
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Layout measures every word of the document on every layout pass, and the
|
|
5
5
|
* renderer shapes every text run again to place its glyphs. Without a cache a
|
|
6
|
-
* document with a few thousand words runs thousands of
|
|
6
|
+
* document with a few thousand words runs thousands of shaping calls per
|
|
7
7
|
* frame, which is what made a large page unusable. This module memoises
|
|
8
8
|
* `(face, text) -> glyphs + advance` behind a two-generation cache: when the
|
|
9
9
|
* budget is exceeded the *spare* generation is dropped and becomes the current
|
|
@@ -35,9 +35,10 @@ constexpr uint64_t kMaxEntries = 8192;
|
|
|
35
35
|
struct Key {
|
|
36
36
|
Font *font = nullptr;
|
|
37
37
|
std::string text;
|
|
38
|
+
float letter_spacing = 0.0f;
|
|
38
39
|
|
|
39
40
|
bool operator==(const Key &other) const {
|
|
40
|
-
return font == other.font && text == other.text;
|
|
41
|
+
return font == other.font && text == other.text && letter_spacing == other.letter_spacing;
|
|
41
42
|
}
|
|
42
43
|
};
|
|
43
44
|
|
|
@@ -47,6 +48,7 @@ struct KeyHash {
|
|
|
47
48
|
// FNV-ish mix of the text, then combine — cheap and stable.
|
|
48
49
|
size_t textHash = std::hash<std::string>()(key.text);
|
|
49
50
|
hash ^= textHash + 0x9e3779b97f4a7c15ull + (hash << 6) + (hash >> 2);
|
|
51
|
+
hash ^= std::hash<float>()(key.letter_spacing) + 0x9e3779b97f4a7c15ull + (hash << 6) + (hash >> 2);
|
|
50
52
|
return hash;
|
|
51
53
|
}
|
|
52
54
|
};
|
|
@@ -78,12 +80,14 @@ void respectBudget() {
|
|
|
78
80
|
|
|
79
81
|
} // namespace
|
|
80
82
|
|
|
81
|
-
const ShapedRun *xt_text_shape_cached(const std::string &utf8, Font *font
|
|
83
|
+
const ShapedRun *xt_text_shape_cached(const std::string &utf8, Font *font,
|
|
84
|
+
float letter_spacing) {
|
|
82
85
|
if (font == nullptr) return nullptr;
|
|
83
86
|
if (utf8.empty()) return nullptr;
|
|
84
87
|
Key key;
|
|
85
88
|
key.font = font;
|
|
86
89
|
key.text = utf8;
|
|
90
|
+
key.letter_spacing = letter_spacing;
|
|
87
91
|
Generation &generation = g_generations[g_current];
|
|
88
92
|
auto found = generation.runs.find(key);
|
|
89
93
|
if (found != generation.runs.end()) {
|
|
@@ -94,6 +98,11 @@ const ShapedRun *xt_text_shape_cached(const std::string &utf8, Font *font) {
|
|
|
94
98
|
auto inserted = generation.runs.emplace(std::move(key), ShapedRun());
|
|
95
99
|
ShapedRun &run = inserted.first->second;
|
|
96
100
|
run.width = xt_text_shape_run(utf8, font, run.glyphs);
|
|
101
|
+
if (letter_spacing != 0.0f && !run.glyphs.empty()) {
|
|
102
|
+
/* CSS adds the spacing after every typographic unit, including the last. */
|
|
103
|
+
for (ShapedGlyph &glyph : run.glyphs) glyph.x_advance += letter_spacing;
|
|
104
|
+
run.width += letter_spacing * (float)run.glyphs.size();
|
|
105
|
+
}
|
|
97
106
|
generation.bytes += estimateBytes(inserted.first->first, run);
|
|
98
107
|
respectBudget();
|
|
99
108
|
return &run;
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — font discovery and the process-wide face database.
|
|
3
|
+
*
|
|
4
|
+
* `text_fonts.cpp` consumes this database for CSS family/weight matching. This
|
|
5
|
+
* unit owns discovery: the curated generic-family files, the one-time scan of
|
|
6
|
+
* the platform font directories, sfnt metadata extraction and the document's
|
|
7
|
+
* author `@font-face` faces.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include "text_fonts_internal.h"
|
|
11
|
+
|
|
12
|
+
#include "text_font_paths.h"
|
|
13
|
+
#include "text_font_sfnt.h"
|
|
14
|
+
|
|
15
|
+
#include <algorithm>
|
|
16
|
+
#include <cctype>
|
|
17
|
+
#include <cstdio>
|
|
18
|
+
#include <cstdlib>
|
|
19
|
+
#include <filesystem>
|
|
20
|
+
#include <string>
|
|
21
|
+
#include <utility>
|
|
22
|
+
#include <vector>
|
|
23
|
+
|
|
24
|
+
namespace xtgui {
|
|
25
|
+
|
|
26
|
+
namespace {
|
|
27
|
+
|
|
28
|
+
std::string lowerAscii(const std::string &text) {
|
|
29
|
+
std::string out = text;
|
|
30
|
+
for (char &c : out) {
|
|
31
|
+
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
std::string trimText(const std::string &text) {
|
|
37
|
+
size_t first = 0;
|
|
38
|
+
size_t last = text.size();
|
|
39
|
+
auto space = [](unsigned char c) {
|
|
40
|
+
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
|
|
41
|
+
};
|
|
42
|
+
while (first < last && space((unsigned char)text[first])) first++;
|
|
43
|
+
while (last > first && space((unsigned char)text[last - 1])) last--;
|
|
44
|
+
return text.substr(first, last - first);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
bool startsWithIgnoreCase(const std::string &text, const char *prefix) {
|
|
48
|
+
size_t index = 0;
|
|
49
|
+
for (; prefix[index] != '\0'; index++) {
|
|
50
|
+
if (index >= text.size()) return false;
|
|
51
|
+
char c = text[index];
|
|
52
|
+
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
53
|
+
if (c != prefix[index]) return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
std::string decodePercentEscapes(const std::string &text) {
|
|
59
|
+
std::string out;
|
|
60
|
+
out.reserve(text.size());
|
|
61
|
+
for (size_t i = 0; i < text.size(); i++) {
|
|
62
|
+
if (text[i] == '%' && i + 2 < text.size() && std::isxdigit((unsigned char)text[i + 1]) &&
|
|
63
|
+
std::isxdigit((unsigned char)text[i + 2])) {
|
|
64
|
+
auto hex = [](char c) -> int {
|
|
65
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
66
|
+
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
67
|
+
return c - 'A' + 10;
|
|
68
|
+
};
|
|
69
|
+
out.push_back((char)(hex(text[i + 1]) * 16 + hex(text[i + 2])));
|
|
70
|
+
i += 2;
|
|
71
|
+
} else {
|
|
72
|
+
out.push_back(text[i]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Drop a URL's `?query`/`#fragment`; neither is part of the local file name. */
|
|
79
|
+
std::string stripUrlSuffix(const std::string &text) {
|
|
80
|
+
const size_t cut = text.find_first_of("?#");
|
|
81
|
+
return cut == std::string::npos ? text : text.substr(0, cut);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Map an `@font-face` URL to a local path. Relative paths are interpreted as
|
|
85
|
+
* local files (the `file:` protocol) against the process working directory, the
|
|
86
|
+
* same policy as image sources. Remote and `data:` URLs have no loader here, so
|
|
87
|
+
* they return an empty string and the next `src` entry is tried. */
|
|
88
|
+
std::string localFontPath(const std::string &url) {
|
|
89
|
+
const std::string text = trimText(url);
|
|
90
|
+
if (text.empty()) return {};
|
|
91
|
+
const std::string lower = lowerAscii(text);
|
|
92
|
+
if (startsWithIgnoreCase(lower, "http:") || startsWithIgnoreCase(lower, "https:") ||
|
|
93
|
+
startsWithIgnoreCase(lower, "ftp:") || startsWithIgnoreCase(lower, "data:") ||
|
|
94
|
+
text.rfind("//", 0) == 0) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
if (startsWithIgnoreCase(lower, "file:")) {
|
|
98
|
+
std::string path = text.substr(5);
|
|
99
|
+
if (path.rfind("//", 0) == 0) {
|
|
100
|
+
path = path.substr(2);
|
|
101
|
+
/* `file://host/abs` is not supported; keep only the path. A Windows
|
|
102
|
+
* drive letter is a path, not an authority. */
|
|
103
|
+
const bool drive =
|
|
104
|
+
path.size() >= 2 && std::isalpha((unsigned char)path[0]) && path[1] == ':';
|
|
105
|
+
if (!drive && !path.empty() && path[0] != '/') {
|
|
106
|
+
const size_t slash = path.find('/');
|
|
107
|
+
if (slash == std::string::npos) return {};
|
|
108
|
+
path = path.substr(slash);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/* `file:///C:/…` -> `C:/…`. */
|
|
112
|
+
if (path.size() >= 3 && path[0] == '/' && std::isalpha((unsigned char)path[1]) &&
|
|
113
|
+
path[2] == ':') {
|
|
114
|
+
path = path.substr(1);
|
|
115
|
+
}
|
|
116
|
+
return decodePercentEscapes(stripUrlSuffix(path));
|
|
117
|
+
}
|
|
118
|
+
return decodePercentEscapes(stripUrlSuffix(text));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
std::string compactFamily(const std::string &normalized) {
|
|
122
|
+
std::string out;
|
|
123
|
+
for (unsigned char c : normalized) {
|
|
124
|
+
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) out.push_back((char)c);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
bool hasFontExtension(const std::string &path) {
|
|
130
|
+
const size_t dot = path.find_last_of('.');
|
|
131
|
+
if (dot == std::string::npos) return false;
|
|
132
|
+
const std::string ext = lowerAscii(path.substr(dot));
|
|
133
|
+
return ext == ".ttf" || ext == ".ttc" || ext == ".otf" || ext == ".otc";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
void appendUnique(std::vector<std::string> &paths, const std::string &path) {
|
|
137
|
+
for (const std::string &entry : paths) {
|
|
138
|
+
if (entry == path) return;
|
|
139
|
+
}
|
|
140
|
+
paths.push_back(path);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The defaults list with an environment override at the front. The override is
|
|
144
|
+
* only kept when the file exists, which mirrors the old `XT_GUI_FONT`
|
|
145
|
+
* behaviour (a bad path was ignored). */
|
|
146
|
+
std::vector<std::string> defaultPaths(FontGeneric kind) {
|
|
147
|
+
std::vector<std::string> paths;
|
|
148
|
+
const char *sans_env = std::getenv("XT_GUI_FONT");
|
|
149
|
+
const char *mono_env = std::getenv("XT_GUI_FONT_MONO");
|
|
150
|
+
if (kind == FontGeneric::Sans && sans_env != nullptr && fileExists(sans_env)) {
|
|
151
|
+
paths.push_back(sans_env);
|
|
152
|
+
}
|
|
153
|
+
if (kind == FontGeneric::Monospace) {
|
|
154
|
+
if (mono_env != nullptr && fileExists(mono_env)) {
|
|
155
|
+
paths.push_back(mono_env);
|
|
156
|
+
} else if (sans_env != nullptr && fileExists(sans_env) &&
|
|
157
|
+
lowerAscii(sans_env).find("mono") != std::string::npos) {
|
|
158
|
+
paths.push_back(sans_env);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const std::string &path : xt_font_default_paths(kind)) {
|
|
162
|
+
if (fileExists(path)) appendUnique(paths, path);
|
|
163
|
+
}
|
|
164
|
+
return paths;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
void addFace(const SfntFace &raw, const std::string &path) {
|
|
168
|
+
Database &db = fontDatabase();
|
|
169
|
+
const FontFaceRef ref{path, raw.index};
|
|
170
|
+
const std::string key = faceKey(ref);
|
|
171
|
+
const auto found = db.by_path.find(path);
|
|
172
|
+
if (found != db.by_path.end()) {
|
|
173
|
+
for (size_t index : found->second) {
|
|
174
|
+
if (faceKey(db.faces[index].ref) == key) return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
FaceRecord face;
|
|
179
|
+
face.ref = ref;
|
|
180
|
+
face.primary_family = normalizeFamily(raw.primary_family);
|
|
181
|
+
face.weight = clampedWeight(raw.weight);
|
|
182
|
+
face.weight_min = face.weight;
|
|
183
|
+
face.weight_max = face.weight;
|
|
184
|
+
face.italic = raw.italic;
|
|
185
|
+
face.monospace = raw.monospace;
|
|
186
|
+
face.truetype = raw.truetype;
|
|
187
|
+
for (const std::string &name : raw.families) {
|
|
188
|
+
const std::string normalized = normalizeFamily(name);
|
|
189
|
+
if (normalized.empty()) continue;
|
|
190
|
+
if (std::find(face.families.begin(), face.families.end(), normalized) ==
|
|
191
|
+
face.families.end()) {
|
|
192
|
+
face.families.push_back(normalized);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (face.primary_family.empty() && !face.families.empty()) {
|
|
196
|
+
face.primary_family = face.families.front();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const size_t index = db.faces.size();
|
|
200
|
+
db.faces.push_back(std::move(face));
|
|
201
|
+
db.by_path[path].push_back(index);
|
|
202
|
+
for (const std::string &name : db.faces[index].families) {
|
|
203
|
+
db.by_family[name].push_back(index);
|
|
204
|
+
const std::string compact = compactFamily(name);
|
|
205
|
+
if (!compact.empty()) db.by_compact[compact].push_back(index);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Rebuild the lookup maps after user faces are replaced. User faces are
|
|
210
|
+
* indexed first so an author `@font-face` named like a system family wins ties;
|
|
211
|
+
* `by_path` stays system-only because it drives generic-family default
|
|
212
|
+
* selection, which must not follow an author's declared name. */
|
|
213
|
+
void rebuildIndexes() {
|
|
214
|
+
Database &db = fontDatabase();
|
|
215
|
+
db.by_family.clear();
|
|
216
|
+
db.by_compact.clear();
|
|
217
|
+
db.by_path.clear();
|
|
218
|
+
auto index = [&db](size_t index, bool with_path) {
|
|
219
|
+
const FaceRecord &face = db.faces[index];
|
|
220
|
+
if (with_path) db.by_path[face.ref.path].push_back(index);
|
|
221
|
+
for (const std::string &name : face.families) {
|
|
222
|
+
db.by_family[name].push_back(index);
|
|
223
|
+
const std::string compact = compactFamily(name);
|
|
224
|
+
if (!compact.empty()) db.by_compact[compact].push_back(index);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
for (size_t i = 0; i < db.faces.size(); i++) {
|
|
228
|
+
if (db.faces[i].user) index(i, false);
|
|
229
|
+
}
|
|
230
|
+
for (size_t i = 0; i < db.faces.size(); i++) {
|
|
231
|
+
if (!db.faces[i].user) index(i, true);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
void addUserFace(const UserFontFace &request) {
|
|
236
|
+
Database &db = fontDatabase();
|
|
237
|
+
const std::string family = normalizeFamily(request.family);
|
|
238
|
+
if (family.empty()) return;
|
|
239
|
+
for (const UserFontSource &source : request.sources) {
|
|
240
|
+
const std::string path = localFontPath(source.url);
|
|
241
|
+
if (path.empty() || !fileExists(path)) continue;
|
|
242
|
+
std::vector<SfntFace> faces;
|
|
243
|
+
if (!xt_sfnt_read_faces(path, faces) || faces.empty()) continue;
|
|
244
|
+
const SfntFace &metadata = faces.front();
|
|
245
|
+
FaceRecord face;
|
|
246
|
+
face.ref = FontFaceRef{path, metadata.index};
|
|
247
|
+
face.primary_family = family;
|
|
248
|
+
face.families.push_back(family);
|
|
249
|
+
if (request.has_weight) {
|
|
250
|
+
const int minimum = clampedWeight(request.weight_min);
|
|
251
|
+
const int maximum = clampedWeight(request.weight_max);
|
|
252
|
+
face.weight_min = std::min(minimum, maximum);
|
|
253
|
+
face.weight_max = std::max(minimum, maximum);
|
|
254
|
+
face.weight = face.weight_min + (face.weight_max - face.weight_min) / 2;
|
|
255
|
+
} else {
|
|
256
|
+
face.weight = clampedWeight(metadata.weight);
|
|
257
|
+
face.weight_min = face.weight;
|
|
258
|
+
face.weight_max = face.weight;
|
|
259
|
+
}
|
|
260
|
+
face.italic = request.has_italic ? request.italic : metadata.italic;
|
|
261
|
+
face.monospace = metadata.monospace;
|
|
262
|
+
face.truetype = metadata.truetype;
|
|
263
|
+
face.user = true;
|
|
264
|
+
db.faces.push_back(std::move(face));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
std::string userFaceKey(const UserFontFace &face) {
|
|
270
|
+
std::string key = normalizeFamily(face.family);
|
|
271
|
+
key += '|';
|
|
272
|
+
if (face.has_weight) {
|
|
273
|
+
key += std::to_string(std::min(face.weight_min, face.weight_max));
|
|
274
|
+
key += '-';
|
|
275
|
+
key += std::to_string(std::max(face.weight_min, face.weight_max));
|
|
276
|
+
} else {
|
|
277
|
+
key += "auto";
|
|
278
|
+
}
|
|
279
|
+
key += face.has_italic ? (face.italic ? "|italic" : "|normal") : "|auto";
|
|
280
|
+
return key;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
void scanDirectory(const std::string &directory, std::vector<std::string> &paths) {
|
|
284
|
+
std::error_code error;
|
|
285
|
+
std::filesystem::recursive_directory_iterator iterator(
|
|
286
|
+
directory, std::filesystem::directory_options::skip_permission_denied, error);
|
|
287
|
+
const std::filesystem::recursive_directory_iterator end;
|
|
288
|
+
while (!error && iterator != end) {
|
|
289
|
+
const std::filesystem::directory_entry &entry = *iterator;
|
|
290
|
+
std::error_code entry_error;
|
|
291
|
+
if (entry.is_regular_file(entry_error)) {
|
|
292
|
+
std::string path = entry.path().string();
|
|
293
|
+
if (hasFontExtension(path)) paths.push_back(std::move(path));
|
|
294
|
+
}
|
|
295
|
+
iterator.increment(error);
|
|
296
|
+
if (error) break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
} // namespace
|
|
301
|
+
|
|
302
|
+
Database &fontDatabase() {
|
|
303
|
+
static Database instance;
|
|
304
|
+
return instance;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
size_t kindIndex(FontGeneric kind) { return (size_t)kind; }
|
|
308
|
+
|
|
309
|
+
bool fileExists(const std::string &path) {
|
|
310
|
+
FILE *file = std::fopen(path.c_str(), "rb");
|
|
311
|
+
if (file == nullptr) return false;
|
|
312
|
+
std::fclose(file);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** CSS family names compare case-insensitively with whitespace collapsed. Only
|
|
317
|
+
* ASCII is case-folded so non-ASCII names still match byte-for-byte. */
|
|
318
|
+
std::string normalizeFamily(const std::string &name) {
|
|
319
|
+
std::string out;
|
|
320
|
+
bool pending_space = false;
|
|
321
|
+
for (unsigned char raw : name) {
|
|
322
|
+
if (raw <= 0x20 || raw == 0x7f) {
|
|
323
|
+
pending_space = !out.empty();
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (pending_space) {
|
|
327
|
+
out.push_back(' ');
|
|
328
|
+
pending_space = false;
|
|
329
|
+
}
|
|
330
|
+
char c = (char)raw;
|
|
331
|
+
if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
|
332
|
+
out.push_back(c);
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
std::string faceKey(const FontFaceRef &ref) {
|
|
338
|
+
return ref.path + '\x1f' + std::to_string(ref.index);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
void addFontFile(const std::string &path) {
|
|
342
|
+
Database &db = fontDatabase();
|
|
343
|
+
if (!db.parsed_paths.insert(path).second) return;
|
|
344
|
+
std::vector<SfntFace> faces;
|
|
345
|
+
xt_sfnt_read_faces(path, faces);
|
|
346
|
+
for (const SfntFace &face : faces) addFace(face, path);
|
|
347
|
+
/* Keep an empty vector so `by_path[path]` distinguishes "parsed but no
|
|
348
|
+
* usable face" from "not parsed yet". */
|
|
349
|
+
db.by_path.emplace(path, std::vector<size_t>());
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
void ensureDefaults() {
|
|
353
|
+
Database &db = fontDatabase();
|
|
354
|
+
if (db.defaults_loaded) return;
|
|
355
|
+
db.defaults_loaded = true;
|
|
356
|
+
for (int i = 0; i < (int)kGenericCount; i++) {
|
|
357
|
+
db.defaults[(size_t)i] = defaultPaths((FontGeneric)i);
|
|
358
|
+
for (const std::string &path : db.defaults[(size_t)i]) addFontFile(path);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
void ensureScanned() {
|
|
363
|
+
Database &db = fontDatabase();
|
|
364
|
+
if (db.scanned) return;
|
|
365
|
+
db.scanned = true;
|
|
366
|
+
ensureDefaults();
|
|
367
|
+
|
|
368
|
+
std::vector<std::string> paths;
|
|
369
|
+
for (const std::string &directory : xt_font_search_directories()) {
|
|
370
|
+
scanDirectory(directory, paths);
|
|
371
|
+
}
|
|
372
|
+
/* A font supplied through XT_GUI_FONT may live outside the platform font
|
|
373
|
+
* directories; scanning its directory finds the bold/italic siblings. */
|
|
374
|
+
for (const char *variable : {"XT_GUI_FONT", "XT_GUI_FONT_MONO"}) {
|
|
375
|
+
const char *value = std::getenv(variable);
|
|
376
|
+
if (value == nullptr || !fileExists(value)) continue;
|
|
377
|
+
const std::filesystem::path parent = std::filesystem::path(value).parent_path();
|
|
378
|
+
if (!parent.empty()) scanDirectory(parent.string(), paths);
|
|
379
|
+
}
|
|
380
|
+
for (const std::string &path : paths) addFontFile(path);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
std::vector<size_t> lookupFamily(const std::string &name) {
|
|
384
|
+
Database &db = fontDatabase();
|
|
385
|
+
const std::string normalized = normalizeFamily(name);
|
|
386
|
+
if (!normalized.empty()) {
|
|
387
|
+
const auto found = db.by_family.find(normalized);
|
|
388
|
+
if (found != db.by_family.end()) return found->second;
|
|
389
|
+
}
|
|
390
|
+
const std::string compact = compactFamily(normalized);
|
|
391
|
+
if (!compact.empty()) {
|
|
392
|
+
const auto found = db.by_compact.find(compact);
|
|
393
|
+
if (found != db.by_compact.end()) return found->second;
|
|
394
|
+
}
|
|
395
|
+
return {};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
void xt_font_set_user_faces(const std::vector<UserFontFace> &faces) {
|
|
399
|
+
Database &db = fontDatabase();
|
|
400
|
+
db.faces.erase(std::remove_if(db.faces.begin(), db.faces.end(),
|
|
401
|
+
[](const FaceRecord &face) { return face.user; }),
|
|
402
|
+
db.faces.end());
|
|
403
|
+
|
|
404
|
+
/* A later `@font-face` with the same family and descriptors replaces an
|
|
405
|
+
* earlier one, so keep only the last request per key. */
|
|
406
|
+
std::unordered_map<std::string, size_t> last;
|
|
407
|
+
for (size_t i = 0; i < faces.size(); i++) last[userFaceKey(faces[i])] = i;
|
|
408
|
+
for (size_t i = 0; i < faces.size(); i++) {
|
|
409
|
+
const auto found = last.find(userFaceKey(faces[i]));
|
|
410
|
+
if (found != last.end() && found->second == i) addUserFace(faces[i]);
|
|
411
|
+
}
|
|
412
|
+
rebuildIndexes();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
void xt_font_db_shutdown() {
|
|
416
|
+
Database &db = fontDatabase();
|
|
417
|
+
db.defaults_loaded = false;
|
|
418
|
+
db.scanned = false;
|
|
419
|
+
db.faces.clear();
|
|
420
|
+
db.by_family.clear();
|
|
421
|
+
db.by_compact.clear();
|
|
422
|
+
db.by_path.clear();
|
|
423
|
+
db.parsed_paths.clear();
|
|
424
|
+
for (std::vector<std::string> &paths : db.defaults) paths.clear();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
} // namespace xtgui
|