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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Following are rules that work or will work soon. Shorthand properties are not li
| <code>margin</code> | `em`, `px`, `%`, `cm` etc, `auto` | ✅&zwj;&nbsp;Works |
| <code>max-height</code>, <code>max-width</code>,<br><code>min-height</code>, <code>min-width</code> | `em`, `px`, `%`, `cm` etc, `auto` | 🚧&zwj;&nbsp;Planned |
| <code>padding</code> | `em`, `px`, `%`, `cm` etc | ✅&zwj;&nbsp;Works |
| <code>position</code> | `absolute` | 🚧&zwj;&nbsp;Planned |
| <code>position</code> | `absolute` | &zwj;&nbsp;Works |
| <code>position</code> | `fixed` | 🚧&zwj;&nbsp;Planned |
| <code>position</code> | `relative` | ✅&zwj;&nbsp;Works |
| <code>transform</code> | | 🚧&zwj;&nbsp;Planned |
Expand Down
11 changes: 8 additions & 3 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import '#register-default-environment';
import {HTMLElement, TextNode} from './dom.ts';
import {DeclaredStyle, getOriginStyle, computeElementStyle} from './style.ts';
import {fonts, FontFace, createFaceFromTables, createFaceFromTablesSync, onLoadWalkerTextNodeForFonts, onLoadWalkerElementForFonts} from './text-font.ts';
import {generateBlockContainer, layoutBlockLevelBox} from './layout-flow.ts';
import {generateBlockContainer, layoutBlockLevelBox, layoutAbsolutes} from './layout-flow.ts';
import HtmlPaintBackend from './paint-html.ts';
import SvgPaintBackend from './paint-svg.ts';
import CanvasPaintBackend from './paint-canvas.ts';

import paint from './paint.ts';
import {BoxArea, Layout, prelayout, postlayout} from './layout-box.ts';
import type {Box, StaticPosition} from './layout-box.ts';
import {onLoadWalkerElementForImage} from './layout-image.ts';
import {id, uuid} from './util.ts';

Expand Down Expand Up @@ -48,9 +49,13 @@ export function layout(rootElement: HTMLElement): Layout {
export function reflow(layout: Layout, width = 640, height = 480) {
const initialContainingBlock = new BoxArea(layout.root(), 0, 0, width, height);

// Only alive for the length of this reflow, so nothing is retained on the Layout
const staticPositions = new Map<Box, StaticPosition>();

prelayout(layout, initialContainingBlock);
layoutBlockLevelBox(layout, layout.root(), {});
postlayout(layout);
layoutBlockLevelBox(layout, layout.root(), {staticPositions});
layoutAbsolutes(layout, {staticPositions});
postlayout(layout, staticPositions);
}

/**
Expand Down
99 changes: 97 additions & 2 deletions src/layout-box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,11 @@ export abstract class FormattingBox extends Box {
}

isOutOfFlow() {
return this.style.float !== 'none'; // TODO: or position === 'absolute'
return this.style.float !== 'none' || this.style.position === 'absolute';
}

isAbsolute() {
return this.style.position === 'absolute';
}

propagate(parent: Box) {
Expand Down Expand Up @@ -634,6 +638,21 @@ export class BoxArea {
this.parent = p;
}

blockSizeForPotentiallyOrthogonal(box: FormattingBox) {
if (!this.parent) return this.blockSize; // root area
if (!this.box.isBlockContainer()) return this.blockSize; // cannot be orthogonal
const cb1 = this.box.getContainingBlock();
const cb2 = box.getContainingBlock();
if (
(this.box.getWritingModeAsParticipant(cb1) === 'horizontal-tb') !==
(box.getWritingModeAsParticipant(cb2) === 'horizontal-tb')
) {
return this.inlineSize;
} else {
return this.blockSize;
}
}

inlineSizeForPotentiallyOrthogonal(box: FormattingBox) {
if (!this.parent) return this.inlineSize; // root area
if (!this.box.isBlockContainer()) return this.inlineSize; // cannot be orthogonal
Expand Down Expand Up @@ -715,6 +734,56 @@ export class BoxArea {
}
}

/**
* An absolutely positioned box whose insets are `auto` on an axis sits at the
* position it would have had in flow, which is known in the axes of its in-flow
* parent, not of its containing block. Both of those are ancestors, so both are
* already absolute when postlayout reaches this box, and the offset can be
* mapped through physical coordinates.
*
* `area` is the content area of the in-flow parent, which is the nearest block
* container ancestor: the only boxes that can sit between them are inlines.
*/
function shiftToStaticPosition(box: Box, staticPosition: StaticPosition, area: BoxArea) {
const [blockOffset, inlineOffset] = staticPosition;
const borderArea = box.getBorderArea();
const containingBlock = borderArea.parent;
if (!containingBlock) throw new Error('Assertion failed');
// Layout only leaves room for the static position when it had no inset to
// position against, which is the same question the box model asked
const needsBlock = box.style.getInsetBlockStart(containingBlock) === 'auto' &&
box.style.getInsetBlockEnd(containingBlock) === 'auto';
const needsLineLeft = box.style.getInsetLineLeft(containingBlock) === 'auto' &&
box.style.getInsetLineRight(containingBlock) === 'auto';
if (!needsBlock && !needsLineLeft) return;
const parentWritingMode = area.getEstablishedWritingMode();
let x, y;

if (parentWritingMode === 'vertical-lr') {
x = area.x + blockOffset;
y = area.y + inlineOffset;
} else if (parentWritingMode === 'vertical-rl') {
x = area.x + area.width - blockOffset;
y = area.y + inlineOffset;
} else { // 'horizontal-tb'
x = area.x + inlineOffset;
y = area.y + blockOffset;
}

const writingMode = containingBlock.getEstablishedWritingMode();

if (writingMode === 'vertical-lr') {
if (needsBlock) borderArea.blockStart += x - containingBlock.x;
if (needsLineLeft) borderArea.lineLeft += y - containingBlock.y;
} else if (writingMode === 'vertical-rl') {
if (needsBlock) borderArea.blockStart += containingBlock.x + containingBlock.width - x;
if (needsLineLeft) borderArea.lineLeft += y - containingBlock.y;
} else { // 'horizontal-tb'
if (needsBlock) borderArea.blockStart += y - containingBlock.y;
if (needsLineLeft) borderArea.lineLeft += x - containingBlock.x;
}
}

export function prelayout(layout: Layout, icb: BoxArea) {
const parents: (BlockContainer | Inline)[] = [];
const ifcs: BlockContainerOfInlines[] = [];
Expand Down Expand Up @@ -772,11 +841,26 @@ export function prelayout(layout: Layout, icb: BoxArea) {
}
}

export function postlayout(layout: Layout) {
export function postlayout(layout: Layout, staticPositions: Map<Box, StaticPosition>) {
const parents: (BlockContainer | Inline)[] = [];

for (let i = 0; i < layout.tree.length; i++) {
const item = layout.tree[i];

if (item.isFormattingBox() && item.isAbsolute()) {
// The offset was recorded in the axes of the in-flow parent, which the
// preorder walk has already absolutified
const staticPosition = staticPositions.get(item);
if (staticPosition) {
let inflowParent;
for (let j = parents.length - 1; j >= 0 && !inflowParent; j--) {
if (parents[j].isBlockContainer()) inflowParent = parents[j];
}
if (!inflowParent) throw new Error('Assertion failed');
shiftToStaticPosition(item, staticPosition, inflowParent.getContentArea());
}
}

item.postlayoutPreorder(layout);
if (item.isBlockContainer() || item.isInline()) {
parents.push(item);
Expand Down Expand Up @@ -836,6 +920,17 @@ export function log(layout: Layout, logger?: Logger, options?: TreeLogOptions) {
logger.flush();
}

/**
* Where an absolutely positioned box would have been if it were in flow, in the
* logical axes of the content area of its in-flow parent. Only used when both
* insets on that axis are `auto` (CSS 2.2 § 10.3.7, § 10.6.4).
*
* Only needed between line building and postlayout, so it is not stored on the
* `Layout`: it rides along on the layout context and is gone once `reflow`
* returns.
*/
export type StaticPosition = [blockOffset: number, inlineOffset: number];

export class Layout {
tree: InlineLevel[];

Expand Down
Loading