always set lane

This commit is contained in:
2026-02-25 20:00:43 +01:00
parent ea22da9e5a
commit f3b645ac53
13 changed files with 325 additions and 102 deletions

View File

@@ -12,7 +12,10 @@ import 'timeline_view.dart';
/// A drop target wrapper for a timeline group.
///
/// Wraps group lanes content and handles drag-and-drop operations.
/// Wraps the entire group column (header + lanes) and handles drag-and-drop
/// operations. The [verticalOffset] accounts for the header height and padding
/// so that lane calculations are correct relative to the lanes stack.
///
/// The ghost overlay is rendered by the parent widget in the same Stack.
class GroupDropTarget extends StatelessWidget {
const GroupDropTarget({
@@ -24,6 +27,7 @@ class GroupDropTarget extends StatelessWidget {
required this.laneHeight,
required this.lanesCount,
required this.onEntryMoved,
required this.verticalOffset,
required this.child,
super.key,
});
@@ -36,6 +40,11 @@ class GroupDropTarget extends StatelessWidget {
final double laneHeight;
final int lanesCount;
final OnEntryMoved? onEntryMoved;
/// The vertical offset from the top of this widget to the top of the lanes
/// stack. This accounts for the group header height and any padding.
final double verticalOffset;
final Widget child;
@override
@@ -65,8 +74,12 @@ class GroupDropTarget extends StatelessWidget {
viewport.end,
);
// Subtract header + padding offset so Y is relative to the lanes stack.
// When the cursor is over the header, adjustedY is negative and clamps
// to lane 1.
final adjustedY = local.dy - verticalOffset;
final rawLane = LayoutCoordinateService.yToLane(
y: local.dy,
y: adjustedY,
laneHeight: laneHeight,
);
final maxAllowedLane = (lanesCount <= 0 ? 1 : lanesCount) + 1;

View File

@@ -84,7 +84,7 @@ class ZTimelineView extends StatelessWidget {
projected[group.id] ?? const <ProjectedEntry>[];
final lanesCount = _countLanes(groupEntries);
return Column(
final column = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_GroupHeader(title: group.title, height: groupHeaderHeight),
@@ -98,11 +98,31 @@ class ZTimelineView extends StatelessWidget {
colorBuilder: colorBuilder,
labelBuilder: labelBuilder,
contentWidth: contentWidth,
onEntryMoved: onEntryMoved,
enableDrag: enableDrag,
),
],
);
// Wrap the entire group (header + lanes) in a DragTarget
// so dragging over headers still updates the ghost position.
if (enableDrag && onEntryMoved != null) {
return GroupDropTarget(
group: group,
entries: groupEntries,
allEntries: entries,
viewport: viewport,
contentWidth: contentWidth,
laneHeight: laneHeight,
lanesCount: lanesCount,
onEntryMoved: onEntryMoved,
verticalOffset:
groupHeaderHeight +
ZTimelineConstants.verticalOuterPadding,
child: column,
);
}
return column;
},
);
},
@@ -157,7 +177,6 @@ class _GroupLanes extends StatelessWidget {
required this.labelBuilder,
required this.colorBuilder,
required this.contentWidth,
required this.onEntryMoved,
required this.enableDrag,
});
@@ -170,7 +189,6 @@ class _GroupLanes extends StatelessWidget {
final EntryLabelBuilder labelBuilder;
final EntryColorBuilder colorBuilder;
final double contentWidth;
final OnEntryMoved? onEntryMoved;
final bool enableDrag;
@override
@@ -264,21 +282,6 @@ class _GroupLanes extends StatelessWidget {
),
);
// Wrap with DragTarget if drag is enabled
if (enableDrag && onEntryMoved != null) {
innerStack = GroupDropTarget(
group: group,
entries: entries,
allEntries: allEntries,
viewport: viewport,
contentWidth: contentWidth,
laneHeight: laneHeight,
lanesCount: lanesCount,
onEntryMoved: onEntryMoved,
child: innerStack,
);
}
return AnimatedSize(
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,