implement position: absolute - #34
Conversation
|
This looks promising! Before I dive into this, can you disclose if you used AI or not? |
I leverage AI models for automated unit test generation, technical writing, and code documentation. |
|
Cool. I got absolute positioning 90% done a year or two ago but I think I got stuck on where to save the static positions (more on that below). It'll take me a while to re-familiarize myself and finish what I'm working on, but it looks good. The other fixes you mentioned sound like a quicker review. One thing that stands out is that my end goal for the |
|
Apologies if you already had work in progress. I didn't see anything in the repository. Having spent some time testing it, I just thought it would be helpful to share what I've found so far. |
|
On the map: it's only alive between line building and postlayout, and |
|
I'm much more interested in your work here vs mine. Sorry, I re-read my last comment and realized it was ambiguous: the things I'm working on right now are totally unrelated to absolute positioning. Since I agree with all the other changes you mentioned, though. I'll give this a full review soon, only had an hour to play around with it yesterday. |
|
Yeah, that's exactly what it does, the problem was only where to keep the number until then. By the time postlayout gets there, layout has already put the inset/margin position in But it's just two numbers and none of it had to stick around, so I pushed that: |
chearon
left a comment
There was a problem hiding this comment.
This implementation was written as the reference solution for a coding-challenge submission that was rejected for being too easy — three of four independent blind solvers reproduced it. That is a reason for confidence in the design rather than against it: four independent implementations converged on the same architecture. The code is unencumbered.
Can you elaborate on what you mean here? What is a "blind solver"?
This is good, but it's not the best solution. In particular, it walks the whole layout tree even if there aren't absolutes. It should only re-walk where necessary. It doesn't need to use extra memory for the static position, and it double-wraps position: absolute when it's among blocks. It leaves inline static positioning totally unsolved.
That said, If you're in it for the long haul, this is undoubtedly a great start.
Here's my reference implementation. It passes all of your tests, and isn't much more code. I'm not sure if I even removed unused functions.
all design changes except the more efficient box tree (patch is against ac41203)
diff --git a/src/api.ts b/src/api.ts
index 3eac20e..f8fbc14 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -2,14 +2,13 @@ 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, layoutAbsolutes} from './layout-flow.ts';
+import {generateBlockContainer, layoutBlockLevelBox} 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';
@@ -51,13 +50,9 @@ 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(), {staticPositions});
- layoutAbsolutes(layout, {staticPositions});
- postlayout(layout, staticPositions);
+ layoutBlockLevelBox(layout, layout.root(), {});
+ postlayout(layout);
}
/**
diff --git a/src/layout-box.ts b/src/layout-box.ts
index 1d53958..564c291 100644
--- a/src/layout-box.ts
+++ b/src/layout-box.ts
@@ -148,22 +148,23 @@ export abstract class Box extends TreeNode {
// block) because anonymously created boxes cannot invoke those modes.
isInline: 1 << 8,
isBfcRoot: 1 << 9,
- // 8..13: propagation bits: Inline <- Run
+ // 8..13: propagation bits: Inline+ <- Run
hasText: 1 << 8,
hasComplexText: 1 << 9,
hasSoftHyphen: 1 << 10,
hasNewlines: 1 << 11,
hasSoftWrap: 1 << 12,
hasWordSpacing: 1 << 13,
- // 14..15: propagation bits: Inline <- Inline
+ // 14..15: propagation bits: Inline+ <- Inline
hasPaintedInlines: 1 << 14,
hasSizedInline: 1 << 15,
- // 16: propagation bits: Inline <- Break, Inline, ReplacedBox
+ // 16: propagation bits: Inline+ <- Break, Inline, ReplacedBox
hasBreakInlineOrReplaced: 1 << 16,
- // 17..18: propagation bits: Inline <- FormattingBox
+ // 17..18: propagation bits: Inline+ <- FormattingBox
hasFloatOrReplaced: 1 << 17,
hasInlineBlocks: 1 << 18,
- // 19..31: if you take them, remove them from PROPAGATES_TO_INLINE_BITS
+ hasAbsoluteInCb: 1 << 19,
+ // 20..31: if you take them, remove them from PROPAGATES_TO_INLINE_BITS
};
/**
@@ -415,6 +416,10 @@ export abstract class Box extends TreeNode {
return Boolean(this.bitfield & Box.BITS.hasForegroundInDescendent);
}
+ hasAbsoluteInCb() {
+ return Boolean(this.bitfield & Box.BITS.hasAbsoluteInCb);
+ }
+
postlayoutPreorder(layout: Layout) {
// TODO: Inlines don't use this yet. Get rid of paragraph's backgroundBoxes
// and use normal inline areas instead, with fragmentation
@@ -568,11 +573,19 @@ export abstract class FormattingBox extends Box {
if (this.isFloat()) {
parent.bitfield |= Box.BITS.hasFloatOrReplaced;
}
+
+ if (this.isAbsolute() || this.hasAbsoluteInCb() && !this.isPositioned()) {
+ parent.bitfield |= Box.BITS.hasAbsoluteInCb;
+ }
}
isInlineLevel() {
return this.style.display.outer === 'inline';
}
+
+ isAbsoluteContainingBlock() {
+ return (this.isPositioned() || this.treeStart === 0) && this.hasAbsoluteInCb();
+ }
}
export class BoxArea {
@@ -610,6 +623,14 @@ export class BoxArea {
return this.box.style.direction;
}
+ getWritingModeAsParticipant() {
+ return this.parent ? this.parent.getEstablishedWritingMode() : 'horizontal-tb';
+ }
+
+ getDirectionAsParticipant() {
+ return this.parent ? this.parent.getEstablishedDirection() : 'ltr';
+ }
+
get x() {
return this.lineLeft;
}
@@ -634,6 +655,30 @@ export class BoxArea {
return this.blockSize;
}
+ getBlockEndInset() {
+ if (!this.parent) return 0;
+ if (
+ (this.getWritingModeAsParticipant() === 'horizontal-tb') !==
+ (this.parent.getWritingModeAsParticipant() === 'horizontal-tb')
+ ) {
+ return this.parent.inlineSize - this.blockStart - this.blockSize;
+ } else {
+ return this.parent.blockSize - this.blockStart - this.blockSize;
+ }
+ }
+
+ getLineRightInset() {
+ if (!this.parent) return 0;
+ if (
+ (this.getWritingModeAsParticipant() === 'horizontal-tb') !==
+ (this.parent.getWritingModeAsParticipant() === 'horizontal-tb')
+ ) {
+ return this.parent.blockSize - this.lineLeft - this.inlineSize;
+ } else {
+ return this.parent.inlineSize - this.lineLeft - this.inlineSize;
+ }
+ }
+
setParent(p: BoxArea) {
this.parent = p;
}
@@ -668,6 +713,41 @@ export class BoxArea {
}
}
+ getLineLeftOfAreaAgainstSelf(area: BoxArea) {
+ const thisCb = this.box.getContainingBlock();
+ const thisWm = this.box.getWritingModeAsParticipant(thisCb);
+ const areaCb = area.box.getContainingBlock();
+ const areaWm = area.box.getWritingModeAsParticipant(areaCb);
+
+ if (thisWm === 'horizontal-tb') {
+ if (areaWm === 'vertical-rl') return area.getBlockEndInset();
+ if (areaWm === 'vertical-lr') return area.blockStart;
+ } else { // 'vertical-rl', 'vertical-lr'
+ if (areaWm === 'horizontal-tb') return area.blockStart;
+ }
+
+ return area.lineLeft;
+ }
+
+ getBlockStartOfAreaAgainstSelf(area: BoxArea) {
+ const thisCb = this.box.getContainingBlock();
+ const thisWm = this.getEstablishedWritingMode();
+ const areaCb = area.box.getContainingBlock();
+ const areaWm = area.box.getWritingModeAsParticipant(areaCb);
+
+ if (thisWm === 'horizontal-tb') {
+ if (areaWm !== 'horizontal-tb') return area.lineLeft;
+ } else if (thisWm === 'vertical-rl') {
+ if (areaWm === 'horizontal-tb') return area.getLineRightInset();
+ if (areaWm === 'vertical-lr') return area.getBlockEndInset();
+ } else { // 'vertical-lr'
+ if (areaWm === 'horizontal-tb') return area.lineLeft;
+ if (areaWm === 'vertical-rl') return area.getBlockEndInset();
+ }
+
+ return area.blockStart;
+ }
+
absolutify() {
let x, y, width, height;
@@ -734,56 +814,6 @@ 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[] = [];
@@ -841,26 +871,11 @@ export function prelayout(layout: Layout, icb: BoxArea) {
}
}
-export function postlayout(layout: Layout, staticPositions: Map<Box, StaticPosition>) {
+export function postlayout(layout: Layout) {
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);
@@ -920,17 +935,6 @@ 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[];
diff --git a/src/layout-flow.ts b/src/layout-flow.ts
index 02af535..b9eedce 100644
--- a/src/layout-flow.ts
+++ b/src/layout-flow.ts
@@ -17,7 +17,7 @@ import {getImage} from './layout-image.ts';
import {Box, FormattingBox, TreeNode, Layout} from './layout-box.ts';
import type {InlineMetrics, ShapedItem, InlineFragment} from './layout-text.ts';
-import type {BoxArea, PrelayoutContext, StaticPosition} from './layout-box.ts';
+import type {BoxArea, PrelayoutContext} from './layout-box.ts';
import type {AllocatedUint16Array} from './text-harfbuzz.ts';
function assumePx(v: any): asserts v is number {
@@ -44,12 +44,6 @@ export interface LayoutContext {
* This is only undefined for the root box or when an element is out of flow.
*/
bfc?: BlockFormattingContext
- /**
- * Where the out-of-flow boxes seen so far would have been in flow, for the
- * ones that need it. Read by the absolute layout pass and by postlayout, then
- * dropped.
- */
- staticPositions: Map<Box, StaticPosition>
}
class MarginCollapseCollection {
@@ -117,7 +111,7 @@ export class BlockFormattingContext {
this.hypotheticals = EMPTY_MAP;
}
- collapseStart(layout: Layout, box: BlockLevel) {
+ collapseStart(layout: Layout, box: BlockLevel, ctx: LayoutContext) {
const containingBlock = box.getContainingBlock();
const marginBlockStart = box.style.getMarginBlockStart(containingBlock);
let floatBottom = 0;
@@ -143,7 +137,7 @@ export class BlockFormattingContext {
if (adjoinsPrevious) {
this.margin.collection.add(marginBlockStart);
} else {
- this.positionBlockContainers();
+ this.positionBlockContainers(layout, ctx);
const c = floatBottom - this.cbBlockStart;
this.margin = {level: this.level, collection: new MarginCollapseCollection(c)};
if (box.canCollapseThrough(layout)) this.margin.clearanceAtLevel = this.level;
@@ -157,7 +151,7 @@ export class BlockFormattingContext {
const borderBlockStartWidth = box.style.getBorderBlockStartWidth(containingBlock);
const adjoinsNext = paddingBlockStart === 0 && borderBlockStartWidth === 0;
- this.collapseStart(layout, box);
+ this.collapseStart(layout, box, ctx);
this.last = 'start';
this.level += 1;
@@ -178,12 +172,12 @@ export class BlockFormattingContext {
}
if (!adjoinsNext) {
- this.positionBlockContainers();
+ this.positionBlockContainers(layout, ctx);
this.margin = {level: this.level, collection: new MarginCollapseCollection()};
}
}
- boxEnd(layout: Layout, box: BlockContainer) {
+ boxEnd(layout: Layout, box: BlockContainer, ctx: LayoutContext) {
const containingBlock = box.getContainingBlock();
const {lineLeft, lineRight} = box.getContainingBlockToContent(containingBlock);
const paddingBlockEnd = box.style.getPaddingBlockEnd(containingBlock);
@@ -212,7 +206,7 @@ export class BlockFormattingContext {
this.cbLineRight -= lineRight;
if (!adjoins) {
- this.positionBlockContainers();
+ this.positionBlockContainers(layout, ctx);
this.margin = {level: this.level, collection: new MarginCollapseCollection()};
}
@@ -231,13 +225,13 @@ export class BlockFormattingContext {
this.last = 'end';
}
- boxAtomic(layout: Layout, box: BlockLevel) {
+ boxAtomic(layout: Layout, box: BlockLevel, ctx: LayoutContext) {
const containingBlock = box.getContainingBlock();
const marginBlockEnd = box.style.getMarginBlockEnd(containingBlock);
assumePx(marginBlockEnd);
- this.collapseStart(layout, box);
+ this.collapseStart(layout, box, ctx);
this.fctx?.boxStart();
- this.positionBlockContainers();
+ this.positionBlockContainers(layout, ctx);
box.setBlockPosition(this.cbBlockStart);
this.margin.collection = new MarginCollapseCollection();
this.margin.collection.add(marginBlockEnd);
@@ -270,14 +264,21 @@ export class BlockFormattingContext {
return this.fctx || (this.fctx = new FloatContext(this, blockOffset));
}
- finalize(box: BlockContainer) {
+ finalize(layout: Layout, box: BlockContainer, ctx: LayoutContext) {
if (!box.isBfcRoot()) throw new Error('This is for bfc roots only');
const containingBlock = box.getContainingBlock();
const blockSize = box.style.getBlockSize(containingBlock);
+ let insetBlockStart: number | 'auto' = 'auto';
+ let insetBlockEnd: number | 'auto' = 'auto';
- this.positionBlockContainers();
+ this.positionBlockContainers(layout, ctx);
- if (blockSize === 'auto') {
+ if (box.isAbsolute()) {
+ insetBlockStart = box.style.getInsetBlockStart(containingBlock);
+ insetBlockEnd = box.style.getInsetBlockEnd(containingBlock);
+ }
+
+ if (blockSize === 'auto' && !(insetBlockStart !== 'auto' && insetBlockEnd !== 'auto')) {
let lineboxHeight = 0;
if (box.isBlockContainerOfInlines()) {
lineboxHeight = box.getContentArea().blockSize;
@@ -285,9 +286,11 @@ export class BlockFormattingContext {
const blockSize = Math.max(lineboxHeight, this.cbBlockStart, this.fctx?.getBothBottom() ?? 0);
box.setBlockSize(containingBlock, blockSize);
}
+
+ finalizeBlockContainer(layout, box, ctx);
}
- positionBlockContainers() {
+ positionBlockContainers(layout: Layout, ctx: LayoutContext) {
const sizeStack = this.sizeStack;
const offsetStack = this.offsetStack;
const margin = this.margin.collection.get();
@@ -311,6 +314,8 @@ export class BlockFormattingContext {
box.setBlockSize(containingBlock, childSize);
}
+ if (!box.isBfcRoot()) finalizeBlockContainer(layout, box, ctx);
+
const blockSize = box.getBorderArea().blockSize;
sizeStack[level] += blockSize;
@@ -960,7 +965,8 @@ export class BlockContainerOfInlines extends BlockContainerBase {
return inline.hasText()
|| inline.hasSizedInline()
|| inline.hasFloatOrReplaced()
- || inline.hasInlineBlocks();
+ || inline.hasInlineBlocks()
+ || inline.hasAbsoluteInCb();
}
doTextLayout(layout: Layout, ctx: LayoutContext) {
@@ -1072,28 +1078,6 @@ function doBlockBoxModelForBlockBox(layout: Layout, box: BlockContainer) {
}
}
-/**
- * An inline formatting context with nothing to lay out still has to answer for
- * the absolutely positioned boxes inside it, which would have started at its
- * content edge (CSS 2.2 § 10.3.7, § 10.6.4).
- */
-function setStaticPositionsWithoutLines(
- layout: Layout,
- box: BlockContainerOfInlines,
- ctx: LayoutContext
-) {
- const contentArea = box.getContentArea();
- const ltr = box.style.direction === 'ltr';
-
- for (let i = box.treeStart + 1; i <= box.treeFinal; i++) {
- const item = layout.tree[i];
- if (item.isFormattingBox() && item.isAbsolute()) {
- ctx.staticPositions.set(item, [0, ltr ? 0 : contentArea.inlineSize]);
- i = item.treeFinal;
- }
- }
-}
-
function layoutBlockBoxInner(
layout: Layout,
box: BlockContainer,
@@ -1113,11 +1097,7 @@ function layoutBlockBoxInner(
// Child flow is now possible
if (box.isBlockContainerOfInlines()) {
- if (!box.shouldLayoutContent(layout)) {
- // No lines will be built, so the boxes taken out of this flow would all
- // have started at the content edge
- setStaticPositionsWithoutLines(layout, box, cctx);
- } else if (containingBfc) {
+ if (containingBfc) {
// text layout happens in bfc.boxStart
} else {
box.doTextLayout(layout, cctx);
@@ -1132,7 +1112,7 @@ function layoutBlockBoxInner(
}
if (establishedBfc) {
- establishedBfc.finalize(box);
+ establishedBfc.finalize(layout, box, ctx);
if (establishedBfc.fctx) {
if (box.loggingEnabled()) {
console.log('Left floats');
@@ -1144,7 +1124,7 @@ function layoutBlockBoxInner(
}
}
- containingBfc?.boxEnd(layout, box);
+ containingBfc?.boxEnd(layout, box, ctx);
}
function layoutBlockBox(
@@ -1168,7 +1148,7 @@ function layoutReplacedBox(
box.fillAreas(containingBlock);
doInlineBoxModelForBlockBox(box);
box.setBlockSize(containingBlock, box.getDefiniteInnerBlockSize());
- ctx.bfc!.boxAtomic(layout, box);
+ ctx.bfc!.boxAtomic(layout, box, ctx);
}
export function layoutBlockLevelBox(
@@ -1270,22 +1250,6 @@ export function layoutFloatBox(
}
}
-/**
- * Absolutely positioned boxes are laid out after the flow they were taken out
- * of, because their containing block - the padding area of the nearest
- * positioned ancestor, or the initial containing block - only has a size once
- * that ancestor is done. Tree order means an ancestor is always resolved before
- * a box it contains.
- */
-export function layoutAbsolutes(layout: Layout, ctx: LayoutContext) {
- for (let i = 1; i < layout.tree.length; i++) {
- const item = layout.tree[i];
- if (item.isFormattingBox() && item.isAbsolute()) {
- layoutAbsoluteBox(layout, item, ctx);
- }
- }
-}
-
function getShrinkToFitInlineSize(
layout: Layout,
box: BlockLevel,
@@ -1299,8 +1263,8 @@ function getShrinkToFitInlineSize(
// § 10.3.7
function doInlineBoxModelForAbsoluteBox(
layout: Layout,
- box: BlockLevel,
- staticPosition: StaticPosition | undefined
+ staticContainingBlock: BoxArea | null,
+ box: BlockLevel
) {
const containingBlock = box.getContainingBlock();
const cInlineSize = containingBlock.inlineSizeForPotentiallyOrthogonal(box);
@@ -1314,7 +1278,12 @@ function doInlineBoxModelForAbsoluteBox(
let marginLineRight = styleMarginLineRight === 'auto' ? 0 : styleMarginLineRight;
let sizedFromInsets = false;
let inlineSize;
+ let lineLeft = insetLineLeft === 'auto' ? 0 : insetLineLeft;
+ // solve:
+ // left = px , width = auto, right = px
+ // left = px , width = auto, right = auto
+ // left = px , width = px , right = auto
if (definiteInlineSize !== undefined) {
inlineSize = definiteInlineSize;
} else if (box.isReplacedBox()) {
@@ -1338,6 +1307,8 @@ function doInlineBoxModelForAbsoluteBox(
- marginLineRight;
}
+ // solve:
+ // left = px , width = px , right = px
if (!sizedFromInsets && insetLineLeft !== 'auto' && insetLineRight !== 'auto') {
// Paragraph 2: the equation is solvable, so what is left over goes to the
// margins that are auto
@@ -1356,44 +1327,54 @@ function doInlineBoxModelForAbsoluteBox(
} else if (styleMarginLineRight === 'auto') {
marginLineRight = rest - marginLineLeft;
}
+
// Otherwise the values are over-constrained. The line-right inset is the
// one ignored in ltr and the line-left one in rtl, which is what falls out
// of positioning against the retained side below
+ if (styleMarginLineLeft !== 'auto' && styleMarginLineRight !== 'auto' && !ltr) {
+ lineLeft = cInlineSize - insetLineRight - marginLineRight - inlineSize;
+ } else {
+ lineLeft += marginLineLeft;
+ }
}
- box.setInlineOuterSize(containingBlock, inlineSize);
+ // solve:
+ // left = auto, width = auto, right = auto
+ // left = auto, width = px , right = auto
+ if (insetLineLeft === 'auto' && insetLineRight === 'auto') {
+ lineLeft = box.getBorderArea().lineLeft; // against static cb
+ let area = staticContainingBlock;
+ while (area && area !== containingBlock) {
+ lineLeft += containingBlock.getLineLeftOfAreaAgainstSelf(area);
+ area = area.parent;
+ }
+ if (!ltr) lineLeft -= inlineSize;
+ }
- if (insetLineLeft !== 'auto' && (insetLineRight === 'auto' || ltr)) {
- box.setInlinePosition(insetLineLeft + marginLineLeft);
- } else if (insetLineRight !== 'auto') {
- box.setInlinePosition(cInlineSize - insetLineRight - marginLineRight - inlineSize);
- } else if (staticPosition) {
- // Paragraphs 1 and 4: both insets are auto, so the box stays where it would
- // have been. In rtl it is the line-right margin edge that was recorded
- box.setInlinePosition(ltr ? marginLineLeft : -(inlineSize + marginLineRight));
- } else {
- box.setInlinePosition(marginLineLeft);
+ // solve:
+ // left = auto, width = auto, right = px
+ // left = auto, width = px , right = px
+ if (insetLineLeft === 'auto' && insetLineRight !== 'auto') {
+ lineLeft = cInlineSize - insetLineRight - marginLineRight - inlineSize;
}
-}
-// § 10.6.4
-interface AbsoluteBlockAxis {
- usesContentBlockSize: boolean;
- blockSize: number | undefined;
- insetBlockStart: number | 'auto';
- insetBlockEnd: number | 'auto';
- cBlockSize: number;
+ box.setInlineOuterSize(containingBlock, inlineSize);
+ box.setInlinePosition(lineLeft);
}
-function doBlockBoxModelForAbsoluteBox(box: BlockLevel): AbsoluteBlockAxis {
+// § 10.6.4
+function doBlockBoxModelForAbsoluteBox(
+ staticContainingBlock: BoxArea | null,
+ box: BlockLevel
+) {
const containingBlock = box.getContainingBlock();
+ const borderArea = box.getBorderArea();
const cBlockSize = containingBlock.blockSizeForPotentiallyOrthogonal(box);
const insetBlockStart = box.style.getInsetBlockStart(containingBlock);
const insetBlockEnd = box.style.getInsetBlockEnd(containingBlock);
- const marginBlockStart = box.style.getMarginBlockStart(containingBlock);
- const marginBlockEnd = box.style.getMarginBlockEnd(containingBlock);
+ let marginBlockStart = box.style.getMarginBlockStart(containingBlock);
+ let marginBlockEnd = box.style.getMarginBlockEnd(containingBlock);
let blockSize = box.getDefiniteInnerBlockSize(containingBlock);
- let usesContentBlockSize = blockSize === undefined;
if (
blockSize === undefined &&
@@ -1415,69 +1396,100 @@ function doBlockBoxModelForAbsoluteBox(box: BlockLevel): AbsoluteBlockAxis {
- paddingBlockStart
- paddingBlockEnd
- borderBlockEndWidth);
- usesContentBlockSize = false;
}
- if (blockSize !== undefined) box.setBlockSize(containingBlock, blockSize);
-
- return {usesContentBlockSize, blockSize, insetBlockStart, insetBlockEnd, cBlockSize};
-}
+ if (blockSize !== undefined) {
+ box.setBlockSize(containingBlock, blockSize);
+ }
-function setBlockPositionForAbsoluteBox(box: BlockLevel, axis: AbsoluteBlockAxis) {
- const containingBlock = box.getContainingBlock();
- const {insetBlockStart, insetBlockEnd, cBlockSize} = axis;
- const outerBlockSize = box.getBorderArea().blockSize;
- const styleMarginBlockStart = box.style.getMarginBlockStart(containingBlock);
- const styleMarginBlockEnd = box.style.getMarginBlockEnd(containingBlock);
- let marginBlockStart = styleMarginBlockStart === 'auto' ? 0 : styleMarginBlockStart;
- let marginBlockEnd = styleMarginBlockEnd === 'auto' ? 0 : styleMarginBlockEnd;
+ const outerBlockSize = borderArea.blockSize;
+ let usedMarginBlockStart = marginBlockStart === 'auto' ? 0 : marginBlockStart;
+ let usedMarginBlockEnd = marginBlockEnd === 'auto' ? 0 : marginBlockEnd;
if (insetBlockStart !== 'auto' && insetBlockEnd !== 'auto') {
const rest = cBlockSize - insetBlockStart - insetBlockEnd - outerBlockSize;
- if (styleMarginBlockStart === 'auto' && styleMarginBlockEnd === 'auto') {
+ if (marginBlockStart === 'auto' && marginBlockEnd === 'auto') {
// Paragraph 6: equal margins center the box, negative values included
- marginBlockStart = marginBlockEnd = rest / 2;
- } else if (styleMarginBlockStart === 'auto') {
- marginBlockStart = rest - marginBlockEnd;
- } else if (styleMarginBlockEnd === 'auto') {
- marginBlockEnd = rest - marginBlockStart;
+ usedMarginBlockStart = marginBlockEnd = rest / 2;
+ } else if (marginBlockStart === 'auto') {
+ usedMarginBlockStart = rest - usedMarginBlockEnd;
+ } else if (marginBlockEnd === 'auto') {
+ usedMarginBlockEnd = rest - marginBlockStart;
}
// Otherwise over-constrained, and the block-end inset is the one ignored
}
if (insetBlockStart !== 'auto') {
- box.setBlockPosition(insetBlockStart + marginBlockStart);
+ box.setBlockPosition(insetBlockStart + usedMarginBlockStart);
} else if (insetBlockEnd !== 'auto') {
- box.setBlockPosition(cBlockSize - insetBlockEnd - marginBlockEnd - outerBlockSize);
+ box.setBlockPosition(cBlockSize - insetBlockEnd - usedMarginBlockEnd - outerBlockSize);
} else {
- // Both insets are auto, so the box stays where it would have been. Postlayout
- // shifts it there once the in-flow parent has absolute coordinates
- box.setBlockPosition(marginBlockStart);
+ let blockStart = borderArea.blockStart + usedMarginBlockStart;
+ let area = staticContainingBlock;
+ while (area && area !== containingBlock) {
+ blockStart += containingBlock.getBlockStartOfAreaAgainstSelf(area);
+ area = area.parent;
+ }
+ box.setBlockPosition(blockStart);
}
}
-function layoutAbsoluteBox(layout: Layout, box: BlockLevel, ctx: LayoutContext) {
+function layoutAbsoluteBox(
+ layout: Layout,
+ staticContainingBlock: BoxArea | null,
+ box: BlockLevel,
+ ctx: LayoutContext
+) {
const cctx: LayoutContext = {...ctx, bfc: undefined};
- const containingBlock = box.getContainingBlock();
- const staticPosition = ctx.staticPositions.get(box);
- box.fillAreas(containingBlock);
- doInlineBoxModelForAbsoluteBox(layout, box, staticPosition);
- const axis = doBlockBoxModelForAbsoluteBox(box);
+ // NB: fillAreas was called in layoutStaticBox
+ doInlineBoxModelForAbsoluteBox(layout, staticContainingBlock, box);
+ doBlockBoxModelForAbsoluteBox(staticContainingBlock, box);
if (box.isBlockContainer()) {
layoutBlockBoxInner(layout, box, cctx);
- // The formatting context sizes an auto block size from the content, which is
- // only the used value when an inset on that axis is auto
- if (!axis.usesContentBlockSize && axis.blockSize !== undefined) {
- box.setBlockSize(containingBlock, axis.blockSize);
+ } else {
+ // replaced boxes have no layout. they were sized by doInline/Block above
+ }
+}
+
+function finalizeBlockContainer(
+ layout: Layout,
+ box: BlockContainer,
+ ctx: LayoutContext
+) {
+ if ((box.isPositioned() || box.treeStart === 0) && box.hasAbsoluteInCb()) {
+ const staticParents: FormattingBox[] = [];
+
+ for (let i = box.treeStart + 1; i <= box.treeFinal; i++) {
+ const box = layout.tree[i];
+
+ if (box.isFormattingBox()) {
+ if (!box.isPositioned()) staticParents.push(box);
+ if (box.isAbsolute()) {
+ let staticContainingBlock = null;
+ if (staticParents.length) {
+ const box = staticParents[staticParents.length - 1];
+ staticContainingBlock = box.getContentArea();
+ }
+ layoutAbsoluteBox(layout, staticContainingBlock, box, ctx);
+ }
+ }
+
+ while (
+ staticParents.length &&
+ i === staticParents[staticParents.length - 1].treeFinal
+ ) staticParents.pop();
+
+ if (box.isBox() && box.isPositioned()) i = box.treeFinal;
}
- } else if (axis.usesContentBlockSize) {
- box.setBlockSize(containingBlock, box.getDefiniteInnerBlockSize());
}
+}
- setBlockPositionForAbsoluteBox(box, axis);
+export function layoutStaticBox(box: FormattingBox) {
+ const containingBlock = box.getContainingBlock();
+ box.fillAreas(containingBlock);
}
export class Break extends TreeNode {
@@ -1537,6 +1549,8 @@ export class Inline extends Box {
// Bits that propagate to Inline propagate again if the parent is Inline
parent.bitfield |= (this.bitfield & Box.PROPAGATES_TO_INLINE_BITS);
+ } else if (parent.isFormattingBox()) {
+ if (this.hasAbsoluteInCb()) parent.bitfield |= Box.BITS.hasAbsoluteInCb;
}
}
@@ -1802,7 +1816,7 @@ export function inlineIteratorStateNext(state: InlineIteratorState) {
} else if (item.isBreak()) {
state.buffered.push({state: 'break', index: state.index});
} else {
- if (item.isFloat()) {
+ if (item.isOutOfFlow()) {
state.buffered.push({state: 'box', item});
} else {
state.buffered.push({state: 'box', item});
diff --git a/src/layout-text.ts b/src/layout-text.ts
index cf9be11..6731380 100644
--- a/src/layout-text.ts
+++ b/src/layout-text.ts
@@ -6,6 +6,7 @@ import {
IfcVacancy,
Inline,
layoutFloatBox,
+ layoutStaticBox,
layoutContribution,
createInlineIteratorState,
inlineIteratorStateNext,
@@ -2926,11 +2927,11 @@ export function createIfcLineboxes(
// line is where it would have been if it were in flow, which is the
// position it uses when its insets are auto (CSS 2.2 § 10.3.7, § 10.6.4)
const contentArea = ifc.block.getContentArea();
+ const {lineLeft, lineRight} = mark.box.getMarginsAutoIsZero(containingBlock);
const ltr = ifc.block.style.direction === 'ltr';
- ctx.staticPositions.set(mark.box, [
- ifc.vacancy.blockOffset,
- ltr ? 0 : contentArea.inlineSize
- ]);
+ layoutStaticBox(mark.box);
+ mark.box.setBlockPosition(ifc.vacancy.blockOffset);
+ mark.box.setInlinePosition(ltr ? lineLeft : contentArea.inlineSize - lineRight);
}
if (mark.inlinePost) {| * that ancestor is done. Tree order means an ancestor is always resolved before | ||
| * a box it contains. | ||
| */ | ||
| export function layoutAbsolutes(layout: Layout, ctx: LayoutContext) { |
There was a problem hiding this comment.
Walking the whole layout tree all over again isn't acceptable, especially because there may not even be any absolutes. If there are, they can be laid out when their containing block is done being sized. That way, we're only re-walking a subset of the tree, which is important for performance (cache locality).
The way this should be done is by adding a new propagation bit: hasAbsoluteInCb. It propagates from an absolute and goes no further than a positioned box or the root box. When we finish sizing a positioned box or the root box, we can check if it has the bit set and if so, only walk descendants and without crossing into other positioned descendants.
There was a problem hiding this comment.
Done, with your bit. layoutAbsolutes is gone. hasAbsoluteInCb propagates from an absolute and stops at a positioned box or the root, finalizeBlockContainer runs off isAbsoluteContainingBlock(), and the walk jumps to treeFinal at any positioned descendant so it never crosses into another containing block.
One reorder on top of your version: the loop that pops static parents ran before the i = item.treeFinal skip, so a static parent whose treeFinal coincided with that of a positioned descendant stayed on the stack and became the static parent of the next absolute. It is masked as long as every absolute among blocks gets an anonymous IFC wrapper, which is why the suite didn't see it; once the wrapper goes away (your last comment) it reproduces — <div rel><div style="padding:5px"><div rel>x</div></div><div abs></div></div> put the box at x: 5 instead of 0. Test: takes a static position from a static parent that already ended.
|
|
||
| box.fillAreas(containingBlock); | ||
| doInlineBoxModelForAbsoluteBox(layout, box, staticPosition); | ||
| const axis = doBlockBoxModelForAbsoluteBox(box); |
There was a problem hiding this comment.
I don't see any reason why this object has to be created and returned. Can you merge the setBlockSize parts below and everything setBlockPositionForAbsoluteBox does into doBlockBoxModelForAbsoluteBox? That would make it mirror the other layout functions like layoutFloatBox
There was a problem hiding this comment.
Merged. doBlockBoxModelForAbsoluteBox sizes and positions and returns nothing, like layoutFloatBox. AbsoluteBlockAxis and setBlockPositionForAbsoluteBox are gone.
| ifc.vacancy.blockOffset, | ||
| ltr ? 0 : contentArea.inlineSize | ||
| ]); | ||
| } |
There was a problem hiding this comment.
This isn't setting the position correctly for inline absolutes. Here's a test case that fails on this PR:
hello <span style="position: absolute;">__</span>is there anybody out there?The __ should appear underneath "is".
There was a problem hiding this comment.
Fixed __ sits under is now.
Instead of accumulating the width by hand, the box goes on the line as a zero-advance item (addBox(treeStart, treeFinal, 0)) and positionPhysicalLineItems stops skipping it. That way line.inlineOffset (floats and text-align), the bidi reorder, and the case where the box ends up on a later line all come for free, and nothing is stored anywhere.
Two things fell out of it:
- An IFC whose only content is an absolute never finished a line, so nothing positioned the box. It finishes one now, but a line built only for out-of-flow boxes is not a line box, so
ifc.blockOffsetis restored afterwards and it adds no block size. That is what took the place ofsetStaticPositionsWithoutLines. - rtl with inline content before the box I could not pin down, so there is no test for it. An in-flow zero-width reference doesn't help:
aaa<span style="display:inline-block;width:10px">in an rtl block puts the inline-block at the line-right edge, to the right ofaaa, which is not where I'd expect it after a strong-LTR run in an RTL paragraph. I didn't want to freeze my guess into a test on top of that. In ltr the absolute matches such a reference exactly. Is the in-flow rtl placement what you expect there?
| * `Layout`: it rides along on the layout context and is gone once `reflow` | ||
| * returns. | ||
| */ | ||
| export type StaticPosition = [blockOffset: number, inlineOffset: number]; |
There was a problem hiding this comment.
We don't need to allocate extra memory for this. The absolute box can be positioned using the normal lineLeft and blockStart offsets of its principal box when the parent layout visits it.
When the absolute is actually laid out, those offsets can be interpreted as relative to the static containing block. The hard part is that they have to get converted from the writing mode and direction of the static containing block to the real containing block. Similar to but different from shiftToStaticPosition above.
There was a problem hiding this comment.
Done: no map, no StaticPosition, postlayout back to one argument and reflow allocating nothing. The box records the static position in the lineLeft/blockStart of its own border area and layout maps those two numbers from the static containing block's axes into the real one's.
Two fixes inside that mapping:
getLineLeftOfAreaAgainstSelfused the writing mode the containing block participates in whilegetBlockStartOfAreaAgainstSelfuses the one it establishes. Withposition: relative; writing-mode: vertical-rlinside a horizontal root and all four insets auto, the box landed aty: 180inside a 150px tall containing block. Established is the right one on both axes. Test:maps a fully auto static position through a vertical-rl containing block— my earlier vertical-rl case hadtop: 6px, so that axis was never auto, which is why it went unnoticed.doInlineBoxModelForAbsoluteBoxdroppedmarginLineLefteverywhere except the over-constrained arm, soleft: 10px; margin-left: 5pxpositioned the box at 10, and so didleft: 10px; right: 30px; margin-left: 5px(which subtracted the margin from the size but not from the position). The solve arms are one chain now, with the over-constrained rtl case as its own branch.
| i = item.treeFinal; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This function isn't necessary if you adjust shouldLayoutContent to account for "has absolutes". That's how we should do it since that's how floats are treated.
There was a problem hiding this comment.
Done: hasAbsoluteInCb() sits next to hasFloatOrReplaced() in shouldLayoutContent, and setStaticPositionsWithoutLines is gone.
One wrinkle, which I've also noted on the layout-text.ts thread: an IFC whose only content is an absolute never finished a line, so nothing positioned the box. It finishes one now, and since a line built only for out-of-flow boxes is not a line box, ifc.blockOffset is restored afterwards so it adds no block size.
| preBcInlineChild(tree, ctx); | ||
| tree.push(new Break(childEl.style)); | ||
| } else if (childEl.style.display.outer === 'block') { | ||
| if (childEl.style.isOutOfFlow()) { |
There was a problem hiding this comment.
I don't know if this is the right path for abspos because it generates kind of a messy box tree:
<div style="position:absolute;">:D</div>Produces:
◼︎ Block 0
▭ Inline 1
◼︎ Block 2
▭ Inline 3
Ͳ 0,2 ":D"
But it could just be:
◼︎ Block 0
◼︎ Block 2
▭ Inline 3
Ͳ 0,2 ":D"
In other words, shouldn't an abspos among block-level content be positioned by the BFC, not by an IFC? It will be more code, and the BFC will have to handle abspos specially, for example, by never collapsing its margins with any in the BFC.
There was a problem hiding this comment.
Agreed, and done. <div style="position:absolute">:D</div> is Block > Inline > ":D" under the root now, with no wrapper.
Generation: an absolute block-level child joins whichever formatting context is already open preBcInlineChild only when an IFC is, preBcBlockChild otherwise. Floats are untouched.
BFC: block positions are deferred, so cbBlockStart is stale at the child, and calling positionBlockContainers() there would consume the pending margin and break collapsing across the box. So boxOutOfFlow pushes an {abs} marker onto this.stack, and positionBlockContainers gives it the offset a box start would get without pushing to sizeStack/offsetStack and without adding its size: it takes no space and collapses with nothing. Test: siblings with margin-bottom: 20px and margin-top: 30px around it still collapse to 30.
One corner I'd like your call on: with margin: 100px on the absolute between those two siblings, the static position lands after the whole collapsed 30, so y: 140. Placing it after only the first sibling's own 20 would give 130. Both readings fit "its margins never collapse with any in the BFC"; I took the collapsed value because that is what the margin collection holds when the marker is resolved.
1b75136 to
98b240d
Compare
|
Sorry, that paragraph was written for a different audience and does not apply to the description of a code challenge. By “blind solver,” I meant an independent attempt to solve the same problem using only the problem statement and this repository; the goal is for students to be able to research and solve the problem on their own. |
|
I hope you don't mind if I use your repository for a collaboration with my university for a contest. I thought it would be a good idea to collaborate by presenting it, since it was a real-world implementation. |
Closes #6.
The support matrix listed
position: absoluteas planned, and #6 notes that the blocker was containingblocks being assigned during layout rather than before it. That blocker is gone since "assign the containing
blocks in the prelayout step", so this fills in the rest.
What it does
Absolutely positioned boxes are laid out against the padding box of their nearest positioned ancestor, or
the initial containing block when there is none, and they leave normal flow: they do not contribute to their
parent's block size, do not appear on a line, do not add to a line's width and do not interact with floats.
They establish a block formatting context of their own, so floats inside them are contained, and a
positioned inline is blockified the way a float is.
Sizing and placement follow CSS 2.1 §10.3.7 and §10.6.4: a size from a pair of insets, shrink-to-fit for an
auto inline size, auto-margin centering, a single auto margin taking the remainder, and the over-constrained
rules — the line-right inset is dropped in ltr, the line-left one in rtl, and the block-end one always.
Percentages in insets and sizes resolve against the containing block's padding box. When both insets on an
axis are auto the box stays at its static position, taken as the start of the line box it would have
appeared on.
The part worth reviewing closely
Layout here is logical while
top/right/bottom/leftare physical, so the insets are mapped through thecontaining block's writing mode exactly the way margins, padding and borders already are — the logical maps
gain four inset entries per mode, with
insetBlockStartbeingtopinhorizontal-tb,leftinvertical-lrandrightinvertical-rl. The static position is resolved during postlayout, when everyancestor area is already physical, so a chain that crosses writing modes costs nothing; resolving it during
layout by summing logical offsets is wrong as soon as an orthogonal block intervenes.
One thing that is easy to miss: the mark iterator only consumed a box mark when it was a float or
inline-level, so making a positioned box out-of-flow without touching it turns line building into an
infinite loop.
Deliberately out of scope
position: fixed(still listed as planned); a relatively positioned inline as a containing block, since onlyblock containers establish one today; and the static position accounting for content earlier on the line —
the start of the line box is used instead, which is enough for the placements the tests cover.
Tests
test/position-absolute.spec.js, wired intotest/ci.js: 27 cases over containing-block resolution,out-of-flow behaviour, sizing from insets, shrink-to-fit, auto margins, the over-constrained rules,
percentages against the padding box, static positions including rtl and vertical writing modes, replaced
boxes, formatting-context containment, blockification and paint order.
npm testis 470 passing andnpm run tscis clean.Provenance
This implementation was written as the reference solution for a coding-challenge submission that was
rejected for being too easy — three of four independent blind solvers reproduced it. That is a reason for
confidence in the design rather than against it: four independent implementations converged on the same
architecture. The code is unencumbered.
Two pre-existing bugs surfaced while those implementations were being graded. Both reproduce on unmodified
masterand I have kept them out of this PR; I will file them separately:treeIndex === 0as a sentinel, which resolves to the root box, sopositionPhysicalLineItemsmoves the whole root. Repro:<span><div style="float:left"></div></span>.<span>with a line-right gap throwsAssertion failedinLineCandidates.inlinePost. Repro:<span style="padding:4px">ab<div style="float:left"></div></span>.