Skip to content

Commit

Permalink
Fix inspector scrollbars.
Browse files Browse the repository at this point in the history
  • Loading branch information
jacob314 committed Feb 17, 2021
1 parent dc6fbf6 commit 896cbf5
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 95 deletions.
5 changes: 5 additions & 0 deletions packages/devtools_app/lib/src/inspector/inspector_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class InspectorScreenBodyState extends State<InspectorScreenBody>
bool get enableButtons =>
actionInProgress == false && connectionInProgress == false;

static const summaryTreeKey = Key('Summary Tree');
static const detailsTreeKey = Key('Details Tree');

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -100,11 +103,13 @@ class InspectorScreenBodyState extends State<InspectorScreenBody>
border: Border.all(color: Theme.of(context).focusColor),
),
child: InspectorTree(
key: summaryTreeKey,
controller: summaryTreeController,
isSummaryTree: true,
),
);
final detailsTree = InspectorTree(
key: detailsTreeKey,
controller: detailsTreeController,
);

Expand Down
242 changes: 147 additions & 95 deletions packages/devtools_app/lib/src/inspector/inspector_tree_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import '../common_widgets.dart';
import '../theme.dart';
import '../ui/colors.dart';
import '../ui/theme.dart';
import '../ui/utils.dart';
import 'diagnostics.dart';
import 'diagnostics_node.dart';
import 'inspector_tree.dart';
Expand All @@ -27,12 +28,16 @@ class _InspectorTreeRowWidget extends StatefulWidget {
@required Key key,
@required this.row,
@required this.inspectorTreeState,
@required this.scrollControllerX,
@required this.viewportWidth,
}) : super(key: key);

final _InspectorTreeState inspectorTreeState;

InspectorTreeNode get node => row.node;
final InspectorTreeRow row;
final ScrollController scrollControllerX;
final double viewportWidth;

@override
_InspectorTreeRowState createState() => _InspectorTreeRowState();
Expand All @@ -48,6 +53,8 @@ class _InspectorTreeRowState extends State<_InspectorTreeRowWidget>
row: widget.row,
expandArrowAnimation: expandArrowAnimation,
controller: widget.inspectorTreeState.controller,
scrollControllerX: widget.scrollControllerX,
viewportWidth: widget.viewportWidth,
onToggle: () {
setExpanded(!isExpanded);
},
Expand Down Expand Up @@ -272,7 +279,7 @@ class _InspectorTreeState extends State<InspectorTree>
if (rowIndex == controller.numRows) {
return 0;
}
final endY = y += _scrollControllerY.position.viewportDimension;
final endY = y += safeViewportHeight;
for (int i = rowIndex; i < controller.numRows; i++) {
final rowY = controller.getRowY(i);
if (rowY >= endY) break;
Expand Down Expand Up @@ -300,25 +307,32 @@ class _InspectorTreeState extends State<InspectorTree>
}
currentAnimateTarget = rect;
final targetY = _computeTargetOffsetY(
_scrollControllerY,
rect.top,
rect.bottom,
);
currentAnimateY = _scrollControllerY.animateTo(
targetY,
duration: longDuration,
curve: defaultCurve,
);
if (_scrollControllerY.hasClients) {
currentAnimateY = _scrollControllerY.animateTo(
targetY,
duration: longDuration,
curve: defaultCurve,
);
} else {
currentAnimateY = null;
_scrollControllerY = ScrollController(initialScrollOffset: targetY);
}

// Determine a target X coordinate consistent with the target Y coordinate
// we will end up as so we get a smooth animation to the final destination.
final targetX = _computeTargetX(targetY);

unawaited(_scrollControllerX.animateTo(
targetX,
duration: longDuration,
curve: defaultCurve,
));
if (_scrollControllerX.hasClients) {
unawaited(_scrollControllerX.animateTo(
targetX,
duration: longDuration,
curve: defaultCurve,
));
} else {
_scrollControllerX = ScrollController(initialScrollOffset: targetX);
}

try {
await currentAnimateY;
Expand All @@ -329,22 +343,28 @@ class _InspectorTreeState extends State<InspectorTree>
currentAnimateTarget = null;
}

double get safeViewportHeight {
return _scrollControllerY.hasClients
? _scrollControllerY.position.viewportDimension
: 1000.0;
}

/// Animate so that the entire range minOffset to maxOffset is within view.
double _computeTargetOffsetY(
ScrollController controller,
double minOffset,
double maxOffset,
) {
final currentOffset = controller.offset;
final viewportDimension = _scrollControllerY.position.viewportDimension;
final currentOffset = _scrollControllerY.hasClients
? _scrollControllerY.offset
: _scrollControllerY.initialScrollOffset;
final viewportDimension = safeViewportHeight;
final currentEndOffset = viewportDimension + currentOffset;

// If the requested range is larger than what the viewport can show at once,
// prioritize showing the start of the range.
maxOffset = min(viewportDimension + minOffset, maxOffset);
if (currentOffset <= minOffset && currentEndOffset >= maxOffset) {
return controller
.offset; // Nothing to do. The whole range is already in view.
return currentOffset; // Nothing to do. The whole range is already in view.
}
if (currentOffset > minOffset) {
// Need to scroll so the minOffset is in view.
Expand Down Expand Up @@ -395,49 +415,65 @@ class _InspectorTreeState extends State<InspectorTree>
// Indicate the tree is loading.
return const CenteredCircularProgressIndicator();
}
if (controller.numRows == 0) {
// This works around a bug when Scrollbars are present on a short lived
// widget.
return const SizedBox();
}

return Scrollbar(
isAlwaysShown: true,
controller: _scrollControllerX,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _scrollControllerX,
child: SizedBox(
width: controller.rowWidth + controller.maxRowIndent,
// TODO(kenz): this scrollbar needs to be sticky to the right side of
// the visible container - right now it is lined up to the right of
// the widest row (which is likely not visible). This may require some
// refactoring.
child: Scrollbar(
isAlwaysShown: true,
controller: _scrollControllerY,
child: GestureDetector(
onTap: _focusNode.requestFocus,
child: Focus(
onKey: _handleKeyEvent,
autofocus: widget.isSummaryTree,
focusNode: _focusNode,
child: ListView.custom(
itemExtent: rowHeight,
childrenDelegate: SliverChildBuilderDelegate(
(context, index) {
final InspectorTreeRow row =
controller.root?.getRow(index);
return _InspectorTreeRowWidget(
key: PageStorageKey(row?.node),
inspectorTreeState: this,
row: row,
);
},
childCount: controller.numRows,
return LayoutBuilder(
builder: (context, constraints) {
final viewportWidth = constraints.maxWidth;
return Scrollbar(
isAlwaysShown: true,
controller: _scrollControllerX,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _scrollControllerX,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: controller.rowWidth + controller.maxRowIndent),
// TODO(kenz): this scrollbar needs to be sticky to the right side of
// the visible container - right now it is lined up to the right of
// the widest row (which is likely not visible). This may require some
// refactoring.
child: GestureDetector(
onTap: _focusNode.requestFocus,
child: Focus(
onKey: _handleKeyEvent,
autofocus: widget.isSummaryTree,
focusNode: _focusNode,
child: OffsetScrollbar(
isAlwaysShown: true,
axis: Axis.vertical,
controller: _scrollControllerY,
offsetController: _scrollControllerX,
offsetControllerViewportDimension: viewportWidth,
child: ListView.custom(
itemExtent: rowHeight,
childrenDelegate: SliverChildBuilderDelegate(
(context, index) {
final InspectorTreeRow row =
controller.root?.getRow(index);
return _InspectorTreeRowWidget(
key: PageStorageKey(row?.node),
inspectorTreeState: this,
row: row,
scrollControllerX: _scrollControllerX,
viewportWidth: viewportWidth,
);
},
childCount: controller.numRows,
),
controller: _scrollControllerY,
),
),
controller: _scrollControllerY,
),
),
),
),
),
),
);
},
);
}

Expand Down Expand Up @@ -528,12 +564,16 @@ class InspectorRowContent extends StatelessWidget {
@required this.controller,
@required this.onToggle,
@required this.expandArrowAnimation,
@required this.scrollControllerX,
@required this.viewportWidth,
});

final InspectorTreeRow row;
final InspectorTreeControllerFlutter controller;
final VoidCallback onToggle;
final Animation<double> expandArrowAnimation;
final ScrollController scrollControllerX;
final double viewportWidth;

@override
Widget build(BuildContext context) {
Expand All @@ -551,51 +591,63 @@ class InspectorRowContent extends StatelessWidget {
}

final node = row.node;
return CustomPaint(
painter: _RowPainter(row, controller, colorScheme),
size: Size(currentX, rowHeight),
child: Padding(
padding: EdgeInsets.only(left: currentX),
child: ClipRect(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
node.showExpandCollapse
? InkWell(
onTap: onToggle,
child: RotationTransition(
turns: expandArrowAnimation,
child: const Icon(
Icons.expand_more,
size: defaultIconSize,
),
),
)
: const SizedBox(
width: defaultSpacing, height: defaultSpacing),
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
child: InkWell(
onTap: () {
controller.onSelectRow(row);
// TODO(gmoothart): It may be possible to capture the tap
// and request focus directly from the InspectorTree. Then
// we wouldn't need this.
controller.requestFocus();
},
child: Container(
height: rowHeight,
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: DiagnosticsNodeDescription(node.diagnostic),
final content = Padding(
padding: EdgeInsets.only(left: currentX),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
node.showExpandCollapse
? InkWell(
onTap: onToggle,
child: RotationTransition(
turns: expandArrowAnimation,
child: const Icon(
Icons.expand_more,
size: defaultIconSize,
),
),
)
: const SizedBox(width: defaultSpacing, height: defaultSpacing),
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor,
),
child: InkWell(
onTap: () {
controller.onSelectRow(row);
// TODO(gmoothart): It may be possible to capture the tap
// and request focus directly from the InspectorTree. Then
// we wouldn't need this.
controller.requestFocus();
},
child: Container(
height: rowHeight,
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: DiagnosticsNodeDescription(node.diagnostic),
),
),
],
),
),
],
),
);
return CustomPaint(
painter: _RowPainter(row, controller, colorScheme),
size: Size(currentX, rowHeight),
child: Align(
alignment: Alignment.topLeft,
child: AnimatedBuilder(
animation: scrollControllerX,
builder: (context, child) {
final rowWidth =
scrollControllerX.offset + viewportWidth - defaultSpacing;
return SizedBox(
width: max(rowWidth, currentX + 100),
child: rowWidth > currentX ? content : const SizedBox(),
);
},
child: content,
),
),
);
Expand Down
Loading

0 comments on commit 896cbf5

Please sign in to comment.