@ng-native/fabric 0.6.0 → 0.7.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/engine.d.ts CHANGED
@@ -303,6 +303,10 @@ export interface EngineNode extends HostNode {
303
303
  committedUnder: EngineNode | null;
304
304
  /** The hoisted views this node keeps committed for, by element name. See `HoistOptions`. */
305
305
  kept?: Map<string, KeptHoist>;
306
+ /** On a text field, the row aligned by baseline that takes a baseline from it: `lineBaseline`. */
307
+ onBaseline?: EngineNode;
308
+ /** On such a row, the fields it has marked, which it unmarks when they are no longer its own. */
309
+ baselineFields?: Set<EngineNode>;
306
310
  /** Commit this node again with its children so Fabric measures it again. `remeasureText`. */
307
311
  remeasure?: true;
308
312
  /**
@@ -1254,6 +1258,27 @@ export declare class Engine implements HostEngine {
1254
1258
  * that colour as a number and throws on a string, taking the whole surface down with it.
1255
1259
  */
1256
1260
  private processFilters;
1261
+ /**
1262
+ * Mark the text field each box of a row aligned by baseline takes its baseline from, before
1263
+ * the field is merged, and unmark it when the row is aligned another way.
1264
+ */
1265
+ private noteBaselineFields;
1266
+ private onBaselineOf;
1267
+ /**
1268
+ * Have the text a row aligned by baseline takes a baseline from measured again, where that
1269
+ * text is inside one of the row's boxes and the row is committed again. React Native keeps
1270
+ * what a paragraph measured on the one copy of its node that was measured, and Yoga copies a
1271
+ * node it lays out around: a row that is laid out again asks a box for its baseline, the box
1272
+ * asks the paragraph in it, and a copy that was never measured works it out then, on a node
1273
+ * that is by then not to be changed. A debug build stops there. Committed to be measured
1274
+ * again (`remeasure`), the paragraph is measured in this commit and has the answer.
1275
+ *
1276
+ * ponytail: the first box in the flow at each level, which is the one Yoga asks unless
1277
+ * another is aligned by baseline itself. A row a commit lays out again without the engine
1278
+ * cloning it, as its room changing does, is not reached: clone on a layout event if one is
1279
+ * seen to stop there.
1280
+ */
1281
+ private freshBaselines;
1257
1282
  /**
1258
1283
  * Copy a list of gradients, running every stop's colour through the host's converter.
1259
1284
  *
package/dist/engine.js CHANGED
@@ -10,7 +10,7 @@
10
10
  import { boundDeclaration, faded, fills, inlineInherited, setsInherited, StyleResolver, usesActive, elementsAtEnd, placeReadElsewhere, siblingReach, } from "./css.js";
11
11
  import { applyAria } from "./aria-props.js";
12
12
  import { STYLED_ELEMENTS } from "./element-styles.js";
13
- import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, tracksOf, } from "./transition.js";
13
+ import { animationEvent, bezier, interpolate, readsInheritedColour, sample, step, steppedKeys, tracksOf, } from "./transition.js";
14
14
  import { isFormsInput } from "./forms-inputs.js";
15
15
  import { transformList } from "./inline-transform.js";
16
16
  import { tokenFromValue } from "./inline-token.js";
@@ -389,6 +389,33 @@ const VIRTUAL_TEXT = 'VirtualText';
389
389
  const DEFAULT_VIEW = 'View';
390
390
  /** A top-level text, the one view that aligns its lines. A nested text is a `VirtualText`. */
391
391
  const PARAGRAPH = 'Paragraph';
392
+ /**
393
+ * The box Yoga takes a view's baseline from where the view has none of its own: its first child
394
+ * that is laid out in its flow. Words written straight into the view are the paragraph the
395
+ * engine makes around them, and a child that is not displayed has no view to ask.
396
+ */
397
+ function firstInFlow(node) {
398
+ for (const child of node.children ?? []) {
399
+ // Not the paragraph itself, which holds the words it was made around.
400
+ if (child.kind === 'text' && child.box)
401
+ return child.box === node ? undefined : child.box;
402
+ if (child.kind !== 'element' || ownLayout(child, 'display') === 'none')
403
+ continue;
404
+ if (ownLayout(child, 'position') !== 'absolute')
405
+ return child;
406
+ }
407
+ return undefined;
408
+ }
409
+ const isNode = (node) => node !== undefined;
410
+ /** Whether a row lays a child out among its others, and so can take its baseline from it. */
411
+ const inFlow = (child) => child.kind === 'element' && ownLayout(child, 'position') !== 'absolute';
412
+ /** The text field a row's box takes its baseline from: itself, or the first in its flow down. */
413
+ function fieldOnBaseline(item) {
414
+ let at = item;
415
+ while (at?.kind === 'element' && !TEXT_INPUTS.has(viewNameOf(at)))
416
+ at = firstInFlow(at);
417
+ return at?.kind === 'element' ? at : undefined;
418
+ }
392
419
  const textDirection = (value) => value === 'ltr' || value === 'rtl' ? value : undefined;
393
420
  /**
394
421
  * What iOS scales a text's `lineHeight` by: the system text size, capped by the text's own
@@ -433,6 +460,57 @@ function rowsTall(props, fontScale) {
433
460
  const lines = rows * line * textScale(props, fontScale);
434
461
  props['height'] = edges.reduce((sum, part) => sum + part, lines);
435
462
  }
463
+ /** A field's own minimum height: none where it says `auto`, which a web element starts with. */
464
+ const ownMinimum = (minHeight) => (minHeight === 'auto' ? 0 : (minHeight ?? 0));
465
+ /** How tall the system's font is, as a share of its size: its rise over the baseline and fall under. */
466
+ const FONT_LINE = 0.95 + 0.24;
467
+ /**
468
+ * How tall a field is by its line: the line, padding and border of a border-box field, Yoga's
469
+ * default, and its content alone for a content-box one, which Yoga adds the padding and border
470
+ * to itself. In a row aligned by baseline some of the line is kept as padding: `lineBaseline`.
471
+ */
472
+ function lineBox(props, box, onBaseline, fontScale) {
473
+ const half = onBaseline ? lineBaseline(props, box[0], box, fontScale) : 0;
474
+ if (props['boxSizing'] === 'content-box')
475
+ return box[0] - 2 * half;
476
+ return box.reduce((sum, part) => sum + part, 0);
477
+ }
478
+ /**
479
+ * `lineBaseline` for a field a height sizes: the room is what the height leaves its text, and
480
+ * the field stays that height. A content-box field's height is its content's, so what is now
481
+ * padding comes out of it.
482
+ */
483
+ function heightBaseline(props, height, onBaseline, fontScale) {
484
+ if (!onBaseline)
485
+ return;
486
+ const edges = [...blockEdge(props, 'Top', 'Start'), ...blockEdge(props, 'Bottom', 'End')];
487
+ if (!edges.every((part) => typeof part === 'number'))
488
+ return;
489
+ const content = props['boxSizing'] === 'content-box';
490
+ const room = content ? height : edges.reduce((left, part) => left - part, height);
491
+ const half = lineBaseline(props, room, [room, ...edges], fontScale);
492
+ if (content)
493
+ props['height'] = height - 2 * half;
494
+ }
495
+ /**
496
+ * Give a field with its line height left out the baseline of a paragraph in a line as tall, for
497
+ * a field in a row aligned by baseline: half the room the line has over the font is kept as
498
+ * padding over and under the text. React Native says a field's baseline from its text and its
499
+ * top padding, so without this it is the font's own, and the row lifts the paragraphs beside the
500
+ * field to it: a form field's prefix and suffix sit higher than what is typed. Answers the half.
501
+ *
502
+ * ponytail: the font is taken to be as tall as the system's, `FONT_LINE` of its size. Read from
503
+ * the font if a field in another face is seen a point off its prefix.
504
+ */
505
+ function lineBaseline(props, line, [, top, , bottom], fontScale) {
506
+ const size = props['fontSize'];
507
+ if (typeof size !== 'number')
508
+ return 0;
509
+ const half = Math.max(0, (line - size * textScale(props, fontScale) * FONT_LINE) / 2);
510
+ props['paddingTop'] = top + half;
511
+ props['paddingBottom'] = bottom + half;
512
+ return half;
513
+ }
436
514
  /**
437
515
  * Centre the text of a single-line text field that has a line height, as Chrome centres an
438
516
  * input's, keeping the height the line height gives it.
@@ -453,7 +531,7 @@ function rowsTall(props, fontScale) {
453
531
  * height alone for a content-box one. A null `height` is no height. A value that is not a number
454
532
  * cannot be added up, and leaves the field as it was.
455
533
  */
456
- function centreSingleLine(viewName, props, fontScale) {
534
+ function centreSingleLine(viewName, props, fontScale, onBaseline = false) {
457
535
  const lineHeight = props['lineHeight'];
458
536
  if (!TEXT_INPUTS.has(viewName) || typeof lineHeight !== 'number')
459
537
  return;
@@ -462,6 +540,7 @@ function centreSingleLine(viewName, props, fontScale) {
462
540
  const height = props['height'];
463
541
  if (typeof height === 'number') {
464
542
  delete props['lineHeight'];
543
+ heightBaseline(props, height, onBaseline, fontScale);
465
544
  return;
466
545
  }
467
546
  // A percentage is a fixed height only where the parent's is definite, known at layout alone.
@@ -473,15 +552,12 @@ function centreSingleLine(viewName, props, fontScale) {
473
552
  const edge = (side, logical) => blockEdge(props, side, logical);
474
553
  const line = lineHeight * textScale(props, fontScale);
475
554
  const box = [line, ...edge('Top', 'Start'), ...edge('Bottom', 'End')];
476
- const own = props['minHeight'] ?? 0;
555
+ const own = ownMinimum(props['minHeight']);
477
556
  const max = props['maxHeight'] ?? Infinity;
478
557
  if (![...box, own, max].every((part) => typeof part === 'number'))
479
558
  return;
480
559
  delete props['lineHeight'];
481
- // A content-box field's minHeight is its content's: Yoga adds the padding and border itself.
482
- const content = props['boxSizing'] === 'content-box'
483
- ? line
484
- : box.reduce((sum, part) => sum + part, 0);
560
+ const content = lineBox(props, box, onBaseline, fontScale);
485
561
  props['minHeight'] = Math.max(own, Math.min(content, max));
486
562
  }
487
563
  const NO_STYLE = {};
@@ -544,6 +620,10 @@ const STRETCHES = new Set([undefined, null, 'auto', 'stretch']);
544
620
  * kept, since it stops the stretch already.
545
621
  */
546
622
  function fitContent(node, props) {
623
+ const capped = {
624
+ row: capAtContent(props, 'width', 'maxWidth'),
625
+ column: capAtContent(props, 'height', 'maxHeight'),
626
+ };
547
627
  const width = props['width'] === 'fit-content';
548
628
  const height = props['height'] === 'fit-content';
549
629
  if (!width && !height)
@@ -558,11 +638,46 @@ function fitContent(node, props) {
558
638
  const container = containerOf(parent);
559
639
  parent.fitContainer = container;
560
640
  const [direction, align] = container.split(' ');
641
+ if (capped[direction === 'row' ? 'row' : 'column'])
642
+ withoutGrowth(props);
561
643
  const across = direction === 'row' ? height : width;
562
644
  if (across && align === 'stretch' && STRETCHES.has(props['alignSelf'])) {
563
645
  props['alignSelf'] = 'flex-start';
564
646
  }
565
647
  }
648
+ /**
649
+ * A box capped at its content along its container's main axis, where it would grow: a browser
650
+ * clamps a growing item by its maximum, and there is no such cap to hand Yoga. So it does not
651
+ * grow, and starts from its content, which is as far as the cap would let it get. It may still
652
+ * shrink.
653
+ */
654
+ function withoutGrowth(props) {
655
+ if (typeof props['flex'] === 'number' && props['flex'] > 0) {
656
+ delete props['flex'];
657
+ props['flexShrink'] ??= 1;
658
+ }
659
+ delete props['flexGrow'];
660
+ delete props['flexBasis'];
661
+ }
662
+ /**
663
+ * `max-width: max-content`, and the same of a height: no bigger than its content, which for a
664
+ * box that would fill its container is `fit-content`. Yoga has no such cap. Answers whether the
665
+ * box is the size of its content for it: one that would fill, or one that was `fit-content` as
666
+ * written.
667
+ *
668
+ * ponytail: a size in points is left as it is, since nothing here knows how big the content
669
+ * is. Measure it if a box is ever given both.
670
+ */
671
+ function capAtContent(props, size, cap) {
672
+ if (props[cap] !== 'max-content')
673
+ return false;
674
+ delete props[cap];
675
+ const own = props[size];
676
+ const fills = own == null || own === 'auto' || (typeof own === 'string' && own.endsWith('%'));
677
+ if (fills)
678
+ props[size] = 'fit-content';
679
+ return fills || own === 'fit-content';
680
+ }
566
681
  const isPercent = (value) => typeof value === 'string' && value.endsWith('%');
567
682
  /** Whether a `flex-basis` is a length or a percentage of something: not zero, and not `auto`. */
568
683
  const isSizedBasis = (basis) => isPercent(basis)
@@ -1759,6 +1874,16 @@ const now = () => globalThis.performance?.now?.() ?? Date.now();
1759
1874
  function sameHandles(a, b) {
1760
1875
  return a.length === b.length && a.every((handle, i) => handle === b[i]);
1761
1876
  }
1877
+ /**
1878
+ * Whether a node could be the element a compound is written for, by its classes alone: its own,
1879
+ * and those of one alternative of each `:is()` on it, which is where Tailwind names a group or
1880
+ * a peer. A compound with no class in either could be any element.
1881
+ */
1882
+ function couldBe(compound, node) {
1883
+ if (!compound.classes.every((name) => node.classes?.has(name)))
1884
+ return false;
1885
+ return (compound.is ?? []).every((alternatives) => alternatives.some((option) => couldBe(option, node)));
1886
+ }
1762
1887
  const FRAME_BUDGET_MS = 8;
1763
1888
  export class Engine {
1764
1889
  /** Scopes the CSS resolver's memo. Bumped once per commit. */
@@ -1959,9 +2084,7 @@ export class Engine {
1959
2084
  const { own, elsewhere } = this.activeUse;
1960
2085
  if (!own && !elsewhere.length)
1961
2086
  return;
1962
- // By its classes alone: a compound with none could be any element.
1963
- const asked = elsewhere.some((compound) => compound.classes.every((name) => node.classes?.has(name)));
1964
- if (asked)
2087
+ if (elsewhere.some((compound) => couldBe(compound, node)))
1965
2088
  return this.markProps(node);
1966
2089
  node.stateDirty = true;
1967
2090
  this.markPath(node.parent ?? node);
@@ -3054,7 +3177,11 @@ export class Engine {
3054
3177
  alignMultiline(viewName, style);
3055
3178
  rowsTall(style, this.fontScale);
3056
3179
  const merged = composeTransform(node, this.animated(node, this.transitioned(node, style)));
3057
- centreSingleLine(viewName, merged, this.fontScale);
3180
+ // On iOS: Android's field says the baseline of the line it is sized and centred by.
3181
+ // Still under the row that marked it: a row taken away is not committed again to unmark it.
3182
+ const row = node.onBaseline;
3183
+ const onBaseline = viewName === 'TextInput' && row !== undefined && isWithin(node, row);
3184
+ centreSingleLine(viewName, merged, this.fontScale, onBaseline);
3058
3185
  // After a transition, which eases the basis as the basis it was written as.
3059
3186
  basisAsSize(node, merged);
3060
3187
  // After the basis, which is a size given where it is committed as one.
@@ -3693,12 +3820,7 @@ export class Engine {
3693
3820
  return props;
3694
3821
  const state = (node.transitions ??= new Map());
3695
3822
  const now = this.now();
3696
- // The properties a transition *names*, even where the node has none yet. A property is only
3697
- // transitioned once it has been seen settling, and one that does not exist is never seen - so
3698
- // `transition: transform` with no transform until a class adds one would arrive instantly,
3699
- // which is the commonest way to write one. `all` names nothing, so it keeps the old rule.
3700
- const named = spec ? Object.keys(spec).filter((key) => key !== 'all') : [];
3701
- for (const key of new Set([...Object.keys(props), ...named])) {
3823
+ for (const key of steppedKeys(props, spec, state)) {
3702
3824
  if (step(state, key, props, spec?.[key] ?? spec?.['all'], now)) {
3703
3825
  this.running.add(node);
3704
3826
  this.emitTransition(node, 'topTransitionstart', key);
@@ -3867,6 +3989,58 @@ export class Engine {
3867
3989
  };
3868
3990
  });
3869
3991
  }
3992
+ /**
3993
+ * Mark the text field each box of a row aligned by baseline takes its baseline from, before
3994
+ * the field is merged, and unmark it when the row is aligned another way.
3995
+ */
3996
+ noteBaselineFields(row) {
3997
+ const aligned = ownLayout(row, 'alignItems') === 'baseline';
3998
+ const before = row.baselineFields;
3999
+ if (!aligned && !before)
4000
+ return;
4001
+ const now = new Set(aligned ? row.children.filter(inFlow).map(fieldOnBaseline).filter(isNode) : []);
4002
+ // One that is no longer this row's, and that no other row has marked since.
4003
+ for (const field of before ?? []) {
4004
+ if (!now.has(field) && field.onBaseline === row)
4005
+ this.onBaselineOf(field, undefined);
4006
+ }
4007
+ for (const field of now)
4008
+ if (field.onBaseline !== row)
4009
+ this.onBaselineOf(field, row);
4010
+ row.baselineFields = now.size ? now : undefined;
4011
+ }
4012
+ onBaselineOf(field, row) {
4013
+ field.onBaseline = row;
4014
+ this.markProps(field, false);
4015
+ }
4016
+ /**
4017
+ * Have the text a row aligned by baseline takes a baseline from measured again, where that
4018
+ * text is inside one of the row's boxes and the row is committed again. React Native keeps
4019
+ * what a paragraph measured on the one copy of its node that was measured, and Yoga copies a
4020
+ * node it lays out around: a row that is laid out again asks a box for its baseline, the box
4021
+ * asks the paragraph in it, and a copy that was never measured works it out then, on a node
4022
+ * that is by then not to be changed. A debug build stops there. Committed to be measured
4023
+ * again (`remeasure`), the paragraph is measured in this commit and has the answer.
4024
+ *
4025
+ * ponytail: the first box in the flow at each level, which is the one Yoga asks unless
4026
+ * another is aligned by baseline itself. A row a commit lays out again without the engine
4027
+ * cloning it, as its room changing does, is not reached: clone on a layout event if one is
4028
+ * seen to stop there.
4029
+ */
4030
+ freshBaselines(row) {
4031
+ if (!row.committed || ownLayout(row, 'alignItems') !== 'baseline')
4032
+ return;
4033
+ for (const item of row.children) {
4034
+ let at = firstInFlow(item);
4035
+ while (at?.committed && committedViewName(at) !== PARAGRAPH)
4036
+ at = firstInFlow(at);
4037
+ if (!at?.committed)
4038
+ continue;
4039
+ at.remeasure = true;
4040
+ for (let up = at; up && up !== row; up = up.parent)
4041
+ up.subtreeDirty = true;
4042
+ }
4043
+ }
3870
4044
  /**
3871
4045
  * Copy a list of gradients, running every stop's colour through the host's converter.
3872
4046
  *
@@ -3910,6 +4084,8 @@ export class Engine {
3910
4084
  this.refitChildren(node);
3911
4085
  const viewName = committedViewName(node);
3912
4086
  this.forgetRenamed(node, viewName);
4087
+ this.noteBaselineFields(node);
4088
+ this.freshBaselines(node);
3913
4089
  const childHandles = this.reconcileChildren(node, viewName, style);
3914
4090
  noteTouches(node, style);
3915
4091
  const previous = node.committed;