PraxaLing / mobile /lib /theme /widgets /brutal_button.dart
Reubencf's picture
Deploy PraxaLing: component refactor, bug fixes, unified neo-brutal design, Qwen3.5 everywhere
2992997
Raw
History Blame Contribute Delete
2.71 kB
import 'package:flutter/material.dart';
import '../app_theme.dart';
/// Neo-brutal button. Black 2px border, hard 4px black offset shadow,
/// collapses to (0,0) shadow on press while the body translates (4,4) —
/// matches the web `active:translate-x-[1px] translate-y-[1px]` behavior
/// scaled up for touch.
class BrutalButton extends StatefulWidget {
const BrutalButton({
super.key,
required this.onPressed,
required this.child,
this.color = kBrutalTeal,
this.foregroundColor = kBrutalWhite,
this.padding = const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
this.radius = kBrutalRadius,
this.expand = false,
this.enabled = true,
});
final VoidCallback? onPressed;
final Widget child;
final Color color;
final Color foregroundColor;
final EdgeInsetsGeometry padding;
final double radius;
final bool expand;
final bool enabled;
@override
State<BrutalButton> createState() => _BrutalButtonState();
}
class _BrutalButtonState extends State<BrutalButton> {
bool _pressed = false;
void _setPressed(bool v) {
if (!widget.enabled) return;
if (_pressed == v) return;
setState(() => _pressed = v);
}
@override
Widget build(BuildContext context) {
final disabled = !widget.enabled || widget.onPressed == null;
final offset = _pressed ? 0.0 : 4.0;
final btn = AnimatedContainer(
duration: const Duration(milliseconds: 80),
curve: Curves.easeOut,
padding: widget.padding,
transform: Matrix4.translationValues(_pressed ? 4 : 0, _pressed ? 4 : 0, 0),
decoration: BoxDecoration(
color: disabled ? kBrutalWhite : widget.color,
borderRadius: BorderRadius.circular(widget.radius),
border: Border.all(color: kBrutalBlack, width: 2),
boxShadow: [
BoxShadow(
color: kBrutalBlack,
offset: Offset(offset, offset),
blurRadius: 0,
),
],
),
child: DefaultTextStyle.merge(
style: brutalFont(
color: disabled ? kBrutalMuted : widget.foregroundColor,
fontWeight: FontWeight.w900,
fontSize: 15,
),
textAlign: TextAlign.center,
child: IconTheme.merge(
data: IconThemeData(color: disabled ? kBrutalMuted : widget.foregroundColor, size: 20),
child: widget.child,
),
),
);
return GestureDetector(
onTapDown: (_) => _setPressed(true),
onTapUp: (_) => _setPressed(false),
onTapCancel: () => _setPressed(false),
onTap: disabled ? null : widget.onPressed,
child: widget.expand
? SizedBox(width: double.infinity, child: btn)
: btn,
);
}
}