style: ui perfection

This commit is contained in:
2026-06-24 09:09:00 +03:30
parent 108a972761
commit 08937a66cb
4 changed files with 536 additions and 172 deletions
@@ -9,12 +9,10 @@ import 'package:flutter/material.dart';
import '../../../../../core/service/app_sounds.dart';
import '../../../domain/entities/game_entities.dart';
import '../../bloc/game_bloc.dart';
import 'badges.dart';
import 'card_codes.dart';
import 'card_component.dart';
import 'shuffle_animation.dart';
import 'table_pieces.dart';
import 'turn_timer.dart';
/// صحنه‌ی میز حکم با انیمیشن. شما همیشه پایین میز (rel=0) هستید.
/// کامپوننت‌های دست و trick ماندگارند و با افکت حرکت جابه‌جا می‌شوند.
@@ -38,7 +36,6 @@ class HokmGame extends FlameGame {
final Map<String, CardComponent> _trick = {};
final List<Component> _backs = [];
final List<Component> _info = [];
final Map<String, Sprite> _badge = {}; // نشان‌های رتبه (پیش‌بارگذاری)
// ابعادِ کارت‌های روی میز (بر اساس عرض صفحه محاسبه می‌شود).
double _cardW = 60;
@@ -72,17 +69,11 @@ class HokmGame extends FlameGame {
HokmGame(this.cubit, {this.skin = 'simple'});
@override
Color backgroundColor() => const Color(0xFF2C0A10);
Color backgroundColor() => const Color(0xFF0C3A1B);
@override
Future<void> onLoad() async {
add(Felt());
// پیش‌بارگذاریِ نشان‌های رتبه (اگر فایلی نبود، بی‌نشان رد می‌شود).
for (final t in const ['bronze', 'silver', 'gold', 'diamond', 'king']) {
try {
_badge[t] = await loadSprite('badge/$t.png');
} catch (_) {}
}
_sub = cubit.stream.listen((ui) {
if (ui.state == null) return;
final s = ui.state!;
@@ -534,8 +525,8 @@ class HokmGame extends FlameGame {
_addBacks(_rel(seat), count);
}
_addTricksWon(s);
_addScorePucks(s);
_addInfo(s);
// امتیاز/نام/نشان/تاج/شمارنده‌ی نوبت اکنون در اوورلیِ Flutter (TableHud) کشیده
// می‌شوند تا متن واضح‌تر و چیدمان نزدیک به تصویرِ مرجع باشد.
}
// شمارنده ۱: دست‌های برده‌ی این هَند (tricks_won) — دسته‌کارتِ پشت‌رو + عدد.
@@ -561,24 +552,6 @@ class HokmGame extends FlameGame {
}
}
// شمارنده ۲: امتیاز بازی (scores = هندهای برده) — دیسکِ هر تیم، ۰ تا ۷.
// یکی جلوی شما (پایین)، دیگری جلوی حریف (راست) — نه جلوی یارِ بالا.
void _addScorePucks(GameState s) {
final radius = size.x * 0.05;
for (var team = 0; team < 2; team++) {
final score = team < s.scores.length ? s.scores[team] : 0;
final mine = team == s.yourSeat % 2;
final pos =
mine
? Vector2(size.x * 0.50, size.y * 0.74)
: Vector2(size.x * 0.84, size.y * 0.44);
final puck = ScorePuck(score, radius: radius, position: pos)
..priority = 22;
_info.add(puck);
add(puck);
}
}
// پشت‌کارت‌های یک حریف به‌صورت بادبزنی.
void _addBacks(int rel, int count) {
if (count <= 0) return;
@@ -605,99 +578,6 @@ class HokmGame extends FlameGame {
}
}
void _addInfo(GameState s) {
if (s.trump != null) {
final (sym, col) = _trumpGlyph(s.trump!);
_addLabel(
'حکم: $sym',
Vector2(size.x * 0.04, size.y * 0.04),
size.x * 0.05,
color: col,
anchor: Anchor.topLeft,
bold: true,
);
}
final a = s.scores.isNotEmpty ? s.scores[0] : 0;
final b = s.scores.length > 1 ? s.scores[1] : 0;
_addLabel(
'$a - $b',
Vector2(size.x / 2, size.y * 0.04),
size.x * 0.05,
anchor: Anchor.topCenter,
);
for (final p in s.players) {
final pos = _seatLabelPos(_rel(p.seat));
final isTurn = s.turn == p.seat;
_addLabel(
'${p.name}${p.bot ? ' (ربات)' : ''}${p.connected ? '' : ''}',
pos,
size.x * 0.035,
color: isTurn ? const Color(0xFFE9B949) : Colors.white70,
bold: isTurn,
);
// نشانِ رتبه (راستِ نام) و سکه (چپِ نام). جایگاهِ پایین روی همان خطِ نام،
// بقیه کمی زیرِ نام تا خارج از صفحه نرود.
if (!p.bot) {
final isBottom = pos.y > size.y * 0.75;
final y = isBottom ? pos.y : pos.y + size.y * 0.032;
// سکه: چپِ نام (متن به سمتِ چپ کشیده می‌شود).
_addLabel(
'${p.coins}',
Vector2(pos.x - size.x * 0.06, y),
size.x * 0.034,
color: const Color(0xFFE9B949),
anchor: Anchor.centerRight,
);
// نشانِ رتبه: راستِ نام.
final spr = _badge[p.rankTier];
if (spr != null) {
final b = SpriteComponent(
sprite: spr,
anchor: Anchor.centerLeft,
size: Vector2.all(size.x * 0.06),
position: Vector2(pos.x + size.x * 0.07, y),
)..priority = 22;
_info.add(b);
add(b);
}
}
// تاجِ حاکم (روی همه‌ی فازها نشان داده می‌شود تا حاکم مشخص باشد).
// y را داخلِ صفحه نگه می‌داریم؛ برای بازیکنِ بالا، تاج زیرِ نام می‌نشیند.
if (s.hakem == p.seat) {
final above = pos.y > size.y * 0.5; // پایین/کنار ⇒ تاج بالای نام
final cy = above
? pos.y - size.y * 0.066
: pos.y + size.y * 0.05; // بازیکنِ بالا ⇒ تاج پایینِ نام
final crown = CrownBadge(size.x * 0.07, Vector2(pos.x, cy))
..priority = 25;
_info.add(crown);
add(crown);
}
if (isTurn) {
final tpos = pos - Vector2(0, size.y * 0.03);
final human = !p.bot && p.connected;
// حلقه‌ی شمارشِ نوبت برای بازیکنِ انسان. اگر سرور مهلت را نفرستد،
// پیش‌فرضِ ۳۰ ثانیه استفاده می‌شود تا شمارنده همیشه نمایش داده شود.
if (human && (s.phase == 'playing' || s.phase == 'choose_trump')) {
final dur = s.turnTimeoutMs > 0 ? s.turnTimeoutMs / 1000.0 : 30.0;
final timer = TurnTimer(
radius: size.x * 0.045,
durationSeconds: dur,
position: tpos,
)..priority = 20;
_info.add(timer);
add(timer);
}
// ستاره‌ی نوبت (مطابقِ تصویرِ مرجع) با ضربان.
final star = StarBadge(size.x * 0.015, tpos)..priority = 24;
star.add(pulse());
_info.add(star);
add(star);
}
}
}
// ===== کمکی‌های ساختِ عنصر (ثبت در فهرستِ بازساخته‌شونده) =====
void _addBack(Vector2 pos, Vector2 sz, {required int priority}) {
@@ -740,19 +620,6 @@ class HokmGame extends FlameGame {
add(t);
}
Vector2 _seatLabelPos(int rel) {
switch (rel) {
case 1:
return Vector2(size.x * 0.07, size.y * 0.30);
case 2:
return Vector2(size.x / 2, size.y * 0.07);
case 3:
return Vector2(size.x * 0.93, size.y * 0.30);
default:
return Vector2(size.x / 2, size.y * 0.975);
}
}
(String, Color) _trumpGlyph(String suit) {
switch (suit) {
case 'hearts':
@@ -7,49 +7,72 @@ import 'package:flutter/material.dart';
const _gold = Color(0xFFE9B949);
const _goldDark = Color(0xFFB8860B);
/// نمدِ سبزِ بیضی‌شکلِ میز با عمق: لبه‌ی برجسته، گرادیانِ شعاعی و وینیت.
/// میز: زمینِ سبزِ نمدی در کلِ صفحه + یک «میزِ» مرکزیِ سرمهای با قابِ طلاییِ
/// دولایه (مطابقِ تصویرِ مرجع). کارت‌های زمین داخلِ ناحیه‌ی سرمه‌ای انداخته می‌شوند
/// و پشت‌کارتِ حریفان روی نمدِ سبزِ دورِ آن می‌نشیند.
class Felt extends PositionComponent with HasGameReference {
@override
void render(Canvas canvas) {
final w = game.size.x, h = game.size.y;
final center = Offset(w / 2, h / 2);
final rect = Rect.fromCenter(center: center, width: w * 0.86, height: h * 0.62);
final radius = Radius.circular(h * 0.3);
final felt = RRect.fromRectAndRadius(rect, radius);
// ۱) لبه‌ی چوبیِ بیرونی (ریل) با گرادیان — حسِ برجستگی بدون drawShadowِ هر-فریمی.
final rail = RRect.fromRectAndRadius(
rect.inflate(w * 0.03), Radius.circular(h * 0.33));
// ۱) زمینِ سبزِ نمدی (کلِ صفحه) با وینیتِ شعاعی برای عمق.
canvas.drawRect(
Rect.fromLTWH(0, 0, w, h),
Paint()
..shader = RadialGradient(
center: Alignment.center,
radius: 1.0,
colors: const [Color(0xFF1C7A3E), Color(0xFF0C3A1B)],
stops: const [0.5, 1.0],
).createShader(Rect.fromLTWH(0, 0, w, h)),
);
// ۲) میزِ مرکزیِ سرمه‌ای با قابِ طلایی.
final rect = Rect.fromCenter(
center: center, width: w * 0.72, height: h * 0.46);
final radius = Radius.circular(h * 0.05);
final table = RRect.fromRectAndRadius(rect, radius);
// سایه‌ی نرمِ زیرِ میز (یک‌بار، نه هر-فریمی سنگین).
canvas.drawRRect(
rail,
table.shift(const Offset(0, 6)),
Paint()
..color = const Color(0x66000000)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12),
);
// قابِ طلاییِ بیرونی.
canvas.drawRRect(
RRect.fromRectAndRadius(rect.inflate(w * 0.012), radius),
Paint()
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF5A2E12), Color(0xFF2E1608)],
).createShader(rail.outerRect),
colors: [_gold, _goldDark],
).createShader(rect),
);
// ۲) حلقه‌ی طلایی بین ریل و نمد.
// سطحِ سرمه‌ایِ میز با گرادیانِ شعاعی (مرکز روشن‌تر).
canvas.drawRRect(
RRect.fromRectAndRadius(rect.inflate(w * 0.008), radius),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = w * 0.012
..color = _gold,
);
// ۳) نمدِ سبز با گرادیانِ شعاعی (مرکز روشن، لبه‌ها تیره) — خودش حسِ وینیت/عمق می‌دهد.
canvas.drawRRect(
felt,
table,
Paint()
..shader = RadialGradient(
center: Alignment.center,
radius: 0.95,
colors: const [Color(0xFF34A03F), Color(0xFF0E3614)],
stops: const [0.45, 1.0],
radius: 0.9,
colors: const [Color(0xFF153A63), Color(0xFF07172E)],
stops: const [0.4, 1.0],
).createShader(rect),
);
// خطِ طلاییِ نازکِ داخلی (قابِ دولایه).
canvas.drawRRect(
RRect.fromRectAndRadius(rect.deflate(w * 0.018), Radius.circular(h * 0.04)),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = w * 0.004
..color = _gold.withValues(alpha: 0.65),
);
}
}
@@ -0,0 +1,473 @@
import 'package:flutter/material.dart';
import 'package:random_avatar/random_avatar.dart';
import '../../../../core/network/ws_client.dart';
import '../../domain/entities/game_entities.dart';
/// اوورلیِ روی صحنه‌ی Flame: آواتارِ بازیکنان با قابِ طلایی، نام، نشانِ رتبه،
/// شمارنده‌ی نوبت، تاجِ حاکم و تعدادِ کارت — به‌علاوه‌ی پنلِ امتیاز/حکم (بالا-چپ)،
/// نشانگرِ اتصال و دکمه‌ی خروج (بالا-راست) و دکمه‌ی گفتگو (پایین-راست).
/// همه‌ی چیدمان نسبت به اندازه‌ی صفحه است تا با کارت‌های Flame هم‌تراز بماند.
class TableHud extends StatelessWidget {
final GameState s;
final WsStatus connection;
final VoidCallback onExit;
final VoidCallback onChat;
const TableHud({
super.key,
required this.s,
required this.connection,
required this.onExit,
required this.onChat,
});
// مکانِ آواتارِ هر جایگاهِ نسبی (۰=پایین/شما، ۱=چپ، ۲=بالا، ۳=راست).
static const _spots = {
0: Alignment(0, 1.0),
1: Alignment(-0.94, -0.08),
2: Alignment(0, -0.92),
3: Alignment(0.94, -0.08),
};
int _rel(int seat) => (seat - s.yourSeat + 4) % 4;
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, c) {
final w = c.maxWidth;
final avatarD = (w * 0.17).clamp(48.0, 84.0);
return Stack(
children: [
for (final p in s.players)
Align(
alignment: _spots[_rel(p.seat)]!,
child: Padding(
padding: EdgeInsets.only(
bottom: _rel(p.seat) == 0 ? 2 : 0,
top: _rel(p.seat) == 2 ? 6 : 0,
),
child: _PlayerSeat(
player: p,
diameter: avatarD,
isTurn: s.turn == p.seat,
isHakem: s.hakem == p.seat,
nameAbove: _rel(p.seat) == 2,
cardCount:
p.seat < s.handCounts.length ? s.handCounts[p.seat] : 0,
turnToken: '${s.turn}|${s.trick.length}|${s.phase}',
turnSeconds: s.turnTimeoutMs > 0 ? s.turnTimeoutMs / 1000 : 30,
showRing: s.phase == 'playing' || s.phase == 'choose_trump',
),
),
),
Positioned(top: 8, left: 8, child: SafeArea(child: _scorePanel())),
Positioned(
top: 8,
right: 8,
child: SafeArea(child: _topRight(context)),
),
Positioned(
bottom: 16,
right: 12,
child: _RoundBtn(
icon: Icons.chat_bubble,
color: const Color(0xFF1E5FA8),
onTap: onChat,
),
),
],
);
});
}
// پنلِ امتیاز (ما/حریف) + چیپِ حکم. امتیازِ تیمِ شما = scores[yourSeat%2].
Widget _scorePanel() {
final myTeam = s.yourSeat % 2;
final mine = myTeam < s.scores.length ? s.scores[myTeam] : 0;
final opp = (1 - myTeam) < s.scores.length ? s.scores[1 - myTeam] : 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: _panelDeco(),
child: Row(
children: [
_scoreCol('ما', mine),
Container(
width: 1,
height: 30,
margin: const EdgeInsets.symmetric(horizontal: 12),
color: const Color(0x33E9B949),
),
_scoreCol('حریف', opp),
],
),
),
const SizedBox(height: 6),
_hokmChip(),
],
);
}
Widget _scoreCol(String label, int v) => Column(
children: [
Text(label,
style: const TextStyle(color: Color(0xFFE9B949), fontSize: 12)),
Text('$v',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold)),
],
);
Widget _hokmChip() {
final (glyph, color) = _trumpGlyph(s.trump);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: _panelDeco(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('حکم',
style: TextStyle(
color: Color(0xFFE9B949),
fontSize: 13,
fontWeight: FontWeight.bold)),
const SizedBox(width: 6),
Text(glyph, style: TextStyle(color: color, fontSize: 18)),
],
),
);
}
Widget _topRight(BuildContext context) {
final ok = connection == WsStatus.connected;
return Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: _panelDeco(),
child: Row(
children: [
Icon(ok ? Icons.wifi : Icons.wifi_off,
color: ok ? const Color(0xFF4CD964) : Colors.redAccent,
size: 16),
const SizedBox(width: 5),
Text(ok ? 'آنلاین' : 'قطع',
style: TextStyle(
color: ok ? const Color(0xFF4CD964) : Colors.redAccent,
fontSize: 12,
fontWeight: FontWeight.bold)),
],
),
),
const SizedBox(width: 8),
_RoundBtn(
icon: Icons.logout,
color: const Color(0xFFB81D2A),
onTap: onExit,
size: 40,
),
],
);
}
static BoxDecoration _panelDeco() => BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF2A2018), Color(0xFF14100B)],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFB8860B), width: 1.4),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)],
);
(String, Color) _trumpGlyph(String? suit) {
switch (suit) {
case 'hearts':
return ('', const Color(0xFFE53935));
case 'diamonds':
return ('', const Color(0xFFE53935));
case 'clubs':
return ('', Colors.white);
case 'spades':
return ('', Colors.white);
default:
return ('', Colors.white54);
}
}
}
class _PlayerSeat extends StatelessWidget {
final GamePlayer player;
final double diameter;
final bool isTurn;
final bool isHakem;
final bool nameAbove;
final int cardCount;
final String turnToken;
final double turnSeconds;
final bool showRing;
const _PlayerSeat({
required this.player,
required this.diameter,
required this.isTurn,
required this.isHakem,
required this.nameAbove,
required this.cardCount,
required this.turnToken,
required this.turnSeconds,
required this.showRing,
});
@override
Widget build(BuildContext context) {
final ringW = diameter * 0.08;
final name = Container(
constraints: BoxConstraints(maxWidth: diameter * 1.7),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xCC14100B),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isTurn ? const Color(0xFFE9B949) : const Color(0x55B8860B)),
),
child: Text(
player.bot ? '${player.name} (ربات)' : player.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: isTurn ? const Color(0xFFE9B949) : Colors.white,
fontSize: diameter * 0.18,
fontWeight: FontWeight.bold,
),
),
);
final avatar = SizedBox(
width: diameter,
height: diameter,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
// قابِ طلایی + آواتار.
Container(
width: diameter,
height: diameter,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFF3D27A), Color(0xFFB8860B)],
),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)],
),
padding: EdgeInsets.all(ringW),
child: ClipOval(
child: Container(
color: const Color(0xFF1A2740),
child: player.bot
? Icon(Icons.smart_toy,
color: Colors.white70, size: diameter * 0.5)
: RandomAvatar(player.name.isEmpty ? '?' : player.name),
),
),
),
// حلقه‌ی شمارشِ نوبت (روی قاب).
if (isTurn && showRing)
Positioned.fill(
child: _TurnRing(
token: turnToken,
seconds: turnSeconds,
stroke: ringW * 1.1,
),
),
// تاجِ حاکم بالای آواتار.
if (isHakem)
Positioned(
top: -diameter * 0.32,
child: Icon(Icons.workspace_premium,
color: const Color(0xFFFFC107), size: diameter * 0.42),
),
// حبابِ تعدادِ کارت (پایین-راست).
if (cardCount > 0)
Positioned(
right: -diameter * 0.04,
bottom: -diameter * 0.04,
child: Container(
padding: EdgeInsets.all(diameter * 0.06),
constraints: BoxConstraints(
minWidth: diameter * 0.34, minHeight: diameter * 0.34),
decoration: BoxDecoration(
color: const Color(0xFF0E2A12),
shape: BoxShape.circle,
border: Border.all(color: const Color(0xFFE9B949), width: 1.4),
),
alignment: Alignment.center,
child: Text('$cardCount',
style: TextStyle(
color: Colors.white,
fontSize: diameter * 0.18,
fontWeight: FontWeight.bold)),
),
),
],
),
);
// نشانِ رتبه‌ی فشرده (تصویر) کنارِ آواتار.
final badge = (!player.bot && player.rankTier.isNotEmpty)
? Image.asset(
'assets/images/badge/${player.rankTier}.png',
width: diameter * 0.34,
height: diameter * 0.34,
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
)
: const SizedBox.shrink();
final nameRow = Row(
mainAxisSize: MainAxisSize.min,
children: [
badge,
if (!player.bot && player.rankTier.isNotEmpty) const SizedBox(width: 4),
Flexible(child: name),
if (!player.bot && player.coins > 0) ...[
const SizedBox(width: 4),
Text('${player.coins}',
style: TextStyle(
color: const Color(0xFFE9B949), fontSize: diameter * 0.16)),
],
],
);
return Column(
mainAxisSize: MainAxisSize.min,
children: nameAbove
? [nameRow, SizedBox(height: diameter * 0.06), avatar]
: [avatar, SizedBox(height: diameter * 0.06), nameRow],
);
}
}
/// حلقه‌ی شمارشِ معکوسِ نوبت: از پر به خالی طیِ `seconds`؛ نزدیکِ پایان قرمز
/// می‌شود. با تغییرِ `token` (نوبتِ جدید) از نو شروع می‌شود.
class _TurnRing extends StatefulWidget {
final String token;
final double seconds;
final double stroke;
const _TurnRing(
{required this.token, required this.seconds, required this.stroke});
@override
State<_TurnRing> createState() => _TurnRingState();
}
class _TurnRingState extends State<_TurnRing>
with SingleTickerProviderStateMixin {
late final AnimationController _c;
@override
void initState() {
super.initState();
_c = AnimationController(
vsync: this, duration: Duration(milliseconds: (widget.seconds * 1000).round()))
..forward();
}
@override
void didUpdateWidget(covariant _TurnRing old) {
super.didUpdateWidget(old);
if (old.token != widget.token) {
_c.duration = Duration(milliseconds: (widget.seconds * 1000).round());
_c
..reset()
..forward();
}
}
@override
void dispose() {
_c.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _c,
builder: (_, __) {
final left = (1 - _c.value).clamp(0.0, 1.0);
return CustomPaint(
painter: _RingPainter(left: left, stroke: widget.stroke),
);
},
);
}
}
class _RingPainter extends CustomPainter {
final double left; // ۱..۰
final double stroke;
_RingPainter({required this.left, required this.stroke});
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
final center = rect.center;
final radius = (size.shortestSide - stroke) / 2;
const start = -1.5708; // بالا
final sweep = 6.28318 * left;
final color = left < 0.3 ? const Color(0xFFE53935) : const Color(0xFF4CD964);
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
start,
sweep,
false,
Paint()
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeWidth = stroke
..color = color,
);
}
@override
bool shouldRepaint(covariant _RingPainter old) =>
old.left != left || old.stroke != stroke;
}
class _RoundBtn extends StatelessWidget {
final IconData icon;
final Color color;
final VoidCallback onTap;
final double size;
const _RoundBtn(
{required this.icon,
required this.color,
required this.onTap,
this.size = 46});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: const Color(0xFFE9B949), width: 1.6),
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 6)],
),
child: Icon(icon, color: Colors.white, size: size * 0.5),
),
);
}
}