fix: pack visually colliding timeline cards

This commit is contained in:
Ashley Venn 2026-06-30 14:22:48 -07:00
parent acee1da264
commit dd6552e68d
2 changed files with 50 additions and 15 deletions

View file

@ -73,7 +73,10 @@ class _TimelineViewState extends State<TimelineView> {
minCardHeight: 88, minCardHeight: 88,
); );
final contentHeight = geometry.totalHeight + 24; final contentHeight = geometry.totalHeight + 24;
final placements = _TimelineCardPlacement.pack(widget.cards); final placements = _TimelineCardPlacement.pack(
widget.cards,
geometry: geometry,
);
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
_scheduleInitialScroll(geometry, constraints.maxHeight); _scheduleInitialScroll(geometry, constraints.maxHeight);
@ -195,7 +198,10 @@ class _TimelineCardPlacement {
final int lane; final int lane;
final int laneCount; final int laneCount;
static List<_TimelineCardPlacement> pack(List<TimelineCardModel> cards) { static List<_TimelineCardPlacement> pack(
List<TimelineCardModel> cards, {
required TimelineGeometry geometry,
}) {
if (cards.isEmpty) { if (cards.isEmpty) {
return const []; return const [];
} }
@ -213,41 +219,50 @@ class _TimelineCardPlacement {
}); });
final placements = <_TimelineCardPlacement>[]; final placements = <_TimelineCardPlacement>[];
var groupStart = 0; var groupStart = 0;
var groupEnd = sorted.first.endMinutes; var groupEnd = _visualEnd(sorted.first, geometry);
for (var index = 1; index < sorted.length; index += 1) { for (var index = 1; index < sorted.length; index += 1) {
final card = sorted[index]; final card = sorted[index];
if (card.startMinutes < groupEnd) { final cardStart = _visualStart(card, geometry);
if (card.endMinutes > groupEnd) { final cardEnd = _visualEnd(card, geometry);
groupEnd = card.endMinutes; if (cardStart < groupEnd) {
if (cardEnd > groupEnd) {
groupEnd = cardEnd;
} }
continue; continue;
} }
placements.addAll(_packGroup(sorted.sublist(groupStart, index))); placements.addAll(
_packGroup(sorted.sublist(groupStart, index), geometry: geometry),
);
groupStart = index; groupStart = index;
groupEnd = card.endMinutes; groupEnd = cardEnd;
} }
placements.addAll(_packGroup(sorted.sublist(groupStart))); placements.addAll(
_packGroup(sorted.sublist(groupStart), geometry: geometry),
);
return placements; return placements;
} }
static List<_TimelineCardPlacement> _packGroup( static List<_TimelineCardPlacement> _packGroup(
List<TimelineCardModel> group, List<TimelineCardModel> group, {
) { required TimelineGeometry geometry,
final laneEnds = <int>[]; }) {
final laneEnds = <double>[];
final laneByCard = <TimelineCardModel, int>{}; final laneByCard = <TimelineCardModel, int>{};
for (final card in group) { for (final card in group) {
final cardStart = _visualStart(card, geometry);
final cardEnd = _visualEnd(card, geometry);
var assignedLane = -1; var assignedLane = -1;
for (var lane = 0; lane < laneEnds.length; lane += 1) { for (var lane = 0; lane < laneEnds.length; lane += 1) {
if (laneEnds[lane] <= card.startMinutes) { if (laneEnds[lane] <= cardStart) {
assignedLane = lane; assignedLane = lane;
break; break;
} }
} }
if (assignedLane == -1) { if (assignedLane == -1) {
assignedLane = laneEnds.length; assignedLane = laneEnds.length;
laneEnds.add(card.endMinutes); laneEnds.add(cardEnd);
} else { } else {
laneEnds[assignedLane] = card.endMinutes; laneEnds[assignedLane] = cardEnd;
} }
laneByCard[card] = assignedLane; laneByCard[card] = assignedLane;
} }
@ -262,6 +277,18 @@ class _TimelineCardPlacement {
]; ];
} }
static double _visualStart(
TimelineCardModel card,
TimelineGeometry geometry,
) {
return geometry.yForMinutesSinceMidnight(card.startMinutes);
}
static double _visualEnd(TimelineCardModel card, TimelineGeometry geometry) {
final duration = card.endMinutes - card.startMinutes;
return _visualStart(card, geometry) + geometry.heightForDuration(duration);
}
double left(double trackWidth) { double left(double trackWidth) {
final laneWidth = _laneWidth(trackWidth); final laneWidth = _laneWidth(trackWidth);
final laneGap = _laneGapForWidth(trackWidth); final laneGap = _laneGapForWidth(trackWidth);

View file

@ -242,6 +242,12 @@ void main() {
final waterPlants = tester.getRect( final waterPlants = tester.getRect(
find.byKey(const ValueKey('water-plants')), find.byKey(const ValueKey('water-plants')),
); );
final cleanCoffee = tester.getRect(
find.byKey(const ValueKey('clean-coffee-maker')),
);
final cleanedKitchenEarly = tester.getRect(
find.byKey(const ValueKey('cleaned-kitchen-counter-early')),
);
expect(startLaundry.left, greaterThan(sortMail.left)); expect(startLaundry.left, greaterThan(sortMail.left));
expect(waterPlants.left, closeTo(sortMail.left, 1)); expect(waterPlants.left, closeTo(sortMail.left, 1));
@ -249,6 +255,8 @@ void main() {
expect(waterPlants.width, closeTo(sortMail.width, 1)); expect(waterPlants.width, closeTo(sortMail.width, 1));
expect(sortMail.right, lessThan(startLaundry.left)); expect(sortMail.right, lessThan(startLaundry.left));
expect(waterPlants.right, lessThan(startLaundry.left)); expect(waterPlants.right, lessThan(startLaundry.left));
expect(cleanedKitchenEarly.left, greaterThan(cleanCoffee.left));
expect(cleanCoffee.right, lessThan(cleanedKitchenEarly.left));
}); });
testWidgets('top bar controls scale inside a compact header width', ( testWidgets('top bar controls scale inside a compact header width', (