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/widgets.dart';
12 : import 'package:flutter/material.dart'
13 : show
14 : ButtonStyle,
15 : ButtonStyleButton,
16 : ColorScheme,
17 : Colors,
18 : InkRipple,
19 : InteractiveInkFeatureFactory,
20 : MaterialState,
21 : MaterialStateProperty,
22 : MaterialTapTargetSize,
23 : OutlinedButtonTheme,
24 : Theme,
25 : ThemeData,
26 : VisualDensity,
27 : kThemeChangeDuration;
28 :
29 : /// A vertical Material Design "Outlined Button"; essentially a [MongolTextButton]
30 : /// with an outlined border.
31 : ///
32 : /// Outlined buttons are medium-emphasis buttons. They contain actions
33 : /// that are important, but they aren’t the primary action in an app.
34 : ///
35 : /// An outlined button is a label [child] displayed on a (zero
36 : /// elevation) [Material] widget. The label's [MongolText] and [Icon]
37 : /// widgets are displayed in the [style]'s
38 : /// [ButtonStyle.foregroundColor] and the outline's weight and color
39 : /// are defined by [ButtonStyle.side]. The button reacts to touches
40 : /// by filling with the [style]'s [ButtonStyle.backgroundColor].
41 : ///
42 : /// The outlined button's default style is defined by [defaultStyleOf].
43 : /// The style of this outline button can be overridden with its [style]
44 : /// parameter. The style of all text buttons in a subtree can be
45 : /// overridden with the [OutlinedButtonTheme] and the style of all of the
46 : /// outlined buttons in an app can be overridden with the [Theme]'s
47 : /// [ThemeData.outlinedButtonTheme] property.
48 : ///
49 : /// Unlike [MongolTextButton] or [MongolElevatedButton], outline buttons have a
50 : /// default [ButtonStyle.side] which defines the appearance of the
51 : /// outline. Because the default `side` is non-null, it
52 : /// unconditionally overrides the shape's [OutlinedBorder.side]. In
53 : /// other words, to specify an outlined button's shape _and_ the
54 : /// appearance of its outline, both the [ButtonStyle.shape] and
55 : /// [ButtonStyle.side] properties must be specified.
56 : ///
57 : /// {@tool dartpad --template=stateless_widget_scaffold_center}
58 : ///
59 : /// Here is an example of a basic [MongolOutlinedButton].
60 : ///
61 : /// ```dart
62 : /// Widget build(BuildContext context) {
63 : /// return MongolOutlinedButton(
64 : /// onPressed: () {
65 : /// print('Received click');
66 : /// },
67 : /// child: const MongolText('Click Me'),
68 : /// );
69 : /// }
70 : /// ```
71 : /// {@end-tool}
72 : ///
73 : /// The static [styleFrom] method is a convenient way to create a
74 : /// outlined button [ButtonStyle] from simple values.
75 : ///
76 : /// See also:
77 : ///
78 : /// * [MongolElevatedButton], a filled vertical material design button with a shadow.
79 : /// * [MongolTextButton], a vertical material design button without a shadow.
80 : /// * <https://material.io/design/components/buttons.html>
81 : class MongolOutlinedButton extends ButtonStyleButton {
82 : /// Create a MongolOutlinedButton.
83 : ///
84 : /// The [autofocus] and [clipBehavior] arguments must not be null.
85 0 : const MongolOutlinedButton({
86 : Key? key,
87 : required VoidCallback? onPressed,
88 : VoidCallback? onLongPress,
89 : ButtonStyle? style,
90 : FocusNode? focusNode,
91 : bool autofocus = false,
92 : Clip clipBehavior = Clip.none,
93 : required Widget child,
94 0 : }) : super(
95 : key: key,
96 : onPressed: onPressed,
97 : onLongPress: onLongPress,
98 : style: style,
99 : focusNode: focusNode,
100 : autofocus: autofocus,
101 : clipBehavior: clipBehavior,
102 : child: child,
103 : );
104 :
105 : /// Create a text button from a pair of widgets that serve as the button's
106 : /// [icon] and [label].
107 : ///
108 : /// The icon and label are arranged in a column and padded by 12 logical pixels
109 : /// at the start, and 16 at the end, with an 8 pixel gap in between.
110 : ///
111 : /// The [icon] and [label] arguments must not be null.
112 : factory MongolOutlinedButton.icon({
113 : Key? key,
114 : required VoidCallback? onPressed,
115 : VoidCallback? onLongPress,
116 : ButtonStyle? style,
117 : FocusNode? focusNode,
118 : bool? autofocus,
119 : Clip? clipBehavior,
120 : required Widget icon,
121 : required Widget label,
122 : }) = _MongolOutlinedButtonWithIcon;
123 :
124 : /// A static convenience method that constructs an outlined button
125 : /// [ButtonStyle] given simple values.
126 : ///
127 : /// The [primary], and [onSurface] colors are used to create a
128 : /// [MaterialStateProperty] [ButtonStyle.foregroundColor] value in the same
129 : /// way that [defaultStyleOf] uses the [ColorScheme] colors with the same
130 : /// names. Specify a value for [primary] to specify the color of the button's
131 : /// text and icons as well as the overlay colors used to indicate the hover,
132 : /// focus, and pressed states. Use [onSurface] to specify the button's
133 : /// disabled text and icon color.
134 : ///
135 : /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
136 : /// parameters are used to construct [ButtonStyle.mouseCursor].
137 : ///
138 : /// All of the other parameters are either used directly or used to
139 : /// create a [MaterialStateProperty] with a single value for all
140 : /// states.
141 : ///
142 : /// All parameters default to null, by default this method returns
143 : /// a [ButtonStyle] that doesn't override anything.
144 : ///
145 : /// For example, to override the default shape and outline for an
146 : /// [MongolOutlinedButton], one could write:
147 : ///
148 : /// ```dart
149 : /// MongolOutlinedButton(
150 : /// style: OutlinedButton.styleFrom(
151 : /// shape: StadiumBorder(),
152 : /// side: BorderSide(width: 2, color: Colors.green),
153 : /// ),
154 : /// )
155 : /// ```
156 0 : static ButtonStyle styleFrom({
157 : Color? primary,
158 : Color? onSurface,
159 : Color? backgroundColor,
160 : Color? shadowColor,
161 : double? elevation,
162 : TextStyle? textStyle,
163 : EdgeInsetsGeometry? padding,
164 : Size? minimumSize,
165 : Size? fixedSize,
166 : Size? maximumSize,
167 : BorderSide? side,
168 : OutlinedBorder? shape,
169 : MouseCursor? enabledMouseCursor,
170 : MouseCursor? disabledMouseCursor,
171 : VisualDensity? visualDensity,
172 : MaterialTapTargetSize? tapTargetSize,
173 : Duration? animationDuration,
174 : bool? enableFeedback,
175 : AlignmentGeometry? alignment,
176 : InteractiveInkFeatureFactory? splashFactory,
177 : }) {
178 : final MaterialStateProperty<Color?>? foregroundColor =
179 : (onSurface == null && primary == null)
180 : ? null
181 0 : : _OutlinedButtonDefaultForeground(primary, onSurface);
182 : final MaterialStateProperty<Color?>? overlayColor =
183 0 : (primary == null) ? null : _OutlinedButtonDefaultOverlay(primary);
184 : final MaterialStateProperty<MouseCursor>? mouseCursor =
185 : (enabledMouseCursor == null && disabledMouseCursor == null)
186 : ? null
187 0 : : _OutlinedButtonDefaultMouseCursor(
188 : enabledMouseCursor!, disabledMouseCursor!);
189 :
190 0 : return ButtonStyle(
191 0 : textStyle: ButtonStyleButton.allOrNull<TextStyle>(textStyle),
192 : foregroundColor: foregroundColor,
193 0 : backgroundColor: ButtonStyleButton.allOrNull<Color>(backgroundColor),
194 : overlayColor: overlayColor,
195 0 : shadowColor: ButtonStyleButton.allOrNull<Color>(shadowColor),
196 0 : elevation: ButtonStyleButton.allOrNull<double>(elevation),
197 0 : padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding),
198 0 : minimumSize: ButtonStyleButton.allOrNull<Size>(minimumSize),
199 0 : fixedSize: ButtonStyleButton.allOrNull<Size>(fixedSize),
200 : // TODO: add when becomes available in stable channel
201 : //maximumSize: ButtonStyleButton.allOrNull<Size>(maximumSize),
202 0 : side: ButtonStyleButton.allOrNull<BorderSide>(side),
203 0 : shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
204 : mouseCursor: mouseCursor,
205 : visualDensity: visualDensity,
206 : tapTargetSize: tapTargetSize,
207 : animationDuration: animationDuration,
208 : enableFeedback: enableFeedback,
209 : alignment: alignment,
210 : splashFactory: splashFactory,
211 : );
212 : }
213 :
214 : /// Defines the button's default appearance.
215 : ///
216 : /// With the exception of [ButtonStyle.side], which defines the
217 : /// outline, and [ButtonStyle.padding], the returned style is the
218 : /// same as for [MongolTextButton].
219 : ///
220 : /// The button [child]'s [MongolText] and [Icon] widgets are rendered with
221 : /// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
222 : /// the style's overlay color when the button is focused, hovered
223 : /// or pressed. The button's background color becomes its [Material]
224 : /// color and is transparent by default.
225 : ///
226 : /// All of the ButtonStyle's defaults appear below. In this list
227 : /// "Theme.foo" is shorthand for `Theme.of(context).foo`. Color
228 : /// scheme values like "onSurface(0.38)" are shorthand for
229 : /// `onSurface.withOpacity(0.38)`. [MaterialStateProperty] valued
230 : /// properties that are not followed by a sublist have the same
231 : /// value for all states, otherwise the values are as specified for
232 : /// each state and "others" means all other states.
233 : ///
234 : /// The color of the [ButtonStyle.textStyle] is not used, the
235 : /// [ButtonStyle.foregroundColor] is used instead.
236 : ///
237 : /// * `textStyle` - Theme.textTheme.button
238 : /// * `backgroundColor` - transparent
239 : /// * `foregroundColor`
240 : /// * disabled - Theme.colorScheme.onSurface(0.38)
241 : /// * others - Theme.colorScheme.primary
242 : /// * `overlayColor`
243 : /// * hovered - Theme.colorScheme.primary(0.04)
244 : /// * focused or pressed - Theme.colorScheme.primary(0.12)
245 : /// * `shadowColor` - Theme.shadowColor
246 : /// * `elevation` - 0
247 : /// * `padding`
248 : /// * `textScaleFactor <= 1` - vertical(16)
249 : /// * `1 < textScaleFactor <= 2` - lerp(vertical(16), vertical(8))
250 : /// * `2 < textScaleFactor <= 3` - lerp(vertical(8), vertical(4))
251 : /// * `3 < textScaleFactor` - vertical(4)
252 : /// * `minimumSize` - Size(36, 64)
253 : /// * `fixedSize` - null
254 : /// * `maximumSize` - Size.infinite
255 : /// * `side` - BorderSide(width: 1, color: Theme.colorScheme.onSurface(0.12))
256 : /// * `shape` - RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))
257 : /// * `mouseCursor`
258 : /// * disabled - SystemMouseCursors.forbidden
259 : /// * others - SystemMouseCursors.click
260 : /// * `visualDensity` - theme.visualDensity
261 : /// * `tapTargetSize` - theme.materialTapTargetSize
262 : /// * `animationDuration` - kThemeChangeDuration
263 : /// * `enableFeedback` - true
264 : /// * `alignment` - Alignment.center
265 : /// * `splashFactory` - InkRipple.splashFactory
266 0 : @override
267 : ButtonStyle defaultStyleOf(BuildContext context) {
268 0 : final ThemeData theme = Theme.of(context);
269 0 : final ColorScheme colorScheme = theme.colorScheme;
270 :
271 0 : final EdgeInsetsGeometry scaledPadding = ButtonStyleButton.scaledPadding(
272 : const EdgeInsets.symmetric(vertical: 16),
273 : const EdgeInsets.symmetric(vertical: 8),
274 : const EdgeInsets.symmetric(vertical: 4),
275 0 : MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
276 : );
277 :
278 0 : return styleFrom(
279 0 : primary: colorScheme.primary,
280 0 : onSurface: colorScheme.onSurface,
281 : backgroundColor: Colors.transparent,
282 0 : shadowColor: theme.shadowColor,
283 : elevation: 0,
284 0 : textStyle: theme.textTheme.button,
285 : padding: scaledPadding,
286 : minimumSize: const Size(36, 64),
287 : maximumSize: Size.infinite,
288 0 : side: BorderSide(
289 0 : color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
290 : width: 1,
291 : ),
292 : shape: const RoundedRectangleBorder(
293 : borderRadius: BorderRadius.all(Radius.circular(4))),
294 : enabledMouseCursor: SystemMouseCursors.click,
295 : disabledMouseCursor: SystemMouseCursors.forbidden,
296 0 : visualDensity: theme.visualDensity,
297 0 : tapTargetSize: theme.materialTapTargetSize,
298 : animationDuration: kThemeChangeDuration,
299 : enableFeedback: true,
300 : alignment: Alignment.center,
301 : splashFactory: InkRipple.splashFactory,
302 : );
303 : }
304 :
305 0 : @override
306 : ButtonStyle? themeStyleOf(BuildContext context) {
307 0 : return OutlinedButtonTheme.of(context).style;
308 : }
309 : }
310 :
311 : @immutable
312 : class _OutlinedButtonDefaultForeground extends MaterialStateProperty<Color?>
313 : with Diagnosticable {
314 0 : _OutlinedButtonDefaultForeground(this.primary, this.onSurface);
315 :
316 : final Color? primary;
317 : final Color? onSurface;
318 :
319 0 : @override
320 : Color? resolve(Set<MaterialState> states) {
321 0 : if (states.contains(MaterialState.disabled)) {
322 0 : return onSurface?.withOpacity(0.38);
323 : }
324 0 : return primary;
325 : }
326 : }
327 :
328 : @immutable
329 : class _OutlinedButtonDefaultOverlay extends MaterialStateProperty<Color?>
330 : with Diagnosticable {
331 0 : _OutlinedButtonDefaultOverlay(this.primary);
332 :
333 : final Color primary;
334 :
335 0 : @override
336 : Color? resolve(Set<MaterialState> states) {
337 0 : if (states.contains(MaterialState.hovered)) {
338 0 : return primary.withOpacity(0.04);
339 : }
340 0 : if (states.contains(MaterialState.focused) ||
341 0 : states.contains(MaterialState.pressed)) {
342 0 : return primary.withOpacity(0.12);
343 : }
344 : return null;
345 : }
346 : }
347 :
348 : @immutable
349 : class _OutlinedButtonDefaultMouseCursor
350 : extends MaterialStateProperty<MouseCursor> with Diagnosticable {
351 0 : _OutlinedButtonDefaultMouseCursor(this.enabledCursor, this.disabledCursor);
352 :
353 : final MouseCursor enabledCursor;
354 : final MouseCursor disabledCursor;
355 :
356 0 : @override
357 : MouseCursor resolve(Set<MaterialState> states) {
358 0 : if (states.contains(MaterialState.disabled)) return disabledCursor;
359 0 : return enabledCursor;
360 : }
361 : }
362 :
363 : class _MongolOutlinedButtonWithIcon extends MongolOutlinedButton {
364 0 : _MongolOutlinedButtonWithIcon({
365 : Key? key,
366 : required VoidCallback? onPressed,
367 : VoidCallback? onLongPress,
368 : ButtonStyle? style,
369 : FocusNode? focusNode,
370 : bool? autofocus,
371 : Clip? clipBehavior,
372 : required Widget icon,
373 : required Widget label,
374 0 : }) : super(
375 : key: key,
376 : onPressed: onPressed,
377 : onLongPress: onLongPress,
378 : style: style,
379 : focusNode: focusNode,
380 : autofocus: autofocus ?? false,
381 : clipBehavior: clipBehavior ?? Clip.none,
382 0 : child: _MongolOutlinedButtonWithIconChild(icon: icon, label: label),
383 : );
384 : }
385 :
386 : class _MongolOutlinedButtonWithIconChild extends StatelessWidget {
387 0 : const _MongolOutlinedButtonWithIconChild({
388 : Key? key,
389 : required this.label,
390 : required this.icon,
391 0 : }) : super(key: key);
392 :
393 : final Widget label;
394 : final Widget icon;
395 :
396 0 : @override
397 : Widget build(BuildContext context) {
398 0 : final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
399 : final double gap =
400 0 : scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
401 0 : return Column(
402 : mainAxisSize: MainAxisSize.min,
403 0 : children: <Widget>[icon, SizedBox(height: gap), Flexible(child: label)],
404 : );
405 : }
406 : }
|