Hold a timeline resize in one ResizeSession

This commit is contained in:
2026-10-16 14:37:44 +02:00
parent ea71ab3ab9
commit 7687910a2f
@@ -626,25 +626,8 @@ class TimelineDragController {
*/
private var originSlot: Triple<LocalDate, Int, Int>? = null
/** The edge a resize is dragging, or null for a plain drag (#141). */
private var resizeEdge: ResizeEdge? = null
/** The day the resized event starts on; its edges are minutes from that midnight. */
private var resizeDay: LocalDate? = null
private var resizeStartMin = 0
private var resizeEndMin = 0
/** How far the finger sits past the edge it grabbed, so the edge doesn't jump to it. */
private var resizeGrabMin = 0f
/** The column the resized edge is in, held until the finger is well into another. */
private var resizeColumn: Int? = null
/** The slot the resized edge sits on, held against a finger trembling between two. */
private var resizeSlot: Int? = null
/** The day, counted from the event's start day, the finger is resizing in. */
private var resizeEdgeDay = 0
/** The resize in progress, or null for a plain drag (#141). */
private var resize: ResizeSession? = null
fun begin(
block: TimedBlock,
@@ -691,10 +674,7 @@ class TimelineDragController {
}
fun cancel() {
resizeEdge = null
resizeDay = null
resizeColumn = null
resizeSlot = null
resize = null
source = null
clipOffsetMin = 0
dragZone = null
@@ -800,11 +780,9 @@ class TimelineDragController {
pointerInRoot: Offset,
) {
val span = wallClockSpan(block.event, zone)
val session = ResizeSession(edge, span.day, span.startMin, span.endMin)
source = block
resizeEdge = edge
resizeDay = span.day
resizeStartMin = span.startMin
resizeEndMin = span.endMin
resize = session
clipOffsetMin = 0
this.titleLines = titleLines
settling = null
@@ -813,12 +791,12 @@ class TimelineDragController {
liftedInstanceId = block.event.instanceId
pointer = pointerInRoot
// Measured from the edge's own column: the handle overhangs into the next.
val edgeDay = if (edge == ResizeEdge.Top) 0 else (resizeEndMin - 1).floorDiv(MINUTES_PER_DAY).coerceAtLeast(0)
val edgeDay = if (edge == ResizeEdge.Top) 0 else (session.endMin - 1).floorDiv(MINUTES_PER_DAY).coerceAtLeast(0)
val edgeIndex = geometry.days.indexOf(span.day.plus(edgeDay, DateTimeUnit.DAY))
resizeColumn = edgeIndex.takeIf { it >= 0 }
session.column = edgeIndex.takeIf { it >= 0 }
?.let { column(it) }
resizeGrabMin = (pointerResizeMinutes() ?: 0f) -
if (edge == ResizeEdge.Top) resizeStartMin else resizeEndMin
session.grabMin = (pointerResizeMinutes(session) ?: 0f) -
if (edge == ResizeEdge.Top) session.startMin else session.endMin
originSlot = null
recompute()
originSlot = drag?.slot
@@ -834,7 +812,7 @@ class TimelineDragController {
fun preview(zone: TimeZone): EventInstance? {
val (date, startMin, spanMin) = landingSlot?.takeIf { it != originSlot } ?: return null
val event = source?.event ?: return null
val edge = resizeEdge
val edge = resize?.edge
if (edge != null) {
val drop = resizedDrop(event, edge, date, startMin, spanMin, zone)
return event.copy(start = drop.start, end = drop.end)
@@ -850,7 +828,7 @@ class TimelineDragController {
fun finishResize(zone: TimeZone): TimelineResizeDrop? {
val landed = drag
val origin = originSlot
val edge = resizeEdge
val edge = resize?.edge
cancel()
if (landed == null || landed.slot == origin) return null
val drop = resizedDrop(landed.event, edge, landed.date, landed.eventStartMin, landed.eventSpanMin, zone)
@@ -862,9 +840,8 @@ class TimelineDragController {
* Where the finger is, in minutes from the resized event's start day —
* past 1440 in a later column, negative in an earlier one.
*/
private fun pointerResizeMinutes(): Float? {
private fun pointerResizeMinutes(session: ResizeSession): Float? {
val grid = geometry.grid?.takeIf { it.isAttached } ?: return null
val day = resizeDay ?: return null
val hourPx = geometry.hourPx
val columnPx = geometry.columnWidthPx
val days = geometry.days
@@ -872,22 +849,23 @@ class TimelineDragController {
val origin = grid.positionInRoot()
val held = stickyColumn(
position = (pointer.x - origin.x) / columnPx,
held = resizeColumn,
held = session.column,
lastIndex = days.lastIndex,
)
resizeColumn = held
val dayOffset = (days[column(held)].toEpochDays() - day.toEpochDays()).toInt()
resizeEdgeDay = dayOffset
session.column = held
val dayOffset = (days[column(held)].toEpochDays() - session.day.toEpochDays()).toInt()
session.edgeDay = dayOffset
return dayOffset * MINUTES_PER_DAY + (pointer.y - origin.y) / hourPx * 60f
}
private fun recomputeResize(edge: ResizeEdge) {
private fun recomputeResize(session: ResizeSession) {
val block = source ?: return
val day = resizeDay ?: return
val raw = pointerResizeMinutes() ?: return
val slot = stickySnap(raw - resizeGrabMin, resizeSlot)
resizeSlot = slot
val (start, end) = resizedSpan(edge, resizeStartMin, resizeEndMin, slot.toFloat(), resizeEdgeDay)
val edge = session.edge
val day = session.day
val raw = pointerResizeMinutes(session) ?: return
val slot = stickySnap(raw - session.grabMin, session.slot)
session.slot = slot
val (start, end) = resizedSpan(edge, session.startMin, session.endMin, slot.toFloat(), session.edgeDay)
val days = geometry.days
val origin = geometry.grid?.takeIf { it.isAttached }?.positionInRoot() ?: return
// Which column the start day sits in, possibly off either end of the view.
@@ -946,7 +924,7 @@ class TimelineDragController {
* no move events while auto-scroll walks the content underneath it.
*/
fun recompute() {
resizeEdge?.let {
resize?.let {
recomputeResize(it)
return
}
@@ -1049,6 +1027,27 @@ class TimelineDragController {
}
}
/** One resize gesture (#141): the edge it drags, and the edges as minutes from [day]'s midnight. */
private class ResizeSession(
val edge: ResizeEdge,
/** The day the resized event starts on. */
val day: LocalDate,
val startMin: Int,
val endMin: Int,
) {
/** How far the finger sits past the edge it grabbed, so the edge doesn't jump to it. */
var grabMin = 0f
/** The column the resized edge is in, held until the finger is well into another. */
var column: Int? = null
/** The slot the resized edge sits on, held against a finger trembling between two. */
var slot: Int? = null
/** The day, counted from the event's start day, the finger is resizing in. */
var edgeDay = 0
}
@Composable
fun rememberTimelineDragController(): TimelineDragController {
val controller = remember { TimelineDragController() }