blob: 3ee4a16ced4a2a2e26a34a30c75edf25f7917457 [file]
// Copyright 2019 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/material.dart';
import '../primitives/byte_utils.dart';
import '../primitives/trees.dart';
import '../primitives/utils.dart';
/// Defines how a column should display data in a table.
///
/// [ColumnData] objects should be defined as static where possible, and should
/// not manage any stateful data. The table controllers expect columns to be
/// solely responsible for declaring how to layout table data.
///
/// Any data that can't be stored on the [dataObject] may be accessed by passing
/// a long-living controller to the constructor of the [ColumnData] subclass.
///
/// The controller is expected to be alive for the duration of the app
/// connection.
abstract class ColumnData<T> {
ColumnData(
this.title, {
required double this.fixedWidthPx,
this.titleTooltip,
this.alignment = ColumnAlignment.left,
this.headerAlignment = TextAlign.left,
this.showTooltip = false,
}) : minWidthPx = null;
ColumnData.wide(
this.title, {
this.titleTooltip,
this.minWidthPx,
this.alignment = ColumnAlignment.left,
this.headerAlignment = TextAlign.left,
this.showTooltip = false,
}) : fixedWidthPx = null;
final bool showTooltip;
final String title;
final String? titleTooltip;
/// Width of the column expressed as a fixed number of pixels.
final double? fixedWidthPx;
/// The minimum width that should be used for a variable width column.
final double? minWidthPx;
/// How much to indent the data object by.
///
/// This should only be non-zero for [TreeColumnData].
double getNodeIndentPx(T dataObject) => 0.0;
final ColumnAlignment alignment;
final TextAlign headerAlignment;
bool get numeric => false;
bool get includeHeader => true;
bool get supportsSorting => true;
int compare(T a, T b) {
final valueA = getValue(a);
final valueB = getValue(b);
if (valueA == null && valueB == null) return 0;
if (valueA == null) return -1;
if (valueB == null) return 1;
if (valueA is! Comparable || valueB is! Comparable) return 0;
return valueA.compareTo(valueB);
}
/// Get the cell's value from the given [dataObject].
Object? getValue(T dataObject);
/// Get the cell's display value from the given [dataObject].
String getDisplayValue(T dataObject) =>
getValue(dataObject)?.toString() ?? '';
String? getCaption(T dataObject) => null;
// TODO: remove redundant getTooltip overrides now that [showToolTip] is
// available.
/// Get the cell's tooltip value from the given [dataObject].
String getTooltip(T dataObject) =>
showTooltip ? getDisplayValue(dataObject) : '';
/// Get the cell's rich tooltip span from the given [dataObject].
///
/// If both [getTooltip] and [getRichTooltip] are provided, the rich tooltip
/// will take precedence.
InlineSpan? getRichTooltip(T dataObject, BuildContext context) => null;
/// Get the cell's text color from the given [dataObject].
Color? getTextColor(T dataObject) => null;
TextStyle? contentTextStyle(
BuildContext context,
T dataObject, {
bool isSelected = false,
}) {
final theme = Theme.of(context);
final textColor = getTextColor(dataObject) ?? theme.colorScheme.onSurface;
return theme.regularTextStyleWithColor(textColor);
}
/// The configuration for the column. Configuration changes to columns
/// will cause the table to be rebuilt.
///
/// Defaults to title.
String get config => title;
@override
String toString() => title;
}
abstract class TreeColumnData<T extends TreeNode<T>> extends ColumnData<T> {
TreeColumnData(super.title) : super.wide();
static double get treeToggleWidth => scaleByFontFactor(14.0);
@override
double getNodeIndentPx(T dataObject) {
return dataObject.level * treeToggleWidth;
}
}
enum ColumnAlignment { left, right, center }
mixin PinnableListEntry {
/// Determines if the row should be pinned to the top of the table.
bool get pinToTop => false;
}
/// Defines a group of columns for use in a table.
///
/// Use a column group when multiple columns should be grouped together in the
/// table with a common title. In a table with column groups, visual dividers
/// will be drawn between groups and an additional header row will be added to
/// the table to display the column group titles.
class ColumnGroup {
ColumnGroup({required this.title, required this.range});
ColumnGroup.fromText({
required String title,
required Range range,
String? tooltip,
}) : this(
title: maybeWrapWithTooltip(child: Text(title), tooltip: tooltip),
range: range,
);
final Widget title;
/// The range of column indices for columns that make up this group.
final Range range;
}
extension ColumnDataExtension<T> on ColumnData<T> {
MainAxisAlignment get mainAxisAlignment {
switch (alignment) {
case ColumnAlignment.center:
return MainAxisAlignment.center;
case ColumnAlignment.right:
return MainAxisAlignment.end;
case ColumnAlignment.left:
return MainAxisAlignment.start;
}
}
TextAlign get contentTextAlignment {
switch (alignment) {
case ColumnAlignment.center:
return TextAlign.center;
case ColumnAlignment.right:
return TextAlign.right;
case ColumnAlignment.left:
return TextAlign.left;
}
}
}
typedef RichTooltipBuilder<T> = InlineSpan? Function(T, BuildContext);
/// Column that, for each row, shows a time value in milliseconds and the
/// percentage that the time value is of the total time for this data set.
///
/// Both time and percentage are provided through callbacks [timeProvider] and
/// [percentAsDoubleProvider], respectively.
///
/// When [percentageOnly] is true, the time value will be omitted, and only the
/// percentage will be displayed.
abstract class TimeAndPercentageColumn<T> extends ColumnData<T> {
TimeAndPercentageColumn({
required String title,
required this.percentAsDoubleProvider,
this.timeProvider,
this.tooltipProvider,
this.richTooltipProvider,
this.secondaryCompare,
this.percentageOnly = false,
double columnWidth = _defaultTimeColumnWidth,
super.titleTooltip,
}) : super(title, fixedWidthPx: scaleByFontFactor(columnWidth));
static const _defaultTimeColumnWidth = 120.0;
Duration Function(T)? timeProvider;
double Function(T) percentAsDoubleProvider;
String Function(T)? tooltipProvider;
RichTooltipBuilder<T>? richTooltipProvider;
Comparable Function(T)? secondaryCompare;
final bool percentageOnly;
@override
bool get numeric => true;
@override
int compare(T a, T b) {
final result = super.compare(a, b);
if (result == 0 && secondaryCompare != null) {
return secondaryCompare!(a).compareTo(secondaryCompare!(b));
}
return result;
}
@override
double getValue(T dataObject) =>
percentageOnly
? percentAsDoubleProvider(dataObject)
: timeProvider!(dataObject).inMicroseconds.toDouble();
@override
String getDisplayValue(T dataObject) {
if (percentageOnly) return _percentDisplay(dataObject);
return _timeAndPercentage(dataObject);
}
@override
String getTooltip(T dataObject) {
if (tooltipProvider != null) {
return tooltipProvider!(dataObject);
}
if (percentageOnly && timeProvider != null) {
return _timeAndPercentage(dataObject);
}
return '';
}
@override
InlineSpan? getRichTooltip(T dataObject, BuildContext context) =>
richTooltipProvider?.call(dataObject, context);
String _timeAndPercentage(T dataObject) =>
'${durationText(timeProvider!(dataObject), fractionDigits: 2)} (${_percentDisplay(dataObject)})';
String _percentDisplay(T dataObject) =>
percent(percentAsDoubleProvider(dataObject));
}
/// Column that, for each row, shows a memory value and the percentage that the
/// memory value is of the total memory for this data set.
///
/// Both memory and percentage are provided through callbacks [sizeProvider] and
/// [percentAsDoubleProvider], respectively.
///
/// When [percentageOnly] is true, the memory value will be omitted, and only the
/// percentage will be displayed.
abstract class SizeAndPercentageColumn<T> extends ColumnData<T> {
SizeAndPercentageColumn({
required String title,
required this.percentAsDoubleProvider,
this.sizeProvider,
this.tooltipProvider,
this.richTooltipProvider,
this.secondaryCompare,
this.percentageOnly = false,
double columnWidth = _defaultMemoryColumnWidth,
super.titleTooltip,
}) : super(title, fixedWidthPx: scaleByFontFactor(columnWidth));
static const _defaultMemoryColumnWidth =
TimeAndPercentageColumn._defaultTimeColumnWidth;
int Function(T)? sizeProvider;
double Function(T) percentAsDoubleProvider;
String Function(T)? tooltipProvider;
RichTooltipBuilder<T>? richTooltipProvider;
Comparable Function(T)? secondaryCompare;
final bool percentageOnly;
@override
bool get numeric => true;
@override
int compare(T a, T b) {
final result = super.compare(a, b);
if (result == 0 && secondaryCompare != null) {
return secondaryCompare!(a).compareTo(secondaryCompare!(b));
}
return result;
}
@override
double getValue(T dataObject) =>
percentageOnly
? percentAsDoubleProvider(dataObject)
: sizeProvider!(dataObject).toDouble();
@override
String getDisplayValue(T dataObject) {
if (percentageOnly) return _percentDisplay(dataObject);
return _memoryAndPercentage(dataObject);
}
@override
String getTooltip(T dataObject) {
if (tooltipProvider != null) {
return tooltipProvider!(dataObject);
}
if (percentageOnly && sizeProvider != null) {
return _memoryAndPercentage(dataObject);
}
return '';
}
@override
InlineSpan? getRichTooltip(T dataObject, BuildContext context) =>
richTooltipProvider?.call(dataObject, context);
String _memoryAndPercentage(T dataObject) =>
'${prettyPrintBytes(sizeProvider!(dataObject), includeUnit: true, kbFractionDigits: 0)}'
' (${_percentDisplay(dataObject)})';
String _percentDisplay(T dataObject) =>
percent(percentAsDoubleProvider(dataObject));
}