//Gemini 3.1 pro mob
Here is the complete, single-file solution in Kotlin for Jetpack Compose.
To achieve this, we intercept the system's default text selection toolbar by overriding LocalTextToolbar. We track the selection using a TextFieldValue and display a custom Popup that aligns near the selected text, providing the exact custom actions you requested.
```kotlin
package com.example.customtextselection
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
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 androidx.compose.ui.window.PopupProperties
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
CustomTextSelectionScreen()
}
}
}
}
}
@Composable
fun CustomTextSelectionScreen() {
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
val density = LocalDensity.current
// State to hold text and selection range
var textFieldValue by remember {
mutableStateOf(
TextFieldValue(
"Jetpack Compose is Android’s modern toolkit for building native UI. " +
"It simplifies and accelerates UI development on Android. " +
"Long press or double tap to select this text and see the custom context menu in action! " +
"You can Copy, Copy All, Share, or Translate."
)
)
}
// States for custom toolbar visibility and positioning
var showCustomMenu by remember { mutableStateOf(false) }
var selectionRect by remember { mutableStateOf(Rect.Zero) }
// Derive the currently selected text
val selectedText = remember(textFieldValue) {
if (!textFieldValue.selection.collapsed) {
textFieldValue.annotatedString.substring(textFieldValue.selection).text
} else {
""
}
}
// Hide menu automatically if selection collapses (e.g., user clicked outside)
LaunchedEffect(textFieldValue.selection) {
if (textFieldValue.selection.collapsed) {
showCustomMenu = false
}
}
// Create a custom TextToolbar to intercept the default system selection menu
val customTextToolbar = remember {
object : TextToolbar {
override val status: TextToolbarStatus
get() = if (showCustomMenu) TextToolbarStatus.Shown else TextToolbarStatus.Hidden
override fun hide() {
showCustomMenu = false
}
override fun showMenu(
rect: Rect,
onCopyRequested: (() -> Unit)?,
onPasteRequested: (() -> Unit)?,
onCutRequested: (() -> Unit)?,
onSelectAllRequested: (() -> Unit)?
) {
// Save the bounding box of the selected text to anchor our popup
selectionRect = rect
showCustomMenu = true
}
}
}
// Provide our custom toolbar to the Compose hierarchy
CompositionLocalProvider(LocalTextToolbar provides customTextToolbar) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
// Using BasicTextField configured as read-only to support text selection easily
BasicTextField(
value = textFieldValue,
onValueChange = { newValue ->
// Only accept selection changes, ignoring text edits since it's "read-only"
textFieldValue = textFieldValue.copy(selection = newValue.selection)
},
readOnly = true,
textStyle = TextStyle(
fontSize = 20.sp,
color = MaterialTheme.colorScheme.onBackground,
lineHeight = 28.sp
),
modifier = Modifier.fillMaxWidth()
)
// Show Custom Popup Context Menu
if (showCustomMenu && !textFieldValue.selection.collapsed) {
// Calculate vertical offset relative to the text selection coordinates
val yOffsetPx = with(density) {
// Position roughly 60dp above the selection rect top
(selectionRect.top - 60.dp.toPx()).toInt()
}
Popup(
alignment = Alignment.TopCenter,
offset = IntOffset(x = 0, y = yOffsetPx.coerceAtLeast(0)), // Prevent going off top of screen
properties = PopupProperties(
focusable = false,
dismissOnBackPress = false,
dismissOnClickOutside = false
)
) {
AnimatedVisibility(
visible = showCustomMenu,
enter = fadeIn(tween(200)),
exit = fadeOut(tween(200))
) {
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.shadow(8.dp, RoundedCornerShape(8.dp))
) {
Row(
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 1. Copy
TextButton(onClick = {
clipboardManager.setText(AnnotatedString(selectedText))
showCustomMenu = false
}) {
Text("Copy")
}
// 2. Copy All
TextButton(onClick = {
clipboardManager.setText(AnnotatedString(textFieldValue.text))
showCustomMenu = false
}) {
Text("Copy All")
}
// 3. Share
TextButton(onClick = {
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, selectedText)
}
context.startActivity(Intent.createChooser(shareIntent, "Share text"))
showCustomMenu = false
}) {
Text("Share")
}
// 4. Translate
TextButton(onClick = {
val translateIntent = Intent(Intent.ACTION_PROCESS_TEXT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText)
putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
}
try {
context.startActivity(translateIntent)
} catch (e: Exception) {
Toast.makeText(context, "No translation app available", Toast.LENGTH_SHORT).show()
}
showCustomMenu = false
}) {
Text("Translate")
}
}
}
}
}
}
}
}
}
```
### How it Works:
1. **LocalTextToolbar provides customTextToolbar**: Jetpack Compose gives developers the ability to override the system text action bar by hooking into LocalTextToolbar. The showMenu callback suppresses the default Android system floating menu and gives us the bounds (Rect) of the highlighted text.
2. **Reading Text State**: We use a BasicTextField marked as readOnly. This naturally enables Jetpack Compose text selection handling. The variable selectedText reacts dynamically as the user drags the selection cursor.
3. **Floating UI positioning**: The layout uses a Popup window that floats above all content, dynamically offset along the Y-axis so it gracefully hovers exactly over the text the user just highlighted.
4. **Android Intent Integration**: ACTION_SEND handles cross-app sharing natively, and ACTION_PROCESS_TEXT securely invokes the standard system Translate functionality without relying on third-party SDKs.