diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/TextSelectionHelper.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/TextSelectionHelper.kt index 30f98a0..0c88ce0 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/TextSelectionHelper.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/TextSelectionHelper.kt @@ -8,24 +8,41 @@ import android.view.HapticFeedbackConstants import android.view.View import android.widget.Toast import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup import com.rosetta.messenger.ui.onboarding.PrimaryBlue +import kotlinx.coroutines.delay data class LayoutInfo( val layout: Layout, @@ -94,6 +111,7 @@ class TextSelectionHelper { handleViewProgress = 1f view?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + showToolbar = false } fun updateSelectionStart(charOffset: Int) { @@ -132,6 +150,53 @@ class TextSelectionHelper { fun endHandleDrag() { movingHandle = false + showFloatingToolbar() + } + + var showToolbar by mutableStateOf(false) + private set + + fun showFloatingToolbar() { + if (isInSelectionMode && !movingHandle) { + showToolbar = true + } + } + + fun hideFloatingToolbar() { + showToolbar = false + } + + private var magnifier: android.widget.Magnifier? = null + private var magnifierView: View? = null + + fun setMagnifierView(view: View?) { + magnifierView = view + } + + fun showMagnifier(x: Float, y: Float) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) return + val view = magnifierView ?: return + if (!movingHandle) return + if (magnifier == null) { + magnifier = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + android.widget.Magnifier.Builder(view) + .setSize(200, 80) + .setCornerRadius(16f) + .build() + } else { + android.widget.Magnifier(view) + } + } + val info = layoutInfo ?: return + val localX = (x - info.windowX).coerceIn(0f, view.width.toFloat()) + val localY = (y - info.windowY).coerceIn(0f, view.height.toFloat()) + magnifier?.show(localX, localY) + } + + fun hideMagnifier() { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) return + magnifier?.dismiss() + magnifier = null } fun getCharOffsetFromCoords(x: Int, y: Int): Int { @@ -174,6 +239,8 @@ class TextSelectionHelper { isActive = false handleViewProgress = 0f movingHandle = false + showToolbar = false + hideMagnifier() } } @@ -183,6 +250,68 @@ private val HighlightCorner = 6.dp private val HighlightColor = PrimaryBlue.copy(alpha = 0.3f) private val HandleColor = PrimaryBlue +@Composable +private fun FloatingToolbarPopup( + helper: TextSelectionHelper +) { + val context = LocalContext.current + + LaunchedEffect(helper.isActive, helper.movingHandle) { + if (helper.isActive && !helper.movingHandle && !helper.showToolbar) { + delay(200) + helper.showFloatingToolbar() + } + } + + if (!helper.showToolbar || !helper.isInSelectionMode) return + + val info = helper.layoutInfo ?: return + val layout = info.layout + val startLine = layout.getLineForOffset(helper.selectionStart.coerceIn(0, info.text.length)) + val toolbarY = (layout.getLineTop(startLine) + info.windowY - 52).coerceAtLeast(0) + val toolbarX = ((helper.startHandleX + helper.endHandleX) / 2f - 80f).coerceAtLeast(0f) + + Popup( + alignment = Alignment.TopStart, + offset = IntOffset(toolbarX.toInt(), toolbarY) + ) { + Row( + modifier = Modifier + .shadow(4.dp, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF333333)) + .padding(horizontal = 4.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Copy", + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clickable { helper.copySelectedText(context) } + .padding(horizontal = 12.dp, vertical = 8.dp) + ) + val allSelected = helper.selectionStart <= 0 && + helper.selectionEnd >= info.text.length + if (!allSelected) { + Text( + text = "Select All", + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier + .clickable { + helper.selectAll() + helper.hideFloatingToolbar() + } + .padding(horizontal = 12.dp, vertical = 8.dp) + ) + } + } + } +} + @Composable fun TextSelectionOverlay( helper: TextSelectionHelper, @@ -196,6 +325,7 @@ fun TextSelectionOverlay( val highlightCornerPx = with(density) { HighlightCorner.toPx() } Box(modifier = modifier.fillMaxSize()) { + FloatingToolbarPopup(helper = helper) Canvas( modifier = Modifier .fillMaxSize() @@ -225,10 +355,12 @@ fun TextSelectionOverlay( when { startRect.contains(Offset(x, y)) -> { helper.beginHandleDrag(isStart = true, x, y) + helper.hideFloatingToolbar() change.consume() } endRect.contains(Offset(x, y)) -> { helper.beginHandleDrag(isStart = false, x, y) + helper.hideFloatingToolbar() change.consume() } else -> { @@ -238,9 +370,11 @@ fun TextSelectionOverlay( } change.pressed && helper.movingHandle -> { helper.moveHandle(change.position.x, change.position.y) + helper.showMagnifier(change.position.x, change.position.y) change.consume() } !change.pressed && helper.movingHandle -> { + helper.hideMagnifier() helper.endHandleDrag() change.consume() }