Skip to content

Instantly share code, notes, and snippets.

@lukepighetti
Last active January 22, 2025 21:09
Show Gist options
  • Save lukepighetti/b4d224c92c86a99456ab0f825965ba5c to your computer and use it in GitHub Desktop.
Save lukepighetti/b4d224c92c86a99456ab0f825965ba5c to your computer and use it in GitHub Desktop.
/// style_widget concept
/// decorators without the namespace clutter
/// i sympathize with people who don't want decorators to complexify flutter codebases
/// i also sympathize with the benefits of decorator syntax
/// i don't think just whitelabeling all widgets as decorators is a good idea
/// this attempts to contain the decorator syntax to a single widget while enjoying
/// the benefits and resolving some nits from the way flutter styling works today
/// some improvements
/// - opacity of 0 disables taps
/// - FontWeight works on variable weight fonts
/// - implicit animations with linked duration, curve, and delay
/// - foregroundColor that applies to text and icons
///
/// NOTE: some have pointed out this is a lot like jetpack compose modifiers
///
/// demo https://dartpad.dev/b4d224c92c86a99456ab0f825965ba5c
/// package https://pub.dev/packages/style_widget
/// inspiration https://x.com/luke_pighetti/status/1882101453571154076
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var visible = true;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'styled_widget',
home: Material(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Switch.adaptive(
value: visible,
onChanged: (x) => setState(() => visible = x),
),
SizedBox(
height: 200,
child: Center(
child: Placeholder(
child: StyleWidget(
style: (s) => s
.marginAll()
.paddingAll()
.backgroundColor(Colors.black)
.foregroundColor(Colors.white)
.boxShadow(const BoxShadow(blurRadius: 8))
.iconSize(24)
.borderRadius(8)
.animated(delay: const Duration(milliseconds: 500))
.collapseY(!visible)
.collapseX(!visible)
.opacity(visible ? 1 : 0)
.translate(y: visible ? 0 : 20)
.rotate(visible ? 0 : 45)
.fontSize(20)
.fontFamily('courier')
.fontWeight(FontWeight.bold)
.rotateAlignment(Alignment.center),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Hello"),
const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check),
Text("Subtitle"),
],
),
FilledButton(
child: const Text("Continue"),
onPressed: () {
print("boom");
},
)
],
),
),
),
),
),
],
),
),
),
);
}
}
//////////////////////////////////////////////////
class StyleWidget extends StatefulWidget {
const StyleWidget({
super.key,
required this.style,
required this.child,
});
final void Function(StyleContext s) style;
final Widget child;
@override
State<StyleWidget> createState() => _StyleWidgetState();
}
class _StyleWidgetState extends State<StyleWidget> {
@override
Widget build(BuildContext context) {
final s = StyleContext();
widget.style(s);
return TweenAnimationBuilder(
duration: s._duration,
curve: s._curve,
tween: Tween(end: s._rotate),
builder: (_, t, child) {
return Transform.rotate(
angle: t,
alignment: s._rotateAlignment,
child: child,
);
},
child: TweenAnimationBuilder(
tween: Tween(end: s._translation),
duration: s._duration,
curve: s._curve,
builder: (_, t, child) {
return Transform.translate(offset: t, child: child);
},
child: AnimatedOpacity(
opacity: s._opacity,
duration: s._duration,
curve: s._curve,
child: ClipRect(
child: AnimatedAlign(
alignment: Alignment.center,
widthFactor: s._collapseY ? 0 : 1,
heightFactor: s._collapseX ? 0 : 1,
duration: s._duration,
curve: s._curve,
child: ClipRect(
child: AnimatedContainer(
padding: s._padding,
margin: s._margin,
duration: s._duration,
curve: s._curve,
decoration: BoxDecoration(
color: s._backgroundColor,
borderRadius: BorderRadius.circular(s._borderRadius ?? 0),
boxShadow: s._boxShadows,
),
child: IgnorePointer(
ignoring: s._opacity <= 0.01,
child: DefaultTextStyle(
style: DefaultTextStyle.of(context)
.style
.merge(s._textStyle)
.copyWith(
color: s._foregroundColor,
),
child: IconTheme(
data: IconTheme.of(context).copyWith(
color: s._foregroundColor,
size: s._iconSize,
),
child: widget.child,
),
),
),
),
),
),
),
),
),
);
}
}
class StyleContext {
Duration _duration = Duration.zero;
Curve _curve = Curves.easeInOut;
StyleContext animated({
final Duration delay = Duration.zero,
final Duration duration = const Duration(milliseconds: 500),
final Curve curve = Curves.easeInOut,
}) {
_assertUseOnce(_duration == Duration.zero, "animated()");
final (d, c) = _delayedCurve(delay, duration, curve);
_duration = d;
_curve = c;
return this;
}
double _opacity = 1;
StyleContext opacity(double x) {
_opacity = x;
return this;
}
Offset _translation = Offset.zero;
StyleContext translate({double x = 0, double y = 0}) {
_translation = _translation + Offset(x, y);
return this;
}
double _rotate = 0;
StyleContext rotate(double degrees) {
_rotate = degrees * pi / 180;
return this;
}
StyleContext rotateRadians(double radians) {
_rotate = radians;
return this;
}
bool _collapseY = false;
StyleContext collapseY(bool value) {
_collapseY = value;
return this;
}
bool _collapseX = false;
StyleContext collapseX(bool value) {
_collapseX = value;
return this;
}
AlignmentGeometry _rotateAlignment = Alignment.center;
@visibleForTesting
// isn't behaving the way I'd expect it to
StyleContext rotateAlignment(AlignmentGeometry x) {
_rotateAlignment = x;
return this;
}
EdgeInsetsGeometry _padding = EdgeInsets.zero;
StyleContext paddingAll([double value = 10]) {
_padding = _padding.add(EdgeInsetsDirectional.all(value));
return this;
}
StyleContext padding(
{double s = 0, double t = 0, double e = 0, double b = 0}) {
_padding = _padding.add(EdgeInsetsDirectional.fromSTEB(s, t, e, b));
return this;
}
StyleContext paddingLTRB(
{double l = 0, double t = 0, double r = 0, double b = 0}) {
_padding = _padding.add(EdgeInsets.fromLTRB(l, t, r, b));
return this;
}
StyleContext paddingXY({double x = 0, double y = 0}) {
_padding = _padding
.add(EdgeInsetsDirectional.symmetric(horizontal: x, vertical: y));
return this;
}
EdgeInsetsGeometry _margin = EdgeInsets.zero;
StyleContext marginAll([double value = 10]) {
_margin = _margin.add(EdgeInsetsDirectional.all(value));
return this;
}
StyleContext margin(
{double s = 0, double t = 0, double e = 0, double b = 0}) {
_margin = _margin.add(EdgeInsetsDirectional.fromSTEB(s, t, e, b));
return this;
}
StyleContext marginLTRB(
{double l = 0, double t = 0, double r = 0, double b = 0}) {
_margin = _margin.add(EdgeInsets.fromLTRB(l, t, r, b));
return this;
}
StyleContext marginXY({double x = 0, double y = 0}) {
_margin = _margin
.add(EdgeInsetsDirectional.symmetric(horizontal: x, vertical: y));
return this;
}
Color _backgroundColor = Colors.transparent;
StyleContext backgroundColor(Color color) {
_assertUseOnce(_backgroundColor == Colors.transparent, "backgroundColor()");
_backgroundColor = color;
return this;
}
Color? _foregroundColor;
StyleContext foregroundColor(Color color) {
_assertUseOnce(_foregroundColor == null, "foregroundColor()");
_foregroundColor = color;
return this;
}
double? _iconSize;
StyleContext iconSize(double size) {
_assertUseOnce(_iconSize == null, "iconSize()");
_iconSize = size;
return this;
}
double? _borderRadius;
StyleContext borderRadius(double radius) {
_assertUseOnce(_borderRadius == null, "borderRadius()");
_borderRadius = radius;
return this;
}
final List<BoxShadow> _boxShadows = [];
StyleContext boxShadow(BoxShadow shadow) {
_boxShadows.add(shadow);
return this;
}
TextStyle _textStyle = const TextStyle();
StyleContext textStyle(TextStyle style) {
_textStyle = _textStyle.merge(style);
return this;
}
StyleContext fontSize(double size) {
textStyle(TextStyle(fontSize: size));
return this;
}
StyleContext fontWeight(FontWeight weight) {
textStyle(
TextStyle(
fontWeight: weight,
fontVariations: [
FontVariation.weight(weight.value.toDouble()),
],
),
);
return this;
}
StyleContext fontFamily(String family) {
textStyle(TextStyle(fontFamily: family));
return this;
}
}
void _assertUseOnce(bool test, String tag) {
assert(test, "You can only use $tag once");
}
(Duration, Curve) _delayedCurve(
Duration delay, Duration duration, Curve curve) {
final total = delay + duration;
final tStart = delay.inMicroseconds / total.inMicroseconds;
return (total, Interval(tStart, 1, curve: curve));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment