@geastack/core 0.1.24 → 0.1.26

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/events.cpp CHANGED
@@ -50,6 +50,8 @@ const char *PointerEvent::typeName() const
50
50
  return "keydown";
51
51
  case PointerEventType::Rotary:
52
52
  return "rotary";
53
+ case PointerEventType::Scroll:
54
+ return "scroll";
53
55
  }
54
56
  return "";
55
57
  }
package/include/canvas.h CHANGED
@@ -95,6 +95,11 @@ public:
95
95
  int width() const;
96
96
  int height() const;
97
97
  int strideBytes() const;
98
+ // Read or replace one logical framebuffer pixel in native format. Reads are
99
+ // independent of the current clip; writes obey the clip and mark the logical
100
+ // pixel dirty. Writes replace exactly (they do not apply global alpha).
101
+ pixel::native_t readPixelNative(int x, int y) const;
102
+ void writePixelNativeExact(int x, int y, pixel::native_t value);
98
103
 
99
104
  // Software-scroll register: rotates the logical → physical row mapping
100
105
  // inside a scroll region without moving any pixels. A pixel write at
package/include/events.h CHANGED
@@ -14,7 +14,8 @@ enum class PointerEventType {
14
14
  Click,
15
15
  Input,
16
16
  KeyDown,
17
- Rotary
17
+ Rotary,
18
+ Scroll
18
19
  };
19
20
 
20
21
  enum class EventPhase {
package/index.d.ts CHANGED
@@ -102,10 +102,11 @@ declare global {
102
102
  * namespace actually defines. `warn`/`info`/`debug` would compile and then
103
103
  * fail to link, which is worse than not being declared.
104
104
  */
105
- const console: {
105
+ interface Console {
106
106
  log(message: string): void
107
107
  error(message: string): void
108
108
  }
109
+ const console: Console
109
110
 
110
111
  function requestAnimationFrame(callback: (timestampMs: number) => void): number
111
112
 
@@ -184,6 +185,8 @@ export type StyleLength =
184
185
  | `max(${string})`
185
186
  | `clamp(${string})`
186
187
  export type StyleBox = StyleLength | string
188
+ export type StylePreferredSize = StyleLength | 'auto' | 'min-content' | 'max-content' | 'fit-content'
189
+ export type StyleOverflow = 'visible' | 'hidden' | 'clip' | 'scroll' | 'auto' | 'overlay'
187
190
  export type ClassMap = Record<string, string | number | boolean | null | undefined>
188
191
  export type ClassValue = string | ClassMap
189
192
 
@@ -212,15 +215,27 @@ export interface TouchHost {
212
215
  export declare const touch: TouchHost
213
216
 
214
217
  export interface Style {
218
+ font?: string
215
219
  display?: 'block' | 'flex' | 'grid' | 'none'
216
- flexDirection?: 'row' | 'column'
217
- flexWrap?: 'nowrap' | 'wrap'
220
+ visibility?: 'visible' | 'hidden' | 'collapse'
221
+ flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse'
222
+ flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse'
223
+ flexFlow?: string
224
+ boxSizing?: 'content-box' | 'border-box'
225
+ float?: 'none' | 'left' | 'right'
226
+ clear?: 'none' | 'left' | 'right' | 'both'
227
+ writingMode?: 'horizontal-tb' | 'vertical-lr' | 'vertical-rl' | 'sideways-lr' | 'sideways-rl'
228
+ direction?: 'ltr' | 'rtl'
229
+ rowGap?: StyleLength
230
+ columnGap?: StyleLength
231
+ order?: number
232
+ alignContent?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'space-between' | 'space-around'
218
233
  justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around'
219
234
  alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch'
220
235
  alignSelf?: 'auto' | 'flex-start' | 'center' | 'flex-end' | 'stretch'
221
236
  gap?: StyleLength
222
- width?: StyleLength
223
- height?: StyleLength
237
+ width?: StylePreferredSize
238
+ height?: StylePreferredSize
224
239
  minWidth?: StyleLength
225
240
  minHeight?: StyleLength
226
241
  maxWidth?: StyleLength
@@ -256,9 +271,10 @@ export interface Style {
256
271
  fontFamily?: string
257
272
  fontSize?: StyleLength
258
273
  textAlign?: 'left' | 'center' | 'right'
259
- overflow?: 'visible' | 'hidden' | 'scroll' | 'auto'
260
- overflowX?: 'visible' | 'hidden' | 'scroll' | 'auto'
261
- overflowY?: 'visible' | 'hidden' | 'scroll' | 'auto'
274
+ whiteSpace?: 'normal' | 'nowrap' | 'pre' | 'pre-wrap' | 'pre-line' | 'break-spaces'
275
+ overflow?: StyleOverflow | `${StyleOverflow} ${StyleOverflow}`
276
+ overflowX?: StyleOverflow
277
+ overflowY?: StyleOverflow
262
278
  transform?: string | number
263
279
  rotate?: string | number
264
280
  scale?: string | number
@@ -444,6 +460,7 @@ export interface GeaEventMap {
444
460
  input: InputEvent
445
461
  keydown: KeyEvent
446
462
  rotary: RotaryEvent
463
+ scroll: Event
447
464
  }
448
465
 
449
466
  export type TouchEventHandler = (event: TouchEvent) => void
@@ -1522,6 +1539,7 @@ export interface NativeButtonProps extends NativeEventAttributes {
1522
1539
  }
1523
1540
 
1524
1541
  export interface NativeVirtualListProps extends NativeEventAttributes {
1542
+ onScroll?: (event: Event) => void
1525
1543
  id?: string
1526
1544
  class?: ClassValue
1527
1545
  style?: Style
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geastack/core",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "GeaStack app API, reactivity headers, runtime sources, and type declarations.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -60,10 +60,10 @@
60
60
  "@babel/parser": "^7.23.0",
61
61
  "@babel/traverse": "^7.23.0",
62
62
  "@babel/types": "^7.23.0",
63
- "@geajs/core": "1.4.2",
64
- "@geajs/vite-plugin": "1.4.2",
63
+ "@geajs/core": "1.4.3",
64
+ "@geajs/vite-plugin": "1.4.3",
65
65
  "@geastack/compiler": "^1.0.15",
66
- "@geastack/geatsc-plugin-gea": "^0.1.6",
66
+ "@geastack/geatsc-plugin-gea": "^0.1.7",
67
67
  "opentype.js": "^2.0.0",
68
68
  "vite": "^8.0.0"
69
69
  }
package/runtime.cpp CHANGED
@@ -35,6 +35,13 @@
35
35
  #define GEA_EMBEDDED_WIFI_EARLY_CONNECT 0
36
36
  #endif
37
37
 
38
+ // UI/audio applications can keep touch event-driven rather than spending CPU
39
+ // on back-to-back no-op frames between samples. Touch events still request an
40
+ // immediate frame; animations continue on the configured frame timer.
41
+ #ifndef GEA_EMBEDDED_CONTINUOUS_TOUCH_FRAMES
42
+ #define GEA_EMBEDDED_CONTINUOUS_TOUCH_FRAMES 1
43
+ #endif
44
+
38
45
  #include "css/declarative.h"
39
46
  #include "css/engine.h"
40
47
  #include "ui/style.h"
@@ -190,6 +197,7 @@ void dispatch_event(const events::Event &event, const RuntimeOptions &options)
190
197
  for (;;) {
191
198
  const bool catchUp = services::FrameScheduler::takeCatchUpRequest();
192
199
  const bool activeDrag =
200
+ GEA_EMBEDDED_CONTINUOUS_TOUCH_FRAMES &&
193
201
  events::TouchRuntime::gestureActiveWithin(services::FrameScheduler::nowMs(), 50);
194
202
  // TE single-clock: never run a frame back-to-back here — the panel TE posts the
195
203
  // next frame exactly one VBlank later, which is the whole point. Back-to-back
@@ -1491,6 +1491,7 @@ function parseStaticSimpleSelectorSpec(rawSimple) {
1491
1491
  wantsRoot: false,
1492
1492
  wantsFirstChild: false,
1493
1493
  wantsLastChild: false,
1494
+ wantsHover: false,
1494
1495
  }
1495
1496
  let i = 0
1496
1497
  if (simple[i] !== '.' && simple[i] !== '#' && simple[i] !== ':') {
@@ -1514,13 +1515,14 @@ function parseStaticSimpleSelectorSpec(rawSimple) {
1514
1515
  if (token === 'root') spec.wantsRoot = true
1515
1516
  else if (token === 'first-child') spec.wantsFirstChild = true
1516
1517
  else if (token === 'last-child') spec.wantsLastChild = true
1518
+ else if (token === 'hover') spec.wantsHover = true
1517
1519
  else return null
1518
1520
  } else {
1519
1521
  return null
1520
1522
  }
1521
1523
  }
1522
1524
  if (!spec.tag && !spec.id && spec.classes.length === 0 &&
1523
- !spec.wantsRoot && !spec.wantsFirstChild && !spec.wantsLastChild) {
1525
+ !spec.wantsRoot && !spec.wantsFirstChild && !spec.wantsLastChild && !spec.wantsHover) {
1524
1526
  return null
1525
1527
  }
1526
1528
  return spec
@@ -1555,7 +1557,7 @@ function staticSimpleSelectorSpecCode(simple) {
1555
1557
  const tag = simple.tag ? JSON.stringify(simple.tag) : 'nullptr'
1556
1558
  const id = simple.id ? JSON.stringify(simple.id) : 'nullptr'
1557
1559
  const classes = simple.classes.map((className) => JSON.stringify(className)).join(', ')
1558
- return `{${tag}, ${id}, {${classes}}, ${simple.wantsRoot ? 'true' : 'false'}, ${simple.wantsFirstChild ? 'true' : 'false'}, ${simple.wantsLastChild ? 'true' : 'false'}}`
1560
+ return `{${tag}, ${id}, {${classes}}, ${simple.wantsRoot ? 'true' : 'false'}, ${simple.wantsFirstChild ? 'true' : 'false'}, ${simple.wantsLastChild ? 'true' : 'false'}, ${simple.wantsHover ? 'true' : 'false'}}`
1559
1561
  }
1560
1562
 
1561
1563
  function staticSelectorPartSpecCode(part) {
@@ -1872,8 +1874,8 @@ function parseStaticCssLengthSpec(rawValue) {
1872
1874
 
1873
1875
  function cssAlignKeywordValue(value) {
1874
1876
  const align = new Map([
1875
- ['flex-start', 0],
1876
- ['start', 0],
1877
+ ['flex-start', 6],
1878
+ ['start', 6],
1877
1879
  ['stretch', 0],
1878
1880
  ['normal', 0],
1879
1881
  ['center', 1],
@@ -1939,8 +1941,9 @@ function cssStaticColorTarget(property) {
1939
1941
  case 'color':
1940
1942
  return 'Color'
1941
1943
  case 'background':
1942
- case 'background-color':
1943
1944
  return 'Background'
1945
+ case 'background-color':
1946
+ return 'BackgroundColor'
1944
1947
  case 'active-background':
1945
1948
  case 'active-background-color':
1946
1949
  return 'ActiveBackground'
@@ -1997,9 +2000,6 @@ function parseStaticCssColorRef(rawValue) {
1997
2000
  function cssIgnoredProperty(property) {
1998
2001
  return new Set([
1999
2002
  'color-scheme',
2000
- 'background-position',
2001
- 'box-sizing',
2002
- 'font',
2003
2003
  'grid-column',
2004
2004
  'isolation',
2005
2005
  'letter-spacing',
@@ -2008,7 +2008,6 @@ function cssIgnoredProperty(property) {
2008
2008
  'scroll-snap-type',
2009
2009
  'scrollbar-width',
2010
2010
  'text-shadow',
2011
- 'transform-style',
2012
2011
  'transition',
2013
2012
  'cursor',
2014
2013
  '-webkit-tap-highlight-color',
@@ -2047,6 +2046,10 @@ function cssStaticPropertyValue(property, rawValue) {
2047
2046
  const values = new Map([['block', 0], ['none', 1], ['grid', 2], ['inline-grid', 2], ['flex', 3], ['inline-flex', 3]])
2048
2047
  return values.has(value) ? direct('Display', values.get(value)) : null
2049
2048
  }
2049
+ case 'box-sizing':
2050
+ if (value === 'content-box') return direct('BoxSizing', 0)
2051
+ if (value === 'border-box') return direct('BoxSizing', 1)
2052
+ return null
2050
2053
  case 'flex-direction':
2051
2054
  if (value === 'column') return direct('FlexDirection', 0)
2052
2055
  if (value === 'row') return direct('FlexDirection', 1)
@@ -2134,10 +2137,6 @@ function cssStaticPropertyValue(property, rawValue) {
2134
2137
  if (lower === 'none') return direct('ImageFit', 3)
2135
2138
  if (lower === 'scale-down') return direct('ImageFit', 4)
2136
2139
  return null
2137
- case 'rotate': {
2138
- const angle = parseStaticAngleTenths(value)
2139
- return angle === null ? null : direct('TransformRotate', angle)
2140
- }
2141
2140
  case 'text-align':
2142
2141
  if (value === 'left' || value === 'start') return direct('TextAlign', 0)
2143
2142
  if (value === 'center') return direct('TextAlign', 1)
@@ -2208,11 +2207,8 @@ function cssStaticPropertyValues(property, rawValue) {
2208
2207
  }
2209
2208
  const boxLength = directLengthValue(length)
2210
2209
  if (boxLength !== null) {
2211
- if (property === 'border') return [direct('BorderWidth', boxLength)]
2212
- if (property === 'border-top') return [direct('BorderTopWidth', boxLength)]
2213
- if (property === 'border-right') return [direct('BorderRightWidth', boxLength)]
2214
- if (property === 'border-bottom') return [direct('BorderBottomWidth', boxLength)]
2215
- if (property === 'border-left') return [direct('BorderLeftWidth', boxLength)]
2210
+ const border = {border:'Border','border-top':'BorderTop','border-right':'BorderRight','border-bottom':'BorderBottom','border-left':'BorderLeft'}[property]
2211
+ if (border) return [direct(`${border}Width`, boxLength), direct(`${border}Relief`, 0), direct(`${border}ColorCurrent`, 1)]
2216
2212
  if (property === 'margin') {
2217
2213
  return [
2218
2214
  direct('MarginTop', boxLength),
@@ -2250,14 +2246,37 @@ function cssStaticPropertyValues(property, rawValue) {
2250
2246
  }
2251
2247
  }
2252
2248
 
2253
- if (property === 'scale') {
2254
- const scale = parseStaticScalePermille(value)
2255
- if (scale !== null) {
2256
- return [
2257
- direct('TransformScaleX', scale),
2258
- direct('TransformScaleY', scale),
2259
- ]
2249
+ if (property === 'rotate') {
2250
+ const parts = value.toLowerCase().split(/\s+/)
2251
+ if (parts.length > 1 && (/(deg|rad|turn|grad)$/.test(parts[0]) ||
2252
+ (parts.length === 2 && ['x', 'y', 'z'].includes(parts[1])))) parts.push(parts.shift())
2253
+ const none = value.toLowerCase() === 'none'
2254
+ const angle = none ? 0 : parseStaticAngleTenths(parts.at(-1))
2255
+ if (angle === null || ![1, 2, 4].includes(parts.length)) return []
2256
+ if (!none && !/(deg|rad|turn)$/i.test(parts.at(-1)) && Number(parts.at(-1)) !== 0) return []
2257
+ let axes = [0, 0, 1000000]
2258
+ if (parts.length === 2) {
2259
+ const axis = ['x', 'y', 'z'].indexOf(parts[0])
2260
+ if (axis < 0) return []
2261
+ axes = [0, 0, 0]; axes[axis] = 1000000
2262
+ } else if (parts.length === 4) {
2263
+ const vector = parts.slice(0, 3).map(Number)
2264
+ if (!vector.every(Number.isFinite)) return []
2265
+ const magnitude = Math.max(...vector.map(Math.abs))
2266
+ axes = magnitude ? vector.map(v => roundCssNumber(v / magnitude * 1000000)) : [0, 0, 0]
2260
2267
  }
2268
+ return [direct('RotatePresent', none ? 0 : 1), direct('RotateAngle', axes.some(Boolean) ? angle : 0),
2269
+ direct('RotateAxisX', axes[0]), direct('RotateAxisY', axes[1]), direct('RotateAxisZ', axes[2])]
2270
+ }
2271
+ if (property === 'scale') {
2272
+ const none = value.toLowerCase() === 'none'
2273
+ const parts = none ? ['1'] : value.split(/\s+/)
2274
+ if (parts.length < 1 || parts.length > 3) return []
2275
+ const factors = parts.map(v => v.endsWith('%') ? Number(v.slice(0, -1)) / 100 : Number(v))
2276
+ if (!factors.every(Number.isFinite)) return []
2277
+ const scales = factors.map(v => roundCssNumber(Math.max(-32768, Math.min(32767, v * 1000))))
2278
+ return [direct('ScalePresent', none ? 0 : 1), direct('ScaleX', scales[0]),
2279
+ direct('ScaleY', scales[1] ?? scales[0]), direct('ScaleZ', scales[2] ?? 1000)]
2261
2280
  }
2262
2281
 
2263
2282
  return []
@@ -2311,27 +2330,15 @@ function cssStaticFlexValue(property, rawValue) {
2311
2330
  const setup = []
2312
2331
  const words = splitCssWords(rawValue.trim())
2313
2332
  if (words.length === 0) return null
2333
+ // The compact static API has no shrink field. Preserve declarations it
2334
+ // cannot represent through the full compiler, including keyword semantics.
2335
+ if (words.some(word => /^(none|auto|content|max-content|min-content|fit-content)$/i.test(word))) return null
2314
2336
  for (const token of words) {
2315
- const lower = token.toLowerCase()
2316
- if (lower === 'none') {
2317
- grow = 0
2318
- hasBasis = false
2319
- continue
2320
- }
2321
- if (
2322
- lower === 'auto' ||
2323
- lower === 'content' ||
2324
- lower === 'max-content' ||
2325
- lower === 'min-content' ||
2326
- lower === 'fit-content' ||
2327
- lower.includes('%')
2328
- ) {
2329
- hasBasis = false
2330
- continue
2331
- }
2332
2337
  const number = Number(token)
2333
2338
  if (Number.isFinite(number) && token.match(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/)) {
2339
+ if (number < 0 || numberIndex > 1) return null
2334
2340
  if (numberIndex === 0) grow = roundCssNumber(number)
2341
+ else if (number !== 1) return null
2335
2342
  numberIndex++
2336
2343
  continue
2337
2344
  }
@@ -2399,6 +2406,7 @@ function cssStaticBorderRadiusValue(property, rawValue) {
2399
2406
 
2400
2407
  function cssStaticBorderValue(property, rawValue) {
2401
2408
  if (property !== 'border') return null
2409
+ if (/\b(?:groove|ridge|inset|outset)\b/i.test(rawValue)) return null
2402
2410
  const parts = splitCssWords(rawValue.trim())
2403
2411
  if (parts.length === 0) return null
2404
2412
  const width = parseStaticCssLengthSpec(parts[0])
@@ -2414,6 +2422,8 @@ function cssStaticBorderValue(property, rawValue) {
2414
2422
  }
2415
2423
 
2416
2424
  function cssStaticBorderShorthandValue(property, rawValue) {
2425
+ // The compact width/color API cannot carry a shaded border style.
2426
+ if (/\b(?:groove|ridge|inset|outset)\b/i.test(rawValue)) return null
2417
2427
  const targets = {
2418
2428
  border: { width: 'BorderWidth', color: 'Border', colorProperty: 'border-color', side: false },
2419
2429
  'border-top': { width: 'BorderTopWidth', color: 'BorderTop', colorProperty: 'border-top-color', side: true },
@@ -2441,7 +2451,8 @@ function cssStaticBorderShorthandValue(property, rawValue) {
2441
2451
  }
2442
2452
  }
2443
2453
  if (!targets.side && color && !colorVar) return null
2444
- return { setup: width.setup, widthTarget: targets.width, width: width.spec, color, colorVar }
2454
+ return { setup: width.setup, widthTarget: targets.width, reliefTarget: targets.width.replace('Width', 'Relief'),
2455
+ currentColorTarget: `${targets.color}ColorCurrent`, width: width.spec, color, colorVar }
2445
2456
  }
2446
2457
 
2447
2458
  function staticColorCode(color) {
@@ -2781,6 +2792,9 @@ function cssStaticBackgroundValue(property, rawValue) {
2781
2792
  if (cssHasDynamicValue(rawValue)) return null
2782
2793
  const layers = splitTopLevelCss(rawValue, ',')
2783
2794
  if (layers.length === 0) return null
2795
+ // The gradient registration API has no base-color field. Keep combined
2796
+ // color/image shorthands on the complete native declaration parser.
2797
+ if (layers.some(layer => splitCssWords(layer).some(word => parseStaticCssColor(word) || /^(?:none|border-box|padding-box|content-box)$/i.test(word)))) return null
2784
2798
 
2785
2799
  const out = {
2786
2800
  setup: [],
@@ -2816,6 +2830,7 @@ function cssStaticBackgroundValue(property, rawValue) {
2816
2830
  }
2817
2831
 
2818
2832
  if (!out.gradient) return null
2833
+ if (layers.length !== 1 + Number(out.hasOverlayGradient) + Number(Boolean(out.radialGradient?.enabled)) + Number(Boolean(out.gridX)) + Number(Boolean(out.gridY))) return null
2819
2834
  if (out.gridX && out.gridY && !staticColorEquals(out.gridX.color, out.gridY.color) && out.gridOrder.join('') !== 'xy') return null
2820
2835
  return out
2821
2836
  }
@@ -2837,6 +2852,9 @@ function cssStaticBackgroundFullValue(property, rawValue) {
2837
2852
  return null
2838
2853
  const layers = splitTopLevelCss(rawValue, ',')
2839
2854
  if (layers.length === 0) return null
2855
+ // The gradient registration API has no base-color field. Keep combined
2856
+ // color/image shorthands on the complete native declaration parser.
2857
+ if (layers.some(layer => splitCssWords(layer).some(word => parseStaticCssColor(word) || /^(?:none|border-box|padding-box|content-box)$/i.test(word)))) return null
2840
2858
 
2841
2859
  const out = {
2842
2860
  setup: [],
@@ -2883,6 +2901,7 @@ function cssStaticBackgroundFullValue(property, rawValue) {
2883
2901
  }
2884
2902
 
2885
2903
  if (!out.gradient) return null
2904
+ if (layers.length !== 1 + Number(out.hasOverlayGradient) + Number(Boolean(out.radialGradient?.enabled)) + Number(Boolean(out.gridX)) + Number(Boolean(out.gridY))) return null
2886
2905
  if (out.gridX && out.gridY && !staticColorEquals(out.gridX.color, out.gridY.color) && out.gridOrder.join('') !== 'xy') return null
2887
2906
  return out.needsFullApi ? out : null
2888
2907
  }
@@ -2891,12 +2910,15 @@ function cssStaticBackgroundSizeValue(property, rawValue) {
2891
2910
  if (property !== 'background-size') return null
2892
2911
  if (cssHasDynamicValue(rawValue)) return null
2893
2912
  const layers = splitTopLevelCss(rawValue, ',')
2894
- if (layers.length === 0) return null
2913
+ if (layers.length !== 1) return null
2895
2914
  const parts = splitCssWords(layers[0])
2896
- if (parts.length === 0) return null
2897
- const x = parseStaticCssLengthSpec(parts[0])
2915
+ if (parts.length === 0 || parts.length > 2) return null
2916
+ const parseSize = value => value.toLowerCase() === 'auto'
2917
+ ? { setup: [], spec: { unit: 'Auto', value: '0' } }
2918
+ : parseStaticCssLengthSpec(value)
2919
+ const x = parseSize(parts[0])
2898
2920
  if (!x) return null
2899
- const y = parts.length > 1 ? parseStaticCssLengthSpec(parts[1]) : x
2921
+ const y = parseSize(parts.length > 1 ? parts[1] : 'auto')
2900
2922
  if (!y) return null
2901
2923
  return { setup: [...x.setup, ...(y === x ? [] : y.setup)], stepX: x.spec, stepY: y.spec }
2902
2924
  }
@@ -3192,6 +3214,7 @@ const staticTransformFlags = {
3192
3214
  translateZ: 1 << 5,
3193
3215
  scaleX: 1 << 8,
3194
3216
  scaleY: 1 << 9,
3217
+ scaleZ: 1 << 13,
3195
3218
  }
3196
3219
 
3197
3220
  function parseStaticAngleTenths(rawValue) {
@@ -3212,7 +3235,14 @@ function parseStaticScalePermille(rawValue) {
3212
3235
  return roundCssNumber(value * 1000)
3213
3236
  }
3214
3237
 
3238
+ function multiplyStaticScalePermille(left, right) {
3239
+ return clampInt16(roundCssNumber((left * right) / 1000))
3240
+ }
3241
+
3215
3242
  function parseStaticTransformValue(rawValue) {
3243
+ // Multiple rotation functions need ordered composition. Use the shared C++
3244
+ // CSS compiler rather than independently decomposing a matrix in JS.
3245
+ if ((rawValue.match(/rotate(?:x|y|z|3d)?\s*\(/gi) || []).length > 1) return null
3216
3246
  const value = rawValue.trim()
3217
3247
  const out = {
3218
3248
  setup: [],
@@ -3225,6 +3255,7 @@ function parseStaticTransformValue(rawValue) {
3225
3255
  translateZ: zeroStaticLengthSpec(),
3226
3256
  scaleX: 1000,
3227
3257
  scaleY: 1000,
3258
+ scaleZ: 1000,
3228
3259
  }
3229
3260
  let sawTransform = false
3230
3261
  let i = 0
@@ -3247,6 +3278,9 @@ function parseStaticTransformValue(rawValue) {
3247
3278
  if (depth !== 0) return null
3248
3279
  const arg = value.slice(argStart, i - 1)
3249
3280
  const args = splitTopLevelCss(arg, ',')
3281
+ const axes = name === 'translatex' ? 1 : name === 'translatey' ? 2 : name === 'translatez' ? 4 :
3282
+ (name === 'translate' || name === 'translate3d') ? (1 << Math.min(3, args.length)) - 1 : 0
3283
+ out.flags = (out.flags & ~(axes << 10)) | ((out.flags & 7) ? 0 : (axes << 10))
3250
3284
  sawTransform = true
3251
3285
 
3252
3286
  const setLength = (field, flag, raw) => {
@@ -3288,19 +3322,34 @@ function parseStaticTransformValue(rawValue) {
3288
3322
  const sx = parseStaticScalePermille(args[0] ?? arg)
3289
3323
  const sy = parseStaticScalePermille(args[1] ?? args[0] ?? arg)
3290
3324
  if (sx === null || sy === null) return null
3291
- out.scaleX = sx
3292
- out.scaleY = sy
3325
+ out.scaleX = multiplyStaticScalePermille(out.scaleX, sx)
3326
+ out.scaleY = multiplyStaticScalePermille(out.scaleY, sy)
3293
3327
  out.flags |= staticTransformFlags.scaleX | staticTransformFlags.scaleY
3328
+ } else if (name === 'scale3d') {
3329
+ if (args.length !== 3) return null
3330
+ const sx = parseStaticScalePermille(args[0])
3331
+ const sy = parseStaticScalePermille(args[1])
3332
+ const sz = parseStaticScalePermille(args[2])
3333
+ if (sx === null || sy === null || sz === null) return null
3334
+ out.scaleX = multiplyStaticScalePermille(out.scaleX, sx)
3335
+ out.scaleY = multiplyStaticScalePermille(out.scaleY, sy)
3336
+ out.scaleZ = multiplyStaticScalePermille(out.scaleZ, sz)
3337
+ out.flags |= staticTransformFlags.scaleX | staticTransformFlags.scaleY | staticTransformFlags.scaleZ
3294
3338
  } else if (name === 'scalex') {
3295
3339
  const scale = parseStaticScalePermille(arg)
3296
3340
  if (scale === null) return null
3297
- out.scaleX = scale
3341
+ out.scaleX = multiplyStaticScalePermille(out.scaleX, scale)
3298
3342
  out.flags |= staticTransformFlags.scaleX
3299
3343
  } else if (name === 'scaley') {
3300
3344
  const scale = parseStaticScalePermille(arg)
3301
3345
  if (scale === null) return null
3302
- out.scaleY = scale
3346
+ out.scaleY = multiplyStaticScalePermille(out.scaleY, scale)
3303
3347
  out.flags |= staticTransformFlags.scaleY
3348
+ } else if (name === 'scalez') {
3349
+ const scale = parseStaticScalePermille(arg)
3350
+ if (scale === null) return null
3351
+ out.scaleZ = multiplyStaticScalePermille(out.scaleZ, scale)
3352
+ out.flags |= staticTransformFlags.scaleZ
3304
3353
  } else {
3305
3354
  return null
3306
3355
  }
@@ -3313,17 +3362,29 @@ function cssStaticTransformValue(property, rawValue) {
3313
3362
  return parseStaticTransformValue(rawValue)
3314
3363
  }
3315
3364
 
3316
- function staticTransformArgs(transform) {
3317
- return `${transform.flags}, ${transform.rotateX}, ${transform.rotateY}, ${transform.rotateZ}, ${staticLengthSpecCode(transform.translateX)}, ${staticLengthSpecCode(transform.translateY)}, ${staticLengthSpecCode(transform.translateZ)}, ${transform.scaleX}, ${transform.scaleY}`
3365
+ function staticTransformArgs(transform, mediaArg = null) {
3366
+ const fields = `${transform.flags}, ${transform.rotateX}, ${transform.rotateY}, ${transform.rotateZ}, ${staticLengthSpecCode(transform.translateX)}, ${staticLengthSpecCode(transform.translateY)}, ${staticLengthSpecCode(transform.translateZ)}, ${transform.scaleX}, ${transform.scaleY}`
3367
+ if (mediaArg === null) return `${fields}, ${transform.scaleZ}`
3368
+ return `${fields}${mediaArg || ', nullptr'}, ${transform.scaleZ}`
3318
3369
  }
3319
3370
 
3320
3371
  const staticAnimatableLengthTargets = new Set(['Width', 'Height', 'Top', 'Left'])
3321
- const staticAnimatableColorTargets = new Set(['Color', 'Background'])
3372
+ const staticAnimatableColorTargets = new Set(['Color', 'Background', 'BackgroundColor'])
3322
3373
  const staticAnimatableDirectProperties = new Set([
3323
3374
  'gea::embedded::ui::Property::Opacity',
3375
+ 'gea::embedded::ui::Property::RotatePresent',
3376
+ 'gea::embedded::ui::Property::RotateAngle',
3377
+ 'gea::embedded::ui::Property::RotateAxisX',
3378
+ 'gea::embedded::ui::Property::RotateAxisY',
3379
+ 'gea::embedded::ui::Property::RotateAxisZ',
3380
+ 'gea::embedded::ui::Property::ScalePresent',
3381
+ 'gea::embedded::ui::Property::ScaleX',
3382
+ 'gea::embedded::ui::Property::ScaleY',
3383
+ 'gea::embedded::ui::Property::ScaleZ',
3324
3384
  'gea::embedded::ui::Property::TransformRotate',
3325
3385
  'gea::embedded::ui::Property::TransformScaleX',
3326
3386
  'gea::embedded::ui::Property::TransformScaleY',
3387
+ 'gea::embedded::ui::Property::TransformScaleZ',
3327
3388
  ])
3328
3389
 
3329
3390
  function cssStaticFilterBlurValue(property, rawValue) {
@@ -3437,12 +3498,14 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3437
3498
  `__gea_stylesheet.registerStaticPropertyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${direct.property}, ${direct.value}${mediaArg});`,
3438
3499
  ]
3439
3500
  }
3440
- const entries = directProperties
3441
- .map((direct) => `{${direct.property}, ${direct.value}}`)
3442
- .join(', ')
3443
- return [
3444
- `__gea_stylesheet.registerStaticPropertyGroupRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, {${entries}}${mediaArg});`,
3445
- ]
3501
+ // Each native compiled property group has four slots.
3502
+ const registrations = []
3503
+ for (let i = 0; i < directProperties.length; i += 4) {
3504
+ const entries = directProperties.slice(i, i + 4)
3505
+ .map(direct => `{${direct.property}, ${direct.value}}`).join(', ')
3506
+ registrations.push(`__gea_stylesheet.registerStaticPropertyGroupRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, {${entries}}${mediaArg});`)
3507
+ }
3508
+ return registrations
3446
3509
  }
3447
3510
  const flex = cssStaticFlexValue(name, value)
3448
3511
  if (flex) {
@@ -3478,6 +3541,7 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3478
3541
  const calls = [
3479
3542
  ...borderShorthand.setup,
3480
3543
  `__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${borderShorthand.widthTarget}, ${staticLengthSpecCode(borderShorthand.width)}${mediaArg});`,
3544
+ `__gea_stylesheet.registerStaticPropertyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::Property::${borderShorthand.reliefTarget}, 0${mediaArg});`,
3481
3545
  ]
3482
3546
  if (borderShorthand.colorVar) {
3483
3547
  const colorVar = borderShorthand.colorVar
@@ -3485,6 +3549,8 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3485
3549
  } else if (borderShorthand.color) {
3486
3550
  const color = borderShorthand.color
3487
3551
  calls.push(`__gea_stylesheet.registerStaticColorRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${color.target}, ${color.color.r}, ${color.color.g}, ${color.color.b}, ${color.color.a}${mediaArg});`)
3552
+ } else {
3553
+ calls.push(`__gea_stylesheet.registerStaticPropertyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::Property::${borderShorthand.currentColorTarget}, 1${mediaArg});`)
3488
3554
  }
3489
3555
  return calls
3490
3556
  }
@@ -3519,14 +3585,14 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3519
3585
  if (fullBackground) {
3520
3586
  return [
3521
3587
  ...fullBackground.setup,
3522
- `__gea_stylesheet.registerStaticBackgroundFullRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientRefCode(fullBackground.gradient)}, ${staticLinearGradientRefCode(fullBackground.overlayGradient)}, ${fullBackground.hasOverlayGradient ? 'true' : 'false'}, ${staticRadialGradientRefCode(fullBackground.radialGradient)}, ${staticGridLineCode(fullBackground.gridX)}, ${staticGridLineCode(fullBackground.gridY)}${mediaArg});`,
3588
+ `__gea_stylesheet.registerStaticBackgroundFullRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientRefCode(fullBackground.gradient)}, ${staticLinearGradientRefCode(fullBackground.overlayGradient)}, ${fullBackground.hasOverlayGradient ? 'true' : 'false'}, ${staticRadialGradientRefCode(fullBackground.radialGradient)}, ${staticGridLineCode(fullBackground.gridX)}, ${staticGridLineCode(fullBackground.gridY)}${mediaArg || ", nullptr"}, ${name === "background-image" ? "true" : "false"});`,
3523
3589
  ]
3524
3590
  }
3525
3591
  const background = cssStaticBackgroundValue(name, value)
3526
3592
  if (background) {
3527
3593
  return [
3528
3594
  ...background.setup,
3529
- `__gea_stylesheet.registerStaticBackgroundRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientCode(background.gradient)}, ${staticLinearGradientCode(background.overlayGradient)}, ${background.hasOverlayGradient ? 'true' : 'false'}, ${staticGridLineCode(background.gridX)}, ${staticGridLineCode(background.gridY)}${mediaArg});`,
3595
+ `__gea_stylesheet.registerStaticBackgroundRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientCode(background.gradient)}, ${staticLinearGradientCode(background.overlayGradient)}, ${background.hasOverlayGradient ? 'true' : 'false'}, ${staticGridLineCode(background.gridX)}, ${staticGridLineCode(background.gridY)}${mediaArg || ", nullptr"}, ${name === "background-image" ? "true" : "false"});`,
3530
3596
  ]
3531
3597
  }
3532
3598
  const backgroundSize = cssStaticBackgroundSizeValue(name, value)
@@ -3591,7 +3657,7 @@ function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
3591
3657
  if (transform) {
3592
3658
  return [
3593
3659
  ...transform.setup,
3594
- `__gea_stylesheet.registerStaticTransformRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticTransformArgs(transform)}${mediaArg});`,
3660
+ `__gea_stylesheet.registerStaticTransformRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticTransformArgs(transform, mediaArg)});`,
3595
3661
  ]
3596
3662
  }
3597
3663
  }
@@ -3810,6 +3876,9 @@ function staticCssTapeLengthSpecParts(code) {
3810
3876
  if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null
3811
3877
  const parts = splitTopLevelCppArgs(trimmed.slice(1, -1))
3812
3878
  if (parts.length !== 2) return null
3879
+ // The tape is constexpr: a length expression's value is the id its
3880
+ // registration returned at run time, so that rule stays a plain call.
3881
+ if (!/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?f?$/i.test(parts[1].trim())) return null
3813
3882
  return { unit: parts[0], value: parts[1] }
3814
3883
  }
3815
3884
 
@@ -3989,31 +4058,38 @@ auto __gea_css_tape_selector_kind = [](std::uint8_t kind) {
3989
4058
  auto __gea_css_tape_length = [](std::uint8_t unit, float value) {
3990
4059
  \treturn gea::embedded::ui::StaticStyleLengthSpec{static_cast<gea::embedded::ui::StaticStyleLengthUnit>(unit), value};
3991
4060
  };
3992
- auto __gea_register_static_css_tape = [&](
3993
- \tstd::initializer_list<__GeaStaticCssTapeKind> order,
3994
- \tstd::initializer_list<__GeaStaticCssPropertyOp> propertyOps,
3995
- \tstd::initializer_list<__GeaStaticCssLengthOp> lengthOps,
3996
- \tstd::initializer_list<__GeaStaticCssLengthOp> lengthSpecOps,
3997
- \tstd::initializer_list<__GeaStaticCssColorOp> colorOps,
3998
- \tstd::initializer_list<__GeaStaticCssColorVarOp> colorVarOps,
3999
- \tstd::initializer_list<__GeaStaticCssCustomLengthOp> customLengthOps,
4000
- \tstd::initializer_list<__GeaStaticCssCustomColorOp> customColorOps,
4001
- \tstd::initializer_list<__GeaStaticCssSelectorOnlyOp> boxShadowNoneOps,
4002
- \tstd::initializer_list<__GeaStaticCssNamedOp> fontFamilyOps,
4003
- \tstd::initializer_list<__GeaStaticCssLengthOnlyOp> filterBlurOps,
4004
- \tstd::initializer_list<__GeaStaticCssLineHeightOp> lineHeightOps) {
4005
- \tauto propertyIt = propertyOps.begin();
4006
- \tauto lengthIt = lengthOps.begin();
4007
- \tauto lengthSpecIt = lengthSpecOps.begin();
4008
- \tauto colorIt = colorOps.begin();
4009
- \tauto colorVarIt = colorVarOps.begin();
4010
- \tauto customLengthIt = customLengthOps.begin();
4011
- \tauto customColorIt = customColorOps.begin();
4012
- \tauto boxShadowNoneIt = boxShadowNoneOps.begin();
4013
- \tauto fontFamilyIt = fontFamilyOps.begin();
4014
- \tauto filterBlurIt = filterBlurOps.begin();
4015
- \tauto lineHeightIt = lineHeightOps.begin();
4016
- \tfor (const auto kind : order) {
4061
+ // Each tape's lists are static constexpr arrays the call points into: an
4062
+ // initializer_list argument is a temporary array, which the compiler builds on
4063
+ // the stack by copying it out of read-only data at every call.
4064
+ struct __GeaStaticCssTape {
4065
+ \tstd::size_t orderCount;
4066
+ \tconst __GeaStaticCssTapeKind *order;
4067
+ \tconst __GeaStaticCssPropertyOp *propertyOps;
4068
+ \tconst __GeaStaticCssLengthOp *lengthOps;
4069
+ \tconst __GeaStaticCssLengthOp *lengthSpecOps;
4070
+ \tconst __GeaStaticCssColorOp *colorOps;
4071
+ \tconst __GeaStaticCssColorVarOp *colorVarOps;
4072
+ \tconst __GeaStaticCssCustomLengthOp *customLengthOps;
4073
+ \tconst __GeaStaticCssCustomColorOp *customColorOps;
4074
+ \tconst __GeaStaticCssSelectorOnlyOp *boxShadowNoneOps;
4075
+ \tconst __GeaStaticCssNamedOp *fontFamilyOps;
4076
+ \tconst __GeaStaticCssLengthOnlyOp *filterBlurOps;
4077
+ \tconst __GeaStaticCssLineHeightOp *lineHeightOps;
4078
+ };
4079
+ auto __gea_register_static_css_tape = [&](const __GeaStaticCssTape &tape) {
4080
+ \tconst auto *propertyIt = tape.propertyOps;
4081
+ \tconst auto *lengthIt = tape.lengthOps;
4082
+ \tconst auto *lengthSpecIt = tape.lengthSpecOps;
4083
+ \tconst auto *colorIt = tape.colorOps;
4084
+ \tconst auto *colorVarIt = tape.colorVarOps;
4085
+ \tconst auto *customLengthIt = tape.customLengthOps;
4086
+ \tconst auto *customColorIt = tape.customColorOps;
4087
+ \tconst auto *boxShadowNoneIt = tape.boxShadowNoneOps;
4088
+ \tconst auto *fontFamilyIt = tape.fontFamilyOps;
4089
+ \tconst auto *filterBlurIt = tape.filterBlurOps;
4090
+ \tconst auto *lineHeightIt = tape.lineHeightOps;
4091
+ \tfor (std::size_t step = 0; step < tape.orderCount; ++step) {
4092
+ \t\tconst auto kind = tape.order[step];
4017
4093
  \t\tswitch (kind) {
4018
4094
  \t\tcase __GeaStaticCssTapeKind::Property: {
4019
4095
  \t\t\tconst auto &op = *propertyIt++;
@@ -4114,12 +4190,27 @@ function compactStaticCssTapeCalls(calls) {
4114
4190
  }
4115
4191
  usedTape = true
4116
4192
  const order = chunk.map((entry) => `__GeaStaticCssTapeKind::${entry.kind}`).join(', ')
4117
- const listFor = (kind) => {
4118
- const entries = chunk.filter((entry) => entry.kind === kind).map((entry) => entry.code)
4119
- if (entries.length === 0) return '{}'
4120
- return `{\n${entries.map((entry) => `\t\t${entry}`).join(',\n')}\n\t}`
4193
+ const opTypes = {
4194
+ Property: '__GeaStaticCssPropertyOp',
4195
+ Length: '__GeaStaticCssLengthOp',
4196
+ LengthSpec: '__GeaStaticCssLengthOp',
4197
+ Color: '__GeaStaticCssColorOp',
4198
+ ColorVar: '__GeaStaticCssColorVarOp',
4199
+ CustomLength: '__GeaStaticCssCustomLengthOp',
4200
+ CustomColor: '__GeaStaticCssCustomColorOp',
4201
+ BoxShadowNone: '__GeaStaticCssSelectorOnlyOp',
4202
+ FontFamily: '__GeaStaticCssNamedOp',
4203
+ FilterBlur: '__GeaStaticCssLengthOnlyOp',
4204
+ LineHeight: '__GeaStaticCssLineHeightOp',
4121
4205
  }
4122
- out.push(`__gea_register_static_css_tape(\n\t{${order}},\n\t${kinds.map(listFor).join(',\n\t')});`)
4206
+ const arrays = [`\tstatic constexpr __GeaStaticCssTapeKind order[] = {${order}};`]
4207
+ const pointers = kinds.map((kind) => {
4208
+ const entries = chunk.filter((entry) => entry.kind === kind).map((entry) => entry.code)
4209
+ if (entries.length === 0) return 'nullptr'
4210
+ arrays.push(`\tstatic constexpr ${opTypes[kind]} ops${kind}[] = {\n${entries.map((entry) => `\t\t${entry}`).join(',\n')}\n\t};`)
4211
+ return `ops${kind}`
4212
+ })
4213
+ out.push(`{\n${arrays.join('\n')}\n\t__gea_register_static_css_tape({${chunk.length}, order, ${pointers.join(', ')}});\n}`)
4123
4214
  chunk = []
4124
4215
  }
4125
4216
  for (const call of calls) {
@@ -4135,6 +4226,7 @@ function compactStaticCssTapeCalls(calls) {
4135
4226
  return { calls: out, usedTape, stringLiterals }
4136
4227
  }
4137
4228
 
4229
+ let staticCssRegistrationCalls = []
4138
4230
  function cssRegistrationCode(viteOutDir) {
4139
4231
  const calls = []
4140
4232
  staticCssLengthExpressionCounter = 0
@@ -4153,6 +4245,10 @@ function cssRegistrationCode(viteOutDir) {
4153
4245
  emitCssRules(stripAtBlocks(block.body), block.condition, calls)
4154
4246
  }
4155
4247
  }
4248
+ // The calls themselves, before the tape packs them: a target that resolves
4249
+ // styles at build time (the Pebble compiled UI) reads these rather than
4250
+ // re-deriving the cascade from the CSS text.
4251
+ staticCssRegistrationCalls = calls
4156
4252
  if (calls.length === 0) return ''
4157
4253
  const compacted = compactStaticCssTapeCalls(calls)
4158
4254
  const tapePrelude = compacted.usedTape ? staticCssTapePrelude(compacted.stringLiterals) : ''
@@ -4359,6 +4455,7 @@ const viteBundle = findFirstJsFile(viteOutDir)
4359
4455
  if (!viteBundle) fail(`Vite produced no JS bundle in ${viteOutDir}`)
4360
4456
  const cssPrelude = geaEmbeddedCompat ? cssRegistrationCode(viteOutDir) : ''
4361
4457
  const cssPreludePath = path.join(outDir, 'gea-style-registration.cppfrag')
4458
+ fs.writeFileSync(path.join(outDir, 'gea-style-registration.calls.json'), JSON.stringify(staticCssRegistrationCalls, null, 1) + '\n')
4362
4459
  if (cssPrelude || cppPreludeSymbol) {
4363
4460
  fs.writeFileSync(cssPreludePath, cssPrelude)
4364
4461
  } else {