about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2025-04-03 18:37:29 +0200
committerbptato <nincsnevem662@gmail.com>2025-04-03 18:37:29 +0200
commit19c573e794fc74cb57833f4d2cbe53e002958685 (patch)
tree4c1787f9467df009991aa41e05c6681999375971
parent4703192bcd6bd444964116fc64a9d0cb2aef2a1f (diff)
downloadchawan-19c573e794fc74cb57833f4d2cbe53e002958685.tar.gz
layout: correct table-caption width calculation
It's surprisingly tricky.
-rw-r--r--src/css/layout.nim94
-rw-r--r--test/layout/float-next-to-caption.expected2
-rw-r--r--test/layout/table-caption-intrinsic-size-affects-inner-table-and-vice-versa.color.expected13
-rw-r--r--test/layout/table-caption-intrinsic-size-affects-inner-table-and-vice-versa.html51
-rw-r--r--test/layout/table-caption-margins.expected2
-rw-r--r--test/layout/table-caption-margins.html2
-rw-r--r--todo1
7 files changed, 126 insertions, 39 deletions
diff --git a/src/css/layout.nim b/src/css/layout.nim
index ca16d7b0..ac64f6be 100644
--- a/src/css/layout.nim
+++ b/src/css/layout.nim
@@ -144,6 +144,12 @@ func applySizeConstraint(u: LUnit; availableSize: SizeConstraint): LUnit =
 func outerSize(box: BlockBox; dim: DimensionType; sizes: ResolvedSizes): LUnit =
   return sizes.margin[dim].sum() + box.state.size[dim]
 
+func outerSize(box: BlockBox; sizes: ResolvedSizes): Size =
+  return size(
+    w = box.outerSize(dtHorizontal, sizes),
+    h = box.outerSize(dtVertical, sizes)
+  )
+
 func max(span: Span): LUnit =
   return max(span.start, span.send)
 
@@ -1568,10 +1574,7 @@ proc layoutFloat(fstate: var FlowState; child: BlockBox) =
   let lctx = fstate.lctx
   let sizes = lctx.resolveFloatSizes(fstate.space, child.computed)
   lctx.layoutRootBlock(child, fstate.offset + sizes.margin.topLeft, sizes)
-  let outerSize = size(
-    w = child.outerSize(dtHorizontal, sizes),
-    h = child.outerSize(dtVertical, sizes)
-  )
+  let outerSize = child.outerSize(sizes)
   if fstate.space.w.t == scFitContent:
     # Float position depends on the available width, but in this case
     # the parent width is not known.  Skip this box; we will position
@@ -1766,10 +1769,7 @@ proc layoutInlineBlock(fstate: var FlowState; ibox: InlineBlockBox) =
       ibox: ibox,
       baseline: box.state.baseline + sizes.margin.top,
       vertalign: box.computed{"vertical-align"},
-      size: size(
-        w = box.outerSize(dtHorizontal, sizes),
-        h = box.outerSize(dtVertical, sizes)
-      )
+      size: box.outerSize(sizes)
     )
     var istate = InlineState(ibox: ibox)
     discard fstate.addAtom(istate, iastate)
@@ -2522,25 +2522,10 @@ proc layoutTableRows(tctx: TableContext; table: BlockBox;
     # min-height...
     table.state.size.h = y
 
-proc layoutCaption(tctx: TableContext; parent, box: BlockBox) =
-  let lctx = tctx.lctx
-  let space = availableSpace(w = stretch(parent.state.size.w), h = maxContent())
-  let sizes = lctx.resolveBlockSizes(space, box.computed)
-  lctx.layoutRootBlock(box, offset(x = sizes.margin.left, y = 0), sizes)
-  box.state.offset.x += sizes.margin.left
-  box.state.offset.y += sizes.margin.top
-  let outerHeight = box.outerSize(dtVertical, sizes)
-  let outerWidth = box.outerSize(dtHorizontal, sizes)
-  let table = BlockBox(parent.firstChild)
-  case box.computed{"caption-side"}
-  of CaptionSideTop, CaptionSideBlockStart:
-    table.state.offset.y += outerHeight
-  of CaptionSideBottom, CaptionSideBlockEnd:
-    box.state.offset.y += table.state.size.h
-  parent.state.size.w = max(parent.state.size.w, outerWidth)
-  parent.state.intr.w = max(parent.state.intr.w, box.state.intr.w)
-  parent.state.size.h += outerHeight
-  parent.state.intr.h += outerHeight - box.state.size.h + box.state.intr.h
+proc layoutCaption(lctx: LayoutContext; box: BlockBox; space: AvailableSpace;
+    sizes: out ResolvedSizes) =
+  sizes = lctx.resolveBlockSizes(space, box.computed)
+  lctx.layoutRootBlock(box, sizes.margin.topLeft, sizes)
 
 proc layoutInnerTable(tctx: var TableContext; table, parent: BlockBox;
     sizes: ResolvedSizes) =
@@ -2570,22 +2555,61 @@ proc layoutInnerTable(tctx: var TableContext; table, parent: BlockBox;
   # specified, ergo it always equals the intrinisc minimum height.
   table.state.intr.h = table.state.size.h
 
-# As per standard, we must put the caption outside the actual table, inside a
-# block-level wrapper box.
+# As per standard, we must put the caption outside the actual table,
+# inside a block-level wrapper box.
+#
+# Note that computing the caption's width isn't as simple as it sounds.
+# First, the caption's intrinsic minimum size overrides the available
+# space (unlike what happens in flow, where available space wins).
+# Second, table and caption width has a cyclic dependency, in that the
+# larger of the two must be used for layouting both the cells and the
+# caption.
+#
+# So conceptually we first layout caption, relayout with its intrinsic
+# min size if needed, then layout table, then caption again if table's
+# width exceeds caption's width.  (In practice, the second layout is
+# skipped if there will be a third one, so we never layout more than
+# twice.)
 proc layoutTable(bctx: BlockContext; box: BlockBox; sizes: ResolvedSizes) =
+  let lctx = bctx.lctx
   let table = BlockBox(box.firstChild)
   table.resetState()
-  var tctx = TableContext(lctx: bctx.lctx, space: sizes.space)
+  var tctx = TableContext(lctx: lctx, space: sizes.space)
+  let caption = BlockBox(table.next)
+  var captionSpace = availableSpace(
+    w = fitContent(sizes.space.w),
+    h = maxContent()
+  )
+  var captionSizes: ResolvedSizes
+  if caption != nil:
+    lctx.layoutCaption(caption, captionSpace, captionSizes)
+    if captionSpace.w.isDefinite():
+      if caption.state.intr.w != captionSpace.w.u:
+        captionSpace.w.u = caption.state.intr.w
+      if tctx.space.w.t == scStretch and tctx.space.w.u < captionSpace.w.u:
+        tctx.space.w.u = captionSpace.w.u
   tctx.layoutInnerTable(table, box, sizes)
   box.state.size = table.state.size
   box.state.baseline = table.state.size.h
   box.state.firstBaseline = table.state.size.h
   box.state.intr = table.state.intr
-  if table.next != nil:
-    # do it here, so that caption's box can stretch to our width
-    let caption = BlockBox(table.next)
-    #TODO also count caption width in table width
-    tctx.layoutCaption(box, caption)
+  if caption != nil:
+    if captionSpace.w.isDefinite():
+      if table.state.size.w > captionSpace.w.u:
+        captionSpace.w = stretch(table.state.size.w)
+      if captionSpace.w.u != caption.state.size.w: # desired size changed; redo
+        lctx.layoutCaption(caption, captionSpace, captionSizes)
+    let outerSize = caption.outerSize(captionSizes)
+    case caption.computed{"caption-side"}
+    of CaptionSideTop, CaptionSideBlockStart:
+      table.state.offset.y += outerSize.h
+    of CaptionSideBottom, CaptionSideBlockEnd:
+      caption.state.offset.y += table.state.size.h
+    box.state.size.w = max(box.state.size.w, outerSize.w)
+    box.state.intr.w = max(box.state.intr.w, caption.state.intr.w)
+    box.state.size.h += outerSize.h
+    box.state.intr.h += outerSize.h - caption.state.size.h +
+      caption.state.intr.h
 
 # Flex layout.
 type
diff --git a/test/layout/float-next-to-caption.expected b/test/layout/float-next-to-caption.expected
index 3021c733..31bd0d05 100644
--- a/test/layout/float-next-to-caption.expected
+++ b/test/layout/float-next-to-caption.expected
@@ -1,2 +1,2 @@
 test                                                                      test 1
-2
+ 2
diff --git a/test/layout/table-caption-intrinsic-size-affects-inner-table-and-vice-versa.color.expected b/test/layout/table-caption-intrinsic-size-affects-inner-table-and-vice-versa.color.expected
new file mode 100644
index 00000000..e9f490c1
--- /dev/null
+++ b/test/layout/table-caption-intrinsic-size-affects-inner-table-and-vice-versa.color.expected
@@ -0,0 +1,13 @@
+                
+testtesttesttest
+test
+testtesttesttest
+test test
+test 
+test 
+test 
+test 
+testtesttesttest
+test test
+          
+atest test
diff --git a/test/layout/table-caption-intrinsic-size-affects-inner-table-and-vice-versa.html b/test/layout/table-caption-intrinsic-size-affects-inner-table-and-vice-versa.html
new file mode 100644
index 00000000..a1862ec9
--- /dev/null
+++ b/test/layout/table-caption-intrinsic-size-affects-inner-table-and-vice-versa.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<style>
+xtable { display: table; background: green }
+xcaption { display: table-caption; background: pink }
+xtr { display: table-row; background: blue }
+xtd { display: table-cell; background: red }
+</style>
+
+<!-- caption grows to its intrinsic min width -->
+<div style="width: 1ch">
+<xtable>
+<xcaption><div style="width: 20%; background: blue; height: 1em"></div>testtesttesttest</xcaption>
+<xtr><xtd>test</xtd></xtr>
+</xtable>
+</div>
+
+<!-- available space constrains table, caption is larger than space
+     -> table grows to caption's intrinsic min width -->
+<div style="width: 5ch">
+<xtable>
+<xcaption>testtesttesttest</xcaption>
+<xtr><xtd>test test</xtd></xtr>
+</xtable>
+</div>
+
+<!-- caption & table both fit in available space
+     -> break lines -->
+<div style="width: 5ch">
+<xtable>
+<xcaption>test test</xcaption>
+<xtr><xtd>test test</xtd></xtr>
+</xtable>
+</div>
+
+<!-- caption & table are not constrained in size, table is smaller
+     -> table remains smaller -->
+<xtable>
+<xcaption>testtesttesttest</xcaption>
+<xtr><xtd>test test</xtd></xtr>
+</xtable>
+
+<!-- caption & table are not constrained in size, table is larger
+     -> caption stretched to table size -->
+<xtable>
+<xcaption><div style="width: 50%; height: 1em; background: blue"></div></xcaption>
+<xtr>
+<xtd>
+atest test
+</xtd>
+</xtr>
+</xtable>
diff --git a/test/layout/table-caption-margins.expected b/test/layout/table-caption-margins.expected
index 7fe2215d..330acfa6 100644
--- a/test/layout/table-caption-margins.expected
+++ b/test/layout/table-caption-margins.expected
@@ -4,4 +4,4 @@
 
 
 
-        testing
+ testing
diff --git a/test/layout/table-caption-margins.html b/test/layout/table-caption-margins.html
index a3e3759e..aaf89d8a 100644
--- a/test/layout/table-caption-margins.html
+++ b/test/layout/table-caption-margins.html
@@ -6,7 +6,7 @@ something something
 <td>
 row 2
 <caption style="caption-side: bottom; margin-top: 2em; margin-left: 1ch">
-<div style="margin-top: 2em">
+<div style="margin-top: 2em; text-align: left">
 testing
 </div>
 </caption>
diff --git a/todo b/todo
index dc012e55..83a050e4 100644
--- a/todo
+++ b/todo
@@ -69,7 +69,6 @@ layout:
 	  1ch in x direction and 1em in y direction.
 	* table is high priority; for other boxes, I don't know if it
 	  would work
-- table layout: include caption in width calculation
 - flexbox: align-self, align-items, justify-content, proper margin handling
 - details element
 - layout caching