diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/css/values.nim | 196 | ||||
-rw-r--r-- | src/layout/engine.nim | 504 |
2 files changed, 537 insertions, 163 deletions
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) |