@ng-native/fabric 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/css.js CHANGED
@@ -89,6 +89,80 @@ function drawNoBorder(own) {
89
89
  * The cost is one integer increment per comparison, against a Set lookup and several property
90
90
  * reads, so it does not distort what it measures.
91
91
  */
92
+ /**
93
+ * What a sheet asks about a pressed element. `own` is whether a rule styles one, `.button:active`,
94
+ * which matching the pressed elements again answers. `elsewhere` is each compound that asks about
95
+ * a pressed element from another one: `.card:active .title`, `.a:active + .b`, or `:active`
96
+ * inside `:not()`, `:is()` or `:has()`. An element such a compound could be is restyled with
97
+ * everything under it when it is pressed, as what it changes is not itself alone.
98
+ */
99
+ export function usesActive(sheet) {
100
+ const known = ACTIVE_USE.get(sheet);
101
+ if (known)
102
+ return known;
103
+ let own = false;
104
+ const elsewhere = [];
105
+ for (const rule of sheet.rules) {
106
+ rule.compounds.forEach((compound, at) => {
107
+ const subject = at === rule.compounds.length - 1;
108
+ // Asked of the element itself, on the compound or in its `:is()` and `:not()`: the
109
+ // subject's is its own, and any other compound's is of the element that compound is.
110
+ if (activeItself(compound, elsewhere)) {
111
+ if (subject)
112
+ own = true;
113
+ else
114
+ elsewhere.push(compound);
115
+ }
116
+ activeAround(compound, elsewhere);
117
+ });
118
+ }
119
+ const use = { own, elsewhere };
120
+ ACTIVE_USE.set(sheet, use);
121
+ return use;
122
+ }
123
+ const isActive = (compound) => compound.pseudo?.includes('active') === true;
124
+ /**
125
+ * Whether a compound asks about the pressed state of the element it matches: on itself, in a
126
+ * `:not()`, or as the last compound of an `:is()` argument. The compounds before the last in
127
+ * such an argument are other elements, and are collected as they are.
128
+ */
129
+ function activeItself(compound, elsewhere) {
130
+ let itself = isActive(compound);
131
+ for (const inner of compound.not ?? []) {
132
+ if (activeItself(inner, elsewhere))
133
+ itself = true;
134
+ activeAround(inner, elsewhere);
135
+ }
136
+ for (const chain of compound.is ?? []) {
137
+ chain.forEach((inner, at) => {
138
+ const asked = activeItself(inner, elsewhere);
139
+ if (asked && at === chain.length - 1)
140
+ itself = true;
141
+ else if (asked)
142
+ elsewhere.push(inner);
143
+ activeAround(inner, elsewhere);
144
+ });
145
+ }
146
+ return itself;
147
+ }
148
+ /** Each compound that asks about a pressed element around the one a compound matches. */
149
+ function activeAround(compound, elsewhere) {
150
+ const collect = (value) => {
151
+ if (!value || typeof value !== 'object')
152
+ return;
153
+ if (Array.isArray(value))
154
+ return value.forEach(collect);
155
+ if (isActive(value))
156
+ elsewhere.push(value);
157
+ for (const inside of Object.values(value))
158
+ collect(inside);
159
+ };
160
+ for (const [key, inside] of Object.entries(compound)) {
161
+ if (key !== 'is' && key !== 'not' && key !== 'pseudo')
162
+ collect(inside);
163
+ }
164
+ }
165
+ const ACTIVE_USE = new WeakMap();
92
166
  export const styleStats = {
93
167
  /** Calls to `matchesCompound`, the innermost unit of matching work. */
94
168
  compoundTests: 0,
@@ -190,6 +264,21 @@ function matchesCompound(node, compound, sheet) {
190
264
  return false;
191
265
  }
192
266
  }
267
+ if (compound.has) {
268
+ for (const test of compound.has) {
269
+ if (!hasBeneath(node, test, sheet))
270
+ return false;
271
+ }
272
+ }
273
+ if (compound.parents) {
274
+ const parent = node.parent;
275
+ if (!parent)
276
+ return false;
277
+ for (const above of compound.parents) {
278
+ if (!matchesCompound(parent, above, sheet))
279
+ return false;
280
+ }
281
+ }
193
282
  if (compound.ancestors) {
194
283
  for (const ancestor of compound.ancestors) {
195
284
  let found = false;
@@ -212,6 +301,18 @@ function matchesCompound(node, compound, sheet) {
212
301
  }
213
302
  return true;
214
303
  }
304
+ /** Whether some element beneath `node`, or some child of it for `:has(> x)`, matches the test. */
305
+ function hasBeneath(node, test, sheet) {
306
+ for (const child of node.children ?? []) {
307
+ if (child.kind !== undefined && child.kind !== 'element')
308
+ continue;
309
+ if (test.any.some((option) => matchesCompound(child, option, sheet)))
310
+ return true;
311
+ if (!test.child && hasBeneath(child, test, sheet))
312
+ return true;
313
+ }
314
+ return false;
315
+ }
215
316
  /**
216
317
  * Right-to-left matching, as every CSS engine does it: the rightmost compound is the cheapest
217
318
  * filter and rejects almost everything immediately.
@@ -341,6 +442,133 @@ function overriddenBy(deferred, declarations, important) {
341
442
  }
342
443
  return kept.length ? kept : null;
343
444
  }
445
+ /**
446
+ * The logical property and the physical one for the same corner or edge, left to right. CSS
447
+ * settles the two as one property, the later declaration winning. A native view has a prop for
448
+ * each and takes one whatever their order: React Native the physical corner, Yoga the logical
449
+ * edge. So a declaration of either takes the other out of the cascade.
450
+ */
451
+ const SIDES_LTR = [
452
+ ['borderStartStartRadius', 'borderTopLeftRadius'],
453
+ ['borderStartEndRadius', 'borderTopRightRadius'],
454
+ ['borderEndStartRadius', 'borderBottomLeftRadius'],
455
+ ['borderEndEndRadius', 'borderBottomRightRadius'],
456
+ ...['margin', 'padding'].flatMap((box) => [
457
+ [`${box}Start`, `${box}Left`],
458
+ [`${box}End`, `${box}Right`],
459
+ ]),
460
+ ...['Width', 'Color'].flatMap((part) => [
461
+ [`borderStart${part}`, `borderLeft${part}`],
462
+ [`borderEnd${part}`, `borderRight${part}`],
463
+ ]),
464
+ ['start', 'left'],
465
+ ['end', 'right'],
466
+ ];
467
+ const mirrored = (physical) => physical.includes('Left') || physical === 'left'
468
+ ? physical.replace('Left', 'Right').replace('left', 'right')
469
+ : physical.replace('Right', 'Left').replace('right', 'left');
470
+ /** Each property's other form, both ways, in a layout of each direction. */
471
+ const twins = (pairs) => Object.fromEntries(pairs.flatMap(([logical, physical]) => [
472
+ [logical, physical],
473
+ [physical, logical],
474
+ ]));
475
+ const TWIN = {
476
+ ltr: twins(SIDES_LTR),
477
+ rtl: twins(SIDES_LTR.map(([logical, physical]) => [logical, mirrored(physical)])),
478
+ };
479
+ const sidedOf = new WeakMap();
480
+ function sided(rule) {
481
+ let found = sidedOf.get(rule);
482
+ if (found !== undefined)
483
+ return found;
484
+ const twin = TWIN.ltr;
485
+ const plain = Object.keys(rule.declarations).filter((key) => key in twin);
486
+ const important = rule.important ? Object.keys(rule.important).filter((key) => key in twin) : [];
487
+ for (const declaration of rule.deferred ?? []) {
488
+ for (const prop of declaration.props) {
489
+ if (prop in twin)
490
+ (declaration.important ? important : plain).push(prop);
491
+ }
492
+ }
493
+ found = plain.length || important.length ? { plain, important } : null;
494
+ sidedOf.set(rule, found);
495
+ return found;
496
+ }
497
+ /** Of `keys`, in the order a rule wrote them, each whose other form comes after it. */
498
+ function writtenEarlier(keys, twin) {
499
+ return keys.filter((key, at) => keys.indexOf(twin[key]) > at);
500
+ }
501
+ /** The other form of each of `keys`, as the record `overriddenBy` reads. */
502
+ function twinsOf(keys, twin) {
503
+ const out = {};
504
+ for (const key of keys)
505
+ out[twin[key]] = true;
506
+ return out;
507
+ }
508
+ /**
509
+ * One cascade's settling of the corners and edges set in both forms: each rule's are taken out of
510
+ * what the rules before it left, in the form the rule does not use.
511
+ */
512
+ class OtherForms {
513
+ /** Worked out for the first rule that sets a corner or an edge, which most elements have none of. */
514
+ twin = null;
515
+ overImportant = null;
516
+ direction;
517
+ constructor(direction) {
518
+ this.direction = direction;
519
+ }
520
+ /** Before `rule` is applied: the other form of what it sets goes, as the same property would. */
521
+ before(rule, normal, important, deferred) {
522
+ const sides = sided(rule);
523
+ if (!sides)
524
+ return deferred;
525
+ const twin = (this.twin ??= TWIN[this.direction()]);
526
+ const plain = twinsOf(sides.plain, twin);
527
+ for (const key in plain)
528
+ delete normal[key];
529
+ let kept = deferred && overriddenBy(deferred, plain, false);
530
+ if (!sides.important.length)
531
+ return kept;
532
+ const gone = twinsOf(sides.important, twin);
533
+ for (const key in gone)
534
+ delete important[key];
535
+ kept &&= overriddenBy(kept, gone, true);
536
+ Object.assign((this.overImportant ??= {}), gone);
537
+ return kept;
538
+ }
539
+ /**
540
+ * Once `rule` is applied, its deferred values among `deferred`: where it wrote both forms, the
541
+ * one it wrote later stands, among its plain declarations and among its important ones. The
542
+ * order is the rule's own, sent with it, since a value settled on device is in another list.
543
+ */
544
+ within(rule, normal, important, deferred) {
545
+ if (!rule.sides && !rule.importantSides)
546
+ return deferred;
547
+ const twin = (this.twin ??= TWIN[this.direction()]);
548
+ let kept = deferred;
549
+ const settle = (order, into, tier) => {
550
+ const lost = {};
551
+ for (const key of writtenEarlier(order, twin)) {
552
+ delete into[key];
553
+ lost[key] = true;
554
+ }
555
+ kept &&= overriddenBy(kept, lost, tier);
556
+ };
557
+ if (rule.sides)
558
+ settle(rule.sides, normal, false);
559
+ if (rule.importantSides)
560
+ settle(rule.importantSides, important, true);
561
+ return kept;
562
+ }
563
+ /** An important one stands over the other form wherever that was set, as over its own. */
564
+ after(normal, deferred) {
565
+ if (!this.overImportant)
566
+ return deferred;
567
+ for (const key in this.overImportant)
568
+ delete normal[key];
569
+ return deferred && overriddenBy(deferred, this.overImportant, false);
570
+ }
571
+ }
344
572
  /**
345
573
  * The deferred values still standing once `rule` has been applied, plus its own.
346
574
  *
@@ -801,6 +1029,9 @@ export class StyleResolver {
801
1029
  cached.epoch = epoch;
802
1030
  return cached;
803
1031
  }
1032
+ const kept = this.kept(node, parentContext, epoch);
1033
+ if (kept)
1034
+ return kept;
804
1035
  const parentInherited = parent ? parent.inherited : EMPTY;
805
1036
  // A box the engine made around loose text is in no template, so no rule matches it.
806
1037
  if (node.anonymous || this.hasNoRules(node)) {
@@ -817,11 +1048,70 @@ export class StyleResolver {
817
1048
  context: {},
818
1049
  parentContext,
819
1050
  ...styled,
1051
+ ...this.record(matched),
820
1052
  };
821
1053
  node.styleCache = cache;
822
1054
  node.styleDirty = false;
1055
+ node.hasDirty = false;
1056
+ node.stateDirty = false;
823
1057
  return cache;
824
1058
  }
1059
+ /** The rules a node matched, for its cache to keep, while a sheet uses `:has()`. */
1060
+ record(matched) {
1061
+ return this.tracksHas ? { matched } : {};
1062
+ }
1063
+ /**
1064
+ * Set by the engine once a sheet uses `:has()`. Until then no cache keeps the rules it matched,
1065
+ * so an app that never asks pays nothing for it.
1066
+ */
1067
+ tracksHas = false;
1068
+ /**
1069
+ * The cache of a node whose subtree changed, and nothing else, when it matches the rules it
1070
+ * did: keeping the object is what leaves everything under it alone. Nothing when it has to be
1071
+ * resolved, as it does with no record of what it matched, made before any sheet asked.
1072
+ */
1073
+ keptBeneath(node, parentContext, epoch) {
1074
+ const cached = node.styleCache;
1075
+ const stands = (node.hasDirty === true || node.stateDirty === true) &&
1076
+ cached?.matched !== undefined &&
1077
+ !node.styleDirty &&
1078
+ cached.parentContext === parentContext &&
1079
+ cached.generation === this.generation;
1080
+ if (!stands)
1081
+ return null;
1082
+ const candidates = this.rulesFor(node);
1083
+ const now = this.matched(node, node.styled ? ELEMENT_ENTRIES.concat(candidates) : candidates);
1084
+ const before = cached.matched;
1085
+ if (now.length !== before.length || now.some((rule, i) => rule !== before[i]))
1086
+ return null;
1087
+ node.hasDirty = false;
1088
+ node.stateDirty = false;
1089
+ cached.epoch = epoch;
1090
+ return cached;
1091
+ }
1092
+ /** A cache that still stands for a node marked for its subtree or its state alone. */
1093
+ kept(node, parentContext, epoch) {
1094
+ return (this.keptBeneath(node, parentContext, epoch) ?? this.keptUnmatched(node, parentContext, epoch));
1095
+ }
1096
+ /**
1097
+ * The cache of a node whose pressed state changed and nothing else, where no rule can match
1098
+ * it at all: it has no style of its own to change, so it and all under it stay as they are.
1099
+ */
1100
+ keptUnmatched(node, parentContext, epoch) {
1101
+ const cached = node.styleCache;
1102
+ const stands = node.stateDirty === true &&
1103
+ cached !== null &&
1104
+ !node.styleDirty &&
1105
+ !node.hasDirty &&
1106
+ cached.parentContext === parentContext &&
1107
+ cached.generation === this.generation &&
1108
+ (node.anonymous === true || this.hasNoRules(node));
1109
+ if (!stands)
1110
+ return null;
1111
+ node.stateDirty = false;
1112
+ cached.epoch = epoch;
1113
+ return cached;
1114
+ }
825
1115
  /**
826
1116
  * Whether a node's last answer still stands under a parent that resolved to `parentContext`,
827
1117
  * without resolving it. What lets a commit step over a clean child in one comparison.
@@ -836,6 +1126,8 @@ export class StyleResolver {
836
1126
  reusable(cached, node, parentContext) {
837
1127
  return (cached !== null &&
838
1128
  !node.styleDirty &&
1129
+ !node.hasDirty &&
1130
+ !node.stateDirty &&
839
1131
  cached.parentContext === parentContext &&
840
1132
  cached.generation === this.generation);
841
1133
  }
@@ -1022,7 +1314,7 @@ export class StyleResolver {
1022
1314
  ...matched,
1023
1315
  { declarations: inline ?? EMPTY, ...(bound && { deferred: bound }) },
1024
1316
  ]
1025
- : matched);
1317
+ : matched, parentInherited);
1026
1318
  // Tokens are in scope for this node's own declarations as well as its descendants', so they
1027
1319
  // are merged before any `var()` here is resolved.
1028
1320
  const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
@@ -1048,14 +1340,16 @@ export class StyleResolver {
1048
1340
  return out;
1049
1341
  }
1050
1342
  /** The declarations of the rules a node matched. The list is sorted, so later simply wins. */
1051
- cascade(matched) {
1343
+ cascade(matched, parentInherited) {
1052
1344
  const normal = {};
1053
1345
  const important = {};
1054
1346
  let hasImportant = false;
1055
1347
  let tokens = null;
1056
1348
  let importantTokens = null;
1057
1349
  let deferred = null;
1350
+ const forms = new OtherForms(() => this.directionOf(matched, parentInherited));
1058
1351
  for (const rule of this.layered ? byImportance(matched, this.layerPlaces) : matched) {
1352
+ deferred = forms.before(rule, normal, important, deferred);
1059
1353
  Object.assign(normal, rule.declarations);
1060
1354
  if (rule.important) {
1061
1355
  Object.assign(important, rule.important);
@@ -1067,10 +1361,11 @@ export class StyleResolver {
1067
1361
  if (rule.importantTokens) {
1068
1362
  Object.assign((importantTokens ??= {}), pick(rule.tokens, rule.importantTokens));
1069
1363
  }
1070
- deferred = carryDeferred(deferred, rule);
1364
+ deferred = forms.within(rule, normal, important, carryDeferred(deferred, rule));
1071
1365
  }
1072
1366
  if (importantTokens)
1073
1367
  Object.assign(tokens, importantTokens);
1368
+ deferred = forms.after(normal, deferred);
1074
1369
  return {
1075
1370
  declarations: hasImportant ? { ...normal, ...important } : normal,
1076
1371
  important: hasImportant ? important : null,
@@ -1078,6 +1373,37 @@ export class StyleResolver {
1078
1373
  deferred,
1079
1374
  };
1080
1375
  }
1376
+ /**
1377
+ * An inline style over the style the cascade settled, both already in `style`: a corner or an
1378
+ * edge the inline style sets takes the other form of it out, as a later rule would.
1379
+ */
1380
+ overOtherForms(inline, style) {
1381
+ let twin = null;
1382
+ const written = Object.keys(inline);
1383
+ for (const [at, key] of written.entries()) {
1384
+ if (!(key in TWIN.ltr))
1385
+ continue;
1386
+ twin ??= TWIN[(style['direction'] ?? this.conditions.direction) === 'rtl' ? 'rtl' : 'ltr'];
1387
+ const other = twin[key];
1388
+ // Where the inline style sets both, the one it set later stands.
1389
+ if (written.indexOf(other) < at)
1390
+ delete style[other];
1391
+ }
1392
+ }
1393
+ /**
1394
+ * The direction an element is laid out in: the last `direction` its own rules give it, the one
1395
+ * it inherits, or the app's. A logical corner is a different physical one in each.
1396
+ */
1397
+ directionOf(matched, parentInherited) {
1398
+ let plain;
1399
+ let important;
1400
+ for (const rule of matched) {
1401
+ plain = rule.declarations['direction'] ?? plain;
1402
+ important = rule.important?.['direction'] ?? important;
1403
+ }
1404
+ const direction = important ?? plain ?? parentInherited['direction'] ?? this.conditions.direction;
1405
+ return direction === 'rtl' ? 'rtl' : 'ltr';
1406
+ }
1081
1407
  /**
1082
1408
  * Whether a rule's media query holds, memoised per rule.
1083
1409
  *
@@ -1115,7 +1441,9 @@ export class StyleResolver {
1115
1441
  }
1116
1442
  /** What the device answers for a feature that is a keyword rather than a measurement. */
1117
1443
  preference(feature) {
1118
- const { width, height, colorScheme, reducedMotion } = this.conditions;
1444
+ const { width, height, colorScheme, reducedMotion, direction } = this.conditions;
1445
+ if (feature === 'direction')
1446
+ return direction ?? 'ltr';
1119
1447
  if (feature === 'orientation')
1120
1448
  return width > height ? 'landscape' : 'portrait';
1121
1449
  if (feature === 'prefers-color-scheme')