Line data Source code
1 : // Copyright 2014 The Flutter Authors.
2 : // Copyright 2021 Suragch.
3 : // All rights reserved.
4 : // Use of this source code is governed by a BSD-style license that can be
5 : // found in the LICENSE file.
6 :
7 : import 'dart:math' as math;
8 : import 'dart:ui' show lerpDouble;
9 :
10 : import 'package:flutter/foundation.dart';
11 : import 'package:flutter/material.dart'
12 : show
13 : ButtonStyle,
14 : ButtonStyleButton,
15 : ColorScheme,
16 : Colors,
17 : InkRipple,
18 : InteractiveInkFeatureFactory,
19 : MaterialStateProperty,
20 : MaterialTapTargetSize,
21 : MaterialState,
22 : TextButtonTheme,
23 : Theme,
24 : ThemeData,
25 : VisualDensity,
26 : kThemeChangeDuration;
27 : import 'package:flutter/widgets.dart';
28 :
29 : /// A vertical Material Design "Text Button".
30 : ///
31 : /// Use text buttons on toolbars, in dialogs, or inline with other
32 : /// content but offset from that content with padding so that the
33 : /// button's presence is obvious. Text buttons do not have visible
34 : /// borders and must therefore rely on their position relative to
35 : /// other content for context. In dialogs and cards, they should be
36 : /// grouped together in one of the bottom corners. Avoid using text
37 : /// buttons where they would blend in with other content, for example
38 : /// in the middle of lists.
39 : ///
40 : /// A text button is a label [child] displayed on a (zero elevation)
41 : /// [Material] widget. The label's [MongolText] and [Icon] widgets are
42 : /// displayed in the [style]'s [ButtonStyle.foregroundColor]. The
43 : /// button reacts to touches by filling with the [style]'s
44 : /// [ButtonStyle.backgroundColor].
45 : ///
46 : /// The text button's default style is defined by [defaultStyleOf].
47 : /// The style of this text button can be overridden with its [style]
48 : /// parameter. The style of all text buttons in a subtree can be
49 : /// overridden with the [TextButtonTheme] and the style of all of the
50 : /// text buttons in an app can be overridden with the [Theme]'s
51 : /// [ThemeData.textButtonTheme] property.
52 : ///
53 : /// The static [styleFrom] method is a convenient way to create a
54 : /// text button [ButtonStyle] from simple values.
55 : ///
56 : /// If the [onPressed] and [onLongPress] callbacks are null, then this
57 : /// button will be disabled, it will not react to touch.
58 : ///
59 : /// {@tool dartpad --template=stateless_widget_scaffold}
60 : ///
61 : /// This sample shows how to render a disabled TextButton, an enabled MongolTextButton
62 : /// and lastly a MongolTextButton with gradient background.
63 : ///
64 : /// ```dart
65 : /// Widget build(BuildContext context) {
66 : /// return Center(
67 : /// child: Column(
68 : /// mainAxisSize: MainAxisSize.min,
69 : /// children: <Widget>[
70 : /// MongolTextButton(
71 : /// style: MongolTextButton.styleFrom(
72 : /// textStyle: const TextStyle(fontSize: 20),
73 : /// ),
74 : /// onPressed: null,
75 : /// child: const Text('Disabled'),
76 : /// ),
77 : /// const SizedBox(height: 30),
78 : /// MongolTextButton(
79 : /// style: MongolTextButton.styleFrom(
80 : /// textStyle: const TextStyle(fontSize: 20),
81 : /// ),
82 : /// onPressed: () {},
83 : /// child: const MongolText('Enabled'),
84 : /// ),
85 : /// const SizedBox(height: 30),
86 : /// ClipRRect(
87 : /// borderRadius: BorderRadius.circular(4),
88 : /// child: Stack(
89 : /// children: <Widget>[
90 : /// Positioned.fill(
91 : /// child: Container(
92 : /// decoration: const BoxDecoration(
93 : /// gradient: LinearGradient(
94 : /// colors: <Color>[
95 : /// Color(0xFF0D47A1),
96 : /// Color(0xFF1976D2),
97 : /// Color(0xFF42A5F5),
98 : /// ],
99 : /// ),
100 : /// ),
101 : /// ),
102 : /// ),
103 : /// MongolTextButton(
104 : /// style: MongolTextButton.styleFrom(
105 : /// padding: const EdgeInsets.all(16.0),
106 : /// primary: Colors.white,
107 : /// textStyle: const TextStyle(fontSize: 20),
108 : /// ),
109 : /// onPressed: () {},
110 : /// child: const MongolText('Gradient'),
111 : /// ),
112 : /// ],
113 : /// ),
114 : /// ),
115 : /// ],
116 : /// ),
117 : /// );
118 : /// }
119 : ///
120 : /// ```
121 : /// {@end-tool}
122 : ///
123 : /// See also:
124 : ///
125 : /// * [MongolOutlinedButton], a [MongolTextButton] with a border outline.
126 : /// * [MongolElevatedButton], a filled button whose material elevates when pressed.
127 : /// * <https://material.io/design/components/buttons.html>
128 : class MongolTextButton extends ButtonStyleButton {
129 : /// Create a MongolTextButton.
130 : ///
131 : /// The [autofocus] and [clipBehavior] arguments must not be null.
132 2 : const MongolTextButton({
133 : Key? key,
134 : required VoidCallback? onPressed,
135 : VoidCallback? onLongPress,
136 : ButtonStyle? style,
137 : FocusNode? focusNode,
138 : bool autofocus = false,
139 : Clip clipBehavior = Clip.none,
140 : required Widget child,
141 1 : }) : super(
142 : key: key,
143 : onPressed: onPressed,
144 : onLongPress: onLongPress,
145 : style: style,
146 : focusNode: focusNode,
147 : autofocus: autofocus,
148 : clipBehavior: clipBehavior,
149 : child: child,
150 : );
151 :
152 : /// Create a text button from a pair of widgets that serve as the button's
153 : /// [icon] and [label].
154 : ///
155 : /// The icon and label are arranged in a column and padded by 8 logical pixels
156 : /// at the ends, with an 8 pixel gap in between.
157 : ///
158 : /// The [icon] and [label] arguments must not be null.
159 : factory MongolTextButton.icon({
160 : Key? key,
161 : required VoidCallback? onPressed,
162 : VoidCallback? onLongPress,
163 : ButtonStyle? style,
164 : FocusNode? focusNode,
165 : bool? autofocus,
166 : Clip? clipBehavior,
167 : required Widget icon,
168 : required Widget label,
169 : }) = _MongolTextButtonWithIcon;
170 :
171 : /// A static convenience method that constructs a text button
172 : /// [ButtonStyle] given simple values.
173 : ///
174 : /// The [primary], and [onSurface] colors are used to create a
175 : /// [MaterialStateProperty] [ButtonStyle.foregroundColor] value in the same
176 : /// way that [defaultStyleOf] uses the [ColorScheme] colors with the same
177 : /// names. Specify a value for [primary] to specify the color of the button's
178 : /// text and icons as well as the overlay colors used to indicate the hover,
179 : /// focus, and pressed states. Use [onSurface] to specify the button's
180 : /// disabled text and icon color.
181 : ///
182 : /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
183 : /// parameters are used to construct [ButtonStyle.mouseCursor].
184 : ///
185 : /// All of the other parameters are either used directly or used to
186 : /// create a [MaterialStateProperty] with a single value for all
187 : /// states.
188 : ///
189 : /// All parameters default to null. By default this method returns
190 : /// a [ButtonStyle] that doesn't override anything.
191 : ///
192 : /// For example, to override the default text and icon colors for a
193 : /// [MongolTextButton], as well as its overlay color, with all of the
194 : /// standard opacity adjustments for the pressed, focused, and
195 : /// hovered states, one could write:
196 : ///
197 : /// ```dart
198 : /// MongolTextButton(
199 : /// style: TextButton.styleFrom(primary: Colors.green),
200 : /// )
201 : /// ```
202 1 : static ButtonStyle styleFrom({
203 : Color? primary,
204 : Color? onSurface,
205 : Color? backgroundColor,
206 : Color? shadowColor,
207 : double? elevation,
208 : TextStyle? textStyle,
209 : EdgeInsetsGeometry? padding,
210 : Size? minimumSize,
211 : Size? fixedSize,
212 : Size? maximumSize,
213 : BorderSide? side,
214 : OutlinedBorder? shape,
215 : MouseCursor? enabledMouseCursor,
216 : MouseCursor? disabledMouseCursor,
217 : VisualDensity? visualDensity,
218 : MaterialTapTargetSize? tapTargetSize,
219 : Duration? animationDuration,
220 : bool? enableFeedback,
221 : AlignmentGeometry? alignment,
222 : InteractiveInkFeatureFactory? splashFactory,
223 : }) {
224 : final MaterialStateProperty<Color?>? foregroundColor =
225 : (onSurface == null && primary == null)
226 : ? null
227 1 : : _TextButtonDefaultForeground(primary, onSurface);
228 : final MaterialStateProperty<Color?>? overlayColor =
229 1 : (primary == null) ? null : _TextButtonDefaultOverlay(primary);
230 : final MaterialStateProperty<MouseCursor>? mouseCursor =
231 : (enabledMouseCursor == null && disabledMouseCursor == null)
232 : ? null
233 1 : : _TextButtonDefaultMouseCursor(
234 : enabledMouseCursor!, disabledMouseCursor!);
235 :
236 1 : return ButtonStyle(
237 1 : textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
238 1 : backgroundColor: ButtonStyleButton.allOrNull<Color>(backgroundColor),
239 : foregroundColor: foregroundColor,
240 : overlayColor: overlayColor,
241 1 : shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
242 1 : elevation: ButtonStyleButton.allOrNull<double>(elevation),
243 1 : padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
244 1 : minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
245 1 : fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
246 : // TODO: add when becomes available in stable channel
247 : //maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
248 1 : side: ButtonStyleButton.allOrNull<BorderSide>(side),
249 1 : shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
250 : mouseCursor: mouseCursor,
251 : visualDensity: visualDensity,
252 : tapTargetSize: tapTargetSize,
253 : animationDuration: animationDuration,
254 : enableFeedback: enableFeedback,
255 : alignment: alignment,
256 : splashFactory: splashFactory,
257 : );
258 : }
259 :
260 : /// Defines the button's default appearance.
261 : ///
262 : /// The button [child]'s [MongolText] and [Icon] widgets are rendered with
263 : /// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
264 : /// the style's overlay color when the button is focused, hovered
265 : /// or pressed. The button's background color becomes its [Material]
266 : /// color and is transparent by default.
267 : ///
268 : /// All of the ButtonStyle's defaults appear below.
269 : ///
270 : /// In this list "Theme.foo" is shorthand for
271 : /// `Theme.of(context).foo`. Color scheme values like
272 : /// "onSurface(0.38)" are shorthand for
273 : /// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued
274 : /// properties that are not followed by a sublist have the same
275 : /// value for all states, otherwise the values are as specified for
276 : /// each state and "others" means all other states.
277 : ///
278 : /// The `textScaleFactor` is the value of
279 : /// `MediaQuery.of(context).textScaleFactor` and the names of the
280 : /// EdgeInsets constructors and `EdgeInsetsGeometry.lerp` have been
281 : /// abbreviated for readability.
282 : ///
283 : /// The color of the [ButtonStyle.textStyle] is not used, the
284 : /// [ButtonStyle.foregroundColor] color is used instead.
285 : ///
286 : /// * `textStyle` - Theme.textTheme.button
287 : /// * `backgroundColor` - transparent
288 : /// * `foregroundColor`
289 : /// * disabled - Theme.colorScheme.onSurface(0.38)
290 : /// * others - Theme.colorScheme.primary
291 : /// * `overlayColor`
292 : /// * hovered - Theme.colorScheme.primary(0.04)
293 : /// * focused or pressed - Theme.colorScheme.primary(0.12)
294 : /// * `shadowColor` - Theme.shadowColor
295 : /// * `elevation` - 0
296 : /// * `padding`
297 : /// * `textScaleFactor <= 1` - all(8)
298 : /// * `1 < textScaleFactor <= 2` - lerp(all(8), vertical(8))
299 : /// * `2 < textScaleFactor <= 3` - lerp(vertical(8), vertical(4))
300 : /// * `3 < textScaleFactor` - vertical(4)
301 : /// * `minimumSize` - Size(36, 64)
302 : /// * `fixedSize` - null
303 : /// * `maximumSize` - Size.infinite
304 : /// * `side` - null
305 : /// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
306 : /// * `mouseCursor`
307 : /// * disabled - SystemMouseCursors.forbidden
308 : /// * others - SystemMouseCursors.click
309 : /// * `visualDensity` - theme.visualDensity
310 : /// * `tapTargetSize` - theme.materialTapTargetSize
311 : /// * `animationDuration` - kThemeChangeDuration
312 : /// * `enableFeedback` - true
313 : /// * `alignment` - Alignment.center
314 : /// * `splashFactory` - InkRipple.splashFactory
315 : ///
316 : /// The default padding values for the [MongolTextButton.icon] factory are slightly different:
317 : ///
318 : /// * `padding`
319 : /// * `textScaleFactor <= 1` - all(8)
320 : /// * `1 < textScaleFactor <= 2 `- lerp(all(8), vertical(4))
321 : /// * `2 < textScaleFactor` - vertical(4)
322 : ///
323 : /// The default value for `side`, which defines the appearance of the button's
324 : /// outline, is null. That means that the outline is defined by the button
325 : /// shape's [OutlinedBorder.side]. Typically the default value of an
326 : /// [OutlinedBorder]'s side is [BorderSide.none], so an outline is not drawn.
327 1 : @override
328 : ButtonStyle defaultStyleOf(BuildContext context) {
329 1 : final ThemeData theme = Theme.of(context);
330 1 : final ColorScheme colorScheme = theme.colorScheme;
331 :
332 1 : final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
333 : const EdgeInsets.all(8),
334 : const EdgeInsets.symmetric(vertical: 8),
335 : const EdgeInsets.symmetric(vertical: 4),
336 2 : MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
337 : );
338 :
339 1 : return styleFrom(
340 1 : primary: colorScheme.primary,
341 1 : onSurface: colorScheme.onSurface,
342 : backgroundColor: Colors.transparent,
343 1 : shadowColor: theme.shadowColor,
344 : elevation: 0,
345 2 : textStyle: theme.textTheme.button,
346 : padding: scaledPadding,
347 : minimumSize: const Size(36, 64),
348 : maximumSize: Size.infinite,
349 : side: null,
350 : shape: const RoundedRectangleBorder(
351 : borderRadius: BorderRadius.all(Radius.circular(4))),
352 : enabledMouseCursor: SystemMouseCursors.click,
353 : disabledMouseCursor: SystemMouseCursors.forbidden,
354 1 : visualDensity: theme.visualDensity,
355 1 : tapTargetSize: theme.materialTapTargetSize,
356 : animationDuration: kThemeChangeDuration,
357 : enableFeedback: true,
358 : alignment: Alignment.center,
359 : splashFactory: InkRipple.splashFactory,
360 : );
361 : }
362 :
363 : /// Returns the [TextButtonThemeData.style] of the closest
364 : /// [TextButtonTheme] ancestor.
365 1 : @override
366 : ButtonStyle? themeStyleOf(BuildContext context) {
367 2 : return TextButtonTheme.of(context).style;
368 : }
369 : }
370 :
371 : @immutable
372 : class _TextButtonDefaultForeground extends MaterialStateProperty<Color?> {
373 1 : _TextButtonDefaultForeground(this.primary, this.onSurface);
374 :
375 : final Color? primary;
376 : final Color? onSurface;
377 :
378 1 : @override
379 : Color? resolve(Set<MaterialState> states) {
380 1 : if (states.contains(MaterialState.disabled)) {
381 2 : return onSurface?.withOpacity(0.38);
382 : }
383 1 : return primary;
384 : }
385 :
386 0 : @override
387 : String toString() {
388 0 : return '{disabled: ${onSurface?.withOpacity(0.38)}, otherwise: $primary}';
389 : }
390 : }
391 :
392 : @immutable
393 : class _TextButtonDefaultOverlay extends MaterialStateProperty<Color?> {
394 1 : _TextButtonDefaultOverlay(this.primary);
395 :
396 : final Color primary;
397 :
398 1 : @override
399 : Color? resolve(Set<MaterialState> states) {
400 1 : if (states.contains(MaterialState.hovered)) {
401 2 : return primary.withOpacity(0.04);
402 : }
403 1 : if (states.contains(MaterialState.focused) ||
404 1 : states.contains(MaterialState.pressed)) {
405 2 : return primary.withOpacity(0.12);
406 : }
407 : return null;
408 : }
409 :
410 0 : @override
411 : String toString() {
412 0 : return '{hovered: ${primary.withOpacity(0.04)}, focused,pressed: ${primary.withOpacity(0.12)}, otherwise: null}';
413 : }
414 : }
415 :
416 : @immutable
417 : class _TextButtonDefaultMouseCursor extends MaterialStateProperty<MouseCursor>
418 : with Diagnosticable {
419 1 : _TextButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
420 :
421 : final MouseCursor enabledCursor;
422 : final MouseCursor disabledCursor;
423 :
424 1 : @override
425 : MouseCursor resolve(Set<MaterialState> states) {
426 2 : if (states.contains(MaterialState.disabled)) return disabledCursor;
427 1 : return enabledCursor;
428 : }
429 : }
430 :
431 : class _MongolTextButtonWithIcon extends MongolTextButton {
432 1 : _MongolTextButtonWithIcon({
433 : Key? key,
434 : required VoidCallback? onPressed,
435 : VoidCallback? onLongPress,
436 : ButtonStyle? style,
437 : FocusNode? focusNode,
438 : bool? autofocus,
439 : Clip? clipBehavior,
440 : required Widget icon,
441 : required Widget label,
442 1 : }) : super(
443 : key: key,
444 : onPressed: onPressed,
445 : onLongPress: onLongPress,
446 : style: style,
447 : focusNode: focusNode,
448 : autofocus: autofocus ?? false,
449 : clipBehavior: clipBehavior ?? Clip.none,
450 1 : child: _MongolTextButtonWithIconChild(icon: icon, label: label),
451 : );
452 :
453 1 : @override
454 : ButtonStyle defaultStyleOf(BuildContext context) {
455 1 : final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
456 : const EdgeInsets.all(8),
457 : const EdgeInsets.symmetric(vertical: 4),
458 : const EdgeInsets.symmetric(vertical: 4),
459 2 : MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
460 : );
461 2 : return super.defaultStyleOf(context).copyWith(
462 1 : padding: MaterialStateProperty.all<EdgeInsetsGeometry>(scaledPadding),
463 : );
464 : }
465 : }
466 :
467 : class _MongolTextButtonWithIconChild extends StatelessWidget {
468 1 : const _MongolTextButtonWithIconChild({
469 : Key? key,
470 : required this.label,
471 : required this.icon,
472 1 : }) : super(key: key);
473 :
474 : final Widget label;
475 : final Widget icon;
476 :
477 1 : @override
478 : Widget build(BuildContext context) {
479 2 : final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
480 : final double gap =
481 4 : scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
482 1 : return Column(
483 : mainAxisSize: MainAxisSize.min,
484 5 : children: <Widget>[icon, SizedBox(height: gap), Flexible(child: label)],
485 : );
486 : }
487 : }
|