blob: 4abb7fa693aac5e13614353173ad8ce9cf0820c8 [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.
// TODO(jacobr): make this class production quality. It is missing some
// important edge cases.
/// Use this library to render an inspector tree to HTML elements instead of
/// canvas.
library inspector_tree_html;
import 'dart:html';
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 'diagnostics_node.dart';
import 'inspector_service.dart';
import 'inspector_text_styles.dart';
import 'inspector_tree.dart';
import 'inspector_tree_web.dart';
abstract class HtmlPaintEntry extends PaintEntry {
void paint(Element parent);
Element element;
}
class IconPaintEntry extends HtmlPaintEntry {
IconPaintEntry({
@required this.iconRenderer,
});
@override
Icon get icon => iconRenderer.icon;
final HtmlIconRenderer iconRenderer;
@override
void paint(Element parent) {
element = iconRenderer.createElement();
parent.append(element);
}
@override
void attach(InspectorTree owner) {
// The browser handles painting images when they are available so we don't
// have to do anything.
}
}
class HtmlTextPaintEntry extends HtmlPaintEntry {
HtmlTextPaintEntry({
@required this.text,
@required this.color,
@required this.font,
});
final String text;
final String color;
final String font;
@override
Icon get icon => null;
@override
void paint(Element parent) {
element = Element.span()..text = text;
if (color != null) {
element.style.color = color;
}
if (font != null) {
element.style.font = font;
}
parent.append(element);
}
}
class InspectorTreeNodeRenderHtmlBuilder
extends InspectorTreeNodeRenderBuilder<InspectorTreeNodeHtmlRender> {
InspectorTreeNodeRenderHtmlBuilder({
@required DiagnosticLevel level,
@required DiagnosticsTreeStyle treeStyle,
@required this.allowWrap,
}) : super(level: level, treeStyle: treeStyle);
TextStyle lastStyle;
String font;
String color;
final bool allowWrap;
final List<HtmlPaintEntry> _entries = [];
@override
void appendText(String text, TextStyle textStyle) {
if (text == null || text.isEmpty) {
return;
}
if (textStyle != lastStyle) {
if (textStyle.color != lastStyle?.color) {
if (textStyle.color == regular.color) {
color = null;
} else {
color = colorToCss(textStyle.color);
}
}
if (textStyle == regular) {
font = null;
} else {
font = fontStyleToCss(textStyle);
}
lastStyle = textStyle;
}
_entries.add(HtmlTextPaintEntry(text: text, color: color, font: font));
}
@override
void addIcon(Icon icon) {
_entries.add(IconPaintEntry(iconRenderer: getIconRenderer(icon)));
}
@override
InspectorTreeNodeHtmlRender build() {
// The html renderer does not know what its size is.
final classes = [
'inspector-level-${diagnosticLevelToName[level]}',
'inspector-style-${treeStyleToName[treeStyle]}',
];
if (!allowWrap) {
classes.add('inspector-no-wrap');
}
return InspectorTreeNodeHtmlRender(_entries, const Size(0, 0), classes);
}
}
class InspectorTreeNodeHtmlRender
extends InspectorTreeNodeRender<HtmlPaintEntry> {
InspectorTreeNodeHtmlRender(
List<HtmlPaintEntry> entries, Size size, this.cssClasses)
: super(entries, size);
final List<String> cssClasses;
void paint(Element container) {
container.classes.addAll(cssClasses);
element = container;
for (var entry in entries) {
entry.paint(container);
}
}
Element element;
@override
PaintEntry hitTest(Offset location) {
// TODO(jacobr): consider removing this method from the base class.
throw 'Not yet supported by HTML tree';
}
}
class InspectorTreeNodeHtml extends InspectorTreeNode {
@override
InspectorTreeNodeRenderBuilder createRenderBuilder() {
return InspectorTreeNodeRenderHtmlBuilder(
level: diagnostic.level,
treeStyle: diagnostic.style,
allowWrap: diagnostic.allowWrap,
);
}
}
class InspectorTreeHtml extends InspectorTree implements InspectorTreeWeb {
InspectorTreeHtml({
@required bool summaryTree,
@required FlutterTreeType treeType,
NodeAddedCallback onNodeAdded,
VoidCallback onSelectionChange,
TreeEventCallback onExpand,
TreeHoverEventCallback onHover,
}) : _container = div(c: 'inspector-tree-html'),
super(
summaryTree: summaryTree,
treeType: treeType,
onNodeAdded: onNodeAdded,
onSelectionChange: onSelectionChange,
onExpand: onExpand,
onHover: onHover,
) {
_container.onClick.listen(onMouseClick);
_container.element.onMouseMove.listen(onMouseMove);
_container.element.onMouseLeave.listen(onMouseLeave);
}
InspectorTreeRow _resolveTreeRow(Element e) {
while (e != null && !e.classes.contains('inspector-tree-row')) {
e = e.parent;
}
if (e == null) {
return null;
}
final parent = e.parent;
final int index = parent.children.indexOf(e);
assert(index >= 0 && index < numRows);
final row = root.getRow(index, selection: selection);
// TODO(jacobr): figure out why this assert is sometimes failing.
// final InspectorTreeNodeHtmlRender render = row.node.renderObject;
// assert(render.element.parent == e);
return row;
}
Icon _resolveIcon(InspectorTreeRow row, Element e) {
final InspectorTreeNodeHtmlRender render = row?.node?.renderObject;
if (render == null) {
return null;
}
while (e != null && !e.classes.contains('flutter-icon')) {
if (e == render.element) {
return null;
}
e = e.parent;
}
if (e == null) {
return null;
}
for (var entry in render.entries) {
if (entry.element == e) {
return entry.icon;
}
}
return null;
}
final CoreElement _container;
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());
}
}
void _rebuildData() {
if (_recomputeRows) {
_recomputeRows = false;
if (root == null) {
_container.clear();
return;
}
final int rowCount = numRows;
// TODO(jacobr): make this rebuild more incremental.
_container.clear();
for (int i = 0; i < rowCount; i++) {
_container.element.append(paintRow(i, selection: selection));
}
}
}
void onMouseClick(MouseEvent mouseEvent) {
final row = _resolveTreeRow(mouseEvent.target);
if (row == null) {
return;
}
final Icon icon = _resolveIcon(row, mouseEvent.target);
if (row != null) {
onTapIcon(row, icon);
}
}
void onMouseMove(MouseEvent mouseEvent) {
if (onHover != null) {
// TODO(jacobr): determine the icon
onHover(_resolveTreeRow(mouseEvent.target)?.node, null);
}
}
void onMouseLeave(MouseEvent mouseEvent) {
if (onHover != null) {
onHover(null, null);
}
}
@override
CoreElement get element => _container;
@override
InspectorTreeNode createNode() => InspectorTreeNodeHtml();
// Horizontal padding is specified by CSS so including it here would throw
// off calculations.
@override
double get horizontalPadding => 0.0;
Element paintRow(
int index, {
@required InspectorTreeNode selection,
}) {
try {
final container = Element.div();
container.classes.add('inspector-tree-row');
// Variables incremented as part of painting.
double currentX = 0;
final InspectorTreeRow row = root?.getRow(index, selection: selection);
if (row == null) {
return container;
}
final InspectorTreeNode node = row.node;
final diagnostic = node.diagnostic;
// Add an has_property helper on RemoteDiagnosticsNode.
if (diagnostic != null &&
diagnostic.name?.isNotEmpty == true &&
diagnostic.showName &&
diagnostic.showSeparator &&
diagnostic.description != null) {
container.classes.add('property-value');
}
// final bool showExpandCollapse = node.showExpandCollapse;
final InspectorTreeNodeHtmlRender renderObject = node.renderObject;
// TODO(jacobr): port this code to work for the html renderer to support
// drawing lines describing the tree. Likely the best way to render this
// ui is by abusing CSS for drawing borders although we could also consider
// prerendering some base64 images.
/*
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.
if (renderObject == null) {
return container;
}
currentX = getDepthIndent(row.depth - 1) - columnWidth;
if (!row.node.showExpandCollapse) {
currentX += columnWidth;
}
final rowContentContainer = Element.div();
rowContentContainer.classes.add('inspector-tree-row-content');
rowContentContainer.style.paddingLeft = '${currentX}px';
final rowContent = Element.div();
rowContentContainer.append(rowContent);
renderObject.paint(rowContent);
container.append(rowContentContainer);
// TODO(jacobr): handle row selected backgrounds using CSS classes.
return container;
} catch (e, s) {
print(s);
return Element.div()..text = 'Error: $e, $s';
}
}
@override
void animateToTargets(List<InspectorTreeNode> targets) {
// TODO: implement animateToTargets
window.requestAnimationFrame((_) {
for (var target in targets.reversed) {
final InspectorTreeNodeHtmlRender renderObject = target.renderObject;
// TODO(jacobr): be smarter about not calling this on all elements.
renderObject?.element?.scrollIntoView();
}
});
}
@override
String get tooltip => element.tooltip;
@override
set tooltip(String value) {
element.tooltip = value;
}
}