blob: 8fc29e3d2f31167b4d6309db3fd097e2d8110c43 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:html';
import 'dart:math' as math;
import 'dart:math';
import 'package:meta/meta.dart';
import '../ui/elements.dart';
import '../ui/fake_flutter/fake_flutter.dart';
import '../ui/flutter_html_shim.dart';
import '../ui/html_icon_renderer.dart';
import '../ui/icons.dart';
import '../ui/viewport_canvas.dart';
import 'inspector_service.dart';
import 'inspector_tree.dart';
import 'inspector_tree_web.dart';
typedef CanvasPaintCallback = void Function(
CanvasRenderingContext2D canvas,
int index,
double width,
double height,
);
abstract class CanvasPaintEntry extends PaintEntry {
CanvasPaintEntry(this.x);
final double x;
double get right;
void paint(CanvasRenderingContext2D canvas);
}
class IconPaintEntry extends CanvasPaintEntry {
IconPaintEntry({
@required double x,
@required this.iconRenderer,
}) : super(x);
@override
Icon get icon => iconRenderer.icon;
final HtmlIconRenderer iconRenderer;
@override
void paint(CanvasRenderingContext2D canvas) {
final image = iconRenderer.image;
if (image != null) {
canvas.drawImageScaled(
image,
x,
(rowHeight - iconRenderer.iconHeight) / 2,
iconRenderer.iconWidth,
iconRenderer.iconHeight,
);
}
}
@override
double get right => x + icon.iconWidth;
@override
void attach(InspectorTree owner) {
final image = iconRenderer.image;
if (image == null) {
iconRenderer.loadImage().then((_) {
// TODO(jacobr): only repaint what is needed.
owner.setState(() {});
});
}
}
}
class TextPaintEntry extends CanvasPaintEntry {
TextPaintEntry({
@required double x,
@required this.width,
@required this.text,
@required this.color,
@required this.font,
}) : super(x);
final double width;
final String text;
final String color;
final String font;
@override
Icon get icon => null;
@override
void paint(CanvasRenderingContext2D canvas) {
if (color != null) {
canvas.fillStyle = color;
}
if (font != null) {
canvas.font = font;
}
canvas.fillText(text, x, rowHeight - 7);
}
@override
double get right => x + width;
}
class InspectorTreeNodeRenderCanvasBuilder
extends InspectorTreeNodeRenderBuilder<InspectorTreeNodeCanvasRender> {
InspectorTreeNodeRenderCanvasBuilder({
@required DiagnosticLevel level,
@required DiagnosticsTreeStyle treeStyle,
}) : super(level: level, treeStyle: treeStyle);
double x = 0;
TextStyle lastStyle;
String font;
String color;
final List<CanvasPaintEntry> _entries = [];
static final CanvasRenderingContext2D _measurementCanvas =
CanvasElement(width: 1, height: 1).context2D;
@override
void appendText(String text, TextStyle textStyle) {
if (text == null || text.isEmpty) {
return;
}
if (textStyle != lastStyle) {
if (textStyle.color != lastStyle?.color) {
color = colorToCss(textStyle.color);
}
font = fontStyleToCss(textStyle);
lastStyle = textStyle;
_measurementCanvas.font = font;
}
final double width = _measurementCanvas.measureText(text).width;
_entries.add(TextPaintEntry(
x: x, width: width, text: text, color: color, font: font));
x += width;
}
@override
void addIcon(Icon icon) {
final double width = icon.iconWidth + iconPadding;
_entries.add(IconPaintEntry(x: x, iconRenderer: getIconRenderer(icon)));
x += width;
}
@override
InspectorTreeNodeCanvasRender build() {
return InspectorTreeNodeCanvasRender(_entries, Size(x, rowHeight));
}
}
class InspectorTreeNodeCanvasRender
extends InspectorTreeNodeRender<CanvasPaintEntry> {
InspectorTreeNodeCanvasRender(List<CanvasPaintEntry> entries, Size size)
: super(entries, size);
void paint(CanvasRenderingContext2D context, Rect visible) {
for (var entry in entries) {
if (entry.x + offset.dx > visible.right) {
return;
}
if (entry.right + offset.dx >= visible.left) {
entry.paint(context);
}
}
}
@override
PaintEntry hitTest(Offset location) {
if (offset == null) return null;
location = location - offset;
if (location.dy < 0 || location.dy >= size.height) {
return null;
}
// There is no need to optimize this but we could perform a binary search.
for (var entry in entries) {
if (entry.x <= location.dx && entry.right > location.dx) {
return entry;
}
}
return null;
}
}
class InspectorTreeNodeCanvas extends InspectorTreeNode {
@override
InspectorTreeNodeRenderBuilder createRenderBuilder() {
return InspectorTreeNodeRenderCanvasBuilder(
level: diagnostic.level,
treeStyle: diagnostic.style,
);
}
}
class InspectorTreeCanvas extends InspectorTreeFixedRowHeight
implements InspectorTreeWeb {
InspectorTreeCanvas({
@required bool summaryTree,
@required FlutterTreeType treeType,
@required NodeAddedCallback onNodeAdded,
VoidCallback onSelectionChange,
TreeEventCallback onExpand,
TreeHoverEventCallback onHover,
}) : super(
summaryTree: summaryTree,
treeType: treeType,
onNodeAdded: onNodeAdded,
onSelectionChange: onSelectionChange,
onExpand: onExpand,
onHover: onHover,
) {
_viewportCanvas = ViewportCanvas(
paintCallback: _paintCallback,
onTap: onTap,
onMouseMove: onMouseMove,
onMouseLeave: onMouseLeave,
onSizeChange: _updateForContainerResize,
classes: 'inspector-tree inspector-tree-container',
);
}
void _updateForContainerResize(Size size) {
_viewportCanvas.setContentSize(_computeContentWidth(size),
(rowHeight * numRows + verticalPadding * 2).toDouble());
}
void _paintCallback(CanvasRenderingContext2D canvas, Rect rect) {
final int startRow = getRowIndex(rect.top);
final int endRow = math.min(getRowIndex(rect.bottom) + 1, numRows);
for (int i = startRow; i < endRow; i++) {
paintRow(canvas, i, rect);
}
}
bool _recomputeRows = false;
@override
void setState(VoidCallback modifyState) {
// More closely match Flutter semantics where state is set immediately
// instead of after a frame.
modifyState();
if (!_recomputeRows) {
_recomputeRows = true;
window.requestAnimationFrame((_) => _rebuildData());
}
}
double _computeContentWidth(Size size) {
double maxIndent = 0;
for (int i = 0; i < numRows; i++) {
final InspectorTreeRow row = root?.getRow(i, selection: selection);
if (row != null) {
maxIndent = max(maxIndent, getDepthIndent(row.depth));
}
}
return maxIndent + size.width;
}
void _rebuildData() {
if (_recomputeRows) {
_recomputeRows = false;
if (root != null) {
_updateForContainerResize(_viewportCanvas.viewport.size);
} else {
_viewportCanvas.setContentSize(0, 0);
}
}
_viewportCanvas.rebuild(force: true);
}
@override
String get tooltip => _viewportCanvas.element.tooltip;
@override
set tooltip(String value) {
_viewportCanvas.element.tooltip = value;
}
void onMouseLeave() {
if (onHover != null) {
onHover(null, null);
}
}
ViewportCanvas _viewportCanvas;
@override
CoreElement get element => _viewportCanvas.element;
@override
InspectorTreeNode createNode() => InspectorTreeNodeCanvas();
void paintRow(
CanvasRenderingContext2D canvas,
int index,
Rect visible,
) {
canvas.save();
final double y = getRowY(index);
canvas.translate(0, y);
// Variables incremented as part of painting.
double currentX = 0;
Color currentColor;
bool isVisible(double width) {
return currentX <= visible.right && visible.left <= currentX + width;
}
final InspectorTreeRow row = root?.getRow(index, selection: selection);
if (row == null) {
return;
}
final InspectorTreeNode node = row.node;
final bool showExpandCollapse = node.showExpandCollapse;
final InspectorTreeNodeCanvasRender renderObject = node.renderObject;
bool hasPath = false;
void _endPath() {
if (!hasPath) return;
canvas.stroke();
hasPath = false;
}
void _maybeStart([Color color = Colors.grey]) {
if (color != currentColor) {
_endPath();
}
if (hasPath) return;
hasPath = true;
canvas.beginPath();
if (currentColor != color) {
currentColor = color;
canvas.strokeStyle = colorToCss(color);
}
canvas.lineWidth = chartLineStrokeWidth;
}
for (int tick in row.ticks) {
currentX = getDepthIndent(tick) - columnWidth * 0.5;
if (isVisible(1.0)) {
final highlight = row.highlightDepth == tick;
_maybeStart(highlight ? highlightLineColor : defaultTreeLineColor);
canvas
..moveTo(currentX, 0.0)
..lineTo(currentX, rowHeight);
}
}
if (row.lineToParent) {
final highlight = row.highlightDepth == row.depth - 1;
currentX = getDepthIndent(row.depth - 1) - columnWidth * 0.5;
final double width = showExpandCollapse ? columnWidth * 0.5 : columnWidth;
if (isVisible(width)) {
_maybeStart(highlight ? highlightLineColor : defaultTreeLineColor);
canvas
..moveTo(currentX, 0.0)
..lineTo(currentX, rowHeight * 0.5)
..lineTo(currentX + width, rowHeight * 0.5);
}
}
_endPath();
// Render the main row content.
currentX = getDepthIndent(row.depth) - columnWidth;
if (!row.node.showExpandCollapse) {
currentX += columnWidth;
}
if (renderObject == null) {
// Short circuit as nothing can be drawn within the view.
canvas.restore();
return;
}
renderObject.attach(this, Offset(currentX, y));
final Rect paintBounds = renderObject.paintBounds;
if (!paintBounds.overlaps(visible)) {
canvas.restore();
return;
}
if (row.isSelected || row.node == hover) {
final Color backgroundColor =
row.isSelected ? selectedRowBackgroundColor : hoverColor;
final double x = getDepthIndent(row.depth) - columnWidth * 0.15;
if (x <= visible.right) {
final fillStyle = canvas.fillStyle;
canvas.fillStyle = colorToCss(backgroundColor);
canvas.fillRect(
x, 0.0, math.min(visible.right, paintBounds.right) - x, rowHeight);
canvas.fillStyle = fillStyle;
}
}
// We have already translated to the offset in the y axis.
canvas.translate(currentX, 0);
renderObject.paint(canvas, visible);
canvas.restore();
}
@override
Rect getBoundingBox(InspectorTreeRow row) {
return Rect.fromLTWH(
getDepthIndent(row.depth),
getRowY(row.index),
// Hack as we don't know exactly how wide the row is (yet).
// TODO(jacobr): tweak measurement code so we do.
_viewportCanvas.viewport.width * .7,
rowHeight,
);
}
@override
void scrollToRect(Rect targetRect) {
_viewportCanvas.scrollToRect(targetRect);
}
}