Skip to content

Commit 3859fda

Browse files
committed
Refactor input handling to use overlay priority pattern
Replace drawer routing with overlay handler that checks state internally. The handler returns UNHANDLED when drawer is closed, allowing events to fall through to the active screen handler. Eliminates routing desync bugs.
1 parent 05e55c7 commit 3859fda

4 files changed

Lines changed: 42 additions & 37 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ android {
2222
applicationId = "com.nendo.argosy"
2323
minSdk = 26
2424
targetSdk = 34
25-
versionCode = 19
26-
versionName = "0.5.12"
25+
versionCode = 20
26+
versionName = "0.5.13"
2727

2828
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2929
vectorDrawables {

app/src/main/kotlin/com/nendo/argosy/ui/ArgosyApp.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ fun ArgosyApp(
5353
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
5454
val scope = rememberCoroutineScope()
5555

56-
val inputDispatcher = remember(drawerState) {
56+
val inputDispatcher = remember {
5757
InputDispatcher(
5858
hapticManager = viewModel.hapticManager,
5959
soundManager = viewModel.soundManager
60-
).also { it.setDrawerOpenProvider { drawerState.isOpen } }
60+
)
6161
}
6262

6363
val startDestination = if (uiState.isFirstRun) {
@@ -89,11 +89,13 @@ fun ArgosyApp(
8989
}
9090
}
9191

92-
val drawerInputHandler = remember(currentRoute) {
92+
val drawerInputHandler = remember {
9393
viewModel.createDrawerInputHandler(
94+
isDrawerOpen = { drawerState.isOpen },
9495
onNavigate = { route ->
9596
scope.launch { drawerState.close() }
96-
if (route != currentRoute) {
97+
val current = navController.currentDestination?.route
98+
if (route != current) {
9799
navController.navigate(route) {
98100
popUpTo(Screen.Home.route) { saveState = true }
99101
launchSingleTop = true
@@ -106,8 +108,8 @@ fun ArgosyApp(
106108
}
107109

108110
DisposableEffect(drawerInputHandler) {
109-
inputDispatcher.setDrawerHandler(drawerInputHandler)
110-
onDispose { inputDispatcher.setDrawerHandler(null) }
111+
inputDispatcher.setOverlayHandler(drawerInputHandler)
112+
onDispose { inputDispatcher.setOverlayHandler(null) }
111113
}
112114

113115
LaunchedEffect(Unit) {

app/src/main/kotlin/com/nendo/argosy/ui/ArgosyViewModel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,12 @@ class ArgosyViewModel @Inject constructor(
154154
}
155155

156156
fun createDrawerInputHandler(
157+
isDrawerOpen: () -> Boolean,
157158
onNavigate: (String) -> Unit,
158159
onDismiss: () -> Unit
159160
): InputHandler = object : InputHandler {
160161
override fun onUp(): InputResult {
162+
if (!isDrawerOpen()) return InputResult.UNHANDLED
161163
if (_drawerFocusIndex.value > 0) {
162164
_drawerFocusIndex.update { it - 1 }
163165
return InputResult.HANDLED
@@ -166,6 +168,7 @@ class ArgosyViewModel @Inject constructor(
166168
}
167169

168170
override fun onDown(): InputResult {
171+
if (!isDrawerOpen()) return InputResult.UNHANDLED
169172
if (_drawerFocusIndex.value < drawerItems.lastIndex) {
170173
_drawerFocusIndex.update { it + 1 }
171174
return InputResult.HANDLED
@@ -177,16 +180,19 @@ class ArgosyViewModel @Inject constructor(
177180
override fun onRight(): InputResult = InputResult.UNHANDLED
178181

179182
override fun onConfirm(): InputResult {
183+
if (!isDrawerOpen()) return InputResult.UNHANDLED
180184
onNavigate(drawerItems[_drawerFocusIndex.value].route)
181185
return InputResult.HANDLED
182186
}
183187

184188
override fun onBack(): InputResult {
189+
if (!isDrawerOpen()) return InputResult.UNHANDLED
185190
onDismiss()
186191
return InputResult.HANDLED
187192
}
188193

189194
override fun onMenu(): InputResult {
195+
if (!isDrawerOpen()) return InputResult.UNHANDLED
190196
onDismiss()
191197
return InputResult.HANDLED
192198
}

app/src/main/kotlin/com/nendo/argosy/ui/input/InputDispatcher.kt

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,25 @@ class InputDispatcher(
99
private val soundManager: SoundFeedbackManager? = null
1010
) {
1111
private var activeHandler: InputHandler? = null
12-
private var drawerHandler: InputHandler? = null
13-
private var drawerOpenProvider: (() -> Boolean)? = null
12+
private var overlayHandler: InputHandler? = null
1413
private var pendingEvent: GamepadEvent? = null
1514
private var inputBlockedUntil: Long = 0L
1615

17-
private val isDrawerOpen: Boolean
18-
get() = drawerOpenProvider?.invoke() ?: false
19-
2016
fun setActiveScreen(handler: InputHandler?) {
2117
activeHandler = handler
22-
if (handler != null && !isDrawerOpen) {
23-
pendingEvent?.let { event ->
24-
pendingEvent = null
25-
dispatch(event)
26-
}
27-
}
18+
handler?.let { processPendingEvent() }
2819
}
2920

30-
fun setDrawerHandler(handler: InputHandler?) {
31-
drawerHandler = handler
32-
if (handler != null && isDrawerOpen) {
33-
pendingEvent?.let { event ->
34-
pendingEvent = null
35-
dispatch(event)
36-
}
37-
}
21+
fun setOverlayHandler(handler: InputHandler?) {
22+
overlayHandler = handler
23+
handler?.let { processPendingEvent() }
3824
}
3925

40-
fun setDrawerOpenProvider(provider: () -> Boolean) {
41-
drawerOpenProvider = provider
26+
private fun processPendingEvent() {
27+
pendingEvent?.let { event ->
28+
pendingEvent = null
29+
dispatch(event)
30+
}
4231
}
4332

4433
fun blockInputFor(durationMs: Long) {
@@ -50,20 +39,30 @@ class InputDispatcher(
5039
return true
5140
}
5241

53-
val handler = if (isDrawerOpen) drawerHandler else activeHandler
42+
overlayHandler?.let { handler ->
43+
val result = dispatchToHandler(event, handler)
44+
if (result.handled) {
45+
playFeedback(event, result)
46+
return true
47+
}
48+
}
5449

50+
val handler = activeHandler
5551
if (handler == null) {
5652
pendingEvent = event
5753
return false
5854
}
5955

6056
pendingEvent = null
6157
val result = dispatchToHandler(event, handler)
62-
val handled = result.handled
58+
playFeedback(event, result)
59+
return result.handled
60+
}
6361

62+
private fun playFeedback(event: GamepadEvent, result: InputResult) {
6463
when (event) {
6564
GamepadEvent.Up, GamepadEvent.Down, GamepadEvent.Left, GamepadEvent.Right -> {
66-
if (handled) {
65+
if (result.handled) {
6766
hapticManager?.vibrate(HapticPattern.FOCUS_CHANGE)
6867
soundManager?.play(result.soundOverride ?: SoundType.NAVIGATE)
6968
} else {
@@ -72,7 +71,7 @@ class InputDispatcher(
7271
}
7372
}
7473
GamepadEvent.PrevSection, GamepadEvent.NextSection -> {
75-
if (handled) {
74+
if (result.handled) {
7675
hapticManager?.vibrate(HapticPattern.FOCUS_CHANGE)
7776
soundManager?.play(result.soundOverride ?: SoundType.SECTION_CHANGE)
7877
} else {
@@ -81,20 +80,18 @@ class InputDispatcher(
8180
}
8281
}
8382
GamepadEvent.Confirm -> {
84-
if (handled) {
83+
if (result.handled) {
8584
hapticManager?.vibrate(HapticPattern.SELECTION)
8685
soundManager?.play(result.soundOverride ?: SoundType.SELECT)
8786
}
8887
}
8988
GamepadEvent.Back -> {
90-
if (handled) {
89+
if (result.handled) {
9190
soundManager?.play(result.soundOverride ?: SoundType.BACK)
9291
}
9392
}
9493
else -> {}
9594
}
96-
97-
return handled
9895
}
9996

10097
private fun dispatchToHandler(event: GamepadEvent, handler: InputHandler): InputResult {

0 commit comments

Comments
 (0)