-
-
Notifications
You must be signed in to change notification settings - Fork 187
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement MacosDisclosureButton
- Loading branch information
Umar Salim
committed
Jan 14, 2023
1 parent
d6d4a27
commit 516d57d
Showing
7 changed files
with
276 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
), | ||
), | ||
); | ||
}, | ||
), | ||
), | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
], | ||
); | ||
}); | ||
} |