Coverage Summary for Class: DashboardLayoutKt (com.georgeci.moneysurfer.feature.dashboard)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| DashboardLayoutKt |
100%
(19/19)
|
50%
(35/70)
|
100%
(75/75)
|
95.8%
(955/997)
|
| DashboardLayoutKt$DashboardColumn_TN_CM5M$lambda$0$0$$inlined$items$default$1 |
0%
(0/1)
|
|
| DashboardLayoutKt$DashboardColumn_TN_CM5M$lambda$0$0$$inlined$items$default$2 |
0%
(0/1)
|
|
| DashboardLayoutKt$DashboardColumn_TN_CM5M$lambda$0$0$$inlined$items$default$3 |
0%
(0/1)
|
|
| DashboardLayoutKt$DashboardColumn_TN_CM5M$lambda$0$0$$inlined$items$default$4 |
0%
(0/1)
|
|
| DashboardLayoutKt$DashboardGrid_TN_CM5M$lambda$0$0$0$$inlined$items$default$1 |
0%
(0/1)
|
|
| DashboardLayoutKt$DashboardGrid_TN_CM5M$lambda$0$0$0$$inlined$items$default$2 |
0%
(0/1)
|
|
| DashboardLayoutKt$DashboardGrid_TN_CM5M$lambda$0$0$0$$inlined$items$default$3 |
0%
(0/1)
|
|
| DashboardLayoutKt$DashboardGrid_TN_CM5M$lambda$0$0$0$$inlined$items$default$4 |
0%
(0/1)
|
|
| DashboardLayoutKt$DashboardGrid_TN_CM5M$lambda$0$0$0$$inlined$items$default$5 |
0%
(0/1)
|
|
| DashboardLayoutKt$WhenMappings |
|
| Total |
67.9%
(19/28)
|
50%
(35/70)
|
100%
(75/75)
|
95.8%
(955/997)
|
package com.georgeci.moneysurfer.feature.dashboard
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.domain.dashboard.DashboardLayoutItem
import com.georgeci.moneysurfer.domain.dashboard.DashboardWidgetSize
import com.georgeci.moneysurfer.domain.dashboard.DashboardWidgetSpan
import com.georgeci.moneysurfer.uikit.modifier.SurferContentMaxWidth
import com.georgeci.moneysurfer.uikit.modifier.SurferWideContentMaxWidth
import com.georgeci.moneysurfer.uikit.modifier.surferContentContainer
import com.georgeci.moneysurfer.uikit.widgets.LocalSurferWidgetSize
import com.georgeci.moneysurfer.uikit.widgets.SurferWidgetSize
import com.georgeci.moneysurfer.uikit.window.SurferWindowSize
import com.georgeci.moneysurfer.uikit.window.currentSurferWindowSize
/**
* Narrowest grid cell that still gets the Expanded widget treatments. A widget's Expanded density is
* drawn for a phone artboard — 392 dp, of which the widget's own side padding takes 32 — so a cell
* below that is where Compact starts being the honest choice. See [gridSize].
*/
private val DASHBOARD_EXPANDED_MIN_CELL_WIDTH = 392.dp
/** Row-to-row gap in the grid. Side gutters come from the widgets' own padding, this does not. */
private val DASHBOARD_GRID_ROW_SPACING = 8.dp
/**
* The dashboard's widgets, laid out for the window they are in: a single scrolling column below
* expanded width, a [DashboardWidgetSpan.COLUMNS]-column grid at 840 dp and up.
*
* Both read the same [DashboardState.Content.layout] in the same order, so switching between them is
* a matter of how much room each widget gets, never of which widgets are on screen or in what order.
*/
@Composable
internal fun DashboardWidgets(
state: DashboardState.Content,
topInset: Dp,
bottomInset: Dp,
onEvent: (DashboardEvent) -> Unit,
) {
val expanded = currentSurferWindowSize() >= SurferWindowSize.Expanded
// Resolved once, for both layouts: a widget that would draw nothing must not be handed a cell,
// or the grid row it sits in gets a hole where it stood down. See `rendersWidget`.
val items = state.layout.enabledItems.filter { state.rendersWidget(it.type) }
val modifier = Modifier
.fillMaxSize()
.surferContentContainer(if (expanded) SurferWideContentMaxWidth else SurferContentMaxWidth)
.padding(top = topInset)
if (expanded) {
DashboardGrid(state, items, bottomInset, onEvent, modifier)
} else {
DashboardColumn(state, items, bottomInset, onEvent, modifier)
}
}
/**
* The dashboard below expanded width: one widget per row, in layout order, at whatever size each one
* was configured with. [DashboardLayoutItem.span] is not read here — there is one column, so every
* span is the whole of it.
*/
@Composable
private fun DashboardColumn(
state: DashboardState.Content,
items: List<DashboardLayoutItem>,
bottomInset: Dp,
onEvent: (DashboardEvent) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
contentPadding = PaddingValues(bottom = bottomInset),
) {
if (state.periodSwitchVisible) {
item(key = DashboardTestTags.PeriodSwitch) {
DashboardPeriodSwitch(selected = state.period, onEvent = onEvent)
}
}
items(
items = items,
key = { it.type.name },
) { layoutItem ->
// Per-widget size: the enclosing screen no longer dictates one size for the column,
// so the Compact branch inside each widget is reachable from the layout config.
DashboardCell(
layoutItem = layoutItem,
size = layoutItem.cardStyle.size,
state = state,
onEvent = onEvent,
)
}
}
}
/**
* The dashboard from expanded width up: the same order, packed into a
* [DashboardWidgetSpan.COLUMNS]-column grid by each widget's span.
*
* No horizontal content padding and no column spacing, because every widget already draws its own
* 16 dp side padding — the gutters and the outer margin come out of the widgets themselves, exactly
* as they do in [DashboardColumn]. Only the vertical rhythm needs help: the column gets it from the
* widgets that pad themselves vertically, which is uneven once cards sit side by side.
*/
@Composable
private fun DashboardGrid(
state: DashboardState.Content,
items: List<DashboardLayoutItem>,
bottomInset: Dp,
onEvent: (DashboardEvent) -> Unit,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(modifier = modifier) {
val columnWidth = maxWidth / DashboardWidgetSpan.COLUMNS
LazyVerticalGrid(
columns = GridCells.Fixed(DashboardWidgetSpan.COLUMNS),
contentPadding = PaddingValues(bottom = bottomInset),
verticalArrangement = Arrangement.spacedBy(DASHBOARD_GRID_ROW_SPACING),
) {
if (state.periodSwitchVisible) {
item(
key = DashboardTestTags.PeriodSwitch,
span = { GridItemSpan(maxLineSpan) },
) {
DashboardPeriodSwitch(selected = state.period, onEvent = onEvent)
}
}
items(
items = items,
key = { it.type.name },
span = { GridItemSpan(it.span.columns) },
) { layoutItem ->
DashboardCell(
layoutItem = layoutItem,
size = layoutItem.gridSize(columnWidth),
state = state,
onEvent = onEvent,
)
}
}
}
}
/** One widget, drawn at [size] rather than at whatever the layout item itself was configured with. */
@Composable
private fun DashboardCell(
layoutItem: DashboardLayoutItem,
size: DashboardWidgetSize,
state: DashboardState.Content,
onEvent: (DashboardEvent) -> Unit,
) {
CompositionLocalProvider(LocalSurferWidgetSize provides size.toWidgetSize()) {
DashboardWidget(
type = layoutItem.type,
variant = layoutItem.cardStyle.variant,
state = state,
onEvent = onEvent,
)
}
}
/**
* The density a widget draws at in a grid cell [DashboardWidgetSpan.columns] columns wide.
*
* A narrow span is the third density the dashboard would otherwise need: the Expanded treatments are
* drawn for a phone's content width, and a third of an 840 dp window is well under that. Rather than
* add one, a cell too narrow to hold the Expanded card falls back to the Compact one the widget
* already implements — so the user's size choice still applies wherever there is room for it, and the
* same layout stays readable at 840 dp and at 1200.
*/
private fun DashboardLayoutItem.gridSize(columnWidth: Dp): DashboardWidgetSize =
if (columnWidth * span.columns >= DASHBOARD_EXPANDED_MIN_CELL_WIDTH) {
cardStyle.size
} else {
DashboardWidgetSize.Compact
}
private fun DashboardWidgetSize.toWidgetSize(): SurferWidgetSize = when (this) {
DashboardWidgetSize.Expanded -> SurferWidgetSize.Expanded
DashboardWidgetSize.Compact -> SurferWidgetSize.Compact
}