From a7e9e532a5e961fa46845c75e4f5ba962fb21131 Mon Sep 17 00:00:00 2001 From: Paulo Medeiros Date: Mon, 4 Nov 2024 19:39:55 +0100 Subject: [PATCH] vaev-layout: first steps of fragmenting Before there was no concept of fragments and the layout step was taking the box tree as input and saving the used layout metrics into boxes themselves. This commit introduces the concept of Fragments, Fragmentainers and Fragmentation Context (https://drafts.csswg.org/css-break-4/#fragmentation-model). Currently, only pages are supported. The commit mechanism was changed from an enum (yes/no) to support these new concepts. Now, since layouting the box tree generates the Fragment tree, a layout call that should commit a Fragment for a Box into the Fragment tree will: - create the said Fragment - pass a reference to the said Fragment to it child call - add the created Fragment to the list of children of its parent in the Fragment tree by using the passed reference To Fragment, breakpoints with appeal were implemented, following Chrome, https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/core/layout/block_fragmentation_tutorial.md and its golden rule > Break at the breakpoint with the highest appeal (primary criterion) that also fits as much content as possible (secondary criterion). This first version only works for the block formatting context and only PDF is supported with the 'print' subcommand. Co-authored-by: Paulo Medeiros Co-authored-by: Nicolas Van Bossuyt --- src/vaev-driver/render.cpp | 82 ++++++++++------ src/vaev-driver/render.h | 7 +- src/vaev-layout/base.h | 65 ------------- src/vaev-layout/block.cpp | 161 ++++++++++++++++++++++++++++---- src/vaev-layout/block.h | 6 +- src/vaev-layout/box.cpp | 5 +- src/vaev-layout/box.h | 8 +- src/vaev-layout/flex.cpp | 42 ++++----- src/vaev-layout/flex.h | 2 +- src/vaev-layout/frag.h | 63 +++++++++++++ src/vaev-layout/fragmentainer.h | 72 ++++++++++++++ src/vaev-layout/grid.cpp | 2 +- src/vaev-layout/grid.h | 2 +- src/vaev-layout/inline.h | 2 +- src/vaev-layout/input_output.h | 65 +++++++++++++ src/vaev-layout/layout.cpp | 120 +++++++++++++++++++----- src/vaev-layout/layout.h | 4 +- src/vaev-layout/paint.cpp | 78 ++++++++-------- src/vaev-layout/paint.h | 6 +- src/vaev-layout/positioned.cpp | 44 ++++----- src/vaev-layout/positioned.h | 4 +- src/vaev-layout/table.cpp | 33 +++---- src/vaev-layout/table.h | 2 +- src/vaev-layout/tree.h | 12 +++ src/vaev-layout/values.cpp | 8 +- src/vaev-tools/main.cpp | 51 +++++++--- src/vaev-view/view.cpp | 14 +-- 27 files changed, 674 insertions(+), 286 deletions(-) create mode 100644 src/vaev-layout/frag.h create mode 100644 src/vaev-layout/fragmentainer.h create mode 100644 src/vaev-layout/input_output.h create mode 100644 src/vaev-layout/tree.h diff --git a/src/vaev-driver/render.cpp b/src/vaev-driver/render.cpp index b5954f6..803ce13 100644 --- a/src/vaev-driver/render.cpp +++ b/src/vaev-driver/render.cpp @@ -87,17 +87,22 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Vec2 start = Sys::now(); - Layout::layout( + auto root = Layout::Frag(); + + tree.fc.createNextFragmentainer(); + auto outDiscovery = Layout::layout( tree, tree.root, { - .commit = Layout::Commit::YES, + .fragment = &root, .knownSize = {vp.small.width, NONE}, .availableSpace = {vp.small.width, Px{0}}, .containingBlock = {vp.small.width, vp.small.height}, - } + }, + false ); - Layout::layoutPositioned(tree, tree.root, {vp.small.width, vp.small.height}); + + Layout::layoutPositioned(tree, root.children[0], {vp.small.width, vp.small.height}); auto sceneRoot = makeStrong(); @@ -105,8 +110,7 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Vec2 logDebugIf(DEBUG_RENDER, "layout tree layout time: {}", elapsed); auto paintStart = Sys::now(); - - Layout::paint(tree.root, *sceneRoot); + Layout::paint(root.children[0], *sceneRoot); sceneRoot->prepare(); elapsed = Sys::now() - paintStart; @@ -115,11 +119,12 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Vec2 return { std::move(stylebook), makeStrong(std::move(tree.root)), - sceneRoot, + {sceneRoot}, + makeStrong(root) }; } -RenderResult render(Markup::Document &dom, Style::Media const &media, Print::PaperStock paper) { +RenderResult print(Markup::Document &dom, Style::Media const &media, Vec2Px dimensions) { Style::StyleBook stylebook; stylebook.add( fetchStylesheet("bundle://vaev-view/user-agent.css"_url) @@ -134,36 +139,61 @@ RenderResult render(Markup::Document &dom, Style::Media const &media, Print::Pap Style::Computer computer{media, stylebook}; Layout::Viewport vp{ - .small = { - Px{paper.width}, - Px{paper.height}, - }, + .small = dimensions, }; Layout::Tree tree = { Layout::build(computer, dom), vp, + Layout::FragmentationContext(dimensions) }; - Layout::layout( - tree, - tree.root, - { - .commit = Layout::Commit::YES, - .knownSize = {vp.small.width, NONE}, - .availableSpace = {vp.small.width, Px{0}}, - .containingBlock = {vp.small.width, vp.small.height}, - } - ); + auto root = Layout::Frag(); + + while (true) { + tree.fc.createNextFragmentainer(); + + auto outDiscovery = Layout::layout( + tree, + tree.root, + { + .knownSize = {vp.small.width, NONE}, + .availableSpace = {vp.small.width, Px{0}}, + .containingBlock = {vp.small.width, vp.small.height}, + }, + true + ); + + auto outReal = Layout::layout( + tree, + tree.root, + { + .fragment = &root, + .knownSize = {vp.small.width, NONE}, + .availableSpace = {vp.small.width, Px{0}}, + .containingBlock = {vp.small.width, vp.small.height}, + }, + false + ); + + if (tree.fc.getStartPosition(tree.root) == 1) + break; + } - auto sceneRoot = makeStrong(); - Layout::paint(tree.root, *sceneRoot); - sceneRoot->prepare(); + Vec> scenes; + for (auto &c : root.children) { + auto scenePage = makeStrong(); + Layout::paint(c, *scenePage); + scenePage->prepare(); + + scenes.pushBack(scenePage); + } return { std::move(stylebook), makeStrong(std::move(tree.root)), - sceneRoot, + scenes, + makeStrong(root) }; } diff --git a/src/vaev-driver/render.h b/src/vaev-driver/render.h index 4e85572..823eb31 100644 --- a/src/vaev-driver/render.h +++ b/src/vaev-driver/render.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include @@ -11,11 +13,12 @@ namespace Vaev::Driver { struct RenderResult { Style::StyleBook style; Strong layout; - Strong scene; + Vec> scenes; + Strong frag; }; RenderResult render(Markup::Document const &dom, Style::Media const &media, Vec2Px viewport); -RenderResult render(Markup::Document &dom, Style::Media const &media, Print::PaperStock paper); +RenderResult print(Markup::Document &dom, Style::Media const &media, Vec2Px dimensions); } // namespace Vaev::Driver diff --git a/src/vaev-layout/base.h b/src/vaev-layout/base.h index 058b10e..c7b9ad1 100644 --- a/src/vaev-layout/base.h +++ b/src/vaev-layout/base.h @@ -22,71 +22,6 @@ struct Viewport { RectPx dynamic = small; }; -enum struct Commit { - NO, // No, only compute sizes - YES, // Yes, commit computed values to the tree -}; - -/// Input to the layout algorithm. - -struct Input { - Commit commit = Commit::NO; //< Should the computed values be committed to the layout? - IntrinsicSize intrinsic = IntrinsicSize::AUTO; - Math::Vec2> knownSize = {}; - Vec2Px position = {}; - Vec2Px availableSpace = {}; - Vec2Px containingBlock = {}; -}; - -/// Output of the layout algorithm. - -struct Output { - Vec2Px size; - - static Output fromSize(Vec2Px size) { - return Output{size}; - } -}; - -/// Computed layout values. - -struct Layout { - InsetsPx padding{}; - InsetsPx borders{}; - Vec2Px position; //< Position relative to the content box of the containing block - Vec2Px borderSize; - InsetsPx margin{}; - RadiiPx radii{}; - Px fontSize{16}; - - void repr(Io::Emit &e) const { - e("(layout paddings: {} borders: {} position: {} borderSize: {} margin: {} radii: {})", - padding, borders, position, borderSize, margin, radii); - } - - Layout offseted(Vec2Px offset) const { - auto copy = *this; - copy.position = position + offset; - return copy; - } - - RectPx borderBox() const { - return RectPx{position, borderSize}; - } - - RectPx paddingBox() const { - return borderBox().shrink(borders); - } - - RectPx contentBox() const { - return paddingBox().shrink(padding); - } - - RectPx marginBox() const { - return borderBox().grow(margin); - } -}; - struct Box; struct Tree; diff --git a/src/vaev-layout/block.cpp b/src/vaev-layout/block.cpp index 3afd574..100861b 100644 --- a/src/vaev-layout/block.cpp +++ b/src/vaev-layout/block.cpp @@ -1,10 +1,21 @@ -#include "block.h" +#include "input_output.h" -#include "box.h" #include "layout.h" +#include "tree.h" namespace Vaev::Layout { +// assuming second has more content than first +Breakpoint best(Breakpoint const &first, Breakpoint const &second) { + if (first.appeal == 0 and second.appeal == 0) + return first; + + if (first.appeal <= second.appeal) + return second; + else + return first; +} + // https://www.w3.org/TR/CSS22/visuren.html#normal-flow struct BlockFormatingContext { static Px _determineWidth(Tree &tree, Box &box, Input input) { @@ -14,7 +25,6 @@ struct BlockFormatingContext { tree, c, { - .commit = Commit::NO, .intrinsic = input.intrinsic, } ); @@ -25,13 +35,60 @@ struct BlockFormatingContext { return width; } - Output run(Tree &tree, Box &box, Input input) { - Px blockSize = Px{0}; + Output run(Tree &tree, Box &box, Input input, usize startAt, Opt stopAt) { + bool isDiscoveryMode = stopAt == NONE; + Px blockSize{0}; Px inlineSize = input.knownSize.width.unwrapOrElse([&] { return _determineWidth(tree, box, input); }); - for (auto &c : box.children()) { + Breakpoint currentBreakpoint; + + // BREAKPOINT CLASS C (top of content box, non-empty block) + if (tree.fc.allowBreak() and isDiscoveryMode and box.children().len() > 0) { + if (tree.fc.getCurrentFragmentainer()->acceptsFit( + input.position, + Vec2Px{inlineSize, blockSize}, + input.ancestralsBorderPadding + )) { + currentBreakpoint = Breakpoint{ + .endChildren = 0u, + .appeal = 1 + }; + } else { + return Output{ + .size = Vec2Px{blockSize, inlineSize}, + .completelyLaidOut = false, + .breakpoint = Breakpoint{ + .endChildren = 0, // FIXME + .appeal = 0, + }, + }; + } + } + + usize endChildren = stopAt.unwrapOr(box.children().len()); + + bool blockWasCompletelyLaidOutNonDiscovery = (box.children().len() == 0), lastChildCompletelyLaidOut = true; + bool blockWasCompletelyLaidOutDiscovery = true; + + for (usize i = startAt; i < endChildren; ++i) { + auto &c = box.children()[i]; + + // FORCED BREAK + if (tree.fc.allowBreak() and isDiscoveryMode and c.style->break_->before == BreakBetween::PAGE and i > startAt) { + + return { + .size = Vec2Px{inlineSize, blockSize}, + .completelyLaidOut = false, + .breakpoint = Breakpoint{ + .endChildren = i, + // since this is a FORCED break, it will have maximum appeal + .appeal = Limits::MAX + } + }; + } + // TODO: Implement floating // if (c.style->float_ != Float::NONE) // continue; @@ -43,9 +100,10 @@ struct BlockFormatingContext { } Input childInput = { - .commit = input.commit, + .fragment = input.fragment, .availableSpace = {inlineSize, Px{0}}, .containingBlock = {inlineSize, input.knownSize.y.unwrapOr(Px{0})}, + .ancestralsBorderPadding = input.ancestralsBorderPadding }; auto margin = computeMargins(tree, c, childInput); @@ -57,29 +115,94 @@ struct BlockFormatingContext { childInput.position = input.position + Vec2Px{margin.start, blockSize}; - auto ouput = layout( - tree, - c, - childInput - ); + Output output = + tree.fc.allowBreak() + ? layout( + tree, + c, + childInput, + stopAt == NONE + ) + : Output::fromSize(layout(tree, c, childInput).size); if (c.style->position != Position::ABSOLUTE) { - blockSize += ouput.size.y + margin.bottom; + blockSize += output.size.y + margin.bottom; + } + + if (tree.fc.allowBreak()) { + if (isDiscoveryMode) { + // breakpoint inside child (from this blocks perspective) + // BREAK CLASS X (recursive case) + Breakpoint insideChild{ + .endChildren = i + 1, + .appeal = box.style->break_->inside == BreakInside::AVOID + ? min(output.breakpoint.unwrap().appeal, 1u) + : output.breakpoint.unwrap().appeal + }; + + currentBreakpoint = best(currentBreakpoint, insideChild); + + // breakpoint right after child + // this can only happen IF child was fully laid out and its not last child (not class C) + // BREAK CLASS A + if (output.completelyLaidOut) { + if (i + 1 < box.children().len()) { + bool isAvoided = + box.style->break_->inside == BreakInside::AVOID or + c.style->break_->after == BreakBetween::AVOID or + box.children()[i + 1].style->break_->before == BreakBetween::AVOID; + + Breakpoint afterChild{ + .endChildren = i + 1, + .appeal = isAvoided ? 1u : 2u + }; + currentBreakpoint = best(currentBreakpoint, afterChild); + } + } else { + blockWasCompletelyLaidOutDiscovery = false; + break; + } + + // FORCED BREAK + if (c.style->break_->after == BreakBetween::PAGE) { + currentBreakpoint.appeal = Limits::MAX; + return { + .size = Vec2Px{inlineSize, blockSize}, + .completelyLaidOut = false, + .breakpoint = Breakpoint{ + .endChildren = i + 1, + // since this is a FORCED break, it will have maximum appeal + .appeal = Limits::MAX + } + }; + } + } else { + if (i + 1 == endChildren) { + lastChildCompletelyLaidOut = output.completelyLaidOut; + + if (not output.completelyLaidOut) + blockWasCompletelyLaidOutNonDiscovery = false; + else + blockWasCompletelyLaidOutNonDiscovery = i + 1 == box.children().len(); + } + } } } // layoutFloat(t, f, input.containingBlock); - return Output::fromSize({ - inlineSize, - blockSize, - }); + return Output{ + .size = Vec2Px{inlineSize, blockSize}, + .completelyLaidOut = isDiscoveryMode ? blockWasCompletelyLaidOutDiscovery : blockWasCompletelyLaidOutNonDiscovery, + .lastChildCompletelyLaidOut = lastChildCompletelyLaidOut, + .breakpoint = currentBreakpoint + }; } }; -Output blockLayout(Tree &tree, Box &box, Input input) { +Output blockLayout(Tree &tree, Box &box, Input input, usize startAt, Opt stopAt) { BlockFormatingContext fc; - return fc.run(tree, box, input); + return fc.run(tree, box, input, startAt, stopAt); } } // namespace Vaev::Layout diff --git a/src/vaev-layout/block.h b/src/vaev-layout/block.h index ff77b12..9634ed1 100644 --- a/src/vaev-layout/block.h +++ b/src/vaev-layout/block.h @@ -1,9 +1,11 @@ #pragma once -#include "base.h" +#include "input_output.h" + +#include "tree.h" namespace Vaev::Layout { -Output blockLayout(Tree &tree, Box &box, Input input); +Output blockLayout(Tree &tree, Box &box, Input input, usize startAt, Opt stopAt); } // namespace Vaev::Layout diff --git a/src/vaev-layout/box.cpp b/src/vaev-layout/box.cpp index 16dbc0e..1031359 100644 --- a/src/vaev-layout/box.cpp +++ b/src/vaev-layout/box.cpp @@ -2,6 +2,7 @@ #include #include "box.h" +#include "frag.h" namespace Vaev::Layout { @@ -37,7 +38,7 @@ void Box::add(Box &&box) { void Box::repr(Io::Emit &e) const { if (children()) { - e("(box {} {} {} {}", attrs, style->display, style->position, layout.borderBox()); + e("(box {} {} {}", attrs, style->display, style->position); e.indentNewline(); for (auto &c : children()) { c.repr(e); @@ -46,7 +47,7 @@ void Box::repr(Io::Emit &e) const { e.deindent(); e(")"); } else { - e("(box {} {} {} {})", attrs, style->display, style->position, layout.borderBox()); + e("(box {} {} {})", attrs, style->display, style->position); } } diff --git a/src/vaev-layout/box.h b/src/vaev-layout/box.h index 4e01bc2..c6cb3b9 100644 --- a/src/vaev-layout/box.h +++ b/src/vaev-layout/box.h @@ -11,6 +11,8 @@ namespace Vaev::Layout { // MARK: Box ------------------------------------------------------------------ +struct Box; + using Content = Union< None, Vec, @@ -31,7 +33,6 @@ struct Box : public Meta::NoCopy { Strong style; Strong fontFace; Content content = NONE; - Layout layout; Attrs attrs; Box(Strong style, Strong fontFace); @@ -47,9 +48,4 @@ struct Box : public Meta::NoCopy { void repr(Io::Emit &e) const; }; -struct Tree { - Box root; - Viewport viewport; -}; - } // namespace Vaev::Layout diff --git a/src/vaev-layout/flex.cpp b/src/vaev-layout/flex.cpp index b3bb73a..a81570e 100644 --- a/src/vaev-layout/flex.cpp +++ b/src/vaev-layout/flex.cpp @@ -152,17 +152,17 @@ struct FlexItem { FlexItem(Tree &tree, Box &box, bool isRowOriented, Vec2Px containingBlock) : box(&box), flexItemProps(*box.style->flex), fa(isRowOriented) { - speculateValues(tree, Input{Commit::NO}); + speculateValues(tree, Input{.containingBlock = containingBlock}); // TODO: not always we will need min/max content sizes, // this can be lazy computed for performance gains computeContentSizes(tree, containingBlock); } - void commit() { - box->layout.margin.top = margin.top.unwrapOr(speculativeMargin.top); - box->layout.margin.start = margin.start.unwrapOr(speculativeMargin.start); - box->layout.margin.end = margin.end.unwrapOr(speculativeMargin.end); - box->layout.margin.bottom = margin.bottom.unwrapOr(speculativeMargin.bottom); + void commit(MutCursor frag) { + frag->metrics.margin.top = margin.top.unwrapOr(speculativeMargin.top); + frag->metrics.margin.start = margin.start.unwrapOr(speculativeMargin.start); + frag->metrics.margin.end = margin.end.unwrapOr(speculativeMargin.end); + frag->metrics.margin.bottom = margin.bottom.unwrapOr(speculativeMargin.bottom); } void computeContentSizes(Tree &tree, Vec2Px containingBlock) { @@ -234,10 +234,7 @@ struct FlexItem { speculativeMargin = computeMargins( t, *box, - { - .commit = Commit::NO, - .containingBlock = input.availableSpace, // FIXME - } + input ); } @@ -430,7 +427,7 @@ struct FlexItem { } } - void alignCrossStretch(Tree &tree, Input input, Px lineCrossSize) { + void alignCrossStretch(Tree &tree, Px lineCrossSize) { if ( fa.crossAxis(box->style->sizing).type == Size::Type::AUTO and fa.startCrossAxis(*box->style->margin) != Width::Type::AUTO and @@ -444,7 +441,6 @@ struct FlexItem { speculateValues( tree, { - .commit = input.commit, .knownSize = fa.extractMainAxisAndFillOptOther(usedSize, elementSpeculativeCrossSize), } ); @@ -453,7 +449,7 @@ struct FlexItem { fa.crossAxis(position) = getMargin(START_CROSS); } - void alignItem(Tree &tree, Input input, Px lineCrossSize, Style::Align::Keywords parentAlignItems) { + void alignItem(Tree &tree, Px lineCrossSize, Style::Align::Keywords parentAlignItems) { auto align = box->style->aligns.alignSelf.keyword; if (align == Style::Align::AUTO) @@ -473,7 +469,7 @@ struct FlexItem { return; case Style::Align::STRETCH: - alignCrossStretch(tree, input, lineCrossSize); + alignCrossStretch(tree, lineCrossSize); return; default: @@ -482,7 +478,7 @@ struct FlexItem { return; } } -}; +}; // namespace Vaev::Layout struct FlexLine { MutSlice items; @@ -645,7 +641,6 @@ struct FlexFormatingContext { i.speculateValues( tree, { - .commit = Commit::NO, .knownSize = fa.extractMainAxisAndFillOptOther(i.flexBaseSize), .availableSpace = availableSpace, } @@ -1038,7 +1033,6 @@ struct FlexFormatingContext { i.speculateValues( tree, { - .commit = Commit::NO, .knownSize = fa.extractMainAxisAndFillOptOther(i.usedSize), .availableSpace = fa.extractMainAxisAndFillOther(i.usedSize, availableCrossSpace), } @@ -1254,7 +1248,7 @@ struct FlexFormatingContext { } } - if (input.commit == Commit::YES) { + if (input.fragment) { // This is done after any flexible lengths and any auto margins have been resolved. // NOTE: justifying doesnt change sizes/margins, thus will only run when committing and setting positions auto justifyContent = box.style->aligns.justifyContent.keyword; @@ -1318,12 +1312,11 @@ struct FlexFormatingContext { // 14. MARK: Align all flex items ------------------------------------------ // https://www.w3.org/TR/css-flexbox-1/#algo-cross-align - void _alignAllFlexItems(Tree &tree, Box &box, Input input) { + void _alignAllFlexItems(Tree &tree, Box &box) { for (auto &flexLine : _lines) { for (auto &flexItem : flexLine.items) { flexItem.alignItem( tree, - input, flexLine.crossSize, box.style->aligns.alignItems.keyword ); @@ -1443,14 +1436,13 @@ struct FlexFormatingContext { tree, *flexItem.box, { - .commit = Commit::YES, + .fragment = input.fragment, .knownSize = {flexItem.usedSize.x, flexItem.usedSize.y}, .position = flexItem.position, .availableSpace = flexItem.usedSize, } ); - - flexItem.commit(); + flexItem.commit(input.fragment); } } } @@ -1499,7 +1491,7 @@ struct FlexFormatingContext { _resolveCrossAxisAutoMargins(); // 14. Align all flex items along the cross-axis. - _alignAllFlexItems(tree, box, input); + _alignAllFlexItems(tree, box); // 15. Determine the flex container's used cross size _determineFlexContainerUsedCrossSize(input, box); @@ -1508,7 +1500,7 @@ struct FlexFormatingContext { _alignAllFlexLines(box); // XX. Commit - if (input.commit == Commit::YES) + if (input.fragment) _commit(tree, input); return Output::fromSize(fa.buildPair(_usedMainSize, _usedCrossSize)); diff --git a/src/vaev-layout/flex.h b/src/vaev-layout/flex.h index 20895de..a9658cc 100644 --- a/src/vaev-layout/flex.h +++ b/src/vaev-layout/flex.h @@ -1,6 +1,6 @@ #pragma once -#include "layout.h" +#include "input_output.h" namespace Vaev::Layout { diff --git a/src/vaev-layout/frag.h b/src/vaev-layout/frag.h new file mode 100644 index 0000000..a4aedb8 --- /dev/null +++ b/src/vaev-layout/frag.h @@ -0,0 +1,63 @@ +#pragma once + +#include "box.h" + +namespace Vaev::Layout { + +struct Metrics { + InsetsPx padding{}; + InsetsPx borders{}; + Vec2Px position; //< Position relative to the content box of the containing block + Vec2Px borderSize; + InsetsPx margin{}; + RadiiPx radii{}; + + void repr(Io::Emit &e) const { + e("(layout paddings: {} borders: {} position: {} borderSize: {} margin: {} radii: {})", + padding, borders, position, borderSize, margin, radii); + } + + RectPx borderBox() const { + return RectPx{position, borderSize}; + } + + RectPx paddingBox() const { + return borderBox().shrink(borders); + } + + RectPx contentBox() const { + return paddingBox().shrink(padding); + } + + RectPx marginBox() const { + return borderBox().grow(margin); + } +}; + +struct Frag { + MutCursor box; + Metrics metrics; + Vec children; + + Frag(MutCursor box) : box{std::move(box)} {} + + Frag() : box{nullptr} {} + + Style::Computed const &style() const { + return *box->style; + } + + /// Offset the position of this fragment and its subtree. + void offset(Vec2Px d) { + metrics.position = metrics.position + d; + for (auto &c : children) + c.offset(d); + } + + /// Add a child fragment. + void add(Frag &&frag) { + children.pushBack(std::move(frag)); + } +}; + +} // namespace Vaev::Layout diff --git a/src/vaev-layout/fragmentainer.h b/src/vaev-layout/fragmentainer.h new file mode 100644 index 0000000..3f2c4fb --- /dev/null +++ b/src/vaev-layout/fragmentainer.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include + +#include "frag.h" + +namespace Vaev::Layout { + +// TODO: if we have columns inside a page, who determines the position of the columns? the page or the column? +// columns are a pain... + +// https://www.w3.org/TR/css-break-3/#fragmentainer +struct Fragmentainer { + Vec2Px size; + + bool acceptsFit(Vec2Px position, Vec2Px sizeBox, Vec2Px ancestralsBorderPadding) { + Vec2Px total = position + sizeBox + ancestralsBorderPadding; + return total.x <= size.x and total.y <= size.y; + } +}; + +// https://www.w3.org/TR/css-break-3/#fragmentation-context +struct FragmentationContext { + + Vec2Px defaultSize = {Limits::MAX, Limits::MAX}; + + // TODO: discuss this being global with team + // TODO: merge maps into map of tuples (better memory access pattern) + Map layoutUntilComitted, layoutUntilNotComitted; + + FragmentationContext() {} + + FragmentationContext(Vec2Px defaultSize) : defaultSize(defaultSize) {} + + Vec> fragmentainers; + + void createNextFragmentainer() { + fragmentainers.pushBack(makeStrong(Fragmentainer(defaultSize))); + } + + Strong getCurrentFragmentainer() { + return fragmentainers[fragmentainers.len() - 1]; + } + + bool allowBreak() { + return not(defaultSize == Vec2Px{Limits::MAX, Limits::MAX}); + } + + usize getStartPosition(Box &box) { + if (not layoutUntilComitted.has(&box)) + return 0; + else + return layoutUntilComitted.get(&box); + } + + usize getDiscoveredEndPosition(Box &box) { + return layoutUntilNotComitted.get(&box); + } + + void setDiscoveredEndPosition(Box &box, usize pos) { + layoutUntilNotComitted.put(&box, pos); + } + + void setLaidOutEndPosition(Box &box, usize pos) { + layoutUntilComitted.put(&box, pos); + } +}; + +} // namespace Vaev::Layout diff --git a/src/vaev-layout/grid.cpp b/src/vaev-layout/grid.cpp index ad56a4d..b9de004 100644 --- a/src/vaev-layout/grid.cpp +++ b/src/vaev-layout/grid.cpp @@ -6,7 +6,7 @@ namespace Vaev::Layout { Output gridLayout(Tree &tree, Box &box, Input input) { // FIXME: Implement grid layout - return blockLayout(tree, box, input); + return blockLayout(tree, box, input, 0, NONE); } } // namespace Vaev::Layout diff --git a/src/vaev-layout/grid.h b/src/vaev-layout/grid.h index 91ffd99..30ecd11 100644 --- a/src/vaev-layout/grid.h +++ b/src/vaev-layout/grid.h @@ -1,6 +1,6 @@ #pragma once -#include "base.h" +#include "input_output.h" namespace Vaev::Layout { diff --git a/src/vaev-layout/inline.h b/src/vaev-layout/inline.h index 838475f..ecc1d14 100644 --- a/src/vaev-layout/inline.h +++ b/src/vaev-layout/inline.h @@ -1,6 +1,6 @@ #pragma once -#include "base.h" +#include "input_output.h" namespace Vaev::Layout { diff --git a/src/vaev-layout/input_output.h b/src/vaev-layout/input_output.h new file mode 100644 index 0000000..d8a2eec --- /dev/null +++ b/src/vaev-layout/input_output.h @@ -0,0 +1,65 @@ +#pragma once + +#include "base.h" +#include "frag.h" + +namespace Vaev::Layout { + +/// Input to the layout algorithm. +struct Input { + /// Parent fragment where the layout will be attached. + MutCursor fragment = nullptr; + IntrinsicSize intrinsic = IntrinsicSize::AUTO; + Math::Vec2> knownSize = {}; + Vec2Px position = {}; + Vec2Px availableSpace = {}; + Vec2Px containingBlock = {}; + + // TODO: instead of stringing this around, maybe change this (and check method of fragmentainer) to a + // "availableSpaceInFragmentainer" parameter + Vec2Px ancestralsBorderPadding = {}; +}; + +// NOTE: all these comments might be erased once we are secure on the strucutre and have proper documentation + +// TODO: consider adding classification for breakpoints, what would make appeal computing easier and less error prone +struct Breakpoint { + // only children with (index < endChildren) will be laid out + usize endChildren = 0; + // appeal = 0: an overflow occured; we want the earliest breakpoint possible + // appeal > 0: no overflow occured: we want the latest breakpoint possible with biggest appeal + usize appeal = 0; +}; + +struct Output { + // size of subtree maximizing displayed content while respecting + // - endchild constraint or + // - not overflowing fragmentainer or + // - forced break + Vec2Px size; + + // was the box subtree laid out until the end? + // - discovery mode: until the very end of the box + // - non discovery mode: all subtrees until endChildren-1 were completly laid out + // useful for: + // - discovery mode: knowing if a child was complete so we can break after it + // (if not fully laid out, we need to stop the block formatting context) + // - non-discovery mode: knowing if we should start from endChild or endChild + 1 the next pass + // if im the endChild-1, i'll need to revisit me the next pass if + // - my last child will need to be revisted (reason for lastChildCompletelyLaidOut) + // - my last child doesnt need to be revisted, but i still have some children to visit + bool completelyLaidOut; + bool lastChildCompletelyLaidOut = true; + + // only to be used in discovery mode + Opt breakpoint = NONE; + + static Output fromSize(Vec2Px size) { + return { + .size = size, + .completelyLaidOut = true + }; + } +}; + +} // namespace Vaev::Layout diff --git a/src/vaev-layout/layout.cpp b/src/vaev-layout/layout.cpp index c89d241..9ead6be 100644 --- a/src/vaev-layout/layout.cpp +++ b/src/vaev-layout/layout.cpp @@ -6,22 +6,46 @@ #include "grid.h" #include "inline.h" #include "table.h" +#include "tree.h" #include "values.h" namespace Vaev::Layout { -Output _contentLayout(Tree &tree, Box &box, Input input) { +Output _contentLayout(Tree &tree, Box &box, Input input, usize startAt, Opt stopAt) { auto display = box.style->display; if (auto run = box.content.is>()) { - return inlineLayout(tree, box, input); + auto out = inlineLayout(tree, box, input); + + if (not tree.fc.getCurrentFragmentainer()->acceptsFit( + input.position, + out.size, + input.ancestralsBorderPadding + )) { + return Output{ + .size = out.size, + .completelyLaidOut = false, + .breakpoint = Breakpoint{ + .endChildren = 0, + .appeal = 0 + } + }; + } else + return Output{ + .size = out.size, + .completelyLaidOut = true, + .breakpoint = Breakpoint{ + .endChildren = 1, + .appeal = 2 + } + }; } else if ( display == Display::FLOW or display == Display::FLOW_ROOT or display == Display::TABLE_CELL or display == Display::TABLE_CAPTION ) { - return blockLayout(tree, box, input); + return blockLayout(tree, box, input, startAt, stopAt); } else if (display == Display::FLEX) { return flexLayout(tree, box, input); } else if (display == Display::GRID) { @@ -31,7 +55,7 @@ Output _contentLayout(Tree &tree, Box &box, Input input) { } else if (display == Display::INTERNAL) { return Output{}; } else { - return blockLayout(tree, box, input); + return blockLayout(tree, box, input, startAt, stopAt); } } @@ -102,17 +126,17 @@ Vec2Px computeIntrinsicSize(Tree &tree, Box &box, IntrinsicSize intrinsic, Vec2P auto borders = computeBorders(tree, box); auto padding = _computePaddings(tree, box, containingBlock); - auto [size] = _contentLayout( + auto output = _contentLayout( tree, box, { - .commit = Commit::NO, .intrinsic = intrinsic, .knownSize = {NONE, NONE}, - } + }, + 0, NONE ); - return size + padding.all() + borders.all(); + return output.size + padding.all() + borders.all(); } static Opt _computeSpecifiedSize(Tree &tree, Box &box, Size size, Vec2Px containingBlock, bool isWidth) { @@ -141,8 +165,8 @@ static Opt _computeSpecifiedSize(Tree &tree, Box &box, Size size, Vec2Px con } } -Output layout(Tree &tree, Box &box, Input input) { - // FIXME: confirm how the preffered width/height parameters interacts with intrinsic size argument from input +Output layout(Tree &tree, Box &box, Input input, bool discoveryMode) { + // FIXME: confirm how the preferred width/height parameters interacts with intrinsic size argument from input auto borders = computeBorders(tree, box); auto padding = _computePaddings(tree, box, input.containingBlock); auto sizing = box.style->sizing; @@ -166,23 +190,75 @@ Output layout(Tree &tree, Box &box, Input input) { }); input.position = input.position + borders.topStart() + padding.topStart(); + input.ancestralsBorderPadding = input.ancestralsBorderPadding + borders.bottomEnd() + padding.bottomEnd(); + + usize startAt = tree.fc.allowBreak() ? tree.fc.getStartPosition(box) : 0; + + if (discoveryMode) { + if (not tree.fc.allowBreak()) + panic("discovery mode only allowed if we can break"); + auto out = _contentLayout(tree, box, input, startAt, NONE); + + auto size = out.size; + size.width = input.knownSize.width.unwrapOr(size.width); + size.height = input.knownSize.height.unwrapOr(size.height); + + // BREAKPOINT CLASS C (bottom of content box) + if (tree.fc.allowBreak() and out.completelyLaidOut) { + if (tree.fc.getCurrentFragmentainer()->acceptsFit( + input.position, + size, + input.ancestralsBorderPadding + )) { + out.breakpoint = Breakpoint{ + .endChildren = box.children().len(), + .appeal = 1 + }; + } else { + out.completelyLaidOut = false; + } + } + + out.size = size + padding.all() + borders.all(); - auto [size] = _contentLayout(tree, box, input); + tree.fc.setDiscoveredEndPosition(box, out.breakpoint.unwrap().endChildren); - size.width = input.knownSize.width.unwrapOr(size.width); - size.height = input.knownSize.height.unwrapOr(size.height); + return out; + } else { + Opt stopAt = tree.fc.allowBreak() + ? Opt{tree.fc.getDiscoveredEndPosition(box)} + : NONE; - size = size + padding.all() + borders.all(); + auto parentFrag = input.fragment; + Frag currFrag(&box); + input.fragment = &currFrag; - if (input.commit == Commit::YES) { - box.layout.position = input.position - borders.topStart() - padding.topStart(); - box.layout.borderSize = size; - box.layout.padding = padding; - box.layout.borders = borders; - box.layout.radii = _computeRadii(tree, box, size); - } + auto out = _contentLayout(tree, box, input, startAt, stopAt); + + auto size = out.size; + size.width = input.knownSize.width.unwrapOr(size.width); + size.height = input.knownSize.height.unwrapOr(size.height); - return Output::fromSize(size); + size = size + padding.all() + borders.all(); + + if (parentFrag) { + currFrag.metrics.position = input.position - borders.topStart() - padding.topStart(); + currFrag.metrics.borderSize = size; + currFrag.metrics.padding = padding; + currFrag.metrics.borders = borders; + currFrag.metrics.radii = _computeRadii(tree, box, size); + + parentFrag->add(std::move(currFrag)); + } + + if (tree.fc.allowBreak()) + tree.fc.setLaidOutEndPosition(box, out.lastChildCompletelyLaidOut ? stopAt.unwrap() : stopAt.unwrap() - 1); + + return Output{ + .size = size, + .completelyLaidOut = out.completelyLaidOut + }; + } } } // namespace Vaev::Layout diff --git a/src/vaev-layout/layout.h b/src/vaev-layout/layout.h index d274638..de50fdc 100644 --- a/src/vaev-layout/layout.h +++ b/src/vaev-layout/layout.h @@ -1,5 +1,7 @@ #pragma once +#include "input_output.h" + #include "base.h" namespace Vaev::Layout { @@ -10,6 +12,6 @@ InsetsPx computeBorders(Tree &tree, Box &box); Vec2Px computeIntrinsicSize(Tree &tree, Box &box, IntrinsicSize intrinsic, Vec2Px containingBlock); -Output layout(Tree &tree, Box &box, Input input); +Output layout(Tree &tree, Box &box, Input input, bool discoveryMode = false); } // namespace Vaev::Layout diff --git a/src/vaev-layout/paint.cpp b/src/vaev-layout/paint.cpp index 2903b28..da15640 100644 --- a/src/vaev-layout/paint.cpp +++ b/src/vaev-layout/paint.cpp @@ -2,21 +2,22 @@ #include #include "paint.h" +#include "vaev-layout/frag.h" namespace Vaev::Layout { -static bool _paintBorders(Box &box, Gfx::Color currentColor, Gfx::Borders &borders) { - currentColor = resolve(box.style->color, currentColor); +static bool _paintBorders(Frag &frag, Gfx::Color currentColor, Gfx::Borders &borders) { + currentColor = resolve(frag.style().color, currentColor); - borders.radii = box.layout.radii.cast(); + borders.radii = frag.metrics.radii.cast(); - auto bordersLayout = box.layout.borders; + auto bordersLayout = frag.metrics.borders; borders.widths.top = bordersLayout.top.cast(); borders.widths.bottom = bordersLayout.bottom.cast(); borders.widths.start = bordersLayout.start.cast(); borders.widths.end = bordersLayout.end.cast(); - auto bordersStyle = box.style->borders; + auto bordersStyle = frag.style().borders; borders.styles[0] = bordersStyle->top.style; borders.styles[1] = bordersStyle->end.style; borders.styles[2] = bordersStyle->bottom.style; @@ -30,8 +31,8 @@ static bool _paintBorders(Box &box, Gfx::Color currentColor, Gfx::Borders &borde return not borders.widths.zero(); } -static void _paintBox(Box &box, Gfx::Color currentColor, Scene::Stack &stack) { - auto const &backgrounds = box.style->backgrounds; +static void _paintFrag(Frag &frag, Gfx::Color currentColor, Scene::Stack &stack) { + auto const &backgrounds = frag.style().backgrounds; Scene::Box paint; bool hasBackgrounds = any(backgrounds); @@ -49,37 +50,38 @@ static void _paintBox(Box &box, Gfx::Color currentColor, Scene::Stack &stack) { } } - bool hasBorders = _paintBorders(box, currentColor, paint.borders); - paint.bound = box.layout.borderBox().cast(); + bool hasBorders = _paintBorders(frag, currentColor, paint.borders); + paint.bound = frag.metrics.borderBox().cast(); - if (hasBackgrounds or hasBorders) + if (hasBackgrounds or hasBorders) { stack.add(makeStrong(std::move(paint))); + } } -static void _establishStackingContext(Box &box, Scene::Stack &stack); -static void _paintStackingContext(Box &box, Scene::Stack &stack); +static void _establishStackingContext(Frag &frag, Scene::Stack &stack); +static void _paintStackingContext(Frag &frag, Scene::Stack &stack); -static void _paintBox(Box &box, Scene::Stack &stack) { +static void _paintFrag(Frag &frag, Scene::Stack &stack) { Gfx::Color currentColor = Gfx::BLACK; - currentColor = resolve(box.style->color, currentColor); + currentColor = resolve(frag.style().color, currentColor); - _paintBox(box, currentColor, stack); + _paintFrag(frag, currentColor, stack); - if (auto prose = box.content.is>()) { + if (auto prose = frag.box->content.is>()) { (*prose)->_style.color = currentColor; - Karm::Text::Font font = {box.fontFace, box.layout.fontSize.cast()}; + Karm::Text::Font font = {frag.box->fontFace, Px{16}.cast()}; Math::Vec2f baseline = {0, font.metrics().ascend}; stack.add(makeStrong( - box.layout.borderBox().topStart().cast(), + frag.metrics.borderBox().topStart().cast(), *prose )); } } -static void _paintChildren(Box &box, Scene::Stack &stack, auto predicate) { - for (auto &c : box.children()) { - auto &s = *c.style; +static void _paintChildren(Frag &frag, Scene::Stack &stack, auto predicate) { + for (auto &c : frag.children) { + auto &s = c.style(); auto zIndex = s.zIndex; if (zIndex != ZIndex::AUTO) { @@ -97,59 +99,59 @@ static void _paintChildren(Box &box, Scene::Stack &stack, auto predicate) { } if (predicate(s)) - _paintBox(c, stack); + _paintFrag(c, stack); _paintChildren(c, stack, predicate); } } -static void _paintStackingContext(Box &box, Scene::Stack &stack) { +static void _paintStackingContext(Frag &frag, Scene::Stack &stack) { // 1. the background and borders of the element forming the stacking context. - _paintBox(box, stack); + _paintFrag(frag, stack); // 2. the child stacking contexts with negative stack levels (most negative first). - _paintChildren(box, stack, [](Style::Computed const &s) { + _paintChildren(frag, stack, [](Style::Computed const &s) { return s.zIndex.value < 0; }); // 3. the in-flow, non-inline-level, non-positioned descendants. - _paintChildren(box, stack, [](Style::Computed const &s) { + _paintChildren(frag, stack, [](Style::Computed const &s) { return s.zIndex == ZIndex::AUTO and s.display != Display::INLINE and s.position == Position::STATIC; }); // 4. the non-positioned floats. - _paintChildren(box, stack, [](Style::Computed const &s) { + _paintChildren(frag, stack, [](Style::Computed const &s) { return s.zIndex == ZIndex::AUTO and s.position == Position::STATIC and s.float_ != Float::NONE; }); // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. - _paintChildren(box, stack, [](Style::Computed const &s) { + _paintChildren(frag, stack, [](Style::Computed const &s) { return s.zIndex == ZIndex::AUTO and s.display == Display::INLINE and s.position == Position::STATIC; }); // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0. - _paintChildren(box, stack, [](Style::Computed const &s) { + _paintChildren(frag, stack, [](Style::Computed const &s) { return s.zIndex.value == 0 and s.position != Position::STATIC; }); // 7. the child stacking contexts with positive stack levels (least positive first). - _paintChildren(box, stack, [](Style::Computed const &s) { + _paintChildren(frag, stack, [](Style::Computed const &s) { return s.zIndex.value > 0; }); } -static void _establishStackingContext(Box &box, Scene::Stack &stack) { +static void _establishStackingContext(Frag &frag, Scene::Stack &stack) { auto innerStack = makeStrong(); - innerStack->zIndex = box.style->zIndex.value; - _paintStackingContext(box, *innerStack); + innerStack->zIndex = frag.style().zIndex.value; + _paintStackingContext(frag, *innerStack); stack.add(std::move(innerStack)); } -void paint(Box &box, Scene::Stack &stack) { - _paintStackingContext(box, stack); +void paint(Frag &frag, Scene::Stack &stack) { + _paintStackingContext(frag, stack); } -void wireframe(Box &box, Gfx::Canvas &g) { - for (auto &c : box.children()) +void wireframe(Frag &frag, Gfx::Canvas &g) { + for (auto &c : frag.children) wireframe(c, g); g.strokeStyle({ @@ -158,7 +160,7 @@ void wireframe(Box &box, Gfx::Canvas &g) { .align = Gfx::INSIDE_ALIGN, }); - g.stroke(box.layout.borderBox().cast()); + g.stroke(frag.metrics.borderBox().cast()); } } // namespace Vaev::Layout diff --git a/src/vaev-layout/paint.h b/src/vaev-layout/paint.h index 922fe99..c22a915 100644 --- a/src/vaev-layout/paint.h +++ b/src/vaev-layout/paint.h @@ -2,12 +2,12 @@ #include -#include "box.h" +#include "frag.h" namespace Vaev::Layout { -void wireframe(Box &box, Gfx::Canvas &g); +void wireframe(Frag &frag, Gfx::Canvas &g); -void paint(Box &box, Scene::Stack &stack); +void paint(Frag &frag, Scene::Stack &stack); } // namespace Vaev::Layout diff --git a/src/vaev-layout/positioned.cpp b/src/vaev-layout/positioned.cpp index 829d661..db3fa15 100644 --- a/src/vaev-layout/positioned.cpp +++ b/src/vaev-layout/positioned.cpp @@ -5,44 +5,40 @@ namespace Vaev::Layout { -void layoutPositioned(Tree &tree, Box &box, RectPx containingBlock) { - if (box.style->position == Position::ABSOLUTE or box.style->position == Position::RELATIVE) { +void layoutPositioned(Tree &tree, Frag &frag, RectPx containingBlock) { + auto &style = frag.style(); + auto &metrics = frag.metrics; + + if (style.position == Position::ABSOLUTE or style.position == Position::RELATIVE) { auto origin = containingBlock.topStart(); - if (box.style->position == Position::RELATIVE) - origin = box.layout.position; + if (style.position == Position::RELATIVE) + origin = metrics.position; - auto top = box.layout.position.y; - auto start = box.layout.position.x; + auto top = metrics.position.y; + auto start = metrics.position.x; - auto topOffset = box.style->offsets->top; + auto topOffset = style.offsets->top; if (topOffset != Width::AUTO) { - top = origin.y + resolve(tree, box, topOffset, containingBlock.height); + top = origin.y + resolve(tree, *frag.box, topOffset, containingBlock.height); } - auto startOffset = box.style->offsets->start; + auto startOffset = style.offsets->start; if (startOffset != Width::AUTO) { - start = origin.x + resolve(tree, box, startOffset, containingBlock.width); + start = origin.x + resolve(tree, *frag.box, startOffset, containingBlock.width); } - auto endOffset = box.style->offsets->end; + auto endOffset = frag.style().offsets->end; if (endOffset != Width::AUTO) { - start = (origin.x + containingBlock.width) - resolve(tree, box, endOffset, containingBlock.width) - box.layout.borderSize.width; + start = (origin.x + containingBlock.width) - resolve(tree, *frag.box, endOffset, containingBlock.width) - metrics.borderSize.width; } - layout( - tree, - box, - { - .commit = Commit::YES, - .knownSize = box.layout.borderBox().size().cast>(), - .position = {start, top}, - } - ); - - containingBlock = box.layout.contentBox(); + Vec2Px newPositionOffset = Vec2Px{start, top} - metrics.position; + frag.offset(newPositionOffset); + + containingBlock = metrics.contentBox(); } - for (auto &c : box.children()) { + for (auto &c : frag.children) { layoutPositioned(tree, c, containingBlock); } } diff --git a/src/vaev-layout/positioned.h b/src/vaev-layout/positioned.h index 2e9bd17..992bc53 100644 --- a/src/vaev-layout/positioned.h +++ b/src/vaev-layout/positioned.h @@ -1,9 +1,9 @@ #pragma once -#include "box.h" +#include "frag.h" namespace Vaev::Layout { -void layoutPositioned(Tree &tree, Box &box, RectPx containingBlock); +void layoutPositioned(Tree &tree, Frag &frag, RectPx containingBlock); } // namespace Vaev::Layout diff --git a/src/vaev-layout/table.cpp b/src/vaev-layout/table.cpp index ce958d8..280fcdf 100644 --- a/src/vaev-layout/table.cpp +++ b/src/vaev-layout/table.cpp @@ -519,8 +519,7 @@ struct TableFormatingContext { auto captionOutput = layout( tree, wrapperBox.children()[i], - Input{ - .commit = Commit::NO, + { .intrinsic = IntrinsicSize::MIN_CONTENT, } ); @@ -531,8 +530,7 @@ struct TableFormatingContext { auto cellMinOutput = layout( tree, *cell.box, - Input{ - .commit = Commit::NO, + { .intrinsic = IntrinsicSize::MIN_CONTENT, } ); @@ -540,8 +538,7 @@ struct TableFormatingContext { auto cellMaxOutput = layout( tree, *cell.box, - Input{ - .commit = Commit::NO, + { .intrinsic = IntrinsicSize::MAX_CONTENT, } ); @@ -752,12 +749,8 @@ struct TableFormatingContext { tree, *cell.box, { - .commit = Commit::NO, .intrinsic = IntrinsicSize::MIN_CONTENT, - .knownSize = { - colWidth[j], - NONE, - }, + .knownSize = {colWidth[j], NONE}, } ); @@ -809,7 +802,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { tree, wrapper.children()[i], { - .commit = input.commit, + .fragment = input.fragment, .position = {input.position.x, currPositionY}, } ); @@ -844,7 +837,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { table.spacing.y * Px{table.grid.size.y + 1}, }; - if (input.commit == Commit::YES) { + if (input.fragment) { Px currPositionX{input.position.x}; // table box @@ -852,7 +845,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { tree, table.tableBox, { - .commit = Commit::YES, + .fragment = input.fragment, .knownSize = { tableBoxSize.x + table.boxBorder.horizontal(), tableBoxSize.y + table.boxBorder.vertical(), @@ -870,7 +863,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { tree, group.el, { - .commit = Commit::YES, + .fragment = input.fragment, .knownSize = { queryPref(colWidthPref, group.start, group.end), tableBoxSize.y, @@ -889,7 +882,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { tree, col.el, { - .commit = Commit::YES, + .fragment = input.fragment, .knownSize = { queryPref(colWidthPref, col.start, col.end), tableBoxSize.y, @@ -908,7 +901,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { tree, group.el, { - .commit = Commit::YES, + .fragment = input.fragment, .knownSize = { tableBoxSize.x, queryPref(rowHeightPref, group.start, group.end), @@ -927,7 +920,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { tree, row.el, { - .commit = Commit::YES, + .fragment = input.fragment, .knownSize = { tableBoxSize.x, queryPref(rowHeightPref, row.start, row.end), @@ -963,7 +956,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { tree, *cell.box, { - .commit = Commit::YES, + .fragment = input.fragment, .knownSize = { queryPref(colWidthPref, j, j + colSpan - 1) + table.spacing.x * Px{colSpan - 1}, queryPref(rowHeightPref, i, i + rowSpan - 1) + table.spacing.y * Px{rowSpan - 1} @@ -981,7 +974,7 @@ Output tableLayout(Tree &tree, Box &wrapper, Input input) { tree, wrapper.children()[i], { - .commit = input.commit, + .fragment = input.fragment, .knownSize = {tableUsedWidth, NONE}, .position = {input.position.x, currPositionY}, } diff --git a/src/vaev-layout/table.h b/src/vaev-layout/table.h index 6044525..86691a7 100644 --- a/src/vaev-layout/table.h +++ b/src/vaev-layout/table.h @@ -1,6 +1,6 @@ #pragma once -#include "base.h" +#include "input_output.h" namespace Vaev::Layout { diff --git a/src/vaev-layout/tree.h b/src/vaev-layout/tree.h new file mode 100644 index 0000000..de481ce --- /dev/null +++ b/src/vaev-layout/tree.h @@ -0,0 +1,12 @@ +#pragma once + +#include "box.h" +#include "fragmentainer.h" + +namespace Vaev::Layout { +struct Tree { + Box root; + Viewport viewport; + FragmentationContext fc = {}; +}; +} // namespace Vaev::Layout diff --git a/src/vaev-layout/values.cpp b/src/vaev-layout/values.cpp index 2f001d3..9829d64 100644 --- a/src/vaev-layout/values.cpp +++ b/src/vaev-layout/values.cpp @@ -1,14 +1,16 @@ #include "values.h" -#include "box.h" +#include "tree.h" #include "writing.h" namespace Vaev::Layout { Resolver Resolver::from(Tree const &tree, Box const &box) { + Px fontSize{16}; + Resolver resolver; - resolver.rootFont = Text::Font{tree.root.fontFace, tree.root.layout.fontSize.cast()}; - resolver.boxFont = Text::Font{box.fontFace, box.layout.fontSize.cast()}; + resolver.rootFont = Text::Font{tree.root.fontFace, fontSize.cast()}; + resolver.boxFont = Text::Font{box.fontFace, fontSize.cast()}; resolver.viewport = tree.viewport; resolver.boxAxis = mainAxis(box); return resolver; diff --git a/src/vaev-tools/main.cpp b/src/vaev-tools/main.cpp index d34b1ab..12b5706 100644 --- a/src/vaev-tools/main.cpp +++ b/src/vaev-tools/main.cpp @@ -99,12 +99,13 @@ Res<> markupDumpTokens(Mime::Url const &url) { return Ok(); } -Vaev::Style::Media constructMediaForPrint(Print::PaperStock paper) { +Vaev::Style::Media constructMediaForPrint(Vec2Px paperDimensions) { return { .type = Vaev::MediaType::PRINT, - .width = Vaev::Px{paper.width}, - .height = Vaev::Px{paper.height}, - .aspectRatio = paper.width / paper.height, + // FIXME: paper dimensions is in mm not in px + .width = paperDimensions.width, + .height = paperDimensions.height, + .aspectRatio = (double)paperDimensions.width / (double)paperDimensions.height, .orientation = Vaev::Orientation::PORTRAIT, .resolution = Vaev::Resolution::fromDpi(96), @@ -138,12 +139,30 @@ struct PrintOption { bool dumpDom = false; bool dumpLayout = false; bool dumpPaint = false; + + Opt size = NONE; + Opt paper = NONE; }; Res<> print(Mime::Url const &, Strong dom, Io::Writer &output, PrintOption options = {}) { auto paper = Print::A4; - auto media = constructMediaForPrint(paper); - auto [style, layout, paint] = Vaev::Driver::render(*dom, media, paper); + + Vec2Px dimensions = {}; + if (options.size) { + dimensions = Vec2Px{Px{options.size.unwrap().x}, Px{options.size.unwrap().y}}; + } else { + dimensions = Vec2Px{ + Px{(paper.width / Print::INCH_TO_MM) * (Karm::Print::Density::DEFAULT.toDpi())}, + Px{(paper.width / Print::INCH_TO_MM) * (Karm::Print::Density::DEFAULT.toDpi())}, + }; + } + + auto media = constructMediaForPrint(dimensions); + auto [style, layout, pages, frag] = Vaev::Driver::print( + *dom, + media, + dimensions + ); if (options.dumpDom) Sys::println("--- START OF DOM ---\n{}\n--- END OF DOM ---\n", dom); @@ -155,10 +174,12 @@ Res<> print(Mime::Url const &, Strong dom, Io::Writer &output, Sys::println("--- START OF LAYOUT ---\n{}\n--- END OF LAYOUT ---\n", layout); if (options.dumpPaint) - Sys::println("--- START OF PAINT ---\n{}\n--- END OF PAINT ---\n", paint); + Sys::println("--- START OF PAINT ---\n{}\n--- END OF PAINT ---\n", pages); Print::PdfPrinter printer{Print::A4, Print::Density::DEFAULT}; - paint->print(printer); + for (auto &page : pages) { + page->print(printer); + } Io::TextEncoder<> encoder{output}; Io::Emit e{encoder}; @@ -223,7 +244,7 @@ struct RenderOption { Res<> render(Mime::Url const &, Strong dom, Io::Writer &output, RenderOption options = {}) { auto media = constructMediaForRender(options.size); - auto [style, layout, paint] = Vaev::Driver::render(*dom, media, options.size.cast()); + auto [style, layout, paint, frag] = Vaev::Driver::render(*dom, media, options.size.cast()); if (options.dumpDom) Sys::println("--- START OF DOM ---\n{}\n--- END OF DOM ---\n", dom); @@ -235,13 +256,13 @@ Res<> render(Mime::Url const &, Strong dom, Io::Writer &output Sys::println("--- START OF LAYOUT ---\n{}\n--- END OF LAYOUT ---\n", layout); if (options.dumpPaint) - Sys::println("--- START OF PAINT ---\n{}\n--- END OF PAINT ---\n", paint); + Sys::println("--- START OF PAINT ---\n{}\n--- END OF PAINT ---\n", paint[0]); auto image = Gfx::Surface::alloc(options.size, Gfx::RGBA8888); Gfx::CpuCanvas g; g.begin(*image); g.clear(Gfx::WHITE); - paint->paint(g); + paint[0]->paint(g); g.end(); try$(Image::save(image->pixels(), output)); @@ -362,6 +383,8 @@ Async::Task<> entryPointAsync(Sys::Context &ctx) { Cli::Flag dumpDomArg = Cli::flag('d', "dump-dom"s, "Dump the DOM tree"s); Cli::Flag dumpLayoutArg = Cli::flag('l', "dump-layout"s, "Dump the layout tree"s); Cli::Flag dumpPaintArg = Cli::flag('p', "dump-paint"s, "Dump the paint tree"s); + Cli::Option widthArg = Cli::option('w', "width"s, "Width of the output image"s, 800); + Cli::Option heightArg = Cli::option('h', "height"s, "Height of the output image"s, 600); cmd.subCommand( "print"s, @@ -374,6 +397,8 @@ Async::Task<> entryPointAsync(Sys::Context &ctx) { dumpDomArg, dumpLayoutArg, dumpPaintArg, + widthArg, + heightArg, }, [=](Sys::Context &) -> Async::Task<> { Vaev::Tools::PrintOption options{ @@ -381,6 +406,7 @@ Async::Task<> entryPointAsync(Sys::Context &ctx) { .dumpDom = dumpDomArg, .dumpLayout = dumpLayoutArg, .dumpPaint = dumpPaintArg, + .size = Opt{{widthArg, heightArg}}, }; Mime::Url inputUrl = "about:stdin"_url; @@ -405,9 +431,6 @@ Async::Task<> entryPointAsync(Sys::Context &ctx) { } ); - Cli::Option widthArg = Cli::option('w', "width"s, "Width of the output image"s, 800); - Cli::Option heightArg = Cli::option('h', "height"s, "Height of the output image"s, 600); - cmd.subCommand( "render"s, 'r', diff --git a/src/vaev-view/view.cpp b/src/vaev-view/view.cpp index d88d434..e52dfa2 100644 --- a/src/vaev-view/view.cpp +++ b/src/vaev-view/view.cpp @@ -63,14 +63,14 @@ struct View : public Ui::View { g.origin(bound().xy.cast()); g.clip(viewport); - auto [_, layout, paint] = *_renderResult; + auto [_, layout, paint, frag] = *_renderResult; g.clear(rect, Gfx::WHITE); - paint->paint(g); + paint[0]->paint(g); if (Ui::debugShowLayoutBounds) { logDebug("layout tree: {}", layout); - logDebug("paint tree: {}", paint); - Layout::wireframe(*layout, g); + logDebug("paint tree: {}", paint[0]); + Layout::wireframe(*frag, g); } g.pop(); @@ -84,11 +84,11 @@ struct View : public Ui::View { Math::Vec2i size(Math::Vec2i size, Ui::Hint) override { // FIXME: This is wasteful, we should cache the result auto media = _constructMedia(size); - auto [_, layout, _] = Driver::render(*_dom, media, size.cast()); + auto [_, layout, _, frag] = Driver::render(*_dom, media, size.cast()); return { - layout->layout.borderBox().width.cast(), - layout->layout.borderBox().height.cast(), + frag->metrics.borderBox().width.cast(), + frag->metrics.borderBox().height.cast(), }; } };