Fold the selection bar while a chip shows and match it to the FAB (#141)

This commit is contained in:
2026-10-09 18:42:05 +02:00
parent 06959e7a69
commit f50095ab1e
6 changed files with 43 additions and 34 deletions
@@ -391,6 +391,8 @@ fun CalendarHost(
val reschedule: RescheduleViewModel = hiltViewModel()
val movableCalendarIds by reschedule.movableCalendarIds.collectAsStateWithLifecycle()
val eventActions: EventActionsViewModel = hiltViewModel()
val moveOutcome by reschedule.outcome.collectAsStateWithLifecycle()
val selectionFailure by eventActions.failure.collectAsStateWithLifecycle()
val onEditEvent: (EventInstance) -> Unit = { event ->
val key = longArrayOf(
event.eventId,
@@ -543,6 +545,11 @@ fun CalendarHost(
importFormSource = ImportSource.Insert
importForm = form
},
chipShown = moveOutcome != null || selectionFailure != null,
onDismissChip = {
reschedule.consumeOutcome()
eventActions.consumeFailure()
},
)
CalendarFab(chromeHost, todayInToolbar, fabSelection)
},
@@ -600,12 +607,10 @@ 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),
actionFailure = eventActionFailureMessage(selectionFailure),
onActionFailureShown = eventActions::consumeFailure,
)
@@ -90,10 +90,6 @@ class ChromeHost {
var selectionCompact by mutableStateOf(false)
internal set
/** Whether the selection's bar is unfolded, rather than folded into its button. */
var selectionBarExpanded by mutableStateOf(false)
internal set
/** Whether the selection's action bar is showing — it steps aside mid-gesture. */
val selectionBarShown: Boolean get() = selection != null && !selectionBusy
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
@@ -90,8 +89,6 @@ fun CalendarFabColumn(
) {
AnimatedVisibility(
visible = selection?.expanded == true,
// Overflows the row evenly above and below, centred on the FAB.
modifier = Modifier.wrapContentHeight(unbounded = true),
enter = expandHorizontally(
animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(),
expandFrom = Alignment.End,
@@ -51,7 +51,6 @@ private val FAB_BAND = 88.dp
fun EventMoveHost(
viewModel: RescheduleViewModel,
modifier: Modifier = Modifier,
raised: Boolean = false,
actionFailure: String? = null,
onActionFailureShown: () -> Unit = {},
) {
@@ -126,11 +125,7 @@ fun EventMoveHost(
modifier = Modifier
.align(Alignment.BottomStart)
.navigationBarsPadding()
// Above the selection bar while it is up (#141).
.padding(
start = SnackChipMargin,
bottom = SnackChipMargin + if (raised) SelectionBarClearance else 0.dp,
)
.padding(start = SnackChipMargin, bottom = SnackChipMargin)
.height(SnackChipHeight),
contentAlignment = Alignment.CenterStart,
) {
@@ -1,23 +1,27 @@
package de.jeanlucmakiola.calendula.ui.common
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingToolbarDefaults
import androidx.compose.material3.HorizontalFloatingToolbar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
/** The selected event's action bar (#141); Edit and Delete only where its calendar allows. */
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun EventSelectionBar(
canEdit: Boolean,
@@ -28,15 +32,23 @@ fun EventSelectionBar(
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
HorizontalFloatingToolbar(
expanded = true,
colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(),
modifier = modifier,
// The FAB's height and colours, so the two read as one control.
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
shadowElevation = 6.dp,
modifier = modifier.height(56.dp),
) {
if (canEdit) BarAction(Icons.Default.Edit, R.string.event_detail_edit, onEdit)
BarAction(Icons.Default.ContentCopy, R.string.event_detail_duplicate, onDuplicate)
BarAction(Icons.Default.Share, R.string.event_detail_share, onShare)
if (canDelete) BarAction(Icons.Default.Delete, R.string.event_detail_delete, onDelete)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 4.dp),
) {
if (canEdit) BarAction(Icons.Default.Edit, R.string.event_detail_edit, onEdit)
BarAction(Icons.Default.ContentCopy, R.string.event_detail_duplicate, onDuplicate)
BarAction(Icons.Default.Share, R.string.event_detail_share, onShare)
if (canDelete) BarAction(Icons.Default.Delete, R.string.event_detail_delete, onDelete)
}
}
}
@@ -1,7 +1,6 @@
package de.jeanlucmakiola.calendula.ui.common
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -17,8 +16,8 @@ import de.jeanlucmakiola.calendula.domain.EventInstance
import de.jeanlucmakiola.floret.components.SnackChipMargin
import kotlinx.coroutines.launch
/** How far the bottom chips rise while the selection bar is open, to clear it. */
val SelectionBarClearance = 64.dp + SnackChipMargin
/** The bottom band the selection bar and FAB cover, margin included. */
val SelectionBarClearance = 56.dp + SnackChipMargin
/** The FAB's role while an event is selected (#141): its action bar's toggle. Null otherwise. */
@Composable
@@ -27,6 +26,8 @@ fun rememberFabSelection(
viewModel: EventActionsViewModel,
onEdit: (EventInstance) -> Unit,
onDuplicate: (EventForm) -> Unit,
chipShown: Boolean,
onDismissChip: () -> Unit,
): FabSelection? {
val calendars by viewModel.calendars.collectAsStateWithLifecycle()
val context = LocalContext.current
@@ -36,8 +37,8 @@ fun rememberFabSelection(
// What the FAB has been told for this selection, over what the coverage says.
var chosen by remember(host.selection?.event) { mutableStateOf<Boolean?>(null) }
val expanded = chosen ?: !host.selectionCompact
SideEffect { host.selectionBarExpanded = expanded && host.selection != null }
// Folded while a chip is up, which then sits beside the FAB as usual.
val expanded = !chipShown && (chosen ?: !host.selectionCompact)
val selection = host.selection ?: return null
val event = selection.event
@@ -45,7 +46,10 @@ fun rememberFabSelection(
val canEdit = calendar?.canModifyContents == true
return FabSelection(
expanded = expanded,
onToggle = { chosen = !expanded },
onToggle = {
if (chipShown) onDismissChip()
chosen = !expanded
},
bar = {
EventSelectionBar(
canEdit = canEdit,