paint method

void paint(
  1. PaintingContext context,
  2. Offset offset
)

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();
    }
  }
}