chrome-devtools-frontend
Version:
Chrome DevTools UI
512 lines (453 loc) • 18.2 kB
text/typescript
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {PathCommands, Position, Quad} from './common.js';
import {
distance,
getColinearPointAtDistance,
getGapQuadBetweenQuads,
getGapQuads,
getLinesAndItemsQuads,
growQuadToEdgesOf,
intersectSegments,
segmentContains,
uniteQuads,
} from './highlight_flex_common.js';
function createPathCommands(...points: number[]): PathCommands {
if (points.length !== 8) {
throw new Error('Expected 8 coordinates to describe the element');
}
const path: PathCommands = ['M'];
for (let i = 0; i < points.length; i += 2) {
path.push(points[i]);
path.push(points[i + 1]);
path.push('L');
}
path[path.length - 1] = 'Z';
return path;
}
function createItem(...points: number[]): {itemBorder: PathCommands, baseline: number} {
return {
itemBorder: createPathCommands(...points),
baseline: 0,
};
}
function createQuad(...points: number[]): Quad {
if (points.length !== 8) {
throw new Error('Expected 8 coordinates to describe the element');
}
return {
p1: {x: points[0], y: points[1]},
p2: {x: points[2], y: points[3]},
p3: {x: points[4], y: points[5]},
p4: {x: points[6], y: points[7]},
};
}
describe('getLinesAndItemsQuads', () => {
it('creates the right number of line and item quads', () => {
const lineQuads = getLinesAndItemsQuads(
createPathCommands(0, 0, 100, 0, 100, 100, 0, 100),
[
[
createItem(10, 10, 30, 10, 30, 30, 10, 30),
createItem(40, 10, 60, 10, 60, 30, 40, 30),
createItem(70, 10, 90, 10, 90, 30, 70, 30),
],
[
createItem(10, 40, 70, 40, 70, 70, 10, 70),
createItem(80, 40, 90, 40, 90, 70, 80, 70),
],
[
createItem(10, 80, 40, 80, 40, 90, 10, 90),
createItem(50, 80, 90, 80, 90, 90, 50, 90),
],
],
true, false);
assert.lengthOf(lineQuads, 3, '3 line quads got created');
assert.lengthOf(lineQuads[0].items, 3, '3 flex items on the first line got created');
assert.lengthOf(lineQuads[1].items, 2, '2 flex items on the second line got created');
assert.lengthOf(lineQuads[2].items, 2, '2 flex items on the third line got created');
});
it('creates a line quad as big as the container when there is only one line', () => {
const lineQuads = getLinesAndItemsQuads(
createPathCommands(0, 0, 10, 0, 10, 10, 0, 10), [[createItem(2, 2, 8, 2, 8, 8, 2, 8)]], true, false);
assert.deepEqual(lineQuads[0].quad.p1, {x: 0, y: 0});
assert.deepEqual(lineQuads[0].quad.p2, {x: 10, y: 0});
assert.deepEqual(lineQuads[0].quad.p3, {x: 10, y: 10});
assert.deepEqual(lineQuads[0].quad.p4, {x: 0, y: 10});
});
it('creates quads for flex lines that extend to the edges of the container in the main direction', () => {
const lineQuadsRowDirection = getLinesAndItemsQuads(
createPathCommands(0, 0, 100, 0, 100, 80, 0, 80),
[
[
createItem(10, 10, 30, 10, 30, 30, 10, 30),
createItem(40, 10, 60, 10, 60, 30, 40, 30),
createItem(70, 10, 90, 10, 90, 30, 70, 30),
],
[
createItem(10, 40, 70, 40, 70, 70, 10, 70),
createItem(80, 40, 90, 40, 90, 70, 80, 70),
],
],
true, false);
assert.deepEqual(lineQuadsRowDirection[0].quad, createQuad(0, 10, 100, 10, 100, 30, 0, 30));
assert.deepEqual(lineQuadsRowDirection[1].quad, createQuad(0, 40, 100, 40, 100, 70, 0, 70));
const lineQuadsColumnDirection = getLinesAndItemsQuads(
createPathCommands(0, 0, 50, 0, 50, 70, 0, 70),
[
[
createItem(10, 10, 20, 10, 20, 30, 10, 30),
createItem(10, 40, 20, 40, 20, 50, 10, 50),
],
[
createItem(30, 20, 40, 20, 40, 40, 30, 40),
createItem(30, 50, 40, 50, 40, 60, 30, 60),
],
],
false, false);
assert.deepEqual(lineQuadsColumnDirection[0].quad, createQuad(10, 0, 20, 0, 20, 70, 10, 70));
assert.deepEqual(lineQuadsColumnDirection[1].quad, createQuad(30, 0, 40, 0, 40, 70, 30, 70));
});
it('creates normal and extended quads for items', () => {
const lineQuads = getLinesAndItemsQuads(
createPathCommands(0, 0, 70, 0, 70, 40, 0, 40),
[
[
createItem(10, 10, 30, 10, 30, 30, 10, 30),
createItem(40, 10, 60, 10, 60, 30, 40, 30),
],
],
true, false);
assert.deepEqual(
lineQuads[0].items[0], createQuad(10, 10, 30, 10, 30, 30, 10, 30), 'The first flex item quad matches the item');
assert.deepEqual(
lineQuads[0].items[1], createQuad(40, 10, 60, 10, 60, 30, 40, 30),
'The second flex item quad matches the item');
assert.deepEqual(
lineQuads[0].extendedItems[0], createQuad(10, 0, 30, 0, 30, 40, 10, 40),
'The first flex item extended quad extends to the cross edge of the flex line');
assert.deepEqual(
lineQuads[0].extendedItems[1], createQuad(40, 0, 60, 0, 60, 40, 40, 40),
'The second flex item extended quad extends to the cross edge of the flex line');
});
it('creates correct quads with transformed layout', () => {
const lineQuads = getLinesAndItemsQuads(
createPathCommands(10, 70, 70, 10, 110, 50, 50, 110),
[
[
createItem(30, 70, 50, 50, 60, 60, 40, 80),
createItem(60, 40, 70, 30, 80, 40, 70, 50),
],
[
createItem(40, 80, 50, 70, 60, 80, 50, 90),
createItem(50, 70, 80, 40, 90, 50, 60, 80),
],
],
true, false);
assert.deepEqual(lineQuads[0].quad, createQuad(20, 80, 80, 20, 90, 30, 30, 90));
assert.deepEqual(lineQuads[0].extendedItems[0], createQuad(30, 70, 50, 50, 60, 60, 40, 80));
assert.deepEqual(lineQuads[0].extendedItems[1], createQuad(60, 40, 70, 30, 80, 40, 70, 50));
assert.deepEqual(lineQuads[1].quad, createQuad(30, 90, 90, 30, 100, 40, 40, 100));
assert.deepEqual(lineQuads[1].extendedItems[0], createQuad(40, 80, 50, 70, 60, 80, 50, 90));
assert.deepEqual(lineQuads[1].extendedItems[1], createQuad(50, 70, 80, 40, 90, 50, 60, 80));
});
});
describe('getGapQuads', () => {
it('does not return any cross gap if there is only 1 line', () => {
const {crossGaps} = getGapQuads(
{
crossGap: 10,
mainGap: 10,
isHorizontalFlow: true,
isReverse: false,
},
[{
quad: createQuad(0, 0, 100, 0, 100, 100, 0, 100),
items: [],
extendedItems: [],
}]);
assert.lengthOf(crossGaps, 0, 'There cannot be cross gap if there is only one line');
});
it('does not return any main or cross gap if there actually isn\'t any gaps', () => {
const {crossGaps, mainGaps} = getGapQuads(
{
crossGap: 0,
mainGap: 0,
isHorizontalFlow: true,
isReverse: false,
},
[
{
quad: createQuad(0, 10, 100, 10, 100, 30, 0, 30),
items: [
createQuad(10, 10, 30, 10, 30, 30, 10, 30),
createQuad(40, 10, 60, 10, 60, 30, 30, 40),
createQuad(70, 10, 90, 10, 90, 30, 70, 30),
],
extendedItems: [
createQuad(10, 10, 30, 10, 30, 30, 10, 30),
createQuad(40, 10, 60, 10, 60, 30, 30, 40),
createQuad(70, 10, 90, 10, 90, 30, 70, 30),
],
},
{
quad: createQuad(0, 40, 100, 40, 100, 70, 70, 0),
items: [
createQuad(10, 40, 70, 40, 70, 70, 10, 70),
createQuad(80, 40, 90, 40, 90, 70, 80, 70),
],
extendedItems: [
createQuad(10, 40, 70, 40, 70, 70, 10, 70),
createQuad(80, 40, 90, 40, 90, 70, 80, 70),
],
},
]);
assert.lengthOf(crossGaps, 0, 'No cross gap quads created when there is no cross gap');
assert.lengthOf(mainGaps[0], 0, 'No main gap quads created when there is no main gap on the first line');
assert.lengthOf(mainGaps[1], 0, 'No main gap quads created when there is no main gap on the second line');
});
it('returns 1 less gap than the number of lines and the number of items', () => {
const {crossGaps, mainGaps} = getGapQuads(
{
crossGap: 10,
mainGap: 10,
isHorizontalFlow: true,
isReverse: false,
},
[
{
quad: createQuad(0, 10, 100, 10, 100, 30, 0, 30),
items: [
createQuad(10, 10, 30, 10, 30, 30, 10, 30),
createQuad(40, 10, 60, 10, 60, 30, 30, 40),
createQuad(70, 10, 90, 10, 90, 30, 70, 30),
],
extendedItems: [
createQuad(10, 10, 30, 10, 30, 30, 10, 30),
createQuad(40, 10, 60, 10, 60, 30, 30, 40),
createQuad(70, 10, 90, 10, 90, 30, 70, 30),
],
},
{
quad: createQuad(0, 40, 100, 40, 100, 70, 70, 0),
items: [
createQuad(10, 40, 70, 40, 70, 70, 10, 70),
createQuad(80, 40, 90, 40, 90, 70, 80, 70),
],
extendedItems: [
createQuad(10, 40, 70, 40, 70, 70, 10, 70),
createQuad(80, 40, 90, 40, 90, 70, 80, 70),
],
},
{
quad: createQuad(0, 80, 100, 80, 100, 90, 90, 0),
items: [
createQuad(10, 80, 40, 80, 40, 90, 10, 90),
createQuad(50, 80, 90, 80, 90, 90, 50, 90),
],
extendedItems: [
createQuad(10, 80, 40, 80, 40, 90, 10, 90),
createQuad(50, 80, 90, 80, 90, 90, 50, 90),
],
},
]);
assert.lengthOf(crossGaps, 2, 'There are 2 cross gaps for 3 lines');
assert.lengthOf(mainGaps[0], 2, 'There are 2 main gaps on the first line, which has 3 items');
assert.lengthOf(mainGaps[1], 1, 'There is 1 main gap on the second line, which has 2 items');
assert.lengthOf(mainGaps[2], 1, 'There is 1 main gap on the third line, which has 2 items');
});
});
describe('getGapQuadBetweenQuads', () => {
it('creates a quad between 2 quads stacked either vertically or horizontally, also when reversed', () => {
const quadV = getGapQuadBetweenQuads(
createQuad(0, 0, 60, 0, 60, 10, 0, 10),
createQuad(0, 20, 60, 20, 60, 30, 0, 30),
10,
true,
false,
);
assert.deepEqual(quadV, createQuad(0, 10, 60, 10, 60, 20, 0, 20));
const quadVReversed = getGapQuadBetweenQuads(
createQuad(0, 20, 60, 20, 60, 30, 0, 30),
createQuad(0, 0, 60, 0, 60, 10, 0, 10),
10,
true,
true,
);
assert.deepEqual(quadVReversed, quadV);
const quadH = getGapQuadBetweenQuads(
createQuad(0, 0, 10, 0, 10, 50, 50, 0),
createQuad(20, 0, 30, 0, 30, 50, 50, 20),
10,
false,
false,
);
assert.deepEqual(quadH, createQuad(10, 0, 20, 0, 20, 50, 10, 50));
const quadHReversed = getGapQuadBetweenQuads(
createQuad(20, 0, 30, 0, 30, 50, 50, 20),
createQuad(0, 0, 10, 0, 10, 50, 50, 0),
10,
false,
true,
);
assert.deepEqual(quadHReversed, quadH);
});
it('works when the gap is smaller than the distance between the quads', () => {
const quad = getGapQuadBetweenQuads(
createQuad(0, 0, 30, 0, 30, 20, 0, 20),
createQuad(0, 50, 30, 50, 30, 70, 0, 70),
10,
true,
false,
);
assert.deepEqual(quad, createQuad(0, 30, 30, 30, 30, 40, 0, 40));
});
it('works when the quads are transformed', () => {
const quad = getGapQuadBetweenQuads(
createQuad(0, 20, 20, 0, 40, 20, 20, 40),
createQuad(50, 70, 70, 50, 80, 60, 60, 80),
10,
true,
false,
);
// The rounding of coordinates ends up 1px off, but this won't really matter visually, so the test just accounts for
// it here.
assert.deepEqual(quad, createQuad(31, 51, 51, 31, 59, 39, 39, 59));
});
});
describe('uniteQuads', () => {
it('creates a quad that is big enough to contain the 2 passed quads', () => {
const quad = uniteQuads(
createQuad(0, 20, 10, 20, 10, 30, 0, 30),
createQuad(20, 10, 40, 10, 40, 40, 20, 40),
true,
false,
);
assert.deepEqual(quad, createQuad(0, 10, 40, 10, 40, 40, 0, 40));
});
it('can be called multiple times with the previously united quad to construct a flex line out of flex items', () => {
let quad = uniteQuads(
createQuad(0, 20, 10, 20, 10, 30, 0, 30),
createQuad(20, 10, 40, 10, 40, 40, 20, 40),
true,
false,
);
quad = uniteQuads(
quad,
createQuad(60, 30, 90, 30, 90, 50, 60, 50),
true,
false,
);
quad = uniteQuads(
quad,
createQuad(130, 0, 180, 0, 180, 30, 130, 30),
true,
false,
);
assert.deepEqual(quad, createQuad(0, 0, 180, 0, 180, 50, 0, 50));
});
it('also works when the quads are transformed', () => {
const quad = uniteQuads(
createQuad(0, 20, 20, 0, 40, 20, 20, 40),
createQuad(50, 70, 70, 50, 80, 60, 60, 80),
false,
false,
);
assert.deepEqual(quad, createQuad(0, 20, 20, 0, 80, 60, 60, 80));
});
});
describe('growQuadToEdgesOf', () => {
it('works horizontally', () => {
const quad = growQuadToEdgesOf(
createQuad(10, 10, 20, 10, 20, 20, 10, 20),
createQuad(0, 0, 60, 0, 60, 30, 0, 30),
true,
);
assert.deepEqual(quad, createQuad(0, 10, 60, 10, 60, 20, 0, 20));
});
it('works vertically', () => {
const quad = growQuadToEdgesOf(
createQuad(10, 10, 20, 10, 20, 20, 10, 20),
createQuad(0, 0, 60, 0, 60, 30, 0, 30),
false,
);
assert.deepEqual(quad, createQuad(10, 0, 20, 0, 20, 30, 10, 30));
});
it('works with transformed quads', () => {
const quad = growQuadToEdgesOf(
createQuad(30, 60, 60, 30, 80, 50, 50, 80),
createQuad(10, 60, 60, 10, 100, 50, 50, 100),
true,
);
assert.deepEqual(quad, createQuad(20, 70, 70, 20, 90, 40, 40, 90));
});
});
describe('getColinearPointAtDistance', () => {
function assertPoint(p1: Position, p2: Position, distance: number, expected: Position): void {
const point = getColinearPointAtDistance(p1, p2, distance);
assert.deepEqual({x: Math.round(point.x), y: Math.round(point.y)}, expected);
}
it('returns the right coordinates when the line is horizontal', () => {
assertPoint({x: 0, y: 0}, {x: 10, y: 0}, 5, {x: 5, y: 0});
});
it('returns the right coordinates when the line is vertical', () => {
assertPoint({x: 0, y: 0}, {x: 0, y: 10}, 5, {x: 0, y: 5});
});
it('returns the right coordinates when the line is at an angle', () => {
assertPoint({x: 0, y: 0}, {x: 10, y: 10}, 5, {x: 4, y: 4});
});
it('also works when distance is longer than the p1-p2 segment', () => {
assertPoint({x: 10, y: 20}, {x: 10, y: 40}, 50, {x: 10, y: 70});
});
});
describe('distance', () => {
function assertDistance(p1: Position, p2: Position, expected: number): void {
const d = distance(p1, p2);
assert.deepEqual(Math.round(d), expected);
}
it('works', () => {
assertDistance({x: 0, y: 0}, {x: 10, y: 0}, 10);
assertDistance({x: 0, y: 0}, {x: 100, y: 0}, 100);
assertDistance({x: 10, y: 0}, {x: 0, y: 0}, 10);
assertDistance({x: 10, y: 10}, {x: 10, y: 30}, 20);
assertDistance({x: 10, y: 10}, {x: 10, y: 5}, 5);
assertDistance({x: 10, y: 10}, {x: 20, y: 20}, 14);
});
});
describe('segmentContains', () => {
it('works with straight segments', () => {
assert.isFalse(segmentContains([{x: 0, y: 0}, {x: 0, y: 10}], {x: 10, y: 10}));
assert.isFalse(segmentContains([{x: 0, y: 10}, {x: 0, y: 0}], {x: 10, y: 10}));
assert.isFalse(segmentContains([{x: 10, y: 10}, {x: 100, y: 10}], {x: 10, y: 20}));
assert.isFalse(segmentContains([{x: 10, y: 10}, {x: 10, y: 100}], {x: 10, y: 0}));
assert.isTrue(segmentContains([{x: 0, y: 0}, {x: 0, y: 100}], {x: 0, y: 10}));
assert.isTrue(segmentContains([{x: 0, y: 100}, {x: 0, y: 0}], {x: 0, y: 10}));
assert.isTrue(segmentContains([{x: 10, y: 10}, {x: 20, y: 10}], {x: 15, y: 10}));
assert.isTrue(segmentContains([{x: 20, y: 10}, {x: 10, y: 10}], {x: 15, y: 10}));
});
it('works with other segments', () => {
assert.isFalse(segmentContains([{x: 0, y: 0}, {x: 10, y: 10}], {x: 10, y: 100}));
assert.isFalse(segmentContains([{x: 20, y: 20}, {x: 30, y: 0}], {x: 10, y: 100}));
assert.isTrue(segmentContains([{x: 0, y: 0}, {x: 100, y: 100}], {x: 50, y: 50}));
assert.isTrue(segmentContains([{x: 0, y: 100}, {x: 100, y: 0}], {x: 50, y: 50}));
});
});
describe('intersectSegments', () => {
function assertIntersection(s1: Position[], s2: Position[], expected: Position): void {
const point = intersectSegments(s1, s2);
assert.deepEqual({x: Math.round(point.x), y: Math.round(point.y)}, expected);
}
it('works when x or y is 0', () => {
assertIntersection([{x: 0, y: 0}, {x: 0, y: 10}], [{x: 0, y: 5}, {x: 5, y: 5}], {x: 0, y: 5});
assertIntersection([{x: 0, y: 0}, {x: 100, y: 0}], [{x: 50, y: 0}, {x: 50, y: 5}], {x: 50, y: 0});
assertIntersection([{x: -5, y: 0}, {x: 5, y: 0}], [{x: 0, y: -5}, {x: 0, y: 5}], {x: 0, y: 0});
});
it('works in simple cases', () => {
assertIntersection([{x: 5, y: 15}, {x: 15, y: 5}], [{x: 5, y: 5}, {x: 15, y: 15}], {x: 10, y: 10});
assertIntersection([{x: 5, y: 10}, {x: 15, y: 10}], [{x: 10, y: 5}, {x: 10, y: 15}], {x: 10, y: 10});
});
it('works when segments only intersect outside their boundaries', () => {
assertIntersection([{x: 5, y: 5}, {x: 5, y: 15}], [{x: 15, y: 10}, {x: 25, y: 10}], {x: 5, y: 10});
});
});