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

Improve constructor ergonomics #46

Merged
merged 6 commits into from
Jun 19, 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
9 changes: 5 additions & 4 deletions forui/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,22 @@ class _ExampleWidgetState extends State<ExampleWidget> {
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FButton(
design: FButtonVariant.destructive,
text: 'Delete?',
labelText: 'Delete?',
onPress: () => showAdaptiveDialog(
context: context,
builder: (context) => FDialog(
alignment: FDialogAlignment.horizontal,
title: 'Are you absolutely sure?',
subtitle: 'This action cannot be undone. This will permanently delete your account and remove your data from our servers.',
body: 'This action cannot be undone. This will permanently delete your account and remove your data from our servers.',
actions: [
FButton(design: FButtonVariant.outlined, text: 'Cancel', onPress: () {
FButton(design: FButtonVariant.outlined, labelText: 'Cancel', onPress: () {
Navigator.of(context).pop();
}),
FButton(text: 'Continue', onPress: () {}),
FButton(labelText: 'Continue', onPress: () {}),
],
),
),
Expand Down
11 changes: 7 additions & 4 deletions forui/lib/src/widgets/badge/badge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ class FBadge extends StatelessWidget {

/// Creates a [FBadge].
FBadge({
required String label,
String? label,
Widget? rawLabel,
this.design = FBadgeVariant.primary,
super.key,
}) : builder = ((context, style) => FBadgeContent(label: label, style: style));
}) :
assert((label == null) ^ (rawLabel == null), 'Either "label" or "rawLabel" must be provided, but not both.'),
builder = ((context, style) => FBadgeContent(rawLabel: rawLabel, label: label, style: style));

/// Creates a [FBadge].
const FBadge.raw({required this.design, required this.builder, super.key});
Expand Down Expand Up @@ -143,9 +146,9 @@ final class FBadgeStyle with Diagnosticable implements FBadgeDesign {
properties
..add(ColorProperty('background', background))
..add(ColorProperty('border', border))
..add(DiagnosticsProperty<BorderRadius>('borderRadius', borderRadius, defaultValue: BorderRadius.circular(100)))
..add(DiagnosticsProperty('borderRadius', borderRadius, defaultValue: BorderRadius.circular(100)))
..add(DoubleProperty('borderWidth', borderWidth))
..add(DiagnosticsProperty<FBadgeContentStyle>('content', content));
..add(DiagnosticsProperty('content', content));
}

@override
Expand Down
21 changes: 17 additions & 4 deletions forui/lib/src/widgets/badge/badge_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,28 @@ part of 'badge.dart';

@internal final class FBadgeContent extends StatelessWidget {
final FBadgeStyle style;
final String label;
final String? label;
final Widget? rawLabel;

const FBadgeContent({required this.style, required this.label, super.key});
const FBadgeContent({
required this.style,
this.label,
this.rawLabel,
super.key,
});

@override
Widget build(BuildContext context) => Center(
child: Padding(
padding: style.content.padding,
child: Text(label, style: style.content.label.scale(context.theme.typography)),
child: DefaultTextStyle.merge(
style: style.content.label.scale(context.theme.typography),
child: switch ((label, rawLabel)) {
(final String label, _) => Text(label),
(_, final Widget label) => label,
_ => const Placeholder(),
},
),
),
);

Expand All @@ -19,7 +32,7 @@ part of 'badge.dart';
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('style', style))
..add(StringProperty('label', label));
..add(StringProperty('labelText', label));
}
}

Expand Down
135 changes: 98 additions & 37 deletions forui/lib/src/widgets/button/button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'package:meta/meta.dart';
import 'package:sugar/collection.dart';

import 'package:forui/forui.dart';
import 'package:forui/src/foundation/tappable.dart';

part 'button_content.dart';

part 'button_icon.dart';

part 'button_styles.dart';

/// A button.
class FButton extends StatelessWidget {
@useResult
static _Data _of(BuildContext context) {
final theme = context.dependOnInheritedWidgetOfExactType<_InheritedData>();
return theme?.data ?? (style: context.theme.buttonStyles.primary, enabled: true);
}

/// The design. Defaults to [FBadgeVariant.primary].
final FButtonDesign design;

Expand Down Expand Up @@ -44,8 +54,8 @@ class FButton extends StatelessWidget {
/// Called with true if this widget's node gains focus, and false if it loses focus.
final ValueChanged<bool>? onFocusChange;

/// The builder.
final Widget Function(BuildContext, FButtonStyle) builder;
/// The child.
final Widget child;

/// Creates a [FButton].
FButton({
Expand All @@ -55,21 +65,24 @@ class FButton extends StatelessWidget {
this.autofocus = false,
this.focusNode,
this.onFocusChange,
String? text,
SvgAsset? icon,
Widget? prefixIcon,
Widget? suffixIcon,
Widget? label,
String? labelText,
super.key,
}) : builder = ((context, style) => FButtonContent(
text: text,
icon: icon,
style: style,
enabled: onPress != null,
));
}) : assert((label != null) ^ (labelText != null), 'Either label or labelText must be provided, but not both.'),
child = FButtonContent(
prefixIcon: prefixIcon,
suffixIcon: suffixIcon,
label: label,
labelText: labelText,
);

/// Creates a [FButton].
const FButton.raw({
required this.design,
required this.onPress,
required this.builder,
required this.child,
this.onLongPress,
this.autofocus = false,
this.focusNode,
Expand All @@ -87,20 +100,25 @@ class FButton extends StatelessWidget {
FButtonVariant.destructive => context.theme.buttonStyles.destructive,
};

return Semantics(
container: true,
button: true,
enabled: onPress != null || onLongPress != null,
child: FocusableActionDetector(
autofocus: autofocus,
focusNode: focusNode,
onFocusChange: onFocusChange,
child: FTappable(
onTap: onPress,
onLongPress: onLongPress,
child: DecoratedBox(
decoration: onPress == null ? style.disabledBoxDecoration : style.enabledBoxDecoration,
child: builder(context, style),
final enabled = onPress != null || onLongPress != null;

return _InheritedData(
data: (style: style, enabled: enabled),
child: Semantics(
container: true,
button: true,
enabled: enabled,
child: FocusableActionDetector(
autofocus: autofocus,
focusNode: focusNode,
onFocusChange: onFocusChange,
child: FTappable(
onTap: onPress,
onLongPress: onLongPress,
child: DecoratedBox(
decoration: onPress == null ? style.disabledBoxDecoration : style.enabledBoxDecoration,
child: child,
),
),
),
),
Expand All @@ -114,18 +132,18 @@ class FButton extends StatelessWidget {
..add(DiagnosticsProperty('design', design))
..add(DiagnosticsProperty('onPress', onPress))
..add(DiagnosticsProperty('onLongPress', onLongPress))
..add(FlagProperty('autofocus', value: autofocus, defaultValue: 'autofocus'))
..add(FlagProperty('autofocus', value: autofocus, defaultValue: false, ifTrue: 'autofocus'))
..add(DiagnosticsProperty('focusNode', focusNode))
..add(DiagnosticsProperty('onFocusChange', onFocusChange))
..add(DiagnosticsProperty('builder', builder));
..add(DiagnosticsProperty('builder', child));
}
}

/// The button design. Either a pre-defined [FButtonVariant], or a custom [FButtonStyle].
sealed class FButtonDesign {}

/// A pre-defined button variant.
enum FButtonVariant implements FButtonDesign {
enum FButtonVariant implements FButtonDesign {
/// A primary-styled button.
primary,

Expand All @@ -140,40 +158,83 @@ enum FButtonVariant implements FButtonDesign {
}

/// Represents the theme data that is inherited by [FButtonStyle] and used by child [FButton].
class FButtonStyle extends FButtonDesign with Diagnosticable{
/// The content.
final FButtonContentStyle content;

class FButtonStyle extends FButtonDesign with Diagnosticable {
/// The box decoration for an enabled button.
final BoxDecoration enabledBoxDecoration;

/// The box decoration for a disabled button.
final BoxDecoration disabledBoxDecoration;

/// The content.
final FButtonContentStyle content;

/// The icon.
final FButtonIconStyle icon;

/// Creates a [FButtonStyle].
FButtonStyle({
required this.content,
required this.enabledBoxDecoration,
required this.disabledBoxDecoration,
required this.content,
required this.icon,
});

/// Creates a copy of this [FButtonStyle] with the given properties replaced.
FButtonStyle copyWith({
FButtonContentStyle? content,
BoxDecoration? enabledBoxDecoration,
BoxDecoration? disabledBoxDecoration,
FButtonContentStyle? content,
FButtonIconStyle? icon,
}) =>
FButtonStyle(
content: content ?? this.content,
enabledBoxDecoration: enabledBoxDecoration ?? this.enabledBoxDecoration,
disabledBoxDecoration: disabledBoxDecoration ?? this.disabledBoxDecoration,
content: content ?? this.content,
icon: icon ?? this.icon,
);

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties..add(DiagnosticsProperty('content', content))
properties
..add(DiagnosticsProperty('enabledBoxDecoration', enabledBoxDecoration))
..add(DiagnosticsProperty('disabledBoxDecoration', disabledBoxDecoration));
..add(DiagnosticsProperty('disabledBoxDecoration', disabledBoxDecoration))
..add(DiagnosticsProperty('content', content))
..add(DiagnosticsProperty('icon', icon));
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FButtonStyle &&
runtimeType == other.runtimeType &&
enabledBoxDecoration == other.enabledBoxDecoration &&
disabledBoxDecoration == other.disabledBoxDecoration &&
content == other.content &&
icon == other.icon;

@override
int get hashCode => enabledBoxDecoration.hashCode ^ disabledBoxDecoration.hashCode ^ content.hashCode ^ icon.hashCode;
}

typedef _Data = ({FButtonStyle style, bool enabled});

class _InheritedData extends InheritedWidget {
final _Data data;

const _InheritedData({
required this.data,
required super.child,
});

@override
bool updateShouldNotify(covariant _InheritedData old) => data != old.data;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('style', data.style))
..add(FlagProperty('enabled', value: data.enabled, ifTrue: 'enabled'));
}
}
Loading
Loading