From 5145168a941be213d31f972ba9d5735160a451cf Mon Sep 17 00:00:00 2001
From: Dave Severns <dseverns@livefront.com>
Date: Fri, 10 Jan 2025 16:00:01 -0500
Subject: [PATCH] fubar

---
 .../coachmark/CoachMarkContainer.kt           | 94 +++++++++++--------
 .../components/coachmark/CoachMarkScope.kt    |  2 +
 .../components/coachmark/CoachMarkState.kt    | 34 ++++++-
 .../feature/addedit/VaultAddEditScreen.kt     | 10 +-
 4 files changed, 92 insertions(+), 48 deletions(-)

diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt
index 4f1c53519..f9dc2b047 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt
@@ -11,20 +11,26 @@ 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.foundation.shape.GenericShape
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.RoundRect
 import androidx.compose.ui.graphics.ClipOp
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.drawscope.clipPath
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.res.stringResource
@@ -35,6 +41,7 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconBu
 import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
 import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
 import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import timber.log.Timber
 
@@ -61,55 +68,68 @@ fun <T : Enum<T>> CoachMarkContainer(
     content: @Composable CoachMarkScope<T>.() -> Unit,
 ) {
     val scope = rememberCoroutineScope()
-    Box(modifier = Modifier
-        .fillMaxSize()
-        .then(modifier),
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .then(modifier),
     ) {
 
         Timber.i("Coach: I am the full container")
         CoachMarkScopeInstance(coachMarkState = state).content()
-        if (
-            state.currentHighlightBounds.value != Rect.Zero && state.isVisible.value
-        ) {
+        val boundedRectangle by state.currentHighlightBounds
+        val isVisible by state.isVisible
+        val currentHighlightShape by state.currentHighlightShape
 
-            Timber.i("Coach do I get called? Im trying draw the overlay")
-            val boundedRectangle by state.currentHighlightBounds
-            val highlightArea = Rect(
-                topLeft = boundedRectangle.topLeft,
-                bottomRight = boundedRectangle.bottomRight,
-            )
-            val highlightPath = Path().apply {
+        val highlightPath =
+            remember(boundedRectangle, currentHighlightShape) {
+                if (boundedRectangle == Rect.Zero) {
+                    Timber.w("Coach: highlightPath is Rect.Zero")
+                    return@remember Path()
+                }
+                val highlightArea = Rect(
+                    topLeft = boundedRectangle.topLeft,
+                    bottomRight = boundedRectangle.bottomRight,
+                )
                 Timber.i("Coach: I am applying the path for $highlightArea")
-                when (state.currentHighlightShape.value) {
-                    CoachMarkHighlightShape.SQUARE -> addRoundRect(
-                        RoundRect(
-                            rect = highlightArea,
-                            cornerRadius = CornerRadius(
-                                x = ROUNDED_RECT_RADIUS,
+                Path().apply {
+                    when (currentHighlightShape) {
+                        CoachMarkHighlightShape.SQUARE -> addRoundRect(
+                            RoundRect(
+                                rect = highlightArea,
+                                cornerRadius = CornerRadius(
+                                    x = ROUNDED_RECT_RADIUS,
+                                ),
                             ),
-                        ),
-                    )
+                        )
 
-                    CoachMarkHighlightShape.OVAL -> addOval(highlightArea)
+                        CoachMarkHighlightShape.OVAL -> addOval(highlightArea)
+                    }
                 }
             }
+        if (boundedRectangle != Rect.Zero && isVisible) {
+            Timber.i("Coach do I get called? Im trying draw the overlay")
             val backgroundColor = BitwardenTheme.colorScheme.text.primary
             Box(
                 modifier = Modifier
                     .pointerInput(Unit) {
                         detectTapGestures(
                             onTap = {
-                                if (state.isVisible.value) {
-                                    scope.launch {
-                                        Timber.i("Coach calling it in gestures")
-                                        state.showToolTipForCurrentCoachMark()
-                                    }
+                                scope.launch {
+                                    Timber.i("Coach calling it in gestures")
+                                    state.showToolTipForCurrentCoachMark()
                                 }
                             },
                         )
                     }
                     .fillMaxSize()
+//                    .background(
+//                        color = backgroundColor.copy(alpha = .75f)
+//                    )
+//                    .clip(
+//                        CircleShape
+//                    )
                     .drawBehind {
+                        Timber.i("Coach: in drawbehind, $highlightPath based on $boundedRectangle")
                         clipPath(
                             path = highlightPath,
                             clipOp = ClipOp.Difference,
@@ -123,17 +143,17 @@ fun <T : Enum<T>> CoachMarkContainer(
                     },
             )
         }
-    }
-    LaunchedEffect(state.currentHighlightBounds.value, state.currentHighlightShape.value) {
-        if (state.currentHighlightBounds.value != Rect.Zero) {
-            Timber.i("Coach: bounds changed do I get called? Im trying to show the tooltip")
-            state.showToolTipForCurrentCoachMark()
+        LaunchedEffect(state.currentHighlightBounds.value, state.currentHighlightShape.value) {
+            if (state.currentHighlightBounds.value != Rect.Zero) {
+                Timber.i("Coach: bounds changed do I get called? Im trying to show the tooltip")
+                state.showToolTipForCurrentCoachMark()
+            }
         }
-    }
-    LaunchedEffect(Unit) {
-        if (state.isVisible.value && (state.currentHighlight.value != null)) {
-            Timber.i("Coach calling it in Launched effect cause state is visible")
-            state.showCoachMark(state.currentHighlight.value)
+        LaunchedEffect(Unit) {
+            if (state.isVisible.value && (state.currentHighlight.value != null)) {
+                Timber.i("Coach calling it in Launched effect cause state is visible")
+                state.showCoachMark(state.currentHighlight.value)
+            }
         }
     }
 }
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScope.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScope.kt
index 37220fb6c..78c3d95b8 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScope.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScope.kt
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyListScope
 import androidx.compose.foundation.lazy.LazyListState
@@ -199,6 +200,7 @@ private fun TooltipScope.CoachMarkToolTip(
     rightAction: (@Composable RowScope.() -> Unit)?,
 ) {
     RichTooltip(
+        modifier = Modifier.padding(start = 8.dp, end = 2.dp),
         caretSize = DpSize(width = 24.dp, height = 16.dp),
         title = {
             Row(
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt
index 76e6b86de..f54f9b7f7 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt
@@ -1,5 +1,7 @@
 package com.x8bit.bitwarden.ui.platform.components.coachmark
 
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.TooltipState
 import androidx.compose.runtime.Composable
@@ -9,9 +11,12 @@ import androidx.compose.runtime.saveable.Saver
 import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import timber.log.Timber
+import java.util.concurrent.ConcurrentHashMap
 
 /**
  * Manages the state of a coach mark sequence, guiding users through a series of highlights.
@@ -92,7 +97,6 @@ class CoachMarkState<T : Enum<T>>(
         Timber.i("Coach: I have been requested to show the highlight for ${highlightToShow?.key} with bounds: ${currentHighlightBounds.value}")
         if (highlightToShow != null) {
             updateCoachMarkStateInternal(highlightToShow)
-            _isVisible.value = true
         } else {
             showNextCoachMark()
         }
@@ -112,7 +116,6 @@ class CoachMarkState<T : Enum<T>>(
             val index = orderedList.indexOf(previousHighlight?.key)
             if (index < 0 && previousHighlight != null) return
             _currentHighlight.value = orderedList.getOrNull(index + 1)
-            _isVisible.value = currentHighlight.value != null
             getCurrentHighlight()
         }
         Timber.i("Coach: I have been requested to show next highlight which is: ${highlightToShow?.key} with bounds: ${highlightToShow?.highlightBounds}")
@@ -135,7 +138,6 @@ class CoachMarkState<T : Enum<T>>(
                 return
             }
             _currentHighlight.value = orderedList.getOrNull(index - 1)
-            _isVisible.value = this.currentHighlight.value != null
             getCurrentHighlight()
         }
         updateCoachMarkStateInternal(highlightToShow)
@@ -162,6 +164,7 @@ class CoachMarkState<T : Enum<T>>(
     }
 
     private fun updateCoachMarkStateInternal(highlight: CoachMarkHighlightState<T>?) {
+        _isVisible.value = highlight != null
         Timber.i("Coach: I have updated the shape and bounds, the new bounds are ${highlight?.highlightBounds}")
         _currentHighlightShape.value = highlight?.shape ?: CoachMarkHighlightShape.SQUARE
         if (currentHighlightBounds.value != highlight?.highlightBounds) {
@@ -170,7 +173,7 @@ class CoachMarkState<T : Enum<T>>(
         }
     }
 
-    internal suspend fun showToolTipForCurrentCoachMark() {
+    suspend fun showToolTipForCurrentCoachMark() {
         Timber.i("Coach: I am trying to show")
         val currentCoachMark = mutex.withLock {
             getCurrentHighlight()
@@ -180,6 +183,29 @@ class CoachMarkState<T : Enum<T>>(
         currentCoachMark?.toolTipState?.show()
     }
 
+    suspend fun scrollUpToKey(
+        listState: LazyListState,
+        targetKey: T,
+    ) {
+        val scrollAmount = (-1).toFloat()
+        var found = false
+        while (!found) {
+            val layoutInfo = listState.layoutInfo
+            val visibleItems = layoutInfo.visibleItemsInfo
+            if (visibleItems.any { it.key == targetKey }) {
+                found = true
+            } else {
+                if (listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0) {
+                    // Reached the start of the list without finding the key
+                    println("Key $targetKey not found")
+                    found = true
+                } else {
+                    listState.scrollBy(scrollAmount)
+                }
+            }
+        }
+    }
+
     /**
      * Cleans up the tooltip state by dismissing it if visible and calling onDispose.
      */
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt
index 1cd10a1a1..da9480093 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreen.kt
@@ -258,12 +258,6 @@ fun VaultAddEditScreen(
         orderedList = AddEditItemCoachMark.entries,
     )
     val coroutineScope = rememberCoroutineScope()
-    LaunchedEffect(Unit) {
-        delay(3000L)
-        if (coachMarkState.isVisible.value.not()) {
-            coachMarkState.showCoachMark(AddEditItemCoachMark.GENERATE_PASSWORD)
-        }
-    }
     CoachMarkContainer(
         state = coachMarkState,
     ) {
@@ -287,7 +281,9 @@ fun VaultAddEditScreen(
                         BitwardenTextButton(
                             label = stringResource(id = R.string.save),
                             onClick = remember(viewModel) {
-                                { viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) }
+                                { coroutineScope.launch {
+                                    coachMarkState.showCoachMark()
+                                }}
                             },
                             modifier = Modifier.testTag("SaveButton"),
                         )