diff --git a/packages/core/scripts/build-gea-vite-geatsc.mjs b/packages/core/scripts/build-gea-vite-geatsc.mjs index 170a049..fe8ce61 100755 --- a/packages/core/scripts/build-gea-vite-geatsc.mjs +++ b/packages/core/scripts/build-gea-vite-geatsc.mjs @@ -1533,22 +1533,27 @@ function parseStaticSelectorPlanSpec(selector) { if (!text) return null const parts = [] let nextDirect = false + // Relation to the previous part: 1 next sibling (+), 2 any earlier sibling (~). + let nextSibling = 0 + const combinator = (char) => char === '>' || char === '+' || char === '~' let i = 0 while (i < text.length) { while (i < text.length && text.charCodeAt(i) <= 32) i++ if (i >= text.length) break - if (text[i] === '>') { - nextDirect = true + if (combinator(text[i])) { + nextDirect = text[i] === '>' + nextSibling = text[i] === '+' ? 1 : text[i] === '~' ? 2 : 0 i++ continue } const start = i - while (i < text.length && text.charCodeAt(i) > 32 && text[i] !== '>') i++ + while (i < text.length && text.charCodeAt(i) > 32 && !combinator(text[i])) i++ if (i <= start) continue const simple = parseStaticSimpleSelectorSpec(text.slice(start, i)) if (!simple) return null - parts.push({ simple, directParent: nextDirect }) + parts.push({ simple, directParent: nextDirect, sibling: nextSibling }) nextDirect = false + nextSibling = 0 } return parts.length > 0 ? { text, parts } : null } @@ -1561,7 +1566,7 @@ function staticSimpleSelectorSpecCode(simple) { } function staticSelectorPartSpecCode(part) { - return `{${staticSimpleSelectorSpecCode(part.simple)}, ${part.directParent ? 'true' : 'false'}}` + return `{${staticSimpleSelectorSpecCode(part.simple)}, ${part.directParent ? 'true' : 'false'}, ${part.sibling}}` } function staticSelectorPlanRegistration(selector) { @@ -2043,7 +2048,8 @@ function cssStaticPropertyValue(property, rawValue) { const length = simpleStaticLength(value) switch (property) { case 'display': { - const values = new Map([['block', 0], ['none', 1], ['grid', 2], ['inline-grid', 2], ['flex', 3], ['inline-flex', 3]]) + // Keyword flags above the box kind, as in the engine's displayValue(). + const values = new Map([['block', 0], ['none', 1], ['grid', 2], ['inline-grid', 66], ['flex', 3], ['inline-flex', 67], ['flow-root', 32], ['inline', 64], ['inline-block', 96]]) return values.has(value) ? direct('Display', values.get(value)) : null } case 'box-sizing': diff --git a/packages/core/test/test_css_block_and_flexbasis_main.cpp b/packages/core/test/test_css_block_and_flexbasis_main.cpp index 0e9e0dc..a5f0d57 100644 --- a/packages/core/test/test_css_block_and_flexbasis_main.cpp +++ b/packages/core/test/test_css_block_and_flexbasis_main.cpp @@ -4370,11 +4370,12 @@ void testBaselineAlignment() StyleSheet::instance().registerRule("baseline-child", "align-self", "last baseline"); StyleSheet::instance().registerRule("baseline-child", "justify-self", "baseline last"); tree.mount(root, 300, 200); + // The fallback end belongs to the grid, not the child's own + // direction or writing mode (WPT grid-abspos-staticpos-*-002). const bool reversed = std::strcmp(mode, "ltr") != 0; - const bool endAtLeft = reversed && !ownWritingMode; const int areaLeft = positioned && !small ? (reversed ? 21 : 5) : 3; const int areaWidth = small ? 2 : positioned ? 20 : 40; - const int x = areaLeft + (endAtLeft ? 0 : areaWidth - 8); + const int x = areaLeft + (reversed ? 0 : areaWidth - 8); const int y = small ? -1 : positioned ? 19 : 37; expectEqual(tree.node(child).layout.x - tree.node(grid).layout.x, x, "absolute baseline fallback uses logical horizontal end without overflow clamp"); expectEqual(tree.node(child).layout.y - tree.node(grid).layout.y, y, "absolute baseline fallback uses logical vertical end without overflow clamp"); diff --git a/packages/engine/canvas.cpp b/packages/engine/canvas.cpp index fdedaed..3d14711 100644 --- a/packages/engine/canvas.cpp +++ b/packages/engine/canvas.cpp @@ -3756,57 +3756,53 @@ void Canvas::strokeRoundedRect(int x, int y, int w, int h, int tl, int tr, int b markDirty(dirtyX0, dirtyY0, dirtyX1, dirtyY1); return; } - fillRect(x + tl, y, w - tl - tr, lw, color); - fillRect(x + bl, y + h - lw, w - bl - br, lw, color); - fillRect(x, y + tl, lw, h - tl - bl, color); - fillRect(x + w - lw, y + tr, lw, h - tr - br, color); - for (int q = 0; q < 4; q++) { - int r, cx, cy; - switch (q) { - case 0: r = tr; cx = x + w - 1 - r; cy = y + r; break; - case 1: r = tl; cx = x + r; cy = y + r; break; - case 2: r = bl; cx = x + r; cy = y + h - 1 - r; break; - case 3: r = br; cx = x + w - 1 - r; cy = y + h - 1 - r; break; - default: continue; - } - if (r <= 0) continue; - if (cx + r < clip->x0 || cx - r > clip->x1 || - cy + r < clip->y0 || cy - r > clip->y1) + // Each row is painted once as the outer shape's span minus the inner + // (padding-edge) shape's span, using fillRoundedRect's scanline so the ring + // is the exact complement of a fill of the inner box. Separate edge and + // corner pieces used to overlap, blending translucent borders twice where + // they met and bulging 1px into the padding area at the curves. + const int ix = x + lw, iy = y + lw, iw = w - 2 * lw, ih = h - 2 * lw; + const int innerMax = std::min(iw / 2, ih / 2); + auto innerRadius = [&](int r) { return std::min(std::max(0, r - lw), std::max(0, innerMax)); }; + const int itl = innerRadius(tl), itr = innerRadius(tr), ibr = innerRadius(br), ibl = innerRadius(bl); + auto rowSpan = [](int bx, int by, int bw, int bh, int rtl, int rtr, int rbr, int rbl, int sy, int &left, int &right) { + if (bw <= 0 || bh <= 0 || sy < by || sy >= by + bh) return false; + left = bx + bw; + right = bx - 1; + auto corner = [&](int r, int cy, bool leftSide) { + const int dy = sy > cy ? sy - cy : cy - sy; + const int dx = CanvasMath::integerSqrt(r * r - dy * dy); + if (leftSide) left = std::min(left, bx + r - dx); + else right = std::max(right, bx + bw - 1 - r + dx); + }; + const bool inTopLeft = rtl > 0 && sy <= by + rtl, inBottomLeft = rbl > 0 && sy >= by + bh - 1 - rbl; + const bool inTopRight = rtr > 0 && sy <= by + rtr, inBottomRight = rbr > 0 && sy >= by + bh - 1 - rbr; + if (inTopLeft) corner(rtl, by + rtl, true); + if (inBottomLeft) corner(rbl, by + bh - 1 - rbl, true); + if (!inTopLeft && !inBottomLeft) left = bx; + if (inTopRight) corner(rtr, by + rtr, false); + if (inBottomRight) corner(rbr, by + bh - 1 - rbr, false); + if (!inTopRight && !inBottomRight) right = bx + bw - 1; + return left <= right; + }; + auto paint = [&](int sy, int sx0, int sx1) { + if (sx0 < clip->x0) sx0 = clip->x0; + if (sx1 > clip->x1) sx1 = clip->x1; + if (sx0 < 0) sx0 = 0; + if (sx1 >= width_) sx1 = width_ - 1; + if (sx0 <= sx1) fillSpanGlobalAlpha(rowToPhysical(sy), sx0, sx1 - sx0 + 1, color); + }; + int row0 = std::max(std::max(y, clip->y0), 0); + int row1 = std::min(std::min(y + h - 1, clip->y1), height_ - 1); + for (int sy = row0; sy <= row1; ++sy) { + int oL, oR, iL, iR; + if (!rowSpan(x, y, w, h, tl, tr, br, bl, sy, oL, oR)) continue; + if (!rowSpan(ix, iy, iw, ih, itl, itr, ibr, ibl, sy, iL, iR)) { + paint(sy, oL, oR); continue; - int ri = r - lw; - if (ri < 0) ri = 0; - int dy0 = 0; - int dy1 = r; - if (q == 0 || q == 1) { - if (cy - dy0 > clip->y1) dy0 = cy - clip->y1; - if (cy - dy1 < clip->y0) dy1 = cy - clip->y0; - } else { - if (cy + dy0 < clip->y0) dy0 = clip->y0 - cy; - if (cy + dy1 > clip->y1) dy1 = clip->y1 - cy; - } - if (dy0 < 0) dy0 = 0; - if (dy1 > r) dy1 = r; - if (dy0 > dy1) continue; - for (int dy = dy0; dy <= dy1; dy++) { - int dx_outer = CanvasMath::integerSqrt(r * r - dy * dy); - int dx_inner = (dy <= ri) ? CanvasMath::integerSqrt(ri * ri - dy * dy) : 0; - int sx0, sx1, sy; - switch (q) { - case 0: sx0 = cx + dx_inner; sx1 = cx + dx_outer; sy = cy - dy; break; - case 1: sx0 = cx - dx_outer; sx1 = cx - dx_inner; sy = cy - dy; break; - case 2: sx0 = cx - dx_outer; sx1 = cx - dx_inner; sy = cy + dy; break; - case 3: sx0 = cx + dx_inner; sx1 = cx + dx_outer; sy = cy + dy; break; - default: continue; - } - if (sy < clip->y0 || sy > clip->y1) continue; - if (sx0 < clip->x0) sx0 = clip->x0; - if (sx1 > clip->x1) sx1 = clip->x1; - if (sx0 < 0) sx0 = 0; - if (sx1 >= width_) sx1 = width_ - 1; - if (sx0 > sx1 || sy < 0 || sy >= height_) continue; - - fillSpanGlobalAlpha(rowToPhysical(sy), sx0, sx1 - sx0 + 1, color); } + paint(sy, oL, std::min(oR, iL - 1)); + paint(sy, std::max(oL, iR + 1), oR); } markDirtyClipped(x, y, x + w - 1, y + h - 1); } diff --git a/packages/engine/ui/display_invalidation.cpp b/packages/engine/ui/display_invalidation.cpp index 6247a68..7315641 100644 --- a/packages/engine/ui/display_invalidation.cpp +++ b/packages/engine/ui/display_invalidation.cpp @@ -43,6 +43,12 @@ bool DisplayInvalidation::rebuildsNodeDisplayCommands(Property prop) case Property::BackgroundColor: case Property::BackgroundAlpha: case Property::BackgroundClip: + case Property::BackgroundBlendMode: + case Property::BorderImageSource: + case Property::BorderImageSlice: + case Property::BorderImageWidth: + case Property::BorderImageOutset: + case Property::BorderImageRepeat: case Property::BackgroundImage: case Property::Containment: case Property::ActiveBackgroundColor: diff --git a/packages/engine/ui/input.cpp b/packages/engine/ui/input.cpp index 4eaf7c5..294e0b5 100644 --- a/packages/engine/ui/input.cpp +++ b/packages/engine/ui/input.cpp @@ -441,7 +441,7 @@ class InputController { int hitTestNodeId(int id, int x, int y, bool clipped = false, bool groupRoot = true, bool includePositioned = true) { Node &node = Tree::instance().nodes()[id]; - if (node.style.display == 1 || isCollapsedFlexSubtree(node)) return -1; + if (node.style.display == 1 || (node.layout.line_clamp_hidden & 1) || isCollapsedFlexSubtree(node)) return -1; // CSS `pointer-events: none` — this node and its subtree are never the // target of a pointer event, so the hit-test falls through to whatever is // painted behind it. Without this, a decorative overlay image positioned @@ -480,7 +480,7 @@ class InputController { int findScrollNodeId(int id, int x, int y, bool clipped = false, bool groupRoot = true, bool includePositioned = true) { Node &node = Tree::instance().nodes()[id]; - if (node.style.display == 1 || isCollapsedFlexSubtree(node)) return -1; + if (node.style.display == 1 || (node.layout.line_clamp_hidden & 1) || isCollapsedFlexSubtree(node)) return -1; const bool inside = pointInsideNodeHitArea(node, x, y); if (LayoutEngine::isViewportFixed(node)) clipped = false; diff --git a/packages/engine/ui/input_render.cpp b/packages/engine/ui/input_render.cpp index 146bc5b..b88c4b4 100644 --- a/packages/engine/ui/input_render.cpp +++ b/packages/engine/ui/input_render.cpp @@ -156,7 +156,7 @@ void InputRenderer::record(int id) cmd->text.maxWidth = contentW; cmd->text.color = color; cmd->text.scale = textScale; - cmd->text.align = n.style.text_align; + cmd->text.align = static_cast(LayoutEngine::physicalTextAlign(n)); cmd->text.textTransform = n.style.text_transform; cmd->text.lineHeight = n.style.line_height; cmd->text.containerWidth = contentW; @@ -167,6 +167,12 @@ void InputRenderer::record(int id) cmd->text.textOverflow = n.style.text_overflow; cmd->text.maxHeight = 0; cmd->text.firstLineIndent = 0; + cmd->text.alignLast = -1; + cmd->text.lineLimit = 0; + cmd->text.blockEllipsis = 0; + cmd->text.ellipsisWidth = 0; + cmd->text.emphasis = 0; + cmd->text.emphasisColor = 0; } } diff --git a/packages/engine/ui/internal.h b/packages/engine/ui/internal.h index df14394..bf68e56 100644 --- a/packages/engine/ui/internal.h +++ b/packages/engine/ui/internal.h @@ -74,6 +74,9 @@ void rootScrollImageHoldTick(); FillTransformedRoundedRect }; + // DrawText::blockEllipsis for the default ellipsis ("\u2026", or "..."). + constexpr uint16_t kAutoBlockEllipsis = 0xFFFF; + struct DisplayCommand { DisplayCommandType type; @@ -91,6 +94,14 @@ void rootScrollImageHoldTick(); // range. Keep the owner so retained subtree translations can move the // scope with the node (and undo the scroll viewport's own movement). int16_t nodeId; + // A rounded or transformed overflow clip: the padding box lx/ly/lw/lh, + // with its CSS inner radii, mapped to the screen quad qx/qy. The rect + // above is its bounds; replay restores what the clipped content paints + // outside the shape. 0 for a plain rect clip. + uint8_t shaped; + int16_t qx[4], qy[4]; + int16_t lx, ly, lw, lh; + int16_t rx8[4], ry8[4]; } clip; struct { @@ -143,6 +154,8 @@ void rootScrollImageHoldTick(); uint8_t toAlpha; uint16_t toStop; uint8_t hasMid; + // background-blend-mode of the layer; 0 is normal (see BlendMode). + uint8_t blend; } gradient; struct { @@ -155,6 +168,7 @@ void rootScrollImageHoldTick(); uint16_t stopPermille; uint8_t fromAlpha; uint8_t toAlpha; + uint8_t blend; } radialGradient; struct { @@ -200,6 +214,21 @@ void rootScrollImageHoldTick(); // Inline continuation offset for the run's FIRST line only (see // LayoutBox::inline_indent). 0 for every run that starts its own line. int16_t firstLineIndent; + // text-align-last: -1 when every line uses `align`. Otherwise bits 0-1 + // align lines a forced break ends, and bit 2 marks a run whose final + // line also ends its paragraph. + int8_t alignLast; + // line-clamp: paint at most this many lines (0 = all). + int16_t lineLimit; + // block-ellipsis on the last painted line: 0 = none, + // kAutoBlockEllipsis = the default, else a custom string's CSS atom. + // That line may use ellipsisWidth px from x, up to its line box end. + uint16_t blockEllipsis; + int16_t ellipsisWidth; + // text-emphasis marks (ComputedStyle::text_emphasis bits, 0 = none), + // painted in emphasisColor unless the bits say transparent. + uint8_t emphasis; + gea::framework::graphics::pixel::native_t emphasisColor; } text; struct { @@ -255,6 +284,10 @@ void rootScrollImageHoldTick(); int16_t blRx8, blRy8; gea::framework::graphics::pixel::native_t color; uint8_t backfaceHidden; + // Border widths (top, right, bottom, left) when this paints a border + // ring: the padding box, with its CSS inner radii, stays unpainted. + // All zero for a fill. + uint8_t ring[4]; } transformedRoundedRect; struct { @@ -442,6 +475,9 @@ void rootScrollImageHoldTick(); bool layoutNodeScoped(int scope, int treeRoot); void repositionChildren(int id); void resolveAbsoluteCoords(int id, int parentX, int parentY); + // resolveAbsoluteCoords below its entry: `scrollport` is the nearest + // scroll container's padding box (or the viewport) for sticky boxes. + void resolveAbsoluteCoordsIn(int id, int parentX, int parentY, const int *scrollport); // True when `n` is an inline-level box (a span/text/image without an // explicit block-level display) — the inline-formatting input that makes a // plain block flow its children as a row (LayoutNodePass:: @@ -452,6 +488,15 @@ void rootScrollImageHoldTick(); // Inline-level boxes blockified by float, absolute/fixed positioning, or // flex/grid-item status return false. static bool isCssInlineLevelBox(const Node &n, bool hypothetical = false); + // True when the line box closes right after node `id`: a forced break, a + // block-level box, or the end of its block container follows it. + static bool endsFormattingLine(int id); + // A block container with a column count or width (CSS Multi-column). + static bool multicolContainer(const Node &node); + // Resolved direction: true for rtl. + static bool rightToLeftDirection(const Node &node); + // text-align as 0 left, 1 center, 2 right: start and end follow direction. + static int physicalTextAlign(const Node &node); }; // Shared order for recording, retained transform replay, and hit testing. @@ -465,9 +510,22 @@ void rootScrollImageHoldTick(); static std::vector collectChildren(int node, bool groupRoot = true, bool includePositioned = true); }; + // The overflow clip of a node: screen bounds, and for a rounded or + // transformed clip the shape PushClip carries (see DisplayCommand::clip). + struct OverflowClipShape + { + int x, y, w, h; + bool shaped; + int16_t qx[4], qy[4]; + int16_t lx, ly, lw, lh; + int16_t rx8[4], ry8[4]; + }; + class ViewRenderer { public: + // False when the clip cannot be recorded: a perspective-projected box. + static bool overflowClipShape(const Node &node, OverflowClipShape &out); static bool recordClipBegin(const Node &node); static void recordClipEnd(const Node &node); static void recordBox(const Node &node, uint8_t parentAlpha = 255); @@ -517,6 +575,10 @@ void rootScrollImageHoldTick(); static const char *prepareText(const char *text, int textTransform, int whiteSpace, std::string &storage); static void layout(int id, int availableWidth); static int baselineOffset(const Node &node, bool last); + // Font metrics vertical-align works with: the content area around the + // baseline, the x-height, and the strut (the line-height box) of `style`. + struct InlineFontMetrics { int ascent, descent, xHeight, fontSize, strutAscent, strutDescent; }; + static InlineFontMetrics inlineFontMetrics(const ComputedStyle &style); // True when a run's line breaking is reproducible from per-glyph advances, // i.e. the same wrapper the draw path uses. A host that measures whole // strings for us (CoreText on Apple targets) does its own breaking, which @@ -533,7 +595,8 @@ void rootScrollImageHoldTick(); int width, uint8_t *outCoverage); static void drawWrapped(const char *text, int x, int y, int maxWidth, gea::framework::graphics::pixel::native_t color, float scale, int textAlign, int containerWidth, int fontId, int textTransform = 0, int lineHeight = 0, int whiteSpace = 0, int textOverflow = 0, int maxHeight = 0, - int firstLineIndent = 0); + int firstLineIndent = 0, int alignLast = -1, int lineLimit = 0, int blockEllipsis = 0, int ellipsisWidth = 0, + int emphasis = 0, gea::framework::graphics::pixel::native_t emphasisColor = 0); // Single-line width measure for places that don't go through layout() // — used by InputRenderer to position the caret at the end of the value. static int measureWidth(const char *text, int fontId, int fontSize, int textTransform = 0); diff --git a/packages/engine/ui/layout.cpp b/packages/engine/ui/layout.cpp index c03a8fc..6aca02b 100644 --- a/packages/engine/ui/layout.cpp +++ b/packages/engine/ui/layout.cpp @@ -300,6 +300,175 @@ bool hasExplicitHeight(const Node &node) !layoutSizeExpressionUsesPercentage(static_cast(&node - Tree::instance().nodes()), node.style.height_expression); } +// Line clamping (CSS Overflow 4). A block container with continue: collapse +// hides everything after its clamp point, which follows its max-lines'th line +// or, without max-lines, the last content that fits its used height. Lines of +// inline wrappers and plain block descendants count; independent formatting +// contexts are single units. Child coordinates are still parent-relative. +int writingMode(const Node &node); +bool emptyInlineBox(const Node &node); + +struct LineClampWalk { + Node *nodes; + int maxLines; + int limit; + int lines = 0; + int lastLineTop = INT_MIN; + int keptBottom = 0; + // The text run whose line is the last one kept; -1 when that is an atomic box. + int lastRun = -1; + // Lines and boxes kept so far. + int kept = 0; + bool clamped = false; + // The blocks the walk is inside, innermost last. Clamping removes the rest + // of their content, so each still ends with its bottom border and padding, + // its min-height and its bottom margin. + struct OpenBlock { int top, inset, minHeight, margin; }; + std::vector open; + // Where the container's content ends when clamped after content ending at + // `bottom` with bottom margin `margin`. Margins collapse outwards through + // blocks without bottom border, padding or min-height (CSS 2.2 8.3.1); the + // container is a BFC root, so the outermost margin stays inside it. + int endAfter(int bottom, int margin) const + { + for (auto it = open.rbegin(); it != open.rend(); ++it) { + if (it->inset > 0 || it->minHeight > 0) { bottom += margin; margin = 0; } + bottom = std::max(bottom + it->inset, it->top + it->minHeight); + margin = std::max(margin, it->margin); + } + return bottom + margin; + } +}; + +void hideAfterLineClamp(Node *nodes, int id) +{ + nodes[id].layout.line_clamp_hidden = 1; + for (int c = nodes[id].first_child; c >= 0; c = nodes[c].next_sibling) hideAfterLineClamp(nodes, c); +} + +// line-clamp applies to block containers, never to inline boxes, flex or +// grid containers, or multicol containers. +bool clampsLines(const Node &node) +{ + const int flags = rstyle(node.style).line_clamp_flags; + return (flags & 1) && !(flags & 12) && node.type == NodeType::View && node.style.display == kDisplayBlock && + !(LayoutEngine::isCssInlineLevelBox(node) && LayoutEngine::isInlineLevelNode(node)); +} + +// A nested clamp container keeps the state inside it for its own layout. +void clearLineClamp(Node *nodes, int id) +{ + for (int c = nodes[id].first_child; c >= 0; c = nodes[c].next_sibling) { + nodes[c].layout.line_clamp_hidden = 0; + nodes[c].layout.line_clamp_lines = 0; + if (!clampsLines(nodes[c])) clearLineClamp(nodes, c); + } +} + +void walkLineClamp(LineClampWalk &walk, int parent, int originY) +{ + Node *nodes = walk.nodes; + for (int id = nodes[parent].first_child; id >= 0; id = nodes[id].next_sibling) { + Node &child = nodes[id]; + if (walk.clamped) { + hideAfterLineClamp(nodes, id); + continue; + } + if (isDisplayNone(child.style) || isOutOfFlowPosition(child.style.position) || child.style.float_side || + isLineBreak(child) || suppressAnonymousWhitespace(nodes, parent, id)) continue; + const bool inlineWrapper = LayoutEngine::isCssInlineLevelBox(child) && LayoutEngine::isInlineLevelNode(child); + // Phantom line content (collapsible spaces, empty inline boxes) is + // treated as not existing, so it neither counts as a line nor clamps. + const bool collapsible = child.style.white_space == 0 || child.style.white_space == 1 || child.style.white_space == 4; + if (child.type == NodeType::Text ? collapsible && child.text.find_first_not_of(" \t\r\n\f") == std::string::npos + : inlineWrapper && child.first_child < 0 && emptyInlineBox(child)) continue; + const int top = originY + child.layout.y; + // An empty block's margins collapse through it; with clearance the + // collapsed margin sits above its border edge (CSS 2.2 8.3.1). + auto bottomMargin = [&](bool empty) { + if (inlineWrapper) return 0; + return empty && child.layout.height == 0 ? std::max(0, child.style.margin[2] - child.style.margin[0]) : int(child.style.margin[2]); + }; + if (child.type == NodeType::Text) { + const int advance = TextRenderer::measureHeight("X", child.style.font_id, child.style.font_size, 0, child.style.line_height); + const int inner = child.layout.height - boxInsets(child.style, false); + const int count = advance > 0 ? std::max(1, (inner + advance - 1) / advance) : 1; + for (int line = 0; line < count; ++line) { + const int lineTop = top + boxInset(child.style, 0) + line * advance; + const bool newLine = lineTop > walk.lastLineTop; + const int end = walk.endAfter(lineTop + advance, 0); + if (walk.maxLines ? newLine && walk.lines >= walk.maxLines : end > walk.limit) { + walk.clamped = true; + if (line == 0) hideAfterLineClamp(nodes, id); + else child.layout.line_clamp_lines = static_cast(line); + break; + } + if (newLine) { + ++walk.lines; + walk.lastLineTop = lineTop; + } + walk.keptBottom = std::max(walk.keptBottom, end); + walk.lastRun = id; + ++walk.kept; + } + continue; + } + const bool plainBlock = child.style.display == kDisplayBlock && !overflowEstablishesContext(child.style) && + !isFlowRoot(child.style) && child.style.align_content == 0 && !clampsLines(child) && + writingMode(child) == writingMode(nodes[parent]); + if (child.first_child >= 0 && (inlineWrapper || plainBlock)) { + // A block between the last line box and the clamp point takes the + // ellipsis away from that line; its own lines may take it instead. + const int lastRun = walk.lastRun; + if (!inlineWrapper) walk.lastRun = -1; + const int kept = walk.kept; + if (!inlineWrapper) { + const int minHeight = child.style.min_height > 0 ? contentSizeToBorderSize(child.style, child.style.min_height, false) : 0; + walk.open.push_back({top, boxInset(child.style, 2), minHeight, child.style.margin[2]}); + } + walkLineClamp(walk, id, top); + if (!inlineWrapper) walk.open.pop_back(); + // A box whose first content already follows the clamp point is + // hidden whole, backgrounds and borders included. It is not between + // the last line and the clamp point, so that line keeps the ellipsis. + if (walk.clamped && walk.kept == kept) { + hideAfterLineClamp(nodes, id); + walk.lastRun = lastRun; + } + if (!walk.clamped) { + if (!inlineWrapper) walk.keptBottom = std::max(walk.keptBottom, walk.endAfter(top + child.layout.height, bottomMargin(walk.kept == kept))); + ++walk.kept; + } + continue; + } + const int end = walk.endAfter(top + child.layout.height, bottomMargin(child.first_child < 0)); + if (walk.maxLines ? walk.lines >= walk.maxLines : end > walk.limit) { + walk.clamped = true; + hideAfterLineClamp(nodes, id); + continue; + } + walk.keptBottom = std::max(walk.keptBottom, end); + walk.lastRun = -1; + ++walk.kept; + } +} + +void applyLineClamp(int id) +{ + Node *nodes = Tree::instance().nodes(); + Node &node = nodes[id]; + clearLineClamp(nodes, id); + LineClampWalk walk{nodes, rstyle(node.style).max_lines, node.layout.height - boxInset(node.style, 2)}; + walk.keptBottom = boxInset(node.style, 0); + walkLineClamp(walk, id, 0); + // The block ellipsis ends the last line before the clamp point. + if (walk.clamped && (rstyle(node.style).line_clamp_flags & 2) && walk.lastRun >= 0) + nodes[walk.lastRun].layout.line_clamp_hidden |= 2; + if (!walk.clamped || hasExplicitHeight(node)) return; + const int height = clampBorderBoxSize(node.style, walk.keptBottom + boxInset(node.style, 2), false); + if (height < node.layout.height) node.layout.height = clampLayoutExtent(height); +} + int resolvedStyleWidth(const Node &node, int basis) { if (const auto *scope = flexBasisScope(node, true)) return scope->value == kUnset ? basis : scope->value; @@ -386,8 +555,14 @@ int ratioTransferredSize(const Node &node, int crossBorderSize, bool horizontal) { const double ratio = preferredRatio(node); if (ratio <= 0) return 0; - const int crossEdges = node.style.box_sizing ? 0 : boxInsets(node.style, !horizontal); - const int mainEdges = node.style.box_sizing ? 0 : boxInsets(node.style, horizontal); + // `auto ` (stored negative) sizes the content box whatever box-sizing + // says (CSS Sizing 4, 5.1). + float stored = 0; + const int bits = rstyle(node.style).aspect_ratio; + std::memcpy(&stored, &bits, sizeof(stored)); + const bool contentBox = !node.style.box_sizing || stored < 0; + const int crossEdges = contentBox ? boxInsets(node.style, !horizontal) : 0; + const int mainEdges = contentBox ? boxInsets(node.style, horizontal) : 0; const double value = std::max(0, crossBorderSize - crossEdges) * (horizontal ? ratio : 1 / ratio) + mainEdges; return static_cast(std::min(32767.0, std::round(value))); } @@ -403,7 +578,7 @@ bool definiteFlexCrossSize(const Node &parent, const Node &child, bool row) void applyPreferredRatio(Node &node, int availableWidth, int availableHeight) { if (!preferredRatio(node) || (node.type != NodeType::View && node.type != NodeType::Image)) return; - if (node.type != NodeType::Image && LayoutEngine::isInlineLevelNode(node)) return; + if (node.type != NodeType::Image && !isAtomicInline(node.style) && LayoutEngine::isInlineLevelNode(node)) return; bool width = hasExplicitWidth(node), height = hasExplicitHeight(node); if (width && height) return; const Node *parent = node.parent >= 0 ? &Tree::instance().nodes()[node.parent] : nullptr; @@ -428,24 +603,27 @@ void applyPreferredRatio(Node &node, int availableWidth, int availableHeight) width = true; } const bool horizontal = !width && height; - int16_t &target = horizontal ? node.layout.width : node.layout.height; + int32_t &target = horizontal ? node.layout.width : node.layout.height; const int fromRatio = ratioTransferredSize(node, horizontal ? node.layout.height : node.layout.width, horizontal); // Non-scrollable boxes retain a content-based minimum on the automatic axis. const bool contentMinimum = node.type != NodeType::Image && (horizontal ? node.style.min_width : node.style.min_height) == kUnset && !isScrollableOverflow(horizontal ? overflowX(node.style) : overflowY(node.style)); const int result = contentMinimum ? std::max(target, fromRatio) : fromRatio; - target = clampInt16(clampLayoutSize(node, result, horizontal)); + target = clampLayoutExtent(clampLayoutSize(node, result, horizontal)); } int baselineFallbackAlignment(const Node &parent, const Node &child, int alignment, int free, bool horizontal, bool staticPosition) { - // Baseline self-alignment follows the subject's writing mode. Resolving - // automatic insets uses the fallback edge without overflow safety; actual - // in-flow self-alignment uses the safe fallback instead (CSS Position 3.5.1). + // In-flow baseline self-alignment falls back to self-start/self-end in the + // subject's writing mode. An absolutely positioned box shares no baseline; + // browsers resolve its fallback to the containing box's start/end, even when + // the box's own direction or writing mode differs. Resolving automatic insets + // uses the fallback edge without overflow safety; actual in-flow + // self-alignment uses the safe fallback instead (CSS Position 3.5.1). if (!staticPosition && free < 0) return gridAxisReversed(parent, horizontal) ? 2 : 6; const bool last = (alignment & 15) == kAlignLastBaseline; - return gridAxisReversed(child, horizontal) != last ? 2 : 6; + return gridAxisReversed(staticPosition ? parent : child, horizontal) != last ? 2 : 6; } // Self-relative edges use the subject's writing mode, even when it differs @@ -479,6 +657,27 @@ int usedGridAlignment(const Node &parent, const Node &child, int alignment, int return usedAlignment(alignment, free, gridAxisReversed(parent, horizontal)); } +// Half of a free space, rounded half up like a fractional edge snapped to the +// nearest pixel (12.5 -> 13, -12.5 -> -12). +int roundedHalf(int free) { return free >= -1 ? (free + 1) / 2 : -(-free / 2); } + +// An absolute box with both insets of an axis set and an explicit, non-normal +// self-alignment places its margin box inside the inset-modified containing +// block (CSS Position 3, 4.4; CSS Align 3). offset is measured from the +// containing area's start. Auto margins, normal and stretch keep the CSS 2 +// placement from the start inset. +bool insetAlignedOffset(const Node &containing, const Node &absolute, bool horizontal, int areaSize, int start, int end, int &offset) +{ + // justify-self works in the containing block's inline axis, align-self in its block axis. + const int align = horizontal == (writingMode(containing) == 0) ? rstyle(absolute.style).justify_self : absolute.style.align_self; + if (align <= 0 || (absolute.style.margin_auto & (horizontal ? 10 : 5))) return false; + const int before = absolute.style.margin[horizontal ? 3 : 0], after = absolute.style.margin[horizontal ? 1 : 2]; + const int free = areaSize - start - end - (horizontal ? absolute.layout.width : absolute.layout.height) - before - after; + const int used = usedGridAlignment(containing, absolute, align, free, horizontal); + offset = start + before + (used == 1 ? roundedHalf(free) : used == 2 ? free : 0); + return true; +} + void gridPhysicalRange(const Node &parent, bool columns, int start, int end, int &offset, int &size) { const bool horizontal = columns == (writingMode(parent) == 0); @@ -492,8 +691,13 @@ bool flexRowDirection(const ComputedStyle &style) return !style.flex_direction_explicit || (style.flex_direction & 1); } +bool isMulticolContainer(const Node &node); +bool establishesAbsoluteContainingBlock(const Node &node); + int resolvedGap(const Node &node, bool rowGap, int width, int height) { + // A multicol container's column-gap separates its column boxes, not its content. + if (isMulticolContainer(node)) return 0; const int length = rowGap ? node.style.row_gap : node.style.column_gap; const int percent = rowGap ? node.style.row_gap_percent : node.style.column_gap_percent; const bool vertical = writingMode(node) != 0; @@ -511,6 +715,46 @@ int clampBorderSize(LayoutEngine &engine, const Node &node, int size, bool horiz return clampLayoutSize(node, size, horizontal); } +static bool isInlineLevelTag(const char *tag); +static bool inlineBoxView(const Node &n); +bool blockLevelView(const Node &node); +bool splitInlineWrapper(const Node *nodes, int id); + +// The physical side (0 top, 1 right, 2 bottom, 3 left) of a margin-trim flag +// bit of a container: block-start, block-end, inline-start, inline-end. +int marginTrimSide(const Node &container, int flag) +{ + const int mode = writingMode(container); + const bool inlineAxis = flag >= 2, end = flag & 1; + const bool reversed = inlineAxis ? rightToLeft(container) != (mode == 4) : mode == 2 || mode == 3; + const int start = inlineAxis == (mode == 0) ? 3 : 0; + return reversed != end ? (start + 2) % 4 : start; +} + +// Margins zeroed by margin-trim for one layout pass, restored when it ends. +struct TrimmedMargins { + struct Entry { int child, side; int16_t value; bool automatic; }; + std::vector entries; + void cut(int child, int side) + { + Node &item = Tree::instance().nodes()[child]; + if (!item.style.margin[side] && !(item.style.margin_auto & (1 << side))) return; + entries.push_back({child, side, item.style.margin[side], static_cast(item.style.margin_auto & (1 << side))}); + item.style.margin[side] = 0; + item.style.margin_auto &= static_cast(~(1 << side)); + } + void restore() + { + Node *nodes = Tree::instance().nodes(); + for (auto it = entries.rbegin(); it != entries.rend(); ++it) { + nodes[it->child].style.margin[it->side] = it->value; + if (it->automatic) nodes[it->child].style.margin_auto |= static_cast(1 << it->side); + } + entries.clear(); + } + ~TrimmedMargins() { restore(); } +}; + class FlexLayoutPass { public: FlexLayoutPass(LayoutEngine &engine, Node &node, int *children, int childCount, bool isRow, int mainAvail, int padWidth, int padHeight, FlexLine *lines, bool inlineRow = false, bool assignedSize = false) @@ -566,8 +810,8 @@ class FlexLayoutPass { applyFlexBasis(child); Node &item = Tree::instance().nodes()[child]; const int floor = automaticMinimumMainSize(item); - int16_t &main = isRow_ ? item.layout.width : item.layout.height; - if (main < floor) { main = clampInt16(floor); engine_.repositionChildren(child); } + int32_t &main = isRow_ ? item.layout.width : item.layout.height; + if (main < floor) { main = clampLayoutExtent(floor); engine_.repositionChildren(child); } } } } @@ -576,6 +820,9 @@ class FlexLayoutPass { { const int originalWidth = node_.layout.width, originalHeight = node_.layout.height; int lineCount = buildLines(); + // Trimmed margins stay zero until this pass ends. + TrimmedMargins trimmed; + trimEdgeMargins(lineCount, trimmed); expandCrossSizes(lineCount, autosize); if (autosize) autosizeParent(lineCount); positionLines(lineCount); @@ -660,6 +907,7 @@ class FlexLayoutPass { { Node *nodes = Tree::instance().nodes(); const int parentId = static_cast(&node_ - nodes); + widenedRuns_.clear(); int firstLineStyleOwner = -1; for (int ancestor = parentId; ancestor >= 0; ancestor = nodes[ancestor].parent) { const NodeRareData *rare = rareDataFor(ancestor); @@ -701,17 +949,29 @@ class FlexLayoutPass { } std::vector> staticBoundaries; int flowIndex = 0; - for (int child = node_.first_child; child >= 0; child = nodes[child].next_sibling) { - if (isDisplayNone(nodes[child].style) || suppressAnonymousWhitespace(nodes, parentId, child)) continue; - if (isOutOfFlowPosition(nodes[child].style.position)) { - if (auto *rare = rareDataFor(child)) rare->inlineStaticPosition.valid = false; - if (LayoutEngine::isCssInlineLevelBox(nodes[child], true)) - staticBoundaries.emplace_back(flowIndex, child); - continue; + // Split inline wrappers lend their children to this flow, out-of-flow + // ones included. Inside an inline box block-level boxes take static + // positions from the lines too, as it has no block layout of its own. + const bool inlineBox = LayoutEngine::isCssInlineLevelBox(node_) && LayoutEngine::isInlineLevelNode(node_); + auto collectStaticBoundaries = [&](auto &&self, int parent) -> void { + for (int child = nodes[parent].first_child; child >= 0; child = nodes[child].next_sibling) { + if (isDisplayNone(nodes[child].style) || suppressAnonymousWhitespace(nodes, parent, child)) continue; + if (isOutOfFlowPosition(nodes[child].style.position)) { + if (auto *rare = rareDataFor(child)) rare->inlineStaticPosition.valid = false; + if (parent != parentId || inlineBox || LayoutEngine::isCssInlineLevelBox(nodes[child], true)) + staticBoundaries.emplace_back(flowIndex, child); + continue; + } + if (splitInlineWrapper(nodes, child) && + std::find(children_, children_ + childCount_, child) == children_ + childCount_) { + self(self, child); + continue; + } + while (flowIndex < childCount_ && children_[flowIndex] != child) ++flowIndex; + if (flowIndex < childCount_) ++flowIndex; } - while (flowIndex < childCount_ && children_[flowIndex] != child) ++flowIndex; - if (flowIndex < childCount_) ++flowIndex; - } + }; + collectStaticBoundaries(collectStaticBoundaries, parentId); int contentW = mainAvail_ < 0 ? 0 : mainAvail_; if (intrinsicWidthConstraint(node_) == 1) { int pendingWord = 0; @@ -757,6 +1017,9 @@ class FlexLayoutPass { continuationLine && LayoutEngine::isCssInlineLevelBox(node_); rare.inlineStaticPosition.x = clampInt16( (rare.inlineStaticPosition.continuationLine ? 0 : contentLeft) + pen); + rare.inlineStaticPosition.afterContent = pen > 0 || continuationLine; + rare.inlineStaticPosition.anchorRight = false; + rare.inlineStaticPosition.flowNode = static_cast(parentId); } }; @@ -772,6 +1035,8 @@ class FlexLayoutPass { int wrapSpans = 0; int wrapAdvance = 0; int wrapLastWidth = 0; + bool lineAligned = false; + int topItemHeight = 0, bottomItemHeight = 0; std::vector lineStaticChildren; for (; j < childCount_; j++) { @@ -803,7 +1068,8 @@ class FlexLayoutPass { // not of the line it can fill. const int rest = contentW - marginL - marginR - padH; const int firstAvail = rest - pen - gap; - const bool atLineStart = pen == 0 && (j > 0 || blockOpensItsFirstLine); + // A leading space also collapses into one that ends the item before it. + const bool atLineStart = pen == 0 ? j > 0 || blockOpensItsFirstLine : j > 0 && endsWithCollapsibleSpace(nodes[children_[j - 1]]); const InlineFlowMeasure m = TextRenderer::measureInlineFlow(subject, firstAvail, rest, atLineStart); // Without real line metrics childFirstBaseline reports the box's @@ -815,7 +1081,7 @@ class FlexLayoutPass { if (m.lineCount > 0 && haveLineMetrics) { // Its first WORD does not fit what is left: the run starts the // next line box rather than overflowing it or being cut open. - if (pen > 0 && m.firstLineWidth > firstAvail) break; + if (pen > 0 && m.firstLineWidth > firstAvail && !collapsibleSpaceOnly(subject)) break; const int boxH = m.lineCount * m.lineAdvance + padV; const bool wraps = m.lineCount > 1; const int boxW = wraps ? rest + padH : m.firstLineWidth + padH; @@ -827,8 +1093,8 @@ class FlexLayoutPass { // whitespace; that discarded space is not part of the fragment. const int firstLineFragmentX = fragmentItemX + (wraps ? pen + gap : 0); const int firstLineFragmentWidth = m.firstLineWidth + padH; - subject.layout.height = clampInt16(boxH); - subject.layout.width = clampInt16(boxW); + subject.layout.height = clampLayoutExtent(boxH); + subject.layout.width = clampLayoutExtent(boxW); subject.layout.inline_indent = clampInt16(indent); if (innerRun >= 0) { // The wrapper is transparent: it takes the run's box exactly, and @@ -836,8 +1102,8 @@ class FlexLayoutPass { // through the wrapper still lands on the run's own baseline. subject.layout.x = 0; subject.layout.y = 0; - cn.layout.height = clampInt16(boxH); - cn.layout.width = clampInt16(boxW); + cn.layout.height = clampLayoutExtent(boxH); + cn.layout.width = clampLayoutExtent(boxW); cn.layout.inline_indent = clampInt16(indent); } if (wraps) { @@ -863,13 +1129,21 @@ class FlexLayoutPass { cn.layout.inline_indent = 0; } - const int itemMain = cn.layout.width + marginL + marginR; - if (pen > 0 && pen + gap + itemMain > contentW) break; + const int lead = collapsedLead(nodes, j, pen == 0 && !lineIsContinuation); + const int itemMain = cn.layout.width + marginL + marginR - lead; + // An item starts the next line only where the line may break, and only + // when it does not fit together with what no break opportunity + // separates from it: nowrap text and words split across items stay + // together. Collapsible spaces hang instead. + if (pen > 0 && !collapsibleSpaceOnly(cn) && + (j == 0 || trailingSoftWrap(nodes[children_[j - 1]]) || leadingSoftWrap(cn)) && + pen + gap + (isFragmentableRun(subject) ? itemMain : gluedWidth(nodes, j, false)) > contentW) break; if (isFragmentableRun(subject) && captureFirstLine && i == 0) { const int padH = boxInset(subject.style, 1) + boxInset(subject.style, 3); const int rest = contentW - marginL - marginR - padH; const InlineFlowMeasure m = TextRenderer::measureInlineFlow( - subject, rest - pen - gap, rest, pen == 0 && (j > 0 || blockOpensItsFirstLine)); + subject, rest - pen - gap, rest, + pen == 0 ? j > 0 || blockOpensItsFirstLine : j > 0 && endsWithCollapsibleSpace(nodes[children_[j - 1]])); if (m.lineCount > 0 && m.firstLineWidth > 0) { firstLineCandidates.push_back({innerRun >= 0 ? innerRun : child, contentLeft + pen + gap + marginL, @@ -878,12 +1152,46 @@ class FlexLayoutPass { j, m.firstLineTrailingSpace}); } } - const int ascent = cn.style.margin[0] + childBaseline(cn); - const int descent = cn.layout.height + cn.style.margin[2] - childBaseline(cn); - if (ascent > lineAscent) lineAscent = ascent; - if (descent > lineDescent) lineDescent = descent; + const int outerHeight = cn.style.margin[0] + cn.layout.height + cn.style.margin[2]; + if (cn.style.vertical_align) lineAligned = true; + if (cn.style.vertical_align == 1) topItemHeight = std::max(topItemHeight, outerHeight); + else if (cn.style.vertical_align == 2) bottomItemHeight = std::max(bottomItemHeight, outerHeight); + else { + int ascent = 0, descent = 0; + lineExtent(cn, ascent, descent); + if (ascent > lineAscent) lineAscent = ascent; + if (descent > lineDescent) lineDescent = descent; + } pen += gap + itemMain; } + // A line with vertically aligned items is sized as CSS sizes it: from + // the block's strut and every item's reach, which may be negative when + // the line-height is below the font size. top/bottom items only grow a + // line shorter than themselves. + if (lineAligned) { + const auto strut = TextRenderer::inlineFontMetrics(node_.style); + lineAscent = lineIsContinuation ? std::max(strut.strutAscent, inheritedAscent) : strut.strutAscent; + lineDescent = lineIsContinuation ? std::max(strut.strutDescent, inheritedDescent) : strut.strutDescent; + for (int k = i; k < j; ++k) { + const Node &item = nodes[children_[k]]; + if (item.style.vertical_align == 1 || item.style.vertical_align == 2) continue; + int ascent = 0, descent = 0; + if (k == wrapIdx) { + const int run = isFragmentableRun(item) ? -1 : transparentInlineRun(item); + ascent = item.style.margin[0] + childBaseline(item); + descent = runLineDescent(run >= 0 ? nodes[run] : item, wrapAdvance); + } else if (isLineBreak(item)) { + ascent = childBaseline(item); + descent = item.layout.height - childBaseline(item); + } else { + lineExtent(item, ascent, descent); + } + lineAscent = std::max(lineAscent, ascent); + lineDescent = std::max(lineDescent, descent); + } + if (bottomItemHeight > lineAscent + lineDescent) lineAscent = bottomItemHeight - lineDescent; + if (topItemHeight > lineAscent + lineDescent) lineDescent = topItemHeight - lineAscent; + } const bool endedWithBreak = j > i && isLineBreak(nodes[children_[j - 1]]); const bool trailingWrappedStaticBoundary = j >= childCount_ && !endedWithBreak && wrapIdx >= 0; if (j >= childCount_ && !endedWithBreak && !trailingWrappedStaticBoundary) @@ -893,14 +1201,15 @@ class FlexLayoutPass { const int baseline = continuing ? continuedBaseline : lineTop + lineAscent; lineTop = baseline - lineAscent; + int lineCross = lineAscent + lineDescent; + if (lineCross < 0) lineCross = 0; + const int actualLineCross = lineCross; for (int id : lineStaticChildren) { auto &position = ensureRareData(id).inlineStaticPosition; position.y = clampInt16(lineTop); + position.lineHeight = clampInt16(actualLineCross); position.valid = true; } - int lineCross = lineAscent + lineDescent; - if (lineCross < 0) lineCross = 0; - const int actualLineCross = lineCross; if (i == 0) firstLineHeight = lineCross; // The one case the flex path's expandCrossSizes is observable here: a // lone line box in a block with a definite height fills it, so @@ -909,8 +1218,19 @@ class FlexLayoutPass { padHeight_ > lineCross) lineCross = padHeight_; + // text-align moves a line of single-line items as a whole. A text run + // that owns its line, or wraps across lines, is aligned by the drawer. + const int contentEnd = j > i && isLineBreak(nodes[children_[j - 1]]) ? j - 1 : j; + const bool soleRun = !lineIsContinuation && contentEnd - i == 1 && nodes[children_[i]].type == NodeType::Text; + const int align = !lineIsContinuation && wrapIdx < 0 && !soleRun && contentEnd > i ? LayoutEngine::physicalTextAlign(node_) : 0; + int lineShift = 0; + if (align) { + const int used = pen - hangingSpace(nodes[children_[contentEnd - 1]]); + lineShift = std::max(0, align == 1 ? (contentW - used) / 2 : contentW - used); + for (int id : lineStaticChildren) ensureRareData(id).inlineStaticPosition.x += lineShift; + } placeLineItems(nodes, i, j, contentLeft, contentW, lineTop, lineCross, baseline, penStart, - !lineIsContinuation); + !lineIsContinuation, lineShift, align != 0); if (i == 0 && captureFirstLine) { int firstFormattedLineHeight = actualLineCross; if (!firstLineCandidates.empty()) firstFormattedLineHeight = 0; @@ -939,7 +1259,7 @@ class FlexLayoutPass { for (const auto &candidate : firstLineCandidates) { if (candidate.node < 0 || candidate.node >= Tree::instance().nodeCount()) continue; auto &fragment = ensureRareData(candidate.node).firstLineFragment; - fragment.x = clampInt16(candidate.x); + fragment.x = clampInt16(candidate.x + lineShift); fragment.y = clampInt16(lineTop); // Spaces between inline runs remain part of the background; // collapsible spaces at the end of the line do not. @@ -968,6 +1288,7 @@ class FlexLayoutPass { for (int id : trailingStaticChildren) { auto &position = ensureRareData(id).inlineStaticPosition; position.y = clampInt16(continuedBaseline - inheritedAscent); + position.lineHeight = 0; position.valid = true; } } @@ -987,6 +1308,7 @@ class FlexLayoutPass { for (int id : trailingStaticChildren) { auto &position = ensureRareData(id).inlineStaticPosition; position.y = clampInt16(lineTop); + position.lineHeight = 0; position.valid = true; } } @@ -1004,6 +1326,7 @@ class FlexLayoutPass { for (int id : emptyStaticChildren) { auto &position = ensureRareData(id).inlineStaticPosition; position.y = clampInt16(contentTop); + position.lineHeight = 0; position.valid = true; } } @@ -1017,11 +1340,16 @@ class FlexLayoutPass { const bool keepScrollWidth = !intrinsicWidthConstraint(node_) && scrollsOverflowX(node_.style) && node_.layout.width > 0; if (!hasExplicitHeight(node_) && !keepScrollHeight) { const int height = (flowHeight < 0 ? 0 : flowHeight) + boxInset(node_.style, 0) + boxInset(node_.style, 2); - node_.layout.height = clampInt16(clampLayoutSize(node_, height, false)); + node_.layout.height = clampLayoutExtent(clampLayoutSize(node_, height, false)); } if (!hasExplicitWidth(node_) && !keepScrollWidth) { const int width = maxLineWidth + boxInset(node_.style, 1) + boxInset(node_.style, 3); - node_.layout.width = clampInt16(clampLayoutSize(node_, width, true)); + node_.layout.width = clampLayoutExtent(clampLayoutSize(node_, width, true)); + // A widened sole run spans the box's final content width instead, so + // its extent does not hold the box at the width it was offered. + const int content = node_.layout.width - boxInsets(node_.style, true); + for (const WidenedRun &run : widenedRuns_) + nodes[run.id].layout.width = clampLayoutExtent(std::max(run.natural, content + run.extra)); } return extent; } @@ -1093,6 +1421,88 @@ class FlexLayoutPass { return run; } + // The text node at an inline item's start or end, found through inline + // boxes, or -1 for an atomic inline box. + static int edgeTextRun(const Node &item, bool last) + { + const Node *nodes = Tree::instance().nodes(); + const Node *n = &item; + while (n->type == NodeType::View) { + if (!inlineBoxView(*n) || isLineBreak(*n)) return -1; + int c = last ? n->last_child : n->first_child; + while (c >= 0 && (isDisplayNone(nodes[c].style) || isOutOfFlowPosition(nodes[c].style.position) || nodes[c].style.float_side)) + c = last ? nodes[c].prev_sibling : nodes[c].next_sibling; + if (c < 0) return -1; + n = &nodes[c]; + } + return static_cast(n - nodes); + } + + static bool wrapsLines(const Node &text) { return text.style.white_space == 0 || text.style.white_space >= 3; } + static bool collapsesSpaces(const Node &text) { return text.style.white_space == 0 || text.style.white_space == 1 || text.style.white_space == 4; } + static bool collapsibleSpace(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; } + + // Collapsible spaces hang at the end of a line; they never start another. + static bool collapsibleSpaceOnly(const Node &item) + { + return item.type == NodeType::Text && (item.style.white_space == 0 || item.style.white_space == 1) && + item.text.find_first_not_of(" \t\r\n\f") == std::string::npos; + } + + // Soft wrap opportunities at an item's edges. Text only offers one at a + // space its white-space lets wrap; atomic inline boxes always do. + static bool leadingSoftWrap(const Node &item) + { + const int run = edgeTextRun(item, false); + if (run < 0) return true; + const Node &text = Tree::instance().nodes()[run]; + return !text.text.empty() && collapsibleSpace(text.text.front()) && wrapsLines(text); + } + + static bool trailingSoftWrap(const Node &item) + { + const int run = edgeTextRun(item, true); + if (run < 0) return true; + const Node &text = Tree::instance().nodes()[run]; + return !text.text.empty() && collapsibleSpace(text.text.back()) && wrapsLines(text); + } + + // A collapsible space that opens an atomic text item is removed at the + // start of a line, and collapses into one that ends the text before it. + // The item keeps its box and draws over that space; this is how far it + // moves back. + int collapsedLead(const Node *nodes, int k, bool lineStart) const + { + const Node &item = nodes[children_[k]]; + if (item.type != NodeType::Text || isFragmentableRun(item) || item.text.empty() || + !collapsibleSpace(item.text.front()) || !collapsesSpaces(item)) return 0; + if (!lineStart) { + const int before = k > 0 ? edgeTextRun(nodes[children_[k - 1]], true) : -1; + if (before < 0) return 0; + const Node &previous = nodes[before]; + if (previous.text.empty() || !collapsibleSpace(previous.text.back()) || !collapsesSpaces(previous)) return 0; + } + return TextRenderer::measureWidth(" ", item.style.font_id, item.style.font_size); + } + + // Width of children_[j] and of the items after it that no soft wrap + // opportunity separates from it; a text run that follows adds its first word. + int gluedWidth(const Node *nodes, int j, bool lineStart) const + { + int width = 0; + for (int k = j; k < childCount_; ++k) { + const Node &item = nodes[children_[k]]; + if (k > j && (isLineBreak(item) || trailingSoftWrap(nodes[children_[k - 1]]) || leadingSoftWrap(item))) break; + const int inner = isFragmentableRun(item) ? -1 : transparentInlineRun(item); + if (k > j && (isFragmentableRun(item) || inner >= 0)) { + width += item.style.margin[3] + TextRenderer::firstUnbreakableWidth(inner >= 0 ? nodes[inner] : item); + break; + } + width += item.layout.width + item.style.margin[1] + item.style.margin[3] - collapsedLead(nodes, k, lineStart && k == j); + } + return width; + } + // Distance from ONE of a run's line baselines to the bottom of that line box, // including the run's own bottom padding/margin (which only the last line // actually carries — the others are flush, so this is the taller, safe value). @@ -1103,8 +1513,26 @@ class FlexLayoutPass { return descent < 0 ? 0 : descent; } + static bool endsWithCollapsibleSpace(const Node &item) + { + const int run = edgeTextRun(item, true); + if (run < 0) return false; + const Node &text = Tree::instance().nodes()[run]; + return !text.text.empty() && collapsibleSpace(text.text.back()) && collapsesSpaces(text); + } + + // The collapsible space that ends a line item hangs past the line's end. + static int hangingSpace(const Node &item) + { + const int run = edgeTextRun(item, true); + if (run < 0) return 0; + const Node &text = Tree::instance().nodes()[run]; + if (text.text.empty() || !collapsibleSpace(text.text.back()) || !collapsesSpaces(text)) return 0; + return TextRenderer::measureWidth(" ", text.style.font_id, text.style.font_size); + } + void placeLineItems(Node *nodes, int from, int to, int contentLeft, int contentW, int lineTop, - int lineCross, int baseline, int penStart, bool ownsLineStart) + int lineCross, int baseline, int penStart, bool ownsLineStart, int lineShift = 0, bool lineAligned = false) { const int contentEnd = to > from && isLineBreak(nodes[children_[to - 1]]) ? to - 1 : to; const bool soleRun = @@ -1119,23 +1547,33 @@ class FlexLayoutPass { const int marginL = cn.style.margin[3]; const int marginR = cn.style.margin[1]; const int gap = pen > 0 ? mainGap_ : 0; + int lead = 0; if (cn.layout.inline_indent > 0) { // A wrapped continuation keeps a full-width box at the block's left // edge; its indent is what carries the pen it started from. cn.layout.x = clampInt16(contentLeft + marginL); } else { - cn.layout.x = clampInt16(contentLeft + pen + gap + marginL); + lead = collapsedLead(nodes, k, pen == 0 && ownsLineStart); + cn.layout.x = clampInt16(contentLeft + lineShift + pen + gap + marginL - lead); + pen -= lead; } pen += gap + marginL + cn.layout.width + marginR; if (soleRun && cn.type == NodeType::Text && !hasExplicitWidth(cn)) { // text-align needs a box as wide as the line box to align inside. - const int full = contentW - marginL - marginR; - if (full > cn.layout.width) cn.layout.width = clampInt16(full); + // A removed leading space sits before that box's content edge. + const int full = contentW - marginL - marginR + lead; + if (full > cn.layout.width) { + widenedRuns_.push_back({child, cn.layout.width, full - contentW}); + cn.layout.width = clampLayoutExtent(full); + } } int y = baseline - childBaseline(cn); + if (cn.style.vertical_align == 1) y = lineTop + cn.style.margin[0]; + else if (cn.style.vertical_align == 2) y = lineTop + lineCross - cn.layout.height - cn.style.margin[2]; + else y += verticalAlignShift(cn); const int align = usedAlignment(crossAlignFor(cn), lineCross - cn.layout.height - cn.style.margin[0] - cn.style.margin[2]); if (align != 5) { const int crossBefore = cn.style.margin[0]; @@ -1145,17 +1583,19 @@ class FlexLayoutPass { if (align == 1) y += (lineCross - crossTotal) / 2; else if (align == 2) y += lineCross - crossTotal; else if (align == 0 && !hasExplicitHeight(cn) && !isIntrinsicSizeExpression(cn.style.height_expression)) { - cn.layout.height = clampInt16(lineCross - crossBefore - crossAfter); + cn.layout.height = clampLayoutExtent(lineCross - crossBefore - crossAfter); engine_.repositionChildren(child); } } cn.layout.y = clampInt16(y); - cn.render.inline_baseline = 1; + const uint8_t placed = lineAligned ? 3 : 1; + cn.render.inline_baseline = placed; // An inline wrapper (text) contributes its CHILD's glyphs to // this line box, so the flag has to reach the node that actually draws. - if (cn.type == NodeType::View) { + // An atomic inline box (inline-flex) lays out its own children. + if (cn.type == NodeType::View && !isAtomicInline(cn.style)) { for (int c = cn.first_child; c >= 0; c = nodes[c].next_sibling) - if (nodes[c].type == NodeType::Text) nodes[c].render.inline_baseline = 1; + if (nodes[c].type == NodeType::Text) nodes[c].render.inline_baseline = placed; } } } @@ -1265,8 +1705,8 @@ class FlexLayoutPass { // height affects following lines and reversed line order as well. if (ratioCrossChanged) { expandCrossSizes(lineCount, true); - int16_t &cross = isRow_ ? node_.layout.height : node_.layout.width; - cross = clampInt16(clampLayoutSize(node_, totalCrossSize(lineCount) + boxInsets(node_.style, !isRow_), !isRow_)); + int32_t &cross = isRow_ ? node_.layout.height : node_.layout.width; + cross = clampLayoutExtent(clampLayoutSize(node_, totalCrossSize(lineCount) + boxInsets(node_.style, !isRow_), !isRow_)); } int crossOffset = boxInset(node_.style, isRow_ ? 0 : 3); const int crossBox = (isRow_ ? node_.layout.height : node_.layout.width) - boxInsets(node_.style, !isRow_); @@ -1330,7 +1770,11 @@ class FlexLayoutPass { node_.layout.height = clampLayoutSize(node_, height, false); } - if (!hasExplicitWidth(node_) && !keepScrollWidth) { + // A document's root element is block-level in the initial containing + // block, so its automatic width fills it whatever its display (CSS 2.2 + // 10.3.3). A native root sizes to its content. + const bool fillsRoot = node_.parent < 0 && std::strcmp(tagFromId(node_.tag_id), "html") == 0; + if (!hasExplicitWidth(node_) && !keepScrollWidth && !fillsRoot) { const int width = (isRow_ ? maxMainSize(lineCount) : totalCrossSize(lineCount)) + boxInset(node_.style, 1) + boxInset(node_.style, 3); node_.layout.width = clampLayoutSize(node_, width, true); } @@ -1547,13 +1991,19 @@ class FlexLayoutPass { int applied = 0; int lastGrowChild = -1; + // Round the running share, not each item's: every item edge lands on the + // pixel nearest its exact position, as browsers snap fractional edges + // (3:1 of 70px is 53/17, not 52/18). + long long weight = 0; for (int i = 0; i < line.count; i++) { const int child = children_[line.start + i]; Node &childNode = nodes[child]; if (childNode.style.flex <= 0) continue; lastGrowChild = child; - const int change = (delta * childNode.style.flex) / totalGrow; - applied += applyFlexMainSizeDelta(child, childNode, change); + const long long before = (2LL * delta * weight + totalGrow) / (2LL * totalGrow); + weight += childNode.style.flex; + const long long after = (2LL * delta * weight + totalGrow) / (2LL * totalGrow); + applied += applyFlexMainSizeDelta(child, childNode, static_cast(after - before)); } const int remainder = delta - applied; @@ -1622,6 +2072,17 @@ class FlexLayoutPass { const int transferred = ratioTransferredSize(childNode, isRow_ ? childNode.layout.height : childNode.layout.width, isRow_); extent = childNode.type == NodeType::Image ? std::min(extent, transferred) : std::max(extent, transferred); } + // With a preferred aspect ratio the content size suggestion is clamped by + // the definite minimum and maximum cross sizes converted through it + // (CSS Flexbox 4.5). + if (preferredRatio(childNode) > 0) { + const int maxCross = isRow_ ? childNode.style.max_height : childNode.style.max_width; + const int minCross = isRow_ ? childNode.style.min_height : childNode.style.min_width; + if (maxCross != kUnset) + extent = std::min(extent, ratioTransferredSize(childNode, contentSizeToBorderSize(childNode.style, maxCross, !isRow_), isRow_)); + if (minCross != kUnset && minCross > 0) + extent = std::max(extent, ratioTransferredSize(childNode, contentSizeToBorderSize(childNode.style, minCross, !isRow_), isRow_)); + } if (extent <= 0) return 0; const int max = isRow_ ? childNode.style.max_width : childNode.style.max_height; if (max != kUnset) extent = std::min(extent, contentSizeToBorderSize(childNode.style, max, isRow_)); @@ -1650,8 +2111,8 @@ class FlexLayoutPass { else childNode.layout.height = after; if (preferredRatio(childNode) > 0 && !(isRow_ ? hasExplicitHeight(childNode) : hasExplicitWidth(childNode)) && !definiteFlexCrossSize(node_, childNode, isRow_)) { - int16_t &cross = isRow_ ? childNode.layout.height : childNode.layout.width; - cross = clampInt16(clampLayoutSize(childNode, ratioTransferredSize(childNode, after, !isRow_), !isRow_)); + int32_t &cross = isRow_ ? childNode.layout.height : childNode.layout.width; + cross = clampLayoutExtent(clampLayoutSize(childNode, ratioTransferredSize(childNode, after, !isRow_), !isRow_)); } engine_.repositionChildren(child); return delta > 0 ? after - before : before - after; @@ -1676,18 +2137,65 @@ class FlexLayoutPass { } } - void positionLineChildren(const FlexLine &line, Node *nodes, int mainOffset, int crossOffset, int autoSpace = 0, int autoCount = 0, int distributedFree = 0) + // Physical sides (0 top, 1 right, 2 bottom, 3 left) the main axis starts at + // and the first line sits against. + void flexStartSides(bool &reverseMain, bool &reverseCross, int &mainBefore, int &crossBefore) const { const bool flex = node_.style.display == kDisplayFlex; const bool logicalRow = flexRowDirection(node_.style); const int mode = writingMode(node_); const bool blockReversed = mode == 2 || mode == 3; const bool inlineReversed = rightToLeft(node_) != (mode == 4); - const bool reverseMain = flex && ((node_.style.flex_direction_explicit && node_.style.flex_direction >= 2) != (logicalRow ? inlineReversed : blockReversed)); - const bool reverseCross = flex && (((node_.style.flex_wrap & 3) == 2) != (logicalRow ? blockReversed : inlineReversed)); - const int mainBefore = isRow_ ? (reverseMain ? 1 : 3) : (reverseMain ? 2 : 0); + reverseMain = flex && ((node_.style.flex_direction_explicit && node_.style.flex_direction >= 2) != (logicalRow ? inlineReversed : blockReversed)); + reverseCross = flex && (((node_.style.flex_wrap & 3) == 2) != (logicalRow ? blockReversed : inlineReversed)); + mainBefore = isRow_ ? (reverseMain ? 1 : 3) : (reverseMain ? 2 : 0); + crossBefore = isRow_ ? (reverseCross ? 2 : 0) : (reverseCross ? 1 : 3); + } + + // CSS Box 4 margin-trim on a flex container: the margins of items against a + // trimmed edge of the container are zero. Along the main axis that is each + // line's first or last item, across it every item of the first or last line. + void trimEdgeMargins(int lineCount, TrimmedMargins &trimmed) + { + const int trim = rstyle(node_.style).margin_trim; + if (!trim || node_.style.display != kDisplayFlex || lineCount <= 0) return; + Node *nodes = Tree::instance().nodes(); + bool reverseMain, reverseCross; + int mainBefore, crossBefore; + flexStartSides(reverseMain, reverseCross, mainBefore, crossBefore); + for (int flag = 0; flag < 4; ++flag) { + if (!(trim & (1 << flag))) continue; + const int side = marginTrimSide(node_, flag); + if (side == mainBefore || side == (mainBefore + 2) % 4) { + for (int l = 0; l < lineCount; ++l) + if (lines_[l].count) trimmed.cut(children_[side == mainBefore ? lines_[l].start : lines_[l].start + lines_[l].count - 1], side); + } else { + const FlexLine &line = lines_[side == crossBefore ? 0 : lineCount - 1]; + for (int i = 0; i < line.count; ++i) trimmed.cut(children_[line.start + i], side); + } + } + if (trimmed.entries.empty()) return; + for (int l = 0; l < lineCount; ++l) { + FlexLine &line = lines_[l]; + int mainSize = std::max(0, line.count - 1) * mainGap_, crossSize = 0; + for (int i = 0; i < line.count; ++i) { + const Node &item = nodes[children_[line.start + i]]; + mainSize += isRow_ ? item.layout.width + item.style.margin[1] + item.style.margin[3] + : item.layout.height + item.style.margin[0] + item.style.margin[2]; + crossSize = std::max(crossSize, isRow_ ? item.layout.height + item.style.margin[0] + item.style.margin[2] + : item.layout.width + item.style.margin[1] + item.style.margin[3]); + } + line.mainSize = mainSize; + line.crossSize = std::max(line.strutSize, baselineCrossSize(line.start, line.count, crossSize)); + } + } + + void positionLineChildren(const FlexLine &line, Node *nodes, int mainOffset, int crossOffset, int autoSpace = 0, int autoCount = 0, int distributedFree = 0) + { + bool reverseMain, reverseCross; + int mainBefore, crossBefore; + flexStartSides(reverseMain, reverseCross, mainBefore, crossBefore); const int mainAfter = (mainBefore + 2) % 4; - const int crossBefore = isRow_ ? (reverseCross ? 2 : 0) : (reverseCross ? 1 : 3); const int crossAfter = (crossBefore + 2) % 4; int autoUsed = 0; auto autoMargin = [&](const Node &child, int side) { @@ -1704,7 +2212,7 @@ class FlexLayoutPass { if (isRow_) { for (int i = 0; i < line.count; i++) { Node &probe = nodes[children_[line.start + i]]; - if (probe.style.margin_auto & 5) continue; + if ((probe.style.margin_auto & 5) || usesBaselineFallback(probe)) continue; if (crossAlignFor(probe) == kAlignLastBaseline) { const int baseline = childBaseline(probe, true); lineMaxLastDescent = std::max(lineMaxLastDescent, probe.style.margin[2] + probe.layout.height - baseline); @@ -1739,7 +2247,7 @@ class FlexLayoutPass { // Baselines are distances from physical top. Reversing the line // order must not mirror each item's baseline inside that line. const int alignment = crossAlignFor(childNode) & 15; - if (isRow_ && reverseCross && (alignment == 5 || alignment == kAlignLastBaseline)) + if (isRow_ && reverseCross && (alignment == 5 || alignment == kAlignLastBaseline) && !usesBaselineFallback(childNode)) crossPosition = crossOffset + line.crossSize - (crossPosition - crossOffset) - crossSize; } @@ -1758,8 +2266,15 @@ class FlexLayoutPass { const int mainSize = isRow_ ? childNode.layout.width : childNode.layout.height; mainOffset += mainSize + mainMarginAfter + mainGap_; - // Flex items own their boxes; nothing here shares a line box. - childNode.render.inline_baseline = 0; + // Flex items own their boxes; nothing here shares a line box. Ink + // centring stays for a text item the container centres vertically, + // which is where optical centring is what the author asked for. Any + // other text item sits on its baseline, as the anonymous block around + // bare flex text puts it (CSS Flexbox 4). + const int verticalAlignment = isRow_ ? crossAlignFor(childNode) : node_.style.justify_content; + const bool centredText = verticalAlignment >= 0 && (verticalAlignment & 15) == 1; + const bool baselineText = childNode.type == NodeType::Text && node_.style.display == kDisplayFlex && !centredText; + childNode.render.inline_baseline = baselineText ? 1 : 0; } } @@ -1774,10 +2289,23 @@ class FlexLayoutPass { return inlineRow_ ? 5 : node_.style.align_items; } + // Baseline alignment needs baselines that cross the cross axis. When a flex + // item's inline axis runs along the cross axis instead (a column, or a row + // of a vertical container), it uses the fallback: self-start/self-end in its + // own writing mode, which wrap-reverse does not flip. + bool usesBaselineFallback(const Node &childNode) const + { + const int keyword = crossAlignFor(childNode) & 15; + return node_.style.display == kDisplayFlex && !inlineRow_ && (keyword == 5 || keyword == kAlignLastBaseline) && + (writingMode(childNode) == 0) == !isRow_; + } + void positionCrossAxisChild(int child, Node &childNode, int lineCrossSize, int crossMarginBefore, int crossMarginAfter, int crossTotal, int *crossPosition, int lineMaxBaseline, int lineLastBaseline) { const int alignment = crossAlignFor(childNode); int align = physicalSelfAlignment(node_, childNode, alignment, lineCrossSize - crossTotal, !isRow_); + if (align < 0 && usesBaselineFallback(childNode)) + align = baselineFallbackAlignment(node_, childNode, alignment, lineCrossSize - crossTotal, !isRow_, false); if (align >= 0) { const bool reversed = gridAxisReversed(node_, !isRow_) != ((node_.style.flex_wrap & 3) == 2); if (reversed) align = align == 2 ? 6 : 2; @@ -1823,10 +2351,92 @@ class FlexLayoutPass { // places runs at `top + ascender - bearingY`, so this matches the drawn // pixels exactly. Flex items synthesize missing baselines from border edges; // inline boxes use margin edges. + // CSS aligns a non-replaced inline box by its line-height box, which can + // differ from its layout box when the line-height is below the font size. + bool alignsByLineHeight(const Node &item) const + { + return item.type == NodeType::Text || + (item.type == NodeType::View && item.first_child >= 0 && LayoutEngine::isCssInlineLevelBox(item) && + LayoutEngine::isInlineLevelNode(item)); + } + + // The aligned box of an item, as its reach above and below the item's + // own baseline. + void alignedBox(const Node &item, int &above, int &below) const + { + if (alignsByLineHeight(item)) { + const auto own = TextRenderer::inlineFontMetrics(item.style); + above = own.strutAscent; + below = own.strutDescent; + return; + } + above = item.style.margin[0] + childBaseline(item); + below = item.layout.height + item.style.margin[2] - childBaseline(item); + } + + // vertical-align of a one-line inline item: how far below its baseline + // position it sits. top and bottom align to the line box instead. + int verticalAlignShift(const Node &item) const + { + const int align = item.style.vertical_align; + if (align < 3) return 0; + const auto parent = TextRenderer::inlineFontMetrics(node_.style); + int above = 0, below = 0; + alignedBox(item, above, below); + if (align == 3) return (above - below) / 2 - parent.xHeight / 2; + if (align == 4) return above - parent.ascent; + if (align == 5) return parent.descent - below; + return align == 6 ? parent.fontSize / 5 : -(parent.fontSize / 3); + } + + // How far an item reaches above and below the line's baseline. A shifted + // item contributes its aligned box; baseline items keep their layout box. + void lineExtent(const Node &item, int &ascent, int &descent) const + { + const int shift = verticalAlignShift(item); + if (item.style.vertical_align >= 3) alignedBox(item, ascent, descent); + else { + ascent = item.style.margin[0] + childBaseline(item); + descent = item.layout.height + item.style.margin[2] - childBaseline(item); + } + ascent -= shift; + descent += shift; + } + + // The baseline of a block container's last in-flow line box, or -1 without + // one. A block child without line boxes adds none; one that clips its + // overflow stands for its bottom margin edge. + int lastLineBaseline(const Node &block) const + { + const Node *nodes = Tree::instance().nodes(); + for (int c = block.last_child; c >= 0; c = nodes[c].prev_sibling) { + const Node &inner = nodes[c]; + if (isDisplayNone(inner.style) || isOutOfFlowPosition(inner.style.position) || inner.style.float_side || + collapsibleSpaceOnly(inner)) continue; + // Blocks and inline boxes are searched in turn; atomic boxes and text + // sit on a line of this container. + if (inner.type == NodeType::View && inner.style.display == kDisplayBlock && !isAtomicInline(inner.style)) { + if (blockLevelView(inner) && overflowEstablishesContext(inner.style)) + return inner.layout.y + inner.layout.height + inner.style.margin[2]; + const int line = lastLineBaseline(inner); + if (line >= 0) return inner.layout.y + line; + continue; + } + return inner.layout.y + childBaseline(inner, true); + } + return -1; + } + int childBaseline(const Node &childNode, bool last = false) const { if ((childNode.type == NodeType::Text || isLineBreak(childNode)) && childNode.style.font_id >= 0) return TextRenderer::baselineOffset(childNode, last); + // An inline-block takes its last line box's baseline, or its bottom + // margin edge when it has none or clips its overflow (CSS 2.2 10.8.1). + if (childNode.type == NodeType::View && childNode.style.display == kDisplayBlock && isAtomicInline(childNode.style)) { + const int line = overflowEstablishesContext(childNode.style) ? -1 : lastLineBaseline(childNode); + return line >= 0 ? line : childNode.layout.height + childNode.style.margin[2]; + } // An inline wrapper (a around text) carries no text of its own, so // CSS takes the baseline of its FIRST in-flow line box. Its children were // already positioned by measureChildren, and layout.y is still parent- @@ -1838,7 +2448,8 @@ class FlexLayoutPass { const Node *nodes = Tree::instance().nodes(); for (int c = last ? childNode.last_child : childNode.first_child; c >= 0; c = last ? nodes[c].prev_sibling : nodes[c].next_sibling) { const Node &inner = nodes[c]; - if (isDisplayNone(inner.style) || isOutOfFlowPosition(inner.style.position)) continue; + // Collapsible white space alone makes no line box, so no baseline. + if (isDisplayNone(inner.style) || isOutOfFlowPosition(inner.style.position) || collapsibleSpaceOnly(inner)) continue; return inner.layout.y + childBaseline(inner, last); } } @@ -1875,6 +2486,10 @@ class FlexLayoutPass { int padHeight_ = 0; FlexLine *lines_ = nullptr; std::vector extraLines_; + // Sole runs placeLineItems widened to the offered line: the run, its own + // width, and its widened width less the line's. + struct WidenedRun { int id, natural, extra; }; + std::vector widenedRuns_; int mainGap_ = 0; int crossGap_ = 0; }; @@ -1894,28 +2509,93 @@ static bool isInlineLevelTag(const char *tag) std::strcmp(tag, "mark") == 0; } +// A view that is an inline box: an authored display: inline, or an inline-level +// tag with no authored display. +static bool inlineBoxView(const Node &n) +{ + return n.type == NodeType::View && (n.style.display_explicit ? isInlineBoxDisplay(n.style) : isInlineLevelTag(tagFromId(n.tag_id))); +} + +// A block container with a column count or width (CSS Multi-column 2). +// Spanning elements (column-span: all) split the columns into sets, which +// this layout does not do: such a container keeps a single column. +bool isMulticolContainer(const Node &node) +{ + const RareStyle &rs = rstyle(node.style); + if ((rs.column_count <= 0 && rs.column_width <= 0) || node.type != NodeType::View || node.style.display != kDisplayBlock || + (LayoutEngine::isCssInlineLevelBox(node) && LayoutEngine::isInlineLevelNode(node))) return false; + const Node *nodes = Tree::instance().nodes(); + for (int c = node.first_child; c >= 0; c = nodes[c].next_sibling) + if (rstyle(nodes[c].style).line_clamp_flags & 64) return false; + return true; +} + +// column-gap: normal is 1em in a multicol container. +int multicolGap(const Node &node, int available) +{ + const auto &s = node.style; + if (s.column_gap_percent != kUnset) return std::max(0, resolvePercentSize(available, s.column_gap_percent)); + if (s.column_gap != kUnset) return std::max(0, static_cast(s.column_gap)); + return s.gap > 0 ? s.gap : s.font_size; +} + +// An inline box with nothing to show: no insets and no visible box. +bool emptyInlineBox(const Node &node) +{ + return !boxInsets(node.style, true) && !boxInsets(node.style, false) && FlexLayoutPass::paintsNoBox(node); +} + +// A view that is block-level in its parent: neither an inline box nor an +// atomic inline-level box (inline-block, inline-flex). +bool blockLevelView(const Node &node) +{ + return node.type == NodeType::View && !isAtomicInline(node.style) && !inlineBoxView(node); +} + // Box-tree projection for inline ancestors split by in-flow blocks. The DOM // ancestry still owns inheritance, events, relative translation, and painting. // Visible inline decorations require fragments; keep those on their own path. bool splitInlineWrapper(const Node *nodes, int id) { const Node &n = nodes[id]; - if (n.type != NodeType::View || n.style.display_explicit || n.style.display != kDisplayBlock || - !isInlineLevelTag(tagFromId(n.tag_id)) || n.style.float_side || isOutOfFlowPosition(n.style.position) || + if (!inlineBoxView(n) || n.style.float_side || isOutOfFlowPosition(n.style.position) || !FlexLayoutPass::paintsNoBox(n) || boxInsets(n.style, true) || boxInsets(n.style, false) || n.style.margin[1] || n.style.margin[3] || n.style.opacity != 255 || overflowEstablishesContext(n.style)) return false; bool containsBlock = false; for (int c = n.first_child; c >= 0; c = nodes[c].next_sibling) { const Node &child = nodes[c]; if (isDisplayNone(child.style)) continue; - if (isOutOfFlowPosition(child.style.position)) return false; - if (child.style.float_side) continue; - if (child.type == NodeType::View && (child.style.display_explicit || !isInlineLevelTag(tagFromId(child.tag_id)))) containsBlock = true; + // Out-of-flow children stay with the wrapper, whose fragments contain them + // (LayoutNodePass::positionFragmentedInlineChildren). + if (isOutOfFlowPosition(child.style.position)) continue; + // A float inside an inline belongs to the enclosing block's float context. + if (child.style.float_side) { containsBlock = true; continue; } + // A forced break splits the inline across line boxes. + if (isLineBreak(child)) { containsBlock = true; continue; } + if (blockLevelView(child)) containsBlock = true; if (splitInlineWrapper(nodes, c)) containsBlock = true; } return containsBlock; } +// An inline box with visible decorations split by an in-flow block (CSS 2.2 +// 9.2.1.1) keeps its box but lays its inline runs out as fragments: shrink- +// wrapped like the unsplit span, open on the split sides. The blocks sit on +// the parent's content edges. Only a span on its own line (Gea reads vertical +// margins so) starts at those edges. +bool splitDecoratedInline(const Node *nodes, int id) +{ + const Node &n = nodes[id]; + if (!LayoutEngine::isCssInlineLevelBox(n) || LayoutEngine::isInlineLevelNode(n) || n.type != NodeType::View || + n.style.display != kDisplayBlock || emptyInlineBox(n) || overflowEstablishesContext(n.style)) return false; + for (int c = n.first_child; c >= 0; c = nodes[c].next_sibling) { + const Node &child = nodes[c]; + if (!isDisplayNone(child.style) && !isOutOfFlowPosition(child.style.position) && !child.style.float_side && + blockLevelView(child)) return true; + } + return false; +} + int formattingEdgeNode(const Node *nodes, int id, bool last) { if (!splitInlineWrapper(nodes, id)) return id; @@ -1956,7 +2636,7 @@ class LayoutNodePass { if (isLineBreak(node_) && LayoutEngine::isInlineLevelNode(node_) && (node_.parent < 0 || (nodes_[node_.parent].style.display != kDisplayFlex && !isDisplayGrid(nodes_[node_.parent].style)))) { node_.layout.width = 0; - node_.layout.height = clampInt16(TextRenderer::measureHeight(" ", node_.style.font_id, + node_.layout.height = clampLayoutExtent(TextRenderer::measureHeight(" ", node_.style.font_id, node_.style.font_size, 0, node_.style.line_height)); return; } @@ -1978,6 +2658,17 @@ class LayoutNodePass { return; } + if (NodeRareData *rare = rareDataFor(id_)) { + rare->multicol.valid = false; + rare->inlineParts.clear(); + } + const ColumnGeometry columns = columnGeometry(); + if (columns.count > 0) layoutColumns(columns); + else layoutContent(); + } + + void layoutContent() + { const int padWidth = paddedWidth(); const int padHeight = paddedHeight(); const bool inlineRow = resolveInlineFormattingRow(); @@ -2009,6 +2700,7 @@ class LayoutNodePass { encloseFloatDescendants(); updateScrollContentSize(); positionAbsoluteChildren(); + positionFragmentedInlineChildren(); applyRelativeOffsets(); return; } @@ -2042,6 +2734,9 @@ class LayoutNodePass { if (node_.type == NodeType::Text || node_.type == NodeType::Image) return; invalidateInlineChildMetadata(); if (NodeRareData *rare = rareDataFor(id_)) rare->firstLineBackground.lineValid = false; + // A multicol container's content keeps the column width it was laid out in. + const ColumnGeometry columns = columnGeometry(); + ColumnWidthScope columnWidth(node_, columns.count > 0 ? columns.width + boxInsets(node_.style, true) : -1); const int padWidth = paddedWidth(); const int padHeight = paddedHeight(); @@ -2068,6 +2763,7 @@ class LayoutNodePass { if (layoutFloatChildren(scratch.children(), childCount, padWidth, padHeight, false)) { updateScrollContentSize(); positionAbsoluteChildren(); + positionFragmentedInlineChildren(); applyRelativeOffsets(); return; } @@ -2088,6 +2784,110 @@ class LayoutNodePass { applyRelativeOffsets(); } + struct ColumnGeometry { + int count = 0, width = 0, gap = 0; + }; + + // CSS Multi-column 3.4: the used column count and width of a multicol + // container. count 0: not a multicol container. + ColumnGeometry columnGeometry() const + { + ColumnGeometry g; + if (!isMulticolContainer(node_)) return g; + const RareStyle &rs = rstyle(node_.style); + const int available = paddedWidth(); + g.gap = multicolGap(node_, available); + int count = rs.column_count; + if (rs.column_width > 0) { + const int fit = std::max(1, (available + g.gap) / (rs.column_width + g.gap)); + count = count > 0 ? std::min(count, fit) : fit; + } + g.count = std::max(1, count); + g.width = std::max(1, (available - (g.count - 1) * g.gap) / g.count); + return g; + } + + // Lays content out in a narrower border box, restoring the width after. + struct ColumnWidthScope { + Node &node; + int16_t width; + ColumnWidthScope(Node &n, int columnWidth) : node(n), width(n.layout.width) + { + if (columnWidth >= 0) n.layout.width = clampLayoutExtent(columnWidth); + } + ~ColumnWidthScope() { node.layout.width = width; } + }; + + // A multicol container lays its content out as one column (the flow + // thread) of the column width; painting slices it into column boxes (see + // RenderRecorder). Column height follows a definite height under + // column-fill: auto, else balances the flow without cutting a line box. + void layoutColumns(const ColumnGeometry &g) + { + const int insetsX = boxInsets(node_.style, true); + const int insetsY = boxInsets(node_.style, false); + const bool definite = hasExplicitHeight(node_); + const int given = paddedHeight(); + int flow = 0; + { + ColumnWidthScope columnWidth(node_, g.width + insetsX); + layoutContent(); + if (node_.first_child >= 0) updateScrollContentSize(); + flow = node_.first_child >= 0 ? std::max(0, node_.layout.scroll_content_height - insetsY) : 0; + } + const int flags = rstyle(node_.style).line_clamp_flags; + int height = definite && (flags & 16) ? given : balancedColumnHeight(flow, g.count); + if (definite) height = std::min(height, given); + height = std::max(1, height); + if (!definite) node_.layout.height = clampLayoutExtent(clampLayoutSize(node_, height + insetsY, false)); + // Overflow columns follow the last column box; continue: discard drops them. + int used = std::max(1, (flow + height - 1) / height); + if (flags & 32) used = std::min(used, g.count); + ensureRareData(id_).multicol = MulticolLayout{clampInt16(g.width), clampInt16(g.gap), clampInt16(height), clampInt16(used), true, (flags & 32) != 0}; + node_.layout.scroll_content_width = clampInt16(std::max(node_.layout.width, insetsX + used * (g.width + g.gap) - g.gap)); + node_.layout.scroll_content_height = node_.layout.height; + } + + // The shortest column height that fits the flow into `count` columns with + // no column boundary cutting a line box or a replaced element. + int balancedColumnHeight(int flow, int count) const + { + if (count <= 1 || flow <= 0) return flow; + std::vector> units; + collectMonolithicUnits(id_, -boxInset(node_.style, 0), units); + for (int height = (flow + count - 1) / count; height < flow; ++height) { + bool fits = true; + for (const auto &unit : units) { + const int column = unit.first / height; + if (column < count - 1 && unit.second > (column + 1) * height) { fits = false; break; } + } + if (fits) return height; + } + return flow; + } + + void collectMonolithicUnits(int parent, int originY, std::vector> &units) const + { + for (int id = nodes_[parent].first_child; id >= 0; id = nodes_[id].next_sibling) { + const Node &child = nodes_[id]; + if (isDisplayNone(child.style) || isOutOfFlowPosition(child.style.position)) continue; + const int top = originY + child.layout.y; + if (child.type == NodeType::Text) { + const int advance = TextRenderer::measureHeight("X", child.style.font_id, child.style.font_size, 0, child.style.line_height); + const int inner = child.layout.height - boxInsets(child.style, false); + const int lines = advance > 0 ? std::max(1, (inner + advance - 1) / advance) : 1; + for (int line = 0; line < lines; ++line) { + const int lineTop = top + boxInset(child.style, 0) + line * advance; + units.emplace_back(lineTop, std::min(lineTop + advance, top + child.layout.height)); + } + } else if (child.type == NodeType::Image) { + units.emplace_back(top, top + child.layout.height); + } else { + collectMonolithicUnits(id, top, units); + } + } + } + bool containsChildMargins() const { // A stable border box is not sufficient for isolated relayout: child @@ -2133,9 +2933,48 @@ class LayoutNodePass { } }; collect(collect, id_); + deferUnbreakableFloats(children, count); return count; } + // True when a line may end just before children[k]: at a block or line + // start, or after a soft wrap opportunity. Whitespace-only nowrap text + // collapses into its neighbours, so the search continues through it. + bool softWrapBefore(const int *children, int k) const + { + for (int prev = k - 1; prev >= 0; --prev) { + const Node &item = nodes_[children[prev]]; + if (item.style.float_side) continue; + if (blockBox(item) || isLineBreak(item) || FlexLayoutPass::trailingSoftWrap(item)) return true; + const int run = FlexLayoutPass::edgeTextRun(item, true); + if (nodes_[run].text.find_first_not_of(" \t\r\n\f") != std::string::npos) return false; + } + return true; + } + + // Floats end inline runs in this layout, so a float anchored where the line + // cannot break (inside a word or nowrap text) would split its line. CSS + // places such a float after the line holding its anchor instead: move it, + // with any floats beside it, to the next soft wrap opportunity. + void deferUnbreakableFloats(int *children, int count) const + { + for (int k = 0; k < count; ++k) { + if (!nodes_[children[k]].style.float_side) continue; + int group = k + 1; + while (group < count && nodes_[children[group]].style.float_side) ++group; + int target = group; + if (!softWrapBefore(children, k)) { + while (target < count) { + const Node &item = nodes_[children[target]]; + if (item.style.float_side || blockBox(item) || isLineBreak(item) || FlexLayoutPass::leadingSoftWrap(item)) break; + if (FlexLayoutPass::trailingSoftWrap(nodes_[children[target++]])) break; + } + } + if (target > group) std::rotate(children + k, children + group, children + target); + k = target - 1; + } + } + struct FloatBox { int left, top, right, bottom, side; }; // One exclusion space for the entire BFC, including ordinary wrappers. // Coordinates exclude relative-position offsets. Clearance is used layout @@ -2158,8 +2997,7 @@ class LayoutNodePass { bool blockBox(const Node &n) const { - return n.type == NodeType::View && !isDisplayNone(n.style) && - (n.style.display_explicit || !isInlineLevelTag(tagFromId(n.tag_id))); + return !isDisplayNone(n.style) && blockLevelView(n); } bool flowChild(int parent, int child) const @@ -2168,10 +3006,13 @@ class LayoutNodePass { !nodes_[child].style.float_side && !suppressAnonymousWhitespace(nodes_, parent, child); } + // A non-normal align-content makes a block container a BFC root (CSS Align 3 + // 5.1), just like display: flow-root and a line-clamp container. bool establishesBlockContext(const Node &n) const { return n.parent < 0 || isOutOfFlowPosition(n.style.position) || n.style.float_side || - n.style.display != kDisplayBlock || overflowEstablishesContext(n.style) || + n.style.display != kDisplayBlock || isFlowRoot(n.style) || (blockBox(n) && n.style.align_content != 0) || clampsLines(n) || + overflowEstablishesContext(n.style) || isMulticolContainer(n) || (n.parent >= 0 && (nodes_[n.parent].style.display == kDisplayFlex || isDisplayGrid(nodes_[n.parent].style) || writingMode(n) != writingMode(nodes_[n.parent]))); } @@ -2289,6 +3130,29 @@ class LayoutNodePass { return margin; } + // The exact border-box height of a box sized by a plain percentage of + // `basis`, or -1 when its integer height is already exact (lengths, auto, + // calc(), or min/max clamping). + double exactPercentHeight(const Node &child, int basis) const + { + const auto &s = child.style; + if (s.height_percent == kUnset || s.height != kUnset || s.height_expression >= 0 || + percentageHeightDependsOnIntrinsicSize(child) || flexBasisScope(child, false) || intrinsicSizeScope(child, false)) return -1; + const int insets = s.box_sizing == 0 ? boxInsets(s, false) : 0; + if (resolvePercentSize(basis, s.height_percent) + insets != child.layout.height) return -1; + return static_cast(basis) * s.height_percent / 1000.0 + insets; + } + + // Where a block-level box sits in the free inline space of its horizontal + // container: auto margins take it (CSS 2.2 10.3.3); otherwise it starts at + // the container's start edge. + int blockInlineOffset(const Node &child, int slack) const + { + const bool autoLeft = child.style.margin_auto & (1 << 3), autoRight = child.style.margin_auto & (1 << 1); + if (autoLeft || autoRight) return slack > 0 && autoLeft ? (autoRight ? slack / 2 : slack) : 0; + return rightToLeft(node_) ? slack : 0; + } + bool layoutBlockChildren(int *children, int count, int contentWidth, int contentHeight, bool autosize) { if (node_.style.display != kDisplayBlock || node_.style.flex_direction_explicit) return false; @@ -2314,7 +3178,7 @@ class LayoutNodePass { !(preferredRatio(child) > 0 && hasExplicitWidth(child))) { const int height = clampLayoutSize(child, std::max(0, contentHeight - child.style.margin[0] - child.style.margin[2]), false); if (child.layout.height != height) { - child.layout.height = clampInt16(height); engine_.repositionChildren(id); + child.layout.height = clampLayoutExtent(height); engine_.repositionChildren(id); } } const int slack = contentHeight - child.layout.height - child.style.margin[0] - child.style.margin[2]; @@ -2348,21 +3212,21 @@ class LayoutNodePass { if (!hasExplicitWidth(child) && !isIntrinsicSizeExpression(child.style.width_expression) && writingMode(child) == 0 && !(preferredRatio(child) > 0 && hasExplicitHeight(child))) { const int width = clampLayoutSize(child, std::max(0, contentWidth - child.style.margin[1] - child.style.margin[3]), true); if (child.layout.width != width) { - child.layout.width = clampInt16(width); + child.layout.width = clampLayoutExtent(width); engine_.repositionChildren(id); } } const int slack = contentWidth - child.layout.width - child.style.margin[1] - child.style.margin[3]; - const bool autoLeft = child.style.margin_auto & (1 << 3); - const bool autoRight = child.style.margin_auto & (1 << 1); - int offset = slack > 0 && autoLeft ? (autoRight ? slack / 2 : slack) : 0; - if (!autoLeft && !autoRight && rightToLeft(node_)) offset = slack; - child.layout.x = boxInset(node_.style, 3) + child.style.margin[3] + offset; + child.layout.x = boxInset(node_.style, 3) + child.style.margin[3] + blockInlineOffset(child, slack); } } const bool mergeTop = collapsesWithChildren(id_, false); const bool mergeBottom = collapsesWithChildren(id_, true); int cursor = 0; + // The exact flow position minus cursor. Percentage heights are + // fractional; each such box ends on the pixel nearest its exact bottom + // edge, as browsers snap, so 75% and 25% of 70px are 53 and 17. + double drift = 0; bool first = true; CollapsedMargin pending; for (int i = 0; i < count; ++i) { @@ -2382,6 +3246,16 @@ class LayoutNodePass { pending.add(childEdgeMargin(id_, id, false)); cursor += first && mergeTop ? 0 : pending.value(); (vertical ? child.layout.x : child.layout.y) = boxInset(node_.style, blockMarginSide(false)) + cursor; + if (const double exact = vertical ? -1 : exactPercentHeight(child, contentHeight); exact >= 0) { + const double bottom = cursor + drift + exact; + const int height = static_cast(std::floor(bottom + 0.5)) - cursor; + if (height >= 0 && height != child.layout.height) { + if (child.layout.scroll_content_height == child.layout.height) child.layout.scroll_content_height = clampInt16(height); + child.layout.height = clampLayoutExtent(height); + engine_.repositionChildren(id); + } + drift = bottom - cursor - child.layout.height; + } cursor += vertical ? child.layout.width : child.layout.height; pending = childEdgeMargin(id_, id, true); first = false; @@ -2442,6 +3316,34 @@ class LayoutNodePass { int intrinsicWidth = 0; CollapsedMargin pending; bool first = true, clearedThrough = false; + // A split decorated inline (splitDecoratedInline): the runs between its + // blocks become fragments, which reach past its content box on their + // open sides. outer[] is margin, border and padding per side. + const bool split = splitDecoratedInline(nodes_, id_); + const bool rtl = rightToLeft(node_); + int outer[4]; + for (int s = 0; s < 4; ++s) outer[s] = split ? node_.style.margin[s] + boxInset(node_.style, s) : 0; + std::vector parts; + int partTop = -1, partWidth = 0, trimLeft = 0, trimRight = 0; + auto blockFollows = [&](int from) { + for (int k = from; k < count; ++k) + if (blockBox(nodes_[children[k]]) && !nodes_[children[k]].style.float_side) return true; + return false; + }; + auto closePart = [&]() { + const int left = rtl ? contentWidth + trimRight - partWidth - (trimLeft ? 0 : boxInset(node_.style, 3)) : -trimLeft - (trimLeft ? 0 : boxInset(node_.style, 3)); + const int right = rtl ? contentWidth + trimRight + (trimRight ? 0 : boxInset(node_.style, 1)) : -trimLeft + partWidth + (trimRight ? 0 : boxInset(node_.style, 1)); + const int top = partTop - boxInset(node_.style, 0), bottom = flowY + boxInset(node_.style, 2); + InlinePart part; + part.x = clampInt16(boxInset(node_.style, 3) + left); + part.y = clampInt16(boxInset(node_.style, 0) + top); + part.width = clampInt16(right - left); + part.height = clampInt16(bottom - top); + part.sides = static_cast(5 | (trimRight ? 0 : 2) | (trimLeft ? 0 : 8)); + parts.push_back(part); + partTop = -1; + partWidth = 0; + }; for (int i = 0; i < count; ++i) { Node &child = nodes_[children[i]]; if (!blockBox(child) && !child.style.float_side) { @@ -2452,6 +3354,15 @@ class LayoutNodePass { end < count && !blockBox(nodes_[children[end]]) && !nodes_[children[end]].style.float_side) ++end; flowY += pending.value(); pending = {}; + if (split && partTop < 0) { + const bool firstPart = parts.empty(), lastPart = !blockFollows(end); + if (!firstPart) flowY += outer[0]; + partTop = flowY; + trimLeft = (rtl ? !lastPart : !firstPart) ? outer[3] : 0; + trimRight = (rtl ? !firstPart : !lastPart) ? outer[1] : 0; + } + const int runWidth = contentWidth + (split ? trimLeft + trimRight : 0); + const int runShift = split ? trimLeft : 0; ScratchFrame inlineScratch; if (inlineScratch.valid()) { auto layoutRun = [&](int width) { @@ -2461,7 +3372,7 @@ class LayoutNodePass { }; NodeRareData *ownerRare = rareDataFor(id_); const bool ownerLineWasValid = ownerRare && ownerRare->firstLineBackground.lineValid; - auto extent = layoutRun(contentWidth); + auto extent = layoutRun(runWidth); // If even the first unbreakable unit cannot fit beside a float, // move this line down to the next float boundary and retry. // Once the whole containing width is available, the word may overflow. @@ -2476,20 +3387,34 @@ class LayoutNodePass { minimum += item.style.margin[1] + item.style.margin[3]; if (item.type == NodeType::Text) minimum += boxInsets(item.style, true); } - int left = 0, right = contentWidth; + int left = 0, right = runWidth; for (;;) { - left = 0; right = contentWidth; + left = 0; right = runWidth; int nextBottom = INT_MAX; for (const auto &f : floats) { if (originY + flowY >= f.bottom || originY + flowY + extent.firstLineHeight <= f.top) continue; - if (f.side == 1) left = std::max(left, f.right - originX); - else right = std::min(right, f.left - originX); + if (f.side == 1) left = std::max(left, f.right - originX + runShift); + else right = std::min(right, f.left - originX + runShift); nextBottom = std::min(nextBottom, f.bottom - originY); } - if (minimum <= right-left || (left == 0 && right == contentWidth) || nextBottom == INT_MAX) break; + if (minimum <= right-left || (left == 0 && right == runWidth) || nextBottom == INT_MAX) break; flowY = nextBottom; } - if (right - left != contentWidth) extent = layoutRun(std::max(0, right - left)); + if (right - left != runWidth) extent = layoutRun(std::max(0, right - left)); + // A float after a one-line run goes to the top of that line when it + // fits beside the run's content (CSS 2.2 9.5.1): place the float + // first, then flow the run again beside it. + if (end < count && nodes_[children[end]].style.float_side && !nodes_[children[end]].style.clear_side && + !isLineBreak(nodes_[children[end - 1]]) && extent.height == extent.firstLineHeight) { + const Node &next = nodes_[children[end]]; + engine_.layoutNode(children[end], contentWidth, contentHeight, measureWidth); + if (extent.width + next.layout.width + next.style.margin[1] + next.style.margin[3] <= right - left) { + if (ownerRare) ownerRare->firstLineBackground.lineValid = ownerLineWasValid; + std::rotate(children + i, children + end, children + end + 1); + --i; + continue; + } + } if (!ownerLineWasValid && ownerRare && ownerRare->firstLineBackground.lineValid) ownerRare->firstLineBackground.lineY = clampInt16(ownerRare->firstLineBackground.lineY + flowY); for (int j = i; j < end; ++j) { @@ -2500,18 +3425,24 @@ class LayoutNodePass { if (NodeRareData *rare = rareDataFor(fragmentId)) { auto &fragment = rare->firstLineFragment; if (fragment.valid && fragment.contextNode == id_) { - fragment.x = clampInt16(fragment.x + left); + fragment.x = clampInt16(fragment.x + left - runShift); fragment.y = clampInt16(fragment.y + flowY); } } } } for (int j = i; j < end; ++j) { - nodes_[children[j]].layout.x += left; + nodes_[children[j]].layout.x += left - runShift; nodes_[children[j]].layout.y += flowY; } flowY += extent.height; intrinsicWidth = std::max(intrinsicWidth, left + extent.width); + if (split) { + // A one-line run's collapsible trailing space hangs past the fragment. + const int lastItem = children[isLineBreak(nodes_[children[end - 1]]) && end - 1 > i ? end - 2 : end - 1]; + const int hang = extent.height == extent.firstLineHeight ? FlexLayoutPass::hangingSpace(nodes_[lastItem]) : 0; + partWidth = std::max(partWidth, left + extent.width - hang); + } const Node &last = nodes_[children[end - 1]]; if (isLineBreak(last)) for (const auto &f : floats) if (last.style.clear_side & f.side) flowY = std::max(flowY, f.bottom - originY); @@ -2520,7 +3451,14 @@ class LayoutNodePass { i = end - 1; continue; } - engine_.layoutNode(children[i], contentWidth, contentHeight, measureWidth); + const bool splitBlock = split && !child.style.float_side && blockBox(child); + if (splitBlock && partTop >= 0) { + closePart(); + flowY += outer[2]; + } + // A block splitting an inline fills the parent's content box. + const int area = splitBlock ? contentWidth + outer[1] + outer[3] : contentWidth; + engine_.layoutNode(children[i], area, contentHeight, measureWidth); const bool block = !child.style.float_side && blockBox(child); const int measuredOuterW = child.layout.width + child.style.margin[3] + child.style.margin[1]; const bool automaticWidth = block && !hasExplicitWidth(child) && !isIntrinsicSizeExpression(child.style.width_expression) && @@ -2528,9 +3466,9 @@ class LayoutNodePass { // Ordinary blocks still fill their containing block beside floats; // only their line boxes are shortened by float exclusions. if (automaticWidth) { - const int width = clampLayoutSize(child, std::max(0, contentWidth - child.style.margin[1] - child.style.margin[3]), true); + const int width = clampLayoutSize(child, std::max(0, area - child.style.margin[1] - child.style.margin[3]), true); if (child.layout.width != width) { - child.layout.width = clampInt16(width); + child.layout.width = clampLayoutExtent(width); engine_.repositionChildren(children[i]); } } @@ -2559,16 +3497,22 @@ class LayoutNodePass { // when that requires negative clearance above the top margin. y = clearance ? clearBottom : std::max(y, clearBottom); if (clearance && !context.hasClearance(children[i])) context.cleared.push_back(children[i]); - int left = 0, right = contentWidth, contextX = 0; + int left = 0, right = area, contextX = 0, contextFree = 0; const bool independentBlock = block && establishesBlockContext(child); if (child.style.float_side || child.style.display != kDisplayBlock || establishesBlockContext(child)) { + // An automatic-width BFC tries the widest opportunity at each y + // first and narrows only while its height meets more floats. Its + // width never grows back, so a height that depends on the width + // (aspect-ratio, wrapping) cannot make this search oscillate. + const bool searchWidth = independentBlock && automaticWidth; + int extent = searchWidth ? 1 : independentBlock ? int(child.layout.height) : outerH; for (;;) { - left = 0; right = contentWidth; + left = 0; right = area; int nextY = 32767; bool hasLeft = false, hasRight = false; for (const auto &global : floats) { const FloatBox f{global.left - originX, global.top - originY, global.right - originX, global.bottom - originY, global.side}; - if (y >= f.bottom || y + std::max(1, independentBlock ? int(child.layout.height) : outerH) <= f.top) continue; + if (y >= f.bottom || y + std::max(1, extent) <= f.top) continue; if (f.side == 1) { left = std::max(left, f.right); hasLeft = true; } else { right = std::min(right, f.left); hasRight = true; } nextY = std::min(nextY, f.bottom); @@ -2579,33 +3523,42 @@ class LayoutNodePass { // Automatic width uses the remaining interval. Even a // zero-width float prevents a negative margin crossing it. const int start = hasLeft ? std::max(child.style.margin[3], left) : child.style.margin[3]; - const int end = hasRight ? std::min(contentWidth - child.style.margin[1], right) : contentWidth - child.style.margin[1]; + const int end = hasRight ? std::min(area - child.style.margin[1], right) : area - child.style.margin[1]; contextX = start - child.style.margin[3]; - if (automaticWidth) { - const int width = clampLayoutSize(child, std::max(0, end - start), true); + if (searchWidth && end >= start) { + const int width = clampLayoutSize(child, end - start, true); if (child.layout.width != width) { - const int previousHeight = child.layout.height; engine_.layoutNode(children[i], width + child.style.margin[1] + child.style.margin[3], contentHeight); - child.layout.width = clampInt16(width); + child.layout.width = clampLayoutExtent(width); engine_.repositionChildren(children[i]); outerW = width + child.style.margin[1] + child.style.margin[3]; outerH = child.layout.height + child.style.margin[0] + child.style.margin[2]; - if (previousHeight != child.layout.height) continue; } + if (child.layout.height > extent) { extent = child.layout.height; continue; } } fits = child.layout.width <= end - start; + contextFree = end - start - child.layout.width; } if (fits || nextY == 32767) break; y = nextY; + if (searchWidth) extent = 1; } } - const int x = child.style.float_side == 2 ? right - outerW : independentBlock ? contextX : left; + // A normal block ignores floats; auto margins center it, and in + // right-to-left flow one that does not fill the line hugs the right. + // A block formatting context beside floats centers with auto margins in + // the space between them. + const bool autoMargins = child.style.margin_auto & 10; + const int x = (block && !independentBlock ? left + blockInlineOffset(child, right - left - outerW) : + child.style.float_side == 2 ? right - outerW : + independentBlock ? contextX + (autoMargins ? blockInlineOffset(child, contextFree) : 0) : left) - + (splitBlock ? outer[3] : 0); intrinsicWidth = std::max(intrinsicWidth, child.style.float_side ? left + outerW + contentWidth - right : measuredOuterW); child.layout.x = boxInset(node_.style, 3) + x + child.style.margin[3]; child.layout.y = boxInset(node_.style, 0) + y + (block ? 0 : child.style.margin[0]); if (block && !establishesBlockContext(child) && child.first_child >= 0) { - LayoutNodePass nested(engine_, children[i], contentWidth, contentHeight); + LayoutNodePass nested(engine_, children[i], area, contentHeight); if (!nested.resolveInlineFormattingRow()) { ScratchFrame nestedScratch; if (nestedScratch.valid()) { @@ -2662,10 +3615,12 @@ class LayoutNodePass { clearedThrough = false; } else flowY = y + outerH; } + if (partTop >= 0) closePart(); + if (split) ensureRareData(id_).inlineParts = std::move(parts); if (clearedThrough || (!collapsesWithChildren(id_, true) && !(first && collapsesWithChildren(id_, false)))) flowY += pending.value(); if (measureWidth) - node_.layout.width = clampInt16(clampLayoutSize(node_, intrinsicWidth + boxInsets(node_.style, true), true)); + node_.layout.width = clampLayoutExtent(clampLayoutSize(node_, intrinsicWidth + boxInsets(node_.style, true), true)); if (autosize && !hasExplicitHeight(node_)) { // Float enclosure is finalized with all same-context descendants. node_.layout.height = clampBorderSize(engine_, node_, flowY + boxInsets(node_.style, false), false); @@ -2890,6 +3845,17 @@ class LayoutNodePass { occupy(p); cursorColumn = p.column.end; } + // CSS Box 4 margin-trim: an item in the first or last row or column has + // no margin against that edge of the grid, until this pass ends. + TrimmedMargins trimmed; + if (const int trim = rstyle(node_.style).margin_trim) { + for (int i = 0; i < childCount; ++i) { + const auto &p = placements[i]; + const bool atEdge[4] = {p.row.start == 0, p.row.end == rowCount, p.column.start == 0, p.column.end == columnCount}; + for (int flag = 0; flag < 4; ++flag) + if ((trim & (1 << flag)) && atEdge[flag]) trimmed.cut(children[i], marginTrimSide(node_, flag)); + } + } const int columnGap = resolvedGap(node_, false, padWidth, padHeight); const int rowGap = resolvedGap(node_, true, padWidth, padHeight); @@ -3169,20 +4135,23 @@ class LayoutNodePass { for (int c = node_.first_child; c >= 0; c = nodes_[c].next_sibling) if (isOutOfFlowPosition(nodes_[c].style.position)) nodes_[c].layout.static_block_axis = 0; if (node_.style.display != kDisplayBlock || node_.style.flex_direction_explicit || - (LayoutEngine::isInlineLevelNode(node_) && !isOutOfFlowPosition(node_.style.position))) return; + (LayoutEngine::isInlineLevelNode(node_) && !isAtomicInline(node_.style) && !isOutOfFlowPosition(node_.style.position))) return; const bool vertical = writingMode(node_) != 0; const bool reversed = vertical && gridAxisReversed(node_, true); - int cursor = boxInset(node_.style, blockMarginSide(false)); + int cursor = boxInset(node_.style, blockMarginSide(false)) + contentShift_; bool first = true; CollapsedMargin pending; visitFormattingChildren(nodes_, id_, [&](int id) { Node &child = nodes_[id]; if (isDisplayNone(child.style)) return; if (isOutOfFlowPosition(child.style.position)) { - if (child.parent != id_ || !blockBox(child)) return; + // An inline-level box that no line box placed (inlineStaticPosition) + // starts a hypothetical line here, where a block-level one would. + if (child.parent != id_) return; const int edge = cursor + (first && collapsesWithChildren(id_, false) ? 0 : pending.value()); child.layout.static_block_start = clampInt16(reversed ? node_.layout.width - edge : edge); child.layout.static_block_axis = vertical ? 2 : 1; + if (!vertical && LayoutEngine::isCssInlineLevelBox(child, true)) captureHypotheticalLine(id, edge); return; } if (child.style.float_side || suppressAnonymousWhitespace(nodes_, child.parent, id)) return; @@ -3198,8 +4167,70 @@ class LayoutNodePass { }); } + // An inline-level box that no line placed would open an empty line at edge: + // the floats before it shorten that line and text-align places the static + // position on it. In a right-to-left block the box's right edge meets it + // (CSS 2.2 10.3.7). + void captureHypotheticalLine(int id, int edge) + { + if (const NodeRareData *rare = rareDataFor(id); rare && rare->inlineStaticPosition.valid) return; + int left = boxInset(node_.style, 3), right = node_.layout.width - boxInset(node_.style, 1); + for (int f = node_.first_child; f >= 0 && f != id; f = nodes_[f].next_sibling) { + const Node &box = nodes_[f]; + if (!box.style.float_side || isDisplayNone(box.style) || isOutOfFlowPosition(box.style.position)) continue; + if (edge < box.layout.y - box.style.margin[0] || edge >= box.layout.y + box.layout.height + box.style.margin[2]) continue; + if (box.style.float_side == 1) left = std::max(left, box.layout.x + box.layout.width + box.style.margin[1]); + else right = std::min(right, box.layout.x - box.style.margin[3]); + } + right = std::max(left, right); + const int align = LayoutEngine::physicalTextAlign(node_); + auto &position = ensureRareData(id).inlineStaticPosition; + position = InlineStaticPosition{}; + position.x = clampInt16(align == 1 ? left + (right - left) / 2 : align == 2 ? right : left); + position.y = clampInt16(edge); + position.flowNode = static_cast(id_); + position.anchorRight = rightToLeft(node_); + position.valid = true; + } + + // CSS Align 3: center or end align-content on a block container moves its + // in-flow content down when its content box is taller; with no content, the + // static positions of its absolute children move. Recomputed from the + // content's current top, so a second call leaves it where it is. + int contentShift_ = 0; + void alignBlockContent() + { + contentShift_ = 0; + const int keyword = node_.style.align_content & 15; + const bool center = keyword == 1 || keyword == 4 || keyword == kAlignSpaceEvenly, end = keyword == 2 || keyword == kAlignEnd; + // An automatic height already fits the content, floats included. + if ((!center && !end) || (!hasExplicitHeight(node_) && node_.style.min_height <= 0) || + node_.style.display != kDisplayBlock || node_.style.flex_direction_explicit || + inlineBoxView(node_) || writingMode(node_) != 0 || overflowEstablishesContext(node_.style) || isMulticolContainer(node_)) return; + const int top = boxInset(node_.style, 0); + int first = INT_MAX, last = top; + for (int c = node_.first_child; c >= 0; c = nodes_[c].next_sibling) { + const Node &child = nodes_[c]; + if (isDisplayNone(child.style) || isOutOfFlowPosition(child.style.position) || suppressAnonymousWhitespace(nodes_, id_, c)) continue; + first = std::min(first, child.layout.y - child.style.margin[0]); + last = std::max(last, child.layout.y + child.layout.height + child.style.margin[2]); + } + if (first == INT_MAX) first = top; + const int free = node_.layout.height - boxInsets(node_.style, false) - (last - first); + if (free <= 0) return; + contentShift_ = end ? free : roundedHalf(free); + const int delta = top + contentShift_ - first; + if (!delta) return; + for (int c = node_.first_child; c >= 0; c = nodes_[c].next_sibling) { + Node &child = nodes_[c]; + if (isDisplayNone(child.style) || isOutOfFlowPosition(child.style.position)) continue; + child.layout.y = clampInt16(child.layout.y + delta); + } + } + void positionAbsoluteChildren() { + alignBlockContent(); captureBlockStaticPositions(); for (int child = node_.first_child; child >= 0; child = nodes_[child].next_sibling) { Node &childNode = nodes_[child]; @@ -3214,6 +4245,110 @@ class LayoutNodePass { } } + // A positioned inline split into fragments across this block's lines is + // the containing block of its out-of-flow children: the rect from its first + // fragment's inline-start and block-start edges to its last fragment's + // inline-end and block-end edges (CSS Position 3, 3.2). Its box, otherwise + // empty at its parent's origin, takes that rect, its in-flow content keeps + // its place, and its out-of-flow children are positioned against it. + void positionFragmentedInlineChildren() + { + if (!splitInlineChildren_) return; + auto visit = [&](auto &&self, int parent, int originX, int originY) -> void { + for (int c = nodes_[parent].first_child; c >= 0; c = nodes_[c].next_sibling) { + if (!splitInlineWrapper(nodes_, c)) continue; + Node &wrapper = nodes_[c]; + int x = originX + wrapper.layout.x, y = originY + wrapper.layout.y; + bool contains = false; + for (int k = wrapper.first_child; k >= 0 && !contains; k = nodes_[k].next_sibling) + contains = !isDisplayNone(nodes_[k].style) && isOutOfFlowPosition(nodes_[k].style.position); + int rect[4]; + if (contains && establishesAbsoluteContainingBlock(wrapper) && fragmentRect(c, x, y, rect)) { + const int dx = rect[0] - x, dy = rect[1] - y; + wrapper.layout.x = clampInt16(wrapper.layout.x + dx); + wrapper.layout.y = clampInt16(wrapper.layout.y + dy); + wrapper.layout.width = clampLayoutExtent(rect[2]); + wrapper.layout.height = clampLayoutExtent(rect[3]); + for (int k = wrapper.first_child; k >= 0; k = nodes_[k].next_sibling) { + if (isOutOfFlowPosition(nodes_[k].style.position)) continue; + nodes_[k].layout.x = clampInt16(nodes_[k].layout.x - dx); + nodes_[k].layout.y = clampInt16(nodes_[k].layout.y - dy); + } + x = rect[0]; + y = rect[1]; + LayoutNodePass wrapperPass(engine_, c, rect[2], rect[3]); + wrapperPass.positionAbsoluteChildren(); + } + self(self, c, x, y); + } + }; + visit(visit, id_, 0, 0); + } + + // The fragments of a split inline at (originX, originY): x, y, width and + // height from its first in-flow inline content to its last. + bool fragmentRect(int wrapper, int originX, int originY, int rect[4]) const + { + struct Leaf { int id, x, y; }; + Leaf first{-1, 0, 0}, last{-1, 0, 0}; + auto walk = [&](auto &&self, int parent, int ox, int oy) -> void { + for (int c = nodes_[parent].first_child; c >= 0; c = nodes_[c].next_sibling) { + const Node &child = nodes_[c]; + if (isDisplayNone(child.style) || isOutOfFlowPosition(child.style.position) || child.style.float_side || + isLineBreak(child) || blockBox(child) || suppressAnonymousWhitespace(nodes_, parent, c)) continue; + if (child.type == NodeType::Text && child.text.find_first_not_of(" \t\r\n\f") == std::string::npos) continue; + if (splitInlineWrapper(nodes_, c)) { + self(self, c, ox + child.layout.x, oy + child.layout.y); + continue; + } + if (first.id < 0) first = {c, ox + child.layout.x, oy + child.layout.y}; + last = {c, ox + child.layout.x, oy + child.layout.y}; + } + }; + walk(walk, wrapper, originX, originY); + if (first.id < 0) return false; + const Node &a = nodes_[first.id], &b = nodes_[last.id]; + int firstLeft, firstRight, firstLastLeft, firstLastRight, lastFirstLeft, lastFirstRight, lastLeft, lastRight; + inlineExtent(a, first.x, firstLeft, firstRight, firstLastLeft, firstLastRight); + inlineExtent(b, last.x, lastFirstLeft, lastFirstRight, lastLeft, lastRight); + int left = firstLeft, right = lastRight; + if (rightToLeft(nodes_[wrapper])) { + left = lastLeft; + right = firstRight; + } + rect[0] = left; + rect[1] = first.y; + rect[2] = std::max(0, right - left); + rect[3] = std::max(0, last.y + b.layout.height - first.y); + return true; + } + + // The x ranges an inline item's first and last line actually cover. A text + // run's box can be wider than its glyphs (a lone run fills its line for + // text-align), so text is measured again from where its first line starts. + void inlineExtent(const Node &item, int x, int &firstStart, int &firstEnd, int &lastStart, int &lastEnd) const + { + firstStart = lastStart = x; + firstEnd = lastEnd = x + item.layout.width; + if (item.type != NodeType::Text) return; + const int contentLeft = boxInset(node_.style, 3); + const int lineStart = x + std::max(0, item.layout.inline_indent); + const int contentWidth = paddedWidth(); + const InlineFlowMeasure m = TextRenderer::measureInlineFlow(item, contentWidth - (lineStart - contentLeft), contentWidth, true); + if (m.lineCount <= 0) return; + const int hang = FlexLayoutPass::hangingSpace(item); + const int firstWidth = m.firstLineWidth - (m.lineCount == 1 ? hang : m.firstLineTrailingSpace); + const int lastWidth = m.lastLineWidth - hang; + // Unless the line layout placed it, the drawer aligns each line in the box. + const int align = (item.render.inline_baseline & 2) ? 0 : LayoutEngine::physicalTextAlign(item); + const int box = item.layout.width - boxInsets(item.style, true); + const auto offset = [&](int width) { return std::max(0, align == 1 ? (box - width) / 2 : align == 2 ? box - width : 0); }; + firstStart = lineStart + offset(firstWidth); + firstEnd = firstStart + firstWidth; + lastStart = m.lineCount == 1 ? firstStart : x + offset(lastWidth); + lastEnd = lastStart + lastWidth; + } + void stretchAbsoluteChild(int child, Node &childNode, int childAvailWidth, int childAvailHeight) { bool resized = false; @@ -3250,10 +4385,19 @@ class LayoutNodePass { { int x, y, width, height; LayoutEngine::absoluteContainingArea(node_, childNode, x, y, width, height); - if (hasPositionOffset(childNode, 3)) childNode.layout.x = x + resolvedPositionOffsetWithBasis(childNode, 3, width) + childNode.style.margin[3]; + int offset = 0; + if (hasPositionOffset(childNode, 3) && hasPositionOffset(childNode, 1) && + insetAlignedOffset(node_, childNode, true, width, resolvedPositionOffsetWithBasis(childNode, 3, width), + resolvedPositionOffsetWithBasis(childNode, 1, width), offset)) + childNode.layout.x = x + offset; + else if (hasPositionOffset(childNode, 3)) childNode.layout.x = x + resolvedPositionOffsetWithBasis(childNode, 3, width) + childNode.style.margin[3]; else if (hasPositionOffset(childNode, 1)) childNode.layout.x = x + width - childNode.layout.width - resolvedPositionOffsetWithBasis(childNode, 1, width) - childNode.style.margin[1]; else childNode.layout.x = alignedAbsoluteChildPosition(childNode, true); - if (hasPositionOffset(childNode, 0)) childNode.layout.y = y + resolvedPositionOffsetWithBasis(childNode, 0, height) + childNode.style.margin[0]; + if (hasPositionOffset(childNode, 0) && hasPositionOffset(childNode, 2) && + insetAlignedOffset(node_, childNode, false, height, resolvedPositionOffsetWithBasis(childNode, 0, height), + resolvedPositionOffsetWithBasis(childNode, 2, height), offset)) + childNode.layout.y = y + offset; + else if (hasPositionOffset(childNode, 0)) childNode.layout.y = y + resolvedPositionOffsetWithBasis(childNode, 0, height) + childNode.style.margin[0]; else if (hasPositionOffset(childNode, 2)) childNode.layout.y = y + height - childNode.layout.height - resolvedPositionOffsetWithBasis(childNode, 2, height) - childNode.style.margin[2]; else childNode.layout.y = alignedAbsoluteChildPosition(childNode, false); } @@ -3334,7 +4478,8 @@ bool establishesTransformContainingBlock(const Node &node) bool establishesAbsoluteContainingBlock(const Node &node) { - return isOutOfFlowPosition(node.style.position) || node.style.position == 2 || establishesTransformContainingBlock(node); + return isOutOfFlowPosition(node.style.position) || node.style.position == 2 || node.style.position == kPositionSticky || + establishesTransformContainingBlock(node); } int containingBlockForAbsoluteNode(int node, int root, Node *nodes) @@ -3496,15 +4641,23 @@ void positionAbsoluteNodeInContainingBlock(int node, int containing, Node *nodes if (!hasPositionOffsetValue(absolute, 0) && !hasPositionOffsetValue(absolute, 2)) absolute.layout.y = LayoutEngine::alignedAbsoluteOffset(nodes[parent], absolute, false, &containingNode, areaY - parentOffsetY, areaHeight); - int containingX = absolute.layout.x + parentOffsetX; - if (hasPositionOffsetValue(absolute, 3)) + int containingX = absolute.layout.x + parentOffsetX, offset = 0; + if (hasPositionOffsetValue(absolute, 3) && hasPositionOffsetValue(absolute, 1) && + insetAlignedOffset(containingNode, absolute, true, areaWidth, resolvedPositionOffsetForBasis(absolute, 3, areaWidth), + resolvedPositionOffsetForBasis(absolute, 1, areaWidth), offset)) + containingX = areaX + offset; + else if (hasPositionOffsetValue(absolute, 3)) containingX = areaX + resolvedPositionOffsetForBasis(absolute, 3, areaWidth) + absolute.style.margin[3]; else if (hasPositionOffsetValue(absolute, 1)) containingX = areaX + areaWidth - absolute.layout.width - resolvedPositionOffsetForBasis(absolute, 1, areaWidth) - absolute.style.margin[1]; int containingY = absolute.layout.y + parentOffsetY; - if (hasPositionOffsetValue(absolute, 0)) + if (hasPositionOffsetValue(absolute, 0) && hasPositionOffsetValue(absolute, 2) && + insetAlignedOffset(containingNode, absolute, false, areaHeight, resolvedPositionOffsetForBasis(absolute, 0, areaHeight), + resolvedPositionOffsetForBasis(absolute, 2, areaHeight), offset)) + containingY = areaY + offset; + else if (hasPositionOffsetValue(absolute, 0)) containingY = areaY + resolvedPositionOffsetForBasis(absolute, 0, areaHeight) + absolute.style.margin[0]; else if (hasPositionOffsetValue(absolute, 2)) containingY = areaY + areaHeight - absolute.layout.height - @@ -3632,36 +4785,88 @@ int LayoutEngine::alignedAbsoluteOffset(const Node &parent, const Node &childNod const Node *treeNodes = Tree::instance().nodes(); const int childId = static_cast(&childNode - treeNodes); if (const auto *rare = rareDataFor(childId); rare && rare->inlineStaticPosition.valid) { - int continuationOrigin = 0; - if (horizontal && rare->inlineStaticPosition.continuationLine) { - // Resolve fragmented inline origins here, after ancestor layout has - // finalized their first-fragment x positions. The continuation line - // starts at the nearest formatting block's content edge, not at the - // inline parent's first-fragment padding edge. - int inlineOffset = 0; - int cursor = static_cast(&parent - treeNodes); - while (cursor >= 0 && isCssInlineLevelBox(treeNodes[cursor])) { - inlineOffset += treeNodes[cursor].layout.x; - cursor = treeNodes[cursor].parent; + const auto &position = rare->inlineStaticPosition; + // A block-level box that a line placed stands where the block it would be + // starts: at the next line box when content precedes it on its line + // (CSS 2.2 10.3.7). + const bool nextLine = blockLevelView(childNode) && position.afterContent && writingMode(parent) == 0; + // A box inside a split inline wrapper was placed by an ancestor's lines; + // the wrappers in between may have moved to their fragment rectangles. + int dx = 0, dy = 0, cursor = static_cast(&parent - treeNodes); + for (; cursor >= 0 && cursor != position.flowNode; cursor = treeNodes[cursor].parent) { + dx += treeNodes[cursor].layout.x; + dy += treeNodes[cursor].layout.y; + } + if (cursor < 0) dx = dy = 0; + if (horizontal) { + int x = position.x - dx; + if (position.continuationLine || nextLine) { + // Resolve fragmented inline origins here, after ancestor layout has + // finalized their first-fragment x positions. A new line starts at + // the nearest formatting block's content edge, not at the inline + // parent's first-fragment padding edge. + int inlineOffset = 0; + int cursor = static_cast(&parent - treeNodes); + while (cursor >= 0 && isCssInlineLevelBox(treeNodes[cursor])) { + inlineOffset += treeNodes[cursor].layout.x; + cursor = treeNodes[cursor].parent; + } + const int origin = cursor >= 0 ? boxInset(treeNodes[cursor].style, 3) - inlineOffset : 0; + x = (nextLine ? 0 : position.x) + origin; } - if (cursor >= 0) continuationOrigin = boxInset(treeNodes[cursor].style, 3) - inlineOffset; + // The static-position rectangle has no inline size, so an explicit + // justify-self centers or ends the margin box on it. + const int justify = rstyle(childNode.style).justify_self; + const int free = -(childNode.layout.width + childNode.style.margin[1] + childNode.style.margin[3]); + const int used = justify > 0 && writingMode(parent) == 0 ? usedGridAlignment(parent, childNode, justify, free, true, true) + : position.anchorRight ? 2 : 6; + return x + childNode.style.margin[3] + (used == 1 ? roundedHalf(free) : used == 2 ? free : 0); + } + // The static-position rectangle spans the line box's block extent, so an + // explicit align-self aligns within it (CSS Position 3, 4.1). + int y = position.y - dy + (nextLine ? position.lineHeight : 0) + childNode.style.margin[0]; + const int align = childNode.style.align_self; + if (align >= 0 && writingMode(parent) == 0) { + const int free = position.lineHeight - childNode.layout.height - + childNode.style.margin[0] - childNode.style.margin[2]; + const int used = usedGridAlignment(parent, childNode, align, free, false, true); + if (used == 1) y += roundedHalf(free); + else if (used == 2) y += free; } - return (horizontal ? rare->inlineStaticPosition.x + continuationOrigin + childNode.style.margin[3] - : rare->inlineStaticPosition.y + childNode.style.margin[0]); + return y; } if (parent.style.display == kDisplayBlock && !parent.style.flex_direction_explicit && childNode.layout.static_block_axis == (horizontal ? 2 : 1)) { + // A block-level static-position rectangle has no block size: the margin + // box starts at it, or with an explicit align-self centers or ends on it + // (CSS Align 3). A reversed block axis (vertical-rl) starts on the right. const bool reversed = horizontal && gridAxisReversed(parent, true); - return childNode.layout.static_block_start + (reversed ? -childNode.layout.width - childNode.style.margin[1] - : childNode.style.margin[horizontal ? 3 : 0]); + const int free = -((horizontal ? childNode.layout.width : childNode.layout.height) + + childNode.style.margin[horizontal ? 3 : 0] + childNode.style.margin[horizontal ? 1 : 2]); + const int align = childNode.style.align_self; + const int used = align >= 0 ? usedGridAlignment(parent, childNode, align, free, horizontal, true) : reversed ? 2 : 6; + return childNode.layout.static_block_start + childNode.style.margin[horizontal ? 3 : 0] + + (used == 1 ? roundedHalf(free) : used == 2 ? free : 0); } // In block layout the hypothetical box starts at the parent's inline // content edge. RTL anchors its inline-end margin edge, even when the absolute // box is wider than its static-position parent. Its actual containing // block can be an entirely different ancestor. if (horizontal == (writingMode(parent) == 0) && parent.style.display == kDisplayBlock && - (!isInlineLevelNode(parent) || isOutOfFlowPosition(parent.style.position)) && + (!isInlineLevelNode(parent) || isAtomicInline(parent.style) || isOutOfFlowPosition(parent.style.position)) && !parent.style.flex_direction_explicit) { + // An explicit justify-self aligns the margin box in the parent's content + // box, the static-position rectangle's inline extent. auto stays normal + // for an absolutely positioned box (CSS Align 3, 6.1). + const int justify = rstyle(childNode.style).justify_self; + if (justify >= 0) { + const int free = (horizontal ? parent.layout.width : parent.layout.height) - boxInsets(parent.style, horizontal) - + (horizontal ? childNode.layout.width : childNode.layout.height) - + childNode.style.margin[horizontal ? 3 : 0] - childNode.style.margin[horizontal ? 1 : 2]; + const int used = usedGridAlignment(parent, childNode, justify, free, horizontal, true); + return boxInset(parent.style, horizontal ? 3 : 0) + childNode.style.margin[horizontal ? 3 : 0] + + (used == 1 ? roundedHalf(free) : used == 2 ? free : 0); + } return rightToLeft(parent) ? (horizontal ? parent.layout.width : parent.layout.height) - boxInset(parent.style, horizontal ? 1 : 2) - (horizontal ? childNode.layout.width : childNode.layout.height) - childNode.style.margin[horizontal ? 1 : 2] @@ -3759,34 +4964,79 @@ int LayoutEngine::alignedAbsoluteOffset(const Node &parent, const Node &childNod bool LayoutEngine::isInlineLevelNode(const Node &n) { - if (n.style.display == kDisplayFlex || n.style.display == kDisplayGrid || n.style.display == kDisplayNone) - return false; + if (n.style.display == kDisplayNone) return false; + if (isAtomicInline(n.style)) return true; + if (n.style.display == kDisplayFlex || n.style.display == kDisplayGrid) return false; // An explicit `display:block` makes even an inline-level tag (a ) — or a // Text/Image node — a BLOCK-LEVEL box, so it stacks vertically among siblings // instead of flowing into an inline row. Browsers do this; gea must too (e.g. // the weather city list's .city-name/.city-detail spans set display:block to // stack the name over the region). Without an explicit display, fall back to // the tag's intrinsic inline-ness. - if (n.style.display == kDisplayBlock && n.style.display_explicit) return false; + if (n.style.display == kDisplayBlock && n.style.display_explicit && !isInlineBoxDisplay(n.style)) return false; // In Gea's native layout model, vertical margins on inline-level wrappers are // used as an authoring signal that the wrapper occupies its own line. Keep // default spans inline, but do not merge title/score spans with margin-bottom // into one row. if (n.style.margin[0] != 0 || n.style.margin[2] != 0) return false; if (n.type == NodeType::Text || n.type == NodeType::Image) return true; - return isInlineLevelTag(tagFromId(n.tag_id)); + return inlineBoxView(n); +} + +// Mirror of startsFormattingLine: walk forward through siblings and inline +// ancestors to the next content that shares or closes this line box. +bool LayoutEngine::multicolContainer(const Node &node) +{ + return isMulticolContainer(node); +} + +bool LayoutEngine::rightToLeftDirection(const Node &node) +{ + return rightToLeft(node); +} + +int LayoutEngine::physicalTextAlign(const Node &node) +{ + switch (node.style.text_align) { + case 1: return 1; + case 2: return 2; + case 3: return 0; + case 4: return rightToLeft(node) ? 0 : 2; + default: return rightToLeft(node) ? 2 : 0; + } +} + +bool LayoutEngine::endsFormattingLine(int id) +{ + const Node *nodes = Tree::instance().nodes(); + while (isCssInlineLevelBox(nodes[id]) && isInlineLevelNode(nodes[id])) { + for (int sibling = nodes[id].next_sibling; sibling >= 0; sibling = nodes[sibling].next_sibling) { + const Node &next = nodes[sibling]; + if (isDisplayNone(next.style) || isOutOfFlowPosition(next.style.position) || next.style.float_side) + continue; + if (isAnonymousTextNode(next) && + (next.style.white_space == 0 || next.style.white_space == 1 || next.style.white_space == 4) && + next.text.find_first_not_of(" \t\r\n\f") == std::string::npos) continue; + const int edge = formattingEdgeNode(nodes, sibling, false); + if (edge < 0) continue; + return isLineBreak(nodes[edge]) || !isInlineLevelNode(nodes[edge]); + } + id = nodes[id].parent; + if (id < 0) return true; + } + return true; } bool LayoutEngine::isCssInlineLevelBox(const Node &n, bool hypothetical) { - if (n.style.display == kDisplayNone || n.style.display_explicit) return false; + if (n.style.display == kDisplayNone || (n.style.display_explicit && !isInlineBoxDisplay(n.style))) return false; if (!hypothetical && (n.style.float_side || isOutOfFlowPosition(n.style.position))) return false; if (!hypothetical && n.parent >= 0) { const Node &parent = Tree::instance().nodes()[n.parent]; if (parent.style.display == kDisplayFlex || isDisplayGrid(parent.style)) return false; } if (n.type == NodeType::Text || n.type == NodeType::Image) return true; - return n.type == NodeType::View && isInlineLevelTag(tagFromId(n.tag_id)); + return inlineBoxView(n); } int LayoutEngine::clampSize(int size, int minSize, int maxSize) const @@ -3840,10 +5090,11 @@ void LayoutEngine::layoutNode(int id, int avail_w, int avail_h, bool intrinsicBo { Node &node = Tree::instance().nodes()[id]; const Node *parent = node.parent >= 0 ? &Tree::instance().nodes()[node.parent] : nullptr; - // Horizontal non-replaced floats use the same min/max-content measurement - // and final used-width phase as fit-content. Floats on flex/grid items are - // ignored; orthogonal automatic inline sizing is handled separately. - const bool shrinkFloat = node.type == NodeType::View && node.style.float_side && + // Horizontal non-replaced floats and atomic inline boxes (inline-flex) use + // the same min/max-content measurement and final used-width phase as + // fit-content. Flex and grid items are blockified, so they are left alone; + // orthogonal automatic inline sizing is handled separately. + const bool shrinkFloat = node.type == NodeType::View && (node.style.float_side || isAtomicInline(node.style)) && !isOutOfFlowPosition(node.style.position) && (!parent || (parent->style.display != kDisplayFlex && !isDisplayGrid(parent->style))) && node.style.width == kUnset && node.style.width_percent == kUnset && node.style.width_expression == -1 && @@ -3927,6 +5178,7 @@ void LayoutEngine::layoutNode(int id, int avail_w, int avail_h, bool intrinsicBo applyPreferredRatio(node, avail_w, avail_h); if (node.first_child >= 0 && (node.layout.width != beforeRatioWidth || node.layout.height != beforeRatioHeight)) LayoutNodePass(*this, id, node.layout.width, node.layout.height).repositionChildren(); + if (clampsLines(node)) applyLineClamp(id); if (node.parent < 0) { // The initial containing block places the root; there is no parent // layout pass to position its margin box or apply float alignment. @@ -3956,6 +5208,7 @@ void LayoutEngine::repositionChildren(int id) refreshPerfStatsMutable().treeLayoutRepositionCalls++; Node *node = &Tree::instance().nodes()[id]; LayoutNodePass(*this, id, node->layout.width, node->layout.height).repositionChildren(); + if (clampsLines(*node)) applyLineClamp(id); } bool LayoutEngine::layoutNodeScoped(int scope, int treeRoot) @@ -4043,8 +5296,8 @@ bool LayoutEngine::layoutNodeScoped(int scope, int treeRoot) // A parent-owned box cannot be reproduced by laying only the child's // subtree. Put those dimensions back and position its descendants inside // the same box exactly as the omitted parent pass would. - a.layout.width = static_cast(prevW); - a.layout.height = static_cast(prevH); + a.layout.width = prevW; + a.layout.height = prevH; repositionChildren(scope); } a.layout.x = prevX; @@ -4072,7 +5325,75 @@ bool LayoutEngine::layoutNodeScoped(int scope, int treeRoot) return true; } +namespace { + +int stickyInset(const Node &node, int side, int basis) +{ + int inset = node.style.pos_offsets[side] != kUnset ? node.style.pos_offsets[side] : 0; + if (node.style.pos_offset_percent[side] != kUnset) inset += basis * node.style.pos_offset_percent[side] / 1000; + return inset; +} + +bool hasStickyInset(const Node &node, int side) +{ + return node.style.pos_offsets[side] != kUnset || node.style.pos_offset_percent[side] != kUnset; +} + +// position: sticky (CSS Position 3): shift the box, never out of its +// containing block, just enough to keep it inset from its scrollport edges. +// `port` is that scrollport, x0 y0 x1 y1 exclusive, in resolved coordinates. +void applyStickyOffset(Node &node, const Node &block, const int port[4]) +{ + const int portW = port[2] - port[0], portH = port[3] - port[1]; + auto axis = [&](int start, int size, int marginStart, int marginEnd, int portStart, int portEnd, + int blockStart, int blockEnd, int startSide, int endSide, int basis) { + int shift = 0; + if (hasStickyInset(node, startSide)) { + const int limit = portStart + stickyInset(node, startSide, basis); + if (start < limit) shift = std::min(limit - start, std::max(0, blockEnd - marginEnd - (start + size))); + } + if (hasStickyInset(node, endSide)) { + const int limit = portEnd - stickyInset(node, endSide, basis); + if (start + size + shift > limit) + shift = std::max(limit - (start + size), std::min(0, blockStart + marginStart - start)); + } + return shift; + }; + const int blockX0 = block.layout.x + boxInset(block.style, 3), blockX1 = block.layout.x + block.layout.width - boxInset(block.style, 1); + const int blockY0 = block.layout.y + boxInset(block.style, 0), blockY1 = block.layout.y + block.layout.height - boxInset(block.style, 2); + node.layout.y += axis(node.layout.y, node.layout.height, node.style.margin[0], node.style.margin[2], port[1], port[3], + blockY0, blockY1, 0, 2, portH); + node.layout.x += axis(node.layout.x, node.layout.width, node.style.margin[3], node.style.margin[1], port[0], port[2], + blockX0, blockX1, 3, 1, portW); +} + +// A scroll container's padding box, in resolved coordinates. +void scrollportOf(const Node &node, int port[4]) +{ + port[0] = node.layout.x + std::max(node.style.border_width, rstyle(node.style).border_side_width[3]); + port[1] = node.layout.y + std::max(node.style.border_width, rstyle(node.style).border_side_width[0]); + port[2] = node.layout.x + node.layout.width - std::max(node.style.border_width, rstyle(node.style).border_side_width[1]); + port[3] = node.layout.y + node.layout.height - std::max(node.style.border_width, rstyle(node.style).border_side_width[2]); +} + +} // namespace + void LayoutEngine::resolveAbsoluteCoords(int id, int parent_x, int parent_y) +{ + Node *nodes = Tree::instance().nodes(); + if (nodes[id].parent < 0) treeState().stickyPresent = false; + // A subtree pass (scoped relayout) inherits its nearest scroll container's + // scrollport; its ancestors are already resolved. + int port[4] = {0, 0, Tree::instance().mountedWidth(), Tree::instance().mountedHeight()}; + for (int ancestor = nodes[id].parent; ancestor >= 0; ancestor = nodes[ancestor].parent) { + if (!overflowEstablishesContext(nodes[ancestor].style)) continue; + scrollportOf(nodes[ancestor], port); + break; + } + resolveAbsoluteCoordsIn(id, parent_x, parent_y, port); +} + +void LayoutEngine::resolveAbsoluteCoordsIn(int id, int parent_x, int parent_y, const int *scrollport) { Node *nodes = Tree::instance().nodes(); Node *node = &nodes[id]; @@ -4081,6 +5402,10 @@ void LayoutEngine::resolveAbsoluteCoords(int id, int parent_x, int parent_y) node->layout.x += parent_x; node->layout.y += parent_y; } + if (node->style.position == kPositionSticky && node->parent >= 0 && !isDisplayNone(node->style)) { + applyStickyOffset(*node, nodes[node->parent], scrollport); + treeState().stickyPresent = true; + } int childParentX = node->layout.x; int childParentY = node->layout.y; @@ -4088,9 +5413,15 @@ void LayoutEngine::resolveAbsoluteCoords(int id, int parent_x, int parent_y) if (scrollsOverflowX(node->style)) childParentX -= node->layout.scroll_x; if (scrollsOverflowY(node->style)) childParentY -= node->layout.scroll_y; } + // A scroll container's padding box is the scrollport of sticky descendants. + int port[4]; + if (overflowEstablishesContext(node->style)) { + scrollportOf(*node, port); + scrollport = port; + } for (int child = node->first_child; child >= 0; child = nodes[child].next_sibling) { - resolveAbsoluteCoords(child, childParentX, childParentY); + resolveAbsoluteCoordsIn(child, childParentX, childParentY, scrollport); } } diff --git a/packages/engine/ui/layout_snapshot.cpp b/packages/engine/ui/layout_snapshot.cpp index 62d4a4a..b07506e 100644 --- a/packages/engine/ui/layout_snapshot.cpp +++ b/packages/engine/ui/layout_snapshot.cpp @@ -61,6 +61,7 @@ void LayoutSnapshot::capture() state.nodes[i].render.previous_perspective_origin_y = rs.perspective_origin_y; } state.nodes[i].render.previous_filter_blur_radius = rs.filter_blur_radius; + state.nodes[i].render.previous_box_shadow_extent = static_cast(std::min(32767, boxShadowExtent(state.nodes[i].style))); state.nodes[i].render.dirty = 0; state.nodes[i].render.layout_dirty = 0; state.nodes[i].render.scroll_dirty = 0; diff --git a/packages/engine/ui/node_lifecycle.cpp b/packages/engine/ui/node_lifecycle.cpp index 7f23e57..52efca1 100644 --- a/packages/engine/ui/node_lifecycle.cpp +++ b/packages/engine/ui/node_lifecycle.cpp @@ -88,6 +88,10 @@ void NodeLifecycle::init(Node *n, NodeType type) n->style.line_height = 0; n->style.line_height_multiplier = -1; n->style.text_align = 0; + n->style.text_align_last = 0; + n->style.text_emphasis = 0; + n->style.text_emphasis_color = 0; + n->style.vertical_align = 0; n->style.text_decoration = 0; n->style.text_transform = 0; n->style.white_space = 0; @@ -113,6 +117,8 @@ void NodeLifecycle::init(Node *n, NodeType type) n->layout.width = 0; n->layout.height = 0; n->layout.inline_indent = 0; + n->layout.line_clamp_hidden = 0; + n->layout.line_clamp_lines = 0; n->layout.scroll_x = 0; n->layout.scroll_y = 0; n->layout.scroll_content_width = 0; @@ -151,6 +157,7 @@ void NodeLifecycle::init(Node *n, NodeType type) n->render.previous_perspective_origin_y = 500; n->render.previous_transformable_box = 0; n->render.previous_filter_blur_radius = 0; + n->render.previous_box_shadow_extent = 0; n->render.inline_baseline = 0; n->render.dirty = 0; n->render.scroll_dirty = 0; diff --git a/packages/engine/ui/node_model.h b/packages/engine/ui/node_model.h index 00789f0..791788e 100644 --- a/packages/engine/ui/node_model.h +++ b/packages/engine/ui/node_model.h @@ -29,6 +29,20 @@ using style_color_t = gea::framework::graphics::pixel::native_t; #endif inline constexpr int kMaxNodes = GEA_EMBEDDED_MAX_NODES; inline constexpr int kUnset = -32768; +// Position offsets are int16 with kUnset meaning auto. Saturate every other +// length so a huge inset stays far away instead of wrapping or reading as auto. +inline int16_t storedPositionOffset(int value) +{ + return static_cast(value == kUnset ? kUnset : std::clamp(value, -32767, 32767)); +} +// Width and height (declared and laid out) are 32-bit: a box can be larger than +// int16 allows and still fit the screen under a scale transform. The bound +// keeps every size exact in the float math of transform projection. +inline constexpr int kMaxLayoutExtent = 1 << 24; +inline int32_t clampLayoutExtent(int value) +{ + return std::clamp(value, -kMaxLayoutExtent, kMaxLayoutExtent); +} // Internal value carrier for CSS z-index:auto; numeric stack levels stay int16. inline constexpr int kZIndexAuto = INT32_MIN; // Dimension expression slots use nonnegative values for pooled expressions, @@ -68,7 +82,18 @@ inline constexpr int8_t kDisplayBlock = 0; inline constexpr int8_t kDisplayNone = 1; inline constexpr int8_t kDisplayGrid = 2; inline constexpr int8_t kDisplayFlex = 3; +// A Display property value carries the box kind in its low bits and extra +// keyword bits above kDisplayFlagShift; the extra bits land in display_explicit. +inline constexpr int kDisplayKindMask = 15; +inline constexpr int kDisplayFlagShift = 4; +inline constexpr int8_t kDisplayExplicit = 1; +inline constexpr int8_t kDisplayFlowRoot = 2; // display: flow-root +// display: inline on a block kind; with kDisplayFlowRoot it is inline-block, and +// on a flex or grid kind inline-flex or inline-grid. Those three are atomic. +inline constexpr int8_t kDisplayInline = 4; inline constexpr int kPositionFixed = 3; +// position: sticky stays in flow; the absolute-coordinate pass shifts it. +inline constexpr int kPositionSticky = 4; inline bool isOutOfFlowPosition(int position) { return position == 1 || position == kPositionFixed; } // Existing alignment values occupy the low nibble. Overflow-position is // independent of the alignment keyword and fits in the existing int8 fields. @@ -135,6 +160,11 @@ inline bool isNativeButtonNodeType(NodeType type) struct RareStyle { // Logical edges: block-start/end, inline-start/end. uint8_t margin_trim = 0; + // Multi-column: column-count (0 = auto). Sits in padding. + uint8_t column_count = 0; + // CSS atom of a custom block-ellipsis string; 0 = the default ellipsis. + // Only used when line_clamp_flags has block-ellipsis set. Sits in padding. + uint16_t block_ellipsis = 0; // IEEE float bits travel through the existing integer style-value transport. // Negative ratios mean `auto ` (prefer a replaced element's natural ratio). int32_t aspect_ratio = 0; @@ -207,6 +237,8 @@ struct RareStyle { // only relevant when a side width > 0, which is rare) --- int16_t border_side_width[4] = {}; // 0: flat; 1: groove; 2: ridge; 3: inset; 4: outset. + // Per side: 1-4 groove / ridge / inset / outset, and kBorderStyleNone when + // border-style is none or hidden (the side then keeps a zero width). uint8_t border_relief[4] = {}; // Bits 0..3: explicit side colors; bit 4: literal common color; // bits 5..8: explicit side currentColor. Unspecified colors use currentColor. @@ -224,6 +256,8 @@ struct RareStyle { // NodeLifecycle::init: alphas 255, stops 500/1000, angle 1800, radial // center/extent 500/1000) --- uint16_t bg_image_layer_count = 1; // Includes none layers. + // Multi-column: column-width in px, -1 = auto. Sits in padding. + int16_t column_width = -1; int32_t bg_size_list = -1, bg_position_list = -1, bg_repeat_list = -1; int32_t bg_attachment_list = -1, bg_origin_list = -1; uint16_t bg_gradient_layer = 0, bg_overlay_gradient_layer = 0, bg_radial_gradient_layer = 0; @@ -233,22 +267,30 @@ struct RareStyle { uint8_t bg_gradient_from_alpha = 255; uint8_t bg_gradient_mid_alpha = 255; uint8_t bg_gradient_to_alpha = 255; + // border-image-* (see StyleValues::borderImageSource/Sides/Repeat) sit in + // padding here: interned handles, zero is the initial value. + uint8_t border_image_source = 0; uint16_t bg_gradient_mid_stop = 500; uint16_t bg_gradient_to_stop = 1000; uint8_t bg_gradient_has_mid = 0; + uint8_t border_image_slice = 0; int16_t bg_gradient_angle = 1800; uint8_t bg_overlay_gradient = 0; + uint8_t bg_blend = 0; // Interned background-blend-mode list; zero is all normal. style_color_t bg_overlay_gradient_from_color = 0; style_color_t bg_overlay_gradient_mid_color = 0; style_color_t bg_overlay_gradient_to_color = 0; uint8_t bg_overlay_gradient_from_alpha = 255; uint8_t bg_overlay_gradient_mid_alpha = 255; uint8_t bg_overlay_gradient_to_alpha = 255; + uint8_t border_image_width = 0; uint16_t bg_overlay_gradient_mid_stop = 500; uint16_t bg_overlay_gradient_to_stop = 1000; uint8_t bg_overlay_gradient_has_mid = 0; + uint8_t border_image_outset = 0; int16_t bg_overlay_gradient_angle = 1800; uint8_t bg_radial_gradient = 0; + uint8_t border_image_repeat = 0; // bits 0-1 horizontal, 2-3 vertical: stretch, repeat, round, space style_color_t bg_radial_gradient_from_color = 0; style_color_t bg_radial_gradient_to_color = 0; uint8_t bg_radial_gradient_from_alpha = 255; @@ -265,6 +307,13 @@ struct RareStyle { uint16_t bg_grid_step_y = 0; uint8_t bg_grid_line_x = 0; uint8_t bg_grid_line_y = 0; + // CSS Overflow 4 line clamping: max-lines (0 = none, capped at 255) and + // flags: 1 = continue: collapse or discard, 2 = block-ellipsis other than + // none, 4 / 8 = column-count / column-width set (a multicol container never + // clamps), 16 = column-fill: auto, 32 = continue: discard, 64 = column-span: + // all. Both sit in trailing padding, so RareStyle keeps its size. + uint8_t max_lines = 0; + uint8_t line_clamp_flags = 0; }; // Individual translation is applied outside the transform list. Its translation @@ -287,7 +336,7 @@ struct ComputedStyle { // inline-formatting heuristic distinguish an explicit `display:block` on an // inline-level tag (e.g. , which is block-level // and stacks) from a span's default inline behaviour. Mirrors - // flex_direction_explicit. + // flex_direction_explicit. Other bits: kDisplayFlowRoot, kDisplayInline. int8_t display_explicit; int8_t flex_direction; int8_t flex_direction_explicit; @@ -298,6 +347,9 @@ struct ComputedStyle { int8_t align_content; int8_t align_self; int16_t gap; + // text-emphasis-color when text_emphasis says it is explicit. Inherited; + // sits in padding on 16-bit colour builds. + style_color_t text_emphasis_color; int8_t box_sizing; // 0: content-box (CSS initial), 1: border-box int8_t float_side; // 0: none, 1: left, 2: right int8_t clear_side; // 0: none, 1: left, 2: right, 3: both @@ -312,7 +364,7 @@ struct ComputedStyle { int32_t line_height_multiplier; int32_t width_expression, height_expression; - int16_t width, height; + int32_t width, height; // kUnset: auto; see kMaxLayoutExtent int16_t width_percent, height_percent; int16_t min_width, min_height; int16_t max_width, max_height; @@ -338,15 +390,25 @@ struct ComputedStyle { int8_t has_bg; uint8_t bg_alpha; int8_t bg_fill; + // text-emphasis (inherited): bits 0-2 mark 0 none, 1 dot, 2 circle, + // 3 double-circle, 4 triangle, 5 sesame; bit 3 open; bit 4 under; bits 5-6 + // colour 0 currentColor, 1 text_emphasis_color, 2 transparent. In padding. + uint8_t text_emphasis; // linear/overlay/radial gradients + background-grid moved to RareStyle (rare). style_color_t active_bg_color; int8_t has_active_bg; + // CSS `vertical-align` of an inline-level box: 0 baseline, 1 top, 2 bottom, + // 3 middle, 4 text-top, 5 text-bottom, 6 sub, 7 super. Sits in padding. + int8_t vertical_align; style_color_t text_color; uint8_t text_alpha; uint8_t opacity; int16_t blink_interval_ms; int32_t blink_started_ms; uint8_t blink_visible; + // CSS `text-align-last`: 0 = auto (follow text-align), otherwise a text_align + // value plus one. Inherited. Sits in padding, so ComputedStyle keeps its size. + int8_t text_align_last; int16_t border_width; style_color_t border_color; @@ -362,7 +424,7 @@ struct ComputedStyle { int16_t font_size; int16_t font_weight; int16_t line_height; - int8_t text_align; + int8_t text_align; // 0 start, 1 center, 2 right, 3 left, 4 end int8_t overflow; int8_t overflow_x; int8_t overflow_y; @@ -523,6 +585,9 @@ inline int computedBorderWidth(const ComputedStyle &style, int side) return specific > style.border_width ? specific : style.border_width; } +// RareStyle::border_relief bit for border-style: none / hidden on that side. +inline constexpr uint8_t kBorderStyleNone = 0x80; + inline bool setComputedBorderWidth(ComputedStyle &style, int side, int value, const ComputedStyle *parent) { int widths[4]; @@ -530,6 +595,8 @@ inline bool setComputedBorderWidth(ComputedStyle &style, int side, int value, co widths[i] = computedBorderWidth(style, i); if (side < 0 || side == i) widths[i] = value == kInheritedBorderWidth ? (parent ? computedBorderWidth(*parent, i) : 0) : value; + // A side whose style is none has no border, whatever width it declares. + if (rstyle(style).border_relief[i] & kBorderStyleNone) widths[i] = 0; } const bool uniform = side < 0 && widths[0] == widths[1] && widths[0] == widths[2] && widths[0] == widths[3]; // A side can override a wider common border with a narrower or zero edge. @@ -643,6 +710,26 @@ inline bool isDisplayGrid(const ComputedStyle &style) return style.display == kDisplayGrid; } +inline bool isFlowRoot(const ComputedStyle &style) +{ + return style.display == kDisplayBlock && (style.display_explicit & kDisplayFlowRoot); +} + +// inline-block, inline-flex and inline-grid sit on a line like an image, sized +// to their content. Floats and absolutely positioned boxes are blockified. +inline bool isAtomicInline(const ComputedStyle &style) +{ + return (style.display_explicit & kDisplayInline) && (style.display != kDisplayBlock || (style.display_explicit & kDisplayFlowRoot)) && + !style.float_side && !isOutOfFlowPosition(style.position); +} + +// An authored display: inline, which makes any element an inline box like a +// . +inline bool isInlineBoxDisplay(const ComputedStyle &style) +{ + return style.display == kDisplayBlock && (style.display_explicit & (kDisplayInline | kDisplayFlowRoot)) == kDisplayInline; +} + inline bool usesRowLayout(const ComputedStyle &style) { // `flex-direction` applies to FLEX CONTAINERS, and to nothing else -- @@ -705,6 +792,17 @@ inline bool hasSideBorder(const ComputedStyle &style) r.border_side_width[3] > 0; } +// How far an outer box-shadow can paint outside the border box. +inline int boxShadowExtent(const ComputedStyle &style) +{ + const RareStyle &r = rstyle(style); + if (r.box_shadow_inset || r.box_shadow_alpha == 0) return 0; + const int ox = r.box_shadow_offset_x < 0 ? -r.box_shadow_offset_x : r.box_shadow_offset_x; + const int oy = r.box_shadow_offset_y < 0 ? -r.box_shadow_offset_y : r.box_shadow_offset_y; + const int reach = r.box_shadow_spread + (r.box_shadow_blur_radius > 0 ? r.box_shadow_blur_radius : 0) + (ox > oy ? ox : oy); + return reach > 0 ? reach : 0; +} + inline bool hasAnyBorder(const ComputedStyle &style) { return style.border_width > 0 || hasSideBorder(style); @@ -713,13 +811,14 @@ inline bool hasAnyBorder(const ComputedStyle &style) inline bool hasBorderRelief(const ComputedStyle &style) { const auto &r = rstyle(style); - return r.border_relief[0] || r.border_relief[1] || r.border_relief[2] || r.border_relief[3]; + constexpr uint8_t relief = static_cast(~kBorderStyleNone); + return (r.border_relief[0] & relief) || (r.border_relief[1] & relief) || (r.border_relief[2] & relief) || (r.border_relief[3] & relief); } struct LayoutBox { int16_t x, y; int32_t width_expression, height_expression; - int16_t width, height; + int32_t width, height; // see kMaxLayoutExtent // Inline formatting: x offset, inside this box's content area, at which the // run's FIRST line starts. A text run that begins part-way along a line box @@ -734,9 +833,17 @@ struct LayoutBox { // and absolute-coordinate conversion; retained refresh reuses this anchor. int16_t static_block_start = 0; uint8_t static_block_axis = 0; // 0: unavailable, 1: y, 2: x + // line-clamp: bit 0 when the box lies after an ancestor's clamp point, which + // hides it; bit 1 on the text run whose last painted line ends before the + // clamp point and carries the block ellipsis. Like line_clamp_lines below + // it sits in padding. + uint8_t line_clamp_hidden = 0; int16_t previous_x, previous_y; - int16_t previous_width, previous_height; + // line-clamp: a text run its container's clamp point cuts paints only + // this many lines. 0 paints them all. + int16_t line_clamp_lines = 0; + int32_t previous_width, previous_height; // Scroll geometry must be 32-bit: a scrolls over a virtual // content height of itemCount * rowHeight (e.g. 5000 * 259 ≈ 1.29M px), @@ -838,7 +945,12 @@ struct RenderState { // the line advance: ink centring shifts by however far that particular string's // glyphs reach, so runs of different words — or different fonts — on one line // end up at different heights. A run that owns its line keeps the centring. + // Bit 1: the line layout already applied text-align to this box's line, so + // the run draws start-aligned in its own box. uint8_t inline_baseline; + // boxShadowExtent at the last snapshot, so a moved or restyled shadow + // invalidates the pixels it used to cover. Sits in trailing padding. + int16_t previous_box_shadow_extent; }; inline bool hadIndividualLinearTransform(const RenderState &s) diff --git a/packages/engine/ui/render.cpp b/packages/engine/ui/render.cpp index 927fe21..664d9c4 100644 --- a/packages/engine/ui/render.cpp +++ b/packages/engine/ui/render.cpp @@ -1507,6 +1507,33 @@ int gLastScrollUiFrame = -1000; using TransformedRoundedRectCommand = decltype(DisplayCommand{}.transformedRoundedRect); + bool transformedRoundedRectIsRing(const TransformedRoundedRectCommand &r) + { + return (r.ring[0] | r.ring[1] | r.ring[2] | r.ring[3]) != 0; + } + + // The unpainted inside of a border ring: the padding box, whose radii + // are the outer radii less the adjoining border widths (CSS Backgrounds 5.2). + TransformedRoundedRectCommand transformedRoundedRectHole(const TransformedRoundedRectCommand &r) + { + TransformedRoundedRectCommand hole = r; + hole.ring[0] = hole.ring[1] = hole.ring[2] = hole.ring[3] = 0; + hole.lx = static_cast(r.lx + r.ring[3]); + hole.ly = static_cast(r.ly + r.ring[0]); + hole.lw = static_cast(std::max(0, r.lw - r.ring[1] - r.ring[3])); + hole.lh = static_cast(std::max(0, r.lh - r.ring[0] - r.ring[2])); + hole.tlRx8 = static_cast(std::max(0, r.tlRx8 - 8 * r.ring[3])); + hole.tlRy8 = static_cast(std::max(0, r.tlRy8 - 8 * r.ring[0])); + hole.trRx8 = static_cast(std::max(0, r.trRx8 - 8 * r.ring[1])); + hole.trRy8 = static_cast(std::max(0, r.trRy8 - 8 * r.ring[0])); + hole.brRx8 = static_cast(std::max(0, r.brRx8 - 8 * r.ring[1])); + hole.brRy8 = static_cast(std::max(0, r.brRy8 - 8 * r.ring[2])); + hole.blRx8 = static_cast(std::max(0, r.blRx8 - 8 * r.ring[3])); + hole.blRy8 = static_cast(std::max(0, r.blRy8 - 8 * r.ring[2])); + return hole; + } + + struct RoundedRectScreenSpan { int x; @@ -1649,7 +1676,7 @@ int gLastScrollUiFrame = -1000; { const auto &r = command.transformedRoundedRect; RoundedRectScreenSpan span{}; - if (!transformedRoundedRectToScreenSpan(r, &span)) + if (transformedRoundedRectIsRing(r) || !transformedRoundedRectToScreenSpan(r, &span)) return false; if (!roundedRectScreenSpanFitsCanvas(span)) return false; @@ -1850,7 +1877,7 @@ int gLastScrollUiFrame = -1000; pixel = gea::framework::graphics::pixel::blendNative(color, pixel, effectiveAlpha); } - bool roundedRectContainsFast(const TransformedRoundedRectCommand &r, float sx, float sy) + bool roundedRectOuterContainsFast(const TransformedRoundedRectCommand &r, float sx, float sy) { const float left = static_cast(r.lx); const float top = static_cast(r.ly); @@ -1881,6 +1908,13 @@ int gLastScrollUiFrame = -1000; return true; } + bool roundedRectContainsFast(const TransformedRoundedRectCommand &r, float sx, float sy) + { + if (!roundedRectOuterContainsFast(r, sx, sy)) + return false; + return !transformedRoundedRectIsRing(r) || !roundedRectOuterContainsFast(transformedRoundedRectHole(r), sx, sy); + } + int roundedRectCoverageFast(const TransformedRoundedRectCommand &r, float localX, float localY, @@ -1915,6 +1949,8 @@ int gLastScrollUiFrame = -1000; float marginX, float marginY) { + if (transformedRoundedRectIsRing(r)) + return false; const float left = static_cast(r.lx); const float top = static_cast(r.ly); const float right = left + static_cast(r.lw); @@ -1957,6 +1993,8 @@ int gLastScrollUiFrame = -1000; int roundedRectCoverageHomography(const TransformedRoundedRectCommand &r, const double *homography, int x, int y, int samples) { const float kernelWidth = transformedRoundedRectAntialiasKernelWidth(samples, r); + const bool ring = transformedRoundedRectIsRing(r); + const TransformedRoundedRectCommand hole = ring ? transformedRoundedRectHole(r) : r; int coverage = 0; for (int iy = 0; iy < samples; ++iy) { @@ -1981,13 +2019,222 @@ int gLastScrollUiFrame = -1000; r.blRx8, r.blRy8, sx, - sy)) + sy) && + (!ring || !roundedRectOuterContainsFast(hole, static_cast(sx), static_cast(sy)))) ++coverage; } } return coverage; } + + // Multicol column copies: commands [begin, end) repeat those `offset` + // indices earlier, translated into a later column box. Text-clipped + // backgrounds in a copy take their glyph mask from the same copy. + struct ColumnCopy + { + int begin, end, offset; + }; + + std::vector &columnCopies() + { + static std::vector value; + return value; + } + + int columnCopyOffset(const DisplayCommand &command) + { + if (!state.commands || &command < state.commands || &command >= state.commands + state.commandCount) + return 0; + const int index = static_cast(&command - state.commands); + for (const auto © : columnCopies()) + if (index >= copy.begin && index < copy.end) + return copy.offset; + return 0; + } + + // Rounded and transformed overflow clips. Display clips are rectangles, so + // replay saves the canvas pixels a shaped clip's bounds may expose, lets + // the clipped content paint, then puts back every pixel outside the shape + // (in part where antialiasing covers it in part). + class ShapedClips + { + public: + static std::size_t depth() { return entries().size(); } + + static void begin(const DisplayCommand &clip) + { + const auto &c = clip.clip; + TransformedRoundedRectCommand shape{}; + shape.x0 = c.qx[0]; shape.y0 = c.qy[0]; + shape.x1 = c.qx[1]; shape.y1 = c.qy[1]; + shape.x2 = c.qx[2]; shape.y2 = c.qy[2]; + shape.x3 = c.qx[3]; shape.y3 = c.qy[3]; + shape.lx = c.lx; shape.ly = c.ly; shape.lw = c.lw; shape.lh = c.lh; + shape.tlRx8 = c.rx8[0]; shape.tlRy8 = c.ry8[0]; + shape.trRx8 = c.rx8[1]; shape.trRy8 = c.ry8[1]; + shape.brRx8 = c.rx8[2]; shape.brRy8 = c.ry8[2]; + shape.blRx8 = c.rx8[3]; shape.blRy8 = c.ry8[3]; + begin(shape); + } + + static void begin(const OverflowClipShape &clip) + { + TransformedRoundedRectCommand shape{}; + shape.x0 = clip.qx[0]; shape.y0 = clip.qy[0]; + shape.x1 = clip.qx[1]; shape.y1 = clip.qy[1]; + shape.x2 = clip.qx[2]; shape.y2 = clip.qy[2]; + shape.x3 = clip.qx[3]; shape.y3 = clip.qy[3]; + shape.lx = clip.lx; shape.ly = clip.ly; shape.lw = clip.lw; shape.lh = clip.lh; + shape.tlRx8 = clip.rx8[0]; shape.tlRy8 = clip.ry8[0]; + shape.trRx8 = clip.rx8[1]; shape.trRy8 = clip.ry8[1]; + shape.brRx8 = clip.rx8[2]; shape.brRy8 = clip.ry8[2]; + shape.blRx8 = clip.rx8[3]; shape.blRy8 = clip.ry8[3]; + begin(shape); + } + + static void endTo(std::size_t mark) + { + while (entries().size() > mark) + end(); + } + + static void end() + { + const Entry entry = entries().back(); + entries().pop_back(); + restore(entry); + saved().resize(entry.offset); + } + + private: + struct Box + { + int x0, y0, x1, y1; + }; + struct Entry + { + TransformedRoundedRectCommand shape; + float inv00, inv01, inv10, inv11; + Box boxes[4]; + int boxCount; + std::size_t offset; + }; + + static std::vector &entries() + { + static std::vector value; + return value; + } + + static std::vector &saved() + { + static std::vector value; + return value; + } + + static void begin(const TransformedRoundedRectCommand &shape) + { + Entry entry{}; + entry.shape = shape; + entry.offset = saved().size(); + auto *canvas = gea::platform::display::Display::canvas(); + const float a = shape.lw ? static_cast(shape.x1 - shape.x0) / shape.lw : 0.0f; + const float b = shape.lh ? static_cast(shape.x3 - shape.x0) / shape.lh : 0.0f; + const float c = shape.lw ? static_cast(shape.y1 - shape.y0) / shape.lw : 0.0f; + const float d = shape.lh ? static_cast(shape.y3 - shape.y0) / shape.lh : 0.0f; + const float det = a * d - b * c; + if (canvas && canvas->pixels() && std::fabs(det) >= 1e-6f) + { + entry.inv00 = d / det; + entry.inv01 = -b / det; + entry.inv10 = -c / det; + entry.inv11 = a / det; + Box candidates[4]; + int count = 0; + if (transformedRoundedRectLooksAxisAligned(shape) && shape.x1 - shape.x0 == shape.lw && shape.y3 - shape.y0 == shape.lh) + { + // Only the corner boxes can lie outside an untransformed rounded rect. + const auto extent = [](int radius8) { return (std::max(0, radius8) + 7) / 8; }; + const int left = shape.lx, top = shape.ly, right = shape.lx + shape.lw - 1, bottom = shape.ly + shape.lh - 1; + const int tlW = extent(shape.tlRx8), tlH = extent(shape.tlRy8), trW = extent(shape.trRx8), trH = extent(shape.trRy8); + const int brW = extent(shape.brRx8), brH = extent(shape.brRy8), blW = extent(shape.blRx8), blH = extent(shape.blRy8); + if (tlW && tlH) candidates[count++] = {left, top, left + tlW - 1, top + tlH - 1}; + if (trW && trH) candidates[count++] = {right - trW + 1, top, right, top + trH - 1}; + if (brW && brH) candidates[count++] = {right - brW + 1, bottom - brH + 1, right, bottom}; + if (blW && blH) candidates[count++] = {left, bottom - blH + 1, left + blW - 1, bottom}; + } + else + { + int x0, y0, x1, y1; + const int16_t xs[4] = {shape.x0, shape.x1, shape.x2, shape.x3}; + const int16_t ys[4] = {shape.y0, shape.y1, shape.y2, shape.y3}; + x0 = std::min({xs[0], xs[1], xs[2], xs[3]}); x1 = std::max({xs[0], xs[1], xs[2], xs[3]}); + y0 = std::min({ys[0], ys[1], ys[2], ys[3]}); y1 = std::max({ys[0], ys[1], ys[2], ys[3]}); + candidates[count++] = {x0, y0, x1, y1}; + } + int clipX0 = 0, clipY0 = 0, clipX1 = -1, clipY1 = -1; + gea::platform::display::Display::clip(&clipX0, &clipY0, &clipX1, &clipY1); + clipX0 = std::max(clipX0, 0); + clipY0 = std::max(clipY0, 0); + clipX1 = std::min(clipX1, canvas->width() - 1); + clipY1 = std::min(clipY1, canvas->height() - 1); + const int stride = canvas->strideBytes() / static_cast(sizeof(gea::framework::graphics::pixel::native_t)); + for (int i = 0; i < count; ++i) + { + Box box{std::max(candidates[i].x0, clipX0), std::max(candidates[i].y0, clipY0), + std::min(candidates[i].x1, clipX1), std::min(candidates[i].y1, clipY1)}; + if (box.x0 > box.x1 || box.y0 > box.y1) + continue; + for (int y = box.y0; y <= box.y1; ++y) + { + const auto *row = canvas->pixels() + canvas->rowToPhysical(y) * stride; + saved().insert(saved().end(), row + box.x0, row + box.x1 + 1); + } + entry.boxes[entry.boxCount++] = box; + } + } + entries().push_back(entry); + } + + static void restore(const Entry &entry) + { + auto *canvas = gea::platform::display::Display::canvas(); + if (canvas && canvas->pixels() && entry.boxCount > 0) + { + const auto &r = entry.shape; + const int stride = canvas->strideBytes() / static_cast(sizeof(gea::framework::graphics::pixel::native_t)); + const int samples = gea::framework::graphics::Canvas::antialiasSamples(); + const int sampleCount = samples >= 2 ? samples * samples : 1; + std::size_t index = entry.offset; + for (int i = 0; i < entry.boxCount; ++i) + { + const Box &box = entry.boxes[i]; + for (int y = box.y0; y <= box.y1; ++y) + { + auto *row = canvas->pixels() + canvas->rowToPhysical(y) * stride; + const float py = static_cast(y) + 0.5f - static_cast(r.y0); + for (int x = box.x0; x <= box.x1; ++x, ++index) + { + const float px = static_cast(x) + 0.5f - static_cast(r.x0); + const float localX = static_cast(r.lx) + entry.inv00 * px + entry.inv01 * py; + const float localY = static_cast(r.ly) + entry.inv10 * px + entry.inv11 * py; + const int coverage = samples >= 2 + ? roundedRectCoverageFast(r, localX, localY, entry.inv00, entry.inv10, entry.inv01, entry.inv11, samples) + : (roundedRectContainsFast(r, localX, localY) ? 1 : 0); + if (coverage >= sampleCount) + continue; + const auto outside = saved()[index]; + row[x] = coverage <= 0 ? outside + : gea::framework::graphics::pixel::blendNative(row[x], outside, combinedCoverageAlpha(255, coverage, sampleCount)); + } + } + canvas->markDirty(box.x0, box.y0, box.x1, box.y1); + } + } + } + }; + bool nodeFilterShapeContains(const Node &node, double sx, double sy) { const double x = static_cast(node.layout.x); @@ -4457,6 +4704,8 @@ int gLastScrollUiFrame = -1000; }; static const int quadEdges[4][2] = {{0, 1}, {1, 2}, {2, 3}, {3, 0}}; const int aaSpanPad = useCoverageAntialias ? 1 : 0; + const bool ring = transformedRoundedRectIsRing(r); + const TransformedRoundedRectCommand hole = ring ? transformedRoundedRectHole(r) : r; for (int y = drawY0; y <= drawY1; ++y) { @@ -4508,7 +4757,9 @@ int gLastScrollUiFrame = -1000; float localY = static_cast(r.ly) + inv10 * (static_cast(rowDrawX0) + 0.5f - static_cast(r.x0)) + inv11 * py; for (int x = rowDrawX0; x <= rowDrawX1; ++x) { - const int coverage = useCoverageAntialias + const int coverage = ring && roundedRectSolidCoreContainsFast(hole, localX, localY, sampleMarginX, sampleMarginY) + ? 0 + : useCoverageAntialias ? (roundedRectSolidCoreContainsFast(r, localX, localY, sampleMarginX, sampleMarginY) ? sampleCount : roundedRectCoverageFast(r, localX, localY, inv00, inv10, inv01, inv11, aaSamples)) @@ -4674,7 +4925,9 @@ int gLastScrollUiFrame = -1000; r.blRx8, r.blRy8, sx, - sy) + sy) && + (!transformedRoundedRectIsRing(r) || + !roundedRectOuterContainsFast(transformedRoundedRectHole(r), static_cast(sx), static_cast(sy))) ? 1 : 0; } @@ -4741,9 +4994,11 @@ int gLastScrollUiFrame = -1000; static void clampToNode(const Node &node, int *cx0, int *cy0, int *cx1, int *cy1) { - int nx0, ny0, w, h; - overflowClipBounds(node, nx0, ny0, w, h); - int nx1 = nx0 + w - 1, ny1 = ny0 + h - 1; + OverflowClipShape shape; + if (!ViewRenderer::overflowClipShape(node, shape)) + return; + const int nx0 = shape.x, ny0 = shape.y; + int nx1 = nx0 + shape.w - 1, ny1 = ny0 + shape.h - 1; if (*cx0 < nx0) *cx0 = nx0; if (*cy0 < ny0) @@ -4924,10 +5179,20 @@ int gLastScrollUiFrame = -1000; case DisplayCommandType::PushClip: { const int id = c->clip.nodeId; const Node *owner = id >= 0 && id < Tree::instance().nodeCount() ? &Tree::instance().nodes()[id] : nullptr; - if (!owner || overflowX(owner->style)) c->clip.x += dx; - else c->bx -= dx; - if (!owner || overflowY(owner->style)) c->clip.y += dy; - else c->by -= dy; + const int clipDx = !owner || overflowX(owner->style) ? dx : 0; + const int clipDy = !owner || overflowY(owner->style) ? dy : 0; + c->clip.x += clipDx; + c->bx -= dx - clipDx; + c->clip.y += clipDy; + c->by -= dy - clipDy; + if (c->clip.shaped) { + for (int i = 0; i < 4; ++i) { + c->clip.qx[i] += clipDx; + c->clip.qy[i] += clipDy; + } + c->clip.lx += clipDx; + c->clip.ly += clipDy; + } break; } case DisplayCommandType::FillRect: @@ -5173,6 +5438,121 @@ int gLastScrollUiFrame = -1000; } }; + // background-blend-mode (Compositing 1, section 10): a gradient layer + // blends with what is painted under it. The backdrop should be only the + // element's lower layers; the canvas holds them over whatever is behind the + // element, which differs only where those layers are not opaque. + struct BlendDrawer + { + static float softLightD(float cb) + { + return cb <= 0.25f ? ((16.0f * cb - 12.0f) * cb + 4.0f) * cb : std::sqrt(cb); + } + + static float channel(int mode, float cb, float cs) + { + switch (mode) + { + case 1: return cb * cs; + case 2: return cb + cs - cb * cs; + case 3: return channel(8, cs, cb); + case 4: return std::min(cb, cs); + case 5: return std::max(cb, cs); + case 6: + if (cb <= 0.0f) return 0.0f; + return cs >= 1.0f ? 1.0f : std::min(1.0f, cb / (1.0f - cs)); + case 7: + if (cb >= 1.0f) return 1.0f; + return cs <= 0.0f ? 0.0f : 1.0f - std::min(1.0f, (1.0f - cb) / cs); + case 8: return cs <= 0.5f ? cb * 2.0f * cs : channel(2, cb, 2.0f * cs - 1.0f); + case 9: + if (cs <= 0.5f) return cb - (1.0f - 2.0f * cs) * cb * (1.0f - cb); + return cb + (2.0f * cs - 1.0f) * (softLightD(cb) - cb); + case 10: return std::fabs(cb - cs); + case 11: return cb + cs - 2.0f * cb * cs; + default: return cs; + } + } + + static float lum(const float c[3]) { return 0.3f * c[0] + 0.59f * c[1] + 0.11f * c[2]; } + + static void setLum(float c[3], float l) + { + const float d = l - lum(c); + for (int i = 0; i < 3; ++i) c[i] += d; + l = lum(c); + const float n = std::min({c[0], c[1], c[2]}), x = std::max({c[0], c[1], c[2]}); + for (int i = 0; i < 3; ++i) { + if (n < 0.0f) c[i] = l + (c[i] - l) * l / (l - n); + if (x > 1.0f) c[i] = l + (c[i] - l) * (1.0f - l) / (x - l); + } + } + + static void setSat(float c[3], float s) + { + int lo = 0, hi = 0; + for (int i = 1; i < 3; ++i) { + if (c[i] < c[lo]) lo = i; + if (c[i] >= c[hi]) hi = i; + } + if (lo == hi) { c[0] = c[1] = c[2] = 0.0f; return; } + const int mid = 3 - lo - hi; + c[mid] = (c[mid] - c[lo]) * s / (c[hi] - c[lo]); + c[hi] = s; + c[lo] = 0.0f; + } + + static float sat(const float c[3]) { return std::max({c[0], c[1], c[2]}) - std::min({c[0], c[1], c[2]}); } + + // `out` = B(cb, cs), channels in 0..1. + static void blend(int mode, const float cb[3], const float cs[3], float out[3]) + { + if (mode < 12) { + for (int i = 0; i < 3; ++i) out[i] = channel(mode, cb[i], cs[i]); + return; + } + const float *hueSource = mode == 13 ? cb : cs; + for (int i = 0; i < 3; ++i) out[i] = mode == 15 ? cb[i] : hueSource[i]; + if (mode == 12) setSat(out, sat(cb)); + if (mode == 13) setSat(out, sat(cs)); + setLum(out, mode == 15 ? lum(cs) : lum(cb)); + } + + // Blends the source colour the callback gives for each pixel of the + // clipped rows into the canvas. `color(x, y, rgb, &alpha)` returns false + // where the layer paints nothing. + template + static void replay(int mode, int x0, int y0, int x1, int y1, Span span, Color color) + { + auto *canvas = gea::platform::display::Display::canvas(); + if (!canvas) return; + x0 = std::max(x0, 0); y0 = std::max(y0, 0); + x1 = std::min(x1, canvas->width() - 1); y1 = std::min(y1, canvas->height() - 1); + const int baseAlpha = gea::platform::display::Display::alpha(); + for (int y = y0; y <= y1; ++y) { + int rowX0 = x0, rowX1 = x1; + span(y, &rowX0, &rowX1); + for (int x = rowX0; x <= rowX1; ++x) { + int rgb[3], alpha = 0; + if (!color(x, y, rgb, &alpha)) continue; + alpha = alpha * baseAlpha / 255; + if (alpha <= 0) continue; + int br, bg, bb, ba; + gea::framework::graphics::pixel::unpackNative8(canvas->readPixelNative(x, y), &br, &bg, &bb, &ba); + const float cb[3] = {br / 255.0f, bg / 255.0f, bb / 255.0f}; + const float cs[3] = {rgb[0] / 255.0f, rgb[1] / 255.0f, rgb[2] / 255.0f}; + float mixed[3]; + blend(mode, cb, cs, mixed); + const float a = alpha / 255.0f; + int out[3]; + for (int i = 0; i < 3; ++i) + out[i] = std::clamp(static_cast((cb[i] + (mixed[i] - cb[i]) * a) * 255.0f + 0.5f), 0, 255); + canvas->writePixelNativeExact(x, y, gea::framework::graphics::pixel::packNative8(out[0], out[1], out[2])); + } + } + } + }; + struct LinearGradientDrawer { static constexpr double kPi = 3.14159265358979323846; @@ -5422,6 +5802,62 @@ int gLastScrollUiFrame = -1000; *x1 = std::min(*x1, rowRight); } + // The gradient line: the position of pixel centre (px, py) along it is + // px*dx + py*dy, running from minProjection over span. + static void axis(const DisplayCommand &c, double *dxOut, double *dyOut, double *minProjectionOut, double *spanOut) + { + const double angleRadians = (static_cast(c.gradient.angle) * kPi) / 1800.0; + double dx = std::sin(angleRadians); + double dy = -std::cos(angleRadians); + // Snap axis-aligned angles to exact unit vectors: sin(pi) is 1.2e-16, + // not 0, so a plain `to bottom` gradient would miss the constant-row + // fast path below (and drag ~1e-16 noise through the projection). + switch (c.gradient.angle % 3600) + { + case 0: dx = 0.0; dy = -1.0; break; + case 900: dx = 1.0; dy = 0.0; break; + case 1800: dx = 0.0; dy = 1.0; break; + case 2700: dx = -1.0; dy = 0.0; break; + default: break; + } + const double gx0 = static_cast(c.gradient.x); + const double gy0 = static_cast(c.gradient.y); + const double gx1 = static_cast(c.gradient.x + c.gradient.w); + const double gy1 = static_cast(c.gradient.y + c.gradient.h); + const double p0 = gx0 * dx + gy0 * dy; + const double p1 = gx1 * dx + gy0 * dy; + const double p2 = gx1 * dx + gy1 * dy; + const double p3 = gx0 * dx + gy1 * dy; + const double minProjection = std::min(std::min(p0, p1), std::min(p2, p3)); + const double maxProjection = std::max(std::max(p0, p1), std::max(p2, p3)); + *dxOut = dx; + *dyOut = dy; + *minProjectionOut = minProjection; + *spanOut = maxProjection - minProjection; + } + + static void replayBlended(const DisplayCommand &c, int x0, int y0, int x1, int y1) + { + double dx, dy, minProjection, projectionSpan; + axis(c, &dx, &dy, &minProjection, &projectionSpan); + const double invSpanPermille = projectionSpan > 0.001 ? 1000.0 / projectionSpan : 0.0; + int lastPermille = -1, lastRgb[3] = {}, lastAlpha = 0; + BlendDrawer::replay(c.gradient.blend, x0, y0, x1, y1, + [&](int y, int *a, int *b) { roundedRowSpan(c, y, a, b); }, + [&](int x, int y, int rgb[3], int *alpha) { + const double projection = (x + 0.5) * dx + (y + 0.5) * dy; + const int permille = clampPermille(static_cast((projection - minProjection) * invSpanPermille + 0.5)); + if (permille != lastPermille) { + rgb888At(c, permille, &lastRgb[0], &lastRgb[1], &lastRgb[2]); + lastAlpha = alphaAt(c, permille); + lastPermille = permille; + } + for (int i = 0; i < 3; ++i) rgb[i] = lastRgb[i]; + *alpha = lastAlpha; + return true; + }); + } + static void fillRun(int x, int y, int w, uint16_t color, uint8_t alpha, uint8_t baseAlpha) { const uint8_t effectiveAlpha = combineAlpha(baseAlpha, alpha); @@ -5444,6 +5880,31 @@ int gLastScrollUiFrame = -1000; const int y1 = std::min(c.gradient.y + c.gradient.h - 1, cy1); if (x1 < x0 || y1 < y0) return; + if (c.gradient.blend) + { + replayBlended(c, x0, y0, x1, y1); + return; + } + + // An opaque constant gradient, e.g. linear-gradient(green, green), is a + // solid fill: dithering it would speckle a colour a plain fill paints flat. + const auto &g = c.gradient; + if (g.fromColor == g.toColor && g.fromAlpha == 255 && g.toAlpha == 255 && + (!g.hasMid || (g.midColor == g.fromColor && g.midAlpha == 255))) + { + if (g.tl == 0 && g.tr == 0 && g.br == 0 && g.bl == 0) + { + gea::platform::display::Display::fillRect(x0, y0, x1 - x0 + 1, y1 - y0 + 1, g.fromColor); + return; + } + for (int y = y0; y <= y1; y++) + { + int rowX0 = x0, rowX1 = x1; + roundedRowSpan(c, y, &rowX0, &rowX1); + if (rowX0 <= rowX1) gea::platform::display::Display::fillRect(rowX0, y, rowX1 - rowX0 + 1, 1, g.fromColor); + } + return; + } // Static-background gradient cache: a large opaque gradient (the app backdrop) // is re-rasterized over the moving content's dirty region every frame (~23ms @@ -5520,31 +5981,8 @@ int gLastScrollUiFrame = -1000; if (transCacheable && transCache.blit(transKey, 14, x0, y0, x1, y1, scrW, scrH, transSpan)) return; - const double angleRadians = (static_cast(c.gradient.angle) * kPi) / 1800.0; - double dx = std::sin(angleRadians); - double dy = -std::cos(angleRadians); - // Snap axis-aligned angles to exact unit vectors: sin(pi) is 1.2e-16, - // not 0, so a plain `to bottom` gradient would miss the constant-row - // fast path below (and drag ~1e-16 noise through the projection). - switch (c.gradient.angle % 3600) - { - case 0: dx = 0.0; dy = -1.0; break; - case 900: dx = 1.0; dy = 0.0; break; - case 1800: dx = 0.0; dy = 1.0; break; - case 2700: dx = -1.0; dy = 0.0; break; - default: break; - } - const double gx0 = static_cast(c.gradient.x); - const double gy0 = static_cast(c.gradient.y); - const double gx1 = static_cast(c.gradient.x + c.gradient.w); - const double gy1 = static_cast(c.gradient.y + c.gradient.h); - const double p0 = gx0 * dx + gy0 * dy; - const double p1 = gx1 * dx + gy0 * dy; - const double p2 = gx1 * dx + gy1 * dy; - const double p3 = gx0 * dx + gy1 * dy; - const double minProjection = std::min(std::min(p0, p1), std::min(p2, p3)); - const double maxProjection = std::max(std::max(p0, p1), std::max(p2, p3)); - const double projectionSpan = maxProjection - minProjection; + double dx, dy, minProjection, projectionSpan; + axis(c, &dx, &dy, &minProjection, &projectionSpan); if (projectionSpan <= 0.001) { const uint8_t baseAlpha = gea::platform::display::Display::alpha(); @@ -6010,6 +6448,21 @@ int gLastScrollUiFrame = -1000; static_cast(c.radialGradient.h) * static_cast(c.radialGradient.ryPermille) / 1000.0f); const float invRadiusX = 1.0f / radiusX; const float invRadiusY = 1.0f / radiusY; + if (c.radialGradient.blend) + { + BlendDrawer::replay(c.radialGradient.blend, x0, y0, x1, y1, + [&](int y, int *a, int *b) { roundedRowSpan(c, y, a, b); }, + [&](int x, int y, int rgb[3], int *alpha) { + const float dxv = (static_cast(x) + 0.5f - centerX) * invRadiusX; + const float dyv = (static_cast(y) + 0.5f - centerY) * invRadiusY; + const int permille = std::min(1000, static_cast(std::sqrt(dxv * dxv + dyv * dyv) * 1000.0f + 0.5f)); + LinearGradientDrawer::interpolatePremultipliedRgb888(c.radialGradient.fromColor, c.radialGradient.fromAlpha, + c.radialGradient.toColor, c.radialGradient.toAlpha, localPermille(c, permille), &rgb[0], &rgb[1], &rgb[2]); + *alpha = alphaAt(c, permille); + return true; + }); + return; + } // Static-gradient bitmap cache (shared StaticGradientCache). The shell's // warm-glow radial is static — fixed colors + box, identical across themes @@ -6194,17 +6647,24 @@ int gLastScrollUiFrame = -1000; int x0, y0, x1, y1; }; + // `opaque`: no opacity between the owner and this node. Opaque glyphs + // paint over the background they clip with the very same coverage, so + // that background never shows; leaving them out of the mask also keeps + // it out of their antialiased edges, as the text alone would paint them. static void collectTextInk(int id, int owner, int x0, int y0, int x1, int y1, - std::vector &ink) + std::vector &ink, bool opaque = true, int copyOffset = 0) { auto &tree = Tree::instance(); if (id < 0 || id >= tree.nodeCount()) return; const auto &node = tree.nodes()[id]; if (node.style.display == 1 || (id != owner && isOutOfFlowPosition(node.style.position))) return; + opaque = opaque && node.style.opacity == 255; if (state.hasNodeScratchFor(id) && node.style.visibility == 0) { - const int begin = state.nodeDrawStart[id], end = state.nodeDrawEnd[id]; + const bool hidesBackground = opaque && node.type == NodeType::Text && node.style.text_alpha == 255; + const int begin = state.nodeDrawStart[id] + copyOffset, end = state.nodeDrawEnd[id] + copyOffset; for (int ci = std::max(0, begin); ci < end && ci < state.commandCount; ++ci) { const auto &c = state.commands[ci]; + if (hidesBackground && c.type == DisplayCommandType::DrawText) continue; if (c.type == DisplayCommandType::DrawText || c.type == DisplayCommandType::DrawProjectedText || c.textDecorationInk) ink.push_back({&c, x0, y0, x1, y1}); } @@ -6212,7 +6672,7 @@ int gLastScrollUiFrame = -1000; // Match the overflow clip recorded for descendants of this node. // Positioned in-flow and stacking-context descendants still contribute; // their opacity does not change the shape of the mask. - const int clipIndex = state.hasNodeScratchFor(id) ? state.nodeDrawEnd[id] : -1; + const int clipIndex = state.hasNodeScratchFor(id) ? state.nodeDrawEnd[id] + copyOffset : -1; if (clipIndex >= 0 && clipIndex < state.commandCount) { const auto &clip = state.commands[clipIndex]; if (clip.type == DisplayCommandType::PushClip && clip.clip.nodeId == id) { @@ -6222,7 +6682,40 @@ int gLastScrollUiFrame = -1000; } if (x0 > x1 || y0 > y1) return; for (int child = node.first_child; child >= 0; child = tree.nodes()[child].next_sibling) - collectTextInk(child, owner, x0, y0, x1, y1, ink); + collectTextInk(child, owner, x0, y0, x1, y1, ink, opaque, copyOffset); + } + + // The first command of the text-clipped background `command` belongs to: + // the element's layers are recorded together under one owner. + static int textClipGroupStart(const DisplayCommand &command, int *index) + { + *index = -1; + if (!state.commands || &command < state.commands || &command >= state.commands + state.commandCount) return -1; + *index = static_cast(&command - state.commands); + int start = *index; + while (start > 0 && state.commands[start - 1].textClipOwner == command.textClipOwner) --start; + for (int i = start; i < *index; ++i) { + const auto type = state.commands[i].type; + if (type != DisplayCommandType::SetAlpha && type != DisplayCommandType::PushClip && type != DisplayCommandType::PopClip) return start; + } + return -1; + } + + // Paints the group's layers in [start, end) unclipped by the text. + static void replayTextClipBackdrop(int start, int end) + { + using gea::platform::display::Display; + const uint8_t alpha = Display::alpha(); + int pushed = 0; + for (int i = start; i < end; ++i) { + DisplayCommand c = state.commands[i]; + c.textClipOwner = -1; + if (c.type == DisplayCommandType::PushClip) { Display::pushClip(c.clip.x, c.clip.y, c.clip.w, c.clip.h); ++pushed; } + else if (c.type != DisplayCommandType::PopClip) replay(c); + else if (pushed > 0) { Display::popClip(); --pushed; } + } + while (pushed-- > 0) Display::popClip(); + Display::setAlpha(alpha); } static void replayTextClipped(const DisplayCommand &command) @@ -6239,14 +6732,16 @@ int gLastScrollUiFrame = -1000; y1 = std::min({y1, canvas->height()-1, command.by+command.bh-1}); if (x0 > x1 || y0 > y1) return; std::vector ink; - collectTextInk(command.textClipOwner, command.textClipOwner, x0, y0, x1, y1, ink); + collectTextInk(command.textClipOwner, command.textClipOwner, x0, y0, x1, y1, ink, true, columnCopyOffset(command)); if (ink.empty()) return; DisplayCommand paint = command; paint.textClipOwner = -1; + int index = -1; + const int groupStart = textClipGroupStart(command, &index); // Stack-local, bounded scratch remains safe if independent dirty rows // are replayed on separate workers. No viewport-sized backing store. constexpr int chunkSize = 128; - pixel::native_t before[chunkSize]; + pixel::native_t before[chunkSize], painted[chunkSize], under[chunkSize]; uint8_t coverage[chunkSize]; for (int y = y0; y <= y1; ++y) for (int x = x0; x <= x1; x += chunkSize) { const int width = std::min(chunkSize, x1-x+1); @@ -6268,9 +6763,35 @@ int gLastScrollUiFrame = -1000; if (std::any_of(coverage, coverage+width, [](uint8_t a) { return a != 0; })) { for (int i = 0; i < width; ++i) before[i] = canvas->readPixelNative(x+i, y); replay(paint); - for (int i = 0; i < width; ++i) if (coverage[i] != 255) - canvas->writePixelNativeExact(x+i, y, coverage[i] == 0 ? before[i] : - pixel::blendNative(canvas->readPixelNative(x+i, y), before[i], coverage[i])); + if (groupStart >= 0 && std::any_of(coverage, coverage+width, [](uint8_t a) { return a != 0 && a != 255; })) { + // CSS clips all of an element's layers to the text once. At a + // partly covered pixel the canvas already mixes the lower layers + // with what is behind them, so add this layer's change to their + // unclipped composite instead of blending over that mix. + for (int i = 0; i < width; ++i) { + painted[i] = canvas->readPixelNative(x+i, y); + canvas->writePixelNativeExact(x+i, y, before[i]); + } + replayTextClipBackdrop(groupStart, index); + for (int i = 0; i < width; ++i) under[i] = canvas->readPixelNative(x+i, y); + replay(paint); + for (int i = 0; i < width; ++i) { + if (coverage[i] == 255 || coverage[i] == 0) { + canvas->writePixelNativeExact(x+i, y, coverage[i] ? painted[i] : before[i]); + continue; + } + int b[4], u[4], p[4]; + pixel::unpackNative8(before[i], &b[0], &b[1], &b[2], &b[3]); + pixel::unpackNative8(under[i], &u[0], &u[1], &u[2], &u[3]); + pixel::unpackNative8(canvas->readPixelNative(x+i, y), &p[0], &p[1], &p[2], &p[3]); + for (int k = 0; k < 3; ++k) b[k] = std::clamp(b[k] + ((p[k] - u[k]) * coverage[i] + (p[k] >= u[k] ? 127 : -127)) / 255, 0, 255); + canvas->writePixelNativeExact(x+i, y, pixel::packNative8(b[0], b[1], b[2])); + } + } else { + for (int i = 0; i < width; ++i) if (coverage[i] != 255) + canvas->writePixelNativeExact(x+i, y, coverage[i] == 0 ? before[i] : + pixel::blendNative(canvas->readPixelNative(x+i, y), before[i], coverage[i])); + } } Display::popClip(); } @@ -6369,7 +6890,9 @@ int gLastScrollUiFrame = -1000; TextRenderer::drawWrapped(c.text.text, c.text.x, c.text.y, c.text.maxWidth, c.text.color, c.text.scale, c.text.align, c.text.containerWidth, c.text.fontId, c.text.textTransform, c.text.lineHeight, - c.text.whiteSpace, c.text.textOverflow, c.text.maxHeight, c.text.firstLineIndent); + c.text.whiteSpace, c.text.textOverflow, c.text.maxHeight, c.text.firstLineIndent, + c.text.alignLast, c.text.lineLimit, + c.text.blockEllipsis, c.text.ellipsisWidth, c.text.emphasis, c.text.emphasisColor); break; case DisplayCommandType::BlitImage: #if GEA_PIXEL_STORAGE_PACKED @@ -6623,6 +7146,68 @@ int gLastScrollUiFrame = -1000; ~SuspendedRecordClips() { restore(clips); } }; + // Column k of a multicol container shows its flow thread's band + // [k * height, (k + 1) * height). The first column stays open above and + // the last below, so ink overflowing the fragmentation edges still shows. + static bool appendColumnClip(DisplayList &list, const Node &n, const MulticolLayout &columns, int k) + { + DisplayCommand *cmd = list.append(); + if (!cmd) + return false; + const int top = n.layout.y + boxInset(n.style, 0); + const int y0 = k == 0 ? -16384 : top; + const int y1 = k == columns.used - 1 && !columns.discard ? 16383 : top + columns.height; + cmd->type = DisplayCommandType::PushClip; + cmd->bx = cmd->clip.x = -16384; + cmd->bw = cmd->clip.w = 32767; + cmd->by = cmd->clip.y = static_cast(y0); + cmd->bh = cmd->clip.h = static_cast(y1 - y0); + cmd->clip.nodeId = -1; + cmd->clip.shaped = 0; + return true; + } + + static void appendColumnPop(DisplayList &list) + { + DisplayCommand *cmd = list.append(); + if (!cmd) + return; + cmd->type = DisplayCommandType::PopClip; + cmd->bx = cmd->by = 0; + cmd->bw = cmd->bh = 0; + cmd->clip.nodeId = -1; + } + + // Paints the flow thread recorded at [begin, end) into every further + // column box: a copy translated k columns along the inline direction and + // k column heights up. In rtl the columns progress from the right edge. + static void replicateColumns(DisplayList &list, const Node &n, const MulticolLayout &columns, int begin, int end) + { + const bool rtl = LayoutEngine::rightToLeftDirection(n); + const int contentWidth = n.layout.width - boxInset(n.style, 1) - boxInset(n.style, 3); + const int step = rtl ? -(columns.width + columns.gap) : columns.width + columns.gap; + const int start = rtl ? contentWidth - columns.width : 0; + if (start != 0) + for (int ci = begin; ci < end; ++ci) + DisplayCommandTranslator::translate(&state.commands[ci], start, 0); + appendColumnPop(list); + for (int k = 1; k < columns.used; ++k) { + if (!appendColumnClip(list, n, columns, k)) + return; + const int copyBegin = state.commandCount; + for (int ci = begin; ci < end; ++ci) { + const DisplayCommand copy = state.commands[ci]; + DisplayCommand *cmd = list.append(); + if (!cmd) + break; + *cmd = copy; + DisplayCommandTranslator::translate(cmd, k * step, -k * columns.height); + } + columnCopies().push_back({copyBegin, state.commandCount, copyBegin - begin}); + appendColumnPop(list); + } + } + static void recordNode(int id, uint8_t parent_alpha, int cx0, int cy0, int cx1, int cy1, const Node *mask_node, bool insideDirty = false, const RecordClipFrame *clips = nullptr, bool groupRoot = true, bool includePositioned = true) { if (!state.hasNodeScratchFor(id)) @@ -6639,7 +7224,7 @@ int gLastScrollUiFrame = -1000; return; } Node *n = &Tree::instance().nodes()[id]; - if (n->style.display == 1 || isCollapsedFlexSubtree(*n) || ViewRenderer::backfaceSubtreeHidden(*n)) + if (n->style.display == 1 || (n->layout.line_clamp_hidden & 1) || isCollapsedFlexSubtree(*n) || ViewRenderer::backfaceSubtreeHidden(*n)) { state.clearNodeRange(id); return; @@ -6798,6 +7383,21 @@ int gLastScrollUiFrame = -1000; else if (pushed_clip) ClipMath::clampToNode(*n, &child_cx0, &child_cy0, &child_cx1, &child_cy1); + // A multicol flow thread is recorded once, clipped to the first column, + // then copied into the others. Its content reaches past the container's + // box before it is translated, so it is never culled there. + MulticolLayout columns{}; + if (const NodeRareData *rare = rareDataFor(id); rare && rare->multicol.valid && !state.appendOverride && + (rare->multicol.used > 1 || LayoutEngine::rightToLeftDirection(*n))) + columns = rare->multicol; + int columnFlowBegin = -1; + if (columns.valid) { + child_cy0 = -32768; + child_cy1 = 32767; + child_cx1 = 32767; + if (appendColumnClip(list, *n, columns, 0)) columnFlowBegin = state.commandCount; + } + const RecordClipFrame ownClip{*n, clips}; for (int child : children) { // A positioned descendant can paint in this context while its @@ -6826,6 +7426,8 @@ int gLastScrollUiFrame = -1000; recordNode(child, cur_alpha, x0, y0, x1, y1, active_mask, dirty, childClips, PaintOrder::isGroup(child), PaintOrder::isContext(child)); for (auto frame = frames.rbegin(); frame != frames.rend(); ++frame) ViewRenderer::recordClipEnd(frame->node); } + if (columnFlowBegin >= 0) + replicateColumns(list, *n, columns, columnFlowBegin, state.commandCount); state.recordDepth--; @@ -7413,7 +8015,8 @@ int gLastScrollUiFrame = -1000; return false; } - static bool nodeRecordsOverflowClip(int node_id, Node *nodes, int nodeCount) + // Mirrors ViewRenderer::recordClipBegin; `shape` receives the clip. + static bool nodeRecordsOverflowClip(int node_id, Node *nodes, int nodeCount, OverflowClipShape *shape = nullptr) { if (node_id < 0 || node_id >= nodeCount) return false; @@ -7422,7 +8025,8 @@ int gLastScrollUiFrame = -1000; return false; if (node.first_child < 0) return false; - return !nodeHasTransformChain(node_id, nodes, nodeCount); + OverflowClipShape local; + return ViewRenderer::overflowClipShape(node, shape ? *shape : local); } static std::uint8_t GEA_RENDER_HOT_SRAM effectiveAlphaForNode(int node_id, Node *nodes, int nodeCount) @@ -7462,12 +8066,12 @@ int gLastScrollUiFrame = -1000; for (int i = count - 1; i > 0; i--) { const int id = chain[i]; - if (!nodeRecordsOverflowClip(id, nodes, nodeCount)) + OverflowClipShape shape; + if (!nodeRecordsOverflowClip(id, nodes, nodeCount, &shape)) continue; - const Node &clip = nodes[id]; - int x, y, w, h; - overflowClipBounds(clip, x, y, w, h); - gea::platform::display::Display::pushClip(x, y, w, h); + gea::platform::display::Display::pushClip(shape.x, shape.y, shape.w, shape.h); + if (shape.shaped) + ShapedClips::begin(shape); pushed++; } return pushed; @@ -7987,6 +8591,7 @@ int gLastScrollUiFrame = -1000; if (start < 0 || end <= start || end > state.commandCount) return; + const std::size_t shapedMark = ShapedClips::depth(); const int pushedClips = pushNodeReplayClips(node_id, nodes, nodeCount); if (pushedClips < 0) return; @@ -8000,6 +8605,7 @@ int gLastScrollUiFrame = -1000; replayCommandRange(start, end, x0, y0, x1, y1, roundedRects); if (stateChanged) roundedRects.flush(); + ShapedClips::endTo(shapedMark); for (int i = 0; i < pushedClips; i++) gea::platform::display::Display::popClip(); if (stateChanged) @@ -8325,10 +8931,11 @@ int gLastScrollUiFrame = -1000; bool clipped = false; for (int cursor = node_id; cursor >= 0 && cursor < nodeCount; cursor = nodes[cursor].parent) { - if (!nodeRecordsOverflowClip(cursor, nodes, nodeCount)) + OverflowClipShape shape; + if (!nodeRecordsOverflowClip(cursor, nodes, nodeCount, &shape)) continue; const Node &clip = nodes[cursor]; - if (clip.layout.x > x0 || clip.layout.y > y0 || + if (shape.shaped || clip.layout.x > x0 || clip.layout.y > y0 || clip.layout.x + clip.layout.width - 1 < x1 || clip.layout.y + clip.layout.height - 1 < y1) { @@ -8404,10 +9011,11 @@ int gLastScrollUiFrame = -1000; bool clipped = false; for (int cursor = node_id; cursor >= 0 && cursor < nodeCount; cursor = nodes[cursor].parent) { - if (!nodeRecordsOverflowClip(cursor, nodes, nodeCount)) + OverflowClipShape shape; + if (!nodeRecordsOverflowClip(cursor, nodes, nodeCount, &shape)) continue; const Node &clip = nodes[cursor]; - if (clip.layout.x > x0 || clip.layout.y > y0 || + if (shape.shaped || clip.layout.x > x0 || clip.layout.y > y0 || clip.layout.x + clip.layout.width - 1 < x1 || clip.layout.y + clip.layout.height - 1 < y1) { @@ -8621,6 +9229,7 @@ int gLastScrollUiFrame = -1000; if (!commandRangeOverlapsAnyRegion(start, end, regions, count)) return; + const std::size_t shapedMark = ShapedClips::depth(); const int pushedClips = pushNodeReplayClips(node_id, nodes, nodeCount); if (pushedClips < 0) return; @@ -8662,6 +9271,7 @@ int gLastScrollUiFrame = -1000; } if (stateChanged || alphaTouched) roundedRects.flush(); + ShapedClips::endTo(shapedMark); for (int i = 0; i < pushedClips; i++) gea::platform::display::Display::popClip(); if (stateChanged || alphaTouched) @@ -8835,6 +9445,12 @@ int gLastScrollUiFrame = -1000; static void GEA_RENDER_HOT_SRAM replay() { + // A stream without its PopClip still restores what its shaped clips saved. + struct ShapedClipGuard + { + std::size_t mark = ShapedClips::depth(); + ~ShapedClipGuard() { ShapedClips::endTo(mark); } + } shapedClipGuard; int cx0 = 0, cy0 = 0, cx1 = 0, cy1 = 0; int clip_dirty = 1; int clip_stack_depth = 0; @@ -8955,8 +9571,10 @@ int gLastScrollUiFrame = -1000; ny1 = cy1; int changed = nx0 != cx0 || ny0 != cy0 || nx1 != cx1 || ny1 != cy1; + // Bit 0: a Display clip was pushed; bit 1: a shaped clip began. + const bool shaped = c->clip.shaped && state.ensureReplayClipCapacity(clip_stack_depth + 1); if (state.ensureReplayClipCapacity(clip_stack_depth + 1)) - state.replayClipPushed[clip_stack_depth++] = (uint8_t)changed; + state.replayClipPushed[clip_stack_depth++] = static_cast(changed | (shaped ? 2 : 0)); if (changed) { gea::platform::display::Display::pushClip(c->clip.x, c->clip.y, c->clip.w, c->clip.h); @@ -8965,16 +9583,23 @@ int gLastScrollUiFrame = -1000; cx1 = nx1; cy1 = ny1; } + if (shaped) + ShapedClips::begin(*c); continue; } case DisplayCommandType::PopClip: + { roundedRects.flush(); - if (clip_stack_depth <= 0 || state.replayClipPushed[--clip_stack_depth]) + const uint8_t pushed = clip_stack_depth > 0 ? state.replayClipPushed[--clip_stack_depth] : 1; + if (pushed & 2) + ShapedClips::end(); + if (pushed & 1) { gea::platform::display::Display::popClip(); clip_dirty = 1; } continue; + } case DisplayCommandType::SetAlpha: roundedRects.flush(); gea::platform::display::Display::setAlpha(c->alpha.alpha); @@ -9014,8 +9639,10 @@ int gLastScrollUiFrame = -1000; const auto &n = nodes[id]; const bool item = n.parent >= 0 && !isOutOfFlowPosition(n.style.position) && (nodes[n.parent].style.display == kDisplayFlex || isDisplayGrid(nodes[n.parent].style)); + // A multicol container paints its whole flow thread, positioned content + // included, so every column box can repeat it. return n.parent < 0 || n.style.position == kPositionFixed || ChildZSorter::effectGroup(n) || - (!n.style.z_index_auto && (n.style.position != 0 || item)); + (!n.style.z_index_auto && (n.style.position != 0 || item)) || LayoutEngine::multicolContainer(n); } bool PaintOrder::isGroup(int id) @@ -9024,7 +9651,9 @@ int gLastScrollUiFrame = -1000; const auto &n = nodes[id]; const bool item = n.parent >= 0 && (nodes[n.parent].style.display == kDisplayFlex || isDisplayGrid(nodes[n.parent].style)); - return isContext(id) || n.style.position != 0 || n.style.float_side || item; + // An inline-block, inline-flex or inline-grid paints atomically, like a + // float (CSS 2.2 Appendix E). + return isContext(id) || n.style.position != 0 || n.style.float_side || item || isAtomicInline(n.style); } std::vector PaintOrder::collectChildren(int root, bool groupRoot, bool includePositioned) @@ -9191,6 +9820,7 @@ int gLastScrollUiFrame = -1000; state.textClippedBackgrounds = false; state.commandOverflow = false; state.drawNodeOrderCount = 0; + columnCopies().clear(); if (!state.ensureNodeScratchCapacity(tree.nodeCount())) return; for (int i = 0; i < tree.nodeCount(); i++) @@ -9808,6 +10438,9 @@ int gLastScrollUiFrame = -1000; return false; if (nodes[i].style.overflow == 2) return false; + // Multicol content paints as copies no node range describes. + if (LayoutEngine::multicolContainer(nodes[i])) + return false; } (void)width; (void)height; @@ -12254,7 +12887,7 @@ int gLastScrollUiFrame = -1000; // an empty range, which `old_len == 0 && tmp_len > 0` below (correctly) refuses // to splice — and the refusal cost the whole frame its retained display list. const bool outsideRecordClip = !isDocumentCanvasRoot(*n) && nodeOutsideRecordClip(node, tree.mountedWidth(), tree.mountedHeight()); - if (!outsideRecordClip && n->style.visibility == 0 && !isCollapsedFlexSubtree(*n) && !ViewRenderer::backfaceSubtreeHidden(*n) && + if (!outsideRecordClip && n->style.visibility == 0 && !(n->layout.line_clamp_hidden & 1) && !isCollapsedFlexSubtree(*n) && !ViewRenderer::backfaceSubtreeHidden(*n) && n->style.display != 1 && n->style.opacity != 0 && !(n->style.blink_interval_ms > 0 && !n->style.blink_visible) && diff --git a/packages/engine/ui/root_scroll_refresh.cpp b/packages/engine/ui/root_scroll_refresh.cpp index 5b5b4bf..ebb1d95 100644 --- a/packages/engine/ui/root_scroll_refresh.cpp +++ b/packages/engine/ui/root_scroll_refresh.cpp @@ -485,6 +485,11 @@ int RootScrollOnlyRefresh::refresh(int root, int width, int height) auto &state = treeState(); auto &perf = refreshPerfStatsMutable(); perf.rootScrollCalls++; + // Shifting content by the scroll delta would drag sticky boxes along. + if (state.stickyPresent) { + perf.rootScrollRejected++; + return 0; + } DirtyNode extraDirtyNodes[kMaxExtraDirtyNodes]; int extraDirtyCount = 0; int slotNodes[kMaxSlotRepositionNodes]; diff --git a/packages/engine/ui/style.cpp b/packages/engine/ui/style.cpp index ead3ec8..7a91b67 100644 --- a/packages/engine/ui/style.cpp +++ b/packages/engine/ui/style.cpp @@ -388,6 +388,11 @@ bool styleEqualExceptTextPaint(const ComputedStyle &a, const ComputedStyle &b) a.float_side != b.float_side || a.clear_side != b.clear_side || ar.margin_trim != br.margin_trim || + ar.max_lines != br.max_lines || + ar.line_clamp_flags != br.line_clamp_flags || + ar.block_ellipsis != br.block_ellipsis || + ar.column_count != br.column_count || + ar.column_width != br.column_width || a.writing_mode != b.writing_mode || a.direction != b.direction || a.row_gap != b.row_gap || @@ -458,6 +463,12 @@ bool styleEqualExceptTextPaint(const ComputedStyle &a, const ComputedStyle &b) a.bg_fill != b.bg_fill || ar.containment != br.containment || ar.bg_clip != br.bg_clip || + ar.bg_blend != br.bg_blend || + ar.border_image_source != br.border_image_source || + ar.border_image_slice != br.border_image_slice || + ar.border_image_width != br.border_image_width || + ar.border_image_outset != br.border_image_outset || + ar.border_image_repeat != br.border_image_repeat || ar.bg_size_list != br.bg_size_list || ar.bg_position_list != br.bg_position_list || ar.bg_repeat_list != br.bg_repeat_list || @@ -559,6 +570,10 @@ bool styleEqualExceptTextPaint(const ComputedStyle &a, const ComputedStyle &b) a.font_weight != b.font_weight || a.line_height != b.line_height || a.text_align != b.text_align || + a.text_align_last != b.text_align_last || + a.text_emphasis != b.text_emphasis || + a.text_emphasis_color != b.text_emphasis_color || + a.vertical_align != b.vertical_align || a.overflow != b.overflow || a.overflow_x != b.overflow_x || a.overflow_y != b.overflow_y || @@ -586,6 +601,11 @@ bool styleEqualExceptLocalDisplayCommands(const ComputedStyle &a, const Computed a.float_side != b.float_side || a.clear_side != b.clear_side || ar.margin_trim != br.margin_trim || + ar.max_lines != br.max_lines || + ar.line_clamp_flags != br.line_clamp_flags || + ar.block_ellipsis != br.block_ellipsis || + ar.column_count != br.column_count || + ar.column_width != br.column_width || a.writing_mode != b.writing_mode || a.direction != b.direction || a.row_gap != b.row_gap || @@ -661,6 +681,10 @@ bool styleEqualExceptLocalDisplayCommands(const ComputedStyle &a, const Computed a.font_weight != b.font_weight || a.line_height != b.line_height || a.text_align != b.text_align || + a.text_align_last != b.text_align_last || + a.text_emphasis != b.text_emphasis || + a.text_emphasis_color != b.text_emphasis_color || + a.vertical_align != b.vertical_align || a.overflow != b.overflow || a.overflow_x != b.overflow_x || a.overflow_y != b.overflow_y || @@ -1012,10 +1036,96 @@ enum class CssRuleProperty : std::uint8_t { using CssDeclarationId = StyleDeclaration; +// CSS Logical Properties 4, resolved for horizontal-tb ltr: the inline axis is +// horizontal and starts on the left. Other writing modes and rtl still map this +// way. A logical longhand classifies as its physical property. +const char *physicalLogicalLonghand(const char *property) +{ + static const std::pair longhands[] = { + {"inline-size", "width"}, + {"block-size", "height"}, + {"min-inline-size", "min-width"}, + {"min-block-size", "min-height"}, + {"max-inline-size", "max-width"}, + {"max-block-size", "max-height"}, + {"margin-inline-start", "margin-left"}, + {"margin-inline-end", "margin-right"}, + {"margin-block-start", "margin-top"}, + {"margin-block-end", "margin-bottom"}, + {"padding-inline-start", "padding-left"}, + {"padding-inline-end", "padding-right"}, + {"padding-block-start", "padding-top"}, + {"padding-block-end", "padding-bottom"}, + {"inset-inline-start", "left"}, + {"inset-inline-end", "right"}, + {"inset-block-start", "top"}, + {"inset-block-end", "bottom"}, + {"border-inline-start", "border-left"}, + {"border-inline-end", "border-right"}, + {"border-block-start", "border-top"}, + {"border-block-end", "border-bottom"}, + {"border-inline-start-width", "border-left-width"}, + {"border-inline-start-color", "border-left-color"}, + {"border-inline-start-style", "border-left-style"}, + {"border-inline-end-width", "border-right-width"}, + {"border-inline-end-color", "border-right-color"}, + {"border-inline-end-style", "border-right-style"}, + {"border-block-start-width", "border-top-width"}, + {"border-block-start-color", "border-top-color"}, + {"border-block-start-style", "border-top-style"}, + {"border-block-end-width", "border-bottom-width"}, + {"border-block-end-color", "border-bottom-color"}, + {"border-block-end-style", "border-bottom-style"}, + {"border-start-start-radius", "border-top-left-radius"}, + {"border-start-end-radius", "border-top-right-radius"}, + {"border-end-start-radius", "border-bottom-left-radius"}, + {"border-end-end-radius", "border-bottom-right-radius"} + }; + for (const auto &entry : longhands) + if (std::strcmp(property, entry.first) == 0) return entry.second; + return nullptr; +} + +// A logical shorthand for two physical sides: `split` values give the second +// side its own value (one value sets both); otherwise both take the value. +struct LogicalPair { + const char *name; + StyleDeclaration declaration; + StyleDeclaration first, second; + bool split; +}; + +const LogicalPair *logicalPairFor(const char *property, StyleDeclaration declaration = StyleDeclaration::Unknown) +{ + static const LogicalPair pairs[] = { + {"margin-inline", StyleDeclaration::MarginInline, StyleDeclaration::MarginLeft, StyleDeclaration::MarginRight, true}, + {"margin-block", StyleDeclaration::MarginBlock, StyleDeclaration::MarginTop, StyleDeclaration::MarginBottom, true}, + {"padding-inline", StyleDeclaration::PaddingInline, StyleDeclaration::PaddingLeft, StyleDeclaration::PaddingRight, true}, + {"padding-block", StyleDeclaration::PaddingBlock, StyleDeclaration::PaddingTop, StyleDeclaration::PaddingBottom, true}, + {"inset-inline", StyleDeclaration::InsetInline, StyleDeclaration::Left, StyleDeclaration::Right, true}, + {"inset-block", StyleDeclaration::InsetBlock, StyleDeclaration::Top, StyleDeclaration::Bottom, true}, + {"border-inline", StyleDeclaration::BorderInline, StyleDeclaration::BorderLeft, StyleDeclaration::BorderRight, false}, + {"border-block", StyleDeclaration::BorderBlock, StyleDeclaration::BorderTop, StyleDeclaration::BorderBottom, false}, + {"border-inline-width", StyleDeclaration::BorderInlineWidth, StyleDeclaration::BorderLeftWidth, StyleDeclaration::BorderRightWidth, true}, + {"border-block-width", StyleDeclaration::BorderBlockWidth, StyleDeclaration::BorderTopWidth, StyleDeclaration::BorderBottomWidth, true}, + {"border-inline-color", StyleDeclaration::BorderInlineColor, StyleDeclaration::BorderLeftColor, StyleDeclaration::BorderRightColor, true}, + {"border-block-color", StyleDeclaration::BorderBlockColor, StyleDeclaration::BorderTopColor, StyleDeclaration::BorderBottomColor, true}, + {"border-inline-style", StyleDeclaration::BorderInlineStyle, StyleDeclaration::BorderLeftStyle, StyleDeclaration::BorderRightStyle, true}, + {"border-block-style", StyleDeclaration::BorderBlockStyle, StyleDeclaration::BorderTopStyle, StyleDeclaration::BorderBottomStyle, true} + }; + for (const auto &pair : pairs) + if (property ? std::strcmp(property, pair.name) == 0 : pair.declaration == declaration) return &pair; + return nullptr; +} + CssDeclarationId classifyDeclaration(const char *property) { if (!property || !*property) return CssDeclarationId::Unknown; if (property[0] == '-' && property[1] == '-') return CssDeclarationId::Custom; + if (std::strstr(property, "inline") || std::strstr(property, "block") || std::strstr(property, "start") || std::strstr(property, "end")) { + if (const char *physical = physicalLogicalLonghand(property)) return classifyDeclaration(physical); + if (const LogicalPair *pair = logicalPairFor(property)) return pair->declaration; + } if (std::strcmp(property, "color-scheme") == 0 || std::strcmp(property, "isolation") == 0 || std::strcmp(property, "letter-spacing") == 0 || @@ -1086,6 +1196,16 @@ CssDeclarationId classifyDeclaration(const char *property) if (std::strcmp(property, "float") == 0) return CssDeclarationId::Float; if (std::strcmp(property, "aspect-ratio") == 0) return CssDeclarationId::AspectRatio; if (std::strcmp(property, "margin-trim") == 0) return CssDeclarationId::MarginTrim; + if (std::strcmp(property, "line-clamp") == 0) return CssDeclarationId::LineClamp; + if (std::strcmp(property, "-webkit-line-clamp") == 0) return CssDeclarationId::WebkitLineClamp; + if (std::strcmp(property, "max-lines") == 0) return CssDeclarationId::MaxLines; + if (std::strcmp(property, "continue") == 0) return CssDeclarationId::Continue; + if (std::strcmp(property, "block-ellipsis") == 0) return CssDeclarationId::BlockEllipsis; + if (std::strcmp(property, "columns") == 0) return CssDeclarationId::Columns; + if (std::strcmp(property, "column-count") == 0) return CssDeclarationId::ColumnCount; + if (std::strcmp(property, "column-width") == 0) return CssDeclarationId::ColumnWidth; + if (std::strcmp(property, "column-fill") == 0) return CssDeclarationId::ColumnFill; + if (std::strcmp(property, "column-span") == 0) return CssDeclarationId::ColumnSpan; if (std::strcmp(property, "clear") == 0) return CssDeclarationId::Clear; if (std::strcmp(property, "direction") == 0) return CssDeclarationId::Direction; if (std::strcmp(property, "writing-mode") == 0) return CssDeclarationId::WritingMode; @@ -1098,6 +1218,13 @@ CssDeclarationId classifyDeclaration(const char *property) if (std::strcmp(property, "contain") == 0) return CssDeclarationId::Contain; if (std::strcmp(property, "background-color") == 0) return CssDeclarationId::BackgroundColor; if (std::strcmp(property, "background-clip") == 0) return CssDeclarationId::BackgroundClip; + if (std::strcmp(property, "background-blend-mode") == 0) return CssDeclarationId::BackgroundBlendMode; + if (std::strcmp(property, "border-image") == 0) return CssDeclarationId::BorderImage; + if (std::strcmp(property, "border-image-source") == 0) return CssDeclarationId::BorderImageSourceDeclaration; + if (std::strcmp(property, "border-image-slice") == 0) return CssDeclarationId::BorderImageSliceDeclaration; + if (std::strcmp(property, "border-image-width") == 0) return CssDeclarationId::BorderImageWidthDeclaration; + if (std::strcmp(property, "border-image-outset") == 0) return CssDeclarationId::BorderImageOutsetDeclaration; + if (std::strcmp(property, "border-image-repeat") == 0) return CssDeclarationId::BorderImageRepeatDeclaration; if (std::strcmp(property, "background-image") == 0) return CssDeclarationId::BackgroundImage; if (std::strcmp(property, "background") == 0) return CssDeclarationId::Background; if (std::strcmp(property, "background-size") == 0) return CssDeclarationId::BackgroundSize; @@ -1119,6 +1246,11 @@ CssDeclarationId classifyDeclaration(const char *property) if (std::strcmp(property, "border-right-width") == 0) return CssDeclarationId::BorderRightWidth; if (std::strcmp(property, "border-bottom-width") == 0) return CssDeclarationId::BorderBottomWidth; if (std::strcmp(property, "border-left-width") == 0) return CssDeclarationId::BorderLeftWidth; + if (std::strcmp(property, "border-style") == 0) return CssDeclarationId::BorderStyle; + if (std::strcmp(property, "border-top-style") == 0) return CssDeclarationId::BorderTopStyle; + if (std::strcmp(property, "border-right-style") == 0) return CssDeclarationId::BorderRightStyle; + if (std::strcmp(property, "border-bottom-style") == 0) return CssDeclarationId::BorderBottomStyle; + if (std::strcmp(property, "border-left-style") == 0) return CssDeclarationId::BorderLeftStyle; if (std::strcmp(property, "border-top-color") == 0) return CssDeclarationId::BorderTopColor; if (std::strcmp(property, "border-right-color") == 0) return CssDeclarationId::BorderRightColor; if (std::strcmp(property, "border-bottom-color") == 0) return CssDeclarationId::BorderBottomColor; @@ -1133,6 +1265,12 @@ CssDeclarationId classifyDeclaration(const char *property) if (std::strcmp(property, "font-weight") == 0) return CssDeclarationId::FontWeight; if (std::strcmp(property, "line-height") == 0) return CssDeclarationId::LineHeight; if (std::strcmp(property, "text-align") == 0) return CssDeclarationId::TextAlign; + if (std::strcmp(property, "text-align-last") == 0) return CssDeclarationId::TextAlignLast; + if (std::strcmp(property, "text-emphasis") == 0) return CssDeclarationId::TextEmphasis; + if (std::strcmp(property, "text-emphasis-style") == 0) return CssDeclarationId::TextEmphasisStyleDeclaration; + if (std::strcmp(property, "text-emphasis-color") == 0) return CssDeclarationId::TextEmphasisColorDeclaration; + if (std::strcmp(property, "text-emphasis-position") == 0) return CssDeclarationId::TextEmphasisPositionDeclaration; + if (std::strcmp(property, "vertical-align") == 0) return CssDeclarationId::VerticalAlign; if (std::strcmp(property, "text-decoration") == 0 || std::strcmp(property, "text-decoration-line") == 0) return CssDeclarationId::TextDecoration; if (std::strcmp(property, "text-transform") == 0) return CssDeclarationId::TextTransform; if (std::strcmp(property, "white-space") == 0) return CssDeclarationId::WhiteSpace; @@ -1755,6 +1893,9 @@ std::uint16_t storeDirectPropertyCompiledValue(Property property, int value) return static_cast(list.size() - 1); } +// CssCompiledValue::values holds the count and up to five property/value pairs. +constexpr int kDirectPropertyGroupCapacity = 5; + std::uint16_t storeDirectPropertyGroupCompiledValue(std::initializer_list properties) { auto &list = compiledCssValues(); @@ -1763,7 +1904,7 @@ std::uint16_t storeDirectPropertyGroupCompiledValue(std::initializer_list= 4) break; + if (count >= kDirectPropertyGroupCapacity) break; compiled.values[1 + count * 2] = static_cast(entry.property); compiled.values[2 + count * 2] = entry.value; count++; @@ -2172,10 +2313,31 @@ std::vector> &backgroundClipLists() return lists; } +std::vector> &backgroundBlendLists() +{ + static std::vector> lists; + return lists; +} + +std::vector &borderImageSources() +{ + static std::vector list; + return list; +} + +std::vector &borderImageSideLists() +{ + static std::vector list; + return list; +} + void clearCompiledCssBackgrounds() { compiledCssBackgrounds().clear(); backgroundClipLists().clear(); + backgroundBlendLists().clear(); + borderImageSources().clear(); + borderImageSideLists().clear(); backgroundPlacementLists().clear(); } @@ -3599,26 +3761,61 @@ int containmentValue(const std::string &value) std::vector splitFunctionAwareWords(const std::string &value); int parseOriginPart(const std::string &part, int fallback); -int parseCalcExpression(const std::string &expr, int nodeId, LengthAxis axis) +// The operator a calc() sum or product splits at, following CSS precedence: the +// last top-level + or - (sums are left-associative), else the last * or /. +// A + or - is binary only after an operand, not as a number's sign or exponent. +std::size_t calcOperatorSplit(const std::string &text) { - const std::string text = trimCssValue(expr); - for (char op : {'/', '*', '+', '-'}) { + for (const char *ops : {"+-", "*/"}) { int depth = 0; + std::size_t split = std::string::npos; for (std::size_t i = 0; i < text.size(); ++i) { const char c = text[i]; if (c == '(') depth++; else if (c == ')' && depth > 0) depth--; - else if (c == op && depth == 0 && i > 0) { - const int left = parseLengthForNode(text.substr(0, i), nodeId, axis); - const double right = std::strtod(text.substr(i + 1).c_str(), nullptr); - if (op == '/') return right == 0.0 ? 0 : roundToInt(static_cast(left) / right); - if (op == '*') return roundToInt(static_cast(left) * right); - const int rightLength = parseLengthForNode(text.substr(i + 1), nodeId, axis); - return op == '+' ? left + rightLength : left - rightLength; + else if (depth == 0 && i > 0 && std::strchr(ops, c)) { + std::size_t p = i; + while (p > 0 && text[p - 1] == ' ') --p; + if (p == 0) continue; + const char previous = text[p - 1]; + if (ops[0] == '+' && (std::strchr("+-*/(", previous) || + (p == i && (previous == 'e' || previous == 'E') && p >= 2 && std::isdigit(static_cast(text[p - 2]))))) continue; + split = i; } } + if (split != std::string::npos) return split; } - return parseLengthForNode(text, nodeId, axis); + return std::string::npos; +} + +bool calcNumber(const std::string &raw, double &out) +{ + const std::string text = trimCssValue(raw); + char *end = nullptr; + out = std::strtod(text.c_str(), &end); + return !text.empty() && end && *end == '\0'; +} + +int parseCalcExpression(const std::string &expr, int nodeId, LengthAxis axis) +{ + const std::string text = trimCssValue(expr); + const std::size_t split = calcOperatorSplit(text); + if (split == std::string::npos) { + if (text.size() > 1 && text.front() == '(' && text.back() == ')') return parseCalcExpression(text.substr(1, text.size() - 2), nodeId, axis); + return parseLengthForNode(text, nodeId, axis); + } + const char op = text[split]; + const std::string left = text.substr(0, split), right = text.substr(split + 1); + if (op == '+' || op == '-') { + const int a = parseCalcExpression(left, nodeId, axis), b = parseCalcExpression(right, nodeId, axis); + return op == '+' ? a + b : a - b; + } + double scalar = 0.0; + if (op == '*' && calcNumber(left, scalar)) return roundToInt(static_cast(parseCalcExpression(right, nodeId, axis)) * scalar); + scalar = std::strtod(right.c_str(), nullptr); + const int length = parseCalcExpression(left, nodeId, axis); + if (op == '/') return scalar == 0.0 ? 0 : roundToInt(static_cast(length) / scalar); + return roundToInt(static_cast(length) * scalar); } int parseLengthForNode(const std::string &rawValue, int nodeId, LengthAxis axis) @@ -4531,8 +4728,13 @@ int flexAlignValue(const std::string &raw) int displayValue(const std::string &value) { if (value == "none") return kDisplayNone; - if (value == "grid" || value == "inline-grid") return kDisplayGrid; - if (value == "flex" || value == "inline-flex") return kDisplayFlex; + if (value == "grid") return kDisplayGrid; + if (value == "inline-grid") return kDisplayGrid | (kDisplayInline << kDisplayFlagShift); + if (value == "flex") return kDisplayFlex; + if (value == "inline-flex") return kDisplayFlex | (kDisplayInline << kDisplayFlagShift); + if (value == "flow-root") return kDisplayBlock | (kDisplayFlowRoot << kDisplayFlagShift); + if (value == "inline") return kDisplayBlock | (kDisplayInline << kDisplayFlagShift); + if (value == "inline-block") return kDisplayBlock | ((kDisplayInline | kDisplayFlowRoot) << kDisplayFlagShift); return kDisplayBlock; } @@ -4571,6 +4773,9 @@ int selfAlignValue(const std::string &raw, bool physical = false) } if (position == "self-start") return kAlignSelfStart | overflow; if (position == "self-end") return kAlignSelfEnd | overflow; + // Without a default anchor box, anchor-center behaves as center (CSS Anchor + // Positioning); Gea has no anchors. + if (value == "anchor-center") return 1; if (physical && position == "left") return kAlignLeft | overflow; if (physical && position == "right") return kAlignRight | overflow; const int alignment = flexAlignValue(value); @@ -4585,6 +4790,7 @@ int justifySelfValue(const std::string &raw) const std::string value = toLowerAscii(trimCssValue(raw)); if (value == "auto") return -1; if (value == "normal" || value == "stretch") return 0; + if (value == "anchor-center") return 1; const int baseline = flexAlignValue(value); if (baseline == 5 || baseline == kAlignLastBaseline) return baseline; const auto words = splitFunctionAwareWords(value); @@ -4653,13 +4859,36 @@ int positionValue(const std::string &value) if (value == "absolute") return 1; if (value == "fixed") return kPositionFixed; if (value == "relative") return 2; + if (value == "sticky" || value == "-webkit-sticky") return kPositionSticky; return 0; } +// 0 start, 1 center, 2 right, 3 left, 4 end; justify is unsupported and starts. +// LayoutEngine::physicalTextAlign resolves start and end by direction. int textAlignValue(const std::string &value) { if (value == "center") return 1; - if (value == "right" || value == "end") return 2; + if (value == "right") return 2; + if (value == "left") return 3; + if (value == "end") return 4; + return 0; +} + +// Keywords only; a length or percentage shift keeps the previous value. +int verticalAlignValue(const std::string &value) +{ + static const char *const keywords[] = {"baseline", "top", "bottom", "middle", "text-top", "text-bottom", "sub", "super"}; + for (int i = 0; i < 8; ++i) if (value == keywords[i]) return i; + return value == "initial" || value == "unset" ? 0 : -1; +} + +// 0 = auto, else the physical alignment plus one: 1 left, 2 center, 3 right. +// start and end map as in ltr. Justification is unsupported and starts. +int textAlignLastValue(const std::string &value) +{ + if (value == "left" || value == "start" || value == "justify") return 1; + if (value == "center") return 2; + if (value == "right" || value == "end") return 3; return 0; } @@ -4807,7 +5036,9 @@ struct ParsedBoxShadow { ParsedCssColor color; }; -ParsedBoxShadow parseInsetBoxShadow(const std::string &value, int nodeId) +// Gea paints one shadow per box: the first inset layer when there is one, +// otherwise the first outer layer. +ParsedBoxShadow parseBoxShadow(const std::string &value, int nodeId) { ParsedBoxShadow out; out.color.r = 0; @@ -4825,6 +5056,7 @@ ParsedBoxShadow parseInsetBoxShadow(const std::string &value, int nodeId) return out; } + ParsedBoxShadow outer; for (const auto &rawLayer : splitTopLevel(text, ',')) { const auto tokens = splitFunctionAwareWords(rawLayer); bool inset = false; @@ -4844,18 +5076,20 @@ ParsedBoxShadow parseInsetBoxShadow(const std::string &value, int nodeId) if (lower == "outset") continue; lengths.push_back(token); } - if (!inset) continue; - out.valid = true; - out.inset = true; - out.color = color; - if (!lengths.empty()) out.offsetX = parseLengthForNode(lengths[0], nodeId, LengthAxis::Horizontal); - if (lengths.size() > 1) out.offsetY = parseLengthForNode(lengths[1], nodeId, LengthAxis::Vertical); - if (lengths.size() > 2) out.blur = parseLengthForNode(lengths[2], nodeId, LengthAxis::None); - if (lengths.size() > 3) out.spread = parseLengthForNode(lengths[3], nodeId, LengthAxis::None); - if (out.blur < 0) out.blur = 0; - if (out.blur > 96) out.blur = 96; - return out; - } + if (!inset && outer.valid) continue; + ParsedBoxShadow &layer = inset ? out : outer; + layer.valid = true; + layer.inset = inset; + layer.color = color; + if (!lengths.empty()) layer.offsetX = parseLengthForNode(lengths[0], nodeId, LengthAxis::Horizontal); + if (lengths.size() > 1) layer.offsetY = parseLengthForNode(lengths[1], nodeId, LengthAxis::Vertical); + if (lengths.size() > 2) layer.blur = parseLengthForNode(lengths[2], nodeId, LengthAxis::None); + if (lengths.size() > 3) layer.spread = parseLengthForNode(lengths[3], nodeId, LengthAxis::None); + if (layer.blur < 0) layer.blur = 0; + if (layer.blur > 96) layer.blur = 96; + if (inset) return out; + } + if (outer.valid) return outer; out.valid = true; out.inset = false; @@ -4865,8 +5099,8 @@ ParsedBoxShadow parseInsetBoxShadow(const std::string &value, int nodeId) void applyBoxShadowValue(NodeHandle node, const std::string &value, StyleApplicationSource source) { - const ParsedBoxShadow shadow = parseInsetBoxShadow(value, node.id()); - if (!shadow.valid || !shadow.inset || shadow.color.a == 0) { + const ParsedBoxShadow shadow = parseBoxShadow(value, node.id()); + if (!shadow.valid || shadow.color.a == 0) { setStyleValue(node, Property::BoxShadowInset, 0, source); setStyleValue(node, Property::BoxShadowOffsetX, 0, source); setStyleValue(node, Property::BoxShadowOffsetY, 0, source); @@ -4882,7 +5116,7 @@ void applyBoxShadowValue(NodeHandle node, const std::string &value, StyleApplica setStyleValue(node, Property::BoxShadowSpread, shadow.spread, source); setStyleValue(node, Property::BoxShadowColor, cssColorStyleValue(shadow.color), source); setStyleValue(node, Property::BoxShadowAlpha, shadow.color.a, source); - setStyleValue(node, Property::BoxShadowInset, 1, source); + setStyleValue(node, Property::BoxShadowInset, shadow.inset ? 1 : 0, source); } int numericLength(double value) @@ -5396,37 +5630,29 @@ bool parseCompiledLengthSpec(const std::string &raw, CssLengthSpec &out, bool al bool parseCompiledCalcExpression(const std::string &expr, CssLengthSpec &out) { const std::string text = trimCssValue(expr); - for (char op : {'/', '*', '+', '-'}) { - int depth = 0; - for (std::size_t i = 0; i < text.size(); ++i) { - const char c = text[i]; - if (c == '(') depth++; - else if (c == ')' && depth > 0) depth--; - else if (c == op && depth == 0 && i > 0) { - CssLengthExpression expression; - if (!parseCompiledLengthSpec(text.substr(0, i), expression.a)) return false; - if (op == '/' || op == '*') { - const std::string right = trimCssValue(text.substr(i + 1)); - char *end = nullptr; - const double scalar = std::strtod(right.c_str(), &end); - if (end == right.c_str()) return false; - while (*end == ' ') ++end; - if (*end != '\0') return false; - expression.scalar = static_cast(scalar); - expression.kind = op == '/' - ? CssLengthExpressionKind::Divide - : CssLengthExpressionKind::Multiply; - } else { - if (!parseCompiledLengthSpec(text.substr(i + 1), expression.b)) return false; - expression.kind = op == '+' - ? CssLengthExpressionKind::Add - : CssLengthExpressionKind::Subtract; - } - return storeCompiledCssLengthExpressionSpec(expression, out); - } + const std::size_t split = calcOperatorSplit(text); + if (split == std::string::npos) { + if (text.size() > 1 && text.front() == '(' && text.back() == ')') return parseCompiledCalcExpression(text.substr(1, text.size() - 2), out); + return parseCompiledLengthSpec(text, out); + } + const char op = text[split]; + const std::string left = text.substr(0, split), right = text.substr(split + 1); + CssLengthExpression expression; + if (op == '+' || op == '-') { + if (!parseCompiledCalcExpression(left, expression.a) || !parseCompiledCalcExpression(right, expression.b)) return false; + expression.kind = op == '+' ? CssLengthExpressionKind::Add : CssLengthExpressionKind::Subtract; + } else { + // A product takes its number on either side; a quotient divides by one. + double scalar = 0.0; + if (op == '*' && calcNumber(left, scalar)) { + if (!parseCompiledCalcExpression(right, expression.a)) return false; + } else if (!calcNumber(right, scalar) || !parseCompiledCalcExpression(left, expression.a)) { + return false; } + expression.scalar = static_cast(scalar); + expression.kind = op == '/' ? CssLengthExpressionKind::Divide : CssLengthExpressionKind::Multiply; } - return parseCompiledLengthSpec(text, out); + return storeCompiledCssLengthExpressionSpec(expression, out); } bool compileLengthExpressionSpec(const std::string &raw, CssLengthSpec &out) @@ -6132,6 +6358,7 @@ bool compileBoxShadowValue(const std::string &value, CssCompiledValue &compiled) compiled.values[1] = 0; if (text.empty() || lowerText == "none") return true; + // aux: 1 = inset layer, 2 = outer layer (see parseBoxShadow for the choice). for (const auto &rawLayer : splitTopLevel(text, ',')) { const auto tokens = splitFunctionAwareWords(rawLayer); bool inset = false; @@ -6158,26 +6385,16 @@ bool compileBoxShadowValue(const std::string &value, CssCompiledValue &compiled) if (!parseCompiledLengthSpec(token, length)) return false; if (lengths.size() < 4) lengths.push_back(length); } - if (!inset) continue; - compiled.aux = 1; - for (std::size_t i = 0; i < lengths.size(); ++i) compiled.lengths[i] = lengths[i]; + if (!inset && compiled.aux == 2) continue; + compiled.aux = inset ? 1 : 2; + for (std::size_t i = 0; i < 4; ++i) compiled.lengths[i] = i < lengths.size() ? lengths[i] : CssLengthSpec{}; compiled.values[0] = static_cast(cssColorStyleValue(color)); compiled.values[1] = color.a; - return true; + if (inset) return true; } return true; } -bool boxShadowValueHasInsetLayer(const std::string &value) -{ - for (const auto &rawLayer : splitTopLevel(value, ',')) { - for (const auto &token : splitFunctionAwareWords(rawLayer)) { - if (toLowerAscii(trimCssValue(token)) == "inset") return true; - } - } - return false; -} - bool compileBorderWidthBox(const std::string &value, CssCompiledValue &compiled) { const auto parts = splitFunctionAwareWords(value); @@ -6550,7 +6767,7 @@ int compileBackgroundClip(const std::string &value, bool shorthand = false) for (const auto &layer : splitTopLevel(lower, ',')) { if (!shorthand) { const auto word = trimCssValue(layer); - const int clip = word == "text" ? 3 : backgroundBoxValue(word); + const int clip = word == "text" ? 3 : word == "border-area" ? 4 : backgroundBoxValue(word); if (clip < 0) return -1; clips.push_back(clip); } else { @@ -6568,6 +6785,31 @@ int compileBackgroundClip(const std::string &value, bool shorthand = false) return storeBackgroundClipList(clips); } +// background-blend-mode as an interned per-layer list; the handle is one +// byte, so an app gets 255 distinct non-normal lists. +int compileBackgroundBlendMode(const std::string &value) +{ + static constexpr const char *kModes[] = {"normal", "multiply", "screen", "overlay", "darken", "lighten", + "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", + "color", "luminosity"}; + const auto lower = toLowerAscii(trimCssValue(value)); + if (lower == "initial" || lower == "unset") return 0; + std::vector modes; + for (const auto &layer : splitTopLevel(lower, ',')) { + const auto word = trimCssValue(layer); + const auto *mode = std::find_if(std::begin(kModes), std::end(kModes), [&](const char *name) { return word == name; }); + if (mode == std::end(kModes)) return -1; + modes.push_back(static_cast(mode - std::begin(kModes))); + } + if (modes.empty()) return -1; + if (std::all_of(modes.begin(), modes.end(), [](auto mode) { return mode == 0; })) return 0; + auto &lists = backgroundBlendLists(); + for (std::size_t i = 0; i < lists.size(); ++i) if (lists[i] == modes) return static_cast(i + 1); + if (lists.size() >= 255) return -1; + lists.push_back(modes); + return static_cast(lists.size()); +} + ParsedCssColor backgroundBaseColor(const std::string &value) { ParsedCssColor color; @@ -6582,6 +6824,425 @@ ParsedCssColor backgroundBaseColor(const std::string &value) const CssCompiledBackground *compiledCssBackgroundForHandle(std::uint16_t handle); bool backgroundImageIsValid(int handle, int nodeId); +// line-clamp (CSS Overflow 4) and its longhands as MaxLines, LineClampContinue, +// BlockEllipsis and BlockEllipsisString writes. A custom ellipsis string is +// interned as a CSS atom; an empty one paints nothing, like none. +// Returns the write count, 0 for an invalid value, -1 for other declarations. +int lineClampWrites(CssDeclarationId declaration, const std::string &value, StaticStylePropertyValue out[4]) +{ + const bool shorthand = declaration == CssDeclarationId::LineClamp || declaration == CssDeclarationId::WebkitLineClamp; + if (!shorthand && declaration != CssDeclarationId::MaxLines && declaration != CssDeclarationId::Continue && + declaration != CssDeclarationId::BlockEllipsis) return -1; + const std::string trimmed = trimCssValue(value); + std::string lower = toLowerAscii(trimmed); + bool quoted = false; + std::string custom; + const auto quote = lower.find_first_of("\"'"); + if (quote != std::string::npos) { + const auto close = lower.find(lower[quote], quote + 1); + if (close == std::string::npos) return 0; + custom = trimmed.substr(quote + 1, close - quote - 1); + lower.erase(quote, close - quote + 1); + quoted = true; + } + const int customAtom = custom.empty() ? 0 : internCssAtom(custom); + auto lineCount = [](const std::string &word, int &n) { + if (word.empty() || word.size() > 6 || word.find_first_not_of("0123456789") != std::string::npos) return false; + n = std::min(255, std::atoi(word.c_str())); + return n >= 1; + }; + const auto words = splitWords(lower); + if (declaration == CssDeclarationId::MaxLines) { + int lines = 0; + if (quoted || words.size() != 1 || (words[0] != "none" && !lineCount(words[0], lines))) return 0; + out[0] = {Property::MaxLines, lines}; + return 1; + } + if (declaration == CssDeclarationId::Continue) { + if (quoted || words.size() != 1 || (words[0] != "auto" && words[0] != "collapse" && words[0] != "discard")) return 0; + out[0] = {Property::LineClampContinue, words[0] != "auto"}; + out[1] = {Property::LineClampDiscard, words[0] == "discard"}; + return 2; + } + if (declaration == CssDeclarationId::BlockEllipsis) { + const bool keyword = !quoted && words.size() == 1 && (words[0] == "none" || words[0] == "auto"); + if (!keyword && !(quoted && words.empty())) return 0; + out[0] = {Property::BlockEllipsis, quoted ? customAtom != 0 : words[0] == "auto"}; + out[1] = {Property::BlockEllipsisString, customAtom}; + return 2; + } + int lines = 0; + bool automatic = false, ellipsis = true; + if (!quoted && words.size() == 1 && words[0] == "none") { + out[0] = {Property::MaxLines, 0}; + out[1] = {Property::LineClampContinue, 0}; + out[2] = {Property::BlockEllipsis, 0}; + out[3] = {Property::BlockEllipsisString, 0}; + return 4; + } + for (const auto &word : words) { + if (lineCount(word, lines)) continue; + if (word == "auto" && declaration == CssDeclarationId::LineClamp) automatic = true; + else if (word == "no-ellipsis" && declaration == CssDeclarationId::LineClamp) ellipsis = false; + else if (word != "-webkit-legacy") return 0; + } + if ((lines > 0) == automatic || (!ellipsis && quoted)) return 0; + // -webkit-line-clamp is the legacy form, which clamps only display: + // -webkit-box. Gea has no such display, so it never collapses here. + out[0] = {Property::MaxLines, lines}; + out[1] = {Property::LineClampContinue, declaration == CssDeclarationId::LineClamp}; + out[2] = {Property::BlockEllipsis, ellipsis && (!quoted || customAtom != 0)}; + out[3] = {Property::BlockEllipsisString, customAtom}; + return 4; +} + +// border-style and its longhands as per-side relief writes (CSS Backgrounds +// 3.3); none and hidden set kBorderStyleNone, which keeps the side's width 0. +// dashed, dotted and double paint as solid. +int borderStyleWrites(CssDeclarationId declaration, const std::string &value, StaticStylePropertyValue out[4]) +{ + int first = -1; + switch (declaration) { + case CssDeclarationId::BorderStyle: first = 0; break; + case CssDeclarationId::BorderTopStyle: first = 0; break; + case CssDeclarationId::BorderRightStyle: first = 1; break; + case CssDeclarationId::BorderBottomStyle: first = 2; break; + case CssDeclarationId::BorderLeftStyle: first = 3; break; + default: return -1; + } + const auto words = splitWords(toLowerAscii(trimCssValue(value))); + const bool shorthand = declaration == CssDeclarationId::BorderStyle; + if (words.empty() || words.size() > (shorthand ? 4u : 1u)) return 0; + int reliefs[4]; + for (std::size_t i = 0; i < words.size(); ++i) { + const auto &w = words[i]; + if (w == "none" || w == "hidden") reliefs[i] = kBorderStyleNone; + else if (w == "solid" || w == "dashed" || w == "dotted" || w == "double") reliefs[i] = 0; + else if (w == "groove" || w == "ridge" || w == "inset" || w == "outset") + reliefs[i] = w == "groove" ? 1 : w == "ridge" ? 2 : w == "inset" ? 3 : 4; + else return 0; + } + if (!shorthand) { + out[0] = {static_cast(static_cast(Property::BorderTopRelief) + first), reliefs[0]}; + return 1; + } + // top, right, bottom, left from one to four values. + const int count = static_cast(words.size()); + const int side[4] = {reliefs[0], reliefs[count > 1 ? 1 : 0], reliefs[count > 2 ? 2 : 0], reliefs[count > 3 ? 3 : count > 1 ? 1 : 0]}; + for (int i = 0; i < 4; ++i) out[i] = {static_cast(static_cast(Property::BorderTopRelief) + i), side[i]}; + return 4; +} + +// text-emphasis (CSS Text Decoration 3.3) and its longhands as writes to the +// ComputedStyle::text_emphasis bits and colour. A string mark is unsupported. +BorderImageSides initialBorderImageSides(int which) +{ + BorderImageSides sides; + for (int i = 0; i < 4; ++i) { + sides.kind[i] = which == 0 ? 2 : 0; + sides.value[i] = which == 0 ? 100.0f : which == 1 ? 1.0f : 0.0f; + } + return sides; +} + +bool sameBorderImageSides(const BorderImageSides &a, const BorderImageSides &b) +{ + return a.fill == b.fill && std::equal(a.kind, a.kind + 4, b.kind) && std::equal(a.value, a.value + 4, b.value); +} + +// One-byte handle for a slice (0), width (1) or outset (2) list; 0 is initial. +int internBorderImageSides(int which, const BorderImageSides &sides) +{ + if (sameBorderImageSides(sides, initialBorderImageSides(which))) return 0; + auto &list = borderImageSideLists(); + for (std::size_t i = 0; i < list.size(); ++i) if (sameBorderImageSides(list[i], sides)) return static_cast(i + 1); + if (list.size() >= 255) return -1; + list.push_back(sides); + return static_cast(list.size()); +} + +// Tokens of a border-image value: function-aware words, with '/' on its own. +std::vector borderImageTokens(const std::string &value) +{ + std::vector tokens; + std::string current; + int depth = 0; + for (const char c : value) { + if (c == '(') ++depth; + else if (c == ')' && depth > 0) --depth; + if (depth == 0 && (c == '/' || std::isspace(static_cast(c)))) { + if (!current.empty()) tokens.push_back(current); + current.clear(); + if (c == '/') tokens.emplace_back("/"); + continue; + } + current += c; + } + if (!current.empty()) tokens.push_back(current); + return tokens; +} + +// A border-image-source handle: 0 for none and for images Gea cannot paint +// (only linear-gradient() paints), -1 when invalid. +int borderImageSourceHandle(const std::string &token) +{ + const auto lower = toLowerAscii(trimCssValue(token)); + if (lower == "none") return 0; + if (lower.rfind("linear-gradient(", 0) != 0) return lower.find('(') != std::string::npos ? 0 : -1; + const ParsedLinearGradient parsed = parseLinearGradient(token); + if (!parsed.valid) return -1; + const CssCompiledLinearGradient g = compileLinearGradientLayer(parsed); + const BorderImageSource source{g.fromNativeColor, g.midNativeColor, g.toNativeColor, g.fromAlpha, g.midAlpha, g.toAlpha, + g.hasMid, g.midStopPermille, g.toStopPermille, g.angleTenths}; + auto same = [&](const BorderImageSource &a) { + return a.from == source.from && a.mid == source.mid && a.to == source.to && a.fromAlpha == source.fromAlpha && + a.midAlpha == source.midAlpha && a.toAlpha == source.toAlpha && a.hasMid == source.hasMid && + a.midStop == source.midStop && a.toStop == source.toStop && a.angle == source.angle; + }; + auto &list = borderImageSources(); + for (std::size_t i = 0; i < list.size(); ++i) + if (same(list[i])) return static_cast(i + 1); + if (list.size() >= 255) return -1; + list.push_back(source); + return static_cast(list.size()); +} + +// Kind of one slice (0), width (1) or outset (2) value, -1 when invalid. +// Font-relative lengths are not resolved here and are rejected. +int borderImageSideValue(const std::string &token, int which, float *value) +{ + const auto word = toLowerAscii(token); + if (which == 1 && word == "auto") { *value = 0.0f; return 3; } + CssLengthSpec length; + if (!parseCompiledLengthSpec(word, length) || length.value < 0.0f) return -1; + *value = length.value; + if (length.unit == CssLengthUnit::Raw) return 0; + if (length.unit == CssLengthUnit::Percent && which != 2) return 2; + if (length.unit != CssLengthUnit::Px || which == 0) return -1; + *value = static_cast(cssPixelLength(length.value)); + return 1; +} + +bool borderImageSideToken(const std::string &token, int which) +{ + float value = 0.0f; + return (which == 0 && toLowerAscii(token) == "fill") || borderImageSideValue(token, which, &value) >= 0; +} + +// One to four values in [begin, end) as box sides, plus fill for a slice. +bool parseBorderImageSides(const std::vector &tokens, std::size_t begin, std::size_t end, int which, BorderImageSides &out) +{ + int count = 0; + for (std::size_t i = begin; i < end; ++i) { + if (which == 0 && toLowerAscii(tokens[i]) == "fill") { + if (out.fill) return false; + out.fill = 1; + continue; + } + if (count == 4) return false; + const int kind = borderImageSideValue(tokens[i], which, &out.value[count]); + if (kind < 0) return false; + out.kind[count++] = static_cast(kind); + } + if (!count) return false; + for (int i = count; i < 4; ++i) { + const int from = count == 1 ? 0 : i - 2; + out.kind[i] = out.kind[from]; + out.value[i] = out.value[from]; + } + return true; +} + +int borderImageRepeatKeyword(const std::string &token) +{ + const auto word = toLowerAscii(token); + return word == "stretch" ? 0 : word == "repeat" ? 1 : word == "round" ? 2 : word == "space" ? 3 : -1; +} + +int borderImageRepeatValue(const std::vector &tokens, std::size_t begin, std::size_t end) +{ + if (end <= begin || end - begin > 2) return -1; + const int horizontal = borderImageRepeatKeyword(tokens[begin]); + const int vertical = end - begin == 2 ? borderImageRepeatKeyword(tokens[begin + 1]) : horizontal; + return horizontal < 0 || vertical < 0 ? -1 : horizontal | vertical << 2; +} + +// border-image (CSS Backgrounds 3, section 6) and its longhands as writes to +// the RareStyle handles. The shorthand resets all five longhands. +int borderImageWrites(CssDeclarationId declaration, const std::string &value, StaticStylePropertyValue out[5]) +{ + using D = CssDeclarationId; + static constexpr Property kSideProperties[] = {Property::BorderImageSlice, Property::BorderImageWidth, Property::BorderImageOutset}; + const int side = declaration == D::BorderImageSliceDeclaration ? 0 : declaration == D::BorderImageWidthDeclaration ? 1 : + declaration == D::BorderImageOutsetDeclaration ? 2 : -1; + if (side < 0 && declaration != D::BorderImage && declaration != D::BorderImageSourceDeclaration && + declaration != D::BorderImageRepeatDeclaration) return -1; + const auto tokens = borderImageTokens(trimCssValue(value)); + if (tokens.empty()) return 0; + const auto keyword = toLowerAscii(trimCssValue(value)); + const bool initial = keyword == "initial" || keyword == "unset"; + if (declaration == D::BorderImageSourceDeclaration) { + const int source = initial ? 0 : tokens.size() == 1 ? borderImageSourceHandle(tokens[0]) : -1; + if (source < 0) return 0; + out[0] = {Property::BorderImageSource, source}; + return 1; + } + if (declaration == D::BorderImageRepeatDeclaration) { + const int repeat = initial ? 0 : borderImageRepeatValue(tokens, 0, tokens.size()); + if (repeat < 0) return 0; + out[0] = {Property::BorderImageRepeat, repeat}; + return 1; + } + if (side >= 0) { + BorderImageSides sides; + if (!initial && !parseBorderImageSides(tokens, 0, tokens.size(), side, sides)) return 0; + const int handle = internBorderImageSides(side, initial ? initialBorderImageSides(side) : sides); + if (handle < 0) return 0; + out[0] = {kSideProperties[side], handle}; + return 1; + } + // || [ / | / ? / ]? || + int source = -1, repeat = -1; + bool present[3] = {}; + BorderImageSides sides[3]; + for (std::size_t i = 0; !initial && i < tokens.size();) { + const auto word = toLowerAscii(tokens[i]); + if (source < 0 && (word == "none" || word.find('(') != std::string::npos)) { + source = borderImageSourceHandle(tokens[i++]); + if (source < 0) return 0; + continue; + } + if (repeat < 0 && borderImageRepeatKeyword(word) >= 0) { + std::size_t end = i + 1; + if (end < tokens.size() && borderImageRepeatKeyword(tokens[end]) >= 0) ++end; + repeat = borderImageRepeatValue(tokens, i, end); + i = end; + continue; + } + if (present[0]) return 0; + std::size_t end = i; + while (end < tokens.size() && borderImageSideToken(tokens[end], 0)) ++end; + if (!parseBorderImageSides(tokens, i, end, 0, sides[0])) return 0; + present[0] = true; + i = end; + for (int part = 1; part <= 2 && i < tokens.size() && tokens[i] == "/"; ++part) { + end = ++i; + while (end < tokens.size() && borderImageSideToken(tokens[end], part)) ++end; + if (end > i) { + if (!parseBorderImageSides(tokens, i, end, part, sides[part])) return 0; + present[part] = true; + } else if (part == 2 || i >= tokens.size() || tokens[i] != "/") { + return 0; + } + i = end; + } + } + out[0] = {Property::BorderImageSource, std::max(0, source)}; + for (int part = 0; part < 3; ++part) { + const int handle = internBorderImageSides(part, present[part] ? sides[part] : initialBorderImageSides(part)); + if (handle < 0) return 0; + out[1 + part] = {kSideProperties[part], handle}; + } + out[4] = {Property::BorderImageRepeat, std::max(0, repeat)}; + return 5; +} + +int textEmphasisWrites(CssDeclarationId declaration, const std::string &value, StaticStylePropertyValue out[4]) +{ + const bool shorthand = declaration == CssDeclarationId::TextEmphasis; + if (!shorthand && declaration != CssDeclarationId::TextEmphasisStyleDeclaration && + declaration != CssDeclarationId::TextEmphasisColorDeclaration && declaration != CssDeclarationId::TextEmphasisPositionDeclaration) return -1; + const auto words = splitFunctionAwareWords(trimCssValue(value)); + if (words.empty()) return 0; + if (declaration == CssDeclarationId::TextEmphasisPositionDeclaration) { + int under = -1; + for (const auto &word : words) { + const auto lower = toLowerAscii(word); + if (lower == "over") under = 0; + else if (lower == "under") under = 0x10; + else if (lower != "right" && lower != "left") return 0; + } + if (under < 0) return 0; + out[0] = {Property::TextEmphasisPosition, under}; + return 1; + } + int mark = -1, open = -1, colorMode = -1, color = 0; + for (const auto &word : words) { + const auto lower = toLowerAscii(word); + if (declaration != CssDeclarationId::TextEmphasisColorDeclaration) { + if (lower == "none") { mark = 0; continue; } + if (lower == "filled" || lower == "open") { open = lower == "open" ? 8 : 0; continue; } + if (lower == "dot" || lower == "circle" || lower == "double-circle" || lower == "triangle" || lower == "sesame") { + mark = lower == "dot" ? 1 : lower == "circle" ? 2 : lower == "double-circle" ? 3 : lower == "triangle" ? 4 : 5; + continue; + } + } + if (declaration == CssDeclarationId::TextEmphasisStyleDeclaration || colorMode >= 0) return 0; + if (lower == "currentcolor") { colorMode = 0; continue; } + const ParsedCssColor parsed = parseCssColor(word); + if (!parsed.valid) return 0; + colorMode = parsed.a == 0 ? 0x40 : 0x20; + color = static_cast(cssColorStyleValue(parsed)); + } + int count = 0; + if (declaration != CssDeclarationId::TextEmphasisColorDeclaration) { + if (mark < 0 && open < 0) return 0; + // filled / open alone mean a dot in horizontal text; a shape alone is filled. + out[count++] = {Property::TextEmphasisStyle, mark == 0 ? 0 : (mark < 0 ? 1 : mark) | (open < 0 ? 0 : open)}; + } + if (declaration != CssDeclarationId::TextEmphasisStyleDeclaration) { + if (colorMode < 0) colorMode = 0; + out[count++] = {Property::TextEmphasisColorMode, colorMode}; + if (colorMode == 0x20) out[count++] = {Property::TextEmphasisColor, color}; + } + return count; +} + +// Only whether columns are set matters: line-clamp skips multicol containers. +// columns / column-count / column-width / column-fill as ColumnCount(Set) and +// ColumnWidth(Set) writes plus the column-fill flag. A column width other than +// px is not resolved here and leaves the width auto. +int multicolWrites(CssDeclarationId declaration, const std::string &value, StaticStylePropertyValue out[4]) +{ + const auto words = splitWords(toLowerAscii(trimCssValue(value))); + if (declaration == CssDeclarationId::ColumnSpan) { + if (words.size() != 1 || (words[0] != "none" && words[0] != "all")) return 0; + out[0] = {Property::ColumnSpanAll, words[0] == "all"}; + return 1; + } + if (declaration == CssDeclarationId::ColumnFill) { + if (words.size() != 1 || (words[0] != "auto" && words[0] != "balance" && words[0] != "balance-all")) return 0; + out[0] = {Property::ColumnFillAuto, words[0] == "auto"}; + return 1; + } + const bool shorthand = declaration == CssDeclarationId::Columns; + if (!shorthand && declaration != CssDeclarationId::ColumnCount && declaration != CssDeclarationId::ColumnWidth) return -1; + if (words.empty() || words.size() > (shorthand ? 2u : 1u)) return 0; + bool count = false, width = false; + int countValue = 0, widthValue = -1; + for (const auto &word : words) { + if (word == "auto") continue; + if (word.find_first_not_of("0123456789") == std::string::npos && word != "0" && declaration != CssDeclarationId::ColumnWidth) { + count = true; + countValue = std::min(255, std::atoi(word.c_str())); + } else if (CssLengthSpec length; declaration != CssDeclarationId::ColumnCount && parseCompiledLengthSpec(word, length)) { + width = true; + if (length.unit == CssLengthUnit::Px || length.unit == CssLengthUnit::Raw) + widthValue = std::max(1, std::min(32767, length.unit == CssLengthUnit::Px ? cssPixelLength(length.value) : rawNumber(length.value))); + } else return 0; + } + int n = 0; + if (declaration != CssDeclarationId::ColumnWidth) { + out[n++] = {Property::ColumnCountSet, count}; + out[n++] = {Property::ColumnCount, countValue}; + } + if (declaration != CssDeclarationId::ColumnCount) { + out[n++] = {Property::ColumnWidthSet, width}; + out[n++] = {Property::ColumnWidth, widthValue}; + } + return n; +} + int marginTrimValue(const std::string &raw) { const auto value = toLowerAscii(trimCssValue(raw)); @@ -6824,6 +7485,12 @@ bool compileKeywordValue(CssDeclarationId declaration, const std::string &value, case CssDeclarationId::TextAlign: compiled.values[0] = textAlignValue(value); return true; + case CssDeclarationId::TextAlignLast: + compiled.values[0] = textAlignLastValue(value); + return true; + case CssDeclarationId::VerticalAlign: + compiled.values[0] = verticalAlignValue(value); + return compiled.values[0] >= 0; case CssDeclarationId::TextDecoration: compiled.values[0] = textDecorationValue(value); return true; @@ -7065,6 +7732,31 @@ std::uint16_t compileCssValue(CssDeclarationId declaration, const CssText &rawVa compiled.values[1] = clip; return storeCompiledCssValue(compiled); } + if (declaration == CssDeclarationId::BackgroundBlendMode && !hasVar) { + const int blend = compileBackgroundBlendMode(value); + if (blend < 0) return kNoCompiledCssValue; + compiled.kind = CssCompiledKind::DirectProperty; + compiled.values[0] = static_cast(Property::BackgroundBlendMode); + compiled.values[1] = blend; + return storeCompiledCssValue(compiled); + } + + StaticStylePropertyValue clampWrites[kDirectPropertyGroupCapacity]; + int clampCount = hasVar ? -1 : lineClampWrites(declaration, value, clampWrites); + if (clampCount < 0 && !hasVar) clampCount = multicolWrites(declaration, value, clampWrites); + if (clampCount < 0 && !hasVar) clampCount = borderStyleWrites(declaration, value, clampWrites); + if (clampCount < 0 && !hasVar) clampCount = textEmphasisWrites(declaration, value, clampWrites); + if (clampCount < 0 && !hasVar) clampCount = borderImageWrites(declaration, value, clampWrites); + if (const int count = clampCount; count >= 0) { + if (count == 0) return kNoCompiledCssValue; + compiled.kind = CssCompiledKind::DirectPropertyGroup; + compiled.values[0] = count; + for (int i = 0; i < count; ++i) { + compiled.values[1 + i * 2] = static_cast(clampWrites[i].property); + compiled.values[2 + i * 2] = clampWrites[i].value; + } + return storeCompiledCssValue(compiled); + } if (declaration == CssDeclarationId::MarginTrim && !hasVar) { const int flags = marginTrimValue(value); @@ -7351,12 +8043,7 @@ std::uint16_t compileCssValue(CssDeclarationId declaration, const CssText &rawVa return storeCompiledCssValue(compiled); case CssDeclarationId::BoxShadow: if (!compiledCssValueFeatureEnabled(kCssCompiledFeatureEffects)) return kNoCompiledCssValue; - if (hasDynamicCssValue(value)) { - if (boxShadowValueHasInsetLayer(value)) return kNoCompiledCssValue; - compiled.kind = CssCompiledKind::BoxShadow; - compiled.aux = 0; - return storeCompiledCssValue(compiled); - } + if (hasDynamicCssValue(value)) return kNoCompiledCssValue; if (!compileBoxShadowValue(value, compiled)) return kNoCompiledCssValue; compiled.kind = CssCompiledKind::BoxShadow; return storeCompiledCssValue(compiled); @@ -7445,8 +8132,8 @@ bool setClassRuleValueFastUnchecked(Node &target, Property property, int value) ComputedStyle &style = target.style; switch (property) { case Property::Display: - style.display_explicit = 1; - style.display = value; + style.display_explicit = kDisplayExplicit | (value >> kDisplayFlagShift); + style.display = value & kDisplayKindMask; return true; case Property::FlexDirection: style.flex_direction_explicit = 1; @@ -7455,6 +8142,12 @@ bool setClassRuleValueFastUnchecked(Node &target, Property property, int value) case Property::FlexWrap: style.flex_wrap = value; return true; case Property::FlexLineCount: rstyleMut(style).flex_line_count = value; return true; case Property::BackgroundClip: rstyleMut(style).bg_clip = value; return true; + case Property::BackgroundBlendMode: rstyleMut(style).bg_blend = value; return true; + case Property::BorderImageSource: rstyleMut(style).border_image_source = value; return true; + case Property::BorderImageSlice: rstyleMut(style).border_image_slice = value; return true; + case Property::BorderImageWidth: rstyleMut(style).border_image_width = value; return true; + case Property::BorderImageOutset: rstyleMut(style).border_image_outset = value; return true; + case Property::BorderImageRepeat: rstyleMut(style).border_image_repeat = value; return true; case Property::BackgroundSizeList: rstyleMut(style).bg_size_list = value; return true; case Property::BackgroundPositionList: rstyleMut(style).bg_position_list = value; return true; case Property::BackgroundRepeatList: rstyleMut(style).bg_repeat_list = value; return true; @@ -7476,6 +8169,22 @@ bool setClassRuleValueFastUnchecked(Node &target, Property property, int value) case Property::Float: style.float_side = value; return true; case Property::AspectRatio: rstyleMut(style).aspect_ratio = value; return true; case Property::MarginTrim: rstyleMut(style).margin_trim = value; return true; + case Property::MaxLines: rstyleMut(style).max_lines = static_cast(value); return true; + case Property::BlockEllipsisString: rstyleMut(style).block_ellipsis = static_cast(value); return true; + case Property::ColumnCount: rstyleMut(style).column_count = static_cast(value); return true; + case Property::ColumnWidth: rstyleMut(style).column_width = static_cast(value); return true; + case Property::LineClampContinue: + case Property::BlockEllipsis: + case Property::ColumnCountSet: + case Property::ColumnWidthSet: + case Property::ColumnFillAuto: + case Property::LineClampDiscard: + case Property::ColumnSpanAll: { + const int bit = lineClampFlagBit(property); + auto &flags = rstyleMut(style).line_clamp_flags; + flags = static_cast(value ? flags | bit : flags & ~bit); + return true; + } case Property::Clear: style.clear_side = value; return true; case Property::WritingMode: style.writing_mode = value; return true; case Property::Direction: style.direction = value; return true; @@ -7540,19 +8249,19 @@ bool setClassRuleValueFastUnchecked(Node &target, Property property, int value) case Property::MarginLeft: if (rstyle(style).margin_expression[3] >= 0) rstyleMut(style).margin_expression[3] = -1; style.margin_auto &= ~8; style.margin[3] = value; return true; case Property::Position: if (value == kPositionFixed) treeState().fixedPositionUsed = true; style.position = value; return true; case Property::Top: - style.pos_offsets[0] = value; + style.pos_offsets[0] = storedPositionOffset(value); style.pos_offset_percent[0] = kUnset; return true; case Property::Right: - style.pos_offsets[1] = value; + style.pos_offsets[1] = storedPositionOffset(value); style.pos_offset_percent[1] = kUnset; return true; case Property::Bottom: - style.pos_offsets[2] = value; + style.pos_offsets[2] = storedPositionOffset(value); style.pos_offset_percent[2] = kUnset; return true; case Property::Left: - style.pos_offsets[3] = value; + style.pos_offsets[3] = storedPositionOffset(value); style.pos_offset_percent[3] = kUnset; return true; case Property::TopPercent: @@ -7624,12 +8333,15 @@ bool setClassRuleValueFastUnchecked(Node &target, Property property, int value) case Property::BorderLeftRelief: { const int side = static_cast(property) - static_cast(Property::BorderTopRelief); if (rstyle(style).border_relief[side] != value) rstyleMut(style).border_relief[side] = static_cast(value); + if (value & kBorderStyleNone) setComputedBorderWidth(style, side, 0, nullptr); return true; } - case Property::BorderRelief: - if (value == 0 && !hasBorderRelief(style)) return true; + case Property::BorderRelief: { + const auto &relief = rstyle(style).border_relief; + if (value == 0 && !(relief[0] | relief[1] | relief[2] | relief[3])) return true; for (int side = 0; side < 4; ++side) rstyleMut(style).border_relief[side] = static_cast(value); return true; + } case Property::BorderTopWidth: setComputedBorderWidth(style, 0, value, target.parent >= 0 ? &treeState().nodes[target.parent].style : nullptr); return true; case Property::BorderRightWidth: setComputedBorderWidth(style, 1, value, target.parent >= 0 ? &treeState().nodes[target.parent].style : nullptr); return true; case Property::BorderBottomWidth: setComputedBorderWidth(style, 2, value, target.parent >= 0 ? &treeState().nodes[target.parent].style : nullptr); return true; @@ -7702,6 +8414,12 @@ bool setClassRuleValueFastUnchecked(Node &target, Property property, int value) if (rstyle(style).line_height_expression >= 0) rstyleMut(style).line_height_expression = -1; style.line_height = resolveLineHeightMultiplier(static_cast(&target - treeState().nodes), value); return true; case Property::TextAlign: style.text_align = value; return true; + case Property::TextAlignLast: style.text_align_last = value; return true; + case Property::TextEmphasisStyle: style.text_emphasis = static_cast((style.text_emphasis & ~0x0f) | (value & 0x0f)); return true; + case Property::TextEmphasisPosition: style.text_emphasis = static_cast((style.text_emphasis & ~0x10) | (value & 0x10)); return true; + case Property::TextEmphasisColorMode: style.text_emphasis = static_cast((style.text_emphasis & ~0x60) | (value & 0x60)); return true; + case Property::TextEmphasisColor: style.text_emphasis_color = StyleValues::pixelFromStyleValue(value); return true; + case Property::VerticalAlign: style.vertical_align = value; return true; case Property::TextDecoration: style.text_decoration = value; return true; case Property::TextTransform: style.text_transform = value; return true; case Property::WhiteSpace: style.white_space = static_cast(value); return true; @@ -8058,6 +8776,22 @@ void setIndividualTranslation(NodeHandle node, const TransformComponents &t, Sty setStyleValue(node, Property::TranslateYPercent, t.translateYPercent, source); } +// transform: inherit takes the parent's transform list as it computed. +TransformComponents inheritedTransformComponents(int nodeId) +{ + TransformComponents t; + const int parent = Tree::instance().node(nodeId).parent; + if (parent < 0) return t; + const RareStyle &r = rstyle(Tree::instance().node(parent).style); + t.translateOuterAxes = r.transform_translate_outer_axes; + t.rotateX = r.transform_rotate_x; t.rotateY = r.transform_rotate_y; t.rotateZ = r.transform_rotate; + t.translateX = r.transform_translate_x; t.translateY = r.transform_translate_y; t.translateZ = r.transform_translate_z; + t.translateXPercent = r.transform_translate_x_percent; t.translateYPercent = r.transform_translate_y_percent; + t.scaleX = r.transform_scale_x; t.scaleY = r.transform_scale_y; t.scaleZ = r.transform_scale_z; + t.hasRotateZ = r.transform_present; + return t; +} + void setTransformComponents(NodeHandle node, const TransformComponents &t, StyleApplicationSource source) { setStyleValue(node, Property::TransformTranslateOuterAxes, t.translateOuterAxes, source); @@ -8222,9 +8956,10 @@ void applyBorderShorthand(NodeHandle node, const std::string &value, StyleApplic const auto parts = splitFunctionAwareWords(value); const int width = resolveBorderWidth(compiled.lengths[0], node.id()); if (width < 0) return; - setUniformBorderWidth(node, width, source); + // The shorthand resets border-style first, so a side that was none takes the width. for (int side = 0; side < 4; ++side) setStyleValue(node, static_cast(static_cast(Property::BorderTopRelief) + side), compiled.values[2], source); + setUniformBorderWidth(node, width, source); for (const auto &part : parts) { if (part.empty()) continue; if (part[0] == '#' || part.find("rgb") != std::string::npos || toLowerAscii(part) == "transparent") { @@ -8547,7 +9282,9 @@ bool propertyAffectsDescendantStyle(Property property) property == Property::FontSize || property == Property::FontWeight || property == Property::LineHeight || property == Property::LineHeightExpression || property == Property::LineHeightMultiplier || - property == Property::TextAlign || + property == Property::TextAlign || property == Property::TextAlignLast || + property == Property::TextEmphasisStyle || property == Property::TextEmphasisPosition || + property == Property::TextEmphasisColorMode || property == Property::TextEmphasisColor || property == Property::TextTransform || property == Property::WhiteSpace || property == Property::Visibility || property == Property::BorderWidth || (property >= Property::BorderTopWidth && property <= Property::BorderLeftWidth); @@ -8571,6 +9308,9 @@ void applyInheritedStyleDefaults(int node) if (parentStyle.line_height_multiplier >= 0) style.line_height_multiplier = parentStyle.line_height_multiplier; style.text_align = parentStyle.text_align; + style.text_align_last = parentStyle.text_align_last; + style.text_emphasis = parentStyle.text_emphasis; + style.text_emphasis_color = parentStyle.text_emphasis_color; style.text_transform = parentStyle.text_transform; style.white_space = parentStyle.white_space; style.visibility = parentStyle.visibility; @@ -8765,6 +9505,14 @@ bool applyKnownResolvedPropertyWithSource(NodeHandle node, CssDeclarationId decl if (clip >= 0) setStyleValue(node, Property::BackgroundClip, clip, source); return true; } + case CssDeclarationId::BackgroundBlendMode: { + const int parent = Tree::instance().node(nodeId).parent; + const int blend = toLowerAscii(trimCssValue(value)) == "inherit" + ? (parent >= 0 ? rstyle(Tree::instance().node(parent).style).bg_blend : 0) + : compileBackgroundBlendMode(value); + if (blend >= 0) setStyleValue(node, Property::BackgroundBlendMode, blend, source); + return true; + } case CssDeclarationId::Contain: { const int parent = Tree::instance().node(nodeId).parent; const int flags = toLowerAscii(trimCssValue(value)) == "inherit" @@ -8828,6 +9576,40 @@ bool applyKnownResolvedPropertyWithSource(NodeHandle node, CssDeclarationId decl if (value == "left") setStyleValue(node, Property::Float, 1, source); if (value == "right") setStyleValue(node, Property::Float, 2, source); return true; + case CssDeclarationId::LineClamp: + case CssDeclarationId::WebkitLineClamp: + case CssDeclarationId::MaxLines: + case CssDeclarationId::Continue: + case CssDeclarationId::BlockEllipsis: + case CssDeclarationId::Columns: + case CssDeclarationId::ColumnCount: + case CssDeclarationId::ColumnWidth: + case CssDeclarationId::ColumnFill: + case CssDeclarationId::ColumnSpan: + case CssDeclarationId::BorderStyle: + case CssDeclarationId::BorderTopStyle: + case CssDeclarationId::BorderRightStyle: + case CssDeclarationId::BorderBottomStyle: + case CssDeclarationId::BorderLeftStyle: + case CssDeclarationId::TextEmphasis: + case CssDeclarationId::TextEmphasisStyleDeclaration: + case CssDeclarationId::TextEmphasisColorDeclaration: + case CssDeclarationId::TextEmphasisPositionDeclaration: + case CssDeclarationId::BorderImage: + case CssDeclarationId::BorderImageSourceDeclaration: + case CssDeclarationId::BorderImageSliceDeclaration: + case CssDeclarationId::BorderImageWidthDeclaration: + case CssDeclarationId::BorderImageOutsetDeclaration: + case CssDeclarationId::BorderImageRepeatDeclaration: { + StaticStylePropertyValue writes[kDirectPropertyGroupCapacity]; + int count = lineClampWrites(declaration, value, writes); + if (count < 0) count = multicolWrites(declaration, value, writes); + if (count < 0) count = borderStyleWrites(declaration, value, writes); + if (count < 0) count = textEmphasisWrites(declaration, value, writes); + if (count < 0) count = borderImageWrites(declaration, value, writes); + for (int i = 0; i < count; ++i) setStyleValue(node, writes[i].property, writes[i].value, source); + return true; + } case CssDeclarationId::MarginTrim: { const int parent = Tree::instance().node(nodeId).parent; const int flags = toLowerAscii(trimCssValue(value)) == "inherit" @@ -9051,6 +9833,29 @@ bool applyKnownResolvedPropertyWithSource(NodeHandle node, CssDeclarationId decl if (compileFlexBasisValue(value, compiled)) setFlexBasisValue(node, compiled.aux != 0, compiled.lengths[0], source); return true; } + case CssDeclarationId::MarginInline: + case CssDeclarationId::MarginBlock: + case CssDeclarationId::PaddingInline: + case CssDeclarationId::PaddingBlock: + case CssDeclarationId::InsetInline: + case CssDeclarationId::InsetBlock: + case CssDeclarationId::BorderInline: + case CssDeclarationId::BorderBlock: + case CssDeclarationId::BorderInlineWidth: + case CssDeclarationId::BorderBlockWidth: + case CssDeclarationId::BorderInlineColor: + case CssDeclarationId::BorderBlockColor: + case CssDeclarationId::BorderInlineStyle: + case CssDeclarationId::BorderBlockStyle: + { + const LogicalPair *pair = logicalPairFor(nullptr, declaration); + if (!pair) return true; + const auto parts = pair->split ? splitFunctionAwareWords(value) : std::vector{value}; + if (parts.empty() || parts.size() > 2) return true; + applyKnownResolvedPropertyWithSource(node, pair->first, parts[0], source); + applyKnownResolvedPropertyWithSource(node, pair->second, parts.size() > 1 ? parts[1] : parts[0], source); + return true; + } case CssDeclarationId::Padding: case CssDeclarationId::PaddingTop: case CssDeclarationId::PaddingRight: @@ -9227,6 +10032,12 @@ bool applyKnownResolvedPropertyWithSource(NodeHandle node, CssDeclarationId decl case CssDeclarationId::TextAlign: setStyleValue(node, Property::TextAlign, textAlignValue(value), source); return true; + case CssDeclarationId::TextAlignLast: + setStyleValue(node, Property::TextAlignLast, textAlignLastValue(value), source); + return true; + case CssDeclarationId::VerticalAlign: + if (const int align = verticalAlignValue(value); align >= 0) setStyleValue(node, Property::VerticalAlign, align, source); + return true; case CssDeclarationId::TextDecoration: setStyleValue(node, Property::TextDecoration, textDecorationValue(value), source); return true; @@ -9281,7 +10092,8 @@ bool applyKnownResolvedPropertyWithSource(NodeHandle node, CssDeclarationId decl return true; } case CssDeclarationId::Transform: - setTransformComponents(node, parseTransformComponents(value, nodeId), source); + setTransformComponents(node, toLowerAscii(trimCssValue(value)) == "inherit" ? inheritedTransformComponents(nodeId) + : parseTransformComponents(value, nodeId), source); return true; case CssDeclarationId::Rotate: { IndividualRotation rotation; @@ -10017,9 +10829,9 @@ bool applyRuntimeBorderShorthandValue(NodeHandle node, Node &target = state.nodes[nodeId]; const int snappedWidth = resolveBorderWidth(width, nodeId); if (snappedWidth < 0) return true; - setUniformBorderWidth(node, snappedWidth, source); for (int side = 0; side < 4; ++side) setStyleValue(node, static_cast(static_cast(Property::BorderTopRelief) + side), relief, source); + setUniformBorderWidth(node, snappedWidth, source); if (alpha >= 0) { setStyleValue(node, Property::BorderColor, color, source); setStyleValue(node, Property::BorderAlpha, alpha, source); @@ -10161,7 +10973,7 @@ bool applyCompiledBoxShadowValue(NodeHandle node, const CssCompiledValue &compil source); setStyleValue(node, Property::BoxShadowColor, compiled.values[0], source); setStyleValue(node, Property::BoxShadowAlpha, compiled.values[1], source); - setStyleValue(node, Property::BoxShadowInset, 1, source); + setStyleValue(node, Property::BoxShadowInset, compiled.aux == 1 ? 1 : 0, source); return true; } @@ -10226,7 +11038,7 @@ bool applyCompiledCssValueWithSource(NodeHandle node, const CssCompiledValue &co case CssCompiledKind::DirectPropertyGroup: { int count = compiled.values[0]; if (count < 0) count = 0; - if (count > 4) count = 4; + if (count > kDirectPropertyGroupCapacity) count = kDirectPropertyGroupCapacity; for (int i = 0; i < count; ++i) { const int propertyIndex = compiled.values[1 + i * 2]; if (propertyIndex < 0 || propertyIndex >= static_cast(Property::Count)) return false; @@ -10252,6 +11064,8 @@ bool applyCompiledCssValueWithSource(NodeHandle node, const CssCompiledValue &co case CssDeclarationId::AlignSelf: setStyleValue(node, Property::AlignSelf, compiled.values[0], source); return true; case CssDeclarationId::Position: setStyleValue(node, Property::Position, compiled.values[0], source); return true; case CssDeclarationId::TextAlign: setStyleValue(node, Property::TextAlign, compiled.values[0], source); return true; + case CssDeclarationId::TextAlignLast: setStyleValue(node, Property::TextAlignLast, compiled.values[0], source); return true; + case CssDeclarationId::VerticalAlign: setStyleValue(node, Property::VerticalAlign, compiled.values[0], source); return true; case CssDeclarationId::TextDecoration: setStyleValue(node, Property::TextDecoration, compiled.values[0], source); return true; case CssDeclarationId::TextTransform: setStyleValue(node, Property::TextTransform, compiled.values[0], source); return true; case CssDeclarationId::WhiteSpace: setStyleValue(node, Property::WhiteSpace, compiled.values[0], source); return true; @@ -10611,6 +11425,30 @@ bool removeInlineStyleProperties(int node, std::initializer_list prope bool removeInlineStyleProperty(NodeHandle node, const std::string &property) { if (!node) return false; + if (const char *physical = physicalLogicalLonghand(property.c_str())) return removeInlineStyleProperty(node, physical); + if (const LogicalPair *pair = logicalPairFor(property.c_str())) { + const auto &names = [](StyleDeclaration side) { + static const std::pair table[] = { + {StyleDeclaration::MarginLeft, "margin-left"}, {StyleDeclaration::MarginRight, "margin-right"}, + {StyleDeclaration::MarginTop, "margin-top"}, {StyleDeclaration::MarginBottom, "margin-bottom"}, + {StyleDeclaration::PaddingLeft, "padding-left"}, {StyleDeclaration::PaddingRight, "padding-right"}, + {StyleDeclaration::PaddingTop, "padding-top"}, {StyleDeclaration::PaddingBottom, "padding-bottom"}, + {StyleDeclaration::Left, "left"}, {StyleDeclaration::Right, "right"}, {StyleDeclaration::Top, "top"}, {StyleDeclaration::Bottom, "bottom"}, + {StyleDeclaration::BorderLeft, "border-left"}, {StyleDeclaration::BorderRight, "border-right"}, + {StyleDeclaration::BorderTop, "border-top"}, {StyleDeclaration::BorderBottom, "border-bottom"}, + {StyleDeclaration::BorderLeftWidth, "border-left-width"}, {StyleDeclaration::BorderRightWidth, "border-right-width"}, + {StyleDeclaration::BorderTopWidth, "border-top-width"}, {StyleDeclaration::BorderBottomWidth, "border-bottom-width"}, + {StyleDeclaration::BorderLeftColor, "border-left-color"}, {StyleDeclaration::BorderRightColor, "border-right-color"}, + {StyleDeclaration::BorderTopColor, "border-top-color"}, {StyleDeclaration::BorderBottomColor, "border-bottom-color"}, + {StyleDeclaration::BorderLeftStyle, "border-left-style"}, {StyleDeclaration::BorderRightStyle, "border-right-style"}, + {StyleDeclaration::BorderTopStyle, "border-top-style"}, {StyleDeclaration::BorderBottomStyle, "border-bottom-style"}, + }; + for (const auto &entry : table) if (entry.first == side) return entry.second; + return ""; + }; + const bool first = removeInlineStyleProperty(node, names(pair->first)); + return removeInlineStyleProperty(node, names(pair->second)) || first; + } if (property.rfind("--", 0) == 0) { NodeRareData *rare = rareDataFor(node.id()); if (!rare) return false; @@ -10704,6 +11542,16 @@ bool removeInlineStyleProperty(NodeHandle node, const std::string &property) if (property == "box-sizing") return removeInlineStyleProperties(id, {Property::BoxSizing}); if (property == "float") return removeInlineStyleProperties(id, {Property::Float}); if (property == "margin-trim") return removeInlineStyleProperties(id, {Property::MarginTrim}); + if (property == "line-clamp" || property == "-webkit-line-clamp") + return removeInlineStyleProperties(id, {Property::MaxLines, Property::LineClampContinue, Property::BlockEllipsis, Property::BlockEllipsisString}); + if (property == "max-lines") return removeInlineStyleProperties(id, {Property::MaxLines}); + if (property == "continue") return removeInlineStyleProperties(id, {Property::LineClampContinue, Property::LineClampDiscard}); + if (property == "block-ellipsis") return removeInlineStyleProperties(id, {Property::BlockEllipsis, Property::BlockEllipsisString}); + if (property == "columns") return removeInlineStyleProperties(id, {Property::ColumnCountSet, Property::ColumnWidthSet, Property::ColumnCount, Property::ColumnWidth}); + if (property == "column-count") return removeInlineStyleProperties(id, {Property::ColumnCountSet, Property::ColumnCount}); + if (property == "column-width") return removeInlineStyleProperties(id, {Property::ColumnWidthSet, Property::ColumnWidth}); + if (property == "column-fill") return removeInlineStyleProperties(id, {Property::ColumnFillAuto}); + if (property == "column-span") return removeInlineStyleProperties(id, {Property::ColumnSpanAll}); if (property == "clear") return removeInlineStyleProperties(id, {Property::Clear}); if (property == "direction") return removeInlineStyleProperties(id, {Property::Direction}); if (property == "writing-mode") return removeInlineStyleProperties(id, {Property::WritingMode}); @@ -10715,6 +11563,15 @@ bool removeInlineStyleProperty(NodeHandle node, const std::string &property) if (property == "background") return removeInlineStyleProperties(id, {Property::BackgroundColor, Property::BackgroundAlpha, Property::BackgroundImage, Property::BackgroundClip, Property::HasBackground, Property::BackgroundSizeList, Property::BackgroundPositionList, Property::BackgroundRepeatList, Property::BackgroundAttachmentList, Property::BackgroundOriginList}); if (property == "background-clip") return removeInlineStyleProperties(id, {Property::BackgroundClip}); + if (property == "background-blend-mode") return removeInlineStyleProperties(id, {Property::BackgroundBlendMode}); + if (property == "border-image") + return removeInlineStyleProperties(id, {Property::BorderImageSource, Property::BorderImageSlice, Property::BorderImageWidth, + Property::BorderImageOutset, Property::BorderImageRepeat}); + if (property == "border-image-source") return removeInlineStyleProperties(id, {Property::BorderImageSource}); + if (property == "border-image-slice") return removeInlineStyleProperties(id, {Property::BorderImageSlice}); + if (property == "border-image-width") return removeInlineStyleProperties(id, {Property::BorderImageWidth}); + if (property == "border-image-outset") return removeInlineStyleProperties(id, {Property::BorderImageOutset}); + if (property == "border-image-repeat") return removeInlineStyleProperties(id, {Property::BorderImageRepeat}); if (property == "background-size") return removeInlineStyleProperties(id, {Property::BackgroundSizeList}); if (property == "background-position") return removeInlineStyleProperties(id, {Property::BackgroundPositionList}); if (property == "background-repeat") return removeInlineStyleProperties(id, {Property::BackgroundRepeatList}); @@ -10738,6 +11595,11 @@ bool removeInlineStyleProperty(NodeHandle node, const std::string &property) if (property == "border-right-width") return removeInlineStyleProperties(id, {Property::BorderRightWidth}); if (property == "border-bottom-width") return removeInlineStyleProperties(id, {Property::BorderBottomWidth}); if (property == "border-left-width") return removeInlineStyleProperties(id, {Property::BorderLeftWidth}); + if (property == "border-style") return removeInlineStyleProperties(id, {Property::BorderTopRelief, Property::BorderRightRelief, Property::BorderBottomRelief, Property::BorderLeftRelief}); + if (property == "border-top-style") return removeInlineStyleProperties(id, {Property::BorderTopRelief}); + if (property == "border-right-style") return removeInlineStyleProperties(id, {Property::BorderRightRelief}); + if (property == "border-bottom-style") return removeInlineStyleProperties(id, {Property::BorderBottomRelief}); + if (property == "border-left-style") return removeInlineStyleProperties(id, {Property::BorderLeftRelief}); if (property == "border-top-color") return removeInlineStyleProperties(id, {Property::BorderTopColor, Property::BorderTopAlpha, Property::BorderTopColorCurrent}); if (property == "border-right-color") return removeInlineStyleProperties(id, {Property::BorderRightColor, Property::BorderRightAlpha, Property::BorderRightColorCurrent}); if (property == "border-bottom-color") return removeInlineStyleProperties(id, {Property::BorderBottomColor, Property::BorderBottomAlpha, Property::BorderBottomColorCurrent}); @@ -10765,6 +11627,12 @@ bool removeInlineStyleProperty(NodeHandle node, const std::string &property) if (property == "font-weight") return removeInlineStyleProperties(id, {Property::FontWeight}); if (property == "line-height") return removeInlineStyleProperties(id, {Property::LineHeight, Property::LineHeightExpression, Property::LineHeightMultiplier}); if (property == "text-align") return removeInlineStyleProperties(id, {Property::TextAlign}); + if (property == "text-align-last") return removeInlineStyleProperties(id, {Property::TextAlignLast}); + if (property == "text-emphasis") return removeInlineStyleProperties(id, {Property::TextEmphasisStyle, Property::TextEmphasisColorMode, Property::TextEmphasisColor}); + if (property == "text-emphasis-style") return removeInlineStyleProperties(id, {Property::TextEmphasisStyle}); + if (property == "text-emphasis-color") return removeInlineStyleProperties(id, {Property::TextEmphasisColorMode, Property::TextEmphasisColor}); + if (property == "text-emphasis-position") return removeInlineStyleProperties(id, {Property::TextEmphasisPosition}); + if (property == "vertical-align") return removeInlineStyleProperties(id, {Property::VerticalAlign}); if (property == "text-decoration" || property == "text-decoration-line") return removeInlineStyleProperties(id, {Property::TextDecoration}); if (property == "text-transform") return removeInlineStyleProperties(id, {Property::TextTransform}); @@ -10907,6 +11775,16 @@ bool isRootNode(int node) return state.nodes[node].parent < 0; } +// The element sibling before node; text and generated content are not elements. +int previousElementSibling(int node) +{ + const auto &state = treeState(); + if (node < 0 || node >= state.nodeCount) return -1; + for (int sibling = state.nodes[node].prev_sibling; sibling >= 0; sibling = state.nodes[sibling].prev_sibling) + if (!isGeneratedPseudoNode(state.nodes[sibling]) && !isAnonymousTextNode(state.nodes[sibling])) return sibling; + return -1; +} + bool isFirstElementChild(int node) { const auto &state = treeState(); @@ -11072,12 +11950,16 @@ ParsedSimpleSelector parseSimpleSelector(const char *rawSimple, std::size_t rawL struct SelectorPart { ParsedSimpleSelector simple; bool directParent = false; + // Relation to the part on its left: 1 next sibling (+), 2 any earlier sibling (~). + std::uint8_t sibling = 0; }; struct SelectorPlan { SmallSelectorList parts; SmallSelectorList ancestorClasses; SmallSelectorList ancestorTags; + SmallSelectorList siblingClasses; + SmallSelectorList siblingTags; CssAtomId rightmostId = kInvalidCssAtom; CssAtomId rightmostClass = kInvalidCssAtom; int16_t rightmostTag = -1; @@ -11125,13 +12007,16 @@ void finishSelectorPlan(SelectorPlan &plan) } for (std::size_t p = 0; p + 1 < plan.parts.size(); ++p) { + // A part joined to the next by + or ~ matches an earlier sibling, not an + // ancestor: a change to it restyles the siblings that follow. + const bool sibling = plan.parts.at(p + 1).sibling != 0; const ParsedSimpleSelector &ancestor = plan.parts.at(p).simple; for (std::size_t c = 0, n = ancestor.classIds.size(); c < n; ++c) { const CssAtomId cls = ancestor.classIds.at(c); - if (cls != kInvalidCssAtom) appendUnique(plan.ancestorClasses, cls); + if (cls != kInvalidCssAtom) appendUnique(sibling ? plan.siblingClasses : plan.ancestorClasses, cls); } if (ancestor.hasTag && !ancestor.rootTag && ancestor.tagId >= 0) - appendUnique(plan.ancestorTags, ancestor.tagId); + appendUnique(sibling ? plan.siblingTags : plan.ancestorTags, ancestor.tagId); } } @@ -11140,22 +12025,27 @@ SelectorPlan parseNormalizedSelectorPlan(const char *selectorText, std::size_t s SelectorPlan plan; const char *selector = selectorText ? selectorText : ""; bool nextDirect = false; + std::uint8_t nextSibling = 0; + auto combinator = [](char c) { return c == '>' || c == '+' || c == '~'; }; std::size_t i = 0; while (i < selectorLength) { while (i < selectorLength && static_cast(selector[i]) <= ' ') ++i; if (i >= selectorLength) break; - if (selector[i] == '>') { - nextDirect = true; + if (combinator(selector[i])) { + nextDirect = selector[i] == '>'; + nextSibling = selector[i] == '+' ? 1 : selector[i] == '~' ? 2 : 0; ++i; continue; } const std::size_t start = i; - while (i < selectorLength && static_cast(selector[i]) > ' ' && selector[i] != '>') ++i; + while (i < selectorLength && static_cast(selector[i]) > ' ' && !combinator(selector[i])) ++i; if (i <= start) continue; SelectorPart part; part.simple = parseSimpleSelector(selector + start, i - start); part.directParent = nextDirect; + part.sibling = nextSibling; nextDirect = false; + nextSibling = 0; if (!part.simple.valid) plan.valid = false; plan.specificity += simpleSelectorSpecificity(part.simple); plan.parts.push_back(std::move(part)); @@ -11242,6 +12132,7 @@ std::uint16_t storeStaticSelectorPlan(const char *selector, SelectorPart part; part.simple = staticSimpleSelectorForSpec(spec.simple); part.directParent = spec.directParent; + part.sibling = spec.sibling; if (!part.simple.valid) plan.valid = false; plan.specificity += simpleSelectorSpecificity(part.simple); plan.parts.push_back(std::move(part)); @@ -11311,6 +12202,19 @@ bool selectorPlanMatchesNode(const SelectorPlan &plan, int node) for (int target = last - 1; target >= 0; --target) { const bool direct = parts.at(static_cast(target + 1)).directParent; + const std::uint8_t sibling = parts.at(static_cast(target + 1)).sibling; + if (sibling) { + bool found = false; + for (int earlier = previousElementSibling(current); earlier >= 0; earlier = sibling == 1 ? -1 : previousElementSibling(earlier)) { + if (matchSimpleSelector(earlier, parts.at(static_cast(target)).simple)) { + current = earlier; + found = true; + break; + } + } + if (!found) return false; + continue; + } const int parent = current >= 0 && current < state.nodeCount ? state.nodes[current].parent : -1; if (direct) { current = parent; @@ -12068,6 +12972,11 @@ struct RuleIndex { // recomputed in full — a custom-prop/inheritance diff alone wouldn't catch it. DenseIdSet ancestorClasses; DenseIdSet ancestorTags; + // The same for parts joined by + or ~, which match an earlier sibling: a + // change to one of these keys restyles the siblings after the node. + DenseIdSet siblingClasses; + DenseIdSet siblingTags; + bool hasSiblingRules = false; // CSS specificity per rule (indexed by rule index), packed as // ids*10000 + (classes+pseudo-classes)*100 + (elements+pseudo-elements). Used to // order the cascade: a more-specific rule wins over a less-specific one @@ -12158,6 +13067,9 @@ void rebuildRuleIndexIfNeeded() clearCachedStyleApplyOps(); g_ruleIndex.ancestorClasses.clear(); g_ruleIndex.ancestorTags.clear(); + g_ruleIndex.siblingClasses.clear(); + g_ruleIndex.siblingTags.clear(); + g_ruleIndex.hasSiblingRules = false; const auto &list = rules(); g_ruleIndex.specificity.assign(list.size(), 0); g_ruleIndex.writeMasks.assign(list.size(), {}); @@ -12213,6 +13125,12 @@ void rebuildRuleIndexIfNeeded() const int16_t tag = plan->ancestorTags.at(ancestor); if (tag >= 0) g_ruleIndex.ancestorTags.insert(tag); } + for (std::size_t p = 0; p < plan->parts.size(); ++p) + if (plan->parts.at(p).sibling) g_ruleIndex.hasSiblingRules = true; + for (std::size_t sibling = 0, count = plan->siblingClasses.size(); sibling < count; ++sibling) + g_ruleIndex.siblingClasses.insert(plan->siblingClasses.at(sibling)); + for (std::size_t sibling = 0, count = plan->siblingTags.size(); sibling < count; ++sibling) + g_ruleIndex.siblingTags.insert(plan->siblingTags.at(sibling)); } break; } @@ -12953,6 +13871,8 @@ bool addCachedKeywordDeclaration(CachedStyleApplyOp &op, CssDeclarationId declar case CssDeclarationId::AlignSelf: return addCachedStyleApplyProperty(op, Property::AlignSelf, value); case CssDeclarationId::Position: return addCachedStyleApplyProperty(op, Property::Position, value); case CssDeclarationId::TextAlign: return addCachedStyleApplyProperty(op, Property::TextAlign, value); + case CssDeclarationId::TextAlignLast: return addCachedStyleApplyProperty(op, Property::TextAlignLast, value); + case CssDeclarationId::VerticalAlign: return addCachedStyleApplyProperty(op, Property::VerticalAlign, value); case CssDeclarationId::TextDecoration: return addCachedStyleApplyProperty(op, Property::TextDecoration, value); case CssDeclarationId::TextTransform: return addCachedStyleApplyProperty(op, Property::TextTransform, value); case CssDeclarationId::WhiteSpace: return addCachedStyleApplyProperty(op, Property::WhiteSpace, value); @@ -13331,8 +14251,8 @@ bool buildCachedStyleApplyOp(const CssCompiledValue *compiled, CachedStyleApplyO op.values[3] = compiled->aux != 0 ? compiled->values[1] : -1; return true; } - if (!addCachedUniformBorderWidth(op, width)) return false; if (!addCachedStyleApplyProperty(op, Property::BorderRelief, compiled->values[2])) return false; + if (!addCachedUniformBorderWidth(op, width)) return false; return compiled->aux == 0 ? addCachedStyleApplyProperty(op, Property::BorderColorCurrent, 1) : addCachedStyleApplyProperty(op, Property::BorderColor, compiled->values[0]); } case CssCompiledKind::BorderSideShorthand: { @@ -13821,7 +14741,7 @@ bool addCompiledValueWrites(const CssCompiledValue &compiled, PropertyWriteMask case CssCompiledKind::DirectPropertyGroup: { int count = compiled.values[0]; if (count < 0) count = 0; - if (count > 4) count = 4; + if (count > kDirectPropertyGroupCapacity) count = kDirectPropertyGroupCapacity; for (int i = 0; i < count; ++i) { const int propertyIndex = compiled.values[1 + i * 2]; if (propertyIndex < 0 || propertyIndex >= static_cast(Property::Count)) return false; @@ -13847,6 +14767,8 @@ bool addCompiledValueWrites(const CssCompiledValue &compiled, PropertyWriteMask case CssDeclarationId::AlignSelf: addPropertyWrite(mask, Property::AlignSelf); return true; case CssDeclarationId::Position: addPropertyWrite(mask, Property::Position); return true; case CssDeclarationId::TextAlign: addPropertyWrite(mask, Property::TextAlign); return true; + case CssDeclarationId::TextAlignLast: addPropertyWrite(mask, Property::TextAlignLast); return true; + case CssDeclarationId::VerticalAlign: addPropertyWrite(mask, Property::VerticalAlign); return true; case CssDeclarationId::TextDecoration: addPropertyWrite(mask, Property::TextDecoration); return true; case CssDeclarationId::TextTransform: addPropertyWrite(mask, Property::TextTransform); return true; case CssDeclarationId::WhiteSpace: addPropertyWrite(mask, Property::WhiteSpace); return true; @@ -14947,6 +15869,9 @@ struct ParentStyleSnapshot { std::int16_t line_height; std::int32_t line_height_multiplier; std::uint8_t text_align; + std::uint8_t text_align_last; + std::uint8_t text_emphasis; + style_color_t text_emphasis_color; std::uint8_t text_transform; std::uint8_t white_space; std::uint8_t visibility; @@ -14958,7 +15883,9 @@ ParentStyleSnapshot snapshotParentStyle(const ComputedStyle &s) return ParentStyleSnapshot{s.text_color, s.text_alpha, static_cast(s.font_id), static_cast(s.font_size), static_cast(s.font_weight), static_cast(s.line_height), s.line_height_multiplier, - static_cast(s.text_align), static_cast(s.text_transform), + static_cast(s.text_align), static_cast(s.text_align_last), + s.text_emphasis, s.text_emphasis_color, + static_cast(s.text_transform), static_cast(s.white_space), static_cast(s.visibility), {computedBorderWidth(s, 0), computedBorderWidth(s, 1), computedBorderWidth(s, 2), computedBorderWidth(s, 3)}}; } @@ -14967,7 +15894,8 @@ bool parentStylesDiffer(const ParentStyleSnapshot &a, const ParentStyleSnapshot { return a.text_color != b.text_color || a.text_alpha != b.text_alpha || a.font_id != b.font_id || a.font_size != b.font_size || a.font_weight != b.font_weight || a.line_height != b.line_height || a.line_height_multiplier != b.line_height_multiplier || - a.text_align != b.text_align || a.text_transform != b.text_transform || + a.text_align != b.text_align || a.text_align_last != b.text_align_last || a.text_transform != b.text_transform || + a.text_emphasis != b.text_emphasis || a.text_emphasis_color != b.text_emphasis_color || a.white_space != b.white_space || a.visibility != b.visibility || a.border_widths != b.border_widths; } @@ -15029,6 +15957,18 @@ struct NodeClassSnapshot { } }; +bool classTokensTouchSiblingSelectors(int node, const NodeClassSnapshot &oldTokens, const NodeClassList ¤t) +{ + rebuildRuleIndexIfNeeded(); + if (!g_ruleIndex.hasSiblingRules) return false; + if (g_ruleIndex.siblingTags.contains(treeState().nodes[node].tag_id)) return true; + for (std::size_t i = 0; i < oldTokens.count; ++i) + if (g_ruleIndex.siblingClasses.contains(oldTokens.at(i))) return true; + for (std::size_t i = 0, n = current.size(); i < n; ++i) + if (g_ruleIndex.siblingClasses.contains(current.at(i))) return true; + return false; +} + bool classTokensTouchAncestorSelectors(const NodeClassSnapshot &oldTokens, const NodeClassList ¤t) { if (g_ruleIndex.ancestorClasses.empty()) return false; @@ -15163,10 +16103,14 @@ void snapshotCustomProperties(const NodeRareData *rareData, CustomPropertySnapsh void noteClassMutationForIncremental(int node, const NodeClassSnapshot &oldTokens) { - if (!g_styleMountBatchActive) return; - rebuildRuleIndexIfNeeded(); const auto &state = treeState(); if (node < 0 || node >= state.nodeCount) return; + // A + or ~ selector keyed on the old or new classes (or the tag) restyles + // the siblings after this node. + if (classTokensTouchSiblingSelectors(node, oldTokens, state.classLists[node])) + StyleSheet::instance().recomputeSiblingsFrom(state.nodes[node].next_sibling); + if (!g_styleMountBatchActive) return; + rebuildRuleIndexIfNeeded(); if (classTokensTouchAncestorSelectors(oldTokens, state.classLists[node])) g_forceFullSubtreeMark.insert(node); } @@ -16142,15 +17086,59 @@ bool StyleValues::hasTextBackgroundClip(const ComputedStyle &style) return std::find(list.begin(), list.end(), 3) != list.end(); } -int StyleValues::backgroundClip(const ComputedStyle &style, int layer) +const BorderImageSource *StyleValues::borderImageSource(const ComputedStyle &style) { - const auto handle = rstyle(style).bg_clip; - const auto &lists = backgroundClipLists(); + const auto handle = rstyle(style).border_image_source; + const auto &list = borderImageSources(); + return handle && handle <= list.size() ? &list[handle - 1] : nullptr; +} + +BorderImageSides StyleValues::borderImageSides(const ComputedStyle &style, int which) +{ + const auto &r = rstyle(style); + const auto handle = which == 0 ? r.border_image_slice : which == 1 ? r.border_image_width : r.border_image_outset; + const auto &list = borderImageSideLists(); + return handle && handle <= list.size() ? list[handle - 1] : initialBorderImageSides(which); +} + +int StyleValues::borderImageOutsetExtent(const ComputedStyle &style) +{ + if (!rstyle(style).border_image_source) return 0; + const auto outset = borderImageSides(style, 2); + float extent = 0.0f; + for (int i = 0; i < 4; ++i) + extent = std::max(extent, outset.kind[i] == 1 ? outset.value[i] : outset.value[i] * computedBorderWidth(style, i)); + return static_cast(std::ceil(extent)); +} + +int StyleValues::backgroundBlendMode(const ComputedStyle &style, int layer) +{ + const auto handle = rstyle(style).bg_blend; + const auto &lists = backgroundBlendLists(); if (!handle || handle > lists.size()) return 0; const auto &list = lists[handle - 1]; return list[std::max(0, layer) % list.size()]; } +int StyleValues::backgroundClip(const ComputedStyle &style, int layer) +{ + const auto handle = rstyle(style).bg_clip; + const auto &lists = backgroundClipLists(); + int clip = 0; + if (handle && handle <= lists.size()) { + const auto &list = lists[handle - 1]; + clip = list[std::max(0, layer) % list.size()]; + } + // A local background scrolls with a scroll container's contents, so its + // painting area is the scrollport: border-box clipping acts as padding-box. + if (clip != 0 || !overflowEstablishesContext(style)) return clip; + const int attachments = rstyle(style).bg_attachment_list; + const auto &placements = backgroundPlacementLists(); + if (attachments < 0 || attachments >= static_cast(placements.size()) || placements[attachments].empty()) return clip; + const auto &list = placements[attachments]; + return list[std::max(0, layer) % list.size()].a == 2 ? 1 : clip; +} + BackgroundPlacement StyleValues::backgroundPlacement(const ComputedStyle &style, int nodeId, int layer, int x, int y, int width, int height) { @@ -16164,19 +17152,21 @@ BackgroundPlacement StyleValues::backgroundPlacement(const ComputedStyle &style, if (const auto *v = entry(r.bg_attachment_list)) out.attachment = v->a; if (const auto *v = entry(r.bg_origin_list)) out.origin = v->a; if (const auto *v = entry(r.bg_repeat_list)) { out.repeatX = v->a; out.repeatY = v->b; } - const Node &node = treeState().nodes[nodeId]; + const auto &state = treeState(); + // Detached boxes (nodeIndex() == -1) have no scroll or edge state to read. + const Node *node = nodeId >= 0 && nodeId < state.nodeCount ? &state.nodes[nodeId] : nullptr; if (out.attachment == 1) { - x = y = 0; width = treeState().mountedWidth; height = treeState().mountedHeight; - } else { + x = y = 0; width = state.mountedWidth; height = state.mountedHeight; + } else if (node) { if (out.attachment == 2) { - width = std::max(width, node.layout.scroll_content_width); - height = std::max(height, node.layout.scroll_content_height); - x -= node.layout.scroll_x; y -= node.layout.scroll_y; + width = std::max(width, node->layout.scroll_content_width); + height = std::max(height, node->layout.scroll_content_height); + x -= node->layout.scroll_x; y -= node->layout.scroll_y; } if (out.origin != 0) { int inset[4]; - for (int i = 0; i < 4; ++i) inset[i] = std::max(node.style.border_width, rstyle(node.style).border_side_width[i]) + - (out.origin == 2 ? std::max(0, node.style.padding[i]) : 0); + for (int i = 0; i < 4; ++i) inset[i] = std::max(node->style.border_width, rstyle(node->style).border_side_width[i]) + + (out.origin == 2 ? std::max(0, node->style.padding[i]) : 0); x += inset[3]; y += inset[0]; width -= inset[1]+inset[3]; height -= inset[0]+inset[2]; } } @@ -17011,6 +18001,15 @@ void StyleSheet::recomputeSubtree(int nodeId) const recomputeSubtreeClassStyles(nodeId); } +void StyleSheet::recomputeSiblingsFrom(int nodeId) const +{ + rebuildRuleIndexIfNeeded(); + if (!g_ruleIndex.hasSiblingRules) return; + auto &state = treeState(); + for (int sibling = nodeId; sibling >= 0 && sibling < state.nodeCount; sibling = state.nodes[sibling].next_sibling) + recomputeSubtreeClassStyles(sibling); +} + void StyleSheet::startCssAnimations(std::uint32_t nowMs) const { rebuildRuleIndexIfNeeded(); diff --git a/packages/engine/ui/style.h b/packages/engine/ui/style.h index aa1f967..4edeb6d 100644 --- a/packages/engine/ui/style.h +++ b/packages/engine/ui/style.h @@ -199,9 +199,40 @@ enum class Property : int { BorderBottomRelief, BorderLeftRelief, BorderRelief, + TextAlignLast, + MaxLines, + LineClampContinue, + BlockEllipsis, + ColumnCountSet, + ColumnWidthSet, + VerticalAlign, + BlockEllipsisString, + ColumnCount, + ColumnWidth, + ColumnFillAuto, + LineClampDiscard, + ColumnSpanAll, + TextEmphasisStyle, + TextEmphasisPosition, + TextEmphasisColorMode, + TextEmphasisColor, + BackgroundBlendMode, + BorderImageSource, + BorderImageSlice, + BorderImageWidth, + BorderImageOutset, + BorderImageRepeat, Count }; +// Property -> bit in RareStyle::line_clamp_flags. +inline int lineClampFlagBit(Property property) +{ + return property == Property::LineClampContinue ? 1 : property == Property::BlockEllipsis ? 2 : + property == Property::ColumnCountSet ? 4 : property == Property::ColumnWidthSet ? 8 : + property == Property::ColumnFillAuto ? 16 : property == Property::LineClampDiscard ? 32 : 64; +} + enum class StyleDeclaration : std::uint8_t { Unknown, Ignored, @@ -331,7 +362,50 @@ enum class StyleDeclaration : std::uint8_t { BackgroundRepeat, BackgroundAttachment, BackgroundOrigin, - MarginTrim + MarginTrim, + TextAlignLast, + LineClamp, + WebkitLineClamp, + MaxLines, + Continue, + BlockEllipsis, + Columns, + ColumnCount, + ColumnWidth, + ColumnFill, + ColumnSpan, + BorderStyle, + BorderTopStyle, + BorderRightStyle, + BorderBottomStyle, + BorderLeftStyle, + TextEmphasis, + TextEmphasisStyleDeclaration, + TextEmphasisColorDeclaration, + TextEmphasisPositionDeclaration, + BackgroundBlendMode, + BorderImage, + BorderImageSourceDeclaration, + BorderImageSliceDeclaration, + BorderImageWidthDeclaration, + BorderImageOutsetDeclaration, + BorderImageRepeatDeclaration, + // Logical two-sided shorthands (see logicalPairFor in style.cpp). + MarginInline, + MarginBlock, + PaddingInline, + PaddingBlock, + InsetInline, + InsetBlock, + BorderInline, + BorderBlock, + BorderInlineWidth, + BorderBlockWidth, + BorderInlineColor, + BorderBlockColor, + BorderInlineStyle, + BorderBlockStyle, + VerticalAlign }; class Style { @@ -394,6 +468,8 @@ struct StaticStyleSimpleSelectorSpec { struct StaticStyleSelectorPartSpec { StaticStyleSimpleSelectorSpec simple; bool directParent = false; + // Relation to the part on its left: 1 next sibling (+), 2 any earlier sibling (~). + std::uint8_t sibling = 0; }; enum class StaticStyleMediaFeatureKind : std::uint8_t { @@ -847,6 +923,8 @@ class StyleSheet { bool applyNumberProperty(NodeHandle node, const char *property, double value) const; bool removeProperty(NodeHandle node, const std::string &property) const; void recomputeSubtree(int nodeId) const; + // Restyles nodeId and the siblings after it when a + or ~ selector exists. + void recomputeSiblingsFrom(int nodeId) const; void hoverChanged() const; void startCssAnimations(std::uint32_t nowMs) const; diff --git a/packages/engine/ui/style_values.h b/packages/engine/ui/style_values.h index 50b2bc0..4ce2d60 100644 --- a/packages/engine/ui/style_values.h +++ b/packages/engine/ui/style_values.h @@ -8,6 +8,20 @@ namespace gea::embedded::ui { struct ComputedStyle; +// A border-image-source that paints: a linear-gradient(). +struct BorderImageSource { + gea::framework::graphics::pixel::native_t from, mid, to; + std::uint8_t fromAlpha, midAlpha, toAlpha, hasMid; + std::uint16_t midStop, toStop; + std::int16_t angle; +}; +// border-image-slice, -width or -outset for top, right, bottom, left. kind 0: +// number, 1: device px, 2: percentage, 3: auto. +struct BorderImageSides { + float value[4] = {}; + std::uint8_t kind[4] = {}; + std::uint8_t fill = 0; +}; struct BackgroundPlacement { int x, y, width, height; int repeatX = 0, repeatY = 0; // repeat, no-repeat, round, space @@ -22,7 +36,17 @@ class StyleValues { static bool applyBackgroundImage(ComputedStyle &style, int handle, int nodeId); // 0: border-box, 1: padding-box, 2: content-box. Lists repeat by image layer. static int backgroundClip(const ComputedStyle &style, int layer); + // 0 normal, then multiply, screen, overlay, darken, lighten, color-dodge, + // color-burn, hard-light, soft-light, difference, exclusion, hue, + // saturation, color, luminosity. + static int backgroundBlendMode(const ComputedStyle &style, int layer); static bool hasTextBackgroundClip(const ComputedStyle &style); + // Null when border-image-source is none or an image Gea cannot paint. + static const BorderImageSource *borderImageSource(const ComputedStyle &style); + // 0: slice, 1: width, 2: outset. + static BorderImageSides borderImageSides(const ComputedStyle &style, int which); + // How far the border image reaches outside the border box, in px. + static int borderImageOutsetExtent(const ComputedStyle &style); static BackgroundPlacement backgroundPlacement(const ComputedStyle &style, int nodeId, int layer, int x, int y, int width, int height); // Convert a raw style-value colour int into this board's native pixel, applied diff --git a/packages/engine/ui/text.cpp b/packages/engine/ui/text.cpp index 7d702f4..7942032 100644 --- a/packages/engine/ui/text.cpp +++ b/packages/engine/ui/text.cpp @@ -4,6 +4,7 @@ #include "graphics/font.h" #include "refresh_perf.h" #include "tree_state.h" +#include "css_atom.h" #include #include @@ -732,7 +733,7 @@ bool recordProjectedRasterText(const Node &node, const char *text, uint8_t /*par const int fontLineHeight = font.lineHeight(); const int lineAdvance = node.style.line_height > 0 ? node.style.line_height : fontLineHeight; const int lineBoxOffset = node.style.line_height > 0 ? (lineAdvance - fontLineHeight) / 2 : 0; - const int srcX = tx + alignedOffset(node.style.text_align, tw, lineWidth); + const int srcX = tx + alignedOffset(LayoutEngine::physicalTextAlign(node), tw, lineWidth); const int srcY = ty + lineBoxOffset; const int srcW = lineWidth; const int srcH = fontLineHeight; @@ -1076,7 +1077,8 @@ class TextDrawer { command.text.color, command.text.scale, command.text.align, command.text.containerWidth, command.text.fontId, command.text.textTransform, command.text.lineHeight, command.text.whiteSpace, command.text.textOverflow, command.text.maxHeight, - command.text.firstLineIndent, &sink); + command.text.firstLineIndent, command.text.alignLast, command.text.lineLimit, + command.text.blockEllipsis, command.text.ellipsisWidth, command.text.emphasis, command.text.emphasisColor, &sink); } #ifdef GEA_EMBEDDED_HAS_GENERATED_FONTS @@ -1126,13 +1128,19 @@ class TextDrawer { } } - static void drawWrapped(const char *text, int x, int y, int maxWidth, gea::framework::graphics::pixel::native_t color, float scale, int textAlign, int containerWidth, int fontId, int textTransform, int lineHeight, int whiteSpace, int textOverflow, int maxHeight = 0, int firstLineIndent = 0, TextCoverageSink *coverageSink = nullptr) + static void drawWrapped(const char *text, int x, int y, int maxWidth, gea::framework::graphics::pixel::native_t color, float scale, int textAlign, int containerWidth, int fontId, int textTransform, int lineHeight, int whiteSpace, int textOverflow, int maxHeight = 0, int firstLineIndent = 0, int alignLast = -1, int lineLimit = 0, int blockEllipsis = 0, int ellipsisWidth = 0, int emphasis = 0, gea::framework::graphics::pixel::native_t emphasisColor = 0, TextCoverageSink *coverageSink = nullptr) { if (!text || !text[0]) return; std::string transformed; text = textWithTransform(text, textTransform, transformed, whiteSpace); // Pre preserves forced breaks and paints every line without soft wrap. if (whiteSpace == 2) maxWidth = 32767; + const BlockEllipsis ellipsis{blockEllipsis == kAutoBlockEllipsis ? "\xE2\x80\xA6" : blockEllipsis ? cssAtomText(static_cast(blockEllipsis)) : nullptr, + blockEllipsis == kAutoBlockEllipsis, ellipsisWidth, (emphasis & 7) ? emphasis : 0, emphasisColor}; + // The ellipsis line and emphasis marks go through the wrapped painters; + // nowrap text is one line there. + const bool lineExtras = ellipsis.text || ellipsis.emphasis; + if (lineExtras && whiteSpace == 1) maxWidth = 32767; int clipX0; int clipY0; @@ -1149,18 +1157,21 @@ class TextDrawer { coverageSink = &clippedSink; } - const bool noWrap = whiteSpace == 1; - const bool ellipsis = textOverflow == 1; + const bool noWrap = whiteSpace == 1 && !lineExtras; + const bool overflowEllipsis = textOverflow == 1; + // A nowrap run is one line, so only a paragraph end can realign it. + const int singleLineAlign = alignLast >= 0 && (alignLast & 4) ? alignLast & 3 : textAlign; + const bool hangSpaces = whiteSpace == 0 || whiteSpace == 4; #ifdef GEA_EMBEDDED_HAS_GENERATED_FONTS if (fontId >= 0) { const int fontSize = static_cast(scale * kBitmapFontHeight + 0.5f); if (gea::framework::graphics::FontRegistry::rasterizedFamily(fontId, fontSize).valid()) { if (noWrap) - drawRasterizedSingleLine(text, x, y, maxWidth, color, textAlign, containerWidth, fontId, fontSize, lineHeight, ellipsis, clipX0, clipY0, clipX1, clipY1, coverageSink); + drawRasterizedSingleLine(text, x, y, maxWidth, color, singleLineAlign, containerWidth, fontId, fontSize, lineHeight, overflowEllipsis, clipX0, clipY0, clipX1, clipY1, coverageSink); else drawRasterizedWrapped(text, x, y, maxWidth, color, textAlign, containerWidth, fontId, fontSize, lineHeight, clipX0, clipY0, clipX1, clipY1, - ellipsis ? maxHeight : 0, firstLineIndent, coverageSink); + overflowEllipsis ? maxHeight : 0, firstLineIndent, alignLast, hangSpaces, lineLimit, ellipsis, coverageSink); return; } } @@ -1168,14 +1179,147 @@ class TextDrawer { (void)fontId; #endif if (noWrap) - drawBitmapSingleLine(text, x, y, maxWidth, color, scale, textAlign, containerWidth, lineHeight, ellipsis, clipX0, clipY0, clipX1, clipY1, coverageSink); + drawBitmapSingleLine(text, x, y, maxWidth, color, scale, singleLineAlign, containerWidth, lineHeight, overflowEllipsis, clipX0, clipY0, clipX1, clipY1, coverageSink); else - drawBitmapWrapped(text, x, y, maxWidth, color, scale, textAlign, containerWidth, lineHeight, clipX0, clipY0, clipX1, clipY1, firstLineIndent, coverageSink); + drawBitmapWrapped(text, x, y, maxWidth, color, scale, textAlign, containerWidth, lineHeight, clipX0, clipY0, clipX1, clipY1, firstLineIndent, alignLast, hangSpaces, lineLimit, ellipsis, coverageSink); } private: + // CSS block-ellipsis for the last painted line of a clamped block: the + // string, whether it is the default one, and the room from the run's x to + // the end of its line box. + struct BlockEllipsis { + const char *text; + bool automatic; + int width; + // Also carries the text-emphasis marks of every painted line. + int emphasis; + gea::framework::graphics::pixel::native_t emphasisColor; + }; + + // One text-emphasis mark centred at (cx, cy): rows of spans, painted or + // added to the coverage sink of a text-clipped background. + static void fillEmphasisMark(int shape, bool open, int cx, int cy, int r, gea::framework::graphics::pixel::native_t color, + const TextCoverageSink *sink) + { + const auto span = [&](int y, int x0, int x1) { + if (x1 < x0) return; + if (sink) { + for (int x = x0; x <= x1; ++x) sink->add(x, y, 255); + return; + } + gea::platform::display::Display::fillRect(x0, y, x1 - x0 + 1, 1, color); + }; + const int top = cy - r, bottom = shape == 4 ? cy + r / 2 : cy + r; + for (int y = top; y <= bottom; ++y) { + const float dy = static_cast(y) + 0.5f - static_cast(cy); + if (shape == 4) { + // A triangle pointing up, as wide at its base as it is tall. + const int half = static_cast(static_cast(y - top + 1) * static_cast(r) / static_cast(bottom - top + 1)); + span(y, cx - half, cx + half - 1); + continue; + } + const float rx = shape == 5 ? static_cast(r) * 0.5f : static_cast(r); + const float ry = static_cast(r); + const float inside = 1.0f - (dy * dy) / (ry * ry); + if (inside < 0.0f) continue; + const int half = static_cast(std::lround(rx * std::sqrt(inside))); + const int ring = shape == 3 || open ? std::max(1, r / 3) : 0; + const float innerR = static_cast(r - ring); + const float innerInside = innerR > 0.0f ? 1.0f - (dy * dy) / (innerR * innerR) : -1.0f; + if (ring && innerInside >= 0.0f) { + const int innerHalf = static_cast(std::lround(innerR * (shape == 5 ? 0.5f : 1.0f) * std::sqrt(innerInside))); + span(y, cx - half, cx - innerHalf - 1); + span(y, cx + innerHalf, cx + half - 1); + // A double circle also has a filled dot inside its ring. + const float dot = static_cast(r) * 0.45f; + const float dotInside = shape == 3 ? 1.0f - (dy * dy) / (dot * dot) : -1.0f; + if (dotInside >= 0.0f) { + const int dotHalf = static_cast(std::lround(dot * std::sqrt(dotInside))); + span(y, cx - dotHalf, cx + dotHalf - 1); + } + } else { + span(y, cx - half, cx + half - 1); + } + } + } + + // CSS text-emphasis: one mark per character that is not a space or + // punctuation, centred on its advance, over or under the glyph box. + template + static void drawEmphasisMarks(const char *line, int bytes, int x, int lineBoxY, int glyphHeight, int fontSize, + const BlockEllipsis &extras, const Advance &advanceOf, const TextCoverageSink *sink) + { + const int shape = extras.emphasis & 7; + const bool transparent = (extras.emphasis & 0x60) == 0x40; + if (!shape || (transparent && !sink)) return; + const int r = std::max(1, (shape == 2 || shape == 3 ? 17 : 9) * fontSize / 100); + const int gap = std::max(1, fontSize / 10); + const int cy = (extras.emphasis & 0x10) ? lineBoxY + glyphHeight + gap + r : lineBoxY - gap - r; + int penX = x; + for (const char *p = line, *end = line + bytes; p < end && *p;) { + const int cp = nextUtf8Codepoint(p); + const int advance = advanceOf(cp); + const bool punctuation = cp < 128 && cp > ' ' && std::strchr("!\"#%&'()*,-./:;?@[\\]_{}", cp); + if (cp > ' ' && cp != 0xA0 && !punctuation) + fillEmphasisMark(shape, (extras.emphasis & 8) != 0, penX + advance / 2, cy, r, extras.emphasisColor, sink); + penX += advance; + } + } + + // Keeps the leading glyphs of a line that leave room for the ellipsis, + // drops the spaces that would end them, then appends it. Returns the width + // of what it wrote to `out` (NUL-terminated). + template + static int ellipsizedLine(const char *line, int bytes, int budget, const char *ellipsis, const Advance &advanceOf, + char *out, int capacity) + { + int ellipsisWidth = 0; + for (const char *p = ellipsis; *p;) ellipsisWidth += advanceOf(nextUtf8Codepoint(p)); + const int ellipsisBytes = static_cast(std::strlen(ellipsis)); + int len = 0, width = 0; + int keptLen = 0, keptWidth = 0; + for (const char *p = line, *end = line + bytes; p < end;) { + const char *glyphStart = p; + const int cp = nextUtf8Codepoint(p); + const int advance = advanceOf(cp); + const int glyphBytes = static_cast(p - glyphStart); + if (width + advance + ellipsisWidth > budget || len + glyphBytes + ellipsisBytes >= capacity) break; + std::memcpy(out + len, glyphStart, glyphBytes); + len += glyphBytes; + width += advance; + if (cp != ' ') { keptLen = len; keptWidth = width; } + } + if (keptLen + ellipsisBytes < capacity) { + std::memcpy(out + keptLen, ellipsis, ellipsisBytes); + keptLen += ellipsisBytes; + keptWidth += ellipsisWidth; + } + out[keptLen] = '\0'; + return keptWidth; + } + #ifdef GEA_EMBEDDED_HAS_GENERATED_FONTS - static void drawRasterizedWrapped(const char *text, int x, int y, int maxWidth, std::uint16_t color, int textAlign, int containerWidth, int fontId, int fontSize, int lineHeight, int clipX0, int clipY0, int clipX1, int clipY1, int ellipsisMaxHeight = 0, int firstLineIndent = 0, const TextCoverageSink *coverageSink = nullptr) + static void drawRasterizedEllipsis(const char *line, int bytes, int x, int lineBoxY, int budget, int align, const BlockEllipsis &ellipsis, + std::uint16_t color, int fontId, int fontSize, const gea::framework::graphics::RasterizedFont &font, + const TextCoverageSink *coverageSink) + { + const auto advanceOf = [&font](int cp) { + gea::framework::graphics::Glyph glyph{}; + return font.glyph(cp, &glyph) ? glyph.advance : (font.sizePx() / 2); + }; + // A font without U+2026 substitutes '?'; the default is then "...". + gea::framework::graphics::Glyph glyph{}; + const bool horizontalEllipsis = font.glyph(0x2026, &glyph) && glyph.codepoint == 0x2026; + const char *text = ellipsis.automatic && !horizontalEllipsis ? "..." : ellipsis.text; + char buf[kWrappedLineBufferBytes + 1]; + const int width = ellipsizedLine(line, bytes, budget, text, advanceOf, buf, static_cast(sizeof(buf))); + const int lineX = x + alignedOffset(align, budget, width); + if (coverageSink) rasterizedGlyphRun(buf, lineX, lineBoxY, fontId, fontSize, *coverageSink); + else gea::platform::display::Display::drawTextFontFamily(buf, lineX, lineBoxY, color, fontId, fontSize); + } + + static void drawRasterizedWrapped(const char *text, int x, int y, int maxWidth, std::uint16_t color, int textAlign, int containerWidth, int fontId, int fontSize, int lineHeight, int clipX0, int clipY0, int clipX1, int clipY1, int ellipsisMaxHeight = 0, int firstLineIndent = 0, int alignLast = -1, bool hangSpaces = false, int lineLimit = 0, const BlockEllipsis &ellipsis = {}, const TextCoverageSink *coverageSink = nullptr) { // Inline continuation: the run's first line starts `firstLineIndent` px in // from the box's left edge (the pen position it inherited from the box @@ -1187,12 +1331,23 @@ class TextDrawer { return budget < 1 ? 1 : budget; }; const auto lineOriginX_ = [&](int li) { return li == 0 ? x + firstLineIndent : x; }; + // The trimmed leading space still sits in line 0's width; aligning must skip it. + const auto trimmedIndent_ = [&](int li) { return li == 0 && firstLineIndent < 0 ? -firstLineIndent : 0; }; gea::framework::graphics::RasterizedFont font = gea::framework::graphics::FontRegistry::rasterizedFamily(fontId, fontSize); if (!font.valid()) return; const int glyphHeight = font.lineHeight(); const int lineAdvance = lineHeight > 0 ? lineHeight : glyphHeight; const int lineBoxOffset = lineHeight > 0 ? (lineAdvance - glyphHeight) / 2 : 0; + gea::framework::graphics::Glyph spaceGlyph{}; + const int spaceAdvance = font.glyph(' ', &spaceGlyph) ? spaceGlyph.advance : font.sizePx() / 2; + const auto glyphAdvance = [&font](int cp) { + gea::framework::graphics::Glyph glyph{}; + return font.glyph(cp, &glyph) ? glyph.advance : (font.sizePx() / 2); + }; + // Emphasis marks reach past a line's glyph box, which a row clip must include. + const int markAbove = (ellipsis.emphasis & 7) && !(ellipsis.emphasis & 0x10) ? fontSize : 0; + const int markBelow = (ellipsis.emphasis & 7) && (ellipsis.emphasis & 0x10) ? fontSize : 0; // Multi-line clamp (text-overflow: ellipsis on WRAPPED text): the content // box caps how many lines draw; the last budgeted line renders through // the single-line ellipsis routine when more text would follow it. @@ -1248,8 +1403,16 @@ class TextDrawer { if (entry.textPtr == text) { int penY = y; const char *lineStart = text; + const int lastLine = (lineLimit > 0 && lineLimit < entry.lineCount ? lineLimit : entry.lineCount) - 1; for (int li = 0; li < entry.lineCount; li++) { - if (penY + lineBoxOffset > clipY1) break; + if (penY + lineBoxOffset - markAbove > clipY1 || (lineLimit > 0 && li >= lineLimit)) break; + if (ellipsis.text && li == lastLine) { + if (penY + lineBoxOffset + glyphHeight - 1 >= clipY0) + drawRasterizedEllipsis(lineStart, entry.renderBytes[li], lineOriginX_(li), penY + lineBoxOffset, + ellipsis.width - (li == 0 ? firstLineIndent : 0), textAlign, ellipsis, + color, fontId, fontSize, font, coverageSink); + return; + } if (li == lineBudget - 1 && li < entry.lineCount - 1) { // Last budgeted line with more text behind it: ellipsize the rest. drawRasterizedSingleLine(lineStart, lineOriginX_(li), penY, lineBudget_(li), color, textAlign, containerWidth, @@ -1259,9 +1422,11 @@ class TextDrawer { } const int chars = entry.renderBytes[li]; const int lineBoxY = penY + lineBoxOffset; - if (lineBoxY + glyphHeight - 1 >= clipY0) { + if (lineBoxY + glyphHeight - 1 + markBelow >= clipY0) { const int lineWidth = entry.lineWidths[li]; - const int lineX = lineOriginX_(li) + alignedOffset(textAlign, containerWidth, lineWidth); + const int align = lineAlignment(textAlign, alignLast, lineStart, entry.consumedBytes[li], li == entry.lineCount - 1); + const int hanging = hangSpaces && align ? hangingSpaceWidth(lineStart, chars, spaceAdvance) : 0; + const int lineX = lineOriginX_(li) + alignedOffset(align, containerWidth, lineWidth - hanging - trimmedIndent_(li)); if (lineX <= clipX1 && lineX + lineWidth - 1 >= clipX0) { char lineBuffer[kWrappedLineBufferBytes + 1]; const int copyLen = chars < kWrappedLineBufferBytes ? chars : kWrappedLineBufferBytes; @@ -1270,6 +1435,7 @@ class TextDrawer { if (coverageSink) rasterizedGlyphRun(lineBuffer, lineX, lineBoxY, fontId, fontSize, *coverageSink); else gea::platform::display::Display::drawTextFontFamily(lineBuffer, lineX, lineBoxY, color, fontId, fontSize); } + drawEmphasisMarks(lineStart, chars, lineX, lineBoxY, glyphHeight, fontSize, ellipsis, glyphAdvance, coverageSink); } lineStart += entry.consumedBytes[li]; penY += lineAdvance; @@ -1282,7 +1448,7 @@ class TextDrawer { int penY = y; int li = 0; while (*lineStart) { - if (penY + lineBoxOffset > clipY1) break; + if (penY + lineBoxOffset - markAbove > clipY1 || (lineLimit > 0 && li >= lineLimit)) break; if (li == lineBudget - 1) { // Budget reached: ellipsize whatever remains onto this final line. drawRasterizedSingleLine(lineStart, lineOriginX_(li), penY, lineBudget_(li), color, textAlign, containerWidth, @@ -1299,8 +1465,17 @@ class TextDrawer { }); const int lineBoxY = penY + lineBoxOffset; - if (lineBoxY + glyphHeight - 1 >= clipY0) { - const int lineX = lineOriginX_(lineIndex) + alignedOffset(textAlign, containerWidth, line.width); + if (ellipsis.text && (li == lineLimit || !lineStart[line.consumedBytes])) { + if (lineBoxY + glyphHeight - 1 >= clipY0) + drawRasterizedEllipsis(lineStart, line.renderBytes, lineOriginX_(lineIndex), lineBoxY, + ellipsis.width - (lineIndex == 0 ? firstLineIndent : 0), textAlign, ellipsis, + color, fontId, fontSize, font, coverageSink); + return; + } + if (lineBoxY + glyphHeight - 1 + markBelow >= clipY0) { + const int align = lineAlignment(textAlign, alignLast, lineStart, line.consumedBytes, !lineStart[line.consumedBytes]); + const int hanging = hangSpaces && align ? hangingSpaceWidth(lineStart, line.renderBytes, spaceAdvance) : 0; + const int lineX = lineOriginX_(lineIndex) + alignedOffset(align, containerWidth, line.width - hanging - trimmedIndent_(lineIndex)); if (lineX <= clipX1 && lineX + line.width - 1 >= clipX0) { char lineBuffer[kWrappedLineBufferBytes + 1]; const int copyLen = line.renderBytes < kWrappedLineBufferBytes ? line.renderBytes : kWrappedLineBufferBytes; @@ -1309,6 +1484,7 @@ class TextDrawer { if (coverageSink) rasterizedGlyphRun(lineBuffer, lineX, lineBoxY, fontId, fontSize, *coverageSink); else gea::platform::display::Display::drawTextFontFamily(lineBuffer, lineX, lineBoxY, color, fontId, fontSize); } + drawEmphasisMarks(lineStart, line.renderBytes, lineX, lineBoxY, glyphHeight, fontSize, ellipsis, glyphAdvance, coverageSink); } lineStart += line.consumedBytes; @@ -1392,7 +1568,26 @@ class TextDrawer { } #endif - static void drawBitmapWrapped(const char *text, int x, int y, int maxWidth, std::uint16_t color, float scale, int textAlign, int containerWidth, int lineHeight, int clipX0, int clipY0, int clipX1, int clipY1, int firstLineIndent = 0, const TextCoverageSink *coverageSink = nullptr) + static void drawBitmapEllipsis(const char *line, int bytes, int x, int y, int budget, int align, const BlockEllipsis &ellipsis, + std::uint16_t color, float scale, int glyphWidth, const TextCoverageSink *coverageSink) + { + char buf[kWrappedLineBufferBytes + 1]; + // The bitmap font has no U+2026. + const int width = ellipsizedLine(line, bytes, budget, ellipsis.automatic ? "..." : ellipsis.text, + [glyphWidth](int) { return glyphWidth; }, buf, static_cast(sizeof(buf))); + int penX = x + alignedOffset(align, budget, width); + for (const char *glyph = buf; *glyph;) { + const char *glyphStart = glyph; + nextUtf8Codepoint(glyph); + char one[5] = {}; + std::memcpy(one, glyphStart, static_cast(glyph - glyphStart)); + if (coverageSink) bitmapGlyphRun(one, penX, y, scale, *coverageSink); + else gea::platform::display::Display::drawText(one, penX, y, color, scale); + penX += glyphWidth; + } + } + + static void drawBitmapWrapped(const char *text, int x, int y, int maxWidth, std::uint16_t color, float scale, int textAlign, int containerWidth, int lineHeight, int clipX0, int clipY0, int clipX1, int clipY1, int firstLineIndent = 0, int alignLast = -1, bool hangSpaces = false, int lineLimit = 0, const BlockEllipsis &ellipsis = {}, const TextCoverageSink *coverageSink = nullptr) { if (scale < 0.1f) scale = 1.0f; int glyphWidth = static_cast(kBitmapFontWidth * scale + 0.5f); @@ -1406,16 +1601,25 @@ class TextDrawer { int lineIndex = 0; while (*lineStart) { - if (penY > clipY1) break; + if (penY > clipY1 || (lineLimit > 0 && lineIndex >= lineLimit)) break; // See drawRasterizedWrapped: line 0 of an inline continuation starts at // the inherited pen and has that much less room. const int budget = lineIndex == 0 ? maxWidth - firstLineIndent : maxWidth; const int lineOriginX = lineIndex == 0 ? x + firstLineIndent : x; + const int trimmedIndent = lineIndex == 0 && firstLineIndent < 0 ? -firstLineIndent : 0; const WrappedLine line = nextWrappedLine(lineStart, budget < 1 ? 1 : budget, [&](int) { return glyphWidth; }); lineIndex++; + if (ellipsis.text && (lineIndex == lineLimit || !lineStart[line.consumedBytes])) { + if (penY + glyphHeight - 1 >= clipY0) + drawBitmapEllipsis(lineStart, line.renderBytes, lineOriginX, penY, ellipsis.width - (lineIndex == 1 ? firstLineIndent : 0), + textAlign, ellipsis, color, scale, glyphWidth, coverageSink); + return; + } if (penY + glyphHeight - 1 >= clipY0) { - int penX = lineOriginX + alignedOffset(textAlign, containerWidth, line.width); + const int align = lineAlignment(textAlign, alignLast, lineStart, line.consumedBytes, !lineStart[line.consumedBytes]); + const int hanging = hangSpaces && align ? hangingSpaceWidth(lineStart, line.renderBytes, glyphWidth) : 0; + int penX = lineOriginX + alignedOffset(align, containerWidth, line.width - hanging - trimmedIndent); if (penX <= clipX1 && penX + line.width - 1 >= clipX0) { const char *glyph = lineStart; const char *renderEnd = lineStart + line.renderBytes; @@ -1430,6 +1634,8 @@ class TextDrawer { penX += glyphWidth; } } + drawEmphasisMarks(lineStart, line.renderBytes, lineOriginX + alignedOffset(lineAlignment(textAlign, alignLast, lineStart, line.consumedBytes, !lineStart[line.consumedBytes]), containerWidth, line.width), + penY, glyphHeight, glyphHeight, ellipsis, [glyphWidth](int) { return glyphWidth; }, coverageSink); } lineStart += line.consumedBytes; @@ -1507,6 +1713,23 @@ class TextDrawer { else if (textAlign == 2) offset = containerWidth - lineWidth; return offset < 0 ? 0 : offset; } + + // text-align-last aligns every line a forced break ends and, when the run + // closes its paragraph, the run's final line (see DrawText::alignLast). + static int lineAlignment(int textAlign, int alignLast, const char *lineStart, int consumedBytes, bool finalLine) + { + if (alignLast < 0) return textAlign; + const bool forcedBreak = consumedBytes > 0 && lineStart[consumedBytes - 1] == '\n'; + return forcedBreak || (finalLine && (alignLast & 4)) ? alignLast & 3 : textAlign; + } + + // Collapsible spaces that end a line hang, so alignment must not count them. + static int hangingSpaceWidth(const char *line, int bytes, int spaceAdvance) + { + int width = 0; + for (; bytes > 0 && line[bytes - 1] == ' '; --bytes) width += spaceAdvance; + return width; + } }; } // namespace @@ -1593,7 +1816,7 @@ void Tree::setText(int node, const char *text) // "San Francisco" -> "Lisbon" then dirtied only the new, shorter run (or even a // sliver at the box edge), leaving the old trailing glyphs on the physical panel. const bool gid_canPartial = gid_singleLine && target.style.white_space == 1 && - target.style.text_align == 0 && gid_prefix < gid_newLen; + LayoutEngine::physicalTextAlign(target) == 0 && gid_prefix < gid_newLen; int gid_preW = 0; int gid_rightW = 0; if (gid_canPartial) { @@ -1706,6 +1929,25 @@ bool TextRenderer::canFragmentInlineRuns(const Node &node) return cached != 0; } +TextRenderer::InlineFontMetrics TextRenderer::inlineFontMetrics(const ComputedStyle &style) +{ + const int fontSize = style.font_size > 0 ? style.font_size : 16; + const auto font = gea::framework::graphics::FontRegistry::rasterizedFamily(style.font_id, fontSize); + InlineFontMetrics metrics{fontSize * 4 / 5, fontSize - fontSize * 4 / 5, fontSize / 2, fontSize, 0, 0}; + int glyphHeight = fontSize; + if (font.valid()) { + glyphHeight = font.lineHeight(); + metrics.ascent = font.ascender(); + metrics.descent = glyphHeight - metrics.ascent; + gea::framework::graphics::Glyph x{}; + if (font.glyph('x', &x) && x.bearingY > 0) metrics.xHeight = x.bearingY; + } + const int lineAdvance = style.line_height > 0 ? style.line_height : glyphHeight; + metrics.strutAscent = (lineAdvance - glyphHeight) / 2 + metrics.ascent; + metrics.strutDescent = lineAdvance - metrics.strutAscent; + return metrics; +} + int TextRenderer::baselineOffset(const Node &node, bool last) { const int fontSize = node.style.font_size > 0 ? node.style.font_size : 16; @@ -1875,9 +2117,9 @@ bool TextRenderer::remeasureContentBox(int id, bool keepBoxWidth) return true; } -void TextRenderer::drawWrapped(const char *text, int x, int y, int maxWidth, gea::framework::graphics::pixel::native_t color, float scale, int text_align, int containerWidth, int fontId, int textTransform, int lineHeight, int whiteSpace, int textOverflow, int maxHeight, int firstLineIndent) +void TextRenderer::drawWrapped(const char *text, int x, int y, int maxWidth, gea::framework::graphics::pixel::native_t color, float scale, int text_align, int containerWidth, int fontId, int textTransform, int lineHeight, int whiteSpace, int textOverflow, int maxHeight, int firstLineIndent, int alignLast, int lineLimit, int blockEllipsis, int ellipsisWidth, int emphasis, gea::framework::graphics::pixel::native_t emphasisColor) { - TextDrawer::drawWrapped(text, x, y, maxWidth, color, scale, text_align, containerWidth, fontId, textTransform, lineHeight, whiteSpace, textOverflow, maxHeight, firstLineIndent); + TextDrawer::drawWrapped(text, x, y, maxWidth, color, scale, text_align, containerWidth, fontId, textTransform, lineHeight, whiteSpace, textOverflow, maxHeight, firstLineIndent, alignLast, lineLimit, blockEllipsis, ellipsisWidth, emphasis, emphasisColor); } void TextRenderer::unionCoverageRow(const DisplayCommand &command, int screenY, int screenX, @@ -1906,6 +2148,30 @@ int TextRenderer::measureHeight(const char *text, int fontId, int fontSize, int return measured.height; } +// DrawText::blockEllipsis for a run whose last painted line ends a clamped +// block, and the room from its text origin `textX` to that line box's end. +static uint16_t blockEllipsisCommand(const Node &run, int textX, int &width) +{ + const Tree &tree = Tree::instance(); + for (int ancestor = run.parent; ancestor >= 0; ancestor = tree.node(ancestor).parent) { + const Node &box = tree.node(ancestor); + if (!(rstyle(box.style).line_clamp_flags & 1)) continue; + const int right = box.layout.x + box.layout.width - boxInset(box.style, 1); + width = std::max(0, std::min(32767, right - textX)); + return rstyle(box.style).block_ellipsis ? rstyle(box.style).block_ellipsis : kAutoBlockEllipsis; + } + return 0; +} + +// DrawText::alignLast for a run: -1 unless text-align-last moves some line. +static int8_t textAlignLastCommand(const Node &node) +{ + const int last = node.style.text_align_last - 1; + if (last < 0 || last == LayoutEngine::physicalTextAlign(node)) return -1; + const bool endsParagraph = LayoutEngine::endsFormattingLine(static_cast(&node - Tree::instance().nodes())); + return static_cast(last | (endsParagraph ? 4 : 0)); +} + void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlpha) { const Node *n = &node; @@ -1981,6 +2247,10 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph } + // Physical alignment. A line the inline layout already aligned as a whole + // draws start-aligned inside this run's box. + const bool lineAligned = (n->render.inline_baseline & 2) != 0; + const int textAlign = lineAligned ? 0 : LayoutEngine::physicalTextAlign(*n); int x = n->layout.x; int y = n->layout.y; int w = n->layout.width; @@ -2073,7 +2343,7 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph int iy1 = -1; if (rasterizedTextInkBounds(measureText, drawW, - n->style.text_align, + textAlign, containerW, n->style.font_id, commandFontSize, @@ -2145,7 +2415,7 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph // RenderState::inline_baseline. if (singleLine && !n->render.inline_baseline) { drawY += rasterizedSingleLineInkCenterOffsetY(measureText, - n->style.text_align, + textAlign, containerW, n->style.font_id, commandFontSize, @@ -2153,7 +2423,7 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph } } #endif - const int xOffset = alignedOffset(n->style.text_align, containerW, paintWidth); + const int xOffset = alignedOffset(textAlign, containerW, paintWidth); int paintX0 = drawX + xOffset; int paintY0 = drawY; int paintX1 = paintX0 + paintWidth - 1; @@ -2166,7 +2436,7 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph int iy1 = -1; if (rasterizedTextInkBounds(measureText, noSoftWrap ? 32767 : drawW, - n->style.text_align, + textAlign, containerW, n->style.font_id, commandFontSize, @@ -2208,7 +2478,7 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph int inkOff = 0; #ifdef GEA_EMBEDDED_HAS_GENERATED_FONTS const int genFonts = 1; - inkOff = rasterizedSingleLineInkCenterOffsetY(measureText, n->style.text_align, containerW, + inkOff = rasterizedSingleLineInkCenterOffsetY(measureText, textAlign, containerW, n->style.font_id, dbgFontSize, commandLineHeight); #else const int genFonts = 0; @@ -2227,6 +2497,31 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph } + const int8_t alignLast = transformed || lineAligned ? -1 : textAlignLastCommand(*n); + // The bounds above assume text-align for every line; realigned lines may + // sit anywhere across the content box. + if (alignLast >= 0) { + const int right = std::max(bx + bw, drawX + containerW + 2); + bx = std::min(bx, drawX - 2); + bw = right - bx; + } + + int ellipsisWidth = 0; + const uint16_t blockEllipsis = !transformed && (n->layout.line_clamp_hidden & 2) + ? blockEllipsisCommand(*n, n->layout.x + boxInset(n->style, 3), ellipsisWidth) : 0; + // Emphasis marks sit above (or below) the glyph box, up to half an em. + if (n->style.text_emphasis & 7) { + const int reach = n->style.font_size; + if (n->style.text_emphasis & 0x10) bh += reach; + else { by -= reach; bh += reach; } + } + // The ellipsis may reach past the run's own box to the end of its line. + if (blockEllipsis) { + const int right = std::max(bx + bw, drawX + ellipsisWidth + 2); + bx = std::min(bx, drawX - 2); + bw = right - bx; + } + const uint8_t effectiveAlpha = combineAlpha(parentAlpha, n->style.text_alpha); if (effectiveAlpha != parentAlpha) appendAlphaCommand(effectiveAlpha, bx, by, bw, bh); @@ -2242,7 +2537,7 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph cmd->text.maxWidth = drawW; cmd->text.color = n->style.text_color; cmd->text.scale = textScale; - cmd->text.align = n->style.text_align; + cmd->text.align = static_cast(textAlign); cmd->text.textTransform = n->style.text_transform; cmd->text.lineHeight = commandLineHeight; cmd->text.containerWidth = containerW; @@ -2250,6 +2545,12 @@ void GEA_TEXT_HOT_SRAM TextRenderer::record(const Node &node, uint8_t parentAlph cmd->text.whiteSpace = n->style.white_space; cmd->text.textOverflow = n->style.text_overflow; cmd->text.firstLineIndent = static_cast(inlineIndent); + cmd->text.alignLast = alignLast; + cmd->text.lineLimit = n->layout.line_clamp_lines; + cmd->text.blockEllipsis = blockEllipsis; + cmd->text.ellipsisWidth = static_cast(ellipsisWidth); + cmd->text.emphasis = (n->style.text_emphasis & 7) ? n->style.text_emphasis : 0; + cmd->text.emphasisColor = (n->style.text_emphasis & 0x60) == 0x20 ? n->style.text_emphasis_color : n->style.text_color; { const int contentH = h - boxInset(n->style, 0) - boxInset(n->style, 2); cmd->text.maxHeight = static_cast(contentH > 0 ? (contentH > 32767 ? 32767 : contentH) : 0); diff --git a/packages/engine/ui/tree_nodes.cpp b/packages/engine/ui/tree_nodes.cpp index 8ebc3a5..5be6bd7 100644 --- a/packages/engine/ui/tree_nodes.cpp +++ b/packages/engine/ui/tree_nodes.cpp @@ -506,6 +506,7 @@ void Tree::setParent(int child, int parent) if (reparentWouldCycle(state, child, parent)) return; Node *c = &state.nodes[child]; const bool localMutation = localAbsoluteLeafTreeMutation(state, child); + const int oldNext = c->next_sibling; if (c->parent >= 0) { Node *old_parent = &state.nodes[c->parent]; if (old_parent->first_child == child) old_parent->first_child = c->next_sibling; @@ -540,6 +541,8 @@ void Tree::setParent(int child, int parent) else markDisplayListDirty(); StyleSheet::instance().recomputeSubtree(child); + // Sibling selectors (+, ~) of the siblings after the old position. + StyleSheet::instance().recomputeSiblingsFrom(oldNext); } void Tree::insertBefore(int child, int parent, int reference) @@ -557,6 +560,7 @@ void Tree::insertBefore(int child, int parent, int reference) Node *c = &state.nodes[child]; const bool localMutation = localAbsoluteLeafTreeMutation(state, child); + const int oldNext = c->next_sibling; // Detach `child` from its current position first; this may mutate the // reference node's prev_sibling if they were adjacent, so re-read it after. if (c->parent >= 0) { @@ -595,6 +599,9 @@ void Tree::insertBefore(int child, int parent, int reference) else markDisplayListDirty(); StyleSheet::instance().recomputeSubtree(child); + // Sibling selectors (+, ~) of the siblings after the old and new positions. + if (oldNext != reference) StyleSheet::instance().recomputeSiblingsFrom(oldNext); + StyleSheet::instance().recomputeSiblingsFrom(reference); } void Tree::removeNode(int id) @@ -619,8 +626,12 @@ void Tree::removeNode(int id) if (p->first_child == id) p->first_child = n->next_sibling; if (p->last_child == id) p->last_child = n->prev_sibling; } + const int next = n->next_sibling; if (n->prev_sibling >= 0) state.nodes[n->prev_sibling].next_sibling = n->next_sibling; if (n->next_sibling >= 0) state.nodes[n->next_sibling].prev_sibling = n->prev_sibling; + // Sibling selectors (+, ~) of the siblings that followed it. + StyleSheet::instance().recomputeSiblingsFrom(next); + n = &state.nodes[id]; n->style.display = 1; n->parent = -1; n->first_child = -1; diff --git a/packages/engine/ui/tree_render.cpp b/packages/engine/ui/tree_render.cpp index dcc65cc..62077ee 100644 --- a/packages/engine/ui/tree_render.cpp +++ b/packages/engine/ui/tree_render.cpp @@ -7,6 +7,7 @@ #include "layout_snapshot.h" #include "refresh_perf.h" #include "root_scroll_refresh.h" +#include "style_values.h" #include "tree_state.h" #include @@ -260,7 +261,7 @@ namespace gea::embedded::ui return true; if (node.style.mask_right_fade_width > 0) return true; - if (rstyle(node.style).box_shadow_alpha > 0) + if (rstyle(node.style).box_shadow_alpha > 0 || rstyle(node.style).border_image_source) return true; return node.style.has_bg && (styleHasRoundedRasterEdge(node.style) || node.style.bg_alpha < 255); } @@ -282,7 +283,8 @@ namespace gea::embedded::ui { if (!dirtyRectNeedsRasterGuard(node)) return rect; - return expandDirtyRect(rect, 1); + // border-image-outset paints outside the border box. + return expandDirtyRect(rect, 1 + StyleValues::borderImageOutsetExtent(node.style)); } DirtyRegions::Rect dirtyRectWithRetainedMoveGuard(DirtyRegions::Rect rect) diff --git a/packages/engine/ui/tree_state.h b/packages/engine/ui/tree_state.h index 5772778..d4843de 100644 --- a/packages/engine/ui/tree_state.h +++ b/packages/engine/ui/tree_state.h @@ -99,8 +99,39 @@ struct FirstLineFragment { struct InlineStaticPosition { std::int16_t x = 0; std::int16_t y = 0; + // Block extent of the line box that holds the static position; 0 when the + // box is anchored to a line that has not been laid out yet. + std::int16_t lineHeight = 0; + // The node whose line layout recorded x and y; they are relative to it. + std::int16_t flowNode = -1; bool continuationLine = false; + // Line content precedes the static position on its line. + bool afterContent = false; + // The box's right margin edge meets x: an empty line in a right-to-left + // block. Laid-out lines keep their items in left-to-right order, so a box + // they placed starts at x. + bool anchorRight = false; + bool valid = false; +}; + +// Column boxes of a multicol container (CSS Multi-column). Layout places the +// content as one column `width` wide; painting slices it into column boxes +// `height` tall and `gap` apart, `used` of them, first to last. `discard` +// (continue: discard) drops what overflows the last column box. +// One fragment of an inline box split by blocks: its border box relative to +// the element's, and the sides it paints (bits 0-3: top, right, bottom, left). +struct InlinePart { + std::int16_t x = 0, y = 0, width = 0, height = 0; + std::uint8_t sides = 0; +}; + +struct MulticolLayout { + std::int16_t width = 0; + std::int16_t gap = 0; + std::int16_t height = 0; + std::int16_t used = 0; bool valid = false; + bool discard = false; }; struct NodeCustomProperty { @@ -176,6 +207,8 @@ struct NodeRareData { FirstLineBackground firstLineBackground; FirstLineFragment firstLineFragment; InlineStaticPosition inlineStaticPosition; + MulticolLayout multicol; + std::vector inlineParts; void clear() { @@ -192,6 +225,8 @@ struct NodeRareData { firstLineBackground = FirstLineBackground{}; firstLineFragment = FirstLineFragment{}; inlineStaticPosition = InlineStaticPosition{}; + multicol = MulticolLayout{}; + inlineParts.clear(); } }; @@ -273,6 +308,10 @@ struct TreeState { // old mode-gate regression was that re-scan, not the gate itself). A cached `true` // is NOT held here — it still re-scans per refreshSerial so a removal is seen. bool transformScanValid = false; + // A position: sticky box was placed by the last absolute-coordinate pass. + // Its offset depends on the scroll position, so scroll-only fast paths that + // shift content without that pass must stand down. + bool stickyPresent = false; // Focus + caret state for `` elements. -1 means no input is // focused. caretLastFlipMs/caretVisible drive the blink animation — diff --git a/packages/engine/ui/tree_style.cpp b/packages/engine/ui/tree_style.cpp index 0b56502..6365f6d 100644 --- a/packages/engine/ui/tree_style.cpp +++ b/packages/engine/ui/tree_style.cpp @@ -80,6 +80,18 @@ bool isLayoutProperty(Property prop) case Property::BoxSizing: case Property::Float: case Property::MarginTrim: + case Property::MaxLines: + case Property::LineClampContinue: + case Property::BlockEllipsis: + case Property::ColumnCountSet: + case Property::ColumnWidthSet: + case Property::BlockEllipsisString: + case Property::ColumnCount: + case Property::ColumnWidth: + case Property::ColumnFillAuto: + case Property::LineClampDiscard: + case Property::ColumnSpanAll: + case Property::VerticalAlign: case Property::Clear: case Property::WritingMode: case Property::Direction: @@ -286,8 +298,12 @@ void setStyleValue(Tree &tree, int node, Property prop, int value, bool recordIn // Any application of the `display` property is explicit authoring — record // it so the inline-formatting heuristic treats e.g. `display:block` on a // as block-level (stacks) rather than its default inline behaviour. - if (!n->style.display_explicit) { n->style.display_explicit = 1; changed = 1; } - if (n->style.display != value) { n->style.display = value; changed = 1; } + // Keyword bits above the box kind (flow-root) are recorded with it. + if (n->style.display_explicit != (kDisplayExplicit | (value >> kDisplayFlagShift))) { + n->style.display_explicit = kDisplayExplicit | (value >> kDisplayFlagShift); + changed = 1; + } + if (n->style.display != (value & kDisplayKindMask)) { n->style.display = value & kDisplayKindMask; changed = 1; } break; case Property::FlexDirection: if (!n->style.flex_direction_explicit) { @@ -305,6 +321,22 @@ void setStyleValue(Tree &tree, int node, Property prop, int value, bool recordIn case Property::Containment: if (rstyle(n->style).containment != value) { rstyleMut(n->style).containment = value; changed = 1; } break; case Property::FlexLineCount: if (rstyle(n->style).flex_line_count != value) { rstyleMut(n->style).flex_line_count = value; changed = 1; } break; case Property::MarginTrim: if (rstyle(n->style).margin_trim != value) { rstyleMut(n->style).margin_trim = value; changed = 1; } break; + case Property::MaxLines: if (rstyle(n->style).max_lines != value) { rstyleMut(n->style).max_lines = value; changed = 1; } break; + case Property::BlockEllipsisString: if (rstyle(n->style).block_ellipsis != value) { rstyleMut(n->style).block_ellipsis = value; changed = 1; } break; + case Property::ColumnCount: if (rstyle(n->style).column_count != value) { rstyleMut(n->style).column_count = value; changed = 1; } break; + case Property::ColumnWidth: if (rstyle(n->style).column_width != value) { rstyleMut(n->style).column_width = value; changed = 1; } break; + case Property::LineClampContinue: + case Property::BlockEllipsis: + case Property::ColumnCountSet: + case Property::ColumnWidthSet: + case Property::ColumnFillAuto: + case Property::LineClampDiscard: + case Property::ColumnSpanAll: { + const int bit = lineClampFlagBit(prop); + const int next = value ? rstyle(n->style).line_clamp_flags | bit : rstyle(n->style).line_clamp_flags & ~bit; + if (rstyle(n->style).line_clamp_flags != next) { rstyleMut(n->style).line_clamp_flags = next; changed = 1; } + break; + } case Property::Clear: if (n->style.clear_side != value) { n->style.clear_side = value; changed = 1; } break; case Property::Direction: if (n->style.direction != value) { n->style.direction = value; changed = 1; } break; case Property::WritingMode: if (n->style.writing_mode != value) { n->style.writing_mode = value; changed = 1; } break; @@ -400,29 +432,29 @@ void setStyleValue(Tree &tree, int node, Property prop, int value, bool recordIn case Property::MarginLeft: if (rstyle(n->style).margin_expression[3] >= 0) { rstyleMut(n->style).margin_expression[3] = -1; changed = 1; } if (n->style.margin[3] != value || (n->style.margin_auto & 8)) { n->style.margin[3] = value; n->style.margin_auto &= ~8; changed = 1; } break; case Property::Position: if (value == kPositionFixed) state.fixedPositionUsed = true; if (n->style.position != value) { n->style.position = value; changed = 1; } break; case Property::Top: - if (n->style.pos_offsets[0] != value || n->style.pos_offset_percent[0] != kUnset) { - n->style.pos_offsets[0] = value; + if (n->style.pos_offsets[0] != storedPositionOffset(value) || n->style.pos_offset_percent[0] != kUnset) { + n->style.pos_offsets[0] = storedPositionOffset(value); n->style.pos_offset_percent[0] = kUnset; changed = 1; } break; case Property::Right: - if (n->style.pos_offsets[1] != value || n->style.pos_offset_percent[1] != kUnset) { - n->style.pos_offsets[1] = value; + if (n->style.pos_offsets[1] != storedPositionOffset(value) || n->style.pos_offset_percent[1] != kUnset) { + n->style.pos_offsets[1] = storedPositionOffset(value); n->style.pos_offset_percent[1] = kUnset; changed = 1; } break; case Property::Bottom: - if (n->style.pos_offsets[2] != value || n->style.pos_offset_percent[2] != kUnset) { - n->style.pos_offsets[2] = value; + if (n->style.pos_offsets[2] != storedPositionOffset(value) || n->style.pos_offset_percent[2] != kUnset) { + n->style.pos_offsets[2] = storedPositionOffset(value); n->style.pos_offset_percent[2] = kUnset; changed = 1; } break; case Property::Left: - if (n->style.pos_offsets[3] != value || n->style.pos_offset_percent[3] != kUnset) { - n->style.pos_offsets[3] = value; + if (n->style.pos_offsets[3] != storedPositionOffset(value) || n->style.pos_offset_percent[3] != kUnset) { + n->style.pos_offsets[3] = storedPositionOffset(value); n->style.pos_offset_percent[3] = kUnset; changed = 1; } @@ -475,6 +507,12 @@ void setStyleValue(Tree &tree, int node, Property prop, int value, bool recordIn break; } case Property::BackgroundClip: if (rstyle(n->style).bg_clip != value) { rstyleMut(n->style).bg_clip = value; changed = 1; } break; + case Property::BackgroundBlendMode: if (rstyle(n->style).bg_blend != value) { rstyleMut(n->style).bg_blend = value; changed = 1; } break; + case Property::BorderImageSource: if (rstyle(n->style).border_image_source != value) { rstyleMut(n->style).border_image_source = value; changed = 1; } break; + case Property::BorderImageSlice: if (rstyle(n->style).border_image_slice != value) { rstyleMut(n->style).border_image_slice = value; changed = 1; } break; + case Property::BorderImageWidth: if (rstyle(n->style).border_image_width != value) { rstyleMut(n->style).border_image_width = value; changed = 1; } break; + case Property::BorderImageOutset: if (rstyle(n->style).border_image_outset != value) { rstyleMut(n->style).border_image_outset = value; changed = 1; } break; + case Property::BorderImageRepeat: if (rstyle(n->style).border_image_repeat != value) { rstyleMut(n->style).border_image_repeat = value; changed = 1; } break; case Property::BackgroundSizeList: if (rstyle(n->style).bg_size_list != value) { rstyleMut(n->style).bg_size_list = value; changed = 1; } break; case Property::BackgroundPositionList: if (rstyle(n->style).bg_position_list != value) { rstyleMut(n->style).bg_position_list = value; changed = 1; } break; case Property::BackgroundRepeatList: if (rstyle(n->style).bg_repeat_list != value) { rstyleMut(n->style).bg_repeat_list = value; changed = 1; } break; @@ -558,6 +596,8 @@ void setStyleValue(Tree &tree, int node, Property prop, int value, bool recordIn rstyleMut(n->style).border_relief[side] = static_cast(value); changed = 1; } + // border-style: none leaves the side without a border. + if (value & kBorderStyleNone) changed |= setComputedBorderWidth(n->style, side, 0, nullptr); } break; case Property::BorderRightWidth: changed |= setComputedBorderWidth(n->style, 1, value, n->parent >= 0 ? &state.nodes[n->parent].style : nullptr); break; @@ -669,6 +709,21 @@ void setStyleValue(Tree &tree, int node, Property prop, int value, bool recordIn } break; } case Property::TextAlign: if (n->style.text_align != value) { n->style.text_align = value; changed = 1; } break; + case Property::TextAlignLast: if (n->style.text_align_last != value) { n->style.text_align_last = value; changed = 1; } break; + case Property::TextEmphasisStyle: + case Property::TextEmphasisPosition: + case Property::TextEmphasisColorMode: { + const int mask = prop == Property::TextEmphasisStyle ? 0x0f : prop == Property::TextEmphasisPosition ? 0x10 : 0x60; + const int next = (n->style.text_emphasis & ~mask) | (value & mask); + if (n->style.text_emphasis != next) { n->style.text_emphasis = static_cast(next); changed = 1; } + break; + } + case Property::TextEmphasisColor: { + const auto color = StyleValues::pixelFromStyleValue(value); + if (n->style.text_emphasis_color != color) { n->style.text_emphasis_color = color; changed = 1; } + break; + } + case Property::VerticalAlign: if (n->style.vertical_align != value) { n->style.vertical_align = value; changed = 1; } break; case Property::TextDecoration: if (n->style.text_decoration != value) { n->style.text_decoration = value; changed = 1; } break; case Property::TextTransform: if (n->style.text_transform != value) { n->style.text_transform = value; changed = 1; } break; case Property::WhiteSpace: if (n->style.white_space != static_cast(value)) { n->style.white_space = static_cast(value); changed = 1; } break; diff --git a/packages/engine/ui/view.cpp b/packages/engine/ui/view.cpp index 6e9db53..37f7f27 100644 --- a/packages/engine/ui/view.cpp +++ b/packages/engine/ui/view.cpp @@ -67,8 +67,20 @@ bool borderIsSameOpaqueSolidBackground(const Node &node, uint8_t parentAlpha) combineAlpha(parentAlpha, node.style.bg_alpha) == combineAlpha(parentAlpha, borderPaintAlpha(node.style, 0)); } +// Display commands carry int16 rects. A box past that range (see +// kMaxLayoutExtent) records the part inside it; no screen reaches beyond. +void saturateRect16(int &x, int &y, int &w, int &h) +{ + const int right = std::min(x + w, 32767), bottom = std::min(y + h, 32767); + x = std::max(x, -32768); + y = std::max(y, -32768); + w = std::clamp(right - x, 0, 32767); + h = std::clamp(bottom - y, 0, 32767); +} + void appendAlphaCommand(uint8_t alpha, int bx, int by, int bw, int bh) { + saturateRect16(bx, by, bw, bh); DisplayCommand *cmd = DisplayList::instance().append(); if (!cmd) return; cmd->type = DisplayCommandType::SetAlpha; @@ -186,6 +198,8 @@ uint8_t gradientAlphaAt(const Node &node, int permille) void appendFillRectWithAlpha(int x, int y, int w, int h, uint16_t color, uint8_t alpha, uint8_t parentAlpha, int bx, int by, int bw, int bh) { + saturateRect16(x, y, w, h); + saturateRect16(bx, by, bw, bh); const uint8_t effectiveAlpha = combineAlpha(parentAlpha, alpha); if (effectiveAlpha != parentAlpha) appendAlphaCommand(effectiveAlpha, bx, by, bw, bh); DisplayCommand *cmd = DisplayList::instance().append(); @@ -201,6 +215,8 @@ void appendFillRectWithAlpha(int x, int y, int w, int h, uint16_t color, uint8_t void appendFillRectRaw(int x, int y, int w, int h, uint16_t color, int bx, int by, int bw, int bh) { + saturateRect16(x, y, w, h); + saturateRect16(bx, by, bw, bh); DisplayCommand *cmd = DisplayList::instance().append(); if (!cmd) return; cmd->type = DisplayCommandType::FillRect; @@ -283,10 +299,25 @@ void appendFillQuadStrokeSegmentRaw(const int16_t *xs, const int16_t *ys, uint16 void boundsFromCorners(const int16_t *xs, const int16_t *ys, int *x0, int *y0, int *x1, int *y1); +// background-blend-mode of `layer`. An element's layers blend as an isolated +// group, so a layer with none of the element's paint under it (its color or a +// lower layer) has a transparent backdrop and paints normally. +uint8_t backgroundLayerBlend(const Node &node, int layer) +{ + const int mode = StyleValues::backgroundBlendMode(node.style, layer); + if (!mode) return 0; + const auto &r = rstyle(node.style); + const bool backdrop = (node.style.has_bg && node.style.bg_alpha > 0) || + (node.style.bg_fill == 1 && r.bg_gradient_layer > layer) || + (r.bg_overlay_gradient && r.bg_overlay_gradient_layer > layer) || + (r.bg_radial_gradient && r.bg_radial_gradient_layer > layer); + return backdrop ? static_cast(mode) : 0; +} + DisplayCommand *appendLinearGradientRectRaw(const Node &node, int x, int y, int w, int h, uint16_t fromColor, uint16_t midColor, uint16_t toColor, uint16_t midStop, uint16_t toStop, int16_t angle, uint8_t fromAlpha, - uint8_t midAlpha, uint8_t toAlpha, uint8_t hasMid) + uint8_t midAlpha, uint8_t toAlpha, uint8_t hasMid, uint8_t blend = 0) { DisplayCommand *cmd = DisplayList::instance().append(); if (!cmd) return nullptr; @@ -313,6 +344,7 @@ DisplayCommand *appendLinearGradientRectRaw(const Node &node, int x, int y, int cmd->gradient.midAlpha = midAlpha; cmd->gradient.toAlpha = toAlpha; cmd->gradient.hasMid = hasMid; + cmd->gradient.blend = blend; return cmd; } @@ -332,7 +364,8 @@ DisplayCommand *appendLinearGradientRectRaw(const Node &node, int x, int y, int rstyle(node.style).bg_gradient_from_alpha, rstyle(node.style).bg_gradient_mid_alpha, rstyle(node.style).bg_gradient_to_alpha, - rstyle(node.style).bg_gradient_has_mid); + rstyle(node.style).bg_gradient_has_mid, + backgroundLayerBlend(node, rstyle(node.style).bg_gradient_layer)); } DisplayCommand *appendRadialGradientRectRaw(const Node &node, int x, int y, int w, int h) @@ -361,6 +394,7 @@ DisplayCommand *appendRadialGradientRectRaw(const Node &node, int x, int y, int cmd->radialGradient.stopPermille = rstyle(node.style).bg_radial_gradient_stop; cmd->radialGradient.fromAlpha = rstyle(node.style).bg_radial_gradient_from_alpha; cmd->radialGradient.toAlpha = rstyle(node.style).bg_radial_gradient_to_alpha; + cmd->radialGradient.blend = backgroundLayerBlend(node, rstyle(node.style).bg_radial_gradient_layer); return cmd; } @@ -673,9 +707,9 @@ void appendFillCircleWithAlpha(int cx, int cy, int r, gea::framework::graphics:: appendAlphaCommand(parentAlpha, x, y, d, d); } -void appendStrokeWithAlpha(const Node &node, uint8_t parentAlpha) +void appendStrokeWithAlpha(const Node &node, uint8_t parentAlpha, uint16_t color, uint8_t alpha) { - const uint8_t effectiveAlpha = combineAlpha(parentAlpha, borderPaintAlpha(node.style, 0)); + const uint8_t effectiveAlpha = combineAlpha(parentAlpha, alpha); if (effectiveAlpha != parentAlpha) appendAlphaCommand(effectiveAlpha, node.layout.x, node.layout.y, node.layout.width, node.layout.height); @@ -701,13 +735,13 @@ void appendStrokeWithAlpha(const Node &node, uint8_t parentAlpha) if (cmd->strokeRoundedRect.rx8[i] > std::min(node.layout.width, node.layout.height) * 4 || cmd->strokeRoundedRect.ry8[i] > std::min(node.layout.width, node.layout.height) * 4) cmd->strokeRoundedRect.cssRadii = 1; - cmd->strokeRoundedRect.color = borderPaintColor(node.style, 0); + cmd->strokeRoundedRect.color = color; } else { cmd->type = DisplayCommandType::StrokeRect; cmd->bx = node.layout.x; cmd->by = node.layout.y; cmd->bw = node.layout.width; cmd->bh = node.layout.height; cmd->stroke.x = node.layout.x; cmd->stroke.y = node.layout.y; cmd->stroke.w = node.layout.width; cmd->stroke.h = node.layout.height; - cmd->stroke.color = borderPaintColor(node.style, 0); + cmd->stroke.color = color; } } @@ -715,6 +749,11 @@ void appendStrokeWithAlpha(const Node &node, uint8_t parentAlpha) appendAlphaCommand(parentAlpha, node.layout.x, node.layout.y, node.layout.width, node.layout.height); } +void appendStrokeWithAlpha(const Node &node, uint8_t parentAlpha) +{ + appendStrokeWithAlpha(node, parentAlpha, borderPaintColor(node.style, 0), borderPaintAlpha(node.style, 0)); +} + void boundsFromCorners(const int16_t *xs, const int16_t *ys, int *x0, int *y0, int *x1, int *y1) { *x0 = *x1 = xs[0]; @@ -1494,7 +1533,7 @@ void appendReliefBordersWithAlpha(const Node &node, uint8_t parentAlpha) const bool transformed = ViewGeometry::hasTransformChain(node, false); for (int side = 0; side < 4; ++side) { if (widths[side] <= 0) continue; - const int relief = rstyle(node.style).border_relief[side]; + const int relief = rstyle(node.style).border_relief[side] & ~kBorderStyleNone; const int bands = relief == 1 || relief == 2 ? 2 : 1; const auto color = borderPaintColor(node.style, side); int r, g, b; pixel::unpackRgb565(color, &r, &g, &b); @@ -1528,10 +1567,74 @@ void appendReliefBordersWithAlpha(const Node &node, uint8_t parentAlpha) } } -void appendSideBordersWithAlpha(const Node &node, uint8_t parentAlpha) +void appendBorderSideWithAlpha(const Node &node, int side, uint16_t color, uint8_t alpha, uint8_t parentAlpha, bool transformed) { const int x = node.layout.x; const int y = node.layout.y; + const int w = node.layout.width; + const int h = node.layout.height; + const int borderWidth = std::max(node.style.border_width, rstyle(node.style).border_side_width[side]); + if (borderWidth <= 0) return; + int sx = x; + int sy = y; + int sw = w; + int sh = h; + if (side == 0) { + sh = std::min(borderWidth, h); + } else if (side == 1) { + sw = std::min(borderWidth, w); + sx = x + w - sw; + } else if (side == 2) { + sh = std::min(borderWidth, h); + sy = y + h - sh; + } else { + sw = std::min(borderWidth, w); + } + // Horizontal edges own the corners. Avoid compositing a translucent + // asymmetric border twice where two side rectangles would overlap. + if (side == 1 || side == 3) { + const int top = std::min(std::max(node.style.border_width, rstyle(node.style).border_side_width[0]), h); + const int bottom = std::min(std::max(node.style.border_width, rstyle(node.style).border_side_width[2]), h - top); + sy += top; + sh -= top + bottom; + } + if (sw <= 0 || sh <= 0) return; + if (transformed) { + int16_t xs[4], ys[4]; + ViewGeometry::transformRectCorners(node, false, sx, sy, sw, sh, xs, ys); + int bx0, by0, bx1, by1; + boundsFromCorners(xs, ys, &bx0, &by0, &bx1, &by1); + appendFillQuadWithAlpha(xs, + ys, + color, + alpha, + parentAlpha, + bx0, + by0, + bx1 - bx0 + 1, + by1 - by0 + 1, + sx, + sy, + sw, + sh, + true); + } else { + appendFillRectWithAlpha(sx, + sy, + sw, + sh, + color, + alpha, + parentAlpha, + sx, + sy, + sw, + sh); + } +} + +void appendSideBordersWithAlpha(const Node &node, uint8_t parentAlpha) +{ const int w = node.layout.width; const int h = node.layout.height; if (hasBorderRelief(node.style)) { appendReliefBordersWithAlpha(node, parentAlpha); return; } @@ -1540,68 +1643,30 @@ void appendSideBordersWithAlpha(const Node &node, uint8_t parentAlpha) if (w <= 0 || h <= 0 || (!hasSideBorder(node.style) && !borderColorsDiffer(node.style) && !textClippedTransform)) return; const bool transformed = ViewGeometry::hasTransformChain(node, false); - for (int side = 0; side < 4; ++side) { - const int borderWidth = std::max(node.style.border_width, rstyle(node.style).border_side_width[side]); - const auto color = borderPaintColor(node.style, side); - const auto alpha = borderPaintAlpha(node.style, side); - if (borderWidth <= 0) continue; - int sx = x; - int sy = y; - int sw = w; - int sh = h; - if (side == 0) { - sh = std::min(borderWidth, h); - } else if (side == 1) { - sw = std::min(borderWidth, w); - sx = x + w - sw; - } else if (side == 2) { - sh = std::min(borderWidth, h); - sy = y + h - sh; - } else { - sw = std::min(borderWidth, w); - } - // Horizontal edges own the corners. Avoid compositing a translucent - // asymmetric border twice where two side rectangles would overlap. - if (side == 1 || side == 3) { - const int top = std::min(std::max(node.style.border_width, rstyle(node.style).border_side_width[0]), h); - const int bottom = std::min(std::max(node.style.border_width, rstyle(node.style).border_side_width[2]), h - top); - sy += top; - sh -= top + bottom; - } - if (sw <= 0 || sh <= 0) continue; - if (transformed) { - int16_t xs[4], ys[4]; - ViewGeometry::transformRectCorners(node, false, sx, sy, sw, sh, xs, ys); - int bx0, by0, bx1, by1; - boundsFromCorners(xs, ys, &bx0, &by0, &bx1, &by1); - appendFillQuadWithAlpha(xs, - ys, - color, - alpha, - parentAlpha, - bx0, - by0, - bx1 - bx0 + 1, - by1 - by0 + 1, - sx, - sy, - sw, - sh, - true); - } else { - appendFillRectWithAlpha(sx, - sy, - sw, - sh, - color, - alpha, - parentAlpha, - sx, - sy, - sw, - sh); - } + for (int side = 0; side < 4; ++side) + appendBorderSideWithAlpha(node, side, borderPaintColor(node.style, side), borderPaintAlpha(node.style, side), parentAlpha, transformed); +} + +// background-clip: border-area paints the background only where the border +// strokes, whatever the border color. Trace the geometry the border painters +// use: one stroke for a uniform width, side rectangles otherwise. +void recordBorderAreaFill(const Node &node, uint8_t parentAlpha) +{ + const bool transformed = ViewGeometry::hasTransformChain(node, false); + if (node.style.border_width > 0 && !hasSideBorder(node.style) && !transformed) { + appendStrokeWithAlpha(node, parentAlpha, node.style.bg_color, node.style.bg_alpha); + return; } + for (int side = 0; side < 4; ++side) + appendBorderSideWithAlpha(node, side, node.style.bg_color, node.style.bg_alpha, parentAlpha, transformed); +} + +// A transformed linear-gradient face paints a uniform border as its edge frame. +bool gradientEdgeCarriesBorder(const Node &node) +{ + return !StyleValues::hasTextBackgroundClip(node.style) && node.style.border_width > 0 && !hasBorderRelief(node.style) && + !hasSideBorder(node.style) && !borderColorsDiffer(node.style) && borderPaintAlpha(node.style, 0) > 0 && + !isFullyRoundedShape(node); } void recordLinearGradientBackground(const Node &node, uint8_t parentAlpha) @@ -1661,8 +1726,7 @@ void recordLinearGradientBackground(const Node &node, uint8_t parentAlpha) // span rasterizer paints the first/last edgeWidth px of every span, tracing // the quad outline). Emitting projected stroke quads instead would disarm // the transform-reproject fast path every frame. - if (!StyleValues::hasTextBackgroundClip(node.style) && node.style.border_width > 0 && !hasBorderRelief(node.style) && !hasSideBorder(node.style) && !borderColorsDiffer(node.style) && borderPaintAlpha(node.style, 0) > 0 && - !isFullyRoundedShape(node)) { + if (gradientEdgeCarriesBorder(node)) { g.edgeColor = borderPaintColor(node.style, 0); g.edgeAlpha = borderPaintAlpha(node.style, 0); const int ew = node.style.border_width < 1 ? 1 : node.style.border_width; @@ -1709,7 +1773,8 @@ void recordOverlayLinearGradientBackground(const Node &node) rstyle(node.style).bg_overlay_gradient_from_alpha, rstyle(node.style).bg_overlay_gradient_mid_alpha, rstyle(node.style).bg_overlay_gradient_to_alpha, - rstyle(node.style).bg_overlay_gradient_has_mid); + rstyle(node.style).bg_overlay_gradient_has_mid, + backgroundLayerBlend(node, rstyle(node.style).bg_overlay_gradient_layer)); } // Background positioning and painting areas are distinct. In particular, the @@ -1746,6 +1811,7 @@ bool recordPlacedBackgrounds(const Node &geometry, const Node &source, bool canv begin->type = DisplayCommandType::PushClip; begin->bx = begin->clip.x = x; begin->by = begin->clip.y = y; begin->bw = begin->clip.w = w; begin->bh = begin->clip.h = h; begin->clip.nodeId = -1; + begin->clip.shaped = 0; struct TileAxis { double start, step; int count; }; auto axis = [](int position, int size, int mode, int area, int areaSize, int clipStart, int clipSize) -> TileAxis { if (mode == 1) return {static_cast(position), static_cast(size), 1}; @@ -1767,7 +1833,8 @@ bool recordPlacedBackgrounds(const Node &geometry, const Node &source, bool canv else paint = appendLinearGradientRectRaw(source,tx,ty,p.width,p.height, r.bg_overlay_gradient_from_color,r.bg_overlay_gradient_mid_color,r.bg_overlay_gradient_to_color, r.bg_overlay_gradient_mid_stop,r.bg_overlay_gradient_to_stop,r.bg_overlay_gradient_angle, - r.bg_overlay_gradient_from_alpha,r.bg_overlay_gradient_mid_alpha,r.bg_overlay_gradient_to_alpha,r.bg_overlay_gradient_has_mid); + r.bg_overlay_gradient_from_alpha,r.bg_overlay_gradient_mid_alpha,r.bg_overlay_gradient_to_alpha,r.bg_overlay_gradient_has_mid, + backgroundLayerBlend(source, layer)); // Radius belongs to the painting box, never to each repeated image. if (paint) { auto &cmd = *paint; @@ -1874,6 +1941,66 @@ void recordTransformedRoundedRectFill(const Node &node, uint8_t parentAlpha, int r.blRx8 = rx8[3]; r.blRy8 = ry8[3]; r.color = node.style.bg_color; r.backfaceHidden = node.style.backface_hidden ? 1 : 0; + r.ring[0] = r.ring[1] = r.ring[2] = r.ring[3] = 0; + } + + if (effectiveAlpha != parentAlpha) appendAlphaCommand(parentAlpha, bx0, by0, bx1 - bx0 + 1, by1 - by0 + 1); +} + +// A uniform border on a transformed box: one shaded ring between the border +// box and the padding box, each rounded as CSS rounds it. +void recordTransformedBorderRing(const Node &node, uint8_t parentAlpha) +{ + const int x = node.layout.x; + const int y = node.layout.y; + const int w = node.layout.width; + const int h = node.layout.height; + if (w <= 0 || h <= 0) return; + // The ring command carries its local box as int16 and each ring width as + // a byte. A box or border past that (a huge box under a tiny scale) paints + // its sides as transformed quads instead, with square corners. + int ring = 0; + for (int side = 0; side < 4; ++side) + ring = std::max({ring, node.style.border_width, rstyle(node.style).border_side_width[side]}); + if (w > 32767 || h > 32767 || ring > 255) { + for (int side = 0; side < 4; ++side) + appendBorderSideWithAlpha(node, side, borderPaintColor(node.style, side), borderPaintAlpha(node.style, side), parentAlpha, true); + return; + } + + int16_t xs[4], ys[4]; + ViewGeometry::transformRectCorners(node, false, x, y, w, h, xs, ys); + int bx0, by0, bx1, by1; + boundsFromCorners(xs, ys, &bx0, &by0, &bx1, &by1); + const uint8_t effectiveAlpha = combineAlpha(parentAlpha, borderPaintAlpha(node.style, 0)); + if (effectiveAlpha != parentAlpha) appendAlphaCommand(effectiveAlpha, bx0, by0, bx1 - bx0 + 1, by1 - by0 + 1); + int16_t rx8[4]{}; + int16_t ry8[4]{}; + resolvedBorderRadii8(node, rx8, ry8); + DisplayCommand *cmd = DisplayList::instance().append(); + if (cmd) { + cmd->type = DisplayCommandType::FillTransformedRoundedRect; + cmd->bx = bx0; + cmd->by = by0; + cmd->bw = bx1 - bx0 + 1; + cmd->bh = by1 - by0 + 1; + auto &r = cmd->transformedRoundedRect; + r.x0 = xs[0]; r.y0 = ys[0]; + r.x1 = xs[1]; r.y1 = ys[1]; + r.x2 = xs[2]; r.y2 = ys[2]; + r.x3 = xs[3]; r.y3 = ys[3]; + r.lx = static_cast(x); + r.ly = static_cast(y); + r.lw = static_cast(w); + r.lh = static_cast(h); + r.tlRx8 = rx8[0]; r.tlRy8 = ry8[0]; + r.trRx8 = rx8[1]; r.trRy8 = ry8[1]; + r.brRx8 = rx8[2]; r.brRy8 = ry8[2]; + r.blRx8 = rx8[3]; r.blRy8 = ry8[3]; + r.color = borderPaintColor(node.style, 0); + r.backfaceHidden = node.style.backface_hidden ? 1 : 0; + for (int i = 0; i < 4; ++i) + r.ring[i] = static_cast(std::clamp(std::max(node.style.border_width, rstyle(node.style).border_side_width[i]), 1, 255)); } if (effectiveAlpha != parentAlpha) appendAlphaCommand(parentAlpha, bx0, by0, bx1 - bx0 + 1, by1 - by0 + 1); @@ -2181,11 +2308,10 @@ bool shadowContourRow(const ShadowContour &shape, int y, int &left, int &right) return left <= right; } -void appendShadowRect(const Node &node, uint8_t parentAlpha, int x, int y, int w, int h) +void appendShadowRect(const Node &node, uint8_t parentAlpha, uint8_t alpha, int x, int y, int w, int h) { if (w <= 0 || h <= 0) return; const auto color = rstyle(node.style).box_shadow_color; - const auto alpha = rstyle(node.style).box_shadow_alpha; if (!ViewGeometry::hasTransformChain(node, false)) { appendFillRectWithAlpha(x, y, w, h, color, alpha, parentAlpha, x, y, w, h); return; @@ -2227,7 +2353,7 @@ void recordSharpInsetShadow(const Node &node, uint8_t parentAlpha, const ShadowC } struct Run { int left = 0, right = -1, y = 0, height = 0; } runs[2]; auto flush = [&](Run &run) { - if (run.height > 0) appendShadowRect(node, parentAlpha, run.left, run.y, run.right - run.left + 1, run.height); + if (run.height > 0) appendShadowRect(node, parentAlpha, rstyle(node.style).box_shadow_alpha, run.left, run.y, run.right - run.left + 1, run.height); run.height = 0; }; for (int y = static_cast(clip.y); y < clip.y + clip.h; ++y) { @@ -2252,6 +2378,225 @@ void recordSharpInsetShadow(const Node &node, uint8_t parentAlpha, const ShadowC for (auto &run : runs) flush(run); } +// Outer shadows start from the border box, not the padding box. +ShadowContour borderBoxShadowContour(const Node &node) +{ + ShadowContour box{float(node.layout.x), float(node.layout.y), float(node.layout.width), float(node.layout.height)}; + int16_t rx8[4], ry8[4]; + resolvedBorderRadii8(node, rx8, ry8); + for (int i = 0; i < 4; ++i) { + box.rx[i] = rx8[i] * 0.125f; + box.ry[i] = ry8[i] * 0.125f; + } + constrainShadowRadii(box); + return box; +} + +// CSS Backgrounds' outset-adjusted radius: a small radius grows by less than +// the spread, so a near-square corner stays near square, unless the radius +// already covers the whole side (a pill or a circle keeps its shape). +float outsetShadowRadius(float radius, float outset, float coverage) +{ + if (outset <= 0) return std::max(0.0f, radius + outset); + if (radius > outset || coverage > 1) return radius + outset; + const float remainder = 1 - radius / outset; + return radius + outset * (1 - remainder * remainder * remainder * (1 - coverage * coverage * coverage)); +} + +ShadowContour outerShadowShape(const ShadowContour &box, int spread, int ox, int oy) +{ + ShadowContour shape = box; + shape.x += ox - spread; + shape.y += oy - spread; + shape.w = std::max(0.0f, box.w + 2 * spread); + shape.h = std::max(0.0f, box.h + 2 * spread); + for (int i = 0; i < 4; ++i) { + const float coverage = box.w > 0 && box.h > 0 ? 2 * std::min(box.rx[i] / box.w, box.ry[i] / box.h) : 0; + shape.rx[i] = outsetShadowRadius(box.rx[i], spread, coverage); + shape.ry[i] = outsetShadowRadius(box.ry[i], spread, coverage); + } + constrainShadowRadii(shape); + return shape; +} + +// The contour `d` px outside (negative: inside) a rounded shape. +ShadowContour offsetShadowContour(const ShadowContour &shape, float d) +{ + ShadowContour out = shape; + out.x -= d; + out.y -= d; + out.w = std::max(0.0f, shape.w + 2 * d); + out.h = std::max(0.0f, shape.h + 2 * d); + for (int i = 0; i < 4; ++i) { + out.rx[i] = std::max(0.0f, shape.rx[i] + d); + out.ry[i] = std::max(0.0f, shape.ry[i] + d); + } + constrainShadowRadii(out); + return out; +} + +// Gaussian coverage `d` px outside a straight shadow edge. CSS makes the blur +// radius twice the standard deviation. +uint8_t outerShadowAlphaAt(float d, int blur, uint8_t baseAlpha) +{ + return static_cast(std::lround(baseAlpha * 0.5f * std::erfc(d * 1.41421356f / blur))); +} + +// Paints `outer` minus `hole` minus the border-box knockout, merging rows with +// equal spans into rectangles so every pixel composites once. +void appendShadowSpans(const Node &node, uint8_t parentAlpha, uint8_t alpha, const ShadowContour &outer, + const ShadowContour *hole, const ShadowContour &box) +{ + if (alpha == 0 || outer.w <= 0 || outer.h <= 0) return; + const int top = static_cast(std::floor(outer.y)); + const int bottom = static_cast(std::ceil(outer.y + outer.h)); + const int bx = static_cast(std::floor(outer.x)); + const int bw = static_cast(std::ceil(outer.x + outer.w)) - bx; + // One alpha scope for the whole contour; transformed quads carry their own. + const bool transformed = ViewGeometry::hasTransformChain(node, false); + const uint8_t effectiveAlpha = combineAlpha(parentAlpha, alpha); + const bool scoped = !transformed && effectiveAlpha != parentAlpha; + if (scoped) appendAlphaCommand(effectiveAlpha, bx, top, bw, bottom - top); + struct Run { int left = 0, right = -1, y = 0, height = 0; } runs[4]; + auto flush = [&](Run &run) { + if (run.height <= 0) return; + const int w = run.right - run.left + 1; + if (transformed) appendShadowRect(node, parentAlpha, alpha, run.left, run.y, w, run.height); + else appendFillRectRaw(run.left, run.y, w, run.height, rstyle(node.style).box_shadow_color, run.left, run.y, w, run.height); + run.height = 0; + }; + auto cut = [](int (*spans)[2], int &count, int left, int right) { + int kept[4][2], n = 0; + for (int i = 0; i < count; ++i) { + const int s0 = spans[i][0], s1 = spans[i][1]; + if (right < s0 || left > s1) { + kept[n][0] = s0; kept[n][1] = s1; ++n; + continue; + } + if (s0 < left && n < 4) { kept[n][0] = s0; kept[n][1] = left - 1; ++n; } + if (s1 > right && n < 4) { kept[n][0] = right + 1; kept[n][1] = s1; ++n; } + } + for (int i = 0; i < n; ++i) { spans[i][0] = kept[i][0]; spans[i][1] = kept[i][1]; } + count = n; + }; + for (int y = top; y < bottom; ++y) { + int spans[4][2], count = 0, left = 0, right = -1; + if (shadowContourRow(outer, y, left, right)) { spans[0][0] = left; spans[0][1] = right; count = 1; } + if (count && hole && shadowContourRow(*hole, y, left, right)) cut(spans, count, left, right); + if (count && shadowContourRow(box, y, left, right)) cut(spans, count, left, right); + for (int i = 0; i < 4; ++i) { + auto &run = runs[i]; + if (i >= count) { flush(run); continue; } + if (run.height && run.left == spans[i][0] && run.right == spans[i][1]) { ++run.height; continue; } + flush(run); + run = Run{spans[i][0], spans[i][1], y, 1}; + } + } + for (auto &run : runs) flush(run); + if (scoped) appendAlphaCommand(parentAlpha, bx, top, bw, bottom - top); +} + +// One native rounded fill (lineWidth 0) or ring for a shadow contour. +void appendShadowRoundedRect(const Node &node, uint8_t parentAlpha, uint8_t alpha, const ShadowContour &shape, int lineWidth) +{ + const int x = static_cast(shape.x), y = static_cast(shape.y); + const int w = static_cast(shape.w), h = static_cast(shape.h); + if (alpha == 0 || w <= 0 || h <= 0) return; + int16_t radii[4]; + for (int i = 0; i < 4; ++i) radii[i] = static_cast(std::lround(shape.rx[i])); + const uint8_t effectiveAlpha = combineAlpha(parentAlpha, alpha); + if (effectiveAlpha != parentAlpha) appendAlphaCommand(effectiveAlpha, x, y, w, h); + DisplayCommand *cmd = DisplayList::instance().append(); + if (cmd) { + cmd->bx = x; cmd->by = y; cmd->bw = w; cmd->bh = h; + if (lineWidth > 0) { + cmd->type = DisplayCommandType::StrokeRoundedRect; + cmd->strokeRoundedRect = {}; + auto &ring = cmd->strokeRoundedRect; + ring.x = x; ring.y = y; ring.w = w; ring.h = h; + ring.tl = radii[0]; ring.tr = radii[1]; ring.br = radii[2]; ring.bl = radii[3]; + for (int i = 0; i < 4; ++i) ring.rx8[i] = ring.ry8[i] = static_cast(radii[i] * 8); + ring.lineWidth = lineWidth; + ring.color = rstyle(node.style).box_shadow_color; + } else { + cmd->type = DisplayCommandType::FillRoundedRect; + auto &fill = cmd->fillRoundedRect; + fill.x = x; fill.y = y; fill.w = w; fill.h = h; + fill.tl = radii[0]; fill.tr = radii[1]; fill.br = radii[2]; fill.bl = radii[3]; + fill.color = rstyle(node.style).box_shadow_color; + } + } + if (effectiveAlpha != parentAlpha) appendAlphaCommand(parentAlpha, x, y, w, h); +} + +// True when `inner`'s bounding rectangle lies inside the convex `outer`: its +// four corners do, so an opaque box over `outer` hides all of `inner`. +bool shadowContourInside(const ShadowContour &inner, const ShadowContour &outer) +{ + if (inner.w <= 0 || inner.h <= 0) return true; + for (int i = 0; i < 4; ++i) { + const bool right = i == 1 || i == 2, bottom = i >= 2; + const float px = right ? inner.x + inner.w : inner.x; + const float py = bottom ? inner.y + inner.h : inner.y; + if (px < outer.x || px > outer.x + outer.w || py < outer.y || py > outer.y + outer.h) return false; + const float rx = outer.rx[i], ry = outer.ry[i]; + if (rx <= 0 || ry <= 0) continue; + const float dx = (right ? px - (outer.x + outer.w - rx) : outer.x + rx - px) / rx; + const float dy = (bottom ? py - (outer.y + outer.h - ry) : outer.y + ry - py) / ry; + if (dx > 0 && dy > 0 && dx * dx + dy * dy > 1) return false; + } + return true; +} + +// An opaque border-box background paints over whatever the shadow puts under +// the box, so the knockout can be skipped in favour of native rounded fills. +bool outerShadowKnockoutHidden(const Node &node, uint8_t parentAlpha) +{ + if (!node.style.has_bg || combineAlpha(parentAlpha, node.style.bg_alpha) != 255) return false; + if (StyleValues::backgroundClip(node.style, rstyle(node.style).bg_image_layer_count - 1) != 0) return false; + if (ViewGeometry::hasTransformChain(node, false) || isDocumentCanvasRoot(node)) return false; + const int canvasSource = ViewRenderer::canvasBackgroundSource(); + return canvasSource < 0 || &treeState().nodes[canvasSource] != &node; +} + +// Outer box-shadow: the spread shape, offset, blurred and painted only outside +// the border box. Blur is a Gaussian approximated by constant-alpha bands +// across [-blur, blur] around the shape edge. +void recordOuterBoxShadow(const Node &node, uint8_t parentAlpha) +{ + const auto &r = rstyle(node.style); + if (r.box_shadow_inset || r.box_shadow_alpha == 0) return; + if (node.layout.width <= 0 || node.layout.height <= 0) return; + const ShadowContour box = borderBoxShadowContour(node); + const ShadowContour shape = outerShadowShape(box, r.box_shadow_spread, r.box_shadow_offset_x, r.box_shadow_offset_y); + bool native = outerShadowKnockoutHidden(node, parentAlpha); + for (int i = 0; i < 4; ++i) native &= std::fabs(shape.rx[i] - shape.ry[i]) < 0.5f; + const int blur = std::max(0, r.box_shadow_blur_radius); + // Natively painted parts the opaque box fully covers are skipped: a full- + // screen card would otherwise refill its whole core on every dirty rect. + if (blur == 0) { + if (!native) appendShadowSpans(node, parentAlpha, r.box_shadow_alpha, shape, nullptr, box); + else if (!shadowContourInside(shape, box)) appendShadowRoundedRect(node, parentAlpha, r.box_shadow_alpha, shape, 0); + return; + } + // Span bands cost a command per corner row, so they get fewer, wider bands. + const int band = std::max(1, blur / (native ? 8 : 5)); + const ShadowContour core = offsetShadowContour(shape, static_cast(-blur)); + const uint8_t coreAlpha = outerShadowAlphaAt(static_cast(-blur), blur, r.box_shadow_alpha); + if (!native) appendShadowSpans(node, parentAlpha, coreAlpha, core, nullptr, box); + else if (!shadowContourInside(core, box)) appendShadowRoundedRect(node, parentAlpha, coreAlpha, core, 0); + for (int d = -blur; d < blur; d += band) { + const ShadowContour outer = offsetShadowContour(shape, static_cast(d + band)); + const uint8_t alpha = outerShadowAlphaAt(d + band * 0.5f, blur, r.box_shadow_alpha); + if (native) { + if (!shadowContourInside(outer, box)) appendShadowRoundedRect(node, parentAlpha, alpha, outer, band); + } else { + const ShadowContour inner = offsetShadowContour(shape, static_cast(d)); + appendShadowSpans(node, parentAlpha, alpha, outer, &inner, box); + } + } +} + void recordInsetBoxShadow(const Node &node, uint8_t parentAlpha) { if (!rstyle(node.style).box_shadow_inset || rstyle(node.style).box_shadow_alpha == 0) return; @@ -2320,6 +2665,12 @@ void GEA_VIEW_HOT_SRAM_SECTION("view_renderer_transformed_bounds") ViewRenderer: int rotate = use_prev ? n->render.previous_transform_rotate : rstyle(n->style).transform_rotate; auto expandForBlur = [&]() { + // An outer box-shadow paints beyond the box as well. + const int shadow = use_prev ? n->render.previous_box_shadow_extent : boxShadowExtent(n->style); + *x0 -= shadow; + *y0 -= shadow; + *x1 += shadow; + *y1 += shadow; const int radius = use_prev ? n->render.previous_filter_blur_radius : rstyle(n->style).filter_blur_radius; if (radius <= 0) return; const int extentX = std::max(1, radius) * 5; @@ -2395,23 +2746,87 @@ bool GEA_VIEW_HOT_SRAM_SECTION("view_renderer_any_transform_active") ViewRendere return ViewGeometry::anyTransformPresent(); } +bool ViewRenderer::overflowClipShape(const Node &node, OverflowClipShape &out) +{ + int x, y, w, h; + overflowClipBounds(node, x, y, w, h); + const bool transformed = ViewGeometry::hasTransformChain(node, false); + // Clip commands carry int16 rects. Past that range the saturated rect + // clips exactly as much: no screen reaches beyond it. A transformed clip + // takes its rect from the projected corners instead. + if (!transformed) { + w = std::min({w, 32767 - x, 32767}); + h = std::min({h, 32767 - y, 32767}); + } + out = OverflowClipShape{x, y, w, h, false, {}, {}, 0, 0, 0, 0, {}, {}}; + // The padding edge is rounded with the outer radii less the border widths. + const auto &s = node.style; + const int border[4] = {boxInset(s, 0) - s.padding[0], boxInset(s, 1) - s.padding[1], + boxInset(s, 2) - s.padding[2], boxInset(s, 3) - s.padding[3]}; + int16_t rx8[4]{}; + int16_t ry8[4]{}; + if (hasAnyRadius(node)) resolvedBorderRadii8(node, rx8, ry8); + bool rounded = false; + for (int i = 0; i < 4; ++i) { + out.rx8[i] = static_cast(std::max(0, rx8[i] - 8 * border[i == 0 || i == 3 ? 3 : 1])); + out.ry8[i] = static_cast(std::max(0, ry8[i] - 8 * border[i < 2 ? 0 : 2])); + rounded |= out.rx8[i] > 0 && out.ry8[i] > 0; + } + // A clip open on one axis stays a rectangle; a transformed one clips nothing. + if (!overflowX(s) || !overflowY(s) || (!transformed && !rounded)) return !transformed; + out.lx = static_cast(x); + out.ly = static_cast(y); + out.lw = static_cast(w); + out.lh = static_cast(h); + if (transformed) { + ViewGeometry::transformRectCorners(node, false, x, y, w, h, out.qx, out.qy); + // A perspective projection is not a parallelogram; keep it unclipped. + if (std::abs(out.qx[0] + out.qx[2] - out.qx[1] - out.qx[3]) > 1 || + std::abs(out.qy[0] + out.qy[2] - out.qy[1] - out.qy[3]) > 1) return false; + int bx0, by0, bx1, by1; + boundsFromCorners(out.qx, out.qy, &bx0, &by0, &bx1, &by1); + out.x = bx0; + out.y = by0; + out.w = bx1 - bx0 + 1; + out.h = by1 - by0 + 1; + } else { + const int16_t right = static_cast(x + w), bottom = static_cast(y + h); + out.qx[0] = out.qx[3] = static_cast(x); + out.qx[1] = out.qx[2] = right; + out.qy[0] = out.qy[1] = static_cast(y); + out.qy[2] = out.qy[3] = bottom; + } + out.shaped = true; + return true; +} + bool ViewRenderer::recordClipBegin(const Node &node) { const Node *n = &node; - if (ViewGeometry::hasTransformChain(*n, false)) return 0; if (!isViewLikeNodeType(n->type) || n->style.overflow == 0) return 0; if (n->first_child < 0) return 0; - int x, y, w, h; - overflowClipBounds(node, x, y, w, h); + OverflowClipShape shape; + if (!overflowClipShape(node, shape)) return 0; DisplayCommand *cmd = DisplayList::instance().append(); if (cmd) { cmd->type = DisplayCommandType::PushClip; - cmd->bx = cmd->clip.x = x; - cmd->by = cmd->clip.y = y; - cmd->bw = cmd->clip.w = w; - cmd->bh = cmd->clip.h = h; + cmd->bx = cmd->clip.x = static_cast(shape.x); + cmd->by = cmd->clip.y = static_cast(shape.y); + cmd->bw = cmd->clip.w = static_cast(shape.w); + cmd->bh = cmd->clip.h = static_cast(shape.h); cmd->clip.nodeId = static_cast(&node - Tree::instance().nodes()); + cmd->clip.shaped = shape.shaped ? 1 : 0; + for (int i = 0; i < 4; ++i) { + cmd->clip.qx[i] = shape.qx[i]; + cmd->clip.qy[i] = shape.qy[i]; + cmd->clip.rx8[i] = shape.rx8[i]; + cmd->clip.ry8[i] = shape.ry8[i]; + } + cmd->clip.lx = shape.lx; + cmd->clip.ly = shape.ly; + cmd->clip.lw = shape.lw; + cmd->clip.lh = shape.lh; } return 1; } @@ -2462,6 +2877,126 @@ int ViewRenderer::canvasBackgroundSource() return root; } +// A split inline (splitDecoratedInline in layout.cpp) paints its fragments +// instead of one box: each fragment's background colour and solid border, +// open on its split sides. +bool recordInlineParts(const Node &node, uint8_t parentAlpha) +{ + const NodeRareData *rare = rareDataFor(ViewGeometry::nodeIndex(node)); + if (!rare || rare->inlineParts.empty() || ViewGeometry::hasTransformChain(node, false)) return false; + for (const auto &part : rare->inlineParts) { + const int x = node.layout.x + part.x, y = node.layout.y + part.y, w = part.width, h = part.height; + if (w <= 0 || h <= 0) continue; + if (node.style.has_bg && node.style.bg_alpha > 0) + appendFillRectWithAlpha(x, y, w, h, node.style.bg_color, node.style.bg_alpha, parentAlpha, x, y, w, h); + // Horizontal edges own the corners, as for an unsplit box. + const int top = part.sides & 1 ? std::min(computedBorderWidth(node.style, 0), h) : 0; + const int bottom = part.sides & 4 ? std::min(computedBorderWidth(node.style, 2), h - top) : 0; + for (int side = 0; side < 4; ++side) { + const int width = computedBorderWidth(node.style, side); + if (!(part.sides & (1 << side)) || width <= 0 || borderPaintAlpha(node.style, side) == 0) continue; + int sx = x, sy = y + top, sw = std::min(width, w), sh = h - top - bottom; + if (side == 0 || side == 2) { sx = x; sw = w; sy = side ? y + h - bottom : y; sh = side ? bottom : top; } + else if (side == 1) sx = x + w - sw; + if (sw > 0 && sh > 0) + appendFillRectWithAlpha(sx, sy, sw, sh, borderPaintColor(node.style, side), borderPaintAlpha(node.style, side), parentAlpha, sx, sy, sw, sh); + } + } + return true; +} + +// border-image (CSS Backgrounds 3, section 6): the source, sized to the border +// image area, is cut by the slices into nine parts drawn over the border image +// widths. A gradient part is its gradient box scaled so the slice lands on the +// tile, clipped to the tile. +bool recordBorderImage(const Node &node) +{ + const auto *source = StyleValues::borderImageSource(node.style); + if (!source || ViewGeometry::hasTransformChain(node, false)) return false; + const auto slice = StyleValues::borderImageSides(node.style, 0); + const auto widths = StyleValues::borderImageSides(node.style, 1); + const auto outset = StyleValues::borderImageSides(node.style, 2); + const int repeat = rstyle(node.style).border_image_repeat; + double border[4], out[4]; + for (int i = 0; i < 4; ++i) { + border[i] = computedBorderWidth(node.style, i); + out[i] = outset.kind[i] == 1 ? outset.value[i] : outset.value[i] * border[i]; + } + const double ax = node.layout.x - out[3], ay = node.layout.y - out[0]; + const double aw = node.layout.width + out[1] + out[3], ah = node.layout.height + out[0] + out[2]; + if (aw <= 0 || ah <= 0) return true; + // A gradient has no natural size: the image is the border image area, and + // auto widths fall back to the border widths. + double s[4], w[4]; + for (int i = 0; i < 4; ++i) { + const double size = i % 2 ? aw : ah; + s[i] = std::min(size, slice.kind[i] == 2 ? slice.value[i] * size / 100.0 : slice.value[i]); + w[i] = widths.kind[i] == 0 ? widths.value[i] * border[i] : widths.kind[i] == 1 ? widths.value[i] : + widths.kind[i] == 2 ? widths.value[i] * size / 100.0 : border[i]; + } + const double fit = std::min({1.0, w[1] + w[3] > aw ? aw / (w[1] + w[3]) : 1.0, w[0] + w[2] > ah ? ah / (w[0] + w[2]) : 1.0}); + for (double &v : w) v *= fit; + const double dx[4] = {ax, ax + w[3], ax + aw - w[1], ax + aw}, dy[4] = {ay, ay + w[0], ay + ah - w[2], ay + ah}; + const double sx[4] = {0, s[3], aw - s[1], aw}, sy[4] = {0, s[0], ah - s[2], ah}; + // A part's scale along the tiled axis, from the edge it follows. + auto factor = [](double first, double firstSlice, double second, double secondSlice) { + if (firstSlice > 0 && first > 0) return first / firstSlice; + return secondSlice > 0 && second > 0 ? second / secondSlice : 1.0; + }; + // Tile origins and size along one axis of a part. + struct Tiles { double start, size, step; int count; }; + auto tile = [](int mode, double d0, double d1, double size) -> Tiles { + const double span = d1 - d0; + if (mode == 0 || size <= 0 || span / size > 64) return {d0, span, span, 1}; + if (mode == 2) { + const int n = std::max(1, static_cast(std::lround(span / size))); + return {d0, span / n, span / n, n}; + } + if (mode == 3) { + const int n = static_cast(span / size); + const double gap = n ? (span - n * size) / (n + 1) : 0; + return {d0 + gap, size, size + gap, n}; + } + const double first = d0 + (span - size) / 2 - std::ceil((span - size) / 2 / size) * size; + return {first, size, size, static_cast(std::ceil((d1 - first) / size))}; + }; + for (int row = 0; row < 3; ++row) for (int col = 0; col < 3; ++col) { + if (row == 1 && col == 1 && !slice.fill) continue; + const double dw = dx[col + 1] - dx[col], dh = dy[row + 1] - dy[row]; + const double sw = sx[col + 1] - sx[col], sh = sy[row + 1] - sy[row]; + if (dw <= 0 || dh <= 0 || sw <= 0 || sh <= 0) continue; + const double kx = col != 1 ? dw / sw : row == 1 ? factor(w[0], s[0], w[2], s[2]) : dh / sh; + const double ky = row != 1 ? dh / sh : col == 1 ? factor(w[3], s[3], w[1], s[1]) : dw / sw; + const Tiles tx = col == 1 ? tile(repeat & 3, dx[1], dx[2], sw * kx) : Tiles{dx[col], dw, dw, 1}; + const Tiles ty = row == 1 ? tile(repeat >> 2, dy[1], dy[2], sh * ky) : Tiles{dy[row], dh, dh, 1}; + for (int j = 0; j < ty.count; ++j) for (int i = 0; i < tx.count; ++i) { + const double t0 = tx.start + i * tx.step, u0 = ty.start + j * ty.step; + const int cx0 = std::lround(std::max(t0, dx[col])), cx1 = std::lround(std::min(t0 + tx.size, dx[col + 1])); + const int cy0 = std::lround(std::max(u0, dy[row])), cy1 = std::lround(std::min(u0 + ty.size, dy[row + 1])); + if (cx1 <= cx0 || cy1 <= cy0) continue; + const double gx = tx.size / sw, gy = ty.size / sh; + const long vx = std::lround(t0 - sx[col] * gx), vy = std::lround(u0 - sy[row] * gy); + const long vw = std::lround(aw * gx), vh = std::lround(ah * gy); + if (std::max({std::labs(vx), std::labs(vy), vx + vw, vy + vh}) > 32000) continue; + auto *begin = DisplayList::instance().append(); + if (!begin) return true; + begin->type = DisplayCommandType::PushClip; + begin->bx = begin->clip.x = cx0; begin->by = begin->clip.y = cy0; + begin->bw = begin->clip.w = cx1 - cx0; begin->bh = begin->clip.h = cy1 - cy0; + begin->clip.nodeId = -1; + begin->clip.shaped = 0; + if (auto *paint = appendLinearGradientRectRaw(node, vx, vy, vw, vh, source->from, source->mid, source->to, + source->midStop, source->toStop, source->angle, source->fromAlpha, source->midAlpha, source->toAlpha, source->hasMid)) { + paint->gradient.tl = paint->gradient.tr = paint->gradient.br = paint->gradient.bl = 0; + paint->bx = cx0; paint->by = cy0; paint->bw = cx1 - cx0; paint->bh = cy1 - cy0; + } + auto *end = DisplayList::instance().append(); + if (end) { end->type = DisplayCommandType::PopClip; end->bx = cx0; end->by = cy0; end->bw = cx1 - cx0; end->bh = cy1 - cy0; end->clip.nodeId = -1; } + } + } + return true; +} + void GEA_VIEW_HOT_SRAM_SECTION("view_renderer_record_box") ViewRenderer::recordBox(const Node &node, uint8_t parentAlpha) { const Node *n = &node; @@ -2487,6 +3022,9 @@ void GEA_VIEW_HOT_SRAM_SECTION("view_renderer_record_box") ViewRenderer::recordB } } if (w <= 0 || h <= 0) return; + if (recordInlineParts(*n, parentAlpha)) return; + recordOuterBoxShadow(*n, parentAlpha); + bool borderOnGradientEdge = false; if (n->style.has_bg) { // The color is the bottom layer; image longhands never replace it. @@ -2496,6 +3034,8 @@ void GEA_VIEW_HOT_SRAM_SECTION("view_renderer_record_box") ViewRenderer::recordB TextBackgroundClipScope textClip(*n, clip); if (clip == 1 || clip == 2) { recordTransformedRoundedRectFill(*n, parentAlpha, clip); + } else if (clip == 4) { + recordBorderAreaFill(*n, parentAlpha); } else if (ViewGeometry::hasTransformChain(*n, false)) { int16_t xs[4], ys[4]; int bx0, by0, bx1, by1; @@ -2522,6 +3062,7 @@ void GEA_VIEW_HOT_SRAM_SECTION("view_renderer_record_box") ViewRenderer::recordB if (n->style.bg_fill == 1) { TextBackgroundClipScope textClip(*n, StyleValues::backgroundClip(n->style, rstyle(n->style).bg_gradient_layer)); recordLinearGradientBackground(*n, parentAlpha); + borderOnGradientEdge = ViewGeometry::hasTransformChain(*n, false) && gradientEdgeCarriesBorder(*n); } { TextBackgroundClipScope textClip(*n, StyleValues::backgroundClip(n->style, rstyle(n->style).bg_radial_gradient_layer)); @@ -2537,10 +3078,13 @@ void GEA_VIEW_HOT_SRAM_SECTION("view_renderer_record_box") ViewRenderer::recordB } recordInsetBoxShadow(*n, parentAlpha); + // A painted border image replaces the border styles. + if (recordBorderImage(*n)) return; if (n->style.border_width > 0 && !hasBorderRelief(n->style) && !hasSideBorder(n->style) && !borderColorsDiffer(n->style) && !borderIsSameOpaqueSolidBackground(*n, parentAlpha)) { if (ViewGeometry::hasTransformChain(*n, false)) { if (isFullyRoundedShape(*n)) recordTransformedEllipseStroke(*n, parentAlpha); + else if (!borderOnGradientEdge) recordTransformedBorderRing(*n, parentAlpha); } else { appendStrokeWithAlpha(*n, parentAlpha); } diff --git a/packages/geatsc-plugin-gea/src/utils.ts b/packages/geatsc-plugin-gea/src/utils.ts index c0e5def..26e3640 100644 --- a/packages/geatsc-plugin-gea/src/utils.ts +++ b/packages/geatsc-plugin-gea/src/utils.ts @@ -65,6 +65,8 @@ export function styleKeywordPropertyValuesForPropertyName(propertyName: string, } if (propertyName === 'position') return keywordValue('Position', POSITION_KEYWORDS, value) if (propertyName === 'text-align') return keywordValue('TextAlign', TEXT_ALIGN_KEYWORDS, value) + if (propertyName === 'text-align-last') return keywordValue('TextAlignLast', TEXT_ALIGN_LAST_KEYWORDS, value) + if (propertyName === 'vertical-align') return keywordValue('VerticalAlign', VERTICAL_ALIGN_KEYWORDS, value) if (propertyName === 'white-space') return keywordValue('WhiteSpace', WHITE_SPACE_KEYWORDS, value) if (propertyName === 'text-overflow') return keywordValue('TextOverflow', TEXT_OVERFLOW_KEYWORDS, value) if (propertyName === 'backface-visibility') return keywordValue('Backface', BACKFACE_KEYWORDS, value) @@ -153,6 +155,11 @@ const STYLE_DECLARATION_ENUM_BY_PROPERTY = new Map([ ['float', 'Float'], ['clear', 'Clear'], ['margin-trim', 'MarginTrim'], + ['line-clamp', 'LineClamp'], + ['-webkit-line-clamp', 'WebkitLineClamp'], + ['max-lines', 'MaxLines'], + ['continue', 'Continue'], + ['block-ellipsis', 'BlockEllipsis'], ['writing-mode', 'WritingMode'], ['direction', 'Direction'], ['flex-flow', 'FlexFlow'], @@ -251,6 +258,8 @@ const STYLE_DECLARATION_ENUM_BY_PROPERTY = new Map([ ['font-weight', 'FontWeight'], ['line-height', 'LineHeight'], ['text-align', 'TextAlign'], + ['text-align-last', 'TextAlignLast'], + ['vertical-align', 'VerticalAlign'], ['text-decoration', 'TextDecoration'], ['text-decoration-line', 'TextDecoration'], ['text-transform', 'TextTransform'], @@ -369,6 +378,8 @@ const STYLE_KEYWORD_PROPERTIES_BY_PROPERTY = new Map([ ['place-items', ['AlignItems', 'JustifyItems']], ['position', ['Position']], ['text-align', ['TextAlign']], + ['text-align-last', ['TextAlignLast']], + ['vertical-align', ['VerticalAlign']], ['white-space', ['WhiteSpace']], ['text-overflow', ['TextOverflow']], ['backface-visibility', ['Backface']], @@ -381,13 +392,18 @@ const STYLE_KEYWORD_PROPERTIES_BY_PROPERTY = new Map([ ['text-transform', ['TextTransform']], ]) +// As in the engine's displayValue(): keyword flags sit above the box kind, +// 32 for flow-root and 64 for inline (both together: inline-block). const DISPLAY_KEYWORDS = new Map([ ['block', 0], ['none', 1], ['grid', 2], - ['inline-grid', 2], + ['inline-grid', 66], ['flex', 3], - ['inline-flex', 3], + ['inline-flex', 67], + ['flow-root', 32], + ['inline', 64], + ['inline-block', 96], ]) const FLEX_DIRECTION_KEYWORDS = new Map([ @@ -428,14 +444,39 @@ const POSITION_KEYWORDS = new Map([ ['static', 0], ['absolute', 1], ['relative', 2], + ['sticky', 4], + ['-webkit-sticky', 4], ]) +// 0 start, 1 center, 2 right, 3 left, 4 end (see textAlignValue in style.cpp). const TEXT_ALIGN_KEYWORDS = new Map([ - ['left', 0], ['start', 0], ['center', 1], ['right', 2], - ['end', 2], + ['left', 3], + ['end', 4], +]) + +const VERTICAL_ALIGN_KEYWORDS = new Map([ + ['baseline', 0], + ['top', 1], + ['bottom', 2], + ['middle', 3], + ['text-top', 4], + ['text-bottom', 5], + ['sub', 6], + ['super', 7], +]) + +// The engine keeps auto distinct from start: text-align plus one, 0 = auto. +const TEXT_ALIGN_LAST_KEYWORDS = new Map([ + ['auto', 0], + ['left', 1], + ['start', 1], + ['justify', 1], + ['center', 2], + ['right', 3], + ['end', 3], ]) const WHITE_SPACE_KEYWORDS = new Map([