performPaint method
Paints the flame chart timeline.
Maps temporal spans into spatial columns using w.offsetX and w.zoomLevel.
Implementation
@override
void performPaint(Buffer buffer, Offset offset) {
final w = widget as TimelineCanvas;
final width = size.width;
final height = size.height;
final startX = offset.dx;
final startY = offset.dy;
void drawCell(int cx, int cy, String char, Style style) {
if (cx >= startX &&
cx < startX + width &&
cy >= startY &&
cy < startY + height) {
_safeSetCell(buffer, cx, cy, char, style);
}
}
final borderStyle = const Style(foreground: Color(120, 120, 120));
// Paint left/right borders
for (var y = 0; y < height; y++) {
drawCell(startX, startY + y, '│', borderStyle);
drawCell(startX + width - 1, startY + y, '│', borderStyle);
}
if (w.selectionStartUs != null && w.selectionEndUs != null) {
final startUs = w.selectionStartUs!.toDouble();
final endUs = w.selectionEndUs!.toDouble();
final startColF = (startUs - w.offsetX) / w.zoomLevel;
final endColF = (endUs - w.offsetX) / w.zoomLevel;
final startCol = startColF.floor().clamp(0, width - 3);
final endCol = endColF.floor().clamp(0, width - 3);
if (endCol >= 0 && startCol < width - 2) {
final highlightStyle = const Style(background: Color(40, 40, 80));
for (var col = startCol; col <= endCol; col++) {
final x = startX + 1 + col;
for (var cy = startY; cy < startY + height; cy++) {
_safeSetCell(buffer, x, cy, ' ', highlightStyle);
}
}
}
}
final viewportStartUs = w.offsetX;
final viewportEndUs = w.offsetX + (width - 2) * w.zoomLevel;
final visibleSpans = _getCulledSpans(
w.spans,
viewportStartUs,
viewportEndUs,
);
final mainSpans = visibleSpans;
final Set<int> drawnCells = {};
for (final span in mainSpans) {
final depth = span.depth;
final visualY = depth - w.offsetY;
if (visualY < 0 || visualY >= height) continue;
final y = startY + visualY;
final spanStartUs = span.startUs.toDouble();
final spanEndUs = span.endUs.toDouble();
final startColF = (spanStartUs - w.offsetX) / w.zoomLevel;
final endColF = (spanEndUs - w.offsetX) / w.zoomLevel;
final startCol = startColF.floor().clamp(0, width - 3);
final endCol = endColF.floor().clamp(0, width - 3);
final style = getCategoryStyle(span.category, span.name);
if (spanStartUs == spanEndUs) {
final x = startX + 1 + startCol;
final cellKey = (x << 16) | y;
if (drawnCells.contains(cellKey)) continue;
final bgArgb = buffer.getBackground(x, y);
final bg = bgArgb == 0 ? null : Color.argb(bgArgb);
drawCell(x, y, '│', style.merge(Style(background: bg)));
drawnCells.add(cellKey);
continue;
}
final bgColStyle = Style(
background: style.foreground,
foreground: Colors.white,
);
final fallbackStyle = style;
final nameWithPrefix = '▼ ${span.displayLabel}';
final spanColWidth = endCol - startCol + 1;
for (var col = startCol; col <= endCol; col++) {
final x = startX + 1 + col;
final cellKey = (x << 16) | y;
if (drawnCells.contains(cellKey)) continue;
final cellStart = w.offsetX + col * w.zoomLevel;
final cellEnd = cellStart + w.zoomLevel;
final cov = _coverage(spanStartUs, spanEndUs, cellStart, cellEnd);
if (cov > 0) {
String char;
Style cellStyle = bgColStyle;
if (cov > 0.99) {
final relativeCol = col - startCol;
if (spanColWidth >= nameWithPrefix.length &&
relativeCol >= 0 &&
relativeCol < nameWithPrefix.length) {
char = nameWithPrefix[relativeCol];
} else {
char = ' ';
}
drawnCells.add(cellKey);
} else {
if (startCol == endCol) {
char = cov <= 0.1 ? '│' : _getFractionalBlock(cov);
cellStyle = fallbackStyle;
} else if (col == startCol) {
char = _getFractionalBlock(1.0 - cov);
cellStyle = style.merge(
Style(
foreground: null,
background: style.foreground ?? const Color(255, 255, 255),
),
);
} else {
char = _getFractionalBlock(cov);
cellStyle = fallbackStyle;
}
if (char == '│' || char == '█') {
drawnCells.add(cellKey);
}
}
drawCell(x, y, char, cellStyle);
}
}
}
// Draw Caliper Overlay
if (w.measureStartMs != null && w.measureEndMs != null) {
final startUs = w.measureStartMs! * 1000.0;
final endUs = w.measureEndMs! * 1000.0;
final col1 = ((startUs - w.offsetX) / w.zoomLevel).round();
final col2 = ((endUs - w.offsetX) / w.zoomLevel).round();
final leftCol = min(col1, col2).clamp(0, width - 3) + 1;
final rightCol = max(col1, col2).clamp(0, width - 3) + 1;
final spanWidth = rightCol - leftCol + 1;
final durationMs = (endUs - startUs).abs() / 1000.0;
final text = ' ${durationMs.toStringAsFixed(1)}ms ';
String caliperStr;
if (spanWidth >= text.length + 4) {
final dashes = spanWidth - 2 - text.length;
final leftDashes = dashes ~/ 2;
final rightDashes = dashes - leftDashes;
caliperStr = '├${'─' * leftDashes}$text${'─' * rightDashes}┤';
} else if (spanWidth >= 2) {
caliperStr = '├${'─' * (spanWidth - 2)}┤';
} else {
caliperStr = '│';
}
final caliperStyle = const Style(
foreground: Color(0, 255, 255),
modifiers: Modifier.bold,
);
final caliperRow = startY - 1;
// We draw directly into the buffer for startY - 1, bypassing drawCell's cy >= startY check
for (var i = 0; i < caliperStr.length; i++) {
final cx = startX + leftCol + i;
if (cx >= startX && cx < startX + width) {
final bgArgb = buffer.getBackground(cx, caliperRow);
final bg = bgArgb == 0 ? null : Color.argb(bgArgb);
_safeSetCell(
buffer,
cx,
caliperRow,
caliperStr[i],
caliperStyle.merge(Style(background: bg)),
);
}
}
for (var y = caliperRow + 1; y < startY + height; y++) {
final bgArgb1 = buffer.getBackground(startX + leftCol, y);
final bg1 = bgArgb1 == 0 ? null : Color.argb(bgArgb1);
drawCell(
startX + leftCol,
y,
'│',
caliperStyle.merge(Style(background: bg1)),
);
if (rightCol > leftCol) {
final bgArgb2 = buffer.getBackground(startX + rightCol, y);
final bg2 = bgArgb2 == 0 ? null : Color.argb(bgArgb2);
drawCell(
startX + rightCol,
y,
'│',
caliperStyle.merge(Style(background: bg2)),
);
}
}
}
}