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: