paint method
Called whenever the object needs to paint. The given Canvas has its
coordinate space configured such that the origin is at the top left of the
box. The area of the box is the size of the size argument.
Paint operations should remain inside the given area. Graphical operations outside the bounds may be silently ignored, clipped, or not clipped. It may sometimes be difficult to guarantee that a certain operation is inside the bounds (e.g., drawing a rectangle whose size is determined by user inputs). In that case, consider calling Canvas.clipRect at the beginning of paint so everything that follows will be guaranteed to only draw within the clipped area.
Implementations should be wary of correctly pairing any calls to Canvas.save/Canvas.saveLayer and Canvas.restore, otherwise all subsequent painting on this canvas may be affected, with potentially hilarious but confusing results.
To paint text on a Canvas, use a TextPainter.
To paint an image on a Canvas:
-
Obtain an ImageStream, for example by calling ImageProvider.resolve on an AssetImage or NetworkImage object.
-
Whenever the ImageStream's underlying ImageInfo object changes (see ImageStream.addListener), create a new instance of your custom paint delegate, giving it the new ImageInfo object.
-
In your delegate's paint method, call the Canvas.drawImage, Canvas.drawImageRect, or Canvas.drawImageNine methods to paint the ImageInfo.image object, applying the ImageInfo.scale value to obtain the correct rendering size.
Implementation
@override
void paint(Canvas canvas, Size size) {
final rows = grid.rows, cols = grid.columns;
if (rows == 0 || cols == 0) return;
glyphs.beginFrame();
// Build any glyphs requested last frame, then start a fresh batch. The
// rebuild only does work when the glyph set grew, so steady state is free.
final atlas = this.atlas;
atlas?.rebuildIfNeeded();
atlas?.beginBatch((rows + 1) * cols);
// Clear the whole layer to the default background first (like alacritty
// clearing the framebuffer to its bg color), so the per-cell pass can SKIP
// default-bg cells (P2) without depending on a host-provided background.
// `backgroundOpacity < 1` leaves the default-bg regions translucent so the
// host can show content behind a translucent terminal.
final bgAlpha = (backgroundOpacity.clamp(0.0, 1.0) * 255).round();
if (bgAlpha > 0) {
canvas.drawRect(
Offset.zero & size,
Paint()
..isAntiAlias = false
..color = Color((bgAlpha << 24) | grid.defaultBg),
);
}
final shifted =
_pushScrollShift(canvas, size, grid.scrollFraction, rows, cellHeight);
// The overscan row (grid row -1) fills the sliver revealed by the shift.
final firstRow = shifted ? -1 : 0;
// Pass 1: backgrounds (so a wide glyph isn't overwritten by the spacer's bg).
// No anti-aliasing: cell metrics are sub-pixel (cell_metrics.dart), so AA'd
// edges on adjacent same-color rects leave half-covered seams between every
// cell — a faint grid, worst on fractional DPR / fractional widget offsets
// when embedded. Solid pixel-aligned fills tile exactly (matches alacritty's
// background quads). Same reasoning for the selection / cursor fills.
//
// Like native alacritty (RenderableCell.bg_alpha == 0 for an untouched
// default-bg cell), cells equal to the grid's default bg are NOT filled —
// the layer is cleared to the default bg by the host, so we emit rects only
// for cells that differ, run-length merging horizontally adjacent same-color
// spans into one drawRect. A typical text row drops from `cols` rects to a
// handful.
final bgPaint = Paint()..isAntiAlias = false;
final selPaint = Paint()
..isAntiAlias = false
..color = Color(selectionColor);
final int defaultBg = grid.defaultBg;
for (var row = firstRow; row < rows; row++) {
final y = row * cellHeight;
// Run-length merge: accumulate a [runStart, col) span of one color.
var runStart = 0;
var runColor = -1; // -1 = no open run (a default-bg gap)
void flushRun(int endCol) {
if (runColor < 0) return;
bgPaint.color = Color(0xFF000000 | runColor);
canvas.drawRect(
Rect.fromLTWH(runStart * cellWidth, y,
(endCol - runStart) * cellWidth, cellHeight),
bgPaint,
);
runColor = -1;
}
for (var col = 0; col < cols; col++) {
final flags = grid.flagsAt(row, col);
final bool overlayLink = linkOverlay.isLinkCell(row, col);
final int effFlags = overlayLink ? (flags | kFlagHyperlink) : flags;
final ec = applyMatchOrHint(
effFlags,
effectiveColors(effFlags, grid.fgAt(row, col), grid.bgAt(row, col)),
searchColors,
hintColors,
);
// Skip the default-bg gap; flush any open run at the boundary.
if (ec.bg == defaultBg) {
flushRun(col);
} else if (ec.bg != runColor) {
flushRun(col);
runStart = col;
runColor = ec.bg;
}
// Selection overlay is per-cell (translucent), so it can't be merged
// into the opaque bg runs; draw it on top of the background.
if (isSelected(flags)) {
canvas.drawRect(
Rect.fromLTWH(col * cellWidth, y, cellWidth, cellHeight), selPaint);
}
}
flushRun(cols);
}
// Pass 2: glyphs / geometry.
final lineWidth = (cellHeight * 0.08).clamp(1.0, 4.0);
final decoPaint = Paint()..strokeWidth = lineWidth;
// Underline/strikeout segments are deferred so they paint ON TOP of the
// atlas batch (emitted after this loop); otherwise the batched glyphs would
// cover a strikethrough. (a, b, packed-0x00RRGGBB).
final decoSegments = <(Offset, Offset, int)>[];
var needsWarmupFrame = false;
for (var row = firstRow; row < rows; row++) {
final y = row * cellHeight;
for (var col = 0; col < cols; col++) {
final flags = grid.flagsAt(row, col);
if (flags & kFlagWideSpacer != 0) continue; // covered by the wide glyph at col-1
final cp = grid.codepointAt(row, col);
if (cp == 32 || cp == 0) continue;
final bool overlayLink = linkOverlay.isLinkCell(row, col);
final int effFlags = overlayLink ? (flags | kFlagHyperlink) : flags;
final ec = applyMatchOrHint(
effFlags,
effectiveColors(effFlags, grid.fgAt(row, col), grid.bgAt(row, col)),
searchColors,
hintColors,
);
final fg = Color(0xFF000000 | ec.fg);
final bold = effFlags & kFlagBold != 0;
final italic = effFlags & kFlagItalic != 0;
final wide = effFlags & kFlagWide != 0;
final cellRect = Rect.fromLTWH(col * cellWidth, y, cellWidth, cellHeight);
if (isBoxDrawing(cp) && paintBoxGlyph(canvas, cellRect, cp, fg, lineWidth)) {
// Box-drawing stays a direct vector draw (not atlased); fall through
// for underline/strikeout decorations.
} else if (atlas != null) {
// Atlas path: tint a white coverage mask via drawRawAtlas (one call
// for the whole frame, emitted after the loop). A glyph not yet in the
// atlas is requested (built next frame) and drawn via the paragraph
// fallback this frame so nothing is ever missing.
final key = GlyphAtlas.keyFor(cp, bold: bold, italic: italic, wide: wide);
if (atlas.request(key)) {
atlas.addSprite(key, col * cellWidth, y, 0xFF000000 | ec.fg);
} else {
// Not yet atlased (queued for next frame) or past the atlas cap
// (permanent fallback): draw via paragraph. Only reschedule when the
// cache itself couldn't build the glyph this frame — `atlas.hasPending`
// (checked after the loop) covers the queued case, so a capped glyph
// doesn't spin scheduleFrame forever.
final paragraph =
glyphs.tryGet(cp, ec.fg, bold: bold, italic: italic, wide: wide);
if (paragraph != null) {
canvas.drawParagraph(paragraph, Offset(col * cellWidth, y));
} else {
needsWarmupFrame = true;
}
}
} else {
final paragraph =
glyphs.tryGet(cp, ec.fg, bold: bold, italic: italic, wide: wide);
if (paragraph != null) {
canvas.drawParagraph(paragraph, Offset(col * cellWidth, y));
} else {
needsWarmupFrame = true;
}
}
if (effFlags & (kFlagUnderline | kFlagStrikeout | kFlagHyperlink) != 0) {
final x = col * cellWidth;
final ys = decorationYs(y, cellHeight);
if (effFlags & (kFlagUnderline | kFlagHyperlink) != 0) {
decoSegments.add(
(Offset(x, ys.underline), Offset(x + cellWidth, ys.underline), ec.fg));
}
if (effFlags & kFlagStrikeout != 0) {
decoSegments.add(
(Offset(x, ys.strikeout), Offset(x + cellWidth, ys.strikeout), ec.fg));
}
}
}
}
// Emit the whole frame's glyphs in one drawRawAtlas, then the decorations
// on top.
atlas?.drawBatch(canvas, _atlasPaint);
for (final (a, b, color) in decoSegments) {
decoPaint.color = Color(0xFF000000 | color);
canvas.drawLine(a, b, decoPaint);
}
if (needsWarmupFrame || (atlas?.hasPending ?? false)) {
SchedulerBinding.instance.scheduleFrame();
}
if (shifted) canvas.restore();
}