mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
fix: embed surface no longer blacks out on double-tap / first open
Two black-surface reports traced to the active-tab bookkeeping: - Double-tapping a bottom-bar tab pops and re-adds the SAME route, so the outgoing screen disposes AFTER the incoming one has already called setActive(id). clearActiveIfMatches(id) then nulled the active id the new instance just set (same id "matches"), leaving no active tab — every warm surface gets shoved off-screen and the embed goes black. setActive now returns a monotonic ownership token and the disposer clears only if it's still the latest claim (clearActiveIfOwner), so a re-nav can't null the new owner. clearActiveChrome got the same twin guard. - Revert the reportBounds active-id guard added in the audit batch: it tied contentBounds to the same fragile activeId, so a first-ever embed whose bounds reported while the id was momentarily unset stayed at Rect.Zero (parked off-screen → black). Bounds are reported unconditionally again; the two screens cover the same content area, so there's nothing to clobber. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
This commit is contained in:
+3
-3
@@ -144,9 +144,9 @@ private fun EmbeddedFavoriteTab(
|
||||
|
||||
val bottomBarFlow = accountViewModel.settings.uiSettingsFlow.bottomBarItems
|
||||
DisposableEffect(id) {
|
||||
EmbeddedTabHost.setActive(id)
|
||||
val token = EmbeddedTabHost.setActive(id)
|
||||
onDispose {
|
||||
EmbeddedTabHost.clearActiveIfMatches(id)
|
||||
EmbeddedTabHost.clearActiveIfOwner(token)
|
||||
EmbeddedTabHost.clearActiveChrome(id)
|
||||
// Only bottom-row apps stay warm; anything else restarts when it leaves.
|
||||
if (id !in bottomBarFlow.value.favoriteIds()) EmbeddedTabHost.evict(id)
|
||||
@@ -165,7 +165,7 @@ private fun EmbeddedFavoriteTab(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.onGloballyPositioned { EmbeddedTabHost.reportBounds(id, it.boundsInWindow()) },
|
||||
.onGloballyPositioned { EmbeddedTabHost.reportBounds(it.boundsInWindow()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-10
@@ -82,7 +82,10 @@ object EmbeddedTabHost {
|
||||
}
|
||||
|
||||
fun clearActiveChrome(id: String) {
|
||||
if (chromeOwner == id) {
|
||||
// Same twin race as the active id: on a same-route re-nav the new instance has already published
|
||||
// its chrome (same id) before the old one disposes. Only clear when this id is no longer the
|
||||
// active tab, so the incoming tab's chrome survives.
|
||||
if (chromeOwner == id && activeId != id) {
|
||||
chromeOwner = null
|
||||
activeChrome = null
|
||||
}
|
||||
@@ -99,20 +102,27 @@ object EmbeddedTabHost {
|
||||
return controller
|
||||
}
|
||||
|
||||
fun setActive(id: String) {
|
||||
// Monotonic ownership token. Double-tapping a bottom-bar tab pops and re-adds the SAME route, so the
|
||||
// outgoing screen instance disposes *after* the incoming one has already called setActive with the
|
||||
// same id. A plain `clearActiveIfMatches(id)` would then null the active id the new instance just set
|
||||
// (same id → it "matches"), leaving no active tab and blacking the surface out. Tokening each
|
||||
// setActive lets the disposer clear only if nobody claimed active in the meantime.
|
||||
private var activeToken = 0L
|
||||
|
||||
/** Marks [id] the active tab and returns the ownership token to hand back to [clearActiveIfOwner]. */
|
||||
fun setActive(id: String): Long {
|
||||
activeId = id
|
||||
activeToken += 1
|
||||
return activeToken
|
||||
}
|
||||
|
||||
fun clearActiveIfMatches(id: String) {
|
||||
if (activeId == id) activeId = null
|
||||
/** Clears the active tab only if [token] is still the latest claim (no newer [setActive] ran). */
|
||||
fun clearActiveIfOwner(token: Long) {
|
||||
if (activeToken == token) activeId = null
|
||||
}
|
||||
|
||||
fun reportBounds(
|
||||
id: String,
|
||||
bounds: Rect,
|
||||
) {
|
||||
// Only the active tab positions the surface; a cross-fading outgoing screen must not move it.
|
||||
if (activeId == id) contentBounds = bounds
|
||||
fun reportBounds(bounds: Rect) {
|
||||
contentBounds = bounds
|
||||
}
|
||||
|
||||
fun evict(id: String) {
|
||||
|
||||
+3
-3
@@ -158,9 +158,9 @@ private fun EmbeddedNappletTab(
|
||||
|
||||
val bottomBarFlow = accountViewModel.settings.uiSettingsFlow.bottomBarItems
|
||||
DisposableEffect(id) {
|
||||
EmbeddedTabHost.setActive(id)
|
||||
val token = EmbeddedTabHost.setActive(id)
|
||||
onDispose {
|
||||
EmbeddedTabHost.clearActiveIfMatches(id)
|
||||
EmbeddedTabHost.clearActiveIfOwner(token)
|
||||
EmbeddedTabHost.clearActiveChrome(id)
|
||||
// Only bottom-row apps stay warm; anything else restarts when it leaves.
|
||||
if (id !in bottomBarFlow.value.favoriteIds()) EmbeddedTabHost.evict(id)
|
||||
@@ -200,7 +200,7 @@ private fun EmbeddedNappletTab(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.onGloballyPositioned { EmbeddedTabHost.reportBounds(id, it.boundsInWindow()) },
|
||||
.onGloballyPositioned { EmbeddedTabHost.reportBounds(it.boundsInWindow()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user