Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
021d0c0
Resolve abspos baseline fallback against the containing grid
skyturkish Sep 25, 2026
b98c25c
Saturate position offsets instead of truncating them to int16
skyturkish Sep 25, 2026
2331810
Use the baseline fallback when flex baselines run along the cross axis
skyturkish Sep 25, 2026
1319e4c
Apply align-self to abspos static positions inside a line box
skyturkish Sep 25, 2026
b15d092
Skip node-dependent background placement for detached boxes
skyturkish Sep 25, 2026
97ad6f3
Clip local backgrounds of scroll containers to the padding box
skyturkish Sep 25, 2026
90c6d82
Paint each rounded-border row once as outer minus inner span
skyturkish Sep 25, 2026
d028e10
Support background-clip: border-area for background colors
skyturkish Sep 25, 2026
e5fec08
Support text-align-last and ignore edge spaces when aligning lines
skyturkish Sep 25, 2026
9e371a2
Paint outer box-shadows
skyturkish Sep 25, 2026
9d42214
Paint opaque constant linear gradients as solid fills
skyturkish Sep 25, 2026
1078806
Support line-clamp, max-lines and continue: collapse
skyturkish Sep 25, 2026
92efa1e
Support vertical-align keywords in inline formatting
skyturkish Sep 25, 2026
000c073
Support position: sticky
skyturkish Sep 25, 2026
2a285de
Keep floats from splitting unbreakable inline content
skyturkish Sep 25, 2026
062a5f0
Paint the block ellipsis at a line-clamp clamp point
skyturkish Sep 25, 2026
cc57d96
Paint transformed borders and clip overflow to rounded shapes
skyturkish Sep 25, 2026
146b753
Keep opaque glyphs out of the background-clip: text mask
skyturkish Sep 25, 2026
26f45fc
Lay out multi-column containers
skyturkish Sep 25, 2026
737d93b
Support border-style and its longhands
skyturkish Sep 25, 2026
330c3b5
Support logical properties for horizontal-tb ltr
skyturkish Sep 25, 2026
8347666
Align lines of several inline items and resolve start by direction
skyturkish Sep 25, 2026
14053ea
Contain abspos children of fragmented inlines in their fragments
skyturkish Sep 26, 2026
80aa245
Support text-emphasis
skyturkish Sep 26, 2026
fd123d7
Support background-blend-mode and clip text backgrounds once
skyturkish Sep 26, 2026
8075a8c
Support border-image with gradient sources
skyturkish Sep 26, 2026
b182578
Split decorated inline boxes around their blocks
skyturkish Sep 26, 2026
9b76481
Snap fractional flex and percentage edges like browsers
skyturkish Sep 26, 2026
877eb2e
Establish block formatting contexts for flow-root and align-content
skyturkish Sep 26, 2026
1b788fc
Follow CSS operator precedence in calc()
skyturkish Sep 26, 2026
7fca60a
Clamp lines by height with box ends, margins and BFC boundaries
skyturkish Sep 26, 2026
d0a7d48
Lay out inline-flex and inline-grid as atomic inline-level boxes
skyturkish Sep 26, 2026
cef148d
Align absolute boxes on their static position in block containers
skyturkish Sep 26, 2026
0fdbdf6
Lay out display: inline and inline-block
skyturkish Sep 26, 2026
493d7f7
Align absolute boxes between insets and on their static rectangle
skyturkish Sep 26, 2026
6b3cf07
Apply center/end align-content to fixed-height block containers
skyturkish Sep 26, 2026
143b5a4
Place inline-level absolute boxes on a hypothetical line in block flow
skyturkish Sep 26, 2026
02bea8e
Size block flex items to their content in a flex row
skyturkish Sep 26, 2026
5b806bb
Place a float after a one-line run at the top of that line
skyturkish Sep 26, 2026
36b4848
Trim item margins against flex and grid container edges
skyturkish Sep 26, 2026
b4a1ab5
Resolve transform: inherit to the parent's transform
skyturkish Sep 26, 2026
56fde5f
Support the + and ~ sibling combinators in selectors
skyturkish Sep 26, 2026
dac860c
Size auto aspect ratios by content box and clamp flex minimums by them
skyturkish Sep 26, 2026
b89644d
Honor auto margins for blocks laid out in a float context
skyturkish Sep 26, 2026
bfe7496
Draw bare flex text on its baseline unless the container centres it
skyturkish Sep 26, 2026
dea97af
Store box widths and heights as 32-bit so huge boxes lay out and paint
skyturkish Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions packages/core/scripts/build-gea-vite-geatsc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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':
Expand Down
5 changes: 3 additions & 2 deletions packages/core/test/test_css_block_and_flexbasis_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
94 changes: 45 additions & 49 deletions packages/engine/canvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/engine/ui/display_invalidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions packages/engine/ui/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion packages/engine/ui/input_render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int8_t>(LayoutEngine::physicalTextAlign(n));
cmd->text.textTransform = n.style.text_transform;
cmd->text.lineHeight = n.style.line_height;
cmd->text.containerWidth = contentW;
Expand All @@ -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;
}
}

Expand Down
65 changes: 64 additions & 1 deletion packages/engine/ui/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ void rootScrollImageHoldTick();
FillTransformedRoundedRect
};

// DrawText::blockEllipsis for the default ellipsis ("\u2026", or "...").
constexpr uint16_t kAutoBlockEllipsis = 0xFFFF;

struct DisplayCommand
{
DisplayCommandType type;
Expand All @@ -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
{
Expand Down Expand Up @@ -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
{
Expand All @@ -155,6 +168,7 @@ void rootScrollImageHoldTick();
uint16_t stopPermille;
uint8_t fromAlpha;
uint8_t toAlpha;
uint8_t blend;
} radialGradient;
struct
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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::
Expand All @@ -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.
Expand All @@ -465,9 +510,22 @@ void rootScrollImageHoldTick();
static std::vector<int> 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);
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
Loading
Loading