Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add switch #31

Merged
merged 4 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions forui/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
version: "2.2.5"
path_provider_foundation:
dependency: transitive
description:
Expand Down Expand Up @@ -478,10 +478,10 @@ packages:
dependency: transitive
description:
name: pubspec_parse
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
url: "https://pub.dev"
source: hosted
version: "1.2.3"
version: "1.3.0"
shelf:
dependency: transitive
description:
Expand Down Expand Up @@ -627,10 +627,10 @@ packages:
dependency: transitive
description:
name: web_socket
sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712"
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
url: "https://pub.dev"
source: hosted
version: "0.1.4"
version: "0.1.5"
web_socket_channel:
dependency: transitive
description:
Expand Down Expand Up @@ -665,4 +665,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.19.2"
flutter: ">=3.22.0"
1 change: 1 addition & 0 deletions forui/lib/forui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export 'src/widgets/box.dart';
export 'src/widgets/badge/badge.dart' hide FBadgeContent;
export 'src/widgets/card/card.dart' hide FCardContent;
export 'src/widgets/separator.dart';
export 'src/widgets/switch.dart';
18 changes: 14 additions & 4 deletions forui/lib/src/theme/theme_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class FThemeData with Diagnosticable {
/// The separator styles.
final FSeparatorStyles separatorStyles;

/// The switch style.
final FSwitchStyle switchStyle;

/// Creates a [FThemeData].
FThemeData({
required this.colorScheme,
Expand All @@ -34,6 +37,7 @@ class FThemeData with Diagnosticable {
required this.boxStyle,
required this.cardStyle,
required this.separatorStyles,
required this.switchStyle,
});

/// Creates a [FThemeData] that inherits the given arguments' properties.
Expand All @@ -45,7 +49,8 @@ class FThemeData with Diagnosticable {
badgeStyles = FBadgeStyles.inherit(colorScheme: colorScheme, font: font, style: style),
boxStyle = FBoxStyle.inherit(colorScheme: colorScheme),
cardStyle = FCardStyle.inherit(colorScheme: colorScheme, font: font, style: style),
separatorStyles = FSeparatorStyles.inherit(colorScheme: colorScheme, style: style);
separatorStyles = FSeparatorStyles.inherit(colorScheme: colorScheme, style: style),
switchStyle = FSwitchStyle.inherit(colorScheme: colorScheme);

/// Creates a copy of this [FThemeData] with the given properties replaced.
FThemeData copyWith({
Expand All @@ -56,6 +61,7 @@ class FThemeData with Diagnosticable {
FBoxStyle? boxStyle,
FCardStyle? cardStyle,
FSeparatorStyles? separatorStyles,
FSwitchStyle? switchStyle,
}) => FThemeData(
colorScheme: colorScheme ?? this.colorScheme,
font: font ?? this.font,
Expand All @@ -64,6 +70,7 @@ class FThemeData with Diagnosticable {
boxStyle: boxStyle ?? this.boxStyle,
cardStyle: cardStyle ?? this.cardStyle,
separatorStyles: separatorStyles ?? this.separatorStyles,
switchStyle: switchStyle ?? this.switchStyle,
);

@override
Expand All @@ -76,7 +83,8 @@ class FThemeData with Diagnosticable {
..add(DiagnosticsProperty<FBadgeStyles>('badgeStyles', badgeStyles, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty<FBoxStyle>('boxStyle', boxStyle, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty<FCardStyle>('cardStyle', cardStyle, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty<FSeparatorStyles>('separatorStyles', separatorStyles, level: DiagnosticLevel.debug));
..add(DiagnosticsProperty<FSeparatorStyles>('separatorStyles', separatorStyles, level: DiagnosticLevel.debug))
..add(DiagnosticsProperty<FSwitchStyle>('switchStyle', switchStyle));
}

@override
Expand All @@ -90,7 +98,8 @@ class FThemeData with Diagnosticable {
badgeStyles == other.badgeStyles &&
boxStyle == other.boxStyle &&
cardStyle == other.cardStyle &&
separatorStyles == other.separatorStyles;
separatorStyles == other.separatorStyles &&
switchStyle == other.switchStyle;

@override
int get hashCode =>
Expand All @@ -100,5 +109,6 @@ class FThemeData with Diagnosticable {
badgeStyles.hashCode ^
boxStyle.hashCode ^
cardStyle.hashCode ^
separatorStyles.hashCode;
separatorStyles.hashCode ^
switchStyle.hashCode;
}
177 changes: 177 additions & 0 deletions forui/lib/src/widgets/switch.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';

import 'package:forui/forui.dart';

/// A control that allows the user to toggle between checked and not checked.
class FSwitch extends StatelessWidget {

/// The style of the switch.
final FSwitchStyle? style;

/// Whether this switch is on or off.
final bool value;

/// Called when the user toggles with switch on or off.
///
/// The switch passes the new value to the callback but does not actually
/// change state until the parent widget rebuilds the switch with the new
/// value.
///
/// If null, the switch will be displayed as disabled, which has a reduced opacity.
///
/// The callback provided to onChanged should update the state of the parent
/// [StatefulWidget] using the [State.setState] method, so that the parent
/// gets rebuilt; for example:
///
/// ```dart
/// FSwitch(
/// value: _giveVerse,
/// onChanged: (bool newValue) {
/// setState(() {
/// _giveVerse = newValue;
/// });
/// },
/// )
/// ```
final ValueChanged<bool>? onChanged;

/// True if this widget will be selected as the initial focus when no other node in its scope is currently focused.
///
/// Ideally, there is only one widget with autofocus set in each FocusScope. If there is more than one widget with
/// autofocus set, then the first one added to the tree will get focus.
///
/// Defaults to false.
final bool autofocus;

/// An optional focus node to use as the focus node for this widget.
///
/// If one is not supplied, then one will be automatically allocated, owned, and managed by this widget. The widget
/// will be focusable even if a [focusNode] is not supplied. If supplied, the given `focusNode` will be hosted by this
/// widget, but not owned. See [FocusNode] for more information on what being hosted and/or owned implies.
///
/// Supplying a focus node is sometimes useful if an ancestor to this widget wants to control when this widget has the
/// focus. The owner will be responsible for calling [FocusNode.dispose] on the focus node when it is done with it,
/// but this widget will attach/detach and reparent the node when needed.
final FocusNode? focusNode;

/// Handler called when the focus changes.
///
/// Called with true if this widget's node gains focus, and false if it loses focus.
final ValueChanged<bool>? onFocusChange;

/// Determines the way that drag start behavior is handled.
///
/// If set to [DragStartBehavior.start], the drag behavior used to move the
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved
/// switch from on to off will begin at the position where the drag gesture won
/// the arena. If set to [DragStartBehavior.down] it will begin at the position
/// where a down event was first detected.
///
/// In general, setting this to [DragStartBehavior.start] will make drag
/// animation smoother and setting it to [DragStartBehavior.down] will make
/// drag behavior feel slightly more reactive.
///
/// By default, the drag start behavior is [DragStartBehavior.start].
final DragStartBehavior dragStartBehavior;

/// Creates a [FSwitch].
const FSwitch({
required this.value,
required this.onChanged,
this.style,
this.autofocus = false,
this.focusNode,
this.onFocusChange,
this.dragStartBehavior = DragStartBehavior.start,
super.key,
});

@override
Widget build(BuildContext context) {
final style = this.style ?? context.theme.switchStyle;
return CupertinoSwitch(
value: value,
onChanged: onChanged,
activeColor: style.checked,
trackColor: style.unchecked,
thumbColor: style.thumb,
focusColor: style.focus,
autofocus: autofocus,
focusNode: focusNode,
onFocusChange: onFocusChange,
dragStartBehavior: dragStartBehavior,
);
}

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('value', value))
..add(DiagnosticsProperty('autofocus', autofocus))
..add(DiagnosticsProperty('dragStartBehavior', dragStartBehavior))
..add(DiagnosticsProperty('style', style))
..add(DiagnosticsProperty<ValueChanged<bool>>('onChanged', onChanged))
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
..add(DiagnosticsProperty<ValueChanged<bool>>('onFocusChange', onFocusChange));
}
}

/// The style of a [FSwitch].
final class FSwitchStyle with Diagnosticable {

/// The color of the switch when it is checked.
final Color checked;

/// The color of the switch when it is unchecked.
final Color unchecked;

/// The color of the switch's thumb.
final Color thumb;

/// The color of the switch when it is focused. Defaults to a slightly transparent [checked] color.
final Color focus;

/// Creates a [FSwitchStyle].
const FSwitchStyle({
required this.checked,
required this.unchecked,
required this.thumb,
required this.focus,
});

/// Creates a [FSwitchStyle] that inherits its properties from [colorScheme].
FSwitchStyle.inherit({required FColorScheme colorScheme})
: checked = colorScheme.primary,
unchecked = colorScheme.border,
thumb = colorScheme.background,
focus = HSLColor.fromColor(colorScheme.primary.withOpacity(0.80))
kawaijoe marked this conversation as resolved.
Show resolved Hide resolved
.withLightness(0.69)
.withSaturation(0.835)
.toColor();

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(ColorProperty('checked', checked))
..add(ColorProperty('unchecked', unchecked))
..add(ColorProperty('thumb', thumb))
..add(ColorProperty('focus', focus));
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FSwitchStyle &&
runtimeType == other.runtimeType &&
checked == other.checked &&
unchecked == other.unchecked &&
thumb == other.thumb &&
focus == other.focus;

@override
int get hashCode => checked.hashCode ^ unchecked.hashCode ^ thumb.hashCode ^ focus.hashCode;

}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions forui/test/src/widgets/switch_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:flutter/cupertino.dart';

import 'package:flutter_test/flutter_test.dart';

import 'package:forui/forui.dart';
import '../test_scaffold.dart';

void main() {
group('FSeparator', () {
for (final (name, theme, _) in TestScaffold.themes) {
for (final (checked, value) in [('checked', true), ('unchecked', false)]) {
testWidgets('$name - $checked - unfocused', (tester) async {
await tester.pumpWidget(
TestScaffold(
data: theme,
child: Center(
child: FSwitch(
value: value,
onChanged: (_) {},
),
),
),
);

await expectLater(
find.byType(TestScaffold),
matchesGoldenFile('switch/$name-$checked-unfocused.png'),
);
});

testWidgets('$name - $checked - focused', (tester) async {
await tester.pumpWidget(
TestScaffold(
data: theme,
child: Center(
child: FSwitch(
value: value,
autofocus: true,
onChanged: (_) {},
),
),
),
);

await expectLater(
find.byType(TestScaffold),
matchesGoldenFile('switch/$name-$checked-focused.png'),
);
});

testWidgets('$name - $checked - disabled', (tester) async {
await tester.pumpWidget(
TestScaffold(
data: theme,
child: Center(
child: FSwitch(
value: value,
autofocus: true,
onChanged: null,
),
),
),
);

await expectLater(
find.byType(TestScaffold),
matchesGoldenFile('switch/$name-$checked-disabled.png'),
);
});
}
}
});
}
Loading