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.
Files changed (122) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/driver/version.d.ts +1 -1
  3. package/dist/src/driver/version.js +1 -1
  4. package/dist/src/extensions/gui/features.d.ts +9 -6
  5. package/dist/src/extensions/gui/features.js +14 -9
  6. package/dist/src/extensions/gui/features.js.map +1 -1
  7. package/dist/tests/e2e/gui-css-coverage.test.d.ts +9 -0
  8. package/dist/tests/e2e/gui-css-coverage.test.js +333 -0
  9. package/dist/tests/e2e/gui-css-coverage.test.js.map +1 -0
  10. package/dist/tests/e2e/gui-css-features.test.d.ts +6 -0
  11. package/dist/tests/e2e/gui-css-features.test.js +270 -0
  12. package/dist/tests/e2e/gui-css-features.test.js.map +1 -0
  13. package/dist/tests/e2e/gui-effects.test.js +92 -11
  14. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  15. package/dist/tests/e2e/gui-font-face.test.d.ts +10 -0
  16. package/dist/tests/e2e/gui-font-face.test.js +142 -0
  17. package/dist/tests/e2e/gui-font-face.test.js.map +1 -0
  18. package/dist/tests/e2e/gui-fonts.test.d.ts +10 -0
  19. package/dist/tests/e2e/gui-fonts.test.js +150 -0
  20. package/dist/tests/e2e/gui-fonts.test.js.map +1 -0
  21. package/dist/tests/e2e/gui-helpers.d.ts +5 -0
  22. package/dist/tests/e2e/gui-helpers.js +4 -0
  23. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  24. package/dist/tests/e2e/gui-layout.test.js +3 -2
  25. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  26. package/dist/tests/e2e/gui-track-sizing.test.d.ts +6 -0
  27. package/dist/tests/e2e/gui-track-sizing.test.js +173 -0
  28. package/dist/tests/e2e/gui-track-sizing.test.js.map +1 -0
  29. package/dist/tests/e2e/gui-transform.test.js +3 -2
  30. package/dist/tests/e2e/gui-transform.test.js.map +1 -1
  31. package/dist/tests/extensions/gui-features.test.js +8 -1
  32. package/dist/tests/extensions/gui-features.test.js.map +1 -1
  33. package/doc/ai/extensions.md +10 -5
  34. package/doc/ai/zh-CN/extensions.md +9 -4
  35. package/doc/gui/animation.md +39 -9
  36. package/doc/gui/build.md +6 -5
  37. package/doc/gui/dom.md +20 -4
  38. package/doc/gui/known-issues.md +31 -23
  39. package/doc/gui/layout.md +30 -7
  40. package/doc/gui/rendering.md +84 -26
  41. package/doc/gui.md +62 -14
  42. package/doc/zh-CN/gui/animation.md +33 -10
  43. package/doc/zh-CN/gui/build.md +5 -5
  44. package/doc/zh-CN/gui/dom.md +16 -3
  45. package/doc/zh-CN/gui/known-issues.md +21 -12
  46. package/doc/zh-CN/gui/layout.md +22 -5
  47. package/doc/zh-CN/gui/rendering.md +61 -20
  48. package/doc/zh-CN/gui.md +53 -15
  49. package/package.json +7 -7
  50. package/runtime/ext_gui/animation_values.cpp +425 -0
  51. package/runtime/ext_gui/animation_values.h +64 -0
  52. package/runtime/ext_gui/css.cpp +172 -21
  53. package/runtime/ext_gui/css.h +75 -4
  54. package/runtime/ext_gui/css_match.cpp +4 -0
  55. package/runtime/ext_gui/css_sheet.cpp +218 -2
  56. package/runtime/ext_gui/document.cpp +42 -192
  57. package/runtime/ext_gui/document.h +25 -12
  58. package/runtime/ext_gui/document_animation.cpp +428 -0
  59. package/runtime/ext_gui/effect_values.cpp +245 -21
  60. package/runtime/ext_gui/gradient_values.cpp +24 -0
  61. package/runtime/ext_gui/grid_values.cpp +23 -0
  62. package/runtime/ext_gui/gui_cursor.cpp +50 -0
  63. package/runtime/ext_gui/gui_events.cpp +16 -4
  64. package/runtime/ext_gui/gui_internal.h +3 -0
  65. package/runtime/ext_gui/image_api.h +3 -1
  66. package/runtime/ext_gui/image_feature.cpp +4 -2
  67. package/runtime/ext_gui/inline_svg.cpp +95 -8
  68. package/runtime/ext_gui/inline_svg.h +5 -1
  69. package/runtime/ext_gui/layout.cpp +3 -2
  70. package/runtime/ext_gui/layout.h +6 -0
  71. package/runtime/ext_gui/layout_flex.cpp +134 -29
  72. package/runtime/ext_gui/layout_flow.cpp +58 -7
  73. package/runtime/ext_gui/layout_flow_internal.h +39 -4
  74. package/runtime/ext_gui/layout_grid.cpp +31 -0
  75. package/runtime/ext_gui/layout_inline.cpp +26 -2
  76. package/runtime/ext_gui/layout_vertical.cpp +196 -0
  77. package/runtime/ext_gui/paint.cpp +67 -9
  78. package/runtime/ext_gui/paint.h +61 -8
  79. package/runtime/ext_gui/paint_backdrop.cpp +211 -0
  80. package/runtime/ext_gui/paint_background.cpp +175 -97
  81. package/runtime/ext_gui/paint_effects.cpp +12 -6
  82. package/runtime/ext_gui/paint_internal.h +27 -0
  83. package/runtime/ext_gui/paint_outline.cpp +44 -0
  84. package/runtime/ext_gui/paint_shadow.cpp +368 -0
  85. package/runtime/ext_gui/renderer.cpp +15 -7
  86. package/runtime/ext_gui/renderer.h +10 -1
  87. package/runtime/ext_gui/renderer_backdrop.cpp +374 -0
  88. package/runtime/ext_gui/renderer_bitmap.cpp +82 -0
  89. package/runtime/ext_gui/renderer_gradient.cpp +51 -17
  90. package/runtime/ext_gui/renderer_image.cpp +6 -1
  91. package/runtime/ext_gui/renderer_internal.h +21 -0
  92. package/runtime/ext_gui/renderer_layer.cpp +27 -1
  93. package/runtime/ext_gui/renderer_text.cpp +2 -1
  94. package/runtime/ext_gui/style.cpp +62 -8
  95. package/runtime/ext_gui/style.h +111 -4
  96. package/runtime/ext_gui/style_cascade.cpp +83 -0
  97. package/runtime/ext_gui/style_internal.h +13 -1
  98. package/runtime/ext_gui/style_length.cpp +92 -0
  99. package/runtime/ext_gui/style_print.cpp +83 -2
  100. package/runtime/ext_gui/style_svg.cpp +40 -0
  101. package/runtime/ext_gui/style_text.cpp +302 -0
  102. package/runtime/ext_gui/style_ui.cpp +150 -0
  103. package/runtime/ext_gui/style_values.cpp +76 -62
  104. package/runtime/ext_gui/style_values_internal.h +23 -0
  105. package/runtime/ext_gui/text.cpp +4 -2
  106. package/runtime/ext_gui/text.h +11 -4
  107. package/runtime/ext_gui/text_cache.cpp +11 -2
  108. package/runtime/ext_gui/text_font_db.cpp +427 -0
  109. package/runtime/ext_gui/text_font_paths.cpp +250 -0
  110. package/runtime/ext_gui/text_font_paths.h +39 -0
  111. package/runtime/ext_gui/text_font_sfnt.cpp +396 -0
  112. package/runtime/ext_gui/text_font_sfnt.h +44 -0
  113. package/runtime/ext_gui/text_fonts.cpp +348 -0
  114. package/runtime/ext_gui/text_fonts.h +78 -0
  115. package/runtime/ext_gui/text_fonts_internal.h +77 -0
  116. package/runtime/ext_gui/text_hb.cpp +63 -179
  117. package/runtime/ext_gui/text_stb.cpp +49 -193
  118. package/runtime/ext_gui/transform.cpp +107 -9
  119. package/runtime/ext_gui/transform.h +5 -0
  120. package/runtime/ext_gui/window.cpp +1 -1
  121. package/src/driver/version.ts +1 -1
  122. package/src/extensions/gui/features.ts +13 -9
@@ -30,21 +30,27 @@ running transition is recorded and re-applied every frame until it finishes;
30
30
  the engine advances the clock with real frame deltas (`XtDocument::advance`),
31
31
  and `win.advance(ms)` lets tests step it deterministically.
32
32
 
33
- - Supported properties: `background-color`, `color`, `border-color`,
34
- `border-radius` and `transform`; `transition: all` covers them. A
35
- layout-affecting property (width, position, …) is still not animated — that
36
- would reflow every frame — but `transform` is paint-time, so animating it costs
37
- one matrix per animated box per frame ([Layout](./layout.md#transforms-m15)).
33
+ - Supported properties: `background-color`, `color`, `border-color`, `opacity`,
34
+ `border-radius`, `transform`, `text-shadow`, `box-shadow`, every box offset
35
+ and size length (`width`/`height`/`top`/`left`/`margin-*`/`padding-*`/…), the
36
+ gap/flex-basis lengths and `filter` (matching filter lists interpolate their
37
+ blur lengths, amounts and drop-shadow colours). `transition: all` covers the
38
+ whole list. Lengths
39
+ interpolate when their units line up (a mixed-unit change is CSS's discrete
40
+ fallback); `opacity`, colours and `transform` interpolate normally.
38
41
  - Both the `transition` shorthand and the `transition-property` / `-duration` /
39
42
  `-delay` / `-timing-function` longhands are parsed; time values accept `s` and
40
- `ms`; timing functions are `linear`, `ease`, `ease-in`, `ease-out` and
41
- `ease-in-out` (`ease` is a smoothstep approximation).
43
+ `ms`; `calc()` is accepted inside the shorthand and longhands.
44
+ - Timing functions are `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out`,
45
+ `cubic-bezier(x1, y1, x2, y2)`, `steps(n, start|end)` and their `jump-*`
46
+ spellings. The engine solves the cubic Bézier numerically; keyword curves use
47
+ the CSS control points.
42
48
  - Retargeting mid-flight starts a new transition from the current interpolated
43
49
  value, so reversing a hover animates smoothly from wherever it was.
44
50
  - `computedStyle()` reports the displayed (interpolated) value, so transitions
45
51
  are directly observable in tests.
46
52
 
47
- `transform` lists interpolate **element-wise** (`document.cpp`,
53
+ `transform` lists interpolate **element-wise** (`animation_values.cpp`,
48
54
  `xt_transform_interpolate`), which is what makes the common pair
49
55
  `translateY(-4px) rotate(-0.35deg)` <-> `none` glide instead of jumping: the
50
56
  missing side is filled with identity ops of the same kind. Two lists whose shapes
@@ -52,6 +58,29 @@ or units do not line up (`rotate(10deg)` vs `rotate(10rad)`, or a different numb
52
58
  of functions) are not interpolated at all — the transition snaps to the target,
53
59
  which is CSS's discrete fallback rather than a wrong-looking sweep.
54
60
 
61
+ ## CSS animations (`@keyframes`)
62
+
63
+ `@keyframes` rules are parsed into `StyleSheet::keyframes` (offsets from
64
+ `from`/`to`/percent selectors, sorted). `animation` and every `animation-*`
65
+ longhand are supported: name, duration, delay, timing function, iteration count
66
+ (including `infinite`), direction (`normal`/`reverse`/`alternate`/
67
+ `alternate-reverse`), fill mode (`none`/`forwards`/`backwards`/`both`) and
68
+ `paused`/`running`.
69
+
70
+ `XtDocument` starts one active animation per element/name when the computed
71
+ style first declares it, restarts it when the shorthand changes, and leaves it
72
+ alone across unrelated restyles. Each frame it resolves the local time to an
73
+ iteration progress, applies the timing function **per keyframe interval**, and
74
+ interpolates every animatable property. A property that a middle keyframe does
75
+ not mention keeps interpolating between the nearest keyframes that do declare
76
+ it (so a transform declared at 0% and 100% runs straight through a 50% step that
77
+ only changes `opacity`). Missing 0%/100% steps use the element's underlying
78
+ style. `forwards`/`backwards` hold the edge value outside the active time, and
79
+ `computedStyle()` reports the animated value.
80
+
81
+ Animations and transitions share `animation_values.cpp`, so both understand the
82
+ same property set, shadow lists and `transform` identity filling.
83
+
55
84
  ## CSS transforms (M15)
56
85
 
57
86
  `transform` and `transform-origin` are supported as *paint-time* effects
@@ -93,5 +122,6 @@ Both are exercised by `examples/gui/pelican-svg` (per-frame sway plus a
93
122
  hover transition on the caption) and pinned by
94
123
  `tests/e2e/gui-transform.test.ts`.
95
124
 
96
- Still to do for animation: `@keyframes` animations and `cubic-bezier(...)`.
125
+ `calc()` can be used in any animated length (for example
126
+ `transition: width 300ms ease-out; width: calc(100% - 20px)`).
97
127
 
package/doc/gui/build.md CHANGED
@@ -131,7 +131,7 @@ features** selected per program:
131
131
  | `media` | `media_bridge.c` + `media.a` (FFmpeg) | an HTML `<video>`/`<audio>` tag, `createElement("video"/"audio")`, `new Audio()`, `decodeAudioData`, or runtime-built markup |
132
132
  | `gpu` | `gpu_bridge.c` (the implementation is already in `gui.a`) | a `getContext("webgl"/"webgl2"/"experimental-webgl")` (or a kind decided at run time), any `webgl` string, `mix-blend-mode`/`mixBlendMode`, or runtime-built markup |
133
133
  | `images` | `image_bridge.c` (the implementation is already in `gui.a`) | `<img>`, inline `<svg>`, `background-image`/`url(...)`, a `.svg` path, `drawImage`/`createImageBitmap`/`Image`, or runtime-built markup |
134
- | `shaping` | `text_bridge.c` (the implementation is already in `gui.a`) | non-ASCII text (CJK/Cyrillic/Greek/…), an escaped/entity codepoint, or runtime-built markup |
134
+ | `shaping` | `text_bridge.c` (the implementation is already in `gui.a`) | non-ASCII text (CJK/Cyrillic/Greek/…), an escaped/entity codepoint, an author `@font-face` rule (custom fonts may be CFF/OTF), or runtime-built markup |
135
135
  | `webgl` | `webgl_bridge.c` (the implementation is already in `gui.a`; requires `gpu`) | a WebGL `getContext(...)`, a `webgl` string, or runtime-built markup |
136
136
 
137
137
  The driver scans the bundled AST (`src/driver/usage.ts`): every string literal
@@ -206,10 +206,11 @@ TrueType outlines and a per-codepoint mapping plus the font's `kern` table, so
206
206
  CFF/OTF and variable fonts, OpenType ligatures and complex-script shaping
207
207
  (Arabic/Indic/Thai) are not available. The fallback chain still selects another
208
208
  face for codepoints the primary lacks. Selecting the optional **`shaping`**
209
- feature (automatically for non-ASCII text, or with `--feature shaping`) links
210
- HarfBuzz + FreeType back in (~1.2 MiB) and restores full OpenType shaping and
211
- CFF/OTF/variable-font support. A Latin-only DOM page therefore stays near
212
- 3.1 MiB; a Chinese/complex-script page is closer to 4.3 MiB.
209
+ feature (automatically for non-ASCII text or an author `@font-face` rule, or
210
+ with `--feature shaping`) links HarfBuzz + FreeType back in (~1.2 MiB) and
211
+ restores full OpenType shaping and CFF/OTF/variable-font support. A Latin-only
212
+ DOM page therefore stays near 3.1 MiB; a Chinese/complex-script page is closer
213
+ to 4.3 MiB.
213
214
 
214
215
  ## Running the example
215
216
 
package/doc/gui/dom.md CHANGED
@@ -21,11 +21,26 @@ commas, brackets or parentheses without splitting the list. Selectors: type,
21
21
  `.class`, `#id`, attribute (`=`, `~=`, `|=`, `^=`, `$=`, `*=`), the four
22
22
  combinators (descendant, child, adjacent and general sibling), the pseudo-classes
23
23
  `:first-child`, `:last-child`, `:only-child`, `:empty`, `:root`, `:not(...)`,
24
- `:nth-child(an+b)`, `:disabled`, `:checked`, plus the stateful `:hover` and
25
- `:focus` (see [Input](./input.md)), and the pseudo-elements `::before`/`::after`
24
+ `:nth-child(an+b)`, `:disabled`, `:checked`, plus the stateful `:hover`,
25
+ `:focus`, `:focus-visible` (currently an alias of `:focus`) and `:active` (see
26
+ [Input](./input.md)), and the pseudo-elements `::before`/`::after`
26
27
  (legacy `:before`/`:after` too). Values: lengths (`px`, `%`, `em`, `rem`, `vw`,
27
28
  `vh`, `pt`, `pc`, `in`, `cm`, `mm`, `q`), colors (hex, `rgb()`/`rgba()`, a named
28
- subset), numbers, keywords and shorthands (`margin`/`padding`/`border`/`flex`).
29
+ subset), numbers, keywords and shorthands (`margin`/`padding`/`border`/`flex`/
30
+ `font`/`outline`). `display: inline-flex`/`inline-grid` make a flex/grid
31
+ container inline-level; `grid-area`, `aspect-ratio`, `letter-spacing`,
32
+ `text-transform`, `writing-mode` (`vertical-rl`/`vertical-lr` text),
33
+ `vertical-align` (length form), percentage `border-radius`, `background-size`
34
+ (size + default repeat for `url()` layers) and `currentColor` are parsed too.
35
+ `font-variant-numeric`, `text-wrap`, `text-rendering`, `touch-action`,
36
+ `user-select`, `overscroll-behavior`, `scroll-behavior`, `isolation` and
37
+ `vector-effect` are accepted but have no engine behaviour (the text stack exposes no OpenType features and the engine
38
+ has no touch/selection/smooth-scroll subsystem); `cursor` maps to the SDL system
39
+ cursor. A document rule may style SVG children with the presentation properties
40
+ `fill`, `fill-opacity`, `fill-rule`, `stroke`, `stroke-opacity`, `stroke-width`,
41
+ `stroke-dasharray`, `stroke-dashoffset`, `stroke-linecap` and `stroke-linejoin`;
42
+ the serializer writes them into the inline `<svg>` markup as NanoSVG-readable
43
+ inline styles. `skewX()`/`skewY()`/`skew()` transform functions are supported.
29
44
  `transform` (`translate`/`translate3d`/`translateX`/`translateY`, `scale`/
30
45
  `scale3d`/`scaleX`/`scaleY`, `rotate`/`rotateZ`, `matrix`; the `3d` spellings'
31
46
  `z` argument is dropped) and `transform-origin` are parsed into the computed
@@ -33,7 +48,8 @@ style as a list of operations plus two (unresolved) origin lengths; an
33
48
  unsupported function invalidates the whole declaration
34
49
  ([Layout](./layout.md#transforms-m15)). M19 adds `background-image` layer lists
35
50
  (linear/radial), `clip-path` (`inset`/`circle`/`ellipse`/`polygon`), the
36
- `filter`/`backdrop-filter` function lists, `text-shadow` and `content`.
51
+ `filter`/`backdrop-filter` function lists, `text-shadow`, `box-shadow` and
52
+ `content`.
37
53
 
38
54
  **Cascade & computed style** (`runtime/ext_gui/style.{h,cpp}`): a small built-in
39
55
  UA stylesheet, author rules sorted by `(!important, specificity, source order)`,
@@ -64,29 +64,37 @@
64
64
  *class change* on a flex container has also been observed to crash the same
65
65
  way; `examples/gui/pelican-bike` reveals its game-over overlay with `opacity`
66
66
  instead.
67
- - **CSS coverage is a deliberate subset.** Grid, `clamp()`/`min()`/`max()`/`env()`,
68
- `var()` custom properties, the `inset` shorthand, `box-sizing`, `visibility` and
69
- `pointer-events`, `@media` queries, generated content (`::before`/`::after`),
70
- multi-layer linear/radial backgrounds, `text-shadow`, `clip-path`
71
- (`inset`/`circle`/`ellipse`/`polygon`) and `filter` colour functions are
72
- implemented. Known limits: `background-clip`,
73
- `background-position`/`size`/`repeat` and `backdrop-filter` compositing are not
74
- implemented (`backdrop-filter` parses and cascades, but sampling the backdrop
75
- needs an offscreen layer, so it paints nothing); `mix-blend-mode` and repeating
76
- gradients are implemented (see [Rendering](./rendering.md)), but group
77
- `opacity` is a per-shape alpha rather than a true group composite, and the
78
- blend compositor is unavailable on the D3D12/DXIL backend, where groups fall
79
- back to normal source-over painting;
80
- `filter: blur()` is a nine-tap per-shape spread rather than a true group blur,
81
- so overlapping descendants can darken; `drop-shadow` is approximated from box
82
- silhouettes; at most two nested `clip-path` shapes are intersected (deeper
83
- nesting keeps the outer two); and images/canvases are not colour-filtered
84
- except for the brightness/opacity tint the image shader can carry. Canvas
85
- textures are sampled nearest-neighbour. A background linear gradient remains
86
- solid bands (18 strips, 64 while repeating), so a large soft ramp may show
87
- slight banding; a radial gradient is rasterised per pixel into a texture
88
- (capped at 1024 px per edge, then stretched, so a hard stop in a very large
89
- box comes out slightly soft).
67
+ - **CSS coverage is a deliberate subset.** Grid, `calc()` (including
68
+ nested arithmetic and `calc()` inside `min()`/`max()`/`clamp()`),
69
+ `clamp()`/`min()`/`max()`/`env()`, `var()` custom properties, the `inset`
70
+ shorthand, `box-sizing`, `visibility` and `pointer-events`, `@media` queries,
71
+ generated content (`::before`/`::after`), multi-layer linear/radial
72
+ backgrounds, `text-shadow` (true Gaussian), `box-shadow` (outer/inset, spread,
73
+ multiple layers), `clip-path` (`inset`/`circle`/`ellipse`/`polygon`),
74
+ `filter` colour functions, `@keyframes` animations, the full `transition`
75
+ timing-function set, `letter-spacing`, `text-transform`, inline-level
76
+ `flex`/`grid`, `grid-area`, `aspect-ratio`, auto margins, percentage
77
+ `border-radius`, `outline`, `background-size` tiling for `url()` layers and
78
+ document CSS on SVG children (`fill`/`stroke`/…) are implemented. Known
79
+ limits: `background-clip`, `background-position` and `background-repeat`
80
+ remain unimplemented (`background-size` covers the tiled default);
81
+ `font-variant-numeric`, `text-wrap`, `text-rendering`, `touch-action`,
82
+ `user-select`, `overscroll-behavior`, `scroll-behavior`, `isolation` and
83
+ `vector-effect` are parsed but inert; vertical writing mode is text-only (nested/mixed content falls back to
84
+ horizontal); group `opacity` is
85
+ a per-shape alpha rather than a true group composite; the blend and
86
+ `backdrop-filter` compositors are unavailable on the D3D12/DXIL backend, where
87
+ groups fall back to normal source-over painting and the backdrop filter is
88
+ skipped. `filter: blur()` is still a nine-tap per-shape spread rather than a
89
+ true group blur, so overlapping descendants can darken; `drop-shadow` is
90
+ approximated from box silhouettes; at most two nested `clip-path` shapes are
91
+ intersected (deeper nesting keeps the outer two); and images/canvases are not
92
+ colour-filtered except for the brightness/opacity tint the image shader can
93
+ carry. Canvas textures are sampled nearest-neighbour. Background gradients
94
+ (linear and radial) are rasterised per pixel into a texture and then stretched
95
+ when the box is larger than the texture (1024 px per edge for a radial ramp,
96
+ 4096 px along the line for a linear one, which is only one texel thick across
97
+ it), so a hard stop in a very large box can come out slightly soft.
90
98
  - **SVG is NanoSVG's subset.** SVG *files* (`<img src="logo.svg">`) and inline
91
99
  `<svg>` trees share the vendored NanoSVG parser/rasteriser
92
100
  (`runtime/ext_gui/svg.cpp`, `inline_svg.cpp`), which renders
package/doc/gui/layout.md CHANGED
@@ -25,7 +25,10 @@ with absolute (viewport-relative) geometry:
25
25
  measured with the stb_truetype stack (see [Text stack](./rendering.md#text-stack-m4b)).
26
26
  - **Flexbox** — `row`/`column` (and the `-reverse` variants) with `gap`,
27
27
  `flex-basis`/`flex-grow`/`flex-shrink`, `justify-content` and `align-items`
28
- (including `stretch` when the cross size is definite). `flex-wrap: wrap`
28
+ (including `stretch` when the cross size is definite). On both axes the main
29
+ size comes from `flex-basis` (or the measured size when it is `auto`), positive
30
+ free space is split by `flex-grow` and a deficit by `flex-shrink`, and an
31
+ item's `align-self` overrides the container's `align-items`. `flex-wrap: wrap`
29
32
  breaks items into several lines, `align-content`
30
33
  (`stretch`/`flex-start`/`flex-end`/`center`/`space-between`/`space-around`)
31
34
  distributes the lines on the cross axis and `wrap-reverse` flips that axis.
@@ -37,14 +40,34 @@ with absolute (viewport-relative) geometry:
37
40
  `row-gap` / `column-gap`, row-major auto-placement, simple `grid-column` /
38
41
  `grid-row` line placement, and `align/justify-items` plus
39
42
  `align/justify-self` (including `place-items` / `place-self`). Implicit rows
40
- and columns grow from the items; a single-cell grid with `place-items: center` centres a card in the whole overlay. Item measurement is a two-pass
43
+ and columns grow from the items; a definite grid height is shared across `fr`
44
+ rows after the fixed and auto rows are sized, so `aspect-ratio` can drive a
45
+ `repeat(n, 1fr)` grid. A single-cell grid with `place-items: center` centres a
46
+ card in the whole overlay. Item measurement is a two-pass
41
47
  layout (intrinsic size, then final placement), so text fragments are cleared
42
48
  between passes.
43
- - **CSS value functions and custom properties** — `clamp(min, value, max)`,
44
- `min(...)` and `max(...)` keep their arguments unresolved in the computed style
45
- and are evaluated at layout time against the element's font size and the
46
- viewport (`vw`/`vh`); `env(safe-area-inset-*)` resolves to zero in a desktop
47
- window. `var(--name[, fallback])` is substituted from inherited custom
49
+ - **Inline-level containers, auto margins and aspect ratio** — `display:
50
+ inline-flex`/`inline-grid` lay their children out with the flex/grid algorithm
51
+ but participate in the parent line as one atomic inline box (`vertical-align`
52
+ shifts it by its length value). A block-level box with `margin-left: auto` /
53
+ `margin-right: auto` absorbs the leftover inline space (both auto → centred);
54
+ a flex item's auto main-axis margins absorb the line's free space before
55
+ `justify-content`, which is what pushes a footer or button hint to the end.
56
+ `aspect-ratio` derives a `height: auto` box from its used width (the
57
+ width-from-height direction is not implemented). `border-radius` accepts a
58
+ percentage and resolves against the smaller border-box side.
59
+ - **Vertical writing mode** — `writing-mode: vertical-rl|vertical-lr` lays text
60
+ out one character per column step: CJK/kana/Hangul glyphs stay upright and
61
+ other scripts are rotated 90° at paint time. A definite height starts a new
62
+ column; a flex container's row/column axes swap with the writing mode. This is
63
+ a text-focused implementation: nested blocks and mixed horizontal content in a
64
+ vertical container still fall back to horizontal layout.
65
+ - **CSS value functions and custom properties** — `calc()` arithmetic (including
66
+ nested `calc()` and `calc()` inside the other functions), `clamp(min, value,
67
+ max)`, `min(...)` and `max(...)` keep their expression unresolved in the
68
+ computed style and are evaluated at layout time against the element's font
69
+ size and the viewport (`vw`/`vh`); `env(safe-area-inset-*)` resolves to zero in
70
+ a desktop window. `var(--name[, fallback])` is substituted from inherited custom
48
71
  properties during the cascade. `inset` is the four-value
49
72
  `top/right/bottom/left` shorthand, `box-sizing: border-box` subtracts padding
50
73
  and borders from an explicit width/height, `visibility: hidden` skips painting
@@ -76,17 +76,27 @@ divided back into logical units, exactly the way HiDPI scaling already worked.
76
76
 
77
77
  The window background (`setBackground`) is the render-pass clear colour.
78
78
 
79
+ `background-size` on a `url()` layer emits one image quad per tile (default
80
+ `repeat`, 4096-tile cap) instead of stretching the picture to the box;
81
+ percentage sizes resolve against the box, `auto` keeps the box dimension.
82
+ `outline` paints four edge rectangles outside the border box (offset included)
83
+ after the border and before the content. A vertical-writing-mode text run is
84
+ painted by rotating its glyph quads 90° (CJK glyphs stay upright and are centred
85
+ in their column).
86
+
79
87
  ### Backgrounds, generated content and effects (M19)
80
88
 
81
89
  `background-color` and every `background-image` layer are painted. A
82
90
  comma-separated layer list is painted bottom-up (the first declaration layer is
83
91
  topmost), and both gradient kinds are supported:
84
92
 
85
- - a **linear** gradient is sampled at 18 points (64 when repeating) and emitted
86
- as solid rectangles; `to <side>`, `deg`/`grad`/`rad`/`turn`, percentage or
87
- length (`px`/`em`/…) offsets, the `color <start> <end>` shorthand and
88
- multi-stop colours work (the band walk is axis-aligned, so an oblique angle
89
- uses the nearest axis);
93
+ - a **linear** gradient follows the CSS gradient line (through the box centre,
94
+ at the declared angle, with the box extent projected onto it as its length, so
95
+ `%` and `%`-inside-`calc()` offsets resolve against that length) and is
96
+ resolved per pixel like a radial one. The ramp only varies along the line, so
97
+ the texture is one row (or one column) of device pixels. `to <side>`,
98
+ `deg`/`grad`/`rad`/`turn`, percentage or length (`px`/`em`/…) offsets, the
99
+ `color <start> <end>` shorthand and multi-stop colours work;
90
100
  - a **radial** gradient is resolved per pixel in `renderer_gradient.cpp` into one
91
101
  RGBA texture that is drawn as a single image quad (`PaintGradient`). Nesting the
92
102
  bands instead would composite their alpha once per band — since every band
@@ -98,6 +108,14 @@ topmost), and both gradient kinds are supported:
98
108
  the side or corner, as CSS does. Stops interpolate in premultiplied alpha, and
99
109
  the last stop extends past the ending shape over the rest of the box.
100
110
 
111
+ Both kinds share that per-pixel path: a walk of solid rectangles composites a
112
+ translucent stop once per band and cannot place a stop narrower than a band, so a
113
+ `transparent … calc(100% - 1px), rgba(…) calc(100% - 1px)` divider line either
114
+ fell between two samples (and vanished) or widened into a full band. A stop
115
+ position given as `calc()` keeps its percentage relative to the gradient line,
116
+ and a stop whose resolved offset is negative stays where the declaration put it
117
+ instead of being treated as "no offset declared".
118
+
101
119
  `repeating-linear-gradient()` and `repeating-radial-gradient()` tile the stop
102
120
  pattern from 0 to the last stop's offset across the whole box; the stop list is
103
121
  not forced to end at 100%, so `red 0%, blue 10%` repeats every 10%. A normal
@@ -105,18 +123,34 @@ gradient whose last stop is below 100% holds that colour for the rest of the box
105
123
  as in CSS.
106
124
 
107
125
  The background colour sits under every layer. Gradients are partially
108
- transparent if their stops are; a radial gradient is exact per pixel, while a
109
- linear one still samples 18 bands and can show banding on a large soft ramp.
126
+ transparent if their stops are; both kinds are exact per pixel, so hard stops and
127
+ 1px details stay hard instead of banding.
110
128
 
111
129
  **Generated content** needs no new display-list kind: `::before`/`::after` are
112
130
  ordinary layout boxes owned by the per-restyle arena in `style.cpp`, so their
113
131
  background, border, text and transforms flow through the same paint walk.
114
132
  `content` is resolved from strings, `attr()` and open/close quotes.
115
133
 
116
- **`text-shadow`** is emitted as shadow `PaintText` runs *before* the real run, so
117
- the text pipeline draws them without a new shader. A blurred shadow is nine
118
- translucent copies spread around the offset (the same approximation `filter:
119
- blur()` uses).
134
+ **`text-shadow`** with no blur is emitted as shadow `PaintText` runs *before* the
135
+ real run, so the text pipeline draws it crisply. A blurred shadow is a real
136
+ Gaussian: `paint_shadow.cpp` rasterises the glyph coverage, convolves it with a
137
+ separable kernel (`sigma = blur / 2`, cropped at ±3σ), and emits one tinted image
138
+ quad. The mask is cached by text/font/blur, so an animated colour only changes
139
+ the quad's tint.
140
+
141
+ **`box-shadow`** uses the same Gaussian mask: the rounded-rectangle silhouette is
142
+ rasterised (spread expands or shrinks it), convolved, and composited behind the
143
+ background; `inset` shadows invert the mask, crop it to the padding box and paint
144
+ above the background. A zero-blur outer shadow stays an exact rounded rect. The
145
+ declaration list is painted last-layer-first, matching CSS.
146
+
147
+ **`backdrop-filter`** is composited by the same offscreen machinery as
148
+ `mix-blend-mode`: when the painter reaches the box, the renderer copies the
149
+ scene painted so far into a scratch texture, applies the Gaussian and one
150
+ combined colour matrix (brightness/contrast/grayscale/sepia/invert/saturate/
151
+ hue-rotate/opacity multiplied together) in a full-screen shader, and composites
152
+ the result back through the box's rounded border-box mask. A `backdrop-filter`
153
+ therefore shows as a `backdrop …` op in `paintList()`.
120
154
 
121
155
  **`clip-path`** is resolved in `paint_clip.cpp` to a device-space polygon (the
122
156
  box transform is applied to every point), triangulated with ear clipping, and
@@ -133,8 +167,8 @@ colour, so a box and its descendants see the filter. `blur()` and
133
167
  `drop-shadow()` are approximations at the display-list level: blur spreads nine
134
168
  translucent copies per shape, drop-shadow is a box/text silhouette. This keeps
135
169
  the ordinary painter single-pass at the cost of group semantics: overlapping
136
- descendants of a blurred/filtered group can darken, and `backdrop-filter` cannot
137
- sample what is behind the element, so it parses and cascades but paints nothing.
170
+ descendants of a blurred/filtered group can darken. `backdrop-filter` is the
171
+ exception — it has its own offscreen snapshot/filter pass (see above).
138
172
 
139
173
  **`mix-blend-mode`** is the exception that does need an offscreen layer. A box
140
174
  with a non-`normal` mode is wrapped in `LayerBegin`/`LayerEnd` markers (visible
@@ -149,7 +183,8 @@ SPIRV-Cross (Metal); the two scene textures per nesting level are reused across
149
183
  frames and ping-pong so the backdrop stays sampleable while it is written.
150
184
  Backends that consume neither format — the D3D12/DXIL path, which has no shader
151
185
  compiler here — keep painting the group with ordinary source-over blending.
152
- `backdrop-filter` still paints nothing.
186
+ `backdrop-filter` uses the same runtime-compiled path; when the shader backend is
187
+ unavailable it is skipped while the rest of the page still paints.
153
188
 
154
189
  ## Text stack (M4b)
155
190
 
@@ -157,22 +192,43 @@ compiler here — keep painting the group with ordinary source-over blending.
157
192
  backend table. The default backend is the bundled **stb_truetype**
158
193
  (`text_stb.cpp`, `runtime/ext_gui/stb_truetype.h`). The optional **`shaping`**
159
194
  link feature installs `text_hb.cpp`, a HarfBuzz + FreeType backend, and is
160
- selected automatically for non-ASCII text.
195
+ selected automatically for non-ASCII text or an author `@font-face` rule. Both
196
+ backends share the font database in `text_font_db.cpp` (discovery and indexing)
197
+ and the CSS matcher in `text_fonts.cpp`, plus the sfnt metadata reader in
198
+ `text_font_sfnt.cpp`, so a CSS style resolves to the same real face whichever
199
+ shaper is linked.
161
200
 
162
- - Fonts are resolved from well-known system paths (Helvetica/Arial on macOS,
163
- DejaVu/Liberation on Linux, Segoe UI/Arial on Windows), overridable with
164
- `XT_GUI_FONT` (and `XT_GUI_FONT_MONO`), and cached per `(family class, size)`.
165
- Only regular upright faces are used for now; weight/italic selection is a
166
- later refinement.
201
+ - Fonts are resolved from the installed system faces. The `font-family` list is
202
+ matched case-insensitively; `sans-serif`, `serif`, `monospace` (and the
203
+ `system-ui`/`ui-*` aliases) map to platform defaults, while any other family
204
+ name discovers the platform font directories once. `font-weight` (1–1000) and
205
+ `font-style: italic` select that family's real bold/italic face using the CSS
206
+ Fonts weight-matching order; a missing weight picks the nearest face and a
207
+ family without an italic face uses its upright face — there is no synthetic
208
+ bold or oblique. Faces are cached per `(file, collection index, size)`.
209
+ `XT_GUI_FONT` / `XT_GUI_FONT_MONO` still override the generic defaults.
210
+ - Author **`@font-face`** rules load local sfnt faces (`.ttf`, `.ttc`, `.otf`,
211
+ `.otc`). The `src` list is tried from left to right; `local(...)`, remote and
212
+ `data:` entries are skipped. A relative URL is a local `file:` path resolved
213
+ against the process working directory — the same policy as image sources — and
214
+ `file://` URLs (including percent-encoded paths) are accepted. The
215
+ `font-weight`/`font-style` descriptors select the face; a weight range
216
+ (`font-weight: 100 900`) is honoured, and later rules with the same family and
217
+ descriptors replace earlier ones. CSS matches the declared family name even
218
+ when it differs from the font file's internal name. When no source is
219
+ readable the family simply falls through to the next family in the CSS list.
167
220
  - The default candidates prefer a **CJK-capable TrueType/TTC** face (Microsoft
168
221
  YaHei on Windows, PingFang/STHeiti on macOS, Noto Sans CJK/WenQuanYi on Linux)
169
- before the Latin-only ones. A candidate that stb_truetype cannot parse (for
170
- example a CFF/OTF collection) is skipped in favour of the next one.
222
+ before the Latin-only ones. A family's weight/style variants are resolved
223
+ across its files, and a candidate stb_truetype cannot parse (for example a
224
+ CFF/OTF collection) is skipped in favour of the next one; the `shaping`
225
+ backend loads it instead.
171
226
  - **Codepoints the primary face lacks fall back to another face.** The engine
172
227
  keeps a small fallback chain — the platform's symbol face (Segoe UI Symbol,
173
- Apple Symbols, Noto Sans Symbols, DejaVu Sans) first, then the other family
174
- class — and resolves each codepoint to the first face in it that has the
175
- glyph. The run is shaped in segments, one per face, so a mixed run (Latin
228
+ Apple Symbols, Noto Sans Symbols, DejaVu Sans) first, then the default sans,
229
+ serif and monospace faces — and resolves each codepoint to the first face in
230
+ it that has the glyph. The run is shaped in segments, one per face, so a mixed
231
+ run (Latin
176
232
  plus an arrow, box-drawing or geometric shape, or Latin inside CJK) draws
177
233
  every glyph instead of dropping the missing ones as `.notdef`. Each shaped
178
234
  glyph carries its face (`ShapedGlyph::font`) so the renderer rasterises it
@@ -219,7 +275,9 @@ is what `tests/e2e/gui-perf.test.ts` asserts on.
219
275
  glyphs / 8,192 entries; when a generation is full the *spare* one is dropped
220
276
  and becomes current, so the hot path has no LRU bookkeeping. A word is shaped
221
277
  once and reused by every later layout pass and repaint that draws it. Primary
222
- face lookups are cached by (family class, size) as well.
278
+ face lookups are cached by (family, weight, style, size) as well; the font
279
+ database itself scans the platform directories only once, on the first
280
+ non-generic family request.
223
281
  - **No-op writes.** `textContent` and `style.<property>` do nothing when the
224
282
  node already shows that value: the document is not dirtied, so no restyle,
225
283
  relayout, repaint or upload follows. A HUD that assigns the same clock string,
package/doc/gui.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xbintsc GUI extension (self-hosted HTML/CSS renderer)
2
2
 
3
- Status: **M20** — features (M1–M15) are complete: HTML parsing, CSS selector
3
+ Status: **M22** — features (M1–M15) are complete: HTML parsing, CSS selector
4
4
  matching, the cascade, computed styles and layout (block, inline, Flexbox and
5
5
  CSS positioning) are
6
6
  in place, and the engine *paints*: it builds a display list of rectangles, images
@@ -44,13 +44,17 @@ them) can be translated, scaled and rotated without reflowing anything — and
44
44
  **M19** fills in the decorative CSS layer: `@media` queries, generated content
45
45
  (`::before`/`::after`), multi-layer linear/radial/repeating backgrounds,
46
46
  `text-shadow`, `clip-path`, `mix-blend-mode` (through an offscreen compositor)
47
- and the `filter` colour/shadow effects. `backdrop-filter` still needs a backdrop
48
- snapshot, and true group blur remains an approximation — see
47
+ and the `filter` colour effects. See
49
48
  [Rendering](./gui/rendering.md#backgrounds-generated-content-and-effects-m19).
50
49
  **M20** makes large documents interactive: `@media` is evaluated once per rule,
51
50
  layout and paint share a bounded shaping cache, and a frame only does (and
52
51
  presents) the work a change requires — see
53
52
  [Performance](./gui/rendering.md#performance-m20).
53
+ **M21** completes the value/animation gaps: `calc()` arithmetic everywhere a
54
+ length is accepted, true Gaussian `text-shadow`/`box-shadow`, backdrop
55
+ compositing, `@keyframes` and the full transition timing-function set. See
56
+ [Rendering](./gui/rendering.md#backgrounds-generated-content-and-effects-m19)
57
+ and [Animation](./gui/animation.md#css-animations-keyframes).
54
58
  This overview records the locked decisions, the architecture, the milestone plan
55
59
  and the current progress; the engine itself is documented by function in the
56
60
  pages above.
@@ -179,8 +183,10 @@ engine were Rust.
179
183
  rounded-rect SDL_GPU pipeline (`paint.*`, `renderer.*`).
180
184
  - **M4b-1 — text stack + metrics** ✅ the text module (`text.*`) with font
181
185
  resolution/caching and shaping-based metrics used by layout
182
- (`measureText`/`fontMetrics`); originally HarfBuzz + FreeType, now the
183
- bundled stb_truetype.
186
+ (`measureText`/`fontMetrics`); the CSS family list, weight and italic style
187
+ select real installed faces, and author `@font-face` rules load local font
188
+ files (relative URLs resolve as `file:` paths against the process working
189
+ directory). Originally HarfBuzz + FreeType, now the bundled stb_truetype.
184
190
  - **M4b-2 — glyph rendering** ✅ stb_truetype rasterisation, a shared shelf-packed
185
191
  glyph atlas, textured text quads in the display list and HiDPI-aware raster
186
192
  scaling. Gradients remain.
@@ -195,8 +201,8 @@ engine were Rust.
195
201
  per-file GPU textures and textured quads (`image.*`, `svg.*`, `paint.*`,
196
202
  `renderer.*`).
197
203
  - **M6b — transitions/animations** ✅ `transition` shorthand + longhands,
198
- animated `background-color`/`color`/`border-color`/`border-radius`, retargeting
199
- and `win.advance(ms)`. `@keyframes` remain.
204
+ retargeting, `cubic-bezier()`/`steps()`, `win.advance(ms)` and (M21)
205
+ `@keyframes` with every animation longhand.
200
206
  7. **M7 — CI & releases** ✅ (Linux/macOS/Windows x64 build) / 🚧 (run + Windows arm64)
201
207
  - `compile-examples` builds `gui.a` and `media.a` on Linux and macOS
202
208
  (required), then runs the examples and `tests/e2e/gui-*.test.ts` under
@@ -348,16 +354,18 @@ engine were Rust.
348
354
  - Generated content: `::before`/`::after` selectors, inherited pseudo styles,
349
355
  `content` strings/escapes/`attr()`, and synthetic layout boxes that never
350
356
  leak into DOM traversal.
351
- - Multi-layer `background-image` with linear gradients painted bottom-up as
352
- solid bands (18 strips) and radial gradients rasterised per pixel into one
353
- image texture (`renderer_gradient.cpp`).
354
- - `text-shadow` as shadow text runs, including a nine-tap blur approximate.
357
+ - Multi-layer `background-image` with linear and radial gradients rasterised
358
+ per pixel into one image texture (`renderer_gradient.cpp`), so hard stops
359
+ and sub-pixel details (a 1px divider) are exact.
360
+ - `text-shadow` and `box-shadow` (outer/inset, spread, multiple layers),
361
+ blurred with a true separable Gaussian over the shape mask (M21).
355
362
  - `clip-path` polygons (`inset`/`circle`/`ellipse`/`polygon`) resolved to
356
363
  device space, ear-clipped and intersected with shape geometry in
357
364
  `renderer_clip.cpp` — no stencil/shader change.
358
365
  - `filter` colour functions, nine-tap `blur()` and silhouette
359
- `drop-shadow()`; `backdrop-filter` parses and cascades but needs an
360
- offscreen backdrop pass (see [Known issues](./gui/known-issues.md)).
366
+ `drop-shadow()`; `backdrop-filter` snapshots the scene, applies a Gaussian
367
+ and one combined colour matrix, and composites through the border-box mask
368
+ (M21).
361
369
  - E2E coverage in `tests/e2e/gui-effects.test.ts`; see
362
370
  [Rendering](./gui/rendering.md#backgrounds-generated-content-and-effects-m19).
363
371
  20. **M20 — rendering performance** ✅
@@ -372,6 +380,28 @@ engine were Rust.
372
380
  dirty frame, and redundant render-state binds removed.
373
381
  - `win.stats()`/`XT_GUI_PERF` counters and `tests/e2e/gui-perf.test.ts`; see
374
382
  [Performance](./gui/rendering.md#performance-m20).
383
+ 21. **M21 — CSS value/animation completeness** ✅
384
+ - `calc()` is parsed as an unresolved expression tree and resolved by both
385
+ layout (`toPx`) and paint; nested arithmetic, `calc()` inside
386
+ `min`/`max`/`clamp`, and calc in shadows/transforms/filters are covered.
387
+ - `box-shadow` (outer/inset, spread, multiple layers) and blurred
388
+ `text-shadow` are rasterised into a Gaussian mask, cached and composited
389
+ as one tinted image quad.
390
+ - `backdrop-filter` snapshots the painted scene and composites a Gaussian +
391
+ colour matrix through the border-box mask in a runtime-compiled shader.
392
+ - `@keyframes` plus every `animation-*` longhand (delay, iterations,
393
+ direction, fill mode, paused/running) share the property interpolator with
394
+ transitions; transitions now cover opacity, layout lengths, box shadows
395
+ and `cubic-bezier()`/`steps()`/`step-start`/`step-end`.
396
+ - E2E: `tests/e2e/gui-css-features.test.ts`.
397
+ 22. **M22 — CSS coverage for the boxing UI** ✅
398
+ - `letter-spacing`, `text-transform`, the `font` shorthand, length-form
399
+ `vertical-align`, `writing-mode` (text-only vertical layout), percentage
400
+ `border-radius`, `inline-flex`/`inline-grid`, `grid-area`, `aspect-ratio`,
401
+ auto margins, `outline` + `:focus-visible`/`:active`, `currentColor`,
402
+ `skewX()`/`skewY()`, `background-size` tiling for `url()` layers and
403
+ document CSS on SVG children (`fill`/`stroke`/…) are now honoured.
404
+ - E2E: `tests/e2e/gui-css-coverage.test.ts`.
375
405
 
376
406
  ## Progress log
377
407
 
@@ -522,9 +552,27 @@ engine were Rust.
522
552
  `paint_background.cpp` resolves and paints it (radial layers become one
523
553
  `PaintGradient`, which `renderer_gradient.cpp` rasterises into a texture),
524
554
  `paint_effects.cpp` applies filter colour functions plus nine-tap
525
- blur/drop-shadow and emits `text-shadow` runs, and
555
+ blur/drop-shadow, `paint_shadow.cpp` rasterises Gaussian text/box shadows,
556
+ `paint_backdrop.cpp` + `renderer_layer.cpp` composite `backdrop-filter`, and
526
557
  `paint_clip.cpp`/`renderer_clip.cpp` clip geometry against triangulated
527
558
  `clip-path` polygons. E2E: `tests/e2e/gui-effects.test.ts`.
559
+ - **M22** ✅ CSS coverage for the boxing UI. The cascade/declaration parser
560
+ gained `letter-spacing`, `text-transform`, the `font` shorthand,
561
+ `vertical-align`, `writing-mode`, percentage `border-radius`, `outline`,
562
+ `background-size`, `currentColor` and the SVG presentation properties;
563
+ layout gained inline-level flex/grid, `grid-area`, `aspect-ratio`, auto
564
+ margins and text-only vertical writing mode; paint gained outline edges,
565
+ sized/tiled image backgrounds and rotated vertical text; `:focus-visible` is
566
+ an alias of `:focus` and `:active` is tracked from pointer down/up. E2E:
567
+ `tests/e2e/gui-css-coverage.test.ts`.
568
+ - **M21** ✅ CSS value/animation completeness. `calc()` is parsed as an
569
+ unresolved expression tree and resolved by layout/paint; `box-shadow` and
570
+ blurred `text-shadow` use a separable Gaussian mask cached as a texture;
571
+ `backdrop-filter` gets an offscreen snapshot pass; `@keyframes` (plus every
572
+ `animation-*` longhand, delay, iteration, direction and fill mode) shares the
573
+ property interpolator with transitions, which now also cover opacity, layout
574
+ lengths, box shadows and `cubic-bezier()`/`steps()`. E2E:
575
+ `tests/e2e/gui-css-features.test.ts`.
528
576
  - **M20** ✅ rendering performance. `@media` is evaluated once per rule per
529
577
  restyle, generated content is skipped for sheets without `::before`/`::after`,
530
578
  and `:hover`/`:focus` only restyle when the sheet tracks them