Skip to content

Commit 015ee96

Browse files
authored
Push button (#40)
* Stub out buttons with corresponding Styles * Stub out Fields and Labels with corresponding Styles * Stub out Selectors * Placeholder Indicators * Remove stubs for RoundButton and BevelButton Apple docs say not to use these * Start working on PushButton * Get PushButton color from style's primaryColor instead of PushButton's disabledColor Also: * Add button_states.dart * Add PushButtonStyle stub to Style * Add Diagnosticable mixin to PushButtonStyle * Tweak PushButton's disabledColor * Start work on PushButtonStyle * Add color, disabledColor, padding, and borderRadius properties to class with contructor and corresponding debug properties * Include PushButtonStyle when returning default style in theme.dart * Get PushButton color from current style if possible, otherwise fall back to defaults * Add todos for implementing other PushButtonStyle props in PushButton * Use proper padding and border radius for small and large PushButtons * Test PushButton in example app * Export push_button_theme.dart, move todo * Documentation updates * Documentation for PushButtonTheme and PushButtonThemeData * Tweak MacosThemeData documentation * Revert stubs * Update version and CHANGELOG.md * Ensure text color is correct when PushButton is disabled * Add PushButton section to README.md * Try embedding PushButton images as an album * Try embedding PushButton images as an album (2) * Revert to individual images
1 parent edefee2 commit 015ee96

File tree

11 files changed

+410
-24
lines changed

11 files changed

+410
-24
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## [0.0.5]
2+
* Adds the `PushButton` widget along with `PushButtonTheme` and `PushButtonThemeData`
3+
* Removes the `height` property from `Typography`'s `TextStyle`s
4+
* Updates `Typography.headline`'s weight and letter spacing
5+
16
## [0.0.4]
27
* Major theme refactor that more closely resembles flutter/material and flutter/cupertino
38
* The `Style` class is now `MacosThemeData`

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Implements Apple's macOS Design System in Flutter. Based on the official documen
1010
- [Layout](#layout)
1111
- [Scaffold](#scaffold)
1212
- [Buttons](#buttons)
13+
- [PushButton](#pushbutton)
1314
- [Switch](#switch)
1415
- [Indicators](#indicators)
1516
- [ProgressCircle](#progresscircle)
@@ -37,13 +38,27 @@ dragging left or right. See the documentation for all customization options.
3738
<img src="https://imgur.com/jTPXGuq.gif" width="75%"/>
3839

3940
# Buttons
41+
42+
## PushButton
43+
44+
<img src="https://imgur.com/D7CSk09.jpg"/>
45+
<img src="https://imgur.com/JDPdmWo.jpg"/>
46+
<img src="https://imgur.com/E5gECIP.jpg"/>
47+
<img src="https://imgur.com/lpT18wZ.jpg"/>
48+
<img src="https://imgur.com/W8ZA9nv.jpg"/>
49+
<img src="https://imgur.com/uKRyLph.jpg"/>
50+
<img src="https://imgur.com/w4hvYZQ.jpg"/>
51+
<img src="https://imgur.com/ztwz6ut.jpg"/>
52+
4053
## Switch
4154
<img src="https://imgur.com/IBh5jkz.jpg" width="50%" height="50%"/>
4255

4356
<img src="https://imgur.com/qK1VCVr.jpg" width="50%" height="50%"/>
4457

4558
# Indicators
59+
4660
## ProgressCircle
61+
4762
A `ProgressCircle` can be either determinate or indeterminate. If indeterminate, Flutter's
4863
`CupertinoActivityIndicator` will be shown.
4964

example/lib/main.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:macos_ui/macos_ui.dart';
22
import 'package:provider/provider.dart';
3+
34
import 'theme.dart';
45

56
void main() {
@@ -18,7 +19,7 @@ class MyApp extends StatelessWidget {
1819
theme: MacosThemeData.light(),
1920
darkTheme: MacosThemeData.dark(),
2021
themeMode: ThemeMode.dark,
21-
debugShowCheckedModeBanner: false, //yay!
22+
debugShowCheckedModeBanner: false,
2223
home: Demo(),
2324
);
2425
},
@@ -43,9 +44,10 @@ class _DemoState extends State<Demo> {
4344
body: Column(
4445
mainAxisAlignment: MainAxisAlignment.center,
4546
children: [
46-
Switch(
47-
value: value,
48-
onChanged: (v) => setState(() => value = v),
47+
PushButton(
48+
buttonSize: ButtonSize.small,
49+
child: Text('Button'),
50+
onPressed: () {},
4951
),
5052
],
5153
),

example/pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ packages:
7373
path: ".."
7474
relative: true
7575
source: path
76-
version: "0.0.4"
76+
version: "0.0.5"
7777
matcher:
7878
dependency: transitive
7979
description:

lib/macos_ui.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
library macos_ui;
22

3+
/// todo: package-level docs
34
export 'package:flutter/cupertino.dart'
45
show CupertinoColors, CupertinoDynamicColor;
56
export 'package:flutter/material.dart'
@@ -11,15 +12,24 @@ export 'package:flutter/material.dart'
1112
PageTransitionsBuilder,
1213
FlutterLogo,
1314
CircleAvatar;
15+
export 'package:flutter/widgets.dart' hide Icon, TextBox;
1416

15-
/// todo: package-level docs
16-
export 'package:flutter/widgets.dart' hide Icon, IconTheme, TextBox;
17-
17+
export 'src/buttons/push_button.dart';
18+
export 'src/buttons/push_button_theme.dart';
19+
export 'src/buttons/switch.dart';
20+
export 'src/buttons/switch.dart';
21+
export 'src/buttons/switch.dart';
1822
export 'src/buttons/switch.dart';
1923
export 'src/indicators/progress_indicators.dart';
24+
export 'src/indicators/progress_indicators.dart';
25+
export 'src/layout/scaffold.dart';
26+
export 'src/layout/scaffold.dart';
2027
export 'src/layout/scaffold.dart';
2128
export 'src/macos_app.dart';
29+
export 'src/macos_app.dart';
2230
export 'src/styles/macos_theme.dart';
2331
export 'src/styles/macos_theme_data.dart';
2432
export 'src/styles/typography.dart';
33+
export 'src/styles/typography.dart';
34+
export 'src/util.dart';
2535
export 'src/util.dart';

lib/src/buttons/push_button.dart

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:macos_ui/macos_ui.dart';
3+
4+
enum ButtonSize {
5+
large,
6+
small,
7+
}
8+
9+
const EdgeInsetsGeometry _kSmallButtonPadding =
10+
EdgeInsets.symmetric(vertical: 3.0, horizontal: 8.0);
11+
const EdgeInsetsGeometry _kLargeButtonPadding =
12+
EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0);
13+
14+
const BorderRadius _kSmallButtonRadius =
15+
const BorderRadius.all(Radius.circular(5.0));
16+
const BorderRadius _kLargeButtonRadius =
17+
const BorderRadius.all(Radius.circular(7.0));
18+
19+
/// A macOS-style button.
20+
class PushButton extends StatefulWidget {
21+
const PushButton({
22+
Key? key,
23+
required this.child,
24+
required this.buttonSize,
25+
this.padding,
26+
this.color,
27+
this.disabledColor,
28+
this.onPressed,
29+
this.pressedOpacity = 0.4,
30+
this.borderRadius = const BorderRadius.all(Radius.circular(4.0)),
31+
this.alignment = Alignment.center,
32+
}) : assert(pressedOpacity == null ||
33+
(pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
34+
super(key: key);
35+
36+
/// The widget below this widget in the tree.
37+
///
38+
/// Typically a [Text] widget.
39+
final Widget child;
40+
41+
/// The size of the button.
42+
///
43+
/// Must be either [ButtonSize.small] or [ButtonSize.large].
44+
///
45+
/// Small buttons have a `padding` of [_kSmallButtonPadding] and a
46+
/// `borderRadius` of [_kSmallButtonRadius]. Large buttons have a `padding`
47+
/// of [_kLargeButtonPadding] and a `borderRadius` of [_kLargeButtonRadius].
48+
final ButtonSize buttonSize;
49+
50+
/// The amount of space to surround the child inside the bounds of the button.
51+
///
52+
/// Leave blank to use the default padding provided by [_kSmallButtonPadding]
53+
/// or [_kLargeButtonPadding].
54+
final EdgeInsetsGeometry? padding;
55+
56+
/// The color of the button's background.
57+
final Color? color;
58+
59+
/// The color of the button's background when the button is disabled.
60+
///
61+
/// Ignored if the [PushButton] doesn't also have a [color].
62+
///
63+
/// Defaults to [CupertinoColors.quaternarySystemFill] when [color] is
64+
/// specified. Must not be null.
65+
final Color? disabledColor;
66+
67+
/// The callback that is called when the button is tapped or otherwise activated.
68+
///
69+
/// If this is set to null, the button will be disabled.
70+
final VoidCallback? onPressed;
71+
72+
/// The opacity that the button will fade to when it is pressed.
73+
/// The button will have an opacity of 1.0 when it is not pressed.
74+
///
75+
/// This defaults to 0.4. If null, opacity will not change on pressed if using
76+
/// your own custom effects is desired.
77+
final double? pressedOpacity;
78+
79+
/// The radius of the button's corners when it has a background color.
80+
///
81+
/// Leave blank to use the default radius provided by [_kSmallButtonRadius]
82+
/// or [_kLargeButtonRadius].
83+
final BorderRadius? borderRadius;
84+
85+
/// The alignment of the button's [child].
86+
///
87+
/// Typically buttons are sized to be just big enough to contain the child and its
88+
/// [padding]. If the button's size is constrained to a fixed size, for example by
89+
/// enclosing it with a [SizedBox], this property defines how the child is aligned
90+
/// within the available space.
91+
///
92+
/// Always defaults to [Alignment.center].
93+
final AlignmentGeometry alignment;
94+
95+
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
96+
/// enable a button, set its [onPressed] property to a non-null value.
97+
bool get enabled => onPressed != null;
98+
99+
@override
100+
_PushButtonState createState() => _PushButtonState();
101+
}
102+
103+
class _PushButtonState extends State<PushButton>
104+
with SingleTickerProviderStateMixin {
105+
// Eyeballed values. Feel free to tweak.
106+
static const Duration kFadeOutDuration = Duration(milliseconds: 10);
107+
static const Duration kFadeInDuration = Duration(milliseconds: 100);
108+
final Tween<double> _opacityTween = Tween<double>(begin: 1.0);
109+
110+
late AnimationController _animationController;
111+
late Animation<double> _opacityAnimation;
112+
113+
@override
114+
void initState() {
115+
super.initState();
116+
_animationController = AnimationController(
117+
duration: const Duration(milliseconds: 200),
118+
value: 0.0,
119+
vsync: this,
120+
);
121+
_opacityAnimation = _animationController
122+
.drive(CurveTween(curve: Curves.decelerate))
123+
.drive(_opacityTween);
124+
_setTween();
125+
}
126+
127+
@override
128+
void didUpdateWidget(PushButton old) {
129+
super.didUpdateWidget(old);
130+
_setTween();
131+
}
132+
133+
void _setTween() {
134+
_opacityTween.end = widget.pressedOpacity ?? 1.0;
135+
}
136+
137+
@override
138+
void dispose() {
139+
_animationController.dispose();
140+
super.dispose();
141+
}
142+
143+
bool _buttonHeldDown = false;
144+
145+
void _handleTapDown(TapDownDetails event) {
146+
if (!_buttonHeldDown) {
147+
_buttonHeldDown = true;
148+
_animate();
149+
}
150+
}
151+
152+
void _handleTapUp(TapUpDetails event) {
153+
if (_buttonHeldDown) {
154+
_buttonHeldDown = false;
155+
_animate();
156+
}
157+
}
158+
159+
void _handleTapCancel() {
160+
if (_buttonHeldDown) {
161+
_buttonHeldDown = false;
162+
_animate();
163+
}
164+
}
165+
166+
void _animate() {
167+
if (_animationController.isAnimating) return;
168+
final bool wasHeldDown = _buttonHeldDown;
169+
final TickerFuture ticker = _buttonHeldDown
170+
? _animationController.animateTo(1.0, duration: kFadeOutDuration)
171+
: _animationController.animateTo(0.0, duration: kFadeInDuration);
172+
ticker.then<void>((void value) {
173+
if (mounted && wasHeldDown != _buttonHeldDown) _animate();
174+
});
175+
}
176+
177+
@override
178+
Widget build(BuildContext context) {
179+
final bool enabled = widget.enabled;
180+
final MacosThemeData theme = MacosTheme.of(context);
181+
final Color? backgroundColor = widget.color == null
182+
? theme.pushButtonTheme.color
183+
: CupertinoDynamicColor.maybeResolve(widget.color, context);
184+
185+
final Color? disabledColor = widget.disabledColor == null
186+
? theme.pushButtonTheme.disabledColor
187+
: CupertinoDynamicColor.maybeResolve(widget.disabledColor, context);
188+
189+
final EdgeInsetsGeometry? buttonPadding = widget.padding == null
190+
? widget.buttonSize == ButtonSize.small
191+
? _kSmallButtonPadding
192+
: _kLargeButtonPadding
193+
: widget.padding;
194+
195+
final BorderRadius? borderRadius = widget.borderRadius == null
196+
? widget.buttonSize == ButtonSize.small
197+
? _kSmallButtonRadius
198+
: _kLargeButtonRadius
199+
: widget.borderRadius;
200+
201+
final Color? foregroundColor = widget.enabled
202+
? textLuminance(backgroundColor!)
203+
: theme.brightness!.isDark
204+
? Color.fromRGBO(255, 255, 255, 0.25)
205+
: Color.fromRGBO(0, 0, 0, 0.25);
206+
207+
final TextStyle textStyle =
208+
theme.typography!.headline!.copyWith(color: foregroundColor);
209+
210+
return GestureDetector(
211+
behavior: HitTestBehavior.opaque,
212+
onTapDown: enabled ? _handleTapDown : null,
213+
onTapUp: enabled ? _handleTapUp : null,
214+
onTapCancel: enabled ? _handleTapCancel : null,
215+
onTap: widget.onPressed,
216+
child: Semantics(
217+
button: true,
218+
child: ConstrainedBox(
219+
constraints: BoxConstraints(
220+
minWidth: 49,
221+
minHeight: 20,
222+
),
223+
child: FadeTransition(
224+
opacity: _opacityAnimation,
225+
child: DecoratedBox(
226+
decoration: BoxDecoration(
227+
borderRadius: borderRadius,
228+
color: !enabled
229+
? CupertinoDynamicColor.resolve(disabledColor!, context)
230+
: backgroundColor,
231+
),
232+
child: Padding(
233+
padding: buttonPadding!,
234+
child: Align(
235+
alignment: widget.alignment,
236+
widthFactor: 1.0,
237+
heightFactor: 1.0,
238+
//todo: show proper text color in light theme
239+
child: DefaultTextStyle(
240+
style: textStyle,
241+
child: widget.child,
242+
),
243+
),
244+
),
245+
),
246+
),
247+
),
248+
),
249+
);
250+
}
251+
}

0 commit comments

Comments
 (0)