// Copyright 2014 The Flutter 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'; import 'about.dart'; import 'scales.dart'; @immutable class GalleryOptions { const GalleryOptions({ this.themeMode, this.textScaleFactor, this.visualDensity, this.textDirection = TextDirection.ltr, this.timeDilation = 1.0, this.platform, this.showOffscreenLayersCheckerboard = false, this.showRasterCacheImagesCheckerboard = false, this.showPerformanceOverlay = false, }); final ThemeMode? themeMode; final GalleryTextScaleValue? textScaleFactor; final GalleryVisualDensityValue? visualDensity; final TextDirection textDirection; final double timeDilation; final TargetPlatform? platform; final bool showPerformanceOverlay; final bool showRasterCacheImagesCheckerboard; final bool showOffscreenLayersCheckerboard; GalleryOptions copyWith({ ThemeMode? themeMode, GalleryTextScaleValue? textScaleFactor, GalleryVisualDensityValue? visualDensity, TextDirection? textDirection, double? timeDilation, TargetPlatform? platform, bool? showPerformanceOverlay, bool? showRasterCacheImagesCheckerboard, bool? showOffscreenLayersCheckerboard, }) { return GalleryOptions( themeMode: themeMode ?? this.themeMode, textScaleFactor: textScaleFactor ?? this.textScaleFactor, visualDensity: visualDensity ?? this.visualDensity, textDirection: textDirection ?? this.textDirection, timeDilation: timeDilation ?? this.timeDilation, platform: platform ?? this.platform, showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay, showOffscreenLayersCheckerboard: showOffscreenLayersCheckerboard ?? this.showOffscreenLayersCheckerboard, showRasterCacheImagesCheckerboard: showRasterCacheImagesCheckerboard ?? this.showRasterCacheImagesCheckerboard, ); } @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is GalleryOptions && other.themeMode == themeMode && other.textScaleFactor == textScaleFactor && other.visualDensity == visualDensity && other.textDirection == textDirection && other.platform == platform && other.showPerformanceOverlay == showPerformanceOverlay && other.showRasterCacheImagesCheckerboard == showRasterCacheImagesCheckerboard && other.showOffscreenLayersCheckerboard == showRasterCacheImagesCheckerboard; } @override int get hashCode => Object.hash( themeMode, textScaleFactor, visualDensity, textDirection, timeDilation, platform, showPerformanceOverlay, showRasterCacheImagesCheckerboard, showOffscreenLayersCheckerboard, ); @override String toString() { return '$runtimeType($themeMode)'; } } const double _kItemHeight = 48.0; const EdgeInsetsDirectional _kItemPadding = EdgeInsetsDirectional.only(start: 56.0); class _OptionsItem extends StatelessWidget { const _OptionsItem({ this.child }); final Widget? child; @override Widget build(BuildContext context) { final double textScaleFactor = MediaQuery.textScaleFactorOf(context); return MergeSemantics( child: Container( constraints: BoxConstraints(minHeight: _kItemHeight * textScaleFactor), padding: _kItemPadding, alignment: AlignmentDirectional.centerStart, child: DefaultTextStyle( style: DefaultTextStyle.of(context).style, maxLines: 2, overflow: TextOverflow.fade, child: IconTheme( data: Theme.of(context).primaryIconTheme, child: child!, ), ), ), ); } } class _BooleanItem extends StatelessWidget { const _BooleanItem(this.title, this.value, this.onChanged, { this.switchKey }); final String title; final bool value; final ValueChanged<bool> onChanged; // [switchKey] is used for accessing the switch from driver tests. final Key? switchKey; @override Widget build(BuildContext context) { final bool isDark = Theme.of(context).brightness == Brightness.dark; return _OptionsItem( child: Row( children: <Widget>[ Expanded(child: Text(title)), Switch( key: switchKey, value: value, onChanged: onChanged, activeColor: const Color(0xFF39CEFD), activeTrackColor: isDark ? Colors.white30 : Colors.black26, ), ], ), ); } } class _ActionItem extends StatelessWidget { const _ActionItem(this.text, this.onTap); final String text; final VoidCallback? onTap; @override Widget build(BuildContext context) { return _OptionsItem( child: _TextButton( onPressed: onTap, child: Text(text), ), ); } } class _TextButton extends StatelessWidget { const _TextButton({ this.onPressed, this.child }); final VoidCallback? onPressed; final Widget? child; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return TextButton( style: TextButton.styleFrom( primary: theme.colorScheme.onPrimary, textStyle: theme.textTheme.subtitle1, padding: EdgeInsets.zero, ), onPressed: onPressed, child: child!, ); } } class _Heading extends StatelessWidget { const _Heading(this.text); final String text; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return _OptionsItem( child: DefaultTextStyle( style: theme.textTheme.headline6!.copyWith( fontFamily: 'GoogleSans', color: theme.colorScheme.onPrimary, fontWeight: FontWeight.w700, ), child: Semantics( header: true, child: Text(text), ), ), ); } } class _ThemeModeItem extends StatelessWidget { const _ThemeModeItem(this.options, this.onOptionsChanged); final GalleryOptions? options; final ValueChanged<GalleryOptions>? onOptionsChanged; static final Map<ThemeMode, String> modeLabels = <ThemeMode, String>{ ThemeMode.system: 'System Default', ThemeMode.light: 'Light', ThemeMode.dark: 'Dark', }; @override Widget build(BuildContext context) { return _OptionsItem( child: Row( children: <Widget>[ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ const Text('Theme'), Text( modeLabels[options!.themeMode!]!, style: Theme.of(context).primaryTextTheme.bodyText2, ), ], ), ), PopupMenuButton<ThemeMode>( padding: const EdgeInsetsDirectional.only(end: 16.0), icon: const Icon(Icons.arrow_drop_down), initialValue: options!.themeMode, itemBuilder: (BuildContext context) { return ThemeMode.values.map<PopupMenuItem<ThemeMode>>((ThemeMode mode) { return PopupMenuItem<ThemeMode>( value: mode, child: Text(modeLabels[mode]!), ); }).toList(); }, onSelected: (ThemeMode mode) { onOptionsChanged!( options!.copyWith(themeMode: mode), ); }, ), ], ), ); } } class _TextScaleFactorItem extends StatelessWidget { const _TextScaleFactorItem(this.options, this.onOptionsChanged); final GalleryOptions? options; final ValueChanged<GalleryOptions>? onOptionsChanged; @override Widget build(BuildContext context) { return _OptionsItem( child: Row( children: <Widget>[ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ const Text('Text size'), Text( options!.textScaleFactor!.label, style: Theme.of(context).primaryTextTheme.bodyText2, ), ], ), ), PopupMenuButton<GalleryTextScaleValue>( padding: const EdgeInsetsDirectional.only(end: 16.0), icon: const Icon(Icons.arrow_drop_down), itemBuilder: (BuildContext context) { return kAllGalleryTextScaleValues.map<PopupMenuItem<GalleryTextScaleValue>>((GalleryTextScaleValue scaleValue) { return PopupMenuItem<GalleryTextScaleValue>( value: scaleValue, child: Text(scaleValue.label), ); }).toList(); }, onSelected: (GalleryTextScaleValue scaleValue) { onOptionsChanged!( options!.copyWith(textScaleFactor: scaleValue), ); }, ), ], ), ); } } class _VisualDensityItem extends StatelessWidget { const _VisualDensityItem(this.options, this.onOptionsChanged); final GalleryOptions? options; final ValueChanged<GalleryOptions>? onOptionsChanged; @override Widget build(BuildContext context) { return _OptionsItem( child: Row( children: <Widget>[ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ const Text('Visual density'), Text( options!.visualDensity!.label, style: Theme.of(context).primaryTextTheme.bodyText2, ), ], ), ), PopupMenuButton<GalleryVisualDensityValue>( padding: const EdgeInsetsDirectional.only(end: 16.0), icon: const Icon(Icons.arrow_drop_down), itemBuilder: (BuildContext context) { return kAllGalleryVisualDensityValues.map<PopupMenuItem<GalleryVisualDensityValue>>((GalleryVisualDensityValue densityValue) { return PopupMenuItem<GalleryVisualDensityValue>( value: densityValue, child: Text(densityValue.label), ); }).toList(); }, onSelected: (GalleryVisualDensityValue densityValue) { onOptionsChanged!( options!.copyWith(visualDensity: densityValue), ); }, ), ], ), ); } } class _TextDirectionItem extends StatelessWidget { const _TextDirectionItem(this.options, this.onOptionsChanged); final GalleryOptions? options; final ValueChanged<GalleryOptions>? onOptionsChanged; @override Widget build(BuildContext context) { return _BooleanItem( 'Force RTL', options!.textDirection == TextDirection.rtl, (bool value) { onOptionsChanged!( options!.copyWith( textDirection: value ? TextDirection.rtl : TextDirection.ltr, ), ); }, switchKey: const Key('text_direction'), ); } } class _TimeDilationItem extends StatelessWidget { const _TimeDilationItem(this.options, this.onOptionsChanged); final GalleryOptions? options; final ValueChanged<GalleryOptions>? onOptionsChanged; @override Widget build(BuildContext context) { return _BooleanItem( 'Slow motion', options!.timeDilation != 1.0, (bool value) { onOptionsChanged!( options!.copyWith( timeDilation: value ? 20.0 : 1.0, ), ); }, switchKey: const Key('slow_motion'), ); } } class _PlatformItem extends StatelessWidget { const _PlatformItem(this.options, this.onOptionsChanged); final GalleryOptions? options; final ValueChanged<GalleryOptions>? onOptionsChanged; String _platformLabel(TargetPlatform platform) { switch (platform) { case TargetPlatform.android: return 'Mountain View'; case TargetPlatform.fuchsia: return 'Fuchsia'; case TargetPlatform.iOS: return 'Cupertino'; case TargetPlatform.linux: return 'Material Desktop (linux)'; case TargetPlatform.macOS: return 'Material Desktop (macOS)'; case TargetPlatform.windows: return 'Material Desktop (Windows)'; } } @override Widget build(BuildContext context) { return _OptionsItem( child: Row( children: <Widget>[ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ const Text('Platform mechanics'), Text( _platformLabel(options!.platform!), style: Theme.of(context).primaryTextTheme.bodyText2, ), ], ), ), PopupMenuButton<TargetPlatform>( padding: const EdgeInsetsDirectional.only(end: 16.0), icon: const Icon(Icons.arrow_drop_down), itemBuilder: (BuildContext context) { return TargetPlatform.values.map((TargetPlatform platform) { return PopupMenuItem<TargetPlatform>( value: platform, child: Text(_platformLabel(platform)), ); }).toList(); }, onSelected: (TargetPlatform platform) { onOptionsChanged!( options!.copyWith(platform: platform), ); }, ), ], ), ); } } class GalleryOptionsPage extends StatelessWidget { const GalleryOptionsPage({ super.key, this.options, this.onOptionsChanged, this.onSendFeedback, }); final GalleryOptions? options; final ValueChanged<GalleryOptions>? onOptionsChanged; final VoidCallback? onSendFeedback; List<Widget> _enabledDiagnosticItems() { // Boolean showFoo options with a value of null: don't display // the showFoo option at all. if (options == null) return const <Widget>[]; return <Widget>[ const Divider(), const _Heading('Diagnostics'), _BooleanItem( 'Highlight offscreen layers', options!.showOffscreenLayersCheckerboard, (bool value) { onOptionsChanged!(options!.copyWith(showOffscreenLayersCheckerboard: value)); }, ), _BooleanItem( 'Highlight raster cache images', options!.showRasterCacheImagesCheckerboard, (bool value) { onOptionsChanged!(options!.copyWith(showRasterCacheImagesCheckerboard: value)); }, ), _BooleanItem( 'Show performance overlay', options!.showPerformanceOverlay, (bool value) { onOptionsChanged!(options!.copyWith(showPerformanceOverlay: value)); }, ), ]; } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); return DefaultTextStyle( style: theme.primaryTextTheme.subtitle1!, child: ListView( padding: const EdgeInsets.only(bottom: 124.0), children: <Widget>[ const _Heading('Display'), _ThemeModeItem(options, onOptionsChanged), _TextScaleFactorItem(options, onOptionsChanged), _VisualDensityItem(options, onOptionsChanged), _TextDirectionItem(options, onOptionsChanged), _TimeDilationItem(options, onOptionsChanged), const Divider(), const ExcludeSemantics(child: _Heading('Platform mechanics')), _PlatformItem(options, onOptionsChanged), ..._enabledDiagnosticItems(), const Divider(), const _Heading('Flutter gallery'), _ActionItem('About Flutter Gallery', () { showGalleryAboutDialog(context); }), _ActionItem('Send feedback', onSendFeedback), ], ), ); } }