// Copyright 2019 The Flutter team. 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:async'; import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' show timeDilation; import 'package:gallery/constants.dart'; enum CustomTextDirection { localeBased, ltr, rtl, } // See http://en.wikipedia.org/wiki/Right-to-left const List rtlLanguages = [ 'ar', // Arabic 'fa', // Farsi 'he', // Hebrew 'ps', // Pashto 'ur', // Urdu ]; // Fake locale to represent the system Locale option. const systemLocaleOption = Locale('system'); Locale _deviceLocale; Locale get deviceLocale => _deviceLocale; set deviceLocale(Locale locale) { if (_deviceLocale == null) { _deviceLocale = locale; } } class GalleryOptions { const GalleryOptions({ this.themeMode, double textScaleFactor, this.customTextDirection, Locale locale, this.timeDilation, this.platform, }) : _textScaleFactor = textScaleFactor, _locale = locale; final ThemeMode themeMode; final double _textScaleFactor; final CustomTextDirection customTextDirection; final Locale _locale; final double timeDilation; final TargetPlatform platform; // We use a sentinel value to indicate the system text scale option. By // default, return the actual text scale factor, otherwise return the // sentinel value. double textScaleFactor(BuildContext context, {bool useSentinel = false}) { if (_textScaleFactor == systemTextScaleFactorOption) { return useSentinel ? systemTextScaleFactorOption : MediaQuery.of(context).textScaleFactor; } else { return _textScaleFactor; } } Locale get locale => _locale ?? deviceLocale ?? // TODO: When deviceLocale can be obtained on macOS, this won't be necessary // https://github.com/flutter/flutter/issues/45343 (!kIsWeb && Platform.isMacOS ? Locale('en', 'US') : null); /// Returns the text direction based on the [CustomTextDirection] setting. /// If the locale cannot be determined, returns null. TextDirection textDirection() { switch (customTextDirection) { case CustomTextDirection.localeBased: final String language = locale?.languageCode?.toLowerCase(); if (language == null) return null; return rtlLanguages.contains(language) ? TextDirection.rtl : TextDirection.ltr; case CustomTextDirection.rtl: return TextDirection.rtl; default: return TextDirection.ltr; } } GalleryOptions copyWith({ ThemeMode themeMode, double textScaleFactor, CustomTextDirection customTextDirection, Locale locale, double timeDilation, TargetPlatform platform, }) { return GalleryOptions( themeMode: themeMode ?? this.themeMode, textScaleFactor: textScaleFactor ?? this._textScaleFactor, customTextDirection: customTextDirection ?? this.customTextDirection, locale: locale ?? this.locale, timeDilation: timeDilation ?? this.timeDilation, platform: platform ?? this.platform, ); } @override bool operator ==(Object other) => other is GalleryOptions && themeMode == other.themeMode && _textScaleFactor == other._textScaleFactor && customTextDirection == other.customTextDirection && locale == other.locale && timeDilation == other.timeDilation && platform == other.platform; @override int get hashCode => hashValues( themeMode, _textScaleFactor, customTextDirection, locale, timeDilation, platform, ); static GalleryOptions of(BuildContext context) { final _ModelBindingScope scope = context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>(); return scope.modelBindingState.currentModel; } static void update(BuildContext context, GalleryOptions newModel) { final _ModelBindingScope scope = context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>(); scope.modelBindingState.updateModel(newModel); } } // Applies text GalleryOptions to a widget class ApplyTextOptions extends StatelessWidget { const ApplyTextOptions({@required this.child}); final Widget child; @override Widget build(BuildContext context) { final options = GalleryOptions.of(context); final textDirection = options.textDirection(); final textScaleFactor = options.textScaleFactor(context); Widget widget = MediaQuery( data: MediaQuery.of(context).copyWith( textScaleFactor: textScaleFactor, ), child: child, ); return textDirection == null ? widget : Directionality( textDirection: textDirection, child: widget, ); } } // Everything below is boilerplate except code relating to time dilation. // See https://medium.com/flutter/managing-flutter-application-state-with-inheritedwidgets-1140452befe1 class _ModelBindingScope extends InheritedWidget { _ModelBindingScope({ Key key, @required this.modelBindingState, Widget child, }) : assert(modelBindingState != null), super(key: key, child: child); final _ModelBindingState modelBindingState; @override bool updateShouldNotify(_ModelBindingScope oldWidget) => true; } class ModelBinding extends StatefulWidget { ModelBinding({ Key key, this.initialModel = const GalleryOptions(), this.child, }) : assert(initialModel != null), super(key: key); final GalleryOptions initialModel; final Widget child; _ModelBindingState createState() => _ModelBindingState(); } class _ModelBindingState extends State { GalleryOptions currentModel; Timer _timeDilationTimer; @override void initState() { super.initState(); currentModel = widget.initialModel; } @override void dispose() { _timeDilationTimer?.cancel(); _timeDilationTimer = null; super.dispose(); } void handleTimeDilation(GalleryOptions newModel) { if (currentModel.timeDilation != newModel.timeDilation) { _timeDilationTimer?.cancel(); _timeDilationTimer = null; if (newModel.timeDilation > 1) { // We delay the time dilation change long enough that the user can see // that UI has started reacting and then we slam on the brakes so that // they see that the time is in fact now dilated. _timeDilationTimer = Timer(const Duration(milliseconds: 150), () { timeDilation = newModel.timeDilation; }); } else { timeDilation = newModel.timeDilation; } } } void updateModel(GalleryOptions newModel) { if (newModel != currentModel) { handleTimeDilation(newModel); setState(() { currentModel = newModel; }); } } @override Widget build(BuildContext context) { return _ModelBindingScope( modelBindingState: this, child: widget.child, ); } }