diff --git a/CHANGELOG.md b/CHANGELOG.md
index c43f3627..3b37dea0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## [0.0.5]
+* Adds the `PushButton` widget along with `PushButtonTheme` and `PushButtonThemeData`
+* Removes the `height` property from `Typography`'s `TextStyle`s
+* Updates `Typography.headline`'s weight and letter spacing
+
## [0.0.4]
* Major theme refactor that more closely resembles flutter/material and flutter/cupertino
* The `Style` class is now `MacosThemeData`
diff --git a/README.md b/README.md
index 209ace94..5a6ae236 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@ Implements Apple's macOS Design System in Flutter. Based on the official documen
- [Layout](#layout)
- [Scaffold](#scaffold)
- [Buttons](#buttons)
+ - [PushButton](#pushbutton)
- [Switch](#switch)
- [Indicators](#indicators)
- [ProgressCircle](#progresscircle)
@@ -37,13 +38,27 @@ dragging left or right. See the documentation for all customization options.
# Buttons
+
+## PushButton
+
+
+
+
+
+
+
+
+
+
## Switch
# Indicators
+
## ProgressCircle
+
A `ProgressCircle` can be either determinate or indeterminate. If indeterminate, Flutter's
`CupertinoActivityIndicator` will be shown.
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 3ffb8815..6bd962f6 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,5 +1,6 @@
import 'package:macos_ui/macos_ui.dart';
import 'package:provider/provider.dart';
+
import 'theme.dart';
void main() {
@@ -18,7 +19,7 @@ class MyApp extends StatelessWidget {
theme: MacosThemeData.light(),
darkTheme: MacosThemeData.dark(),
themeMode: ThemeMode.dark,
- debugShowCheckedModeBanner: false, //yay!
+ debugShowCheckedModeBanner: false,
home: Demo(),
);
},
@@ -43,9 +44,10 @@ class _DemoState extends State {
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Switch(
- value: value,
- onChanged: (v) => setState(() => value = v),
+ PushButton(
+ buttonSize: ButtonSize.small,
+ child: Text('Button'),
+ onPressed: () {},
),
],
),
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 65b0b769..6e3bc834 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -73,7 +73,7 @@ packages:
path: ".."
relative: true
source: path
- version: "0.0.4"
+ version: "0.0.5"
matcher:
dependency: transitive
description:
diff --git a/lib/macos_ui.dart b/lib/macos_ui.dart
index 877dbed1..32d9a821 100644
--- a/lib/macos_ui.dart
+++ b/lib/macos_ui.dart
@@ -1,5 +1,6 @@
library macos_ui;
+/// todo: package-level docs
export 'package:flutter/cupertino.dart'
show CupertinoColors, CupertinoDynamicColor;
export 'package:flutter/material.dart'
@@ -11,15 +12,24 @@ export 'package:flutter/material.dart'
PageTransitionsBuilder,
FlutterLogo,
CircleAvatar;
+export 'package:flutter/widgets.dart' hide Icon, TextBox;
-/// todo: package-level docs
-export 'package:flutter/widgets.dart' hide Icon, IconTheme, TextBox;
-
+export 'src/buttons/push_button.dart';
+export 'src/buttons/push_button_theme.dart';
+export 'src/buttons/switch.dart';
+export 'src/buttons/switch.dart';
+export 'src/buttons/switch.dart';
export 'src/buttons/switch.dart';
export 'src/indicators/progress_indicators.dart';
+export 'src/indicators/progress_indicators.dart';
+export 'src/layout/scaffold.dart';
+export 'src/layout/scaffold.dart';
export 'src/layout/scaffold.dart';
export 'src/macos_app.dart';
+export 'src/macos_app.dart';
export 'src/styles/macos_theme.dart';
export 'src/styles/macos_theme_data.dart';
export 'src/styles/typography.dart';
+export 'src/styles/typography.dart';
+export 'src/util.dart';
export 'src/util.dart';
diff --git a/lib/src/buttons/push_button.dart b/lib/src/buttons/push_button.dart
new file mode 100644
index 00000000..32ccf98f
--- /dev/null
+++ b/lib/src/buttons/push_button.dart
@@ -0,0 +1,251 @@
+import 'package:flutter/foundation.dart';
+import 'package:macos_ui/macos_ui.dart';
+
+enum ButtonSize {
+ large,
+ small,
+}
+
+const EdgeInsetsGeometry _kSmallButtonPadding =
+ EdgeInsets.symmetric(vertical: 3.0, horizontal: 8.0);
+const EdgeInsetsGeometry _kLargeButtonPadding =
+ EdgeInsets.symmetric(vertical: 6.0, horizontal: 8.0);
+
+const BorderRadius _kSmallButtonRadius =
+ const BorderRadius.all(Radius.circular(5.0));
+const BorderRadius _kLargeButtonRadius =
+ const BorderRadius.all(Radius.circular(7.0));
+
+/// A macOS-style button.
+class PushButton extends StatefulWidget {
+ const PushButton({
+ Key? key,
+ required this.child,
+ required this.buttonSize,
+ this.padding,
+ this.color,
+ this.disabledColor,
+ this.onPressed,
+ this.pressedOpacity = 0.4,
+ this.borderRadius = const BorderRadius.all(Radius.circular(4.0)),
+ this.alignment = Alignment.center,
+ }) : assert(pressedOpacity == null ||
+ (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
+ super(key: key);
+
+ /// The widget below this widget in the tree.
+ ///
+ /// Typically a [Text] widget.
+ final Widget child;
+
+ /// The size of the button.
+ ///
+ /// Must be either [ButtonSize.small] or [ButtonSize.large].
+ ///
+ /// Small buttons have a `padding` of [_kSmallButtonPadding] and a
+ /// `borderRadius` of [_kSmallButtonRadius]. Large buttons have a `padding`
+ /// of [_kLargeButtonPadding] and a `borderRadius` of [_kLargeButtonRadius].
+ final ButtonSize buttonSize;
+
+ /// The amount of space to surround the child inside the bounds of the button.
+ ///
+ /// Leave blank to use the default padding provided by [_kSmallButtonPadding]
+ /// or [_kLargeButtonPadding].
+ final EdgeInsetsGeometry? padding;
+
+ /// The color of the button's background.
+ final Color? color;
+
+ /// The color of the button's background when the button is disabled.
+ ///
+ /// Ignored if the [PushButton] doesn't also have a [color].
+ ///
+ /// Defaults to [CupertinoColors.quaternarySystemFill] when [color] is
+ /// specified. Must not be null.
+ final Color? disabledColor;
+
+ /// The callback that is called when the button is tapped or otherwise activated.
+ ///
+ /// If this is set to null, the button will be disabled.
+ final VoidCallback? onPressed;
+
+ /// The opacity that the button will fade to when it is pressed.
+ /// The button will have an opacity of 1.0 when it is not pressed.
+ ///
+ /// This defaults to 0.4. If null, opacity will not change on pressed if using
+ /// your own custom effects is desired.
+ final double? pressedOpacity;
+
+ /// The radius of the button's corners when it has a background color.
+ ///
+ /// Leave blank to use the default radius provided by [_kSmallButtonRadius]
+ /// or [_kLargeButtonRadius].
+ final BorderRadius? borderRadius;
+
+ /// The alignment of the button's [child].
+ ///
+ /// Typically buttons are sized to be just big enough to contain the child and its
+ /// [padding]. If the button's size is constrained to a fixed size, for example by
+ /// enclosing it with a [SizedBox], this property defines how the child is aligned
+ /// within the available space.
+ ///
+ /// Always defaults to [Alignment.center].
+ final AlignmentGeometry alignment;
+
+ /// Whether the button is enabled or disabled. Buttons are disabled by default. To
+ /// enable a button, set its [onPressed] property to a non-null value.
+ bool get enabled => onPressed != null;
+
+ @override
+ _PushButtonState createState() => _PushButtonState();
+}
+
+class _PushButtonState extends State
+ with SingleTickerProviderStateMixin {
+ // Eyeballed values. Feel free to tweak.
+ static const Duration kFadeOutDuration = Duration(milliseconds: 10);
+ static const Duration kFadeInDuration = Duration(milliseconds: 100);
+ final Tween _opacityTween = Tween(begin: 1.0);
+
+ late AnimationController _animationController;
+ late Animation _opacityAnimation;
+
+ @override
+ void initState() {
+ super.initState();
+ _animationController = AnimationController(
+ duration: const Duration(milliseconds: 200),
+ value: 0.0,
+ vsync: this,
+ );
+ _opacityAnimation = _animationController
+ .drive(CurveTween(curve: Curves.decelerate))
+ .drive(_opacityTween);
+ _setTween();
+ }
+
+ @override
+ void didUpdateWidget(PushButton old) {
+ super.didUpdateWidget(old);
+ _setTween();
+ }
+
+ void _setTween() {
+ _opacityTween.end = widget.pressedOpacity ?? 1.0;
+ }
+
+ @override
+ void dispose() {
+ _animationController.dispose();
+ super.dispose();
+ }
+
+ bool _buttonHeldDown = false;
+
+ void _handleTapDown(TapDownDetails event) {
+ if (!_buttonHeldDown) {
+ _buttonHeldDown = true;
+ _animate();
+ }
+ }
+
+ void _handleTapUp(TapUpDetails event) {
+ if (_buttonHeldDown) {
+ _buttonHeldDown = false;
+ _animate();
+ }
+ }
+
+ void _handleTapCancel() {
+ if (_buttonHeldDown) {
+ _buttonHeldDown = false;
+ _animate();
+ }
+ }
+
+ void _animate() {
+ if (_animationController.isAnimating) return;
+ final bool wasHeldDown = _buttonHeldDown;
+ final TickerFuture ticker = _buttonHeldDown
+ ? _animationController.animateTo(1.0, duration: kFadeOutDuration)
+ : _animationController.animateTo(0.0, duration: kFadeInDuration);
+ ticker.then((void value) {
+ if (mounted && wasHeldDown != _buttonHeldDown) _animate();
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final bool enabled = widget.enabled;
+ final MacosThemeData theme = MacosTheme.of(context);
+ final Color? backgroundColor = widget.color == null
+ ? theme.pushButtonTheme.color
+ : CupertinoDynamicColor.maybeResolve(widget.color, context);
+
+ final Color? disabledColor = widget.disabledColor == null
+ ? theme.pushButtonTheme.disabledColor
+ : CupertinoDynamicColor.maybeResolve(widget.disabledColor, context);
+
+ final EdgeInsetsGeometry? buttonPadding = widget.padding == null
+ ? widget.buttonSize == ButtonSize.small
+ ? _kSmallButtonPadding
+ : _kLargeButtonPadding
+ : widget.padding;
+
+ final BorderRadius? borderRadius = widget.borderRadius == null
+ ? widget.buttonSize == ButtonSize.small
+ ? _kSmallButtonRadius
+ : _kLargeButtonRadius
+ : widget.borderRadius;
+
+ final Color? foregroundColor = widget.enabled
+ ? textLuminance(backgroundColor!)
+ : theme.brightness!.isDark
+ ? Color.fromRGBO(255, 255, 255, 0.25)
+ : Color.fromRGBO(0, 0, 0, 0.25);
+
+ final TextStyle textStyle =
+ theme.typography!.headline!.copyWith(color: foregroundColor);
+
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTapDown: enabled ? _handleTapDown : null,
+ onTapUp: enabled ? _handleTapUp : null,
+ onTapCancel: enabled ? _handleTapCancel : null,
+ onTap: widget.onPressed,
+ child: Semantics(
+ button: true,
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minWidth: 49,
+ minHeight: 20,
+ ),
+ child: FadeTransition(
+ opacity: _opacityAnimation,
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ borderRadius: borderRadius,
+ color: !enabled
+ ? CupertinoDynamicColor.resolve(disabledColor!, context)
+ : backgroundColor,
+ ),
+ child: Padding(
+ padding: buttonPadding!,
+ child: Align(
+ alignment: widget.alignment,
+ widthFactor: 1.0,
+ heightFactor: 1.0,
+ //todo: show proper text color in light theme
+ child: DefaultTextStyle(
+ style: textStyle,
+ child: widget.child,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/buttons/push_button_theme.dart b/lib/src/buttons/push_button_theme.dart
new file mode 100644
index 00000000..d18affba
--- /dev/null
+++ b/lib/src/buttons/push_button_theme.dart
@@ -0,0 +1,87 @@
+import 'package:flutter/foundation.dart';
+import 'package:macos_ui/macos_ui.dart';
+
+/// Overrides the default style of its [PushButton] descendants.
+///
+/// See also:
+///
+/// * [PushButtonThemeData], which is used to configure this theme.
+class PushButtonTheme extends InheritedTheme {
+ /// Create a [PushButtonTheme].
+ ///
+ /// The [data] parameter must not be null.
+ const PushButtonTheme({
+ Key? key,
+ required this.data,
+ required Widget child,
+ }) : super(key: key, child: child);
+
+ /// The configuration of this theme.
+ final PushButtonThemeData data;
+
+ /// The closest instance of this class that encloses the given context.
+ ///
+ /// If there is no enclosing [PushButtonTheme] widget, then
+ /// [MacosThemeData.pushButtonTheme] is used.
+ ///
+ /// Typical usage is as follows:
+ ///
+ /// ```dart
+ /// PushButtonTheme theme = PushButtonTheme.of(context);
+ /// ```
+ static PushButtonThemeData of(BuildContext context) {
+ final PushButtonTheme? buttonTheme =
+ context.dependOnInheritedWidgetOfExactType();
+ return buttonTheme?.data ?? MacosTheme.of(context).pushButtonTheme;
+ }
+
+ @override
+ Widget wrap(BuildContext context, Widget child) {
+ return PushButtonTheme(data: data, child: child);
+ }
+
+ @override
+ bool updateShouldNotify(PushButtonTheme oldWidget) => data != oldWidget.data;
+}
+
+/// A style that overrides the default appearance of
+/// [PushButton]s when it's used with [PushButtonTheme] or with the
+/// overall [MacosTheme]'s [MacosThemeData.pushButtonTheme].
+///
+/// See also:
+///
+/// * [PushButtonTheme], the theme which is configured with this class.
+/// * [MacosThemeData.pushButtonTheme], which can be used to override the default
+/// style for [PushButton]s below the overall [MacosTheme].
+class PushButtonThemeData with Diagnosticable {
+ /// Creates a [PushButtonThemeData].
+ ///
+ /// The [style] may be null.
+ const PushButtonThemeData({
+ required this.color,
+ required this.disabledColor,
+ });
+
+ /// The default background color for [PushButton]
+ final Color color;
+
+ /// The default disabled color for [PushButton]
+ final Color disabledColor;
+
+ PushButtonThemeData copyWith(PushButtonThemeData? themeData) {
+ if (themeData == null) {
+ return this;
+ }
+ return PushButtonThemeData(
+ color: themeData.color,
+ disabledColor: themeData.disabledColor,
+ );
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ properties.add(DiagnosticsProperty('color', color));
+ properties.add(DiagnosticsProperty('disabledColor', disabledColor));
+ }
+}
diff --git a/lib/src/styles/macos_theme_data.dart b/lib/src/styles/macos_theme_data.dart
index e1b475d3..2f5e46aa 100644
--- a/lib/src/styles/macos_theme_data.dart
+++ b/lib/src/styles/macos_theme_data.dart
@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
+import 'package:macos_ui/src/buttons/push_button_theme.dart';
import '../../macos_ui.dart';
@@ -21,6 +22,7 @@ class MacosThemeData with Diagnosticable {
Curve? animationCurve,
Duration? mediumAnimationDuration,
Typography? typography,
+ PushButtonThemeData? pushButtonTheme,
}) {
final Brightness _brightness = brightness ?? Brightness.light;
final bool isDark = _brightness == Brightness.dark;
@@ -34,6 +36,12 @@ class MacosThemeData with Diagnosticable {
mediumAnimationDuration = Duration(milliseconds: 300);
typography = Typography.defaultTypography(brightness: _brightness)
.copyWith(typography);
+ pushButtonTheme ??= PushButtonThemeData(
+ color: primaryColor,
+ disabledColor: isDark
+ ? Color.fromRGBO(255, 255, 255, 0.1)
+ : Color.fromRGBO(244, 245, 245, 1.0),
+ );
return MacosThemeData._raw(
brightness: _brightness,
@@ -42,6 +50,7 @@ class MacosThemeData with Diagnosticable {
animationCurve: animationCurve,
mediumAnimationDuration: mediumAnimationDuration,
typography: typography,
+ pushButtonTheme: pushButtonTheme,
);
}
@@ -52,6 +61,7 @@ class MacosThemeData with Diagnosticable {
required this.animationCurve,
required this.mediumAnimationDuration,
required this.typography,
+ required this.pushButtonTheme,
});
// todo: documentation
@@ -67,12 +77,14 @@ class MacosThemeData with Diagnosticable {
/// The brightness override for macOS descendants.
final Brightness? brightness;
- /// A color used on interactive elements of the theme.
+ /// A color used on primary interactive elements of the theme.
///
/// Defaults to [CupertinoColors.activeBlue].
final Color? primaryColor;
- // todo: documentation
+ /// A color used on accent interactive elements of the theme.
+ ///
+ /// Defaults to [CupertinoColors.activeBlue].
final Color? accentColor;
// todo: documentation
@@ -81,9 +93,12 @@ class MacosThemeData with Diagnosticable {
// todo: documentation
final Duration? mediumAnimationDuration;
- // todo: documentation
+ /// The default text styling for this theme.
final Typography? typography;
+ /// The default style for [PushButton]s below the overall [MacosTheme].
+ final PushButtonThemeData pushButtonTheme;
+
MacosThemeData resolveFrom(BuildContext context) {
/*Color? convertColor(Color? color) =>
CupertinoDynamicColor.maybeResolve(color, context);*/
@@ -95,6 +110,11 @@ class MacosThemeData with Diagnosticable {
animationCurve: animationCurve,
mediumAnimationDuration: mediumAnimationDuration,
typography: typography,
+ pushButtonTheme: pushButtonTheme,
);
}
}
+
+extension BrightnessX on Brightness {
+ bool get isDark => this == Brightness.dark;
+}
diff --git a/lib/src/styles/typography.dart b/lib/src/styles/typography.dart
index 6ceeb70a..7e22e2e4 100644
--- a/lib/src/styles/typography.dart
+++ b/lib/src/styles/typography.dart
@@ -70,68 +70,58 @@ class Typography with Diagnosticable {
largeTitle: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 26,
- height: 32,
color: color,
),
title1: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 22,
- height: 26,
color: color,
),
title2: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 17,
- height: 22,
color: color,
),
title3: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 15,
- height: 20,
color: color,
),
headline: TextStyle(
fontFamily: 'SanFranciscoPro',
- fontWeight: FontWeight.bold,
+ fontWeight: FontWeight.w400,
fontSize: 13,
- height: 16,
+ letterSpacing: 0.08,
color: color,
),
subheadline: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 11,
- height: 14,
color: color,
),
body: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 13,
- height: 16,
color: color,
),
callout: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 12,
- height: 15,
color: color,
),
footnote: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 10,
- height: 13,
color: color,
),
caption1: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 10,
- height: 13,
color: color,
),
caption2: TextStyle(
fontFamily: 'SanFranciscoPro',
fontSize: 10,
- height: 13,
color: color,
),
);
diff --git a/lib/src/util.dart b/lib/src/util.dart
index b354a588..3068cedf 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -11,3 +11,9 @@ bool debugCheckHasMacosTheme(BuildContext context, [bool check = true]) {
);
return has;
}
+
+Color textLuminance(Color backgroundColor) {
+ return backgroundColor.computeLuminance() > 0.5
+ ? CupertinoColors.black
+ : CupertinoColors.white;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 5dd8b5a0..1f5f98fb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: macos_ui
description: Implements Apple's macOS Design System in Flutter. Based on the official documentation.
-version: 0.0.4
+version: 0.0.5
homepage: 'https://github.com/GroovinChip/macos_ui'
environment: