[android] input over(lay)haul 2: Individual scaling of buttons (#2562)

### (Needs testing)

This PR makes it possible to adjust the scale of each touch input overlay button independently from the global scale
This individual value always goes on top of the global scale.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2562
Co-authored-by: nyx <contact@innix.space>
Co-committed-by: nyx <contact@innix.space>
This commit is contained in:
nyx
2025-09-29 22:38:26 +02:00
committed by crueter
parent 85b5e650cc
commit 824dc6948e
15 changed files with 506 additions and 77 deletions

View File

@@ -924,6 +924,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
IntSetting.OVERLAY_OPACITY.reset()
binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.resetLayoutVisibilityAndPlacement()
binding.surfaceInputOverlay.resetIndividualControlScale()
}
}
@@ -1546,6 +1547,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
.setNeutralButton(R.string.slider_default) { _: DialogInterface?, _: Int ->
setControlScale(50)
setControlOpacity(100)
binding.surfaceInputOverlay.resetIndividualControlScale()
}
.show()
}

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay
@@ -13,6 +13,8 @@ import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
@@ -52,6 +54,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
private var scaleDialog: OverlayScaleDialog? = null
private var touchStartX = 0f
private var touchStartY = 0f
private var hasMoved = false
private val moveThreshold = 20f
private lateinit var windowInsets: WindowInsets
var layout = OverlayLayout.Landscape
@@ -254,23 +262,44 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
) {
buttonBeingConfigured = button
buttonBeingConfigured!!.onConfigureTouch(event)
touchStartX = event.getX(pointerIndex)
touchStartY = event.getY(pointerIndex)
hasMoved = false
}
MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) {
buttonBeingConfigured!!.onConfigureTouch(event)
invalidate()
return true
val moveDistance = kotlin.math.sqrt(
(event.getX(pointerIndex) - touchStartX).let { it * it } +
(event.getY(pointerIndex) - touchStartY).let { it * it }
)
if (moveDistance > moveThreshold) {
hasMoved = true
buttonBeingConfigured!!.onConfigureTouch(event)
invalidate()
return true
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) {
// Persist button position by saving new place.
saveControlPosition(
buttonBeingConfigured!!.overlayControlData.id,
buttonBeingConfigured!!.bounds.centerX(),
buttonBeingConfigured!!.bounds.centerY(),
layout
)
if (!hasMoved) {
showScaleDialog(
buttonBeingConfigured,
null,
null,
fingerPositionX,
fingerPositionY
)
} else {
saveControlPosition(
buttonBeingConfigured!!.overlayControlData.id,
buttonBeingConfigured!!.bounds.centerX(),
buttonBeingConfigured!!.bounds.centerY(),
individuaScale = buttonBeingConfigured!!.overlayControlData.individualScale,
layout
)
}
buttonBeingConfigured = null
}
}
@@ -287,23 +316,46 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
) {
dpadBeingConfigured = dpad
dpadBeingConfigured!!.onConfigureTouch(event)
touchStartX = event.getX(pointerIndex)
touchStartY = event.getY(pointerIndex)
hasMoved = false
}
MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) {
dpadBeingConfigured!!.onConfigureTouch(event)
invalidate()
return true
val moveDistance = kotlin.math.sqrt(
(event.getX(pointerIndex) - touchStartX).let { it * it } +
(event.getY(pointerIndex) - touchStartY).let { it * it }
)
if (moveDistance > moveThreshold) {
hasMoved = true
dpadBeingConfigured!!.onConfigureTouch(event)
invalidate()
return true
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) {
// Persist button position by saving new place.
saveControlPosition(
OverlayControl.COMBINED_DPAD.id,
dpadBeingConfigured!!.bounds.centerX(),
dpadBeingConfigured!!.bounds.centerY(),
layout
)
if (!hasMoved) {
// This was a click, show scale dialog for dpad
showScaleDialog(
null,
dpadBeingConfigured,
null,
fingerPositionX,
fingerPositionY
)
} else {
// This was a move, save position
saveControlPosition(
OverlayControl.COMBINED_DPAD.id,
dpadBeingConfigured!!.bounds.centerX(),
dpadBeingConfigured!!.bounds.centerY(),
individuaScale = dpadBeingConfigured!!.individualScale,
layout
)
}
dpadBeingConfigured = null
}
}
@@ -317,21 +369,43 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
) {
joystickBeingConfigured = joystick
joystickBeingConfigured!!.onConfigureTouch(event)
touchStartX = event.getX(pointerIndex)
touchStartY = event.getY(pointerIndex)
hasMoved = false
}
MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) {
joystickBeingConfigured!!.onConfigureTouch(event)
invalidate()
val moveDistance = kotlin.math.sqrt(
(event.getX(pointerIndex) - touchStartX).let { it * it } +
(event.getY(pointerIndex) - touchStartY).let { it * it }
)
if (moveDistance > moveThreshold) {
hasMoved = true
joystickBeingConfigured!!.onConfigureTouch(event)
invalidate()
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
saveControlPosition(
joystickBeingConfigured!!.prefId,
joystickBeingConfigured!!.bounds.centerX(),
joystickBeingConfigured!!.bounds.centerY(),
layout
)
if (!hasMoved) {
showScaleDialog(
null,
null,
joystickBeingConfigured,
fingerPositionX,
fingerPositionY
)
} else {
saveControlPosition(
joystickBeingConfigured!!.prefId,
joystickBeingConfigured!!.bounds.centerX(),
joystickBeingConfigured!!.bounds.centerY(),
individuaScale = joystickBeingConfigured!!.individualScale,
layout
)
}
joystickBeingConfigured = null
}
}
@@ -607,25 +681,117 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
invalidate()
}
private fun saveControlPosition(id: String, x: Int, y: Int, layout: OverlayLayout) {
private fun saveControlPosition(
id: String,
x: Int,
y: Int,
individuaScale: Float,
layout: OverlayLayout
) {
val windowSize = getSafeScreenSize(context, Pair(measuredWidth, measuredHeight))
val min = windowSize.first
val max = windowSize.second
val overlayControlData = NativeConfig.getOverlayControlData()
val data = overlayControlData.firstOrNull { it.id == id }
val newPosition = Pair((x - min.x).toDouble() / max.x, (y - min.y).toDouble() / max.y)
when (layout) {
OverlayLayout.Landscape -> data?.landscapePosition = newPosition
OverlayLayout.Portrait -> data?.portraitPosition = newPosition
OverlayLayout.Foldable -> data?.foldablePosition = newPosition
}
data?.individualScale = individuaScale
NativeConfig.setOverlayControlData(overlayControlData)
}
fun setIsInEditMode(editMode: Boolean) {
inEditMode = editMode
if (!editMode) {
scaleDialog?.dismiss()
scaleDialog = null
}
}
private fun showScaleDialog(
button: InputOverlayDrawableButton?,
dpad: InputOverlayDrawableDpad?,
joystick: InputOverlayDrawableJoystick?,
x: Int, y: Int
) {
val overlayControlData = NativeConfig.getOverlayControlData()
// prevent dialog from being spam opened
scaleDialog?.dismiss()
when {
button != null -> {
val buttonData =
overlayControlData.firstOrNull { it.id == button.overlayControlData.id }
if (buttonData != null) {
scaleDialog =
OverlayScaleDialog(context, button.overlayControlData) { newScale ->
saveControlPosition(
button.overlayControlData.id,
button.bounds.centerX(),
button.bounds.centerY(),
individuaScale = newScale,
layout
)
refreshControls()
}
scaleDialog?.showDialog(x,y, button.bounds.width(), button.bounds.height())
}
}
dpad != null -> {
val dpadData =
overlayControlData.firstOrNull { it.id == OverlayControl.COMBINED_DPAD.id }
if (dpadData != null) {
scaleDialog = OverlayScaleDialog(context, dpadData) { newScale ->
saveControlPosition(
OverlayControl.COMBINED_DPAD.id,
dpad.bounds.centerX(),
dpad.bounds.centerY(),
newScale,
layout
)
refreshControls()
}
scaleDialog?.showDialog(x,y, dpad.bounds.width(), dpad.bounds.height())
}
}
joystick != null -> {
val joystickData = overlayControlData.firstOrNull { it.id == joystick.prefId }
if (joystickData != null) {
scaleDialog = OverlayScaleDialog(context, joystickData) { newScale ->
saveControlPosition(
joystick.prefId,
joystick.bounds.centerX(),
joystick.bounds.centerY(),
individuaScale = newScale,
layout
)
refreshControls()
}
scaleDialog?.showDialog(x,y, joystick.bounds.width(), joystick.bounds.height())
}
}
}
}
/**
* Applies and saves all default values for the overlay
*/
@@ -664,12 +830,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
val overlayControlData = NativeConfig.getOverlayControlData()
overlayControlData.forEach {
it.enabled = OverlayControl.from(it.id)?.defaultVisibility == true
it.individualScale = OverlayControl.from(it.id)?.defaultIndividualScaleResource!!
}
NativeConfig.setOverlayControlData(overlayControlData)
refreshControls()
}
fun resetIndividualControlScale() {
val overlayControlData = NativeConfig.getOverlayControlData()
overlayControlData.forEach { data ->
val defaultControlData = OverlayControl.from(data.id) ?: return@forEach
data.individualScale = defaultControlData.defaultIndividualScaleResource
}
NativeConfig.setOverlayControlData(overlayControlData)
NativeConfig.saveGlobalConfig()
refreshControls()
}
private fun defaultOverlayPositionByLayout(layout: OverlayLayout) {
val overlayControlData = NativeConfig.getOverlayControlData()
for (data in overlayControlData) {
@@ -860,6 +1038,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
// Apply individual scale
scale *= overlayControlData.individualScale
// Initialize the InputOverlayDrawableButton.
val defaultStateBitmap = getBitmap(context, defaultResId, scale)
val pressedStateBitmap = getBitmap(context, pressedResId, scale)
@@ -922,11 +1103,20 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// Resources handle for fetching the initial Drawable resource.
val res = context.resources
// Get the dpad control data for individual scale
val overlayControlData = NativeConfig.getOverlayControlData()
val dpadData = overlayControlData.firstOrNull { it.id == OverlayControl.COMBINED_DPAD.id }
// Decide scale based on button ID and user preference
var scale = 0.25f
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
// Apply individual scale
if (dpadData != null) {
scale *= dpadData.individualScale
}
// Initialize the InputOverlayDrawableDpad.
val defaultStateBitmap =
getBitmap(context, defaultResId, scale)
@@ -1000,6 +1190,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale /= 100f
// Apply individual scale
scale *= overlayControlData.individualScale
// Initialize the InputOverlayDrawableJoystick.
val bitmapOuter = getBitmap(context, resOuter, scale)
val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f)

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay
@@ -42,6 +42,8 @@ class InputOverlayDrawableDpad(
val width: Int
val height: Int
var individualScale: Float = 1.0f
private val defaultStateBitmap: BitmapDrawable
private val pressedOneDirectionStateBitmap: BitmapDrawable
private val pressedTwoDirectionsStateBitmap: BitmapDrawable

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay
@@ -51,6 +51,8 @@ class InputOverlayDrawableJoystick(
val width: Int
val height: Int
var individualScale: Float = 1.0f
private var opacity: Int = 0
private var virtBounds: Rect

View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay
import android.app.Dialog
import android.content.Context
import android.view.Gravity
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.TextView
import com.google.android.material.button.MaterialButton
import com.google.android.material.slider.Slider
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
class OverlayScaleDialog(
context: Context,
private val overlayControlData: OverlayControlData,
private val onScaleChanged: (Float) -> Unit
) : Dialog(context) {
private var currentScale = overlayControlData.individualScale
private val originalScale = overlayControlData.individualScale
private lateinit var scaleValueText: TextView
private lateinit var scaleSlider: Slider
init {
setupDialog()
}
private fun setupDialog() {
val view = LayoutInflater.from(context).inflate(R.layout.dialog_overlay_scale, null)
setContentView(view)
window?.setBackgroundDrawable(null)
window?.apply {
attributes = attributes.apply {
flags = flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND.inv()
flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
}
}
scaleValueText = view.findViewById(R.id.scaleValueText)
scaleSlider = view.findViewById(R.id.scaleSlider)
val resetButton = view.findViewById<MaterialButton>(R.id.resetButton)
val confirmButton = view.findViewById<MaterialButton>(R.id.confirmButton)
val cancelButton = view.findViewById<MaterialButton>(R.id.cancelButton)
scaleValueText.text = String.format("%.1fx", currentScale)
scaleSlider.value = currentScale
scaleSlider.addOnChangeListener { _, value, input ->
if (input) {
currentScale = value
scaleValueText.text = String.format("%.1fx", currentScale)
}
}
scaleSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) {
// pass
}
override fun onStopTrackingTouch(slider: Slider) {
onScaleChanged(currentScale)
}
})
resetButton.setOnClickListener {
currentScale = 1.0f
scaleSlider.value = 1.0f
scaleValueText.text = String.format("%.1fx", currentScale)
onScaleChanged(currentScale)
}
confirmButton.setOnClickListener {
overlayControlData.individualScale = currentScale
//slider value is already saved on touch dispatch but just to be sure
onScaleChanged(currentScale)
dismiss()
}
// both cancel button and back gesture should revert the scale change
cancelButton.setOnClickListener {
onScaleChanged(originalScale)
dismiss()
}
setOnCancelListener {
onScaleChanged(originalScale)
dismiss()
}
}
fun showDialog(anchorX: Int, anchorY: Int, anchorHeight: Int, anchorWidth: Int) {
show()
show()
// TODO: this calculation is a bit rough, improve it later on
window?.let { window ->
val layoutParams = window.attributes
layoutParams.gravity = Gravity.TOP or Gravity.START
val density = context.resources.displayMetrics.density
val dialogWidthPx = (320 * density).toInt()
val dialogHeightPx = (400 * density).toInt() // set your estimated dialog height
val screenHeight = context.resources.displayMetrics.heightPixels
layoutParams.x = anchorX + anchorWidth / 2 - dialogWidthPx / 2
layoutParams.y = anchorY + anchorHeight / 2 - dialogHeightPx / 2
layoutParams.width = dialogWidthPx
window.attributes = layoutParams
}
}
}

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay.model
@@ -12,126 +12,144 @@ enum class OverlayControl(
val defaultVisibility: Boolean,
@IntegerRes val defaultLandscapePositionResources: Pair<Int, Int>,
@IntegerRes val defaultPortraitPositionResources: Pair<Int, Int>,
@IntegerRes val defaultFoldablePositionResources: Pair<Int, Int>
@IntegerRes val defaultFoldablePositionResources: Pair<Int, Int>,
val defaultIndividualScaleResource: Float,
) {
BUTTON_A(
"button_a",
true,
Pair(R.integer.BUTTON_A_X, R.integer.BUTTON_A_Y),
Pair(R.integer.BUTTON_A_X_PORTRAIT, R.integer.BUTTON_A_Y_PORTRAIT),
Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE)
Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE),
1.0f
),
BUTTON_B(
"button_b",
true,
Pair(R.integer.BUTTON_B_X, R.integer.BUTTON_B_Y),
Pair(R.integer.BUTTON_B_X_PORTRAIT, R.integer.BUTTON_B_Y_PORTRAIT),
Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE)
Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE),
1.0f
),
BUTTON_X(
"button_x",
true,
Pair(R.integer.BUTTON_X_X, R.integer.BUTTON_X_Y),
Pair(R.integer.BUTTON_X_X_PORTRAIT, R.integer.BUTTON_X_Y_PORTRAIT),
Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE)
Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE),
1.0f
),
BUTTON_Y(
"button_y",
true,
Pair(R.integer.BUTTON_Y_X, R.integer.BUTTON_Y_Y),
Pair(R.integer.BUTTON_Y_X_PORTRAIT, R.integer.BUTTON_Y_Y_PORTRAIT),
Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE)
Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE),
1.0f
),
BUTTON_PLUS(
"button_plus",
true,
Pair(R.integer.BUTTON_PLUS_X, R.integer.BUTTON_PLUS_Y),
Pair(R.integer.BUTTON_PLUS_X_PORTRAIT, R.integer.BUTTON_PLUS_Y_PORTRAIT),
Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE)
Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE),
1.0f
),
BUTTON_MINUS(
"button_minus",
true,
Pair(R.integer.BUTTON_MINUS_X, R.integer.BUTTON_MINUS_Y),
Pair(R.integer.BUTTON_MINUS_X_PORTRAIT, R.integer.BUTTON_MINUS_Y_PORTRAIT),
Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE)
Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE),
1.0f
),
BUTTON_HOME(
"button_home",
false,
Pair(R.integer.BUTTON_HOME_X, R.integer.BUTTON_HOME_Y),
Pair(R.integer.BUTTON_HOME_X_PORTRAIT, R.integer.BUTTON_HOME_Y_PORTRAIT),
Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE)
Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE),
1.0f
),
BUTTON_CAPTURE(
"button_capture",
false,
Pair(R.integer.BUTTON_CAPTURE_X, R.integer.BUTTON_CAPTURE_Y),
Pair(R.integer.BUTTON_CAPTURE_X_PORTRAIT, R.integer.BUTTON_CAPTURE_Y_PORTRAIT),
Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE)
Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE),
1.0f
),
BUTTON_L(
"button_l",
true,
Pair(R.integer.BUTTON_L_X, R.integer.BUTTON_L_Y),
Pair(R.integer.BUTTON_L_X_PORTRAIT, R.integer.BUTTON_L_Y_PORTRAIT),
Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE)
Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE),
1.0f
),
BUTTON_R(
"button_r",
true,
Pair(R.integer.BUTTON_R_X, R.integer.BUTTON_R_Y),
Pair(R.integer.BUTTON_R_X_PORTRAIT, R.integer.BUTTON_R_Y_PORTRAIT),
Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE)
Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE),
1.0f
),
BUTTON_ZL(
"button_zl",
true,
Pair(R.integer.BUTTON_ZL_X, R.integer.BUTTON_ZL_Y),
Pair(R.integer.BUTTON_ZL_X_PORTRAIT, R.integer.BUTTON_ZL_Y_PORTRAIT),
Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE)
Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE),
1.0f
),
BUTTON_ZR(
"button_zr",
true,
Pair(R.integer.BUTTON_ZR_X, R.integer.BUTTON_ZR_Y),
Pair(R.integer.BUTTON_ZR_X_PORTRAIT, R.integer.BUTTON_ZR_Y_PORTRAIT),
Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE)
Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE),
1.0f
),
BUTTON_STICK_L(
"button_stick_l",
true,
Pair(R.integer.BUTTON_STICK_L_X, R.integer.BUTTON_STICK_L_Y),
Pair(R.integer.BUTTON_STICK_L_X_PORTRAIT, R.integer.BUTTON_STICK_L_Y_PORTRAIT),
Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE)
Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE),
1.0f
),
BUTTON_STICK_R(
"button_stick_r",
true,
Pair(R.integer.BUTTON_STICK_R_X, R.integer.BUTTON_STICK_R_Y),
Pair(R.integer.BUTTON_STICK_R_X_PORTRAIT, R.integer.BUTTON_STICK_R_Y_PORTRAIT),
Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE)
Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE),
1.0f
),
STICK_L(
"stick_l",
true,
Pair(R.integer.STICK_L_X, R.integer.STICK_L_Y),
Pair(R.integer.STICK_L_X_PORTRAIT, R.integer.STICK_L_Y_PORTRAIT),
Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE)
Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE),
1.0f
),
STICK_R(
"stick_r",
true,
Pair(R.integer.STICK_R_X, R.integer.STICK_R_Y),
Pair(R.integer.STICK_R_X_PORTRAIT, R.integer.STICK_R_Y_PORTRAIT),
Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE)
Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE),
1.0f
),
COMBINED_DPAD(
"combined_dpad",
true,
Pair(R.integer.COMBINED_DPAD_X, R.integer.COMBINED_DPAD_Y),
Pair(R.integer.COMBINED_DPAD_X_PORTRAIT, R.integer.COMBINED_DPAD_Y_PORTRAIT),
Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE)
Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE),
1.0f
);
fun getDefaultPositionForLayout(layout: OverlayLayout): Pair<Double, Double> {
@@ -173,7 +191,8 @@ enum class OverlayControl(
defaultVisibility,
getDefaultPositionForLayout(OverlayLayout.Landscape),
getDefaultPositionForLayout(OverlayLayout.Portrait),
getDefaultPositionForLayout(OverlayLayout.Foldable)
getDefaultPositionForLayout(OverlayLayout.Foldable),
defaultIndividualScaleResource
)
companion object {

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay.model
@@ -8,7 +8,8 @@ data class OverlayControlData(
var enabled: Boolean,
var landscapePosition: Pair<Double, Double>,
var portraitPosition: Pair<Double, Double>,
var foldablePosition: Pair<Double, Double>
var foldablePosition: Pair<Double, Double>,
var individualScale: Float
) {
fun positionFromLayout(layout: OverlayLayout): Pair<Double, Double> =
when (layout) {

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
@@ -170,7 +170,8 @@ object DirectoryInitialization {
buttonEnabled,
Pair(landscapeXPosition, landscapeYPosition),
Pair(portraitXPosition, portraitYPosition),
Pair(foldableXPosition, foldableYPosition)
Pair(foldableXPosition, foldableYPosition),
OverlayControl.map[buttonId]?.defaultIndividualScaleResource ?: 1.0f
)
overlayControlDataMap[buttonId] = controlData
setOverlayData = true

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <common/logging/log.h>
#include <input_common/main.h>
@@ -103,6 +103,7 @@ void AndroidConfig::ReadOverlayValues() {
ReadDoubleSetting(std::string("foldable\\x_position"));
control_data.foldable_position.second =
ReadDoubleSetting(std::string("foldable\\y_position"));
control_data.individual_scale = static_cast<float>(ReadDoubleSetting(std::string("individual_scale")));
AndroidSettings::values.overlay_control_data.push_back(control_data);
}
EndArray();
@@ -255,6 +256,7 @@ void AndroidConfig::SaveOverlayValues() {
control_data.foldable_position.first);
WriteDoubleSetting(std::string("foldable\\y_position"),
control_data.foldable_position.second);
WriteDoubleSetting(std::string("individual_scale"), static_cast<double>(control_data.individual_scale));
}
EndArray();

View File

@@ -24,6 +24,7 @@ namespace AndroidSettings {
std::pair<double, double> landscape_position;
std::pair<double, double> portrait_position;
std::pair<double, double> foldable_position;
float individual_scale;
};
struct Values {

View File

@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <string>
@@ -369,7 +369,9 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getOverlayControlData(JN
env->NewObject(Common::Android::GetOverlayControlDataClass(),
Common::Android::GetOverlayControlDataConstructor(),
Common::Android::ToJString(env, control_data.id), control_data.enabled,
jlandscapePosition, jportraitPosition, jfoldablePosition);
jlandscapePosition, jportraitPosition, jfoldablePosition,
control_data.individual_scale);
env->SetObjectArrayElement(joverlayControlDataArray, i, jcontrolData);
}
return joverlayControlDataArray;
@@ -418,9 +420,12 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData(
env,
env->GetObjectField(jfoldablePosition, Common::Android::GetPairSecondField())));
float individual_scale = static_cast<float>(env->GetFloatField(
joverlayControlData, Common::Android::GetOverlayControlDataIndividualScaleField()));
AndroidSettings::values.overlay_control_data.push_back(AndroidSettings::OverlayControlData{
Common::Android::GetJString(env, jidString), enabled, landscape_position,
portrait_position, foldable_position});
portrait_position, foldable_position, individual_scale});
}
}

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="320dp"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp"
app:cardElevation="8dp"
app:cardBackgroundColor="#CC222222">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/scaleValueText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1.0x"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:layout_marginBottom="4dp"
android:textColor="#FFFFFF"
android:textStyle="bold" />
<com.google.android.material.slider.Slider
android:id="@+id/scaleSlider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="0.5"
android:valueTo="4.0"
android:stepSize="0.1"
android:value="1.0"
android:layout_marginBottom="8dp"
app:trackColorActive="@color/eden_border_gradient_start"
app:trackColorInactive="@color/eden_border_gradient_end"
app:tickColor="@color/eden_border_gradient_start"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<com.google.android.material.button.MaterialButton
android:id="@+id/resetButton"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:layout_marginEnd="4dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/cancelButton"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
android:layout_marginEnd="4dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/confirmButton"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/eden_button_secondary_bg"
android:textColor="@color/eden_border_gradient_end"
android:text="@string/confirm" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -855,6 +855,7 @@
<string name="touchscreen">Touchscreen</string>
<string name="lock_drawer">Lock drawer</string>
<string name="unlock_drawer">Unlock drawer</string>
<string name="reset">Reset</string>
<string name="load_settings">Loading settings…</string>

View File

@@ -1,7 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <jni.h>
@@ -49,6 +46,7 @@ static jclass s_overlay_control_data_class;
static jmethodID s_overlay_control_data_constructor;
static jfieldID s_overlay_control_data_id_field;
static jfieldID s_overlay_control_data_enabled_field;
static jfieldID s_overlay_control_data_individual_scale_field;
static jfieldID s_overlay_control_data_landscape_position_field;
static jfieldID s_overlay_control_data_portrait_position_field;
static jfieldID s_overlay_control_data_foldable_position_field;
@@ -244,6 +242,10 @@ namespace Common::Android {
return s_overlay_control_data_enabled_field;
}
jfieldID GetOverlayControlDataIndividualScaleField() {
return s_overlay_control_data_individual_scale_field;
}
jfieldID GetOverlayControlDataLandscapePositionField() {
return s_overlay_control_data_landscape_position_field;
}
@@ -494,7 +496,7 @@ namespace Common::Android {
reinterpret_cast<jclass>(env->NewGlobalRef(overlay_control_data_class));
s_overlay_control_data_constructor =
env->GetMethodID(overlay_control_data_class, "<init>",
"(Ljava/lang/String;ZLkotlin/Pair;Lkotlin/Pair;Lkotlin/Pair;)V");
"(Ljava/lang/String;ZLkotlin/Pair;Lkotlin/Pair;Lkotlin/Pair;F)V");
s_overlay_control_data_id_field =
env->GetFieldID(overlay_control_data_class, "id", "Ljava/lang/String;");
s_overlay_control_data_enabled_field =
@@ -505,6 +507,8 @@ namespace Common::Android {
env->GetFieldID(overlay_control_data_class, "portraitPosition", "Lkotlin/Pair;");
s_overlay_control_data_foldable_position_field =
env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;");
s_overlay_control_data_individual_scale_field =
env->GetFieldID(overlay_control_data_class, "individualScale", "F");
env->DeleteLocalRef(overlay_control_data_class);
const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch");

View File

@@ -1,7 +1,4 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -67,6 +64,7 @@ jclass GetOverlayControlDataClass();
jmethodID GetOverlayControlDataConstructor();
jfieldID GetOverlayControlDataIdField();
jfieldID GetOverlayControlDataEnabledField();
jfieldID GetOverlayControlDataIndividualScaleField();
jfieldID GetOverlayControlDataLandscapePositionField();
jfieldID GetOverlayControlDataPortraitPositionField();
jfieldID GetOverlayControlDataFoldablePositionField();