Skip to content

Commit

Permalink
feat: implement MacosDisclosureButton
Browse files Browse the repository at this point in the history
  • Loading branch information
Umar Salim committed Jan 14, 2023
1 parent d6d4a27 commit 516d57d
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [1.7.7]
* Added implementation of `MacosDisclosureButton`

## [1.7.6]
* Fixed a bug where `MacosPopupButton` would report that a `ScrollController` was not attached to any views

Expand Down
18 changes: 18 additions & 0 deletions example/lib/pages/buttons_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class _ButtonsPageState extends State<ButtonsPage> {
String popupValue = 'One';
String languagePopupValue = 'English';
bool switchValue = false;
bool isDisclosureButtonPressed = false;
final _tabController = MacosTabController(initialIndex: 0, length: 3);

@override
Expand Down Expand Up @@ -91,6 +92,23 @@ class _ButtonsPageState extends State<ButtonsPage> {
],
),
const SizedBox(height: 20),
const Text('MacosDisclosureButton'),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MacosDisclosureButton(
isPressed: isDisclosureButtonPressed,
onPressed: () {
debugPrint('click');
setState(() {
isDisclosureButtonPressed =
!isDisclosureButtonPressed;
});
}),
],
),
const SizedBox(height: 20),
const Text('MacosIconButton'),
const SizedBox(height: 8),
Row(
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.7.6"
version: "1.7.7"
matcher:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions lib/macos_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ library macos_ui;

export 'src/buttons/back_button.dart';
export 'src/buttons/checkbox.dart';
export 'src/buttons/disclosure_button.dart';
export 'src/buttons/help_button.dart';
export 'src/buttons/icon_button.dart';
export 'src/buttons/popup_button.dart';
Expand Down
193 changes: 193 additions & 0 deletions lib/src/buttons/disclosure_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import 'package:flutter/foundation.dart';
import 'package:macos_ui/macos_ui.dart';
import 'package:macos_ui/src/library.dart';

/// A macOS style disclosure button.
class MacosDisclosureButton extends StatefulWidget {
/// Creates a `DisclosureButton` with the appropriate icon/background colors based
/// on light/dark themes.
const MacosDisclosureButton({
super.key,
this.fillColor,
this.semanticLabel,
this.isPressed = false,
this.mouseCursor = SystemMouseCursors.basic,
this.onPressed,
});

final VoidCallback? onPressed;

/// The color to fill the space around the icon with.
final Color? fillColor;

/// The semantic label used by screen readers.
final String? semanticLabel;

/// The mouse cursor to use when hovering over this widget.
final MouseCursor? mouseCursor;

final bool isPressed;

/// 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
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ColorProperty('fillColor', fillColor));
properties.add(ColorProperty('hoverColor', fillColor));
properties.add(StringProperty('semanticLabel', semanticLabel));
properties.add(FlagProperty(
'enabled',
value: enabled,
ifFalse: 'disabled',
));
}

@override
MacosDisclosureButtonState createState() => MacosDisclosureButtonState();
}

class MacosDisclosureButtonState extends State<MacosDisclosureButton>
with SingleTickerProviderStateMixin {
static const Duration kFadeOutDuration = Duration(milliseconds: 10);
static const Duration kFadeInDuration = Duration(milliseconds: 100);
final Tween<double> _opacityTween = Tween<double>(begin: 1.0);

late AnimationController _animationController;
late Animation<double> _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(MacosDisclosureButton oldWidget) {
super.didUpdateWidget(oldWidget);
_setTween();
}

void _setTween() {
_opacityTween.end = 1.0;
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

@visibleForTesting
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>((void value) {
if (mounted && wasHeldDown != buttonHeldDown) _animate();
});
}

@override
Widget build(BuildContext context) {
final bool enabled = widget.enabled;
final brightness = MacosTheme.of(context).brightness;
final iconColor = brightness == Brightness.dark
? CupertinoColors.white
: CupertinoColors.black;

Color? fillColor;
if (widget.fillColor != null) {
fillColor = widget.fillColor;
} else {
fillColor = brightness == Brightness.dark
? const Color(0xff323232)
: const Color(0xffF4F5F5);
}

return MouseRegion(
cursor: widget.mouseCursor!,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: enabled ? _handleTapDown : null,
onTapUp: enabled ? _handleTapUp : null,
onTapCancel: enabled ? _handleTapCancel : null,
onTap: () {
if (enabled) {
widget.onPressed!();
}
},
child: Semantics(
button: true,
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: FadeTransition(
opacity: _opacityAnimation,
child: AnimatedBuilder(
animation: _opacityAnimation,
builder: (context, widget1) {
return DecoratedBox(
decoration: BoxDecoration(
color: buttonHeldDown
? brightness == Brightness.dark
? const Color(0xff3C383C)
: const Color(0xffE5E5E5)
: fillColor,
borderRadius: BorderRadius.circular(7),
),
child: RotatedBox(
quarterTurns: widget.isPressed ? 1 : 3,
child: Icon(
CupertinoIcons.back,
size: 14,
color: iconColor,
),
),
);
},
),
),
),
),
),
);
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: macos_ui
description: Flutter widgets and themes implementing the current macOS design language.
version: 1.7.6
version: 1.7.7
homepage: "https://macosui.dev"
repository: "https://github.com/GroovinChip/macos_ui"

Expand Down
59 changes: 59 additions & 0 deletions test/buttons/disclosure_button_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:macos_ui/macos_ui.dart';

import '../mocks.dart';

void main() {
late MockOnPressedFunction mockOnPressedFunction;

setUp(() {
mockOnPressedFunction = MockOnPressedFunction();
});

testWidgets('MacosDisclosureButton onPressed works', (tester) async {
await tester.pumpWidget(
MacosApp(
home: MacosWindow(
child: MacosScaffold(
children: [
ContentArea(
builder: (context, scrollController) {
return MacosDisclosureButton(
onPressed: mockOnPressedFunction.handler,
);
},
),
],
),
),
),
);

final disclosureButton = find.byType(MacosDisclosureButton);
await tester.tap(disclosureButton);
await tester.pumpAndSettle();

expect(mockOnPressedFunction.called, 2);
});

testWidgets('debugFillProperties', (tester) async {
final builder = DiagnosticPropertiesBuilder();
const MacosDisclosureButton().debugFillProperties(builder);

final description = builder.properties
.where((node) => !node.isFiltered(DiagnosticLevel.info))
.map((node) => node.toString())
.toList();

expect(
description,
[
'fillColor: null',
'hoverColor: null',
'semanticLabel: null',
'disabled',
],
);
});
}

0 comments on commit 516d57d

Please sign in to comment.