fluffychat/lib/pangea/choreographer/igc/segmented_circular_progress.dart
wcjord 473ffbaf24
docs: writing assistance redesign design spec (#5655) (#5696)
* "docs: writing assistance redesign design spec (#5655)

Add comprehensive design doc for the WA redesign:
- AssistanceRing replaces StartIGCButton (segmented ring around Pangea icon)
- Background highlights with category colors (not red/orange error tones)
- Simplified match lifecycle: open → viewed → accepted (no ignore)
- Persistent span card with smooth transitions between matches
- Send always available, no gate on unresolved matches

Remove superseded design docs (SPAN_CARD_REDESIGN_FINALIZED.md,
SPAN_CARD_REDESIGN_Q_AND_A.md, choreographer.instructions.md)."

* feat: replace ignored status with viewed status, initial updates to span card

* resolve merge conflicts

* rebuild input bar on active match update to fix span hightlighting

* cleanup

* allow opening span cards for closed matches

* no gate on sending, update underline colors

* animate span card transitions

* initial updates to add segmented IGC progress ring

* update segment colors / opacities based on match statuses

* use same widget for igc loading and fetched

* more segment animation changes

* fix scrolling and wrap in span card

* better disabled color

* close span card on assistance state change

* remove print statements

* update design doc

* cleanup

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
2026-02-25 13:07:53 -05:00

105 lines
2.6 KiB
Dart

import 'dart:math';
import 'package:flutter/material.dart';
class SegmentedCircularProgress extends StatelessWidget {
final List<Segment> segments;
final double strokeWidth;
final Widget? child;
const SegmentedCircularProgress({
super.key,
required this.segments,
this.strokeWidth = 4,
this.child,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _SegmentedPainter(segments: segments, strokeWidth: strokeWidth),
child: child,
);
}
}
class Segment {
final double value; // relative value
final Color color;
final double opacity;
Segment(this.value, this.color, {this.opacity = 1.0});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Segment &&
runtimeType == other.runtimeType &&
value == other.value &&
color == other.color &&
opacity == other.opacity;
@override
int get hashCode => value.hashCode ^ color.hashCode ^ opacity.hashCode;
}
class _SegmentedPainter extends CustomPainter {
final List<Segment> segments;
final double strokeWidth;
final double gapFactor = 1.4;
const _SegmentedPainter({required this.segments, this.strokeWidth = 10});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..isAntiAlias = true;
final rect = Offset.zero & size;
final arcRect = rect.deflate(strokeWidth / 2);
final radius = arcRect.width / 2;
final center = arcRect.center;
if (segments.isEmpty) return;
if (segments.length == 1) {
final segment = segments.first;
paint.color = segment.color.withAlpha((segment.opacity * 255).ceil());
canvas.drawCircle(center, radius, paint);
return;
}
final total = segments.fold<double>(0, (sum, s) => sum + s.value);
paint.strokeCap = StrokeCap.round;
final baseCapAngle = strokeWidth / radius;
final capAngle = baseCapAngle * gapFactor;
double startAngle = -pi / 2;
for (final segment in segments) {
final rawSweep = (segment.value / total) * 2 * pi;
final sweep = rawSweep - capAngle;
if (sweep <= 0) {
startAngle += rawSweep;
continue;
}
paint.color = segment.color.withAlpha((segment.opacity * 255).ceil());
canvas.drawArc(arcRect, startAngle + capAngle / 2, sweep, false, paint);
startAngle += rawSweep;
}
}
@override
bool shouldRepaint(covariant _SegmentedPainter oldDelegate) {
return oldDelegate.segments != segments ||
oldDelegate.strokeWidth != strokeWidth;
}
}