Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-26 14:13:06 +04:00
parent e1ef1c4b8a
commit 03f6c241ac
12 changed files with 680 additions and 16 deletions

View file

@ -97,6 +97,7 @@ private fun Preview_Grid() {
},
content = {
GridItems(
itemPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing4),
items = persistentListOf(
stringReference("Fist item"),
stringReference("Second item"),
@ -104,6 +105,7 @@ private fun Preview_Grid() {
itemContent = {
PreviewItem(text = it)
},
horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
)
},
)
@ -141,6 +143,7 @@ private fun Preview_List() {
itemContent = {
PreviewItem(it)
},
verticalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
)
},
)
@ -159,7 +162,7 @@ private fun Preview_Plain() {
) {
TooltipText(
text = stringReference("Title"),
onInfoClick = { /* [REDACTED_TODO_COMMENT]*/ },
onInfoClick = { },
)
Text(
@ -176,6 +179,38 @@ private fun Preview_Plain() {
}
}
@Composable
@Preview(showBackground = true, widthDp = 328)
@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_Tree() {
TangemThemePreview {
InformationBlock(
title = {
TooltipText(
text = stringReference("Tree title"),
onInfoClick = { },
)
},
content = {
ArrowRowItems(
itemPadding = PaddingValues(vertical = TangemTheme.dimens.spacing4),
items = persistentListOf(
stringReference("Fist item"),
stringReference("Second item"),
stringReference("Third item"),
),
rootContent = {
PreviewItem(stringReference("Root"))
},
itemContent = {
PreviewItem(it)
},
)
},
)
}
}
@Composable
private fun PreviewItem(text: TextReference) {
Box(

View file

@ -8,26 +8,30 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.rows.ArrowRow
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun <T : Any> InformationBlockContentScope.ListItems(
inline fun <T : Any> InformationBlockContentScope.ListItems(
items: ImmutableList<T>,
itemContent: @Composable BoxScope.(T) -> Unit,
modifier: Modifier = Modifier,
itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
verticalArragement: Arrangement.Vertical = Arrangement.Top,
) {
Column(
modifier = modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8, Alignment.Top),
modifier = modifier.fillMaxWidth(),
horizontalAlignment = horizontalAlignment,
verticalArrangement = verticalArragement,
) {
items.fastForEach { item ->
Box(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(itemPadding)
.fillMaxWidth(),
) {
itemContent(item)
}
@ -36,10 +40,13 @@ fun <T : Any> InformationBlockContentScope.ListItems(
}
@Composable
fun <T : Any> InformationBlockContentScope.GridItems(
inline fun <T : Any> InformationBlockContentScope.GridItems(
items: ImmutableList<T>,
itemContent: @Composable BoxScope.(T) -> Unit,
modifier: Modifier = Modifier,
itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
verticalAlignment: Alignment.Vertical = Alignment.Top,
horizontalArragement: Arrangement.Horizontal = Arrangement.Start,
) {
val rowItems by remember(items) {
derivedStateOf {
@ -57,15 +64,15 @@ fun <T : Any> InformationBlockContentScope.GridItems(
) {
rowItems.fastForEach { row ->
Row(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.fillMaxWidth(),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12, Alignment.Start),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = verticalAlignment,
horizontalArrangement = horizontalArragement,
) {
row.fastForEach { item ->
Box(
modifier = Modifier.weight(1f),
modifier = Modifier
.padding(itemPadding)
.weight(1f),
contentAlignment = Alignment.Center,
) {
itemContent(item)
@ -74,4 +81,33 @@ fun <T : Any> InformationBlockContentScope.GridItems(
}
}
}
}
@Composable
inline fun <T : Any> InformationBlockContentScope.ArrowRowItems(
items: ImmutableList<T>,
rootContent: @Composable BoxScope.() -> Unit,
itemContent: @Composable BoxScope.(T) -> Unit,
modifier: Modifier = Modifier,
itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart,
content = rootContent,
)
items.forEachIndexed { index, item ->
ArrowRow(
modifier = Modifier.fillMaxWidth(),
content = { itemContent(item) },
contentPadding = itemPadding,
isLastItem = index == items.size - 1,
)
}
}
}

View file

@ -0,0 +1,168 @@
package com.tangem.core.ui.components.rows
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.*
@Composable
inline fun ArrowRow(
isLastItem: Boolean,
content: @Composable() (BoxScope.() -> Unit),
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
) {
val density = LocalDensity.current.density
val defaultRowHeight = TangemTheme.dimens.size0
var itemHeight by remember { mutableStateOf(defaultRowHeight) }
Row(
modifier = modifier.onSizeChanged { size ->
val height = size.height.toFloat()
if (height != itemHeight.toPx(density)) {
itemHeight = convertPxToDp(px = height, density = density)
}
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
ChildArrow(
childHeight = itemHeight,
isLastChild = isLastItem,
)
Box(
modifier = Modifier
.padding(contentPadding)
.fillMaxWidth(),
contentAlignment = Alignment.CenterStart,
) {
content()
}
}
}
@Suppress("LongParameterList")
@Immutable
private class ChildArrowScope(
val figureRect: Rect,
val arrowHeadRect: Rect,
val curvedArrowRect: Rect,
val arrowStrokeWidth: Float,
val arrowHeadRadius: Float,
val strokeColor: Color,
drawScope: DrawScope,
) : DrawScope by drawScope
@Composable
fun ChildArrow(childHeight: Dp, isLastChild: Boolean) {
val figureWidth = TangemTheme.dimens.size40
val strokeColor = TangemTheme.colors.stroke.secondary
val arrowStrokeWidthDp = TangemTheme.dimens.size1
val arrowHeadRadiusDp = TangemTheme.dimens.size1
val figureRectDp = DpRect(
origin = DpOffset.Zero,
size = DpSize(width = TangemTheme.dimens.size40, height = childHeight),
)
val arrowHeadSize = DpSize(
width = TangemTheme.dimens.size6,
height = TangemTheme.dimens.size6,
)
val arrowHeadRectDp = DpRect(
origin = DpOffset(
x = figureWidth - arrowHeadSize.width,
y = figureRectDp.size.center.y - arrowHeadSize.center.y,
),
size = arrowHeadSize,
)
val curvedArrowRectDp = DpRect(
top = figureRectDp.top,
left = TangemTheme.dimens.size18,
right = figureRectDp.right - arrowHeadRectDp.width,
bottom = figureRectDp.size.center.y,
)
Canvas(
modifier = Modifier
.width(figureWidth)
.height(childHeight),
) {
val scope = ChildArrowScope(
figureRect = figureRectDp.toRect(),
arrowHeadRect = arrowHeadRectDp.toRect(),
curvedArrowRect = curvedArrowRectDp.toRect(),
arrowStrokeWidth = arrowStrokeWidthDp.toPx(),
arrowHeadRadius = arrowHeadRadiusDp.toPx(),
strokeColor = strokeColor,
drawScope = this,
)
scope.drawCurveArrow()
scope.drawArrowHead()
if (!isLastChild) {
scope.drawArrowLine()
}
}
}
private fun ChildArrowScope.drawArrowHead() {
val arrowHeadPath = Path().apply {
moveTo(arrowHeadRect.centerRight)
lineTo(arrowHeadRect.topLeft)
lineTo(arrowHeadRect.bottomLeft)
close()
}
val paint = Paint().apply {
color = strokeColor
style = PaintingStyle.Fill
pathEffect = PathEffect.cornerPathEffect(arrowHeadRadius)
}
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(arrowHeadPath),
paint = paint,
)
}
}
private fun ChildArrowScope.drawCurveArrow() {
val curveArrowPath = Path().apply {
moveTo(curvedArrowRect.topLeft)
quadraticBezierTo(
control = curvedArrowRect.bottomLeft,
end = curvedArrowRect.bottomRight,
)
}
drawPath(
path = curveArrowPath,
color = strokeColor,
style = Stroke(width = arrowStrokeWidth),
)
}
private fun ChildArrowScope.drawArrowLine() {
drawLine(
color = strokeColor,
start = curvedArrowRect.topLeft,
end = Offset(curvedArrowRect.left, figureRect.bottom),
strokeWidth = arrowStrokeWidth,
)
}

View file

@ -0,0 +1,99 @@
package com.tangem.core.ui.components.rows
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2737-2800&t=ewlXfWwbDnRhjw4B-4)
* */
@Composable
fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier) {
RowContentContainer(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size52)
.padding(
vertical = TangemTheme.dimens.spacing16,
horizontal = TangemTheme.dimens.spacing8,
),
icon = {
RowIcon(
modifier = Modifier.size(TangemTheme.dimens.size22),
resId = model.icon.resId,
isColored = model.icon.isColored,
showAccentBadge = model.isAccented,
)
},
text = {
RowTitleAndSubtitle(
title = model.name,
subtitle = model.type,
accentTitle = model.usePrimaryTextColor,
accentSubtitle = model.isAccented,
)
},
action = action,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvider::class) state: BlockchainRowUM) {
TangemThemePreview {
ArrowRow(
modifier = Modifier.background(TangemTheme.colors.background.primary),
isLastItem = false,
content = {
BlockchainRow(
model = state,
action = {
TangemSwitch(onCheckedChange = { /* [REDACTED_TODO_COMMENT]*/ }, checked = true)
},
)
},
)
}
}
private class BlockchainRowParameterProvider : CollectionPreviewParameterProvider<BlockchainRowUM>(
collection = listOf(
BlockchainRowUM(
name = stringReference("BNB BEACON CHAIN"),
type = stringReference("BEP20"),
icon = BlockchainRowUM.Icon(R.drawable.ic_bsc_16, isColored = false),
isAccented = true,
usePrimaryTextColor = false,
),
BlockchainRowUM(
name = stringReference("1234567890111213141516171819"),
type = stringReference("BEP20"),
icon = BlockchainRowUM.Icon(R.drawable.ic_bsc_16, isColored = false),
isAccented = false,
usePrimaryTextColor = true,
),
BlockchainRowUM(
name = stringReference("BNB BEACON CHAIN"),
type = stringReference("1234567890111213141516171819"),
icon = BlockchainRowUM.Icon(R.drawable.img_bsc_22, isColored = true),
isAccented = false,
usePrimaryTextColor = false,
),
),
)
// endregion Preview

View file

@ -0,0 +1,117 @@
package com.tangem.core.ui.components.rows
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1608-1147&t=ewlXfWwbDnRhjw4B-4)
* */
@Composable
fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composable BoxScope.() -> Unit = {}) {
RowContentContainer(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size68)
.padding(
vertical = TangemTheme.dimens.spacing12,
horizontal = TangemTheme.dimens.spacing14,
),
icon = {
RowIcon(
modifier = Modifier.size(TangemTheme.dimens.size36),
resId = model.icon.resId,
isColored = model.icon.isColored,
showAccentBadge = false,
)
},
text = {
RowTitleAndSubtitle(
title = model.name,
subtitle = model.type,
accentTitle = true,
accentSubtitle = false,
)
},
action = action,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ChainRow(@PreviewParameter(ChainRowParameterProvider::class) state: ChainRowUM) {
TangemThemePreview {
ChainRow(
modifier = Modifier.background(TangemTheme.colors.background.primary),
model = state,
action = {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
tint = TangemTheme.colors.icon.informative,
painter = painterResource(id = R.drawable.ic_chevron_24),
contentDescription = null,
)
}
},
)
}
}
private class ChainRowParameterProvider : CollectionPreviewParameterProvider<ChainRowUM>(
collection = listOf(
ChainRowUM(
name = stringReference("Cardano"),
type = stringReference("ADA"),
icon = ChainRowUM.Icon(
resId = R.drawable.img_cardano_22,
isColored = true,
),
),
ChainRowUM(
name = stringReference("Binance"),
type = stringReference("BNB"),
icon = ChainRowUM.Icon(
resId = R.drawable.ic_bsc_16,
isColored = false,
),
),
ChainRowUM(
name = stringReference("123456789010111213141516"),
type = stringReference("BNB"),
icon = ChainRowUM.Icon(
resId = R.drawable.ic_bsc_16,
isColored = false,
),
),
ChainRowUM(
name = stringReference("123456789010111213141516"),
type = stringReference("123456789010111213141516"),
icon = ChainRowUM.Icon(
resId = R.drawable.ic_bsc_16,
isColored = false,
),
),
),
)
// endregion Preview

View file

@ -0,0 +1,139 @@
package com.tangem.core.ui.components.rows
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal inline fun RowContentContainer(
icon: @Composable BoxScope.() -> Unit,
text: @Composable BoxScope.() -> Unit,
action: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8, Alignment.Start),
) {
Box(
contentAlignment = Alignment.Center,
content = icon,
)
Box(
modifier = Modifier
.weight(1f)
.heightIn(min = TangemTheme.dimens.size22),
contentAlignment = Alignment.CenterStart,
content = text,
)
Box(
modifier = Modifier
.requiredWidthIn(max = TangemTheme.dimens.size48)
.heightIn(min = TangemTheme.dimens.size24),
contentAlignment = Alignment.CenterEnd,
content = action,
)
}
}
@Composable
internal fun RowTitleAndSubtitle(
title: TextReference,
subtitle: TextReference,
accentTitle: Boolean,
accentSubtitle: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
Text(
modifier = Modifier.weight(weight = 10f, fill = false),
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = if (accentTitle) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = Modifier.weight(weight = 4f, fill = false),
text = subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = if (accentSubtitle) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
internal fun RowIcon(
@DrawableRes resId: Int,
isColored: Boolean,
showAccentBadge: Boolean,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
if (isColored) {
Image(
modifier = Modifier.matchParentSize(),
painter = painterResource(id = resId),
contentDescription = null,
)
} else {
Icon(
modifier = Modifier
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
)
.matchParentSize(),
painter = painterResource(id = resId),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
if (showAccentBadge) {
Badge(modifier = Modifier.align(Alignment.TopEnd))
}
}
}
@Composable
private fun Badge(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size6)
.background(
color = TangemTheme.colors.background.primary,
shape = CircleShape,
),
) {
Box(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing1)
.matchParentSize()
.background(
color = TangemTheme.colors.icon.accent,
shape = CircleShape,
),
)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.core.ui.components.rows.model
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
data class BlockchainRowUM(
val name: TextReference,
val type: TextReference,
val icon: Icon,
val isAccented: Boolean,
val usePrimaryTextColor: Boolean,
) {
@Immutable
data class Icon(
@DrawableRes val resId: Int,
val isColored: Boolean,
)
}

View file

@ -0,0 +1,19 @@
package com.tangem.core.ui.components.rows.model
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
data class ChainRowUM(
val name: TextReference,
val type: TextReference,
val icon: Icon,
) {
@Immutable
data class Icon(
@DrawableRes val resId: Int,
val isColored: Boolean,
)
}

View file

@ -45,6 +45,7 @@ data class TangemDimens internal constructor(
val size2: Dp = 2.dp,
val size4: Dp = 4.dp,
val size5: Dp = 5.dp,
val size6: Dp = 6.dp,
val size7: Dp = 7.dp,
val size8: Dp = 8.dp,
val size9: Dp = 9.dp,
@ -56,6 +57,7 @@ data class TangemDimens internal constructor(
val size16: Dp = 16.dp,
val size18: Dp = 18.dp,
val size20: Dp = 20.dp,
val size22: Dp = 22.dp,
val size24: Dp = 24.dp,
val size28: Dp = 28.dp,
val size30: Dp = 30.dp,

View file

@ -139,7 +139,7 @@ private fun lightThemeColors(): TangemColors {
),
stroke = TangemColors.Stroke(
primary = TangemColorPalette.Light2,
secondary = TangemColorPalette.Dark5,
secondary = TangemColorPalette.Light5,
transparency = TangemColorPalette.White,
),
field = TangemColors.Field(

View file

@ -0,0 +1,18 @@
package com.tangem.core.ui.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
@Stable
@Composable
fun Dp.toPx(): Float = toPx(density = LocalDensity.current.density)
@Stable
@Composable
fun convertPxToDp(px: Float): Dp = convertPxToDp(px, density = LocalDensity.current.density)
fun Dp.toPx(density: Float): Float = this.value * density
fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density)

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.utils
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path
fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y)
fun Path.moveTo(offset: Offset) = moveTo(offset.x, offset.y)
fun Path.quadraticBezierTo(control: Offset, end: Offset) = quadraticBezierTo(control.x, control.y, end.x, end.y)