1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'template.dart';
6
7class TimePickerTemplate extends TokenTemplate {
8 const TimePickerTemplate(
9 super.blockName,
10 super.fileName,
11 super.tokens, {
12 super.colorSchemePrefix = '_colors.',
13 super.textThemePrefix = '_textTheme.',
14 });
15
16 static const String tokenGroup = 'md.comp.time-picker';
17 static const String hourMinuteComponent = '$tokenGroup.time-selector';
18 static const String dayPeriodComponent = '$tokenGroup.period-selector';
19 static const String dialComponent = '$tokenGroup.clock-dial';
20 static const String variant = '';
21
22 @override
23 String generate() =>
24 '''
25class _${blockName}DefaultsM3 extends _TimePickerDefaults {
26 _${blockName}DefaultsM3(this.context, { this.entryMode = TimePickerEntryMode.dial });
27
28 final BuildContext context;
29 final TimePickerEntryMode entryMode;
30
31 late final ColorScheme _colors = Theme.of(context).colorScheme;
32 late final TextTheme _textTheme = Theme.of(context).textTheme;
33
34 @override
35 Color get backgroundColor {
36 return ${componentColor("$tokenGroup.container")};
37 }
38
39 @override
40 ButtonStyle get cancelButtonStyle {
41 return TextButton.styleFrom();
42 }
43
44 @override
45 ButtonStyle get confirmButtonStyle {
46 return TextButton.styleFrom();
47 }
48
49 @override
50 BorderSide get dayPeriodBorderSide {
51 return ${border('$dayPeriodComponent.outline')};
52 }
53
54 @override
55 Color get dayPeriodColor {
56 return MaterialStateColor.resolveWith((Set<MaterialState> states) {
57 if (states.contains(MaterialState.selected)) {
58 return ${componentColor("$dayPeriodComponent.selected.container")};
59 }
60 // The unselected day period should match the overall picker dialog color.
61 // Making it transparent enables that without being redundant and allows
62 // the optional elevation overlay for dark mode to be visible.
63 return Colors.transparent;
64 });
65 }
66
67 @override
68 OutlinedBorder get dayPeriodShape {
69 return ${shape("$dayPeriodComponent.container")}.copyWith(side: dayPeriodBorderSide);
70 }
71
72 @override
73 Size get dayPeriodPortraitSize {
74 return ${size('$dayPeriodComponent.vertical.container')};
75 }
76
77 @override
78 Size get dayPeriodLandscapeSize {
79 return ${size('$dayPeriodComponent.horizontal.container')};
80 }
81
82 @override
83 Size get dayPeriodInputSize {
84 // Input size is eight pixels smaller than the portrait size in the spec,
85 // but there's not token for it yet.
86 return Size(dayPeriodPortraitSize.width, dayPeriodPortraitSize.height - 8);
87 }
88
89 @override
90 Color get dayPeriodTextColor {
91 return MaterialStateColor.resolveWith((Set<MaterialState> states) {
92 if (states.contains(MaterialState.selected)) {
93 if (states.contains(MaterialState.focused)) {
94 return ${componentColor("$dayPeriodComponent.selected.focus.label-text")};
95 }
96 if (states.contains(MaterialState.hovered)) {
97 return ${componentColor("$dayPeriodComponent.selected.hover.label-text")};
98 }
99 if (states.contains(MaterialState.pressed)) {
100 return ${componentColor("$dayPeriodComponent.selected.pressed.label-text")};
101 }
102 return ${componentColor("$dayPeriodComponent.selected.label-text")};
103 }
104 if (states.contains(MaterialState.focused)) {
105 return ${componentColor("$dayPeriodComponent.unselected.focus.label-text")};
106 }
107 if (states.contains(MaterialState.hovered)) {
108 return ${componentColor("$dayPeriodComponent.unselected.hover.label-text")};
109 }
110 if (states.contains(MaterialState.pressed)) {
111 return ${componentColor("$dayPeriodComponent.unselected.pressed.label-text")};
112 }
113 return ${componentColor("$dayPeriodComponent.unselected.label-text")};
114 });
115 }
116
117 @override
118 TextStyle get dayPeriodTextStyle {
119 return ${textStyle("$dayPeriodComponent.label-text")}!.copyWith(color: dayPeriodTextColor);
120 }
121
122 @override
123 Color get dialBackgroundColor {
124 return ${componentColor(dialComponent)};
125 }
126
127 @override
128 Color get dialHandColor {
129 return ${componentColor('$dialComponent.selector.handle.container')};
130 }
131
132 @override
133 Size get dialSize {
134 return ${size("$dialComponent.container")};
135 }
136
137 @override
138 double get handWidth {
139 return ${size("$dialComponent.selector.track.container")}.width;
140 }
141
142 @override
143 double get dotRadius {
144 return ${size("$dialComponent.selector.handle.container")}.width / 2;
145 }
146
147 @override
148 double get centerRadius {
149 return ${size("$dialComponent.selector.center.container")}.width / 2;
150 }
151
152 @override
153 Color get dialTextColor {
154 return MaterialStateColor.resolveWith((Set<MaterialState> states) {
155 if (states.contains(MaterialState.selected)) {
156 return ${componentColor('$dialComponent.selected.label-text')};
157 }
158 return ${componentColor('$dialComponent.unselected.label-text')};
159 });
160 }
161
162 @override
163 TextStyle get dialTextStyle {
164 return ${textStyle('$dialComponent.label-text')}!;
165 }
166
167 @override
168 double get elevation {
169 return ${elevation("$tokenGroup.container")};
170 }
171
172 @override
173 Color get entryModeIconColor {
174 return _colors.onSurface;
175 }
176
177 @override
178 TextStyle get helpTextStyle {
179 return MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
180 final TextStyle textStyle = ${textStyle('$tokenGroup.headline')}!;
181 return textStyle.copyWith(color: ${componentColor('$tokenGroup.headline')});
182 });
183 }
184
185 @override
186 EdgeInsetsGeometry get padding {
187 return const EdgeInsets.all(24);
188 }
189
190 @override
191 Color get hourMinuteColor {
192 return MaterialStateColor.resolveWith((Set<MaterialState> states) {
193 if (states.contains(MaterialState.selected)) {
194 Color overlayColor = ${componentColor('$hourMinuteComponent.selected.container')};
195 if (states.contains(MaterialState.pressed)) {
196 overlayColor = ${componentColor('$hourMinuteComponent.selected.pressed.state-layer')};
197 } else if (states.contains(MaterialState.hovered)) {
198 const double hoverOpacity = ${opacity('$hourMinuteComponent.hover.state-layer.opacity')};
199 overlayColor = ${componentColor('$hourMinuteComponent.selected.hover.state-layer')}.withOpacity(hoverOpacity);
200 } else if (states.contains(MaterialState.focused)) {
201 const double focusOpacity = ${opacity('$hourMinuteComponent.focus.state-layer.opacity')};
202 overlayColor = ${componentColor('$hourMinuteComponent.selected.focus.state-layer')}.withOpacity(focusOpacity);
203 }
204 return Color.alphaBlend(overlayColor, ${componentColor('$hourMinuteComponent.selected.container')});
205 } else {
206 Color overlayColor = ${componentColor('$hourMinuteComponent.unselected.container')};
207 if (states.contains(MaterialState.pressed)) {
208 overlayColor = ${componentColor('$hourMinuteComponent.unselected.pressed.state-layer')};
209 } else if (states.contains(MaterialState.hovered)) {
210 const double hoverOpacity = ${opacity('$hourMinuteComponent.hover.state-layer.opacity')};
211 overlayColor = ${componentColor('$hourMinuteComponent.unselected.hover.state-layer')}.withOpacity(hoverOpacity);
212 } else if (states.contains(MaterialState.focused)) {
213 const double focusOpacity = ${opacity('$hourMinuteComponent.focus.state-layer.opacity')};
214 overlayColor = ${componentColor('$hourMinuteComponent.unselected.focus.state-layer')}.withOpacity(focusOpacity);
215 }
216 return Color.alphaBlend(overlayColor, ${componentColor('$hourMinuteComponent.unselected.container')});
217 }
218 });
219 }
220
221 @override
222 ShapeBorder get hourMinuteShape {
223 return ${shape('$hourMinuteComponent.container')};
224 }
225
226 @override
227 Size get hourMinuteSize {
228 return ${size('$hourMinuteComponent.container')};
229 }
230
231 @override
232 Size get hourMinuteSize24Hour {
233 return Size(${size('$hourMinuteComponent.24h-vertical.container')}.width, hourMinuteSize.height);
234 }
235
236 @override
237 Size get hourMinuteInputSize {
238 // Input size is eight pixels smaller than the regular size in the spec, but
239 // there's not token for it yet.
240 return Size(hourMinuteSize.width, hourMinuteSize.height - 8);
241 }
242
243 @override
244 Size get hourMinuteInputSize24Hour {
245 // Input size is eight pixels smaller than the regular size in the spec, but
246 // there's not token for it yet.
247 return Size(hourMinuteSize24Hour.width, hourMinuteSize24Hour.height - 8);
248 }
249
250 @override
251 Color get hourMinuteTextColor {
252 return MaterialStateColor.resolveWith((Set<MaterialState> states) {
253 return _hourMinuteTextColor.resolve(states);
254 });
255 }
256
257 MaterialStateProperty<Color> get _hourMinuteTextColor {
258 return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
259 if (states.contains(MaterialState.selected)) {
260 if (states.contains(MaterialState.pressed)) {
261 return ${componentColor("$hourMinuteComponent.selected.pressed.label-text")};
262 }
263 if (states.contains(MaterialState.hovered)) {
264 return ${componentColor("$hourMinuteComponent.selected.hover.label-text")};
265 }
266 if (states.contains(MaterialState.focused)) {
267 return ${componentColor("$hourMinuteComponent.selected.focus.label-text")};
268 }
269 return ${componentColor("$hourMinuteComponent.selected.label-text")};
270 } else {
271 // unselected
272 if (states.contains(MaterialState.pressed)) {
273 return ${componentColor("$hourMinuteComponent.unselected.pressed.label-text")};
274 }
275 if (states.contains(MaterialState.hovered)) {
276 return ${componentColor("$hourMinuteComponent.unselected.hover.label-text")};
277 }
278 if (states.contains(MaterialState.focused)) {
279 return ${componentColor("$hourMinuteComponent.unselected.focus.label-text")};
280 }
281 return ${componentColor("$hourMinuteComponent.unselected.label-text")};
282 }
283 });
284 }
285
286 @override
287 TextStyle get hourMinuteTextStyle {
288 return MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
289 // TODO(tahatesser): Update this when https://github.com/flutter/flutter/issues/131247 is fixed.
290 // This is using the correct text style from Material 3 spec.
291 // https://m3.material.io/components/time-pickers/specs#fd0b6939-edab-4058-82e1-93d163945215
292 return switch (entryMode) {
293 TimePickerEntryMode.dial || TimePickerEntryMode.dialOnly
294 => _textTheme.displayLarge!.copyWith(color: _hourMinuteTextColor.resolve(states)),
295 TimePickerEntryMode.input || TimePickerEntryMode.inputOnly
296 => _textTheme.displayMedium!.copyWith(color: _hourMinuteTextColor.resolve(states)),
297 };
298 });
299 }
300
301 @override
302 InputDecorationThemeData get inputDecorationTheme {
303 // This is NOT correct, but there's no token for
304 // 'time-input.container.shape', so this is using the radius from the shape
305 // for the hour/minute selector. It's a BorderRadiusGeometry, so we have to
306 // resolve it before we can use it.
307 final BorderRadius selectorRadius = ${shape('$hourMinuteComponent.container')}
308 .borderRadius
309 .resolve(Directionality.of(context));
310 return InputDecorationThemeData(
311 contentPadding: EdgeInsets.zero,
312 filled: true,
313 // This should be derived from a token, but there isn't one for 'time-input'.
314 fillColor: hourMinuteColor,
315 // This should be derived from a token, but there isn't one for 'time-input'.
316 focusColor: _colors.primaryContainer,
317 enabledBorder: OutlineInputBorder(
318 borderRadius: selectorRadius,
319 borderSide: const BorderSide(color: Colors.transparent),
320 ),
321 errorBorder: OutlineInputBorder(
322 borderRadius: selectorRadius,
323 borderSide: BorderSide(color: _colors.error, width: 2),
324 ),
325 focusedBorder: OutlineInputBorder(
326 borderRadius: selectorRadius,
327 borderSide: BorderSide(color: _colors.primary, width: 2),
328 ),
329 focusedErrorBorder: OutlineInputBorder(
330 borderRadius: selectorRadius,
331 borderSide: BorderSide(color: _colors.error, width: 2),
332 ),
333 hintStyle: hourMinuteTextStyle.copyWith(color: _colors.onSurface.withOpacity(0.36)),
334 // Prevent the error text from appearing.
335 // TODO(rami-a): Remove this workaround once
336 // https://github.com/flutter/flutter/issues/54104
337 // is fixed.
338 errorStyle: const TextStyle(fontSize: 0),
339 );
340 }
341
342 @override
343 ShapeBorder get shape {
344 return ${shape("$tokenGroup.container")};
345 }
346
347 @override
348 MaterialStateProperty<Color?>? get timeSelectorSeparatorColor {
349 // TODO(tahatesser): Update this when tokens are available.
350 // This is taken from https://m3.material.io/components/time-pickers/specs.
351 return MaterialStatePropertyAll<Color>(_colors.onSurface);
352 }
353
354 @override
355 MaterialStateProperty<TextStyle?>? get timeSelectorSeparatorTextStyle {
356 // TODO(tahatesser): Update this when tokens are available.
357 // This is taken from https://m3.material.io/components/time-pickers/specs.
358 return MaterialStatePropertyAll<TextStyle?>(_textTheme.displayLarge);
359 }
360}
361''';
362}
363