diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 7d14bdbb5..273495321 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -766,6 +766,9 @@ Por ejemplo, META para la tecla META, \n Movimiento relativo del mouse Entrada del mouse Toque + Diseño del cajón + Elementos de menú + Pestañas Trackpad Modo de pantalla táctil Asignar a la palanca derecha diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index c4f1aa222..0a29f6160 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1673,6 +1673,9 @@ Installeret sti: RTS-gestusser Rediger Berøring + Skuffelayout + Menupunkter + Faner Tilknyt højre stik Touchskærm-tilstand Pegeplade diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f05017af3..f85e9f8fa 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1663,6 +1663,9 @@ Installierter Pfad: RTS-Gesten Bearbeiten Touch + Drawer-Layout + Menüpunkte + Registerkarten Auf rechten Stick mappen Touchscreen-Modus Trackpad diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5f0838b74..5a0b8ba0c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1663,6 +1663,9 @@ Ruta instalada: Gestos RTS Editar Táctil + Diseño del cajón + Elementos de menú + Pestañas Asignar al stick derecho Modo pantalla táctil Trackpad diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 54085eb61..84a5da621 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -766,6 +766,9 @@ E.g. META for META-näppäin, \n Suhteellinen hiiren liike Hiiren syöte Kosketus + Laatikon asettelu + Valikkokohteet + Välilehdet Kosketuslevy Kosketusnäyttötila Yhdistä oikeaan sauvaan diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ddcd3cb26..67ffee7ae 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1663,6 +1663,9 @@ Chemin installé : Gestes RTS Modifier Tactile + Disposition du tiroir + Éléments de menu + Onglets Mapper sur le stick droit Mode écran tactile Trackpad diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 82c532387..26cdfc5d9 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1559,6 +1559,9 @@ RTS जेस्चर संपादित करें टच + ड्रॉअर लेआउट + मेनू आइटम + टैब दाएँ स्टिक से मैप करें टचस्क्रीन मोड ट्रैकपैड diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 2f986a14f..6e4410a3b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1663,6 +1663,9 @@ Percorso installato: Gesti RTS Modifica Tocco + Layout del cassetto + Voci di menu + Schede Mappa su stick destro Modalità touchscreen Trackpad diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c705cfe38..a86958431 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -766,6 +766,9 @@ 相対マウス移動 マウス入力 タッチ + ドロワーレイアウト + メニュー項目 + タブ トラックパッド タッチスクリーンモード 右スティックに割り当て diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 73ad8364e..bd82fbd7b 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1664,6 +1664,9 @@ RTS 제스처 편집 터치 + 서랍 레이아웃 + 메뉴 항목 + 탭 오른쪽 스틱에 매핑 터치스크린 모드 트랙패드 diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 2dc71d3c2..5db1598d1 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -766,6 +766,9 @@ F.eks. META for META-tast, \n Relativ musebevegelse Museinndata Berøring + Skuffelayout + Menyelementer + Faner Styreplate Berøringsskjermmodus Tilordne til høyre spak diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 05615e628..b0bffa68c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1669,6 +1669,9 @@ Zainstalowana ścieżka: Gesty RTS Edytuj Dotyk + Układ szuflady + Elementy menu + Karty Mapuj na prawą gałkę Tryb ekranu dotykowego Gładzik diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index de55c3f40..557f5a8ce 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1663,6 +1663,9 @@ Caminho instalado: Gestos RTS Editar Toque + Layout da gaveta + Itens do menu + Abas Mapear para o analógico direito Modo Tela Sensível ao Toque Trackpad diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 4397c2d55..e20327460 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -766,6 +766,9 @@ Por ex. META para tecla META, \n Movimento Relativo do Rato Entrada do Rato Toque + Layout da gaveta + Itens de menu + Abas Trackpad Modo de Ecrã Tátil Mapear para o Manípulo Direito diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index c2d26081b..bb457bd1d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1662,6 +1662,9 @@ Cale instalata: Gesturi RTS Editează Tactil + Aspectul sertarului + Elemente de meniu + File Mapează pe stick dreapta Mod ecran tactil Trackpad diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3b28d603d..890906d90 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1569,6 +1569,9 @@ Жесты RTS Изменить Касание + Раскладка панели + Элементы меню + Вкладки Назначить на правый стик Режим сенсорного экрана Трекпад diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 8e4430621..cc0f53cbd 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -766,6 +766,9 @@ T.ex. META för META-tangent, \n Relativ musrörelse Musinmatning Touch + Lådans layout + Menyalternativ + Flikar Styrplatta Pekskärmsläge Mappa till höger spak diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 652e3af4a..1c038bf66 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -766,6 +766,9 @@ การเคลื่อนเมาส์แบบสัมพัทธ์ อินพุตเมาส์ สัมผัส + เค้าโครงแผง + รายการเมนู + แท็บ แทร็คแพด โหมดหน้าจอสัมผัส แมปกับสติ๊กขวา diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 75035df48..097347b2d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -766,6 +766,9 @@ E.g. META için META tuşu, \n Göreli Fare Hareketi Fare Girişi Dokunma + Çekmece düzeni + Menü öğeleri + Sekmeler Dokunmatik Yüzey Dokunmatik Ekran Modu Sağ Çubuğa Eşle diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 94ac36adf..5a70f103c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1669,6 +1669,9 @@ RTS-жести Редагувати Дотик + Макет панелі + Пункти меню + Вкладки Призначити на правий стік Режим сенсорного екрана Тачпад diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 34707a59e..6af0bd05a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1663,6 +1663,9 @@ RTS 手势 编辑 触摸 + 抽屉布局 + 菜单项 + 选项卡 映射到右摇杆 触摸屏模式 触控板 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index a7fc0db62..bf1ed7dd7 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1662,6 +1662,9 @@ RTS 手勢 編輯 觸控 + 抽屜版面 + 選單項目 + 索引標籤 對應至右搖桿 觸控螢幕模式 觸控板 diff --git a/app/src/main/res/values/refs.xml b/app/src/main/res/values/refs.xml index dd8186d2d..6f2f592ac 100644 --- a/app/src/main/res/values/refs.xml +++ b/app/src/main/res/values/refs.xml @@ -24,6 +24,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff7611973..b2c0a2ade 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -867,6 +867,9 @@ E.g. META for META key, \n Relative Mouse Movement Mouse Input Touch + Drawer Layout + Menu Items + Tabs Trackpad Touchscreen Mode Map to Right Stick diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 9f4d1b080..338110cc1 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -608,6 +608,7 @@ private static class ReshadeLiveEffect { private float drawerEdgeGestureStartX = 0f; private float drawerEdgeGestureStartY = 0f; private int drawerEdgeGesturePointerId = -1; + private com.winlator.cmod.runtime.display.DrawerSide drawerEdgeGestureSide = null; private SensorManager sensorManager; private Sensor gyroSensor; @@ -2050,7 +2051,19 @@ public void run() { getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { - handleNavigationBackPressed(); + // Back only closes overlay → pane → drawer. With nothing open it is + // swallowed entirely: never opens the drawer and never exits the + // container while a game is running. + if (drawerStateHolder != null && drawerStateHolder.consumeOverlayBack()) { + return; + } + if (drawerStateHolder != null && drawerStateHolder.isPaneOpen()) { + drawerStateHolder.closeOpenPane(); + return; + } + if (drawerStateHolder != null && drawerStateHolder.isDrawerOpen()) { + closeDrawerMenu(); + } } }); @@ -2060,9 +2073,14 @@ public void handleOnBackPressed() { final int edgePx = (int) (XServerDisplayHostKt.XSERVER_DRAWER_EDGE_SWIPE_DP * getResources().getDisplayMetrics().density); final Runnable applyExclusion = () -> { if (gestureExclusionView.getHeight() <= 0) return; - gestureExclusionView.setSystemGestureExclusionRects( - java.util.Collections.singletonList( - new android.graphics.Rect(0, 0, edgePx, gestureExclusionView.getHeight()))); + int height = gestureExclusionView.getHeight(); + int width = gestureExclusionView.getWidth(); + java.util.ArrayList rects = new java.util.ArrayList<>(); + rects.add(new android.graphics.Rect(0, 0, edgePx, height)); + if (width > 0) { + rects.add(new android.graphics.Rect(width - edgePx, 0, width, height)); + } + gestureExclusionView.setSystemGestureExclusionRects(rects); }; gestureExclusionView.post(applyExclusion); gestureExclusionView.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or_, ob) -> applyExclusion.run()); @@ -5292,26 +5310,29 @@ protected void onStop() { } private void handleNavigationBackPressed() { - if (environment != null) { - if (drawerStateHolder != null && drawerStateHolder.consumeOverlayBack()) { - return; - } - if (drawerStateHolder != null && drawerStateHolder.isPaneOpen()) { - drawerStateHolder.closeOpenPane(); - return; - } - if (drawerStateHolder == null || !drawerStateHolder.isDrawerOpen()) { - openDrawerMenu(); - } - else closeDrawerMenu(); + // Back closes overlay → pane → drawer, and NEVER opens the drawer. + if (environment == null) return; + if (drawerStateHolder != null && drawerStateHolder.consumeOverlayBack()) { + return; + } + if (drawerStateHolder != null && drawerStateHolder.isPaneOpen()) { + drawerStateHolder.closeOpenPane(); + return; + } + if (drawerStateHolder != null && drawerStateHolder.isDrawerOpen()) { + closeDrawerMenu(); } } private void openDrawerMenu() { + openDrawerMenu(com.winlator.cmod.runtime.display.DrawerSide.LEFT); + } + + private void openDrawerMenu(com.winlator.cmod.runtime.display.DrawerSide side) { releasePointerCapture(); renderDrawerMenu(); if (drawerStateHolder != null) { - drawerStateHolder.openDrawer(); + drawerStateHolder.openDrawer(side); } if (touchpadView != null) { touchpadView.setOnCapturedPointerListener(null); @@ -10560,9 +10581,19 @@ private void handleDrawerEdgeSwipe(MotionEvent event) { drawerEdgeGestureStartX = event.getX(); drawerEdgeGestureStartY = event.getY(); drawerEdgeGesturePointerId = event.getPointerId(0); + float edgePx = getDrawerEdgeSwipePx(); + int screenWidth = getResources().getDisplayMetrics().widthPixels; + boolean onLeft = drawerEdgeGestureStartX <= edgePx; + boolean onRight = screenWidth > 0 && drawerEdgeGestureStartX >= screenWidth - edgePx; drawerEdgeGesturePossible = - drawerEdgeGestureStartX <= getDrawerEdgeSwipePx() + (onLeft || onRight) && !isTouchInsideMagnifier(drawerEdgeGestureStartX, drawerEdgeGestureStartY); + drawerEdgeGestureSide = + drawerEdgeGesturePossible + ? (onLeft + ? com.winlator.cmod.runtime.display.DrawerSide.LEFT + : com.winlator.cmod.runtime.display.DrawerSide.RIGHT) + : null; break; } case MotionEvent.ACTION_MOVE: { @@ -10576,18 +10607,34 @@ private void handleDrawerEdgeSwipe(MotionEvent event) { float dx = event.getX(pointerIndex) - drawerEdgeGestureStartX; float dy = event.getY(pointerIndex) - drawerEdgeGestureStartY; int slop = android.view.ViewConfiguration.get(this).getScaledTouchSlop(); - - if (dx > getDrawerOpenTriggerPx() - && dx > Math.abs(dy) * XServerDisplayHostKt.XSERVER_DRAWER_OPEN_HORIZONTAL_RATIO) { + int trigger = getDrawerOpenTriggerPx(); + boolean horizontal = + Math.abs(dx) > trigger + && Math.abs(dx) + > Math.abs(dy) + * XServerDisplayHostKt.XSERVER_DRAWER_OPEN_HORIZONTAL_RATIO; + boolean openLeft = + drawerEdgeGestureSide == com.winlator.cmod.runtime.display.DrawerSide.LEFT + && dx > 0 + && horizontal; + boolean openRight = + drawerEdgeGestureSide == com.winlator.cmod.runtime.display.DrawerSide.RIGHT + && dx < 0 + && horizontal; + + if (openLeft || openRight) { if (touchpadView != null) { touchpadView.resetInputState(); } if (inputControlsView != null) { inputControlsView.cancelActiveTouches(); } - openDrawerMenu(); + openDrawerMenu( + openLeft + ? com.winlator.cmod.runtime.display.DrawerSide.LEFT + : com.winlator.cmod.runtime.display.DrawerSide.RIGHT); resetDrawerEdgeGesture(); - } else if (Math.abs(dy) > slop && Math.abs(dy) > dx) { + } else if (Math.abs(dy) > slop && Math.abs(dy) > Math.abs(dx)) { resetDrawerEdgeGesture(); } break; @@ -10619,6 +10666,7 @@ private boolean isTouchInsideMagnifier(float x, float y) { private void resetDrawerEdgeGesture() { drawerEdgeGesturePossible = false; drawerEdgeGesturePointerId = -1; + drawerEdgeGestureSide = null; } @Override diff --git a/app/src/main/runtime/display/XServerDisplayHost.kt b/app/src/main/runtime/display/XServerDisplayHost.kt index b93475a2b..e8b40432d 100644 --- a/app/src/main/runtime/display/XServerDisplayHost.kt +++ b/app/src/main/runtime/display/XServerDisplayHost.kt @@ -56,19 +56,26 @@ import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.math.roundToInt -const val XSERVER_DRAWER_EDGE_SWIPE_DP = 35 +const val XSERVER_DRAWER_EDGE_SWIPE_DP = 24 // Horizontal swipe distance to open the drawer; shared with XServerDisplayActivity. -const val XSERVER_DRAWER_OPEN_TRIGGER_DP = 32 +// 16dp: a short swipe from the edge reliably opens (was 32dp) so you no longer have to +// drag toward the middle for the drawer to appear. +const val XSERVER_DRAWER_OPEN_TRIGGER_DP = 16 -// Open only on a clearly rightward swipe: dx must exceed this * |dy| (~27deg of horizontal). -const val XSERVER_DRAWER_OPEN_HORIZONTAL_RATIO = 2f +// Open only on a clearly horizontal swipe: dx must exceed this * |dy| (~27deg of horizontal). +// 1.5f: tolerate a slightly diagonal swipe (was 2f) so an imperfect angle doesn't bounce back. +const val XSERVER_DRAWER_OPEN_HORIZONTAL_RATIO = 1.5f private val DrawerWidth = 300.dp private val DrawerStartPadding = 6.dp private val DrawerVerticalPadding = 6.dp private const val DrawerSettleAnimationMs = 200 -private const val DrawerOpenSettleThreshold = 0.4f +// Dragging out any visible amount of the drawer keeps it open. The settle check is +// drawerProgress >= this, and progress = draggedPx / drawerWidthPx (~300dp), so 0 means +// “as soon as the gesture was claimed (past the open trigger) it stays open”; a positive +// value would again bounce back for anything dragged less than threshold × width. +private const val DrawerOpenSettleThreshold = 0f private const val DrawerCloseSettleThreshold = 0.65f private val DrawerSettleAnimationSpec = tween( @@ -118,6 +125,10 @@ private fun XServerDisplayHost( val animationScope = rememberCoroutineScope() val density = LocalDensity.current val viewConfiguration = LocalViewConfiguration.current + val screenWidthPx = + with(density) { + androidx.compose.ui.platform.LocalConfiguration.current.screenWidthDp.dp.toPx() + } val closedFallbackPx = with(density) { -(DrawerWidth + DrawerStartPadding).toPx() } var drawerOffsetPx by remember { mutableFloatStateOf(closedFallbackPx) } var drawerWidthPx by remember { mutableFloatStateOf(0f) } @@ -128,6 +139,16 @@ private fun XServerDisplayHost( closedFallbackPx } val drawerOpenOffset = 0f + var lastSide by remember { + androidx.compose.runtime.mutableStateOf( + null, + ) + } + LaunchedEffect(stateHolder.openSide) { + if (stateHolder.openSide != null) { + lastSide = stateHolder.openSide + } + } // "Engaged" = on or sliding onto the screen; drives the card-reveal animation (the content itself is always composed). val drawerEngaged = drawerWidthPx <= 0f || drawerOffsetPx > drawerClosedOffset + 1f || @@ -150,7 +171,6 @@ private fun XServerDisplayHost( } LaunchedEffect(stateHolder.isDrawerOpen, drawerWidthPx) { - if (drawerWidthPx <= 0f) return@LaunchedEffect val target = if (stateHolder.isDrawerOpen) drawerOpenOffset else drawerClosedOffset if (drawerOffsetPx != target) { callbacks.onDrawerSlide() @@ -191,17 +211,30 @@ private fun XServerDisplayHost( val edgeWidthPx = XSERVER_DRAWER_EDGE_SWIPE_DP.dp.toPx() val openTriggerPx = XSERVER_DRAWER_OPEN_TRIGGER_DP.dp.toPx() + val drawerOpen = stateHolder.isDrawerOpen + val sheetAtRight = + (stateHolder.openSide ?: lastSide) == + com.winlator.cmod.runtime.display.DrawerSide.RIGHT + val sheetLeftPx = screenWidthPx - drawerWidthPx val canStartFromHere = - if (stateHolder.isDrawerOpen) { - down.position.x >= drawerWidthPx && - down.position.x <= drawerWidthPx + edgeWidthPx + if (drawerOpen) { + if (sheetAtRight) { + down.position.x >= sheetLeftPx - edgeWidthPx && + down.position.x <= sheetLeftPx + } else { + down.position.x >= drawerWidthPx && + down.position.x <= drawerWidthPx + edgeWidthPx + } } else { - down.position.x <= edgeWidthPx + down.position.x <= edgeWidthPx || + down.position.x >= screenWidthPx - edgeWidthPx } if (!canStartFromHere) { - if (stateHolder.isDrawerOpen && down.position.x > drawerWidthPx - && !callbacks.isControllerConnected()) { - stateHolder.closeDrawer() + if (drawerOpen && !callbacks.isControllerConnected()) { + val outside = + if (sheetAtRight) down.position.x < sheetLeftPx + else down.position.x > drawerWidthPx + if (outside) stateHolder.closeDrawer() } return@awaitEachGesture } @@ -222,16 +255,27 @@ private fun XServerDisplayHost( totalDy += delta.y if (!gestureClaimed) { - if (abs(totalDy) > viewConfiguration.touchSlop && abs(totalDy) > abs(totalDx)) { + // Only an *obviously* vertical drag cancels the gesture; a slightly + // diagonal swipe (dy up to 2x dx) still counts as horizontal so a + // not-quite-straight swipe opens instead of bouncing back. + if (abs(totalDy) > viewConfiguration.touchSlop && + abs(totalDy) > abs(totalDx) * 2f + ) { cancelledByVerticalDrag = true break } val horizontalDragClaimed = if (stateHolder.isDrawerOpen) { - totalDx < -viewConfiguration.touchSlop && abs(totalDx) > abs(totalDy) + val closeDx = if (sheetAtRight) totalDx > 0f else totalDx < 0f + abs(totalDx) > viewConfiguration.touchSlop && + abs(totalDx) > abs(totalDy) && + closeDx } else { - totalDx > openTriggerPx && - totalDx > abs(totalDy) * XSERVER_DRAWER_OPEN_HORIZONTAL_RATIO + val openDx = if (sheetAtRight) totalDx < 0f else totalDx > 0f + abs(totalDx) > openTriggerPx && + abs(totalDx) > + abs(totalDy) * XSERVER_DRAWER_OPEN_HORIZONTAL_RATIO && + openDx } if (horizontalDragClaimed) { gestureClaimed = true @@ -343,6 +387,8 @@ private fun XServerDisplayHost( } // Closed, the sheet sits flush on the edge and its rounded corners leak a hairline. + val openRight = + (stateHolder.openSide ?: lastSide) == com.winlator.cmod.runtime.display.DrawerSide.RIGHT if (drawerContentComposed) { ModalDrawerSheet( drawerShape = RoundedCornerShape(20.dp), @@ -353,14 +399,20 @@ private fun XServerDisplayHost( modifier = Modifier .zIndex(2f) - .padding(start = DrawerStartPadding, top = drawerTopInset, bottom = DrawerVerticalPadding) + .then(if (openRight) Modifier.align(Alignment.CenterEnd) else Modifier) + .padding( + start = if (openRight) 0.dp else DrawerStartPadding, + end = if (openRight) DrawerStartPadding else 0.dp, + top = drawerTopInset, + bottom = DrawerVerticalPadding, + ) .fillMaxHeight() .width(scaledDrawerWidth) .offset { - androidx.compose.ui.unit.IntOffset( - drawerOffsetPx.roundToInt(), - 0, - ) + val effective = + if (stateHolder.isDrawerOpen) drawerOffsetPx else drawerClosedOffset + val dx = effective.roundToInt() + androidx.compose.ui.unit.IntOffset(if (openRight) -dx else dx, 0) }, ) { XServerDrawerContent( @@ -371,6 +423,11 @@ private fun XServerDisplayHost( onOpenPaneChange = { stateHolder.setOpenPaneAndNotify(it) }, listener = listener, onDismiss = { stateHolder.closeDrawer() }, + openSide = stateHolder.openSide, + onDrawerLayoutChanged = { newItems, newPaneSides -> + stateHolder.state = + stateHolder.state.copy(items = newItems, paneSides = newPaneSides) + }, revealCards = drawerEngaged, menuNavRegion = stateHolder.menuNavRegion, menuNavIndex = stateHolder.menuNavIndex, diff --git a/app/src/main/runtime/display/XServerDrawerMenu.kt b/app/src/main/runtime/display/XServerDrawerMenu.kt index d7cf9f127..732939e2d 100644 --- a/app/src/main/runtime/display/XServerDrawerMenu.kt +++ b/app/src/main/runtime/display/XServerDrawerMenu.kt @@ -577,6 +577,8 @@ internal val DisFrameGenTargetRates = listOf(60, 90, 120, 144, 165, 185) internal const val DisFrameGenScaleMin = 64 internal const val DisFrameGenScaleMax = 1080 +enum class DrawerSide { LEFT, RIGHT, BOTH } + data class XServerDrawerItem( val itemId: Int, val title: String, @@ -584,6 +586,7 @@ data class XServerDrawerItem( val icon: ImageVector, val active: Boolean = false, val enabled: Boolean = true, + val side: DrawerSide = DrawerSide.LEFT, ) data class RecordUiConfig( @@ -597,6 +600,7 @@ data class RecordUiConfig( data class XServerDrawerState( val items: List, + val paneSides: Map = emptyMap(), val hudTransparency: Float = 1.0f, val hudBackgroundAlphaEnabled: Boolean = false, val hudBackgroundTransparency: Float = 1.0f, @@ -727,6 +731,7 @@ class XServerDrawerStateHolder( flushLogsBufferToState() } private var drawerOpen by mutableStateOf(false) + internal var openSide by mutableStateOf(null) internal var openPane by mutableStateOf(null) var menuNavRegion by mutableStateOf(0) private set @@ -756,8 +761,11 @@ class XServerDrawerStateHolder( val isDrawerOpen: Boolean get() = drawerOpen - fun openDrawer() { + fun openDrawer() = openDrawer(DrawerSide.LEFT) + + fun openDrawer(side: DrawerSide) { drawerOpen = true + openSide = side } private fun regionSize(r: Int) = when (r) { @@ -886,6 +894,7 @@ class XServerDrawerStateHolder( fun closeDrawer() { drawerOpen = false + openSide = null openPane = null } @@ -1395,9 +1404,22 @@ fun buildXServerDrawerState( icon = Icons.AutoMirrored.Outlined.ExitToApp, ) + items += + XServerDrawerItem( + itemId = R.id.main_menu_drawer_layout, + title = context.getString(R.string.session_drawer_layout), + subtitle = "", + icon = Icons.Outlined.Tune, + ) + + val itemSideMap = loadDrawerItemSides(context) + val finalItems = items.map { it.copy(side = itemSideMap[it.itemId] ?: DrawerSide.LEFT) } + val paneSides = loadDrawerPaneSides(context) + return XServerDrawerState( recordConfig = recordConfig, - items = items, + items = finalItems, + paneSides = paneSides, hudTransparency = hudTransparency, hudBackgroundAlphaEnabled = hudBackgroundAlphaEnabled, hudBackgroundTransparency = hudBackgroundTransparency, @@ -1651,6 +1673,8 @@ internal fun XServerDrawerContent( onOpenPaneChange: (DrawerPane?) -> Unit, listener: XServerDrawerActionListener, onDismiss: () -> Unit, + openSide: DrawerSide? = null, + onDrawerLayoutChanged: (List, Map) -> Unit = { _, _ -> }, revealCards: Boolean = true, menuNavRegion: Int = 0, menuNavIndex: Int = 0, @@ -1706,6 +1730,8 @@ internal fun XServerDrawerContent( TopRail( state = state, openPane = openPane, + openSide = openSide, + paneSides = state.paneSides, onTabClick = { spec -> onOpenPaneChange(if (openPane == spec.pane) null else spec.pane) }, @@ -1778,6 +1804,8 @@ internal fun XServerDrawerContent( state = state, listener = listener, cardsRevealed = cardsRevealed.value, + openSide = openSide, + onDrawerLayoutChanged = onDrawerLayoutChanged, onOpenTaskManager = { onOpenPaneChange(DrawerPane.TASK_MANAGER) }, onOpenLogs = { onOpenPaneChange(DrawerPane.LOGS) }, onOpenTouch = { onOpenPaneChange(DrawerPane.TOUCH) }, @@ -1825,6 +1853,8 @@ private data class RailTileBounds(val offsetX: Float, val width: Float, val heig private fun TopRail( state: XServerDrawerState, openPane: DrawerPane?, + openSide: DrawerSide? = null, + paneSides: Map = emptyMap(), onTabClick: (RailPaneSpec) -> Unit, onMenuClick: () -> Unit, region: Int = 0, @@ -1836,7 +1866,13 @@ private fun TopRail( ) { val paneScale = LocalPaneScale.current val density = LocalDensity.current - val activeSpecs = RAIL_PANES.filter { spec -> state.items.any { it.itemId == spec.itemId } } + val activeSpecs = + RAIL_PANES.filter { spec -> + state.items.any { it.itemId == spec.itemId } && + (openSide == null || + (paneSides[spec.itemId] ?: DrawerSide.LEFT) == openSide || + (paneSides[spec.itemId] ?: DrawerSide.LEFT) == DrawerSide.BOTH) + } val tabCount = activeSpecs.size + 1 LaunchedEffect(tabCount) { onSetNavCount(tabCount) } @@ -2077,6 +2113,8 @@ private fun ActionCardGrid( state: XServerDrawerState, listener: XServerDrawerActionListener, cardsRevealed: Boolean, + openSide: DrawerSide? = null, + onDrawerLayoutChanged: (List, Map) -> Unit = { _, _ -> }, onOpenTaskManager: () -> Unit, onOpenLogs: () -> Unit, onOpenTouch: () -> Unit, @@ -2090,14 +2128,20 @@ private fun ActionCardGrid( val paneScale = LocalPaneScale.current val cards = state.items.filter { - it.itemId !in RAIL_PANE_ITEM_IDS && it.itemId !in PINNED_BOTTOM_ITEM_IDS + it.itemId !in RAIL_PANE_ITEM_IDS && + it.itemId !in PINNED_BOTTOM_ITEM_IDS && + (openSide == null || it.side == openSide || it.side == DrawerSide.BOTH) } var showRecordSettings by remember { mutableStateOf(false) } + var showDrawerLayout by remember { mutableStateOf(false) } + val gridContext = androidx.compose.ui.platform.LocalContext.current fun cardClick(item: XServerDrawerItem) { when (item.itemId) { R.id.main_menu_task_manager -> onOpenTaskManager() R.id.main_menu_logs -> onOpenLogs() + R.id.main_menu_drawer_layout -> showDrawerLayout = true + // Recording: stop if active, otherwise open the settings popup. R.id.main_menu_record -> if (item.active) listener.onActionSelected(item.itemId) else showRecordSettings = true @@ -2178,8 +2222,182 @@ private fun ActionCardGrid( }, ) } + + if (showDrawerLayout) { + DrawerLayoutDialog( + items = + state.items.filter { + it.itemId !in RAIL_PANE_ITEM_IDS && + it.itemId !in PINNED_BOTTOM_ITEM_IDS && + it.itemId != R.id.main_menu_drawer_layout + }, + paneSides = state.paneSides, + onDismiss = { showDrawerLayout = false }, + onApply = { menuSides, paneSides -> + androidx.preference.PreferenceManager.getDefaultSharedPreferences(gridContext) + .edit() + .putString( + "drawer_item_sides", + menuSides.entries.joinToString(",") { "${it.key}:${it.value.name}" }, + ) + .putString( + "drawer_pane_sides", + paneSides.entries.joinToString(",") { "${it.key}:${it.value.name}" }, + ) + .apply() + onDrawerLayoutChanged( + state.items.map { item -> + menuSides[item.itemId]?.let { item.copy(side = it) } ?: item + }, + paneSides, + ) + showDrawerLayout = false + }, + ) + } +} + +private fun loadDrawerItemSides(context: android.content.Context): Map { + val raw = + androidx.preference.PreferenceManager.getDefaultSharedPreferences(context) + .getString("drawer_item_sides", "") + .orEmpty() + if (raw.isBlank()) return emptyMap() + return parseDrawerSides(raw) +} + +private fun loadDrawerPaneSides(context: android.content.Context): Map { + val raw = + androidx.preference.PreferenceManager.getDefaultSharedPreferences(context) + .getString("drawer_pane_sides", "") + .orEmpty() + if (raw.isBlank()) return emptyMap() + return parseDrawerSides(raw) } +private fun parseDrawerSides(raw: String): Map = + raw.split(",").mapNotNull { seg -> + val parts = seg.split(":") + val id = parts.getOrNull(0)?.toIntOrNull() ?: return@mapNotNull null + val side = + when (parts.getOrNull(1)) { + "RIGHT" -> DrawerSide.RIGHT + "BOTH" -> DrawerSide.BOTH + else -> DrawerSide.LEFT + } + id to side + }.toMap() + + +@Composable +private fun DrawerLayoutDialog( + items: List, + paneSides: Map = emptyMap(), + onDismiss: () -> Unit, + onApply: (Map, Map) -> Unit, +) { + val context = androidx.compose.ui.platform.LocalContext.current + var menuSides by remember { mutableStateOf(items.associate { it.itemId to it.side }) } + var paneMap by remember { mutableStateOf(paneSides) } + var activeTab by remember { mutableStateOf(0) } + + @Composable + fun sideRow(title: String, current: DrawerSide, onPick: (DrawerSide) -> Unit) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = title, fontSize = 13.sp, modifier = Modifier.weight(1f)) + DrawerSide.entries.forEach { option -> + androidx.compose.material3.RadioButton( + selected = current == option, + onClick = { onPick(option) }, + ) + Text( + when (option) { + DrawerSide.LEFT -> "\u2190" + DrawerSide.RIGHT -> "\u2192" + DrawerSide.BOTH -> "\u2194" + }, + fontSize = 14.sp, + ) + Spacer(Modifier.width(4.dp)) + } + } + } + + androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) { + androidx.compose.material3.Surface( + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + color = androidx.compose.material3.MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + ) { + Text( + text = stringResource(R.string.session_drawer_layout), + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ) + Spacer(Modifier.height(10.dp)) + Row(modifier = Modifier.fillMaxWidth()) { + listOf( + stringResource(R.string.session_drawer_layout_items), + stringResource(R.string.session_drawer_layout_tabs), + ).forEachIndexed { index, label -> + androidx.compose.material3.FilterChip( + selected = activeTab == index, + onClick = { activeTab = index }, + label = { Text(label) }, + modifier = Modifier.weight(1f), + ) + } + } + Spacer(Modifier.height(10.dp)) + + if (activeTab == 0) { + items.forEach { item -> + sideRow(item.title, menuSides[item.itemId] ?: DrawerSide.LEFT) { + menuSides = menuSides + (item.itemId to it) + } + } + } else { + RAIL_PANES.forEach { spec -> + val label = + runCatching { context.getString(spec.labelRes) }.getOrNull() + ?: spec.pane?.name ?: "" + sideRow(label, paneMap[spec.itemId] ?: DrawerSide.LEFT) { + // Rebuild the map instead of mutating in place: Compose only + // recomposes on a new reference, and in-place mutation of a plain + // MutableMap never re-renders the radio selection. + paneMap = paneMap + (spec.itemId to it) + } + } + } + + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + androidx.compose.material3.TextButton(onClick = onDismiss) { + Text(stringResource(R.string.common_ui_cancel)) + } + Spacer(Modifier.width(8.dp)) + androidx.compose.material3.Button(onClick = { onApply(menuSides, paneMap) }) { + Text(stringResource(R.string.common_ui_ok)) + } + } + } + } + } +} + + @Composable private fun ActionCard( item: XServerDrawerItem,