Keep the dragged copy's text inside the timeline (#267)
A multi-day event drags one piece per day, and a middle day's piece is a full 24 hours — taller than the viewport, its top at a midnight scrolled out of sight. The range went to the tallest piece by raw height, so it was drawn at that top, off the timeline entirely, and the overlay draws unclipped so the piece painted over the headers as well. The copy is now clipped to the timeline, the range goes to the piece with the most of itself in view, and a piece whose top has scrolled off brings its text down to the visible edge and budgets from there. The title is also served in full before the range: the hour gutter still says where the copy sits, so the range is the half that can afford to go.
This commit is contained in:
@@ -24,6 +24,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.withFrameNanos
|
import androidx.compose.runtime.withFrameNanos
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.LayoutCoordinates
|
import androidx.compose.ui.layout.LayoutCoordinates
|
||||||
@@ -616,6 +617,7 @@ const val SETTLE_FADE_MILLIS: Int = 250
|
|||||||
@Composable
|
@Composable
|
||||||
fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier = Modifier) {
|
fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier = Modifier) {
|
||||||
var origin by remember { mutableStateOf(Offset.Zero) }
|
var origin by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
val density = LocalDensity.current
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
val use24Hour = LocalUse24HourFormat.current
|
val use24Hour = LocalUse24HourFormat.current
|
||||||
val locale = currentLocale()
|
val locale = currentLocale()
|
||||||
@@ -677,13 +679,49 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier =
|
|||||||
val endMin = if (rawEnd > MINUTES_PER_DAY) rawEnd % MINUTES_PER_DAY else rawEnd
|
val endMin = if (rawEnd > MINUTES_PER_DAY) rawEnd % MINUTES_PER_DAY else rawEnd
|
||||||
val label = "${formatMinuteOfDay(startMin, use24Hour, locale)}–" +
|
val label = "${formatMinuteOfDay(startMin, use24Hour, locale)}–" +
|
||||||
formatMinuteOfDay(endMin, use24Hour, locale)
|
formatMinuteOfDay(endMin, use24Hour, locale)
|
||||||
// The tallest piece carries the range: on the smallest it would be
|
// The timeline's own bounds. The copy is drawn over the whole calendar
|
||||||
// clipped away, which is exactly the case when a short tail is held.
|
// so no column clip or rounded corner cuts it, but it still belongs to
|
||||||
val labelled = drag.pieces.indices.maxByOrNull { drag.pieces[it].sizePx.height }
|
// the timeline: a piece a whole day tall reaches far past both ends of
|
||||||
|
// the viewport, and unclipped it paints over the headers above (#267).
|
||||||
|
val port = controller.geometry.viewport?.takeIf { it.isAttached }?.boundsInRoot()
|
||||||
|
// How much of each piece the viewport actually shows. A multi-day event
|
||||||
|
// has a piece per day, and the widest one is a full 24 hours — taller
|
||||||
|
// than the screen, its top at a midnight scrolled out of sight. Picking
|
||||||
|
// the range's piece by raw height put it there, off the top of the
|
||||||
|
// timeline; picking by *visible* height puts it where it can be read.
|
||||||
|
val shown = drag.pieces.map { piece ->
|
||||||
|
if (port == null) {
|
||||||
|
piece.sizePx.height.toFloat()
|
||||||
|
} else {
|
||||||
|
val top = maxOf(piece.topLeftInRoot.y, port.top)
|
||||||
|
val bottom = minOf(piece.topLeftInRoot.y + piece.sizePx.height, port.bottom)
|
||||||
|
bottom - top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val labelled = shown.indices.maxByOrNull { shown[it] }
|
||||||
|
Box(
|
||||||
|
modifier = if (port == null) {
|
||||||
|
Modifier
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
.absoluteOffset {
|
||||||
|
IntOffset(
|
||||||
|
(port.left - origin.x).roundToInt(),
|
||||||
|
(port.top - origin.y).roundToInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.size(
|
||||||
|
width = with(density) { port.width.toDp() },
|
||||||
|
height = with(density) { port.height.toDp() },
|
||||||
|
)
|
||||||
|
.clipToBounds()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val pieceOrigin = port?.topLeft ?: origin
|
||||||
drag.pieces.forEachIndexed { index, piece ->
|
drag.pieces.forEachIndexed { index, piece ->
|
||||||
DragCopy(
|
DragCopy(
|
||||||
topLeftInRoot = piece.topLeftInRoot,
|
topLeftInRoot = piece.topLeftInRoot,
|
||||||
overlayOrigin = origin,
|
overlayOrigin = pieceOrigin,
|
||||||
sizePx = piece.sizePx,
|
sizePx = piece.sizePx,
|
||||||
paint = paint,
|
paint = paint,
|
||||||
shape = timedBlockShape(piece.continuesBefore, piece.continuesAfter),
|
shape = timedBlockShape(piece.continuesBefore, piece.continuesAfter),
|
||||||
@@ -692,11 +730,20 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier =
|
|||||||
alpha = copyAlpha,
|
alpha = copyAlpha,
|
||||||
title = title,
|
title = title,
|
||||||
titleLines = drag.titleLines,
|
titleLines = drag.titleLines,
|
||||||
|
// How far this piece's top has scrolled out of the timeline,
|
||||||
|
// so its text can come down to meet the edge instead of
|
||||||
|
// being clipped away with it.
|
||||||
|
hiddenTopPx = if (port == null) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
(port.top - piece.topLeftInRoot.y).coerceAtLeast(0f)
|
||||||
|
},
|
||||||
// Once for the whole event: repeated, it would name it per day.
|
// Once for the whole event: repeated, it would name it per day.
|
||||||
label = label.takeIf { index == labelled },
|
label = label.takeIf { index == labelled },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One piece of a dragged block, floating over the calendar. */
|
/** One piece of a dragged block, floating over the calendar. */
|
||||||
@@ -712,6 +759,7 @@ private fun DragCopy(
|
|||||||
alpha: Float,
|
alpha: Float,
|
||||||
title: String,
|
title: String,
|
||||||
titleLines: Int,
|
titleLines: Int,
|
||||||
|
hiddenTopPx: Float,
|
||||||
label: String?,
|
label: String?,
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -725,14 +773,25 @@ private fun DragCopy(
|
|||||||
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
||||||
}
|
}
|
||||||
// The block's size, spent the block's way, so nothing reflows under the
|
// The block's size, spent the block's way, so nothing reflows under the
|
||||||
// finger (#267) — but the range is paid for first, since it may be showing
|
// finger (#267). The title is served in full first and the range lives off
|
||||||
// where a block too short for it will land, and nothing clips the overrun.
|
// what is left: the hour gutter down the side still says where the copy
|
||||||
|
// sits, so the range is the half that can afford to go.
|
||||||
val available = height - BLOCK_TEXT_INSET * 2
|
val available = height - BLOCK_TEXT_INSET * 2
|
||||||
val reserved = if (label == null) 0.dp else timeLineHeight
|
// A piece whose top has scrolled off brings its text down to the edge, and
|
||||||
val titleBudget = ((available - reserved) / titleLineHeight).toInt().coerceAtLeast(0)
|
// budgets against what is left below it rather than against a height most
|
||||||
|
// of which is above the timeline.
|
||||||
|
val textTop = with(density) { hiddenTopPx.toDp() }
|
||||||
|
.coerceIn(0.dp, (available - titleLineHeight).coerceAtLeast(0.dp))
|
||||||
|
val room = available - textTop
|
||||||
|
val titleBudget = (room / titleLineHeight).toInt().coerceAtLeast(0)
|
||||||
val lines = titleLines.coerceAtMost(titleBudget)
|
val lines = titleLines.coerceAtMost(titleBudget)
|
||||||
val spare = available - titleLineHeight * lines - reserved
|
val left = room - titleLineHeight * lines
|
||||||
val timeMaxLines = if (label == null) 1 else blockTimeLines(label, textWidth, spare)
|
val showTime = label != null && left >= timeLineHeight
|
||||||
|
val timeMaxLines = if (showTime) {
|
||||||
|
blockTimeLines(label!!, textWidth, left - timeLineHeight)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// Absolute: these are root coordinates, and the direction-aware
|
// Absolute: these are root coordinates, and the direction-aware
|
||||||
@@ -756,7 +815,7 @@ private fun DragCopy(
|
|||||||
.eventSurface(paint, shape, cuts)
|
.eventSurface(paint, shape, cuts)
|
||||||
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = BLOCK_TEXT_INSET),
|
.padding(horizontal = BLOCK_TEXT_PADDING, vertical = BLOCK_TEXT_INSET),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(modifier = Modifier.padding(top = textTop)) {
|
||||||
if (lines > 0) {
|
if (lines > 0) {
|
||||||
BlockTitle(
|
BlockTitle(
|
||||||
title = title,
|
title = title,
|
||||||
@@ -767,7 +826,7 @@ private fun DragCopy(
|
|||||||
fontWeight = paint.titleWeight,
|
fontWeight = paint.titleWeight,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (label != null) {
|
if (showTime) {
|
||||||
val overflow = eventTitleOverflow(singleLine = timeMaxLines == 1)
|
val overflow = eventTitleOverflow(singleLine = timeMaxLines == 1)
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
|
|||||||
Reference in New Issue
Block a user