blob: e7c6da71f5d5f5f7110e52cb9a06d036a3cb2130 [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 'package:fidl_fuchsia_modular/fidl_async.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
import 'package:fuchsia_logger/logger.dart';
import 'package:lib.widgets/model.dart';
import '../models/surface/surface.dart';
import '../models/surface/surface_graph.dart';
import 'surface_resize.dart';
/// Debug Flag
const bool _storytellerDebug = true;
/// Size/Padding/Margin related constants
const double _labelWidgetHeight = 30.0;
const double _labelHorizontalPadding = 24.0;
const double _pageHorizontalPadding = 28.0;
const double _pageTopPadding = 28.0;
const double _branchWidth = 160.0;
const double _branchPadding = 48.0;
const double _surfaceWidgetWidth = 150.0;
const double _surfaceWidgetHeight = 100.0;
const double _surfaceWidgetTextHeight = 30.0;
const double _relationshipWidgetWidth = 120.0;
const double _relationshipWidgetHeight = 60.0;
const double _relationshipTreeFontSize = 10.0;
const FontWeight _relationshipTreeFontWeight = FontWeight.w200;
const double _iconSize = 12.0;
const Color _storytellerPrimaryColor = Color(0xFF6315F6);
/// Printable names for relation arrangement
const Map<SurfaceArrangement, String> relName =
<SurfaceArrangement, String>{
SurfaceArrangement.none: 'None',
SurfaceArrangement.copresent: 'Co-present',
SurfaceArrangement.sequential: 'Sequential',
SurfaceArrangement.ontop: 'Ontop',
};
/// Printable names for relation dependency
const Map<SurfaceDependency, String> depName =
<SurfaceDependency, String>{
SurfaceDependency.dependent: 'Dependent',
SurfaceDependency.none: 'Independent',
};
/// A tree graph that shows the surface relationships (StoryTeller).
///
/// Shows the depth of the tree, presentational relationships, dependencies,
/// and emphasis between parent and child surfaces.
/// Also shows the state of each surface via [_buildStateIndicator].
class SurfaceRelationships extends StatelessWidget {
/// Constructor
const SurfaceRelationships({Key key}) : super(key: key);
@override
Widget build(BuildContext context) => LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Container(
width: constraints.maxWidth,
height: constraints.maxHeight,
color: Colors.grey[100],
child: ScopedModelDescendant<SurfaceGraph>(
builder:
(BuildContext context, Widget child, SurfaceGraph graph) {
if (graph.focusStack.isEmpty) {
log.warning('focusedSurfaceHistory is empty');
return Container();
}
return _buildRelationshipsPage(context, constraints, graph);
},
),
);
},
);
Widget _buildRelationshipsPage(
BuildContext context, BoxConstraints constraints, SurfaceGraph graph) {
Map<String, GlobalKey> surfaceKeys = <String, GlobalKey>{};
GlobalKey stackKey = GlobalKey();
Set<Surface> firstDepthSurfaces = <Surface>{};
for (Surface s in graph.focusStack.toList()) {
firstDepthSurfaces.add(s.root);
}
int maxHeight = 0;
for (Surface s in firstDepthSurfaces) {
if (s.node.height > maxHeight) {
maxHeight = s.node.height;
}
}
if (_storytellerDebug) {
log.info('*** The height of the StoryGraph is $maxHeight');
}
double totalLabelWidth = _pageHorizontalPadding * 2 +
_surfaceWidgetWidth +
(_surfaceWidgetWidth + _branchWidth) * (maxHeight);
if (totalLabelWidth < MediaQuery.of(context).size.width) {
totalLabelWidth = MediaQuery.of(context).size.width;
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildLabel(totalLabelWidth, maxHeight),
_buildContent(graph, stackKey, surfaceKeys, firstDepthSurfaces),
],
),
);
}
Widget _buildLabel(double labelWidth, int maxHeight) {
return Material(
elevation: 4.0,
child: Container(
width: labelWidth,
child: Row(
children: _buildLabelWidgetList(maxHeight),
),
),
);
}
Widget _buildContent(SurfaceGraph graph, GlobalKey stackKey,
Map<String, GlobalKey> surfaceKeys, Set<Surface> firstSurfaces) {
return Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Stack(
key: stackKey,
children: <Widget>[
_buildEdges(stackKey, surfaceKeys, firstSurfaces),
_buildNodes(graph, surfaceKeys, firstSurfaces),
],
),
),
);
}
Widget _buildEdges(GlobalKey stackKey, Map<String, GlobalKey> surfaceKeys,
Set<Surface> firstSurfaces) {
return CustomPaint(
painter: _RelationshipTreeEdges(
firstSurfaces: firstSurfaces,
surfaceKeys: surfaceKeys,
backgroundKey: stackKey,
),
);
}
Widget _buildNodes(SurfaceGraph graph, Map<String, GlobalKey> surfaceKeys,
Set<Surface> firstSurfaces) {
return Container(
padding: EdgeInsets.only(
left: _pageHorizontalPadding,
right: _pageHorizontalPadding,
top: _pageTopPadding,
),
child: Column(
children: firstSurfaces
.map((surface) => _buildTree(surfaceKeys, graph, surface,
(graph.focusStack.last == surface)))
.toList(),
),
);
}
/// Builds the labels that shows the depth of each column.
List<Widget> _buildLabelWidgetList(int maxHeight) {
List<Widget> labelWidgets = <Widget>[];
double totalLength = _pageHorizontalPadding + _surfaceWidgetWidth;
labelWidgets.add(Container(
/// Depth 1
padding: EdgeInsets.only(right: _labelHorizontalPadding),
width: totalLength,
height: _labelWidgetHeight,
decoration: _labelBoxDecoration(true, true),
alignment: Alignment.centerRight,
child: Text(
'Depth 1',
style: TextStyle(
color: Colors.black,
fontSize: 10.0,
),
),
));
double labelWidgetWidth = _surfaceWidgetWidth + _branchWidth;
// Other labels: Depth 2 ~
for (int i = 0; i < maxHeight; i++) {
totalLength += labelWidgetWidth;
labelWidgets.add(Container(
padding: EdgeInsets.only(right: _labelHorizontalPadding),
width: labelWidgetWidth,
height: _labelWidgetHeight,
decoration: _labelBoxDecoration(true, true),
alignment: Alignment.centerRight,
child: Text(
'Depth ${i + 2}',
style: TextStyle(
color: Colors.black,
fontSize: 10.0,
),
),
));
}
labelWidgets.add(Expanded(
child: Container(
height: _labelWidgetHeight,
decoration: _labelBoxDecoration(false, true),
),
));
return labelWidgets;
}
BoxDecoration _labelBoxDecoration(bool rightBorder, bool bottomBorder) {
return BoxDecoration(
color: Colors.white,
border: Border(
right: BorderSide(
color: rightBorder ? Colors.grey[400] : Colors.transparent,
width: 0.5,
),
bottom: BorderSide(
color: bottomBorder ? Colors.grey[400] : Colors.transparent,
width: 0.5,
),
),
);
}
/// Builds the surface relationship tree recursively.
///
/// The tree's depth grows horizontally.
/// The sibling nodes are added vertically.
Widget _buildTree(Map<String, GlobalKey> globalKeys, SurfaceGraph graph,
Surface surface, bool isFocused) {
if (_storytellerDebug) {
if (graph == null) {
log.shout('*** The storyGraph is null!');
}
if (surface == null) {
log.shout('*** The current surface is null!');
}
}
assert(graph != null);
assert(surface != null);
if (_storytellerDebug) {
log
..info('*** This surface ID: ${surface.node.value}')
..info('*** This surface\'s parentId: ${surface.parentId}')
..info(
'*** The length of the focus stack: ${graph.focusStack.toList().length}');
}
String id = surface.node.value;
globalKeys[id] = GlobalKey();
Widget thisWidget = _buildChildNode(globalKeys[id], surface, isFocused);
List<Surface> children = List<Surface>.from(surface.children.toList())
..removeWhere((child) => child == null);
if (_storytellerDebug) {
log.info(
'*** BUILDING TREE... surface($id)\'s global key = ${globalKeys[id]}');
}
if (_storytellerDebug) {
log.info('*** The number of children of surface $id: ${children.length}');
}
if (children.isEmpty) {
return thisWidget;
} else {
if (_storytellerDebug) {
log.info('*** The first child: ${children[0]}');
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
thisWidget,
(children.length == 1)
? _buildTree(globalKeys, graph, children[0],
(graph.focusStack.toList().last == children[0]))
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children.map((child) {
return _buildTree(globalKeys, graph, child,
(graph.focusStack.toList().last == child));
}).toList(),
),
],
);
}
}
/// Returns a container containing a relationship widget and a surface widget
Widget _buildChildNode(GlobalKey key, Surface surface, bool isFocused) {
bool isFirstDepthNode = (surface.parentId == null);
return Container(
padding: EdgeInsets.only(bottom: _branchPadding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
(isFirstDepthNode)
? Container()
: _buildRelationshipWidget(surface),
_buildSurfaceWidget(key, surface, isFocused),
],
),
);
}
/// Builds a branch tag of the tree that shows the relationship between
/// a surface and its parent.
///
/// This tag appears on a branch between two surfaces.
Widget _buildRelationshipWidget(Surface surface) {
String presentation = relName[surface.relation.arrangement] ?? 'Unknown';
String dependency = depName[surface.relation.dependency] ?? 'Unknown';
String emphasis = surface.relation.emphasis.toStringAsPrecision(2);
return Container(
width: _branchWidth,
height: _surfaceWidgetHeight,
child: Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
width: _relationshipWidgetWidth,
height: _relationshipWidgetHeight,
decoration: _relationshipBoxDeco(dependency),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildRelationshipRow(Icons.filter, presentation, dependency),
_buildRelationshipRow(Icons.link, dependency, dependency),
_buildRelationshipRow(Icons.data_usage, emphasis, dependency),
],
),
),
),
);
}
Widget _buildRelationshipRow(IconData icon, String text, String dependency) {
return Row(
children: <Widget>[
_relationshipIcon(icon, dependency),
Container(
padding: EdgeInsets.only(left: 10.0),
child: Text(
text,
style: _relationshipTextStyle(dependency),
),
),
],
);
}
BoxDecoration _relationshipBoxDeco(String dependency) {
return BoxDecoration(
color: (dependency == 'Independent')
? Colors.white
: _storytellerPrimaryColor,
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: _storytellerPrimaryColor,
width: 2.0,
),
);
}
TextStyle _relationshipTextStyle(String dependency) {
return TextStyle(
color: (dependency == 'Independent')
? _storytellerPrimaryColor
: Colors.white,
fontSize: _relationshipTreeFontSize,
fontWeight: _relationshipTreeFontWeight,
);
}
Icon _relationshipIcon(IconData icon, String dependency) {
return Icon(
icon,
color: (dependency == 'Independent')
? _storytellerPrimaryColor
: Colors.white,
size: _iconSize,
);
}
/// Builds a node of the tree that shows a surface, the surface's id and state.
Widget _buildSurfaceWidget(GlobalKey key, Surface surface, bool isFocused) {
return Container(
height: _surfaceWidgetHeight + _surfaceWidgetTextHeight + 8.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
_buildSurfaceImage(key, surface),
_buildSurfaceInfoText(surface, isFocused),
],
),
);
}
Widget _buildSurfaceImage(GlobalKey key, Surface surface) {
return Material(
key: key,
color: Colors.white,
elevation: 2.0,
borderRadius: BorderRadius.circular(8.0),
child: Container(
width: _surfaceWidgetWidth,
height: _surfaceWidgetHeight,
child: Center(
child: SurfaceResize(
child: ChildView(
connection: surface.connection,
hitTestable: false,
),
),
),
),
);
}
Widget _buildSurfaceInfoText(Surface surface, bool isFocused) {
return Container(
width: _surfaceWidgetWidth,
height: _surfaceWidgetTextHeight,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildStateIndicator(surface.dismissed, isFocused),
Expanded(
child: Text(
'${surface.node.value}',
style: TextStyle(
color: (surface.dismissed) ? Colors.grey[400] : Colors.black,
fontSize: _relationshipTreeFontSize,
fontWeight: _relationshipTreeFontWeight,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: TextAlign.start,
),
),
],
),
);
}
/// Shows the state of a surface.
///
/// Focused: A violet circle.
/// Active: A white circle with violet border.
/// Dismissed: A grey circle.
Widget _buildStateIndicator(bool isDismissed, bool isFocused) {
Color stateFillColor;
Color stateBorderColor;
if (isDismissed) {
stateFillColor = Colors.grey[400];
stateBorderColor = Colors.grey[400];
} else if (isFocused) {
stateFillColor = _storytellerPrimaryColor;
stateBorderColor = _storytellerPrimaryColor;
} else {
stateFillColor = Colors.white;
stateBorderColor = _storytellerPrimaryColor;
}
return Padding(
padding: EdgeInsets.only(top: 4.0, left: 8.0, right: 8.0),
child: Container(
width: 6.0,
height: 6.0,
decoration: BoxDecoration(
color: stateFillColor,
borderRadius: BorderRadius.circular(3.0),
border: Border.all(
width: 1.0,
color: stateBorderColor,
),
),
),
);
}
}
/// The edge lines between parent and child surfaces.
///
/// The position of an edge is decided by the positions of the surface widget.
class _RelationshipTreeEdges extends CustomPainter {
final Set<Surface> firstSurfaces;
final Map<String, GlobalKey> surfaceKeys;
final GlobalKey backgroundKey;
_RelationshipTreeEdges(
{@required this.firstSurfaces,
@required this.surfaceKeys,
@required this.backgroundKey});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = _storytellerPrimaryColor
..strokeWidth = 1.5
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
final bgContext = backgroundKey.currentContext;
final RenderBox bgBox = bgContext.findRenderObject();
Set<Surface> currentSurfaces = Set<Surface>.from(firstSurfaces);
while (currentSurfaces.isNotEmpty) {
for (Surface surface in currentSurfaces) {
double startX;
double endX;
double minY;
double maxY;
// Draws horizontal edges
for (int i = 0; i < surface.children.length; i++) {
String childId = surface.children.toList()[i].node.value;
final childSurfaceContext = surfaceKeys[childId].currentContext;
final RenderBox childSurfaceBox =
childSurfaceContext.findRenderObject();
final childSurfaceGlobalPos =
childSurfaceBox.localToGlobal(Offset(0.0, 0.0));
final childSurfacePos = bgBox.globalToLocal(childSurfaceGlobalPos);
if (_storytellerDebug) {
log
..info('*** x: ${childSurfacePos.dx}')
..info('*** y: ${childSurfacePos.dy}');
}
double y = childSurfacePos.dy + (childSurfaceBox.size.height / 2);
if (i == 0) {
startX = childSurfacePos.dx - _branchWidth;
endX = childSurfacePos.dx;
minY = y;
} else if (i == 1) {
startX += (_branchWidth - _relationshipWidgetWidth) / 4;
}
maxY = y;
canvas.drawLine(Offset(startX, y), Offset(endX, y), paint);
}
// Draws a vertical edge
if (surface.children.length > 1) {
canvas.drawLine(Offset(startX, minY), Offset(startX, maxY), paint);
}
}
Set<Surface> nextSurfaces = <Surface>{};
for (Surface s in currentSurfaces) {
for (int i = 0; i < s.children.length; i++) {
nextSurfaces.add(s.children.toList()[i]);
}
}
currentSurfaces = nextSurfaces;
}
}
@override
bool shouldRepaint(_RelationshipTreeEdges oldDelegate) {
return false;
}
}