diff --git a/src/libs/karm-base/rc.h b/src/libs/karm-base/rc.h index 2d1c702d..5e12bba3 100644 --- a/src/libs/karm-base/rc.h +++ b/src/libs/karm-base/rc.h @@ -242,7 +242,7 @@ struct _Rc { if (not Meta::Same and not Meta::Derive and - not _cell->id() == Meta::idOf()) { + not(_cell->id() == Meta::idOf())) { return nullptr; } diff --git a/src/libs/karm-gfx/canvas.cpp b/src/libs/karm-gfx/canvas.cpp index b9a4d966..70e2869f 100644 --- a/src/libs/karm-gfx/canvas.cpp +++ b/src/libs/karm-gfx/canvas.cpp @@ -128,10 +128,10 @@ void Canvas::fill(Text::Prose& prose) { if (cell.span and cell.span->color) { push(); fillStyle(*cell.span->color); - fill(prose._style.font, cell.glyph, Vec2Au{block.pos + cell.pos, line.baseline}.cast()); + fill(prose._style.font, cell.glyph(), Vec2Au{block.pos + cell.pos, line.baseline}.cast()); pop(); } else { - fill(prose._style.font, cell.glyph, Vec2Au{block.pos + cell.pos, line.baseline}.cast()); + fill(prose._style.font, cell.glyph(), Vec2Au{block.pos + cell.pos, line.baseline}.cast()); } } } diff --git a/src/libs/karm-pdf/canvas.cpp b/src/libs/karm-pdf/canvas.cpp index 4f2de81d..782008b1 100644 --- a/src/libs/karm-pdf/canvas.cpp +++ b/src/libs/karm-pdf/canvas.cpp @@ -141,7 +141,7 @@ void Canvas::fill(Text::Prose& prose) { _e("[<"); for (auto& block : line.blocks()) { for (auto& cell : block.cells()) { - auto glyphAdvance = prose._style.font.advance(cell.glyph); + auto glyphAdvance = prose._style.font.advance(cell.glyph()); auto nextEndPosWithoutKern = prevEndPos + glyphAdvance; auto nextDesiredEndPos = (block.pos + cell.pos + cell.adv).cast(); diff --git a/src/libs/karm-print/pdf-printer.h b/src/libs/karm-print/pdf-printer.h index 8176e139..d597cdba 100644 --- a/src/libs/karm-print/pdf-printer.h +++ b/src/libs/karm-print/pdf-printer.h @@ -45,6 +45,10 @@ struct PdfPrinter : public FilePrinter { for (auto& [_, value] : fontManager.mapping._els) { auto& [id, fontFace] = value; + if (not fontFace.is()) { + panic("no support for printing fonts other than TrueType"); + } + TrueTypeFontAdapter ttfAdapter{ fontFace.cast().unwrap(), alloc diff --git a/src/libs/karm-text/family.cpp b/src/libs/karm-text/family.cpp index f7ba4146..bd93dc72 100644 --- a/src/libs/karm-text/family.cpp +++ b/src/libs/karm-text/family.cpp @@ -71,6 +71,14 @@ FontMetrics FontFamily::metrics() const { return metrics; } +BaselineSet FontFamily::baselineSet() { + BaselineSet bs = {}; + + // FIXME + + return bs; +} + FontAttrs FontFamily::attrs() const { FontAttrs attrs; diff --git a/src/libs/karm-text/family.h b/src/libs/karm-text/family.h index bab57e8d..e354e081 100644 --- a/src/libs/karm-text/family.h +++ b/src/libs/karm-text/family.h @@ -50,6 +50,7 @@ struct FontFamily : public Fontface { static Builder make(FontBook const& book); FontMetrics metrics() const override; + BaselineSet baselineSet() override; FontAttrs attrs() const override; diff --git a/src/libs/karm-text/font.cpp b/src/libs/karm-text/font.cpp index d252c0e8..53080449 100644 --- a/src/libs/karm-text/font.cpp +++ b/src/libs/karm-text/font.cpp @@ -32,6 +32,17 @@ FontMetrics Font::metrics() const { return m; } +BaselineSet Font::baselineSet() { + auto bs = fontface->baselineSet(); + + bs.alphabetic *= fontsize; + bs.xHeight *= fontsize; + bs.xMiddle *= fontsize; + bs.capHeight *= fontsize; + + return bs; +} + Glyph Font::glyph(Rune rune) { return fontface->glyph(rune); } diff --git a/src/libs/karm-text/font.h b/src/libs/karm-text/font.h index b9e19e73..d17759af 100644 --- a/src/libs/karm-text/font.h +++ b/src/libs/karm-text/font.h @@ -46,6 +46,15 @@ struct FontMetrics { } }; +// https://drafts.csswg.org/css-align-3/#baseline-set +// https://www.w3.org/TR/css-inline-3/#baseline-types +struct BaselineSet { + f64 alphabetic; + f64 xHeight; + f64 xMiddle; + f64 capHeight; +}; + struct FontMeasure { Math::Rectf capbound; Math::Rectf linebound; @@ -59,6 +68,8 @@ struct Fontface { virtual FontMetrics metrics() const = 0; + virtual BaselineSet baselineSet() = 0; + virtual FontAttrs attrs() const = 0; virtual Glyph glyph(Rune rune) = 0; @@ -78,6 +89,7 @@ struct Font { static Font fallback(); FontMetrics metrics() const; + BaselineSet baselineSet(); Glyph glyph(Rune rune); diff --git a/src/libs/karm-text/prose.cpp b/src/libs/karm-text/prose.cpp index 6490bd4d..09d4df5f 100644 --- a/src/libs/karm-text/prose.cpp +++ b/src/libs/karm-text/prose.cpp @@ -20,25 +20,44 @@ void Prose::_beginBlock() { }); } -void Prose::append(Rune rune) { +void Prose::_append(Cell&& cell) { if (any(_blocks) and last(_blocks).newline()) _beginBlock(); if (any(_blocks) and last(_blocks).spaces()) _beginBlock(); - auto glyph = _style.font.glyph(rune == '\n' ? ' ' : rune); + _cells.pushBack(std::move(cell)); - _cells.pushBack({ + last(_blocks).cellRange.size++; + last(_blocks).runeRange.end(_runes.len()); +} + +void Prose::append(Rc strut) { + // FIXME: _append encasulates _cell but we are using its size to find the index + _structCellsIndexes.pushBack(_cells.len()); + _runes.pushBack(0); + _append({ .prose = this, .span = _currentSpan, .runeRange = {_runes.len(), 1}, - .glyph = glyph, + ._content = strut, }); +} + +void Prose::append(Rune rune) { + auto glyph = _style.font.glyph(rune == '\n' ? ' ' : rune); + + Cell cell{ + .prose = this, + .span = _currentSpan, + .runeRange = {_runes.len(), 1}, + ._content = makeRc(glyph), + }; _runes.pushBack(rune); - last(_blocks).cellRange.size++; - last(_blocks).runeRange.end(_runes.len()); + + _append(std::move(cell)); } void Prose::clear() { @@ -46,8 +65,28 @@ void Prose::clear() { _cells.clear(); _blocks.clear(); _blocksMeasured = false; - _beginBlock(); _lines.clear(); + + _beginBlock(); +} + +void Prose::copySpanStack(Prose const& prose) { + Vec> spans; + auto currSpan = prose._currentSpan; + while (currSpan) { + spans.pushBack(*currSpan); + currSpan = currSpan->parent; + } + + reverse(mutSub(spans)); + _spans = std::move(spans); + + for (usize i = 0; i + 1 < _spans.len(); ++i) { + _spans[i + 1]->parent = &*_spans[i]; + } + + if (_spans.len()) + _currentSpan = &*last(_spans); } void Prose::append(Slice runes) { @@ -59,26 +98,6 @@ void Prose::append(Slice runes) { // MARK: Layout ------------------------------------------------------------- -void Prose::_measureBlocks() { - for (auto& block : _blocks) { - auto adv = 0_au; - bool first = true; - Glyph prev = Glyph::TOFU; - for (auto& cell : block.cells()) { - if (not first) - adv += Au{_style.font.kern(prev, cell.glyph)}; - else - first = false; - - cell.pos = adv; - cell.adv = Au{_style.font.advance(cell.glyph)}; - adv += cell.adv; - prev = cell.glyph; - } - block.width = adv; - } -} - void Prose::_wrapLines(Au width) { _lines.clear(); @@ -170,22 +189,4 @@ Au Prose::_layoutHorizontaly(Au width) { return maxWidth; } -Vec2Au Prose::layout(Au width) { - if (isEmpty(_blocks)) - return {}; - - // Blocks measurements can be reused between layouts changes - // only line wrapping need to be re-done - if (not _blocksMeasured) { - _measureBlocks(); - _blocksMeasured = true; - } - - _wrapLines(width); - auto textHeight = _layoutVerticaly(); - auto textWidth = _layoutHorizontaly(width); - _size = {textWidth, textHeight}; - return {textWidth, textHeight}; -} - } // namespace Karm::Text diff --git a/src/libs/karm-text/prose.h b/src/libs/karm-text/prose.h index ba3f588f..9879399f 100644 --- a/src/libs/karm-text/prose.h +++ b/src/libs/karm-text/prose.h @@ -63,15 +63,49 @@ struct Prose : public Meta::Pinned { Opt color; }; + struct CellContent { + virtual Glyph glyph() const = 0; + + virtual ~CellContent() {}; + }; + + struct StrutCell : public CellContent { + always_inline Glyph glyph() const override { + return Glyph::TOFU; + } + }; + + struct RuneCell : public CellContent { + Glyph _glyph; + + RuneCell(Glyph glyph) : _glyph(glyph) {} + + always_inline Glyph glyph() const override { + return _glyph; + } + }; + struct Cell { MutCursor prose; MutCursor span; urange runeRange; - Glyph glyph; Au pos = 0_au; //< Position of the glyph within the block Au adv = 0_au; //< Advance of the glyph + Rc _content; + + void measureAdvance(auto measureStrut) { + if (_content.is()) + adv = Au{prose->_style.font.advance(_content->glyph())}; + else + adv = measureStrut(_content); + } + + Glyph glyph() const { + return _content->glyph(); + } + MutSlice runes() { return mutSub(prose->_runes, runeRange); } @@ -150,6 +184,7 @@ struct Prose : public Meta::Pinned { Vec _runes; Vec _cells; + Vec _structCellsIndexes; Vec _blocks; Vec _lines; @@ -180,7 +215,9 @@ struct Prose : public Meta::Pinned { append(rune); } + void _append(Cell&& cell); void append(Slice runes); + void append(Rc strut); // MARK: Span -------------------------------------------------------------- @@ -189,7 +226,9 @@ struct Prose : public Meta::Pinned { void pushSpan() { _spans.pushBack(makeBox(_currentSpan)); + auto parentSpan = _currentSpan; _currentSpan = &*last(_spans); + _currentSpan->parent = parentSpan; } void spanColor(Gfx::Color color) { @@ -204,9 +243,40 @@ struct Prose : public Meta::Pinned { _currentSpan = _currentSpan->parent; } + void copySpanStack(Prose const& prose); + + // MARK: Strut ------------------------------------------------------------ + + // FIXME: can be a generator + Vec> cellsWithStruts() { + Vec> cells; + for (auto i : _structCellsIndexes) + cells.pushBack(&_cells[i]); + return cells; + } + // MARK: Layout ------------------------------------------------------------ - void _measureBlocks(); + void _measureBlocks(auto measureStrut) { + for (auto& block : _blocks) { + auto adv = 0_au; + bool first = true; + Glyph prev = Glyph::TOFU; + for (auto& cell : block.cells()) { + if (not first) + adv += Au{_style.font.kern(prev, cell.glyph())}; + else + first = false; + + cell.pos = adv; + cell.measureAdvance(measureStrut); + + adv += cell.adv; + prev = cell.glyph(); + } + block.width = adv; + } + } void _wrapLines(Au width); @@ -214,7 +284,29 @@ struct Prose : public Meta::Pinned { Au _layoutHorizontaly(Au width); - Vec2Au layout(Au width); + Vec2Au layout(Au width, auto measureStrut) { + if (isEmpty(_blocks)) + return {}; + + // Blocks measurements can be reused between layouts changes + // only line wrapping need to be re-done + if (not _blocksMeasured) { + _measureBlocks(measureStrut); + _blocksMeasured = true; + } + + _wrapLines(width); + auto textHeight = _layoutVerticaly(); + auto textWidth = _layoutHorizontaly(width); + _size = {textWidth, textHeight}; + return {textWidth, textHeight}; + } + + Vec2Au layout(Au width) { + return layout(width, [](Rc) { + return 0_au; + }); + } // MARK: Paint ------------------------------------------------------------- diff --git a/src/libs/karm-text/ttf.cpp b/src/libs/karm-text/ttf.cpp index b082f5d7..b1c164db 100644 --- a/src/libs/karm-text/ttf.cpp +++ b/src/libs/karm-text/ttf.cpp @@ -24,6 +24,16 @@ FontMetrics TtfFontface::metrics() const { }; } +BaselineSet TtfFontface::baselineSet() { + auto xHeight = _parser.glyphMetrics(glyph('x')).y; + return { + .alphabetic = 0, + .xHeight = xHeight, + .xMiddle = xHeight / 2, + .capHeight = _parser.glyphMetrics(glyph('H')).y, + }; +} + FontAttrs TtfFontface::attrs() const { FontAttrs attrs; diff --git a/src/libs/karm-text/ttf.h b/src/libs/karm-text/ttf.h index e8e4a1d2..c2320cb6 100644 --- a/src/libs/karm-text/ttf.h +++ b/src/libs/karm-text/ttf.h @@ -22,6 +22,8 @@ struct TtfFontface : public Fontface { FontMetrics metrics() const override; + BaselineSet baselineSet() override; + FontAttrs attrs() const override; Glyph glyph(Rune rune) override; diff --git a/src/libs/karm-text/vga.h b/src/libs/karm-text/vga.h index 81f6b5de..d10f189e 100644 --- a/src/libs/karm-text/vga.h +++ b/src/libs/karm-text/vga.h @@ -26,6 +26,15 @@ struct VgaFontface : public Fontface { }; } + BaselineSet baselineSet() override { + return { + .alphabetic = 0, + .xHeight = 10, + .xMiddle = 5, + .capHeight = 10, + }; + } + FontAttrs attrs() const override { return { .family = "IBM VGA8"s, diff --git a/src/web/vaev-base/baseline.h b/src/web/vaev-base/baseline.h new file mode 100644 index 00000000..3df0edf9 --- /dev/null +++ b/src/web/vaev-base/baseline.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include "keywords.h" + +namespace Vaev::Style { + +using BaselineSource = Union; + +using AlignmentBaseline = Union< + Keywords::Baseline, + Keywords::TextBottom, + Keywords::Alphabetic, + Keywords::Ideographic, + Keywords::Middle, + Keywords::Central, + Keywords::Mathematical, + Keywords::TextTop>; + +using DominantBaseline = Union< + Keywords::Auto, + Keywords::Baseline, + Keywords::TextBottom, + Keywords::Alphabetic, + Keywords::Ideographic, + Keywords::Middle, + Keywords::Central, + Keywords::Mathematical, + Keywords::TextTop>; + +struct Baseline { + BaselineSource source = Keywords::AUTO; + AlignmentBaseline alignment = Keywords::BASELINE; + DominantBaseline dominant = Keywords::AUTO; + + void repr(Io::Emit& e) const { + e("(baselines"); + e(" source={}", source); + e(" alignment={}", alignment); + e(" dominant={}", dominant); + e(")"); + } +}; + +} // namespace Vaev::Style diff --git a/src/web/vaev-base/keywords.h b/src/web/vaev-base/keywords.h index 6b301842..40106082 100644 --- a/src/web/vaev-base/keywords.h +++ b/src/web/vaev-base/keywords.h @@ -49,6 +49,36 @@ static constexpr inline Thick THICK{}; using Thin = Keyword<"thin">; static constexpr inline Thin THIN{}; +using FirstLine = Keyword<"first-line">; +static constexpr inline FirstLine FIRST_LINE{}; + +using LastLine = Keyword<"last-line">; +static constexpr inline LastLine LAST_LINE{}; + +using Baseline = Keyword<"baseline">; +static constexpr inline Baseline BASELINE{}; + +using TextBottom = Keyword<"text-bottom">; +static constexpr inline TextBottom TEXT_BOTTOM{}; + +using Alphabetic = Keyword<"alphabetic">; +static constexpr inline Alphabetic ALPHABETIC{}; + +using Ideographic = Keyword<"ideographic">; +static constexpr inline Ideographic IDEOGRAPHIC{}; + +using Middle = Keyword<"middle">; +static constexpr inline Middle MIDDLE{}; + +using Central = Keyword<"central">; +static constexpr inline Central CENTRAL{}; + +using Mathematical = Keyword<"mathematical">; +static constexpr inline Mathematical MATHEMATICAL{}; + +using TextTop = Keyword<"text-top">; +static constexpr inline TextTop TEXT_TOP{}; + } // namespace Keywords } // namespace Vaev diff --git a/src/web/vaev-dom/html/parser.cpp b/src/web/vaev-dom/html/parser.cpp index 68502759..df0f3bb2 100644 --- a/src/web/vaev-dom/html/parser.cpp +++ b/src/web/vaev-dom/html/parser.cpp @@ -1121,8 +1121,34 @@ void HtmlParser::_handleInBody(HtmlToken const& t) { } // TODO: An end tag whose tag name is "br" - // TODO: A start tag whose tag name is one of: "area", "br", "embed", "img", "keygen", "wbr" + else if ( + t.name == "br" or + (t.type == HtmlToken::START_TAG and + (t.name == "area" or t.name == "br" or t.name == "embed" or t.name == "img" or t.name == "keygen" or t.name == "wbr") + ) + ) { + if (t.type == HtmlToken::END_TAG) { + // Parse error. + _raise(); + + // Drop the attributes from the token, and act as described in the next entry; i.e. act as if + // this was a "br" start tag token with no attributes, rather than the end tag token that it actually is. + // FIXME: cannot drop attributes since token is const + } + + // Reconstruct the active formatting elements, if any. + _reconstructActiveFormattingElements(); + + // Insert an HTML element for the token. Immediately pop the current node off the stack of open elements. + _insertHtmlElement(t); + _openElements.popBack(); + + // TODO: Acknowledge the token's self-closing flag, if it is set. + + // Set the frameset-ok flag to "not ok". + _framesetOk = false; + } // TODO: A start tag whose tag name is "input" else if (t.type == HtmlToken::START_TAG and t.name == "input") { diff --git a/src/web/vaev-layout/base.cpp b/src/web/vaev-layout/base.cpp index 578aeb6d..c17acf30 100644 --- a/src/web/vaev-layout/base.cpp +++ b/src/web/vaev-layout/base.cpp @@ -1,6 +1,7 @@ module; #include +#include #include #include @@ -232,10 +233,70 @@ export struct BreakpointTraverser { export struct FormatingContext; export struct Box; +export struct InlineBox { + /* NOTE: + This is a sketch implementation of the data model for InlineBox. We should be able to: + - add different inline elements to it, from different types (Prose, Image, inline-block) + - retrieve the added data to be displayed in the same Inline Formatting Context (break lines and display + into line boxes) + - respect different styling for the same line (font, fontsize, color, etc) + */ + Text::ProseStyle const _style; + Rc prose; + Vec<::Box> atomicBoxes; + + InlineBox(Text::ProseStyle style) : _style(style), prose(makeRc(_style)) {} + + InlineBox(Rc prose) : _style(prose->_style), prose(prose) {} + + void startInlineBox(Text::ProseStyle proseStyle) { + // FIXME: ugly workaround while we dont fix the Prose data structure + prose->pushSpan(); + if (proseStyle.color) + prose->spanColor(proseStyle.color.unwrap()); + } + + void endInlineBox() { + prose->popSpan(); + } + + struct BoxStrutCell : Text::Prose::StrutCell { + usize boxIndex; + + BoxStrutCell(usize boxIndex) : boxIndex(boxIndex) {} + }; + + void add(Box&& b); + + bool active() { + return prose->_runes.len(); + } + + void repr(Io::Emit& e) const { + e("(inline box {}", prose->_runes); + e.indentNewline(); + for (auto& c : atomicBoxes) { + e("{}", c); + e.newline(); + } + e.deindent(); + e(")"); + } + + static InlineBox fromInterruptedInlineBox(InlineBox const& inlineBox) { + auto oldProse = inlineBox.prose; + + auto newInlineBox = InlineBox(inlineBox._style); + newInlineBox.prose->copySpanStack(*oldProse); + + return newInlineBox; + } +}; + export using Content = Union< None, Vec, - Rc, + InlineBox, Karm::Image::Picture>; export struct Attrs { @@ -293,12 +354,23 @@ struct Box : public Meta::NoCopy { } e.deindent(); e(")"); + } else if (content.is()) { + e("(box {} {} {}", attrs, style->display, style->position); + e.indentNewline(); + e("{}", content.unwrap()); + e.deindent(); + e(")"); } else { e("(box {} {} {})", attrs, style->display, style->position); } } }; +void InlineBox::add(Box&& b) { + prose->append(makeRc(atomicBoxes.len())); + atomicBoxes.pushBack(makeBox(std::move(b))); +} + export struct Viewport { Resolution dpi = Resolution::fromDpi(96); // https://drafts.csswg.org/css-values/#small-viewport-size @@ -456,6 +528,36 @@ export struct Input { } }; +// https://drafts.csswg.org/css-align-3/#baseline-set +// https://drafts.csswg.org/css-writing-modes-3/#baseline +// https://www.w3.org/TR/css-inline-3/#baseline-types +// https://www.w3.org/TR/css-inline-3/#dominant-baseline-property +// NOTE: positions are relative to box top, not absolute +export struct BaselinePositionsSet { + Au alphabetic; + Au xHeight; + Au xMiddle; + Au capHeight; + + BaselinePositionsSet translate(Au delta) const { + return { + alphabetic + delta, + xHeight + delta, + xMiddle + delta, + capHeight + delta, + }; + } + + void repr(Io::Emit& e) const { + e("(baselineset "); + e(" alphabetic {}", alphabetic); + e(" xHeight {}", xHeight); + e(" xMiddle {}", xMiddle); + e(" capHeight {}", capHeight); + e(")\n"); + } +}; + export struct Output { // size of subtree maximizing displayed content while respecting // - endchild constraint or @@ -475,6 +577,8 @@ export struct Output { // only to be used in discovery mode Opt breakpoint = NONE; + BaselinePositionsSet const baselineSet = {}; + static Output fromSize(Vec2Au size) { return { .size = size, @@ -496,6 +600,44 @@ export struct Output { struct FormatingContext { virtual ~FormatingContext() = default; + // https://drafts.csswg.org/css-inline-3/#valdef-baseline-source-auto + // https://drafts.csswg.org/css-inline-3/#valdef-baseline-source-first + void considerFirstBaseline( + Box& b, + BaselinePositionsSet& parentBaselineSet, + BaselinePositionsSet const& childBaselineSet, + Au childPosition, + Au parentPosition + ) { + if (b.style->baseline->source == Keywords::LAST_LINE) + return; + if ( + b.style->baseline->source == Keywords::AUTO and + b.style->display == Display::INLINE and b.style->display == Display::BLOCK + ) + return; + + parentBaselineSet = childBaselineSet.translate(childPosition - parentPosition); + } + + // https://drafts.csswg.org/css-inline-3/#valdef-baseline-source-auto + // https://drafts.csswg.org/css-inline-3/#valdef-baseline-source-last + void considerLastBaseline( + Box& b, + BaselinePositionsSet& parentBaselineSet, + BaselinePositionsSet const& childBaselineSet, + Au childPosition, + Au parentPosition + ) { + if ( + b.style->baseline->source == Keywords::LAST_LINE or + (b.style->baseline->source == Keywords::AUTO and + b.style->display == Display::INLINE and b.style->display == Display::BLOCK) + ) { + parentBaselineSet = childBaselineSet.translate(childPosition - parentPosition); + } + } + virtual void build(Tree&, Box&) {}; virtual Output run(Tree& tree, Box& box, Input input, usize startAt, Opt stopAt) = 0; }; diff --git a/src/web/vaev-layout/block.cpp b/src/web/vaev-layout/block.cpp index 2b90464a..fe9975bf 100644 --- a/src/web/vaev-layout/block.cpp +++ b/src/web/vaev-layout/block.cpp @@ -1,6 +1,8 @@ module; +#include #include +#include #include #include #include @@ -152,6 +154,7 @@ struct BlockFormatingContext : public FormatingContext { } Output run(Tree& tree, Box& box, Input input, usize startAt, Opt stopAt) override { + // logDebug("oiiiiiiiiiiiiiiiiiiiiiiii1"); Au blockSize = 0_au; Au inlineSize = input.knownSize.width.unwrapOr(0_au); @@ -165,13 +168,16 @@ struct BlockFormatingContext : public FormatingContext { inlineSize = run(tree, box, input.withFragment(nullptr), startAt, stopAt).width(); Breakpoint currentBreakpoint; + BaselinePositionsSet baselineSet; + // logDebug("oiiiiiiiiiiiiiiiiiiiiiiii2"); usize endChildren = stopAt.unwrapOr(box.children().len()); bool blockWasCompletelyLaidOut = false; Au lastMarginBottom = 0_au; for (usize i = startAt; i < endChildren; ++i) { + // logDebug("oiiiiiiiiiiiiiiiiiiiiiiii3"); auto& c = box.children()[i]; try$( @@ -236,6 +242,11 @@ struct BlockFormatingContext : public FormatingContext { output.breakpoint ); + if (i == startAt) + considerFirstBaseline(box, baselineSet, output.baselineSet, childInput.position.y, input.position.y); + considerLastBaseline(box, baselineSet, output.baselineSet, childInput.position.y, input.position.y); + + // logDebug("oiiiiiii5"); try$(processBreakpointsAfterChild( tree.fc, currentBreakpoint, @@ -250,12 +261,17 @@ struct BlockFormatingContext : public FormatingContext { } inlineSize = max(inlineSize, output.size.x + margin.horizontal()); + + // logDebug("oiiiiiii4"); } + // logDebug("found baseline set of {}", baselineSet); + return { .size = Vec2Au{inlineSize, blockSize}, .completelyLaidOut = blockWasCompletelyLaidOut, - .breakpoint = currentBreakpoint + .breakpoint = currentBreakpoint, + .baselineSet = baselineSet, }; } }; diff --git a/src/web/vaev-layout/builder.cpp b/src/web/vaev-layout/builder.cpp index 77f55d60..85a0b958 100644 --- a/src/web/vaev-layout/builder.cpp +++ b/src/web/vaev-layout/builder.cpp @@ -12,7 +12,10 @@ import :values; namespace Vaev::Layout { -static void _buildNode(Style::Computer& c, Gc::Ref node, Box& parent); +static void _buildBlockLevelBox(Style::Computer& c, Gc::Ref el, Rc style, Box& parent, Display display); +static void _buildTable(Style::Computer& c, Rc style, Gc::Ref el, Box& parent); +static void _buildImage(Style::Computer& c, Gc::Ref el, Rc parentStyle, InlineBox& rootInlineBox); +static void _buildChildInternalDisplay(Style::Computer& c, Gc::Ref child, Rc childStyle, Box& parent); // MARK: Attributes ------------------------------------------------------------ @@ -84,17 +87,12 @@ static Rc _lookupFontface(Text::FontBook& fontBook, Style: return Text::Fontface::fallback(); } -auto RE_SEGMENT_BREAK = Re::single('\n', '\r', '\f', '\v'); - -static void _buildRun(Style::Computer& c, Gc::Ref node, Box& parent) { - auto style = makeRc(Style::Computed::initial()); - style->inherit(*parent.style); +bool isSegmentBreak(Rune rune) { + return rune == '\n' or rune == '\r' or rune == '\f' or rune == '\v'; +} - auto fontFace = _lookupFontface(c.fontBook, *style); - Io::SScan scan{node->data()}; - scan.eat(Re::space()); - if (scan.ended()) - return; +Text::ProseStyle _buildProseStyle(Style::Computer& c, Rc parentStyle) { + auto fontFace = _lookupFontface(c.fontBook, *parentStyle); // FIXME: We should pass this around from the top in order to properly resolve rems Resolver resolver{ @@ -104,12 +102,13 @@ static void _buildRun(Style::Computer& c, Gc::Ref node, Box& parent) Text::ProseStyle proseStyle{ .font = { fontFace, - resolver.resolve(style->font->size).cast(), + resolver.resolve(parentStyle->font->size).cast(), }, + .color = parentStyle->color, .multiline = true, }; - switch (style->text->align) { + switch (parentStyle->text->align) { case TextAlign::START: case TextAlign::LEFT: proseStyle.align = Text::TextAlign::LEFT; @@ -129,75 +128,305 @@ static void _buildRun(Style::Computer& c, Gc::Ref node, Box& parent) break; } - auto prose = makeRc(proseStyle); - auto whitespace = style->text->whiteSpace; + return proseStyle; +} - while (not scan.ended()) { - switch (style->text->transform) { - case TextTransform::UPPERCASE: - prose->append(toAsciiUpper(scan.next())); - break; +void _transformAndAppendRuneToProse(Rc prose, Rune rune, TextTransform transform) { + switch (transform) { + case TextTransform::UPPERCASE: + prose->append(toAsciiUpper(rune)); + break; + + case TextTransform::LOWERCASE: + prose->append(toAsciiLower(rune)); + break; - case TextTransform::LOWERCASE: - prose->append(toAsciiLower(scan.next())); - break; + case TextTransform::NONE: + default: + prose->append(rune); + break; + } +} - case TextTransform::NONE: - prose->append(scan.next()); - break; +// https://www.w3.org/TR/css-text-3/#white-space-phase-1 +// https://www.w3.org/TR/css-text-3/#white-space-phase-2 +void _appendTextToInlineBox(Io::SScan scan, Rc parentStyle, Rc prose) { + auto whitespace = parentStyle->text->whiteSpace; + bool whiteSpacesAreCollapsible = + whitespace == WhiteSpace::NORMAL or + whitespace == WhiteSpace::NOWRAP or + whitespace == WhiteSpace::PRE_LINE; - default: - break; + // A sequence of collapsible spaces at the beginning of a line is removed. + if (not prose->_runes.len()) + scan.eat(Re::space()); + + while (not scan.ended()) { + auto rune = scan.next(); + + if (not isAsciiSpace(rune)) { + _transformAndAppendRuneToProse(prose, rune, parentStyle->text->transform); + continue; } - if (whitespace == WhiteSpace::PRE) { - auto tok = scan.token(Re::space()); - if (tok) - prose->append(tok); - } else if (whitespace == WhiteSpace::PRE_LINE) { - bool hasBlank = false; - if (scan.eat(Re::blank())) { - hasBlank = true; - } + // https://www.w3.org/TR/css-text-3/#collapse + if (whiteSpacesAreCollapsible) { + // Any sequence of collapsible spaces and tabs immediately preceding or following a segment break is removed. + bool visitedSegmentBreak = false; + while (true) { + if (isSegmentBreak(rune)) + visitedSegmentBreak = true; - if (scan.eat(RE_SEGMENT_BREAK)) { - prose->append('\n'); - scan.eat(Re::blank()); - hasBlank = false; + if (scan.ended() or not isAsciiSpace(scan.peek())) + break; + + rune = scan.next(); } - if (hasBlank) + // Any collapsible space immediately following another collapsible space—​even one outside the boundary + // of the inline containing that space, provided both spaces are within the same inline formatting + // context—​is collapsed to have zero advance width. (It is invisible, but retains its soft wrap + // opportunity, if any.) + // TODO: not compliant regarding wrap opportunity + + // https://www.w3.org/TR/css-text-3/#valdef-white-space-pre-line + // Collapsible segment breaks are transformed for rendering according to the segment + // break transformation rules. + if (whitespace == WhiteSpace::PRE_LINE and visitedSegmentBreak) + prose->append('\n'); + else prose->append(' '); + } else if (whitespace == WhiteSpace::PRE) { + prose->append(rune); } else { - // NORMAL - if (scan.eat(Re::space())) - prose->append(' '); + panic("unimplemented whitespace case"); } } +} + +void _buildText(Gc::Ref node, Rc parentStyle, InlineBox& rootInlineBox) { + Io::SScan scan{node->data()}; + scan.eat(Re::space()); + if (scan.ended()) + return; - parent.add({style, fontFace, std::move(prose)}); + _appendTextToInlineBox(node->data(), parentStyle, rootInlineBox.prose); } -// MARK: Build Block ----------------------------------------------------------- +// https://www.w3.org/TR/css-inline-3/#model +void _flushRootInlineBoxIntoAnonymousBox(Box& parent, Rc& rootInlineBox) { + if (not rootInlineBox->active()) + return; -void _buildChildren(Style::Computer& c, Gc::Ref node, Box& parent) { - for (auto child = node->firstChild(); child; child = child->nextSibling()) { - _buildNode(c, *child, parent); + // The root inline box inherits from its parent block container, but is otherwise unstyleable. + auto style = makeRc(Style::Computed::initial()); + style->inherit(*parent.style); + style->display = Display{Display::Inside::FLOW, Display::Outside::BLOCK}; + + auto newInlineBox = makeRc(InlineBox::fromInterruptedInlineBox(*rootInlineBox)); + parent.add({style, parent.fontFace, std::move(*rootInlineBox)}); + rootInlineBox = newInlineBox; +} + +// Similar abstraction to https://webkit.org/blog/115/webcore-rendering-ii-blocks-and-inlines/ +export struct InlineFlowBuilder { + Box& rootBox; + Rc& rootInlineBox; + + InlineFlowBuilder(Box& rootBox, Rc& rootInlineBox) + : rootBox(rootBox), rootInlineBox(rootInlineBox) {} + + // https://www.w3.org/TR/css-display-3/#box-generation + void _buildChildBoxDisplay(Style::Computer& c, Gc::Ref child, Rc childStyle, Display display) { + if (display == Display::NONE) + return; + else + _buildChildren(c, child, childStyle); + } + + void _buildChildDefaultDisplay(Style::Computer& c, Gc::Ref child, Rc childStyle, Display display); + + // Dispatching children from block-level context based on their node type and display property in case of element + // https://www.w3.org/TR/css-display-3/#the-display-properties + void _buildChildren(Style::Computer& c, Gc::Ref el, Rc style) { + for (auto child = el->firstChild(); child; child = child->nextSibling()) { + if (auto el = child->is()) { + auto childStyle = c.computeFor(*style, *el); + auto display = childStyle->display; + + if (display.type() == Display::Type::BOX) { + _buildChildBoxDisplay(c, *el, style, display); + } else if (display.type() == Display::Type::INTERNAL) { + _buildChildInternalDisplay(c, *el, childStyle, rootBox); + } else { + _buildChildDefaultDisplay(c, *el, childStyle, display); + } + } else if (auto text = child->is()) { + _buildText(*text, style, *rootInlineBox); + } + } } + + void buildFromElement(Style::Computer& c, Gc::Ref el, Rc& style) { + if (el->tagName == Html::IMG) { + _buildImage(c, *el, rootBox.style, *rootInlineBox); + } else if (el->tagName == Html::BR) { + _flushRootInlineBoxIntoAnonymousBox(rootBox, rootInlineBox); + } else { + auto proseStyle = _buildProseStyle(c, style); + rootInlineBox->startInlineBox(proseStyle); + _buildChildren(c, el, style); + rootInlineBox->endInlineBox(); + } + } +}; + +export struct BlockFlowBuilder { + Box& box; + Rc rootInlineBox; + + BlockFlowBuilder(Style::Computer& c, Box& box) + : box(box), rootInlineBox(makeRc(_buildProseStyle(c, box.style))) {} + + // https://www.w3.org/TR/css-display-3/#box-generation + void _buildChildBoxDisplay(Style::Computer& c, Gc::Ref child, Display display) { + if (display == Display::NONE) + return; + else + _buildChildren(c, child); + } + + // Dispatching children from an Inline Flow context based on their outer and inner roles + // https://www.w3.org/TR/css-display-3/#outer-role + // https://www.w3.org/TR/css-display-3/#inner-model + void _buildChildDefaultDisplay(Style::Computer& c, Gc::Ref child, Rc childStyle, Display display) { + if (display != Display::Outside::INLINE) { + _flushRootInlineBoxIntoAnonymousBox(box, rootInlineBox); + _buildBlockLevelBox(c, child, childStyle, box, display); + return; + } + + if (display == Display::Inside::FLOW) { + InlineFlowBuilder{box, rootInlineBox}.buildFromElement(c, child, childStyle); + } else if (display == Display::Inside::FLOW_ROOT) { + auto font = _lookupFontface(c.fontBook, *childStyle); + Box box = {childStyle, font}; + BlockFlowBuilder{c, box}.buildFromElement(c, child); + + box.attrs = _parseDomAttr(child); + + rootInlineBox->add(std::move(box)); + } else { + // FIXME: fallback to FLOW since not implemented + InlineFlowBuilder{box, rootInlineBox}.buildFromElement(c, child, childStyle); + } + } + + // Dispatching children from block-level context based on their node type and display property in case of element + // https://www.w3.org/TR/css-display-3/#the-display-properties + void _buildChildren(Style::Computer& c, Gc::Ref parent) { + for (auto child = parent->firstChild(); child; child = child->nextSibling()) { + if (auto el = child->is()) { + auto childStyle = c.computeFor(*box.style, *el); + auto display = childStyle->display; + + if (display.type() == Display::Type::BOX) { + _buildChildBoxDisplay(c, *el, display); + } else if (display.type() == Display::Type::INTERNAL) { + _buildChildInternalDisplay(c, *el, childStyle, box); + } else { + _buildChildDefaultDisplay(c, *el, childStyle, display); + } + } else if (auto text = child->is()) { + _buildText(*text, box.style, *rootInlineBox); + } + } + } + + void _finalizeParentBoxAndFlushInline(Box& parent, Rc& rootInlineBox) { + if (not rootInlineBox->active()) + return; + + if (parent.children()) { + _flushRootInlineBoxIntoAnonymousBox(parent, rootInlineBox); + return; + } + + auto newRootInlineBox = makeRc(InlineBox::fromInterruptedInlineBox(*rootInlineBox)); + parent.content = std::move(*rootInlineBox); + rootInlineBox = newRootInlineBox; + } + + void buildFromElement(Style::Computer& c, Gc::Ref el) { + if (el->tagName == Html::IMG) { + _buildImage(c, *el, box.style, *rootInlineBox); + } else if (el->tagName == Html::SVG) { + // TODO: _buildSVG + } else if (el->tagName == Html::BR) { + // do nothing + } else { + _buildChildren(c, el); + } + _finalizeParentBoxAndFlushInline(box, rootInlineBox); + } + + void build(Style::Computer& c, Gc::Ref node) { + if (auto el = node->is()) { + buildFromElement(c, *el); + return; + } + + _buildChildren(c, node); + _finalizeParentBoxAndFlushInline(box, rootInlineBox); + } + + static void fromElement(Style::Computer& c, Box& parent, Rc style, Gc::Ref el) { + auto font = _lookupFontface(c.fontBook, *style); + Box box = {style, font}; + BlockFlowBuilder{c, box}.buildFromElement(c, el); + + box.attrs = _parseDomAttr(el); + parent.add(std::move(box)); + } +}; + +// https://www.w3.org/TR/css-display-3/#layout-specific-display +void _buildChildInternalDisplay(Style::Computer& c, Gc::Ref child, Rc childStyle, Box& parent) { + // FIXME: We should create wrapping boxes related to table or ruby, following the FC specification. However, for now, + // we just wrap it in a single box. + BlockFlowBuilder::fromElement(c, parent, childStyle, child); } -static void _buildBlock(Style::Computer& c, Rc style, Gc::Ref el, Box& parent) { - auto font = _lookupFontface(c.fontBook, *style); - Box box = {style, font}; - _buildChildren(c, el, box); - box.attrs = _parseDomAttr(el); - parent.add(std::move(box)); +// Dispatching children from an Inline Flow context based on their outer and inner roles +// https://www.w3.org/TR/css-display-3/#outer-role +// https://www.w3.org/TR/css-display-3/#inner-model +void InlineFlowBuilder::_buildChildDefaultDisplay(Style::Computer& c, Gc::Ref child, Rc childStyle, Display display) { + if (display != Display::Outside::INLINE) { + _flushRootInlineBoxIntoAnonymousBox(rootBox, rootInlineBox); + _buildBlockLevelBox(c, child, childStyle, rootBox, display); + return; + } + + if (display == Display::Inside::FLOW) { + buildFromElement(c, child, childStyle); + } else if (display == Display::Inside::FLOW_ROOT) { + auto font = _lookupFontface(c.fontBook, *childStyle); + Box box = {childStyle, font}; + BlockFlowBuilder{c, box}.buildFromElement(c, child); + + box.attrs = _parseDomAttr(child); + + rootInlineBox->add(std::move(box)); + } else { + // FIXME: fallback to FLOW since not implemented + buildFromElement(c, child, childStyle); + } } // MARK: Build Replace --------------------------------------------------------- -static void _buildImage(Style::Computer& c, Gc::Ref el, Box& parent) { - auto style = c.computeFor(*parent.style, el); +static void _buildImage(Style::Computer& c, Gc::Ref el, Rc parentStyle, InlineBox& rootInlineBox) { + auto style = c.computeFor(*parentStyle, el); auto font = _lookupFontface(c.fontBook, *style); auto src = el->getAttribute(Html::SRC_ATTR).unwrapOr(""s); @@ -207,7 +436,8 @@ static void _buildImage(Style::Computer& c, Gc::Ref el, Box& paren auto img = Karm::Image::load(url).unwrapOrElse([] { return Karm::Image::loadOrFallback("bundle://vaev-driver/missing.qoi"_url).unwrap(); }); - parent.add({style, font, img}); + + rootInlineBox.add({style, font}); } // MARK: Build Table ----------------------------------------------------------- @@ -226,7 +456,10 @@ static void _buildTableChildren(Style::Computer& c, Gc::Ref node, Box for (auto child = node->firstChild(); child; child = child->nextSibling()) { if (auto el = child->is()) { if (el->tagName == Html::CAPTION) { - _buildNode(c, *el, tableWrapperBox); + BlockFlowBuilder::fromElement( + c, tableWrapperBox, + c.computeFor(*tableWrapperBox.style, *el), *el + ); } } } @@ -235,7 +468,10 @@ static void _buildTableChildren(Style::Computer& c, Gc::Ref node, Box for (auto child = node->firstChild(); child; child = child->nextSibling()) { if (auto el = child->is()) { if (el->tagName != Html::CAPTION) { - _buildNode(c, *el, tableBox); + BlockFlowBuilder::fromElement( + c, tableBox, + c.computeFor(*tableBox.style, *el), *el + ); } } } @@ -245,13 +481,28 @@ static void _buildTableChildren(Style::Computer& c, Gc::Ref node, Box for (auto child = node->firstChild(); child; child = child->nextSibling()) { if (auto el = child->is()) { if (el->tagName == Html::CAPTION) { - _buildNode(c, *el, tableWrapperBox); + BlockFlowBuilder::fromElement( + c, tableWrapperBox, + c.computeFor(*tableWrapperBox.style, *el), *el + ); } } } } } +// https://www.w3.org/TR/css-display-3/#outer-role +static void _buildBlockLevelBox(Style::Computer& c, Gc::Ref el, Rc style, Box& parent, Display display) { + if (display == Display::Inside::TABLE) { + _buildTable(c, style, el, parent); + } else if (display == Display::Inside::FLOW or display == Display::Inside::FLEX) { + BlockFlowBuilder::fromElement(c, parent, style, el); + } else { + // FIXME: fallback to FLOW since not implemented + BlockFlowBuilder::fromElement(c, parent, style, el); + } +} + static void _buildTable(Style::Computer& c, Rc style, Gc::Ref el, Box& parent) { auto font = _lookupFontface(c.fontBook, *style); @@ -268,44 +519,13 @@ static void _buildTable(Style::Computer& c, Rc style, Gc::Ref el, Box& parent) { - if (el->tagName == Html::IMG) { - _buildImage(c, el, parent); - return; - } - - auto style = c.computeFor(*parent.style, el); - auto font = _lookupFontface(c.fontBook, *style); - - auto display = style->display; - - if (display == Display::NONE) { - // Do nothing - } else if (display == Display::CONTENTS) { - _buildChildren(c, el, parent); - } else if (display == Display::TABLE) { - _buildTable(c, style, el, parent); - } else { - _buildBlock(c, style, el, parent); - } -} - -static void _buildNode(Style::Computer& c, Gc::Ref node, Box& parent) { - if (auto el = node->is()) { - _buildElement(c, *el, parent); - } else if (auto text = node->is()) { - _buildRun(c, *text, parent); - } else if (auto doc = node->is()) { - _buildChildren(c, *doc, parent); - } -} - export Box build(Style::Computer& c, Gc::Ref doc) { if (auto el = doc->documentElement()) { auto style = c.computeFor(Style::Computed::initial(), *el); auto font = _lookupFontface(c.fontBook, *style); - Box root = {style, _lookupFontface(c.fontBook, *style)}; - _buildChildren(c, *el, root); + + Box root = {style, font}; + BlockFlowBuilder{c, root}.build(c, doc); return root; } // NOTE: Fallback in case of an empty document @@ -335,7 +555,7 @@ export Box buildForPseudoElement(Text::FontBook& fontBook, Rc s auto prose = makeRc(proseStyle); if (style->content) { prose->append(style->content.str()); - return {style, fontFace, prose}; + return {style, fontFace, InlineBox{prose}}; } return {style, fontFace}; diff --git a/src/web/vaev-layout/inline.cpp b/src/web/vaev-layout/inline.cpp index 9b8e6bec..e05ec513 100644 --- a/src/web/vaev-layout/inline.cpp +++ b/src/web/vaev-layout/inline.cpp @@ -1,17 +1,33 @@ module; +#include #include export module Vaev.Layout:inline_; import :base; +import :layout; namespace Vaev::Layout { struct InlineFormatingContext : public FormatingContext { + + BaselinePositionsSet _computeBaselinePositions(InlineBox& inlineBox, Au baselinePosition) { + // FIXME + auto nonConstCopy = inlineBox._style.font; + auto baselineSet = nonConstCopy.baselineSet(); + + return BaselinePositionsSet{ + .alphabetic = Au{baselineSet.alphabetic} + baselinePosition, + .xHeight = Au{baselineSet.xHeight} + baselinePosition, + .xMiddle = Au{baselineSet.xMiddle} + baselinePosition, + .capHeight = Au{baselineSet.capHeight} + baselinePosition, + }; + } + virtual Output run([[maybe_unused]] Tree& tree, Box& box, Input input, [[maybe_unused]] usize startAt, [[maybe_unused]] Opt stopAt) override { // NOTE: We are not supposed to get there if the content is not a prose - auto& prose = *box.content.unwrap>("inlineLayout"); + auto& inlineBox = box.content.unwrap("inlineLayout"); auto inlineSize = input.knownSize.x.unwrapOrElse([&] { if (input.intrinsic == IntrinsicSize::MIN_CONTENT) { @@ -23,7 +39,60 @@ struct InlineFormatingContext : public FormatingContext { } }); - auto size = prose.layout(inlineSize); + auto& prose = inlineBox.prose; + + auto size = prose->layout(inlineSize, [&](Rc cell) -> Au { + if (not cell.is()) + panic("xiiii"); + + auto& boxStrutCell = cell.unwrap(); + + return layout( + tree, + inlineBox.atomicBoxes[boxStrutCell.boxIndex], + Input{ + .knownSize = {NONE, NONE}, + } + ) + .size.x; + }); + + auto baselineSetPositions = _computeBaselinePositions(inlineBox, prose->_lines[0].baseline); + + for (auto strutCell : prose->cellsWithStruts()) { + auto runeIdx = strutCell->runeRange.start; + auto position = prose->queryPosition(runeIdx); + + auto& boxStrutCell = strutCell->_content.unwrap(); + + // logDebug("estou mandando a box {}", inlineBox.atomicBoxes[boxStrutCell.boxIndex]); + + auto outputWithBaselines = layout( + tree, + inlineBox.atomicBoxes[boxStrutCell.boxIndex], + Input{ + .knownSize = {NONE, NONE}, + .position = input.position + position, + } + ); + + auto childBaselineSet = outputWithBaselines.baselineSet; + + // logDebug("mandei a box {} e ganhei {}", inlineBox.atomicBoxes[boxStrutCell.boxIndex], childBaselineSet); + + auto alignedPosition = input.position + position; + alignedPosition.y -= childBaselineSet.alphabetic; + + layout( + tree, + inlineBox.atomicBoxes[boxStrutCell.boxIndex], + Input{ + .fragment = input.fragment, + .knownSize = {NONE, NONE}, + .position = alignedPosition, + } + ); + } if (tree.fc.allowBreak() and not tree.fc.acceptsFit( input.position.y, @@ -40,6 +109,7 @@ struct InlineFormatingContext : public FormatingContext { return { .size = size, .completelyLaidOut = true, + .baselineSet = baselineSetPositions, }; } }; diff --git a/src/web/vaev-layout/layout-impl.cpp b/src/web/vaev-layout/layout-impl.cpp index dc91873f..1f39e163 100644 --- a/src/web/vaev-layout/layout-impl.cpp +++ b/src/web/vaev-layout/layout-impl.cpp @@ -24,7 +24,7 @@ static Opt> _constructFormatingContext(Box& box) { if (box.content.is()) { return constructReplacedFormatingContext(box); - } else if (box.content.is>()) { + } else if (box.content.is()) { return constructInlineFormatingContext(box); } else if ( display == Display::FLOW or @@ -265,12 +265,15 @@ Output layout(Tree& tree, Box& box, Input input) { out.breakpoint ); - out.size = size + padding.all() + borders.all(); - if (isMonolithicDisplay) tree.fc.leaveMonolithicBox(); - return out; + return { + .size = size + padding.all() + borders.all(), + .completelyLaidOut = out.completelyLaidOut, + .breakpoint = out.breakpoint, + .baselineSet = out.baselineSet.translate(padding.top + borders.top), + }; } else { Opt stopAt = tree.fc.allowBreak() ? input.breakpointTraverser.getEnd() @@ -310,7 +313,8 @@ Output layout(Tree& tree, Box& box, Input input) { return Output{ .size = size, - .completelyLaidOut = out.completelyLaidOut + .completelyLaidOut = out.completelyLaidOut, + .baselineSet = out.baselineSet.translate(padding.top + borders.top), }; } } diff --git a/src/web/vaev-layout/paint.cpp b/src/web/vaev-layout/paint.cpp index f7ab7ea0..dd030f0e 100644 --- a/src/web/vaev-layout/paint.cpp +++ b/src/web/vaev-layout/paint.cpp @@ -105,10 +105,8 @@ static void _paintFrag(Frag& frag, Scene::Stack& stack) { _paintFragBordersAndBackgrounds(frag, stack); - if (auto prose = frag.box->content.is>()) { - (*prose)->_style.color = frag.style().color; - - stack.add(makeRc(frag.metrics.borderBox().topStart().cast(), *prose)); + if (auto ic = frag.box->content.is()) { + stack.add(makeRc(frag.metrics.contentBox().topStart().cast(), ic->prose)); } else if (auto image = frag.box->content.is()) { stack.add(makeRc(frag.metrics.borderBox().cast(), *image)); } diff --git a/src/web/vaev-layout/tests/manifest.json b/src/web/vaev-layout/tests/manifest.json new file mode 100644 index 00000000..54277791 --- /dev/null +++ b/src/web/vaev-layout/tests/manifest.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.cute.engineering/stable/cutekit.manifest.component.v1", + "id": "vaev-layout.tests", + "type": "lib", + "props": { + "cpp-excluded": true + }, + "requires": [ + "vaev-layout", + "vaev-driver" + ], + "injects": [ + "__tests__" + ] +} diff --git a/src/web/vaev-layout/tests/test-box-tree-builder.cpp b/src/web/vaev-layout/tests/test-box-tree-builder.cpp new file mode 100644 index 00000000..32083ae7 --- /dev/null +++ b/src/web/vaev-layout/tests/test-box-tree-builder.cpp @@ -0,0 +1,526 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +import Vaev.Driver; +import Vaev.Layout; + +namespace Vaev::Layout::Tests { + +static Style::Media const TEST_MEDIA = { + .type = MediaType::SCREEN, + .width = 1920_au, + .height = 1080_au, + .aspectRatio = 16.0 / 9.0, + .orientation = Print::Orientation::LANDSCAPE, + + .resolution = Resolution::fromDpi(96), + .scan = Scan::PROGRESSIVE, + .grid = false, + .update = Update::NONE, + .overflowBlock = OverflowBlock::NONE, + .overflowInline = OverflowInline::NONE, + + .color = 8, + .colorIndex = 256, + .monochrome = 0, + .colorGamut = ColorGamut::SRGB, + .pointer = Pointer::NONE, + .hover = Hover::NONE, + .anyPointer = Pointer::FINE, + .anyHover = Hover::HOVER, + + .prefersReducedMotion = ReducedMotion::REDUCE, + .prefersReducedTransparency = ReducedTransparency::NO_PREFERENCE, + .prefersContrast = Contrast::LESS, + .forcedColors = Colors::NONE, + .prefersColorScheme = ColorScheme::LIGHT, + .prefersReducedData = ReducedData::REDUCE, + + .deviceWidth = 1920_au, + .deviceHeight = 1080_au, + .deviceAspectRatio = 16.0 / 9.0, +}; + +struct FakeBox { + bool stablishesInline; + bool isBlockLevel; + Vec children{}; + + bool matches(Box const& b) { + if (not b.style->display.is(Display::Type::DEFAULT)) + return false; + + bool boxStablishesInline = b.content.is(); + bool boxIsBlockLevel = (b.style->display.outside() == Display::Outside::BLOCK); + + // logDebug("box: {} {}, expected: {} {}", boxStablishesInline, boxIsBlockLevel, stablishesInline, isBlockLevel); + + if (boxIsBlockLevel != isBlockLevel) + return false; + + if (boxStablishesInline != stablishesInline) + return false; + + if (boxStablishesInline) { + if (children.len() != 1) + return false; + auto rootInlineBox = extractInlineBoxTree(b.content.unwrap().prose->_spans); + return children[0].matches(rootInlineBox); + } else { + // logDebug("box children: {} expected children: {}", b.children().len(), children.len()); + if (children.len() != b.children().len()) + return false; + + for (usize i = 0; i < children.len(); ++i) { + if (not children[i].matches(b.children()[i])) + return false; + } + return true; + } + } + + struct InlineBox { + Vec children{}; + + InlineBox& add(InlineBox&& box) { + children.pushBack(std::move(box)); + return last(children); + } + + void repr(Io::Emit& e) const { + e("("); + for (auto& c : children) + e("{}", c); + e(")"); + } + }; + + bool matches(InlineBox const& b) { + // FIXME: fix this when we have support to inline-block + if (isBlockLevel or not stablishesInline) + return false; + + if (children.len() != b.children.len()) + return false; + + for (usize i = 0; i < children.len(); ++i) { + if (not children[i].matches(b.children[i])) + return false; + } + return true; + } + + InlineBox extractInlineBoxTree(Vec<::Box> const& spans) { + InlineBox inlineBoxTree; + + Vec> stackInlineBoxes = {&inlineBoxTree}; + Vec stackSpans = {nullptr}; + for (auto& span : spans) { + while (span->parent != last(stackSpans)) { + stackSpans.popBack(); + stackInlineBoxes.popBack(); + } + + stackSpans.pushBack(&(span.unwrap())); + stackInlineBoxes.pushBack(&last(stackInlineBoxes)->add(InlineBox{})); + } + + return inlineBoxTree; + } +}; + +test$("no span") { + Gc::Heap gc; + + auto dom = gc.alloc(Mime::Url()); + Dom::HtmlParser parser{gc, dom}; + + parser.write( + "hello, world" + ); + + auto expectedBodySubtree = + FakeBox{ + // body + .stablishesInline = true, + .isBlockLevel = true, + .children{ + FakeBox{ + // root inline with "hello, world" + .stablishesInline = true, + .isBlockLevel = false, + } + } + }; + + auto rootBox = Vaev::Driver::render(dom, TEST_MEDIA, Viewport{.small = Vec2Au{100_au, 100_au}}).layout; + auto const& bodyBox = rootBox->children()[0].children()[0]; + + expect$(expectedBodySubtree.matches(bodyBox)); + + return Ok(); +} + +test$("no span") { + Gc::Heap gc; + + auto dom = gc.alloc(Mime::Url()); + Dom::HtmlParser parser{gc, dom}; + + parser.write( + "hello,
brrrrr world" + ); + + auto expectedBodySubtree = + FakeBox{ + // body + .stablishesInline = false, + .isBlockLevel = true, + .children{ + FakeBox{ + // anon box for hello, + .stablishesInline = true, + .isBlockLevel = true, + .children{ + FakeBox{ + // root inline with "hello," + .stablishesInline = true, + .isBlockLevel = false, + }, + } + }, + FakeBox{ + // anon box brrrrr world + .stablishesInline = true, + .isBlockLevel = true, + .children{ + FakeBox{ + // root inline with "brrrrr world" + .stablishesInline = true, + .isBlockLevel = false, + }, + } + }, + } + }; + + auto rootBox = Vaev::Driver::render(dom, TEST_MEDIA, Viewport{.small = Vec2Au{100_au, 100_au}}).layout; + auto const& bodyBox = rootBox->children()[0].children()[0]; + + expect$(expectedBodySubtree.matches(bodyBox)); + + return Ok(); +} + +test$("no span, breaking block") { + Gc::Heap gc; + + auto dom = gc.alloc(Mime::Url()); + Dom::HtmlParser parser{gc, dom}; + + parser.write( + "hello,
cruel
world" + ); + + auto expectedBodySubtree = + FakeBox{ + // body + .stablishesInline = false, + .isBlockLevel = true, + .children = { + FakeBox{ + // anon block + .stablishesInline = true, + .isBlockLevel = true, + .children{ + FakeBox{ + // root inline with "hello, " + .stablishesInline = true, + .isBlockLevel = false, + } + } + }, + FakeBox{ + // div block + .stablishesInline = true, + .isBlockLevel = true, + .children{ + FakeBox{ + // root inline with "cruel" + .stablishesInline = true, + .isBlockLevel = false, + } + } + }, + FakeBox{ + // anon block + .stablishesInline = true, + .isBlockLevel = true, + .children{ + FakeBox{ + // root inline with "world" + .stablishesInline = true, + .isBlockLevel = false, + } + } + }, + } + }; + + auto rootBox = Vaev::Driver::render(dom, TEST_MEDIA, Viewport{.small = Vec2Au{100_au, 100_au}}).layout; + auto const& bodyBox = rootBox->children()[0].children()[0]; + + expect$(expectedBodySubtree.matches(bodyBox)); + + return Ok(); +} + +test$("span and breaking block 1") { + Gc::Heap gc; + + auto dom = gc.alloc(Mime::Url()); + Dom::HtmlParser parser{gc, dom}; + + parser.write( + "" + "hello" + "cruel" + "world" + "
" + "
" + "melancholy" + "" + ); + + auto expectedBodySubtree = + FakeBox{ + // body + .stablishesInline = false, + .isBlockLevel = true, + .children = { + FakeBox{ + // anon div for inlines + .stablishesInline = true, + .isBlockLevel = true, + .children{ + FakeBox{ + // root inline box + .stablishesInline = true, + .isBlockLevel = false, + .children{ + FakeBox{ + // span with hello + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // span with cruel + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // span with world + .stablishesInline = true, + .isBlockLevel = false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + FakeBox{ + // div block + .stablishesInline = false, + .isBlockLevel = true, + }, + FakeBox{ + .stablishesInline = true, + .isBlockLevel = true, + .children = { + FakeBox{ + // root inline box + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // span that had hello + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // span that had cruel + .stablishesInline = true, + .isBlockLevel = false, + .children = {FakeBox{ + // span that had world + .stablishesInline = true, + .isBlockLevel = false, + }}, + }, + }, + }, + // root inline with have "melancholy" + }, + }, + }, + }, + } + }; + + auto rootBox = Vaev::Driver::render(dom, TEST_MEDIA, Viewport{.small = Vec2Au{100_au, 100_au}}).layout; + auto const& bodyBox = rootBox->children()[0].children()[0]; + + expect$(expectedBodySubtree.matches(bodyBox)); + + return Ok(); +} + +test$("span and breaking block 2") { + Gc::Heap gc; + + auto dom = gc.alloc(Mime::Url()); + Dom::HtmlParser parser{gc, dom}; + + parser.write( + "" + "hello" + "cruel" + "world" + "
" + "melancholy" + "
kidding
" + "good vibes" + "" + ); + + auto expectedBodySubtree = + FakeBox{ + // body + .stablishesInline = false, + .isBlockLevel = true, + .children = { + FakeBox{ + // anon div for inlines + .stablishesInline = true, + .isBlockLevel = true, + .children{ + FakeBox{ + // root inline box + .stablishesInline = true, + .isBlockLevel = false, + .children{ + FakeBox{ + // hello + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // cruel + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // world + .stablishesInline = true, + .isBlockLevel = false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + FakeBox{ + // misplaced div 1 + .stablishesInline = false, + .isBlockLevel = true, + }, + FakeBox{ + // anon div for inline + .stablishesInline = true, + .isBlockLevel = true, + .children = { + FakeBox{ + // root inline box + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // span with hello + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // span with cruel + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // span with melancholy + .stablishesInline = true, + .isBlockLevel = false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + FakeBox{ + // misplaced div 2 + .stablishesInline = true, + .isBlockLevel = true, + .children = { + FakeBox{ + // root inline box with "kidding" + .stablishesInline = true, + .isBlockLevel = false, + }, + }, + }, + FakeBox{ + // anon div for inline + .stablishesInline = true, + .isBlockLevel = true, + .children = { + FakeBox{ + // root inline box + .stablishesInline = true, + .isBlockLevel = false, + .children = { + FakeBox{ + // span that had "hello" + .stablishesInline = true, + .isBlockLevel = false, + }, + // root inline with have good vibes in the end + }, + }, + }, + }, + } + }; + + auto rootBox = Vaev::Driver::render(dom, TEST_MEDIA, Viewport{.small = Vec2Au{100_au, 100_au}}).layout; + auto const& bodyBox = rootBox->children()[0].children()[0]; + + expect$(expectedBodySubtree.matches(bodyBox)); + + return Ok(); +} + +} // namespace Vaev::Layout::Tests diff --git a/src/web/vaev-style/computed.h b/src/web/vaev-style/computed.h index 73eb3fea..aafcf678 100644 --- a/src/web/vaev-style/computed.h +++ b/src/web/vaev-style/computed.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,10 @@ struct Computed { Cow sizing; Overflows overflows; + // CSS Inline Layout Module Level 3 + // https://drafts.csswg.org/css-inline-3/ + Cow baseline; + // 9.3 Positioning schemes // https://www.w3.org/TR/CSS22/visuren.html#positioning-scheme Position position;