Fix review findings on selection, resize and chips (#141)
This commit is contained in:
@@ -58,6 +58,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventActionsViewModel
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventMoveHost
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventSelectionHost
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventActionFailureMessage
|
||||
import de.jeanlucmakiola.calendula.ui.common.rememberFabSelection
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventMoveScope
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||
@@ -600,18 +601,17 @@ fun CalendarHost(
|
||||
|
||||
// Scope prompt + confirmation/undo snackbar for a dropped event, declared
|
||||
// right after the calendar views so later overlays cover it.
|
||||
val actionFailure by eventActions.failure.collectAsStateWithLifecycle()
|
||||
EventMoveHost(
|
||||
reschedule,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
raised = chromeHost.selectionBarShown && chromeHost.selectionBarExpanded,
|
||||
actionFailure = eventActionFailureMessage(actionFailure),
|
||||
onActionFailureShown = eventActions::consumeFailure,
|
||||
)
|
||||
|
||||
// The delete prompt and failure chip of a selected event's actions (#141).
|
||||
EventSelectionHost(
|
||||
host = chromeHost,
|
||||
viewModel = eventActions,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
EventSelectionHost(host = chromeHost, viewModel = eventActions)
|
||||
|
||||
// Search overlay — below detail/edit in the Box so a tapped result's
|
||||
// detail screen draws on top, and closing it returns to the results.
|
||||
|
||||
@@ -250,7 +250,7 @@ fun CalendarFab(host: ChromeHost, todayInToolbar: Boolean, selection: FabSelecti
|
||||
todayText = chrome.todayText,
|
||||
onToday = { host.actions?.onToday?.invoke() },
|
||||
onCreate = { host.actions?.onCreate?.invoke() },
|
||||
selection = selection.takeIf { host.selection != null },
|
||||
selection = selection,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ fun EventMoveHost(
|
||||
viewModel: RescheduleViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
raised: Boolean = false,
|
||||
actionFailure: String? = null,
|
||||
onActionFailureShown: () -> Unit = {},
|
||||
) {
|
||||
val prompt by viewModel.scopePrompt.collectAsStateWithLifecycle()
|
||||
val outcome by viewModel.outcome.collectAsStateWithLifecycle()
|
||||
@@ -78,8 +80,10 @@ 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.
|
||||
val message = when (outcome) {
|
||||
null -> null
|
||||
null -> actionFailure
|
||||
is MoveOutcome.Moved -> if (resizedLabel != null) {
|
||||
stringResource(R.string.event_resize_done, resizedLabel)
|
||||
} else {
|
||||
@@ -104,6 +108,11 @@ fun EventMoveHost(
|
||||
// tapped in the last moments of the window leaves the outcome at Moved on
|
||||
// purpose, and this timer would otherwise fire mid-undo and close the chip
|
||||
// just before the same chip has to say "undone".
|
||||
LaunchedEffect(actionFailure) {
|
||||
if (actionFailure == null) return@LaunchedEffect
|
||||
delay(CHIP_MILLIS)
|
||||
onActionFailureShown()
|
||||
}
|
||||
LaunchedEffect(outcome, undoTick, writeInFlight) {
|
||||
if (outcome == null || writeInFlight) return@LaunchedEffect
|
||||
delay(if (outcome == MoveOutcome.Undone) UNDONE_CHIP_MILLIS else CHIP_MILLIS)
|
||||
@@ -130,7 +139,7 @@ fun EventMoveHost(
|
||||
// write anyway, so offering it would be a button that does nothing.
|
||||
val undo = content.undo?.takeIf { !writeInFlight }
|
||||
SnackChip(
|
||||
visible = outcome != null,
|
||||
visible = outcome != null || actionFailure != null,
|
||||
message = content.message,
|
||||
maxWidth = chipMaxWidth,
|
||||
actionLabel = stringResource(R.string.event_move_undo).takeIf { undo != null },
|
||||
@@ -162,5 +171,4 @@ private fun formatMovedTo(
|
||||
|
||||
/** "10:30" — the end of a resized event, read in the device zone like its start. */
|
||||
private fun formatTime(millis: Long, use24Hour: Boolean, locale: Locale): String =
|
||||
localizedDateFormatter(locale, if (use24Hour) "Hm" else "hm")
|
||||
.format(Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()))
|
||||
timeOfDayFormatter(use24Hour, locale).format(Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()))
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
package de.jeanlucmakiola.calendula.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -23,17 +14,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.domain.EventForm
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.floret.components.SnackChip
|
||||
import de.jeanlucmakiola.floret.components.SnackChipHeight
|
||||
import de.jeanlucmakiola.floret.components.SnackChipMargin
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** How far the bottom chips rise while the selection bar is open, to clear it. */
|
||||
val SelectionBarClearance = 64.dp + SnackChipMargin
|
||||
|
||||
private const val FAILURE_CHIP_MILLIS = 4_000L
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -95,57 +81,21 @@ fun rememberFabSelection(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* What else a selected event's actions bring up over the calendar (#141): the
|
||||
* delete confirmation, and a chip for an action that failed.
|
||||
*/
|
||||
/** What a failed action on a selected event says, for the move host's chip to show (#141). */
|
||||
@Composable
|
||||
fun EventSelectionHost(
|
||||
host: ChromeHost,
|
||||
viewModel: EventActionsViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun eventActionFailureMessage(failure: EventActionFailure?): String? = when (failure) {
|
||||
null -> null
|
||||
EventActionFailure.LoadFailed -> stringResource(R.string.event_action_load_failed)
|
||||
EventActionFailure.ShareFailed -> stringResource(R.string.event_share_failed)
|
||||
EventActionFailure.DeleteFailed -> stringResource(R.string.event_delete_failed)
|
||||
EventActionFailure.WriteDenied -> stringResource(R.string.event_delete_write_denied)
|
||||
EventActionFailure.Gone -> stringResource(R.string.event_move_gone)
|
||||
}
|
||||
|
||||
/** The delete confirmation a selected event's Delete brings up (#141). */
|
||||
@Composable
|
||||
fun EventSelectionHost(host: ChromeHost, viewModel: EventActionsViewModel) {
|
||||
val deletePrompt by viewModel.deletePrompt.collectAsStateWithLifecycle()
|
||||
val failure by viewModel.failure.collectAsStateWithLifecycle()
|
||||
|
||||
val message = when (failure) {
|
||||
null -> null
|
||||
EventActionFailure.LoadFailed -> stringResource(R.string.event_action_load_failed)
|
||||
EventActionFailure.ShareFailed -> stringResource(R.string.event_share_failed)
|
||||
EventActionFailure.DeleteFailed -> stringResource(R.string.event_delete_failed)
|
||||
EventActionFailure.WriteDenied -> stringResource(R.string.event_delete_write_denied)
|
||||
EventActionFailure.Gone -> stringResource(R.string.event_move_gone)
|
||||
}
|
||||
val shownMessage = remember { mutableStateOf("") }
|
||||
if (message != null) shownMessage.value = message
|
||||
LaunchedEffect(failure) {
|
||||
if (failure == null) return@LaunchedEffect
|
||||
delay(FAILURE_CHIP_MILLIS)
|
||||
viewModel.consumeFailure()
|
||||
}
|
||||
val barOpen = host.selectionBarShown && host.selectionBarExpanded
|
||||
BoxWithConstraints(modifier.fillMaxSize()) {
|
||||
// Clear of the FAB's band, as the move chip is.
|
||||
val chipMaxWidth = maxWidth - SnackChipMargin * 2 - SelectionBarClearance
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
start = SnackChipMargin,
|
||||
bottom = SnackChipMargin + if (barOpen) SelectionBarClearance else 0.dp,
|
||||
)
|
||||
.height(SnackChipHeight),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
SnackChip(
|
||||
visible = failure != null,
|
||||
message = shownMessage.value,
|
||||
maxWidth = chipMaxWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deletePrompt?.let { prompt ->
|
||||
DeleteEventDialog(
|
||||
isRecurring = prompt.isRecurring,
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.jeanlucmakiola.calendula.ui.week.MINUTES_PER_DAY
|
||||
import de.jeanlucmakiola.floret.locale.currentLocale
|
||||
|
||||
/** Width of the hour gutter down the start edge of the day and week timelines. */
|
||||
@@ -136,7 +137,8 @@ fun HourGutter(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = formatGutterTime(startMin, use24Hour, locale),
|
||||
// A resized end can sit on midnight: 00:00, not a clamped 23:59.
|
||||
text = formatGutterTime(startMin % MINUTES_PER_DAY, use24Hour, locale),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
maxLines = 1,
|
||||
|
||||
@@ -491,10 +491,7 @@ class RescheduleViewModel @Inject constructor(
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the dragged occurrence now begins, in the same anchoring the views
|
||||
* read dates back in: UTC midnight for an all-day event.
|
||||
*/
|
||||
/** Where a resized occurrence now ends; null for a move, which keeps its length. */
|
||||
private fun resizedEndMillis(prepared: PreparedMove): Long? {
|
||||
if (prepared.request.target !is MoveTarget.Span) return null
|
||||
val form = prepared.updated
|
||||
@@ -502,6 +499,10 @@ class RescheduleViewModel @Inject constructor(
|
||||
.toEpochMilliseconds()
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the dragged occurrence now begins, in the same anchoring the views
|
||||
* read dates back in: UTC midnight for an all-day event.
|
||||
*/
|
||||
private fun movedStartMillis(prepared: PreparedMove): Long {
|
||||
val form = prepared.updated
|
||||
return if (form.isAllDay) {
|
||||
|
||||
@@ -32,7 +32,7 @@ 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.
|
||||
* 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.
|
||||
@@ -40,6 +40,7 @@ private val HANDLE_RING = 2.dp
|
||||
@Composable
|
||||
fun ResizeHandle(
|
||||
color: Color,
|
||||
enabled: Boolean,
|
||||
onStart: (pointerInRoot: Offset) -> Unit,
|
||||
onMove: (pointerInRoot: Offset) -> Unit,
|
||||
onEnd: () -> Unit,
|
||||
@@ -51,6 +52,9 @@ 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.
|
||||
val currentEnabled by rememberUpdatedState(enabled)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
@@ -60,6 +64,7 @@ fun ResizeHandle(
|
||||
.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown()
|
||||
if (!currentEnabled) return@awaitEachGesture
|
||||
down.consume()
|
||||
var started = false
|
||||
var ended = false
|
||||
|
||||
@@ -78,7 +78,7 @@ private fun formatSelectionWhen(event: EventInstance, use24Hour: Boolean, locale
|
||||
val start = event.start.toJavaInstant().atZone(zone)
|
||||
val end = event.end.toJavaInstant().atZone(zone)
|
||||
val day = localizedDateFormatter(locale, "EEEdMMM")
|
||||
val time = localizedDateFormatter(locale, if (use24Hour) "Hm" else "hm")
|
||||
val time = timeOfDayFormatter(use24Hour, locale)
|
||||
val endsSameDay = end.toLocalDate() == start.toLocalDate() ||
|
||||
(end.toLocalTime().toSecondOfDay() == 0 && end.toLocalDate() == start.toLocalDate().plusDays(1))
|
||||
return if (endsSameDay) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -112,16 +113,12 @@ data class TimelineDrag(
|
||||
val slot: Triple<LocalDate, Int, Int> get() = Triple(date, startMin, eventSpanMin)
|
||||
}
|
||||
|
||||
/**
|
||||
* The event a long press left selected (#141): lifted on the grid, with the
|
||||
* action bar up. [date] is the column the press landed in. [moved] holds from a
|
||||
* move or resize of it until the grid shows where it landed.
|
||||
*/
|
||||
/** The event a long press left selected (#141). */
|
||||
data class TimelineSelection(
|
||||
val event: EventInstance,
|
||||
val date: LocalDate,
|
||||
/** Set from a move or resize of it until the grid shows where it landed. */
|
||||
val moved: Boolean = false,
|
||||
/** Selected by letting go of a lifted block: the block takes the lift over as it is. */
|
||||
/** Selected by letting go of a lifted block, which then keeps that lift as it is. */
|
||||
val fromLift: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -413,8 +410,9 @@ class TimelineDragController {
|
||||
var selection: TimelineSelection? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
fun select(event: EventInstance, date: LocalDate, moved: Boolean = false, fromLift: Boolean = false) {
|
||||
selection = TimelineSelection(event, date, moved, fromLift)
|
||||
fun select(event: EventInstance, moved: Boolean = false, fromLift: Boolean = false) {
|
||||
if (selection?.event?.instanceId != event.instanceId) selectionCovered = false
|
||||
selection = TimelineSelection(event, moved, fromLift)
|
||||
}
|
||||
|
||||
fun deselect() {
|
||||
@@ -422,6 +420,14 @@ class TimelineDragController {
|
||||
selectionCovered = false
|
||||
}
|
||||
|
||||
/** How many blocks on screen show the selected event; none for a while ends the selection. */
|
||||
var selectedBlocks: Int by mutableIntStateOf(0)
|
||||
private set
|
||||
|
||||
fun noteSelectedBlock(shown: Boolean) {
|
||||
selectedBlocks += if (shown) 1 else -1
|
||||
}
|
||||
|
||||
/** Whether the selected block's bottom sits where the action bar would cover it. */
|
||||
var selectionCovered: Boolean by mutableStateOf(false)
|
||||
private set
|
||||
@@ -535,7 +541,7 @@ class TimelineDragController {
|
||||
) {
|
||||
// Picking up another event ends the selection; the selected one keeps
|
||||
// it through the drag, and the screen moves it along with the drop.
|
||||
if (!isSelected(block)) selection = null
|
||||
if (!isSelected(block)) deselect()
|
||||
source = block
|
||||
this.clipOffsetMin = clipOffsetMin
|
||||
this.titleLines = titleLines
|
||||
@@ -647,19 +653,24 @@ class TimelineDragController {
|
||||
pointerInRoot: Offset,
|
||||
) {
|
||||
val start = block.event.start.toLocalDateTime(zone)
|
||||
val end = block.event.end.toLocalDateTime(zone)
|
||||
source = block
|
||||
resizeEdge = edge
|
||||
resizeDay = start.date
|
||||
resizeStartMin = start.time.toSecondOfDay() / 60
|
||||
resizeEndMin = resizeStartMin +
|
||||
(block.event.end - block.event.start).inWholeMinutes.toInt().coerceAtLeast(0)
|
||||
// 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.
|
||||
resizeEndMin = maxOf(
|
||||
resizeStartMin,
|
||||
(end.date.toEpochDays() - start.date.toEpochDays()).toInt() * MINUTES_PER_DAY +
|
||||
end.time.toSecondOfDay() / 60,
|
||||
)
|
||||
clipOffsetMin = 0
|
||||
this.titleLines = titleLines
|
||||
settling = null
|
||||
isDragging = true
|
||||
liftedInstanceId = block.event.instanceId
|
||||
pointer = pointerInRoot
|
||||
resizeGrabMin = 0f
|
||||
resizeGrabMin = (pointerResizeMinutes() ?: 0f) -
|
||||
if (edge == ResizeEdge.Top) resizeStartMin else resizeEndMin
|
||||
originSlot = null
|
||||
|
||||
@@ -80,6 +80,7 @@ import androidx.compose.ui.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalViewMorph
|
||||
import de.jeanlucmakiola.calendula.ui.common.StayPut
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
@@ -533,7 +534,6 @@ private fun WeekContent(
|
||||
val start = drop.startInstant(zone)
|
||||
dragController.select(
|
||||
drop.event.copy(start = start, end = start + (drop.event.end - drop.event.start)),
|
||||
start.toLocalDateTime(zone).date,
|
||||
moved = true,
|
||||
)
|
||||
}
|
||||
@@ -551,7 +551,6 @@ private fun WeekContent(
|
||||
dragController.onResize = { drop: TimelineResizeDrop ->
|
||||
dragController.select(
|
||||
drop.event.copy(start = drop.start, end = drop.end),
|
||||
drop.start.toLocalDateTime(zone).date,
|
||||
moved = true,
|
||||
)
|
||||
move?.move(
|
||||
@@ -578,6 +577,14 @@ private fun WeekContent(
|
||||
snapshotFlow { pagerState.currentPage }.drop(1).collect { dragController.deselect() }
|
||||
}
|
||||
LaunchedEffect(morph != null) { if (morph != null) dragController.deselect() }
|
||||
val orphaned = selection != null && dragController.selectedBlocks == 0 &&
|
||||
!dragController.isDragging && dragController.settling == null
|
||||
LaunchedEffect(orphaned) {
|
||||
if (!orphaned) return@LaunchedEffect
|
||||
// A beat for a re-read to bring the event back under a new row.
|
||||
delay(SELECTION_ORPHAN_GRACE_MILLIS)
|
||||
dragController.deselect()
|
||||
}
|
||||
val openEvent: (EventInstance) -> Unit = { event ->
|
||||
dragController.deselect()
|
||||
onEventClick(event)
|
||||
@@ -1577,14 +1584,14 @@ private fun EventBlock(
|
||||
PickUp.Drag
|
||||
} else {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
dragController.select(block.event, date)
|
||||
dragController.select(block.event)
|
||||
PickUp.Hold
|
||||
}
|
||||
},
|
||||
onMove = dragController::move,
|
||||
onDrop = { dragController.finish()?.let(onDrop) },
|
||||
onCancel = dragController::cancel,
|
||||
onRelease = { dragController.select(block.event, date, fromLift = draggable) },
|
||||
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
|
||||
@@ -1606,6 +1613,10 @@ private fun EventBlock(
|
||||
// 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.
|
||||
DisposableEffect(selected) {
|
||||
if (selected) dragController.noteSelectedBlock(true)
|
||||
onDispose { if (selected) dragController.noteSelectedBlock(false) }
|
||||
}
|
||||
val showHandles = selected && draggable
|
||||
val handlesShown = !lifted && !dragController.isDragging
|
||||
Box(
|
||||
@@ -1679,6 +1690,7 @@ private fun EventBlock(
|
||||
if (!cuts.top) {
|
||||
ResizeHandle(
|
||||
color = accent,
|
||||
enabled = handlesShown,
|
||||
onStart = begin(ResizeEdge.Top),
|
||||
onMove = dragController::move,
|
||||
onEnd = { end() },
|
||||
@@ -1692,6 +1704,7 @@ private fun EventBlock(
|
||||
if (!cuts.bottom) {
|
||||
ResizeHandle(
|
||||
color = accent,
|
||||
enabled = handlesShown,
|
||||
onStart = begin(ResizeEdge.Bottom),
|
||||
onMove = dragController::move,
|
||||
onEnd = { end() },
|
||||
@@ -1706,6 +1719,8 @@ private fun EventBlock(
|
||||
}
|
||||
}
|
||||
|
||||
private const val SELECTION_ORPHAN_GRACE_MILLIS = 500L
|
||||
|
||||
/** How far in from the block's side a resize handle's centre sits. */
|
||||
private val HANDLE_INSET = 16.dp
|
||||
|
||||
|
||||
Reference in New Issue
Block a user