Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions examples/tests/rtl-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { ExampleSettings } from '../common/ExampleSettings.js';

export async function automation(settings: ExampleSettings) {
await test(settings);
await settings.snapshot();
}

/**
* RTL layout mirroring.
*
* Each row places three boxes at the same increasing `x` offsets. In an LTR
* container they read left-to-right; in an RTL container the same offsets are
* mirrored within the container width, so they read right-to-left.
*
* Each box carries a small white marker child at its own `x: 0`. A box's own
* `rtl` flag (not the parent's) controls where that marker lands: under RTL the
* marker mirrors to the box's right edge. The third row keeps the RTL container
* (so box positions still mirror) but sets `rtl: false` on each box, so the
* markers stay on the left edge — demonstrating a sub-tree opting back out.
*
* Default 'left' text alignment also flips to the right edge under RTL.
*/
export default async function test({ renderer, testRoot }: ExampleSettings) {
const CONTAINER_W = 700;
const BOX = 200;
const MARKER = 30;
const GAP = 20;
const colors = [0xff0000ff, 0x00ff00ff, 0x0000ffff];

const makeRow = (
y: number,
containerRtl: boolean | undefined,
boxRtl: boolean | undefined,
label: string,
) => {
const container = renderer.createNode({
x: 20,
y,
w: CONTAINER_W,
h: BOX,
color: 0x222222ff,
rtl: containerRtl,
parent: testRoot,
});

for (let i = 0; i < 3; i++) {
const box = renderer.createNode({
x: i * (BOX + GAP),
w: BOX,
h: BOX,
color: colors[i],
rtl: boxRtl,
parent: container,
});
// Marker at the box's own top-left; mirrors to the right under box RTL.
renderer.createNode({
x: 0,
y: 0,
w: MARKER,
h: MARKER,
color: 0xffffffff,
parent: box,
});
}

renderer.createTextNode({
x: 0,
y: BOX + 4,
w: CONTAINER_W,
fontFamily: 'Ubuntu',
fontSize: 28,
color: 0xffffffff,
forceLoad: true,
text: label,
parent: container,
});
};

makeRow(20, false, undefined, 'LTR container (markers left)');
makeRow(300, true, undefined, 'RTL container (mirrored, markers right)');
makeRow(580, true, false, 'RTL container, boxes rtl:false (markers left)');
}
118 changes: 118 additions & 0 deletions src/core/CoreNode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('set color()', () => {
pivotX: 0,
pivotY: 0,
rotation: 0,
rtl: null,
rtt: false,
scale: 1,
scaleX: 1,
Expand Down Expand Up @@ -1180,4 +1181,121 @@ describe('set color()', () => {
expect(node.clippingRect.h).toBe(30);
});
});

describe('rtl direction', () => {
it('defaults to ltr (false) with no parent', () => {
const node = new CoreNode(stage, defaultProps());
expect(node.rtl).toBe(false);
});

it('reflects an explicit own value', () => {
const node = new CoreNode(stage, defaultProps());
node.rtl = true;
expect(node.rtl).toBe(true);
node.rtl = false;
expect(node.rtl).toBe(false);
});

it('inherits from the parent when own value is null', () => {
const parent = new CoreNode(stage, defaultProps());
parent.rtl = true;
const child = new CoreNode(stage, defaultProps({ parent }));
expect(child.rtl).toBe(true);
});

it('an explicit false on a child overrides an rtl parent', () => {
const parent = new CoreNode(stage, defaultProps());
parent.rtl = true;
const child = new CoreNode(stage, defaultProps({ parent, rtl: false }));
expect(child.rtl).toBe(false);
});

it('resolves _rtl from the parent during update', () => {
const parent = new CoreNode(stage, defaultProps({ w: 200 }));
parent.globalTransform = Matrix3d.identity();
parent.worldAlpha = 1;
parent._rtl = true;

const node = new CoreNode(stage, defaultProps({ parent }));
node.update(0, clippingRect);

expect(node._rtl).toBe(true);
});
});

describe('rtl layout mirroring', () => {
const makeRtlParent = (w: number) => {
const parent = new CoreNode(stage, defaultProps({ w }));
parent.globalTransform = Matrix3d.identity();
parent.worldAlpha = 1;
parent._rtl = true;
return parent;
};

it("mirrors a child's x within the parent width", () => {
const parent = makeRtlParent(200);
const node = new CoreNode(stage, defaultProps({ parent }));
node.x = 20;
node.y = 30;
node.w = 50;
node.h = 40;

node.update(0, clippingRect);

// 200 - x(20) - w(50) * scaleX(1) = 130; y is untouched.
expect(node.globalTransform!.tx).toBe(130);
expect(node.globalTransform!.ty).toBe(30);
});

it('accounts for scaleX when mirroring', () => {
const parent = makeRtlParent(200);
const node = new CoreNode(stage, defaultProps({ parent }));
node.x = 20;
node.w = 50;
node.scaleX = 2;

node.update(0, clippingRect);

// 200 - 20 - 50 * 2 = 80
expect(node.globalTransform!.tx).toBe(80);
});

it('does not mirror when the parent is ltr', () => {
const parent = new CoreNode(stage, defaultProps({ w: 200 }));
parent.globalTransform = Matrix3d.identity();
parent.worldAlpha = 1;

const node = new CoreNode(stage, defaultProps({ parent }));
node.x = 20;
node.w = 50;

node.update(0, clippingRect);

expect(node.globalTransform!.tx).toBe(20);
});

it('does not mirror when the parent width is unknown (0)', () => {
const parent = makeRtlParent(0);
const node = new CoreNode(stage, defaultProps({ parent }));
node.x = 20;
node.w = 50;

node.update(0, clippingRect);

expect(node.globalTransform!.tx).toBe(20);
});

it('restores the cached local transform after mirroring', () => {
const parent = makeRtlParent(200);
const node = new CoreNode(stage, defaultProps({ parent }));
node.x = 20;
node.w = 50;

node.update(0, clippingRect);

// The local transform must stay un-mirrored so the next frame doesn't
// double-apply the mirror.
expect(node.localTransform!.tx).toBe(20);
});
});
});
Loading
Loading