Files
tasq/lib/screens/network_map/widgets/topology_legend.dart
T

246 lines
7.6 KiB
Dart

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<TopologyLegend> createState() => _TopologyLegendState();
}
class _TopologyLegendState extends State<TopologyLegend>
with SingleTickerProviderStateMixin {
bool _expanded = false;
late final AnimationController _anim;
late final Animation<double> _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,
),
),
],
),
);
}
}