From dd6552e68d789df0e1d371339f518dd861f631e0 Mon Sep 17 00:00:00 2001 From: Ashley Venn Date: Tue, 30 Jun 2026 14:22:48 -0700 Subject: [PATCH] fix: pack visually colliding timeline cards --- .../lib/widgets/timeline/timeline_view.dart | 57 ++++++++++++++----- apps/focus_flow_flutter/test/widget_test.dart | 8 +++ 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/apps/focus_flow_flutter/lib/widgets/timeline/timeline_view.dart b/apps/focus_flow_flutter/lib/widgets/timeline/timeline_view.dart index 0d3537f..fe9ea4f 100644 --- a/apps/focus_flow_flutter/lib/widgets/timeline/timeline_view.dart +++ b/apps/focus_flow_flutter/lib/widgets/timeline/timeline_view.dart @@ -73,7 +73,10 @@ class _TimelineViewState extends State { minCardHeight: 88, ); final contentHeight = geometry.totalHeight + 24; - final placements = _TimelineCardPlacement.pack(widget.cards); + final placements = _TimelineCardPlacement.pack( + widget.cards, + geometry: geometry, + ); return LayoutBuilder( builder: (context, constraints) { _scheduleInitialScroll(geometry, constraints.maxHeight); @@ -195,7 +198,10 @@ class _TimelineCardPlacement { final int lane; final int laneCount; - static List<_TimelineCardPlacement> pack(List cards) { + static List<_TimelineCardPlacement> pack( + List cards, { + required TimelineGeometry geometry, + }) { if (cards.isEmpty) { return const []; } @@ -213,41 +219,50 @@ class _TimelineCardPlacement { }); final placements = <_TimelineCardPlacement>[]; var groupStart = 0; - var groupEnd = sorted.first.endMinutes; + var groupEnd = _visualEnd(sorted.first, geometry); for (var index = 1; index < sorted.length; index += 1) { final card = sorted[index]; - if (card.startMinutes < groupEnd) { - if (card.endMinutes > groupEnd) { - groupEnd = card.endMinutes; + final cardStart = _visualStart(card, geometry); + final cardEnd = _visualEnd(card, geometry); + if (cardStart < groupEnd) { + if (cardEnd > groupEnd) { + groupEnd = cardEnd; } continue; } - placements.addAll(_packGroup(sorted.sublist(groupStart, index))); + placements.addAll( + _packGroup(sorted.sublist(groupStart, index), geometry: geometry), + ); groupStart = index; - groupEnd = card.endMinutes; + groupEnd = cardEnd; } - placements.addAll(_packGroup(sorted.sublist(groupStart))); + placements.addAll( + _packGroup(sorted.sublist(groupStart), geometry: geometry), + ); return placements; } static List<_TimelineCardPlacement> _packGroup( - List group, - ) { - final laneEnds = []; + List group, { + required TimelineGeometry geometry, + }) { + final laneEnds = []; final laneByCard = {}; for (final card in group) { + final cardStart = _visualStart(card, geometry); + final cardEnd = _visualEnd(card, geometry); var assignedLane = -1; for (var lane = 0; lane < laneEnds.length; lane += 1) { - if (laneEnds[lane] <= card.startMinutes) { + if (laneEnds[lane] <= cardStart) { assignedLane = lane; break; } } if (assignedLane == -1) { assignedLane = laneEnds.length; - laneEnds.add(card.endMinutes); + laneEnds.add(cardEnd); } else { - laneEnds[assignedLane] = card.endMinutes; + laneEnds[assignedLane] = cardEnd; } 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) { final laneWidth = _laneWidth(trackWidth); final laneGap = _laneGapForWidth(trackWidth); diff --git a/apps/focus_flow_flutter/test/widget_test.dart b/apps/focus_flow_flutter/test/widget_test.dart index 146d61a..cc3b324 100644 --- a/apps/focus_flow_flutter/test/widget_test.dart +++ b/apps/focus_flow_flutter/test/widget_test.dart @@ -242,6 +242,12 @@ void main() { final waterPlants = tester.getRect( 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(waterPlants.left, closeTo(sortMail.left, 1)); @@ -249,6 +255,8 @@ void main() { expect(waterPlants.width, closeTo(sortMail.width, 1)); expect(sortMail.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', (