Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 13 additions & 11 deletions packages/dragon/src/emit/native-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1648,7 +1648,7 @@ export const STYLE_FIELDS = [
'display', 'position', 'top', 'right', 'bottom', 'left', 'overflowX', 'overflowY', 'direction', 'boxSizing', 'width', 'height', 'minWidth', 'minHeight',
'maxWidth', 'maxHeight', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'flexDirection', 'flexWrap', 'flexGrow', 'flexShrink', 'flexBasis', 'order',
'justifyContent', 'alignItems', 'alignSelf', 'alignContent', 'rowGap', 'columnGap', 'textAlign', 'aspectRatio',
'justifyContent', 'alignItems', 'alignSelf', 'alignContent', 'rowGap', 'columnGap', 'textAlign', 'aspectRatio', 'verticalAlign',
] as const;

const VALUE_CLASSES: Readonly<Record<string, string>> = { px: 'Px', percent: 'Percent', auto: 'Auto', none: 'NoneValue', content: 'ContentValue', normal: 'NormalValue', number: 'NumberValue', 'device-px': 'DevicePx' };
Expand Down Expand Up @@ -1714,10 +1714,11 @@ function engineValue(lang: Lang, v: unknown): string {
if (typeof r.width !== 'number' || typeof r.height !== 'number') throw new Error(`aspect ratio ${JSON.stringify(v)} lacks its parts`);
return `${ratio}(${str(r.kind)}, ${doubleLit(r.width)}, ${doubleLit(r.height)})`;
}
const o = v as { kind: string; value?: number };
const o = v as { kind: string; value?: number | string };
if (o.kind === 'keyword') return `VerticalAlignKeywordValue(${str('keyword')}, ${str(o.value as string)})`;
const cls = VALUE_CLASSES[o.kind];
if (cls === undefined) throw new Error(`no engine class for value kind ${o.kind}`);
return o.value === undefined ? `${cls}(${str(o.kind)})` : `${cls}(${str(o.kind)}, ${doubleLit(o.value)})`;
return o.value === undefined ? `${cls}(${str(o.kind)})` : `${cls}(${str(o.kind)}, ${doubleLit(o.value as number)})`;
}

/** A line height as a constructor call: a calculated one is the engine's LineHeightCalc, whose range is non-negative. */
Expand Down Expand Up @@ -1757,16 +1758,17 @@ export function inputFunctions(lang: Lang, root: import('@dragon/layout').Layout
const natural = c.natural.kind === 'image' ? `NaturalSizeValue_image(${str('image')}, ${doubleLit(c.natural.width)}, ${doubleLit(c.natural.height)})` : `NaturalSizeValue_none(${str('none')})`;
return `ReplacedLeaf(${str('replaced')}, ${str(c.id)}, ${styleOf(c.style)}, ${natural}, ${doubleLit(c.defaultWidth)}, ${doubleLit(c.defaultHeight)}, ${str(c.objectFit)}, ${engineValue(lang, c.objectPositionX)}, ${engineValue(lang, c.objectPositionY)})`;
};
const list = (union: string, items: readonly string[]): string => (lang === 'swift' ? `JsArray<any ${union}>([${items.join(', ')}])` : `jsArrayOf<${union}>(${items.join(', ')})`);
const inline = (c: import('@dragon/layout').InlineChild): string => {
if (c.kind === 'text') return `TextLeaf(${str('text')}, ${str(c.id)}, ${str(c.text)}, ${fontSpecValue(lang, c.font)}, ${lineHeightValue(lang, c.lineHeight)}, ${str(c.whiteSpaceCollapse)}, ${str(c.textWrapMode)})`;
if (c.kind === 'br') return `LineBreak(${str('br')}, ${str(c.id)}, ${fontSpecValue(lang, c.font)}, ${lineHeightValue(lang, c.lineHeight)})`;
return `InlineBox(${str('inline')}, ${str(c.id)}, ${styleOf(c.style)}, ${fontSpecValue(lang, c.font)}, ${lineHeightValue(lang, c.lineHeight)}, ${list('U_InlineBox_LineBreak_TextLeaf', c.children.map(inline))})`;
};
const visit = (b: import('@dragon/layout').LayoutBox): string => {
const name = `${prefix}Box${n++}`;
const kids = b.children.map((c) => (c.kind === 'box'
? `${visit(c)}()`
: c.kind === 'replaced'
? replaced(c)
: `TextLeaf(${str('text')}, ${str(c.id)}, ${str(c.text)}, ${fontSpecValue(lang, c.font)}, ${lineHeightValue(lang, c.lineHeight)}, ${str(c.whiteSpaceCollapse)}, ${str(c.textWrapMode)})`));
const style = styleOf(b.style);
const arr = lang === 'swift' ? `JsArray<any U_LayoutBox_ReplacedLeaf_TextLeaf>([${kids.join(', ')}])` : `jsArrayOf<U_LayoutBox_ReplacedLeaf_TextLeaf>(${kids.join(', ')})`;
const body = `LayoutBox(${str('box')}, ${str(b.id)}, ${str(b.boxType)}, ${style}, ${arr})`;
const kids = b.children.map((c) => (c.kind === 'box' ? `${visit(c)}()` : c.kind === 'replaced' ? replaced(c) : inline(c)));
const strut = b.strut === null ? (lang === 'swift' ? 'nil' : 'null') : `LineStrut(${fontSpecValue(lang, b.strut.font)}, ${lineHeightValue(lang, b.strut.lineHeight)})`;
const body = `LayoutBox(${str('box')}, ${str(b.id)}, ${str(b.boxType)}, ${styleOf(b.style)}, ${strut}, ${list('U_InlineBox_LayoutBox_LineBreak_ReplacedLeaf_TextLeaf', kids)})`;
decls.push(lang === 'swift' ? `private func ${name}() -> LayoutBox {\n return ${body}\n}` : `private fun ${name}(): LayoutBox =\n ${body}`);
return name;
};
Expand Down
37 changes: 29 additions & 8 deletions packages/dragon/src/lower/ios-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import type {
FlexBasisValue,
FlexDirection,
FlexWrap,
FontSpec,
GapValue,
InsetValue,
JustifyContent,
LayoutBox,
LayoutStyle,
LengthCalc,
LineHeightValue,
LineStrut,
MarginValue,
Overflow,
MaxSizeValue,
Expand Down Expand Up @@ -257,6 +259,8 @@ function lowerStyleFrom(id: string, get: Get, isInitial: IsInitial, faults: Comp
columnGap: gap(id, get, 'column-gap', l),
textAlign: keyword<TextAlign>(id, get, 'text-align', ['start', 'end', 'left', 'right', 'center', 'justify']),
aspectRatio: aspectRatio(id, get),
// CSS2 §10.8.1: vertical-align is not a Dragon longhand; every box takes its initial value, which only inline boxes read.
verticalAlign: { kind: 'keyword', value: 'baseline' },
};
}

Expand All @@ -269,12 +273,10 @@ export function textFontProblem(t: ResolvedText): string | null {
return `font-family: ${valueToString(family)} on ${t.node.address} has no layout mapping (expected ${AHEM_EXPECTED})`;
}

// goal.md principle 3: the text node carries its inherited text styles, so the lowering reads the text node and never its parent.
function lowerText(t: ResolvedText): TextLeaf {
const id = t.node.address;
const get = (p: TextLonghand): CssValue => (t.props.get(p) as ResolvedValue).value;
/** A font and line-height as the engine reads them (input.ts FontSpec, LineHeightValue), from resolved font-size and line-height. */
function lowerFont(id: string, get: (p: TextLonghand) => CssValue): { readonly font: FontSpec; readonly lineHeight: LineHeightValue } {
const family = get('font-family');
if (textFontProblem(t) !== null) fail(id, 'font-family', family, AHEM_EXPECTED);
if (family.kind !== 'family' || family.value !== 'Ahem') fail(id, 'font-family', family, AHEM_EXPECTED);
const fs = get('font-size');
if (fs.kind !== 'length' || fs.unit !== 'px') fail(id, 'font-size', fs, 'px');
const lh = get('line-height');
Expand All @@ -283,11 +285,30 @@ function lowerText(t: ResolvedText): TextLeaf {
else if (lh.kind === 'number') lineHeight = { kind: 'number', value: lh.value };
else if (lh.kind === 'length' && lh.unit === 'px') lineHeight = { kind: 'px', value: lh.value };
else return fail(id, 'line-height', lh, 'normal | <number> | px');
return { font: { family: 'Ahem', size: fs.value, specifiedSize: { kind: 'px', value: fs.value }, absoluteSize: true }, lineHeight };
}

// goal.md principle 3: the text node carries its inherited text styles, so the lowering reads the text node and never its parent.
function lowerText(t: ResolvedText): TextLeaf {
const id = t.node.address;
const get = (p: TextLonghand): CssValue => (t.props.get(p) as ResolvedValue).value;
const family = get('font-family');
if (textFontProblem(t) !== null) fail(id, 'font-family', family, AHEM_EXPECTED);
const { font, lineHeight } = lowerFont(id, get);
const collapse = get('white-space-collapse');
if (collapse.kind !== 'keyword' || collapse.value !== 'collapse') fail(id, 'white-space-collapse', collapse, 'collapse');
const wrap = get('text-wrap-mode');
if (wrap.kind !== 'keyword' || (wrap.value !== 'wrap' && wrap.value !== 'nowrap')) return fail(id, 'text-wrap-mode', wrap, 'wrap | nowrap');
return { kind: 'text', id, text: t.text, font: { family: 'Ahem', size: fs.value, specifiedSize: { kind: 'px', value: fs.value }, absoluteSize: true }, lineHeight, whiteSpaceCollapse: 'collapse', textWrapMode: wrap.value as TextWrapMode };
return { kind: 'text', id, text: t.text, font, lineHeight, whiteSpaceCollapse: 'collapse', textWrapMode: wrap.value as TextWrapMode };
}

/**
* CSS2 §10.8.1: the strut of a block container with inline content, its own font and line-height; an anonymous box's are its
* parent's (inherited). The text leaves inherit the same values, so the strut equals their font until inline boxes change it.
*/
function strutOf(el: ResolvedElement, hasInline: boolean): LineStrut | null {
if (!hasInline) return null;
return lowerFont(el.element.address, (p) => (el.props.get(p) as ResolvedValue).value);
}

const displayOf = (el: ResolvedElement): string => {
Expand Down Expand Up @@ -317,7 +338,7 @@ function anonymousBox(parent: ResolvedElement, id: string, texts: readonly Resol
// Every non-inherited property of an anonymous box is its initial value.
const style = lowerStyleFrom(id, (p) => values.get(p) as CssValue, (p) => !INHERITED.has(p), faults, ua, { em: null, rem: null });
for (const t of texts) assertTextCarriesContainer(style, id, t);
return { kind: 'box', id, boxType: 'anonymous', style, children: texts.map(lowerText) };
return { kind: 'box', id, boxType: 'anonymous', style, strut: strutOf(parent, texts.length > 0), children: texts.map(lowerText) };
}

/**
Expand Down Expand Up @@ -358,5 +379,5 @@ function lowerBox(el: ResolvedElement, faults: CompilerFaults, ua: UaDataset, ro
}
}
flush();
return { kind: 'box', id, boxType: 'element', style, children };
return { kind: 'box', id, boxType: 'element', style, strut: strutOf(el, children.some((c) => c.kind === 'text')), children };
}
3 changes: 2 additions & 1 deletion packages/dragon/src/lower/native-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,11 @@ function sharedPaint(root: LayoutBox, resolved: ResolvedElement): NodePaint[] {
// A replaced leaf has no children; Phase A paints only its box (background and border), Phase B its content.
if (b.kind === 'replaced') return;
for (const c of b.children) {
if (c.kind !== 'text') {
if (c.kind === 'box' || c.kind === 'replaced') {
visit(c, b.id, own);
continue;
}
if (c.kind !== 'text') throw new ProgramError(`${c.id}: an inline box or line break has no native lowering yet`);
out.push(textPaint(c, b.id, texts));
}
};
Expand Down
4 changes: 2 additions & 2 deletions packages/dragon/src/profiles/native-lanes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
export type NativeLanesVerdict = { readonly recorded: boolean; readonly stale: readonly string[]; readonly notPassing: readonly string[] };

export const NATIVE_LANES: { readonly ios: NativeLanesVerdict; readonly android: NativeLanesVerdict } = {
ios: { recorded: true, stale: [], notPassing: ["device-pixels fail (65 failures (pixel 65))"] },
android: { recorded: true, stale: [], notPassing: ["device-pixels fail (94 failures (pixel 94))"] },
ios: { recorded: true, stale: [], notPassing: ["layout-vectors-device not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)", "device-frames not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)", "device-applied not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)", "device-lines not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)", "device-pixels not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)"] },
android: { recorded: true, stale: [], notPassing: ["layout-vectors-device not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)", "device-frames not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)", "device-applied not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)", "device-lines not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)", "device-pixels not run (run pnpm run parity:lanes -- --run-device (simulators and emulators); the tools are installed, so this is not blocked)"] },
};
18 changes: 11 additions & 7 deletions packages/dragon/test/native-backends.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,14 @@ describe('the LayoutStyle constructor arguments', () => {
if (p.kind !== 'ready') throw new Error(p.reason);
const swift = emitUikitCases([emitCase(p.programs.uikit)]).map((f) => f.text).join('\n');
const kotlin = emitAndroidViewsCases([emitCase(p.programs['android-views'])]).map((f) => f.text).join('\n');
expect(swift).toContain('AspectRatioValue_ratio(JsString("ratio"), 1024.0, 576.0))');
expect(swift).toContain('AspectRatioValue_autoRatio(JsString("auto-ratio"), 128.0, 64.0))');
expect(swift).toContain('JsString("start"), Auto(JsString("auto")))');
expect(kotlin).toContain('AspectRatioValue_ratio("ratio", 1024.0, 576.0))');
expect(kotlin).toContain('AspectRatioValue_autoRatio("auto-ratio", 128.0, 64.0))');
// INL1a: verticalAlign follows aspectRatio as the last LayoutStyle argument (input.ts order), always baseline from the compiler.
const swiftAlign = ', VerticalAlignKeywordValue(JsString("keyword"), JsString("baseline")))';
const kotlinAlign = ', VerticalAlignKeywordValue("keyword", "baseline"))';
expect(swift).toContain(`AspectRatioValue_ratio(JsString("ratio"), 1024.0, 576.0)${swiftAlign}`);
expect(swift).toContain(`AspectRatioValue_autoRatio(JsString("auto-ratio"), 128.0, 64.0)${swiftAlign}`);
expect(swift).toContain(`JsString("start"), Auto(JsString("auto"))${swiftAlign}`);
expect(kotlin).toContain(`AspectRatioValue_ratio("ratio", 1024.0, 576.0)${kotlinAlign}`);
expect(kotlin).toContain(`AspectRatioValue_autoRatio("auto-ratio", 128.0, 64.0)${kotlinAlign}`);
});
});

Expand Down Expand Up @@ -176,11 +179,12 @@ describe('a calculated line height is built as the engine\'s LineHeightCalc (non
it('on a text leaf and inside an lh leaf, in Swift and Kotlin', () => {
const leaf = { kind: 'text', id: 'p:t', text: 'XX', font, lineHeight: lhCalc, whiteSpaceCollapse: 'collapse', textWrapMode: 'wrap' } as const;
const width = { kind: 'calc', range: 'non-negative', expr: { kind: 'lh', value: 2, font, lineHeight: lhCalc } } as const;
const root = { kind: 'box', id: 'p', boxType: 'element', style: style(width), children: [leaf] } as const;
// INL1a: the container's strut has the leaf's font and line-height (validate.ts leaf-font), so it is a third LineHeightCalc.
const root = { kind: 'box', id: 'p', boxType: 'element', style: style(width), strut: { font, lineHeight: lhCalc }, children: [leaf] } as const;
for (const lang of ['swift', 'kotlin'] as const) {
const src = inputFunctions(lang, root as never, 't').decls.join('\n');
const q = lang === 'swift' ? (s: string) => `JsString("${s}")` : (s: string) => `"${s}"`;
expect(src.split(`LineHeightCalc(${q('calc')}, Px(${q('px')}, 12.0), ${q('non-negative')})`).length - 1, lang).toBe(2);
expect(src.split(`LineHeightCalc(${q('calc')}, Px(${q('px')}, 12.0), ${q('non-negative')})`).length - 1, lang).toBe(3);
expect(src, lang).not.toContain(`LengthCalc(${q('calc')}, Px(${q('px')}, 12.0)`);
}
});
Expand Down
4 changes: 2 additions & 2 deletions packages/dragon/test/state-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ const style = (width: number): LayoutBox['style'] => ({
width: px(width), height: auto, minWidth: auto, minHeight: auto, maxWidth: { kind: 'none' }, maxHeight: { kind: 'none' }, marginTop: px(0), marginRight: px(0), marginBottom: px(0), marginLeft: px(0),
paddingTop: px(0), paddingRight: px(0), paddingBottom: px(0), paddingLeft: px(0), borderTopWidth: px(0), borderRightWidth: px(0), borderBottomWidth: px(0), borderLeftWidth: px(0),
flexDirection: 'row', flexWrap: 'nowrap', flexGrow: 0, flexShrink: 1, flexBasis: auto, order: 0, justifyContent: 'flex-start', alignItems: 'stretch', alignSelf: 'auto', alignContent: 'normal',
rowGap: { kind: 'normal' }, columnGap: { kind: 'normal' }, textAlign: 'start', aspectRatio: auto,
rowGap: { kind: 'normal' }, columnGap: { kind: 'normal' }, textAlign: 'start', aspectRatio: auto, verticalAlign: { kind: 'keyword', value: 'baseline' },
}) as unknown as LayoutBox['style'];
const box = (id: string, width: number, children: LayoutBox['children'] = []): LayoutBox => ({ kind: 'box', id, boxType: 'element', style: style(width), children });
const box = (id: string, width: number, children: LayoutBox['children'] = []): LayoutBox => ({ kind: 'box', id, boxType: 'element', style: style(width), strut: null, children });

const node = (id: string, parent: string | null, color: number): ProgramNode => ({
id,
Expand Down
8 changes: 4 additions & 4 deletions packages/dragon/test/text.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import type { LayoutBox, TextLeaf } from '@dragon/layout';
import type { InlineChild, LayoutBox } from '@dragon/layout';
import type { Diagnostic, FrontEndResult, TreeNode } from '../src/index.ts';
import { createProject } from '../src/index.ts';
import { compiledFeatures, createProjectWith, iosLayoutProjection, NO_FAULTS, textTopology } from '../src/internal.ts';
Expand All @@ -20,7 +20,7 @@ function lowered(input: FrontEndResult, faults = NO_FAULTS): LayoutBox {
return p.input.root;
}

function find(b: LayoutBox, id: string): LayoutBox | TextLeaf | undefined {
function find(b: LayoutBox, id: string): LayoutBox | InlineChild | undefined {
if (b.id === id) return b;
for (const c of b.children) {
if (c.id === id && c.kind !== 'replaced') return c;
Expand All @@ -32,8 +32,8 @@ function find(b: LayoutBox, id: string): LayoutBox | TextLeaf | undefined {
return undefined;
}

const summary = (b: LayoutBox | TextLeaf | undefined): unknown =>
b === undefined ? undefined : b.kind === 'text' ? `${b.id}=${JSON.stringify(b.text)}` : `${b.id}(${b.boxType})[${b.children.map((c) => (c.kind === 'text' ? `${c.id}=${JSON.stringify(c.text)}` : c.id)).join(' ')}]`;
const summary = (b: LayoutBox | InlineChild | undefined): unknown =>
b === undefined ? undefined : b.kind === 'text' ? `${b.id}=${JSON.stringify(b.text)}` : b.kind !== 'box' ? b.id : `${b.id}(${b.boxType})[${b.children.map((c) => (c.kind === 'text' ? `${c.id}=${JSON.stringify(c.text)}` : c.id)).join(' ')}]`;

describe('white-space phase I over one inline formatting context (css-text-3 §4.1.1)', () => {
it('collapses each white space sequence to one space kept by the run where it starts, across run boundaries', () => {
Expand Down
Loading