Seat the month row from one MonthWeek.seating
This commit is contained in:
@@ -841,7 +841,7 @@ internal fun monthBandHeight(rowHeight: Dp): Dp =
|
||||
|
||||
/** Whether any day in this week holds more than [lanes] lanes can seat. */
|
||||
private fun MonthWeek.overflowsAt(lanes: Int): Boolean =
|
||||
days.withIndex().any { (col, day) -> overflowEvents(col, day, lanes).isNotEmpty() }
|
||||
days.indices.any { col -> overflowEvents(col, lanes).isNotEmpty() }
|
||||
|
||||
/**
|
||||
* Row height in the continuous grid. The paged grid divides the viewport between
|
||||
@@ -1627,11 +1627,11 @@ internal fun SplitMonthGrid(
|
||||
val inMonth = day.month == month.month && day.year == month.year
|
||||
// Seated by lane rather than gathered by colour, so each dot
|
||||
// is the event the expanded grid draws in that same lane.
|
||||
val seated = week.laneEvents(col, day, SPLIT_DOT_LANES)
|
||||
val seated = week.laneEvents(col, SPLIT_DOT_LANES)
|
||||
SplitDayCell(
|
||||
date = day,
|
||||
events = seated,
|
||||
hidden = week.overflowEvents(col, day, SPLIT_DOT_LANES),
|
||||
hidden = week.overflowEvents(col, SPLIT_DOT_LANES),
|
||||
isToday = day == state.today,
|
||||
// A page marks only the days its own month owns. Paging
|
||||
// moves the selection before this month's replacement
|
||||
@@ -2098,8 +2098,8 @@ private fun MonthWeekRow(
|
||||
val ripple = rememberTodayRipple(active = todayCol != null)
|
||||
val overflowRow = rememberOverflowRowHeight()
|
||||
val laneCap = week.laneCapFor(rowHeight, overflowRow)
|
||||
val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1
|
||||
val shownLanes = laneCount.coerceAtMost(laneCap)
|
||||
// Once per row rather than per frame: a drag recomposes every frame.
|
||||
val seatings = remember(week, laneCap) { week.days.indices.map { week.seating(it, laneCap) } }
|
||||
val morphing = morphInFlight()
|
||||
// Every chip's start time for this row at once, and only when the row's own
|
||||
// inputs change: formatting is a parsed pattern per call, and the dim cutoff
|
||||
@@ -2283,7 +2283,7 @@ private fun MonthWeekRow(
|
||||
.then(if (morphing || dragging) Modifier else Modifier.clipToBounds()),
|
||||
) {
|
||||
// Spanning bars on their shared lanes.
|
||||
week.spans.filter { it.lane < shownLanes }.forEach { span ->
|
||||
week.spans.filter { it.lane < laneCap }.forEach { span ->
|
||||
val cols = span.endCol - span.startCol + 1
|
||||
MonthBar(
|
||||
event = span.event,
|
||||
@@ -2355,19 +2355,13 @@ private fun MonthWeekRow(
|
||||
)
|
||||
}
|
||||
}
|
||||
// Single-day timed pills + overflow, per column. Pills fill the
|
||||
// lane slots no bar occupies on THIS day (top-most first), so a
|
||||
// bar-free day isn't pushed down by a multi-day event that only
|
||||
// sits on other days of the week.
|
||||
// Single-day timed pills + overflow, per column, in the lanes
|
||||
// [seating] gave them.
|
||||
week.days.forEachIndexed { col, d ->
|
||||
val timed = week.timedByDay[d].orEmpty()
|
||||
val occupied = week.spans
|
||||
.filter { it.lane < shownLanes && col in it.startCol..it.endCol }
|
||||
.map { it.lane }
|
||||
.toSet()
|
||||
val freeSlots = (0 until laneCap).filter { it !in occupied }
|
||||
val pillsShown = timed.take(freeSlots.size)
|
||||
pillsShown.forEachIndexed { i, ev ->
|
||||
val seating = seatings[col]
|
||||
seating.seated.forEach { seat ->
|
||||
if (seat.bar) return@forEach
|
||||
val ev = seat.event
|
||||
MonthBar(
|
||||
event = ev,
|
||||
dark = dark,
|
||||
@@ -2379,7 +2373,7 @@ private fun MonthWeekRow(
|
||||
modifier = Modifier
|
||||
.offset(
|
||||
x = colW * col,
|
||||
y = EVENT_ROW_HEIGHT * freeSlots[i],
|
||||
y = EVENT_ROW_HEIGHT * seat.lane,
|
||||
)
|
||||
.morphBounds(MonthMorphKey.Event(d, ev.instanceId))
|
||||
.then(
|
||||
@@ -2395,18 +2389,10 @@ private fun MonthWeekRow(
|
||||
.padding(horizontal = MONTH_CHIP_INSET, vertical = 1.dp),
|
||||
)
|
||||
}
|
||||
val hidden = (week.countByDay[d] ?: 0) - occupied.size - pillsShown.size
|
||||
if (hidden > 0) {
|
||||
val hiddenEvents = buildList {
|
||||
week.spans
|
||||
.filter { it.lane >= shownLanes && col in it.startCol..it.endCol }
|
||||
.forEach { add(it.event) }
|
||||
addAll(timed.drop(pillsShown.size))
|
||||
}
|
||||
if (seating.hidden.isNotEmpty()) {
|
||||
OverflowDots(
|
||||
events = hiddenEvents,
|
||||
events = seating.hidden,
|
||||
day = d,
|
||||
total = hidden,
|
||||
dark = dark,
|
||||
rowHeight = overflowRow,
|
||||
modifier = Modifier
|
||||
@@ -2786,7 +2772,6 @@ private fun MonthBar(
|
||||
private fun OverflowDots(
|
||||
events: List<EventInstance>,
|
||||
day: LocalDate,
|
||||
total: Int,
|
||||
dark: Boolean,
|
||||
rowHeight: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -2808,7 +2793,7 @@ private fun OverflowDots(
|
||||
.background(eventAccent(argb, dark, soften), CircleShape),
|
||||
)
|
||||
}
|
||||
val extra = total - dots.size
|
||||
val extra = events.size - dots.size
|
||||
if (extra > 0) {
|
||||
Text(
|
||||
text = "+$extra",
|
||||
|
||||
@@ -39,33 +39,55 @@ data class MonthWeek(
|
||||
val countByDay: Map<LocalDate, Int>,
|
||||
)
|
||||
|
||||
/** One event seated in [lane] of a day; [bar] is a spanning bar rather than that day's own pill. */
|
||||
data class Seat(val lane: Int, val event: EventInstance, val bar: Boolean)
|
||||
|
||||
/**
|
||||
* The events occupying each lane of [day] — column [col] of this week — in lane
|
||||
* order, capped at [laneCap] lanes.
|
||||
* How one day of a week row is seated with [laneCap] lanes: what sits in each
|
||||
* lane, top-most first with empty lanes left out, and what had no lane left.
|
||||
*/
|
||||
data class DaySeating(val seated: List<Seat>, val hidden: List<EventInstance>)
|
||||
|
||||
/**
|
||||
* The seating of column [col] with [laneCap] lanes — the one every reader of a
|
||||
* row builds on: the bars and pills the paged grid draws, the split style's dots
|
||||
* ([laneEvents]), the hit test ([chipAt]) and the overflow ([overflowEvents]).
|
||||
*
|
||||
* This is the same seating [spans]/[timedByDay] get when the paged grid draws a
|
||||
* week: bars keep the lane the row layout gave them, and the day's timed events
|
||||
* fill whatever slots are left, top-most first. Reading it here means the split
|
||||
* style's dots and the paged style's bars describe a day in the *same order*, so
|
||||
* dot _i_ and lane _i_ are the same event and one can morph into the other (#53).
|
||||
* Bars keep the lane the row layout gave them, and the day's timed events fill
|
||||
* whatever slots are left, top-most first, so a bar-free day isn't pushed down by
|
||||
* a multi-day event that only sits on other days of the week. Overflow is the
|
||||
* exact complement, bars before pills.
|
||||
*/
|
||||
fun MonthWeek.seating(col: Int, laneCap: Int): DaySeating {
|
||||
val covering = spans.filter { col in it.startCol..it.endCol }
|
||||
val byLane = arrayOfNulls<Seat>(laneCap.coerceAtLeast(0))
|
||||
covering.forEach { span ->
|
||||
if (span.lane < laneCap) byLane[span.lane] = Seat(span.lane, span.event, bar = true)
|
||||
}
|
||||
val timed = timedByDay[days[col]].orEmpty()
|
||||
var pills = 0
|
||||
for (lane in byLane.indices) {
|
||||
if (byLane[lane] == null && pills < timed.size) byLane[lane] = Seat(lane, timed[pills++], bar = false)
|
||||
}
|
||||
return DaySeating(
|
||||
seated = byLane.filterNotNull(),
|
||||
hidden = covering.filter { it.lane >= laneCap }.map { it.event } + timed.drop(pills),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The events occupying each lane of column [col], in lane order, capped at
|
||||
* [laneCap] lanes — the same [seating] the paged grid draws a week with. Reading
|
||||
* it here means the split style's dots and the paged style's bars describe a day
|
||||
* in the *same order*, so dot _i_ and lane _i_ are the same event and one can
|
||||
* morph into the other (#53).
|
||||
*
|
||||
* Deriving the dots independently — by distinct colour, as they first were —
|
||||
* left dot _i_ standing for no particular event, and quietly merged two events
|
||||
* that shared a calendar into a single dot.
|
||||
*/
|
||||
fun MonthWeek.laneEvents(col: Int, day: LocalDate, laneCap: Int): List<EventInstance> {
|
||||
val byLane = arrayOfNulls<EventInstance>(laneCap)
|
||||
spans.forEach { span ->
|
||||
if (span.lane < laneCap && col in span.startCol..span.endCol) {
|
||||
byLane[span.lane] = span.event
|
||||
}
|
||||
}
|
||||
val free = (0 until laneCap).filter { byLane[it] == null }
|
||||
timedByDay[day].orEmpty().take(free.size).forEachIndexed { i, event ->
|
||||
byLane[free[i]] = event
|
||||
}
|
||||
return byLane.filterNotNull()
|
||||
}
|
||||
fun MonthWeek.laneEvents(col: Int, laneCap: Int): List<EventInstance> =
|
||||
seating(col, laneCap).seated.map { it.event }
|
||||
|
||||
/**
|
||||
* The event drawn in lane [lane] of column [col], or null for an empty slot.
|
||||
@@ -74,14 +96,7 @@ fun MonthWeek.laneEvents(col: Int, day: LocalDate, laneCap: Int): List<EventInst
|
||||
*/
|
||||
fun MonthWeek.chipAt(col: Int, lane: Int, laneCap: Int): EventInstance? {
|
||||
if (col !in days.indices || lane !in 0 until laneCap) return null
|
||||
spanAt(col, lane)?.let { return it.event }
|
||||
val occupied = spans
|
||||
.filter { it.lane < laneCap && col in it.startCol..it.endCol }
|
||||
.map { it.lane }
|
||||
.toSet()
|
||||
val free = (0 until laneCap).filter { it !in occupied }
|
||||
val index = free.indexOf(lane).takeIf { it >= 0 } ?: return null
|
||||
return timedByDay[days[col]].orEmpty().take(free.size).getOrNull(index)
|
||||
return seating(col, laneCap).seated.firstOrNull { it.lane == lane }?.event
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,19 +113,11 @@ fun MonthWeek.spanAt(col: Int, lane: Int): MonthSpan? =
|
||||
spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol }
|
||||
|
||||
/**
|
||||
* The events on [day] that [laneEvents] had no lane left for — its exact
|
||||
* The events in column [col] that [laneEvents] had no lane left for — its exact
|
||||
* complement, in the same bars-then-pills order. Returned as events rather than
|
||||
* a count because dimming is a per-event question (#79).
|
||||
*/
|
||||
fun MonthWeek.overflowEvents(col: Int, day: LocalDate, laneCap: Int): List<EventInstance> {
|
||||
val seatedLanes = spans.count { it.lane < laneCap && col in it.startCol..it.endCol }
|
||||
return buildList {
|
||||
spans.forEach { span ->
|
||||
if (span.lane >= laneCap && col in span.startCol..span.endCol) add(span.event)
|
||||
}
|
||||
addAll(timedByDay[day].orEmpty().drop(laneCap - seatedLanes))
|
||||
}
|
||||
}
|
||||
fun MonthWeek.overflowEvents(col: Int, laneCap: Int): List<EventInstance> = seating(col, laneCap).hidden
|
||||
|
||||
/**
|
||||
* State for the continuous style (#38): a vertical stream of *self-contained*
|
||||
|
||||
@@ -37,7 +37,7 @@ class LaneEventsTest {
|
||||
val week = rowOfJuly6(listOf(bar, meeting))
|
||||
|
||||
// Jul 7 is column 1 of a Monday-anchored row starting Jul 6.
|
||||
assertThat(week.laneEvents(col = 1, day = LocalDate(2026, 7, 7), laneCap = 3))
|
||||
assertThat(week.laneEvents(col = 1, laneCap = 3))
|
||||
.containsExactly(bar, meeting)
|
||||
.inOrder()
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class LaneEventsTest {
|
||||
val monday = timed(LocalDate(2026, 7, 6), hour = 9, id = 2L)
|
||||
val week = rowOfJuly6(listOf(bar, monday))
|
||||
|
||||
assertThat(week.laneEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3))
|
||||
assertThat(week.laneEvents(col = 0, laneCap = 3))
|
||||
.containsExactly(monday)
|
||||
}
|
||||
|
||||
@@ -58,10 +58,9 @@ class LaneEventsTest {
|
||||
val week = rowOfJuly6(listOf(bar))
|
||||
|
||||
(1..3).forEach { col ->
|
||||
val day = LocalDate(2026, 7, 6 + col)
|
||||
assertThat(week.laneEvents(col, day, laneCap = 3)).containsExactly(bar)
|
||||
assertThat(week.laneEvents(col, laneCap = 3)).containsExactly(bar)
|
||||
}
|
||||
assertThat(week.laneEvents(col = 4, day = LocalDate(2026, 7, 10), laneCap = 3)).isEmpty()
|
||||
assertThat(week.laneEvents(col = 4, laneCap = 3)).isEmpty()
|
||||
}
|
||||
|
||||
/** The bug the colour-gathered dots had: one dot for two events on one calendar. */
|
||||
@@ -71,7 +70,7 @@ class LaneEventsTest {
|
||||
val second = timed(LocalDate(2026, 7, 6), hour = 14, id = 2L, color = RED)
|
||||
val week = rowOfJuly6(listOf(first, second))
|
||||
|
||||
assertThat(week.laneEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3))
|
||||
assertThat(week.laneEvents(col = 0, laneCap = 3))
|
||||
.containsExactly(first, second)
|
||||
.inOrder()
|
||||
}
|
||||
@@ -81,7 +80,7 @@ class LaneEventsTest {
|
||||
val events = (0 until 5).map { timed(LocalDate(2026, 7, 6), hour = 8 + it, id = it + 1L) }
|
||||
val week = rowOfJuly6(events)
|
||||
|
||||
val seated = week.laneEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3)
|
||||
val seated = week.laneEvents(col = 0, laneCap = 3)
|
||||
assertThat(seated).hasSize(3)
|
||||
assertThat(seated).containsExactlyElementsIn(events.take(3)).inOrder()
|
||||
assertThat(week.countByDay[LocalDate(2026, 7, 6)]!! - seated.size).isEqualTo(2)
|
||||
@@ -95,7 +94,7 @@ class LaneEventsTest {
|
||||
}
|
||||
val week = rowOfJuly6(bars)
|
||||
|
||||
val seated = week.laneEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3)
|
||||
val seated = week.laneEvents(col = 0, laneCap = 3)
|
||||
assertThat(seated).hasSize(3)
|
||||
assertThat(week.spans.filter { it.lane >= 3 }.map { it.event })
|
||||
.containsNoneIn(seated)
|
||||
@@ -105,14 +104,13 @@ class LaneEventsTest {
|
||||
fun `overflow is exactly what seating left behind`() {
|
||||
val events = (0 until 5).map { timed(LocalDate(2026, 7, 6), hour = 8 + it, id = it + 1L) }
|
||||
val week = rowOfJuly6(events)
|
||||
val day = LocalDate(2026, 7, 6)
|
||||
|
||||
val seated = week.laneEvents(col = 0, day = day, laneCap = 3)
|
||||
val overflow = week.overflowEvents(col = 0, day = day, laneCap = 3)
|
||||
val seated = week.laneEvents(col = 0, laneCap = 3)
|
||||
val overflow = week.overflowEvents(col = 0, laneCap = 3)
|
||||
|
||||
assertThat(overflow).containsExactlyElementsIn(events.drop(3)).inOrder()
|
||||
assertThat(seated + overflow).containsExactlyElementsIn(events)
|
||||
assertThat(seated.size + overflow.size).isEqualTo(week.countByDay[day])
|
||||
assertThat(seated.size + overflow.size).isEqualTo(week.countByDay[LocalDate(2026, 7, 6)])
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -124,8 +122,7 @@ class LaneEventsTest {
|
||||
val parked = week.spans.filter { it.lane >= 3 }.map { it.event }
|
||||
|
||||
(0..2).forEach { col ->
|
||||
val day = LocalDate(2026, 7, 6 + col)
|
||||
assertThat(week.overflowEvents(col, day, laneCap = 3))
|
||||
assertThat(week.overflowEvents(col, laneCap = 3))
|
||||
.containsExactlyElementsIn(parked)
|
||||
}
|
||||
}
|
||||
@@ -134,7 +131,7 @@ class LaneEventsTest {
|
||||
fun `a day that fits has no overflow`() {
|
||||
val week = rowOfJuly6(listOf(timed(LocalDate(2026, 7, 6), hour = 9, id = 1L)))
|
||||
|
||||
assertThat(week.overflowEvents(col = 0, day = LocalDate(2026, 7, 6), laneCap = 3)).isEmpty()
|
||||
assertThat(week.overflowEvents(col = 0, laneCap = 3)).isEmpty()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -66,14 +66,17 @@ class MonthSeatingTest {
|
||||
assertWithMessage("$where: dot count vs hidden list")
|
||||
.that(row.hiddenCount.coerceAtLeast(0)).isEqualTo(row.hiddenEvents.size)
|
||||
|
||||
assertWithMessage("$where: laneEvents").that(week.laneEvents(col, day, laneCap)).isEqualTo(dots)
|
||||
assertWithMessage("$where: overflowEvents").that(week.overflowEvents(col, day, laneCap)).isEqualTo(overflow)
|
||||
assertWithMessage("$where: laneEvents").that(week.laneEvents(col, laneCap)).isEqualTo(dots)
|
||||
assertWithMessage("$where: overflowEvents").that(week.overflowEvents(col, laneCap)).isEqualTo(overflow)
|
||||
(-1..laneCap).forEach { lane ->
|
||||
assertWithMessage("$where: chipAt lane $lane")
|
||||
.that(week.chipAt(col, lane, laneCap))
|
||||
.isEqualTo(refChipAt(week, col, lane, laneCap))
|
||||
}
|
||||
//SEATING
|
||||
val seating = week.seating(col, laneCap)
|
||||
assertWithMessage("$where: seating lanes")
|
||||
.that(seating.seated.map { it.lane to it.event }).isEqualTo(row.seated)
|
||||
assertWithMessage("$where: seating hidden").that(seating.hidden).isEqualTo(row.hiddenEvents)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user