import 'package:flutter/material.dart'; import '../../../models/network/network_device.dart'; import '../../../models/network/network_link.dart'; /// Collapsible legend overlay explaining device role border colours and link /// type colours. Positioned at the bottom-left of the topology canvas view. class TopologyLegend extends StatefulWidget { const TopologyLegend({super.key}); @override State createState() => _TopologyLegendState(); } class _TopologyLegendState extends State with SingleTickerProviderStateMixin { bool _expanded = false; late final AnimationController _anim; late final Animation _fade; @override void initState() { super.initState(); _anim = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), ); _fade = CurvedAnimation(parent: _anim, curve: Curves.easeOut); } @override void dispose() { _anim.dispose(); super.dispose(); } void _toggle() { setState(() => _expanded = !_expanded); if (_expanded) { _anim.forward(); } else { _anim.reverse(); } } Color _roleColor(ColorScheme cs, NetworkDeviceRole role) { switch (role) { case NetworkDeviceRole.core: return cs.error; case NetworkDeviceRole.distribution: return cs.tertiary; case NetworkDeviceRole.access: return cs.primary; case NetworkDeviceRole.edge: return cs.secondary; case NetworkDeviceRole.endpoint: return cs.outline; } } Color _linkColor(ColorScheme cs, NetworkLinkKind kind) { switch (kind) { case NetworkLinkKind.copper: return cs.primary; case NetworkLinkKind.fiber: return cs.tertiary; case NetworkLinkKind.wireless: return cs.secondary; case NetworkLinkKind.virtual: return cs.outline; case NetworkLinkKind.unknown: return cs.outline; } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final tt = Theme.of(context).textTheme; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Expand/collapse toggle chip Material( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(20), elevation: 2, child: InkWell( onTap: _toggle, borderRadius: BorderRadius.circular(20), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.legend_toggle_outlined, size: 16, color: cs.onSurfaceVariant, ), const SizedBox(width: 6), Text( 'Legend', style: tt.labelSmall?.copyWith( color: cs.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 4), AnimatedRotation( turns: _expanded ? 0.5 : 0, duration: const Duration(milliseconds: 200), child: Icon( Icons.expand_more, size: 16, color: cs.onSurfaceVariant, ), ), ], ), ), ), ), // Expandable panel FadeTransition( opacity: _fade, child: SizeTransition( sizeFactor: _fade, axisAlignment: -1, child: Padding( padding: const EdgeInsets.only(top: 6), child: Material( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), elevation: 3, child: Padding( padding: const EdgeInsets.all(12), child: SizedBox( width: 200, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // ── Device Roles ───────────────────────────── Text( 'Device Roles', style: tt.labelSmall?.copyWith( color: cs.onSurfaceVariant, fontWeight: FontWeight.w700, letterSpacing: 0.8, ), ), const SizedBox(height: 6), for (final role in NetworkDeviceRole.values) _LegendRow( color: _roleColor(cs, role), label: role.label, isLine: false, ), const SizedBox(height: 10), Divider( height: 1, color: cs.outlineVariant.withValues(alpha: 0.5)), const SizedBox(height: 10), // ── Link Types ─────────────────────────────── Text( 'Link Types', style: tt.labelSmall?.copyWith( color: cs.onSurfaceVariant, fontWeight: FontWeight.w700, letterSpacing: 0.8, ), ), const SizedBox(height: 6), for (final kind in NetworkLinkKind.values) _LegendRow( color: _linkColor(cs, kind), label: kind.label, isLine: true, ), ], ), ), ), ), ), ), ), ], ); } } class _LegendRow extends StatelessWidget { const _LegendRow({ required this.color, required this.label, required this.isLine, }); final Color color; final String label; final bool isLine; @override Widget build(BuildContext context) { final tt = Theme.of(context).textTheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( children: [ if (isLine) // Coloured line segment for link types Container( width: 20, height: 3, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), ), ) else // Coloured square for device roles (border indicator) Container( width: 12, height: 12, decoration: BoxDecoration( border: Border.all(color: color, width: 2), borderRadius: BorderRadius.circular(3), ), ), const SizedBox(width: 8), Text( label, style: tt.labelSmall?.copyWith( color: Theme.of(context).colorScheme.onSurface, ), ), ], ), ); } }