about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2025-02-16 19:36:05 +0100
committerbptato <nincsnevem662@gmail.com>2025-02-16 20:01:30 +0100
commitd6eb9be13115582182c4a0980e6be28c63ce3503 (patch)
treea1620e52078aa30ce20626b8037e94bf5016cab1 /src
parent5f4d7d67b6c13ae7633dede4216c958e30751050 (diff)
downloadchawan-d6eb9be13115582182c4a0980e6be28c63ce3503.tar.gz
layout: position absolute boxes relative to their parent
Also fixes an invisible bug where inline-block child absolutes were
queued multiple times.

This adds a pointer to the parent box for CSSBox objects, which isn't
great, but the alternatives (maintaining an explicit stack or adding
another tree traversal) were overly complex and/or too inefficient.

On the flip side, now it should be possible to do both stacking contexts
(with negative z-index) and overflow tracking in layout. (I think.)
Diffstat (limited to 'src')
-rw-r--r--src/css/box.nim1
-rw-r--r--src/css/csstree.nim8
-rw-r--r--src/css/layout.nim98
-rw-r--r--src/css/render.nim31
4 files changed, 71 insertions, 67 deletions
diff --git a/src/css/box.nim b/src/css/box.nim
index f177078e..3d237e1a 100644
--- a/src/css/box.nim
+++ b/src/css/box.nim
@@ -78,6 +78,7 @@ type
     bounds*: Bounds
 
   CSSBox* = ref object of RootObj
+    parent* {.cursor.}: CSSBox
     render*: BoxRenderState # render output
     computed*: CSSValues
     element*: Element
diff --git a/src/css/csstree.nim b/src/css/csstree.nim
index ad7bb1a3..0de914c9 100644
--- a/src/css/csstree.nim
+++ b/src/css/csstree.nim
@@ -481,13 +481,17 @@ proc buildBox(ctx: var TreeContext; frame: TreeFrame; cached: CSSBox): CSSBox =
   else:
     BlockBox(computed: frame.computed, element: frame.parent)
   for child in frame.children:
-    box.children.add(ctx.build(nil, child))
+    let childBox = ctx.build(nil, child)
+    childBox.parent = box
+    box.children.add(childBox)
   if display in DisplayInlineBlockLike:
-    return InlineBlockBox(
+    let wrapper = InlineBlockBox(
       computed: ctx.rootProperties,
       element: frame.parent,
       children: @[box]
     )
+    box.parent = wrapper
+    return wrapper
   return box
 
 proc applyCounters(ctx: var TreeContext; styledNode: StyledNode) =
diff --git a/src/css/layout.nim b/src/css/layout.nim
index c2facdd3..5366c5e2 100644
--- a/src/css/layout.nim
+++ b/src/css/layout.nim
@@ -12,33 +12,22 @@ import utils/twtstr
 import utils/widthconv
 
 type
-  # position: absolute is annoying in that its layout depends on its
-  # containing box's size, which of course is rarely its parent box.
-  # e.g. in
-  # <div style="position: relative; display: inline-block">
-  #   <div>
-  #     <div style="position: absolute; width: 100%; background: red">
-  #     blah
-  #     </div>
-  #   <div>
-  # blah blah
-  # </div>
-  # the width of the absolute box is the same as "blah blah", but we
-  # only know that after the outermost box has been layouted.
+  # position: absolute is annoying in that its position depends on its
+  # containing box's size and position, which of course is rarely its
+  # parent box.
   #
-  # So we must delay this layout until before the outermost box is
+  # So we must delay its positioning until before the outermost box is
   # popped off the stack, and we do this by queuing up absolute boxes in
   # the initial pass.
   #
-  #TODO: welp, turns out this is also true without position: absolute,
-  # so I think we could skip this entirely...  especially now that
-  # we can cache sub-layouts.
+  # (Technically, its layout could be done earlier, but we aren't sure
+  # of the parent's size before its layout is finished, so this way we
+  # can avoid pointless sub-layout passes.)
   QueuedAbsolute = object
     offset: Offset
     child: BlockBox
 
-  PositionedItem = ref object
-    queue: seq[QueuedAbsolute]
+  PositionedItem = seq[QueuedAbsolute]
 
   LayoutContext = ref object
     attrsp: ptr WindowAttributes
@@ -1401,12 +1390,38 @@ proc applyIntr(box: BlockBox; sizes: ResolvedSizes; intr: Size) =
       box.state.size[dim] = max(box.state.size[dim], intr[dim])
 
 proc pushPositioned(lctx: LayoutContext) =
-  lctx.positioned.add(PositionedItem())
+  lctx.positioned.add(@[])
+
+# Offset the child by the offset of its actual parent to its nearest
+# positioned ancestor (`parent').
+# This is only necessary (for the respective axes) if either top,
+# bottom, left or right is specified.
+proc realignAbsolutePosition(child: BlockBox; parent: CSSBox;
+    dims: set[DimensionType]) =
+  var it {.cursor.} = child.parent
+  while it != parent:
+    let offset = if it of BlockBox:
+      BlockBox(it).state.offset
+    else:
+      InlineBox(it).state.startOffset
+    if dtHorizontal in dims:
+      child.state.offset.x -= offset.x
+    if dtVertical in dims:
+      child.state.offset.y -= offset.y
+    it = it.parent
+  if parent of InlineBox:
+    # The renderer does not adjust position for inline parents, so we
+    # must do it here.
+    let offset = InlineBox(parent).state.startOffset
+    if dtHorizontal in dims:
+      child.state.offset.x += offset.x
+    if dtVertical in dims:
+      child.state.offset.y += offset.y
 
 # size is the parent's size
-proc popPositioned(lctx: LayoutContext; size: Size) =
-  let item = lctx.positioned.pop()
-  for it in item.queue:
+proc popPositioned(lctx: LayoutContext; parent: CSSBox; size: Size;
+    skipStatic = true) =
+  for it in lctx.positioned.pop():
     let child = it.child
     var size = size
     #TODO this is very ugly.
@@ -1426,26 +1441,33 @@ proc popPositioned(lctx: LayoutContext; size: Size) =
       sizes.space.w = stretch(child.state.intr.w)
       lctx.layoutRootBlock(child, offset, sizes)
       #TODO what happens with marginBottom?
+    var dims: set[DimensionType] = {}
     if child.computed{"left"}.u != clAuto:
       child.state.offset.x = positioned.left + sizes.margin.left
+      dims.incl(dtHorizontal)
     elif child.computed{"right"}.u != clAuto:
       child.state.offset.x = size.w - positioned.right - child.state.size.w -
         sizes.margin.right
+      dims.incl(dtHorizontal)
     # margin.left is added in layoutRootBlock
     if child.computed{"top"}.u != clAuto:
       child.state.offset.y = positioned.top + sizes.margin.top
+      dims.incl(dtVertical)
     elif child.computed{"bottom"}.u != clAuto:
       child.state.offset.y = size.h - positioned.bottom - child.state.size.h -
         sizes.margin.bottom
+      dims.incl(dtVertical)
     else:
       child.state.offset.y += sizes.margin.top
+    if dims != {}:
+      child.realignAbsolutePosition(parent, dims)
 
 proc queueAbsolute(lctx: LayoutContext; box: BlockBox; offset: Offset) =
   case box.computed{"position"}
   of PositionAbsolute:
-    lctx.positioned[^1].queue.add(QueuedAbsolute(child: box, offset: offset))
+    lctx.positioned[^1].add(QueuedAbsolute(child: box, offset: offset))
   of PositionFixed:
-    lctx.positioned[0].queue.add(QueuedAbsolute(child: box, offset: offset))
+    lctx.positioned[0].add(QueuedAbsolute(child: box, offset: offset))
   else: assert false
 
 proc positionRelative(lctx: LayoutContext; space: AvailableSpace;
@@ -1623,6 +1645,9 @@ proc layoutOuterBlock(fstate: var FlowState; child: BlockBox;
     # Here our job is much easier in the unresolved case: subsequent
     # children's layout doesn't depend on our position; so we can just
     # defer margin resolution to the parent.
+    if fstate.space.w.t == scFitContent:
+      # Do not queue in the first pass.
+      return
     let lctx = fstate.lctx
     var offset = fstate.offset
     fstate.initLine(flag = ilfAbsolute)
@@ -1815,12 +1840,7 @@ proc layoutImage(fstate: var FlowState; ibox: InlineImageBox; padding: LUnit) =
 proc layoutInline(fstate: var FlowState; ibox: InlineBox) =
   let lctx = fstate.lctx
   let computed = ibox.computed
-  ibox.state = InlineBoxState(
-    startOffset: offset(
-      x = fstate.lbstate.widthAfterWhitespace,
-      y = fstate.offset.y
-    )
-  )
+  ibox.state = InlineBoxState()
   let padding = Span(
     start: computed{"padding-left"}.px(fstate.space.w),
     send: computed{"padding-right"}.px(fstate.space.w)
@@ -1846,6 +1866,10 @@ proc layoutInline(fstate: var FlowState; ibox: InlineBox) =
     fstate.layoutImage(ibox, padding.sum())
     fstate.lastTextBox = ibox
   else:
+    ibox.state.startOffset = offset(
+      x = fstate.lbstate.widthAfterWhitespace,
+      y = fstate.offset.y
+    )
     let w = computed{"margin-left"}.px(fstate.space.w)
     if w != 0:
       fstate.initLine()
@@ -1894,7 +1918,7 @@ proc layoutInline(fstate: var FlowState; ibox: InlineBox) =
       # since it uses cellHeight instead of the actual line height for the
       # last line.
       # Well, it seems good enough.
-      lctx.popPositioned(size(
+      lctx.popPositioned(ibox, size(
         w = 0,
         h = fstate.offset.y + fstate.cellHeight - ibox.state.startOffset.y
       ))
@@ -2061,7 +2085,7 @@ proc layoutFlow(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes;
       # I'll just replicate what layoutRootBlock is doing until I find a
       # better solution...
       size.h = max(size.h, bctx.maxFloatHeight)
-    bctx.lctx.popPositioned(size)
+    bctx.lctx.popPositioned(box, size)
 
 proc layoutListItem(bctx: var BlockContext; box: BlockBox;
     sizes: ResolvedSizes) =
@@ -2827,7 +2851,7 @@ proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   for child in fctx.relativeChildren:
     lctx.positionRelative(sizes.space, child)
   if box.computed{"position"} != PositionStatic:
-    lctx.popPositioned(box.state.size)
+    lctx.popPositioned(box, box.state.size)
 
 # Inner layout for boxes that establish a new block formatting context.
 # Returns the bottom margin for the box, collapsed with the appropriate
@@ -2861,7 +2885,7 @@ proc layout*(box: BlockBox; attrsp: ptr WindowAttributes) =
   let lctx = LayoutContext(
     attrsp: attrsp,
     cellSize: size(w = attrsp.ppc, h = attrsp.ppl),
-    positioned: @[PositionedItem(), PositionedItem()],
+    positioned: @[@[], @[]],
     luctx: LUContext()
   )
   let sizes = lctx.resolveBlockSizes(space, box.computed)
@@ -2869,7 +2893,7 @@ proc layout*(box: BlockBox; attrsp: ptr WindowAttributes) =
   lctx.layoutRootBlock(box, sizes.margin.topLeft, sizes)
   var size = size(w = attrsp[].widthPx, h = attrsp[].heightPx)
   # Last absolute layer.
-  lctx.popPositioned(size)
+  lctx.popPositioned(box, size)
   # Fixed containing block.
   # The idea is to move fixed boxes to the real edges of the page,
   # so that they do not overlap with other boxes *and* we don't have
@@ -2878,4 +2902,4 @@ proc layout*(box: BlockBox; attrsp: ptr WindowAttributes) =
   # slow down the renderer to a crawl.)
   size.w = max(size.w, box.state.size.w)
   size.h = max(size.h, box.state.size.h)
-  lctx.popPositioned(size)
+  lctx.popPositioned(box, size, skipStatic = false)
diff --git a/src/css/render.nim b/src/css/render.nim
index 02f19a63..7a859baf 100644
--- a/src/css/render.nim
+++ b/src/css/render.nim
@@ -50,9 +50,6 @@ type
     index: int
 
   RenderState = object
-    # Position of the absolute positioning containing block:
-    # https://drafts.csswg.org/css-position/#absolute-positioning-containing-block
-    absolutePos: seq[Offset]
     clipBoxes: seq[ClipBox]
     bgcolor: CellColor
     attrsp: ptr WindowAttributes
@@ -362,7 +359,6 @@ proc paintInlineBox(grid: var FlexibleGrid; state: var RenderState;
 proc renderInlineBox(grid: var FlexibleGrid; state: var RenderState;
     ibox: InlineBox; offset: Offset; bgcolor0: ARGBColor;
     pass2 = false) =
-  let position = ibox.computed{"position"}
   #TODO stacking contexts
   let bgcolor = ibox.computed{"background-color"}
   var bgcolor0 = bgcolor0
@@ -375,8 +371,7 @@ proc renderInlineBox(grid: var FlexibleGrid; state: var RenderState;
     if bgcolor0.a > 0:
       grid.paintInlineBox(state, ibox, offset,
         bgcolor0.rgb.cellColor(), bgcolor0.a)
-  let startOffset = offset + ibox.state.startOffset
-  ibox.render.offset = startOffset
+  ibox.render.offset = offset + ibox.state.startOffset
   if ibox of InlineTextBox:
     let ibox = InlineTextBox(ibox)
     let format = ibox.computed.toFormat()
@@ -415,18 +410,12 @@ proc renderInlineBox(grid: var FlexibleGrid; state: var RenderState;
         height: ibox.imgstate.size.h.toInt,
         bmp: ibox.bmp
       ))
-  elif ibox of InlineNewLineBox:
-    discard
-  else:
-    if position != PositionStatic:
-      state.absolutePos.add(startOffset)
+  else: # InlineNewLineBox does not have children, so we handle it here
     for child in ibox.children:
       if child of InlineBox:
         grid.renderInlineBox(state, InlineBox(child), offset, bgcolor0)
       else:
         grid.renderBlockBox(state, BlockBox(child), offset)
-    if position != PositionStatic:
-      discard state.absolutePos.pop()
 
 proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
     box: BlockBox; offset: Offset; pass2 = false) =
@@ -437,21 +426,12 @@ proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
     state.nstack.add(StackItem(
       box: box,
       offset: offset,
-      apos: state.absolutePos[^1],
       clipBox: state.clipBox,
       index: zindex
     ))
     return
-  var offset = offset
-  if position in PositionAbsoluteFixed:
-    if box.computed{"left"}.u != clAuto or box.computed{"right"}.u != clAuto:
-      offset.x = state.absolutePos[^1].x
-    if box.computed{"top"}.u != clAuto or box.computed{"bottom"}.u != clAuto:
-      offset.y = state.absolutePos[^1].y
-  offset += box.state.offset
+  let offset = offset + box.state.offset
   box.render.offset = offset
-  if position != PositionStatic:
-    state.absolutePos.add(offset)
   let overflowX = box.computed{"overflow-x"}
   let overflowY = box.computed{"overflow-y"}
   let hasClipBox = overflowX != OverflowVisible or overflowY != OverflowVisible
@@ -506,8 +486,6 @@ proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
           grid.renderBlockBox(state, BlockBox(child), offset)
   if hasClipBox:
     discard state.clipBoxes.pop()
-  if position != PositionStatic:
-    discard state.absolutePos.pop()
 
 proc renderDocument*(grid: var FlexibleGrid; bgcolor: var CellColor;
     rootBox: BlockBox; attrsp: ptr WindowAttributes;
@@ -517,7 +495,6 @@ proc renderDocument*(grid: var FlexibleGrid; bgcolor: var CellColor;
     # no HTML element when we run cascade; just clear all lines.
     return
   var state = RenderState(
-    absolutePos: @[offset(0, 0)],
     clipBoxes: @[ClipBox(send: offset(LUnit.high, LUnit.high))],
     attrsp: attrsp,
     bgcolor: defaultColor
@@ -525,11 +502,9 @@ proc renderDocument*(grid: var FlexibleGrid; bgcolor: var CellColor;
   var stack = @[StackItem(box: rootBox, clipBox: state.clipBox)]
   while stack.len > 0:
     for it in stack:
-      state.absolutePos.add(it.apos)
       state.clipBoxes.add(it.clipBox)
       grid.renderBlockBox(state, it.box, it.offset, true)
       discard state.clipBoxes.pop()
-      discard state.absolutePos.pop()
     stack = move(state.nstack)
     stack.sort(proc(x, y: StackItem): int = cmp(x.index, y.index))
     state.nstack = @[]