From 4dffd92d348ba8932dfbd9c5968acf78bf8c946b Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Mon, 7 Sep 2026 00:31:16 -0500 Subject: [PATCH 1/5] Add per-profile mouse speed settings --- .../app/gamenative/ui/component/QuickMenu.kt | 30 ++++- .../OnScreenControllerSettingsDialog.kt | 118 ++++++++++++++++++ .../xserver/PhysicalControllerHandler.kt | 2 +- .../ui/screen/xserver/XServerScreen.kt | 4 +- .../inputcontrols/ControlElement.java | 8 +- .../inputcontrols/ControlsProfile.java | 8 +- .../winlator/widget/InputControlsView.java | 27 +++- .../com/winlator/widget/TouchpadView.java | 3 +- app/src/main/res/values/strings.xml | 4 + .../OnScreenControllerSettingsDialogTest.kt | 24 ++++ .../ControlsProfileCursorSpeedTest.kt | 45 +++++++ 11 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt create mode 100644 app/src/test/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialogTest.kt create mode 100644 app/src/test/java/com/winlator/inputcontrols/ControlsProfileCursorSpeedTest.kt diff --git a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt index b44f1a2fea..ff82a24bf9 100644 --- a/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt +++ b/app/src/main/java/app/gamenative/ui/component/QuickMenu.kt @@ -102,6 +102,7 @@ import app.gamenative.R import app.gamenative.data.GyroSettings import app.gamenative.powercontrol.PowerManager import app.gamenative.ui.component.dialog.GyroSettingsDialog +import app.gamenative.ui.component.dialog.OnScreenControllerSettingsDialog import app.gamenative.ui.component.quickMenus.PowerControlQuickMenuTab import app.gamenative.ui.data.PerformanceHudConfig import app.gamenative.ui.data.PerformanceHudSize @@ -109,6 +110,7 @@ import app.gamenative.ui.theme.PluviaTheme import app.gamenative.ui.util.adaptivePanelWidth import app.gamenative.utils.MathUtils.normalizedProgress import com.winlator.container.Container +import com.winlator.inputcontrols.ControlsProfile import com.winlator.renderer.GLRenderer import com.winlator.renderer.VulkanRenderer import com.winlator.winhandler.ProcessInfo @@ -522,6 +524,9 @@ fun QuickMenu( // broken D8 codegen path). val inviteMenu = remember(container?.id) { SteamInviteState.createIfAvailable(container) } var showGyroSettingsDialog by rememberSaveable(container?.id) { mutableStateOf(false) } + var onScreenControllerSettingsProfile by remember(container?.id) { + mutableStateOf(null) + } // Owned here, not plumbed through XServerScreen (register limit; see inviteMenu). var lsfgPresentMode by remember(container?.id) { mutableStateOf(container?.let { app.gamenative.utils.LsfgQuickMenuHelper.presentMode(it) } ?: "mailbox") @@ -1082,21 +1087,27 @@ fun QuickMenu( } }, focusRequester = if (index == 0) controllerItemFocusRequester else null, - secondaryIcon = if (item.id == QuickMenuAction.TOUCHSCREEN_MODE && isTouchscreenModeActive) + secondaryIcon = if (item.id == QuickMenuAction.INPUT_CONTROLS && item.id in activeToggleIds) + Icons.Default.Settings + else if (item.id == QuickMenuAction.TOUCHSCREEN_MODE && isTouchscreenModeActive) Icons.Default.Settings else if (item.id == QuickMenuAction.SHOOTER_MODE && isShooterModeActive) Icons.Default.Settings else if (item.id == QuickMenuAction.GYRO && gyroEnabled) Icons.Default.Settings else null, - secondaryContentDescriptionResId = if (item.id == QuickMenuAction.TOUCHSCREEN_MODE && isTouchscreenModeActive) + secondaryContentDescriptionResId = if (item.id == QuickMenuAction.INPUT_CONTROLS && item.id in activeToggleIds) + R.string.on_screen_controller_settings + else if (item.id == QuickMenuAction.TOUCHSCREEN_MODE && isTouchscreenModeActive) R.string.gesture_settings_title else if (item.id == QuickMenuAction.SHOOTER_MODE && isShooterModeActive) R.string.shooter_mode_settings_title else if (item.id == QuickMenuAction.GYRO && gyroEnabled) R.string.gyro_settings_title else null, - onSecondaryClick = if (item.id == QuickMenuAction.TOUCHSCREEN_MODE && isTouchscreenModeActive) + onSecondaryClick = if (item.id == QuickMenuAction.INPUT_CONTROLS && item.id in activeToggleIds) + ({ onScreenControllerSettingsProfile = PluviaApp.inputControlsView?.profile }) + else if (item.id == QuickMenuAction.TOUCHSCREEN_MODE && isTouchscreenModeActive) onTouchGestureSettingsClick else if (item.id == QuickMenuAction.SHOOTER_MODE && isShooterModeActive) onShooterModeSettingsClick @@ -1137,6 +1148,19 @@ fun QuickMenu( ) } + onScreenControllerSettingsProfile?.let { profile -> + OnScreenControllerSettingsDialog( + initialCursorSpeed = profile.cursorSpeed, + onDismiss = { onScreenControllerSettingsProfile = null }, + onSave = { speed -> + profile.cursorSpeed = speed + profile.save() + PluviaApp.touchpadView?.setSensitivity(profile.cursorSpeed) + onScreenControllerSettingsProfile = null + }, + ) + } + LaunchedEffect(isVisible, selectedTab) { onToolsVisibilityChanged(isVisible && selectedTab == QuickMenuTab.TOOLS) } diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt new file mode 100644 index 0000000000..3f7b0b7b9f --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt @@ -0,0 +1,118 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import app.gamenative.R +import app.gamenative.ui.theme.PluviaBackground +import com.winlator.inputcontrols.ControlsProfile +import java.util.Locale + +internal val DEFAULT_MOUSE_SPEED = ControlsProfile.DEFAULT_CURSOR_SPEED +internal const val MIN_MOUSE_SPEED = 0.1f +internal const val MAX_MOUSE_SPEED = 3.0f + +internal fun mouseSpeedOrDefault(value: Float): Float { + return value.takeIf { it.isFinite() && it > 0f } ?: DEFAULT_MOUSE_SPEED +} + +internal fun mouseSpeedForSlider(value: Float): Float { + return mouseSpeedOrDefault(value).coerceIn(MIN_MOUSE_SPEED, MAX_MOUSE_SPEED) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OnScreenControllerSettingsDialog( + initialCursorSpeed: Float, + onDismiss: () -> Unit, + onSave: (Float) -> Unit, +) { + var cursorSpeed by remember(initialCursorSpeed) { + mutableFloatStateOf(mouseSpeedForSlider(initialCursorSpeed)) + } + val locale = Locale.getDefault() + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true, + dismissOnClickOutside = false, + ), + ) { + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = PluviaBackground, + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = stringResource(R.string.on_screen_controller_settings), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + }, + navigationIcon = { + IconButton(onClick = onDismiss) { + Icon(Icons.Default.Close, contentDescription = stringResource(R.string.close)) + } + }, + actions = { + IconButton(onClick = { cursorSpeed = DEFAULT_MOUSE_SPEED }) { + Icon( + Icons.Default.Refresh, + contentDescription = stringResource(R.string.reset_mouse_speed), + ) + } + IconButton(onClick = { onSave(cursorSpeed) }) { + Icon(Icons.Default.Check, contentDescription = stringResource(R.string.save)) + } + }, + ) + }, + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .padding(bottom = 16.dp) + .verticalScroll(rememberScrollState()), + ) { + SettingsDialogSectionHeader(stringResource(R.string.mouse)) + + SettingsSliderBlock( + title = stringResource(R.string.mouse_speed), + subtitle = stringResource(R.string.mouse_speed_subtitle), + value = cursorSpeed, + valueRange = MIN_MOUSE_SPEED..MAX_MOUSE_SPEED, + valueText = String.format(locale, "%.1fx", cursorSpeed), + onValueChange = { cursorSpeed = it }, + ) + } + } + } +} diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/PhysicalControllerHandler.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/PhysicalControllerHandler.kt index 40845ae8a1..34ec737b4e 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/PhysicalControllerHandler.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/PhysicalControllerHandler.kt @@ -354,7 +354,7 @@ class PhysicalControllerHandler( if (magnitude < 0.08) return // Look up cursor speed dynamically so it updates when profile changes - val cursorSpeed = profile?.cursorSpeed ?: 1f + val cursorSpeed = profile?.cursorSpeed ?: ControlsProfile.DEFAULT_CURSOR_SPEED val deltaX = (mouseMoveOffset.x * 10 * cursorSpeed).toInt() val deltaY = (mouseMoveOffset.y * 10 * cursorSpeed).toInt() xServer?.injectPointerMoveDelta(deltaX, deltaY) diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index da27863c23..958dece9ed 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -3331,7 +3331,7 @@ private fun showInputControls(profile: ControlsProfile, winHandler: WinHandler, } } - PluviaApp.touchpadView?.setSensitivity(profile.getCursorSpeed() * 1.0f) + PluviaApp.touchpadView?.setSensitivity(profile.cursorSpeed) // If the selected profile is a virtual gamepad, we must enable the P1 slot. if (container.containerVariant.equals(Container.BIONIC) && profile.isVirtualGamepad()) { @@ -3354,7 +3354,7 @@ private fun hideInputControls() { PluviaApp.inputControlsView?.hideProfileForOverlay() PluviaApp.xServerView?.getxServer()?.winHandler?.refreshControllerMappingsForHotplug() - PluviaApp.touchpadView?.setSensitivity(1.0f) + PluviaApp.touchpadView?.setSensitivity(ControlsProfile.DEFAULT_CURSOR_SPEED) PluviaApp.touchpadView?.setPointerButtonLeftEnabled(true) PluviaApp.touchpadView?.setPointerButtonRightEnabled(true) PluviaApp.touchpadView?.isEnabled()?.let { diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java index a5d674a2a6..3c234c61e0 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java @@ -1377,6 +1377,10 @@ public boolean handleTouchMove(int pointerId, float x, float y) { else if (type == Type.TRACKPAD) { final boolean[] states = {deltaY <= -TRACKPAD_MIN_SPEED, deltaX >= TRACKPAD_MIN_SPEED, deltaY >= TRACKPAD_MIN_SPEED, deltaX <= -TRACKPAD_MIN_SPEED}; if (handleRadialMenuDirectionalMove(pointerId, states, x, y)) return true; + ControlsProfile activeProfile = inputControlsView.getProfile(); + float cursorSpeed = activeProfile != null + ? activeProfile.getCursorSpeed() + : ControlsProfile.DEFAULT_CURSOR_SPEED; int cursorDx = 0; int cursorDy = 0; @@ -1390,10 +1394,10 @@ else if (type == Type.TRACKPAD) { value *= TouchpadView.CURSOR_ACCELERATION; } if (mouseMoveBinding == Binding.MOUSE_MOVE_LEFT || mouseMoveBinding == Binding.MOUSE_MOVE_RIGHT) { - cursorDx = Mathf.roundPoint(value); + cursorDx = Mathf.roundPoint(value * cursorSpeed); } else { - cursorDy = Mathf.roundPoint(value); + cursorDy = Mathf.roundPoint(value * cursorSpeed); } boolean nextState = states[i]; if (!bindingCombo.isSingleBinding() && this.states[i] != nextState) { diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java index 37dcb9a1c1..61dfc9f729 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java @@ -19,9 +19,11 @@ import java.util.Locale; public class ControlsProfile implements Comparable { + public static final float DEFAULT_CURSOR_SPEED = 1.0f; + public final int id; private String name; - private float cursorSpeed = 1.0f; + private volatile float cursorSpeed = DEFAULT_CURSOR_SPEED; private final ArrayList elements = new ArrayList<>(); private final ArrayList controllers = new ArrayList<>(); private final ArrayList radialMenus = new ArrayList<>(); @@ -51,7 +53,9 @@ public float getCursorSpeed() { } public void setCursorSpeed(float cursorSpeed) { - this.cursorSpeed = cursorSpeed; + this.cursorSpeed = Float.isFinite(cursorSpeed) && cursorSpeed > 0.0f + ? cursorSpeed + : DEFAULT_CURSOR_SPEED; } public boolean isVirtualGamepad() { diff --git a/app/src/main/java/com/winlator/widget/InputControlsView.java b/app/src/main/java/com/winlator/widget/InputControlsView.java index 68d58f13ae..7a06a94ab4 100644 --- a/app/src/main/java/com/winlator/widget/InputControlsView.java +++ b/app/src/main/java/com/winlator/widget/InputControlsView.java @@ -421,6 +421,8 @@ private void setProfileInternal(ControlsProfile profile, boolean preserveOverlay } } + if (profile == null) stopMouseMoveTimer(); + onControlsProfileContentChanged(profileChanged); gyroController.setHasProfile(profile != null); } @@ -431,6 +433,7 @@ public void hideProfileForOverlay() { synchronized (this) { this.profile = null; } + stopMouseMoveTimer(); } /** Re-evaluates latched gyro activation after the active profile is edited in place. */ @@ -554,8 +557,7 @@ public int getMaxWidth() { protected void onDetachedFromWindow() { cancelTouchRouting(); gyroController.onDetachedFromWindow(); - if (mouseMoveTimer != null) - mouseMoveTimer.cancel(); + stopMouseMoveTimer(); super.onDetachedFromWindow(); } @@ -569,19 +571,34 @@ public int getMaxHeight() { return (int)Mathf.roundTo(getHeight(), snappingSize); } - private void createMouseMoveTimer() { + private synchronized void createMouseMoveTimer() { if (profile != null && mouseMoveTimer == null) { - final float cursorSpeed = profile.getCursorSpeed(); mouseMoveTimer = new Timer(); mouseMoveTimer.schedule(new TimerTask() { @Override public void run() { - xServer.injectPointerMoveDelta((int)(mouseMoveOffset.x * 10 * cursorSpeed), (int)(mouseMoveOffset.y * 10 * cursorSpeed)); + ControlsProfile currentProfile = getProfile(); + if (currentProfile == null) return; + + float cursorSpeed = currentProfile.getCursorSpeed(); + int deltaX = (int)(mouseMoveOffset.x * 10 * cursorSpeed); + int deltaY = (int)(mouseMoveOffset.y * 10 * cursorSpeed); + if (deltaX != 0 || deltaY != 0) { + xServer.injectPointerMoveDelta(deltaX, deltaY); + } } }, 0, 1000 / 60); } } + private synchronized void stopMouseMoveTimer() { + if (mouseMoveTimer != null) { + mouseMoveTimer.cancel(); + mouseMoveTimer = null; + } + mouseMoveOffset.set(0, 0); + } + private void processJoystickInput(ExternalController controller) { ExternalControllerBinding controllerBinding; final int[] axes = {MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y}; diff --git a/app/src/main/java/com/winlator/widget/TouchpadView.java b/app/src/main/java/com/winlator/widget/TouchpadView.java index 3456791a7e..af08021727 100644 --- a/app/src/main/java/com/winlator/widget/TouchpadView.java +++ b/app/src/main/java/com/winlator/widget/TouchpadView.java @@ -15,6 +15,7 @@ import com.winlator.core.AppUtils; import com.winlator.inputcontrols.Binding; +import com.winlator.inputcontrols.ControlsProfile; import com.winlator.math.Mathf; import com.winlator.math.XForm; import com.winlator.renderer.ViewTransformation; @@ -243,7 +244,7 @@ public TouchpadView(Context context, XServer xServer, boolean capturePointerOnEx this.capturePointerOnExternalMouse = capturePointerOnExternalMouse; this.fingers = new Finger[4]; this.numFingers = (byte) 0; - this.sensitivity = 1.0f; + this.sensitivity = ControlsProfile.DEFAULT_CURSOR_SPEED; this.pointerButtonLeftEnabled = true; this.pointerButtonRightEnabled = true; this.moveCursorToTouchpoint = false; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17e1be9026..e07a471720 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -269,6 +269,10 @@ D-Pad Right On-screen Controller Edit On-screen Controller + On-screen Controller Settings + Mouse Speed + Adjust pointer speed for touch movement and controls assigned to move the mouse. Shooter Mode uses its own look sensitivity. + Reset mouse speed Edit Physical Controller Disconnected Reset On-Screen Controls diff --git a/app/src/test/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialogTest.kt b/app/src/test/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialogTest.kt new file mode 100644 index 0000000000..1e5eb9f339 --- /dev/null +++ b/app/src/test/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialogTest.kt @@ -0,0 +1,24 @@ +package app.gamenative.ui.component.dialog + +import org.junit.Assert.assertEquals +import org.junit.Test + +class OnScreenControllerSettingsDialogTest { + @Test + fun `invalid stored speeds fall back to default`() { + assertEquals(DEFAULT_MOUSE_SPEED, mouseSpeedOrDefault(Float.NaN), 0f) + assertEquals(DEFAULT_MOUSE_SPEED, mouseSpeedOrDefault(-1f), 0f) + assertEquals(DEFAULT_MOUSE_SPEED, mouseSpeedOrDefault(0f), 0f) + } + + @Test + fun `valid stored speeds remain unchanged before slider clamping`() { + assertEquals(4f, mouseSpeedOrDefault(4f), 0f) + } + + @Test + fun `slider speed is constrained to the supported range`() { + assertEquals(MIN_MOUSE_SPEED, mouseSpeedForSlider(0.01f), 0f) + assertEquals(MAX_MOUSE_SPEED, mouseSpeedForSlider(4f), 0f) + } +} diff --git a/app/src/test/java/com/winlator/inputcontrols/ControlsProfileCursorSpeedTest.kt b/app/src/test/java/com/winlator/inputcontrols/ControlsProfileCursorSpeedTest.kt new file mode 100644 index 0000000000..62908e3859 --- /dev/null +++ b/app/src/test/java/com/winlator/inputcontrols/ControlsProfileCursorSpeedTest.kt @@ -0,0 +1,45 @@ +package com.winlator.inputcontrols + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets + +@RunWith(RobolectricTestRunner::class) +class ControlsProfileCursorSpeedTest { + private val context: Context = ApplicationProvider.getApplicationContext() + private val profile = ControlsProfile(context, 1) + + @Test + fun `cursor speed accepts positive finite values`() { + profile.cursorSpeed = 2.5f + + assertEquals(2.5f, profile.cursorSpeed, 0f) + } + + @Test + fun `cursor speed rejects values that would stop or corrupt movement`() { + listOf(Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, 0f, -1f).forEach { value -> + profile.cursorSpeed = value + + assertEquals(ControlsProfile.DEFAULT_CURSOR_SPEED, profile.cursorSpeed, 0f) + } + } + + @Test + fun `legacy profile without cursor speed loads the default`() { + val json = """{"id":7,"name":"Legacy"}""" + val loadedProfile = InputControlsManager.loadProfile( + context, + ByteArrayInputStream(json.toByteArray(StandardCharsets.UTF_8)), + ) + + assertNotNull(loadedProfile) + assertEquals(ControlsProfile.DEFAULT_CURSOR_SPEED, loadedProfile!!.cursorSpeed, 0f) + } +} From a8ce54354a007e8d5736e6299cfa429593ea90a5 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Mon, 7 Sep 2026 00:43:11 -0500 Subject: [PATCH 2/5] Add mouse speed translations --- app/src/main/res/values-da/strings.xml | 4 ++++ app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values-es/strings.xml | 4 ++++ app/src/main/res/values-fr/strings.xml | 4 ++++ app/src/main/res/values-it/strings.xml | 4 ++++ app/src/main/res/values-ja/strings.xml | 4 ++++ app/src/main/res/values-ko/strings.xml | 4 ++++ app/src/main/res/values-pl/strings.xml | 4 ++++ app/src/main/res/values-pt-rBR/strings.xml | 4 ++++ app/src/main/res/values-ro/strings.xml | 4 ++++ app/src/main/res/values-ru/strings.xml | 4 ++++ app/src/main/res/values-uk/strings.xml | 4 ++++ app/src/main/res/values-zh-rCN/strings.xml | 4 ++++ app/src/main/res/values-zh-rTW/strings.xml | 4 ++++ 14 files changed, 56 insertions(+) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 754ef5f1c6..dba077e021 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -210,6 +210,10 @@ D-Pad højre On-screen-controller Redigér on-screen-controller + Indstillinger for on-screen-controller + Musehastighed + Juster markørhastigheden for berøringsbevægelser og betjeningselementer, der er tildelt til at flytte musen. Shooter-tilstand bruger sin egen kigfølsomhed. + Nulstil musehastighed Redigér fysisk controller Afbrudt Nulstil on-screen-kontroller diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 95591d2399..124edadcd2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -274,6 +274,10 @@ D-Pad rechts On-Screen-Controller On-Screen-Controller bearbeiten + Einstellungen für den On-Screen-Controller + Mausgeschwindigkeit + Passe die Zeigergeschwindigkeit für Touch-Bewegungen und Steuerelemente an, die zum Bewegen der Maus zugewiesen sind. Der Shooter-Modus verwendet eine eigene Blickempfindlichkeit. + Mausgeschwindigkeit zurücksetzen Physische Controller bearbeiten Verbindung getrennt On-Screen-Steuerung zurücksetzen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 353ee4cb0f..70f731d842 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -292,6 +292,10 @@ Cruceta derecha Controles en pantalla Editar controles en pantalla + Ajustes de controles en pantalla + Velocidad del ratón + Ajusta la velocidad del puntero para el movimiento táctil y los controles asignados a mover el ratón. El modo shooter usa su propia sensibilidad de vista. + Restablecer velocidad del ratón Editar mando físico Desconectado Restablecer controles en pantalla diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bd0e92dc42..8d34033beb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -282,6 +282,10 @@ D-Pad Droite Contrôleur à l\'écran Modifier le contrôleur à l\'écran + Paramètres du contrôleur à l\'écran + Vitesse de la souris + Règle la vitesse du pointeur pour les mouvements tactiles et les commandes assignées au déplacement de la souris. Le mode tireur utilise sa propre sensibilité du regard. + Réinitialiser la vitesse de la souris Modifier le contrôleur physique Déconnecté Réinitialiser les contrôles à l\'écran diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 96931f1a23..a0c8d0cd14 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -281,6 +281,10 @@ D-Pad Destra Controller a schermo Modifica Controller a schermo + Impostazioni controller a schermo + Velocità del mouse + Regola la velocità del puntatore per i movimenti touch e i controlli assegnati al movimento del mouse. La modalità sparatutto usa una sensibilità di visuale separata. + Reimposta velocità del mouse Modifica Controller Fisico Disconnesso Reimposta Controlli a Schermo diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 566d5c8a2f..f3a17e991f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -235,6 +235,10 @@ 十字キー右 オンスクリーンコントローラー オンスクリーンコントローラーの編集 + オンスクリーンコントローラー設定 + マウス速度 + タッチ操作およびマウス移動に割り当てられたコントロールのポインター速度を調整します。シューターモードでは独自の視点感度が使用されます。 + マウス速度をリセット 物理コントローラーの編集 切断されました オンスクリーンコントロールをリセットする diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c1b5e0a369..e2ca36a888 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -289,6 +289,10 @@ 십자 버튼 오른쪽 화면 컨트롤러 화면 컨트롤러 편집 + 화면 컨트롤러 설정 + 마우스 속도 + 터치 이동 및 마우스 이동에 할당된 컨트롤의 포인터 속도를 조절합니다. 슈터 모드는 별도의 시점 감도를 사용합니다. + 마우스 속도 재설정 물리 컨트롤러 편집 연결 끊김 화면 컨트롤 초기화 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 193cd015c9..11f7df1d3c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -289,6 +289,10 @@ D-Pad Prawo Kontroler ekranowy Edytuj kontroler ekranowy + Ustawienia kontrolera ekranowego + Prędkość myszy + Dostosuj prędkość wskaźnika dla ruchu dotykowego i elementów sterujących przypisanych do poruszania myszą. Tryb strzelanki używa własnej czułości rozglądania. + Resetuj prędkość myszy Edytuj kontroler fizyczny Rozłączono Zresetuj kontroler ekranowy diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 65dacd8720..aee46492d2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -210,6 +210,10 @@ D-Pad Direita Controle On-screen Editar Controle On-screen + Configurações do controle na tela + Velocidade do mouse + Ajuste a velocidade do ponteiro para movimentos por toque e controles atribuídos ao movimento do mouse. O modo tiro usa sua própria sensibilidade de visão. + Redefinir velocidade do mouse Editar Controle Físico Desconectado Redefinir Controles On-screen diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index e8c648651a..c38a3e7b20 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -279,6 +279,10 @@ D‑Pad Dreapta Controller pe ecran Editează controllerul pe ecran + Setări controller pe ecran + Viteza mouse-ului + Reglează viteza indicatorului pentru mișcarea tactilă și comenzile atribuite deplasării mouse-ului. Modul shooter folosește propria sensibilitate a privirii. + Resetează viteza mouse-ului Editează controllerul fizic Deconectat Resetează controalele pe ecran diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8e2dc37dae..fdcbea8a58 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -623,6 +623,10 @@ В процессе Частично отмечено Экранный контроллер + Настройки экранного контроллера + Скорость мыши + Настройте скорость указателя для сенсорного перемещения и элементов управления, назначенных на движение мыши. Режим шутера использует отдельную чувствительность обзора. + Сбросить скорость мыши Установить Установить в любом случае Установить diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 46c7c50a60..f13fc1ba32 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -276,6 +276,10 @@ D-Pad вправо Елементи керування введенням Редагувати екранний контролер + Налаштування екранного контролера + Швидкість миші + Налаштуйте швидкість вказівника для сенсорного переміщення й елементів керування, призначених для руху миші. Режим шутера використовує окрему чутливість огляду. + Скинути швидкість миші Редагувати фізичний контролер Від\'єднано Скинути екранні елементи керування diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 81566c4666..af9a51ad2f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -274,6 +274,10 @@ 方向键 右 屏幕控制器 编辑屏幕控制器 + 屏幕控制器设置 + 鼠标速度 + 调整触摸移动和分配为移动鼠标的控件的指针速度。射击模式使用独立的视角灵敏度。 + 重置鼠标速度 编辑实体控制器 已断开 重置屏幕控制器 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 42fcea9f01..53325fd648 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -274,6 +274,10 @@ 方向鍵 右 螢幕控制器 編輯螢幕控制器 + 螢幕控制器設定 + 滑鼠速度 + 調整觸控移動及指派為移動滑鼠之控制項的指標速度。射擊模式使用獨立的視角靈敏度。 + 重設滑鼠速度 編輯實體控制器 已斷線 重置螢幕控制器 From 4a76bbb307eedcf5ba34f2b19a4300bfa984b2a8 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Mon, 7 Sep 2026 00:55:52 -0500 Subject: [PATCH 3/5] Address mouse speed review feedback --- .../OnScreenControllerSettingsDialog.kt | 26 ++++++++++-- .../component/dialog/SettingsDialogBlocks.kt | 5 +++ .../dialog/ShooterModeSettingsDialog.kt | 4 -- .../inputcontrols/ControlElement.java | 42 +++++++++++++++++-- .../winlator/widget/InputControlsView.java | 33 +++++++++------ app/src/main/res/values-uk/strings.xml | 2 +- .../OnScreenControllerSettingsDialogTest.kt | 10 +++++ .../ControlElementMouseSpeedTest.kt | 33 +++++++++++++++ 8 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 app/src/test/java/com/winlator/inputcontrols/ControlElementMouseSpeedTest.kt diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt index 3f7b0b7b9f..1cb65cab53 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -44,6 +45,10 @@ internal fun mouseSpeedForSlider(value: Float): Float { return mouseSpeedOrDefault(value).coerceIn(MIN_MOUSE_SPEED, MAX_MOUSE_SPEED) } +internal fun mouseSpeedForSave(initialValue: Float, editedValue: Float, wasEdited: Boolean): Float { + return if (wasEdited) mouseSpeedForSlider(editedValue) else mouseSpeedOrDefault(initialValue) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun OnScreenControllerSettingsDialog( @@ -54,6 +59,7 @@ fun OnScreenControllerSettingsDialog( var cursorSpeed by remember(initialCursorSpeed) { mutableFloatStateOf(mouseSpeedForSlider(initialCursorSpeed)) } + var cursorSpeedWasEdited by remember(initialCursorSpeed) { mutableStateOf(false) } val locale = Locale.getDefault() Dialog( @@ -82,13 +88,22 @@ fun OnScreenControllerSettingsDialog( } }, actions = { - IconButton(onClick = { cursorSpeed = DEFAULT_MOUSE_SPEED }) { + IconButton( + onClick = { + cursorSpeed = DEFAULT_MOUSE_SPEED + cursorSpeedWasEdited = true + }, + ) { Icon( Icons.Default.Refresh, contentDescription = stringResource(R.string.reset_mouse_speed), ) } - IconButton(onClick = { onSave(cursorSpeed) }) { + IconButton( + onClick = { + onSave(mouseSpeedForSave(initialCursorSpeed, cursorSpeed, cursorSpeedWasEdited)) + }, + ) { Icon(Icons.Default.Check, contentDescription = stringResource(R.string.save)) } }, @@ -109,8 +124,11 @@ fun OnScreenControllerSettingsDialog( subtitle = stringResource(R.string.mouse_speed_subtitle), value = cursorSpeed, valueRange = MIN_MOUSE_SPEED..MAX_MOUSE_SPEED, - valueText = String.format(locale, "%.1fx", cursorSpeed), - onValueChange = { cursorSpeed = it }, + valueText = multiplierText(cursorSpeed, locale), + onValueChange = { + cursorSpeed = it + cursorSpeedWasEdited = true + }, ) } } diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/SettingsDialogBlocks.kt b/app/src/main/java/app/gamenative/ui/component/dialog/SettingsDialogBlocks.kt index 36223bef31..545052e6e2 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/SettingsDialogBlocks.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/SettingsDialogBlocks.kt @@ -57,6 +57,7 @@ import app.gamenative.ui.theme.settingsTileColors import app.gamenative.ui.theme.settingsTileColorsAlt import com.alorma.compose.settings.ui.SettingsSwitch import com.winlator.inputcontrols.Binding +import java.util.Locale import kotlin.math.ceil import kotlin.math.floor @@ -252,6 +253,10 @@ fun SettingsSliderBlock( if (compact) content() else GestureBlock { content() } } +internal fun multiplierText(value: Float, locale: Locale): String { + return String.format(locale, "%.1fx", value) +} + @Composable internal fun settingsSliderColors(showTickMarks: Boolean = true) = SliderDefaults.colors( thumbColor = MaterialTheme.colorScheme.primary, diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ShooterModeSettingsDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ShooterModeSettingsDialog.kt index a45d6c09e0..cff9452dba 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ShooterModeSettingsDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ShooterModeSettingsDialog.kt @@ -412,10 +412,6 @@ private fun joystickBehaviorLabels(): List = listOf( stringResource(R.string.joystick_behavior_floating), ) -private fun multiplierText(value: Float, locale: Locale): String { - return String.format(locale, "%.1fx", value) -} - private fun pixelText(value: Float, locale: Locale): String { return String.format(locale, "%.1f px", value) } diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java index 3c234c61e0..3c7fb933cb 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java @@ -97,6 +97,8 @@ public static String[] names() { private PointF currentPosition; private RangeScroller scroller; private CubicBezierInterpolator interpolator; + private final MouseDeltaAccumulator trackpadCursorX = new MouseDeltaAccumulator(); + private final MouseDeltaAccumulator trackpadCursorY = new MouseDeltaAccumulator(); private Object touchTime; private String shooterMovementType = "wasd"; private String shooterLookType = "mouse"; @@ -116,6 +118,32 @@ public ControlElement(InputControlsView inputControlsView) { this.inputControlsView = inputControlsView; } + static final class MouseDeltaAccumulator { + private float remainder; + + int scale(float value, float multiplier) { + if (value == 0.0f) return 0; + if (multiplier >= 1.0f) { + remainder = 0.0f; + return Mathf.roundPoint(value * multiplier); + } + + float scaledValue = Mathf.roundPoint(value) * multiplier + remainder; + int wholePixels = (int)scaledValue; + remainder = scaledValue - wholePixels; + return wholePixels; + } + + void reset() { + remainder = 0.0f; + } + } + + private void resetTrackpadCursorRemainders() { + trackpadCursorX.reset(); + trackpadCursorY.reset(); + } + private static Object[] createBindingSources(int count) { Object[] sources = new Object[count]; for (int i = 0; i < count; i++) sources[i] = new Object(); @@ -1288,6 +1316,7 @@ else if (type == Type.RANGE_BUTTON) { } else { if (type == Type.TRACKPAD) { + resetTrackpadCursorRemainders(); if (currentPosition == null) currentPosition = new PointF(); currentPosition.set(x, y); } @@ -1381,8 +1410,8 @@ else if (type == Type.TRACKPAD) { float cursorSpeed = activeProfile != null ? activeProfile.getCursorSpeed() : ControlsProfile.DEFAULT_CURSOR_SPEED; - int cursorDx = 0; - int cursorDy = 0; + float cursorDeltaX = 0; + float cursorDeltaY = 0; for (byte i = 0; i < 4; i++) { float value = (i == 1 || i == 3 ? deltaX : deltaY); @@ -1394,10 +1423,10 @@ else if (type == Type.TRACKPAD) { value *= TouchpadView.CURSOR_ACCELERATION; } if (mouseMoveBinding == Binding.MOUSE_MOVE_LEFT || mouseMoveBinding == Binding.MOUSE_MOVE_RIGHT) { - cursorDx = Mathf.roundPoint(value * cursorSpeed); + cursorDeltaX = value; } else { - cursorDy = Mathf.roundPoint(value * cursorSpeed); + cursorDeltaY = value; } boolean nextState = states[i]; if (!bindingCombo.isSingleBinding() && this.states[i] != nextState) { @@ -1436,6 +1465,8 @@ else if (this.states[i] != states[i]) { } } + int cursorDx = trackpadCursorX.scale(cursorDeltaX, cursorSpeed); + int cursorDy = trackpadCursorY.scale(cursorDeltaY, cursorSpeed); if (cursorDx != 0 || cursorDy != 0) inputControlsView.getXServer().injectPointerMoveDelta(cursorDx, cursorDy); } else { @@ -1515,6 +1546,8 @@ else if (!usesToggleSwitch() || selected) { else if (type == Type.RANGE_BUTTON || type == Type.D_PAD || type == Type.STICK || type == Type.TRACKPAD) { releaseActiveDirectionalStates(); + if (type == Type.TRACKPAD) resetTrackpadCursorRemainders(); + if (type == Type.RANGE_BUTTON) { scroller.handleTouchUp(); } @@ -1555,6 +1588,7 @@ else if (type == Type.BUTTON) { } else if (type == Type.RANGE_BUTTON || type == Type.D_PAD || type == Type.STICK || type == Type.TRACKPAD) { releaseActiveDirectionalStates(); + if (type == Type.TRACKPAD) resetTrackpadCursorRemainders(); if (type == Type.RANGE_BUTTON) scroller.cancelTouch(); currentPosition = null; } diff --git a/app/src/main/java/com/winlator/widget/InputControlsView.java b/app/src/main/java/com/winlator/widget/InputControlsView.java index 7a06a94ab4..db4c311f71 100644 --- a/app/src/main/java/com/winlator/widget/InputControlsView.java +++ b/app/src/main/java/com/winlator/widget/InputControlsView.java @@ -73,7 +73,7 @@ public class InputControlsView extends View { private float offsetX; private float offsetY; private ControlElement selectedElement; - private ControlsProfile profile; + private volatile ControlsProfile profile; // Retained while the overlay controls are hidden so gyro can keep targeting the active gamepad. private ControlsProfile gyroProfile; private float overlayOpacity = DEFAULT_OVERLAY_OPACITY; @@ -82,6 +82,7 @@ public class InputControlsView extends View { private final Bitmap[] icons = new Bitmap[40]; private Timer mouseMoveTimer; private final PointF mouseMoveOffset = new PointF(); + private final Object mouseMoveStateLock = new Object(); private boolean showTouchscreenControls = true; // Shooter mode state @@ -577,14 +578,16 @@ private synchronized void createMouseMoveTimer() { mouseMoveTimer.schedule(new TimerTask() { @Override public void run() { - ControlsProfile currentProfile = getProfile(); - if (currentProfile == null) return; - - float cursorSpeed = currentProfile.getCursorSpeed(); - int deltaX = (int)(mouseMoveOffset.x * 10 * cursorSpeed); - int deltaY = (int)(mouseMoveOffset.y * 10 * cursorSpeed); - if (deltaX != 0 || deltaY != 0) { - xServer.injectPointerMoveDelta(deltaX, deltaY); + synchronized (mouseMoveStateLock) { + ControlsProfile currentProfile = profile; + if (currentProfile == null) return; + + float cursorSpeed = currentProfile.getCursorSpeed(); + int deltaX = (int)(mouseMoveOffset.x * 10 * cursorSpeed); + int deltaY = (int)(mouseMoveOffset.y * 10 * cursorSpeed); + if (deltaX != 0 || deltaY != 0) { + xServer.injectPointerMoveDelta(deltaX, deltaY); + } } } }, 0, 1000 / 60); @@ -596,7 +599,9 @@ private synchronized void stopMouseMoveTimer() { mouseMoveTimer.cancel(); mouseMoveTimer = null; } - mouseMoveOffset.set(0, 0); + synchronized (mouseMoveStateLock) { + mouseMoveOffset.set(0, 0); + } } private void processJoystickInput(ExternalController controller) { @@ -1764,11 +1769,15 @@ else if (binding == Binding.ALT_ENTER) { return; } else if (binding == Binding.MOUSE_MOVE_LEFT || binding == Binding.MOUSE_MOVE_RIGHT) { - mouseMoveOffset.x = isActionDown ? (offset != 0 ? offset : (binding == Binding.MOUSE_MOVE_LEFT ? -1 : 1)) : 0; + synchronized (mouseMoveStateLock) { + mouseMoveOffset.x = isActionDown ? (offset != 0 ? offset : (binding == Binding.MOUSE_MOVE_LEFT ? -1 : 1)) : 0; + } if (isActionDown) createMouseMoveTimer(); } else if (binding == Binding.MOUSE_MOVE_DOWN || binding == Binding.MOUSE_MOVE_UP) { - mouseMoveOffset.y = isActionDown ? (offset != 0 ? offset : (binding == Binding.MOUSE_MOVE_UP ? -1 : 1)) : 0; + synchronized (mouseMoveStateLock) { + mouseMoveOffset.y = isActionDown ? (offset != 0 ? offset : (binding == Binding.MOUSE_MOVE_UP ? -1 : 1)) : 0; + } if (isActionDown) createMouseMoveTimer(); } else { diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f13fc1ba32..0bed85e0ed 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -278,7 +278,7 @@ Редагувати екранний контролер Налаштування екранного контролера Швидкість миші - Налаштуйте швидкість вказівника для сенсорного переміщення й елементів керування, призначених для руху миші. Режим шутера використовує окрему чутливість огляду. + Налаштуйте швидкість вказівника для руху дотиком і елементів керування, призначених для руху миші. Режим шутера використовує окрему чутливість огляду. Скинути швидкість миші Редагувати фізичний контролер Від\'єднано diff --git a/app/src/test/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialogTest.kt b/app/src/test/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialogTest.kt index 1e5eb9f339..6d9aafb102 100644 --- a/app/src/test/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialogTest.kt +++ b/app/src/test/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialogTest.kt @@ -21,4 +21,14 @@ class OnScreenControllerSettingsDialogTest { assertEquals(MIN_MOUSE_SPEED, mouseSpeedForSlider(0.01f), 0f) assertEquals(MAX_MOUSE_SPEED, mouseSpeedForSlider(4f), 0f) } + + @Test + fun `saving without editing preserves a valid value outside the slider range`() { + assertEquals(4f, mouseSpeedForSave(4f, MAX_MOUSE_SPEED, false), 0f) + } + + @Test + fun `saving an edited value uses the slider range`() { + assertEquals(MAX_MOUSE_SPEED, mouseSpeedForSave(4f, 4f, true), 0f) + } } diff --git a/app/src/test/java/com/winlator/inputcontrols/ControlElementMouseSpeedTest.kt b/app/src/test/java/com/winlator/inputcontrols/ControlElementMouseSpeedTest.kt new file mode 100644 index 0000000000..f4dc134a3b --- /dev/null +++ b/app/src/test/java/com/winlator/inputcontrols/ControlElementMouseSpeedTest.kt @@ -0,0 +1,33 @@ +package com.winlator.inputcontrols + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ControlElementMouseSpeedTest { + @Test + fun `default trackpad speed preserves existing rounding`() { + val accumulator = ControlElement.MouseDeltaAccumulator() + + assertEquals(1, accumulator.scale(0.2f, 1f)) + assertEquals(-1, accumulator.scale(-0.2f, 1f)) + } + + @Test + fun `sub one trackpad speed accumulates fractional pixels`() { + val accumulator = ControlElement.MouseDeltaAccumulator() + + assertEquals( + listOf(0, 1, 0, 1), + List(4) { accumulator.scale(0.2f, 0.5f) }, + ) + } + + @Test + fun `reset discards a partial trackpad pixel`() { + val accumulator = ControlElement.MouseDeltaAccumulator() + + assertEquals(0, accumulator.scale(0.2f, 0.5f)) + accumulator.reset() + assertEquals(0, accumulator.scale(0.2f, 0.5f)) + } +} From 933c5c82a58c95cfb086a0d022bafbaf694f95d3 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Mon, 7 Sep 2026 01:08:38 -0500 Subject: [PATCH 4/5] Keep displayed and saved mouse speeds consistent --- .../ui/component/dialog/OnScreenControllerSettingsDialog.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt index 1cb65cab53..024e9cd0ea 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/OnScreenControllerSettingsDialog.kt @@ -124,7 +124,10 @@ fun OnScreenControllerSettingsDialog( subtitle = stringResource(R.string.mouse_speed_subtitle), value = cursorSpeed, valueRange = MIN_MOUSE_SPEED..MAX_MOUSE_SPEED, - valueText = multiplierText(cursorSpeed, locale), + valueText = multiplierText( + mouseSpeedForSave(initialCursorSpeed, cursorSpeed, cursorSpeedWasEdited), + locale, + ), onValueChange = { cursorSpeed = it cursorSpeedWasEdited = true From f17a77507fd10eaeeaaf836950a14de8289b5848 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Mon, 7 Sep 2026 13:31:02 -0500 Subject: [PATCH 5/5] Accumulate on-screen trackpad subpixel movement --- .../inputcontrols/ControlElement.java | 7 +--- .../ControlElementMouseSpeedTest.kt | 40 ++++++++++++++----- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java index 3c7fb933cb..b0e11c4782 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java @@ -123,12 +123,7 @@ static final class MouseDeltaAccumulator { int scale(float value, float multiplier) { if (value == 0.0f) return 0; - if (multiplier >= 1.0f) { - remainder = 0.0f; - return Mathf.roundPoint(value * multiplier); - } - - float scaledValue = Mathf.roundPoint(value) * multiplier + remainder; + float scaledValue = value * multiplier + remainder; int wholePixels = (int)scaledValue; remainder = scaledValue - wholePixels; return wholePixels; diff --git a/app/src/test/java/com/winlator/inputcontrols/ControlElementMouseSpeedTest.kt b/app/src/test/java/com/winlator/inputcontrols/ControlElementMouseSpeedTest.kt index f4dc134a3b..482c20d0ee 100644 --- a/app/src/test/java/com/winlator/inputcontrols/ControlElementMouseSpeedTest.kt +++ b/app/src/test/java/com/winlator/inputcontrols/ControlElementMouseSpeedTest.kt @@ -5,29 +5,51 @@ import org.junit.Test class ControlElementMouseSpeedTest { @Test - fun `default trackpad speed preserves existing rounding`() { + fun `default trackpad speed accumulates transformed subpixels`() { + val positive = ControlElement.MouseDeltaAccumulator() + val negative = ControlElement.MouseDeltaAccumulator() + + assertEquals(listOf(0, 0, 0, 0, 1), List(5) { positive.scale(0.2f, 1f) }) + assertEquals(listOf(0, 0, 0, 0, -1), List(5) { negative.scale(-0.2f, 1f) }) + } + + @Test + fun `sub one trackpad speed remains proportional`() { val accumulator = ControlElement.MouseDeltaAccumulator() - assertEquals(1, accumulator.scale(0.2f, 1f)) - assertEquals(-1, accumulator.scale(-0.2f, 1f)) + assertEquals( + listOf(0, 0, 0, 0, 0, 0, 0, 1), + List(8) { accumulator.scale(0.25f, 0.5f) }, + ) } @Test - fun `sub one trackpad speed accumulates fractional pixels`() { + fun `above one trackpad speed remains proportional`() { val accumulator = ControlElement.MouseDeltaAccumulator() assertEquals( - listOf(0, 1, 0, 1), - List(4) { accumulator.scale(0.2f, 0.5f) }, + listOf(0, 1, 1, 1), + List(4) { accumulator.scale(0.25f, 3f) }, ) } @Test - fun `reset discards a partial trackpad pixel`() { + fun `direction reversal cancels the pending subpixel`() { + val accumulator = ControlElement.MouseDeltaAccumulator() + + assertEquals(0, accumulator.scale(0.75f, 1f)) + assertEquals(0, accumulator.scale(-0.75f, 1f)) + assertEquals(0, accumulator.scale(-0.5f, 1f)) + assertEquals(-1, accumulator.scale(-0.5f, 1f)) + } + + @Test + fun `reset discards a pending subpixel`() { val accumulator = ControlElement.MouseDeltaAccumulator() - assertEquals(0, accumulator.scale(0.2f, 0.5f)) + assertEquals(0, accumulator.scale(0.75f, 1f)) accumulator.reset() - assertEquals(0, accumulator.scale(0.2f, 0.5f)) + assertEquals(0, accumulator.scale(0.5f, 1f)) + assertEquals(1, accumulator.scale(0.5f, 1f)) } }