options.dart 15.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9
// 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';

10
@immutable
11
class GalleryOptions {
12
  const GalleryOptions({
13
    this.themeMode,
14
    this.textScaleFactor,
15
    this.visualDensity,
16 17
    this.textDirection = TextDirection.ltr,
    this.timeDilation = 1.0,
18
    this.platform,
19 20 21
    this.showOffscreenLayersCheckerboard = false,
    this.showRasterCacheImagesCheckerboard = false,
    this.showPerformanceOverlay = false,
22 23
  });

24 25 26
  final ThemeMode? themeMode;
  final GalleryTextScaleValue? textScaleFactor;
  final GalleryVisualDensityValue? visualDensity;
27 28
  final TextDirection textDirection;
  final double timeDilation;
29
  final TargetPlatform? platform;
30 31 32 33 34
  final bool showPerformanceOverlay;
  final bool showRasterCacheImagesCheckerboard;
  final bool showOffscreenLayersCheckerboard;

  GalleryOptions copyWith({
35 36 37 38 39 40 41 42 43
    ThemeMode? themeMode,
    GalleryTextScaleValue? textScaleFactor,
    GalleryVisualDensityValue? visualDensity,
    TextDirection? textDirection,
    double? timeDilation,
    TargetPlatform? platform,
    bool? showPerformanceOverlay,
    bool? showRasterCacheImagesCheckerboard,
    bool? showOffscreenLayersCheckerboard,
44
  }) {
45
    return GalleryOptions(
46
      themeMode: themeMode ?? this.themeMode,
47
      textScaleFactor: textScaleFactor ?? this.textScaleFactor,
48
      visualDensity: visualDensity ?? this.visualDensity,
49 50 51 52 53 54 55 56 57 58
      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
59
  bool operator ==(Object other) {
60
    if (other.runtimeType != runtimeType) {
61
      return false;
62
    }
63 64 65
    return other is GalleryOptions
        && other.themeMode == themeMode
        && other.textScaleFactor == textScaleFactor
66
        && other.visualDensity == visualDensity
67 68 69 70 71
        && other.textDirection == textDirection
        && other.platform == platform
        && other.showPerformanceOverlay == showPerformanceOverlay
        && other.showRasterCacheImagesCheckerboard == showRasterCacheImagesCheckerboard
        && other.showOffscreenLayersCheckerboard == showRasterCacheImagesCheckerboard;
72 73 74
  }

  @override
75
  int get hashCode => Object.hash(
76
    themeMode,
77
    textScaleFactor,
78
    visualDensity,
79 80 81 82 83 84 85 86 87 88
    textDirection,
    timeDilation,
    platform,
    showPerformanceOverlay,
    showRasterCacheImagesCheckerboard,
    showOffscreenLayersCheckerboard,
  );

  @override
  String toString() {
89
    return '$runtimeType($themeMode)';
90 91 92 93
  }
}

const double _kItemHeight = 48.0;
94
const EdgeInsetsDirectional _kItemPadding = EdgeInsetsDirectional.only(start: 56.0);
95 96

class _OptionsItem extends StatelessWidget {
97
  const _OptionsItem({ this.child });
98

99
  final Widget? child;
100 101 102

  @override
  Widget build(BuildContext context) {
103 104
    // ignore: deprecated_member_use, https://github.com/flutter/flutter/issues/128825
    final double textScaleFactor = MediaQuery.textScalerOf(context).textScaleFactor;
105

106 107 108
    return MergeSemantics(
      child: Container(
        constraints: BoxConstraints(minHeight: _kItemHeight * textScaleFactor),
109 110
        padding: _kItemPadding,
        alignment: AlignmentDirectional.centerStart,
111
        child: DefaultTextStyle(
112 113 114
          style: DefaultTextStyle.of(context).style,
          maxLines: 2,
          overflow: TextOverflow.fade,
115
          child: IconTheme(
116
            data: Theme.of(context).primaryIconTheme,
117
            child: child!,
118
          ),
119 120 121 122 123 124 125
        ),
      ),
    );
  }
}

class _BooleanItem extends StatelessWidget {
126
  const _BooleanItem(this.title, this.value, this.onChanged, { this.switchKey });
127 128 129 130

  final String title;
  final bool value;
  final ValueChanged<bool> onChanged;
131
  // [switchKey] is used for accessing the switch from driver tests.
132
  final Key? switchKey;
133 134 135 136

  @override
  Widget build(BuildContext context) {
    final bool isDark = Theme.of(context).brightness == Brightness.dark;
137 138
    return _OptionsItem(
      child: Row(
139
        children: <Widget>[
140 141
          Expanded(child: Text(title)),
          Switch(
142
            key: switchKey,
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
            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;
158
  final VoidCallback? onTap;
159 160 161

  @override
  Widget build(BuildContext context) {
162
    return _OptionsItem(
163
      child: _TextButton(
164
        onPressed: onTap,
165
        child: Text(text),
166 167 168 169 170
      ),
    );
  }
}

171
class _TextButton extends StatelessWidget {
172
  const _TextButton({ this.onPressed, this.child });
173

174 175
  final VoidCallback? onPressed;
  final Widget? child;
176 177 178

  @override
  Widget build(BuildContext context) {
179 180 181
    final ThemeData theme = Theme.of(context);
    return TextButton(
      style: TextButton.styleFrom(
182
        foregroundColor: theme.colorScheme.onPrimary,
183
        textStyle: theme.textTheme.titleMedium,
184
        padding: EdgeInsets.zero,
185
      ),
186
      onPressed: onPressed,
187
      child: child!,
188 189 190 191 192 193 194 195 196 197 198 199
    );
  }
}

class _Heading extends StatelessWidget {
  const _Heading(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
200 201
    return _OptionsItem(
      child: DefaultTextStyle(
202
        style: theme.textTheme.titleLarge!.copyWith(
203
          fontFamily: 'GoogleSans',
204 205
          color: theme.colorScheme.onPrimary,
          fontWeight: FontWeight.w700,
206
        ),
207
        child: Semantics(
208
          header: true,
209
          child: Text(text),
210 211 212 213 214 215
        ),
      ),
    );
  }
}

216 217
class _ThemeModeItem extends StatelessWidget {
  const _ThemeModeItem(this.options, this.onOptionsChanged);
218

219 220
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
221

222 223 224 225 226 227
  static final Map<ThemeMode, String> modeLabels = <ThemeMode, String>{
    ThemeMode.system: 'System Default',
    ThemeMode.light: 'Light',
    ThemeMode.dark: 'Dark',
  };

228 229
  @override
  Widget build(BuildContext context) {
230 231 232 233 234 235 236 237 238
    return _OptionsItem(
      child: Row(
        children: <Widget>[
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text('Theme'),
                Text(
239
                  modeLabels[options!.themeMode!]!,
240
                  style: Theme.of(context).primaryTextTheme.bodyMedium,
241 242 243
                ),
              ],
            ),
244
          ),
245 246 247
          PopupMenuButton<ThemeMode>(
            padding: const EdgeInsetsDirectional.only(end: 16.0),
            icon: const Icon(Icons.arrow_drop_down),
248
            initialValue: options!.themeMode,
249 250 251 252
            itemBuilder: (BuildContext context) {
              return ThemeMode.values.map<PopupMenuItem<ThemeMode>>((ThemeMode mode) {
                return PopupMenuItem<ThemeMode>(
                  value: mode,
253
                  child: Text(modeLabels[mode]!),
254 255 256 257
                );
              }).toList();
            },
            onSelected: (ThemeMode mode) {
258 259
              onOptionsChanged!(
                options!.copyWith(themeMode: mode),
260 261 262 263 264
              );
            },
          ),
        ],
      ),
265 266 267 268 269 270 271
    );
  }
}

class _TextScaleFactorItem extends StatelessWidget {
  const _TextScaleFactorItem(this.options, this.onOptionsChanged);

272 273
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
274 275 276

  @override
  Widget build(BuildContext context) {
277 278
    return _OptionsItem(
      child: Row(
279
        children: <Widget>[
280 281
          Expanded(
            child: Column(
282 283 284
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text('Text size'),
285
                Text(
286
                  options!.textScaleFactor!.label,
287
                  style: Theme.of(context).primaryTextTheme.bodyMedium,
288 289 290 291
                ),
              ],
            ),
          ),
292
          PopupMenuButton<GalleryTextScaleValue>(
293 294 295
            padding: const EdgeInsetsDirectional.only(end: 16.0),
            icon: const Icon(Icons.arrow_drop_down),
            itemBuilder: (BuildContext context) {
296
              return kAllGalleryTextScaleValues.map<PopupMenuItem<GalleryTextScaleValue>>((GalleryTextScaleValue scaleValue) {
297
                return PopupMenuItem<GalleryTextScaleValue>(
298
                  value: scaleValue,
299
                  child: Text(scaleValue.label),
300 301 302 303
                );
              }).toList();
            },
            onSelected: (GalleryTextScaleValue scaleValue) {
304 305
              onOptionsChanged!(
                options!.copyWith(textScaleFactor: scaleValue),
306 307 308 309 310 311 312 313 314
              );
            },
          ),
        ],
      ),
    );
  }
}

315 316 317
class _VisualDensityItem extends StatelessWidget {
  const _VisualDensityItem(this.options, this.onOptionsChanged);

318 319
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
320 321 322 323 324 325 326 327 328 329 330 331

  @override
  Widget build(BuildContext context) {
    return _OptionsItem(
      child: Row(
        children: <Widget>[
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text('Visual density'),
                Text(
332
                  options!.visualDensity!.label,
333
                  style: Theme.of(context).primaryTextTheme.bodyMedium,
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
                ),
              ],
            ),
          ),
          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) {
350 351
              onOptionsChanged!(
                options!.copyWith(visualDensity: densityValue),
352 353 354 355 356 357 358 359 360
              );
            },
          ),
        ],
      ),
    );
  }
}

361 362 363
class _TextDirectionItem extends StatelessWidget {
  const _TextDirectionItem(this.options, this.onOptionsChanged);

364 365
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
366 367 368

  @override
  Widget build(BuildContext context) {
369
    return _BooleanItem(
370
      'Force RTL',
371
      options!.textDirection == TextDirection.rtl,
372
      (bool value) {
373 374
        onOptionsChanged!(
          options!.copyWith(
375 376 377 378
            textDirection: value ? TextDirection.rtl : TextDirection.ltr,
          ),
        );
      },
379
      switchKey: const Key('text_direction'),
380 381 382 383 384 385 386
    );
  }
}

class _TimeDilationItem extends StatelessWidget {
  const _TimeDilationItem(this.options, this.onOptionsChanged);

387 388
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
389 390 391

  @override
  Widget build(BuildContext context) {
392
    return _BooleanItem(
393
      'Slow motion',
394
      options!.timeDilation != 1.0,
395
      (bool value) {
396 397
        onOptionsChanged!(
          options!.copyWith(
398 399 400 401
            timeDilation: value ? 20.0 : 1.0,
          ),
        );
      },
402
      switchKey: const Key('slow_motion'),
403 404 405 406 407 408 409
    );
  }
}

class _PlatformItem extends StatelessWidget {
  const _PlatformItem(this.options, this.onOptionsChanged);

410 411
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
412

413 414
  String _platformLabel(TargetPlatform platform) {
    switch (platform) {
415 416 417 418 419 420
      case TargetPlatform.android:
        return 'Mountain View';
      case TargetPlatform.fuchsia:
        return 'Fuchsia';
      case TargetPlatform.iOS:
        return 'Cupertino';
421 422
      case TargetPlatform.linux:
        return 'Material Desktop (linux)';
423 424
      case TargetPlatform.macOS:
        return 'Material Desktop (macOS)';
425 426
      case TargetPlatform.windows:
        return 'Material Desktop (Windows)';
427 428 429 430 431
    }
  }

  @override
  Widget build(BuildContext context) {
432 433
    return _OptionsItem(
      child: Row(
434
        children: <Widget>[
435 436
          Expanded(
            child: Column(
437 438 439
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Text('Platform mechanics'),
440
                 Text(
441
                   _platformLabel(options!.platform!),
442
                   style: Theme.of(context).primaryTextTheme.bodyMedium,
443 444 445 446
                 ),
              ],
            ),
          ),
447
          PopupMenuButton<TargetPlatform>(
448 449 450 451
            padding: const EdgeInsetsDirectional.only(end: 16.0),
            icon: const Icon(Icons.arrow_drop_down),
            itemBuilder: (BuildContext context) {
              return TargetPlatform.values.map((TargetPlatform platform) {
452
                return PopupMenuItem<TargetPlatform>(
453
                  value: platform,
454
                  child: Text(_platformLabel(platform)),
455 456 457 458
                );
              }).toList();
            },
            onSelected: (TargetPlatform platform) {
459 460
              onOptionsChanged!(
                options!.copyWith(platform: platform),
461 462 463 464 465 466 467 468 469 470 471
              );
            },
          ),
        ],
      ),
    );
  }
}

class GalleryOptionsPage extends StatelessWidget {
  const GalleryOptionsPage({
472
    super.key,
473 474 475
    this.options,
    this.onOptionsChanged,
    this.onSendFeedback,
476
  });
477

478 479 480
  final GalleryOptions? options;
  final ValueChanged<GalleryOptions>? onOptionsChanged;
  final VoidCallback? onSendFeedback;
481 482 483 484

  List<Widget> _enabledDiagnosticItems() {
    // Boolean showFoo options with a value of null: don't display
    // the showFoo option at all.
485
    if (options == null) {
486
      return const <Widget>[];
487
    }
488

489
    return <Widget>[
490 491
      const Divider(),
      const _Heading('Diagnostics'),
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
      _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));
        },
      ),
513
    ];
514 515 516 517 518 519
  }

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);

520
    return DefaultTextStyle(
521
      style: theme.primaryTextTheme.titleMedium!,
522
      child: ListView(
523 524 525
        padding: const EdgeInsets.only(bottom: 124.0),
        children: <Widget>[
          const _Heading('Display'),
526
          _ThemeModeItem(options, onOptionsChanged),
527
          _TextScaleFactorItem(options, onOptionsChanged),
528
          _VisualDensityItem(options, onOptionsChanged),
529 530
          _TextDirectionItem(options, onOptionsChanged),
          _TimeDilationItem(options, onOptionsChanged),
531
          const Divider(),
532
          const ExcludeSemantics(child: _Heading('Platform mechanics')),
533
          _PlatformItem(options, onOptionsChanged),
534 535 536 537 538 539 540 541
          ..._enabledDiagnosticItems(),
          const Divider(),
          const _Heading('Flutter gallery'),
          _ActionItem('About Flutter Gallery', () {
            showGalleryAboutDialog(context);
          }),
          _ActionItem('Send feedback', onSendFeedback),
        ],
542 543 544 545
      ),
    );
  }
}