From 8331ce32eeb692c52edae975599b3f82513a8914 Mon Sep 17 00:00:00 2001 From: Farook Date: Sun, 19 Jan 2025 01:06:19 +0300 Subject: [PATCH] chore: Adjust UI elements to prevent clipping or occlusion + added auto-resizing text to progress circle --- flutter/lib/ui/auto_size_text.dart | 459 ++++++++++++++++++ .../lib/ui/home/benchmark_running_screen.dart | 92 ++-- .../lib/ui/home/benchmark_start_screen.dart | 181 ++++--- 3 files changed, 600 insertions(+), 132 deletions(-) create mode 100644 flutter/lib/ui/auto_size_text.dart diff --git a/flutter/lib/ui/auto_size_text.dart b/flutter/lib/ui/auto_size_text.dart new file mode 100644 index 000000000..e668c0640 --- /dev/null +++ b/flutter/lib/ui/auto_size_text.dart @@ -0,0 +1,459 @@ +// Copyright (c) 2018 Simon Leier + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the 'Software'), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:flutter/widgets.dart'; + +/// Flutter widget that automatically resizes text to fit perfectly within its +/// bounds. +/// +/// All size constraints as well as maxLines are taken into account. If the text +/// overflows anyway, you should check if the parent widget actually constraints +/// the size of this widget. + +// TODO(Farook): make the circle padding part completely optional for general use. +class AutoSizeCircleText extends StatefulWidget { + /// Creates a [AutoSizeCircleText] widget. + /// + /// If the [style] argument is null, the text will use the style from the + /// closest enclosing [DefaultTextStyle]. + const AutoSizeCircleText( + String this.data, { + Key? key, + this.textKey, + this.style, + this.strutStyle, + this.minFontSize = 12, + this.maxFontSize = double.infinity, + this.stepGranularity = 1, + this.presetFontSizes, + this.textAlign, + this.textDirection, + this.locale, + this.softWrap, + this.wrapWords = true, + this.overflow, + this.overflowReplacement, + this.textScaleFactor, + this.maxLines, + this.semanticsLabel, + this.circularPadding = 20, + }) : textSpan = null, + super(key: key); + + /// Creates a [AutoSizeCircleText] widget with a [TextSpan]. + const AutoSizeCircleText.rich( + TextSpan this.textSpan, { + Key? key, + this.textKey, + this.style, + this.strutStyle, + this.minFontSize = 12, + this.maxFontSize = double.infinity, + this.stepGranularity = 1, + this.presetFontSizes, + this.textAlign, + this.textDirection, + this.locale, + this.softWrap, + this.wrapWords = true, + this.overflow, + this.overflowReplacement, + this.textScaleFactor, + this.maxLines, + this.semanticsLabel, + this.circularPadding = 20, + }) : data = null, + super(key: key); + + /// Sets the key for the resulting [Text] widget. + /// + /// This allows you to find the actual `Text` widget built by `AutoSizeText`. + final Key? textKey; + + /// The text to display. + /// + /// This will be null if a [textSpan] is provided instead. + final String? data; + + /// The text to display as a [TextSpan]. + /// + /// This will be null if [data] is provided instead. + final TextSpan? textSpan; + + /// If non-null, the style to use for this text. + /// + /// If the style's "inherit" property is true, the style will be merged with + /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will + /// replace the closest enclosing [DefaultTextStyle]. + final TextStyle? style; + + // The default font size if none is specified. + static const double _defaultFontSize = 14; + + /// The strut style to use. Strut style defines the strut, which sets minimum + /// vertical layout metrics. + /// + /// Omitting or providing null will disable strut. + /// + /// Omitting or providing null for any properties of [StrutStyle] will result + /// in default values being used. It is highly recommended to at least specify + /// a font size. + /// + /// See [StrutStyle] for details. + final StrutStyle? strutStyle; + + /// The minimum text size constraint to be used when auto-sizing text. + /// + /// Is being ignored if [presetFontSizes] is set. + final double minFontSize; + + /// The maximum text size constraint to be used when auto-sizing text. + /// + /// Is being ignored if [presetFontSizes] is set. + final double maxFontSize; + + /// The step size in which the font size is being adapted to constraints. + /// + /// The Text scales uniformly in a range between [minFontSize] and + /// [maxFontSize]. + /// Each increment occurs as per the step size set in stepGranularity. + /// + /// Most of the time you don't want a stepGranularity below 1.0. + /// + /// Is being ignored if [presetFontSizes] is set. + final double stepGranularity; + + /// Predefines all the possible font sizes. + /// + /// **Important:** PresetFontSizes have to be in descending order. + final List? presetFontSizes; + + /// How the text should be aligned horizontally. + final TextAlign? textAlign; + + /// The directionality of the text. + /// + /// This decides how [textAlign] values like [TextAlign.start] and + /// [TextAlign.end] are interpreted. + /// + /// This is also used to disambiguate how to render bidirectional text. For + /// example, if the [data] is an English phrase followed by a Hebrew phrase, + /// in a [TextDirection.ltr] context the English phrase will be on the left + /// and the Hebrew phrase to its right, while in a [TextDirection.rtl] + /// context, the English phrase will be on the right and the Hebrew phrase on + /// its left. + /// + /// Defaults to the ambient [Directionality], if any. + final TextDirection? textDirection; + + /// Used to select a font when the same Unicode character can + /// be rendered differently, depending on the locale. + /// + /// It's rarely necessary to set this property. By default its value + /// is inherited from the enclosing app with `Localizations.localeOf(context)`. + final Locale? locale; + + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was + /// unlimited horizontal space. + final bool? softWrap; + + /// Whether words which don't fit in one line should be wrapped. + /// + /// If false, the fontSize is lowered as far as possible until all words fit + /// into a single line. + final bool wrapWords; + + /// How visual overflow should be handled. + /// + /// Defaults to retrieving the value from the nearest [DefaultTextStyle] ancestor. + final TextOverflow? overflow; + + /// If the text is overflowing and does not fit its bounds, this widget is + /// displayed instead. + final Widget? overflowReplacement; + + /// The number of font pixels for each logical pixel. + /// + /// For example, if the text scale factor is 1.5, text will be 50% larger than + /// the specified font size. + /// + /// This property also affects [minFontSize], [maxFontSize] and [presetFontSizes]. + /// + /// The value given to the constructor as textScaleFactor. If null, will + /// use the [MediaQueryData.textScaleFactor] obtained from the ambient + /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope. + final double? textScaleFactor; + + /// An optional maximum number of lines for the text to span, wrapping if necessary. + /// If the text exceeds the given number of lines, it will be resized according + /// to the specified bounds and if necessary truncated according to [overflow]. + /// + /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the + /// edge of the box. + /// + /// If this is null, but there is an ambient [DefaultTextStyle] that specifies + /// an explicit number for its [DefaultTextStyle.maxLines], then the + /// [DefaultTextStyle] value will take precedence. You can use a [RichText] + /// widget directly to entirely override the [DefaultTextStyle]. + final int? maxLines; + + /// An alternative semantics label for this text. + /// + /// If present, the semantics of this widget will contain this value instead + /// of the actual text. This will overwrite any of the semantics labels applied + /// directly to the [TextSpan]s. + /// + /// This is useful for replacing abbreviations or shorthands with the full + /// text value: + /// + /// ```dart + /// AutoSizeText(r'$$', semanticsLabel: 'Double dollars') + /// ``` + final String? semanticsLabel; + + final double circularPadding; + + @override + _AutoSizeCircleTextState createState() => _AutoSizeCircleTextState(); +} + +class _AutoSizeCircleTextState extends State { + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, size) { + final defaultTextStyle = DefaultTextStyle.of(context); + + var style = widget.style; + if (widget.style == null || widget.style!.inherit) { + style = defaultTextStyle.style.merge(widget.style); + } + if (style!.fontSize == null) { + style = style.copyWith(fontSize: AutoSizeCircleText._defaultFontSize); + } + + final maxLines = widget.maxLines ?? defaultTextStyle.maxLines; + + _validateProperties(style, maxLines); + + final initialResult = _calculateFontSize(size, style, 1); + final fontSizeSingle = initialResult[0] as double; + final textFitsSingle = initialResult[1] as bool; + + final result = _calculateFontSize(size, style, maxLines); + final fontSize = result[0] as double; + final textFits = result[1] as bool; + + Widget text; + + text = _buildText( + textFitsSingle ? fontSizeSingle : fontSize, + style, + maxLines, + textFitsSingle + ? const EdgeInsets.symmetric(horizontal: 20) + : EdgeInsets.symmetric(horizontal: widget.circularPadding)); + + if (widget.overflowReplacement != null && !textFits) { + return widget.overflowReplacement!; + } else { + return text; + } + }); + } + + void _validateProperties(TextStyle style, int? maxLines) { + assert(widget.overflow == null || widget.overflowReplacement == null, + 'Either overflow or overflowReplacement must be null.'); + assert(maxLines == null || maxLines > 0, + 'MaxLines must be greater than or equal to 1.'); + assert(widget.key == null || widget.key != widget.textKey, + 'Key and textKey must not be equal.'); + + if (widget.presetFontSizes == null) { + assert( + widget.stepGranularity >= 0.1, + 'StepGranularity must be greater than or equal to 0.1. It is not a ' + 'good idea to resize the font with a higher accuracy.'); + assert(widget.minFontSize >= 0, + 'MinFontSize must be greater than or equal to 0.'); + assert(widget.maxFontSize > 0, 'MaxFontSize has to be greater than 0.'); + assert(widget.minFontSize <= widget.maxFontSize, + 'MinFontSize must be smaller or equal than maxFontSize.'); + assert(widget.minFontSize / widget.stepGranularity % 1 == 0, + 'MinFontSize must be a multiple of stepGranularity.'); + if (widget.maxFontSize != double.infinity) { + assert(widget.maxFontSize / widget.stepGranularity % 1 == 0, + 'MaxFontSize must be a multiple of stepGranularity.'); + } + } else { + assert(widget.presetFontSizes!.isNotEmpty, + 'PresetFontSizes must not be empty.'); + } + } + + List _calculateFontSize( + BoxConstraints size, TextStyle? style, int? maxLines) { + final span = TextSpan( + style: widget.textSpan?.style ?? style, + text: widget.textSpan?.text ?? widget.data, + children: widget.textSpan?.children, + recognizer: widget.textSpan?.recognizer, + ); + + final userScale = + widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); + + int left; + int right; + + final presetFontSizes = widget.presetFontSizes?.reversed.toList(); + if (presetFontSizes == null) { + final num defaultFontSize = + style!.fontSize!.clamp(widget.minFontSize, widget.maxFontSize); + final defaultScale = defaultFontSize * userScale / style.fontSize!; + if (_checkTextFits(span, defaultScale, maxLines, size)) { + return [defaultFontSize * userScale, true]; + } + + left = (widget.minFontSize / widget.stepGranularity).floor(); + right = (defaultFontSize / widget.stepGranularity).ceil(); + } else { + left = 0; + right = presetFontSizes.length - 1; + } + + var lastValueFits = false; + while (left <= right) { + final mid = (left + (right - left) / 2).floor(); + double scale; + if (presetFontSizes == null) { + scale = mid * userScale * widget.stepGranularity / style!.fontSize!; + } else { + scale = presetFontSizes[mid] * userScale / style!.fontSize!; + } + if (_checkTextFits(span, scale, maxLines, size)) { + left = mid + 1; + lastValueFits = true; + } else { + right = mid - 1; + } + } + + if (!lastValueFits) { + right += 1; + } + + double fontSize; + if (presetFontSizes == null) { + fontSize = right * userScale * widget.stepGranularity; + } else { + fontSize = presetFontSizes[right] * userScale; + } + + return [fontSize, lastValueFits]; + } + + bool _checkTextFits( + TextSpan text, double scale, int? maxLines, BoxConstraints constraints) { + if (!widget.wrapWords) { + final words = text.toPlainText().split(RegExp('\\s+')); + + final wordWrapTextPainter = TextPainter( + text: TextSpan( + style: text.style, + text: words.join('\n'), + ), + textAlign: widget.textAlign ?? TextAlign.left, + textDirection: widget.textDirection ?? TextDirection.ltr, + textScaleFactor: scale, + maxLines: words.length, + locale: widget.locale, + strutStyle: widget.strutStyle, + ); + + wordWrapTextPainter.layout(maxWidth: constraints.maxWidth); + + if (wordWrapTextPainter.didExceedMaxLines || + wordWrapTextPainter.width > constraints.maxWidth) { + return false; + } + } + + final textPainter = TextPainter( + text: text, + textAlign: widget.textAlign ?? TextAlign.left, + textDirection: widget.textDirection ?? TextDirection.ltr, + textScaleFactor: scale, + maxLines: maxLines, + locale: widget.locale, + strutStyle: widget.strutStyle, + ); + + textPainter.layout(maxWidth: constraints.maxWidth); + + return !(textPainter.didExceedMaxLines || + textPainter.height > constraints.maxHeight || + textPainter.width > constraints.maxWidth); + } + + Widget _buildText( + double fontSize, TextStyle style, int? maxLines, EdgeInsets padding) { + if (widget.data != null) { + return Padding( + padding: padding, + child: Text( + widget.data!, + key: widget.textKey, + style: style.copyWith(fontSize: fontSize), + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textDirection: widget.textDirection, + locale: widget.locale, + softWrap: widget.softWrap, + overflow: widget.overflow, + textScaleFactor: 1, + maxLines: maxLines, + semanticsLabel: widget.semanticsLabel, + ), + ); + } else { + return Text.rich( + widget.textSpan!, + key: widget.textKey, + style: style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textDirection: widget.textDirection, + locale: widget.locale, + softWrap: widget.softWrap, + overflow: widget.overflow, + textScaleFactor: fontSize / style.fontSize!, + maxLines: maxLines, + semanticsLabel: widget.semanticsLabel, + ); + } + } + + void _notifySync() { + setState(() {}); + } +} diff --git a/flutter/lib/ui/home/benchmark_running_screen.dart b/flutter/lib/ui/home/benchmark_running_screen.dart index 12ed2184c..9aec651d7 100644 --- a/flutter/lib/ui/home/benchmark_running_screen.dart +++ b/flutter/lib/ui/home/benchmark_running_screen.dart @@ -15,6 +15,7 @@ import 'package:mlperfbench/ui/app_styles.dart'; import 'package:mlperfbench/ui/formatter.dart'; import 'package:mlperfbench/ui/home/progress_circle.dart'; import 'package:mlperfbench/ui/icons.dart'; +import 'package:mlperfbench/ui/auto_size_text.dart'; class BenchmarkRunningScreen extends StatefulWidget { static final GlobalKey scaffoldKey = @@ -53,13 +54,13 @@ class _BenchmarkRunningScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Expanded(flex: 14, child: _title()), + _title(), const SizedBox(height: 20), - Expanded(flex: 30, child: _circle()), + _circle(), const SizedBox(height: 20), - Expanded(flex: 40, child: _taskList()), + Expanded(child: _taskList()), const SizedBox(height: 20), - Expanded(flex: 16, child: _footer()), + _footer(), ], ), ), @@ -90,51 +91,62 @@ class _BenchmarkRunningScreenState extends State { var containerWidth = 0.50 * MediaQuery.of(context).size.width; containerWidth = max(containerWidth, 160); containerWidth = min(containerWidth, 240); - return Stack( - alignment: AlignmentDirectional.center, - children: [ - Container( - width: containerWidth, - height: containerWidth, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: AppColors.progressCircle, - boxShadow: [ - BoxShadow( - color: Colors.black12, - offset: Offset(15, 15), - blurRadius: 10, - ) - ], - ), - child: ClipOval( - child: Padding( - padding: const EdgeInsets.all(4), - child: _circleContent(), + return SizedBox( + width: containerWidth, + height: containerWidth, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: AppColors.progressCircle, + boxShadow: [ + BoxShadow( + color: Colors.black12, + offset: Offset(15, 15), + blurRadius: 10, + ) + ], + ), + child: ClipOval( + child: Padding( + padding: const EdgeInsets.all(4), + child: _circleContent(containerWidth), + ), ), ), - ), - ProgressCircle( - strokeWidth: 6, - size: containerWidth + 20, - ), - ], + ProgressCircle( + strokeWidth: 6, + size: containerWidth + 20, + ), + ], + ), ); } - Widget _circleContent() { + Widget _circleContent(double containerWidth) { Widget? topWidget; String taskNameString; + + final double containerRadius = containerWidth / 2; + final double creepFactor = + 2 + ((containerWidth - 160) / 20); // lerp 2-6 at 160-240 + final double horizontalPadding = containerRadius - + sqrt(pow(containerRadius, 2) - + pow((containerRadius - (8 * creepFactor)), 2)); + const textStyle = TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.lightText, ); if (progress.cooldown) { - topWidget = Text( + topWidget = AutoSizeCircleText( l10n.progressCooldown, textAlign: TextAlign.center, style: textStyle, + circularPadding: horizontalPadding, ); taskNameString = l10n.progressRemainingTime; } else { @@ -154,7 +166,7 @@ class _BenchmarkRunningScreenState extends State { flex: 3, child: Container( alignment: Alignment.bottomCenter, - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 40), + padding: const EdgeInsets.symmetric(vertical: 4), child: topWidget, ), ), @@ -169,11 +181,14 @@ class _BenchmarkRunningScreenState extends State { flex: 3, child: Container( alignment: Alignment.topCenter, - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 20), - child: Text( + padding: const EdgeInsets.symmetric(vertical: 4), + child: AutoSizeCircleText( taskNameString, textAlign: TextAlign.center, style: textStyle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + circularPadding: horizontalPadding, ), ), ), @@ -250,9 +265,9 @@ class _BenchmarkRunningScreenState extends State { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - const Spacer(), - Expanded(flex: 2, child: _footerText()), + _footerText(), _cancelButton(), ], ); @@ -378,6 +393,7 @@ class _StageProgressTextState extends State<_StageProgressText> { progressStr, style: const TextStyle( fontSize: 54, + height: 1.0, fontWeight: FontWeight.bold, color: AppColors.lightText, ), diff --git a/flutter/lib/ui/home/benchmark_start_screen.dart b/flutter/lib/ui/home/benchmark_start_screen.dart index b7ef43c22..fd778f867 100644 --- a/flutter/lib/ui/home/benchmark_start_screen.dart +++ b/flutter/lib/ui/home/benchmark_start_screen.dart @@ -41,22 +41,12 @@ class _BenchmarkStartScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ + _goButtonSection(context), + _infoSection(), Expanded( - flex: 34, - child: _goButtonSection(context), - ), - Expanded( - flex: 6, - child: _infoSection(), - ), - Expanded( - flex: 60, - child: Align( - alignment: Alignment.topCenter, - child: AbsorbPointer( - absorbing: state.state != BenchmarkStateEnum.waiting, - child: const BenchmarkConfigSection(), - ), + child: AbsorbPointer( + absorbing: state.state != BenchmarkStateEnum.waiting, + child: const BenchmarkConfigSection(), ), ) ], @@ -81,10 +71,8 @@ class _BenchmarkStartScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded(flex: 1, child: Text(deviceDescription)), - Expanded(flex: 1, child: Text(selectedBenchmarkText)) - ], + mainAxisSize: MainAxisSize.min, + children: [Text(deviceDescription), Text(selectedBenchmarkText)], ), ), ); @@ -93,89 +81,94 @@ class _BenchmarkStartScreenState extends State { Widget _goButtonSection(BuildContext context) { final circleWidth = MediaQuery.of(context).size.width * WidgetSizes.circleWidthFactor; + const double verticalPadding = 8.0; + final sectionHeight = circleWidth + verticalPadding * 2.0; - return Stack( - alignment: Alignment.topCenter, - children: [ - Container( - width: MediaQuery.of(context).size.width, - alignment: Alignment.center, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: AppGradients.halfScreen, + return SizedBox( + width: MediaQuery.of(context).size.width, + height: sectionHeight, + child: Stack( + alignment: Alignment.topCenter, + children: [ + Container( + alignment: Alignment.center, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: AppGradients.halfScreen, + ), ), ), - ), - Container( - alignment: Alignment.center, - child: ElevatedButton( - key: const Key(WidgetKeys.goButton), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.goCircle, - shape: const CircleBorder(), - minimumSize: Size.fromWidth(circleWidth)), - child: Text( - l10n.mainScreenGo, - style: const TextStyle( - color: AppColors.lightText, - fontSize: 40, + Container( + alignment: Alignment.center, + child: ElevatedButton( + key: const Key(WidgetKeys.goButton), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.goCircle, + shape: const CircleBorder(), + minimumSize: Size.fromWidth(circleWidth)), + child: Text( + l10n.mainScreenGo, + style: const TextStyle( + color: AppColors.lightText, + fontSize: 40, + ), ), - ), - onPressed: () async { - final wrongPathError = await state.validator - .validateExternalResourcesDirectory( - l10n.dialogContentMissingFiles); - if (wrongPathError.isNotEmpty) { - if (!context.mounted) return; - final messages = [ - wrongPathError, - l10n.dialogContentMissingFilesHint - ]; - await showErrorDialog(context, messages); - return; - } - if (store.offlineMode) { - final offlineError = await state.validator - .validateOfflineMode(l10n.dialogContentOfflineWarning); - if (offlineError.isNotEmpty) { + onPressed: () async { + final wrongPathError = await state.validator + .validateExternalResourcesDirectory( + l10n.dialogContentMissingFiles); + if (wrongPathError.isNotEmpty) { if (!context.mounted) return; - switch (await showConfirmDialog(context, offlineError)) { - case ConfirmDialogAction.ok: - break; - case ConfirmDialogAction.cancel: - return; - default: - break; + final messages = [ + wrongPathError, + l10n.dialogContentMissingFilesHint + ]; + await showErrorDialog(context, messages); + return; + } + if (store.offlineMode) { + final offlineError = await state.validator + .validateOfflineMode(l10n.dialogContentOfflineWarning); + if (offlineError.isNotEmpty) { + if (!context.mounted) return; + switch (await showConfirmDialog(context, offlineError)) { + case ConfirmDialogAction.ok: + break; + case ConfirmDialogAction.cancel: + return; + default: + break; + } } } - } - final selectedCount = - state.benchmarks.where((e) => e.isActive).length; - if (selectedCount < 1) { - // Workaround for Dart linter bug. See https://github.com/dart-lang/linter/issues/4007 - // ignore: use_build_context_synchronously - if (!context.mounted) return; - await showErrorDialog( - context, [l10n.dialogContentNoSelectedBenchmarkError]); - return; - } - try { - await state.runBenchmarks(); - } catch (e, t) { - print(t); - // Workaround for Dart linter bug. See https://github.com/dart-lang/linter/issues/4007 - // ignore: use_build_context_synchronously - if (!context.mounted) return; - await showErrorDialog( - context, ['${l10n.runFail}:', e.toString()]); - return; - } - }, + final selectedCount = + state.benchmarks.where((e) => e.isActive).length; + if (selectedCount < 1) { + // Workaround for Dart linter bug. See https://github.com/dart-lang/linter/issues/4007 + // ignore: use_build_context_synchronously + if (!context.mounted) return; + await showErrorDialog( + context, [l10n.dialogContentNoSelectedBenchmarkError]); + return; + } + try { + await state.runBenchmarks(); + } catch (e, t) { + print(t); + // Workaround for Dart linter bug. See https://github.com/dart-lang/linter/issues/4007 + // ignore: use_build_context_synchronously + if (!context.mounted) return; + await showErrorDialog( + context, ['${l10n.runFail}:', e.toString()]); + return; + } + }, + ), ), - ), - ], + ], + ), ); } }