Skip to content

Commit 4bf3b9f

Browse files
add draw auto
1 parent 74d8d47 commit 4bf3b9f

File tree

2 files changed

+145
-57
lines changed

2 files changed

+145
-57
lines changed

example/src/commonMain/kotlin/com/singularityuniverse/lib/userguide/example/UserGuideExample.kt

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
package com.singularityuniverse.lib.userguide.example
22

3-
import androidx.compose.foundation.border
43
import androidx.compose.foundation.layout.*
54
import androidx.compose.material3.*
65
import androidx.compose.runtime.Composable
76
import androidx.compose.runtime.CompositionLocalProvider
87
import androidx.compose.ui.Alignment
98
import androidx.compose.ui.Modifier
10-
import androidx.compose.ui.graphics.Color
9+
import androidx.compose.ui.draw.drawWithContent
10+
import androidx.compose.ui.geometry.center
11+
import androidx.compose.ui.graphics.Path
12+
import androidx.compose.ui.graphics.drawscope.rotate
13+
import androidx.compose.ui.platform.LocalDensity
1114
import androidx.compose.ui.unit.dp
1215
import androidx.compose.ui.zIndex
1316
import com.singularityuniverse.lib.userguide.*
1417
import com.singularityuniverse.lib.userguide.tools.LocalIsDebugModeEnabled
15-
import com.singularityuniverse.lib.userguide.example.tools.borderStroke1Dp
1618
import org.jetbrains.compose.ui.tooling.preview.Preview
1719

1820
@Preview
@@ -32,6 +34,7 @@ fun UserGuideExample() {
3234
@Preview
3335
@Composable
3436
private fun Content(userGuideState: UserGuideState) {
37+
val density = LocalDensity.current
3538
val isDebugMode = true
3639

3740
Scaffold(
@@ -97,50 +100,18 @@ private fun Content(userGuideState: UserGuideState) {
97100
UserGuideOverlay(
98101
state = userGuideState
99102
) { info ->
100-
if (isPreferTop) drawAbove {
103+
// Tool Tip
104+
drawAuto {
101105
Box(
102-
modifier = Modifier
103-
.padding(
104-
start = when {
105-
isTargetHorizontallyCentered -> 0.dp
106-
isPreferLeft -> targetLeftSpace
107-
else -> 0.dp
108-
},
109-
end = when {
110-
isTargetHorizontallyCentered -> 0.dp
111-
isPreferRight -> targetRightSpace
112-
else -> 0.dp
113-
},
114-
top = 8.dp,
115-
bottom = 8.dp
116-
)
117-
.border(borderStroke1Dp(if (isDebugMode) Color.Blue else Color.Transparent)),
106+
modifier = Modifier.padding(10.dp)
118107
) {
119108
ToolTip(content = info.tooltipContent)
120109
}
121110
}
122111

123-
if (isPreferBottom) drawBellow {
124-
Box(
125-
modifier = Modifier
126-
.padding(
127-
start = when {
128-
isTargetHorizontallyCentered -> 0.dp
129-
isPreferLeft -> targetLeftSpace
130-
else -> 0.dp
131-
},
132-
end = when {
133-
isTargetHorizontallyCentered -> 0.dp
134-
isPreferRight -> targetRightSpace
135-
else -> 0.dp
136-
},
137-
top = 8.dp,
138-
bottom = 8.dp
139-
)
140-
.border(borderStroke1Dp(if (isDebugMode) Color.Blue else Color.Transparent)),
141-
) {
142-
ToolTip(content = info.tooltipContent)
143-
}
112+
// Pointer
113+
drawPointer {
114+
Pointer(rotationDegrees = if (isPreferTop) 180f else 0f)
144115
}
145116
}
146117
}
@@ -191,3 +162,35 @@ fun ToolTip(modifier: Modifier = Modifier, content: String) {
191162
}
192163
}
193164
}
165+
166+
@Composable
167+
fun Pointer(
168+
rotationDegrees: Float = 0f
169+
) {
170+
val color = MaterialTheme.colorScheme.surface
171+
Box(
172+
modifier = Modifier
173+
.size(24.dp)
174+
.drawWithContent {
175+
drawContent()
176+
val width = size.width
177+
val height = size.height
178+
179+
val triangle = Path().apply {
180+
// Triangle pointing up
181+
moveTo(width / 2f, 0f) // Top middle
182+
lineTo(0f, height) // Bottom left
183+
lineTo(width, height)
184+
// Bottom right
185+
close()
186+
}
187+
188+
rotate(rotationDegrees, pivot = size.center) {
189+
drawPath(
190+
path = triangle,
191+
color = color
192+
)
193+
}
194+
}
195+
)
196+
}

library/src/commonMain/kotlin/com/singularityuniverse/lib/userguide/UserGuideOverlay.kt

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
package com.singularityuniverse.lib.userguide
22

3+
import androidx.compose.foundation.background
34
import androidx.compose.foundation.border
45
import androidx.compose.foundation.layout.*
56
import androidx.compose.material3.Text
67
import androidx.compose.runtime.Composable
8+
import androidx.compose.runtime.mutableStateOf
9+
import androidx.compose.runtime.remember
710
import androidx.compose.ui.Alignment
811
import androidx.compose.ui.Modifier
912
import androidx.compose.ui.geometry.Rect
1013
import androidx.compose.ui.graphics.Color
14+
import androidx.compose.ui.layout.onSizeChanged
1115
import androidx.compose.ui.platform.LocalDensity
1216
import androidx.compose.ui.unit.Density
1317
import androidx.compose.ui.unit.Dp
18+
import androidx.compose.ui.unit.IntOffset
19+
import androidx.compose.ui.unit.IntSize
20+
import androidx.compose.ui.unit.center
1421
import androidx.compose.ui.unit.dp
1522
import androidx.compose.ui.zIndex
1623
import com.singularityuniverse.lib.userguide.tools.LocalIsDebugModeEnabled
@@ -29,24 +36,28 @@ fun UserGuideOverlay(
2936
.apply { tooltipBuilder(this, currentTarget ?: return) }
3037

3138
if (state.isShowing && state.animationState == GuideAnimationState.NAVIGATING) {
32-
Column {
39+
Box(
40+
modifier = Modifier.fillMaxSize()
41+
) {
42+
// top section container
3343
Box(
3444
modifier = Modifier
3545
.fillMaxWidth()
36-
.height((scope.targetRect.top / density.density).dp)
46+
.height((scope.highlightRect.top / density.density).dp)
3747
.border(borderStroke1Dp(if (isDebugMode) Color.Red else Color.Transparent)),
3848
contentAlignment = when {
3949
scope.isTargetHorizontallyCentered -> Alignment.BottomCenter
4050
scope.isPreferLeft -> Alignment.BottomEnd
4151
else -> Alignment.BottomStart
4252
}
4353
) {
44-
scope.aboveContent?.invoke(this)
54+
scope.aboveContents.map { it.invoke(this) }
4555
}
4656

57+
// top section container
4758
Box(
4859
modifier = Modifier
49-
.padding(top = (scope.targetRect.height / density.density).dp)
60+
.padding(top = (scope.highlightRect.bottom / density.density).dp)
5061
.fillMaxWidth()
5162
.fillMaxHeight()
5263
.border(borderStroke1Dp(if (isDebugMode) Color.Red else Color.Transparent)),
@@ -58,7 +69,7 @@ fun UserGuideOverlay(
5869
}
5970
) {
6071

61-
scope.bellowContent?.invoke(this)
72+
scope.bellowContents.map { it.invoke(this) }
6273

6374
if (isDebugMode)
6475
Text(
@@ -73,6 +84,8 @@ fun UserGuideOverlay(
7384
.zIndex(1f)
7485
)
7586
}
87+
88+
scope.topContents.map { it.invoke(this) }
7689
}
7790
}
7891
}
@@ -86,31 +99,103 @@ class UserGuideOverlayScope(
8699
}
87100

88101
val rootRect: Rect get() = state.fullScreenRect
89-
val targetRect: Rect get() = state.animatedRect
102+
val highlightRect: Rect get() = state.animatedRect
103+
val targetRect: Rect? get() = state.getCurrentTarget()?.rect
90104

91-
val isPreferTop: Boolean get() = targetRect.center.y > rootRect.center.y
105+
val isPreferTop: Boolean get() = highlightRect.center.y > rootRect.center.y
92106
val isPreferBottom: Boolean get() = !isPreferTop
93-
val isPreferLeft: Boolean get() = targetRect.center.x < rootRect.center.x
107+
val isPreferLeft: Boolean get() = highlightRect.center.x < rootRect.center.x
94108
val isPreferRight: Boolean get() = !isPreferLeft
95109

96110
val isTargetHorizontallyCentered: Boolean
97-
get() = (targetRect.center.x - rootRect.center.x).absoluteValue < FLOAT_ROUNDING_PIXEL_TOLERANCE
98-
val isTargetVerticallyCentered: Boolean = targetRect.center.y == rootRect.center.y
111+
get() = (highlightRect.center.x - rootRect.center.x).absoluteValue < FLOAT_ROUNDING_PIXEL_TOLERANCE
112+
val isTargetVerticallyCentered: Boolean = highlightRect.center.y == rootRect.center.y
99113

100-
val targetLeftSpace: Dp = (targetRect.left / density.density).dp
101-
val targetRightSpace: Dp = ((rootRect.width - targetRect.right) / density.density).dp
114+
val targetLeftSpace: Dp = (highlightRect.left / density.density).dp
115+
val targetRightSpace: Dp = ((rootRect.width - highlightRect.right) / density.density).dp
102116

103-
internal var bellowContent: (@Composable BoxScope.() -> Unit)? = null
117+
internal var bellowContents: List<@Composable BoxScope.() -> Unit> = emptyList()
104118
private set
105119

106120
fun drawBellow(content: @Composable BoxScope.() -> Unit) {
107-
bellowContent = content
121+
bellowContents += content
108122
}
109123

110-
internal var aboveContent: (@Composable BoxScope.() -> Unit)? = null
124+
internal var aboveContents: List<@Composable BoxScope.() -> Unit> = emptyList()
111125
private set
112126

113127
fun drawAbove(content: @Composable BoxScope.() -> Unit) {
114-
aboveContent = content
128+
aboveContents += content
129+
}
130+
131+
internal var topContents: List<@Composable BoxScope.() -> Unit> = emptyList()
132+
private set
133+
134+
fun drawOnTop(content: @Composable BoxScope.() -> Unit) {
135+
topContents += content
136+
}
137+
138+
fun drawPointer(content: @Composable BoxScope.() -> Unit) {
139+
topContents += @Composable {
140+
val pointerSize = remember { mutableStateOf(IntSize.Zero) }
141+
val pointerCenter = pointerSize.value.center
142+
143+
Box(
144+
modifier = Modifier
145+
.onSizeChanged { pointerSize.value = it }
146+
// translate container into proper position automatically
147+
.offset {
148+
val targetCenter = targetRect?.center
149+
?.let { IntOffset(it.x.toInt(), it.y.toInt()) }
150+
?: IntOffset.Zero
151+
152+
val center = targetCenter - pointerCenter
153+
154+
val verticalOffsetCorrection = (if (isPreferTop) -1 else 1) *
155+
(pointerSize.value.height + (targetRect?.height ?: 0f)) / 2
156+
157+
center + IntOffset(x = 0, y = verticalOffsetCorrection.toInt())
158+
}
159+
) {
160+
content()
161+
}
162+
}
163+
}
164+
165+
fun drawAuto(content: @Composable BoxScope.() -> Unit) {
166+
if (isPreferTop) drawAbove {
167+
DrawAutoContainer {
168+
content()
169+
}
170+
}
171+
172+
if (isPreferBottom) drawBellow {
173+
DrawAutoContainer {
174+
content()
175+
}
176+
}
177+
}
178+
179+
@Composable
180+
private fun DrawAutoContainer(content: @Composable BoxScope.() -> Unit) {
181+
val isDebugMode = LocalIsDebugModeEnabled.current
182+
Box(
183+
modifier = Modifier
184+
.padding(
185+
start = when {
186+
isTargetHorizontallyCentered -> 0.dp
187+
isPreferLeft -> targetLeftSpace
188+
else -> 0.dp
189+
},
190+
end = when {
191+
isTargetHorizontallyCentered -> 0.dp
192+
isPreferRight -> targetRightSpace
193+
else -> 0.dp
194+
},
195+
)
196+
.border(borderStroke1Dp(if (isDebugMode) Color.Blue else Color.Transparent)),
197+
) {
198+
content()
199+
}
115200
}
116201
}

0 commit comments

Comments
 (0)