mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
fix(embed): no caret handle or Cut/Paste on a readonly field
Suppressing the keyboard for a readonly field was only half of it. The selection UI still treated it as fully editable: a tap-and-hold raised the insertion caret handle, and its toolbar offered Cut and Paste — on a field the page will not let you modify. Cut appeared to work in the mirror while the page kept its text, so the two silently drifted apart. Native, on the same page in the full-screen WebView, gives a readonly field selection handles and a Copy / Select-all bar, and nothing else: no caret handle (there is no caret to place) and no editing actions. Carry the flag into SelectionUiState so the overlay can reason about it: `fieldReadOnly` gates the insertion handle (and with it the Paste/Select-all popup that hangs off it), and the field toolbar drops Cut and Paste. Selection, its handles, Copy and Select-all are untouched — that half is what native offers and it works today. `cutSelection`/`pasteClipboard` refuse on a readonly field too. The toolbar no longer offers them, so this is a backstop, placed next to the ops so a future call site can't reintroduce the drift. Device-verified: readonly long-press selects with handles and shows exactly "Copy | Select all"; a plain tap gives no keyboard and no caret droplet; an editable field still shows all four actions and keeps its caret handle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
37f9c5ad85
commit
b7ef983bd8
+19
-7
@@ -377,6 +377,7 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) {
|
||||
// Cancel any in-flight scroll-hide from the page phase: otherwise the field's own
|
||||
// selection-reveal scrolls keep it armed and the new field handles/toolbar never appear.
|
||||
sel.scrolling = false
|
||||
sel.onFieldReadOnly(event.readOnly)
|
||||
sel.onFieldGeometry(event.geometry, event.text.isNotEmpty())
|
||||
}
|
||||
ImeEvent.WantKeyboard -> {
|
||||
@@ -398,6 +399,7 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) {
|
||||
// Re-arm "the field has text" so a tap can show the insertion handle again (a tab
|
||||
// switch resets it). Geometry stays null here, so this can't pop a handle up on its
|
||||
// own — native shows nothing until the user touches the field.
|
||||
sel.onFieldReadOnly(event.focus.readOnly)
|
||||
sel.onFieldGeometry(null, event.focus.text.isNotEmpty())
|
||||
}
|
||||
ImeEvent.Blur -> {
|
||||
@@ -529,19 +531,29 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) {
|
||||
// In-field (<input>/<textarea>) range selection: cut/copy/paste/select-all routed to the hidden
|
||||
// EditText, plus draggable start/end handles (drag → `ime.fieldextend`). The toolbar hides while a
|
||||
// handle is dragged or the page scrolls; the handles hide only while scrolling.
|
||||
// A readonly field offers only the non-destructive half: its text can be selected and copied, but Cut
|
||||
// and Paste would silently do nothing (the page rejects the edit), so native never offers them there.
|
||||
val fieldItems =
|
||||
listOf(
|
||||
"Cut" to {
|
||||
imeView.cutSelection()
|
||||
Unit
|
||||
listOfNotNull(
|
||||
if (sel.fieldReadOnly) {
|
||||
null
|
||||
} else {
|
||||
"Cut" to {
|
||||
imeView.cutSelection()
|
||||
Unit
|
||||
}
|
||||
},
|
||||
"Copy" to {
|
||||
imeView.copySelection()
|
||||
Unit
|
||||
},
|
||||
"Paste" to {
|
||||
imeView.pasteClipboard()
|
||||
Unit
|
||||
if (sel.fieldReadOnly) {
|
||||
null
|
||||
} else {
|
||||
"Paste" to {
|
||||
imeView.pasteClipboard()
|
||||
Unit
|
||||
}
|
||||
},
|
||||
"Select all" to {
|
||||
imeView.selectAllText()
|
||||
|
||||
+5
-2
@@ -164,9 +164,12 @@ class RemoteImeView(
|
||||
|
||||
fun copySelection(): Boolean = onTextContextMenuItem(android.R.id.copy)
|
||||
|
||||
fun cutSelection(): Boolean = onTextContextMenuItem(android.R.id.cut)
|
||||
// Cut and paste mutate the field, so they are refused on a readonly one: the page would reject the edit
|
||||
// and the mirror would drift out of sync with it. The toolbar already hides them there — this is the
|
||||
// backstop, kept next to the ops themselves so a future call site can't reintroduce the divergence.
|
||||
fun cutSelection(): Boolean = !fieldReadOnly && onTextContextMenuItem(android.R.id.cut)
|
||||
|
||||
fun pasteClipboard(): Boolean = onTextContextMenuItem(android.R.id.paste)
|
||||
fun pasteClipboard(): Boolean = !fieldReadOnly && onTextContextMenuItem(android.R.id.paste)
|
||||
|
||||
fun selectAllText(): Boolean = onTextContextMenuItem(android.R.id.selectAll)
|
||||
|
||||
|
||||
+15
-1
@@ -66,6 +66,14 @@ class SelectionUiState {
|
||||
var fieldHasText by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
/**
|
||||
* The focused field is `readonly`. Its text can still be selected and copied — that is what native offers
|
||||
* — but nothing can be placed or changed in it, so it gets no insertion caret handle and no editing
|
||||
* actions in the toolbar.
|
||||
*/
|
||||
var fieldReadOnly by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
/** Plain page-text (non-editable) selection. */
|
||||
var pageSelection by mutableStateOf<ImeEvent.PageSelection?>(null)
|
||||
private set
|
||||
@@ -86,7 +94,7 @@ class SelectionUiState {
|
||||
// ---- derived visibility (read in composition; track the backing state) ----
|
||||
|
||||
val insertionHandle: SelectionGeometry?
|
||||
get() = if (caretShown && fieldHasText && !fieldHasRange && !scrolling) insertionGeometry else null
|
||||
get() = if (caretShown && fieldHasText && !fieldReadOnly && !fieldHasRange && !scrolling) insertionGeometry else null
|
||||
|
||||
/** Geometry for the insertion-handle Paste/Select-All popup, or null when it shouldn't show. */
|
||||
val insertionPopupAt: SelectionGeometry?
|
||||
@@ -165,10 +173,16 @@ class SelectionUiState {
|
||||
fieldRange = null
|
||||
fieldHasRange = false
|
||||
fieldHasText = false
|
||||
fieldReadOnly = false
|
||||
caretShown = false
|
||||
insertionPopup = false
|
||||
}
|
||||
|
||||
/** Records whether the newly focused field is `readonly` — see [fieldReadOnly]. */
|
||||
fun onFieldReadOnly(readOnly: Boolean) {
|
||||
fieldReadOnly = readOnly
|
||||
}
|
||||
|
||||
fun onPageSelection(sel: ImeEvent.PageSelection?) {
|
||||
pageSelection = sel
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user