import 'package:flutter/material.dart'; /// The full-screen "spotlight" shown by one step of the first-launch /// guided tour: a dimmed barrier with a cut-out hole around whatever /// [targetKey] currently points to (or, if that key isn't laid out yet or /// doesn't resolve, just a plain dimmed barrier with the card centered), /// plus a card explaining that element with Next/Skip controls. /// /// This widget is purely presentational — it doesn't know about tabs, /// routes, or persistence. `MainShell` is responsible for sequencing /// steps, navigating to the right screen/route before each one, and /// recording that the tour's been seen once it ends. class OnboardingTourOverlay extends StatelessWidget { final GlobalKey targetKey; final String title; final String description; final int stepNumber; final int totalSteps; final VoidCallback onNext; final VoidCallback onSkip; const OnboardingTourOverlay({ super.key, required this.targetKey, required this.title, required this.description, required this.stepNumber, required this.totalSteps, required this.onNext, required this.onSkip, }); Rect? _targetRect() { final renderObject = targetKey.currentContext?.findRenderObject(); if (renderObject is! RenderBox || !renderObject.hasSize) return null; return renderObject.localToGlobal(Offset.zero) & renderObject.size; } @override Widget build(BuildContext context) { final rect = _targetRect()?.inflate(6); final media = MediaQuery.of(context); final isLastStep = stepNumber >= totalSteps; final colorScheme = Theme.of(context).colorScheme; // Prefer placing the card below the highlighted element; only flip // above it if there's not enough room below on this screen. final cardBelow = rect == null || (rect.bottom + 260) < media.size.height; final cardTop = cardBelow ? (rect?.bottom ?? media.size.height * 0.38) + 16 : null; final cardBottom = !cardBelow ? media.size.height - rect.top + 16 : null; return Stack( children: [ Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.opaque, // Swallows every tap on the barrier (and the spotlight hole // itself) so the highlighted element can't be accidentally // triggered mid-tour — Next/Skip on the card are the only way // to advance. onTap: () {}, child: CustomPaint( painter: _SpotlightPainter( targetRect: rect, color: Colors.black.withValues(alpha: 0.75), ), ), ), ), if (rect != null) Positioned( left: rect.left, top: rect.top, width: rect.width, height: rect.height, child: IgnorePointer( child: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: colorScheme.primary, width: 2.5), borderRadius: BorderRadius.circular(12), ), ), ), ), Positioned( left: 20, right: 20, top: cardTop, bottom: cardBottom, child: SafeArea( child: Card( elevation: 8, child: Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Step $stepNumber of $totalSteps', style: Theme.of(context).textTheme.labelMedium?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), Text(title, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 8), Text(description, style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton(onPressed: onSkip, child: const Text('Skip Tour')), FilledButton(onPressed: onNext, child: Text(isLastStep ? 'Got It' : 'Next')), ], ), ], ), ), ), ), ), ], ); } } class _SpotlightPainter extends CustomPainter { final Rect? targetRect; final Color color; const _SpotlightPainter({required this.targetRect, required this.color}); @override void paint(Canvas canvas, Size size) { final barrierPaint = Paint()..color = color; final fullPath = Path()..addRect(Offset.zero & size); final rect = targetRect; if (rect == null) { canvas.drawPath(fullPath, barrierPaint); return; } final holePath = Path()..addRRect(RRect.fromRectAndRadius(rect, const Radius.circular(12))); canvas.drawPath(Path.combine(PathOperation.difference, fullPath, holePath), barrierPaint); } @override bool shouldRepaint(covariant _SpotlightPainter oldDelegate) => oldDelegate.targetRect != targetRect || oldDelegate.color != color; }