Share the drag controller's piece building, settling, column flip and slot snap
This commit is contained in:
@@ -390,17 +390,20 @@ internal fun resizedSpan(
|
|||||||
}
|
}
|
||||||
if (wrongWay) return startMin to endMin
|
if (wrongWay) return startMin to endMin
|
||||||
}
|
}
|
||||||
val snapped = (edgeMin / DRAG_SNAP_MINUTES).roundToInt() * DRAG_SNAP_MINUTES
|
val snapped = snapToSlot(edgeMin)
|
||||||
return when (edge) {
|
return when (edge) {
|
||||||
ResizeEdge.Top -> minOf(snapped, endMin - DRAG_SNAP_MINUTES) to endMin
|
ResizeEdge.Top -> minOf(snapped, endMin - DRAG_SNAP_MINUTES) to endMin
|
||||||
ResizeEdge.Bottom -> startMin to maxOf(snapped, startMin + DRAG_SNAP_MINUTES)
|
ResizeEdge.Bottom -> startMin to maxOf(snapped, startMin + DRAG_SNAP_MINUTES)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** [minutes] rounded to the nearest [DRAG_SNAP_MINUTES] slot. */
|
||||||
|
internal fun snapToSlot(minutes: Float): Int = (minutes / DRAG_SNAP_MINUTES).roundToInt() * DRAG_SNAP_MINUTES
|
||||||
|
|
||||||
/** [minutes] snapped, kept at [held] until three quarters of a step away. */
|
/** [minutes] snapped, kept at [held] until three quarters of a step away. */
|
||||||
internal fun stickySnap(minutes: Float, held: Int?): Int {
|
internal fun stickySnap(minutes: Float, held: Int?): Int {
|
||||||
if (held != null && kotlin.math.abs(minutes - held) < DRAG_SNAP_MINUTES * 0.75f) return held
|
if (held != null && kotlin.math.abs(minutes - held) < DRAG_SNAP_MINUTES * 0.75f) return held
|
||||||
return (minutes / DRAG_SNAP_MINUTES).roundToInt() * DRAG_SNAP_MINUTES
|
return snapToSlot(minutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** How far past its column's side the finger has to go before a resize changes day. */
|
/** How far past its column's side the finger has to go before a resize changes day. */
|
||||||
@@ -766,6 +769,12 @@ class TimelineDragController {
|
|||||||
if (landed == null || landed.slot == origin) return null
|
if (landed == null || landed.slot == origin) return null
|
||||||
// A draft dropped on a day off screen would vanish, so it stays where it was (#153).
|
// A draft dropped on a day off screen would vanish, so it stays where it was (#153).
|
||||||
if (landed.edgeOnly && landed.event.isDraft) return null
|
if (landed.edgeOnly && landed.event.isDraft) return null
|
||||||
|
settle(landed)
|
||||||
|
return TimelineDrop(landed.event, landed.date, landed.startMin, clipOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keep [landed]'s copy on the target as [settling] until [release]; [end] is a resize's. */
|
||||||
|
private fun settle(landed: TimelineDrag, end: Instant? = null) {
|
||||||
settlingSelected = selection?.event?.instanceId == landed.event.instanceId
|
settlingSelected = selection?.event?.instanceId == landed.event.instanceId
|
||||||
// An edge piece is a hint for the finger, not a preview of the landing
|
// An edge piece is a hint for the finger, not a preview of the landing
|
||||||
// slot: the event belongs to a day this timeline has no column for, so
|
// slot: the event belongs to a day this timeline has no column for, so
|
||||||
@@ -775,8 +784,8 @@ class TimelineDragController {
|
|||||||
liftedInstanceId = landed.event.instanceId
|
liftedInstanceId = landed.event.instanceId
|
||||||
settledOrigin = landed.event.eventId to landed.event.start
|
settledOrigin = landed.event.eventId to landed.event.start
|
||||||
settledAbandonedMark = abandonedTicks?.value ?: 0
|
settledAbandonedMark = abandonedTicks?.value ?: 0
|
||||||
|
settledEnd = end
|
||||||
drops++
|
drops++
|
||||||
return TimelineDrop(landed.event, landed.date, landed.startMin, clipOffset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -807,7 +816,7 @@ class TimelineDragController {
|
|||||||
val edgeDay = if (edge == ResizeEdge.Top) 0 else (resizeEndMin - 1).floorDiv(MINUTES_PER_DAY).coerceAtLeast(0)
|
val edgeDay = if (edge == ResizeEdge.Top) 0 else (resizeEndMin - 1).floorDiv(MINUTES_PER_DAY).coerceAtLeast(0)
|
||||||
val edgeIndex = geometry.days.indexOf(span.day.plus(edgeDay, DateTimeUnit.DAY))
|
val edgeIndex = geometry.days.indexOf(span.day.plus(edgeDay, DateTimeUnit.DAY))
|
||||||
resizeColumn = edgeIndex.takeIf { it >= 0 }
|
resizeColumn = edgeIndex.takeIf { it >= 0 }
|
||||||
?.let { if (geometry.isRtl) geometry.days.lastIndex - it else it }
|
?.let { column(it) }
|
||||||
resizeGrabMin = (pointerResizeMinutes() ?: 0f) -
|
resizeGrabMin = (pointerResizeMinutes() ?: 0f) -
|
||||||
if (edge == ResizeEdge.Top) resizeStartMin else resizeEndMin
|
if (edge == ResizeEdge.Top) resizeStartMin else resizeEndMin
|
||||||
originSlot = null
|
originSlot = null
|
||||||
@@ -845,13 +854,7 @@ class TimelineDragController {
|
|||||||
cancel()
|
cancel()
|
||||||
if (landed == null || landed.slot == origin) return null
|
if (landed == null || landed.slot == origin) return null
|
||||||
val drop = resizedDrop(landed.event, edge, landed.date, landed.eventStartMin, landed.eventSpanMin, zone)
|
val drop = resizedDrop(landed.event, edge, landed.date, landed.eventStartMin, landed.eventSpanMin, zone)
|
||||||
settlingSelected = selection?.event?.instanceId == landed.event.instanceId
|
settle(landed, end = drop.end)
|
||||||
settling = if (landed.edgeOnly) landed.copy(pieces = emptyList()) else landed
|
|
||||||
liftedInstanceId = landed.event.instanceId
|
|
||||||
settledOrigin = landed.event.eventId to landed.event.start
|
|
||||||
settledAbandonedMark = abandonedTicks?.value ?: 0
|
|
||||||
settledEnd = drop.end
|
|
||||||
drops++
|
|
||||||
return drop
|
return drop
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,14 +870,13 @@ class TimelineDragController {
|
|||||||
val days = geometry.days
|
val days = geometry.days
|
||||||
if (hourPx <= 0f || columnPx <= 0f || days.isEmpty()) return null
|
if (hourPx <= 0f || columnPx <= 0f || days.isEmpty()) return null
|
||||||
val origin = grid.positionInRoot()
|
val origin = grid.positionInRoot()
|
||||||
val column = stickyColumn(
|
val held = stickyColumn(
|
||||||
position = (pointer.x - origin.x) / columnPx,
|
position = (pointer.x - origin.x) / columnPx,
|
||||||
held = resizeColumn,
|
held = resizeColumn,
|
||||||
lastIndex = days.lastIndex,
|
lastIndex = days.lastIndex,
|
||||||
)
|
)
|
||||||
resizeColumn = column
|
resizeColumn = held
|
||||||
val dayIndex = if (geometry.isRtl) days.lastIndex - column else column
|
val dayOffset = (days[column(held)].toEpochDays() - day.toEpochDays()).toInt()
|
||||||
val dayOffset = (days[dayIndex].toEpochDays() - day.toEpochDays()).toInt()
|
|
||||||
resizeEdgeDay = dayOffset
|
resizeEdgeDay = dayOffset
|
||||||
return dayOffset * MINUTES_PER_DAY + (pointer.y - origin.y) / hourPx * 60f
|
return dayOffset * MINUTES_PER_DAY + (pointer.y - origin.y) / hourPx * 60f
|
||||||
}
|
}
|
||||||
@@ -887,8 +889,6 @@ class TimelineDragController {
|
|||||||
resizeSlot = slot
|
resizeSlot = slot
|
||||||
val (start, end) = resizedSpan(edge, resizeStartMin, resizeEndMin, slot.toFloat(), resizeEdgeDay)
|
val (start, end) = resizedSpan(edge, resizeStartMin, resizeEndMin, slot.toFloat(), resizeEdgeDay)
|
||||||
val days = geometry.days
|
val days = geometry.days
|
||||||
val hourPx = geometry.hourPx
|
|
||||||
val columnPx = geometry.columnWidthPx
|
|
||||||
val origin = geometry.grid?.takeIf { it.isAttached }?.positionInRoot() ?: return
|
val origin = geometry.grid?.takeIf { it.isAttached }?.positionInRoot() ?: return
|
||||||
// Which column the start day sits in, possibly off either end of the view.
|
// Which column the start day sits in, possibly off either end of the view.
|
||||||
val startIndex = (day.toEpochDays() - days.first().toEpochDays()).toInt()
|
val startIndex = (day.toEpochDays() - days.first().toEpochDays()).toInt()
|
||||||
@@ -901,20 +901,7 @@ class TimelineDragController {
|
|||||||
eventStartMin = start,
|
eventStartMin = start,
|
||||||
eventSpanMin = end - start,
|
eventSpanMin = end - start,
|
||||||
titleLines = titleLines,
|
titleLines = titleLines,
|
||||||
pieces = slices.map { slice ->
|
pieces = pieces(slices, startIndex, origin),
|
||||||
val dayIndex = startIndex + slice.dayOffset
|
|
||||||
val col = if (geometry.isRtl) days.lastIndex - dayIndex else dayIndex
|
|
||||||
val bounds = dragPieceBounds(slice, hourPx, geometry.blockInsetPx)
|
|
||||||
TimelineDragPiece(
|
|
||||||
topLeftInRoot = Offset(x = origin.x + col * columnPx, y = origin.y + bounds.top),
|
|
||||||
sizePx = IntSize(
|
|
||||||
(columnPx - geometry.columnGapPx).roundToInt(),
|
|
||||||
bounds.height.roundToInt(),
|
|
||||||
),
|
|
||||||
continuesBefore = slice.continuesBefore,
|
|
||||||
continuesAfter = slice.continuesAfter,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
edgeOnly = slices.isEmpty(),
|
edgeOnly = slices.isEmpty(),
|
||||||
markerMin = if (edge == ResizeEdge.Top) {
|
markerMin = if (edge == ResizeEdge.Top) {
|
||||||
start.mod(MINUTES_PER_DAY)
|
start.mod(MINUTES_PER_DAY)
|
||||||
@@ -972,10 +959,9 @@ class TimelineDragController {
|
|||||||
|
|
||||||
val origin = grid.positionInRoot()
|
val origin = grid.positionInRoot()
|
||||||
val rawMinutes = (pointer.y - grab.y - origin.y) / hourPx * 60f
|
val rawMinutes = (pointer.y - grab.y - origin.y) / hourPx * 60f
|
||||||
val snapped = (rawMinutes / DRAG_SNAP_MINUTES).roundToInt() * DRAG_SNAP_MINUTES
|
val snapped = snapToSlot(rawMinutes)
|
||||||
val zone = dragZone ?: return
|
val zone = dragZone ?: return
|
||||||
val column = ((pointer.x - origin.x) / columnPx).toInt().coerceIn(0, days.lastIndex)
|
val dayIndex = column(((pointer.x - origin.x) / columnPx).toInt().coerceIn(0, days.lastIndex))
|
||||||
val dayIndex = if (geometry.isRtl) days.lastIndex - column else column
|
|
||||||
val target = days[dayIndex]
|
val target = days[dayIndex]
|
||||||
val keep = dragKept
|
val keep = dragKept
|
||||||
// Measured where it would land: the drop keeps the real duration, which in
|
// Measured where it would land: the drop keeps the real duration, which in
|
||||||
@@ -1003,28 +989,34 @@ class TimelineDragController {
|
|||||||
titleLines = titleLines,
|
titleLines = titleLines,
|
||||||
// Bounded to the columns this timeline actually shows: a day it
|
// Bounded to the columns this timeline actually shows: a day it
|
||||||
// doesn't has no piece to draw. The write is unaffected.
|
// doesn't has no piece to draw. The write is unaffected.
|
||||||
pieces = slices
|
pieces = pieces(slices, dayIndex, origin),
|
||||||
.map { slice ->
|
|
||||||
val day = dayIndex + slice.dayOffset
|
|
||||||
val col = if (geometry.isRtl) days.lastIndex - day else day
|
|
||||||
val bounds = dragPieceBounds(slice, hourPx, geometry.blockInsetPx)
|
|
||||||
TimelineDragPiece(
|
|
||||||
topLeftInRoot = Offset(
|
|
||||||
x = origin.x + col * columnPx,
|
|
||||||
y = origin.y + bounds.top,
|
|
||||||
),
|
|
||||||
sizePx = IntSize(
|
|
||||||
(columnPx - geometry.columnGapPx).roundToInt(),
|
|
||||||
bounds.height.roundToInt(),
|
|
||||||
),
|
|
||||||
continuesBefore = slice.continuesBefore,
|
|
||||||
continuesAfter = slice.continuesAfter,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
edgeOnly = onGrid.isEmpty(),
|
edgeOnly = onGrid.isEmpty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** [slices] as floating pieces, the first slice's day being [dayIndex] in the shown days. */
|
||||||
|
private fun pieces(slices: List<DragSlice>, dayIndex: Int, origin: Offset): List<TimelineDragPiece> {
|
||||||
|
val columnPx = geometry.columnWidthPx
|
||||||
|
return slices.map { slice ->
|
||||||
|
val bounds = dragPieceBounds(slice, geometry.hourPx, geometry.blockInsetPx)
|
||||||
|
TimelineDragPiece(
|
||||||
|
topLeftInRoot = Offset(
|
||||||
|
x = origin.x + column(dayIndex + slice.dayOffset) * columnPx,
|
||||||
|
y = origin.y + bounds.top,
|
||||||
|
),
|
||||||
|
sizePx = IntSize(
|
||||||
|
(columnPx - geometry.columnGapPx).roundToInt(),
|
||||||
|
bounds.height.roundToInt(),
|
||||||
|
),
|
||||||
|
continuesBefore = slice.continuesBefore,
|
||||||
|
continuesAfter = slice.continuesAfter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The on-screen column of a day index, and back: right-to-left lays the days out mirrored. */
|
||||||
|
private fun column(index: Int): Int = if (geometry.isRtl) geometry.days.lastIndex - index else index
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hold the scroll for the whole drag and nudge it once per frame while the
|
* Hold the scroll for the whole drag and nudge it once per frame while the
|
||||||
* finger sits near an edge. One `scroll` call, since a loop of
|
* finger sits near an edge. One `scroll` call, since a loop of
|
||||||
|
|||||||
Reference in New Issue
Block a user