diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c6bd4af..4a426fce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
+## [2.0.0-beta.2]
+β¨New β¨
+* `MacosSwitch` has been completely rewritten and now matches the native macOS switch in appearance and behavior.
+* A `ControlSize` enum has been introduced, which will allow widgets to more closely match their native counterparts.
+
+π Updated π
+* Some previously missing elements of the `MacosColor` class have been added.
+
## [2.0.0-beta.1]
π¨ Breaking Changes π¨
-* Migrate macos_ui to [macos_window_utils](https://pub.dev/packages/macos_window_utils), which provides the following benefits:
+* Migrate `macos_ui` to [macos_window_utils](https://pub.dev/packages/macos_window_utils), which provides the following benefits:
* Window animation smoothness is drastically improved, particularly when miniaturizing and deminiaturizing the application window.
* Some visual artifacts that occurred while the window was being (de)miniaturized (such as the application's shadow going missing) no longer occur.
* The sidebar remains transparent when the app's brightness setting mismatches the OS setting.
diff --git a/README.md b/README.md
index 4c8f9447..8164314d 100644
--- a/README.md
+++ b/README.md
@@ -657,26 +657,34 @@ PushButton(
## MacosSwitch
-A switch is a visual toggle between two mutually exclusive states β on and off. A switch shows that it's on when the
-accent color is visible and off when the switch appears colorless. [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/buttons/switches/)
+A switch (also known as a toggle) is a control that offers a binary choice between two mutually exclusive states β on and off. A switch shows that it's on when the
+accent color is visible and off when the switch appears colorless.
-| On | Off |
-| ------------------------------------------ | ------------------------------------------ |
-|
|
|
+The `ContolSize` enum can be passed to the `size` property to control the size of the switch. `MacosSwitch` supports the following
+control sizes:
+* `mini`
+* `small`
+* `regular`
+
+| Off | On |
+|--------------------------------------------|--------------------------------------------|
+|
|
|
Here's an example of how to create a basic toggle switch:
```dart
-bool selected = false;
+bool switchValue = false;
MacosSwitch(
- value: selected,
+ value: switchValue,
onChanged: (value) {
- setState(() => selected = value);
+ setState(() => switchValue = value);
},
),
```
+Learn more about switches [here](https://developer.apple.com/design/human-interface-guidelines/toggles).
+
## MacosSegmentedControl
Displays one or more navigational tabs in a single horizontal group. Used by `MacosTabView` to navigate between the
diff --git a/example/lib/pages/buttons_page.dart b/example/lib/pages/buttons_page.dart
index 6c373f5f..befbaef1 100644
--- a/example/lib/pages/buttons_page.dart
+++ b/example/lib/pages/buttons_page.dart
@@ -217,11 +217,32 @@ class _ButtonsPageState extends State {
const SizedBox(height: 20),
const Text('MacosSwitch'),
const SizedBox(height: 8),
- MacosSwitch(
- value: switchValue,
- onChanged: (value) {
- setState(() => switchValue = value);
- },
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ MacosSwitch(
+ value: switchValue,
+ size: ControlSize.mini,
+ onChanged: (value) {
+ setState(() => switchValue = value);
+ },
+ ),
+ const SizedBox(width: 16.0),
+ MacosSwitch(
+ value: switchValue,
+ size: ControlSize.small,
+ onChanged: (value) {
+ setState(() => switchValue = value);
+ },
+ ),
+ const SizedBox(width: 16.0),
+ MacosSwitch(
+ value: switchValue,
+ onChanged: (value) {
+ setState(() => switchValue = value);
+ },
+ ),
+ ],
),
const SizedBox(height: 20),
const Text('MacosPulldownButton'),
diff --git a/example/pubspec.lock b/example/pubspec.lock
index bf64e601..931e90c9 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -97,7 +97,7 @@ packages:
path: ".."
relative: true
source: path
- version: "2.0.0-beta.1"
+ version: "2.0.0-beta.2"
macos_window_utils:
dependency: transitive
description:
diff --git a/lib/macos_ui.dart b/lib/macos_ui.dart
index 4d3ac251..62dcc602 100644
--- a/lib/macos_ui.dart
+++ b/lib/macos_ui.dart
@@ -14,6 +14,9 @@
library macos_ui;
+export 'package:macos_window_utils/macos/ns_window_delegate.dart';
+export 'package:macos_window_utils/macos_window_utils.dart';
+
export 'src/buttons/back_button.dart';
export 'src/buttons/checkbox.dart';
export 'src/buttons/disclosure_button.dart';
@@ -29,6 +32,7 @@ export 'src/buttons/toolbar/toolbar_icon_button.dart';
export 'src/buttons/toolbar/toolbar_overflow_button.dart';
export 'src/buttons/toolbar/toolbar_pulldown_button.dart';
export 'src/dialogs/macos_alert_dialog.dart';
+export 'src/enums/control_size.dart';
export 'src/fields/search_field.dart';
export 'src/fields/text_field.dart';
export 'src/icon/image_icon.dart';
@@ -37,7 +41,6 @@ export 'src/indicators/capacity_indicators.dart';
export 'src/indicators/progress_indicators.dart';
export 'src/indicators/rating_indicator.dart';
export 'src/indicators/relevance_indicator.dart';
-export 'src/layout/scrollbar.dart';
export 'src/indicators/slider.dart';
export 'src/labels/label.dart';
export 'src/labels/tooltip.dart';
@@ -45,6 +48,7 @@ export 'src/layout/content_area.dart';
export 'src/layout/macos_list_tile.dart';
export 'src/layout/resizable_pane.dart';
export 'src/layout/scaffold.dart';
+export 'src/layout/scrollbar.dart';
export 'src/layout/sidebar/sidebar.dart';
export 'src/layout/sidebar/sidebar_item.dart';
export 'src/layout/sidebar/sidebar_items.dart';
@@ -60,8 +64,10 @@ export 'src/layout/toolbar/toolbar_overflow_menu.dart';
export 'src/layout/toolbar/toolbar_overflow_menu_item.dart';
export 'src/layout/toolbar/toolbar_popup.dart';
export 'src/layout/toolbar/toolbar_spacer.dart';
+export 'src/layout/wallpaper_tinted_area.dart';
export 'src/layout/window.dart';
export 'src/macos_app.dart';
+export 'src/macos_window_utils_config.dart';
export 'src/selectors/color_well.dart';
export 'src/selectors/date_picker.dart';
export 'src/selectors/time_picker.dart';
@@ -82,8 +88,3 @@ export 'src/theme/search_field_theme.dart';
export 'src/theme/time_picker_theme.dart';
export 'src/theme/tooltip_theme.dart';
export 'src/theme/typography.dart';
-export 'src/layout/wallpaper_tinted_area.dart';
-export 'src/macos_window_utils_config.dart';
-
-export 'package:macos_window_utils/macos_window_utils.dart';
-export 'package:macos_window_utils/macos/ns_window_delegate.dart';
diff --git a/lib/src/buttons/switch.dart b/lib/src/buttons/switch.dart
index 6dd4375e..25d7f637 100644
--- a/lib/src/buttons/switch.dart
+++ b/lib/src/buttons/switch.dart
@@ -1,23 +1,48 @@
-import 'package:flutter/cupertino.dart' as c;
-import 'package:flutter/foundation.dart';
+import 'dart:ui';
+
import 'package:flutter/gestures.dart';
+import 'package:flutter/rendering.dart';
import 'package:macos_ui/macos_ui.dart';
import 'package:macos_ui/src/library.dart';
+const _kDefaultBorderColor = CupertinoDynamicColor.withBrightness(
+ color: MacosColor.fromRGBO(215, 215, 215, 1.0),
+ darkColor: MacosColor.fromRGBO(101, 101, 101, 1.0),
+);
+
+const _kDefaultTrackColor = CupertinoDynamicColor.withBrightness(
+ color: MacosColor.fromRGBO(228, 226, 228, 1.0),
+ darkColor: MacosColor.fromRGBO(66, 66, 66, 1.0),
+);
+
+// Dark color might be Color.fromRGBO(255, 255, 255, 0.721)??
+const _kDefaultKnobColor = CupertinoDynamicColor.withBrightness(
+ color: MacosColors.white,
+ darkColor: MacosColor.fromRGBO(207, 207, 207, 1.0),
+);
+
/// {@template macosSwitch}
-/// A switch is a visual toggle between two mutually exclusive
-/// states β on and off. A switch shows that it's on when the
-/// accent color is visible and off when the switch appears colorless.
+/// A switch is a control that offers a binary choice between two mutually
+/// exclusive states β on and off.
+///
+/// A switch shows that it's on when the [activeColor] is visible and off when
+/// the [trackColor] is visible.
+///
+/// Additional Reference:
+/// * [Toggles (Human Interface Guidelines)](https://developer.apple.com/design/human-interface-guidelines/components/selection-and-input/toggles)
+/// * [Toggles (Apple Developer)](https://developer.apple.com/documentation/swiftui/toggle)
/// {@endtemplate}
-class MacosSwitch extends StatelessWidget {
+class MacosSwitch extends StatefulWidget {
/// {@macro macosSwitch}
const MacosSwitch({
super.key,
required this.value,
+ this.size = ControlSize.regular,
required this.onChanged,
this.dragStartBehavior = DragStartBehavior.start,
this.activeColor,
this.trackColor,
+ this.knobColor,
this.semanticLabel,
});
@@ -26,6 +51,13 @@ class MacosSwitch extends StatelessWidget {
/// Must not be null.
final bool value;
+ /// The size of the switch, which is [ControlSize.regular] by default.
+ ///
+ /// Allowable sizes are [ControlSize.mini], [ControlSize.small], and
+ /// [ControlSize.regular]. If [ControlSize.large] is used, the switch will
+ /// size itself as a [ControlSize.regular] switch.
+ final ControlSize size;
+
/// Called when the user toggles with switch on or off.
///
/// The switch passes the new value to the callback but does not actually
@@ -39,7 +71,7 @@ class MacosSwitch extends StatelessWidget {
/// gets rebuilt; for example:
///
/// ```dart
- /// Switch(
+ /// MacosSwitch(
/// value: _giveVerse,
/// onChanged: (bool newValue) {
/// setState(() {
@@ -53,19 +85,25 @@ class MacosSwitch extends StatelessWidget {
/// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
- /// The color to use when this switch is on.
+ /// The color to use for the track when this switch is on.
///
/// Defaults to [MacosThemeData.primaryColor] when null.
- final Color? activeColor;
+ final MacosColor? activeColor;
- /// The color to use for the background when the switch is off.
+ /// The color to use for track when this switch is off.
///
- /// Defaults to [CupertinoColors.secondarySystemFill] when null.
- final Color? trackColor;
+ /// Defaults to [MacosTheme.primaryColor] when null.
+ final MacosColor? trackColor;
+
+ /// The color to use for the switch's knob.
+ final MacosColor? knobColor;
/// The semantic label used by screen readers.
final String? semanticLabel;
+ @override
+ State createState() => _MacosSwitchState();
+
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
@@ -74,6 +112,7 @@ class MacosSwitch extends StatelessWidget {
value: value,
ifFalse: 'unchecked',
));
+ properties.add(EnumProperty('size', size));
properties.add(EnumProperty('dragStartBehavior', dragStartBehavior));
properties.add(FlagProperty(
'enabled',
@@ -82,26 +121,606 @@ class MacosSwitch extends StatelessWidget {
));
properties.add(ColorProperty('activeColor', activeColor));
properties.add(ColorProperty('trackColor', trackColor));
+ properties.add(ColorProperty('knobColor', knobColor));
properties.add(StringProperty('semanticLabel', semanticLabel));
}
+}
+
+class _MacosSwitchState extends State
+ with TickerProviderStateMixin {
+ late TapGestureRecognizer _tap;
+ late HorizontalDragGestureRecognizer _drag;
+
+ late AnimationController _positionController;
+ late CurvedAnimation position;
+
+ late AnimationController _reactionController;
+ late Animation _reaction;
+
+ bool get isInteractive => widget.onChanged != null;
+
+ // A non-null boolean value that changes to true at the end of a drag if the
+ // switch must be animated to the position indicated by the widget's value.
+ bool needsPositionAnimation = false;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _tap = TapGestureRecognizer()
+ ..onTapDown = _handleTapDown
+ ..onTapUp = _handleTapUp
+ ..onTap = _handleTap
+ ..onTapCancel = _handleTapCancel;
+ _drag = HorizontalDragGestureRecognizer()
+ ..onStart = _handleDragStart
+ ..onUpdate = _handleDragUpdate
+ ..onEnd = _handleDragEnd
+ ..dragStartBehavior = widget.dragStartBehavior;
+
+ _positionController = AnimationController(
+ duration: _kToggleDuration,
+ value: widget.value ? 1.0 : 0.0,
+ vsync: this,
+ );
+ position = CurvedAnimation(
+ parent: _positionController,
+ curve: Curves.linear,
+ );
+ _reactionController = AnimationController(
+ duration: _kReactionDuration,
+ vsync: this,
+ );
+ _reaction = CurvedAnimation(
+ parent: _reactionController,
+ curve: Curves.ease,
+ );
+ }
+
+ @override
+ void didUpdateWidget(MacosSwitch oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ _drag.dragStartBehavior = widget.dragStartBehavior;
+
+ if (needsPositionAnimation || oldWidget.value != widget.value) {
+ _resumePositionAnimation(isLinear: needsPositionAnimation);
+ }
+ }
+
+ // `isLinear` must be true if the position animation is trying to move the
+ // knob to the closest end after the most recent drag animation, so the curve
+ // does not change when the controller's value is not 0 or 1.
+ //
+ // It can be set to false when it's an implicit animation triggered by
+ // widget.value changes.
+ void _resumePositionAnimation({bool isLinear = true}) {
+ needsPositionAnimation = false;
+ position
+ ..curve = isLinear ? Curves.linear : Curves.ease
+ ..reverseCurve = isLinear ? Curves.linear : Curves.ease.flipped;
+ if (widget.value) {
+ _positionController.forward();
+ } else {
+ _positionController.reverse();
+ }
+ }
+
+ void _handleTapDown(TapDownDetails details) {
+ if (isInteractive) {
+ needsPositionAnimation = false;
+ }
+ _reactionController.forward();
+ }
+
+ void _handleTap() {
+ if (isInteractive) {
+ widget.onChanged!(!widget.value);
+ }
+ }
+
+ void _handleTapUp(TapUpDetails details) {
+ if (isInteractive) {
+ needsPositionAnimation = false;
+ _reactionController.reverse();
+ }
+ }
+
+ void _handleTapCancel() {
+ if (isInteractive) {
+ _reactionController.reverse();
+ }
+ }
+
+ void _handleDragStart(DragStartDetails details) {
+ if (isInteractive) {
+ needsPositionAnimation = false;
+ _reactionController.forward();
+ }
+ }
+
+ void _handleDragUpdate(DragUpdateDetails details) {
+ if (isInteractive) {
+ position
+ ..curve = Curves.linear
+ ..reverseCurve = Curves.linear;
+ final double delta = details.primaryDelta! / widget.size.trackInnerLength;
+ switch (Directionality.of(context)) {
+ case TextDirection.rtl:
+ _positionController.value -= delta;
+ break;
+ case TextDirection.ltr:
+ _positionController.value += delta;
+ break;
+ }
+ }
+ }
+
+ void _handleDragEnd(DragEndDetails details) {
+ // Deferring the animation to the next build phase.
+ setState(() => needsPositionAnimation = true);
+ // Call onChanged when the user's intent to change value is clear.
+ if (position.value >= 0.5 != widget.value) {
+ widget.onChanged!(!widget.value);
+ }
+ _reactionController.reverse();
+ }
+
+ @override
+ void dispose() {
+ _tap.dispose();
+ _drag.dispose();
+
+ _positionController.dispose();
+ _reactionController.dispose();
+ super.dispose();
+ }
@override
Widget build(BuildContext context) {
assert(debugCheckHasMacosTheme(context));
final MacosThemeData theme = MacosTheme.of(context);
+ MacosColor borderColor =
+ MacosDynamicColor.resolve(_kDefaultBorderColor, context).toMacosColor();
+ MacosColor activeColor = MacosColor(MacosDynamicColor.resolve(
+ widget.activeColor ?? theme.primaryColor,
+ context,
+ ).value);
+ MacosColor trackColor = widget.trackColor ??
+ MacosDynamicColor.resolve(_kDefaultTrackColor, context).toMacosColor();
+ MacosColor knobColor = widget.knobColor ??
+ MacosDynamicColor.resolve(_kDefaultKnobColor, context).toMacosColor();
+
+ // Shot in the dark to try and get the border color correct for each
+ // possible color
+ if (widget.value) {
+ if (theme.brightness.isDark) {
+ borderColor.computeLuminance() > 0.5
+ ? borderColor = MacosColor.darken(activeColor, 20)
+ : borderColor = MacosColor.lighten(activeColor, 20);
+ } else {
+ borderColor.computeLuminance() > 0.5
+ ? borderColor = MacosColor.darken(activeColor, 20)
+ : borderColor = MacosColor.lighten(activeColor, 20);
+ }
+ }
+
return Semantics(
- label: semanticLabel,
- checked: value,
- child: c.CupertinoSwitch(
- value: value,
- onChanged: onChanged,
- dragStartBehavior: dragStartBehavior,
- activeColor: MacosDynamicColor.resolve(
- activeColor ?? theme.primaryColor,
- context,
- ),
+ label: widget.semanticLabel,
+ checked: widget.value,
+ child: _MacosSwitchRenderObjectWidget(
+ value: widget.value,
+ size: widget.size,
+ activeColor: activeColor,
trackColor: trackColor,
+ knobColor: knobColor,
+ borderColor: borderColor,
+ onChanged: widget.onChanged,
+ textDirection: Directionality.of(context),
+ state: this,
),
);
}
}
+
+class _MacosSwitchRenderObjectWidget extends LeafRenderObjectWidget {
+ const _MacosSwitchRenderObjectWidget({
+ required this.value,
+ required this.size,
+ required this.activeColor,
+ required this.trackColor,
+ required this.knobColor,
+ required this.borderColor,
+ required this.onChanged,
+ required this.textDirection,
+ required this.state,
+ });
+ final bool value;
+ final ControlSize size;
+ final MacosColor activeColor;
+ final MacosColor trackColor;
+ final MacosColor knobColor;
+ final MacosColor borderColor;
+ final ValueChanged? onChanged;
+ final TextDirection textDirection;
+ final _MacosSwitchState state;
+
+ @override
+ _RenderMacosSwitch createRenderObject(BuildContext context) {
+ return _RenderMacosSwitch(
+ value: value,
+ size: size,
+ activeColor: activeColor,
+ trackColor: trackColor,
+ knobColor: knobColor,
+ borderColor: borderColor,
+ onChanged: onChanged,
+ textDirection: textDirection,
+ state: state,
+ );
+ }
+
+ @override
+ void updateRenderObject(
+ BuildContext context,
+ _RenderMacosSwitch renderObject,
+ ) {
+ assert(renderObject._state == state);
+ renderObject
+ ..value = value
+ ..controlSize = size
+ ..activeColor = activeColor
+ ..trackColor = trackColor
+ ..knobColor = knobColor
+ ..borderColor = borderColor
+ ..onChanged = onChanged
+ ..textDirection = textDirection;
+ }
+}
+
+const Size _kMiniTrackSize = Size(26.0, 15.0);
+const Size _kSmallTrackSize = Size(32.0, 18.0);
+const Size _kRegularTrackSize = Size(38.0, 22.0);
+
+const double _kMiniKnobSize = 13.0;
+const double _kSmallKnobSize = 16.0;
+const double _kRegularKnobSize = 20.0;
+
+// Shortcuts for details about how to create the switch, based on the control
+// size.
+extension _ControlSizeX on ControlSize {
+ Size get trackSize {
+ switch (this) {
+ case ControlSize.mini:
+ return _kMiniTrackSize;
+ case ControlSize.small:
+ return _kSmallTrackSize;
+ default:
+ return _kRegularTrackSize;
+ }
+ }
+
+ double get knobSize {
+ switch (this) {
+ case ControlSize.mini:
+ return _kMiniKnobSize;
+ case ControlSize.small:
+ return _kSmallKnobSize;
+ default:
+ return _kRegularKnobSize;
+ }
+ }
+
+ double get knobRadius => knobSize / 2.0;
+ double get trackInnerStart => trackSize.height / 2.0;
+ double get trackInnerEnd => trackSize.width - trackInnerStart;
+ double get trackInnerLength => trackInnerEnd - trackInnerStart;
+}
+
+const Duration _kReactionDuration = Duration(milliseconds: 400);
+const Duration _kToggleDuration = Duration(milliseconds: 300);
+
+class _RenderMacosSwitch extends RenderConstrainedBox {
+ _RenderMacosSwitch({
+ required bool value,
+ required ControlSize size,
+ required MacosColor activeColor,
+ required MacosColor trackColor,
+ required MacosColor knobColor,
+ required MacosColor borderColor,
+ required ValueChanged? onChanged,
+ required TextDirection textDirection,
+ required _MacosSwitchState state,
+ }) : _value = value,
+ _size = size,
+ _activeColor = activeColor,
+ _trackColor = trackColor,
+ _knobPainter = MacosSwitchKnobPainter(color: knobColor),
+ _borderColor = borderColor,
+ _onChanged = onChanged,
+ _textDirection = textDirection,
+ _state = state,
+ super(
+ additionalConstraints: BoxConstraints.tightFor(
+ width: size.trackSize.width,
+ height: size.trackSize.height,
+ ),
+ ) {
+ state.position.addListener(markNeedsPaint);
+ state._reaction.addListener(markNeedsPaint);
+ }
+
+ final _MacosSwitchState _state;
+
+ bool get value => _value;
+ bool _value;
+ set value(bool newValue) {
+ if (newValue == _value) {
+ return;
+ }
+ _value = newValue;
+ markNeedsSemanticsUpdate();
+ }
+
+ ControlSize get controlSize => _size;
+ ControlSize _size;
+ set controlSize(ControlSize value) {
+ if (value == _size) {
+ return;
+ }
+ _size = value;
+ markNeedsPaint();
+ }
+
+ MacosColor get activeColor => _activeColor;
+ MacosColor _activeColor;
+ set activeColor(MacosColor value) {
+ if (value == _activeColor) {
+ return;
+ }
+ _activeColor = value;
+ markNeedsPaint();
+ }
+
+ MacosColor get trackColor => _trackColor;
+ MacosColor _trackColor;
+ set trackColor(MacosColor value) {
+ if (value == _trackColor) {
+ return;
+ }
+ _trackColor = value;
+ markNeedsPaint();
+ }
+
+ MacosColor get knobColor => _knobPainter.color;
+ MacosSwitchKnobPainter _knobPainter;
+ set knobColor(MacosColor value) {
+ if (value == knobColor) {
+ return;
+ }
+ _knobPainter = MacosSwitchKnobPainter(color: value);
+ markNeedsPaint();
+ }
+
+ MacosColor get borderColor => _borderColor;
+ MacosColor _borderColor;
+ set borderColor(MacosColor value) {
+ if (value == borderColor) {
+ return;
+ }
+ _borderColor = value;
+ markNeedsPaint();
+ }
+
+ ValueChanged? get onChanged => _onChanged;
+ ValueChanged? _onChanged;
+ set onChanged(ValueChanged? value) {
+ if (value == _onChanged) {
+ return;
+ }
+ final bool wasInteractive = isInteractive;
+ _onChanged = value;
+ if (wasInteractive != isInteractive) {
+ markNeedsPaint();
+ markNeedsSemanticsUpdate();
+ }
+ }
+
+ TextDirection get textDirection => _textDirection;
+ TextDirection _textDirection;
+ set textDirection(TextDirection value) {
+ if (value == _textDirection) {
+ return;
+ }
+ _textDirection = value;
+ markNeedsPaint();
+ }
+
+ bool get isInteractive => onChanged != null;
+
+ @override
+ bool hitTestSelf(Offset position) => true;
+
+ @override
+ void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
+ assert(debugHandleEvent(event, entry));
+ if (event is PointerDownEvent && isInteractive) {
+ _state._drag.addPointer(event);
+ _state._tap.addPointer(event);
+ }
+ }
+
+ @override
+ void describeSemanticsConfiguration(SemanticsConfiguration config) {
+ super.describeSemanticsConfiguration(config);
+
+ if (isInteractive) {
+ config.onTap = _state._handleTap;
+ }
+
+ config.isEnabled = isInteractive;
+ config.isToggled = _value;
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ final Canvas canvas = context.canvas;
+ final double currentValue = _state.position.value;
+ final trackSize = controlSize.trackSize;
+ final innerStart = controlSize.trackInnerStart;
+ final innerEnd = controlSize.trackInnerEnd;
+
+ final double visualPosition;
+ switch (textDirection) {
+ case TextDirection.rtl:
+ visualPosition = 1.0 - currentValue;
+ break;
+ case TextDirection.ltr:
+ visualPosition = currentValue;
+ break;
+ }
+
+ final Paint paint = Paint()
+ ..color = MacosColor.lerp(trackColor, activeColor, currentValue);
+
+ final Rect trackRect = Rect.fromLTWH(
+ offset.dx + (size.width - trackSize.width) / 2.0,
+ offset.dy + (size.height - trackSize.height) / 2.0,
+ trackSize.width,
+ trackSize.height,
+ );
+ final RRect trackRRect = RRect.fromRectAndRadius(
+ trackRect,
+ Radius.circular(trackSize.height / 2.0),
+ );
+ canvas.drawRRect(trackRRect, paint);
+ canvas.drawRRect(
+ trackRRect,
+ Paint()
+ ..color = borderColor
+ ..style = PaintingStyle.stroke,
+ );
+
+ final double knobLeft = lerpDouble(
+ trackRect.left + innerStart - controlSize.knobRadius,
+ trackRect.left + innerEnd - controlSize.knobRadius,
+ visualPosition,
+ )!;
+ final double knobRight = lerpDouble(
+ trackRect.left + innerStart + controlSize.knobRadius,
+ trackRect.left + innerEnd + controlSize.knobRadius,
+ visualPosition,
+ )!;
+ final double knobCenterY = offset.dy + size.height / 2.0;
+ final Rect knobBounds = Rect.fromLTRB(
+ knobLeft,
+ knobCenterY - controlSize.knobRadius,
+ knobRight,
+ knobCenterY + controlSize.knobRadius,
+ );
+
+ _clipRRectLayer.layer = context.pushClipRRect(
+ needsCompositing,
+ Offset.zero,
+ knobBounds,
+ trackRRect,
+ (PaintingContext innerContext, Offset offset) {
+ _knobPainter.paint(
+ innerContext.canvas,
+ knobBounds,
+ visualPosition == 1.0,
+ );
+ },
+ oldLayer: _clipRRectLayer.layer,
+ );
+ }
+
+ final LayerHandle _clipRRectLayer =
+ LayerHandle();
+
+ @override
+ void dispose() {
+ _clipRRectLayer.layer = null;
+ super.dispose();
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder description) {
+ super.debugFillProperties(description);
+ description.add(FlagProperty(
+ 'value',
+ value: value,
+ ifTrue: 'checked',
+ ifFalse: 'unchecked',
+ showName: true,
+ ));
+ description.add(FlagProperty(
+ 'isInteractive',
+ value: isInteractive,
+ ifTrue: 'enabled',
+ ifFalse: 'disabled',
+ showName: true,
+ defaultValue: true,
+ ));
+ }
+}
+
+const List _kSwitchOffBoxShadows = [
+ BoxShadow(
+ color: Color(0x26000000),
+ // offset: Offset(1, 1),
+ blurRadius: 8.0,
+ blurStyle: BlurStyle.inner,
+ ),
+ BoxShadow(
+ color: Color(0x0F000000),
+ // offset: Offset(1, 1),
+ blurRadius: 1.0,
+ blurStyle: BlurStyle.inner,
+ ),
+];
+
+const List _kSwitchOnBoxShadows = [
+ BoxShadow(
+ color: Color(0x26000000),
+ // offset: Offset(-3, 1),
+ blurRadius: 8.0,
+ blurStyle: BlurStyle.inner,
+ ),
+ BoxShadow(
+ color: Color(0x0F000000),
+ // offset: Offset(-1, 1),
+ blurRadius: 1.0,
+ blurStyle: BlurStyle.inner,
+ ),
+];
+
+/// Paints a macOS-style switch knob.
+///
+/// Used by [MacosSwitch].
+class MacosSwitchKnobPainter {
+ /// Creates an object that paints a macOS-style switch knob.
+ const MacosSwitchKnobPainter({required this.color});
+
+ /// The color of the interior of the knob.
+ final MacosColor color;
+
+ /// Paints the knob onto the given canvas in the given rectangle.
+ void paint(Canvas canvas, Rect rect, bool isOn) {
+ final RRect rrect = RRect.fromRectAndRadius(
+ rect,
+ Radius.circular(rect.shortestSide / 2.0),
+ );
+
+ if (isOn) {
+ for (final BoxShadow shadow in _kSwitchOnBoxShadows) {
+ canvas.drawRRect(rrect.shift(shadow.offset), shadow.toPaint());
+ }
+ } else {
+ for (final BoxShadow shadow in _kSwitchOffBoxShadows) {
+ canvas.drawRRect(rrect.shift(shadow.offset), shadow.toPaint());
+ }
+ }
+
+ canvas.drawRRect(rrect, Paint()..color = color);
+ }
+}
diff --git a/lib/src/enums/control_size.dart b/lib/src/enums/control_size.dart
new file mode 100644
index 00000000..835812bf
--- /dev/null
+++ b/lib/src/enums/control_size.dart
@@ -0,0 +1,25 @@
+/// The out-of-the-box sizes that certain "control" widgets can be.
+///
+///
+///
+/// Not all controls support all sizes. For example, a [PushButton] can be any
+/// size, but a [MacosSwitch] can be all but large. In cases where a control
+/// doesn't support a certain size, the control will automatically fall back to
+/// the nearest supported size.
+///
+/// Reference:
+/// * https://developer.apple.com/documentation/swiftui/controlsize
+/// * https://developer.apple.com/documentation/swiftui/view/controlsize(_:)
+enum ControlSize {
+ /// A control that is minimally sized.
+ mini,
+
+ /// A control that is proportionally smaller size for space-constrained views.
+ small,
+
+ /// A control that is the default size.
+ regular,
+
+ /// A control that is prominently sized.
+ large,
+}
diff --git a/lib/src/theme/icon_theme.dart b/lib/src/theme/icon_theme.dart
index 9e7377c8..f3b801f7 100644
--- a/lib/src/theme/icon_theme.dart
+++ b/lib/src/theme/icon_theme.dart
@@ -210,7 +210,7 @@ class MacosIconThemeData with Diagnosticable {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
- properties.add(ColorProperty('color', color, defaultValue: null));
+ properties.add(ColorProperty('MacosColor', color, defaultValue: null));
properties.add(DoubleProperty('opacity', opacity, defaultValue: null));
properties.add(DoubleProperty('size', size, defaultValue: null));
}
diff --git a/lib/src/theme/macos_colors.dart b/lib/src/theme/macos_colors.dart
index f5d92d80..73985d8f 100644
--- a/lib/src/theme/macos_colors.dart
+++ b/lib/src/theme/macos_colors.dart
@@ -85,6 +85,77 @@ class MacosColor extends Color {
static int getAlphaFromOpacity(double opacity) {
return (opacity.clamp(0.0, 1.0) * 255).round();
}
+
+ /// Returns a new color that matches this color with the alpha channel
+ /// replaced with the given `opacity` (which ranges from 0.0 to 1.0).
+ ///
+ /// Out of range values will have unexpected effects.
+ @override
+ MacosColor withOpacity(double opacity) {
+ assert(opacity >= 0.0 && opacity <= 1.0);
+ return withAlpha((255.0 * opacity).round());
+ }
+
+ /// Returns a new color that matches this color with the alpha channel
+ /// replaced with `a` (which ranges from 0 to 255).
+ ///
+ /// Out of range values will have unexpected effects.
+ @override
+ MacosColor withAlpha(int a) {
+ return MacosColor.fromARGB(a, red, green, blue);
+ }
+
+ /// Darkens a [MacosColor] by a [percent] amount (100 = black) without
+ /// changing the tint of the color.
+ static MacosColor darken(MacosColor c, [int percent = 10]) {
+ assert(1 <= percent && percent <= 100);
+ var f = 1 - percent / 100;
+ return MacosColor.fromARGB(
+ c.alpha,
+ (c.red * f).round(),
+ (c.green * f).round(),
+ (c.blue * f).round(),
+ );
+ }
+
+ /// Lightens a [MacosColor] by a [percent] amount (100 = white) without
+ /// changing the tint of the color
+ static MacosColor lighten(MacosColor c, [int percent = 10]) {
+ assert(1 <= percent && percent <= 100);
+ var p = percent / 100;
+ return MacosColor.fromARGB(
+ c.alpha,
+ c.red + ((255 - c.red) * p).round(),
+ c.green + ((255 - c.green) * p).round(),
+ c.blue + ((255 - c.blue) * p).round(),
+ );
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is MacosColor && other.value == value;
+ }
+
+ @override
+ int get hashCode => value.hashCode;
+
+ @override
+ String toString() {
+ return 'MacosColor(0x${value.toRadixString(16).padLeft(8, '0')})';
+ }
+}
+
+extension ColorX on Color {
+ /// Returns a [MacosColor] with the same color values as this [Color].
+ MacosColor toMacosColor() {
+ return MacosColor(value);
+ }
}
/// A collection of color values lifted from the macOS system color picker.
diff --git a/pubspec.yaml b/pubspec.yaml
index 02ae5581..c52118c0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: macos_ui
description: Flutter widgets and themes implementing the current macOS design language.
-version: 2.0.0-beta.1
+version: 2.0.0-beta.2
homepage: "https://macosui.dev"
repository: "https://github.com/GroovinChip/macos_ui"
diff --git a/test/buttons/switch_test.dart b/test/buttons/switch_test.dart
index 642f0f11..a02bc40b 100644
--- a/test/buttons/switch_test.dart
+++ b/test/buttons/switch_test.dart
@@ -52,10 +52,12 @@ void main() {
description,
[
'unchecked',
+ 'size: regular',
'dragStartBehavior: start',
'disabled',
'activeColor: null',
'trackColor: null',
+ 'knobColor: null',
'semanticLabel: null',
],
);
diff --git a/test/theme/help_button_theme_test.dart b/test/theme/help_button_theme_test.dart
index 98556548..1e127f80 100644
--- a/test/theme/help_button_theme_test.dart
+++ b/test/theme/help_button_theme_test.dart
@@ -45,8 +45,8 @@ void main() {
expect(
description,
[
- 'color: Color(0xff0433ff)',
- 'disabledColor: Color(0xff8e8e93)',
+ 'color: MacosColor(0xff0433ff)',
+ 'disabledColor: MacosColor(0xff8e8e93)',
],
);
});
diff --git a/test/theme/icon_theme_test.dart b/test/theme/icon_theme_test.dart
index b45e4de6..79076b18 100644
--- a/test/theme/icon_theme_test.dart
+++ b/test/theme/icon_theme_test.dart
@@ -51,7 +51,7 @@ void main() {
expect(
description,
[
- 'color: Color(0xffffffff)',
+ 'MacosColor: MacosColor(0xffffffff)',
'opacity: 0.0',
'size: 20.0',
],
diff --git a/test/theme/popup_button_theme_test.dart b/test/theme/popup_button_theme_test.dart
index 730d86a6..7cbe6b7a 100644
--- a/test/theme/popup_button_theme_test.dart
+++ b/test/theme/popup_button_theme_test.dart
@@ -54,8 +54,8 @@ void main() {
expect(
description,
[
- 'highlightColor: Color(0xff8e8e93)',
- 'backgroundColor: Color(0xff0433ff)',
+ 'highlightColor: MacosColor(0xff8e8e93)',
+ 'backgroundColor: MacosColor(0xff0433ff)',
'popupColor: Color(0x19000000)',
],
);
diff --git a/test/theme/pulldown_button_theme_test.dart b/test/theme/pulldown_button_theme_test.dart
index 0ef34561..87fc9666 100644
--- a/test/theme/pulldown_button_theme_test.dart
+++ b/test/theme/pulldown_button_theme_test.dart
@@ -53,10 +53,10 @@ void main() {
expect(
description,
[
- 'highlightColor: Color(0xff8e8e93)',
- 'backgroundColor: Color(0xff0433ff)',
+ 'highlightColor: MacosColor(0xff8e8e93)',
+ 'backgroundColor: MacosColor(0xff0433ff)',
'pulldownColor: Color(0x19000000)',
- 'iconColor: Color(0xff00f900)',
+ 'iconColor: MacosColor(0xff00f900)',
],
);
});
diff --git a/test/theme/push_button_theme_test.dart b/test/theme/push_button_theme_test.dart
index a910d981..a26619f8 100644
--- a/test/theme/push_button_theme_test.dart
+++ b/test/theme/push_button_theme_test.dart
@@ -46,8 +46,8 @@ void main() {
expect(
description,
[
- 'color: Color(0xff0433ff)',
- 'disabledColor: Color(0xff8e8e93)',
+ 'color: MacosColor(0xff0433ff)',
+ 'disabledColor: MacosColor(0xff8e8e93)',
'secondaryColor: Color(0x19000000)',
],
);