blob: ee481079fbdbd448e7e8f3f1d794162b6284673a [file] [log] [blame]
// Copyright 2020 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 'package:flutter/material.dart' hide Stack;
import 'collapsible_mixin.dart';
import 'theme.dart';
import 'trees.dart';
class TreeView<T extends TreeNode<T>> extends StatefulWidget {
const TreeView({
this.dataRoots,
this.dataDisplayProvider,
this.onItemPressed,
});
final List<T> dataRoots;
final Widget Function(T) dataDisplayProvider;
final void Function(T) onItemPressed;
@override
_TreeViewState<T> createState() => _TreeViewState<T>();
}
class _TreeViewState<T extends TreeNode<T>> extends State<TreeView<T>>
with TreeMixin<T> {
@override
void initState() {
super.initState();
_initData();
_updateItems();
}
@override
void didUpdateWidget(TreeView<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.dataRoots != oldWidget.dataRoots) {
_initData();
_updateItems();
}
}
void _initData() {
dataRoots = List.from(widget.dataRoots);
}
@override
Widget build(BuildContext context) {
if (items.isEmpty) return const SizedBox();
return ListView.builder(
itemCount: items.length,
itemExtent: defaultListItemHeight,
itemBuilder: (context, index) {
final item = items[index];
return TreeViewItem<T>(
item,
display: widget.dataDisplayProvider(item),
onItemPressed: _onItemPressed,
);
},
);
}
// TODO(kenz): animate expansions and collapses.
void _onItemPressed(T item) {
if (!item.isExpandable) return;
// Order of execution matters for the below calls.
item.toggleExpansion();
if (widget.onItemPressed != null) {
widget.onItemPressed(item);
}
_updateItems();
}
void _updateItems() {
setState(() {
items = buildFlatList(dataRoots);
});
}
}
class TreeViewItem<T extends TreeNode<T>> extends StatefulWidget {
const TreeViewItem(this.data, {this.display, this.onItemPressed});
final T data;
final Widget display;
final void Function(T) onItemPressed;
@override
_TreeViewItemState<T> createState() => _TreeViewItemState<T>();
}
class _TreeViewItemState<T extends TreeNode<T>> extends State<TreeViewItem<T>>
with TickerProviderStateMixin, CollapsibleAnimationMixin {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: _onPressed,
child: Padding(
padding: EdgeInsets.only(left: nodeIndent(widget.data)),
child: Row(
mainAxisSize: MainAxisSize.min,
textBaseline: TextBaseline.alphabetic,
children: [
widget.data.isExpandable
? RotationTransition(
turns: expandArrowAnimation,
child: const Icon(
Icons.arrow_drop_down,
size: defaultIconSize,
),
)
: const SizedBox(width: defaultIconSize),
Expanded(child: widget.display),
]),
),
);
}
@override
bool get isExpanded => widget.data.isExpanded;
@override
void onExpandChanged(bool expanded) {}
@override
bool shouldShow() => widget.data.shouldShow();
double nodeIndent(T dataObject) {
return dataObject.level * defaultSpacing;
}
void _onPressed() {
widget.onItemPressed(widget.data);
setExpanded(widget.data.isExpanded);
}
}
mixin TreeMixin<T extends TreeNode<T>> {
List<T> dataRoots;
List<T> items;
List<T> buildFlatList(List<T> roots) {
final flatList = <T>[];
for (T root in roots) {
traverse(root, (n) {
flatList.add(n);
return n.isExpanded;
});
}
return flatList;
}
void traverse(T node, bool Function(T) callback) {
if (node == null) return;
final shouldContinue = callback(node);
if (shouldContinue) {
for (var child in node.children) {
traverse(child, callback);
}
}
}
}