diff --git a/client/src/main/kotlin/io/spine/examples/shareaware/client/DrawScopeExts.kt b/client/src/main/kotlin/io/spine/examples/shareaware/client/DrawScopeExts.kt new file mode 100644 index 00000000..fda2c1d2 --- /dev/null +++ b/client/src/main/kotlin/io/spine/examples/shareaware/client/DrawScopeExts.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.shareaware.client + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import org.jetbrains.skia.Point + +/** + * Draws an Y axis inside the drawing area. + * + * @param color the color of the axis line + * @param axisWidth the width of the axis line + */ +public fun DrawScope.drawYAxis( + color: Color, + axisWidth: Float = 2f +) { + drawLine( + color = color, + start = Offset(0f, 0f), + end = Offset(0f, size.height), + strokeWidth = axisWidth + ) +} + +/** + * Draws an X axis inside the drawing area. + * + * @param color the color of the axis line + * @param axisWidth the width of the axis line + */ +public fun DrawScope.drawXAxis( + color: Color, + axisWidth: Float = 2f +) { + drawLine( + color = color, + start = Offset(0f, size.height - 0f), + end = Offset(size.width, size.height - 0f), + strokeWidth = axisWidth + ) +} + +/** + * Maps a list of points from their original range to pixel coordinates within the drawing area. + * + * @param points the list of points to map + */ +public fun DrawScope.mapToPixelPoints(points: List): List { + val minXValue = points.minOf { it.x } + val maxXValue = points.maxOf { it.x } + val minYValue = points.minOf { it.y } + val maxYValue = points.maxOf { it.y } + return points.map { + val x = it.x.mapToDifferentRange( + inMin = minXValue, + inMax = maxXValue, + outMin = 0f, + outMax = size.width + ) + val y = it.y.mapToDifferentRange( + inMin = minYValue, + inMax = maxYValue, + outMin = size.height, + outMax = 0f + ) + Point(x, y) + } +} + +/** + * Remaps a value from one range to another. + * + * @param inMin The minimum value of the original range + * @param inMax The maximum value of the original range + * @param outMin The minimum value of the target range + * @param outMax The maximum value of the target range + */ +private fun Float.mapToDifferentRange( + inMin: Float, + inMax: Float, + outMin: Float, + outMax: Float +): Float = (this - inMin) * (outMax - outMin) / (inMax - inMin) + outMin diff --git a/client/src/main/kotlin/io/spine/examples/shareaware/client/Theme.kt b/client/src/main/kotlin/io/spine/examples/shareaware/client/Theme.kt index f0e971ea..12f2fd2d 100644 --- a/client/src/main/kotlin/io/spine/examples/shareaware/client/Theme.kt +++ b/client/src/main/kotlin/io/spine/examples/shareaware/client/Theme.kt @@ -118,6 +118,12 @@ private val typography: Typography = Typography( fontWeight = FontWeight.Normal, fontFamily = sanFrancisco ), + headlineLarge = TextStyle( + fontSize = 30.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.5.sp, + fontFamily = sanFrancisco + ), bodyMedium = TextStyle( fontSize = 14.sp, fontWeight = FontWeight.SemiBold, diff --git a/client/src/main/kotlin/io/spine/examples/shareaware/client/component/Chart.kt b/client/src/main/kotlin/io/spine/examples/shareaware/client/component/Chart.kt new file mode 100644 index 00000000..6f638159 --- /dev/null +++ b/client/src/main/kotlin/io/spine/examples/shareaware/client/component/Chart.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2023, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.examples.shareaware.client.component + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import io.spine.examples.shareaware.client.drawXAxis +import io.spine.examples.shareaware.client.drawYAxis +import io.spine.examples.shareaware.client.mapToPixelPoints +import org.jetbrains.skia.Point + +/** + * Draws a chart. + * + * @param points chart points + */ +@Composable +public fun Chart(points: List) { + val axisColor = MaterialTheme.colors.onSecondary + val chartLineColor = MaterialTheme.colors.primary + Canvas( + modifier = Modifier.fillMaxSize() + ) { + drawYAxis(axisColor) + drawXAxis(axisColor) + val pixelPoints = mapToPixelPoints(points) + val path = Path() + pixelPoints.forEachIndexed { index, point -> + if (index == 0) { + path.moveTo(point.x, point.y) + } else { + path.lineTo(point.x, point.y) + } + } + drawPath( + path = path, + color = chartLineColor, + style = Stroke(width = 2f) + ) + } +} diff --git a/client/src/main/kotlin/io/spine/examples/shareaware/client/market/ShareProfileTab.kt b/client/src/main/kotlin/io/spine/examples/shareaware/client/market/ShareProfileTab.kt index 62721522..21c347c5 100644 --- a/client/src/main/kotlin/io/spine/examples/shareaware/client/market/ShareProfileTab.kt +++ b/client/src/main/kotlin/io/spine/examples/shareaware/client/market/ShareProfileTab.kt @@ -26,11 +26,13 @@ package io.spine.examples.shareaware.client.market +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -46,7 +48,6 @@ import androidx.compose.ui.unit.dp import io.spine.examples.shareaware.client.component.ContainerWithPopup import io.spine.examples.shareaware.client.component.PopupConfig import io.spine.examples.shareaware.client.component.PrimaryButton -import io.spine.examples.shareaware.client.share.ShareLogo import io.spine.examples.shareaware.client.share.SharePrice import io.spine.examples.shareaware.share.Share import io.spine.money.Money @@ -121,17 +122,51 @@ private fun ShareProfile( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - ShareLogo(share) + ShareInfo(share, previousPrice) + ButtonSection( + share = share, + purchaseModel = purchaseModel + ) + } +} + +/** + * Displays information about a particular share. + * + * @param share the share to display info about + * @param previousPrice the previous price of the share + */ +@Composable +private fun ShareInfo(share: Share, previousPrice: Money?) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom + ) { Text( text = share.companyName, - style = MaterialTheme.typography.labelMedium, + style = MaterialTheme.typography.headlineLarge, modifier = Modifier.padding(top = 10.dp) ) SharePrice(share.price, previousPrice) - ButtonSection( - share = share, - purchaseModel = purchaseModel - ) + } +} + +/** + * Displays container purposed for drawing chart inside it. + * + * @param chart the chart to draw inside the container + */ +@Composable +private fun ChartContainer(chart: @Composable () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(250.dp) + .background(MaterialTheme.colorScheme.secondary) + .padding(10.dp) + ) { + chart() } }