From e969f7b1f312362542781ac9476c73999dc3a8d7 Mon Sep 17 00:00:00 2001 From: VAN BOSSUYT Nicolas Date: Fri, 13 Dec 2024 01:21:55 +0100 Subject: [PATCH] meta: Update from upstream. --- src/libs/karm-base/_prelude.h | 1 + src/libs/karm-base/array.h | 7 + src/libs/karm-cli/args.h | 10 + src/libs/karm-io/fmt.h | 6 +- src/libs/karm-kira/print-dialog.cpp | 132 ++++-- src/libs/karm-math/insets.h | 10 + src/libs/karm-print/bmp.h | 66 --- src/libs/karm-print/cli/main.cpp | 8 +- src/libs/karm-print/file-printer.h | 6 +- src/libs/karm-print/image-printer.h | 86 ++++ src/libs/karm-print/paper.h | 47 ++ src/libs/karm-print/{pdf.h => pdf-printer.h} | 39 +- src/libs/karm-print/{base.h => printer.h} | 2 +- src/libs/karm-scene/base.h | 10 +- src/libs/karm-scene/box.h | 14 +- src/libs/karm-scene/image.h | 2 +- src/libs/karm-scene/page.h | 13 +- src/libs/karm-scene/shape.h | 2 +- src/libs/karm-scene/stack.h | 8 +- src/libs/karm-scene/text.h | 2 +- src/libs/karm-ui/layout.cpp | 6 + src/libs/karm-ui/view.cpp | 12 +- src/libs/karm-ui/view.h | 2 +- src/web/doc/vendor.md | 11 + src/web/vaev-base/media.h | 7 - src/web/vaev-driver/fetcher.cpp | 50 +- src/web/vaev-driver/fetcher.h | 2 + src/web/vaev-driver/print.cpp | 381 +++++++++++++++ src/web/vaev-driver/print.h | 11 + src/web/vaev-driver/render.cpp | 325 +------------ src/web/vaev-driver/render.h | 7 +- src/web/vaev-driver/res/print.css | 4 +- src/web/vaev-layout/base.h | 112 ----- src/web/vaev-layout/block.cpp | 174 ++++++- src/web/vaev-layout/block.h | 6 +- src/web/vaev-layout/box.cpp | 5 +- src/web/vaev-layout/box.h | 8 +- src/web/vaev-layout/builder.cpp | 1 - src/web/vaev-layout/flex.cpp | 40 +- src/web/vaev-layout/flex.h | 2 +- src/web/vaev-layout/frag.h | 63 +++ src/web/vaev-layout/fragmentainer.h | 59 +++ src/web/vaev-layout/grid.cpp | 2 +- src/web/vaev-layout/grid.h | 2 +- src/web/vaev-layout/inline.h | 2 +- src/web/vaev-layout/input_output.h | 281 +++++++++++ src/web/vaev-layout/layout.cpp | 164 ++++++- src/web/vaev-layout/layout.h | 3 + src/web/vaev-layout/paint.cpp | 77 +-- src/web/vaev-layout/paint.h | 6 +- src/web/vaev-layout/positioned.cpp | 44 +- src/web/vaev-layout/positioned.h | 4 +- src/web/vaev-layout/table.cpp | 437 +++++++++++++++--- src/web/vaev-layout/table.h | 6 +- src/web/vaev-layout/tree.h | 12 + src/web/vaev-layout/values.cpp | 8 +- src/web/vaev-markup/dom.cpp | 21 + src/web/vaev-markup/dom.h | 38 +- src/web/vaev-markup/xml.cpp | 12 +- src/web/vaev-markup/xml.h | 2 +- src/web/vaev-style/computed.h | 11 + src/web/vaev-style/computer.cpp | 4 +- src/web/vaev-style/computer.h | 2 +- src/web/vaev-style/media.h | 5 +- src/web/vaev-style/page.h | 18 +- src/web/vaev-style/rules.cpp | 8 +- src/web/vaev-style/select.cpp | 83 ++-- src/web/vaev-style/select.h | 6 +- src/web/vaev-style/styles.cpp | 2 +- src/web/vaev-style/styles.h | 63 ++- .../vaev-style/tests/test-media-features.cpp | 4 +- src/web/vaev-style/tests/test-media-query.cpp | 2 +- .../vaev-style/tests/test-parse-selectors.cpp | 102 ++-- src/web/vaev-style/values.cpp | 63 ++- src/web/vaev-style/values.h | 16 +- src/web/vaev-view/dialog.cpp | 38 +- src/web/vaev-view/view.cpp | 12 +- 77 files changed, 2346 insertions(+), 973 deletions(-) delete mode 100644 src/libs/karm-print/bmp.h create mode 100644 src/libs/karm-print/image-printer.h rename src/libs/karm-print/{pdf.h => pdf-printer.h} (73%) rename src/libs/karm-print/{base.h => printer.h} (77%) create mode 100644 src/web/doc/vendor.md create mode 100644 src/web/vaev-driver/print.cpp create mode 100644 src/web/vaev-driver/print.h create mode 100644 src/web/vaev-layout/frag.h create mode 100644 src/web/vaev-layout/fragmentainer.h create mode 100644 src/web/vaev-layout/input_output.h create mode 100644 src/web/vaev-layout/tree.h create mode 100644 src/web/vaev-markup/dom.cpp diff --git a/src/libs/karm-base/_prelude.h b/src/libs/karm-base/_prelude.h index 982e87ecb30..406cc730914 100644 --- a/src/libs/karm-base/_prelude.h +++ b/src/libs/karm-base/_prelude.h @@ -19,6 +19,7 @@ // Darwin is poluting the global namespace with unwanted macros. #undef UNDERFLOW #undef DOMAIN +#undef OVERFLOW #endif #include diff --git a/src/libs/karm-base/array.h b/src/libs/karm-base/array.h index 31c9459c653..2fd9d86102e 100644 --- a/src/libs/karm-base/array.h +++ b/src/libs/karm-base/array.h @@ -13,12 +13,19 @@ struct Array { T _buf[N]; always_inline static constexpr Array from(Sliceable auto slice) { + Array res{}; for (usize i = 0; i < N; i++) res[i] = slice[i]; return res; } + always_inline static constexpr Array fill(auto f) { + return [&](std::index_sequence) { + return Array{f(Is)...}; + }(std::make_index_sequence()); + } + always_inline constexpr T &operator[](usize i) { if (i >= N) [[unlikely]] panic("index out of range"); diff --git a/src/libs/karm-cli/args.h b/src/libs/karm-cli/args.h index f3f08577d8c..eb1015b1b88 100644 --- a/src/libs/karm-cli/args.h +++ b/src/libs/karm-cli/args.h @@ -440,6 +440,8 @@ struct Command { if (_usage) { Io::Emit e{Sys::out()}; _showUsage(e); + e.newline(); + co_try$(e.flush()); co_return Ok(); } @@ -447,6 +449,14 @@ struct Command { if (callbackAsync) co_trya$(callbackAsync.unwrap()(ctx)); + if (any(_commands) and c.ended()) { + Io::Emit e{Sys::out()}; + _showUsage(e); + e.newline(); + co_try$(e.flush()); + co_return Error::invalidInput("expected subcommand"); + } + if (not c.ended()) { if (c->kind != Token::OPERAND) co_return Error::invalidInput("expected subcommand"); diff --git a/src/libs/karm-io/fmt.h b/src/libs/karm-io/fmt.h index 40a65df4853..ed64ceb9eea 100644 --- a/src/libs/karm-io/fmt.h +++ b/src/libs/karm-io/fmt.h @@ -277,8 +277,10 @@ struct Formatter> { } Res format(Io::TextWriter &writer, Cased val) { - auto result = try$(changeCase(val._inner, val._case)); - return writer.writeStr(result); + Io::StringWriter sw; + try$(_innerFmt.format(sw, val._inner)); + String result = try$(changeCase(sw.str(), val._case)); + return writer.writeStr(result.str()); } }; diff --git a/src/libs/karm-kira/print-dialog.cpp b/src/libs/karm-kira/print-dialog.cpp index c308613dc43..b926b088a4b 100644 --- a/src/libs/karm-kira/print-dialog.cpp +++ b/src/libs/karm-kira/print-dialog.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -20,9 +21,55 @@ struct State { Vec> pages = preview(settings); }; -using Action = Union; +struct ChangePaper { + Print::PaperStock paper; +}; + +struct ChangeOrientation { + Print::Orientation orientation; +}; + +struct ChangeMargin { + Print::Margins margins; +}; + +struct ToggleHeaderFooter {}; + +struct ToggleBackgroundGraphics {}; + +using Action = Union< + ChangePaper, + ChangeOrientation, + ChangeMargin, + ToggleHeaderFooter, + ToggleBackgroundGraphics>; + +static void reduce(State &s, Action a) { + bool shouldUpdatePreview = false; + + if (auto changePaper = a.is()) { + s.settings.paper = changePaper->paper; + shouldUpdatePreview = true; + } else if (auto changeOrientation = a.is()) { + s.settings.orientation = changeOrientation->orientation; + shouldUpdatePreview = true; + } else if (auto changeMargin = a.is()) { + s.settings.margins = changeMargin->margins; + shouldUpdatePreview = true; + } else if (a.is()) { + s.settings.headerFooter = not s.settings.headerFooter; + shouldUpdatePreview = true; + } else if (a.is()) { + s.settings.backgroundGraphics = not s.settings.backgroundGraphics; + shouldUpdatePreview = true; + } -void reduce(State &, Action) { + if (shouldUpdatePreview) { + auto settings = s.settings; + if (settings.orientation == Print::Orientation::LANDSCAPE) + settings.paper = s.settings.paper.landscape(); + s.pages = s.preview(settings); + } } using Model = Ui::Model; @@ -48,21 +95,34 @@ Ui::Child _printSelect(State const &s, usize index) { Ui::Child _printPaper(State const &s, usize index) { auto scale = 1.; + + auto paper = s.settings.paper; + if (s.settings.orientation == Print::Orientation::LANDSCAPE) + paper = paper.landscape(); + auto isMobile = App::useFormFactor() == App::FormFactor::MOBILE; - if (isMobile) { + if (isMobile) scale = 0.5; - } + + Math::Vec2f previewSize{ + 320 * scale, + 320 * (1. / paper.aspect()) * scale, + }; return Ui::stack( - Ui::canvas(s.pages[index]) | - Ui::box({ + Ui::canvas( + s.pages[index], + { + .showBackgroundGraphics = s.settings.backgroundGraphics, + } + ) | Ui::box({ .borderWidth = 1, .borderFill = Ui::GRAY50.withOpacity(0.1), .backgroundFill = Gfx::WHITE, }), _printSelect(s, index) | Ui::align(Math::Align::BOTTOM_END) ) | - Ui::pinSize(Math::Vec2f{320 * scale, 452 * scale}.cast()); + Ui::pinSize(previewSize.cast()); } Ui::Child _printPreviewMobile(State const &s) { @@ -129,8 +189,8 @@ Ui::Child _destinationSelect() { ); } -Ui::Child _paperSelect() { - return select(selectValue("A4"s), [] -> Ui::Children { +Ui::Child _paperSelect(State const &s) { + return select(selectValue(s.settings.paper.name), [] -> Ui::Children { Vec groups; bool first = false; @@ -138,7 +198,7 @@ Ui::Child _paperSelect() { Vec items; items.pushBack(selectLabel(serie.name)); for (auto const &stock : serie.stocks) { - items.pushBack(selectItem(Ui::NOP, stock.name)); + items.pushBack(selectItem(Model::bind(stock), stock.name)); } if (not first) @@ -152,7 +212,7 @@ Ui::Child _paperSelect() { }); } -Ui::Child _printSettings() { +Ui::Child _printSettings(State const &s) { return Ui::vflow( rowContent( NONE, @@ -187,11 +247,15 @@ Ui::Child _printSettings() { "Pages"s ), selectRow( - selectValue("Portrait"s), + selectValue( + s.settings.orientation == Print::Orientation::PORTRAIT + ? "Portrait"s + : "Landscape"s + ), [] -> Ui::Children { return { - selectItem(Ui::NOP, "Portrait"s), - selectItem(Ui::NOP, "Landscape"s), + selectItem(Model::bind(Print::Orientation::PORTRAIT), "Portrait"s), + selectItem(Model::bind(Print::Orientation::LANDSCAPE), "Landscape"s), }; }, @@ -202,13 +266,13 @@ Ui::Child _printSettings() { NONE, "More settings"s, NONE, - Karm::Ui::Slots{[] -> Ui::Children { + Karm::Ui::Slots{[&] -> Ui::Children { return { rowContent( NONE, "Paper"s, NONE, - _paperSelect() + _paperSelect(s) ), selectRow( selectValue("1"s), @@ -225,28 +289,40 @@ Ui::Child _printSettings() { "Page per sheet"s ), selectRow( - selectValue("Default"s), + selectValue(Io::format("{}", Io::cased(s.settings.margins, Io::Case::CAPITAL)).unwrap()), [] -> Ui::Children { return { - selectItem(Ui::NOP, "None"s), - selectItem(Ui::NOP, "Small"s), - selectItem(Ui::NOP, "Default"s), - selectItem(Ui::NOP, "Large"s), + selectItem(Model::bind(Print::Margins::NONE), "None"s), + selectItem(Model::bind(Print::Margins::MINIMUM), "Minimum"s), + selectItem(Model::bind(Print::Margins::DEFAULT), "Default"s), + selectItem(Model::bind(Print::Margins::CUSTOM), "Custom"s), }; }, "Margins"s ), - checkboxRow(true, NONE, "Header and footers"s), - checkboxRow(false, NONE, "Background graphics"s), + checkboxRow( + s.settings.headerFooter, + [&](auto &n, ...) { + Model::bubble(n); + }, + "Header and footers"s + ), + checkboxRow( + s.settings.backgroundGraphics, + [&](auto &n, ...) { + Model::bubble(n); + }, + "Background graphics"s + ), }; }} ) ); } -Ui::Child _printControls() { - return _printSettings() | +Ui::Child _printControls(State const &s) { + return _printSettings(s) | Ui::vscroll() | Ui::grow() | Ui::minSize({320, Ui::UNCONSTRAINED}); @@ -257,7 +333,7 @@ Ui::Child _printDialog(State const &s) { dialogTitleBar("Print"s), Ui::hflow( _printPreview(s), - _printControls() | Ui::grow() + _printControls(s) | Ui::grow() ) | Ui::maxSize({Ui::UNCONSTRAINED, 500}) | Ui::grow(), Ui::separator(), @@ -276,7 +352,7 @@ Ui::Child _printDialogMobile(State const &s) { _printPreviewMobile(s), Ui::separator(), titleRow("Settings"s), - _printSettings() + _printSettings(s) ) | Ui::minSize(500) | Ui::vscroll() | Ui::grow(), @@ -293,7 +369,7 @@ Ui::Child printDialog(PrintPreview preview) { auto isMobile = App::useFormFactor() == App::FormFactor::MOBILE; if (isMobile) return _printDialogMobile(s); - return _printDialog(s); + return _printDialog(s) | Ui::popoverLayer(); }); } diff --git a/src/libs/karm-math/insets.h b/src/libs/karm-math/insets.h index 7f71373857a..d459952995b 100644 --- a/src/libs/karm-math/insets.h +++ b/src/libs/karm-math/insets.h @@ -58,6 +58,16 @@ struct Insets { return top + bottom; } + template + Insets cast() const { + return { + static_cast(start), + static_cast(top), + static_cast(end), + static_cast(bottom), + }; + } + void repr(Io::Emit &e) const { e("(insets {} {} {} {})", start, top, end, bottom); } diff --git a/src/libs/karm-print/bmp.h b/src/libs/karm-print/bmp.h deleted file mode 100644 index 36e22fd8afc..00000000000 --- a/src/libs/karm-print/bmp.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace Karm::Print { - -struct BMPPrinter : public FilePrinter { - isize const VERTICAL_GAP_PX_SIZE = 16; - PaperStock _paper; - Vec> _pages; - Opt _canvas; - - BMPPrinter(PaperStock stock) - : _paper(stock) {} - - Gfx::Canvas &beginPage() override { - _pages.emplaceBack(Gfx::Surface::alloc(_paper.size().cast(), Gfx::RGBA8888)); - - if (_canvas) - _canvas->end(); - _canvas = Gfx::CpuCanvas{}; - _canvas->begin(*last(_pages)); - _canvas->clear(Gfx::WHITE); - - return *_canvas; - } - - Strong mergedImages() { - isize finalHeight = - _paper.size().cast().y * _pages.len() + - VERTICAL_GAP_PX_SIZE * _pages.len(); - - auto finalImageSize = Math::Vec2i{ - _paper.size().cast().x, - finalHeight, - }; - - auto finalImage = Gfx::Surface::alloc(finalImageSize, Gfx::RGBA8888); - - auto finalCanvas = Gfx::CpuCanvas{}; - finalCanvas.begin(*finalImage); - finalCanvas.clear(Gfx::BLACK); - - isize offsetY{0}; - for (auto &page : _pages) { - finalCanvas.blit( - page->bound(), - page->bound().offset({0, offsetY}), - page->pixels() - ); - offsetY += page->height() + VERTICAL_GAP_PX_SIZE; - } - - finalCanvas.end(); - - return finalImage; - } - - void write(Io::Writer &w) override { - Karm::Image::save(mergedImages()->pixels(), w).take(); - } -}; - -} // namespace Karm::Print diff --git a/src/libs/karm-print/cli/main.cpp b/src/libs/karm-print/cli/main.cpp index e951b680b32..36536325a65 100644 --- a/src/libs/karm-print/cli/main.cpp +++ b/src/libs/karm-print/cli/main.cpp @@ -1,10 +1,10 @@ -#include +#include #include #include Async::Task<> entryPointAsync(Sys::Context &) { - Print::PdfPrinter printer{Print::A4}; - auto &ctx = printer.beginPage(); + Print::PdfPrinter printer{}; + auto &ctx = printer.beginPage(Print::A4); ctx.fillStyle(Gfx::RED); ctx.rect({0, 0, 100, 100}); ctx.fill(Gfx::FillRule::NONZERO); @@ -19,7 +19,7 @@ Async::Task<> entryPointAsync(Sys::Context &) { auto outFile = co_try$(Sys::File::create("file:test.pdf"_url)); Io::TextEncoder<> outEncoder{outFile}; - printer.write(outEncoder); + co_try$(printer.write(outEncoder)); co_try$(outFile.flush()); co_return Ok(); diff --git a/src/libs/karm-print/file-printer.h b/src/libs/karm-print/file-printer.h index c4dd0cd175d..b4ea3821b93 100644 --- a/src/libs/karm-print/file-printer.h +++ b/src/libs/karm-print/file-printer.h @@ -1,13 +1,11 @@ #pragma once -#include +#include namespace Karm::Print { struct FilePrinter : public Printer { - virtual ~FilePrinter() = default; - - virtual void write(Io::Writer &w) = 0; + virtual Res<> write(Io::Writer &w) = 0; }; } // namespace Karm::Print diff --git a/src/libs/karm-print/image-printer.h b/src/libs/karm-print/image-printer.h new file mode 100644 index 00000000000..cfbd56c3791 --- /dev/null +++ b/src/libs/karm-print/image-printer.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +namespace Karm::Print { + +struct ImagePrinter : public FilePrinter { + static constexpr isize GAPS = 16; + + Vec> _pages; + Opt _canvas; + Image::Saver _saver; + + ImagePrinter(Image::Saver saver = {}) + : _saver(saver) {} + + Gfx::Canvas &beginPage(PaperStock paper) override { + _pages.emplaceBack(Gfx::Surface::alloc(paper.size().cast(), Gfx::RGBA8888)); + + if (_canvas) + _canvas->end(); + _canvas = Gfx::CpuCanvas{}; + _canvas->begin(*last(_pages)); + _canvas->clear(Gfx::WHITE); + + return *_canvas; + } + + Strong _mergedImages() { + if (_pages.len() == 0) + return Gfx::Surface::alloc(GAPS, Gfx::RGBA8888); + + isize finalHeight = + iter(_pages) + .map([](auto &page) { + return page->height() + GAPS; + }) + .sum(); + finalHeight -= GAPS; + + isize finalWidth = + iter(_pages) + .map([](auto &page) { + return page->width(); + }) + .max() + .unwrapOr(0); + + auto finalImageSize = Math::Vec2i{ + finalWidth, + finalHeight, + }; + + auto finalImage = Gfx::Surface::alloc(finalImageSize, Gfx::RGBA8888); + + auto finalCanvas = Gfx::CpuCanvas{}; + finalCanvas.begin(*finalImage); + finalCanvas.clear(Gfx::BLACK); + + isize ypos{0}; + for (auto &page : _pages) { + finalCanvas.blit( + page->bound(), + page->bound().offset({0, ypos}), + page->pixels() + ); + ypos += page->height() + GAPS; + } + + finalCanvas.end(); + + return finalImage; + } + + Res<> write(Io::Writer &w) override { + return Image::save( + _mergedImages()->pixels(), + w, + _saver + ); + } +}; + +} // namespace Karm::Print diff --git a/src/libs/karm-print/paper.h b/src/libs/karm-print/paper.h index dac983f38b4..51e623d69b8 100644 --- a/src/libs/karm-print/paper.h +++ b/src/libs/karm-print/paper.h @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace Karm::Print { @@ -18,6 +19,14 @@ struct PaperStock { Math::Vec2f size() const { return {width, height}; } + + f64 aspect() const { + return width / height; + } + + PaperStock landscape() const { + return {name, height, width}; + } }; struct PaperSeries { @@ -115,9 +124,47 @@ static inline Res findPaperStock(Str name) { // MARK: Print Settings -------------------------------------------------------- +struct Margins { + enum struct _Named { + DEFAULT, + NONE, + MINIMUM, + CUSTOM, + + _LEN + }; + using enum _Named; + _Named named; + Math::Insetsf custom = 20 * UNIT; + + Margins(_Named named) : named(named) {} + + Margins(Math::Insetsf custom) : named(_Named::CUSTOM), custom(custom) {} + + bool operator==(_Named named) const { + return this->named == named; + } + + void repr(Io::Emit &e) const { + e("{}", named); + } +}; + +enum struct Orientation { + PORTRAIT, + LANDSCAPE, + + _LEN, +}; + struct Settings { PaperStock paper = Print::A4; + Margins margins = Margins::DEFAULT; + Orientation orientation = Orientation::PORTRAIT; + double scale = 1.; + bool headerFooter = true; + bool backgroundGraphics = false; }; } // namespace Karm::Print diff --git a/src/libs/karm-print/pdf.h b/src/libs/karm-print/pdf-printer.h similarity index 73% rename from src/libs/karm-print/pdf.h rename to src/libs/karm-print/pdf-printer.h index 0ae8fdcedc9..dc6389a8a24 100644 --- a/src/libs/karm-print/pdf.h +++ b/src/libs/karm-print/pdf-printer.h @@ -7,17 +7,18 @@ namespace Karm::Print { +struct PdfPage { + PaperStock paper; + Io::StringWriter data; +}; + struct PdfPrinter : public FilePrinter { - PaperStock _paper; - Vec _pages; + Vec _pages; Opt _canvas; - PdfPrinter(PaperStock stock) - : _paper(stock) {} - - Gfx::Canvas &beginPage() override { - _pages.emplaceBack(); - _canvas = Pdf::Canvas{last(_pages), _paper.size()}; + Gfx::Canvas &beginPage(PaperStock paper) override { + auto &page = _pages.emplaceBack(paper); + _canvas = Pdf::Canvas{page.data, paper.size()}; return *_canvas; } @@ -40,6 +41,13 @@ struct PdfPrinter : public FilePrinter { Pdf::Dict{ {"Type"s, Pdf::Name{"Page"s}}, {"Parent"s, pagesRef}, + {"MediaBox"s, + Pdf::Array{ + usize{0}, + usize{0}, + p.paper.width, + p.paper.height, + }}, { "Contents"s, contentsRef, @@ -51,9 +59,9 @@ struct PdfPrinter : public FilePrinter { contentsRef, Pdf::Stream{ .dict = Pdf::Dict{ - {"Length"s, p.bytes().len()}, + {"Length"s, p.data.bytes().len()}, }, - .data = p.bytes(), + .data = p.data.bytes(), } ); @@ -65,13 +73,6 @@ struct PdfPrinter : public FilePrinter { pagesRef, Pdf::Dict{ {"Type"s, Pdf::Name{"Pages"s}}, - {"MediaBox"s, - Pdf::Array{ - usize{0}, - usize{0}, - _paper.width, - _paper.height, - }}, {"Count"s, _pages.len()}, {"Kids"s, std::move(pagesKids)}, } @@ -99,10 +100,12 @@ struct PdfPrinter : public FilePrinter { pdf().write(e); } - void write(Io::Writer &w) override { + Res<> write(Io::Writer &w) override { Io::TextEncoder<> encoder{w}; Io::Emit e{encoder}; write(e); + try$(e.flush()); + return Ok(); } }; diff --git a/src/libs/karm-print/base.h b/src/libs/karm-print/printer.h similarity index 77% rename from src/libs/karm-print/base.h rename to src/libs/karm-print/printer.h index 273d405ac77..f333ea7ec7c 100644 --- a/src/libs/karm-print/base.h +++ b/src/libs/karm-print/printer.h @@ -10,7 +10,7 @@ namespace Karm::Print { struct Printer { virtual ~Printer() = default; - virtual Gfx::Canvas &beginPage() = 0; + virtual Gfx::Canvas &beginPage(PaperStock paper) = 0; }; } // namespace Karm::Print diff --git a/src/libs/karm-scene/base.h b/src/libs/karm-scene/base.h index 1754a0b35f1..29451dde51a 100644 --- a/src/libs/karm-scene/base.h +++ b/src/libs/karm-scene/base.h @@ -2,10 +2,14 @@ #include #include -#include +#include namespace Karm::Scene { +struct PaintOptions { + bool showBackgroundGraphics = true; +}; + struct Node { isize zIndex = 0; @@ -17,9 +21,9 @@ struct Node { /// The bounding rectangle of the node virtual Math::Rectf bound() { return {}; } - virtual void paint(Gfx::Canvas &, Math::Rectf = Math::Rectf::MAX) {} + virtual void paint(Gfx::Canvas &, Math::Rectf = Math::Rectf::MAX, PaintOptions = {}) {} - virtual void print(Print::Printer &) {} + virtual void print(Print::Printer &, PaintOptions = {.showBackgroundGraphics = false}) {} virtual void repr(Io::Emit &e) const { e("(node z:{})", zIndex); diff --git a/src/libs/karm-scene/box.h b/src/libs/karm-scene/box.h index 1b7aff326bd..9168249d5ff 100644 --- a/src/libs/karm-scene/box.h +++ b/src/libs/karm-scene/box.h @@ -19,15 +19,17 @@ struct Box : public Node { return _bound; } - void paint(Gfx::Canvas &ctx, Math::Rectf r) override { + void paint(Gfx::Canvas &ctx, Math::Rectf r, PaintOptions o) override { if (not r.colide(bound())) return; - for (auto &background : _backgrounds) { - ctx.beginPath(); - auto radii = _borders.radii.reduceOverlap(_bound.size()); - ctx.rect(_bound, radii); - ctx.fill(background); + if (o.showBackgroundGraphics) { + for (auto &background : _backgrounds) { + ctx.beginPath(); + auto radii = _borders.radii.reduceOverlap(_bound.size()); + ctx.rect(_bound, radii); + ctx.fill(background); + } } _borders.paint(ctx, _bound); diff --git a/src/libs/karm-scene/image.h b/src/libs/karm-scene/image.h index bb1aedc68cf..f56e4b026a3 100644 --- a/src/libs/karm-scene/image.h +++ b/src/libs/karm-scene/image.h @@ -18,7 +18,7 @@ struct Image : public Node { return _bound; } - void paint(Gfx::Canvas &ctx, Math::Rectf r) override { + void paint(Gfx::Canvas &ctx, Math::Rectf r, PaintOptions) override { if (not r.colide(bound())) return; diff --git a/src/libs/karm-scene/page.h b/src/libs/karm-scene/page.h index 79a01cba827..b94eb47bf0b 100644 --- a/src/libs/karm-scene/page.h +++ b/src/libs/karm-scene/page.h @@ -5,17 +5,18 @@ namespace Karm::Scene { struct Page : public Stack { - Math::Vec2i _size; + Print::PaperStock _paper; - Page(Math::Vec2i size, Opt transform = NONE) : Stack(transform), _size(size) {} + Page(Print::PaperStock paper, Opt transform = NONE) + : Stack(transform), _paper(paper) {} - void print(Print::Printer &doc) override { - Stack::print(doc); - paint(doc.beginPage(), _size.cast()); + void print(Print::Printer &doc, PaintOptions o) override { + Stack::print(doc, o); + paint(doc.beginPage(_paper), _paper.size().cast(), o); } Math::Rectf bound() override { - return _size.cast(); + return _paper.size().cast(); } void repr(Io::Emit &e) const override { diff --git a/src/libs/karm-scene/shape.h b/src/libs/karm-scene/shape.h index b3b7204b13e..6263177d60d 100644 --- a/src/libs/karm-scene/shape.h +++ b/src/libs/karm-scene/shape.h @@ -19,7 +19,7 @@ struct Shape : public Node { return _path.bound(); } - void paint(Gfx::Canvas &g, Math::Rectf r) override { + void paint(Gfx::Canvas &g, Math::Rectf r, PaintOptions) override { if (not bound().colide(r)) return; diff --git a/src/libs/karm-scene/stack.h b/src/libs/karm-scene/stack.h index 84b0acdd10a..829c55909ef 100644 --- a/src/libs/karm-scene/stack.h +++ b/src/libs/karm-scene/stack.h @@ -30,7 +30,7 @@ struct Stack : public Node { return rect; } - void paint(Gfx::Canvas &g, Math::Rectf r) override { + void paint(Gfx::Canvas &g, Math::Rectf r, PaintOptions o) override { if (not bound().colide(r)) return; @@ -40,15 +40,15 @@ struct Stack : public Node { } for (auto &child : _children) - child->paint(g, r); + child->paint(g, r, o); if (_transform) g.pop(); } - void print(Print::Printer &p) override { + void print(Print::Printer &p, PaintOptions o) override { for (auto &child : _children) - child->print(p); + child->print(p, o); } void repr(Io::Emit &e) const override { diff --git a/src/libs/karm-scene/text.h b/src/libs/karm-scene/text.h index 2401cc9731f..b8822a79ba4 100644 --- a/src/libs/karm-scene/text.h +++ b/src/libs/karm-scene/text.h @@ -17,7 +17,7 @@ struct Text : public Node { return {_origin, _prose->size()}; } - void paint(Gfx::Canvas &g, Math::Rectf r) override { + void paint(Gfx::Canvas &g, Math::Rectf r, PaintOptions) override { if (not bound().colide(r)) return; diff --git a/src/libs/karm-ui/layout.cpp b/src/libs/karm-ui/layout.cpp index daf2124363f..0e4122ca4fd 100644 --- a/src/libs/karm-ui/layout.cpp +++ b/src/libs/karm-ui/layout.cpp @@ -233,6 +233,12 @@ struct Sizing : public ProxyNode { return _rect; } + void reconcile(Sizing &o) override { + _min = o._min; + _max = o._max; + ProxyNode::reconcile(o); + } + void layout(Math::Recti bound) override { _rect = bound; child().layout(bound); diff --git a/src/libs/karm-ui/view.cpp b/src/libs/karm-ui/view.cpp index 0debbd41b93..c043f7ccdd8 100644 --- a/src/libs/karm-ui/view.cpp +++ b/src/libs/karm-ui/view.cpp @@ -367,12 +367,14 @@ Child canvas(OnPaint onPaint) { struct SceneCanvas : public View { Strong _scene; + Scene::PaintOptions _options; - SceneCanvas(Strong scene) - : _scene(std::move(scene)) {} + SceneCanvas(Strong scene, Scene::PaintOptions options) + : _scene(std::move(scene)), _options(options) {} void reconcile(SceneCanvas &o) override { _scene = o._scene; + _options = o._options; View::reconcile(o); } @@ -388,7 +390,7 @@ struct SceneCanvas : public View { auto rectInScene = trans.inverse().apply(rect.cast()).bound(); - _scene->paint(g, Math::Rectf::MAX); + _scene->paint(g, Math::Rectf::MAX, _options); g.pop(); } @@ -402,8 +404,8 @@ struct SceneCanvas : public View { } }; -Child canvas(Strong child) { - return makeStrong(std::move(child)); +Child canvas(Strong child, Scene::PaintOptions options) { + return makeStrong(std::move(child), options); } // MARK: Filter ---------------------------------------------------------------- diff --git a/src/libs/karm-ui/view.h b/src/libs/karm-ui/view.h index f4beed37111..b816af8d9e0 100644 --- a/src/libs/karm-ui/view.h +++ b/src/libs/karm-ui/view.h @@ -121,7 +121,7 @@ using OnPaint = Func; Child canvas(OnPaint onPaint); -Child canvas(Strong child); +Child canvas(Strong child, Scene::PaintOptions options = {}); // MARK: Blur ------------------------------------------------------------------ diff --git a/src/web/doc/vendor.md b/src/web/doc/vendor.md new file mode 100644 index 00000000000..0bb732abf18 --- /dev/null +++ b/src/web/doc/vendor.md @@ -0,0 +1,11 @@ +# List of Vendor Extensions + +**Disclaimer**: Theses extensions are not part of any standard and will change without notice. Don't rely on them. This document is for informational purposes only. + + - @-vaev-top : Select the box containing the top left, center, and right margins of the page. + - @-vaev-bottom : Select the box containing the bottom left, center, and right margins of the page. + - @-vaev-left : Select the box containing the left top, center, and bottom margins of the page. + - @-vaev-right : Select the box containing the right top, center, and bottom margins of the page. + - var(-vaev-datetime) : Date and time of the printout. + - var(-vaev-title): Title of the document. + - var(-vaev-url): URL of the document. diff --git a/src/web/vaev-base/media.h b/src/web/vaev-base/media.h index 7107e83bf88..38794baf61a 100644 --- a/src/web/vaev-base/media.h +++ b/src/web/vaev-base/media.h @@ -14,13 +14,6 @@ enum struct MediaType { _LEN, }; -enum struct Orientation { - PORTRAIT, - LANDSCAPE, - - _LEN, -}; - enum struct Scan { INTERLACE, PROGRESSIVE, diff --git a/src/web/vaev-driver/fetcher.cpp b/src/web/vaev-driver/fetcher.cpp index 0b5fe96a9b4..6033041f123 100644 --- a/src/web/vaev-driver/fetcher.cpp +++ b/src/web/vaev-driver/fetcher.cpp @@ -9,8 +9,8 @@ namespace Vaev::Driver { -Res> loadDocument(Mime::Url const &, Mime::Mime const &mime, Io::Reader &reader) { - auto dom = makeStrong(); +Res> loadDocument(Mime::Url const &url, Mime::Mime const &mime, Io::Reader &reader) { + auto dom = makeStrong(url); auto buf = try$(Io::readAllUtf8(reader)); if (mime.is("text/html"_mime)) { @@ -21,13 +21,13 @@ Res> loadDocument(Mime::Url const &, Mime::Mime const & } else if (mime.is("application/xhtml+xml"_mime)) { Io::SScan scan{buf}; Markup::XmlParser parser; - dom = try$(parser.parse(scan, HTML)); + try$(parser.parse(scan, HTML, *dom)); return Ok(dom); } else if (mime.is("image/svg+xml"_mime)) { Io::SScan scan{buf}; Markup::XmlParser parser; - dom = try$(parser.parse(scan, SVG)); + try$(parser.parse(scan, SVG, *dom)); return Ok(dom); } else { @@ -40,7 +40,7 @@ Res> viewSource(Mime::Url const &url) { auto file = try$(Sys::File::open(url)); auto buf = try$(Io::readAllUtf8(file)); - auto dom = makeStrong(); + auto dom = makeStrong(url); auto body = makeStrong(Html::BODY); dom->appendChild(body); @@ -55,7 +55,7 @@ Res> viewSource(Mime::Url const &url) { } Res> indexOf(Mime::Url const &url) { - auto dom = makeStrong(); + auto dom = makeStrong(url); auto body = makeStrong(Html::BODY); dom->appendChild(body); @@ -106,7 +106,7 @@ Res> fetchDocument(Mime::Url const &url) { if (not mime.has()) return Error::invalidInput("cannot determine MIME type"); - auto dom = makeStrong(); + auto dom = makeStrong(url); auto file = try$(Sys::File::open(url)); return loadDocument(url, *mime, file); } @@ -122,4 +122,40 @@ Res fetchStylesheet(Mime::Url url, Style::Origin origin) { return Ok(Style::StyleSheet::parse(s, origin)); } +void fetchStylesheets(Markup::Node const &node, Style::StyleBook &sb) { + auto el = node.is(); + if (el and el->tagName == Html::STYLE) { + auto text = el->textContent(); + Io::SScan textScan{text}; + auto sheet = Style::StyleSheet::parse(textScan); + sb.add(std::move(sheet)); + } else if (el and el->tagName == Html::LINK) { + auto rel = el->getAttribute(Html::REL_ATTR); + if (rel == "stylesheet"s) { + auto href = el->getAttribute(Html::HREF_ATTR); + if (not href) { + logWarn("link element missing href attribute"); + return; + } + + auto url = Mime::parseUrlOrPath(*href); + if (not url) { + logWarn("link element href attribute is not a valid URL: {}", *href); + return; + } + + auto sheet = fetchStylesheet(url.take(), Style::Origin::AUTHOR); + if (not sheet) { + logWarn("failed to fetch stylesheet: {}", sheet); + return; + } + + sb.add(sheet.take()); + } + } else { + for (auto &child : node.children()) + fetchStylesheets(*child, sb); + } +} + } // namespace Vaev::Driver diff --git a/src/web/vaev-driver/fetcher.h b/src/web/vaev-driver/fetcher.h index a66703b7088..777bc5bd334 100644 --- a/src/web/vaev-driver/fetcher.h +++ b/src/web/vaev-driver/fetcher.h @@ -8,6 +8,8 @@ namespace Vaev::Driver { Res fetchStylesheet(Mime::Url url, Style::Origin origin = Style::Origin::AUTHOR); +void fetchStylesheets(Markup::Node const &node, Style::StyleBook &sb); + Res> fetchDocument(Mime::Url const &url); Res> loadDocument(Mime::Url const &url, Mime::Mime const &mime, Io::Reader &reader); diff --git a/src/web/vaev-driver/print.cpp b/src/web/vaev-driver/print.cpp new file mode 100644 index 00000000000..993c97dca30 --- /dev/null +++ b/src/web/vaev-driver/print.cpp @@ -0,0 +1,381 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fetcher.h" +#include "print.h" + +namespace Vaev::Driver { + +static void _paintMargins(Style::PageComputedStyle &pageStyle, RectPx pageRect, RectPx pageContent, Scene::Stack &stack) { + // MARK: Top Left Corner --------------------------------------------------- + + auto topLeftMarginCornerRect = RectPx::fromTwoPoint( + pageRect.topStart(), + pageContent.topStart() + ); + Layout::Tree topLeftMarginCornerTree{ + .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_LEFT_CORNER)), + .viewport = Layout::Viewport{.small = topLeftMarginCornerRect.size()} + }; + + auto [_, topLeftMarginCornerFrag] = Layout::layoutCreateFragment( + topLeftMarginCornerTree, + { + .knownSize = topLeftMarginCornerRect.size().cast>(), + .position = topLeftMarginCornerRect.topStart(), + .availableSpace = topLeftMarginCornerRect.size(), + .containingBlock = topLeftMarginCornerRect.size(), + } + ); + Layout::paint(topLeftMarginCornerFrag, stack); + + // MARK: Top Right Corner -------------------------------------------------- + + auto topRightMarginCornerRect = RectPx::fromTwoPoint( + pageRect.topEnd(), + pageContent.topEnd() + ); + Layout::Tree topRightMarginCornerTree{ + .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_RIGHT_CORNER)), + .viewport = Layout::Viewport{.small = topRightMarginCornerRect.size()} + }; + + auto [_, topRightMarginCornerFrag] = Layout::layoutCreateFragment( + topRightMarginCornerTree, + { + .knownSize = topRightMarginCornerRect.size().cast>(), + .position = topRightMarginCornerRect.topStart(), + .availableSpace = topRightMarginCornerRect.size(), + .containingBlock = topRightMarginCornerRect.size(), + } + ); + Layout::paint(topRightMarginCornerFrag, stack); + + // MARK: Bottom Left Corner ------------------------------------------------ + + auto bottomLeftMarginCornerRect = RectPx::fromTwoPoint( + pageRect.bottomStart(), + pageContent.bottomStart() + ); + Layout::Tree bottomLeftMarginCornerTree{ + .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_LEFT_CORNER)), + .viewport = Layout::Viewport{.small = bottomLeftMarginCornerRect.size()} + }; + + auto [_, bottomLeftMarginCornerFrag] = Layout::layoutCreateFragment( + bottomLeftMarginCornerTree, + { + .knownSize = bottomLeftMarginCornerRect.size().cast>(), + .position = bottomLeftMarginCornerRect.topStart(), + .availableSpace = bottomLeftMarginCornerRect.size(), + .containingBlock = bottomLeftMarginCornerRect.size(), + } + ); + Layout::paint(bottomLeftMarginCornerFrag, stack); + + // MARK: Bottom Right Corner ----------------------------------------------- + + auto bottomRightMarginCornerRect = RectPx::fromTwoPoint( + pageRect.bottomEnd(), + pageContent.bottomEnd() + ); + Layout::Tree bottomRightMarginCornerTree{ + .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_RIGHT_CORNER)), + .viewport = Layout::Viewport{.small = bottomRightMarginCornerRect.size()} + }; + + auto [_, bottomRightMarginCornerFrag] = Layout::layoutCreateFragment( + bottomRightMarginCornerTree, + { + .knownSize = bottomRightMarginCornerRect.size().cast>(), + .position = bottomRightMarginCornerRect.topStart(), + .availableSpace = bottomRightMarginCornerRect.size(), + .containingBlock = bottomRightMarginCornerRect.size(), + } + ); + Layout::paint(bottomRightMarginCornerFrag, stack); + + // MARK: Top --------------------------------------------------------------- + + auto topRect = RectPx::fromTwoPoint( + topLeftMarginCornerRect.topEnd(), + topRightMarginCornerRect.bottomStart() + ); + + auto topBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP)); + topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_LEFT))); + topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_CENTER))); + topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_RIGHT))); + + Layout::Tree topTree{ + .root = std::move(topBox), + .viewport = Layout::Viewport{.small = topRect.size()} + }; + + auto [_, topFrag] = Layout::layoutCreateFragment( + topTree, + { + .knownSize = topRect.size().cast>(), + .position = topRect.topStart(), + .availableSpace = topRect.size(), + .containingBlock = topRect.size(), + } + ); + Layout::paint(topFrag, stack); + + // MARK: Bottom ------------------------------------------------------------ + + auto bottomRect = RectPx::fromTwoPoint( + bottomLeftMarginCornerRect.topEnd(), + bottomRightMarginCornerRect.bottomStart() + ); + + auto bottomBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM)); + bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_LEFT))); + bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_CENTER))); + bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_RIGHT))); + + Layout::Tree bottomTree{ + .root = std::move(bottomBox), + .viewport = Layout::Viewport{.small = bottomRect.size()} + }; + + auto [_, bottomFrag] = Layout::layoutCreateFragment( + bottomTree, + { + .knownSize = bottomRect.size().cast>(), + .position = bottomRect.topStart(), + .availableSpace = bottomRect.size(), + .containingBlock = bottomRect.size(), + } + ); + + Layout::paint(bottomFrag, stack); + + // MARK: Left -------------------------------------------------------------- + auto leftRect = RectPx::fromTwoPoint( + topLeftMarginCornerRect.bottomEnd(), + bottomLeftMarginCornerRect.topStart() + ); + + auto leftBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT)); + leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_TOP))); + leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_MIDDLE))); + leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_BOTTOM))); + + Layout::Tree leftTree{ + .root = std::move(leftBox), + .viewport = Layout::Viewport{.small = leftRect.size()} + }; + + auto [_, leftFrag] = Layout::layoutCreateFragment( + leftTree, + { + .knownSize = leftRect.size().cast>(), + .position = leftRect.topStart(), + .availableSpace = leftRect.size(), + .containingBlock = leftRect.size(), + } + ); + + Layout::paint(leftFrag, stack); + + // MARK: Right ------------------------------------------------------------- + + auto rightRect = RectPx::fromTwoPoint( + topRightMarginCornerRect.bottomEnd(), + bottomRightMarginCornerRect.topStart() + ); + + auto rightBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT)); + rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_TOP))); + rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_MIDDLE))); + rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_BOTTOM))); + + Layout::Tree rightTree{ + .root = std::move(rightBox), + .viewport = Layout::Viewport{.small = rightRect.size()} + }; + + auto [_, rightFrag] = Layout::layoutCreateFragment( + rightTree, + { + .knownSize = rightRect.size().cast>(), + .position = rightRect.topStart(), + .availableSpace = rightRect.size(), + .containingBlock = rightRect.size(), + } + ); + + Layout::paint(rightFrag, stack); +} + +static Style::Media _constructMedia(Print::Settings const &settings) { + return { + .type = MediaType::SCREEN, + .width = Px{settings.paper.width}, + .height = Px{settings.paper.height}, + .aspectRatio = settings.paper.width / (f64)settings.paper.height, + .orientation = settings.orientation, + + .resolution = Resolution{settings.scale, Resolution::X}, + .scan = Scan::PROGRESSIVE, + .grid = false, + .update = Update::FAST, + + .overflowBlock = OverflowBlock::SCROLL, + .overflowInline = OverflowInline::SCROLL, + + .color = 8, + .colorIndex = 0, + .monochrome = 0, + .colorGamut = ColorGamut::SRGB, + .pointer = Pointer::FINE, + .hover = Hover::HOVER, + .anyPointer = Pointer::FINE, + .anyHover = Hover::HOVER, + + .prefersReducedMotion = ReducedMotion::NO_PREFERENCE, + .prefersReducedTransparency = ReducedTransparency::NO_PREFERENCE, + .prefersContrast = Contrast::NO_PREFERENCE, + .forcedColors = Colors::NONE, + .prefersColorScheme = ColorScheme::LIGHT, + .prefersReducedData = ReducedData::NO_PREFERENCE, + }; +} + +Vec> print(Markup::Document const &dom, Print::Settings const &settings) { + auto media = _constructMedia(settings); + + Style::StyleBook stylebook; + stylebook.add( + fetchStylesheet("bundle://vaev-driver/html.css"_url, Style::Origin::USER_AGENT) + .take("user agent stylesheet not available") + ); + stylebook.add( + fetchStylesheet("bundle://vaev-driver/print.css"_url, Style::Origin::USER_AGENT) + .take("print stylesheet not available") + ); + + fetchStylesheets(dom, stylebook); + + Style::Computer computer{ + media, stylebook + }; + + // MARK: Page and Margins -------------------------------------------------- + + Vec> pages; + + Style::Computed initialStyle = Style::Computed::initial(); + initialStyle.color = Gfx::BLACK; + initialStyle.setCustomProp("-vaev-url", {Css::Token::string(Io::format("\"{}\"", dom.url()).unwrap())}); + initialStyle.setCustomProp("-vaev-title", {Css::Token::string(Io::format("\"{}\"", dom.title()).unwrap())}); + initialStyle.setCustomProp("-vaev-datetime", {Css::Token::string(Io::format("\"{}\"", Sys::now()).unwrap())}); + + auto scaleMatrix = Math::Trans2f::makeScale(media.resolution.toDppx()); + + // MARK: Page Content ------------------------------------------------------ + + Layout::Tree contentTree = { + Layout::build(computer, dom), + }; + + Layout::Breakpoint prevBreakpoint{ + .endIdx = 0, + .advanceCase = Layout::Breakpoint::ADVANCE_CASE::ADVANCE_WITHOUT_CHILDREN + }; + Layout::Breakpoint currBreakpoint; + + while (true) { + Layout::Resolver resolver{}; + Style::Page page{.name = ""s, .number = pages.len(), .blank = false}; + + auto pageStyle = computer.computeFor(initialStyle, page); + RectPx pageRect{ + media.width / Px{media.resolution.toDppx()}, + media.height / Px{media.resolution.toDppx()} + }; + + auto pageSize = pageRect.size().cast(); + auto pageScene = pages.emplaceBack( + // FIXME: it can be that specific pages have different dimensions + makeStrong( + Print::PaperStock{ + "custom"s, + pageSize.width, + pageSize.height, + }, + scaleMatrix + ) + ); + + InsetsPx pageMargin = {}; + + if (settings.margins == Print::Margins::DEFAULT) { + pageMargin = { + resolver.resolve(pageStyle->style->margin->top, pageRect.height), + resolver.resolve(pageStyle->style->margin->end, pageRect.width), + 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) { + } + + RectPx pageContent = pageRect.shrink(pageMargin); + + Layout::Viewport vp{ + .small = pageContent.size(), + }; + + contentTree.viewport = vp; + contentTree.fc.currSize = pageContent.size(); + + if (settings.headerFooter and settings.margins != Print::Margins::NONE) + _paintMargins(*pageStyle, pageRect, pageContent, *pageScene); + + Layout::Input pageLayoutInput{ + .knownSize = {pageContent.width, NONE}, + .position = pageContent.topStart(), + .availableSpace = pageContent.size(), + .containingBlock = pageContent.size(), + }; + + contentTree.fc.enterDiscovery(); + auto outDiscovery = Layout::layout( + contentTree, + pageLayoutInput.withBreakpointTraverser(Layout::BreakpointTraverser(&prevBreakpoint)) + ); + + currBreakpoint = outDiscovery.completelyLaidOut + ? Layout::Breakpoint::buildClassB(1, false) + : outDiscovery.breakpoint.unwrap(); + + contentTree.fc.leaveDiscovery(); + auto [outReal, fragment] = Layout::layoutCreateFragment( + contentTree, + pageLayoutInput + .withBreakpointTraverser(Layout::BreakpointTraverser(&prevBreakpoint, &currBreakpoint)) + ); + + Layout::paint(fragment, *pageScene); + pageScene->prepare(); + + if (outReal.completelyLaidOut) + break; + + std::swap(prevBreakpoint, currBreakpoint); + } + + return pages; +} + +} // namespace Vaev::Driver diff --git a/src/web/vaev-driver/print.h b/src/web/vaev-driver/print.h new file mode 100644 index 00000000000..48f172dbb75 --- /dev/null +++ b/src/web/vaev-driver/print.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include + +namespace Vaev::Driver { + +Vec> print(Markup::Document const &dom, Print::Settings const &settings); + +} // namespace Vaev::Driver diff --git a/src/web/vaev-driver/render.cpp b/src/web/vaev-driver/render.cpp index de8a1fbd5fa..48d773679b8 100644 --- a/src/web/vaev-driver/render.cpp +++ b/src/web/vaev-driver/render.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -16,42 +15,6 @@ namespace Vaev::Driver { static constexpr bool DEBUG_RENDER = false; -static void _collectStyle(Markup::Node const &node, Style::StyleBook &sb) { - auto el = node.is(); - if (el and el->tagName == Html::STYLE) { - auto text = el->textContent(); - Io::SScan textScan{text}; - auto sheet = Style::StyleSheet::parse(textScan); - sb.add(std::move(sheet)); - } else if (el and el->tagName == Html::LINK) { - auto rel = el->getAttribute(Html::REL_ATTR); - if (rel == "stylesheet"s) { - auto href = el->getAttribute(Html::HREF_ATTR); - if (not href) { - logWarn("link element missing href attribute"); - return; - } - - auto url = Mime::parseUrlOrPath(*href); - if (not url) { - logWarn("link element href attribute is not a valid URL: {}", *href); - return; - } - - auto sheet = fetchStylesheet(url.take(), Style::Origin::AUTHOR); - if (not sheet) { - logWarn("failed to fetch stylesheet: {}", sheet); - return; - } - - sb.add(sheet.take()); - } - } else { - for (auto &child : node.children()) - _collectStyle(*child, sb); - } -} - RenderResult render(Markup::Document const &dom, Style::Media const &media, Layout::Viewport viewport) { Style::StyleBook stylebook; stylebook.add( @@ -60,7 +23,7 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Layo ); auto start = Sys::now(); - _collectStyle(dom, stylebook); + fetchStylesheets(dom, stylebook); auto elapsed = Sys::now() - start; logDebugIf(DEBUG_RENDER, "style collection time: {}", elapsed); @@ -78,10 +41,9 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Layo start = Sys::now(); - Layout::layout( + auto [outDiscovery, root] = Layout::layoutCreateFragment( tree, { - .commit = Layout::Commit::YES, .knownSize = {viewport.small.width, NONE}, .availableSpace = {viewport.small.width, 0_px}, .containingBlock = {viewport.small.width, viewport.small.height}, @@ -94,8 +56,7 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Layo logDebugIf(DEBUG_RENDER, "layout tree layout time: {}", elapsed); auto paintStart = Sys::now(); - - Layout::paint(tree.root, *sceneRoot); + Layout::paint(root, *sceneRoot); sceneRoot->prepare(); elapsed = Sys::now() - paintStart; @@ -105,286 +66,8 @@ RenderResult render(Markup::Document const &dom, Style::Media const &media, Layo std::move(stylebook), makeStrong(std::move(tree.root)), sceneRoot, + makeStrong(root) }; } -static void _paintMargins(Style::PageComputedStyle &pageStyle, RectPx pageRect, RectPx pageContent, Scene::Stack &stack) { - // MARK: Top Left Corner --------------------------------------------------- - - auto topLeftMarginCornerRect = RectPx::fromTwoPoint( - pageRect.topStart(), - pageContent.topStart() - ); - Layout::Tree topLeftMarginCornerTree{ - .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_LEFT_CORNER)), - .viewport = Layout::Viewport{.small = topLeftMarginCornerRect.size()} - }; - Layout::layout( - topLeftMarginCornerTree, - { - .commit = Layout::Commit::YES, - .knownSize = topLeftMarginCornerRect.size().cast>(), - .position = topLeftMarginCornerRect.topStart(), - .availableSpace = topLeftMarginCornerRect.size(), - .containingBlock = topLeftMarginCornerRect.size(), - } - ); - Layout::paint(topLeftMarginCornerTree.root, stack); - - // MARK: Top Right Corner -------------------------------------------------- - - auto topRightMarginCornerRect = RectPx::fromTwoPoint( - pageRect.topEnd(), - pageContent.topEnd() - ); - Layout::Tree topRightMarginCornerTree{ - .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_RIGHT_CORNER)), - .viewport = Layout::Viewport{.small = topRightMarginCornerRect.size()} - }; - Layout::layout( - topRightMarginCornerTree, - { - .commit = Layout::Commit::YES, - .knownSize = topRightMarginCornerRect.size().cast>(), - .position = topRightMarginCornerRect.topStart(), - .availableSpace = topRightMarginCornerRect.size(), - .containingBlock = topRightMarginCornerRect.size(), - } - ); - Layout::paint(topRightMarginCornerTree.root, stack); - - // MARK: Bottom Left Corner ------------------------------------------------ - - auto bottomLeftMarginCornerRect = RectPx::fromTwoPoint( - pageRect.bottomStart(), - pageContent.bottomStart() - ); - Layout::Tree bottomLeftMarginCornerTree{ - .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_LEFT_CORNER)), - .viewport = Layout::Viewport{.small = bottomLeftMarginCornerRect.size()} - }; - Layout::layout( - bottomLeftMarginCornerTree, - bottomLeftMarginCornerTree.root, - { - .commit = Layout::Commit::YES, - .knownSize = bottomLeftMarginCornerRect.size().cast>(), - .position = bottomLeftMarginCornerRect.topStart(), - .availableSpace = bottomLeftMarginCornerRect.size(), - .containingBlock = bottomLeftMarginCornerRect.size(), - } - ); - Layout::paint(bottomLeftMarginCornerTree.root, stack); - - // MARK: Bottom Right Corner ----------------------------------------------- - - auto bottomRightMarginCornerRect = RectPx::fromTwoPoint( - pageRect.bottomEnd(), - pageContent.bottomEnd() - ); - Layout::Tree bottomRightMarginCornerTree{ - .root = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_RIGHT_CORNER)), - .viewport = Layout::Viewport{.small = bottomRightMarginCornerRect.size()} - }; - Layout::layout( - bottomRightMarginCornerTree, - { - .commit = Layout::Commit::YES, - .knownSize = bottomRightMarginCornerRect.size().cast>(), - .position = bottomRightMarginCornerRect.topStart(), - .availableSpace = bottomRightMarginCornerRect.size(), - .containingBlock = bottomRightMarginCornerRect.size(), - } - ); - Layout::paint(bottomRightMarginCornerTree.root, stack); - - // MARK: Top --------------------------------------------------------------- - - auto topRect = RectPx::fromTwoPoint( - topLeftMarginCornerRect.topEnd(), - topRightMarginCornerRect.bottomStart() - ); - - auto topBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP)); - topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_LEFT))); - topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_CENTER))); - topBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::TOP_RIGHT))); - - Layout::Tree topTree{ - .root = std::move(topBox), - .viewport = Layout::Viewport{.small = topRect.size()} - }; - - Layout::layout( - topTree, - { - .commit = Layout::Commit::YES, - .knownSize = topRect.size().cast>(), - .position = topRect.topStart(), - .availableSpace = topRect.size(), - .containingBlock = topRect.size(), - } - ); - Layout::paint(topTree.root, stack); - - // MARK: Bottom ------------------------------------------------------------ - - auto bottomRect = RectPx::fromTwoPoint( - bottomLeftMarginCornerRect.topEnd(), - bottomRightMarginCornerRect.bottomStart() - ); - - auto bottomBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM)); - bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_LEFT))); - bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_CENTER))); - bottomBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::BOTTOM_RIGHT))); - - Layout::Tree bottomTree{ - .root = std::move(bottomBox), - .viewport = Layout::Viewport{.small = bottomRect.size()} - }; - - Layout::layout( - bottomTree, - { - .commit = Layout::Commit::YES, - .knownSize = bottomRect.size().cast>(), - .position = bottomRect.topStart(), - .availableSpace = bottomRect.size(), - .containingBlock = bottomRect.size(), - } - ); - - Layout::paint(bottomTree.root, stack); - - // MARK: Left -------------------------------------------------------------- - auto leftRect = RectPx::fromTwoPoint( - topLeftMarginCornerRect.bottomEnd(), - bottomLeftMarginCornerRect.topStart() - ); - - auto leftBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT)); - leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_TOP))); - leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_MIDDLE))); - leftBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::LEFT_BOTTOM))); - - Layout::Tree leftTree{ - .root = std::move(leftBox), - .viewport = Layout::Viewport{.small = leftRect.size()} - }; - - Layout::layout( - leftTree, - { - .commit = Layout::Commit::YES, - .knownSize = leftRect.size().cast>(), - .position = leftRect.topStart(), - .availableSpace = leftRect.size(), - .containingBlock = leftRect.size(), - } - ); - - Layout::paint(leftTree.root, stack); - - // MARK: Right ------------------------------------------------------------- - - auto rightRect = RectPx::fromTwoPoint( - topRightMarginCornerRect.bottomEnd(), - bottomRightMarginCornerRect.topStart() - ); - - auto rightBox = Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT)); - rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_TOP))); - rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_MIDDLE))); - rightBox.add(Layout::buildForPseudoElement(pageStyle.area(Style::PageArea::RIGHT_BOTTOM))); - - Layout::Tree rightTree{ - .root = std::move(rightBox), - .viewport = Layout::Viewport{.small = rightRect.size()} - }; - - Layout::layout( - rightTree, - { - .commit = Layout::Commit::YES, - .knownSize = rightRect.size().cast>(), - .position = rightRect.topStart(), - .availableSpace = rightRect.size(), - .containingBlock = rightRect.size(), - } - ); - - Layout::paint(rightTree.root, stack); -} - -Vec> print(Markup::Document const &dom, Style::Media const &media) { - Style::StyleBook stylebook; - stylebook.add( - fetchStylesheet("bundle://vaev-driver/html.css"_url, Style::Origin::USER_AGENT) - .take("user agent stylesheet not available") - ); - stylebook.add( - fetchStylesheet("bundle://vaev-driver/print.css"_url, Style::Origin::USER_AGENT) - .take("print stylesheet not available") - ); - - _collectStyle(dom, stylebook); - - Style::Computer computer{media, stylebook}; - - // MARK: Page and Margins -------------------------------------------------- - - Vec> pages; - - Layout::Resolver resolver{}; - Style::Page page{.name = ""s, .number = pages.len(), .blank = false}; - auto pageStyle = computer.computeFor(page); - RectPx pageRect{ - media.width / Px{media.resolution.toDppx()}, - media.height / Px{media.resolution.toDppx()} - }; - - auto scaleMatrix = Math::Trans2f::makeScale(media.resolution.toDppx()); - auto pageScene = makeStrong(pageRect.size().cast(), scaleMatrix); - - InsetsPx pageMargin = { - resolver.resolve(pageStyle->style->margin->top, pageRect.height), - resolver.resolve(pageStyle->style->margin->end, pageRect.width), - resolver.resolve(pageStyle->style->margin->bottom, pageRect.height), - resolver.resolve(pageStyle->style->margin->start, pageRect.width), - }; - - RectPx pageContent = pageRect.shrink(pageMargin); - - _paintMargins(*pageStyle, pageRect, pageContent, *pageScene); - - // MARK: Page Content ------------------------------------------------------ - - Layout::Viewport vp{ - .small = pageContent.size(), - }; - - Layout::Tree contentTree = { - Layout::build(computer, dom), - vp, - }; - - Layout::layout( - contentTree, - { - .commit = Layout::Commit::YES, - .knownSize = {pageContent.width, NONE}, - .position = pageContent.topStart(), - .availableSpace = pageContent.size(), - .containingBlock = pageContent.size(), - } - ); - - Layout::paint(contentTree.root, *pageScene); - pageScene->prepare(); - pages.pushBack(pageScene); - - return pages; -} - } // namespace Vaev::Driver diff --git a/src/web/vaev-driver/render.h b/src/web/vaev-driver/render.h index 7fa28748683..931b7241ea4 100644 --- a/src/web/vaev-driver/render.h +++ b/src/web/vaev-driver/render.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -12,11 +14,10 @@ namespace Vaev::Driver { struct RenderResult { Style::StyleBook style; Strong layout; - Strong scene; + Strong scenes; + Strong frag; }; RenderResult render(Markup::Document const &dom, Style::Media const &media, Layout::Viewport viewport); -Vec> print(Markup::Document const &dom, Style::Media const &media); - } // namespace Vaev::Driver diff --git a/src/web/vaev-driver/res/print.css b/src/web/vaev-driver/res/print.css index 0ffaeffac2a..60b6fe79325 100644 --- a/src/web/vaev-driver/res/print.css +++ b/src/web/vaev-driver/res/print.css @@ -20,12 +20,14 @@ text-align: left; vertical-align: middle; flex-grow: 1; + content: var(-vaev-datetime); } @top-center { text-align: center; vertical-align: middle; flex-grow: 1; + content: var(-vaev-title); } @top-right { @@ -106,6 +108,7 @@ text-align: left; vertical-align: middle; flex-grow: 1; + content: var(-vaev-url); } @bottom-center { @@ -118,7 +121,6 @@ text-align: right; vertical-align: middle; flex-grow: 1; - } @bottom-right-corner { diff --git a/src/web/vaev-layout/base.h b/src/web/vaev-layout/base.h index 94681dd1ae8..a17cb40af8e 100644 --- a/src/web/vaev-layout/base.h +++ b/src/web/vaev-layout/base.h @@ -23,118 +23,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 = {}; - - // To be used between table wrapper and table box - Opt capmin = NONE; - - Input withCommit(Commit c) const { - auto copy = *this; - copy.commit = c; - return copy; - } - - Input withIntrinsic(IntrinsicSize i) const { - auto copy = *this; - copy.intrinsic = i; - return copy; - } - - Input withKnownSize(Math::Vec2> size) const { - auto copy = *this; - copy.knownSize = size; - return copy; - } - - Input withPosition(Vec2Px pos) const { - auto copy = *this; - copy.position = pos; - return copy; - } - - Input withAvailableSpace(Vec2Px space) const { - auto copy = *this; - copy.availableSpace = space; - return copy; - } - - Input withContainingBlock(Vec2Px block) const { - auto copy = *this; - copy.containingBlock = block; - return copy; - } -}; - -/// Output of the layout algorithm. - -struct Output { - Vec2Px size; - - Px width() const { - return size.x; - } - - Px height() const { - return size.y; - } - - 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/web/vaev-layout/block.cpp b/src/web/vaev-layout/block.cpp index 9ce79c2b5dd..00b11f1be57 100644 --- a/src/web/vaev-layout/block.cpp +++ b/src/web/vaev-layout/block.cpp @@ -1,10 +1,110 @@ -#include "block.h" +#include "input_output.h" -#include "box.h" #include "layout.h" +#include "tree.h" namespace Vaev::Layout { +void maybeProcessChildBreakpoint(FragmentationContext &fc, Breakpoint ¤tBreakpoint, usize childIndex, bool currBoxIsBreakAvoid, Opt maybeChildBreakpoint) { + if (not fc.isDiscoveryMode()) + return; + + // if we are in a monolitic context, we might not have breakpoints + if (not maybeChildBreakpoint) + return; + + // breakpoint inside child (from this blocks perspective) + // BREAK CLASS X (recursive case) + currentBreakpoint.overrideIfBetter( + Breakpoint::buildFromChild( + std::move(maybeChildBreakpoint.unwrap()), + childIndex + 1, + currBoxIsBreakAvoid + ) + ); +} + +Res processBreakpointsAfterChild(FragmentationContext &fc, Breakpoint ¤tBreakpoint, Box &parentBox, usize childIndex, Vec2Px currentBoxSize, bool childCompletelyLaidOut) { + if (not fc.isDiscoveryMode()) + return Ok(NONE); + + // last child was not completly laid out, we need to abort with our best breakpoint + if (not childCompletelyLaidOut) { + return Output{ + .size = currentBoxSize, + .completelyLaidOut = false, + .breakpoint = currentBreakpoint + }; + } + + Box &childBox = parentBox.children()[childIndex]; + bool isLastChild = childIndex + 1 == parentBox.children().len(); + + // 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 (not isLastChild) { + bool breakIsAvoided = + parentBox.style->break_->inside == BreakInside::AVOID or + childBox.style->break_->after == BreakBetween::AVOID or + (not isLastChild and parentBox.children()[childIndex + 1].style->break_->before == BreakBetween::AVOID); + + currentBreakpoint.overrideIfBetter( + Breakpoint::buildClassB( + childIndex + 1, + breakIsAvoided + ) + ); + } + + // FORCED BREAK + if (childBox.style->break_->after == BreakBetween::PAGE) { + return Output{ + .size = currentBoxSize, + .completelyLaidOut = false, + .breakpoint = Breakpoint::buildForced( + childIndex + 1 + ) + }; + } + + return Ok(NONE); +} + +Res processBreakpointsBeforeChild(usize endAt, Vec2Px currentSize, bool forcedBreakBefore, usize startAt) { + // FORCED BREAK + if (forcedBreakBefore and not(startAt == endAt)) { + return Output{ + .size = currentSize, + .completelyLaidOut = false, + .breakpoint = Breakpoint::buildForced(endAt) + }; + } + + return Ok(NONE); +} + +Output fragmentEmptyBox(Tree &tree, Input input) { + // put this here instead of in layout.py since we want to know if its the empty box case + Vec2Px knownSize{input.knownSize.x.unwrapOr(0_px), input.knownSize.y.unwrapOr(0_px)}; + if (tree.fc.acceptsFit( + input.position.y, + knownSize.y, + input.pendingVerticalSizes + )) { + return Output{ + .size = knownSize, + .completelyLaidOut = true, + }; + } else { + return Output{ + .size = {}, + .completelyLaidOut = false, + .breakpoint = Breakpoint::buildOverflow() + }; + } +} + // https://www.w3.org/TR/CSS22/visuren.html#normal-flow struct BlockFormatingContext { @@ -33,26 +133,48 @@ struct BlockFormatingContext { return capmin; } - Output run(Tree &tree, Box &box, Input input) { - + Output run(Tree &tree, Box &box, Input input, usize startAt, Opt stopAt) { Px blockSize = 0_px; Px inlineSize = input.knownSize.width.unwrapOr(0_px); + if (box.children().len() == 0) { + return fragmentEmptyBox(tree, input); + } + // NOTE: Our parent has no clue about our width but wants us to commit, // we need to compute it first - if (input.commit == Commit::YES and not input.knownSize.width) - inlineSize = run(tree, box, input.withCommit(Commit::NO)).width(); + if (input.fragment and not input.knownSize.width) + inlineSize = run(tree, box, input.withFragment(nullptr), startAt, stopAt).width(); + + Breakpoint currentBreakpoint; + + usize endChildren = stopAt.unwrapOr(box.children().len()); + + bool blockWasCompletelyLaidOut = box.children().len() == 0; + + for (usize i = startAt; i < endChildren; ++i) { + auto &c = box.children()[i]; + + try$( + processBreakpointsBeforeChild( + i, + Vec2Px{inlineSize, blockSize}, + c.style->break_->before == BreakBetween::PAGE, + startAt + ) + ); - for (auto &c : box.children()) { // TODO: Implement floating // if (c.style->float_ != Float::NONE) // continue; Input childInput = { - .commit = input.commit, + .fragment = input.fragment, .intrinsic = input.intrinsic, .availableSpace = {input.availableSpace.x, 0_px}, .containingBlock = {inlineSize, input.knownSize.y.unwrapOr(0_px)}, + .breakpointTraverser = input.breakpointTraverser.traverseInsideUsingIthChild(i), + .pendingVerticalSizes = input.pendingVerticalSizes, }; auto margin = computeMargins(tree, c, childInput); @@ -64,7 +186,7 @@ struct BlockFormatingContext { if (c.style->position != Position::ABSOLUTE) { blockSize += margin.top; - if (input.commit == Commit::YES or input.knownSize.x) + if (input.fragment or input.knownSize.x) childInput.knownSize.width = childInlineSize; } @@ -83,19 +205,41 @@ struct BlockFormatingContext { blockSize += output.size.y + margin.bottom; } + maybeProcessChildBreakpoint( + tree.fc, + currentBreakpoint, + i, + box.style->break_->inside == BreakInside::AVOID, + output.breakpoint + ); + + try$(processBreakpointsAfterChild( + tree.fc, + currentBreakpoint, + box, + i, + Vec2Px{inlineSize, blockSize}, + output.completelyLaidOut + )); + + if (tree.fc.allowBreak() and i + 1 == endChildren) { + blockWasCompletelyLaidOut = output.completelyLaidOut and i + 1 == box.children().len(); + } + inlineSize = max(inlineSize, output.size.x + margin.horizontal()); } - return Output::fromSize({ - inlineSize, - blockSize, - }); + return Output{ + .size = Vec2Px{inlineSize, blockSize}, + .completelyLaidOut = blockWasCompletelyLaidOut, + .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/web/vaev-layout/block.h b/src/web/vaev-layout/block.h index ff77b129109..9634ed1fe6a 100644 --- a/src/web/vaev-layout/block.h +++ b/src/web/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/web/vaev-layout/box.cpp b/src/web/vaev-layout/box.cpp index 16dbc0eb4a7..10313593fa1 100644 --- a/src/web/vaev-layout/box.cpp +++ b/src/web/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/web/vaev-layout/box.h b/src/web/vaev-layout/box.h index 64b5dc21620..bc36c5eace8 100644 --- a/src/web/vaev-layout/box.h +++ b/src/web/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/web/vaev-layout/builder.cpp b/src/web/vaev-layout/builder.cpp index 0c90ca134de..bbccddbe68b 100644 --- a/src/web/vaev-layout/builder.cpp +++ b/src/web/vaev-layout/builder.cpp @@ -320,7 +320,6 @@ Box buildForPseudoElement(Strong style) { auto prose = makeStrong(proseStyle); if (style->content) { - logDebug("content: '{}'", style->content); prose->append(style->content.str()); return {style, fontFace, prose}; } diff --git a/src/web/vaev-layout/flex.cpp b/src/web/vaev-layout/flex.cpp index 6e088427a7a..40f4f14897a 100644 --- a/src/web/vaev-layout/flex.cpp +++ b/src/web/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.containingBlock, - } + input ); } @@ -471,7 +468,7 @@ struct FlexItem { } } - void alignCrossStretch(Tree &tree, Commit commit, Vec2Px availableSpaceInFlexContainer, Px lineCrossSize) { + void alignCrossStretch(Tree &tree, Vec2Px availableSpaceInFlexContainer, Px lineCrossSize) { if ( fa.crossAxis(box->style->sizing).type == Size::Type::AUTO and fa.startCrossAxis(*box->style->margin) != Width::Type::AUTO and @@ -485,7 +482,6 @@ struct FlexItem { speculateValues( tree, { - .commit = commit, .knownSize = fa.extractMainAxisAndFillOptOther(usedSize, elementSpeculativeCrossSize), .availableSpace = availableSpaceInFlexContainer, } @@ -495,7 +491,7 @@ struct FlexItem { fa.crossAxis(position) = getMargin(START_CROSS); } - void alignItem(Tree &tree, Commit commit, Vec2Px availableSpaceInFlexContainer, Px lineCrossSize, Style::Align::Keywords parentAlignItems) { + void alignItem(Tree &tree, Vec2Px availableSpaceInFlexContainer, Px lineCrossSize, Style::Align::Keywords parentAlignItems) { auto align = box->style->aligns.alignSelf.keyword; if (align == Style::Align::AUTO) @@ -515,7 +511,7 @@ struct FlexItem { return; case Style::Align::STRETCH: - alignCrossStretch(tree, commit, availableSpaceInFlexContainer, lineCrossSize); + alignCrossStretch(tree, availableSpaceInFlexContainer, lineCrossSize); return; default: @@ -688,7 +684,6 @@ struct FlexFormatingContext { i.speculateValues( tree, { - .commit = Commit::NO, .knownSize = fa.extractMainAxisAndFillOptOther(i.flexBaseSize), .availableSpace = availableSpace, } @@ -1081,7 +1076,6 @@ struct FlexFormatingContext { i.speculateValues( tree, { - .commit = Commit::NO, .knownSize = fa.extractMainAxisAndFillOptOther(i.usedSize), .availableSpace = fa.extractMainAxisAndFillOther(i.usedSize, availableCrossSpace), } @@ -1295,7 +1289,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; @@ -1359,12 +1353,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.commit, availableSpace, flexLine.crossSize, box.style->aligns.alignItems.keyword @@ -1485,14 +1478,13 @@ struct FlexFormatingContext { tree, *flexItem.box, { - .commit = Commit::YES, + .fragment = input.fragment, .knownSize = {flexItem.usedSize.x, flexItem.usedSize.y}, .position = flexItem.position, .availableSpace = availableSpace, } ); - - flexItem.commit(); + flexItem.commit(input.fragment); } } } @@ -1541,7 +1533,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); @@ -1550,7 +1542,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/web/vaev-layout/flex.h b/src/web/vaev-layout/flex.h index 20895def3bb..a9658ccb6ca 100644 --- a/src/web/vaev-layout/flex.h +++ b/src/web/vaev-layout/flex.h @@ -1,6 +1,6 @@ #pragma once -#include "layout.h" +#include "input_output.h" namespace Vaev::Layout { diff --git a/src/web/vaev-layout/frag.h b/src/web/vaev-layout/frag.h new file mode 100644 index 00000000000..a4aedb83c64 --- /dev/null +++ b/src/web/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/web/vaev-layout/fragmentainer.h b/src/web/vaev-layout/fragmentainer.h new file mode 100644 index 00000000000..55a03f48d84 --- /dev/null +++ b/src/web/vaev-layout/fragmentainer.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +#include "frag.h" + +namespace Vaev::Layout { + +// https://www.w3.org/TR/css-break-3/#fragmentainer +// https://www.w3.org/TR/css-break-3/#fragmentation-context +struct FragmentationContext { + + Vec2Px currSize = {Limits::MAX, Limits::MAX}; + + FragmentationContext() {} + + FragmentationContext(Vec2Px currSize) : currSize(currSize) {} + + // discovery mode: + // first pass that identifies the breakpoints (forced or not); that is, breakpoints are only created and manipulated + // in discovery mode + bool _isDiscoveryMode = false; + + void enterDiscovery() { _isDiscoveryMode = true; } + + void leaveDiscovery() { _isDiscoveryMode = false; } + + bool isDiscoveryMode() { + return allowBreak() and _isDiscoveryMode; + } + + // NOTE: a bit rudimentar, but necessary while some displays do not have fragmentation implemented + usize monolithicCount = 0; + + void enterMonolithicBox() { + monolithicCount++; + } + + void leaveMonolithicBox() { + monolithicCount--; + } + + bool hasInfiniteDimensions() { + return currSize == Vec2Px{Limits::MAX, Limits::MAX}; + } + + bool allowBreak() { + return not hasInfiniteDimensions() and monolithicCount == 0; + } + + bool acceptsFit(Px verticalPosition, Px verticalSize, Px pendingVerticalSizes) { + return verticalPosition + verticalSize + pendingVerticalSizes <= currSize.y; + } +}; + +} // namespace Vaev::Layout diff --git a/src/web/vaev-layout/grid.cpp b/src/web/vaev-layout/grid.cpp index ad56a4decce..b9de0041b25 100644 --- a/src/web/vaev-layout/grid.cpp +++ b/src/web/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/web/vaev-layout/grid.h b/src/web/vaev-layout/grid.h index 91ffd993150..30ecd11e3c5 100644 --- a/src/web/vaev-layout/grid.h +++ b/src/web/vaev-layout/grid.h @@ -1,6 +1,6 @@ #pragma once -#include "base.h" +#include "input_output.h" namespace Vaev::Layout { diff --git a/src/web/vaev-layout/inline.h b/src/web/vaev-layout/inline.h index 838475f4757..ecc1d14800d 100644 --- a/src/web/vaev-layout/inline.h +++ b/src/web/vaev-layout/inline.h @@ -1,6 +1,6 @@ #pragma once -#include "base.h" +#include "input_output.h" namespace Vaev::Layout { diff --git a/src/web/vaev-layout/input_output.h b/src/web/vaev-layout/input_output.h new file mode 100644 index 00000000000..8fc25f78120 --- /dev/null +++ b/src/web/vaev-layout/input_output.h @@ -0,0 +1,281 @@ +#pragma once + +#include "base.h" +#include "frag.h" + +namespace Vaev::Layout { + +// 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 { + + usize endIdx = 0; + + enum struct Appeal : usize { + EMPTY = 0, + OVERFLOW = 1, + AVOID = 2, + CLASS_B = 3, + FORCED = 4, + MAX = Limits::MAX + }; + Appeal appeal = Appeal::EMPTY; + + Vec> children = {}; + + enum struct ADVANCE_CASE { + NOT_ADVANCE, // keeping children + ADVANCE_WITH_CHILDREN, + ADVANCE_WITHOUT_CHILDREN + } advanceCase; + + void overrideIfBetter(Breakpoint &&BPWithMoreContent) { + // in case of overflows, we need the earliest breakpoint possible + if (appeal == Appeal::OVERFLOW and BPWithMoreContent.appeal == Appeal::OVERFLOW) + return; + + if (appeal <= BPWithMoreContent.appeal) + *this = std::move(BPWithMoreContent); + } + + void repr(Io::Emit &e) const { + e("(end: {} appeal: {} advance case: {}", endIdx, appeal, advanceCase); + if (children.len() == 0) + e("; no child)"); + else + e("; children : {})", children); + } + + static Breakpoint buildForced(usize endIdx) { + return Breakpoint{ + .endIdx = endIdx, + // since this is a FORCED break, it will have maximum appeal + .appeal = Appeal::FORCED, + .advanceCase = ADVANCE_CASE::ADVANCE_WITHOUT_CHILDREN, + }; + } + + // NOTE: a bit inconsistent with the rest of the API + void applyAvoid() { + appeal = Appeal::AVOID; + } + + static Breakpoint buildFromChild(Breakpoint &&childBreakpoint, usize endIdx, bool isAvoid) { + Breakpoint b{ + .endIdx = endIdx, + .appeal = childBreakpoint.appeal, + .children = {std::move(childBreakpoint)}, + .advanceCase = ADVANCE_CASE::NOT_ADVANCE, + }; + + if (isAvoid) + b.appeal = Appeal::AVOID; + + return b; + } + + static Breakpoint buildFromChildren(Vec> childrenBreakpoints, usize endIdx, bool isAvoid, bool advance) { + Appeal appeal = Appeal::MAX; + for (auto &breakpoint : childrenBreakpoints) { + if (not breakpoint) + continue; + + appeal = min(appeal, breakpoint.unwrap().appeal); + } + + if (appeal == Appeal::MAX) + panic("cannot build breakpoint from children when no children have a breakpoint"); + + Breakpoint b{ + .endIdx = endIdx, + .appeal = appeal, + .children = {std::move(childrenBreakpoints)}, + .advanceCase = advance ? ADVANCE_CASE::ADVANCE_WITH_CHILDREN : ADVANCE_CASE::NOT_ADVANCE + }; + + if (isAvoid) + b.appeal = Appeal::AVOID; + + return b; + } + + static Breakpoint buildClassB(usize endIdx, bool isAvoid) { + Breakpoint b{ + .endIdx = endIdx, + .appeal = isAvoid ? Appeal::AVOID : Appeal::CLASS_B, + .advanceCase = ADVANCE_CASE::ADVANCE_WITHOUT_CHILDREN + }; + + return b; + } + + static Breakpoint buildOverflow() { + // this is a placeholder breakpoint and should be overriden + return { + .endIdx = 0, + .appeal = Appeal::OVERFLOW, + .advanceCase = ADVANCE_CASE::NOT_ADVANCE + }; + } +}; + +struct BreakpointTraverser { + MutCursor prevIteration, currIteration; + + BreakpointTraverser( + MutCursor prev = nullptr, + MutCursor curr = nullptr + ) : prevIteration(prev), currIteration(curr) {} + + bool isDeactivated() { + return prevIteration == nullptr and currIteration == nullptr; + } + + MutCursor traversePrev(usize i, usize j) { + if (prevIteration and prevIteration->children.len() > 0 and + (i + 1 == prevIteration->endIdx or + (prevIteration->advanceCase == Breakpoint::ADVANCE_CASE::ADVANCE_WITH_CHILDREN and i == prevIteration->endIdx) + )) { + if (prevIteration->children[j]) + return &prevIteration->children[j].unwrap(); + } + return nullptr; + } + + MutCursor traverseCurr(usize i, usize j) { + if (currIteration and currIteration->children.len() > 0 and i + 1 == currIteration->endIdx) { + if (currIteration->children[j]) + return &currIteration->children[j].unwrap(); + } + return nullptr; + } + + BreakpointTraverser traverseInsideUsingIthChildToJthParallelFlow(usize i, usize j) { + BreakpointTraverser deeperBPT; + deeperBPT.prevIteration = traversePrev(i, j); + deeperBPT.currIteration = traverseCurr(i, j); + return deeperBPT; + } + + BreakpointTraverser traverseInsideUsingIthChild(usize i) { + return traverseInsideUsingIthChildToJthParallelFlow(i, 0); + } + + Opt getStart() { + if (prevIteration == nullptr) + return NONE; + return prevIteration->endIdx - (prevIteration->advanceCase == Breakpoint::ADVANCE_CASE::NOT_ADVANCE); + } + + Opt getEnd() { + if (currIteration == nullptr) + return NONE; + return currIteration->endIdx; + } +}; + +/// 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 = {}; + + BreakpointTraverser breakpointTraverser = {}; + + // To be used between table wrapper and table box + Opt capmin = NONE; + + // TODO: instead of stringing this around, maybe change this (and check method of fragmentainer) to a + // "availableSpaceInFragmentainer" parameter + Px pendingVerticalSizes = {}; + + Input withFragment(MutCursor f) const { + auto copy = *this; + copy.fragment = f; + return copy; + } + + Input withIntrinsic(IntrinsicSize i) const { + auto copy = *this; + copy.intrinsic = i; + return copy; + } + + Input withKnownSize(Math::Vec2> size) const { + auto copy = *this; + copy.knownSize = size; + return copy; + } + + Input withPosition(Vec2Px pos) const { + auto copy = *this; + copy.position = pos; + return copy; + } + + Input withAvailableSpace(Vec2Px space) const { + auto copy = *this; + copy.availableSpace = space; + return copy; + } + + Input withContainingBlock(Vec2Px block) const { + auto copy = *this; + copy.containingBlock = block; + return copy; + } + + Input withBreakpointTraverser(BreakpointTraverser bpt) const { + auto copy = *this; + copy.breakpointTraverser = bpt; + return copy; + } + + Input addPendingVerticalSize(Px newPendingVerticalSize) const { + auto copy = *this; + copy.pendingVerticalSizes += newPendingVerticalSize; + return copy; + } +}; + +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 can finish rendering + bool completelyLaidOut; + + // only to be used in discovery mode + Opt breakpoint = NONE; + + static Output fromSize(Vec2Px size) { + return { + .size = size, + .completelyLaidOut = true + }; + } + + Px width() const { + return size.x; + } + + Px height() const { + return size.y; + } +}; + +} // namespace Vaev::Layout diff --git a/src/web/vaev-layout/layout.cpp b/src/web/vaev-layout/layout.cpp index 77103824633..cb000a63a4c 100644 --- a/src/web/vaev-layout/layout.cpp +++ b/src/web/vaev-layout/layout.cpp @@ -7,17 +7,34 @@ #include "inline.h" #include "positioned.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 image = box.content.is()) { return Output::fromSize(image->bound().size().cast()); } else if (auto run = box.content.is>()) { - return inlineLayout(tree, box, input); + auto out = inlineLayout(tree, box, input); + + if (not tree.fc.acceptsFit( + input.position.y, + out.size.y, + input.pendingVerticalSizes + )) { + return Output{ + .size = {}, + .completelyLaidOut = false, + .breakpoint = Breakpoint::buildOverflow() + }; + } else + return Output{ + .size = out.size, + .completelyLaidOut = true, + }; } else if ( display == Display::FLOW or display == Display::FLOW_ROOT or @@ -25,17 +42,17 @@ Output _contentLayout(Tree &tree, Box &box, Input input) { display == Display::TABLE_CAPTION or display == Display::TABLE ) { - 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) { return gridLayout(tree, box, input); } else if (display == Display::TABLE_BOX) { - return tableLayout(tree, box, input); + return tableLayout(tree, box, input, startAt, stopAt); } else if (display == Display::INTERNAL) { return Output{}; } else { - return blockLayout(tree, box, input); + return blockLayout(tree, box, input, startAt, stopAt); } } @@ -106,17 +123,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) { @@ -145,8 +162,41 @@ static Opt _computeSpecifiedSize(Tree &tree, Box &box, Size size, Vec2Px con } } +Res shouldAbortFragmentingBeforeLayout(FragmentationContext &fc, Input input) { + if (not fc.acceptsFit( + input.position.y, + 0_px, + input.pendingVerticalSizes + )) + return Output{ + .size = Vec2Px{0_px, 0_px}, + .completelyLaidOut = false, + .breakpoint = Breakpoint::buildOverflow() + }; + + return Ok(NONE); +} + +void maybeSetMonolithicBreakpoint(FragmentationContext &fc, bool isMonolticDisplay, bool childCompletelyLaidOut, usize boxChildrenLen, Opt &outputBreakpoint) { + if (not(fc.monolithicCount == 1 and isMonolticDisplay) or fc.hasInfiniteDimensions()) + return; + + if (not childCompletelyLaidOut) + panic("monolitic blocks should always be completly laid out"); + + // NOTE: wont abstract this since this is currently a workaround since we dont have fragmentation for table,flex + Breakpoint bottomOfContentBreakForTopMonolitic{ + .endIdx = boxChildrenLen, + .appeal = Breakpoint::Appeal::CLASS_B, + .advanceCase = Breakpoint::ADVANCE_CASE::ADVANCE_WITHOUT_CHILDREN + }; + + outputBreakpoint = bottomOfContentBreakForTopMonolitic; +} + Output layout(Tree &tree, Box &box, Input input) { - // FIXME: confirm how the preffered width/height parameters interacts with intrinsic size argument from input + + // 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; @@ -173,29 +223,97 @@ Output layout(Tree &tree, Box &box, Input input) { input.availableSpace.width = max(0_px, input.availableSpace.width - padding.horizontal() - borders.horizontal()); input.position = input.position + borders.topStart() + padding.topStart(); + input.pendingVerticalSizes += borders.bottom + padding.bottom; - auto [size] = _contentLayout(tree, box, input); + usize startAt = tree.fc.allowBreak() ? input.breakpointTraverser.getStart().unwrapOr(0) : 0; + if (tree.fc.isDiscoveryMode()) { + bool isMonolticDisplay = + box.style->display == Display::Inside::FLEX or + box.style->display == Display::Inside::GRID; - size.width = input.knownSize.width.unwrapOr(size.width); - size.height = input.knownSize.height.unwrapOr(size.height); + if (isMonolticDisplay) + tree.fc.enterMonolithicBox(); - size = size + padding.all() + borders.all(); + try$(shouldAbortFragmentingBeforeLayout(tree.fc, input)); - 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); - } + // TODO: Class C breakpoint + + auto out = _contentLayout(tree, box, input, startAt, NONE); + + // NOTE: assert since algo is still a bit experimental + if (not out.completelyLaidOut and out.breakpoint == NONE) + panic("if it was not completely laid out, there should be a breakpoint"); + + auto size = out.size; + size.width = input.knownSize.width.unwrapOr(size.width); + if (out.completelyLaidOut and not input.breakpointTraverser.prevIteration) { + size.height = input.knownSize.height.unwrapOr(size.height); + } + + // TODO: Class C breakpoint + + maybeSetMonolithicBreakpoint( + tree.fc, + isMonolticDisplay, + out.completelyLaidOut, + box.children().len(), + out.breakpoint + ); - return Output::fromSize(size); + out.size = size + padding.all() + borders.all(); + + if (isMonolticDisplay) + tree.fc.leaveMonolithicBox(); + + return out; + } else { + Opt stopAt = tree.fc.allowBreak() + ? input.breakpointTraverser.getEnd() + : NONE; + + auto parentFrag = input.fragment; + Frag currFrag(&box); + input.fragment = input.fragment ? &currFrag : nullptr; + + auto out = _contentLayout(tree, box, input, startAt, stopAt); + + auto size = out.size; + size.width = input.knownSize.width.unwrapOr(size.width); + if ((not tree.fc.allowBreak()) or (out.completelyLaidOut and not input.breakpointTraverser.prevIteration)) { + size.height = input.knownSize.height.unwrapOr(size.height); + } + + 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)); + } + + return Output{ + .size = size, + .completelyLaidOut = out.completelyLaidOut + }; + } } Output layout(Tree &tree, Input input) { auto out = layout(tree, tree.root, input); - layoutPositioned(tree, tree.root, input.containingBlock); + if (input.fragment) + layoutPositioned(tree, input.fragment->children[0], input.containingBlock); return out; } +Tuple layoutCreateFragment(Tree &tree, Input input) { + auto root = Layout::Frag(); + input.fragment = &root; + auto out = layout(tree, input); + return {out, std::move(root.children[0])}; +} + } // namespace Vaev::Layout diff --git a/src/web/vaev-layout/layout.h b/src/web/vaev-layout/layout.h index 7c0479dbfed..f9a9f389812 100644 --- a/src/web/vaev-layout/layout.h +++ b/src/web/vaev-layout/layout.h @@ -1,5 +1,7 @@ #pragma once +#include "input_output.h" + #include "base.h" namespace Vaev::Layout { @@ -13,5 +15,6 @@ Vec2Px computeIntrinsicSize(Tree &tree, Box &box, IntrinsicSize intrinsic, Vec2P Output layout(Tree &tree, Box &box, Input input); Output layout(Tree &tree, Input input); +Tuple layoutCreateFragment(Tree &tree, Input input); } // namespace Vaev::Layout diff --git a/src/web/vaev-layout/paint.cpp b/src/web/vaev-layout/paint.cpp index af2b09e5112..e20d0d60da0 100644 --- a/src/web/vaev-layout/paint.cpp +++ b/src/web/vaev-layout/paint.cpp @@ -2,22 +2,23 @@ #include #include +#include "frag.h" #include "paint.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; @@ -31,8 +32,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 &cssBackground = box.style->backgrounds; +static void _paintFrag(Frag &frag, Gfx::Color currentColor, Scene::Stack &stack) { + auto const &cssBackground = frag.style().backgrounds; Gfx::Borders borders; Vec backgrounds; @@ -40,40 +41,40 @@ static void _paintBox(Box &box, Gfx::Color currentColor, Scene::Stack &stack) { if (color.alpha != 0) backgrounds.pushBack(color); - bool hasBorders = _paintBorders(box, currentColor, borders); - Math::Rectf bound = box.layout.borderBox().cast(); + bool hasBorders = _paintBorders(frag, currentColor, borders); + Math::Rectf bound = frag.metrics.borderBox().cast(); if (any(backgrounds) or hasBorders) stack.add(makeStrong(bound, std::move(borders), std::move(backgrounds))); } -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; stack.add(makeStrong( - box.layout.borderBox().topStart().cast(), + frag.metrics.borderBox().topStart().cast(), *prose )); - } else if (auto image = box.content.is()) { + } else if (auto image = frag.box->content.is()) { stack.add(makeStrong( - box.layout.borderBox().cast(), + frag.metrics.borderBox().cast(), *image )); } } -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) { @@ -91,59 +92,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({ @@ -152,7 +153,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/web/vaev-layout/paint.h b/src/web/vaev-layout/paint.h index 922fe99488e..c22a915c9ed 100644 --- a/src/web/vaev-layout/paint.h +++ b/src/web/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/web/vaev-layout/positioned.cpp b/src/web/vaev-layout/positioned.cpp index 829d661441c..db3fa155cfc 100644 --- a/src/web/vaev-layout/positioned.cpp +++ b/src/web/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/web/vaev-layout/positioned.h b/src/web/vaev-layout/positioned.h index 2e9bd175341..992bc53a50a 100644 --- a/src/web/vaev-layout/positioned.h +++ b/src/web/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/web/vaev-layout/table.cpp b/src/web/vaev-layout/table.cpp index 8f9ff41cad8..a08d31d5036 100644 --- a/src/web/vaev-layout/table.cpp +++ b/src/web/vaev-layout/table.cpp @@ -74,10 +74,10 @@ struct TableGrid { template struct PrefixSum { - Vec pref; + Vec pref = {}; - PrefixSum(Vec const &v) : pref(v) { - for (usize i = 1; i < v.len(); ++i) + PrefixSum(Vec v = {}) : pref(v) { + for (usize i = 1; i < pref.len(); ++i) pref[i] = pref[i - 1] + pref[i]; } @@ -364,8 +364,7 @@ struct TableFormatingContext { panic("current element should be thead or tbody"); } - if (indexOfHeaderGroup and - indexOfHeaderGroup.unwrap() == (usize)(tableBoxCursor - tableBoxChildren.begin())) { + if (indexOfHeaderGroup == (usize)(tableBoxCursor - tableBoxChildren.begin())) { // table header was already processed in the beggining of the Rows section of the algorithm tableBoxCursor.next(); continue; @@ -803,11 +802,8 @@ struct TableFormatingContext { tree, *cell.box, { - .commit = Commit::NO, - .knownSize = { - colWidth[j], - NONE, - }, + .intrinsic = IntrinsicSize::MIN_CONTENT, + .knownSize = {colWidth[j], NONE}, } ); @@ -827,7 +823,7 @@ struct TableFormatingContext { } } - struct AxisHelper { + struct AxisHelper { // FIXME: find me a better name pls Opt groupIdx = NONE; Opt axisIdx = NONE; }; @@ -845,8 +841,11 @@ struct TableFormatingContext { return helper; }; - Vec2Px tableBoxSize; + Vec2Px tableBoxSize, headerSize = {}, footerSize = {}; Vec rowHelper, colHelper; + PrefixSum colWidthPref, rowHeightPref; + Math::Vec2u dataRowsInterval; + Vec startPositionOfRow; void build(Tree &tree, Input input) { buildHTMLTable(); @@ -871,73 +870,399 @@ struct TableFormatingContext { computeRowHeights(tree); + colWidthPref = PrefixSum{colWidth}; + rowHeightPref = PrefixSum{rowHeight}; + + dataRowsInterval = {numOfHeaderRows, grid.size.y - numOfFooterRows - 1}; + tableBoxSize = Vec2Px{ iter(colWidth).sum() + spacing.x * Px{grid.size.x + 1}, iter(rowHeight).sum() + spacing.y * Px{grid.size.y + 1}, }; + + if (numOfHeaderRows) { + headerSize = Vec2Px{ + tableBoxSize.x, + rowHeightPref.query(0, numOfHeaderRows - 1) + spacing.y * Px{numOfHeaderRows + 1}, + }; + } + + if (numOfFooterRows) { + footerSize = Vec2Px{ + tableBoxSize.x, + rowHeightPref.query(grid.size.y - numOfFooterRows, grid.size.y - 1) + + spacing.y * Px{numOfHeaderRows + 1}, + }; + } + + startPositionOfRow = Buf::init(grid.size.y, 0_px); } - void runTableBox(Tree &tree, Input input, Px &currPositionY) { - PrefixSum colWidthPref{colWidth}, rowHeightPref{rowHeight}; - Px currPositionX{input.position.x}; + Tuple layoutCell(Tree &tree, Input &input, TableCell &cell, MutCursor cellBox, usize startFrag, usize i, usize j, Px currPositionX, usize breakpointIndexOffset) { + + // breakpoint traversing for a cell that started in the previous fragmentainer is not trivial + // since it started in the previous fragmentainer, its breakpoint must be of type ADVANCE_WITH_CHILDREN and thus + // children info will be available at startFrag + // however, breakpoints set in the new iteration can be at i, in case of a cell with 3 or more rows (first row + // in prev frag, second row is startFrag, and third row is i); thus, we need to detach the traversing from the + // previous frag iteration from the current + BreakpointTraverser breakpointsForCell; + if (not input.breakpointTraverser.isDeactivated()) { + // in case of headers or footers, breakpoints would have been deactivated + if (startFrag < cell.anchorIdx.y) + breakpointsForCell = input.breakpointTraverser.traverseInsideUsingIthChildToJthParallelFlow(i - breakpointIndexOffset, j); + else + breakpointsForCell = BreakpointTraverser{ + input.breakpointTraverser.traversePrev(startFrag - breakpointIndexOffset, j), + input.breakpointTraverser.traverseCurr(i - breakpointIndexOffset, j), + }; + } - currPositionX += spacing.x; - currPositionY += spacing.y; - // cells - for (usize i = 0; i < grid.size.y; currPositionY += rowHeight[i] + spacing.y, i++) { - Px innnerCurrPositionX = Px{currPositionX}; - for (usize j = 0; j < grid.size.x; innnerCurrPositionX += colWidth[j] + spacing.x, j++) { - auto cell = grid.get(j, i); + // if the box started being rendered in the previous fragment, + // - its started position must be row starting the fragment + // - its size cant be the one computed in the build phase, thus is NONE + auto rowSpan = cell.box->attrs.rowSpan; + bool boxStartedInPrevFragment = tree.fc.allowBreak() and breakpointsForCell.prevIteration; + Px startPositionY = startPositionOfRow[boxStartedInPrevFragment ? startFrag : cell.anchorIdx.y]; + + Opt verticalSize; + if (not boxStartedInPrevFragment) { + verticalSize = + rowHeightPref.query(cell.anchorIdx.y, cell.anchorIdx.y + rowSpan - 1) + + spacing.y * Px{rowSpan - 1}; + } - if (cell.anchorIdx != Math::Vec2u{j, i}) - continue; + // TODO: In CSS 2.2, the height of a cell box is the minimum + // height required by the content. + // The table cell's 'height' property can influence + // the height of the row (see above), but it does not + // increase the height of the cell box. + // + // (See https://www.w3.org/TR/CSS22/tables.html#height-layout) + auto colSpan = cell.box->attrs.colSpan; + auto outputCell = layout( + tree, + *cell.box, + { + .fragment = input.fragment, + .knownSize = { + colWidthPref.query(j, j + colSpan - 1) + spacing.x * Px{colSpan - 1}, + verticalSize, + }, + .position = {currPositionX, startPositionY}, + .breakpointTraverser = breakpointsForCell, + .pendingVerticalSizes = input.pendingVerticalSizes, + } + ); - auto colSpan = cell.box->attrs.colSpan; - auto rowSpan = cell.box->attrs.rowSpan; + if (tree.fc.isDiscoveryMode()) { + if (cellBox->style->break_->inside == BreakInside::AVOID) { + outputCell.breakpoint->applyAvoid(); + } + } - // TODO: In CSS 2.2, the height of a cell box is the minimum - // height required by the content. - // The table cell's 'height' property can influence - // the height of the row (see above), but it does not - // increase the height of the cell box. - // - // (See https://www.w3.org/TR/CSS22/tables.html#height-layout) - layout( - tree, - *cell.box, - { - .commit = Commit::YES, - .knownSize = { - colWidthPref.query(j, j + colSpan - 1) + spacing.x * Px{colSpan - 1}, - rowHeightPref.query(i, i + rowSpan - 1) + spacing.y * Px{rowSpan - 1} - }, - .position = {innnerCurrPositionX, currPositionY}, - } - ); - }; + return { + outputCell, + startPositionY + outputCell.height() - startPositionOfRow[i] + }; + } + + struct RowOutput { + Px sizeY = 0_px; + + bool allBottomsAndCompletelyLaidOut = true; + bool someBottomsUncompleteLaidOut = false; + + Vec> breakpoints = {}; + Vec isBottom = {}; + }; + + RowOutput layoutRow(Tree &tree, Input input, usize startFrag, usize i, Vec2Px currPosition, bool isBreakpointedRow, usize breakpointIndexOffset = 0) { + startPositionOfRow[i] = currPosition.y; + + RowOutput outputRow; + if (tree.fc.isDiscoveryMode()) { + outputRow.breakpoints = Buf>::init(grid.size.x, NONE); + outputRow.isBottom = Buf::init(grid.size.x, false); + } + + currPosition.x += spacing.x; + for (usize j = 0; j < grid.size.x; currPosition.x += colWidth[j] + spacing.x, j++) { + auto cell = grid.get(j, i); + auto cellBox = grid.get(cell.anchorIdx.x, cell.anchorIdx.y).box; + + if (cell.anchorIdx.x != j) + continue; + + bool isBottomCell = cell.anchorIdx.y + cellBox->attrs.rowSpan - 1 == i; + + if (not tree.fc.isDiscoveryMode() and not(isBottomCell or isBreakpointedRow)) { + continue; + } + + auto [outputCell, cellHeight] = layoutCell(tree, input, cell, cellBox, startFrag, i, j, currPosition.x, breakpointIndexOffset); + + if (tree.fc.isDiscoveryMode()) { + if (isBottomCell) + outputRow.sizeY = max(outputRow.sizeY, cellHeight); + + outputRow.breakpoints[j] = outputCell.breakpoint; + outputRow.isBottom[j] = isBottomCell; + outputRow.allBottomsAndCompletelyLaidOut &= isBottomCell and outputCell.completelyLaidOut; + } else { + outputRow.sizeY = max(outputRow.sizeY, cellHeight); + } + outputRow.someBottomsUncompleteLaidOut |= isBottomCell and not outputCell.completelyLaidOut; + }; + + return outputRow; + } + + // https://www.w3.org/TR/css-tables-3/#freely-fragmentable + bool isFreelyFragmentableRow(usize i, Vec2Px fragmentainerSize) { + /* + NOT freely fragmentable if (AND): + - if the cells spanning the row do not span any subsequent row + - all cells are row span = 1 + - their height is at least twice smaller than both the fragmentainer height and width + - height <= min(frag.height, frag.width) / 2 + + it is freely fragmentable if at least one cell has row span > 1 OR height > min(frag.height, frag.width) / 2 + */ + + bool isSelfContainedRow = true; + for (usize j = 0; j < grid.size.x; ++j) { + if (grid.get(j, i).anchorIdx != Math::Vec2u{j, i} or grid.get(j, i).box->attrs.rowSpan != 1) { + isSelfContainedRow = false; + break; + } } + + return not isSelfContainedRow or + rowHeight[i] * 2_px > min(fragmentainerSize.x, fragmentainerSize.y); } - Output run(Tree &tree, Input input) { - Px currPositionY{input.position.y}; - if (input.commit == Commit::YES) { - runTableBox(tree, input, currPositionY); + bool handlePossibleForcedBreakpointAfterRow(Breakpoint ¤tBreakpoint, bool allBottomsAndCompletelyLaidOut, bool isLastRow, usize i) { + if (not allBottomsAndCompletelyLaidOut or isLastRow) + return true; + + // if row is self contained, the it belongs to has size 1 + bool forcedBreakAfterCurrRow = + rowHelper[i].axisIdx and + rows[rowHelper[i].axisIdx.unwrap()].el.style->break_->after == BreakBetween::PAGE; + + bool forcedBreakBeforeNextRow = + i + 1 <= dataRowsInterval.y and + rowHelper[i + 1].axisIdx and + rows[rowHelper[i + 1].axisIdx.unwrap()].el.style->break_->before == BreakBetween::PAGE; + + bool limitOfCurrRowGroup = i + 1 <= dataRowsInterval.y and rowHelper[i].groupIdx != rowHelper[i + 1].groupIdx; + + bool forcedBreakAfterCurrRowGroup = + limitOfCurrRowGroup and + rowHelper[i].groupIdx and + rowGroups[rowHelper[i].groupIdx.unwrap()].el.style->break_->after == BreakBetween::PAGE; + + bool forcedBreakBeforeNextRowGroup = + limitOfCurrRowGroup and + i + 1 <= dataRowsInterval.y and + rowHelper[i + 1].groupIdx and + rowGroups[rowHelper[i + 1].groupIdx.unwrap()].el.style->break_->before == BreakBetween::PAGE; + + if (forcedBreakAfterCurrRow or forcedBreakBeforeNextRow or + forcedBreakAfterCurrRowGroup or forcedBreakBeforeNextRowGroup) { + currentBreakpoint = Breakpoint::buildForced(i + 1); + return false; } - return Output::fromSize({ - tableUsedWidth, - tableBoxSize.y, - }); + return true; + } + + bool handleUnforcedBreakpointsInsideAndAfterRow(Breakpoint ¤tBreakpoint, RowOutput outputRow, usize i, Vec2Px fragmentainerSize) { + bool rowIsFreelyFragmentable = isFreelyFragmentableRow(i, fragmentainerSize); + + bool avoidBreakInsideTable = tableBox.style->break_->inside == BreakInside::AVOID; + + bool avoidBreakInsideRow = + rowHelper[i].axisIdx and + rows[rowHelper[i].axisIdx.unwrap()].el.style->break_->inside == BreakInside::AVOID; + + bool avoidBreakInsideRowGroup = + rowHelper[i].groupIdx and + rowGroups[rowHelper[i].groupIdx.unwrap()].el.style->break_->inside == BreakInside::AVOID; + + if (rowIsFreelyFragmentable) { + // breakpoint inside of row, take in consideration ALL breakpoints + // should stay in this row next fragmentation + currentBreakpoint.overrideIfBetter(Breakpoint::buildFromChildren( + outputRow.breakpoints, + i + 1, + avoidBreakInsideRow or avoidBreakInsideRowGroup or avoidBreakInsideTable, + false + )); + } + + // we need to abort layout if we cannot fit cells on their last row + if (outputRow.someBottomsUncompleteLaidOut) + return false; + + if (not outputRow.allBottomsAndCompletelyLaidOut) { + // breakpoint outside of row, but taking into consideration ONLY breakpoints of cells which are not + // in their bottom row + // since someBottomsUncompleteLaidOut is False, all bottom cells were able to be completed and their + // computed breakpoints can be disregarded + for (usize j = 0; j < grid.size.x; j++) { + if (outputRow.isBottom[j]) + outputRow.breakpoints[j] = NONE; + } + + currentBreakpoint.overrideIfBetter(Breakpoint::buildFromChildren( + outputRow.breakpoints, + i + 1, + avoidBreakInsideRow or avoidBreakInsideRowGroup or avoidBreakInsideTable, + true + )); + + } else { + + // no cells are being split + currentBreakpoint.overrideIfBetter(Breakpoint::buildClassB( + i + 1, + avoidBreakInsideTable + )); + } + + return true; + } + + Tuple> layoutRows(Tree &tree, Input input, usize startAt, usize stopAt, Px currPositionX, Px &currPositionY, bool shouldRepeatHeaderAndFooter) { + bool completelyLaidOut = false; + Opt rowBreakpoint = NONE; + + if (tree.fc.isDiscoveryMode()) { + completelyLaidOut = true; + rowBreakpoint = Breakpoint(); + + if (shouldRepeatHeaderAndFooter) + input = input.addPendingVerticalSize(footerSize.y); + } + + for (usize i = startAt; i < stopAt; i++) { + auto rowOutput = layoutRow( + tree, input, startAt, + i, + Vec2Px{currPositionX, currPositionY}, + i + 1 == stopAt, + shouldRepeatHeaderAndFooter ? dataRowsInterval.x : 0 + ); + + if (tree.fc.isDiscoveryMode()) { + if ( + not handleUnforcedBreakpointsInsideAndAfterRow(rowBreakpoint.unwrap(), rowOutput, i, tree.fc.currSize) or + not handlePossibleForcedBreakpointAfterRow(rowBreakpoint.unwrap(), rowOutput.allBottomsAndCompletelyLaidOut, (i + 1 == stopAt), i) + ) { + completelyLaidOut = false; + break; + } + } else if (i == (shouldRepeatHeaderAndFooter ? dataRowsInterval.y : grid.size.y - 1)) { + completelyLaidOut = not rowOutput.someBottomsUncompleteLaidOut; + } + + currPositionY += rowOutput.sizeY + spacing.y; + } + return {completelyLaidOut, rowBreakpoint}; + } + + void layoutHeaderFooterRows(Tree &tree, Input input, usize startFrag, Px currPositionX, Px &currPositionY, usize start, usize len) { + for (usize i = 0; i < len; i++) { + auto _ = layoutRow( + tree, + input.withBreakpointTraverser(BreakpointTraverser()), + startFrag, start + i, + Vec2Px{currPositionX, currPositionY}, false + ); + currPositionY += rowHeight[i] + spacing.y; + } + } + + Tuple computeLayoutIntervals(Tree &tree, bool shouldRepeatHeaderAndFooter, usize startAtTable, Opt stopAtTable) { + usize startAt = startAtTable + (shouldRepeatHeaderAndFooter ? dataRowsInterval.x : 0); + usize stopAt; + if (tree.fc.isDiscoveryMode()) { + stopAt = shouldRepeatHeaderAndFooter + ? dataRowsInterval.y + 1 + : grid.size.y; + } else { + stopAt = shouldRepeatHeaderAndFooter + ? dataRowsInterval.x + stopAtTable.unwrapOr(dataRowsInterval.y - dataRowsInterval.x + 1) + : stopAtTable.unwrapOr(grid.size.y); + } + + return {startAt, stopAt}; + } + + Output run(Tree &tree, Input input, usize startAtTable, Opt stopAtTable) { + + // TODO: in every row, at least one cell must be an anchor, or else this row is 'skipable' + + // if shouldRepeatHeaderAndFooter, header and footer are never alone in the fragmentainer and we wont set + // breakpoints on them; + // otherwise, they only appear once, might be alone in the fragmentainer and can be broken into pages + bool shouldRepeatHeaderAndFooter = + tree.fc.allowBreak() and + max(headerSize.y, footerSize.y) * 4_px <= tree.fc.currSize.y and + headerSize.y + footerSize.y * 2_px <= tree.fc.currSize.y; + + Px currPositionX{input.position.x}, currPositionY{input.position.y}; + Px startingPositionY = currPositionY; + currPositionY += spacing.y; + + auto [startAt, stopAt] = computeLayoutIntervals( + tree, shouldRepeatHeaderAndFooter, startAtTable, stopAtTable + ); + + if (shouldRepeatHeaderAndFooter) + layoutHeaderFooterRows( + tree, input, + startAt, + currPositionX, currPositionY, + 0, numOfHeaderRows + ); + + auto [completelyLaidOut, breakpoint] = layoutRows( + tree, input, + startAt, stopAt, + currPositionX, currPositionY, + shouldRepeatHeaderAndFooter + ); + + if (tree.fc.isDiscoveryMode() and shouldRepeatHeaderAndFooter) { + breakpoint.unwrap().endIdx -= dataRowsInterval.x; + } + + if (shouldRepeatHeaderAndFooter) + layoutHeaderFooterRows( + tree, input, + startAt, + currPositionX, currPositionY, + grid.size.y - numOfFooterRows, numOfFooterRows + ); + + return Output{ + .size = {tableUsedWidth, currPositionY - startingPositionY}, + .completelyLaidOut = completelyLaidOut, + .breakpoint = tree.fc.isDiscoveryMode() ? Opt{breakpoint} : NONE, + }; } }; -Output tableLayout(Tree &tree, Box &box, Input input) { +Output tableLayout(Tree &tree, Box &box, Input input, usize startAt, Opt stopAt) { // TODO: - vertical and horizontal alignment // - borders collapse TableFormatingContext table(tree, box); table.build(tree, input); - return table.run(tree, input); + return table.run(tree, input, startAt, stopAt); } } // namespace Vaev::Layout diff --git a/src/web/vaev-layout/table.h b/src/web/vaev-layout/table.h index 6044525cdda..437d4ef5cb5 100644 --- a/src/web/vaev-layout/table.h +++ b/src/web/vaev-layout/table.h @@ -1,9 +1,11 @@ #pragma once -#include "base.h" +#include "input_output.h" + +#include "tree.h" namespace Vaev::Layout { -Output tableLayout(Tree &tree, Box &box, Input input); +Output tableLayout(Tree &tree, Box &box, Input input, usize startAt, Opt stopAt); } // namespace Vaev::Layout diff --git a/src/web/vaev-layout/tree.h b/src/web/vaev-layout/tree.h new file mode 100644 index 00000000000..d362a0b9def --- /dev/null +++ b/src/web/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/web/vaev-layout/values.cpp b/src/web/vaev-layout/values.cpp index 1b876caaf13..4e04d40164d 100644 --- a/src/web/vaev-layout/values.cpp +++ b/src/web/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/web/vaev-markup/dom.cpp b/src/web/vaev-markup/dom.cpp new file mode 100644 index 00000000000..ccfc1ef186a --- /dev/null +++ b/src/web/vaev-markup/dom.cpp @@ -0,0 +1,21 @@ +#include "dom.h" + +namespace Vaev::Markup { + +// MARK: Document -------------------------------------------------------------- + +String Document::title() const { + String res = ""s; + iterDepthFirst([&](auto &node) { + if (auto element = node.template is()) { + if (element->tagName == Html::TITLE) { + res = element->textContent(); + return Iter::BREAK; + } + } + return Iter::CONTINUE; + }); + return res; +} + +} // namespace Vaev::Markup diff --git a/src/web/vaev-markup/dom.h b/src/web/vaev-markup/dom.h index 92406c4b325..6f03bd6b455 100644 --- a/src/web/vaev-markup/dom.h +++ b/src/web/vaev-markup/dom.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "tags.h" @@ -30,6 +31,11 @@ enum struct NodeType { _LEN, }; +enum struct Iter { + CONTINUE, + BREAK, +}; + // https://dom.spec.whatwg.org/#interface-node struct Node : Meta::Static { @@ -140,6 +146,20 @@ struct Node : return _parentIndex() < parentNode()._children.len() - 1; } + // MARK: Iter + + Iter iterDepthFirst(this auto &self, auto f) { + if (f(self) == Iter::BREAK) + return Iter::BREAK; + for (auto &child : self._children) { + if (child->iterDepthFirst(f) == Iter::BREAK) + return Iter::BREAK; + } + return Iter::CONTINUE; + } + + // MARK: Repr + virtual void _repr(Io::Emit &) const {} void repr(Io::Emit &e) const { @@ -178,11 +198,22 @@ enum struct QuirkMode { struct Document : public Node { static constexpr auto TYPE = NodeType::DOCUMENT; + Mime::Url _url; QuirkMode quirkMode{QuirkMode::NO}; + Document(Mime::Url url) + : _url(url) { + } + NodeType nodeType() const override { return TYPE; } + + String title() const; + + Mime::Url const &url() const { + return _url; + } }; // MARK: DocumentType ---------------------------------------------------------- @@ -298,7 +329,7 @@ struct TokenList { } void add(Str token) { - if (not ::contains(_tokens, token)) + if (not::contains(_tokens, token)) _tokens.pushBack(token); } @@ -316,7 +347,7 @@ struct TokenList { } bool replace(Str oldToken, Str newToken) { - if (not ::contains(_tokens, oldToken)) + if (not::contains(_tokens, oldToken)) return false; _tokens.removeAll(oldToken); _tokens.pushBack(newToken); @@ -356,9 +387,8 @@ struct Element : public Node { panic("textContent is not implemented for elements with multiple children"); auto const &child = *_children[0]; - if (auto text = child.is()) { + if (auto text = child.is()) return text->data; - } panic("textContent is not implemented for elements with children other than text nodes"); } diff --git a/src/web/vaev-markup/xml.cpp b/src/web/vaev-markup/xml.cpp index b1bfb082e8f..73a1bf84b6e 100644 --- a/src/web/vaev-markup/xml.cpp +++ b/src/web/vaev-markup/xml.cpp @@ -8,16 +8,14 @@ namespace Vaev::Markup { // 2 MARK: Documents // https://www.w3.org/TR/xml/#sec-documents -Res> XmlParser::parse(Io::SScan &s, Ns ns) { +Res<> XmlParser::parse(Io::SScan &s, Ns ns, Markup::Document &doc) { // document :: = prolog element Misc * - auto doc = makeStrong(); - try$(_parseProlog(s, *doc)); - doc->appendChild(try$(_parseElement(s, ns))); - while (_parseMisc(s, *doc)) + try$(_parseProlog(s, doc)); + doc.appendChild(try$(_parseElement(s, ns))); + while (_parseMisc(s, doc)) ; - - return Ok(doc); + return Ok(); } // 2.2 MARK: Characters diff --git a/src/web/vaev-markup/xml.h b/src/web/vaev-markup/xml.h index 4dbc1d0958e..b5dec78ccab 100644 --- a/src/web/vaev-markup/xml.h +++ b/src/web/vaev-markup/xml.h @@ -5,7 +5,7 @@ namespace Vaev::Markup { struct XmlParser { - Res> parse(Io::SScan &s, Ns ns); + Res<> parse(Io::SScan &s, Ns ns, Markup::Document &doc); Res<> _parseS(Io::SScan &s); diff --git a/src/web/vaev-style/computed.h b/src/web/vaev-style/computed.h index bb9e4bfc155..c0de63cef92 100644 --- a/src/web/vaev-style/computed.h +++ b/src/web/vaev-style/computed.h @@ -111,6 +111,17 @@ struct Computed { e(" variables: {}", variables); e(")"); } + + void setCustomProp(Str varName, Css::Content value) { + variables.cow().put(varName, value); + } + + Css::Content getCustomProp(Str varName) const { + auto value = variables->access(varName); + if (value) + return *value; + return {}; + } }; } // namespace Vaev::Style diff --git a/src/web/vaev-style/computer.cpp b/src/web/vaev-style/computer.cpp index 679592f600d..609469e97d4 100644 --- a/src/web/vaev-style/computer.cpp +++ b/src/web/vaev-style/computer.cpp @@ -111,8 +111,8 @@ Strong Computer::computeFor(Computed const &parent, Markup::Element co return _evalCascade(parent, matchingRules); } -Strong Computer::computeFor(Page const &page) { - auto computed = makeStrong(); +Strong Computer::computeFor(Computed const &parent, Page const &page) { + auto computed = makeStrong(parent); for (auto const &sheet : _styleBook.styleSheets) for (auto const &rule : sheet.rules) diff --git a/src/web/vaev-style/computer.h b/src/web/vaev-style/computer.h index 83e04496e44..4c5d2855868 100644 --- a/src/web/vaev-style/computer.h +++ b/src/web/vaev-style/computer.h @@ -21,7 +21,7 @@ struct Computer { Strong computeFor(Computed const &parent, Markup::Element const &el); - Strong computeFor(Page const &page); + Strong computeFor(Computed const &parent, Page const &page); }; } // namespace Vaev::Style diff --git a/src/web/vaev-style/media.h b/src/web/vaev-style/media.h index 3cd0d6bfcb9..1b885f8da97 100644 --- a/src/web/vaev-style/media.h +++ b/src/web/vaev-style/media.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -32,7 +33,7 @@ struct Media { /// 4.4. Orientation: the orientation feature /// https://drafts.csswg.org/mediaqueries/#orientation - Orientation orientation; + Print::Orientation orientation; // 5. MARK: Display Quality Media Features @@ -307,7 +308,7 @@ using AspectRatioFeature = RangeFeature<"aspect-ratio", Number, &Media::aspectRa /// 4.4. Device Height: the device-height feature /// https://drafts.csswg.org/mediaqueries/#orientation -using OrientationFeature = DiscreteFeature<"orientation", Orientation, &Media::orientation>; +using OrientationFeature = DiscreteFeature<"orientation", Print::Orientation, &Media::orientation>; // 5. MARK: Display Quality Media Features diff --git a/src/web/vaev-style/page.h b/src/web/vaev-style/page.h index 24928260602..241bb0a7c89 100644 --- a/src/web/vaev-style/page.h +++ b/src/web/vaev-style/page.h @@ -43,15 +43,19 @@ struct Page { }; struct PageComputedStyle { - Strong style = makeStrong(Computed::initial()); - Array, static_cast(PageArea::_LEN)> _areas = { -#define ITER(...) makeStrong(Computed::initial()), - FOREACH_PAGE_AREA(ITER) -#undef ITER - }; + using Areas = Array, toUnderlyingType(PageArea::_LEN)>; + + Strong style; + Areas _areas; + + PageComputedStyle(Computed const &initial) + : style(makeStrong(initial)), + _areas(Areas::fill([&](...) { + return makeStrong(initial); + })) {} Strong area(PageArea margin) const { - return _areas[static_cast(margin)]; + return _areas[toUnderlyingType(margin)]; } }; diff --git a/src/web/vaev-style/rules.cpp b/src/web/vaev-style/rules.cpp index be1132cd26a..54defa257f8 100644 --- a/src/web/vaev-style/rules.cpp +++ b/src/web/vaev-style/rules.cpp @@ -42,7 +42,13 @@ StyleRule StyleRule::parse(Css::Sst const &sst, Origin origin) { // Parse the selector. auto &prefix = sst.prefix.unwrap(); Cursor prefixContent = prefix->content; - res.selector = Selector::parse(prefixContent); + auto maybeSelector = Selector::parse(prefixContent); + if (maybeSelector) { + res.selector = maybeSelector.take(); + } else { + logWarn("failed to parse selector: {}: {}", prefix->content, maybeSelector); + res.selector = EmptySelector{}; + } // Parse the properties. for (auto const &item : sst.content) { diff --git a/src/web/vaev-style/select.cpp b/src/web/vaev-style/select.cpp index b8427316978..f5a2c22b7f9 100644 --- a/src/web/vaev-style/select.cpp +++ b/src/web/vaev-style/select.cpp @@ -3,7 +3,6 @@ #include "values.h" namespace Vaev::Style { - static constexpr bool DEBUG_SELECTORS = false; // MARK: Selector Specificity --------------------------------------------------- @@ -240,7 +239,6 @@ static bool _matchLastOfType(Markup::Element const &e) { static bool _match(Pseudo const &s, Markup::Element const &el) { switch (s.type) { - case Pseudo::LINK: return _matchLink(el); @@ -367,6 +365,7 @@ static Selector _parseAttributeSelector(Slice content) { }; } +// consume an Op Code static OpCode _peekOpCode(Cursor &cur) { if (cur.ended()) { return OpCode::NOP; @@ -440,12 +439,13 @@ static OpCode _peekOpCode(Cursor &cur) { } } -static Selector _parseInfixExpr(Selector lhs, Cursor &cur, OpCode opCode = OpCode::NOP); +static Res _parseInfixExpr(Selector lhs, Cursor &cur, OpCode opCode = OpCode::NOP); -static Selector _parseSelectorElement(Cursor &cur, OpCode currentOp) { +// consume a selector element (everything that has a lesser priority than the current OP) +static Res _parseSelectorElement(Cursor &cur, OpCode currentOp) { if (cur.ended()) { logErrorIf(DEBUG_SELECTORS, "ERROR : unterminated selector"); - return EmptySelector{}; + return Error::invalidData("unterminated selector"); } Selector val; @@ -474,13 +474,13 @@ static Selector _parseSelectorElement(Cursor &cur, OpCode currentOp) { cur.next(); if (cur.ended()) { logErrorIf(DEBUG_SELECTORS, "ERROR : unterminated selector"); - return EmptySelector{}; + return Error::invalidData("unterminated selector"); } } if (cur->prefix == Css::Token::function("not(")) { Cursor c = cur->content; - val = Selector::not_(_parseSelectorElement(c, OpCode::NOT)); + val = Selector::not_(try$(_parseSelectorElement(c, OpCode::NOT))); } else { val = Pseudo::make(cur->token.data); } @@ -492,7 +492,7 @@ static Selector _parseSelectorElement(Cursor &cur, OpCode currentOp) { } else if (cur->type == Css::Sst::BLOCK) { val = _parseAttributeSelector(cur->content); } else { - return EmptySelector{}; + return Error::invalidData("unexped sst node"); } cur.next(); @@ -500,16 +500,20 @@ static Selector _parseSelectorElement(Cursor &cur, OpCode currentOp) { Cursor rb = cur; OpCode nextOpCode = _peekOpCode(cur); if (nextOpCode > currentOp) { - val = _parseInfixExpr(val, cur, nextOpCode); + val = try$(_parseInfixExpr(val, cur, nextOpCode)); } else { cur = rb; } } - return val; + return Ok(val); } -static Selector _parseNfixExpr(Selector lhs, OpCode op, Cursor &cur) { - Vec selectors = {lhs, _parseSelectorElement(cur, op)}; +static Res _parseNfixExpr(Selector lhs, OpCode op, Cursor &cur) { + Vec selectors = { + lhs, + try$(_parseSelectorElement(cur, op)), + }; + // all the selectors between the op eg : a,b.B,c -> [a,b.B,c] while (not cur.ended()) { Cursor rollBack = cur; @@ -520,64 +524,71 @@ static Selector _parseNfixExpr(Selector lhs, OpCode op, Cursor &cur) { break; } else if (nextOpCode == op) { // adding the selector to the nfix - selectors.pushBack(_parseSelectorElement(cur, op)); + selectors.pushBack(try$(_parseSelectorElement(cur, op))); } else if (nextOpCode == OpCode::COLUMN or nextOpCode == OpCode::OR or nextOpCode == OpCode::AND) { // parse new nfix if (nextOpCode < op) { cur = rollBack; break; } - - last(selectors) = _parseNfixExpr(last(selectors), nextOpCode, cur); + last(selectors) = try$(_parseNfixExpr(last(selectors), nextOpCode, cur)); } else { - // parse new infix + // parse new infix if the next op is more important if (nextOpCode < op) { cur = rollBack; break; } - selectors.pushBack(_parseInfixExpr(_parseSelectorElement(cur, op), cur, nextOpCode)); + if (not(cur.rem() == 2 and cur.peek(1) == Css::Token::WHITESPACE)) [[likely]] { + last(selectors) = try$(_parseInfixExpr(last(selectors), cur, nextOpCode)); + + // auto const lhs = _parseSelectorElement(cur, op); + // selectors.pushBack(_parseInfixExpr(lhs, cur, nextOpCode)); + } else { + last(selectors) = try$(_parseInfixExpr(last(selectors), cur, nextOpCode)); + cur.next(); + } } } switch (op) { case OpCode::AND: - return Selector::and_(selectors); + return Ok(Selector::and_(selectors)); case OpCode::OR: - return Selector::or_(selectors); + return Ok(Selector::or_(selectors)); default: - return Selector::and_(selectors); + return Ok(Selector::and_(selectors)); } } -static Selector _parseInfixExpr(Selector lhs, Cursor &cur, OpCode opCode) { +static Res _parseInfixExpr(Selector lhs, Cursor &cur, OpCode opCode) { if (opCode == OpCode::NOP) opCode = _peekOpCode(cur); switch (opCode) { case OpCode::NOP: - return lhs; + return Ok(lhs); case OpCode::DESCENDANT: - return Selector::descendant(lhs, _parseSelectorElement(cur, opCode)); + return Ok(Selector::descendant(lhs, try$(_parseSelectorElement(cur, opCode)))); case OpCode::CHILD: - return Selector::child(lhs, _parseSelectorElement(cur, opCode)); + return Ok(Selector::child(lhs, try$(_parseSelectorElement(cur, opCode)))); case OpCode::ADJACENT: - return Selector::adjacent(lhs, _parseSelectorElement(cur, opCode)); + return Ok(Selector::adjacent(lhs, try$(_parseSelectorElement(cur, opCode)))); case OpCode::SUBSEQUENT: - return Selector::subsequent(lhs, _parseSelectorElement(cur, opCode)); + return Ok(Selector::subsequent(lhs, try$(_parseSelectorElement(cur, opCode)))); case OpCode::NOT: - return Selector::not_(_parseSelectorElement(cur, opCode)); + return Ok(Selector::not_(try$(_parseSelectorElement(cur, opCode)))); case OpCode::WHERE: - return Selector::where(_parseSelectorElement(cur, opCode)); + return Ok(Selector::where(try$(_parseSelectorElement(cur, opCode)))); case OpCode::COLUMN: case OpCode::OR: @@ -586,31 +597,29 @@ static Selector _parseInfixExpr(Selector lhs, Cursor &cur, OpCode opCo } } -Selector Selector::parse(Cursor &c) { +Res Selector::parse(Cursor &c) { if (!c) { - logWarn("empty selector"); - return EmptySelector{}; + return Error::invalidData("expected selector"); } logDebugIf(DEBUG_SELECTORS, "PARSING SELECTOR : {}", c); - Selector currentSelector = _parseSelectorElement(c, OpCode::NOP); + Selector currentSelector = try$(_parseSelectorElement(c, OpCode::NOP)); while (not c.ended()) { - currentSelector = _parseInfixExpr(currentSelector, c); + currentSelector = try$(_parseInfixExpr(currentSelector, c)); } - return currentSelector; + return Ok(currentSelector); } -Selector Selector::parse(Io::SScan &s) { +Res Selector::parse(Io::SScan &s) { Css::Lexer lex = s; auto val = consumeSelector(lex); Cursor c{val}; return parse(c); }; -Selector Selector::parse(Str input) { +Res Selector::parse(Str input) { Io::SScan s{input}; return parse(s); }; - } // namespace Vaev::Style diff --git a/src/web/vaev-style/select.h b/src/web/vaev-style/select.h index 4a55d97c99a..3720c6cb790 100644 --- a/src/web/vaev-style/select.h +++ b/src/web/vaev-style/select.h @@ -358,11 +358,11 @@ struct Selector : public _Selector { bool operator==(Selector const &) const = default; - static Selector parse(Cursor &c); + static Res parse(Cursor &c); - static Selector parse(Io::SScan &s); + static Res parse(Io::SScan &s); - static Selector parse(Str input); + static Res parse(Str input); }; inline bool Infix::operator==(Infix const &) const = default; diff --git a/src/web/vaev-style/styles.cpp b/src/web/vaev-style/styles.cpp index f25327d1b50..da7b1fc5d7a 100644 --- a/src/web/vaev-style/styles.cpp +++ b/src/web/vaev-style/styles.cpp @@ -69,7 +69,7 @@ void DeferredProp::apply(Computed const &parent, Computed &c) const { Css::Sst decl{Css::Sst::DECL}; decl.token = Css::Token::ident(propName); Cursor cursor = value; - _expandContent(cursor, c.variables.cow(), decl.content); + _expandContent(cursor, *c.variables, decl.content); // Parse the expanded content Res computed = parseDeclaration(decl, false); diff --git a/src/web/vaev-style/styles.h b/src/web/vaev-style/styles.h index bb34ecfd219..1ecbc6cab47 100644 --- a/src/web/vaev-style/styles.h +++ b/src/web/vaev-style/styles.h @@ -1219,6 +1219,62 @@ struct BorderSpacingProp { } }; +// MARK: Breaks ---------------------------------------------------------------- + +// https://www.w3.org/TR/css-break-3/#propdef-break-after +struct BreakAfterProp { + BreakBetween value = initial(); + + static constexpr Str name() { return "break-after"; } + + static constexpr BreakBetween initial() { return BreakBetween::AUTO; } + + void apply(Computed &c) const { + c.break_.cow().after = value; + } + + Res<> parse(Cursor &c) { + value = try$(parseValue(c)); + return Ok(); + } +}; + +// https://www.w3.org/TR/css-break-3/#propdef-break-before +struct BreakBeforeProp { + BreakBetween value = initial(); + + static constexpr Str name() { return "break-before"; } + + static constexpr BreakBetween initial() { return BreakBetween::AUTO; } + + void apply(Computed &c) const { + c.break_.cow().before = value; + } + + Res<> parse(Cursor &c) { + value = try$(parseValue(c)); + return Ok(); + } +}; + +// https://www.w3.org/TR/css-break-3/#break-within +struct BreakInsideProp { + BreakInside value = initial(); + + static constexpr Str name() { return "break-inside"; } + + static constexpr BreakInside initial() { return BreakInside::AUTO; } + + void apply(Computed &c) const { + c.break_.cow().inside = value; + } + + Res<> parse(Cursor &c) { + value = try$(parseValue(c)); + return Ok(); + } +}; + // MARK: Flex ------------------------------------------------------------------ // https://www.w3.org/TR/css-flexbox-1/#flex-basis-property @@ -2642,7 +2698,7 @@ struct CustomProp { static constexpr Str name() { return "custom prop"; } void apply(Computed &c) const { - c.variables.cow().put(varName, value); + c.setCustomProp(varName, value); } void repr(Io::Emit &e) const { @@ -2760,6 +2816,11 @@ using _StyleProp = Union< // Content ContentProp, + // Breaks + BreakAfterProp, + BreakBeforeProp, + BreakInsideProp, + // Flex FlexBasisProp, FlexDirectionProp, diff --git a/src/web/vaev-style/tests/test-media-features.cpp b/src/web/vaev-style/tests/test-media-features.cpp index f1cac969a70..d62cbb05651 100644 --- a/src/web/vaev-style/tests/test-media-features.cpp +++ b/src/web/vaev-style/tests/test-media-features.cpp @@ -8,7 +8,7 @@ static Media const TEST_MEDIA = { .width = 1920_px, .height = 1080_px, .aspectRatio = 16.0 / 9.0, - .orientation = Orientation::LANDSCAPE, + .orientation = Print::Orientation::LANDSCAPE, .resolution = Resolution::fromDpi(96), .scan = Scan::PROGRESSIVE, @@ -81,7 +81,7 @@ test$("feature-aspect-ratio") { } test$("feature-orientation") { - expect$(OrientationFeature{Orientation::LANDSCAPE} + expect$(OrientationFeature{Print::Orientation::LANDSCAPE} .match(TEST_MEDIA)); return Ok(); diff --git a/src/web/vaev-style/tests/test-media-query.cpp b/src/web/vaev-style/tests/test-media-query.cpp index 2434287934b..1ee55c5a451 100644 --- a/src/web/vaev-style/tests/test-media-query.cpp +++ b/src/web/vaev-style/tests/test-media-query.cpp @@ -8,7 +8,7 @@ static Media const TEST_MEDIA = { .width = Px(1920), .height = Px(1080), .aspectRatio = 16.0 / 9.0, - .orientation = Orientation::LANDSCAPE, + .orientation = Print::Orientation::LANDSCAPE, .resolution = Resolution::fromDpi(96), .scan = Scan::PROGRESSIVE, diff --git a/src/web/vaev-style/tests/test-parse-selectors.cpp b/src/web/vaev-style/tests/test-parse-selectors.cpp index e3c3a2967b3..48e4c8792fc 100644 --- a/src/web/vaev-style/tests/test-parse-selectors.cpp +++ b/src/web/vaev-style/tests/test-parse-selectors.cpp @@ -2,40 +2,38 @@ #include namespace Vaev::Style::Tests { - test$("vaev-style-parse-simple-selectors") { - expectEq$( - Selector::parse(""), - EmptySelector{} + expect$( + not Selector::parse("").has() ); expectEq$( - Selector::parse("html"), + try$(Selector::parse("html")), TypeSelector{Html::HTML} ); expectEq$( - Selector::parse("html "), + try$(Selector::parse("html ")), TypeSelector{Html::HTML} ); expectEq$( - Selector::parse(" html"), + try$(Selector::parse(" html")), TypeSelector{Html::HTML} ); expectEq$( - Selector::parse(".className"), + try$(Selector::parse(".className")), ClassSelector{"className"s} ); expectEq$( - Selector::parse("#idName"), + try$(Selector::parse("#idName")), IdSelector{"idName"s} ); expectEq$( - Selector::parse("*"), + try$(Selector::parse("*")), UniversalSelector{} ); @@ -44,7 +42,7 @@ test$("vaev-style-parse-simple-selectors") { test$("vaev-style-parse-nfix-selectors") { expectEq$( - Selector::parse("html,.className"), + try$(Selector::parse("html,.className")), Selector::or_({ TypeSelector{Html::HTML}, ClassSelector{"className"s}, @@ -52,7 +50,7 @@ test$("vaev-style-parse-nfix-selectors") { ); expectEq$( - Selector::parse("html,.className , \n #idName"), + try$(Selector::parse("html,.className , \n #idName")), Selector::or_({ TypeSelector{Html::HTML}, ClassSelector{"className"s}, @@ -61,7 +59,7 @@ test$("vaev-style-parse-nfix-selectors") { ); expectEq$( - Selector::parse("html,.className , \n #idName,*"), + try$(Selector::parse("html,.className , \n #idName,*")), Selector::or_({ TypeSelector{Html::HTML}, ClassSelector{"className"s}, @@ -71,7 +69,7 @@ test$("vaev-style-parse-nfix-selectors") { ); expectEq$( - Selector::parse("html.className"), + try$(Selector::parse("html.className")), Selector::and_({ TypeSelector{Html::HTML}, ClassSelector{"className"s}, @@ -83,7 +81,7 @@ test$("vaev-style-parse-nfix-selectors") { test$("vaev-style-parse-infix-selectors") { expectEq$( - Selector::parse("html .className"), + try$(Selector::parse("html .className")), Selector::descendant( TypeSelector{Html::HTML}, ClassSelector{"className"s} @@ -91,7 +89,7 @@ test$("vaev-style-parse-infix-selectors") { ); expectEq$( - Selector::parse("html>.className"), + try$(Selector::parse("html>.className")), Selector::child( TypeSelector{Html::HTML}, ClassSelector{"className"s} @@ -99,7 +97,7 @@ test$("vaev-style-parse-infix-selectors") { ); expectEq$( - Selector::parse("html > .className"), + try$(Selector::parse("html > .className")), Selector::child( TypeSelector{Html::HTML}, ClassSelector{"className"s} @@ -107,7 +105,7 @@ test$("vaev-style-parse-infix-selectors") { ); expectEq$( - Selector::parse("html > .className #idName"), + try$(Selector::parse("html > .className #idName")), Selector::descendant( Selector::child( TypeSelector{Html::HTML}, @@ -118,7 +116,7 @@ test$("vaev-style-parse-infix-selectors") { ); expectEq$( - Selector::parse(":not(.className)"), + try$(Selector::parse(":not(.className)")), Selector::not_(ClassSelector{"className"s}) ); @@ -127,7 +125,7 @@ test$("vaev-style-parse-infix-selectors") { test$("vaev-style-parse-adjacent-selectors") { expectEq$( - Selector::parse("html +.className"), + try$(Selector::parse("html +.className")), Selector::adjacent( TypeSelector{Html::HTML}, ClassSelector{"className"s} @@ -139,7 +137,7 @@ test$("vaev-style-parse-adjacent-selectors") { test$("vaev-style-parse-subsequent-selectors") { expectEq$( - Selector::parse("html~.className"), + try$(Selector::parse("html~.className")), Selector::subsequent( TypeSelector{Html::HTML}, ClassSelector{"className"s} @@ -150,10 +148,8 @@ test$("vaev-style-parse-subsequent-selectors") { } test$("vaev-style-parse-mixed-selectors") { - return Error::skipped(); - expectEq$( - Selector::parse("html > .className#idName"), + try$(Selector::parse("html > .className#idName")), Selector::child( TypeSelector{Html::HTML}, Selector::and_({ @@ -164,7 +160,7 @@ test$("vaev-style-parse-mixed-selectors") { ); expectEq$( - Selector::parse("html#idName .className"), + try$(Selector::parse("html#idName .className")), Selector::descendant( Selector::and_({ TypeSelector{Html::HTML}, @@ -175,12 +171,12 @@ test$("vaev-style-parse-mixed-selectors") { ); expectEq$( - Selector::parse(":not(:first-child)"), + try$(Selector::parse(":not(:first-child)")), Selector::not_(Pseudo{Pseudo::FIRST_CHILD}) ); expectEq$( - Selector::parse("tr:not(:last-child) th:not(:first-child)"), + try$(Selector::parse("tr:not(:last-child) th:not(:first-child)")), Selector::descendant( Selector::and_({TypeSelector{Html::TR}, Selector::not_(Pseudo{Pseudo::LAST_CHILD})}), Selector::and_({TypeSelector{Html::TH}, {Selector::not_(Pseudo{Pseudo::FIRST_CHILD})}}) @@ -188,7 +184,23 @@ test$("vaev-style-parse-mixed-selectors") { ); expectEq$( - Selector::parse(".o_content .o_table > thead > tr:not(:last-child) th:not(:first-child)"), + try$(Selector::parse("td, .o_content .o .o_table th ")), + Selector::or_( + {TypeSelector{Html::TD}, + Selector::descendant(Selector::descendant(Selector::descendant(ClassSelector{"o_content"s}, ClassSelector{"o"s}), ClassSelector{"o_table"s}), TypeSelector{Html::TH})} + ) + ); + + expectEq$( + try$(Selector::parse("td, .o_content .o_table th ")), + Selector::or_( + {TypeSelector{Html::TD}, + Selector::descendant(Selector::descendant(ClassSelector{"o_content"s}, ClassSelector{"o_table"s}), TypeSelector{Html::TH})} + ) + ); + + expectEq$( + try$(Selector::parse(".o_content .o_table > thead > tr:not(:last-child) th:not(:first-child)")), Selector::descendant( Selector::child( Selector::descendant( @@ -201,13 +213,6 @@ test$("vaev-style-parse-mixed-selectors") { ) ); - expectEq$( - Selector::parse(".o_content .o_table thead, .o_content .o_table tbody, .o_content .o_table tfoot, .o_content .o_table tr, .o_content .o_table td, .o_content .o_table th "), - Selector::descendant( - Selector::and_({TypeSelector{Html::TR}, Selector::not_(Pseudo{Pseudo::LAST_CHILD})}), - Selector::and_({TypeSelector{Html::TH}, {Selector::not_(Pseudo{Pseudo::FIRST_CHILD})}}) - ) - ); return Ok(); } @@ -218,27 +223,27 @@ test$("vaev-style-parse-pseudo-selectors") { ); expectEq$( - Selector::parse(":root"), + try$(Selector::parse(":root")), Pseudo{Pseudo::ROOT} ); expectEq$( - Selector::parse(":root"), + try$(Selector::parse(":root")), Pseudo{Pseudo::make("root")} ); expectEq$( - Selector::parse(":first-child"), + try$(Selector::parse(":first-child")), Pseudo{Pseudo::FIRST_CHILD} ); expectEq$( - Selector::parse(":last-child"), + try$(Selector::parse(":last-child")), Pseudo{Pseudo::LAST_CHILD} ); expectEq$( - Selector::parse(".class :last-child"), + try$(Selector::parse(".class :last-child")), Selector::descendant( ClassSelector{"class"s}, Pseudo{Pseudo::LAST_CHILD} @@ -246,7 +251,7 @@ test$("vaev-style-parse-pseudo-selectors") { ); expectEq$( - Selector::parse("html:hover"), + try$(Selector::parse("html:hover")), Selector::and_({ TypeSelector{Html::HTML}, Pseudo{Pseudo::HOVER}, @@ -256,7 +261,7 @@ test$("vaev-style-parse-pseudo-selectors") { // this should pass for legacy resons // https://www.w3.org/TR/selectors-3/#pseudo-elements expectEq$( - Selector::parse("html:after"), + try$(Selector::parse("html:after")), Selector::and_({ TypeSelector{Html::HTML}, Pseudo{Pseudo::AFTER}, @@ -264,7 +269,7 @@ test$("vaev-style-parse-pseudo-selectors") { ); expectEq$( - Selector::parse("html::after"), + try$(Selector::parse("html::after")), Selector::and_({ TypeSelector{Html::HTML}, Pseudo{Pseudo::AFTER}, @@ -276,7 +281,7 @@ test$("vaev-style-parse-pseudo-selectors") { test$("vaev-style-parse-attribute-selectors") { expectEq$( - Selector::parse(".className[type]"), + try$(Selector::parse(".className[type]")), Selector::and_({ ClassSelector{"className"s}, AttributeSelector{ @@ -289,7 +294,7 @@ test$("vaev-style-parse-attribute-selectors") { ); expectEq$( - Selector::parse(".className[type='text']"), + try$(Selector::parse(".className[type='text']")), Selector::and_({ ClassSelector{"className"s}, AttributeSelector{ @@ -302,7 +307,7 @@ test$("vaev-style-parse-attribute-selectors") { ); expectEq$( - Selector::parse(".className[ type = 'text' ]"), + try$(Selector::parse(".className[ type = 'text' ]")), Selector::and_({ ClassSelector{"className"s}, AttributeSelector{ @@ -315,7 +320,7 @@ test$("vaev-style-parse-attribute-selectors") { ); expectEq$( - Selector::parse(".className[type*='text']"), + try$(Selector::parse(".className[type*='text']")), Selector::and_({ ClassSelector{"className"s}, AttributeSelector{ @@ -328,7 +333,7 @@ test$("vaev-style-parse-attribute-selectors") { ); expectEq$( - Selector::parse(".className[type='text' s]"), + try$(Selector::parse(".className[type='text' s]")), Selector::and_({ ClassSelector{"className"s}, AttributeSelector{ @@ -342,5 +347,4 @@ test$("vaev-style-parse-attribute-selectors") { return Ok(); } - } // namespace Vaev::Style::Tests diff --git a/src/web/vaev-style/values.cpp b/src/web/vaev-style/values.cpp index 5fbf41431f4..39badd13d93 100644 --- a/src/web/vaev-style/values.cpp +++ b/src/web/vaev-style/values.cpp @@ -175,6 +175,63 @@ Res ValueParser::parse(Cursor &c) { return Error::invalidData("expected border spacing value"); } +// MARK: BreakAfter & BreakBefore +// https://www.w3.org/TR/css-break-3/#propdef-break-after +// https://www.w3.org/TR/css-break-3/#propdef-break-before +Res ValueParser::parse(Cursor &c) { + if (c.ended()) + return Error::invalidData("unexpected end of input"); + + if (c.skip(Css::Token::ident("auto"))) { + return Ok(BreakBetween::AUTO); + } else if (c.skip(Css::Token::ident("avoid"))) { + return Ok(BreakBetween::AVOID); + } else if (c.skip(Css::Token::ident("avoid-page"))) { + return Ok(BreakBetween::AVOID_PAGE); + } else if (c.skip(Css::Token::ident("page"))) { + return Ok(BreakBetween::PAGE); + } else if (c.skip(Css::Token::ident("left"))) { + return Ok(BreakBetween::LEFT); + } else if (c.skip(Css::Token::ident("right"))) { + return Ok(BreakBetween::RIGHT); + } else if (c.skip(Css::Token::ident("recto"))) { + return Ok(BreakBetween::RECTO); + } else if (c.skip(Css::Token::ident("verso"))) { + return Ok(BreakBetween::VERSO); + } else if (c.skip(Css::Token::ident("avoid-column"))) { + return Ok(BreakBetween::AVOID_COLUMN); + } else if (c.skip(Css::Token::ident("column"))) { + return Ok(BreakBetween::COLUMN); + } else if (c.skip(Css::Token::ident("avoid-region"))) { + return Ok(BreakBetween::AVOID_REGION); + } else if (c.skip(Css::Token::ident("region"))) { + return Ok(BreakBetween::REGION); + } + + return Error::invalidData("expected break between value"); +} + +// MARK: BreakInside +// https://www.w3.org/TR/css-break-3/#break-within +Res ValueParser::parse(Cursor &c) { + if (c.ended()) + return Error::invalidData("unexpected end of input"); + + if (c.skip(Css::Token::ident("auto"))) { + return Ok(BreakInside::AUTO); + } else if (c.skip(Css::Token::ident("avoid"))) { + return Ok(BreakInside::AVOID); + } else if (c.skip(Css::Token::ident("avoid-page"))) { + return Ok(BreakInside::AVOID_PAGE); + } else if (c.skip(Css::Token::ident("avoid-column"))) { + return Ok(BreakInside::AVOID_COLUMN); + } else if (c.skip(Css::Token::ident("avoid-region"))) { + return Ok(BreakInside::AVOID_REGION); + } + + return Error::invalidData("expected break between value"); +} + // MARK: Color // https://drafts.csswg.org/css-color @@ -807,14 +864,14 @@ Res ValueParser::parse(Cursor &c) { // MARK: Orientation // https://drafts.csswg.org/mediaqueries/#orientation -Res ValueParser::parse(Cursor &c) { +Res ValueParser::parse(Cursor &c) { if (c.ended()) return Error::invalidData("unexpected end of input"); if (c.skip(Css::Token::ident("portrait"))) - return Ok(Orientation::PORTRAIT); + return Ok(Print::Orientation::PORTRAIT); else if (c.skip(Css::Token::ident("landscape"))) - return Ok(Orientation::LANDSCAPE); + return Ok(Print::Orientation::LANDSCAPE); else return Error::invalidData("expected orientation"); } diff --git a/src/web/vaev-style/values.h b/src/web/vaev-style/values.h index 1a7478d2486..dba244f3ab6 100644 --- a/src/web/vaev-style/values.h +++ b/src/web/vaev-style/values.h @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #include #include #include @@ -72,6 +74,16 @@ struct ValueParser { static Res parse(Cursor &c); }; +template <> +struct ValueParser { + static Res parse(Cursor &c); +}; + +template <> +struct ValueParser { + static Res parse(Cursor &c); +}; + template struct ValueParser> { static Res> parse(Cursor &c) { @@ -283,8 +295,8 @@ struct ValueParser { }; template <> -struct ValueParser { - static Res parse(Cursor &c); +struct ValueParser { + static Res parse(Cursor &c); }; template <> diff --git a/src/web/vaev-view/dialog.cpp b/src/web/vaev-view/dialog.cpp index 3ad03f2062d..f885b7da978 100644 --- a/src/web/vaev-view/dialog.cpp +++ b/src/web/vaev-view/dialog.cpp @@ -1,47 +1,13 @@ #include -#include +#include #include "dialog.h" namespace Vaev::View { -Style::Media _constructMedia(Print::Settings const &settings) { - return { - .type = MediaType::SCREEN, - .width = Px{settings.paper.width}, - .height = Px{settings.paper.height}, - .aspectRatio = settings.paper.width / (f64)settings.paper.height, - .orientation = Orientation::LANDSCAPE, - - .resolution = Resolution{settings.scale, Resolution::X}, - .scan = Scan::PROGRESSIVE, - .grid = false, - .update = Update::FAST, - - .overflowBlock = OverflowBlock::SCROLL, - .overflowInline = OverflowInline::SCROLL, - - .color = 8, - .colorIndex = 0, - .monochrome = 0, - .colorGamut = ColorGamut::SRGB, - .pointer = Pointer::FINE, - .hover = Hover::HOVER, - .anyPointer = Pointer::FINE, - .anyHover = Hover::HOVER, - - .prefersReducedMotion = ReducedMotion::NO_PREFERENCE, - .prefersReducedTransparency = ReducedTransparency::NO_PREFERENCE, - .prefersContrast = Contrast::NO_PREFERENCE, - .forcedColors = Colors::NONE, - .prefersColorScheme = ColorScheme::LIGHT, - .prefersReducedData = ReducedData::NO_PREFERENCE, - }; -} - Ui::Child printDialog(Strong dom) { return Kr::printDialog([dom](Print::Settings const &settings) { - return Driver::print(*dom, _constructMedia(settings)); + return Driver::print(*dom, settings); }); } diff --git a/src/web/vaev-view/view.cpp b/src/web/vaev-view/view.cpp index 23d00033d11..e13d4209493 100644 --- a/src/web/vaev-view/view.cpp +++ b/src/web/vaev-view/view.cpp @@ -18,7 +18,7 @@ struct View : public Ui::View { .width = Px{viewport.width}, .height = Px{viewport.height}, .aspectRatio = viewport.width / (f64)viewport.height, - .orientation = Orientation::LANDSCAPE, + .orientation = Print::Orientation::LANDSCAPE, .resolution = Resolution::fromDpi(96), .scan = Scan::PROGRESSIVE, @@ -63,12 +63,12 @@ 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, rect.offset(-bound().xy).cast()); if (Ui::debugShowLayoutBounds) { - Layout::wireframe(*layout, g); + Layout::wireframe(*frag, g); } g.pop(); @@ -82,11 +82,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, {.small = size.cast()}); + auto [_, layout, _, frag] = Driver::render(*_dom, media, {.small = size.cast()}); return { - layout->layout.borderBox().width.cast(), - layout->layout.borderBox().height.cast(), + frag->metrics.borderBox().width.cast(), + frag->metrics.borderBox().height.cast(), }; } };