Coverage Summary for Class: SurferGoalProgressKt (com.georgeci.moneysurfer.uikit.components.goal)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferGoalProgressKt |
100%
(1/1)
|
80%
(4/5)
|
38%
(19/50)
|
95.9%
(70/73)
|
89.3%
(885/991)
|
package com.georgeci.moneysurfer.uikit.components.goal
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.uikit.preview.SurferComponentPreview
import com.georgeci.moneysurfer.uikit.theme.AppTheme
/**
* Flat progress bar for a goal card. v1 draws the fill only — the mockup's "expected today"
* tick ships with the forecast work (md/goals.md).
*/
@Composable
fun SurferGoalProgressBar(
progress: Float,
modifier: Modifier = Modifier,
color: Color = AppTheme.materialColors.primary,
height: Dp = 8.dp,
) {
Box(
modifier = modifier
.fillMaxWidth()
.height(height)
.clip(CircleShape)
.background(AppTheme.materialColors.surfaceContainerHighest),
) {
Box(
modifier = Modifier
.fillMaxWidth(progress.coerceIn(0f, 1f))
.fillMaxHeight()
.clip(CircleShape)
.background(color),
)
}
}
/**
* Details-screen hero: a progress ring with `% → saved → of target` stacked inside it.
*
* The arc is shared with the dashboard [com.georgeci.moneysurfer.uikit.widgets.SurferGoalsWidget]
* via [SurferGoalRingArc] so the two never drift apart.
*/
@Composable
fun SurferGoalProgressRing(
progress: Float,
percentLabel: String,
savedLabel: String,
targetLabel: String,
modifier: Modifier = Modifier,
size: Dp = 176.dp,
color: Color = AppTheme.materialColors.primary,
) {
Box(modifier = modifier.size(size), contentAlignment = Alignment.Center) {
SurferGoalRingArc(progress = progress, size = size, color = color)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = percentLabel,
style = AppTheme.typography.headlineMedium.copy(fontWeight = FontWeight.SemiBold),
color = AppTheme.materialColors.onSurface,
)
Text(
text = savedLabel,
style = AppTheme.typography.titleMedium,
color = AppTheme.materialColors.onSurface,
)
Text(
text = targetLabel,
style = AppTheme.typography.bodySmall,
color = AppTheme.materialColors.onSurfaceVariant,
)
}
}
}
/** The bare ring: track plus a rounded fill arc starting at twelve o'clock. */
@Composable
fun SurferGoalRingArc(
progress: Float,
size: Dp,
modifier: Modifier = Modifier,
color: Color = AppTheme.materialColors.primary,
) {
val track = AppTheme.materialColors.surfaceContainerHigh
Canvas(modifier = modifier.size(size)) {
val stroke = (this.size.minDimension / RING_STROKE_DIVISOR).coerceAtLeast(MIN_STROKE)
val inset = stroke / 2f
val box = Size(this.size.width - stroke, this.size.height - stroke)
val topLeft = Offset(inset, inset)
drawArc(
color = track,
startAngle = 0f,
sweepAngle = FULL_TURN,
useCenter = false,
topLeft = topLeft,
size = box,
style = Stroke(width = stroke),
)
rotate(degrees = QUARTER_TURN_BACK) {
drawArc(
color = color,
startAngle = 0f,
sweepAngle = FULL_TURN * progress.coerceIn(0f, 1f),
useCenter = false,
topLeft = topLeft,
size = box,
style = Stroke(width = stroke, cap = StrokeCap.Round),
)
}
}
}
private const val RING_STROKE_DIVISOR = 16f
private const val MIN_STROKE = 3f
private const val FULL_TURN = 360f
private const val QUARTER_TURN_BACK = -90f
@Preview
@Composable
private fun SurferGoalProgressPreview() {
SurferComponentPreview {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
SurferGoalProgressBar(progress = 0.62f)
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
SurferGoalProgressRing(
progress = 0.74f,
percentLabel = "74%",
savedLabel = "€8,915",
targetLabel = "of €12,000",
)
}
}
}
}