feat(ui): 레트로 UI 시스템 추가

- PressStart2P 픽셀 폰트 추가
- RetroColors: 레트로 RPG 스타일 색상 팔레트
- RetroPanel: 픽셀 테두리 패널 위젯
- RetroButton: 레트로 스타일 버튼
- RetroProgressBar: 픽셀 스타일 진행 바
- PixelBorderPainter: 커스텀 테두리 페인터
This commit is contained in:
JiWoong Sul
2025-12-30 18:30:51 +09:00
parent 2d797502a3
commit 708148c767
8 changed files with 795 additions and 1 deletions

View File

@@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/src/shared/retro_colors.dart';
/// 레트로 RPG 스타일 버튼
/// 8-bit 게임의 눌림 효과를 재현
class RetroButton extends StatefulWidget {
const RetroButton({
super.key,
required this.child,
this.onPressed,
this.isPrimary = true,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
});
/// 버튼 내부 컨텐츠
final Widget child;
/// 클릭 콜백 (null이면 비활성화)
final VoidCallback? onPressed;
/// Primary 버튼 여부 (색상 차이)
final bool isPrimary;
/// 내부 패딩
final EdgeInsets padding;
@override
State<RetroButton> createState() => _RetroButtonState();
}
class _RetroButtonState extends State<RetroButton> {
bool _isPressed = false;
bool get _isEnabled => widget.onPressed != null;
Color get _backgroundColor {
if (!_isEnabled) return RetroColors.buttonSecondary.withValues(alpha: 0.5);
if (_isPressed) {
return widget.isPrimary
? RetroColors.buttonPrimaryPressed
: RetroColors.buttonSecondaryPressed;
}
return widget.isPrimary
? RetroColors.buttonPrimary
: RetroColors.buttonSecondary;
}
Color get _borderTopLeft {
if (_isPressed) return RetroColors.panelBorderOuter;
return RetroColors.panelBorderInner;
}
Color get _borderBottomRight {
if (_isPressed) return RetroColors.panelBorderInner;
return RetroColors.panelBorderOuter;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _isEnabled ? (_) => setState(() => _isPressed = true) : null,
onTapUp: _isEnabled ? (_) => setState(() => _isPressed = false) : null,
onTapCancel: _isEnabled ? () => setState(() => _isPressed = false) : null,
onTap: widget.onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 50),
padding: widget.padding,
decoration: BoxDecoration(
color: _backgroundColor,
border: Border(
top: BorderSide(color: _borderTopLeft, width: 2),
left: BorderSide(color: _borderTopLeft, width: 2),
bottom: BorderSide(color: _borderBottomRight, width: 2),
right: BorderSide(color: _borderBottomRight, width: 2),
),
),
transform: _isPressed
? Matrix4.translationValues(1, 1, 0)
: Matrix4.identity(),
child: DefaultTextStyle(
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: _isEnabled ? RetroColors.textLight : RetroColors.textDisabled,
),
child: widget.child,
),
),
);
}
}
/// 레트로 텍스트 버튼 (간편 생성용)
class RetroTextButton extends StatelessWidget {
const RetroTextButton({
super.key,
required this.text,
this.onPressed,
this.isPrimary = true,
this.icon,
});
final String text;
final VoidCallback? onPressed;
final bool isPrimary;
final IconData? icon;
@override
Widget build(BuildContext context) {
return RetroButton(
onPressed: onPressed,
isPrimary: isPrimary,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 14, color: RetroColors.textLight),
const SizedBox(width: 8),
],
Text(text.toUpperCase()),
],
),
);
}
}
/// 레트로 아이콘 버튼
class RetroIconButton extends StatelessWidget {
const RetroIconButton({
super.key,
required this.icon,
this.onPressed,
this.size = 32,
});
final IconData icon;
final VoidCallback? onPressed;
final double size;
@override
Widget build(BuildContext context) {
return RetroButton(
onPressed: onPressed,
padding: EdgeInsets.all(size * 0.25),
child: Icon(icon, size: size * 0.5, color: RetroColors.textLight),
);
}
}