From 2ee43c70f85b901e9ab060c321be316a8effaeda Mon Sep 17 00:00:00 2001 From: Lou ! Date: Wed, 14 May 2025 14:26:06 +0200 Subject: [PATCH] vaev-style: Add running positions, content element() and counter = page to allow beautiful margins. --- src/vaev-engine/dom/window.cpp | 2 +- src/vaev-engine/driver/print.cpp | 187 ++++++++---- src/vaev-engine/layout/base/box.cpp | 1 + src/vaev-engine/layout/base/input.cpp | 3 + src/vaev-engine/layout/base/layout-impl.cpp | 15 +- src/vaev-engine/layout/base/mod.cpp | 1 + .../layout/base/running-position.cpp | 119 ++++++++ src/vaev-engine/layout/block.cpp | 5 + src/vaev-engine/layout/builder/mod.cpp | 62 +++- src/vaev-engine/layout/flex.cpp | 12 +- src/vaev-engine/layout/inline.cpp | 9 + src/vaev-engine/layout/paint.cpp | 11 +- src/vaev-engine/layout/positioned.cpp | 24 +- src/vaev-engine/style/props-impl.cpp | 3 +- src/vaev-engine/style/props.cpp | 11 +- src/vaev-engine/style/specified.cpp | 5 +- src/vaev-engine/values/content.cpp | 135 ++++++++ src/vaev-engine/values/defs/keywords.inc | 5 + src/vaev-engine/values/insets.cpp | 40 ++- src/vaev-engine/values/mod.cpp | 1 + src/vaev-engine/values/primitives.cpp | 8 + tests/css/print/margin-content.xhtml | 288 ++++++++++++++++++ 22 files changed, 829 insertions(+), 118 deletions(-) create mode 100644 src/vaev-engine/layout/base/running-position.cpp create mode 100644 src/vaev-engine/values/content.cpp create mode 100644 tests/css/print/margin-content.xhtml diff --git a/src/vaev-engine/dom/window.cpp b/src/vaev-engine/dom/window.cpp index 746da84ec..47697317a 100644 --- a/src/vaev-engine/dom/window.cpp +++ b/src/vaev-engine/dom/window.cpp @@ -96,7 +96,7 @@ export struct Window { } [[clang::coro_wrapper]] - Generator print(Print::Settings settings) const { + Generator print(Print::Settings const& settings) const { return Driver::print(_document.upgrade(), settings); } }; diff --git a/src/vaev-engine/driver/print.cpp b/src/vaev-engine/driver/print.cpp index 3542cd200..beee2711a 100644 --- a/src/vaev-engine/driver/print.cpp +++ b/src/vaev-engine/driver/print.cpp @@ -20,9 +20,9 @@ using namespace Karm; namespace Vaev::Driver { -void _paintCornerMargin(Style::PageSpecifiedValues& pageStyle, Scene::Stack& stack, RectAu const& rect, Style::PageArea area) { +void _paintCornerMargin(Style::PageSpecifiedValues& pageStyle, Scene::Stack& stack, RectAu const& rect, Style::PageArea area, usize currentPage, Layout::RunningPositionMap& runningPosition) { Layout::Tree tree{ - .root = Layout::buildForPseudoElement(pageStyle.area(area)), + .root = Layout::buildForPseudoElement(pageStyle.area(area), currentPage, runningPosition), .viewport = Layout::Viewport{.small = rect.size()} }; auto [_, frag] = Layout::layoutCreateFragment( @@ -37,10 +37,10 @@ void _paintCornerMargin(Style::PageSpecifiedValues& pageStyle, Scene::Stack& sta Layout::paint(frag, stack); } -void _paintMainMargin(Style::PageSpecifiedValues& pageStyle, Scene::Stack& stack, RectAu const& rect, Style::PageArea mainArea, Array subAreas) { - auto box = Layout::buildForPseudoElement(pageStyle.area(mainArea)); +void _paintMainMargin(Style::PageSpecifiedValues& pageStyle, Scene::Stack& stack, RectAu const& rect, Style::PageArea mainArea, Array subAreas, usize currentPage, Layout::RunningPositionMap& runningPosition) { + auto box = Layout::buildForPseudoElement(pageStyle.area(mainArea), currentPage, runningPosition); for (auto subArea : subAreas) { - box.add(Layout::buildForPseudoElement(pageStyle.area(subArea))); + box.add(Layout::buildForPseudoElement(pageStyle.area(subArea), currentPage, runningPosition)); } Layout::Tree tree{ .root = std::move(box), @@ -58,7 +58,7 @@ void _paintMainMargin(Style::PageSpecifiedValues& pageStyle, Scene::Stack& stack Layout::paint(frag, stack); } -void _paintMargins(Style::PageSpecifiedValues& pageStyle, RectAu pageRect, RectAu pageContent, Scene::Stack& stack) { +void _paintMargins(Style::PageSpecifiedValues& pageStyle, RectAu pageRect, RectAu pageContent, Scene::Stack& stack, usize currentPage, Layout::RunningPositionMap& runningPosition) { // Compute all corner rects auto topLeftMarginCornerRect = RectAu::fromTwoPoint(pageRect.topStart(), pageContent.topStart()); auto topRightMarginCornerRect = RectAu::fromTwoPoint(pageRect.topEnd(), pageContent.topEnd()); @@ -66,10 +66,10 @@ void _paintMargins(Style::PageSpecifiedValues& pageStyle, RectAu pageRect, RectA auto bottomRightMarginCornerRect = RectAu::fromTwoPoint(pageRect.bottomEnd(), pageContent.bottomEnd()); // Paint corners - _paintCornerMargin(pageStyle, stack, topLeftMarginCornerRect, Style::PageArea::TOP_LEFT_CORNER); - _paintCornerMargin(pageStyle, stack, topRightMarginCornerRect, Style::PageArea::TOP_RIGHT_CORNER); - _paintCornerMargin(pageStyle, stack, bottomLeftMarginCornerRect, Style::PageArea::BOTTOM_LEFT_CORNER); - _paintCornerMargin(pageStyle, stack, bottomRightMarginCornerRect, Style::PageArea::BOTTOM_RIGHT_CORNER); + _paintCornerMargin(pageStyle, stack, topLeftMarginCornerRect, Style::PageArea::TOP_LEFT_CORNER, currentPage, runningPosition); + _paintCornerMargin(pageStyle, stack, topRightMarginCornerRect, Style::PageArea::TOP_RIGHT_CORNER, currentPage, runningPosition); + _paintCornerMargin(pageStyle, stack, bottomLeftMarginCornerRect, Style::PageArea::BOTTOM_LEFT_CORNER, currentPage, runningPosition); + _paintCornerMargin(pageStyle, stack, bottomRightMarginCornerRect, Style::PageArea::BOTTOM_RIGHT_CORNER, currentPage, runningPosition); // Compute main area rects auto topRect = RectAu::fromTwoPoint(topLeftMarginCornerRect.topEnd(), topRightMarginCornerRect.bottomStart()); @@ -78,46 +78,36 @@ void _paintMargins(Style::PageSpecifiedValues& pageStyle, RectAu pageRect, RectA auto rightRect = RectAu::fromTwoPoint(topRightMarginCornerRect.bottomEnd(), bottomRightMarginCornerRect.topStart()); // Paint main areas - _paintMainMargin(pageStyle, stack, topRect, Style::PageArea::TOP, {Style::PageArea::TOP_LEFT, Style::PageArea::TOP_CENTER, Style::PageArea::TOP_RIGHT}); - _paintMainMargin(pageStyle, stack, bottomRect, Style::PageArea::BOTTOM, {Style::PageArea::BOTTOM_LEFT, Style::PageArea::BOTTOM_CENTER, Style::PageArea::BOTTOM_RIGHT}); - _paintMainMargin(pageStyle, stack, leftRect, Style::PageArea::LEFT, {Style::PageArea::LEFT_TOP, Style::PageArea::LEFT_MIDDLE, Style::PageArea::LEFT_BOTTOM}); - _paintMainMargin(pageStyle, stack, rightRect, Style::PageArea::RIGHT, {Style::PageArea::RIGHT_TOP, Style::PageArea::RIGHT_MIDDLE, Style::PageArea::RIGHT_BOTTOM}); + _paintMainMargin(pageStyle, stack, topRect, Style::PageArea::TOP, {Style::PageArea::TOP_LEFT, Style::PageArea::TOP_CENTER, Style::PageArea::TOP_RIGHT}, currentPage, runningPosition); + _paintMainMargin(pageStyle, stack, bottomRect, Style::PageArea::BOTTOM, {Style::PageArea::BOTTOM_LEFT, Style::PageArea::BOTTOM_CENTER, Style::PageArea::BOTTOM_RIGHT}, currentPage, runningPosition); + _paintMainMargin(pageStyle, stack, leftRect, Style::PageArea::LEFT, {Style::PageArea::LEFT_TOP, Style::PageArea::LEFT_MIDDLE, Style::PageArea::LEFT_BOTTOM}, currentPage, runningPosition); + _paintMainMargin(pageStyle, stack, rightRect, Style::PageArea::RIGHT, {Style::PageArea::RIGHT_TOP, Style::PageArea::RIGHT_MIDDLE, Style::PageArea::RIGHT_BOTTOM}, currentPage, runningPosition); } -export Generator print(Gc::Ref dom, Print::Settings settings) { - auto media = Style::Media::forPrint(settings); - - Font::Database db; - if (not db.loadSystemFonts()) - logWarn("not all fonts were properly loaded into database"); - - Style::Computer computer{ - media, *dom->styleSheets, db - }; - computer.build(); - computer.styleDocument(*dom); - - // MARK: Page and Margins -------------------------------------------------- - - Style::SpecifiedValues initialStyle = Style::SpecifiedValues::initial(); - initialStyle.color = Gfx::BLACK; - initialStyle.setCustomProp("-vaev-url", {Css::Token::string(Io::format("\"{}\"", dom->url()))}); - initialStyle.setCustomProp("-vaev-title", {Css::Token::string(Io::format("\"{}\"", dom->title()))}); - initialStyle.setCustomProp("-vaev-datetime", {Css::Token::string(Io::format("\"{}\"", Sys::now()))}); - - // MARK: Page Content ------------------------------------------------------ - - Layout::Tree contentTree = { - Layout::build(dom), - }; - auto canvasColor = fixupBackgrounds(computer, dom, contentTree); - +struct PaginationContext { + Layout::Tree& contentTree; + Style::Media& media; + Print::Settings const& settings; + Style::Computer& computer; + Style::SpecifiedValues& initialStyle; +}; + +struct PageLayoutInfos { + RectAu pageRect; + RectAu pageContent; + Rc pageStyle; +}; + +Pair, Vec> collectBreakPointsAndRunningPositions(Layout::RunningPositionMap& runningPosition, PaginationContext& context) { + usize pageNumber = 0; Layout::Breakpoint prevBreakpoint{ .endIdx = 0, .advance = Layout::Breakpoint::Advance::WITHOUT_CHILDREN }; - usize pageNumber = 0; + Vec breakpoints = {prevBreakpoint}; + Vec pageInfos = {}; + while (true) { Style::Page page{ .name = ""s, @@ -125,16 +115,19 @@ export Generator print(Gc::Ref dom, Print::Settings .blank = false, }; - auto pageStyle = computer.computeFor(initialStyle, page); + Rc pageStyle = context.computer.computeFor(context.initialStyle, page); RectAu pageRect{ - media.width / Au{media.resolution.toDppx()}, - media.height / Au{media.resolution.toDppx()} + context.media.width / Au{context.media.resolution.toDppx()}, + context.media.height / Au{context.media.resolution.toDppx()} }; + + auto pageSize = pageRect.size().cast(); + auto pageStack = makeRc(); InsetsAu pageMargin; - if (settings.margins == Print::Margins::DEFAULT) { + if (context.settings.margins == Print::Margins::DEFAULT) { Layout::Resolver resolver{}; pageMargin = { resolver.resolve(pageStyle->style->margin->top, pageRect.height), @@ -142,34 +135,35 @@ export Generator print(Gc::Ref dom, Print::Settings resolver.resolve(pageStyle->style->margin->bottom, pageRect.height), resolver.resolve(pageStyle->style->margin->start, pageRect.width), }; - } else if (settings.margins == Print::Margins::CUSTOM) { - pageMargin = settings.margins.custom.cast(); - } else if (settings.margins == Print::Margins::MINIMUM) { + } else if (context.settings.margins == Print::Margins::CUSTOM) { + pageMargin = context.settings.margins.custom.template cast(); + } else if (context.settings.margins == Print::Margins::MINIMUM) { pageMargin = {}; } RectAu pageContent = pageRect.shrink(pageMargin); + pageInfos.pushBack({pageRect, pageContent, pageStyle}); + Layout::Viewport vp{ .small = pageContent.size(), }; - contentTree.viewport = vp; - contentTree.fc = {pageContent.size()}; - - if (settings.headerFooter and settings.margins != Print::Margins::NONE) - _paintMargins(*pageStyle, pageRect, pageContent, *pageStack); + context.contentTree.viewport = vp; + context.contentTree.fc = {pageContent.size()}; Layout::Input pageLayoutInput{ .knownSize = {pageContent.width, NONE}, .position = pageContent.topStart(), .availableSpace = pageContent.size(), .containingBlock = pageContent.size(), + .pageNumber = pageNumber, }; + pageLayoutInput.runningPosition = {&runningPosition}; + context.contentTree.fc.enterDiscovery(); - contentTree.fc.enterDiscovery(); auto outDiscovery = Layout::layout( - contentTree, + context.contentTree, pageLayoutInput.withBreakpointTraverser(Layout::BreakpointTraverser(&prevBreakpoint)) ); @@ -178,13 +172,81 @@ export Generator print(Gc::Ref dom, Print::Settings ? Layout::Breakpoint::classB(1, false) : outDiscovery.breakpoint.unwrap(); - contentTree.fc.leaveDiscovery(); + context.contentTree.fc.leaveDiscovery(); + + breakpoints.pushBack(currBreakpoint); + if (outDiscovery.completelyLaidOut) + break; + + prevBreakpoint = std::move(currBreakpoint); + } + + return {breakpoints, pageInfos}; +} + +export Generator print(Gc::Ref dom, Print::Settings const& settings) { + auto media = Style::Media::forPrint(settings); + + Font::Database db; + if (not db.loadSystemFonts()) + logWarn("not all fonts were properly loaded into database"); + + Style::Computer computer{ + media, *dom->styleSheets, db + }; + computer.build(); + computer.styleDocument(*dom); + + // MARK: Page and Margins -------------------------------------------------- + + Style::SpecifiedValues initialStyle = Style::SpecifiedValues::initial(); + initialStyle.color = Gfx::BLACK; + initialStyle.setCustomProp("-vaev-url", {Css::Token::string(Io::format("\"{}\"", dom->url()))}); + initialStyle.setCustomProp("-vaev-title", {Css::Token::string(Io::format("\"{}\"", dom->title()))}); + initialStyle.setCustomProp("-vaev-datetime", {Css::Token::string(Io::format("\"{}\"", Sys::now()))}); + + // MARK: Page Content ------------------------------------------------------ + + Layout::Tree contentTree = { + Layout::build(dom), + }; + auto canvasColor = fixupBackgrounds(computer, dom, contentTree); + + Layout::RunningPositionMap runningPosition = {}; // Mapping the different Running positions to their respective names and their page. + PaginationContext paginationContext{ + .contentTree = contentTree, + .media = media, + .settings = settings, + .computer = computer, + .initialStyle = initialStyle, + }; + auto [breakpoints, pageInfos] = collectBreakPointsAndRunningPositions(runningPosition, paginationContext); + + for (usize pageNumber = 0; pageNumber < breakpoints.len() - 1; pageNumber++) { + Style::Page page{ + .name = ""s, + .number = pageNumber, + .blank = false, + }; + auto pageStack = makeRc(); + + auto infos = pageInfos[pageNumber]; + Layout::Input pageLayoutInput{ + .knownSize = {infos.pageContent.width, NONE}, + .position = infos.pageContent.topStart(), + .availableSpace = infos.pageContent.size(), + .containingBlock = infos.pageContent.size(), + .pageNumber = pageNumber, + }; auto [outFragmentation, fragment] = Layout::layoutCreateFragment( contentTree, pageLayoutInput - .withBreakpointTraverser(Layout::BreakpointTraverser(&prevBreakpoint, &currBreakpoint)) + .withBreakpointTraverser(Layout::BreakpointTraverser(&breakpoints[pageNumber], &breakpoints[pageNumber + 1])) ); + if (settings.headerFooter and settings.margins != Print::Margins::NONE) + _paintMargins(*pageInfos[pageNumber].pageStyle, pageInfos[pageNumber].pageRect, pageInfos[pageNumber].pageContent, *pageStack, page.number, runningPosition); + Layout::paint(fragment, *pageStack); pageStack->prepare(); @@ -198,12 +260,7 @@ export Generator print(Gc::Ref dom, Print::Settings canvasColor ) ); - - if (outFragmentation.completelyLaidOut) - break; - - prevBreakpoint = std::move(currBreakpoint); } } -} // namespace Vaev::Driver +} // namespace Vaev::Driver \ No newline at end of file diff --git a/src/vaev-engine/layout/base/box.cpp b/src/vaev-engine/layout/base/box.cpp index 660278a0b..70bb0c87e 100644 --- a/src/vaev-engine/layout/base/box.cpp +++ b/src/vaev-engine/layout/base/box.cpp @@ -86,6 +86,7 @@ struct Group { struct SVGRoot : SVG::Group { Opt viewBox; + SVGRoot(Rc style) : SVG::Group(style), viewBox(style->svg->viewBox) {} diff --git a/src/vaev-engine/layout/base/input.cpp b/src/vaev-engine/layout/base/input.cpp index 3bf84a1cd..bf6c441df 100644 --- a/src/vaev-engine/layout/base/input.cpp +++ b/src/vaev-engine/layout/base/input.cpp @@ -2,6 +2,7 @@ export module Vaev.Engine:layout.input; import :layout.breaks; import :layout.fragment; +import :layout.runningPosition; namespace Vaev::Layout { @@ -25,6 +26,8 @@ export struct Input { Vec2Au position = {}; Vec2Au availableSpace = {}; Vec2Au containingBlock = {}; + MutCursor runningPosition = nullptr; + usize pageNumber = 0; BreakpointTraverser breakpointTraverser = {}; diff --git a/src/vaev-engine/layout/base/layout-impl.cpp b/src/vaev-engine/layout/base/layout-impl.cpp index 771f59f99..92bf9d838 100644 --- a/src/vaev-engine/layout/base/layout-impl.cpp +++ b/src/vaev-engine/layout/base/layout-impl.cpp @@ -293,7 +293,16 @@ Output layout(Tree& tree, Box& box, Input input) { ? input.breakpointTraverser.getEnd() : NONE; + if (box.style->position.is()) { + return Output{ + .size = {}, + .completelyLaidOut = true, + .firstBaselineSet = {}, + .lastBaselineSet = {}, + }; + } auto parentFrag = input.fragment; + Frag currFrag(&box); input.fragment = input.fragment ? &currFrag : nullptr; @@ -336,8 +345,10 @@ Output layout(Tree& tree, Box& box, Input input) { Output layout(Tree& tree, Input input) { auto out = layout(tree, tree.root, input); - if (input.fragment) - layoutPositioned(tree, input.fragment->children()[0], input.containingBlock); + if (input.fragment) { + layoutPositioned(tree, input.fragment->children()[0], input.containingBlock, input); + } + return out; } diff --git a/src/vaev-engine/layout/base/mod.cpp b/src/vaev-engine/layout/base/mod.cpp index 97fb1d9f9..7b1b8188a 100644 --- a/src/vaev-engine/layout/base/mod.cpp +++ b/src/vaev-engine/layout/base/mod.cpp @@ -8,3 +8,4 @@ export import :layout.input; export import :layout.layout; export import :layout.output; export import :layout.values; +export import :layout.runningPosition; diff --git a/src/vaev-engine/layout/base/running-position.cpp b/src/vaev-engine/layout/base/running-position.cpp new file mode 100644 index 000000000..051e80209 --- /dev/null +++ b/src/vaev-engine/layout/base/running-position.cpp @@ -0,0 +1,119 @@ +export module Vaev.Engine:layout.runningPosition; + +import Karm.Core; +import Karm.Math; + +import :values.insets; +import :values.content; +import :layout.box; + +using namespace Karm; + +namespace Vaev::Layout { + +// MARK: RunningPos ------------------------------------------------------------ + +export struct RunningPositionInfo { + usize page; + RunningPosition running; + Gc::Ref element; + + RunningPositionInfo(usize page, RunningPosition running, Gc::Ref element) + : page(page), running(running), element(element) { + } + + void repr(Io::Emit& e) const { + e("runningPosInfos \nrunning:{} \npage:{} \nelement:{}", running, page, element); + } +}; + +struct RunningPositionMap { + Map> content; + + void add(usize pageNumber, Layout::Box& box) { + auto& style = box.style; + + if (auto position = style->position.is()) { + auto const origin = box.origin; + if (box.origin == nullptr) + return; + RunningPositionInfo info = {pageNumber, *position, origin.upgrade()}; + content.getOrDefault(position->customIdent) + .pushBack(std::move(info)); + } + } + + // https://www.w3.org/TR/css-gcpm-3/#using-named-strings + Res match(ElementContent elt, usize currentPage = 0) { + auto id = elt.customIdent; + if (not content.has(id)) { + return Error::notFound("element not found"); + } + + auto const& list = content.get(id); + + switch (elt.target) { + case ElementContent::Target::UNDEFINED: + return Ok(list[0]); + + case ElementContent::Target::START: + for (usize i = 0; i < list.len(); i++) { + auto elt = list[i]; + if (elt.page == currentPage and i > 0) { + return Ok(list[i - 1]); + } + } + return Ok(list[0]); + + case ElementContent::Target::FIRST: + case ElementContent::Target::FIRST_EXCEPT: { + auto elements = _searchPage(list, currentPage); + return Ok(elements[0]); + } + + case ElementContent::Target::LAST: { + auto elements = _searchPage(list, currentPage); + return Ok(elements[elements.len() - 1]); + } + } + } + + Slice _searchPage(Slice list, usize page) { + page++; // pages are 1-indexed + // binary search of all running positions that match the page + + auto res = search(list, [&](RunningPositionInfo const& info) { + if (info.page == page) { + return std::strong_ordering::equal; + } + return info.page <=> page; + }); + + if (not res) { + return sub(list, 0, 1); + } + + // a random element of the page + auto index = res.take(); + + // search left side for first element of the page + usize l = index; + while (l > 0 and list[l - 1].page == page) { + l--; + } + + // search right side for last element of the page + usize r = index; + while (r < list.len() - 1 and list[r + 1].page == page) { + r++; + } + + return sub(list, l, r + 1); + } + + void repr(Io::Emit& e) const { + e("{}", content); + } +}; + +} // namespace Vaev::Layout diff --git a/src/vaev-engine/layout/block.cpp b/src/vaev-engine/layout/block.cpp index 7868b7539..9ed059011 100644 --- a/src/vaev-engine/layout/block.cpp +++ b/src/vaev-engine/layout/block.cpp @@ -10,6 +10,7 @@ import Karm.Core; import :values; import :layout.base; import :layout.layout; +import :layout.positioned; namespace Vaev::Layout { @@ -172,8 +173,10 @@ struct BlockFormatingContext : FormatingContext { bool blockWasCompletelyLaidOut = false; Au lastMarginBottom = 0_au; + for (usize i = startAt; i < endChildren; ++i) { auto& c = box.children()[i]; + lookForRunningPosition(input, c); try$( processBreakpointsBeforeChild( @@ -193,6 +196,8 @@ struct BlockFormatingContext : FormatingContext { .intrinsic = input.intrinsic, .availableSpace = {input.availableSpace.x, 0_au}, .containingBlock = {inlineSize, input.knownSize.y.unwrapOr(0_au)}, + .runningPosition = input.runningPosition, + .pageNumber = input.pageNumber, .breakpointTraverser = input.breakpointTraverser.traverseInsideUsingIthChild(i), .pendingVerticalSizes = input.pendingVerticalSizes, }; diff --git a/src/vaev-engine/layout/builder/mod.cpp b/src/vaev-engine/layout/builder/mod.cpp index 11513f330..897729291 100644 --- a/src/vaev-engine/layout/builder/mod.cpp +++ b/src/vaev-engine/layout/builder/mod.cpp @@ -1,6 +1,7 @@ module; #include +#include export module Vaev.Engine:layout.builder; @@ -28,7 +29,7 @@ bool isSegmentBreak(Rune rune) { return rune == '\n' or rune == '\r' or rune == '\f' or rune == '\v'; } -static Gfx::ProseStyle _proseStyleFomStyle(Style::SpecifiedValues& style, Rc fontFace) { +static Gfx::ProseStyle _proseStyleFromStyle(Style::SpecifiedValues& style, Rc fontFace) { // FIXME: We should pass this around from the top in order to properly resolve rems Resolver resolver{ .rootFont = Gfx::Font{fontFace, 16}, @@ -330,7 +331,7 @@ static void _buildInputProse(BuilderContext bc, Gc::Ref el) { .rootFont = Gfx::Font{font, 16}, .boxFont = Gfx::Font{font, 16}, }; - Gfx::ProseStyle proseStyle = _proseStyleFomStyle(*el->specifiedValues(), font); + Gfx::ProseStyle proseStyle = _proseStyleFromStyle(*el->specifiedValues(), font); auto value = ""s; if (el->hasAttribute(Html::VALUE_ATTR)) @@ -361,7 +362,7 @@ void buildSVGElement(Gc::Ref el, SVG::Group& group) { } else if (el->qualifiedName == Svg::FOREIGN_OBJECT_TAG) { Box box{el->specifiedValues(), el}; - InlineBox rootInlineBox{_proseStyleFomStyle( + InlineBox rootInlineBox{_proseStyleFromStyle( *el->specifiedValues(), el->specifiedValues()->fontFace )}; @@ -433,7 +434,7 @@ static void createAndBuildInlineFlowfromElement(BuilderContext bc, RcspecifiedValues()->fontFace); + auto proseStyle = _proseStyleFromStyle(*style, el->specifiedValues()->fontFace); bc.startInlineBox(proseStyle); _buildChildren(bc.toInlineContext(style), el); @@ -455,7 +456,7 @@ static void buildBlockFlowFromElement(BuilderContext bc, Gc::Ref e static Box createAndBuildBoxFromElement(BuilderContext bc, Rc style, Gc::Ref el, Display display) { Box box = {style, el}; - InlineBox rootInlineBox{_proseStyleFomStyle(*style, el->specifiedValues()->fontFace)}; + InlineBox rootInlineBox{_proseStyleFromStyle(*style, el->specifiedValues()->fontFace)}; auto newBc = display == Display::Inside::FLEX ? bc.toFlexContext(box, rootInlineBox) @@ -498,7 +499,7 @@ struct AnonymousTableBoxWrapper { cellStyle->display = Display::Internal::TABLE_CELL; cellBox = Box{cellStyle, nullptr}; - rootInlineBoxForCell = InlineBox{_proseStyleFomStyle(*style, style->fontFace)}; + rootInlineBoxForCell = InlineBox{_proseStyleFromStyle(*style, style->fontFace)}; } void finalizeAndResetCell() { @@ -685,7 +686,7 @@ static Box _createTableWrapperAndBuildTable(BuilderContext bc, Rcmargin = tableStyle->margin; Box wrapper = {wrapperStyle, tableBoxEl}; - InlineBox rootInlineBox{_proseStyleFomStyle(*wrapperStyle, tableBoxEl->specifiedValues()->fontFace)}; + InlineBox rootInlineBox{_proseStyleFromStyle(*wrapperStyle, tableBoxEl->specifiedValues()->fontFace)}; // SPEC: The table wrapper box establishes a block formatting context. _buildTableBox(bc.toBlockContextWithoutRootInline(wrapper), tableBoxEl, tableStyle); @@ -794,7 +795,7 @@ export Box build(Gc::Ref doc) { if (auto el = doc->documentElement()) { root = {el->specifiedValues(), el}; - InlineBox rootInlineBox{_proseStyleFomStyle(*el->specifiedValues(), el->specifiedValues()->fontFace)}; + InlineBox rootInlineBox{_proseStyleFromStyle(*el->specifiedValues(), el->specifiedValues()->fontFace)}; BuilderContext bc{ BuilderContext::From::BLOCK, @@ -811,13 +812,48 @@ export Box build(Gc::Ref doc) { return root; } -export Box buildForPseudoElement(Dom::PseudoElement& el) { - auto proseStyle = _proseStyleFomStyle(*el.specifiedValues(), el.specifiedValues()->fontFace); +export Box buildElement(Gc::Ref elt) { + Box box = {elt->specifiedValues(), elt}; + InlineBox rootInlineBox{_proseStyleFromStyle(*elt->specifiedValues(), elt->specifiedValues()->fontFace)}; - auto prose = makeRc(proseStyle); - if (el.specifiedValues()->content) { - prose->append(el.specifiedValues()->content.str()); + BuilderContext bc{ + BuilderContext::From::BLOCK, + elt->specifiedValues(), + box, + &rootInlineBox, + }; + + buildBlockFlowFromElement(bc, *elt); + + return box; +} + +export Box buildForPseudoElement(Dom::PseudoElement& el, usize currentPage, RunningPositionMap& runningPos) { + auto proseStyle = _proseStyleFromStyle(*el.specifiedValues(), el.specifiedValues()->fontFace); + + if (el.specifiedValues()->content.is()) { + auto prose = makeRc(proseStyle); + + prose->append(el.specifiedValues()->content.unwrap().str()); return {el.specifiedValues(), InlineBox{prose}, nullptr}; + } else if (el.specifiedValues()->content.is()) { + auto elt = el.specifiedValues()->content.unwrap(); + if (auto infos = runningPos.match(elt, currentPage)) { + Box marginBox = {el.specifiedValues(), nullptr}; + + Box box = buildElement(infos.unwrap().element); + box.style->position = Keywords::STATIC; + marginBox.add(std::move(box)); + return marginBox; + } + } else if (el.specifiedValues()->content.is()) { + auto elt = el.specifiedValues()->content.unwrap(); + if (elt.type == Counter::Type::PAGE) { + auto prose = makeRc(proseStyle); + + prose->append(Io::toStr(currentPage + 1).str()); + return {el.specifiedValues(), InlineBox{prose}, nullptr}; + } } return {el.specifiedValues(), nullptr}; diff --git a/src/vaev-engine/layout/flex.cpp b/src/vaev-engine/layout/flex.cpp index dda0afa85..1fe3961b2 100644 --- a/src/vaev-engine/layout/flex.cpp +++ b/src/vaev-engine/layout/flex.cpp @@ -10,6 +10,7 @@ import Karm.Logger; import :values; import :layout.layout; import :layout.values; +import :layout.positioned; namespace Vaev::Layout { @@ -633,10 +634,15 @@ struct FlexFormatingContext : FormatingContext { // XX. MARK: Generate flex items ---------------------------------- Vec _items = {}; - void _generateFlexItems(Tree& tree, Box& box, Vec2Au containingBlock) { + void _generateFlexItems(Input input, Tree& tree, Box& box, Vec2Au containingBlock) { // NOTE: we assume all children are non-absolute positioned for fast mem allocation _items.ensure(box.children().len()); for (auto& c : box.children()) { + // FIXME: + // Look for running position should be called at flexline generation. + // Here it could register multiple time the same box. + lookForRunningPosition(input, c); + if (impliesRemovingFromFlow(c.style->position)) continue; _items.emplaceBack(tree, c, _flex.isRowOriented(), containingBlock); @@ -1447,6 +1453,8 @@ struct FlexFormatingContext : FormatingContext { .knownSize = {flexItem.usedSize.x, flexItem.usedSize.y}, .position = flexItem.position, .availableSpace = availableSpace, + .runningPosition = input.runningPosition, + .pageNumber = input.pageNumber, } ); flexItem.commit(input.fragment); @@ -1466,7 +1474,7 @@ struct FlexFormatingContext : FormatingContext { // NOTE: Done during box building phase // XX. Populate flex items list - _generateFlexItems(tree, box, input.containingBlock); + _generateFlexItems(input, tree, box, input.containingBlock); // XX. Handle absolute positioned children _layoutAbsolutePositionedChildren(); diff --git a/src/vaev-engine/layout/inline.cpp b/src/vaev-engine/layout/inline.cpp index ff58b7735..f36f01571 100644 --- a/src/vaev-engine/layout/inline.cpp +++ b/src/vaev-engine/layout/inline.cpp @@ -7,6 +7,7 @@ import Karm.Core; import :values; import :layout.base; import :layout.layout; +import :layout.positioned; namespace Vaev::Layout { @@ -52,6 +53,7 @@ struct InlineFormatingContext : FormatingContext { auto& prose = inlineBox.prose; for (auto strutCell : prose->cellsWithStruts()) { + auto& boxStrutCell = *strutCell->strut(); auto& atomicBox = *inlineBox.atomicBoxes[boxStrutCell.id]; @@ -93,6 +95,11 @@ struct InlineFormatingContext : FormatingContext { auto& atomicBox = *inlineBox.atomicBoxes[boxStrutCell.id]; Math::Vec2> knownSize; + // FIXME: + // Look for running position should be called at linebox generation. + // Here it could register multiple time the same box. + lookForRunningPosition(input, atomicBox); + if (not impliesRemovingFromFlow(atomicBox.style->position)) { knownSize = { boxStrutCell.size.x, @@ -111,6 +118,8 @@ struct InlineFormatingContext : FormatingContext { input.knownSize.x.unwrapOr(0_au), input.knownSize.y.unwrapOr(0_au) }, + .runningPosition = input.runningPosition, + .pageNumber = input.pageNumber, } ); } diff --git a/src/vaev-engine/layout/paint.cpp b/src/vaev-engine/layout/paint.cpp index 9aabcf552..781a81c9d 100644 --- a/src/vaev-engine/layout/paint.cpp +++ b/src/vaev-engine/layout/paint.cpp @@ -183,6 +183,7 @@ Rc _paintSVGRoot(SVGRootFrag& svgRoot, Gfx::Color currentColor) { static void _paintFrag(Frag& frag, Scene::Stack& stack) { auto& s = frag.style(); + if (s.visibility == Visibility::HIDDEN) return; @@ -231,7 +232,7 @@ static void _paintChildren(Frag& frag, Scene::Stack& stack, auto predicate) { // NOTE: Positioned elements act as if they establish a stacking context auto position = s.position; - if (position != Position::STATIC) { + if (position != Keywords::STATIC) { if (predicate(s)) _paintStackingContext(c, stack); continue; @@ -634,22 +635,22 @@ static void _paintStackingContext(Frag& frag, Scene::Stack& stack) { // 3. the in-flow, non-inline-level, non-positioned descendants. _paintChildren(frag, stack, [](Style::SpecifiedValues const& s) { - return s.zIndex == Keywords::AUTO and s.display != Display::INLINE and s.position == Position::STATIC; + return s.zIndex == Keywords::AUTO and s.display != Display::INLINE and s.position == Keywords::STATIC; }); // 4. the non-positioned floats. _paintChildren(frag, stack, [](Style::SpecifiedValues const& s) { - return s.zIndex == Keywords::AUTO and s.position == Position::STATIC and s.float_ != Float::NONE; + return s.zIndex == Keywords::AUTO and s.position == Keywords::STATIC and s.float_ != Float::NONE; }); // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. _paintChildren(frag, stack, [](Style::SpecifiedValues const& s) { - return s.zIndex == Keywords::AUTO and s.display == Display::INLINE and s.position == Position::STATIC; + return s.zIndex == Keywords::AUTO and s.display == Display::INLINE and s.position == Keywords::STATIC; }); // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0. _paintChildren(frag, stack, [](Style::SpecifiedValues const& s) { - return s.zIndex.unwrapOr(0) == 0 and s.position != Position::STATIC; + return s.zIndex.unwrapOr(0) == 0 and s.position != Keywords::STATIC; }); // 7. the child stacking contexts with positive stack levels (least positive first). diff --git a/src/vaev-engine/layout/positioned.cpp b/src/vaev-engine/layout/positioned.cpp index 668703119..5fc099fca 100644 --- a/src/vaev-engine/layout/positioned.cpp +++ b/src/vaev-engine/layout/positioned.cpp @@ -9,22 +9,24 @@ import :layout.values; namespace Vaev::Layout { Vec2Au _resolveOrigin(Vec2Au fragPosition, Vec2Au containingBlockOrigin, Position position) { - if (position == Position::RELATIVE) { + if (position == Keywords::RELATIVE) { return fragPosition; - } else if (position == Position::ABSOLUTE) { + } else if (position == Keywords::ABSOLUTE) { return containingBlockOrigin; } else { return {0_au, 0_au}; } } -export void layoutPositioned(Tree& tree, Frag& frag, RectAu containingBlock) { +export void layoutPositioned(Tree& tree, Frag& frag, RectAu containingBlock, Input input) { + auto& style = frag.style(); + auto& metrics = frag.metrics; - if (impliesRemovingFromFlow(style.position) or style.position == Position::RELATIVE) { + if (impliesRemovingFromFlow(style.position) or style.position == Keywords::RELATIVE) { auto origin = _resolveOrigin(metrics.position, containingBlock.topStart(), style.position); - auto relativeTo = style.position == Position::FIXED + auto relativeTo = style.position == Keywords::FIXED ? tree.viewport.small : containingBlock; @@ -53,7 +55,17 @@ export void layoutPositioned(Tree& tree, Frag& frag, RectAu containingBlock) { } for (auto& c : frag.children()) { - layoutPositioned(tree, c, containingBlock); + layoutPositioned(tree, c, containingBlock, input); + } +} + +export void lookForRunningPosition(Input& input, Box& box) { + if (not input.runningPosition) + return; + + if (box.style->position.is()) { + auto& runningMap = input.runningPosition.peek(); + runningMap.add(input.pageNumber, box); } } diff --git a/src/vaev-engine/style/props-impl.cpp b/src/vaev-engine/style/props-impl.cpp index 79856d824..26b006968 100644 --- a/src/vaev-engine/style/props-impl.cpp +++ b/src/vaev-engine/style/props-impl.cpp @@ -16,8 +16,9 @@ static bool DEBUG_PROPS = false; bool DeferredProp::_expandVariable(Cursor& c, Map const& env, Css::Content& out) { if (not(c->type == Css::Sst::FUNC and - c->prefix == Css::Token::function("var("))) + c->prefix == Css::Token::function("var("))) { return false; + } Cursor content = c->content; diff --git a/src/vaev-engine/style/props.cpp b/src/vaev-engine/style/props.cpp index 194ff743e..f207511a6 100644 --- a/src/vaev-engine/style/props.cpp +++ b/src/vaev-engine/style/props.cpp @@ -1217,24 +1217,25 @@ export struct BorderWidthProp { // MARK: Content --------------------------------------------------------------- +// https://www.w3.org/TR/css-gcpm-3/ // https://drafts.csswg.org/css-content/#content-property export struct ContentProp { - String value = initial(); + Content value = initial(); static constexpr Str name() { return "content"; } - static String initial() { return ""s; } + static constexpr Content initial() { return Keywords::NORMAL; } void apply(SpecifiedValues& c) const { c.content = value; } - static String load(SpecifiedValues const& c) { + static Content load(SpecifiedValues const& c) { return c.content; } Res<> parse(Cursor& c) { - value = try$(parseValue(c)); + value = try$(parseValue(c)); return Ok(); } }; @@ -2628,7 +2629,7 @@ export struct PositionProp { static Str name() { return "position"; } - static Position initial() { return Position::STATIC; } + static Position initial() { return Keywords::STATIC; } void apply(SpecifiedValues& c) const { c.position = value; diff --git a/src/vaev-engine/style/specified.cpp b/src/vaev-engine/style/specified.cpp index a2d5df92f..14bd1535f 100644 --- a/src/vaev-engine/style/specified.cpp +++ b/src/vaev-engine/style/specified.cpp @@ -57,7 +57,7 @@ export struct SpecifiedValues { ZIndex zIndex = Keywords::AUTO; Overflows overflows; Gfx::Color color; - String content = ""s; + Content content = Keywords::NORMAL; Integer order; AlignProps aligns; Display display; @@ -69,7 +69,7 @@ export struct SpecifiedValues { Visibility visibility; WritingMode writingMode; Direction direction; - Position position; + Position position = Keywords::STATIC; BoxSizing boxSizing; void inherit(SpecifiedValues const& parent) { @@ -136,5 +136,4 @@ export struct SpecifiedValues { e(")"); } }; - } // namespace Vaev::Style diff --git a/src/vaev-engine/values/content.cpp b/src/vaev-engine/values/content.cpp new file mode 100644 index 000000000..31358ab65 --- /dev/null +++ b/src/vaev-engine/values/content.cpp @@ -0,0 +1,135 @@ +module; + +#include + +export module Vaev.Engine:values.content; + +import Karm.Core; +import :css; +import :values.keywords; +import :values.primitives; + +namespace Vaev { + +// https://www.w3.org/TR/css-content-3/ +// https://www.w3.org/TR/css-gcpm-3/#funcdef-element +export struct ElementContent { + enum struct Target { + UNDEFINED, + FIRST, + START, + LAST, + FIRST_EXCEPT + }; + + CustomIdent customIdent = {""_sym}; + Target target = Target::UNDEFINED; + + explicit ElementContent(CustomIdent customIdent, Target target = Target::UNDEFINED) + : customIdent(customIdent), target(target) {} + + void repr(Io::Emit& e) const { + e("element '{}'", customIdent); + } +}; + +// https://drafts.csswg.org/css-lists/#auto-numbering +export struct Counter { + enum struct Type { + PAGE, + }; + Type type = Type::PAGE; + + void repr(Io::Emit& e) const { + e("counter (type:'{}')", type); + } +}; + +export using Content = Union< + Keywords::Normal, + Keywords::None, + ElementContent, + String, + Counter>; + +export template <> +struct ValueParser { + // https://www.w3.org/TR/css-content-3/ + // https://www.w3.org/TR/css-gcpm-3/#funcdef-element + static Res parse(Cursor& c) { + if (c.ended()) + return Error::invalidData("unexpected end of input"); + + if (c.skip(Css::Token::ident("normal"))) + return Ok(Keywords::NORMAL); + else if (c.skip(Css::Token::ident("none"))) + return Ok(Keywords::NONE); + else if (c->type == Css::Sst::FUNC and c->prefix == Css::Token::function("element(")) { + Cursor cur = c->content; + auto element = try$(parseValue(cur)); + c.next(); + return Ok(element); + } else if (c->type == Css::Sst::FUNC and c->prefix == Css::Token::function("counter(")) { + Cursor cur = c->content; + auto element = try$(parseValue(cur)); + c.next(); + return Ok(element); + } else if (c->type == Css::Sst::TOKEN and c->token.type == Css::Token::STRING) { + return Ok(try$(parseValue(c))); + } else + return Error::invalidData("expected content"); + } +}; + +export template <> +struct ValueParser { + // https://www.w3.org/TR/css-gcpm-3/#funcdef-element + static Res parse(Cursor& c) { + if (c.ended()) + return Error::invalidData("unexpected end of input"); + + auto ident = ValueParser::parse(c); + + if (not ident) { + return Error::invalidData("ill formed custom ident in content"); + } + + if (c.ended()) + return Ok(ElementContent{ident.take()}); + + skipOmmitableComma(c); + + ElementContent::Target target = ElementContent::Target::UNDEFINED; + if (c.skip(Css::Token::ident("first"))) { + target = ElementContent::Target::FIRST; + } else if (c.skip(Css::Token::ident("last"))) { + target = ElementContent::Target::LAST; + } else if (c.skip(Css::Token::ident("start"))) { + target = ElementContent::Target::START; + } else if (c.skip(Css::Token::ident("first-except"))) { + target = ElementContent::Target::FIRST_EXCEPT; + } + + eatWhitespace(c); + return (Ok(ElementContent{ident.take(), target})); + } +}; + +export template <> +struct ValueParser { + // https://www.w3.org/TR/css-gcpm-3/#funcdef-element + static Res parse(Cursor& c) { + if (c.ended()) + return Error::invalidData("unexpected end of input"); + + Counter::Type type = Counter::Type::PAGE; + if (c.skip(Css::Token::ident("page"))) { + type = Counter::Type::PAGE; + } else { + return Error::invalidData("unsupported counter"); + } + return (Ok(Counter{type})); + } +}; + +} // namespace Vaev diff --git a/src/vaev-engine/values/defs/keywords.inc b/src/vaev-engine/values/defs/keywords.inc index f563fdab0..bce20a3f8 100644 --- a/src/vaev-engine/values/defs/keywords.inc +++ b/src/vaev-engine/values/defs/keywords.inc @@ -35,3 +35,8 @@ KEYWORD(Bottom, "bottom", BOTTOM) KEYWORD(Center, "center", CENTER) KEYWORD(ClosestSide, "closest-side", CLOSEST_SIDE) KEYWORD(FarthestSide, "farthest-side", FARTHEST_SIDE) +KEYWORD(Fixed, "fixed", FIXED) +KEYWORD(Sticky, "sticky", STICKY) +KEYWORD(Absolute, "absolute", ABSOLUTE) +KEYWORD(Relative, "relative", RELATIVE) +KEYWORD(Static, "static", STATIC) diff --git a/src/vaev-engine/values/insets.cpp b/src/vaev-engine/values/insets.cpp index 14b0f4f77..0463773fb 100644 --- a/src/vaev-engine/values/insets.cpp +++ b/src/vaev-engine/values/insets.cpp @@ -12,39 +12,49 @@ using namespace Karm; namespace Vaev { // MARK: Position -------------------------------------------------------------- -// https://www.w3.org/TR/CSS22/visuren.html#propdef-position -export enum struct Position : u8 { - STATIC, - RELATIVE, - ABSOLUTE, - FIXED, - STICKY, +export struct RunningPosition { + CustomIdent customIdent; - _LEN, + void repr(Io::Emit& e) const { + e("running '{}'", customIdent); + } }; +// https://www.w3.org/TR/CSS22/visuren.html#propdef-position +export using Position = Union; + export bool impliesRemovingFromFlow(Position position) { - return position == Position::ABSOLUTE || position == Position::FIXED; + return position == Keywords::ABSOLUTE || position == Keywords::FIXED || position.is(); } export template <> struct ValueParser { + // https://drafts.csswg.org/css-position-3/#propdef-position static Res parse(Cursor& c) { if (c.ended()) return Error::invalidData("unexpected end of input"); if (c.skip(Css::Token::ident("static"))) - return Ok(Position::STATIC); + return Ok(Keywords::STATIC); else if (c.skip(Css::Token::ident("relative"))) - return Ok(Position::RELATIVE); + return Ok(Keywords::RELATIVE); else if (c.skip(Css::Token::ident("absolute"))) - return Ok(Position::ABSOLUTE); + return Ok(Keywords::ABSOLUTE); else if (c.skip(Css::Token::ident("fixed"))) - return Ok(Position::FIXED); + return Ok(Keywords::FIXED); else if (c.skip(Css::Token::ident("sticky"))) - return Ok(Position::STICKY); - else + return Ok(Keywords::STICKY); + else if (c->type == Css::Sst::FUNC and c->prefix == Css::Token::function("running(")) { + Cursor cur = c->content; + auto res = parseValue(cur); + if (not res) { + return Error::invalidData("ill formed custom-ident in running position"); + } + c.next(); + return Ok(RunningPosition{res.take()}); + + } else return Error::invalidData("expected position"); } }; diff --git a/src/vaev-engine/values/mod.cpp b/src/vaev-engine/values/mod.cpp index 31499af4d..999517732 100644 --- a/src/vaev-engine/values/mod.cpp +++ b/src/vaev-engine/values/mod.cpp @@ -10,6 +10,7 @@ export import :values.borders; export import :values.breaks; export import :values.calc; export import :values.color; +export import :values.content; export import :values.display; export import :values.flex; export import :values.float_; diff --git a/src/vaev-engine/values/primitives.cpp b/src/vaev-engine/values/primitives.cpp index 2cf126f06..926d9668d 100644 --- a/src/vaev-engine/values/primitives.cpp +++ b/src/vaev-engine/values/primitives.cpp @@ -99,6 +99,14 @@ struct ValueParser { struct CustomIdent { Symbol _symbol; + + bool operator==(CustomIdent const& other) const = default; + + auto operator<=>(CustomIdent const& other) const = default; + + void repr(Io::Emit& e) const { + e("custom-ident '{}'", _symbol); + } }; export template <> diff --git a/tests/css/print/margin-content.xhtml b/tests/css/print/margin-content.xhtml new file mode 100644 index 000000000..7dbef5055 --- /dev/null +++ b/tests/css/print/margin-content.xhtml @@ -0,0 +1,288 @@ + + + + + + + + + + + +
+
1
+
+
+
2
+
+ + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + + + + + + + + + +
\ No newline at end of file