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 | |
5 | import 'template.dart'; |
6 | |
7 | class 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 | ''' |
25 | class _ ${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 | |