blob: d5eccbf19c43d0a066ec0db26eb8555bac8f83c9 [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 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import '../primitives/enum_utils.dart';
import '../primitives/utils.dart';
import '../shared/table.dart';
import '../shared/theme.dart';
/// Returns a [TextSpan] that only includes the first [length] characters of
/// [span].
TextSpan truncateTextSpan(TextSpan span, int length) {
int available = length;
TextSpan truncateHelper(TextSpan span) {
var text = span.text;
List<TextSpan>? children;
if (text != null) {
if (text.length > available) {
text = text.substring(0, available);
}
available -= text.length;
}
if (span.children != null) {
children = <TextSpan>[];
for (var child in span.children!) {
if (available <= 0) break;
children.add(truncateHelper(child as TextSpan));
}
if (children.isEmpty) {
children = null;
}
}
return TextSpan(
text: text,
children: children,
style: span.style,
recognizer: span.recognizer,
semanticsLabel: span.semanticsLabel,
);
}
return truncateHelper(span);
}
/// Returns the width in pixels of the [span].
double calculateTextSpanWidth(TextSpan? span) {
final textPainter = TextPainter(
text: span,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
)..layout();
return textPainter.width;
}
/// Returns the height in pixels of the [span].
double calculateTextSpanHeight(TextSpan span) {
final textPainter = TextPainter(
text: span,
textAlign: TextAlign.left,
textDirection: TextDirection.ltr,
)..layout();
return textPainter.height;
}
TextSpan? findLongestTextSpan(List<TextSpan> spans) {
int longestLength = 0;
TextSpan? longestSpan;
for (final span in spans) {
final int currentLength = span.toPlainText().length;
if (currentLength > longestLength) {
longestLength = currentLength;
longestSpan = span;
}
}
return longestSpan;
}
/// Scrollbar that is offset by the amount specified by an [offsetController].
///
/// This makes it possible to create a [ListView] with both vertical and
/// horizontal scrollbars by wrapping the [ListView] in a
/// [SingleChildScrollView] that handles horizontal scrolling. The
/// [offsetController] is the offset of the parent [SingleChildScrollView] in
/// this example.
///
/// This class could be optimized if performance was a concern using a
/// [CustomPainter] instead of an [AnimatedBuilder] so that the
/// [OffsetScrollbar] widget does not need to build on each change to the
/// [offsetController].
class OffsetScrollbar extends StatefulWidget {
const OffsetScrollbar({
Key? key,
this.isAlwaysShown = false,
required this.axis,
required this.controller,
required this.offsetController,
required this.child,
required this.offsetControllerViewportDimension,
}) : super(key: key);
final bool isAlwaysShown;
final Axis axis;
final ScrollController controller;
final ScrollController offsetController;
final Widget child;
/// The current viewport dimension of the offsetController may not be
/// available at build time as it is not updated until later so we require
/// that the known correct viewport dimension is passed into this class.
///
/// This is a workaround because we use an AnimatedBuilder to listen for
/// changes to the offsetController rather than displaying the scrollbar at
/// paint time which would be more difficult.
final double offsetControllerViewportDimension;
@override
_OffsetScrollbarState createState() => _OffsetScrollbarState();
}
class _OffsetScrollbarState extends State<OffsetScrollbar> {
@override
Widget build(BuildContext context) {
if (!widget.offsetController.position.hasContentDimensions) {
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
if (widget.offsetController.position.hasViewportDimension && mounted) {
// TODO(jacobr): find a cleaner way to be notified that the
// offsetController now has a valid dimension. We would probably
// have to implement our own ScrollbarPainter instead of being able
// to use the existing Scrollbar widget.
setState(() {});
}
});
}
return AnimatedBuilder(
animation: widget.offsetController,
builder: (context, child) {
// Compute a delta to move the scrollbar from where it is by default to
// where it should be given the viewport dimension of the
// offsetController not the viewport that is the entire scroll extent
// of the offsetController because this controller is nested within the
// offset controller.
double delta = 0.0;
if (widget.offsetController.position.hasContentDimensions) {
delta = widget.offsetController.offset -
widget.offsetController.position.maxScrollExtent +
widget.offsetController.position.minScrollExtent;
if (widget.offsetController.position.hasViewportDimension) {
// TODO(jacobr): this is a bit of a hack.
// The viewport dimension from the offsetController may be one frame
// behind the true viewport dimension. We add this delta so the
// scrollbar always appears stuck to the side of the viewport.
delta += widget.offsetControllerViewportDimension -
widget.offsetController.position.viewportDimension;
}
}
final offset = widget.axis == Axis.vertical
? Offset(delta, 0.0)
: Offset(0.0, delta);
return Transform.translate(
offset: offset,
child: Scrollbar(
thumbVisibility: widget.isAlwaysShown,
controller: widget.controller,
child: Transform.translate(
offset: -offset,
child: child,
),
),
);
},
child: widget.child,
);
}
}
/// Scrolls to [position] if [position] is not already visible in the scroll view.
void maybeScrollToPosition(
ScrollController scrollController,
double position,
) {
final extentVisible = Range(
scrollController.offset,
scrollController.offset + scrollController.position.extentInside,
);
if (!extentVisible.contains(position)) {
final positionToScrollTo = max(0.0, position - defaultRowHeight);
scrollController.animateTo(
//TODO (carolynqu): should be positionToScrollTo.clamp(0.0, scrollController.position.maxScrollExtent) but maxScrollExtent is not being updated, https://github.com/flutter/devtools/issues/4264
positionToScrollTo,
duration: defaultDuration,
curve: defaultCurve,
);
}
}
class ColorPair {
const ColorPair({required this.background, required this.foreground});
final Color foreground;
final Color background;
}
class ThemedColorPair {
const ThemedColorPair({required this.background, required this.foreground});
factory ThemedColorPair.from(ColorPair colorPair) {
return ThemedColorPair(
foreground: ThemedColor.fromSingle(colorPair.foreground),
background: ThemedColor.fromSingle(colorPair.background),
);
}
final ThemedColor foreground;
final ThemedColor background;
}
/// A theme-dependent color.
///
/// When possible, themed colors should be specified in an extension on
/// [ColorScheme] using the [ColorScheme.isLight] getter. However, this class
/// may be used when access to the [BuildContext] is not available at the time
/// the color needs to be specified.
class ThemedColor {
const ThemedColor({required this.light, required this.dark});
const ThemedColor.fromSingle(Color color)
: light = color,
dark = color;
final Color light;
final Color dark;
Color colorFor(ColorScheme colorScheme) {
return colorScheme.isLight ? light : dark;
}
}
enum MediaSize with EnumIndexOrdering {
xxs,
xs,
s,
m,
l,
xl,
}
class ScreenSize {
ScreenSize(BuildContext context) {
_height = _calculateHeight(context);
_width = _calculateWidth(context);
}
MediaSize get height => _height;
MediaSize get width => _width;
late MediaSize _height;
late MediaSize _width;
MediaSize _calculateWidth(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (width < 300) return MediaSize.xxs;
if (width < 600) return MediaSize.xs;
if (width < 900) return MediaSize.s;
if (width < 1200) return MediaSize.m;
if (width < 1500)
return MediaSize.l;
else
return MediaSize.xl;
}
MediaSize _calculateHeight(BuildContext context) {
final height = MediaQuery.of(context).size.height;
if (height < 300) return MediaSize.xxs;
if (height < 450) return MediaSize.xs;
if (height < 600) return MediaSize.s;
if (height < 750) return MediaSize.m;
if (height < 900)
return MediaSize.l;
else
return MediaSize.xl;
}
}