Trim comments to the project's style (#141)

This commit is contained in:
2026-10-09 18:32:47 +02:00
parent 3fe2cd4e9a
commit 06959e7a69
12 changed files with 33 additions and 112 deletions
@@ -52,11 +52,7 @@ fun EventForm.shiftedByDays(days: Int, deviceZone: TimeZone): EventForm {
return copy(start = newStart, end = (newStart.toInstant(zone) + span).toLocalDateTime(zone))
}
/**
* The form with its start and end set independently — a timeline resize, which
* drags one edge and leaves the other where it stands. Each edge is resolved in
* the form's own zone, as [shiftedTo] does. All-day events are returned untouched.
*/
/** The form with new start and end, each in the form's own zone — a resize. All-day is untouched. */
fun EventForm.resizedTo(newStart: Instant, newEnd: Instant, deviceZone: TimeZone): EventForm {
if (isAllDay) return this
val zone = resolvedZone(deviceZone)
@@ -539,8 +539,7 @@ fun CalendarHost(
viewModel = eventActions,
onEdit = onEditEvent,
onDuplicate = { form ->
// As a duplicate from the detail screen: a fresh event,
// through the prefilled-create overlay (#52).
// A fresh event, like a duplicate from the detail screen (#52).
importFormSource = ImportSource.Insert
importForm = form
},
@@ -82,8 +82,7 @@ fun CalendarFabColumn(
// Held past the selection clearing, so the bar has something to fold away.
val heldBar = remember { mutableStateOf<(@Composable () -> Unit)?>(null) }
selection?.let { heldBar.value = it.bar }
// Held at the FAB's own height whatever the bar does: the bar is taller,
// and a row grown to fit it moved the FAB up as the bar unfolded.
// Held at the FAB's height so the taller bar can't shift the FAB.
Row(
modifier = Modifier.height(FAB_SIZE),
verticalAlignment = Alignment.CenterVertically,
@@ -121,11 +120,7 @@ fun CalendarFabColumn(
}
}
/**
* The FAB's glyph: "+", or with an event selected a chevron pointing where the
* bar unfolds — turned half round once it has, to fold it back. The "+" and the
* chevron spin past each other rather than fading.
*/
/** "+", or with an event selected a chevron that turns to open and fold the bar. */
@Composable
private fun FabGlyph(selecting: Boolean, expanded: Boolean) {
val spatial = MaterialTheme.motionScheme.fastSpatialSpec<Float>()
@@ -35,11 +35,7 @@ data class DeletePrompt(val event: EventInstance, val isRecurring: Boolean)
/** How an action on a selected event ended, when it has something to say. */
enum class EventActionFailure { LoadFailed, ShareFailed, DeleteFailed, WriteDenied, Gone }
/**
* The event actions a selected block offers outside the detail screen (#141):
* duplicate, share and delete, each loading the event on demand. Hosted at
* `CalendarHost` beside [RescheduleViewModel].
*/
/** Duplicate, share and delete for a selected event (#141), loading it on demand. */
@HiltViewModel
class EventActionsViewModel @Inject constructor(
private val repository: CalendarRepository,
@@ -116,8 +116,7 @@ private fun rememberPickUpSurface(
val currentDrop by rememberUpdatedState(onDrop)
val currentCancel by rememberUpdatedState(onCancel)
val currentRelease by rememberUpdatedState(onRelease)
// Read per gesture rather than keyed on: the pickup itself can change it,
// and a restart would cancel the drag it just began.
// Not a key: the pickup changes it, and a restart would cancel the drag.
val currentImmediate by rememberUpdatedState(immediate)
return Modifier
@@ -128,9 +127,7 @@ private fun rememberPickUpSurface(
val down = awaitFirstDown(requireUnconsumed = false)
val origin = down.position
val immediate = currentImmediate
// Null on the timeout — held still — true once an [immediate]
// block has been moved past slop, false when the press is
// abandoned.
// Null: held still. True: an immediate pickup past slop. False: abandoned.
val outcome = withTimeoutOrNull(holdMillis) {
var result: Boolean? = null
while (result == null) {
@@ -80,8 +80,7 @@ fun EventMoveHost(
val resizedLabel = moved?.endMillis?.let { end ->
"$movedLabel–${formatTime(end, use24Hour, locale)}"
}
// One chip for every message down here: a failed selection action (#141)
// shows when no move has anything to say, rather than stacking over it.
// A failed selection action (#141) shares the chip rather than stacking on it.
val message = when (outcome) {
null -> actionFailure
is MoveOutcome.Moved -> if (resizedLabel != null) {
@@ -16,12 +16,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import de.jeanlucmakiola.calendula.R
/**
* The action bar a selected event unrolls from the FAB (#141), in the FAB's own
* colours so it stands clear of the calendar behind it. Edit and Delete are
* offered only where the event's calendar allows them; a read-only event can
* still be duplicated into a writable calendar, or shared.
*/
/** The selected event's action bar (#141); Edit and Delete only where its calendar allows. */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun EventSelectionBar(
@@ -20,11 +20,7 @@ import kotlinx.coroutines.launch
/** How far the bottom chips rise while the selection bar is open, to clear it. */
val SelectionBarClearance = 64.dp + SnackChipMargin
/**
* What the FAB becomes while an event is selected (#141): the toggle for its
* action bar, folded while it would cover the selected event's bottom handle.
* Null with nothing selected.
*/
/** The FAB's role while an event is selected (#141): its action bar's toggle. Null otherwise. */
@Composable
fun rememberFabSelection(
host: ChromeHost,
@@ -29,13 +29,8 @@ private val HANDLE_DOT = 16.dp
private val HANDLE_RING = 2.dp
/**
* A grip on one edge of a selected event (#141). It takes the gesture from the
* first movement, with no hold — the event is already selected — and hands the
* finger on in root coordinates. A tap is swallowed rather than falling through
* to the slot under it. While not [enabled] it takes no new touch.
*
* No semantics of its own: TalkBack reaches the same change through the event's
* edit action.
* A grip on one edge of a selected event (#141), dragged without a hold. No
* semantics: TalkBack resizes through the event's edit action.
*/
@Composable
fun ResizeHandle(
@@ -52,8 +47,7 @@ fun ResizeHandle(
val currentMove by rememberUpdatedState(onMove)
val currentEnd by rememberUpdatedState(onEnd)
val currentCancel by rememberUpdatedState(onCancel)
// Read per gesture: a hidden handle takes no new touch, but turning it off
// mid-resize must not cancel the one it is running.
// Not a key: turning it off mid-resize must not cancel that resize.
val currentEnabled by rememberUpdatedState(enabled)
Box(
contentAlignment = Alignment.Center,
@@ -275,10 +275,8 @@ enum class ResizeEdge { Top, Bottom }
data class TimelineResizeDrop(val event: EventInstance, val start: Instant, val end: Instant)
/**
* The span after dragging [edge] to [edgeMin] in day [edgeDay], all in minutes
* from the start day's midnight.
* The edge snaps to the grid, and never comes closer than one snap step to the
* other end — an event can't be resized away to nothing, or turned inside out.
* The span after dragging [edge] to [edgeMin] in day [edgeDay], in minutes from the
* start day's midnight. Snapped, and at least one snap step long.
*/
internal fun resizedSpan(
edge: ResizeEdge,
@@ -287,9 +285,7 @@ internal fun resizedSpan(
edgeMin: Float,
edgeDay: Int? = null,
): Pair<Int, Int> {
// Dragged into a day past the other end — the start into a day after the
// event ends, the end into one before it starts: no change, rather than an
// event collapsed against its other end.
// An edge dragged into a day past the other end changes nothing.
if (edgeDay != null) {
val endDay = (endMin - 1).floorDiv(MINUTES_PER_DAY).coerceAtLeast(0)
val wrongWay = when (edge) {
@@ -305,11 +301,7 @@ internal fun resizedSpan(
}
}
/**
* [minutes] snapped to the grid, kept at [held] until the finger is three
* quarters of a step away from it: plain rounding flips between two slots
* whenever the finger rests near the line between them.
*/
/** [minutes] snapped, kept at [held] until three quarters of a step away. */
internal fun stickySnap(minutes: Float, held: Int?): Int {
if (held != null && kotlin.math.abs(minutes - held) < DRAG_SNAP_MINUTES * 0.75f) return held
return (minutes / DRAG_SNAP_MINUTES).roundToInt() * DRAG_SNAP_MINUTES
@@ -318,12 +310,7 @@ internal fun stickySnap(minutes: Float, held: Int?): Int {
/** How far past its column's side the finger has to go before a resize changes day. */
private const val COLUMN_HYSTERESIS = 0.25f
/**
* The column under a finger at [position] columns from the grid's start, kept at
* [held] until the finger is a [COLUMN_HYSTERESIS] into the next. A resize drags
* up and down: the finger drifts sideways as it goes, and in a narrow week column
* every drift over a border moved the edge by a whole day and back.
*/
/** The column under [position], kept at [held] until [COLUMN_HYSTERESIS] into another. */
internal fun stickyColumn(position: Float, held: Int?, lastIndex: Int): Int {
val under = position.toInt().coerceIn(0, lastIndex)
if (held == null) return under
@@ -433,13 +420,8 @@ class TimelineDragController {
private set
/**
* Whether [block] is the selected event. Matched on the series and start too,
* since the provider regenerates `Instances` rows and a re-read can bring the
* same occurrence back under a new instance id — and, just after a move, on
* what the event says, since moving one occurrence of a series writes it out
* as an exception row with an id of its own. Only then: two copies of one
* event would otherwise both match, and take the selection back and forth.
* A blank title matches nothing that way.
* Whether [block] is the selected event: by instance, by series and start, and
* just after a move by title and times, since a moved occurrence gets new ids.
*/
fun isSelected(block: TimedBlock): Boolean {
val current = selection ?: return false
@@ -457,11 +439,7 @@ class TimelineDragController {
)
}
/**
* Take over [block]'s event as the selection once the grid shows it, so the
* bar acts on the row that is there now — an exception written by a move has
* its own id, and an abandoned move left the original times standing.
*/
/** Point the selection at [block]'s event, so the bar acts on the row now there. */
fun adoptSelection(block: TimedBlock) {
val current = selection ?: return
if (current.event != block.event || current.moved) {
@@ -539,8 +517,7 @@ class TimelineDragController {
pointerInRoot: Offset,
blockInRoot: Offset,
) {
// Picking up another event ends the selection; the selected one keeps
// it through the drag, and the screen moves it along with the drop.
// The selected event keeps its selection through a drag; any other ends it.
if (!isSelected(block)) deselect()
source = block
this.clipOffsetMin = clipOffsetMin
@@ -658,8 +635,7 @@ class TimelineDragController {
resizeEdge = edge
resizeDay = start.date
resizeStartMin = start.time.toSecondOfDay() / 60
// Wall clock at both ends, as the drop resolves them: across a DST
// change the event's length in minutes isn't its span on the clock.
// Wall clock at both ends, as the drop resolves them (DST-safe).
resizeEndMin = maxOf(
resizeStartMin,
(end.date.toEpochDays() - start.date.toEpochDays()).toInt() * MINUTES_PER_DAY +
@@ -767,11 +743,7 @@ class TimelineDragController {
)
}
/**
* Note where the selected block's bottom edge, [extraPx] of handle included,
* sits against the action bar [clearancePx] tall over the bottom of the
* viewport. Under it, the bar draws itself compact out of the way (#141).
*/
/** Note whether the action bar would cover the selected block's bottom handle (#141). */
fun noteSelectedBottom(block: LayoutCoordinates, extraPx: Float, clearancePx: Float) {
val viewport = geometry.viewport?.takeIf { it.isAttached } ?: return
if (!block.isAttached) return
@@ -938,12 +910,7 @@ fun TimedBlock.continuesAfter(day: LocalDate, zone: TimeZone): Boolean =
fun TimelineDrop.startInstant(zone: TimeZone): Instant =
wallClockInstant(date, startMin - clipOffsetMin, zone)
/**
* [minutes] past [date]'s midnight as an instant, read as wall clock in [zone].
* Resolved as a day plus a minute of it rather than as an offset from an
* instant, which would shift by an hour across a DST boundary; a block can be
* dragged above its midnight and past the next one, so either end may overflow.
*/
/** [minutes] past [date]'s midnight, either way past it, as wall clock in [zone] (DST-safe). */
fun wallClockInstant(date: LocalDate, minutes: Int, zone: TimeZone): Instant {
val day = date.plus(minutes.floorDiv(MINUTES_PER_DAY), DateTimeUnit.DAY)
val minute = minutes.mod(MINUTES_PER_DAY)
@@ -1207,11 +1174,7 @@ private fun DragCopy(
}
}
/**
* How far a selected block is lifted, between 0 and 1 (#141). [snap] takes the
* lift over at once — from the floating copy it replaces — rather than raising
* the block a second time.
*/
/** How far a selected block is lifted, 0 to 1 (#141); [snap] takes it over at once. */
@Composable
fun selectionLift(selected: Boolean, snap: Boolean = false): Float {
val reduceMotion = rememberReduceMotion()
@@ -563,8 +563,7 @@ private fun WeekContent(
)
}
// A long-pressed event stays selected (#141) until something else is touched:
// back, another page, a view switch, or any tap.
// Selection (#141) ends on back, a page or view change, or any tap.
val selection = dragController.selection
PublishSelection(
event = selection?.event,
@@ -1562,9 +1561,7 @@ private fun EventBlock(
val clipOffset = remember(block, date, zone) { block.clipOffsetMinutes(date, zone) }
val shape = remember(cuts) { timedBlockShape(cuts.top, cuts.bottom) }
val topInsetPx = with(density) { topInset.toPx() }
// A long press lifts the block (#141): moved, it drags; let go where it was
// picked up, it stays selected. A block that can't move is selected on the
// spot. Once selected, it drags without another hold.
// Long press (#141): move to drag, let go to select; selected blocks drag at once.
val dragModifier = rememberEventDragSource(
enabled = selectable,
key = block.event.instanceId,
@@ -1594,25 +1591,20 @@ private fun EventBlock(
onRelease = { dragController.select(block.event, fromLift = draggable) },
)
val lifted = draggable && dragController.ghosts(block)
// Let go of without moving, the block takes the floating copy's place as it
// stands: whole and lifted at once, not faded back up and raised again.
// Released in place, the block takes over the copy's lift without a dip.
val takesOverLift = selected && dragController.selection?.fromLift == true
val ghostFade = ghostAlpha(lifted)
val ghost = if (takesOverLift && !lifted) 1f else ghostFade
val raise = selectionLift(selected, snap = takesOverLift)
// Where the selected event's bottom handle sits against the action bar, which
// compacts itself rather than cover it. Read off the piece that ends the event.
// Tells the action bar to fold when it would cover the bottom handle.
val revealExtraPx = with(density) { (ResizeHandleTarget / 2).toPx() }
val barClearancePx = with(density) { SelectionBarClearance.toPx() }
val tracksBar = selected && !cuts.bottom
// Not while the block is waiting on its own write: until then it still
// shows the times from before it.
// Not while its own write is pending: it still shows the old times.
LaunchedEffect(selected, block.event, lifted) {
if (selected && !lifted) dragController.adoptSelection(block)
}
// Handles on the selected event's own ends — not on a cut at midnight. They
// stay composed for the whole selection and only hide while something is in
// flight: removing them would cancel the resize they started.
// Kept composed while hidden: removing them would cancel their own resize.
DisposableEffect(selected) {
if (selected) dragController.noteSelectedBlock(true)
onDispose { if (selected) dragController.noteSelectedBlock(false) }
@@ -680,8 +680,7 @@ class RescheduleViewModelTest {
)
advanceUntilIdle()
// The start changed day but not by a whole number of days, so the
// series anchor can't follow it.
// Not a whole-day shift, so the series anchor can't follow.
assertThat(vm.scopePrompt.value)
.isEqualTo(MoveScopePrompt(occurrenceOnly = true, resize = true))
}