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