Skip to content

Instantly share code, notes, and snippets.

@rrousselGit
Last active March 6, 2022 14:49
Show Gist options
  • Save rrousselGit/3dae341d1ad4051a45bd0bf00f77b053 to your computer and use it in GitHub Desktop.
Save rrousselGit/3dae341d1ad4051a45bd0bf00f77b053 to your computer and use it in GitHub Desktop.
Graph table inspector
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