Last active
March 6, 2022 14:49
-
-
Save rrousselGit/3dae341d1ad4051a45bd0bf00f77b053 to your computer and use it in GitHub Desktop.
Graph table inspector
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math'; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MaterialApp(home: MyHomePage())); | |
} | |
class MyHomePage extends StatefulWidget { | |
const MyHomePage({Key? key}) : super(key: key); | |
@override | |
State<MyHomePage> createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
// Initial selection 'cause why not | |
List<int>? selectedNode = [ | |
initialGraph.values.first.id, | |
initialGraph.values.first.children.values.first.id, | |
initialGraph.values.first.children.values.first.children.values.first.id, | |
initialGraph.values.first.children.values.first.children.values.first | |
.children.values.first.id, | |
]; | |
var graph = initialGraph; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Center( | |
child: Padding( | |
padding: const EdgeInsets.all(40), | |
child: GraphTable( | |
graph: graph, | |
selectedNodePath: selectedNode, | |
onGraphChange: (graph) => setState(() => this.graph = graph), | |
onSelectedNodeChange: (node) => setState(() => selectedNode = node), | |
), | |
), | |
), | |
); | |
} | |
} | |
class GraphTable extends StatelessWidget { | |
const GraphTable({ | |
Key? key, | |
required this.graph, | |
required this.selectedNodePath, | |
required this.onGraphChange, | |
required this.onSelectedNodeChange, | |
}) : super(key: key); | |
final Map<int, Node> graph; | |
final List<int>? selectedNodePath; | |
final ValueChanged<Map<int, Node>>? onGraphChange; | |
final ValueChanged<List<int>?>? onSelectedNodeChange; | |
@override | |
Widget build(BuildContext context) { | |
var _siblings = graph; | |
final selectedNodes = selectedNodePath?.map((id) { | |
final node = _siblings[id]; | |
_siblings = node!.children; | |
return node; | |
}).toList(); | |
return Card( | |
child: Column( | |
children: [ | |
_GraphTableHead( | |
selectedNodes: selectedNodes, | |
onSelectedNodeChange: onSelectedNodeChange, | |
), | |
Expanded( | |
child: LayoutBuilder( | |
builder: (context, constraints) { | |
return _GraphTableListView( | |
selectedNodePath: selectedNodePath, | |
selectedNodes: selectedNodes, | |
graph: graph, | |
onGraphChange: onGraphChange, | |
onSelectedNodeChange: onSelectedNodeChange, | |
columnSize: constraints.maxWidth / 3, | |
); | |
}, | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class _GraphTableListView extends StatefulWidget { | |
const _GraphTableListView({ | |
Key? key, | |
required this.selectedNodePath, | |
required this.selectedNodes, | |
required this.graph, | |
required this.columnSize, | |
required this.onGraphChange, | |
required this.onSelectedNodeChange, | |
}) : super(key: key); | |
final List<int>? selectedNodePath; | |
final List<Node>? selectedNodes; | |
final Map<int, Node> graph; | |
final double columnSize; | |
final ValueChanged<List<int>?>? onSelectedNodeChange; | |
final ValueChanged<Map<int, Node>>? onGraphChange; | |
@override | |
State<_GraphTableListView> createState() => _GraphTableListViewState(); | |
} | |
class _GraphTableListViewState extends State<_GraphTableListView> { | |
final scrollController = ScrollController(); | |
@override | |
void didUpdateWidget(covariant _GraphTableListView oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (oldWidget.selectedNodePath?.length != widget.selectedNodePath?.length || | |
oldWidget.columnSize != widget.columnSize) { | |
final length = max((widget.selectedNodePath?.length ?? 0) - 2, 0); | |
final to = widget.columnSize * length; | |
scrollController.animateTo( | |
to, | |
duration: const Duration(milliseconds: 150), | |
curve: Curves.easeOut, | |
); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
final selectedNodes = widget.selectedNodes; | |
return ListView.builder( | |
physics: const NeverScrollableScrollPhysics(), | |
controller: scrollController, | |
scrollDirection: Axis.horizontal, | |
itemExtent: widget.columnSize, | |
itemCount: selectedNodes != null ? selectedNodes.length + 2 : 2, | |
itemBuilder: (context, index) { | |
Map<int, Node> nodesAtIndex; | |
if (selectedNodes == null || index == 0) { | |
nodesAtIndex = widget.graph; | |
} else if (index - 1 < selectedNodes.length) { | |
nodesAtIndex = selectedNodes[index - 1].children; | |
} else { | |
nodesAtIndex = {}; | |
} | |
return _GraphColumn( | |
nodes: nodesAtIndex, | |
selectedNodePath: widget.selectedNodePath, | |
onAdd: () { | |
final newNode = Node('New node', id: Node._nextId++); | |
final updatedPath = [ | |
...?widget.selectedNodePath?.take(index), | |
newNode.id, | |
]; | |
print('here $updatedPath'); | |
widget.onGraphChange?.call( | |
_cloneWithUpdatedNodeAtPath( | |
updatedPath, | |
newNode, | |
widget.graph, | |
), | |
); | |
}, | |
onTapNode: (node) { | |
widget.onSelectedNodeChange?.call( | |
[...?widget.selectedNodePath?.take(index), node.id], | |
); | |
}, | |
); | |
}, | |
); | |
} | |
} | |
class _GraphColumn extends StatelessWidget { | |
const _GraphColumn({ | |
Key? key, | |
required this.nodes, | |
required this.selectedNodePath, | |
required this.onTapNode, | |
required this.onAdd, | |
}) : super(key: key); | |
final Map<int, Node>? nodes; | |
final List<int>? selectedNodePath; | |
final ValueChanged<Node>? onTapNode; | |
final VoidCallback? onAdd; | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
children: [ | |
ListTile( | |
dense: true, | |
textColor: Colors.blue, | |
iconColor: Colors.blue, | |
minLeadingWidth: 0, | |
onTap: onAdd, | |
leading: const Icon(Icons.add), | |
title: const Text('Add', style: TextStyle(fontWeight: FontWeight.w500)), | |
), | |
if (nodes != null) | |
for (final child in nodes!.values) | |
ListTile( | |
dense: true, | |
onTap: onTapNode == null ? null : () => onTapNode!(child), | |
selected: selectedNodePath?.contains(child.id) ?? false, | |
selectedTileColor: Colors.grey.shade200, | |
title: Text(child.label), | |
), | |
], | |
); | |
} | |
} | |
class _GraphTableHead extends StatelessWidget { | |
const _GraphTableHead({ | |
Key? key, | |
required this.selectedNodes, | |
required this.onSelectedNodeChange, | |
}) : super(key: key); | |
final List<Node>? selectedNodes; | |
final ValueChanged<List<int>?>? onSelectedNodeChange; | |
@override | |
Widget build(BuildContext context) { | |
final selectedNodes = this.selectedNodes; | |
return Container( | |
color: Colors.grey.shade100, | |
padding: const EdgeInsets.all(8), | |
alignment: Alignment.centerLeft, | |
child: Wrap( | |
crossAxisAlignment: WrapCrossAlignment.center, | |
children: [ | |
GestureDetector( | |
onTap: () => onSelectedNodeChange?.call(const []), | |
child: Icon( | |
Icons.home, | |
color: Colors.grey.shade600, | |
), | |
), | |
if (selectedNodes != null) | |
for (var i = 0; i < selectedNodes.length; i++) ...[ | |
Icon( | |
Icons.chevron_right, | |
color: Colors.grey.shade600, | |
), | |
GestureDetector( | |
onTap: () { | |
onSelectedNodeChange?.call( | |
selectedNodes.take(i + 1).map((node) => node.id).toList(), | |
); | |
}, | |
child: Text( | |
selectedNodes[i].label, | |
style: TextStyle(color: Colors.grey.shade600), | |
), | |
), | |
], | |
], | |
), | |
); | |
} | |
} | |
// Not important, just barebone graph logic | |
class Node { | |
Node( | |
this.label, { | |
this.children = const {}, | |
required this.id, | |
}); | |
static int _nextId = 0; | |
final int id; | |
final String label; | |
final Map<int, Node> children; | |
} | |
Map<int, Node> _cloneWithUpdatedNodeAtPath( | |
List<int> path, | |
Node node, | |
Map<int, Node> roots, | |
) { | |
assert(path.isNotEmpty); | |
assert(path.last == node.id); | |
Map<int, Node> updateGraph(Map<int, Node> nodes, {required int offset}) { | |
if (offset >= path.length) return const {}; | |
if (offset == path.length - 1) { | |
return { | |
...nodes, | |
path.last: node, | |
}; | |
} | |
return { | |
for (final entry in nodes.entries) | |
if (entry.value.id == path[offset]) | |
entry.key: Node( | |
entry.value.label, | |
id: entry.value.id, | |
children: updateGraph( | |
entry.value.children, | |
offset: offset + 1, | |
), | |
) | |
else | |
entry.key: entry.value, | |
}; | |
} | |
return updateGraph(roots, offset: 0); | |
} | |
Map<int, Node> _generateData([int depth = 0]) { | |
if (depth > 6) return const {}; | |
final entries = List.generate(3, (index) { | |
final id = Node._nextId++; | |
return MapEntry( | |
id, | |
Node( | |
'$depth-$index', | |
id: id, | |
children: _generateData(depth + 1), | |
), | |
); | |
}); | |
return Map.fromEntries(entries); | |
} | |
final initialGraph = _generateData(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment