Roadbound-BRR/lib/widgets/trip_diagram.dart

280 lines
8.7 KiB
Dart

import "package:flutter/material.dart";
class TripDiagramEntry {
const TripDiagramEntry({required this.label, this.labelIcon, this.subtitle, this.time});
final String label;
final IconData? labelIcon;
final String? subtitle;
final String? time;
}
class TripDiagram extends StatelessWidget {
const TripDiagram({
required this.entries,
super.key,
this.leftOffset = 0,
this.rowHeight = 32,
this.lineWidth = 7,
this.lineColor,
this.highlightLastEntry = false,
this.labelOffset = 26,
this.rightPadding = 16,
this.oddRowColor,
this.terminalRowColor,
this.stationTextStyle,
this.timeTextStyle,
this.missingTimeTextStyle,
this.emptyMessage = "No stations found.",
this.emptyTextStyle,
});
final List<TripDiagramEntry> entries;
final double leftOffset;
final double rowHeight;
final double lineWidth;
final Color? lineColor;
final bool highlightLastEntry;
final double labelOffset;
final double rightPadding;
final Color? oddRowColor;
final Color? terminalRowColor;
final TextStyle? stationTextStyle;
final TextStyle? timeTextStyle;
final TextStyle? missingTimeTextStyle;
final String emptyMessage;
final TextStyle? emptyTextStyle;
@override
Widget build(BuildContext context) {
final resolvedLineColor = lineColor ?? Theme.of(context).colorScheme.primary;
final deduped = entries;
if (deduped.isEmpty) {
return Text(
emptyMessage,
style:
emptyTextStyle ??
TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
);
}
final totalHeight = deduped.length * rowHeight;
return Stack(
children: [
SizedBox(
height: totalHeight,
width: double.infinity,
child: CustomPaint(
painter: _TripDiagramPainter(
count: deduped.length,
rowHeight: rowHeight,
lineWidth: lineWidth,
lineColor: resolvedLineColor,
leftOffset: leftOffset,
),
),
),
Positioned.fill(
child: Column(
children: List.generate(deduped.length, (index) {
final entry = deduped[index];
final time = entry.time;
final isTerminalRow = highlightLastEntry && index == deduped.length - 1;
return Container(
height: rowHeight,
color: isTerminalRow
? (terminalRowColor ?? resolvedLineColor.withValues(alpha: 0.12))
: index.isOdd
? (oddRowColor ?? Colors.white.withValues(alpha: 0.03))
: null,
child: Padding(
padding: EdgeInsets.only(
left: leftOffset + labelOffset,
right: rightPadding,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Text(
entry.label,
style:
stationTextStyle ??
const TextStyle(
fontSize: 15,
color: Color(0xFFDDDDDD),
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
overflow: TextOverflow.ellipsis,
),
if (entry.labelIcon != null) ...[
const SizedBox(width: 4),
Icon(
entry.labelIcon,
size: 12,
color: const Color(0xFFBBBBBB),
),
]
],
),
if (entry.subtitle != null)
Text(
entry.subtitle!,
style:
stationTextStyle ??
const TextStyle(
fontSize: 11,
color: Color(0xFFDDDDDD),
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 12),
Text(
time ?? "--:--",
style: time != null
? (timeTextStyle ??
const TextStyle(
fontSize: 15,
fontFamily: "monospace",
fontWeight: FontWeight.w700,
color: Color(0xFFEEEEEE),
))
: (missingTimeTextStyle ??
const TextStyle(
fontSize: 15,
fontFamily: "monospace",
fontWeight: FontWeight.w700,
color: Color(0xFF555555),
)),
),
],
),
),
);
}),
),
),
],
);
}
}
class _TripDiagramPainter extends CustomPainter {
const _TripDiagramPainter({
required this.count,
required this.rowHeight,
required this.lineWidth,
required this.lineColor,
this.leftOffset = 0,
});
final int count;
final double rowHeight;
final double lineWidth;
final Color lineColor;
final double leftOffset;
static const double _linePad = 4.0;
static const double _blobPad = 4.0;
static const double _centerSize = 4.0;
static const double _cornerFactor = 0.3;
@override
void paint(Canvas canvas, Size size) {
if (count <= 0) return;
final strokeWidth = lineWidth / 2.0;
final ringSize = _centerSize + strokeWidth;
final paddingWidth = strokeWidth + _blobPad;
final centerX = leftOffset + ringSize / 2 + paddingWidth / 2 + 2;
final firstY = rowHeight / 2;
final lastY = (count - 1) * rowHeight + rowHeight / 2;
final nodeRects = List.generate(count, (index) {
final centerY = index * rowHeight + rowHeight / 2;
final rect = Rect.fromCenter(
center: Offset(centerX, centerY),
width: ringSize,
height: ringSize,
);
return RRect.fromRectAndRadius(
rect,
Radius.circular(ringSize * _cornerFactor),
);
});
for (final rect in nodeRects) {
canvas.drawRRect(
rect,
Paint()
..color = Colors.white
..strokeWidth = paddingWidth
..style = PaintingStyle.stroke,
);
}
canvas.drawLine(
Offset(centerX, firstY),
Offset(centerX, lastY),
Paint()
..color = Colors.white
..strokeWidth = lineWidth + _linePad
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
);
canvas.drawLine(
Offset(centerX, firstY),
Offset(centerX, lastY),
Paint()
..color = lineColor
..strokeWidth = lineWidth
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke,
);
for (final rect in nodeRects) {
final centerRect = rect.deflate(strokeWidth / 2);
canvas.drawRRect(
centerRect,
Paint()
..color = Colors.white
..style = PaintingStyle.fill,
);
canvas.drawRRect(
rect,
Paint()
..color = lineColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke,
);
}
}
@override
bool shouldRepaint(covariant _TripDiagramPainter oldDelegate) {
return oldDelegate.count != count ||
oldDelegate.rowHeight != rowHeight ||
oldDelegate.lineWidth != lineWidth ||
oldDelegate.lineColor != lineColor ||
oldDelegate.leftOffset != leftOffset;
}
}