diff options
author | bptato <nincsnevem662@gmail.com> | 2023-11-12 02:58:00 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-11-12 02:59:59 +0100 |
commit | 39e2ef5207ad13c34de00c8cc998e434eeb42ab7 (patch) | |
tree | 98cc813f4945327ef205708467676d43a8199c11 /src/layout | |
parent | a3bd24ae53ea4e1d1c77dfc5a1171a05b5d9ca18 (diff) | |
download | chawan-39e2ef5207ad13c34de00c8cc998e434eeb42ab7.tar.gz |
layout: refactor flow margin propagation, sizing
* Blocks are now positioned before their text contents would be layouted * Untangle calcAvailableSpaceSizes's results from BlockBox * Move a couple of objects from box -> engine * Use Size in a few more places * Set display to block if float is not none
Diffstat (limited to 'src/layout')
-rw-r--r-- | src/layout/box.nim | 111 | ||||
-rw-r--r-- | src/layout/engine.nim | 1118 |
2 files changed, 619 insertions, 610 deletions
diff --git a/src/layout/box.nim b/src/layout/box.nim index da531b56..56bfb47b 100644 --- a/src/layout/box.nim +++ b/src/layout/box.nim @@ -1,5 +1,3 @@ -import options - import css/stylednode import css/values import layout/layoutunit @@ -14,51 +12,6 @@ type w*: LayoutUnit h*: LayoutUnit - # min-content: box width is longest word's width - # max-content: box width is content width without wrapping - # stretch: box width is n px wide - # fit-content: also known as shrink-to-fit, box width is - # min(max-content, stretch(availableWidth)) - # in other words, as wide as needed, but wrap if wider than allowed - # (note: I write width here, but it can apply for any constraint) - SizeConstraintType* = enum - STRETCH, FIT_CONTENT, MIN_CONTENT, MAX_CONTENT - - SizeConstraint* = object - t*: SizeConstraintType - u*: LayoutUnit - - BoxBuilder* = ref object of RootObj - children*: seq[BoxBuilder] - inlinelayout*: bool - computed*: CSSComputedValues - node*: StyledNode - - InlineBoxBuilder* = ref object of BoxBuilder - text*: seq[string] - newline*: bool - splitstart*: bool - splitend*: bool - - BlockBoxBuilder* = ref object of BoxBuilder - - MarkerBoxBuilder* = ref object of InlineBoxBuilder - - ListItemBoxBuilder* = ref object of BoxBuilder - marker*: MarkerBoxBuilder - content*: BlockBoxBuilder - - TableRowGroupBoxBuilder* = ref object of BlockBoxBuilder - - TableRowBoxBuilder* = ref object of BlockBoxBuilder - - TableCellBoxBuilder* = ref object of BlockBoxBuilder - - TableBoxBuilder* = ref object of BlockBoxBuilder - rowgroups*: seq[TableRowGroupBoxBuilder] - - TableCaptionBoxBuilder* = ref object of BlockBoxBuilder - InlineAtomType* = enum INLINE_SPACING, INLINE_PADDING, INLINE_WORD, INLINE_BLOCK @@ -92,8 +45,7 @@ type InlineContext* = ref object offset*: Offset - height*: LayoutUnit - width*: LayoutUnit + size*: Size lines*: seq[LineBox] # baseline of the first line box @@ -104,42 +56,27 @@ type # this is actually xminwidth. minwidth*: LayoutUnit + RelativeRect* = object + top*: LayoutUnit + bottom*: LayoutUnit + left*: LayoutUnit + right*: LayoutUnit + BlockBox* = ref object of RootObj inline*: InlineContext node*: StyledNode nested*: seq[BlockBox] computed*: CSSComputedValues offset*: Offset - - # This is the padding width/height. - width*: LayoutUnit - height*: LayoutUnit - margin_top*: LayoutUnit - margin_bottom*: LayoutUnit - margin_left*: LayoutUnit - margin_right*: LayoutUnit - padding_top*: LayoutUnit - padding_bottom*: LayoutUnit - padding_left*: LayoutUnit - padding_right*: LayoutUnit - min_width*: Option[LayoutUnit] - max_width*: Option[LayoutUnit] - min_height*: Option[LayoutUnit] - max_height*: Option[LayoutUnit] - - # width and height constraints - availableWidth*: SizeConstraint - availableHeight*: SizeConstraint - + size*: Size # padding size + margin*: RelativeRect #TODO get rid of this? positioned*: bool x_positioned*: bool y_positioned*: bool - # very bad name. basically the minimum content width after the contents # have been positioned (usually the width of the shortest word.) used # in table cells. xminwidth*: LayoutUnit - # baseline of the first line box of all descendants firstBaseline*: LayoutUnit # baseline of the last line box of all descendants @@ -147,33 +84,3 @@ type ListItemBox* = ref object of BlockBox marker*: InlineContext - -func minContent*(): SizeConstraint = - return SizeConstraint(t: MIN_CONTENT) - -func maxContent*(): SizeConstraint = - return SizeConstraint(t: MAX_CONTENT) - -func stretch*(u: LayoutUnit): SizeConstraint = - return SizeConstraint(t: STRETCH, u: u) - -func fitContent*(u: LayoutUnit): SizeConstraint = - return SizeConstraint(t: FIT_CONTENT, u: u) - -#TODO ? -func stretch*(sc: SizeConstraint): SizeConstraint = - case sc.t - of MIN_CONTENT, MAX_CONTENT: - return SizeConstraint(t: sc.t, u: sc.u) - 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) - of STRETCH, FIT_CONTENT: - return SizeConstraint(t: FIT_CONTENT, u: sc.u) - -func isDefinite*(sc: SizeConstraint): bool = - return sc.t in {STRETCH, FIT_CONTENT} diff --git a/src/layout/engine.nim b/src/layout/engine.nim index fde7b0cb..a0850be7 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -10,9 +10,96 @@ import layout/box import layout/layoutunit import utils/twtstr -type LayoutState = ref object - attrs: WindowAttributes - positioned: seq[BlockBox] +type + LayoutState = ref object + attrs: WindowAttributes + positioned: seq[ResolvedSizes] + + AvailableSpace = object + w: SizeConstraint + h: SizeConstraint + + # min-content: box width is longest word's width + # max-content: box width is content width without wrapping + # stretch: box width is n px wide + # fit-content: also known as shrink-to-fit, box width is + # min(max-content, stretch(availableWidth)) + # in other words, as wide as needed, but wrap if wider than allowed + # (note: I write width here, but it can apply for any constraint) + SizeConstraintType = enum + STRETCH, FIT_CONTENT, MIN_CONTENT, MAX_CONTENT + + SizeConstraint = object + t: SizeConstraintType + u: LayoutUnit + + ResolvedSizes = object + margin: RelativeRect + padding: RelativeRect + space: AvailableSpace + min_width: Option[LayoutUnit] + max_width: Option[LayoutUnit] + min_height: Option[LayoutUnit] + max_height: Option[LayoutUnit] + +func maxContent(): SizeConstraint = + return SizeConstraint(t: MAX_CONTENT) + +func stretch(u: LayoutUnit): SizeConstraint = + return SizeConstraint(t: STRETCH, u: u) + +func fitContent(u: LayoutUnit): SizeConstraint = + return SizeConstraint(t: FIT_CONTENT, u: u) + +type + BoxBuilder = ref object of RootObj + children: seq[BoxBuilder] + inlinelayout: bool + computed: CSSComputedValues + node: StyledNode + + InlineBoxBuilder = ref object of BoxBuilder + text: seq[string] + newline: bool + splitstart: bool + splitend: bool + + BlockBoxBuilder = ref object of BoxBuilder + + MarkerBoxBuilder = ref object of InlineBoxBuilder + + ListItemBoxBuilder = ref object of BoxBuilder + marker: MarkerBoxBuilder + content: BlockBoxBuilder + + TableRowGroupBoxBuilder = ref object of BlockBoxBuilder + + TableRowBoxBuilder = ref object of BlockBoxBuilder + + TableCellBoxBuilder = ref object of BlockBoxBuilder + + TableBoxBuilder = ref object of BlockBoxBuilder + rowgroups: seq[TableRowGroupBoxBuilder] + + 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) + of STRETCH, FIT_CONTENT: + return SizeConstraint(t: FIT_CONTENT, u: sc.u) + +func isDefinite(sc: SizeConstraint): bool = + return sc.t in {STRETCH, FIT_CONTENT} # Build phase func px(l: CSSLength, lctx: LayoutState, p: LayoutUnit = 0): @@ -81,8 +168,7 @@ type whitespacenum: int format: ComputedFormat lctx: LayoutState - availableWidth: SizeConstraint - availableHeight: SizeConstraint + space: AvailableSpace func whitespacepre(computed: CSSComputedValues): bool = computed{"white-space"} in {WHITESPACE_PRE, WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP} @@ -175,13 +261,13 @@ proc newWord(state: var InlineState) = proc horizontalAlignLine(state: var InlineState, line: var LineBox, computed: CSSComputedValues, last = false) = - let width = case state.availableWidth.t + let width = case state.space.w.t of MIN_CONTENT, MAX_CONTENT: - state.ictx.width + state.ictx.size.w of FIT_CONTENT: - min(state.ictx.width, state.availableWidth.u) + min(state.ictx.size.w, state.space.w.u) of STRETCH: - max(state.ictx.width, state.availableWidth.u) + max(state.ictx.size.w, state.space.w.u) # we don't support directions for now so left = start and right = end case computed{"text-align"} of TEXT_ALIGN_START, TEXT_ALIGN_LEFT, TEXT_ALIGN_CHA_LEFT: @@ -191,14 +277,14 @@ proc horizontalAlignLine(state: var InlineState, line: var LineBox, let x = max(width, line.size.w) - line.size.w for atom in line.atoms.mitems: atom.offset.x += x - state.ictx.width = max(atom.offset.x + atom.size.w, state.ictx.width) + state.ictx.size.w = max(atom.offset.x + atom.size.w, state.ictx.size.w) of TEXT_ALIGN_CENTER, TEXT_ALIGN_CHA_CENTER: # NOTE if we need line x offsets, use: #let width = width - line.offset.x let x = max((max(width, line.size.w)) div 2 - line.size.w div 2, 0) for atom in line.atoms.mitems: atom.offset.x += x - state.ictx.width = max(atom.offset.x + atom.size.w, state.ictx.width) + state.ictx.size.w = max(atom.offset.x + atom.size.w, state.ictx.size.w) of TEXT_ALIGN_JUSTIFY: if not computed.whitespacepre and not last: var sumwidth: LayoutUnit = 0 @@ -218,7 +304,7 @@ proc horizontalAlignLine(state: var InlineState, line: var LineBox, if atom.t == INLINE_SPACING: atom.size.w = spacingwidth line.size.w += atom.size.w - state.ictx.width = max(width, state.ictx.width) #TODO this seems meaningless? + state.ictx.size.w = max(width, state.ictx.size.w) #TODO this seems meaningless? # Align atoms (inline boxes, text, etc.) vertically (i.e. along the inline # axis) inside the line. @@ -374,8 +460,8 @@ proc finishLine(state: var InlineState, computed: CSSComputedValues, if state.ictx.lines.len == 0: state.ictx.firstBaseline = y + state.currentLine.baseline state.ictx.baseline = y + state.currentLine.baseline - state.ictx.height += state.currentLine.size.h - state.ictx.width = max(state.ictx.width, state.currentLine.size.w) + state.ictx.size.h += state.currentLine.size.h + state.ictx.size.w = max(state.ictx.size.w, state.currentLine.size.w) state.ictx.lines.add(state.currentLine.line) state.currentLine = LineBoxState( line: LineBox(offsety: y + state.currentLine.size.h) @@ -398,11 +484,11 @@ func shouldWrap(state: InlineState, w: LayoutUnit, pcomputed: CSSComputedValues): bool = if pcomputed != nil and pcomputed.nowrap: return false - if state.availableWidth.t == MAX_CONTENT: + if state.space.w.t == MAX_CONTENT: return false # no wrap with max-content - if state.availableWidth.t == MIN_CONTENT: + if state.space.w.t == MIN_CONTENT: return true # always wrap with min-content - return state.currentLine.size.w + w > state.availableWidth.u + return state.currentLine.size.w + w > state.space.w.u # pcomputed: computed values of parent, for white-space: pre, line-height. # This isn't necessarily the computed of ictx (e.g. they may differ for nested @@ -510,16 +596,14 @@ proc processWhitespace(state: var InlineState, c: char) = else: inc state.whitespacenum -func newInlineState(lctx: LayoutState, - availableWidth, availableHeight: SizeConstraint): InlineState = +func newInlineState(lctx: LayoutState, space: AvailableSpace): InlineState = return InlineState( currentLine: LineBoxState( line: LineBox() ), ictx: InlineContext(), lctx: lctx, - availableWidth: availableWidth, - availableHeight: availableHeight + space: space ) proc layoutText(state: var InlineState, str: string, @@ -557,125 +641,128 @@ proc layoutText(state: var InlineState, str: string, func isOuterBlock(computed: CSSComputedValues): bool = return computed{"display"} in {DISPLAY_BLOCK, DISPLAY_TABLE} -proc resolveContentWidth(box: BlockBox, widthpx: LayoutUnit, - containingWidth: SizeConstraint, isauto = false) = - if not box.computed.isOuterBlock: +proc resolveContentWidth(sizes: var ResolvedSizes, widthpx: LayoutUnit, + containingWidth: SizeConstraint, computed: CSSComputedValues, + isauto = false) = + if not computed.isOuterBlock: #TODO this is probably needed to avoid double-margin, but it's ugly and # probably also broken. return - if box.availableWidth.t notin {STRETCH, FIT_CONTENT}: + if not sizes.space.w.isDefinite(): # width is indefinite, so no conflicts can be resolved here. return - let computed = box.computed - let total = widthpx + box.margin_left + box.margin_right + - box.padding_left + box.padding_right + let total = widthpx + sizes.margin.left + sizes.margin.right + + sizes.padding.left + sizes.padding.right let underflow = containingWidth.u - total - if isauto or box.availableWidth.t == FIT_CONTENT: + if isauto or sizes.space.w.t == FIT_CONTENT: if underflow >= 0: - box.availableWidth = SizeConstraint(t: box.availableWidth.t, u: underflow) + sizes.space.w = SizeConstraint(t: sizes.space.w.t, u: underflow) else: - box.margin_right += underflow + sizes.margin.right += underflow elif underflow > 0: if not computed{"margin-left"}.auto and not computed{"margin-right"}.auto: - box.margin_right += underflow + sizes.margin.right += underflow elif not computed{"margin-left"}.auto and computed{"margin-right"}.auto: - box.margin_right = underflow + sizes.margin.right = underflow elif computed{"margin-left"}.auto and not computed{"margin-right"}.auto: - box.margin_left = underflow + sizes.margin.left = underflow else: - box.margin_left = underflow div 2 - box.margin_right = underflow div 2 + sizes.margin.left = underflow div 2 + sizes.margin.right = underflow div 2 -proc resolveMargins(box: BlockBox, availableWidth: SizeConstraint, - lctx: LayoutState) = - let computed = box.computed +proc resolveMargins(availableWidth: SizeConstraint, lctx: LayoutState, + computed: CSSComputedValues): RelativeRect = # Note: we use availableWidth for percentage resolution intentionally. - box.margin_top = computed{"margin-top"}.px(lctx, availableWidth) - box.margin_bottom = computed{"margin-bottom"}.px(lctx, availableWidth) - box.margin_left = computed{"margin-left"}.px(lctx, availableWidth) - box.margin_right = computed{"margin-right"}.px(lctx, availableWidth) + return RelativeRect( + top: computed{"margin-top"}.px(lctx, availableWidth), + bottom: computed{"margin-bottom"}.px(lctx, availableWidth), + left: computed{"margin-left"}.px(lctx, availableWidth), + right: computed{"margin-right"}.px(lctx, availableWidth) + ) -proc resolvePadding(box: BlockBox, availableWidth: SizeConstraint, - lctx: LayoutState) = - let computed = box.computed +proc resolvePadding(availableWidth: SizeConstraint, lctx: LayoutState, + computed: CSSComputedValues): RelativeRect = # Note: we use availableWidth for percentage resolution intentionally. - box.padding_top = computed{"padding-top"}.px(lctx, availableWidth) - box.padding_bottom = computed{"padding-bottom"}.px(lctx, availableWidth) - box.padding_left = computed{"padding-left"}.px(lctx, availableWidth) - box.padding_right = computed{"padding-right"}.px(lctx, availableWidth) + return RelativeRect( + top: computed{"padding-top"}.px(lctx, availableWidth), + bottom: computed{"padding-bottom"}.px(lctx, availableWidth), + left: computed{"padding-left"}.px(lctx, availableWidth), + right: computed{"padding-right"}.px(lctx, availableWidth) + ) -proc calcAvailableWidth(box: BlockBox, containingWidth: SizeConstraint, +proc calcAvailableWidth(sizes: var ResolvedSizes, + containingWidth: SizeConstraint, computed: CSSComputedValues, lctx: LayoutState) = - let computed = box.computed let width = computed{"width"} var widthpx: LayoutUnit = 0 if not width.auto and width.canpx(containingWidth): widthpx = width.px(lctx, containingWidth) - box.availableWidth = stretch(widthpx) - box.resolveContentWidth(widthpx, containingWidth, width.auto) + sizes.space.w = stretch(widthpx) + sizes.resolveContentWidth(widthpx, containingWidth, computed, width.auto) if not computed{"max-width"}.auto: let max_width = computed{"max-width"}.px(lctx, containingWidth) - box.max_width = some(max_width) - if box.availableWidth.t in {STRETCH, FIT_CONTENT} and - max_width < box.availableWidth.u or - box.availableWidth.t == MAX_CONTENT: - box.availableWidth = stretch(max_width) #TODO is stretch ok here? - if box.availableWidth.t == STRETCH: + sizes.max_width = some(max_width) + if sizes.space.w.t in {STRETCH, FIT_CONTENT} and + max_width < sizes.space.w.u or + sizes.space.w.t == MAX_CONTENT: + sizes.space.w = stretch(max_width) #TODO is stretch ok here? + if sizes.space.w.t == STRETCH: # available width would stretch over max-width - box.availableWidth = stretch(max_width) + sizes.space.w = stretch(max_width) else: # FIT_CONTENT # available width could be higher than max-width (but not necessarily) - box.availableWidth = fitContent(max_width) - box.resolveContentWidth(max_width, containingWidth) + sizes.space.w = fitContent(max_width) + sizes.resolveContentWidth(max_width, containingWidth, computed) if not computed{"min-width"}.auto: let min_width = computed{"min-width"}.px(lctx, containingWidth) - box.min_width = some(min_width) - if box.availableWidth.t in {STRETCH, FIT_CONTENT} and - min_width > box.availableWidth.u or - box.availableWidth.t == MIN_CONTENT: + sizes.min_width = some(min_width) + if sizes.space.w.t in {STRETCH, FIT_CONTENT} and + min_width > sizes.space.w.u or + sizes.space.w.t == MIN_CONTENT: # two cases: # * available width is stretched under min-width. in this case, # stretch to min-width instead. # * available width is fit under min-width. in this case, stretch to # min-width as well (as we must satisfy min-width >= width). - box.availableWidth = stretch(min_width) - box.resolveContentWidth(min_width, containingWidth) + sizes.space.w = stretch(min_width) + sizes.resolveContentWidth(min_width, containingWidth, computed) -proc calcAvailableHeight(box: BlockBox, containingHeight: SizeConstraint, - percHeight: Option[LayoutUnit], lctx: LayoutState) = - let computed = box.computed +proc calcAvailableHeight(sizes: var ResolvedSizes, + containingHeight: SizeConstraint, percHeight: Option[LayoutUnit], + computed: CSSComputedValues, lctx: LayoutState) = let height = computed{"height"} var heightpx: LayoutUnit = 0 if not height.auto and height.canpx(percHeight): heightpx = height.px(lctx, percHeight).get - box.availableHeight = stretch(heightpx) + sizes.space.h = stretch(heightpx) if not computed{"max-height"}.auto: let max_height = computed{"max-height"}.px(lctx, percHeight) - box.max_height = max_height + sizes.max_height = max_height if max_height.isSome: - if box.availableHeight.t in {STRETCH, FIT_CONTENT} and - max_height.get < box.availableHeight.u or - box.availableHeight.t == MAX_CONTENT: + if sizes.space.h.t in {STRETCH, FIT_CONTENT} and + max_height.get < sizes.space.h.u or + sizes.space.h.t == MAX_CONTENT: # same reasoning as for width. - if box.availableWidth.t == STRETCH: - box.availableWidth = stretch(max_height.get) + if sizes.space.h.t == STRETCH: + sizes.space.h = stretch(max_height.get) else: # FIT_CONTENT - box.availableWidth = fitContent(max_height.get) + sizes.space.h = fitContent(max_height.get) if not computed{"min-height"}.auto: let min_height = computed{"min-height"}.px(lctx, percHeight) if min_height.isSome: - box.min_height = min_height - if box.availableHeight.t in {STRETCH, FIT_CONTENT} and - min_height.get > box.availableHeight.u or - box.availableHeight.t == MIN_CONTENT: + sizes.min_height = min_height + if sizes.space.h.t in {STRETCH, FIT_CONTENT} and + min_height.get > sizes.space.h.u or + sizes.space.h.t == MIN_CONTENT: # same reasoning as for width. - box.availableHeight = stretch(min_height.get) + sizes.space.h = stretch(min_height.get) -proc calcAbsoluteAvailableWidth(box: BlockBox, - containingWidth: SizeConstraint, lctx: LayoutState) = - let left = box.computed{"left"} - let right = box.computed{"right"} - let width = box.computed{"width"} +proc calcAbsoluteAvailableWidth(sizes: var ResolvedSizes, + containingWidth: SizeConstraint, computed: CSSComputedValues, + lctx: LayoutState) = + let left = computed{"left"} + let right = computed{"right"} + let width = computed{"width"} if width.auto: if not left.auto and not right.auto: # width is auto and left & right are not auto. @@ -684,29 +771,30 @@ proc calcAbsoluteAvailableWidth(box: BlockBox, let leftpx = left.px(lctx, containingWidth) let rightpx = right.px(lctx, containingWidth) let u = containingWidth.u - leftpx - rightpx - - box.margin_left - box.margin_right - box.padding_left - - box.padding_right - box.availableWidth = stretch(max(u, 0)) + sizes.margin.left - sizes.margin.right - + sizes.padding.left - sizes.padding.right + sizes.space.w = stretch(max(u, 0)) else: - box.availableWidth = containingWidth + sizes.space.w = containingWidth else: # Return shrink to fit and solve for left/right. # Note that we do not know content width yet, so it is impossible to # solve left/right yet. - box.availableWidth = fitContent(containingWidth) + sizes.space.w = fitContent(containingWidth) else: let widthpx = width.px(lctx, containingWidth) # We could solve for left/right here, as available width is known. # Nevertheless, it is only needed for positioning, so we do not solve # them yet. - box.availableWidth = stretch(widthpx) + sizes.space.w = stretch(widthpx) -proc calcAbsoluteAvailableHeight(box: BlockBox, - containingHeight: SizeConstraint, lctx: LayoutState) = +proc calcAbsoluteAvailableHeight(sizes: var ResolvedSizes, + containingHeight: SizeConstraint, computed: CSSComputedValues, + lctx: LayoutState) = #TODO this might be incorrect because of percHeight? - let top = box.computed{"top"} - let bottom = box.computed{"bottom"} - let height = box.computed{"height"} + let top = computed{"top"} + let bottom = computed{"bottom"} + let height = computed{"height"} if height.auto: if not top.auto and not bottom.auto: # height is auto and top & bottom are not auto. @@ -716,30 +804,34 @@ proc calcAbsoluteAvailableHeight(box: BlockBox, let bottompx = bottom.px(lctx, containingHeight) #TODO I assume border collapsing does not matter here? let u = containingHeight.u - toppx - bottompx - - box.margin_top - box.margin_bottom - box.padding_top - - box.padding_bottom - box.availableHeight = stretch(max(u, 0)) + sizes.margin.top - sizes.margin.bottom - + sizes.padding.top - sizes.padding.bottom + sizes.space.h = stretch(max(u, 0)) else: - box.availableHeight = containingHeight + sizes.space.h = containingHeight else: - box.availableHeight = fitContent(containingHeight) + sizes.space.h = fitContent(containingHeight) else: let heightpx = height.px(lctx, containingHeight) - box.availableHeight = stretch(heightpx) + sizes.space.h = stretch(heightpx) # Calculate and resolve available width & height for absolutely positioned # boxes. -proc calcAbsoluteAvailableSizes(box: BlockBox, lctx: LayoutState) = - let containingWidth = lctx.positioned[^1].availableWidth - let containingHeight = lctx.positioned[^1].availableHeight - box.resolveMargins(containingWidth, lctx) - box.resolvePadding(containingWidth, lctx) - box.calcAbsoluteAvailableWidth(containingWidth, lctx) - box.calcAbsoluteAvailableHeight(containingHeight, lctx) +proc calcAbsoluteAvailableSizes(lctx: LayoutState, computed: CSSComputedValues): + ResolvedSizes = + let containingWidth = lctx.positioned[^1].space.w + let containingHeight = lctx.positioned[^1].space.h + var sizes = ResolvedSizes( + margin: resolveMargins(containingWidth, lctx, computed), + padding: resolvePadding(containingWidth, lctx, computed) + ) + sizes.calcAbsoluteAvailableWidth(containingWidth, computed, lctx) + sizes.calcAbsoluteAvailableHeight(containingHeight, computed, lctx) + return sizes # Calculate and resolve available width & height for box children. -# availableWidth: width of the containing box -# availableHeight: ditto, but with height. +# containingWidth: width of the containing box +# containingHeight: ditto, but with height. # Note that this is not the final content size, just the amount of space # available for content. # The percentage width/height is generally @@ -747,171 +839,138 @@ proc calcAbsoluteAvailableSizes(box: BlockBox, lctx: LayoutState) = # 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 calcAvailableSizes(box: BlockBox, containingWidth, containingHeight: - SizeConstraint, percHeight: Option[LayoutUnit], lctx: LayoutState) = - if box.computed{"position"} == POSITION_ABSOLUTE: - box.calcAbsoluteAvailableSizes(lctx) - else: - box.resolveMargins(containingWidth, lctx) - box.resolvePadding(containingWidth, lctx) +proc resolveSizes(lctx: LayoutState, containingWidth, + containingHeight: SizeConstraint, percHeight: Option[LayoutUnit], + computed: CSSComputedValues): ResolvedSizes = + if computed{"position"} == POSITION_ABSOLUTE: + return calcAbsoluteAvailableSizes(lctx, computed) + var sizes = ResolvedSizes( + margin: resolveMargins(containingWidth, lctx, computed), + padding: resolvePadding(containingWidth, lctx, computed), # Take defined sizes if our width/height resolves to auto. # (For block boxes, this is width: stretch(parentWidth), height: max-content) - box.availableWidth = containingWidth - box.availableHeight = containingHeight - # Finally, calculate available width and height. - box.calcAvailableWidth(containingWidth, lctx) - box.calcAvailableHeight(containingHeight, percHeight, lctx) - -proc calcTableCellAvailableSizes(box: BlockBox, availableWidth, availableHeight: - SizeConstraint, override: bool, lctx: LayoutState) = - let computed = box.computed - box.resolvePadding(availableWidth, lctx) - box.availableWidth = availableWidth - box.availableHeight = availableHeight - + space: AvailableSpace(w: containingWidth, h: containingHeight) + ) + # Finally, calculate available width and height. + sizes.calcAvailableWidth(containingWidth, computed, lctx) + sizes.calcAvailableHeight(containingHeight, percHeight, computed, lctx) + return sizes + +proc resolveTableCellSizes(lctx: LayoutState, containingWidth, + containingHeight: SizeConstraint, override: bool, + computed: CSSComputedValues): ResolvedSizes = + var sizes = ResolvedSizes( + padding: resolvePadding(containingWidth, lctx, computed), + space: AvailableSpace(w: containingWidth, h: containingHeight) + ) if not override: let width = computed{"width"} if not width.auto and width.unit != UNIT_PERC: - box.availableWidth = stretch(width.px(lctx)) - box.availableWidth.u -= box.padding_left - box.availableWidth.u -= box.padding_right - + sizes.space.w = stretch(width.px(lctx)) + sizes.space.w.u -= sizes.padding.left + sizes.space.w.u -= sizes.padding.right if not override: let height = computed{"height"} if not height.auto and height.unit != UNIT_PERC: - box.availableHeight = stretch(height.px(lctx)) - -proc newTableCellBox(lctx: LayoutState, builder: BoxBuilder, - availableWidth, availableHeight: SizeConstraint, override: bool): - BlockBox = - let box = BlockBox( - computed: builder.computed, - node: builder.node - ) - box.calcTableCellAvailableSizes(availableWidth, availableHeight, override, - lctx) - return box - -proc newFlowRootBox(lctx: LayoutState, builder: BoxBuilder, - availableWidth, availableHeight: SizeConstraint, - percHeight: Option[LayoutUnit]): BlockBox = - let box = BlockBox( - computed: builder.computed, - node: builder.node, - positioned: builder.computed{"position"} != POSITION_STATIC, - ) - box.calcAvailableSizes(availableWidth, availableHeight, percHeight, lctx) - return box + sizes.space.h = stretch(height.px(lctx)) + return sizes func toPercSize(sc: SizeConstraint): Option[LayoutUnit] = if sc.isDefinite(): return some(sc.u) return none(LayoutUnit) -proc newBlockBox(lctx: LayoutState, parent: BlockBox, builder: BoxBuilder): - BlockBox = - let box = BlockBox( - computed: builder.computed, - positioned: builder.computed{"position"} != POSITION_STATIC, - node: builder.node - ) - let parentHeight = parent.availableHeight - let availableWidth = parent.availableWidth - let availableHeight = maxContent() #TODO fit-content when clip - let percHeight = parentHeight.toPercSize() - box.calcAvailableSizes(availableWidth, availableHeight, percHeight, lctx) - return box +type + BlockContext = object + lctx: LayoutState + marginTodo: Strut + marginTarget: BlockBox + marginExtra: ptr LayoutUnit + floatOffset: Offset + exclusions: seq[Exclusion] + unpositionedFloats: seq[BlockBox] + + #TODO clear + Exclusion = object + offset: Offset + size: Size -proc newBlockBoxStretch(lctx: LayoutState, parent: BlockBox, - builder: BoxBuilder): BlockBox = - let box = BlockBox( - computed: builder.computed, - positioned: builder.computed{"position"} != POSITION_STATIC, - node: builder.node - ) - let parentWidth = parent.availableWidth - let parentHeight = parent.availableHeight - let availableWidth = stretch(parentWidth) - let availableHeight = maxContent() #TODO fit-content when clip - let percHeight = parentHeight.toPercSize() - box.calcAvailableSizes(availableWidth, availableHeight, percHeight, lctx) - return box + Strut = object + pos: LayoutUnit + neg: LayoutUnit -proc newBlockBoxFit(lctx: LayoutState, parent: BlockBox, builder: BoxBuilder): - BlockBox = - let box = BlockBox( - computed: builder.computed, - positioned: builder.computed{"position"} != POSITION_STATIC, - node: builder.node - ) - let parentWidth = parent.availableWidth - let parentHeight = parent.availableHeight - let availableWidth = fitContent(parentWidth) - let availableHeight = maxContent() #TODO fit-content when clip - let percHeight = parentHeight.toPercSize() - box.calcAvailableSizes(availableWidth, availableHeight, percHeight, lctx) - return box +proc append(a: var Strut, b: LayoutUnit) = + if b < 0: + a.neg = min(b, a.neg) + else: + a.pos = max(b, a.pos) -proc newListItem(lctx: LayoutState, parent: BlockBox, - builder: ListItemBoxBuilder): ListItemBox = - let box = ListItemBox( - computed: builder.computed, - positioned: builder.computed{"position"} != POSITION_STATIC, - node: builder.node - ) - let parentWidth = parent.availableWidth - let parentHeight = parent.availableHeight - let availableWidth = stretch(parentWidth) - let availableHeight = maxContent() #TODO fit-content when clip - let percHeight = parentHeight.toPercSize() - box.calcAvailableSizes(availableWidth, availableHeight, percHeight, lctx) - return box +func sum(a: Strut): LayoutUnit = + return a.pos + a.neg -proc buildBlock(lctx: LayoutState, builder: BlockBoxBuilder, - parent: BlockBox): BlockBox -proc buildInlines(lctx: LayoutState, parent: BlockBox, - inlines: seq[BoxBuilder]): InlineContext -proc buildBlockLayout(lctx: LayoutState, box: BlockBox, - builders: seq[BoxBuilder], node: StyledNode) -proc buildTable(lctx: LayoutState, builder: TableBoxBuilder, - parent: BlockBox): BlockBox +proc buildInlines(lctx: LayoutState, inlines: seq[BoxBuilder], + sizes: ResolvedSizes, computed: CSSComputedValues): InlineContext +proc buildBlockLayout(bctx: var BlockContext, box: BlockBox, + builder: BlockBoxBuilder, marginTopOut: var LayoutUnit, sizes: ResolvedSizes) proc buildTableLayout(lctx: LayoutState, table: BlockBox, - builder: TableBoxBuilder) + builder: TableBoxBuilder, sizes: ResolvedSizes) # Note: padding must still be applied after this. -proc applyWidth(box: BlockBox, maxChildWidth: LayoutUnit) = +proc applyWidth(box: BlockBox, sizes: ResolvedSizes, + maxChildWidth: LayoutUnit) = # Make the box as small/large as the content's width or specified width. - box.width = maxChildWidth.applySizeConstraint(box.availableWidth) + box.size.w = maxChildWidth.applySizeConstraint(sizes.space.w) # Then, clamp it to min_width and max_width (if applicable). - box.width = clamp(box.width, box.min_width.get(0), - box.max_width.get(high(LayoutUnit))) + box.size.w = clamp(box.size.w, sizes.min_width.get(0), + sizes.max_width.get(high(LayoutUnit))) -proc applyInlineDimensions(box: BlockBox) = +proc buildInlineLayout(lctx: LayoutState, box: BlockBox, + children: seq[BoxBuilder], sizes: ResolvedSizes) = + box.inline = lctx.buildInlines(children, sizes, box.computed) box.xminwidth = max(box.xminwidth, box.inline.minwidth) - box.width = box.inline.width + box.padding_left + box.padding_right - box.height = applySizeConstraint(box.inline.height, box.availableHeight) - box.height += box.padding_top + box.padding_bottom - box.inline.offset.x += box.padding_left - box.inline.offset.y += box.padding_top - box.applyWidth(box.inline.width) - box.width += box.padding_left - box.width += box.padding_right + box.size.w = box.inline.size.w + sizes.padding.left + sizes.padding.right + box.size.h = applySizeConstraint(box.inline.size.h, sizes.space.h) + box.size.h += sizes.padding.top + sizes.padding.bottom + box.inline.offset.x += sizes.padding.left + box.inline.offset.y += sizes.padding.top + box.applyWidth(sizes, box.inline.size.w) + box.size.w += sizes.padding.left + box.size.w += sizes.padding.right box.baseline = box.inline.offset.y + box.inline.baseline box.firstBaseline = box.inline.offset.y + box.inline.firstBaseline -proc buildInlineLayout(lctx: LayoutState, parent: BlockBox, - children: seq[BoxBuilder]) = - parent.inline = lctx.buildInlines(parent, children) - parent.applyInlineDimensions() - -proc buildFlowLayout(lctx: LayoutState, box: BlockBox, - builder: BlockBoxBuilder) = +const DisplayBlockLike = {DISPLAY_BLOCK, DISPLAY_LIST_ITEM} + +proc applyMargins(bctx: var BlockContext, builder: BlockBoxBuilder, + box: BlockBox, marginTopOut: var LayoutUnit, sizes: ResolvedSizes) = + if builder.computed{"position"} notin {POSITION_ABSOLUTE, POSITION_FIXED} and + (sizes.padding.top != 0 or sizes.padding.bottom != 0 or + builder.inlinelayout or + builder.computed{"display"} notin DisplayBlockLike): + # Apply uncommitted margins. + let margin = bctx.marginTodo.sum() + if bctx.marginTarget == nil: + box.offset.y += margin + marginTopOut += margin + else: + bctx.marginTarget.offset.y += margin + bctx.marginExtra[] += margin + bctx.marginTarget = nil + bctx.floatOffset.y += margin + bctx.marginTodo = Strut() + +proc buildFlowLayout(bctx: var BlockContext, box: BlockBox, + builder: BlockBoxBuilder, marginTopOut: var LayoutUnit, + sizes: ResolvedSizes) = + bctx.marginTodo.append(sizes.margin.top) + bctx.applyMargins(builder, box, marginTopOut, sizes) if builder.inlinelayout: # Builder only contains inline boxes. - lctx.buildInlineLayout(box, builder.children) + bctx.lctx.buildInlineLayout(box, builder.children, sizes) else: # Builder only contains block boxes. - lctx.buildBlockLayout(box, builder.children, builder.node) + bctx.buildBlockLayout(box, builder, marginTopOut, sizes) + bctx.marginTodo.append(sizes.margin.bottom) func toperc100(sc: SizeConstraint): Option[LayoutUnit] = if sc.isDefinite(): @@ -921,31 +980,41 @@ func toperc100(sc: SizeConstraint): Option[LayoutUnit] = # parentWidth, parentHeight: width/height of the containing block. proc addInlineBlock(state: var InlineState, builder: BlockBoxBuilder, parentWidth, parentHeight: SizeConstraint, computed: CSSComputedValues) = - let innerbox = newFlowRootBox(state.lctx, builder, - fitContent(parentWidth), maxContent(), parentHeight.toperc100()) let lctx = state.lctx + let percHeight = parentHeight.toperc100() + let sizes = lctx.resolveSizes(parentWidth, maxContent(), percHeight, + builder.computed) + let box = BlockBox( + computed: builder.computed, + node: builder.node, + positioned: builder.computed{"position"} != POSITION_STATIC, + margin: sizes.margin + ) + var bctx = BlockContext(lctx: lctx) + var margin_top: LayoutUnit case builder.computed{"display"} of DISPLAY_INLINE_BLOCK: - lctx.buildFlowLayout(innerbox, builder) + bctx.buildFlowLayout(box, builder, margin_top, sizes) of DISPLAY_INLINE_TABLE: - lctx.buildTableLayout(innerbox, TableBoxBuilder(builder)) + lctx.buildTableLayout(box, TableBoxBuilder(builder), sizes) else: assert false, $builder.computed{"display"} + box.offset.y = 0 #TODO ugly hack to prevent double margin # Apply the block box's properties to the atom itself. let iblock = InlineAtom( t: INLINE_BLOCK, - innerbox: innerbox, - offset: Offset(x: innerbox.margin_left), + innerbox: box, + offset: Offset(x: sizes.margin.left), size: Size( - w: innerbox.width + innerbox.margin_left + innerbox.margin_right, - h: innerbox.height + w: box.size.w + sizes.margin.left + sizes.margin.right, + h: box.size.h ) ) let iastate = InlineAtomState( baseline: iblock.innerbox.baseline, vertalign: builder.computed{"vertical-align"}, - margin_top: iblock.innerbox.margin_top, - margin_bottom: iblock.innerbox.margin_bottom + margin_top: margin_top, + margin_bottom: bctx.marginTodo.sum() ) discard state.addAtom(iastate, iblock, computed) @@ -956,12 +1025,10 @@ proc buildInline(state: var InlineState, box: InlineBoxBuilder) = let paddingformat = getComputedFormat(box.computed, box.node) if box.splitstart: - let margin_left = box.computed{"margin-left"}.px(lctx, - state.availableWidth) + let margin_left = box.computed{"margin-left"}.px(lctx, state.space.w) state.currentLine.size.w += margin_left - let padding_left = box.computed{"padding-left"}.px(lctx, - state.availableWidth) + let padding_left = box.computed{"padding-left"}.px(lctx, state.space.w) if padding_left > 0: # We must add spacing to the line to make sure that it is formatted # (i.e. colored) appropriately. @@ -979,28 +1046,25 @@ proc buildInline(state: var InlineState, box: InlineBoxBuilder) = state.buildInline(child) of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE: let child = BlockBoxBuilder(child) - let w = fitContent(state.availableWidth) - let h = state.availableHeight + let w = fitContent(state.space.w) + let h = state.space.h state.addInlineBlock(child, w, h, box.computed) state.whitespacenum = 0 else: assert false, "child.t is " & $child.computed{"display"} if box.splitend: - let padding_right = box.computed{"padding-right"}.px(lctx, - state.availableWidth) + let padding_right = box.computed{"padding-right"}.px(lctx, state.space.w) state.currentLine.size.w += padding_right if padding_right > 0: let height = max(state.currentLine.size.h, 1) state.addPadding(padding_right, height, paddingformat) - let margin_right = box.computed{"margin-right"}.px(lctx, - state.availableWidth) + let margin_right = box.computed{"margin-right"}.px(lctx, state.space.w) state.currentLine.size.w += margin_right -proc buildInlines(lctx: LayoutState, parent: BlockBox, - inlines: seq[BoxBuilder]): InlineContext = - var state = newInlineState(lctx, parent.availableWidth, - parent.availableHeight) +proc buildInlines(lctx: LayoutState, inlines: seq[BoxBuilder], + sizes: ResolvedSizes, computed: CSSComputedValues): InlineContext = + var state = newInlineState(lctx, sizes.space) if inlines.len > 0: for child in inlines: case child.computed{"display"} @@ -1010,54 +1074,135 @@ proc buildInlines(lctx: LayoutState, parent: BlockBox, of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE: #TODO wtf let child = BlockBoxBuilder(child) - let w = fitContent(state.availableWidth) - let h = state.availableHeight - state.addInlineBlock(child, w, h, parent.computed) + let w = fitContent(state.space.w) + let h = state.space.h + state.addInlineBlock(child, w, h, computed) state.whitespacenum = 0 else: assert false, "child.t is " & $child.computed{"display"} - state.finish(parent.computed) + state.finish(computed) return state.ictx -proc buildMarker(builder: MarkerBoxBuilder, parent: BlockBox, +proc buildMarker(builder: MarkerBoxBuilder, space: AvailableSpace, lctx: LayoutState): InlineContext = - let availableWidth = fitContent(parent.availableWidth) - var state = newInlineState(lctx, availableWidth, parent.availableHeight) + let space = AvailableSpace( + w: fitContent(space.w), + h: space.h + ) + var state = newInlineState(lctx, space) state.buildInline(builder) state.finish(builder.computed) return state.ictx -proc buildListItem(lctx: LayoutState, builder: ListItemBoxBuilder, - parent: BlockBox): ListItemBox = - result = newListItem(lctx, parent, builder) +# Build a block box without establishing a new block formatting context. +proc buildBlock(bctx: var BlockContext, builder: BlockBoxBuilder, + space: AvailableSpace, offset: var 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) + let box = BlockBox( + computed: builder.computed, + positioned: builder.computed{"position"} != POSITION_STATIC, + node: builder.node, + offset: offset, + margin: sizes.margin + ) + bctx.buildFlowLayout(box, builder, offset.y, sizes) + return box + +# Establish a new block formatting context and build a block box. +proc buildRootBlock(lctx: LayoutState, builder: BlockBoxBuilder): BlockBox = + let availableWidth = stretch(lctx.attrs.width_px) + let percHeight = some(toLayoutUnit(lctx.attrs.height_px)) + #TODO TODO TODO I have a feeling this breaks when we use position: absolute + # on the root element. + let sizes = lctx.resolveSizes(availableWidth, maxContent(), percHeight, + builder.computed) + let box = BlockBox( + computed: builder.computed, + node: builder.node, + positioned: builder.computed{"position"} != POSITION_STATIC, + margin: sizes.margin + ) + var bctx = BlockContext(lctx: lctx) + lctx.positioned.add(sizes) + # Pass our y offset as marginTopOut, so any margin-top bubbled up to here + # is handled as positioning of this root box. + var margin_top: LayoutUnit + bctx.buildFlowLayout(box, builder, margin_top, sizes) + box.offset.y = 0 #TODO ugly hack to prevent double margin + box.offset.y += margin_top + #TODO pass down bctx.marginTodo.sum() somehow (then we could use this + # in addInlineBlock) + return box + +proc buildListItem(bctx: var BlockContext, builder: ListItemBoxBuilder, + space: AvailableSpace, offset: var 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) + let box = ListItemBox( + computed: builder.computed, + positioned: builder.computed{"position"} != POSITION_STATIC, + node: builder.node, + offset: offset + ) if builder.marker != nil: - result.marker = buildMarker(builder.marker, result, lctx) - lctx.buildFlowLayout(result, builder.content) + box.marker = buildMarker(builder.marker, sizes.space, lctx) + bctx.buildFlowLayout(box, builder.content, offset.y, sizes) + return box -proc positionAbsolute(lctx: LayoutState, box: BlockBox) = +proc buildTable(bctx: var BlockContext, builder: TableBoxBuilder, + space: AvailableSpace, offset: var 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) + let box = BlockBox( + computed: builder.computed, + positioned: builder.computed{"position"} != POSITION_STATIC, + node: builder.node, + offset: offset, + margin: sizes.margin + ) + bctx.marginTodo.append(sizes.margin.top) + bctx.applyMargins(builder, box, offset.y, sizes) + lctx.buildTableLayout(box, builder, sizes) + bctx.marginTodo.append(sizes.margin.bottom) + return box + +proc positionAbsolute(lctx: LayoutState, box: BlockBox, margin: RelativeRect) = let last = lctx.positioned[^1] let left = box.computed{"left"} let right = box.computed{"right"} let top = box.computed{"top"} let bottom = box.computed{"bottom"} let parentWidth = applySizeConstraint(lctx.attrs.width_px, - last.availableWidth) + last.space.w) let parentHeight = applySizeConstraint(lctx.attrs.height_px, - last.availableHeight) + last.space.h) box.x_positioned = not (left.auto and right.auto) box.y_positioned = not (top.auto and bottom.auto) if not left.auto: - box.offset.x += left.px(lctx, parentWidth) - box.offset.x += box.margin_left + box.offset.x = left.px(lctx, parentWidth) + box.offset.x += margin.left elif not right.auto: - box.offset.x += parentWidth - right.px(lctx, parentWidth) - box.width - box.offset.x -= box.margin_right + box.offset.x = parentWidth - right.px(lctx, parentWidth) - box.size.w + box.offset.x -= margin.right if not top.auto: - box.offset.y += top.px(lctx, parentHeight) - box.offset.y += box.margin_top + box.offset.y = top.px(lctx, parentHeight) + box.offset.y += margin.top elif not bottom.auto: - box.offset.y += parentHeight - bottom.px(lctx, parentHeight) - box.height - box.offset.y -= box.margin_bottom + box.offset.y = parentHeight - bottom.px(lctx, parentHeight) - box.size.h + box.offset.y -= margin.bottom proc positionRelative(parent, box: BlockBox, lctx: LayoutState) = let left = box.computed{"left"} @@ -1067,11 +1212,11 @@ proc positionRelative(parent, box: BlockBox, lctx: LayoutState) = if not left.auto: box.offset.x += right.px(lctx) elif not right.auto: - box.offset.x += parent.width - right.px(lctx) - box.width + box.offset.x += parent.size.w - right.px(lctx) - box.size.w if not top.auto: box.offset.y += top.px(lctx) elif not top.auto: - box.offset.y -= parent.height - bottom.px(lctx) - box.height + box.offset.y -= parent.size.h - bottom.px(lctx) - box.size.h type CellWrapper = ref object @@ -1115,20 +1260,40 @@ type proc buildTableCaption(lctx: LayoutState, builder: TableCaptionBoxBuilder, availableWidth, availableHeight: SizeConstraint): BlockBox = - let w = availableWidth - let h = maxContent() - let ph = availableHeight.toperc100() - let box = lctx.newFlowRootBox(builder, w, h, ph) - lctx.buildFlowLayout(box, builder) + let percHeight = availableHeight.toperc100() + let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight, + builder.computed) + let box = BlockBox( + computed: builder.computed, + node: builder.node, + positioned: builder.computed{"position"} != POSITION_STATIC, + margin: sizes.margin + ) + var bctx = BlockContext(lctx: lctx) + var margin_top: LayoutUnit + box.offset = Offset(y: margin_top) + bctx.buildFlowLayout(box, builder, margin_top, sizes) + # Include marginTodo in our own height. + #TODO this is not quite correct, as height should be the padding height. + box.size.h += margin_top + box.size.h += bctx.marginTodo.sum() return box proc buildTableCell(lctx: LayoutState, builder: TableCellBoxBuilder, availableWidth, availableHeight: SizeConstraint, override: bool): BlockBox = - let tableCell = lctx.newTableCellBox(builder, availableWidth, - availableHeight, override) - lctx.buildFlowLayout(tableCell, builder) - return tableCell + let sizes = lctx.resolveTableCellSizes(availableWidth, availableHeight, + override, builder.computed) + let box = BlockBox( + computed: builder.computed, + node: builder.node, + margin: sizes.margin + ) + var ctx = BlockContext(lctx: lctx) + # Table cells ignore margins. + var dummy: LayoutUnit + ctx.buildFlowLayout(box, builder, dummy, sizes) + return box # Sort growing cells, and filter out cells that have grown to their intended # rowspan. @@ -1208,7 +1373,7 @@ proc preBuildTableRow(pctx: var TableContext, box: TableRowBoxBuilder, if ctx.reflow.len < n + colspan: ctx.reflow.setLen(n + colspan) let minw = box.xminwidth div colspan - let w = box.width div colspan + let w = box.size.w div colspan for i in n ..< n + colspan: # Add spacing. ctx.width += pctx.inlinespacing @@ -1275,17 +1440,32 @@ proc alignTableCell(cell: BlockBox, availableHeight, baseline: LayoutUnit) = of VERTICAL_ALIGN_TOP: cell.offset.y = 0 of VERTICAL_ALIGN_MIDDLE: - cell.offset.y = availableHeight div 2 - cell.height div 2 + cell.offset.y = availableHeight div 2 - cell.size.h div 2 of VERTICAL_ALIGN_BOTTOM: - cell.offset.y = availableHeight - cell.height + cell.offset.y = availableHeight - cell.size.h else: cell.offset.y = baseline - cell.firstBaseline +proc newTableRowBox(lctx: LayoutState, parent: BlockBox, + builder: BoxBuilder, sizes: ResolvedSizes): BlockBox = + let availableWidth = stretch(sizes.space.w) + let availableHeight = maxContent() #TODO fit-content when clip + let percHeight = sizes.space.h.toPercSize() + let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight, + builder.computed) + let box = BlockBox( + computed: builder.computed, + positioned: builder.computed{"position"} != POSITION_STATIC, + node: builder.node, + margin: sizes.margin + ) + return box + proc buildTableRow(pctx: TableContext, ctx: RowContext, parent: BlockBox, - builder: TableRowBoxBuilder): BlockBox = + builder: TableRowBoxBuilder, sizes: ResolvedSizes): BlockBox = var x: LayoutUnit = 0 var n = 0 - let row = newBlockBoxStretch(pctx.lctx, parent, builder) + let row = newTableRowBox(pctx.lctx, parent, builder, sizes) var baseline: LayoutUnit = 0 # real cellwrappers of fillers var to_align: seq[CellWrapper] @@ -1313,7 +1493,7 @@ proc buildTableRow(pctx: TableContext, ctx: RowContext, parent: BlockBox, # the TD with a width of 5ch should be 9ch wide as well. cellw.box = pctx.lctx.buildTableCell(cellw.builder, stretch(w), maxContent(), override = true) - w = max(w, cellw.box.width) + w = max(w, cellw.box.size.w) let cell = cellw.box x += pctx.inlinespacing if cell != nil: @@ -1332,22 +1512,22 @@ proc buildTableRow(pctx: TableContext, ctx: RowContext, parent: BlockBox, row.nested.add(cell) if cellw.rowspan > 1: to_height.add(cellw) - row.height = max(row.height, cell.height div cellw.rowspan) + row.size.h = max(row.size.h, cell.size.h div cellw.rowspan) else: let real = cellw.real - row.height = max(row.height, real.box.height div cellw.rowspan) + row.size.h = max(row.size.h, real.box.size.h div cellw.rowspan) to_height.add(real) if cellw.last: to_align.add(real) for cellw in to_height: - cellw.height += row.height + cellw.height += row.size.h for cellw in to_baseline: cellw.baseline = baseline for cellw in to_align: alignTableCell(cellw.box, cellw.height, cellw.baseline) for cell in row.nested: - alignTableCell(cell, row.height, baseline) - row.width = x + alignTableCell(cell, row.size.h, baseline) + row.size.w = x return row proc preBuildTableRows(ctx: var TableContext, rows: seq[TableRowBoxBuilder], @@ -1419,21 +1599,21 @@ proc calcUnspecifiedColIndices(ctx: var TableContext, W: var LayoutUnit, inc i return avail -func needsRedistribution(ctx: TableContext, table: BlockBox): bool = - case table.availableWidth.t +func needsRedistribution(ctx: TableContext, computed: CSSComputedValues, + sizes: ResolvedSizes): bool = + case sizes.space.w.t of MIN_CONTENT, MAX_CONTENT: # bleh return false of STRETCH: - let u = table.availableWidth.u + let u = sizes.space.w.u return u > ctx.maxwidth or u < ctx.maxwidth of FIT_CONTENT: - let u = table.availableWidth.u - return u > ctx.maxwidth and not table.computed{"width"}.auto or - u < ctx.maxwidth + let u = sizes.space.w.u + return u > ctx.maxwidth and not computed{"width"}.auto or u < ctx.maxwidth -proc redistributeWidth(ctx: var TableContext, table: BlockBox) = - var W = table.availableWidth.u +proc redistributeWidth(ctx: var TableContext, sizes: ResolvedSizes) = + var W = sizes.space.w.u # Remove inline spacing from distributable width. W -= ctx.cols.len * ctx.inlinespacing * 2 var weight: float64 @@ -1474,56 +1654,57 @@ proc reflowTableCells(ctx: var TableContext) = ctx.reflow[n] = true dec n -proc buildTableRows(ctx: TableContext, table: BlockBox) = +proc buildTableRows(ctx: TableContext, table: BlockBox, sizes: ResolvedSizes) = var y: LayoutUnit = 0 for roww in ctx.rows: if roww.builder.computed{"visibility"} == VISIBILITY_COLLAPSE: continue y += ctx.blockspacing - let row = ctx.buildTableRow(roww, table, roww.builder) + let row = ctx.buildTableRow(roww, table, roww.builder, sizes) row.offset.y += y - row.offset.x += table.padding_left - row.width += table.padding_left - row.width += table.padding_right + row.offset.x += sizes.padding.left + row.size.w += sizes.padding.left + row.size.w += sizes.padding.right y += ctx.blockspacing - y += row.height + y += row.size.h table.nested.add(row) - table.width = max(row.width, table.width) - table.height = applySizeConstraint(y, table.availableHeight) + table.size.w = max(row.size.w, table.size.w) + table.size.h = applySizeConstraint(y, sizes.space.h) -proc addTableCaption(ctx: TableContext, table: BlockBox) = +proc addTableCaption(ctx: TableContext, table: BlockBox, + sizes: ResolvedSizes) = let lctx = ctx.lctx case ctx.caption.computed{"caption-side"} of CAPTION_SIDE_TOP, CAPTION_SIDE_BLOCK_START: - let caption = lctx.buildTableCaption(ctx.caption, - stretch(table.width), maxContent()) + let caption = lctx.buildTableCaption(ctx.caption, stretch(table.size.w), + maxContent()) for r in table.nested: - r.offset.y += caption.height + r.offset.y += caption.size.h table.nested.insert(caption, 0) - table.height += caption.height - table.width = max(table.width, caption.width) + table.size.h += caption.size.h + table.size.w = max(table.size.w, caption.size.w) of CAPTION_SIDE_BOTTOM, CAPTION_SIDE_BLOCK_END: - let caption = lctx.buildTableCaption(ctx.caption, stretch(table.width), + let caption = lctx.buildTableCaption(ctx.caption, stretch(table.size.w), maxContent()) - caption.offset.y += table.width + caption.offset.y += table.size.w table.nested.add(caption) - table.height += caption.height - table.width = max(table.width, caption.width) + table.size.h += caption.size.h + table.size.w = max(table.size.w, caption.size.w) of CAPTION_SIDE_LEFT, CAPTION_SIDE_INLINE_START: let caption = lctx.buildTableCaption(ctx.caption, - fitContent(table.availableWidth), fitContent(table.height)) + fitContent(sizes.space.w), fitContent(table.size.h)) for r in table.nested: - r.offset.x += caption.width + r.offset.x += caption.size.w table.nested.insert(caption, 0) - table.width += caption.width - table.height = max(table.height, caption.height) + table.size.w += caption.size.w + table.size.h = max(table.size.h, caption.size.h) of CAPTION_SIDE_RIGHT, CAPTION_SIDE_INLINE_END: let caption = lctx.buildTableCaption(ctx.caption, - fitContent(table.availableWidth), fitContent(table.height)) - caption.offset.x += table.width + fitContent(sizes.space.w), fitContent(table.size.h)) + caption.offset.x += table.size.w table.nested.add(caption) - table.width += caption.width - table.height = max(table.height, caption.height) + table.size.w += caption.size.w + table.size.h = max(table.size.h, caption.size.h) # Table layout. We try to emulate w3m's behavior here: # 1. Calculate minimum and preferred width of each column @@ -1535,7 +1716,7 @@ proc addTableCaption(ctx: TableContext, table: BlockBox) = # width. If this would give any cell a width < min_width, set that # cell's width to min_width, then re-do the distribution. proc buildTableLayout(lctx: LayoutState, table: BlockBox, - builder: TableBoxBuilder) = + builder: TableBoxBuilder, sizes: ResolvedSizes) = let collapse = table.computed{"border-collapse"} == BORDER_COLLAPSE_COLLAPSE var ctx = TableContext(lctx: lctx, collapse: collapse) if not ctx.collapse: @@ -1543,195 +1724,116 @@ proc buildTableLayout(lctx: LayoutState, table: BlockBox, ctx.blockspacing = table.computed{"border-spacing"}.b.px(lctx) ctx.preBuildTableRows(builder, table) ctx.reflow = newSeq[bool](ctx.cols.len) - if ctx.needsRedistribution(table): - ctx.redistributeWidth(table) + if ctx.needsRedistribution(table.computed, sizes): + ctx.redistributeWidth(sizes) for col in ctx.cols: - table.width += col.width + table.size.w += col.width ctx.reflowTableCells() - ctx.buildTableRows(table) + ctx.buildTableRows(table, sizes) if ctx.caption != nil: - ctx.addTableCaption(table) - -proc buildTable(lctx: LayoutState, builder: TableBoxBuilder, parent: BlockBox): - BlockBox = - let box = newBlockBoxFit(lctx, parent, builder) - lctx.buildTableLayout(box, builder) - return box + ctx.addTableCaption(table, sizes) type BlockState = object - lctx: LayoutState - x: LayoutUnit - y: LayoutUnit - childHeight: LayoutUnit + offset: Offset maxChildWidth: LayoutUnit - margin_todo: Strut + maxFloatHeight: LayoutUnit nested: seq[BlockBox] - - Strut = object - pos: LayoutUnit - neg: LayoutUnit - -proc append(a: var Strut, b: LayoutUnit) = - if b < 0: - a.neg = min(b, a.neg) - else: - a.pos = max(b, a.pos) - -func sum(a: Strut): LayoutUnit = - return a.pos + a.neg - -proc applyChildPosition(ctx: var BlockState, parent, child: BlockBox) = - if child.computed{"position"} == POSITION_ABSOLUTE: #TODO sticky, fixed - if child.computed{"left"}.auto and child.computed{"right"}.auto: - child.offset.x = ctx.x - if child.computed{"top"}.auto and child.computed{"bottom"}.auto: - child.offset.y = ctx.y + ctx.margin_todo.sum() - child.offset.y += child.margin_top - else: - child.offset.y = ctx.y - child.offset.x = ctx.x - ctx.y += child.height - ctx.childHeight += child.height - ctx.maxChildWidth = max(ctx.maxChildWidth, child.width) - parent.xminwidth = max(parent.xminwidth, child.xminwidth) - ctx.margin_todo = Strut() - ctx.margin_todo.append(child.margin_bottom) + space: AvailableSpace + xminwidth: LayoutUnit proc postAlignChild(box, child: BlockBox, width: LayoutUnit) = case box.computed{"text-align"} of TEXT_ALIGN_CHA_CENTER: - child.offset.x += max(width div 2 - child.width div 2, 0) - of TEXT_ALIGN_CHA_LEFT: discard + child.offset.x += max(width div 2 - child.size.w div 2, 0) of TEXT_ALIGN_CHA_RIGHT: - child.offset.x += max(width - child.width, 0) - else: - child.offset.x += child.margin_left - -proc addBlockChild(ctx: var BlockState, box: BlockBox, - builder: BoxBuilder): BlockBox = - let lctx = ctx.lctx - let box = case builder.computed{"display"} - of DISPLAY_BLOCK: - lctx.buildBlock(BlockBoxBuilder(builder), box) - of DISPLAY_LIST_ITEM: - lctx.buildListItem(ListItemBoxBuilder(builder), box) - of DISPLAY_TABLE: - lctx.buildTable(TableBoxBuilder(builder), box) - else: - assert false, "builder.t is " & $builder.computed{"display"} - BlockBox(nil) - ctx.nested.add(box) - return box + child.offset.x += max(width - child.size.w - child.margin.right, 0) + else: # or -cha-left + child.offset.x += child.margin.left -proc skipAbsolutes(ctx: var BlockState, box: BlockBox, - builders: seq[BoxBuilder]): int = - var i = 0 - while i < builders.len: - let builder = builders[i] - if builder.computed{"position"} != POSITION_ABSOLUTE: - break - let child = ctx.addBlockChild(box, builder) +proc layoutBlockChildren(state: var BlockState, bctx: var BlockContext, + children: seq[BoxBuilder]) = + for builder in children: + var marginExtra: LayoutUnit + let child = case builder.computed{"display"} + of DISPLAY_BLOCK: + bctx.buildBlock(BlockBoxBuilder(builder), state.space, state.offset) + of DISPLAY_LIST_ITEM: + bctx.buildListItem(ListItemBoxBuilder(builder), state.space, + state.offset) + of DISPLAY_TABLE: + bctx.buildTable(TableBoxBuilder(builder), state.space, state.offset) + else: + assert false, "builder.t is " & $builder.computed{"display"} + BlockBox(nil) if child.computed{"position"} != POSITION_ABSOLUTE: - break - ctx.applyChildPosition(box, child) - inc i - return i - -proc buildBlockLayout(lctx: LayoutState, box: BlockBox, - builders: seq[BoxBuilder], node: StyledNode) = + state.offset.y += marginExtra + state.offset.y += child.size.h + bctx.floatOffset.y += child.size.h + state.maxChildWidth = max(state.maxChildWidth, child.size.w) + state.xminwidth = max(state.xminwidth, child.xminwidth) + state.nested.add(child) + +proc buildBlockLayout(bctx: var BlockContext, box: BlockBox, + builder: BlockBoxBuilder, marginTopOut: var LayoutUnit, + sizes: ResolvedSizes) = + let lctx = bctx.lctx let positioned = box.computed{"position"} != POSITION_STATIC if positioned: - lctx.positioned.add(box) - var ctx = BlockState( - x: box.padding_left, - y: box.padding_top, - childHeight: box.padding_top, - lctx: lctx + lctx.positioned.add(sizes) + var state = BlockState( + offset: Offset(x: sizes.padding.left, y: sizes.padding.top), + space: sizes.space ) + bctx.floatOffset.x += state.offset.x + bctx.floatOffset.y += state.offset.y + if bctx.marginTarget == nil: + bctx.marginTarget = box + bctx.marginExtra = addr marginTopOut - # Skip absolute boxes before setting the first margin. - var i = ctx.skipAbsolutes(box, builders) - - if i < builders.len: - let builder = builders[i] - let child = ctx.addBlockChild(box, builder) - ctx.margin_todo.append(box.margin_top) - ctx.margin_todo.append(child.margin_top) - box.margin_top = ctx.margin_todo.sum() - ctx.applyChildPosition(box, child) - box.firstBaseline = child.offset.y + child.firstBaseline - inc i - - while i < builders.len: - let builder = builders[i] - let child = ctx.addBlockChild(box, builder) - if child.computed{"position"} != POSITION_ABSOLUTE: - ctx.margin_todo.append(child.margin_top) - ctx.y += ctx.margin_todo.sum() - ctx.childHeight += ctx.margin_todo.sum() - ctx.applyChildPosition(box, child) - inc i + state.layoutBlockChildren(bctx, builder.children) - if ctx.nested.len > 0: - let lastNested = ctx.nested[^1] + if state.nested.len > 0: + let lastNested = state.nested[^1] box.baseline = lastNested.offset.y + lastNested.baseline - ctx.margin_todo.append(box.margin_bottom) - box.margin_bottom = ctx.margin_todo.sum() - - box.applyWidth(ctx.maxChildWidth) + box.applyWidth(sizes, state.maxChildWidth) # Re-position the children. # The x offset for values in shrink mode depends on the parent box's # width, so we cannot do this in the first pass. - let width = box.width - for child in ctx.nested: + let width = box.size.w + for child in state.nested: if child.computed{"position"} != POSITION_ABSOLUTE: box.postAlignChild(child, width) case child.computed{"position"} of POSITION_RELATIVE: box.positionRelative(child, lctx) of POSITION_ABSOLUTE: - lctx.positionAbsolute(child) + lctx.positionAbsolute(child, child.margin) else: discard #TODO # Finally, add padding. (We cannot do this further up without influencing # positioning.) - box.width += box.padding_left - box.width += box.padding_right - - ctx.childHeight += box.padding_bottom - - box.height = applySizeConstraint(ctx.childHeight, box.availableHeight) - if box.max_height.isSome and box.height > box.max_height.get: - box.height = box.max_height.get - if box.min_height.isSome and box.height < box.min_height.get: - box.height = box.min_height.get - box.nested = ctx.nested + box.size.w += sizes.padding.left + box.size.w += sizes.padding.right + + #TODO this is probably broken? + var childHeight = max(state.offset.y - sizes.padding.top, state.maxFloatHeight) + childHeight += sizes.padding.top + childHeight += sizes.padding.bottom + box.size.h = applySizeConstraint(childHeight, sizes.space.h) + if sizes.max_height.isSome and box.size.h > sizes.max_height.get: + box.size.h = sizes.max_height.get + if sizes.min_height.isSome and box.size.h < sizes.min_height.get: + box.size.h = sizes.min_height.get + + box.nested = state.nested + box.xminwidth = state.xminwidth if positioned: - discard lctx.positioned.pop() - -# Build a block box inside another block box, based on a builder. -proc buildBlock(lctx: LayoutState, builder: BlockBoxBuilder, - parent: BlockBox): BlockBox = - let box = newBlockBox(lctx, parent, builder) - lctx.buildFlowLayout(box, builder) - return box - -# Establish a new flow-root context and build a block box. -proc buildRootBlock(lctx: LayoutState, builder: BlockBoxBuilder): BlockBox = - let w = stretch(lctx.attrs.width_px) - let h = maxContent() - let vh: LayoutUnit = lctx.attrs.height_px - let box = lctx.newFlowRootBox(builder, w, h, some(vh)) - lctx.positioned.add(box) - lctx.buildFlowLayout(box, builder) - # Normally margin-top would be used by positionBlock, but the root block - # doesn't get positioned by the parent, so we have to do it manually here. - #TODO this is kind of ugly. - box.offset.y += box.margin_top - return box + lctx.positioned.setLen(lctx.positioned.len - 1) + bctx.marginTarget = nil + bctx.marginExtra = nil # Generation phase @@ -1789,7 +1891,7 @@ proc getTableCaptionBox(computed: CSSComputedValues): TableCaptionBoxBuilder = ) type BlockGroup = object - parent: BoxBuilder + parent: BlockBoxBuilder boxes: seq[BoxBuilder] type InnerBlockContext = object @@ -1827,7 +1929,7 @@ func canGenerateAnonymousInline(blockgroup: BlockGroup, blockgroup.boxes[^1].computed{"display"} == DISPLAY_INLINE or computed.whitespacepre or not str.onlyWhitespace() -proc newBlockGroup(parent: BoxBuilder): BlockGroup = +proc newBlockGroup(parent: BlockBoxBuilder): BlockGroup = assert parent.computed{"display"} != DISPLAY_INLINE result.parent = parent @@ -2054,7 +2156,7 @@ proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode) = lbox.splitend = true ctx.iflush() -proc newInnerBlockContext(styledNode: StyledNode, box: BoxBuilder, +proc newInnerBlockContext(styledNode: StyledNode, box: BlockBoxBuilder, lctx: LayoutState, parent: ptr InnerBlockContext): InnerBlockContext = result = InnerBlockContext( styledNode: styledNode, |