From b7e624877d6a8e1a49ad8a33a1a6fc972de486c8 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 2 Aug 2026 18:19:43 +0200 Subject: [PATCH] Revert the chip's FAB colours, quicken it, and end the settle flicker (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chip goes back to the neutral fill and its motion is cut to a receipt's length — the default visibility spring overshot and read as sluggish. The landed copy is now handed back when the grid actually draws the drop, not on a timer: releasing early put the source ghost back at full opacity in its old slot for the rest of the re-read, which is the flicker. --- .../calendula/ui/common/EventMoveHost.kt | 6 --- .../calendula/ui/common/TimelineDrag.kt | 51 ++++++++++++++++--- .../calendula/ui/day/DayScreen.kt | 4 ++ .../calendula/ui/month/MonthScreen.kt | 10 ++-- .../calendula/ui/week/WeekScreen.kt | 4 ++ floret-kit | 2 +- 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventMoveHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventMoveHost.kt index 4b3b508..97347c9 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventMoveHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventMoveHost.kt @@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.material3.FloatingActionButtonDefaults -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -108,10 +106,6 @@ fun EventMoveHost(viewModel: RescheduleViewModel, modifier: Modifier = Modifier) actionLabel = stringResource(R.string.event_move_undo) .takeIf { shownUndo != null }, onAction = shownUndo?.let { undo -> { viewModel.undo(undo) } }, - // The FAB's own colours: the chip shares its band, so sharing its - // fill reads as one bottom layer rather than two competing ones. - color = FloatingActionButtonDefaults.containerColor, - contentColor = contentColorFor(FloatingActionButtonDefaults.containerColor), ) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt index c6818f1..feeca09 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt @@ -144,6 +144,15 @@ class TimelineDragController { var isDragging: Boolean by mutableStateOf(false) private set + /** + * Whether the grid itself now draws the settled drop at its landing slot. + * The copy may only be handed back once this is true: releasing on a timer + * puts the source ghost back at full opacity in its *old* slot for whatever + * is left of the re-read — a flicker of the event where it no longer is. + */ + var settledOnGrid: Boolean by mutableStateOf(false) + private set + private var source: TimedBlock? = null private var grab = Offset.Zero private var pointer = Offset.Zero @@ -180,6 +189,25 @@ class TimelineDragController { originSlot = null drag = null settling = null + settledOnGrid = false + } + + /** + * What a day column now holds, so a settled drop can tell when the grid has + * caught up with it. Matched on the landing slot plus either the event row + * or its title: a single-occurrence move writes an exception row with a new + * `eventId`, which nothing else about the drop can predict. + */ + fun noteGrid(date: LocalDate, blocks: List) { + val landed = settling ?: return + if (settledOnGrid || landed.date != date) return + settledOnGrid = blocks.any { block -> + block.startMin == landed.startMin && + ( + block.event.eventId == landed.event.eventId || + block.event.title == landed.event.title + ) + } } /** @@ -201,6 +229,7 @@ class TimelineDragController { fun release() { settling = null liftedInstanceId = null + settledOnGrid = false } /** @@ -301,11 +330,17 @@ fun TimelineDrop.startInstant(zone: TimeZone): Instant = LocalDateTime(date, LocalTime(startMin / 60, startMin % 60)).toInstant(zone) /** - * How long a landed block keeps its position after the write reports back — the - * provider's change notification and the re-query land a beat later, and - * releasing on the write alone would still flash the old slot. + * A beat after the grid draws the drop, so its own block is under way before the + * copy starts dissolving. */ -const val SETTLE_GRACE_MILLIS: Long = 200L +const val SETTLE_GRACE_MILLIS: Long = 60L + +/** + * How long to wait for a grid that never confirms the drop — the event landed on + * a day this timeline doesn't show, or the write failed and nothing changed. The + * copy has to go either way. + */ +private const val SETTLE_TIMEOUT_MILLIS = 900L /** * The hand-over: the copy dissolves over this while the grid's own block slides @@ -337,12 +372,12 @@ fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier = if (controller.isDragging) controller.autoScroll() } // Hold the landed copy until the write is done — including the whole time a - // recurring drop's scope dialog is up — then a grace for the re-read, then - // fade it into the grid's own block, which has slid in underneath by now. + // recurring drop's scope dialog is up — and then until the grid draws the + // drop itself, so the copy dissolves onto a block that is already there. var handingOver by remember(controller.settling) { mutableStateOf(false) } - LaunchedEffect(controller.settling, moveInFlight) { + LaunchedEffect(controller.settling, moveInFlight, controller.settledOnGrid) { if (controller.settling == null || moveInFlight) return@LaunchedEffect - delay(SETTLE_GRACE_MILLIS) + delay(if (controller.settledOnGrid) SETTLE_GRACE_MILLIS else SETTLE_TIMEOUT_MILLIS) handingOver = true delay(SETTLE_FADE_MILLIS.toLong()) controller.release() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt index 3fdb258..2016224 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt @@ -615,6 +615,10 @@ private fun DayColumnCard( val hourPx = with(LocalDensity.current) { hourHeight.toPx() } val showHourLines = LocalShowHourLines.current val hourLineColor = MaterialTheme.colorScheme.outlineVariant + // Tells a settled drop when this column has caught up with it. + LaunchedEffect(blocks, dragController.settling) { + dragController.noteGrid(date, blocks) + } Card( // Plain rectangular column — the soft corners come from the outer // rounded scroll viewport, so inner rounding would look odd at the edges. diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index f782d94..948ae6c 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -92,7 +92,6 @@ import de.jeanlucmakiola.calendula.ui.common.LocalEventMove import de.jeanlucmakiola.calendula.ui.common.moveInFlight import de.jeanlucmakiola.calendula.ui.common.ghostAlpha import de.jeanlucmakiola.calendula.ui.common.DragSnapHaptics -import de.jeanlucmakiola.calendula.ui.common.SETTLE_GRACE_MILLIS import de.jeanlucmakiola.calendula.ui.common.EventMoveScope import de.jeanlucmakiola.calendula.domain.spanFirstDay import androidx.compose.ui.unit.IntSize @@ -466,6 +465,9 @@ fun MonthScreen( } } +/** How long a landed chip holds its day while the write and re-read land. */ +private const val MONTH_SETTLE_HOLD_MILLIS = 450L + /** The chip in flight, drawn over the grid and following the finger. */ @Composable private fun MonthDragOverlay(controller: MonthDragController) { @@ -478,10 +480,12 @@ private fun MonthDragOverlay(controller: MonthDragController) { // every frame, and this composable is the one that is meant to. DragSnapHaptics(controller.drag?.targetDate) // Hold the landed chip until the write is done — including the whole time a - // recurring drop's scope dialog is up — then a grace for the re-read. + // recurring drop's scope dialog is up — then a grace for the re-read. A flat + // wait, unlike the timeline's: the grid re-lays a whole month of rows, so + // there is no single column to watch for the chip arriving. LaunchedEffect(controller.settling, moveInFlight) { if (controller.settling == null || moveInFlight) return@LaunchedEffect - delay(SETTLE_GRACE_MILLIS) + delay(MONTH_SETTLE_HOLD_MILLIS) controller.release() } Box( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt index e8f0759..a5da0e4 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt @@ -764,6 +764,10 @@ private fun DayColumnCard( val hourPx = with(LocalDensity.current) { hourHeight.toPx() } val showHourLines = LocalShowHourLines.current val hourLineColor = MaterialTheme.colorScheme.outlineVariant + // Tells a settled drop when this column has caught up with it. + LaunchedEffect(blocks, dragController.settling) { + dragController.noteGrid(date, blocks) + } Card( // Plain rectangular columns — the soft corners come from the outer // rounded scroll viewport, so inner rounding would look odd at the edges. diff --git a/floret-kit b/floret-kit index 0ce855c..fd86bc4 160000 --- a/floret-kit +++ b/floret-kit @@ -1 +1 @@ -Subproject commit 0ce855c925c9e7af64f10b2595bf151f59f37bd5 +Subproject commit fd86bc4a24675f4fc807d411db6991f2b3ecb7a3