about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-04-02 16:43:03 +0200
committerbptato <nincsnevem662@gmail.com>2024-04-05 01:41:40 +0200
commit372f936f05abf8db2ce5e05c2a267289265095a6 (patch)
treeb4d5843a0b4fd3e93942ddb8cb8451dff378791e
parente673d6712b7f5a3ad543521bffaa479ab7a83fde (diff)
downloadchawan-372f936f05abf8db2ce5e05c2a267289265095a6.tar.gz
Initial flexbox support
Still far from being fully standards-compliant, or even complete, but it
seems to work slightly less horribly than having no flexbox support at
all on sites that do use it.

(Also includes various refactorings in layout to make it possible at all
to add flexbox.)
-rw-r--r--README.md3
-rw-r--r--doc/architecture.md4
-rw-r--r--src/css/values.nim196
-rw-r--r--src/layout/engine.nim504
-rw-r--r--todo6
5 files changed, 546 insertions, 167 deletions
diff --git a/README.md b/README.md
index 46705774..3492fffe 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ Currently implemented features are:
 	* flow layout is supported (now with floats!)
 	* table layout is supported, except for fixed tables
 	* the box model is mostly implemented, except for borders
+	* flexbox layout is supported (some parts are still a WIP)
 * forms
 * incremental loading of various kinds of documents (plain text, HTML, etc.)
 * JavaScript based navigation
@@ -170,7 +171,7 @@ and ANSI text, use `plaintext, pre`.
 ### Why does `$WEBSITE` look awful?
 
 Usually, this is because it uses some CSS features that are not yet implemented
-in Chawan. The most common offenders are flexbox, grid, and CSS variables.
+in Chawan. The most common offenders are grid and CSS variables.
 
 There are three ways of dealing with this:
 
diff --git a/doc/architecture.md b/doc/architecture.md
index 9b62daf7..cee904e1 100644
--- a/doc/architecture.md
+++ b/doc/architecture.md
@@ -291,8 +291,8 @@ It has some problems:
 
 * CSS was designed for pixel-based displays, not for character-based ones. So we
   have to round a lot, and sometimes this goes wrong.
-* In the past years, websites have finally started using flexbox and grid, and
-  we have neither, so things look very ugly.
+* In the past years, websites have finally started using grid, and we don't have
+  it, so those websites look very ugly.
 * Even what we do have has plenty of bugs. (Sad.)
 * It's slow on large documents, because we don't have partial layouting
   capabilities.
diff --git a/src/css/values.nim b/src/css/values.nim
index e1c54c04..5bceed5b 100644
--- a/src/css/values.nim
+++ b/src/css/values.nim
@@ -23,6 +23,7 @@ type
     cstPadding = "padding"
     cstBackground = "background"
     cstListStyle = "list-style"
+    cstFlex = "flex"
 
   CSSUnit* = enum
     UNIT_CM, UNIT_MM, UNIT_IN, UNIT_PX, UNIT_PT, UNIT_PC, UNIT_EM, UNIT_EX,
@@ -79,6 +80,11 @@ type
     cptClear = "clear"
     cptTextTransform = "text-transform"
     cptBgcolorIsCanvas = "-cha-bgcolor-is-canvas"
+    cptFlexDirection = "flex-direction"
+    cptFlexWrap = "flex-wrap"
+    cptFlexGrow = "flex-grow"
+    cptFlexShrink = "flex-shrink"
+    cptFlexBasis = "flex-basis"
 
   CSSValueType* = enum
     cvtNone = ""
@@ -108,6 +114,9 @@ type
     cvtClear = "clear"
     cvtTextTransform = "texttransform"
     cvtBgcolorIsCanvas = "bgcoloriscanvas"
+    cvtFlexDirection = "flexdirection"
+    cvtFlexWrap = "flexwrap"
+    cvtNumber = "number"
 
   CSSGlobalValueType* = enum
     cvtNoglobal, cvtInitial, cvtInherit, cvtRevert, cvtUnset
@@ -118,7 +127,7 @@ type
     DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_HEADER_GROUP,
     DISPLAY_TABLE_FOOTER_GROUP, DISPLAY_TABLE_COLUMN_GROUP, DISPLAY_TABLE_ROW,
     DISPLAY_TABLE_COLUMN, DISPLAY_TABLE_CELL, DISPLAY_TABLE_CAPTION,
-    DISPLAY_FLOW_ROOT
+    DISPLAY_FLOW_ROOT, DISPLAY_FLEX, DISPLAY_INLINE_FLEX
 
   CSSWhitespace* = enum
     WHITESPACE_NORMAL, WHITESPACE_NOWRAP, WHITESPACE_PRE, WHITESPACE_PRE_LINE,
@@ -193,15 +202,28 @@ type
     TEXT_TRANSFORM_LOWERCASE, TEXT_TRANSFORM_FULL_WIDTH,
     TEXT_TRANSFORM_FULL_SIZE_KANA, TEXT_TRANSFORM_CHA_HALF_WIDTH
 
-const RowGroupBox* = {DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_HEADER_GROUP,
-                      DISPLAY_TABLE_FOOTER_GROUP}
-const ProperTableChild* = {DISPLAY_TABLE_ROW, DISPLAY_TABLE_COLUMN,
-                           DISPLAY_TABLE_COLUMN_GROUP, DISPLAY_TABLE_CAPTION} +
-                           RowGroupBox
-const ProperTableRowParent* = {DISPLAY_TABLE, DISPLAY_INLINE_TABLE} + RowGroupBox
-const InternalTableBox* = {DISPLAY_TABLE_CELL, DISPLAY_TABLE_ROW,
-                           DISPLAY_TABLE_COLUMN, DISPLAY_TABLE_COLUMN_GROUP} +
-                           RowGroupBox
+  CSSFlexDirection* = enum
+    FLEX_DIRECTION_ROW, FLEX_DIRECTION_ROW_REVERSE, FLEX_DIRECTION_COLUMN,
+    FLEX_DIRECTION_COLUMN_REVERSE
+
+  CSSFlexWrap* = enum
+    FLEX_WRAP_NOWRAP, FLEX_WRAP_WRAP, FLEX_WRAP_WRAP_REVERSE
+
+const RowGroupBox* = {
+  DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_HEADER_GROUP,
+  DISPLAY_TABLE_FOOTER_GROUP
+}
+const ProperTableChild* = RowGroupBox + {
+  DISPLAY_TABLE_ROW, DISPLAY_TABLE_COLUMN, DISPLAY_TABLE_COLUMN_GROUP,
+  DISPLAY_TABLE_CAPTION
+}
+const ProperTableRowParent* = RowGroupBox + {
+  DISPLAY_TABLE, DISPLAY_INLINE_TABLE
+}
+const InternalTableBox* = RowGroupBox + {
+  DISPLAY_TABLE_CELL, DISPLAY_TABLE_ROW, DISPLAY_TABLE_COLUMN,
+  DISPLAY_TABLE_COLUMN_GROUP
+}
 const TabularContainer* = {DISPLAY_TABLE_ROW} + ProperTableRowParent
 
 type
@@ -245,6 +267,8 @@ type
       whitespace*: CSSWhitespace
     of cvtInteger:
       integer*: int
+    of cvtNumber:
+      number*: float64
     of cvtTextDecoration:
       textdecoration*: set[CSSTextDecoration]
     of cvtWordBreak:
@@ -281,6 +305,10 @@ type
       texttransform*: CSSTextTransform
     of cvtBgcolorIsCanvas:
       bgcoloriscanvas*: bool
+    of cvtFlexDirection:
+      flexdirection*: CSSFlexDirection
+    of cvtFlexWrap:
+      flexwrap*: CSSFlexWrap
     of cvtNone: discard
 
   CSSComputedValues* = ref array[CSSPropertyType, CSSComputedValue]
@@ -303,13 +331,12 @@ type
     importantProperties: array[CSSOrigin, CSSComputedEntries]
     preshints*: CSSComputedValues
 
-const ShorthandNames = {
-  "all": cstAll,
-  "margin": cstMargin,
-  "padding": cstPadding,
-  "background": cstBackground,
-  "list-style": cstListStyle
-}.toTable()
+const ShorthandNames = block:
+  var tab = initTable[string, CSSShorthandType]()
+  for t in CSSShorthandType:
+    if $t != "":
+      tab[$t] = t
+  tab
 
 const PropertyNames = block:
   var tab = initTable[string, CSSPropertyType]()
@@ -318,7 +345,7 @@ const PropertyNames = block:
       tab[$t] = t
   tab
 
-const ValueTypes* = [
+const ValueTypes = [
   cptNone: cvtNone,
   cptColor: cvtColor,
   cptMarginTop: cvtLength,
@@ -367,7 +394,12 @@ const ValueTypes* = [
   cptBoxSizing: cvtBoxSizing,
   cptClear: cvtClear,
   cptTextTransform: cvtTextTransform,
-  cptBgcolorIsCanvas: cvtBgcolorIsCanvas
+  cptBgcolorIsCanvas: cvtBgcolorIsCanvas,
+  cptFlexDirection: cvtFlexDirection,
+  cptFlexWrap: cvtFlexWrap,
+  cptFlexGrow: cvtNumber,
+  cptFlexShrink: cvtNumber,
+  cptFlexBasis: cvtLength
 ]
 
 const InheritedProperties = {
@@ -475,6 +507,22 @@ func px*(l: CSSLength, window: WindowAttributes, p: LayoutUnit): LayoutUnit
   of UNIT_VMAX:
     toLayoutUnit(max(window.width_px, window.width_px) / 100 * l.num)
 
+func blockify*(display: CSSDisplay): CSSDisplay =
+  case display
+  of DISPLAY_BLOCK, DISPLAY_TABLE, DISPLAY_LIST_ITEM, DISPLAY_NONE,
+      DISPLAY_FLOW_ROOT, DISPLAY_FLEX:
+     #TODO grid
+    return display
+  of DISPLAY_INLINE, DISPLAY_INLINE_BLOCK, DISPLAY_TABLE_ROW,
+      DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_COLUMN,
+      DISPLAY_TABLE_COLUMN_GROUP, DISPLAY_TABLE_CELL, DISPLAY_TABLE_CAPTION,
+      DISPLAY_TABLE_HEADER_GROUP, DISPLAY_TABLE_FOOTER_GROUP:
+    return DISPLAY_BLOCK
+  of DISPLAY_INLINE_TABLE:
+    return DISPLAY_TABLE
+  of DISPLAY_INLINE_FLEX:
+    return DISPLAY_FLEX
+
 const UpperAlphaMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toRunes()
 const LowerAlphaMap = "abcdefghijklmnopqrstuvwxyz".toRunes()
 const LowerGreekMap = "αβγδεζηθικλμνξοπρστυφχψω".toRunes()
@@ -710,9 +758,11 @@ func cssColor*(val: CSSComponentValue): Opt[CellColor] =
       return parseANSI(f.value)
   return err()
 
-func isToken(cval: CSSComponentValue): bool {.inline.} = cval of CSSToken
+func isToken(cval: CSSComponentValue): bool {.inline.} =
+  cval of CSSToken
 
-func getToken(cval: CSSComponentValue): CSSToken = (CSSToken)cval
+func getToken(cval: CSSComponentValue): CSSToken {.inline.} =
+  CSSToken(cval)
 
 func cssIdent[T](map: static openArray[(string, T)], cval: CSSComponentValue):
     Opt[T] =
@@ -869,6 +919,8 @@ func cssDisplay(cval: CSSComponentValue): Opt[CSSDisplay] =
     "table-footer-group": DISPLAY_TABLE_FOOTER_GROUP,
     "table-caption": DISPLAY_TABLE_CAPTION,
     "flow-root": DISPLAY_FLOW_ROOT,
+    "flex": DISPLAY_FLEX,
+    "inline-flex": DISPLAY_INLINE_FLEX,
     "none": DISPLAY_NONE
   }
   return cssIdent(DisplayMap, cval)
@@ -1169,6 +1221,31 @@ func cssTextTransform(cval: CSSComponentValue): Opt[CSSTextTransform] =
   }
   return cssIdent(TextTransformMap, cval)
 
+func cssFlexDirection(cval: CSSComponentValue): Opt[CSSFlexDirection] =
+  const FlexDirectionMap = {
+    "row": FLEX_DIRECTION_ROW,
+    "row-reverse": FLEX_DIRECTION_ROW_REVERSE,
+    "column": FLEX_DIRECTION_COLUMN,
+    "column-reverse": FLEX_DIRECTION_COLUMN_REVERSE,
+  }
+  return cssIdent(FlexDirectionMap, cval)
+
+func cssNumber(cval: CSSComponentValue; positive: bool): Opt[float64] =
+  if isToken(cval):
+    let tok = getToken(cval)
+    if tok.tokenType == CSS_NUMBER_TOKEN:
+      if not positive or tok.nvalue >= 0:
+        return ok(tok.nvalue)
+  return err()
+
+func cssFlexWrap(cval: CSSComponentValue): Opt[CSSFlexWrap] =
+  const FlexWrapMap = {
+    "nowrap": FLEX_WRAP_NOWRAP,
+    "wrap": FLEX_WRAP_WRAP,
+    "wrap-reverse": FLEX_WRAP_WRAP_REVERSE
+  }
+  return cssIdent(FlexWrapMap, cval)
+
 proc getValueFromDecl(val: CSSComputedValue, d: CSSDeclaration,
     vtype: CSSValueType, ptype: CSSPropertyType): Err[void] =
   var i = 0
@@ -1192,6 +1269,7 @@ proc getValueFromDecl(val: CSSComputedValue, d: CSSDeclaration,
     of cptPaddingLeft, cptPaddingRight, cptPaddingTop,
        cptPaddingBottom:
       val.length = ?cssLength(cval, has_auto = false)
+    #TODO content for flex-basis
     else:
       val.length = ?cssLength(cval)
   of cvtFontStyle:
@@ -1253,6 +1331,13 @@ proc getValueFromDecl(val: CSSComputedValue, d: CSSDeclaration,
     val.texttransform = ?cssTextTransform(cval)
   of cvtBgcolorIsCanvas:
     return err() # internal value
+  of cvtFlexDirection:
+    val.flexdirection = ?cssFlexDirection(cval)
+  of cvtFlexWrap:
+    val.flexwrap = ?cssFlexWrap(cval)
+  of cvtNumber:
+    const NeedsPositive = {cptFlexGrow}
+    val.number = ?cssNumber(cval, ptype in NeedsPositive)
   of cvtNone:
     discard
   return ok()
@@ -1264,10 +1349,9 @@ func getInitialColor(t: CSSPropertyType): CellColor =
 
 func getInitialLength(t: CSSPropertyType): CSSLength =
   case t
-  of cptWidth, cptHeight, cptWordSpacing,
-     cptLineHeight, cptLeft, cptRight, cptTop,
-     cptBottom, cptMaxWidth, cptMaxHeight,
-     cptMinWidth, cptMinHeight:
+  of cptWidth, cptHeight, cptWordSpacing, cptLineHeight, cptLeft, cptRight,
+      cptTop, cptBottom, cptMaxWidth, cptMaxHeight, cptMinWidth, cptMinHeight,
+      cptFlexBasis:
     return CSSLengthAuto
   else:
     return CSSLength(auto: false, unit: UNIT_PX, num: 0)
@@ -1281,6 +1365,11 @@ func getInitialInteger(t: CSSPropertyType): int =
   else:
     return 0
 
+func getInitialNumber(t: CSSPropertyType): float64 =
+  if t == cptFlexShrink:
+    return 1
+  return 0
+
 func calcInitial(t: CSSPropertyType): CSSComputedValue =
   let v = valueType(t)
   var nv: CSSComputedValue
@@ -1297,6 +1386,8 @@ func calcInitial(t: CSSPropertyType): CSSComputedValue =
     nv = CSSComputedValue(v: v, integer: getInitialInteger(t))
   of cvtQuotes:
     nv = CSSComputedValue(v: v, quotes: CSSQuotes(auto: true))
+  of cvtNumber:
+    nv = CSSComputedValue(v: v, number: getInitialNumber(t))
   else:
     nv = CSSComputedValue(v: v)
   return nv
@@ -1421,6 +1512,8 @@ proc getComputedValues0(res: var seq[CSSComputedEntry], d: CSSDeclaration):
     var valid = true
     if global == cvtNoglobal:
       for tok in d.value:
+        if tok == CSS_WHITESPACE_TOKEN:
+          continue
         if (let r = cssListStylePosition(tok); r.isOk):
           positionVal = CSSComputedValue(
             v: cvtListStylePosition,
@@ -1438,6 +1531,42 @@ proc getComputedValues0(res: var seq[CSSComputedEntry], d: CSSDeclaration):
     if valid:
       res.add((cptListStylePosition, positionVal, global))
       res.add((cptListStyleType, typeVal, global))
+  of cstFlex:
+    let global = cssGlobal(d)
+    if global == cvtNoglobal:
+      var i = 0
+      d.value.skipWhitespace(i)
+      if i >= d.value.len:
+        return err()
+      if (let r = cssNumber(d.value[i], positive = true); r.isSome):
+        # flex-grow
+        let val = CSSComputedValue(v: cvtNumber, number: r.get)
+        res.add((cptFlexGrow, val, global))
+        d.value.skipWhitespace(i)
+        if i < d.value.len:
+          if not d.value[i].isToken:
+            return err()
+          if (let r = cssNumber(d.value[i], positive = true); r.isSome):
+            # flex-shrink
+            let val = CSSComputedValue(v: cvtNumber, number: r.get)
+            res.add((cptFlexShrink, val, global))
+            d.value.skipWhitespace(i)
+      if res.len < 1: # flex-grow omitted, default to 1
+        let val = CSSComputedValue(v: cvtNumber, number: 1)
+        res.add((cptFlexGrow, val, global))
+      if res.len < 2: # flex-shrink omitted, default to 1
+        let val = CSSComputedValue(v: cvtNumber, number: 1)
+        res.add((cptFlexShrink, val, global))
+      if i < d.value.len:
+        # flex-basis
+        let val = CSSComputedValue(v: cvtLength, length: ?cssLength(d.value[i]))
+        res.add((cptFlexBasis, val, global))
+      else: # omitted, default to 0px
+        let val = CSSComputedValue(
+          v: cvtLength,
+          length: CSSLength(unit: UNIT_PX, num: 0)
+        )
+        res.add((cptFlexGrow, val, global))
   return ok()
 
 proc getComputedValues(d: CSSDeclaration): seq[CSSComputedEntry] =
@@ -1570,16 +1699,7 @@ func buildComputedValues*(builder: CSSComputedValuesBuilder):
       else:
         result[prop] = getDefault(prop)
   if result{"float"} != FLOAT_NONE:
-    case result{"display"}
-    of DISPLAY_BLOCK, DISPLAY_TABLE, DISPLAY_LIST_ITEM, DISPLAY_NONE,
-        DISPLAY_FLOW_ROOT:
-       #TODO flex, grid
-      discard
-      {.linearScanEnd.}
-    of DISPLAY_INLINE, DISPLAY_INLINE_BLOCK, DISPLAY_TABLE_ROW,
-        DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_COLUMN,
-        DISPLAY_TABLE_COLUMN_GROUP, DISPLAY_TABLE_CELL, DISPLAY_TABLE_CAPTION,
-        DISPLAY_TABLE_HEADER_GROUP, DISPLAY_TABLE_FOOTER_GROUP:
-      result{"display"} = DISPLAY_BLOCK
-    of DISPLAY_INLINE_TABLE:
-      result{"display"} = DISPLAY_TABLE
+    #TODO it may be better to handle this in layout
+    let display = result{"display"}.blockify()
+    if display != result{"display"}:
+      result{"display"} = display
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index ca5e506f..a32decf0 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -88,18 +88,10 @@ type
 
   TableCaptionBoxBuilder = ref object of BlockBoxBuilder
 
-#TODO ?
-func stretch(sc: SizeConstraint): SizeConstraint =
-  case sc.t
-  of MIN_CONTENT, MAX_CONTENT:
-    return sc
-  of STRETCH, FIT_CONTENT:
-    return SizeConstraint(t: STRETCH, u: sc.u)
-
 func fitContent(sc: SizeConstraint): SizeConstraint =
   case sc.t
   of MIN_CONTENT, MAX_CONTENT:
-    return SizeConstraint(t: sc.t)
+    return sc
   of STRETCH, FIT_CONTENT:
     return SizeConstraint(t: FIT_CONTENT, u: sc.u)
 
@@ -1133,12 +1125,12 @@ proc resolveFloatSizes(lctx: LayoutState, containingWidth,
 # differs for the root height (TODO: and all heights in quirks mode) in that
 # it uses the lctx height. Therefore we pass percHeight as a separate
 # parameter. (TODO surely there is a better solution to this?)
-proc resolveSizes(lctx: LayoutState, containingWidth,
-    containingHeight: SizeConstraint, percHeight: Option[LayoutUnit],
-    computed: CSSComputedValues): ResolvedSizes =
+proc resolveSizes(lctx: LayoutState; containingWidth,
+    containingHeight: SizeConstraint; percHeight: Option[LayoutUnit];
+    computed: CSSComputedValues; flexItem = false): ResolvedSizes =
   if computed{"position"} == POSITION_ABSOLUTE:
     return lctx.resolveAbsoluteSizes(computed)
-  elif computed{"float"} != FLOAT_NONE:
+  elif computed{"float"} != FLOAT_NONE or flexItem:
     return lctx.resolveFloatSizes(containingWidth, containingHeight,
       percHeight, computed)
   else:
@@ -1159,12 +1151,14 @@ proc append(a: var Strut, b: LayoutUnit) =
 func sum(a: Strut): LayoutUnit =
   return a.pos + a.neg
 
-proc layoutRootInline(bctx: var BlockContext, inlines: seq[BoxBuilder],
-  space: AvailableSpace, computed: CSSComputedValues, offset,
-  bfcOffset: Offset): RootInlineFragment
-proc layoutBlock(bctx: var BlockContext, box: BlockBox,
-  builder: BlockBoxBuilder, sizes: ResolvedSizes)
-proc layoutTable(lctx: LayoutState, table: BlockBox, builder: TableBoxBuilder,
+proc layoutRootInline(bctx: var BlockContext; inlines: seq[BoxBuilder];
+  space: AvailableSpace; computed: CSSComputedValues;
+  offset, bfcOffset: Offset): RootInlineFragment
+proc layoutBlock(bctx: var BlockContext; box: BlockBox;
+  builder: BlockBoxBuilder; sizes: ResolvedSizes)
+proc layoutTable(lctx: LayoutState; table: BlockBox; builder: TableBoxBuilder;
+  sizes: ResolvedSizes)
+proc layoutFlex(bctx: var BlockContext; box: BlockBox; builder: BlockBoxBuilder;
   sizes: ResolvedSizes)
 
 # Note: padding must still be applied after this.
@@ -1335,14 +1329,11 @@ func establishesBFC(computed: CSSComputedValues): bool =
   return computed{"float"} != FLOAT_NONE or
     computed{"position"} == POSITION_ABSOLUTE or
     computed{"display"} in {DISPLAY_INLINE_BLOCK, DISPLAY_FLOW_ROOT} +
-      InternalTableBox
-    #TODO overflow, contain, flex, grid, multicol, column-span
+      InternalTableBox + {DISPLAY_FLEX, DISPLAY_INLINE_FLEX}
+    #TODO overflow, contain, grid, multicol, column-span
 
-proc layoutFlow(bctx: var BlockContext, box: BlockBox, builder: BlockBoxBuilder,
+proc layoutFlow(bctx: var BlockContext; box: BlockBox; builder: BlockBoxBuilder;
     sizes: ResolvedSizes) =
-  let isBfc = builder.computed.establishesBFC()
-  if not isBfc:
-    bctx.marginTodo.append(sizes.margin.top)
   if builder.canFlushMargins(sizes):
     bctx.flushMargins(box)
     bctx.positionFloats()
@@ -1354,8 +1345,6 @@ proc layoutFlow(bctx: var BlockContext, box: BlockBox, builder: BlockBoxBuilder,
   else:
     # Builder only contains block boxes.
     bctx.layoutBlock(box, builder, sizes)
-  if not isBfc:
-    bctx.marginTodo.append(sizes.margin.bottom)
 
 func toperc100(sc: SizeConstraint): Option[LayoutUnit] =
   if sc.isDefinite():
@@ -1381,6 +1370,8 @@ proc addInlineBlock(ictx: var InlineContext, state: var InlineState,
     bctx.layoutFlow(box, builder, sizes)
   of DISPLAY_INLINE_TABLE:
     lctx.layoutTable(box, TableBoxBuilder(builder), sizes)
+  of DISPLAY_INLINE_FLEX:
+    bctx.layoutFlex(box, builder, sizes)
   else:
     assert false, $builder.computed{"display"}
   bctx.positionFloats()
@@ -1451,7 +1442,7 @@ proc layoutInline(ictx: var InlineContext; box: InlineBoxBuilder):
     of DISPLAY_INLINE:
       let child = ictx.layoutInline(InlineBoxBuilder(child))
       state.fragment.children.add(child)
-    of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE:
+    of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE, DISPLAY_INLINE_FLEX:
       # Note: we do not need a separate inline fragment here, because the tree
       # generator already does an iflush() before adding inline blocks.
       let w = fitContent(ictx.space.w)
@@ -1519,28 +1510,9 @@ proc layoutRootInline(bctx: var BlockContext, inlines: seq[BoxBuilder],
   root.xminwidth = ictx.minwidth
   return root
 
-proc buildMarker(builder: MarkerBoxBuilder, space: AvailableSpace,
-    lctx: LayoutState): RootInlineFragment =
-  let space = AvailableSpace(
-    w: fitContent(space.w),
-    h: space.h
-  )
-  #TODO we should put markers right before the first atom of the parent
-  # list item or something...
-  var bctx = BlockContext(lctx: lctx)
-  let children = @[BoxBuilder(builder)]
-  return bctx.layoutRootInline(children, space, builder.computed, Offset(),
-    Offset())
-
 # Build a block box without establishing a new block formatting context.
-proc buildBlock(bctx: var BlockContext, builder: BlockBoxBuilder,
-    space: AvailableSpace, offset: Offset): BlockBox =
-  let lctx = bctx.lctx
-  let availableWidth = space.w
-  let availableHeight = maxContent() #TODO fit-content when clip
-  let percHeight = space.h.toPercSize()
-  let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight,
-    builder.computed)
+proc buildBlock(bctx: var BlockContext; builder: BlockBoxBuilder;
+    sizes: ResolvedSizes; offset: Offset): BlockBox =
   let box = BlockBox(
     computed: builder.computed,
     node: builder.node,
@@ -1550,14 +1522,8 @@ proc buildBlock(bctx: var BlockContext, builder: BlockBoxBuilder,
   bctx.layoutFlow(box, builder, sizes)
   return box
 
-proc buildListItem(bctx: var BlockContext, builder: ListItemBoxBuilder,
-    space: AvailableSpace, offset: Offset): ListItemBox =
-  let availableWidth = stretch(space.w)
-  let availableHeight = maxContent() #TODO fit-content when clip
-  let percHeight = space.h.toPercSize()
-  let lctx = bctx.lctx
-  let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight,
-    builder.computed)
+proc buildListItem(bctx: var BlockContext; builder: ListItemBoxBuilder;
+    sizes: ResolvedSizes; offset: Offset): ListItemBox =
   let box = ListItemBox(
     computed: builder.computed,
     node: builder.node,
@@ -1565,31 +1531,25 @@ proc buildListItem(bctx: var BlockContext, builder: ListItemBoxBuilder,
     margin: sizes.margin
   )
   if builder.marker != nil:
-    box.marker = buildMarker(builder.marker, sizes.space, lctx)
+    #TODO we should put markers right before the first atom of the parent
+    # list item or something...
+    var bctx = BlockContext(lctx: bctx.lctx)
+    let children = @[BoxBuilder(builder.marker)]
+    let space = AvailableSpace(w: fitContent(sizes.space.w), h: sizes.space.h)
+    box.marker = bctx.layoutRootInline(children, space, builder.marker.computed,
+      Offset(), Offset())
   bctx.layoutFlow(box, builder.content, sizes)
   return box
 
-proc buildTable(bctx: var BlockContext, builder: TableBoxBuilder,
-    space: AvailableSpace, offset: Offset): BlockBox =
-  let availableWidth = fitContent(space.w)
-  let availableHeight = maxContent() #TODO fit-content when clip
-  let percHeight = space.h.toPercSize()
-  let lctx = bctx.lctx
-  let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight,
-    builder.computed)
+proc buildTable(bctx: var BlockContext; builder: TableBoxBuilder;
+    sizes: ResolvedSizes; offset: Offset): BlockBox =
   let box = BlockBox(
     computed: builder.computed,
     node: builder.node,
     offset: Offset(x: offset.x + sizes.margin.left, y: offset.y),
     margin: sizes.margin
   )
-  let isBfc = builder.computed.establishesBFC()
-  if not isBfc:
-    bctx.marginTodo.append(sizes.margin.top)
-  bctx.flushMargins(box)
-  lctx.layoutTable(box, builder, sizes)
-  if not isBfc:
-    bctx.marginTodo.append(sizes.margin.bottom)
+  bctx.lctx.layoutTable(box, builder, sizes)
   return box
 
 proc positionAbsolute(lctx: LayoutState, box: BlockBox, margin: RelativeRect) =
@@ -1974,9 +1934,8 @@ proc calcUnspecifiedColIndices(ctx: var TableContext, W: var LayoutUnit,
     weight: var float64): seq[int] =
   # Spacing for each column:
   var avail = newSeqUninitialized[int](ctx.cols.len)
-  var i = 0
   var j = 0
-  while i < ctx.cols.len:
+  for i in 0 ..< ctx.cols.len:
     if not ctx.cols[i].wspecified:
       avail[j] = i
       let colw = ctx.cols[i].width
@@ -1990,7 +1949,6 @@ proc calcUnspecifiedColIndices(ctx: var TableContext, W: var LayoutUnit,
     else:
       W -= ctx.cols[i].width
       avail.del(j)
-    inc i
   return avail
 
 func needsRedistribution(ctx: TableContext, computed: CSSComputedValues): bool =
@@ -2121,36 +2079,295 @@ proc postAlignChild(box, child: BlockBox, width: LayoutUnit) =
   else:
     discard
 
-# Build an outer block box inside an existing block formatting context.
-proc layoutBlockChild(bctx: var BlockContext, builder: BoxBuilder,
-    space: AvailableSpace, offset: Offset): BlockBox =
-  let child = case builder.computed{"display"}
+proc buildFlex(bctx: var BlockContext; builder: BlockBoxBuilder;
+    sizes: ResolvedSizes; offset: Offset): BlockBox =
+  let box = BlockBox(
+    computed: builder.computed,
+    node: builder.node,
+    offset: Offset(x: offset.x + sizes.margin.left, y: offset.y),
+    margin: sizes.margin
+  )
+  bctx.layoutFlex(box, builder, sizes)
+  return box
+
+proc layoutFlexChild(lctx: LayoutState; builder: BoxBuilder;
+    sizes: ResolvedSizes): BlockBox =
+  var bctx = BlockContext(lctx: lctx)
+  # note: we do not append margins here, since those belong to the flex item,
+  # not its inner BFC.
+  var offset = Offset()
+  let box = case builder.computed{"display"}
   of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT:
-    bctx.buildBlock(BlockBoxBuilder(builder), space, offset)
+    bctx.buildBlock(BlockBoxBuilder(builder), sizes, offset)
   of DISPLAY_LIST_ITEM:
-    bctx.buildListItem(ListItemBoxBuilder(builder), space, offset)
+    bctx.buildListItem(ListItemBoxBuilder(builder), sizes, offset)
   of DISPLAY_TABLE:
-    bctx.buildTable(TableBoxBuilder(builder), space, offset)
+    bctx.buildTable(TableBoxBuilder(builder), sizes, offset)
+  of DISPLAY_FLEX:
+    bctx.buildFlex(BlockBoxBuilder(builder), sizes, offset)
   else:
     assert false, "builder.t is " & $builder.computed{"display"}
     BlockBox(nil)
-  return child
+  return box
 
-# Establish a new block formatting context and build a block box.
-proc layoutRootBlock(lctx: LayoutState, builder: BoxBuilder,
-    space: AvailableSpace, offset: Offset, marginBottomOut: var LayoutUnit):
-    BlockBox =
-  var bctx = BlockContext(lctx: lctx)
+type
+  FlexWeightType = enum
+    fwtGrow, fwtShrink
+
+  FlexPendingItem = object
+    child: BlockBox
+    builder: BoxBuilder
+    weights: array[FlexWeightType, float64]
+    space: AvailableSpace
+    sizes: ResolvedSizes
+
+  FlexMainContext = object
+    offset: Offset
+    totalSize: Size
+    maxSize: Size
+    totalWeight: array[FlexWeightType, float64]
+    lctx: LayoutState
+    pending: seq[FlexPendingItem]
+
+const FlexReverse = {FLEX_DIRECTION_ROW_REVERSE, FLEX_DIRECTION_COLUMN_REVERSE}
+const FlexRow = {FLEX_DIRECTION_ROW, FLEX_DIRECTION_ROW_REVERSE}
+
+proc redistributeWidth(mctx: var FlexMainContext; sizes: ResolvedSizes) =
+  #TODO actually use flex-basis
+  let lctx = mctx.lctx
+  if sizes.space.w.isDefinite:
+    var diff = sizes.space.w.u - mctx.totalSize.w
+    let wt = if diff > 0: fwtGrow else: fwtShrink
+    var totalWeight = mctx.totalWeight[wt]
+    while (wt == fwtGrow and diff > 0 or wt == fwtShrink and diff < 0) and
+        totalWeight > 0:
+      mctx.maxSize.h = 0 # redo maxSize calculation; we only need height here
+      let unit = diff / totalWeight
+      # reset total weight & available diff for the next iteration (if there is one)
+      totalWeight = 0
+      diff = 0
+      for it in mctx.pending.mitems:
+        let builder = it.builder
+        if it.weights[wt] == 0:
+          mctx.maxSize.h = max(mctx.maxSize.h, it.child.size.h)
+          continue
+        var w = it.child.size.w + unit * it.weights[wt]
+        # check for min/max violation
+        let minw = max(it.child.xminwidth, it.sizes.minWidth)
+        if minw > w:
+          # min violation
+          if wt == fwtShrink: # freeze
+            diff += w - minw
+            it.weights[wt] = 0
+          w = minw
+        let maxw = it.sizes.maxWidth
+        if maxw < w:
+          # max violation
+          if wt == fwtGrow: # freeze
+            diff += w - maxw
+            it.weights[wt] = 0
+          w = maxw
+        it.space.w = stretch(w)
+        it.sizes = lctx.resolveSizes(it.space.w, it.space.h,
+          it.space.h.toPercSize(), builder.computed)
+        totalWeight += it.weights[wt]
+        #TODO we should call this only on freeze, and then put another loop to
+        # the end for non-freezed items
+        it.child = lctx.layoutFlexChild(builder, it.sizes)
+        mctx.maxSize.h = max(mctx.maxSize.h, it.child.size.h)
+
+proc redistributeHeight(mctx: var FlexMainContext; sizes: ResolvedSizes) =
+  let lctx = mctx.lctx
+  if sizes.space.h.isDefinite and mctx.totalSize.h != sizes.space.h.u:
+    var diff = sizes.space.h.u - mctx.totalSize.h
+    let wt = if diff > 0: fwtGrow else: fwtShrink
+    var totalWeight = mctx.totalWeight[wt]
+    while (wt == fwtGrow and diff > 0 or wt == fwtShrink and diff < 0) and
+        totalWeight > 0:
+      mctx.maxSize.w = 0 # redo maxSize calculation; we only need height here
+      let unit = diff / totalWeight
+      # reset total weight & available diff for the next iteration (if there is one)
+      totalWeight = 0
+      diff = 0
+      for it in mctx.pending.mitems:
+        let builder = it.builder
+        if it.weights[wt] == 0:
+          mctx.maxSize.w = max(mctx.maxSize.w, it.child.size.w)
+          continue
+        var h = max(it.child.size.h + unit * it.weights[wt], 0)
+        # check for min/max violation
+        let minh = it.sizes.minHeight
+        if minh > h:
+          # min violation
+          if wt == fwtShrink: # freeze
+            diff += h - minh
+            it.weights[wt] = 0
+          h = minh
+        let maxh = it.sizes.maxHeight
+        if maxh < h:
+          # max violation
+          if wt == fwtGrow: # freeze
+            diff += h - maxh
+            it.weights[wt] = 0
+          h = maxh
+        it.space.h = stretch(h)
+        it.sizes = lctx.resolveSizes(it.space.w, it.space.h,
+          it.space.h.toPercSize(), builder.computed)
+        totalWeight += it.weights[wt]
+        it.child = lctx.layoutFlexChild(builder, it.sizes)
+        mctx.maxSize.h = max(mctx.maxSize.h, it.child.size.h)
+
+proc flushRow(mctx: var FlexMainContext; box: BlockBox; sizes: ResolvedSizes;
+    totalMaxSize: var Size) =
+  let lctx = mctx.lctx
+  mctx.redistributeWidth(sizes)
+  let h = stretch(mctx.maxSize.h)
+  var offset = mctx.offset
+  for it in mctx.pending.mitems:
+    if it.child.size.h < mctx.maxSize.h and not it.sizes.space.h.isDefinite:
+      # if the max height is greater than our height, then take max height
+      # instead. (if the box's available height is definite, then this will
+      # change nothing, so we skip it as an optimization.)
+      it.sizes.space.h = h
+      it.child = lctx.layoutFlexChild(it.builder, it.sizes)
+    it.child.offset = Offset(
+      x: it.child.offset.x + offset.x,
+      # margins are added here, since they belong to the flex item.
+      y: it.child.offset.y + offset.y + it.child.margin.top +
+        it.child.margin.bottom
+    )
+    offset.x += it.child.size.w
+    box.nested.add(it.child)
+  totalMaxSize.w = max(totalMaxSize.w, offset.x)
+  mctx = FlexMainContext(
+    lctx: mctx.lctx,
+    offset: Offset(
+      x: mctx.offset.x,
+      y: mctx.offset.y + mctx.maxSize.h
+    )
+  )
+
+proc flushColumn(mctx: var FlexMainContext; box: BlockBox;
+    sizes: ResolvedSizes; totalMaxSize: var Size) =
+  let lctx = mctx.lctx
+  mctx.redistributeHeight(sizes)
+  let w = stretch(mctx.maxSize.w)
+  var offset = mctx.offset
+  for it in mctx.pending.mitems:
+    if it.child.size.w < mctx.maxSize.w and not it.sizes.space.w.isDefinite:
+      # see above.
+      it.sizes.space.w = w
+      it.child = lctx.layoutFlexChild(it.builder, it.sizes)
+    # margins belong to the flex item, and influence its positioning
+    offset.y += it.child.margin.top
+    it.child.offset = Offset(
+      x: it.child.offset.x + offset.x,
+      y: it.child.offset.y + offset.y
+    )
+    offset.y += it.child.margin.bottom
+    offset.y += it.child.size.h
+    box.nested.add(it.child)
+  totalMaxSize.h = max(totalMaxSize.h, offset.y)
+  mctx = FlexMainContext(
+    lctx: lctx,
+    offset: Offset(
+      x: mctx.offset.x + mctx.maxSize.w,
+      y: mctx.offset.y
+    )
+  )
+
+proc layoutFlex(bctx: var BlockContext; box: BlockBox; builder: BlockBoxBuilder;
+    sizes: ResolvedSizes) =
+  assert not builder.inlinelayout
+  let lctx = bctx.lctx
+  var i = 0
+  var mctx = FlexMainContext(lctx: lctx)
+  let flexDir = builder.computed{"flex-direction"}
+  let children = if builder.computed{"flex-direction"} in FlexReverse:
+    builder.children.reversed()
+  else:
+    builder.children
+  var totalMaxSize = Size() #TODO find a better name for this
+  let canWrap = box.computed{"flex-wrap"} != FLEX_WRAP_NOWRAP
+  let percHeight = sizes.space.h.toPercSize()
+  while i < children.len:
+    let builder = children[i]
+    let childSizes = lctx.resolveFloatSizes(sizes.space.w, sizes.space.h,
+      percHeight, builder.computed)
+    let child = lctx.layoutFlexChild(builder, childSizes)
+    if flexDir in FlexRow:
+      if canWrap and (sizes.space.w.t == MIN_CONTENT or
+          sizes.space.w.isDefinite and
+          mctx.totalSize.w + child.size.w > sizes.space.w.u):
+        mctx.flushRow(box, sizes, totalMaxSize)
+      mctx.totalSize.w += child.size.w
+    else:
+      if canWrap and (sizes.space.h.t == MIN_CONTENT or
+          sizes.space.h.isDefinite and
+          mctx.totalSize.h + child.size.h > sizes.space.h.u):
+        mctx.flushRow(box, sizes, totalMaxSize)
+      mctx.totalSize.h += child.size.h
+    mctx.maxSize.w = max(mctx.maxSize.w, child.size.w)
+    mctx.maxSize.h = max(mctx.maxSize.h, child.size.h)
+    let grow = builder.computed{"flex-grow"}
+    let shrink = builder.computed{"flex-shrink"}
+    mctx.totalWeight[fwtGrow] += grow
+    mctx.totalWeight[fwtShrink] += shrink
+    mctx.pending.add(FlexPendingItem(
+      child: child,
+      builder: builder,
+      weights: [grow, shrink],
+      space: sizes.space,
+      sizes: childSizes
+    ))
+    inc i # need to increment index here for needsGrow
+  if flexDir in FlexRow:
+    if mctx.pending.len > 0:
+      mctx.flushRow(box, sizes, totalMaxSize)
+    box.applyWidth(sizes, totalMaxSize.w)
+    box.applyHeight(sizes, mctx.offset.y)
+  else:
+    if mctx.pending.len > 0:
+      mctx.flushColumn(box, sizes, totalMaxSize)
+    box.applyWidth(sizes, mctx.offset.x)
+    box.applyHeight(sizes, totalMaxSize.h)
+
+# Build an outer block box inside an existing block formatting context.
+proc layoutBlockChild(bctx: var BlockContext; builder: BoxBuilder;
+    space: AvailableSpace; offset: Offset; appendMargins: bool): BlockBox =
+  let availHeight = maxContent() #TODO also fit-content when clip
+  let availWidth = if builder.computed{"display"} == DISPLAY_TABLE:
+    fitContent(space.w)
+  else:
+    space.w
+  let sizes = bctx.lctx.resolveSizes(availWidth, availHeight,
+    space.h.toPercSize(), builder.computed)
+  if appendMargins:
+    # for nested blocks that do not establish their own BFC, and thus take part
+    # in margin collapsing.
+    bctx.marginTodo.append(sizes.margin.top)
   let box = case builder.computed{"display"}
   of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT:
-    bctx.buildBlock(BlockBoxBuilder(builder), space, offset)
+    bctx.buildBlock(BlockBoxBuilder(builder), sizes, offset)
   of DISPLAY_LIST_ITEM:
-    bctx.buildListItem(ListItemBoxBuilder(builder), space, offset)
+    bctx.buildListItem(ListItemBoxBuilder(builder), sizes, offset)
   of DISPLAY_TABLE:
-    bctx.buildTable(TableBoxBuilder(builder), space, offset)
+    bctx.buildTable(TableBoxBuilder(builder), sizes, offset)
+  of DISPLAY_FLEX:
+    bctx.buildFlex(BlockBoxBuilder(builder), sizes, offset)
   else:
     assert false, "builder.t is " & $builder.computed{"display"}
     BlockBox(nil)
+  if appendMargins:
+    bctx.marginTodo.append(sizes.margin.bottom)
+  return box
+
+# Establish a new block formatting context and build a block box.
+proc layoutRootBlock(lctx: LayoutState; builder: BoxBuilder;
+    space: AvailableSpace; offset: Offset; marginBottomOut: var LayoutUnit):
+    BlockBox =
+  var bctx = BlockContext(lctx: lctx)
+  let box = bctx.layoutBlockChild(builder, space, offset, appendMargins = false)
   bctx.positionFloats()
   marginBottomOut = bctx.marginTodo.sum()
   # If the highest float edge is higher than the box itself, set that as
@@ -2227,7 +2444,8 @@ proc layoutBlockChildren(state: var BlockState, bctx: var BlockContext,
       # of margin todo in bctx2 (margin-bottom) + height.
       dy = child.offset.y - state.offset.y + child.size.h + marginBottomOut
     else:
-      child = bctx.layoutBlockChild(builder, state.space, state.offset)
+      child = bctx.layoutBlockChild(builder, state.space, state.offset,
+        appendMargins = true)
       # delta y is difference between old and new offsets (margin-top),
       # plus height.
       dy = child.offset.y - state.offset.y + child.size.h
@@ -2406,7 +2624,7 @@ proc add(blockgroup: var BlockGroup, box: BoxBuilder) {.inline.} =
     DISPLAY_INLINE_BLOCK}, $box.computed{"display"}
   blockgroup.boxes.add(box)
 
-proc flush(blockgroup: var BlockGroup) {.inline.} =
+proc flush(blockgroup: var BlockGroup) =
   if blockgroup.boxes.len > 0:
     assert blockgroup.parent.computed{"display"} != DISPLAY_INLINE
     let computed = blockgroup.parent.computed.inheritProperties()
@@ -2426,7 +2644,7 @@ func canGenerateAnonymousInline(blockgroup: BlockGroup,
 
 proc newBlockGroup(parent: BlockBoxBuilder): BlockGroup =
   assert parent.computed{"display"} != DISPLAY_INLINE
-  result.parent = parent
+  return BlockGroup(parent: parent)
 
 proc generateTableBox(styledNode: StyledNode, lctx: LayoutState,
   parent: var InnerBlockContext): TableBoxBuilder
@@ -2441,12 +2659,18 @@ proc generateTableCaptionBox(styledNode: StyledNode, lctx: LayoutState,
 proc generateBlockBox(styledNode: StyledNode, lctx: LayoutState,
   marker = none(MarkerBoxBuilder), parent: ptr InnerBlockContext = nil):
   BlockBoxBuilder
+proc generateFlexBox(styledNode: StyledNode; lctx: LayoutState;
+  parent: ptr InnerBlockContext = nil): BlockBoxBuilder
 proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode)
 
-proc generateBlockBox(pctx: var InnerBlockContext, styledNode: StyledNode,
+proc generateBlockBox(pctx: var InnerBlockContext; styledNode: StyledNode;
     marker = none(MarkerBoxBuilder)): BlockBoxBuilder =
   return generateBlockBox(styledNode, pctx.lctx, marker, addr pctx)
 
+proc generateFlexBox(pctx: var InnerBlockContext; styledNode: StyledNode):
+    BlockBoxBuilder =
+  return generateFlexBox(styledNode, pctx.lctx, addr pctx)
+
 proc flushTableRow(ctx: var InnerBlockContext) =
   if ctx.anonRow != nil:
     if ctx.blockgroup.parent.computed{"display"} == DISPLAY_TABLE_ROW:
@@ -2502,15 +2726,19 @@ proc reconstructInlineParents(ctx: var InnerBlockContext): InlineBoxBuilder =
     parent = nbox
   return parent
 
-proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
+proc generateFromElem(ctx: var InnerBlockContext; styledNode: StyledNode) =
   let box = ctx.blockgroup.parent
-
   case styledNode.computed{"display"}
   of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT:
     ctx.iflush()
     ctx.flush()
     let childbox = ctx.generateBlockBox(styledNode)
     box.children.add(childbox)
+  of DISPLAY_FLEX:
+    ctx.iflush()
+    ctx.flush()
+    let childbox = ctx.generateFlexBox(styledNode)
+    box.children.add(childbox)
   of DISPLAY_LIST_ITEM:
     ctx.flush()
     inc ctx.listItemCounter
@@ -2528,7 +2756,7 @@ proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
     box.children.add(childbox)
   of DISPLAY_INLINE:
     ctx.generateInlineBoxes(styledNode)
-  of DISPLAY_INLINE_BLOCK:
+  of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE, DISPLAY_INLINE_FLEX:
     # create a new inline box that we can safely put our inline block into
     ctx.iflush()
     let computed = styledNode.computed.inheritProperties()
@@ -2539,7 +2767,14 @@ proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
       ctx.iroot = iparent
     else:
       ctx.iroot = ctx.ibox
-    let childbox = ctx.generateBlockBox(styledNode)
+    var childbox: BoxBuilder
+    if styledNode.computed{"display"} == DISPLAY_INLINE_BLOCK:
+      childbox = ctx.generateBlockBox(styledNode)
+    elif styledNode.computed{"display"} == DISPLAY_INLINE_TABLE:
+      childbox = styledNode.generateTableBox(ctx.lctx, ctx)
+    else:
+      assert styledNode.computed{"display"} == DISPLAY_INLINE_FLEX
+      childbox = ctx.generateFlexBox(styledNode)
     ctx.ibox.children.add(childbox)
     ctx.iflush()
   of DISPLAY_TABLE:
@@ -2584,20 +2819,6 @@ proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
         wrappervals{"display"} = DISPLAY_TABLE_ROW
         ctx.anonRow = TableRowBoxBuilder(computed: wrappervals)
       ctx.anonRow.children.add(childbox)
-  of DISPLAY_INLINE_TABLE:
-    # create a new inline box that we can safely put our inline block into
-    ctx.iflush()
-    let computed = styledNode.computed.inheritProperties()
-    ctx.ibox = InlineBoxBuilder(computed: computed, node: styledNode)
-    if ctx.inlineStack.len > 0:
-      let iparent = ctx.reconstructInlineParents()
-      iparent.children.add(ctx.ibox)
-      ctx.iroot = iparent
-    else:
-      ctx.iroot = ctx.ibox
-    let childbox = styledNode.generateTableBox(ctx.lctx, ctx)
-    ctx.ibox.children.add(childbox)
-    ctx.iflush()
   of DISPLAY_TABLE_CAPTION:
     ctx.bflush()
     ctx.flushTableRow()
@@ -2703,19 +2924,20 @@ proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode) =
 
 proc newInnerBlockContext(styledNode: StyledNode, box: BlockBoxBuilder,
     lctx: LayoutState, parent: ptr InnerBlockContext): InnerBlockContext =
-  result = InnerBlockContext(
+  var ctx = InnerBlockContext(
     styledNode: styledNode,
     blockgroup: newBlockGroup(box),
     lctx: lctx,
     parent: parent
   )
   if parent != nil:
-    result.listItemCounter = parent[].listItemCounter
-    result.quoteLevel = parent[].quoteLevel
+    ctx.listItemCounter = parent[].listItemCounter
+    ctx.quoteLevel = parent[].quoteLevel
   for reset in styledNode.computed{"counter-reset"}:
     if reset.name == "list-item":
-      result.listItemCounter = reset.num
-      result.listItemReset = true
+      ctx.listItemCounter = reset.num
+      ctx.listItemReset = true
+  return ctx
 
 proc generateInnerBlockBox(ctx: var InnerBlockContext) =
   let box = ctx.blockgroup.parent
@@ -2732,25 +2954,21 @@ proc generateInnerBlockBox(ctx: var InnerBlockContext) =
       ctx.generateReplacement(child, ctx.styledNode)
   ctx.iflush()
 
-proc generateBlockBox(styledNode: StyledNode, lctx: LayoutState,
-    marker = none(MarkerBoxBuilder), parent: ptr InnerBlockContext = nil):
+proc generateBlockBox(styledNode: StyledNode; lctx: LayoutState;
+    marker = none(MarkerBoxBuilder); parent: ptr InnerBlockContext = nil):
     BlockBoxBuilder =
   let box = BlockBoxBuilder(computed: styledNode.computed)
   box.node = styledNode
   var ctx = newInnerBlockContext(styledNode, box, lctx, parent)
-
   if marker.isSome:
     ctx.ibox = marker.get
     ctx.iflush()
-
   ctx.generateInnerBlockBox()
-
   # Flush anonymous tables here, to avoid setting inline layout with tables.
   ctx.flushTableRow()
   ctx.flushTable()
   # (flush here, because why not)
   ctx.flushInherit()
-
   # Avoid unnecessary anonymous block boxes. This also helps set our layout to
   # inline even if no inner anonymous block was generated.
   if box.children.len == 0:
@@ -2760,6 +2978,42 @@ proc generateBlockBox(styledNode: StyledNode, lctx: LayoutState,
   ctx.blockgroup.flush()
   return box
 
+proc generateFlexBox(styledNode: StyledNode; lctx: LayoutState;
+    parent: ptr InnerBlockContext = nil): BlockBoxBuilder =
+  let box = BlockBoxBuilder(computed: styledNode.computed, node: styledNode)
+  var ctx = newInnerBlockContext(styledNode, box, lctx, parent)
+  assert box.computed{"display"} != DISPLAY_INLINE
+  for child in ctx.styledNode.children:
+    case child.t
+    of STYLED_ELEMENT:
+      ctx.iflush()
+      let display = child.computed{"display"}.blockify()
+      if display != child.computed{"display"}:
+        #TODO this is a hack.
+        # it exists because passing down a different `computed' would need
+        # changes in way too many procedures, which I am not ready to make yet.
+        let newChild = StyledNode()
+        newChild[] = child[]
+        newChild.computed = child.computed.copyProperties()
+        newChild.computed{"display"} = display
+        ctx.generateFromElem(newChild)
+      else:
+        ctx.generateFromElem(child)
+    of STYLED_TEXT:
+      if ctx.blockgroup.canGenerateAnonymousInline(box.computed, child.text):
+        ctx.generateAnonymousInlineText(child.text, ctx.styledNode)
+    of STYLED_REPLACEMENT:
+      ctx.generateReplacement(child, ctx.styledNode)
+  ctx.iflush()
+  # Flush anonymous tables here, to avoid setting inline layout with tables.
+  ctx.flushTableRow()
+  ctx.flushTable()
+  # (flush here, because why not)
+  ctx.flushInherit()
+  ctx.blockgroup.flush()
+  assert not box.inlinelayout
+  return box
+
 proc generateTableCellBox(styledNode: StyledNode, lctx: LayoutState,
     parent: var InnerBlockContext): TableCellBoxBuilder =
   let box = TableCellBoxBuilder(computed: styledNode.computed)
diff --git a/todo b/todo
index 0dc4736d..a7ef748a 100644
--- a/todo
+++ b/todo
@@ -67,13 +67,17 @@ layout engine:
 	  where w3m's space distribution algorithm does not work really well :/
 - do not break inline boxes with out-of-flow block boxes (float, absolute, etc.)
 	* this seems hard to fix properly :(
+	* reminder: this does *not* apply to flexbox; in fact it has the inverse
+	  problem AFAICT.
 - table layout: include caption in width calculation
+- flexbox: flex-basis, align-self, align-items, justify-content, proper margin
+  handling
 - details element
 - overflow
 - incremental layout & layout caching
 	* first for tree generation, then for layout.
 - iframe
-- writing-mode, flexbox, grid, ruby, ... (i.e. cool new stuff)
+- writing-mode, grid, ruby, ... (i.e. cool new stuff)
 images:
 - sixel encoding (eventually also kitty) -> actually display them :P
 - more formats (apng, gif: write own decoders, jpeg: use libjpeg, webp: ?)