Skip to content

Commit ecf1d18

Browse files
Add gyro aiming for gamepad sticks and mouse
1 parent 277cc9c commit ecf1d18

7 files changed

Lines changed: 778 additions & 8 deletions

File tree

app/src/main/java/app/gamenative/PrefManager.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,38 @@ object PrefManager {
670670
setPref(PORTRAIT_MODE, value)
671671
}
672672

673+
private val CONTROLS_GYRO_MODE = stringPreferencesKey("controls_gyro_mode")
674+
var controlsGyroMode: String
675+
get() = getPref(CONTROLS_GYRO_MODE, "disabled")
676+
set(value) {
677+
setPref(CONTROLS_GYRO_MODE, value)
678+
}
679+
680+
fun setGyroMode(value: String) {
681+
controlsGyroMode = value
682+
}
683+
684+
private val CONTROLS_GYRO_LAST_TARGET = intPreferencesKey("controls_gyro_last_target")
685+
var controlsGyroLastTarget: Int
686+
get() = getPref(CONTROLS_GYRO_LAST_TARGET, 1).coerceIn(1, 3)
687+
set(value) {
688+
setPref(CONTROLS_GYRO_LAST_TARGET, value.coerceIn(1, 3))
689+
}
690+
691+
private val CONTROLS_GYRO_INVERT_X = booleanPreferencesKey("controls_gyro_invert_x")
692+
var controlsGyroInvertX: Boolean
693+
get() = getPref(CONTROLS_GYRO_INVERT_X, false)
694+
set(value) {
695+
setPref(CONTROLS_GYRO_INVERT_X, value)
696+
}
697+
698+
private val CONTROLS_GYRO_INVERT_Y = booleanPreferencesKey("controls_gyro_invert_y")
699+
var controlsGyroInvertY: Boolean
700+
get() = getPref(CONTROLS_GYRO_INVERT_Y, false)
701+
set(value) {
702+
setPref(CONTROLS_GYRO_INVERT_Y, value)
703+
}
704+
673705
private val BOX_86_VERSION = stringPreferencesKey("box86_version")
674706
var box86Version: String
675707
get() = getPref(BOX_86_VERSION, DefaultVersion.BOX86)

app/src/main/java/app/gamenative/ui/component/QuickMenu.kt

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
2222
import androidx.compose.foundation.layout.Arrangement
2323
import androidx.compose.foundation.layout.Box
2424
import androidx.compose.foundation.layout.Column
25+
import androidx.compose.foundation.layout.FlowRow
2526
import androidx.compose.foundation.layout.Row
2627
import androidx.compose.foundation.layout.Spacer
2728
import androidx.compose.foundation.layout.fillMaxHeight
@@ -220,6 +221,16 @@ fun QuickMenu(
220221
performanceHudConfig: PerformanceHudConfig = PerformanceHudConfig(),
221222
onPerformanceHudConfigChanged: (PerformanceHudConfig) -> Unit = {},
222223
hasPhysicalController: Boolean = false,
224+
gyroEnabled: Boolean = false,
225+
onGyroEnabledChanged: (Boolean) -> Unit = {},
226+
gyroMapping: Int = 1,
227+
onGyroMappingChanged: (Int) -> Unit = {},
228+
gyroSensitivity: Float = 0.35f,
229+
onGyroSensitivityChanged: (Float) -> Unit = {},
230+
gyroInvertX: Boolean = false,
231+
gyroInvertY: Boolean = false,
232+
onGyroInvertXChanged: (Boolean) -> Unit = {},
233+
onGyroInvertYChanged: (Boolean) -> Unit = {},
223234
activeToggleIds: Set<Int> = emptySet(),
224235
modifier: Modifier = Modifier,
225236
) {
@@ -492,7 +503,7 @@ fun QuickMenu(
492503
}
493504
}
494505

495-
else -> {
506+
QuickMenuTab.CONTROLLER -> {
496507
Column(
497508
modifier = Modifier
498509
.fillMaxSize()
@@ -512,6 +523,18 @@ fun QuickMenu(
512523
focusRequester = if (index == 0) controllerItemFocusRequester else null,
513524
)
514525
}
526+
ControllerGyroSection(
527+
gyroEnabled = gyroEnabled,
528+
onGyroEnabledChanged = onGyroEnabledChanged,
529+
gyroMapping = gyroMapping,
530+
onGyroMappingChanged = onGyroMappingChanged,
531+
gyroSensitivity = gyroSensitivity,
532+
onGyroSensitivityChanged = onGyroSensitivityChanged,
533+
gyroInvertX = gyroInvertX,
534+
onGyroInvertXChanged = onGyroInvertXChanged,
535+
gyroInvertY = gyroInvertY,
536+
onGyroInvertYChanged = onGyroInvertYChanged,
537+
)
515538
}
516539
}
517540
}
@@ -537,6 +560,91 @@ fun QuickMenu(
537560
}
538561
}
539562

563+
@Composable
564+
private fun ControllerGyroSection(
565+
gyroEnabled: Boolean,
566+
onGyroEnabledChanged: (Boolean) -> Unit,
567+
gyroMapping: Int,
568+
onGyroMappingChanged: (Int) -> Unit,
569+
gyroSensitivity: Float,
570+
onGyroSensitivityChanged: (Float) -> Unit,
571+
gyroInvertX: Boolean,
572+
onGyroInvertXChanged: (Boolean) -> Unit,
573+
gyroInvertY: Boolean,
574+
onGyroInvertYChanged: (Boolean) -> Unit,
575+
) {
576+
val accentColor = PluviaTheme.colors.accentPurple
577+
Spacer(modifier = Modifier.height(8.dp))
578+
QuickMenuToggleRow(
579+
title = stringResource(R.string.quick_menu_tab_gyro),
580+
subtitle = stringResource(R.string.controller_gyro_mode_subtitle),
581+
enabled = gyroEnabled,
582+
onToggle = { onGyroEnabledChanged(!gyroEnabled) },
583+
accentColor = accentColor,
584+
)
585+
Spacer(modifier = Modifier.height(8.dp))
586+
Column(
587+
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
588+
verticalArrangement = Arrangement.spacedBy(6.dp),
589+
) {
590+
Text(
591+
text = stringResource(R.string.controller_gyro_mapping),
592+
style = MaterialTheme.typography.labelLarge,
593+
color = MaterialTheme.colorScheme.onSurface,
594+
fontWeight = FontWeight.Medium,
595+
)
596+
FlowRow(
597+
horizontalArrangement = Arrangement.spacedBy(8.dp),
598+
verticalArrangement = Arrangement.spacedBy(8.dp),
599+
) {
600+
QuickMenuChoiceChip(
601+
text = stringResource(R.string.left_stick),
602+
selected = gyroMapping == 1,
603+
accentColor = accentColor,
604+
onClick = { onGyroMappingChanged(1) },
605+
)
606+
QuickMenuChoiceChip(
607+
text = stringResource(R.string.right_stick),
608+
selected = gyroMapping == 2,
609+
accentColor = accentColor,
610+
onClick = { onGyroMappingChanged(2) },
611+
)
612+
QuickMenuChoiceChip(
613+
text = stringResource(R.string.mouse),
614+
selected = gyroMapping == 3,
615+
accentColor = accentColor,
616+
onClick = { onGyroMappingChanged(3) },
617+
)
618+
}
619+
}
620+
Spacer(modifier = Modifier.height(8.dp))
621+
QuickMenuAdjustmentRow(
622+
title = stringResource(R.string.controller_gyro_sensitivity),
623+
valueText = stringResource(
624+
R.string.controller_gyro_sensitivity_value,
625+
(gyroSensitivity * 100f).roundToInt(),
626+
),
627+
progress = normalizedProgress(gyroSensitivity, 0.1f, 2.0f),
628+
onDecrease = { onGyroSensitivityChanged((gyroSensitivity - 0.05f).coerceIn(0.1f, 2.0f)) },
629+
onIncrease = { onGyroSensitivityChanged((gyroSensitivity + 0.05f).coerceIn(0.1f, 2.0f)) },
630+
accentColor = accentColor,
631+
)
632+
Spacer(modifier = Modifier.height(8.dp))
633+
QuickMenuToggleRow(
634+
title = stringResource(R.string.controller_gyro_invert_x),
635+
enabled = gyroInvertX,
636+
onToggle = { onGyroInvertXChanged(!gyroInvertX) },
637+
accentColor = accentColor,
638+
)
639+
QuickMenuToggleRow(
640+
title = stringResource(R.string.controller_gyro_invert_y),
641+
enabled = gyroInvertY,
642+
onToggle = { onGyroInvertYChanged(!gyroInvertY) },
643+
accentColor = accentColor,
644+
)
645+
Spacer(modifier = Modifier.height(12.dp))
646+
}
647+
540648
@Composable
541649
private fun PerformanceHudQuickMenuTab(
542650
isPerformanceHudEnabled: Boolean,

app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import androidx.compose.runtime.MutableState
3939
import androidx.compose.runtime.SideEffect
4040
import androidx.compose.runtime.getValue
4141
import androidx.compose.runtime.key
42+
import androidx.compose.runtime.mutableIntStateOf
4243
import androidx.compose.runtime.mutableStateOf
4344
import androidx.compose.runtime.remember
4445
import androidx.compose.runtime.saveable.rememberSaveable
@@ -194,6 +195,30 @@ private val isExiting = AtomicBoolean(false)
194195
private const val EXIT_PROCESS_TIMEOUT_MS = 30_000L
195196
private const val EXIT_PROCESS_POLL_INTERVAL_MS = 1_000L
196197
private const val EXIT_PROCESS_RESPONSE_TIMEOUT_MS = 2_000L
198+
private const val PREF_CONTROLS_GYRO_MODE = "controls_gyro_mode"
199+
private const val PREF_CONTROLS_GYRO_SENSITIVITY = "controls_gyro_sensitivity"
200+
private const val GYRO_MODE_DISABLED = 0
201+
private const val GYRO_MODE_LEFT_STICK = 1
202+
private const val GYRO_MODE_RIGHT_STICK = 2
203+
private const val GYRO_MODE_MOUSE = 3
204+
205+
private fun parseGyroMode(value: String?): Int {
206+
return when (value?.lowercase(Locale.getDefault())) {
207+
"left_stick" -> GYRO_MODE_LEFT_STICK
208+
"right_stick" -> GYRO_MODE_RIGHT_STICK
209+
"mouse" -> GYRO_MODE_MOUSE
210+
else -> GYRO_MODE_DISABLED
211+
}
212+
}
213+
214+
private fun gyroModeToPrefValue(mode: Int): String {
215+
return when (mode) {
216+
GYRO_MODE_LEFT_STICK -> "left_stick"
217+
GYRO_MODE_RIGHT_STICK -> "right_stick"
218+
GYRO_MODE_MOUSE -> "mouse"
219+
else -> "disabled"
220+
}
221+
}
197222

198223
private data class XServerViewReleaseBinding(
199224
val xServerView: XServerView,
@@ -378,6 +403,18 @@ fun XServerScreen(
378403
var hasPhysicalMouse by remember { mutableStateOf(false) }
379404
var hasInternalTouchpad by remember { mutableStateOf(false) }
380405
var hasUpdatedScreenGamepad by remember { mutableStateOf(false) }
406+
var controlsGyroMode by remember {
407+
mutableStateOf(parseGyroMode(PrefManager.getString(PREF_CONTROLS_GYRO_MODE, "disabled")))
408+
}
409+
var controlsGyroLastTarget by remember {
410+
val mode = parseGyroMode(PrefManager.getString(PREF_CONTROLS_GYRO_MODE, "disabled"))
411+
mutableIntStateOf(if (mode != GYRO_MODE_DISABLED) mode else PrefManager.controlsGyroLastTarget.coerceIn(1, 3))
412+
}
413+
var controlsGyroSensitivity by remember {
414+
mutableStateOf(PrefManager.getFloat(PREF_CONTROLS_GYRO_SENSITIVITY, 0.35f).coerceIn(0.1f, 2.0f))
415+
}
416+
var controlsGyroInvertX by remember { mutableStateOf(PrefManager.controlsGyroInvertX) }
417+
var controlsGyroInvertY by remember { mutableStateOf(PrefManager.controlsGyroInvertY) }
381418
var isPerformanceHudEnabled by remember { mutableStateOf(PrefManager.showFps) }
382419

383420
fun loadPerformanceHudConfig(): PerformanceHudConfig {
@@ -1123,8 +1160,13 @@ fun XServerScreen(
11231160
} else {
11241161
var handled = false
11251162
if (isGamepad && it.event != null) {
1163+
// Physical handler and InputControlsView share the same joystick pipeline; do not
1164+
// call both or axes/triggers are applied twice. Match onKeyEvent: physical first,
1165+
// overlay only if nothing consumed (e.g. no PhysicalControllerHandler yet).
11261166
handled = physicalControllerHandler?.onGenericMotionEvent(it.event!!) == true
1127-
if (!handled) handled = PluviaApp.inputControlsView?.onGenericMotionEvent(it.event) == true
1167+
if (!handled) {
1168+
handled = PluviaApp.inputControlsView?.onGenericMotionEvent(it.event) == true
1169+
}
11281170
// Final fallback to WinHandler passthrough
11291171
if (!handled) handled = xServerView!!.getxServer().winHandler.onGenericMotionEvent(it.event)
11301172
}
@@ -1746,6 +1788,10 @@ fun XServerScreen(
17461788

17471789
// Set container-level shooter mode
17481790
setContainerShooterMode(container.isShooterMode)
1791+
setGyroMode(controlsGyroMode)
1792+
setGyroSensitivity(controlsGyroSensitivity)
1793+
setGyroInvertX(controlsGyroInvertX)
1794+
setGyroInvertY(controlsGyroInvertY)
17491795
}
17501796
PluviaApp.inputControlsView = icView
17511797

@@ -2040,6 +2086,53 @@ fun XServerScreen(
20402086
performanceHudConfig = performanceHudConfig,
20412087
onPerformanceHudConfigChanged = ::applyPerformanceHudConfig,
20422088
hasPhysicalController = hasPhysicalController,
2089+
gyroEnabled = controlsGyroMode != GYRO_MODE_DISABLED,
2090+
onGyroEnabledChanged = { enabled ->
2091+
if (enabled) {
2092+
val target = controlsGyroLastTarget.coerceIn(GYRO_MODE_LEFT_STICK, GYRO_MODE_MOUSE)
2093+
controlsGyroLastTarget = target
2094+
controlsGyroMode = target
2095+
PrefManager.controlsGyroLastTarget = target
2096+
PrefManager.setGyroMode(gyroModeToPrefValue(target))
2097+
PluviaApp.inputControlsView?.setGyroMode(target)
2098+
} else {
2099+
val target = controlsGyroLastTarget.coerceIn(GYRO_MODE_LEFT_STICK, GYRO_MODE_MOUSE)
2100+
controlsGyroLastTarget = target
2101+
PrefManager.controlsGyroLastTarget = target
2102+
controlsGyroMode = GYRO_MODE_DISABLED
2103+
PrefManager.setGyroMode("disabled")
2104+
PluviaApp.inputControlsView?.setGyroMode(GYRO_MODE_DISABLED)
2105+
}
2106+
},
2107+
gyroMapping = controlsGyroLastTarget.coerceIn(GYRO_MODE_LEFT_STICK, GYRO_MODE_MOUSE),
2108+
onGyroMappingChanged = { target ->
2109+
val t = target.coerceIn(GYRO_MODE_LEFT_STICK, GYRO_MODE_MOUSE)
2110+
controlsGyroLastTarget = t
2111+
PrefManager.controlsGyroLastTarget = t
2112+
if (controlsGyroMode != GYRO_MODE_DISABLED) {
2113+
controlsGyroMode = t
2114+
PrefManager.setGyroMode(gyroModeToPrefValue(t))
2115+
PluviaApp.inputControlsView?.setGyroMode(t)
2116+
}
2117+
},
2118+
gyroSensitivity = controlsGyroSensitivity,
2119+
onGyroSensitivityChanged = { sensitivity ->
2120+
controlsGyroSensitivity = sensitivity
2121+
PrefManager.setFloat(PREF_CONTROLS_GYRO_SENSITIVITY, sensitivity)
2122+
PluviaApp.inputControlsView?.setGyroSensitivity(sensitivity)
2123+
},
2124+
gyroInvertX = controlsGyroInvertX,
2125+
gyroInvertY = controlsGyroInvertY,
2126+
onGyroInvertXChanged = { invert ->
2127+
controlsGyroInvertX = invert
2128+
PrefManager.controlsGyroInvertX = invert
2129+
PluviaApp.inputControlsView?.setGyroInvertX(invert)
2130+
},
2131+
onGyroInvertYChanged = { invert ->
2132+
controlsGyroInvertY = invert
2133+
PrefManager.controlsGyroInvertY = invert
2134+
PluviaApp.inputControlsView?.setGyroInvertY(invert)
2135+
},
20432136
activeToggleIds = buildSet {
20442137
if (areControlsVisible) add(QuickMenuAction.INPUT_CONTROLS)
20452138
},
@@ -2372,7 +2465,6 @@ private fun showInputControls(profile: ControlsProfile, winHandler: WinHandler,
23722465
private fun hideInputControls() {
23732466
PluviaApp.inputControlsView?.setShowTouchscreenControls(false)
23742467
PluviaApp.inputControlsView?.setVisibility(View.GONE)
2375-
PluviaApp.inputControlsView?.setProfile(null)
23762468

23772469
PluviaApp.touchpadView?.setSensitivity(1.0f)
23782470
PluviaApp.touchpadView?.setPointerButtonLeftEnabled(true)

0 commit comments

Comments
 (0)