import 'dart:math'; import 'package:flutter/material.dart'; import 'profile_dropdown.dart'; /// Top header bar with room navigation tabs and profile dropdown. /// Features a circular "bulge" extending below the header for the logo. class TopHeaderBar extends StatelessWidget { const TopHeaderBar({ super.key, required this.selectedIndex, required this.onRoomSelected, }); final int selectedIndex; final ValueChanged onRoomSelected; // Header dimensions static const double _headerHeight = 56.0; static const double _logoSize = 120.0; static const double _logoMargin = 4.0; // Equal margin top/bottom within bubble static const double _logoCircleRadius = (_logoSize + _logoMargin * 2) / 2; // 64px static const double _bulgeFraction = 0.20; // 20% of circle below header line static const _rooms = [ _RoomDestination( icon: Icons.door_front_door_outlined, selectedIcon: Icons.door_front_door, label: 'Front Hall', ), _RoomDestination( icon: Icons.dns_outlined, selectedIcon: Icons.dns, label: 'Control Room', ), _RoomDestination( icon: Icons.lightbulb_outline, selectedIcon: Icons.lightbulb, label: 'Parlor', ), ]; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; // Calculate bulge: 20% of circle diameter extends below final bulgeExtension = _logoCircleRadius * 2 * _bulgeFraction; return SizedBox( height: _headerHeight + bulgeExtension, child: Stack( clipBehavior: Clip.none, children: [ // Main header bar with custom bottom shape Positioned.fill( child: CustomPaint( painter: _HeaderPainter( color: colorScheme.surface, borderColor: colorScheme.outlineVariant, circleRadius: _logoCircleRadius, bulgeFraction: _bulgeFraction, headerHeight: _headerHeight, circleLeftOffset: 16 + _logoCircleRadius, ), ), ), // Logo centered in the visible bubble area (from top of header to bottom of bulge) Positioned( left: 16 + _logoMargin, // Center logo in visible bubble: from y=0 to y=headerHeight+bulgeExtension top: ((_headerHeight + bulgeExtension) - _logoSize) / 2, child: Image.asset( 'assets/icons/logo.png', height: _logoSize, width: _logoSize, errorBuilder: (_, __, ___) => Icon( Icons.layers, size: 32, color: colorScheme.primary, ), ), ), // Header content (room tabs and profile) Positioned( top: 0, left: 0, right: 0, height: _headerHeight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ // Spacer for logo area SizedBox(width: _logoCircleRadius * 2 + 16), // Room tabs ..._buildRoomTabs(context), const Spacer(), // Profile dropdown const ProfileDropdown(), ], ), ), ), ], ), ); } List _buildRoomTabs(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return List.generate(_rooms.length, (index) { final room = _rooms[index]; final isSelected = index == selectedIndex; return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Tooltip( message: room.label, child: IconButton( icon: Icon( isSelected ? room.selectedIcon : room.icon, color: isSelected ? colorScheme.primary : colorScheme.onSurface, ), onPressed: () => onRoomSelected(index), style: IconButton.styleFrom( backgroundColor: isSelected ? colorScheme.primaryContainer : null, ), ), ), ); }); } } /// Custom painter for the header with a circular bulge for the logo. class _HeaderPainter extends CustomPainter { _HeaderPainter({ required this.color, required this.borderColor, required this.circleRadius, required this.bulgeFraction, required this.headerHeight, required this.circleLeftOffset, }); final Color color; final Color borderColor; final double circleRadius; final double bulgeFraction; // Fraction of circle below header (0.0 to 0.5) final double headerHeight; final double circleLeftOffset; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.fill; final borderPaint = Paint() ..color = borderColor ..style = PaintingStyle.stroke ..strokeWidth = 1; // Calculate circle center position // bulgeFraction of the diameter should be below the header line // bulgeAmount = bulgeFraction * 2 * radius (fraction of diameter) final bulgeAmount = bulgeFraction * 2 * circleRadius; // Center is positioned so that (radius - centerOffset) = bulgeAmount // centerOffset = radius - bulgeAmount final centerY = headerHeight - circleRadius + bulgeAmount; final circleCenter = Offset(circleLeftOffset, centerY); // Calculate the angle where circle intersects header line // At y = headerHeight: distance from center = headerHeight - centerY final distFromCenter = headerHeight - centerY; // cos(angle) = distFromCenter / radius final cosAngle = distFromCenter / circleRadius; final angle = acos(cosAngle.clamp(-1.0, 1.0)); // Arc starts at (π/2 - angle) and ends at (π/2 + angle) // In Flutter, 0 is at 3 o'clock, π/2 is at 6 o'clock final startAngle = (3.14159 / 2) - angle; final sweepAngle = angle * 2; // Calculate where arc intersects header line final halfChord = circleRadius * sin(angle); final arcLeft = circleLeftOffset - halfChord; final arcRight = circleLeftOffset + halfChord; // Create path for header with bulge final path = Path(); // Start from top-left path.moveTo(0, 0); // Top edge path.lineTo(size.width, 0); // Right edge down to header height path.lineTo(size.width, headerHeight); // Bottom edge - going right to left with curved bulge path.lineTo(arcRight, headerHeight); // Arc for the bulge final arcRect = Rect.fromCircle(center: circleCenter, radius: circleRadius); path.arcTo(arcRect, startAngle, sweepAngle, false); // Continue to left edge path.lineTo(0, headerHeight); // Close path path.close(); // Draw filled shape canvas.drawPath(path, paint); // Draw border along bottom edge only (with bulge) final borderPath = Path(); borderPath.moveTo(0, headerHeight); borderPath.lineTo(arcLeft, headerHeight); borderPath.arcTo(arcRect, 3.14159 - startAngle, -sweepAngle, false); borderPath.lineTo(size.width, headerHeight); canvas.drawPath(borderPath, borderPaint); } @override bool shouldRepaint(covariant _HeaderPainter oldDelegate) { return color != oldDelegate.color || borderColor != oldDelegate.borderColor || circleRadius != oldDelegate.circleRadius || bulgeFraction != oldDelegate.bulgeFraction || headerHeight != oldDelegate.headerHeight || circleLeftOffset != oldDelegate.circleLeftOffset; } } class _RoomDestination { const _RoomDestination({ required this.icon, required this.selectedIcon, required this.label, }); final IconData icon; final IconData selectedIcon; final String label; }