paint method
Paint the inline content.
Implementation
void paint(PaintingContext context, Offset offset) {
if (_paragraph == null) return;
final double shiftX = _paragraphMinLeft.isFinite ? _paragraphMinLeft : 0.0;
final bool applyShift = shiftX.abs() > 0.01;
// Diagnostics previously logged paint-time info; now omitted for performance.
if (applyShift) {
context.canvas.save();
context.canvas.translate(-shiftX, 0.0);
}
try {
final CSSRenderStyle containerStyle = (container as RenderBoxModel).renderStyle;
final bool _clipText = containerStyle.backgroundClip == CSSBackgroundBoundary.text;
// Interleave line background and text painting so that later lines can
// visually overlay earlier lines when they cross vertically.
// For each paragraph line: paint decorations for that line, then clip and paint text for that line.
final para = _paragraph!;
if (_paraLines.isEmpty) {
// Fallback: paint decorations then text if no line metrics
_paintInlineSpanDecorations(context, offset);
if (!_clipText) {
context.canvas.drawParagraph(para, offset);
}
} else {
// Pre-scan lines to see if any requires right-side shifting for trailing extras.
bool anyShift = false;
if (_elementRanges.isNotEmpty) {
final Set<RenderBoxModel> hasRightPH = <RenderBoxModel>{};
for (int p = 0; p < _allPlaceholders.length && p < _placeholderBoxes.length; p++) {
final ph = _allPlaceholders[p];
if (ph.kind == _PHKind.rightExtra && ph.owner != null) {
hasRightPH.add(ph.owner!);
}
}
for (int i = 0; i < _paraLines.length && !anyShift; i++) {
double shiftSum = 0.0;
_elementRanges.forEach((RenderBoxModel box, (int start, int end) range) {
if (range.$2 <= range.$1) return;
if (hasRightPH.contains(box)) return; // single-line owners already accounted
final rects = _paragraph!.getBoxesForRange(range.$1, range.$2);
if (rects.isEmpty) return;
final last = rects.last;
final int li = _lineIndexForRect(last);
if (li != i) return;
final s = box.renderStyle;
final double extraR = s.paddingRight.computedValue +
s.effectiveBorderRightWidth.computedValue +
s.marginRight.computedValue;
if (extraR > 0) shiftSum += extraR;
});
if (shiftSum > 0) anyShift = true;
}
}
// Collect per-line vertical-align adjustments for non-atomic inline spans (not needed when using
// text-run placeholders, but kept for future parity; remains empty in common paths).
final Map<int, List<(ui.TextBox tb, double dy)>> vaAdjust = <int, List<(ui.TextBox, double)>>{};
if (_elementRanges.isNotEmpty) {
_elementRanges.forEach((RenderBoxModel box, (int start, int end) range) {
final va = box.renderStyle.verticalAlign;
if (va == VerticalAlign.baseline) return;
if (range.$2 <= range.$1) return;
final rects = _paragraph!.getBoxesForRange(range.$1, range.$2);
if (rects.isEmpty) return;
for (final tb in rects) {
final int li = _lineIndexForRect(tb);
if (li < 0 || li >= _paraLines.length) continue;
final (bandTop, bandBottom, _) = _bandForLine(li);
double dy = 0.0;
switch (va) {
case VerticalAlign.top:
dy = bandTop - tb.top;
break;
case VerticalAlign.bottom:
dy = bandBottom - tb.bottom;
break;
case VerticalAlign.middle:
final double lineMid = (bandTop + bandBottom) / 2.0;
final double boxMid = (tb.top + tb.bottom) / 2.0;
dy = lineMid - boxMid;
break;
default:
// Approximate text-top/text-bottom using line box top/bottom
dy = (va == VerticalAlign.textTop)
? (bandTop - tb.top)
: (va == VerticalAlign.textBottom)
? (bandBottom - tb.bottom)
: 0.0;
break;
}
if (dy.abs() > 0.01) {
(vaAdjust[li] ??= []).add((tb, dy));
}
}
});
}
final bool anyVAAdjust = vaAdjust.values.any((l) => l.isNotEmpty);
// Build relative-position adjustments for inline elements with position:relative
final Map<int, List<(ui.TextBox tb, double dx, double dy)>> relAdjust =
<int, List<(ui.TextBox, double, double)>>{};
if (_elementRanges.isNotEmpty) {
_elementRanges.forEach((RenderBoxModel box, (int start, int end) range) {
final rs = box.renderStyle;
if (rs.position != CSSPositionType.relative) return;
final Offset? rel = CSSPositionedLayout.getRelativeOffset(rs);
if (rel == null || (rel.dx == 0 && rel.dy == 0)) return;
if (range.$2 <= range.$1) return;
final rects = _paragraph!.getBoxesForRange(range.$1, range.$2);
if (rects.isEmpty) return;
for (final tb in rects) {
final int li = _lineIndexForRect(tb);
if (li < 0 || li >= _paraLines.length) continue;
(relAdjust[li] ??= <(ui.TextBox, double, double)>[]).add((tb, rel.dx, rel.dy));
}
});
}
final bool anyRelAdjust = relAdjust.values.any((l) => l.isNotEmpty);
// If no line needs shifting for trailing extras, and no vertical-align/relative adjustment,
// just paint decorations once and draw paragraph once.
if (!anyShift && !anyVAAdjust && !anyRelAdjust) {
_paintInlineSpanDecorations(context, offset);
if (!_clipText) {
context.canvas.drawParagraph(para, offset);
}
} else {
for (int i = 0; i < _paraLines.length; i++) {
final lm = _paraLines[i];
final double lineTop = lm.baseline - lm.ascent;
final double lineBottom = lm.baseline + lm.descent;
// Paint only the decorations belonging to this line
_paintInlineSpanDecorations(context, offset, lineTop: lineTop, lineBottom: lineBottom);
// Determine aggregate right-extras shift for multi-line owners that end on this line.
double shiftSum = 0.0;
double boundaryX = lm.left; // rightmost boundary among owners on this line
if (_elementRanges.isNotEmpty) {
final Set<RenderBoxModel> hasRightPH = <RenderBoxModel>{};
for (int p = 0; p < _allPlaceholders.length && p < _placeholderBoxes.length; p++) {
final ph = _allPlaceholders[p];
if (ph.kind == _PHKind.rightExtra && ph.owner != null) {
hasRightPH.add(ph.owner!);
}
}
_elementRanges.forEach((RenderBoxModel box, (int start, int end) range) {
if (range.$2 <= range.$1) return;
if (hasRightPH.contains(box)) return; // single-line owners already accounted
final rects = _paragraph!.getBoxesForRange(range.$1, range.$2);
if (rects.isEmpty) return;
final last = rects.last;
final int li = _lineIndexForRect(last);
if (li != i) return;
final s = box.renderStyle;
final double extraR = s.paddingRight.computedValue +
s.effectiveBorderRightWidth.computedValue +
s.marginRight.computedValue;
if (extraR > 0) {
shiftSum += extraR;
if (last.right > boundaryX) boundaryX = last.right;
}
});
}
// Build clip regions for normal paint (excluding vertical-align adjusted fragments)
final double lineRight = lm.left + lm.width;
final double clipRight = math.max(para.width, lineRight + shiftSum);
final double lineClipTop = offset.dy + lineTop;
final double lineClipBottom = offset.dy + lineBottom;
final Rect leftBase = Rect.fromLTRB(
offset.dx,
lineClipTop,
offset.dx + boundaryX,
lineClipBottom,
);
final Rect rightBase = Rect.fromLTRB(
offset.dx + boundaryX + shiftSum,
lineClipTop,
offset.dx + clipRight,
lineClipBottom,
);
ui.Path _clipMinusVA(Rect base, bool isRightSlice) {
ui.Path p = ui.Path()..addRect(base);
final adj = vaAdjust[i] ?? const <(ui.TextBox, double)>[];
final rel = relAdjust[i] ?? const <(ui.TextBox, double, double)>[];
if (adj.isEmpty && rel.isEmpty) return p;
ui.Path sub = ui.Path();
for (final (tb, _) in adj) {
final double xShift = isRightSlice ? shiftSum : 0.0;
final Rect r = Rect.fromLTRB(
offset.dx + tb.left + xShift,
offset.dy + tb.top,
offset.dx + tb.right + xShift,
offset.dy + tb.bottom,
);
if (r.overlaps(base)) sub.addRect(r.intersect(base));
}
for (final (tb, dx, dy) in rel) {
final double xShift = isRightSlice ? shiftSum : 0.0;
final Rect r0 = Rect.fromLTRB(
offset.dx + tb.left + xShift,
offset.dy + tb.top,
offset.dx + tb.right + xShift,
offset.dy + tb.bottom,
);
final Rect r = Rect.fromLTRB(
dx < 0 ? r0.left + dx : r0.left,
dy < 0 ? r0.top + dy : r0.top,
dx > 0 ? r0.right + dx : r0.right,
dy > 0 ? r0.bottom + dy : r0.bottom,
);
if (r.overlaps(base)) sub.addRect(r.intersect(base));
}
if (sub.getBounds().isEmpty) return p;
return ui.Path.combine(ui.PathOperation.difference, p, sub);
}
// Left slice (no horizontal shift)
if (!_clipText) {
context.canvas.save();
context.canvas.clipPath(_clipMinusVA(leftBase, false));
context.canvas.drawParagraph(para, offset);
context.canvas.restore();
}
// Right slice (apply shift if needed)
if (!_clipText) {
context.canvas.save();
context.canvas.clipPath(_clipMinusVA(rightBase, true));
if (shiftSum != 0.0) {
context.canvas.translate(shiftSum, 0.0);
}
context.canvas.drawParagraph(para, offset);
context.canvas.restore();
}
// Repaint vertical-align adjusted fragments with vertical translation (and horizontal if in right slice)
final adj = vaAdjust[i] ?? const <(ui.TextBox, double)>[];
for (final (tb, dy) in adj) {
final double leftPartRight = math.min(tb.right, boundaryX);
final double rightPartLeft = math.max(tb.left, boundaryX);
// Left portion (no x shift)
if (tb.left < boundaryX && leftPartRight > tb.left) {
final Rect target = Rect.fromLTRB(
offset.dx + tb.left,
offset.dy + tb.top + dy,
offset.dx + leftPartRight,
offset.dy + tb.bottom + dy,
);
if (!_clipText) {
context.canvas.save();
context.canvas.clipRect(target);
context.canvas.translate(0.0, dy);
context.canvas.drawParagraph(para, offset);
context.canvas.restore();
}
}
// Right portion (apply x shift)
if (tb.right > boundaryX && rightPartLeft < tb.right) {
final Rect target = Rect.fromLTRB(
offset.dx + rightPartLeft + shiftSum,
offset.dy + tb.top + dy,
offset.dx + tb.right + shiftSum,
offset.dy + tb.bottom + dy,
);
if (!_clipText) {
context.canvas.save();
context.canvas.clipRect(target);
context.canvas.translate(shiftSum, dy);
context.canvas.drawParagraph(para, offset);
context.canvas.restore();
}
}
}
// Repaint relative-position adjusted fragments with XY translation (and horizontal shift if in right slice)
final rel = relAdjust[i] ?? const <(ui.TextBox, double, double)>[];
for (final (tb, dx, dy) in rel) {
final double leftPartRight = math.min(tb.right, boundaryX);
final double rightPartLeft = math.max(tb.left, boundaryX);
// Left portion (no x shift)
if (tb.left < boundaryX && leftPartRight > tb.left) {
final Rect target = Rect.fromLTRB(
offset.dx + tb.left + dx,
offset.dy + tb.top + dy,
offset.dx + leftPartRight + dx,
offset.dy + tb.bottom + dy,
);
if (!_clipText) {
context.canvas.save();
context.canvas.clipRect(target);
context.canvas.translate(dx, dy);
context.canvas.drawParagraph(para, offset);
context.canvas.restore();
}
}
// Right portion (apply x shift)
if (tb.right > boundaryX && rightPartLeft < tb.right) {
final Rect target = Rect.fromLTRB(
offset.dx + rightPartLeft + shiftSum + dx,
offset.dy + tb.top + dy,
offset.dx + tb.right + shiftSum + dx,
offset.dy + tb.bottom + dy,
);
if (!_clipText) {
context.canvas.save();
context.canvas.clipRect(target);
context.canvas.translate(shiftSum + dx, dy);
context.canvas.drawParagraph(para, offset);
context.canvas.restore();
}
}
}
}
}
// When using background-clip:text, paint text-shadow separately before the
// gradient mask so that shadows retain their own color instead of being
// tinted by the background.
if (_clipText && _paragraph != null) {
final CSSRenderStyle _rs = (container as RenderBoxModel).renderStyle;
final List<Shadow>? shadows = _rs.textShadow;
if (shadows != null && shadows.isNotEmpty) {
final ui.Paragraph _para = _paragraph!;
final double intrinsicLineWidth = _para.longestLine;
final double layoutWidth = _para.width;
final double w = math.max(layoutWidth, intrinsicLineWidth);
final double h = _para.height;
if (w > 0 && h > 0) {
for (final Shadow s in shadows) {
if (s.color.alpha == 0) continue;
final double blur = s.blurRadius;
// Approximate Flutter's radius→sigma conversion.
double _radiusToSigma(double r) => r > 0 ? (r * 0.57735 + 0.5) : 0.0;
final double sigma = _radiusToSigma(blur);
// Expand layer to accommodate blur spread and offset.
final double pad = blur * 2 + 2;
final Rect layer = Rect.fromLTWH(
offset.dx + s.offset.dx - pad,
offset.dy + s.offset.dy - pad,
w + pad * 2,
h + pad * 2,
);
final Paint layerPaint = Paint();
if (sigma > 0) {
layerPaint.imageFilter = ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma);
}
context.canvas.saveLayer(layer, layerPaint);
// Draw glyph mask shifted by the shadow offset.
context.canvas.drawParagraph(_para, offset.translate(s.offset.dx, s.offset.dy));
// Tint the mask with the shadow color.
final Paint tint = Paint()
..blendMode = BlendMode.srcIn
..color = s.color;
context.canvas.drawRect(layer, tint);
context.canvas.restore();
}
}
}
}
}
// Paint synthetic text-run placeholders (vertical-align on text spans)
if (_textRunParas.isNotEmpty && _allPlaceholders.isNotEmpty && _placeholderBoxes.isNotEmpty) {
// Precompute per-line boundary and shift for right-extras, mirroring the logic above
final List<(double boundaryX, double shiftSum)> lineShift = List.filled(_paraLines.length, (0.0, 0.0));
if (_elementRanges.isNotEmpty) {
final Set<RenderBoxModel> hasRightPH = <RenderBoxModel>{};
for (int p = 0; p < _allPlaceholders.length && p < _placeholderBoxes.length; p++) {
final ph = _allPlaceholders[p];
if (ph.kind == _PHKind.rightExtra && ph.owner != null) hasRightPH.add(ph.owner!);
}
for (int i = 0; i < _paraLines.length; i++) {
double boundaryX = _paraLines[i].left;
double shiftSum = 0.0;
_elementRanges.forEach((RenderBoxModel box, (int start, int end) range) {
if (range.$2 <= range.$1) return;
if (hasRightPH.contains(box)) return;
final rects = _paragraph!.getBoxesForRange(range.$1, range.$2);
if (rects.isEmpty) return;
final last = rects.last;
final int li = _lineIndexForRect(last);
if (li != i) return;
final s = box.renderStyle;
final double extraR = s.paddingRight.computedValue +
s.effectiveBorderRightWidth.computedValue +
s.marginRight.computedValue;
if (extraR > 0) {
if (last.right > boundaryX) boundaryX = last.right;
shiftSum += extraR;
}
});
lineShift[i] = (boundaryX, shiftSum);
}
}
for (int i = 0; i < _allPlaceholders.length && i < _placeholderBoxes.length && i < _textRunParas.length; i++) {
final ph = _allPlaceholders[i];
final sub = _textRunParas[i];
if (ph.kind != _PHKind.textRun || sub == null) continue;
final tb = _placeholderBoxes[i];
final li = _lineIndexForRect(tb);
double xShift = 0.0;
if (li >= 0 && li < lineShift.length) {
final (boundaryX, shiftSum) = lineShift[li];
if (tb.right > boundaryX) xShift = shiftSum;
}
context.canvas.save();
// Clip to placeholder rect (with applied horizontal shift) to avoid overdraw
final Rect clip = Rect.fromLTRB(
offset.dx + tb.left + xShift,
offset.dy + tb.top,
offset.dx + tb.right + xShift,
offset.dy + tb.bottom,
);
context.canvas.clipRect(clip);
context.canvas.drawParagraph(sub, offset.translate(tb.left + xShift, tb.top));
context.canvas.restore();
}
}
// Gradient text via background-clip: text
// Draw a gradient (or background color) clipped to glyphs when requested on the container.
final CSSRenderStyle _rs = (container as RenderBoxModel).renderStyle;
if (_rs.backgroundClip == CSSBackgroundBoundary.text) {
final ui.Paragraph? _para = _paragraph;
if (_para != null) {
final Gradient? _grad = _rs.backgroundImage?.gradient;
final Color? _bgc = _rs.backgroundColor?.value;
if (_grad != null || (_bgc != null && _bgc.alpha != 0)) {
final double intrinsicLineWidth = _para.longestLine;
final double layoutWidth = _para.width;
final double w = math.max(layoutWidth, intrinsicLineWidth);
final double h = _para.height;
if (w > 0 && h > 0) {
final Rect layer = Rect.fromLTWH(offset.dx, offset.dy, w, h);
// Compute container border-box rect in current canvas coordinates.
final double padL = _rs.paddingLeft.computedValue;
final double padT = _rs.paddingTop.computedValue;
final double borL = _rs.effectiveBorderLeftWidth.computedValue;
final double borT = _rs.effectiveBorderTopWidth.computedValue;
final double contLeft = offset.dx - padL - borL;
final double contTop = offset.dy - padT - borT;
final Rect contRect = Rect.fromLTWH(contLeft, contTop, container.size.width, container.size.height);
// Use a layer so we can mask the background with glyph alpha using srcIn.
context.canvas.saveLayer(layer, Paint());
// Draw the paragraph shape into the layer (mask only; no shadows/decoration in clip-text).
context.canvas.drawParagraph(_para, offset);
// Now overlay the background with srcIn so it is clipped to the glyphs we just drew.
final Paint p = Paint()..blendMode = BlendMode.srcIn;
if (_grad != null) {
p.shader = _grad.createShader(contRect);
context.canvas.drawRect(layer, p);
} else {
p.color = _bgc!;
context.canvas.drawRect(layer, p);
}
context.canvas.restore();
// Overlay text fill color on top (browser paints text after background).
// This allows CSS color alpha to tint/cover the gradient, matching browser behavior.
final Color textFill = _rs.color.value;
if (textFill.alpha != 0) {
context.canvas.saveLayer(layer, Paint());
// Paragraph as mask again
context.canvas.drawParagraph(_para, offset);
final Paint fill = Paint()
..blendMode = BlendMode.srcIn
..color = textFill;
context.canvas.drawRect(layer, fill);
context.canvas.restore();
}
// End glyph-mask background pass
}
}
}
}
// Detect inline elements within this paragraph that request background-clip:text.
// Paint inline elements with background-clip:text by masking their glyphs and
// overlaying their background gradient within each text box rect.
if (_elementRanges.isNotEmpty && _paragraph != null) {
final para = _paragraph!;
int paintedCount = 0;
_elementRanges.forEach((RenderBoxModel box, (int start, int end) range) {
final CSSRenderStyle s = box.renderStyle;
if (s.backgroundClip != CSSBackgroundBoundary.text) return;
final Gradient? grad = s.backgroundImage?.gradient;
final Color? bgc = s.backgroundColor?.value;
if (grad == null && (bgc == null || bgc.alpha == 0)) return;
if (range.$2 <= range.$1) return;
// Text boxes for this element's range
final List<ui.TextBox> rects = para.getBoxesForRange(range.$1, range.$2);
if (rects.isEmpty) return;
// Union of rects for shader bounds
double minL = rects.first.left, minT = rects.first.top, maxR = rects.first.right, maxB = rects.first.bottom;
for (final tb in rects) {
if (tb.left < minL) minL = tb.left;
if (tb.top < minT) minT = tb.top;
if (tb.right > maxR) maxR = tb.right;
if (tb.bottom > maxB) maxB = tb.bottom;
}
final Rect union = Rect.fromLTRB(minL, minT, maxR, maxB);
// Helper to build a mask paragraph for a given text (forced opaque glyph color).
ui.Paragraph _buildMaskPara(String text) {
final ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle(
textDirection: (container as RenderBoxModel).renderStyle.direction,
textHeightBehavior: const ui.TextHeightBehavior(
applyHeightToFirstAscent: true,
applyHeightToLastDescent: true,
leadingDistribution: ui.TextLeadingDistribution.even,
),
));
final families = s.fontFamily;
if (families != null && families.isNotEmpty) {
CSSFontFace.ensureFontLoaded(families[0], s.fontWeight, s);
}
final double? heightMultiple = (() {
if (s.lineHeight.type == CSSLengthType.NORMAL) return kTextHeightNone;
if (s.lineHeight.type == CSSLengthType.EM) return s.lineHeight.value;
return s.lineHeight.computedValue / s.fontSize.computedValue;
})();
final Color maskColor = s.isVisibilityHidden ? const Color(0x00000000) : s.color.value.withAlpha(0xFF);
pb.pushStyle(ui.TextStyle(
color: maskColor,
// Suppress decoration/shadow in the mask paragraph for clip-text; they
// are painted via dedicated passes to preserve color/ordering.
decoration: TextDecoration.none,
decorationColor: const Color(0x00000000),
decorationStyle: null,
fontWeight: s.fontWeight,
fontStyle: s.fontStyle,
textBaseline: CSSText.getTextBaseLine(),
fontFamily: (families != null && families.isNotEmpty) ? families.first : null,
fontFamilyFallback: families,
fontSize: s.fontSize.computedValue,
letterSpacing: s.letterSpacing?.computedValue,
wordSpacing: s.wordSpacing?.computedValue,
height: heightMultiple,
locale: CSSText.getLocale(),
background: CSSText.getBackground(),
foreground: CSSText.getForeground(),
shadows: null,
));
pb.addText(text);
final ui.Paragraph p = pb.build();
p.layout(const ui.ParagraphConstraints(width: 1000000.0));
return p;
}
// Group rects by paragraph line index to preserve multi-line runs.
final Map<int, List<ui.TextBox>> lineRects = <int, List<ui.TextBox>>{};
for (final tb in rects) {
final int li = _lineIndexForRect(tb);
(lineRects[li] ??= <ui.TextBox>[]).add(tb);
}
final List<int> lines = lineRects.keys.toList()..sort();
// Binary search to find the maximal end index on a paragraph line.
int _findLineEnd(int startIndex, int targetLine) {
int lo = startIndex + 1;
int hi = range.$2;
int best = startIndex + 1;
while (lo <= hi) {
final int mid = lo + ((hi - lo) >> 1);
final boxes = para.getBoxesForRange(startIndex, mid);
if (boxes.isEmpty) {
lo = mid + 1;
continue;
}
final int lastLine = _lineIndexForRect(boxes.last);
if (lastLine == targetLine) {
best = mid;
lo = mid + 1;
} else if (lastLine < targetLine) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return best;
}
int segStart = range.$1;
// Shader rect covers the element’s entire union area.
final Rect shaderRect =
Rect.fromLTWH(offset.dx + union.left, offset.dy + union.top, union.width, union.height);
for (final int li in lines) {
final boxes = lineRects[li]!;
boxes.sort((a, b) => a.left.compareTo(b.left));
// Compute substring end for this line using binary search.
final int segEnd = _findLineEnd(segStart, li);
if (segEnd <= segStart) {
continue;
}
final String segText = _textContent.substring(segStart, segEnd);
final ui.Paragraph segPara = _buildMaskPara(segText);
// Build union clip for the line's boxes.
double l = boxes.first.left, t = boxes.first.top, r = boxes.first.right, b = boxes.first.bottom;
final ui.Path clip = ui.Path();
for (final tb in boxes) {
clip.addRect(
Rect.fromLTRB(offset.dx + tb.left, offset.dy + tb.top, offset.dx + tb.right, offset.dy + tb.bottom));
if (tb.left < l) l = tb.left;
if (tb.top < t) t = tb.top;
if (tb.right > r) r = tb.right;
if (tb.bottom > b) b = tb.bottom;
}
final Rect layer = Rect.fromLTRB(offset.dx + l, offset.dy + t, offset.dx + r, offset.dy + b);
// Paint text shadows for this inline segment before gradient to keep shadow color.
final List<Shadow>? _segShadows = s.textShadow;
if (_segShadows != null && _segShadows.isNotEmpty) {
final ui.TextBox firstBox = boxes.first;
for (final Shadow sh in _segShadows) {
if (sh.color.alpha == 0) continue;
double _radiusToSigma(double r) => r > 0 ? (r * 0.57735 + 0.5) : 0.0;
final double sigma = _radiusToSigma(sh.blurRadius);
final double pad = sh.blurRadius * 2 + 2;
final Rect shadowLayer = Rect.fromLTRB(
layer.left + sh.offset.dx - pad,
layer.top + sh.offset.dy - pad,
layer.right + sh.offset.dx + pad,
layer.bottom + sh.offset.dy + pad,
);
final Paint lp = Paint();
if (sigma > 0) {
lp.imageFilter = ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma);
}
context.canvas.saveLayer(shadowLayer, lp);
context.canvas.drawParagraph(
segPara,
offset.translate(firstBox.left + sh.offset.dx, firstBox.top + sh.offset.dy),
);
final Paint tint = Paint()
..blendMode = BlendMode.srcIn
..color = sh.color;
context.canvas.drawRect(shadowLayer, tint);
context.canvas.restore();
}
}
// Paint: mask paragraph at the first fragment origin on this line, then srcIn gradient.
context.canvas.saveLayer(layer, Paint());
context.canvas.clipPath(clip);
final ui.TextBox first = boxes.first;
context.canvas.drawParagraph(segPara, offset.translate(first.left, first.top));
final Paint p = Paint()..blendMode = BlendMode.srcIn;
if (grad != null) {
p.shader = grad.createShader(shaderRect);
context.canvas.drawRect(layer, p);
} else {
p.color = bgc!;
context.canvas.drawRect(layer, p);
}
context.canvas.restore();
segStart = segEnd;
}
paintedCount++;
});
}
// Paint atomic inline children using parentData.offset for consistency.
// Convert container-origin offsets to content-local by subtracting the content origin.
final double contentOriginX = container.renderStyle.paddingLeft.computedValue +
container.renderStyle.effectiveBorderLeftWidth.computedValue;
final double contentOriginY =
container.renderStyle.paddingTop.computedValue + container.renderStyle.effectiveBorderTopWidth.computedValue;
for (int i = 0; i < _allPlaceholders.length && i < _placeholderBoxes.length; i++) {
final ph = _allPlaceholders[i];
if (ph.kind != _PHKind.atomic) continue;
final rb = ph.atomic;
if (rb == null) continue;
// Choose the direct child (wrapper) to paint
RenderBox paintBox = rb;
RenderObject? p = rb.parent;
while (p != null && p != container) {
if (p is RenderBox) paintBox = p;
p = p.parent;
}
if (!paintBox.hasSize) continue;
final RenderLayoutParentData pd = paintBox.parentData as RenderLayoutParentData;
final Offset contentLocalOffset = Offset(pd.offset.dx - contentOriginX, pd.offset.dy - contentOriginY);
context.paintChild(paintBox, offset + contentLocalOffset);
}
if (DebugFlags.debugPaintInlineLayoutEnabled) {
_debugPaintParagraph(context, offset);
}
} finally {
if (applyShift) {
context.canvas.restore();
}
}
}