fix(month): let the selection outline travel with its cell (#53)
The outline was the last piece with nothing to morph against, so it could only fade in place while everything around it moved. The expanded grid draws no outline by design — once the cells carry real event bars it is one mark too many — which left it partnerless. Same answer as the multi-day dots: give it an invisible stand-in over the same cell in the expanded grid. It draws nothing and exists only to be the other end of the morph, so the outline now rides its cell between the two layouts. Tagged only while it is the selected day. Tagged unconditionally it would put a key on all 42 cells against the single partner on offer, and the other 41 would fly in from the layout origin — the failure the dots just had. The stand-in carries the same padding as the outline's own bounds, or it would arrive at the wrong size, and it sits in its own layer rather than inside the cell pill, which is itself tagged and would make it travel twice. MonthGrid takes `selected` again, but for an anchor rather than a mark; the other styles pass nothing and compose no extra layer at all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,15 @@ internal sealed interface MonthMorphKey {
|
|||||||
*/
|
*/
|
||||||
data class Overflow(val date: LocalDate) : MonthMorphKey
|
data class Overflow(val date: LocalDate) : MonthMorphKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected day's outline. The expanded grid draws no outline — once the
|
||||||
|
* cells carry real event bars it is one mark too many — so this pairs the
|
||||||
|
* compact grid's outline with an *invisible* stand-in over the same cell
|
||||||
|
* there. Without one it had nowhere to travel and could only fade in place
|
||||||
|
* while everything around it moved.
|
||||||
|
*/
|
||||||
|
data class Selection(val date: LocalDate) : MonthMorphKey
|
||||||
|
|
||||||
/** The grab handle, which slides from the pane's seam to the foot of the grid. */
|
/** The grab handle, which slides from the pane's seam to the foot of the grid. */
|
||||||
data object Handle : MonthMorphKey
|
data object Handle : MonthMorphKey
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -591,6 +591,8 @@ internal fun MonthGrid(
|
|||||||
state: MonthUiState.Success,
|
state: MonthUiState.Success,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
|
/** See [MonthWeekRow]'s `selected`: an anchor for the morph, never a mark. */
|
||||||
|
selected: LocalDate? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -609,6 +611,7 @@ internal fun MonthGrid(
|
|||||||
inMonth = { it.month == month.month && it.year == month.year },
|
inMonth = { it.month == month.month && it.year == month.year },
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
|
selected = selected,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
@@ -1003,6 +1006,7 @@ private fun SplitMonthBody(
|
|||||||
if (expanded) {
|
if (expanded) {
|
||||||
SplitMonthExpanded(
|
SplitMonthExpanded(
|
||||||
state = state,
|
state = state,
|
||||||
|
selected = selected,
|
||||||
slideDir = slideDir,
|
slideDir = slideDir,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
swipeModifier = swipeModifier,
|
swipeModifier = swipeModifier,
|
||||||
@@ -1109,6 +1113,8 @@ private fun SplitMonthCollapsed(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SplitMonthExpanded(
|
private fun SplitMonthExpanded(
|
||||||
state: MonthUiState.Success,
|
state: MonthUiState.Success,
|
||||||
|
/** Marked nowhere here; it only anchors the outline's morph. */
|
||||||
|
selected: LocalDate,
|
||||||
slideDir: Int,
|
slideDir: Int,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
swipeModifier: Modifier,
|
swipeModifier: Modifier,
|
||||||
@@ -1123,18 +1129,19 @@ private fun SplitMonthExpanded(
|
|||||||
// of it, and the handle is the obvious thing to reach for on the way back.
|
// of it, and the handle is the obvious thing to reach for on the way back.
|
||||||
Column(Modifier.fillMaxSize().then(swipeModifier)) {
|
Column(Modifier.fillMaxSize().then(swipeModifier)) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = state,
|
targetState = state to selected,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
contentKey = { it.month },
|
contentKey = { (s, _) -> s.month },
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion)
|
calendarSlideTransition(slideDir, slideSpec, fadeSpec, reduceMotion)
|
||||||
},
|
},
|
||||||
label = "split-expanded-month-transition",
|
label = "split-expanded-month-transition",
|
||||||
) { s ->
|
) { (s, sel) ->
|
||||||
MonthGrid(
|
MonthGrid(
|
||||||
state = s,
|
state = s,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onPickDay,
|
onOpenDay = onPickDay,
|
||||||
|
selected = sel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SplitExpandHandle(expanded = true, onToggle = onCollapse)
|
SplitExpandHandle(expanded = true, onToggle = onCollapse)
|
||||||
@@ -1350,12 +1357,21 @@ private fun SplitDayCell(
|
|||||||
// is laid out where the collapsed cell actually is and simply fades in,
|
// is laid out where the collapsed cell actually is and simply fades in,
|
||||||
// which is the only size it is ever true at.
|
// which is the only size it is ever true at.
|
||||||
//
|
//
|
||||||
// It can stay plain content now that nothing renders into an overlay above
|
// Tagged only while it is the selected day, and paired with an invisible
|
||||||
// the tree — see [morphElement]. It sits above its own pill by ordinary
|
// stand-in over the same cell in the expanded grid, so it travels with its
|
||||||
// z-order and fades with the page it belongs to, in step with the rest.
|
// cell instead of fading in place. Tagged unconditionally it would put a
|
||||||
|
// key on all 42 cells against the one partner the expanded grid offers,
|
||||||
|
// and the other 41 would fly in from the layout origin.
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.then(
|
||||||
|
if (isSelected) {
|
||||||
|
Modifier.morphBounds(MonthMorphKey.Selection(date))
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
)
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
val alpha = selection.value
|
val alpha = selection.value
|
||||||
if (alpha <= 0f) return@drawBehind
|
if (alpha <= 0f) return@drawBehind
|
||||||
@@ -1647,6 +1663,13 @@ private fun MonthWeekRow(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
blankOutside: Boolean = false,
|
blankOutside: Boolean = false,
|
||||||
labelMonthOnFirst: Boolean = false,
|
labelMonthOnFirst: Boolean = false,
|
||||||
|
/**
|
||||||
|
* The selected day, when there is one. Draws *nothing* — it only places the
|
||||||
|
* invisible counterpart the compact grid's outline morphs against. Null for
|
||||||
|
* every style but the split style's expanded form, which composes no extra
|
||||||
|
* layer at all.
|
||||||
|
*/
|
||||||
|
selected: LocalDate? = null,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1
|
val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1
|
||||||
@@ -1846,6 +1869,29 @@ private fun MonthWeekRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invisible stand-in for the compact grid's selection outline, so it
|
||||||
|
// has somewhere to travel to and from. Its own layer rather than a
|
||||||
|
// child of the cell pill: that pill is itself a tagged piece, and a
|
||||||
|
// tag nested inside a tag travels twice. Padded to match the outline's
|
||||||
|
// own bounds over there, or it would arrive at the wrong size.
|
||||||
|
if (selected != null) {
|
||||||
|
Row(Modifier.matchParentSize()) {
|
||||||
|
week.days.forEach { d ->
|
||||||
|
if (d == selected && inMonth(d)) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
||||||
|
.morphBounds(MonthMorphKey.Selection(d)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.weight(1f).fillMaxHeight())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tap layer: in month view a tap on any day opens that day. Padded and
|
// Tap layer: in month view a tap on any day opens that day. Padded and
|
||||||
// clipped to the background pill so the ripple matches it. A blanked
|
// clipped to the background pill so the ripple matches it. A blanked
|
||||||
// cell isn't part of this month, so it takes no taps either.
|
// cell isn't part of this month, so it takes no taps either.
|
||||||
|
|||||||
Reference in New Issue
Block a user