1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.graphics.shapes.testcompose
18 
19 import androidx.compose.foundation.clickable
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.offset
22 import androidx.compose.foundation.layout.size
23 import androidx.compose.foundation.shape.CircleShape
24 import androidx.compose.material3.ColorScheme
25 import androidx.compose.runtime.Composable
26 import androidx.compose.ui.Modifier
27 import androidx.compose.ui.draw.clip
28 import androidx.compose.ui.draw.drawWithContent
29 import androidx.compose.ui.geometry.Offset
30 import androidx.compose.ui.graphics.Color
31 import androidx.compose.ui.graphics.drawscope.DrawScope
32 import androidx.compose.ui.graphics.drawscope.Fill
33 import androidx.compose.ui.graphics.drawscope.Stroke
34 import androidx.compose.ui.platform.LocalDensity
35 import androidx.compose.ui.unit.Dp
36 import androidx.compose.ui.unit.IntOffset
37 import androidx.compose.ui.unit.dp
38 import androidx.graphics.shapes.Cubic
39 import androidx.graphics.shapes.Feature
40 import kotlin.math.roundToInt
41 
debugDrawCubicnull42 internal fun DrawScope.debugDrawCubic(bezier: Cubic, scheme: ColorScheme) {
43     // Draw red circles for start and end.
44     drawCircle(scheme.inverseSurface, radius = 6f, center = bezier.anchor0(), style = Stroke(2f))
45     drawCircle(scheme.inverseSurface, radius = 8f, center = bezier.anchor1(), style = Stroke(2f))
46 
47     // Draw a circle for the first control point, and a line from start to it.
48     // The curve will start in this direction
49     drawLine(scheme.scrim, bezier.anchor0(), bezier.control0(), strokeWidth = 0f)
50     drawCircle(scheme.scrim, radius = 4f, center = bezier.control0(), style = Stroke(2f))
51 
52     // Draw a circle for the second control point, and a line from it to the end.
53     // The curve will end in this direction
54     drawLine(scheme.scrim, bezier.control1(), bezier.anchor1(), strokeWidth = 0f)
55     drawCircle(scheme.scrim, radius = 4f, center = bezier.control1(), style = Stroke(2f))
56 
57     // Draw dots along each curve
58     var t = .1f
59     while (t < 1f) {
60         drawCircle(scheme.primary, radius = 2f, center = bezier.pointOnCurve(t), style = Stroke(2f))
61         t += .1f
62     }
63 }
64 
debugDrawFeaturenull65 internal fun DrawScope.debugDrawFeature(
66     feature: Feature,
67     colorScheme: FeatureColorScheme,
68     backgroundColor: Color,
69     radius: Float
70 ) {
71     val color = featureToColor(feature, colorScheme)
72     val representativePoint = featureRepresentativePoint(feature)
73 
74     // Draw a clickable circle for the representative Point
75     drawCircle(color, radius = radius, center = representativePoint, style = Fill)
76 
77     // With a bit of a background to suggest tapping is possible
78     drawCircle(
79         color.copy(0.2f),
80         radius = radius + (radius * 0.6f),
81         center = representativePoint,
82         style = Fill
83     )
84 
85     // Finally add a border around the representative point
86     drawCircle(
87         backgroundColor,
88         radius = radius,
89         center = representativePoint,
90         style = Stroke(radius / 4)
91     )
92 }
93 
94 @Composable
FeatureRepresentativePointnull95 internal fun FeatureRepresentativePoint(
96     modifier: Modifier = Modifier,
97     feature: Feature,
98     colorScheme: FeatureColorScheme,
99     backgroundColor: Color,
100     model: PanZoomRotateBoxState = PanZoomRotateBoxState(),
101     pointSize: Dp = 15.dp,
102     onClick: () -> Unit,
103 ) {
104     val radius = with(LocalDensity.current) { (pointSize / 2).roundToPx() }
105     val position = model.mapOut(featureRepresentativePoint(feature))
106 
107     Box(
108         modifier
109             .offset {
110                 IntOffset(
111                     (position.x).roundToInt(),
112                     (position.y).roundToInt(),
113                 )
114             }
115             .drawWithContent {
116                 drawContent()
117                 debugDrawFeature(feature, colorScheme, backgroundColor, radius.toFloat())
118             }
119     )
120 
121     Box(
122         modifier
123             .offset {
124                 IntOffset(
125                     (position.x - CLICKABLE_SCALE * radius).roundToInt(),
126                     (position.y - CLICKABLE_SCALE * radius).roundToInt(),
127                 )
128             }
129             .size(pointSize * CLICKABLE_SCALE)
130             .clip(CircleShape)
131             .clickable(onClick = onClick)
132     )
133 }
134 
featureToColornull135 internal fun featureToColor(feature: Feature, scheme: FeatureColorScheme): Color =
136     if (feature.isEdge) {
137         scheme.edgeColor
138     } else if (feature.isConvexCorner) {
139         scheme.convexColor
140     } else {
141         scheme.concaveColor
142     }
143 
144 // TODO: b/378441547 - Remove if explicit / exposed by default
featureRepresentativePointnull145 internal fun featureRepresentativePoint(feature: Feature): Offset =
146     (feature.cubics.first().anchor0() + feature.cubics.last().anchor1()) / 2f
147 
148 internal fun Cubic.anchor0() = Offset(anchor0X, anchor0Y)
149 
150 internal fun Cubic.control0() = Offset(control0X, control0Y)
151 
152 internal fun Cubic.control1() = Offset(control1X, control1Y)
153 
154 internal fun Cubic.anchor1() = Offset(anchor1X, anchor1Y)
155 
156 internal const val CLICKABLE_SCALE = 1.8f
157