blob: eb84c7492b4f4cd9c00dbb5e50024d9adabec8f9 [file] [log] [blame]
// 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 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
import 'package:gallery/constants.dart';
enum CustomTextDirection {
localeBased,
ltr,
rtl,
}
// See http://en.wikipedia.org/wiki/Right-to-left
const List<String> rtlLanguages = <String>[
'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) {
_deviceLocale ??= locale;
}
class GalleryOptions {
const GalleryOptions({
required this.themeMode,
required double? textScaleFactor,
required this.customTextDirection,
required Locale? locale,
required this.timeDilation,
required this.platform,
required this.isTestMode,
}) : _textScaleFactor = textScaleFactor ?? 1.0,
_locale = locale;
final ThemeMode themeMode;
final double _textScaleFactor;
final CustomTextDirection customTextDirection;
final Locale? _locale;
final double timeDilation;
final TargetPlatform? platform;
final bool isTestMode; // True for integration tests.
// 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;
/// Returns a text direction based on the [CustomTextDirection] setting.
/// If it is based on locale and the locale cannot be determined, returns
/// null.
TextDirection? resolvedTextDirection() {
switch (customTextDirection) {
case CustomTextDirection.localeBased:
final 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;
}
}
/// Returns a [SystemUiOverlayStyle] based on the [ThemeMode] setting.
/// In other words, if the theme is dark, returns light; if the theme is
/// light, returns dark.
SystemUiOverlayStyle resolvedSystemUiOverlayStyle() {
Brightness brightness;
switch (themeMode) {
case ThemeMode.light:
brightness = Brightness.light;
break;
case ThemeMode.dark:
brightness = Brightness.dark;
break;
default:
brightness = WidgetsBinding.instance.window.platformBrightness;
}
final overlayStyle = brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark;
return overlayStyle;
}
GalleryOptions copyWith({
ThemeMode? themeMode,
double? textScaleFactor,
CustomTextDirection? customTextDirection,
Locale? locale,
double? timeDilation,
TargetPlatform? platform,
bool? isTestMode,
}) {
return GalleryOptions(
themeMode: themeMode ?? this.themeMode,
textScaleFactor: textScaleFactor ?? _textScaleFactor,
customTextDirection: customTextDirection ?? this.customTextDirection,
locale: locale ?? this.locale,
timeDilation: timeDilation ?? this.timeDilation,
platform: platform ?? this.platform,
isTestMode: isTestMode ?? this.isTestMode,
);
}
@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 &&
isTestMode == other.isTestMode;
@override
int get hashCode => Object.hash(
themeMode,
_textScaleFactor,
customTextDirection,
locale,
timeDilation,
platform,
isTestMode,
);
static GalleryOptions of(BuildContext context) {
final scope =
context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>()!;
return scope.modelBindingState.currentModel;
}
static void update(BuildContext context, GalleryOptions newModel) {
final scope =
context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>()!;
scope.modelBindingState.updateModel(newModel);
}
}
// Applies text GalleryOptions to a widget
class ApplyTextOptions extends StatelessWidget {
const ApplyTextOptions({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
final options = GalleryOptions.of(context);
final textDirection = options.resolvedTextDirection();
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 {
const _ModelBindingScope({
required this.modelBindingState,
required super.child,
});
final _ModelBindingState modelBindingState;
@override
bool updateShouldNotify(_ModelBindingScope oldWidget) => true;
}
class ModelBinding extends StatefulWidget {
const ModelBinding({
super.key,
required this.initialModel,
required this.child,
});
final GalleryOptions initialModel;
final Widget child;
@override
State<ModelBinding> createState() => _ModelBindingState();
}
class _ModelBindingState extends State<ModelBinding> {
late 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,
);
}
}