Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ docs/validation/recorded-tests/** -text -whitespace
# Preserve WebView engine evidence bytes, including native XML CRLF.
docs/validation/webview-engine/** -text -whitespace
docs/validation/native-input/** -text -whitespace

# Preserve touch-search receipts and raw native hierarchy CRLF bytes.
docs/validation/touch-search/** -text -whitespace

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions app/src/main/java/dev/tinycast/android/EmojiUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import androidx.compose.ui.unit.*
/** A palette sub-screen; no Dialog, overlay permission or clipboard monitoring. */
@Composable
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class, ExperimentalLayoutApi::class)
fun EmojiPicker(store: EmojiStore, close: () -> Unit, copy: (String) -> Unit) {
fun EmojiPicker(store: EmojiStore, searchEpoch: Int, actionCurrent: () -> Boolean, close: () -> Unit, copy: (String) -> Unit) {
val searchFocus = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
val view = LocalView.current
Expand All @@ -53,6 +53,7 @@ fun EmojiPicker(store: EmojiStore, close: () -> Unit, copy: (String) -> Unit) {
// of later modifier release or the selection changing after an unpin.
val favoritePresses = remember { mutableMapOf<Int, Long>() }
val windowFocused = LocalWindowInfo.current.isWindowFocused
val paletteMenus = LocalPaletteMenus.current
LaunchedEffect(windowFocused) { if (!windowFocused) favoritePresses.clear() }
fun consumeFavoriteSequence(event: KeyEvent): Boolean {
if (event.keyCode != KeyEvent.KEYCODE_D || favoritePresses[event.deviceId] != event.downTime) return false
Expand Down Expand Up @@ -191,11 +192,26 @@ fun EmojiPicker(store: EmojiStore, close: () -> Unit, copy: (String) -> Unit) {
decorationBox = { inner -> Box(Modifier.fillMaxWidth().heightIn(min=48.dp), contentAlignment = Alignment.CenterStart) {
if (store.query.isEmpty()) Text("Search Emoji & Symbols…", color = PaletteTheme.secondary, fontSize = 20.sp, lineHeight = 26.sp, maxLines = 1, overflow = TextOverflow.Ellipsis); inner()
} })
SearchClearButton(store.query.isNotEmpty(), Triple(store, searchEpoch, windowFocused), "emoji-search-clear",
current = { store.query.isNotEmpty() && actionCurrent() && view.hasWindowFocus() &&
!menu && !actionsMenu && paletteMenus.current == null }) {
searchValue = TextFieldValue("")
store.search("")
copiedName = null
queryEditing = false
}
if (wide) IconButton(onClick = { keyboardRequested = true; searchFocus.requestFocus(); keyboard?.show() }, modifier = Modifier.size(48.dp).semantics { contentDescription = "Show on-screen keyboard" }) {
Glyph("keyboard", PaletteTheme.tertiary, 16)
}
Box {
TextButton(onClick = { menu = true }, modifier = Modifier.heightIn(min=48.dp).testTag("emoji-filter")) { Text(store.filter, maxLines = 1, fontSize = 12.sp, overflow = TextOverflow.Ellipsis, modifier = Modifier.widthIn(max = if (wide) 160.dp else 90.dp)); Text(" ▾") }
// Retain natural scaled text and a 48 dp target without extra
// vertical padding shifting this editor below the launcher.
TextButton(onClick = { menu = true }, contentPadding = PaddingValues(horizontal=12.dp, vertical=4.dp),
modifier = Modifier.heightIn(min=48.dp).testTag("emoji-filter")) {
Text(store.filter, maxLines = 1, fontSize = 12.sp, overflow = TextOverflow.Ellipsis,
modifier = Modifier.widthIn(max = if (wide) 160.dp else 90.dp))
Text(" ▾")
}
PaletteChoiceMenu(menu, { menu = false },
(listOf("All", "Favorites", "Recent") + store.entries.map { it.category }.distinct()).map { category ->
PaletteChoice("emoji-filter-$category", category, checked = store.filter == category) {
Expand Down
14 changes: 13 additions & 1 deletion app/src/main/java/dev/tinycast/android/PaletteUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ fun TinycastApp(store: PaletteStore, globalEnabled: Boolean, run: (Command) -> U
return@PaletteMenuHost
}
if (store.dialog == "emoji") {
EmojiPicker(store.emoji, { store.dialog = null; store.focusEpoch++ }, { copy(it); store.message = null })
val emojiEpoch = store.focusEpoch
EmojiPicker(store.emoji, emojiEpoch,
{ store.dialog == "emoji" && store.focusEpoch == emojiEpoch && store.textReview == null &&
!store.backupBusy && ShortcutRuntime.recordingOwner == null },
{ store.dialog = null; store.focusEpoch++ }, { copy(it); store.message = null })
return@PaletteMenuHost
}
val focus = remember { FocusRequester() }
Expand All @@ -110,6 +114,7 @@ fun TinycastApp(store: PaletteStore, globalEnabled: Boolean, run: (Command) -> U
val paletteMenus = LocalPaletteMenus.current
val view = LocalView.current
val windowFocused = LocalWindowInfo.current.isWindowFocused
val searchEpoch = store.focusEpoch
var searchFocused by remember { mutableStateOf(false) }
var keyboardRequested by launcherKeyboard
val list = launcherList
Expand Down Expand Up @@ -278,6 +283,13 @@ fun TinycastApp(store: PaletteStore, globalEnabled: Boolean, run: (Command) -> U
textStyle=TextStyle(color=PaletteTheme.ink,fontSize=20.sp,lineHeight=26.sp),singleLine=true,cursorBrush=SolidColor(PaletteTheme.accent),
keyboardOptions=KeyboardOptions(imeAction=ImeAction.Search,showKeyboardOnFocus=!wide || keyboardRequested),keyboardActions=KeyboardActions(onSearch={activateCurrent()}),
decorationBox={inner -> Box(Modifier.fillMaxWidth().heightIn(min=48.dp),contentAlignment=Alignment.CenterStart){if(store.query.isEmpty())Text("Search for apps and commands…",color=PaletteTheme.secondary,fontSize=20.sp,lineHeight=26.sp,maxLines=1,overflow=TextOverflow.Ellipsis);inner()}})
SearchClearButton(store.query.isNotEmpty(), Triple(store, searchEpoch, windowFocused), "search-clear",
current = { store.query.isNotEmpty() && store.focusEpoch == searchEpoch && store.dialog == null &&
store.textReview == null && !store.backupBusy && ShortcutRuntime.recordingOwner == null &&
view.hasWindowFocus() && !menu && paletteMenus.current == null }) {
searchValue = TextFieldValue("")
store.search("")
}
if(wide) IconButton(onClick={keyboardRequested=true;focus.requestFocus();keyboard?.show()},modifier=Modifier.size(48.dp).semantics{contentDescription="Show on-screen keyboard"}){Glyph("keyboard",PaletteTheme.tertiary,16)}
}
if (store.category == "Files") FileSearchToolbar(store, run)
Expand Down
34 changes: 34 additions & 0 deletions app/src/main/java/dev/tinycast/android/SearchClearButton.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package dev.tinycast.android

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp

private class SearchClearLifetime { var active = true }

/** Keep the editor's width stable, including when there is nothing to clear. */
@Composable
internal fun SearchClearButton(
visible: Boolean,
owner: Any,
tag: String,
current: () -> Boolean,
clear: () -> Unit
) {
val lifetime = remember(owner) { SearchClearLifetime() }
DisposableEffect(lifetime) { onDispose { lifetime.active = false } }
Box(Modifier.size(48.dp)) {
if (visible) IconButton(
onClick = { if (lifetime.active && current()) clear() },
modifier = Modifier.size(48.dp).semantics { contentDescription = "Clear search" }.testTag(tag)
) { Glyph("close", PaletteTheme.secondary, 18) }
}
}
6 changes: 6 additions & 0 deletions docs/PARITY-TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,9 @@ The next active slice is [native input compatibility #69](https://github.com/moi


The [#108 exact-text slice](NATIVE-INPUT-CONTRACTS.md) under still-open [#69](https://github.com/moikdev/droidcast/issues/69) now compares exact editable characters independently of legitimate IME decoration. The unchanged `fec0a4bf` production APK has **23 controlled positives / seven whole cohorts /108 successful cohort restoration checks**, plus **two intended old-annotation failures /zero passes /29 successful restoration checks**. The base `b55a8f6f` harness and the later same-length Unicode supplement keep their distinct APK/source identities. All thirteen read-only final-preservation checks pass, including both original APKs and all573 existing app/evidence files. The checkpoint binds 296 public files plus its index, 249 current source hashes and 21 raw screenshot sets. Recorded JVM343, Python130 per mode, both build variants and lint0 errors/56 warnings pass. System-IME failures, the insensitive initial controls, the first probes' unrecorded original subtype-history state, and the failed modern cleanup with separate sixteen-check recovery remain explicitly retained. The final collector verifies four raw IME settings and retains ownership through restoration. Exact-head hosted CI, both independent sol/high audits and expected-head merge remain gates. This test/fixture correction clears no full Android8 keyboard, physical/OEM/accessibility, whole-Store, #89 or signing/migration claim.

PR #109 merged reviewed `3f87bf7a4182b74ba16c52c113c67613e5f6bb41` as `d8d8978ba63379e332cbef013fa909ea930075da`; merged/reviewed tree `e1e90ae1d5385c70b3f6b073e31ec7fd9ea5f3f7` is identical. Both independent gpt-6.1-sol/high full-head audits cleared. Exact-head hosted Android run42026037454 and separate local/android58146385516 succeeded; root recorded COMMENT review5784970043 and used an expected-head merge. #108 is closed. #69, original pre-probe IME-history identity, physical/OEM/accessibility, #89 and signing/migration remain open or uncertified.

The [#110 Clear-search slice](TOUCH-SEARCH-CONTROLS.md) adds a recognizable X and a fixed 48 dp slot to launcher and Emoji search, preserving the current route/category/editor focus and keyboard intent. Final app `974b2fa…9d750` / instrumentation `54a86c6…366e1` has **25 scoped positives /11 whole cohorts /171 successful restoration checks**, with **five same-harness intended old-app failures /zero passes /79 checks**. Fourteen separate final-preservation checks match both original modern APKs, all573 original app/evidence files, legacy package absence and the current four-field IME baselines. The checkpoint binds 538 public files plus itself, 254 source inputs, 653 retained capture hashes and 92 selected actual screenshot sets. Builds/lint pass with0 errors/56 warnings per variant; JVM343 is retained UP-TO-DATE evidence; fresh Python135/mode passes. Historical transport/layout failures, external launcher ANR and API26 composition-precondition failure remain failed and separately documented. The four-case API26 Clear smoke earns no composition/keyboard repair. Both independent sol/high audits, actual exact-head hosted Android and an expected-head merge remain gates.

Next layout priority: [#111](https://github.com/moikdev/droidcast/issues/111), readable empty-result feedback above the keyboard in short landscape. The Clear harness explicitly records that measured body-visibility gap while keeping its Clear assertions mandatory. #69 system-IME behavior, #71 typography/accessibility and legacy missing emoji glyphs, #89, whole-Store/platform parity, production signing/migration and original pre-probe IME-history identity remain open or uncertified. No project rename or migration is performed.
Loading
Loading