From 69c20dc93a1c50677aaaf89c03721c49e028a4bb Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Mon, 13 Jul 2026 20:29:05 +0000 Subject: [PATCH 1/8] Touch controls: new themed draw styles with per-profile color themes - Add Slate (new default), Halo and Glint draw styles; Original kept, listed last - Glass style reworked onto the shared engine: theme accents, cached gradients, allocation-free draw paths - 10 accent presets stored per-profile in the .icp; style + theme pickers in the controls editor, theme picker in the session drawer (replaces Button Labels) - Remove built-in Xbox/PlayStation label profiles and the label-theme machinery; stale installed copies cleaned up on update - Default bumper/trigger labels are now LB/RB/LT/RT --- .../inputcontrols/profiles/controls-4.icp | 1 - .../inputcontrols/profiles/controls-5.icp | 1 - app/src/main/res/drawable/icon_style.xml | 2 + .../res/layout/controls_editor_activity.xml | 10 + app/src/main/res/values-b+es+419/strings.xml | 4 - 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-fi/strings.xml | 4 - app/src/main/res/values-fr/strings.xml | 4 - app/src/main/res/values-hi/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-no/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-pt/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-sv/strings.xml | 4 - app/src/main/res/values-th/strings.xml | 4 - app/src/main/res/values-tr/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 - app/src/main/res/values/strings.xml | 8 +- .../display/XServerDisplayActivity.java | 82 +- .../main/runtime/display/XServerDrawerMenu.kt | 24 +- .../runtime/input/ControlsEditorActivity.java | 123 +++ .../runtime/input/controls/AccentTheme.java | 39 + .../input/controls/ControlElement.java | 732 ++++++++++++++++-- .../input/controls/ControlsProfile.java | 20 +- .../input/controls/InputControlsManager.java | 25 +- .../runtime/input/controls/LabelTheme.java | 150 ---- .../runtime/input/controls/RangeScroller.java | 31 +- .../runtime/input/controls/VisualStyle.java | 24 +- .../runtime/input/ui/InputControlsView.java | 20 +- 38 files changed, 976 insertions(+), 404 deletions(-) delete mode 100644 app/src/main/assets/inputcontrols/profiles/controls-4.icp delete mode 100644 app/src/main/assets/inputcontrols/profiles/controls-5.icp create mode 100644 app/src/main/res/drawable/icon_style.xml create mode 100644 app/src/main/runtime/input/controls/AccentTheme.java delete mode 100644 app/src/main/runtime/input/controls/LabelTheme.java diff --git a/app/src/main/assets/inputcontrols/profiles/controls-4.icp b/app/src/main/assets/inputcontrols/profiles/controls-4.icp deleted file mode 100644 index 1aa0a0fca..000000000 --- a/app/src/main/assets/inputcontrols/profiles/controls-4.icp +++ /dev/null @@ -1 +0,0 @@ -{"id":4,"name":"Playstation Controller","cursorSpeed":1,"elements":[{"type":"BUTTON","shape":"ROUND_RECT","customColor":-12435134,"bindings":["GAMEPAD_BUTTON_L2","NONE","NONE","NONE"],"scale":1.25,"x":0.12871287763118744,"y":0.10775047540664673,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-12435134,"bindings":["GAMEPAD_BUTTON_R2","NONE","NONE","NONE"],"scale":1.25,"x":0.8704261779785156,"y":0.10775047540664673,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_L1","NONE","NONE","NONE"],"scale":1.1,"x":0.12871287763118744,"y":0.239130437374115,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_R1","NONE","NONE","NONE"],"scale":1.1,"x":0.8704261779785156,"y":0.239130437374115,"toggleSwitch":false,"text":"","iconId":0},{"type":"D_PAD","shape":"CIRCLE","customColor":-13948632,"bindings":["GAMEPAD_DPAD_UP","GAMEPAD_DPAD_RIGHT","GAMEPAD_DPAD_DOWN","GAMEPAD_DPAD_LEFT"],"scale":0.92,"x":0.2970297038555145,"y":0.7816635370254517,"toggleSwitch":false,"text":"","iconId":0},{"type":"STICK","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_LEFT_THUMB_UP","GAMEPAD_LEFT_THUMB_RIGHT","GAMEPAD_LEFT_THUMB_DOWN","GAMEPAD_LEFT_THUMB_LEFT"],"scale":1.08,"x":0.12742143869400024,"y":0.6153119206428528,"toggleSwitch":false,"text":"","iconId":0},{"type":"STICK","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_RIGHT_THUMB_UP","GAMEPAD_RIGHT_THUMB_RIGHT","GAMEPAD_RIGHT_THUMB_DOWN","GAMEPAD_RIGHT_THUMB_LEFT"],"scale":1.08,"x":0.8721480965614319,"y":0.6153119206428528,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-65281,"bindings":["GAMEPAD_BUTTON_X","NONE","NONE","NONE"],"scale":1,"x":0.6534653306007385,"y":0.6739130616188049,"toggleSwitch":false,"text":"□","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-16711936,"bindings":["GAMEPAD_BUTTON_Y","NONE","NONE","NONE"],"scale":1,"x":0.7029703259468079,"y":0.5652173757553101,"toggleSwitch":false,"text":"△","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-16776961,"bindings":["GAMEPAD_BUTTON_A","NONE","NONE","NONE"],"scale":1,"x":0.7029703259468079,"y":0.7816635370254517,"toggleSwitch":false,"text":"×","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-65536,"bindings":["GAMEPAD_BUTTON_B","NONE","NONE","NONE"],"scale":1,"x":0.7524752616882324,"y":0.6739130616188049,"toggleSwitch":false,"text":"o","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_SELECT","NONE","NONE","NONE"],"scale":0.9,"x":0.44468361139297485,"y":0.95652174949646,"toggleSwitch":false,"text":"","iconId":16},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_START","NONE","NONE","NONE"],"scale":0.9,"x":0.5445544719696045,"y":0.95652174949646,"toggleSwitch":false,"text":"","iconId":15},{"type":"BUTTON","shape":"CIRCLE","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_L3","NONE","NONE","NONE"],"scale":0.78,"x":0.16745588183403015,"y":0.8468809127807617,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_R3","NONE","NONE","NONE"],"scale":0.78,"x":0.7920792102813721,"y":0.8468809127807617,"toggleSwitch":false,"text":"","iconId":0}]} \ No newline at end of file diff --git a/app/src/main/assets/inputcontrols/profiles/controls-5.icp b/app/src/main/assets/inputcontrols/profiles/controls-5.icp deleted file mode 100644 index 9c3a3b095..000000000 --- a/app/src/main/assets/inputcontrols/profiles/controls-5.icp +++ /dev/null @@ -1 +0,0 @@ -{"id":5,"name":"Xbox Controller","cursorSpeed":1,"elements":[{"type":"BUTTON","shape":"ROUND_RECT","customColor":-12435134,"bindings":["GAMEPAD_BUTTON_L2","NONE","NONE","NONE"],"scale":1.25,"x":0.12871287763118744,"y":0.10869564861059189,"toggleSwitch":false,"text":"LT","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-12435134,"bindings":["GAMEPAD_BUTTON_R2","NONE","NONE","NONE"],"scale":1.25,"x":0.8811880946159363,"y":0.10869564861059189,"toggleSwitch":false,"text":"RT","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_L1","NONE","NONE","NONE"],"scale":1.05,"x":0.12871287763118744,"y":0.239130437374115,"toggleSwitch":false,"text":"LB","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_R1","NONE","NONE","NONE"],"scale":1.15,"x":0.8811880946159363,"y":0.239130437374115,"toggleSwitch":false,"text":"RB","iconId":0},{"type":"D_PAD","shape":"CIRCLE","customColor":-13948632,"bindings":["GAMEPAD_DPAD_UP","GAMEPAD_DPAD_RIGHT","GAMEPAD_DPAD_DOWN","GAMEPAD_DPAD_LEFT"],"scale":0.92,"x":0.3164012134075165,"y":0.7816635370254517,"toggleSwitch":false,"text":"","iconId":0},{"type":"STICK","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_LEFT_THUMB_UP","GAMEPAD_LEFT_THUMB_RIGHT","GAMEPAD_LEFT_THUMB_DOWN","GAMEPAD_LEFT_THUMB_LEFT"],"scale":1.08,"x":0.12742143869400024,"y":0.6153119206428528,"toggleSwitch":false,"text":"","iconId":0},{"type":"STICK","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_RIGHT_THUMB_UP","GAMEPAD_RIGHT_THUMB_RIGHT","GAMEPAD_RIGHT_THUMB_DOWN","GAMEPAD_RIGHT_THUMB_LEFT"],"scale":1.08,"x":0.8721480965614319,"y":0.6153119206428528,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-16776961,"bindings":["GAMEPAD_BUTTON_X","NONE","NONE","NONE"],"scale":1,"x":0.6530348658561707,"y":0.6739130616188049,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-256,"bindings":["GAMEPAD_BUTTON_Y","NONE","NONE","NONE"],"scale":1,"x":0.7029703259468079,"y":0.5642722249031067,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-16711936,"bindings":["GAMEPAD_BUTTON_A","NONE","NONE","NONE"],"scale":1,"x":0.7029703259468079,"y":0.7816635370254517,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-65536,"bindings":["GAMEPAD_BUTTON_B","NONE","NONE","NONE"],"scale":1,"x":0.7524752616882324,"y":0.6739130616188049,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_SELECT","NONE","NONE","NONE"],"scale":0.9,"x":0.4451140761375427,"y":0.95652174949646,"toggleSwitch":false,"text":"","iconId":16},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_START","NONE","NONE","NONE"],"scale":0.9,"x":0.5445544719696045,"y":0.95652174949646,"toggleSwitch":false,"text":"","iconId":15},{"type":"BUTTON","shape":"CIRCLE","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_L3","NONE","NONE","NONE"],"scale":0.78,"x":0.16788634657859802,"y":0.8468809127807617,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_R3","NONE","NONE","NONE"],"scale":0.78,"x":0.8015497326850891,"y":0.8468809127807617,"toggleSwitch":false,"text":"","iconId":0}]} \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_style.xml b/app/src/main/res/drawable/icon_style.xml new file mode 100644 index 000000000..f023d2ed3 --- /dev/null +++ b/app/src/main/res/drawable/icon_style.xml @@ -0,0 +1,2 @@ + + diff --git a/app/src/main/res/layout/controls_editor_activity.xml b/app/src/main/res/layout/controls_editor_activity.xml index 88b387e6d..26eb62aa5 100644 --- a/app/src/main/res/layout/controls_editor_activity.xml +++ b/app/src/main/res/layout/controls_editor_activity.xml @@ -65,5 +65,15 @@ android:id="@+id/BTColorPicker" style="@style/ToolButton.Dark" android:src="@drawable/pickercolorcontrol" /> + + + + \ No newline at end of file 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 3b6a60778..dd08f0471 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -361,12 +361,8 @@ Número de asignaciones Seleccionar perfil Seleccionar estilo - Etiquetas de botones Original Vidrio - Original - Xbox - PlayStation Perfil integrado — se creó una copia que puedes editar. Nombre del perfil No hay ningún perfil seleccionado diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 535cf118c..bfa736c5a 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1301,12 +1301,8 @@ Installeret sti: Ældre Steam-emulator Får den emulerede Steam-klient til at melde sig offline, så spillet ikke forsøger onlineforbindelser. Bruges til spil, der hænger ved Steams netværkstjek. Vælg stil - Knapetiketter Original Glass - Original - Xbox - PlayStation Indbygget profil - oprettede en kopi, du kan redigere. Nulstil denne profil til dens oprindelige layout? Alle ændringer, du har lavet, går tabt. Profil nulstillet til dens oprindelige layout. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a8a9e2387..d43b8af51 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1301,12 +1301,8 @@ Installierter Pfad: Älterer Steam-Emulator Lässt den emulierten Steam-Client offline melden, damit das Spiel keine Online-Verbindungen versucht. Für Spiele verwenden, die bei Steams Netzwerkprüfung hängen bleiben. Stil auswählen - Schaltflächenbeschriftungen Original Glass - Original - Xbox - PlayStation Integriertes Profil - eine bearbeitbare Kopie wurde erstellt. Dieses Profil auf sein ursprüngliches Layout zurücksetzen? Alle vorgenommenen Änderungen gehen verloren. Profil auf ursprüngliches Layout zurückgesetzt. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index dcb6f9137..f69c090e4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1301,12 +1301,8 @@ Ruta instalada: Emulador de Steam heredado Hace que el cliente Steam emulado se reporte como desconectado, para que el juego no intente conexiones en línea. Úsalo en juegos que se quedan bloqueados en la comprobación de red de Steam. Seleccionar estilo - Etiquetas de botones Original Glass - Original - Xbox - PlayStation Perfil integrado: se creó una copia que puedes editar. ¿Restablecer este perfil a su diseño original? Se perderán todos los cambios que hayas hecho. Perfil restablecido a su diseño original. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index dfe438791..b40b43692 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -361,12 +361,8 @@ Sidontojen määrä Valitse profiili Valitse tyyli - Painikkeiden nimiöt Alkuperäinen Lasi - Alkuperäinen - Xbox - PlayStation Sisäänrakennettu profiili – luotiin kopio, jota voit muokata. Profiilin nimi Ei valittua profiilia diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2afa57152..df12b6b2f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1301,12 +1301,8 @@ Chemin installé : Émulateur Steam hérité Force le client Steam émulé à se signaler hors ligne afin que le jeu ne tente pas de connexions en ligne. À utiliser pour les jeux qui restent bloqués sur la vérification réseau de Steam. Sélectionner le style - Étiquettes des boutons Original Glass - Original - Xbox - PlayStation Profil intégré : une copie modifiable a été créée. Réinitialiser ce profil à sa disposition d\'origine ? Toutes vos modifications seront perdues. Profil réinitialisé à sa disposition d\'origine. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index f0f8d5518..b68672f9f 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -339,12 +339,8 @@ बाइंडिंग की संख्या प्रोफ़ाइल चुनें शैली चुनें - बटन लेबल मूल Glass - मूल - Xbox - PlayStation अंतर्निहित प्रोफ़ाइल - एक प्रति बनाई गई जिसे आप संपादित कर सकते हैं। प्रोफ़ाइल नाम कोई प्रोफ़ाइल चयनित नहीं diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e4a199276..0858beeeb 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1301,12 +1301,8 @@ Percorso installato: Emulatore Steam legacy Fa sì che il client Steam emulato risulti offline, così il gioco non tenterà connessioni online. Usa per i giochi che si bloccano durante il controllo di rete di Steam. Seleziona stile - Etichette pulsanti Originale Glass - Originale - Xbox - PlayStation Profilo integrato: è stata creata una copia modificabile. Ripristinare questo profilo al layout originale? Tutte le modifiche apportate andranno perse. Profilo ripristinato al layout originale. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 851fe22c0..e3a8caa24 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -361,12 +361,8 @@ バインドの数 プロファイルを選択 スタイルを選択 - ボタンラベル オリジナル Glass - オリジナル - Xbox - PlayStation 組み込みプロファイル — 編集可能なコピーを作成しました。 プロファイル名 プロファイルが選択されていません diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 642d49735..085b244b0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1302,12 +1302,8 @@ 레거시 Steam 에뮬레이터 에뮬레이트된 Steam 클라이언트가 오프라인으로 표시되게 하여 게임이 온라인 연결을 시도하지 않도록 합니다. Steam 네트워크 확인에서 멈추는 게임에 사용하세요. 스타일 선택 - 버튼 레이블 원본 Glass - 원본 - Xbox - PlayStation 기본 제공 프로필입니다. 편집할 수 있는 복사본을 만들었습니다. 이 프로필을 원래 레이아웃으로 초기화할까요? 변경 사항이 모두 손실됩니다. 프로필이 원래 레이아웃으로 초기화되었습니다. diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 11f41be1d..da508fa3f 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -361,12 +361,8 @@ Antall tilordninger Velg profil Velg stil - Knappeetiketter Original Glass - Original - Xbox - PlayStation Innebygd profil – opprettet en kopi du kan redigere. Profilnavn Ingen profil valgt diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1098113cf..7a21030b1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1307,12 +1307,8 @@ Zainstalowana ścieżka: Starszy emulator Steam Sprawia, że emulowany klient Steam zgłasza tryb offline, dzięki czemu gra nie próbuje połączeń online. Używaj dla gier, które zawieszają się na sprawdzaniu sieci Steam. Wybierz styl - Etykiety przycisków Oryginalny Glass - Oryginalny - Xbox - PlayStation Wbudowany profil — utworzono kopię, którą możesz edytować. Zresetować ten profil do oryginalnego układu? Wszystkie wprowadzone zmiany zostaną utracone. Profil zresetowany do oryginalnego układu. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index a81e2c8b8..522f0e5d4 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1301,12 +1301,8 @@ Caminho instalado: Emulador Steam legado Faz o cliente Steam emulado informar que está offline, para que o jogo não tente conexões online. Use em jogos que travam na verificação de rede do Steam. Selecionar estilo - Rótulos dos botões Original Glass - Original - Xbox - PlayStation Perfil integrado - uma cópia editável foi criada. Redefinir este perfil para o layout original? Todas as alterações feitas serão perdidas. Perfil redefinido para o layout original. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 31d08370d..6d952538d 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -361,12 +361,8 @@ Número de associações Selecionar perfil Selecionar estilo - Etiquetas dos botões Original Vidro - Original - Xbox - PlayStation Perfil incorporado — foi criada uma cópia que pode editar. Nome do perfil Nenhum perfil selecionado diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index a60b885a9..a4e7d19e8 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1301,12 +1301,8 @@ Cale instalata: Emulator Steam moștenit Face clientul Steam emulat să raporteze că este offline, astfel încât jocul să nu încerce conexiuni online. Folosește pentru jocurile care se blochează la verificarea rețelei Steam. Selectează stilul - Etichete butoane Original Glass - Original - Xbox - PlayStation Profil integrat - a fost creată o copie editabilă. Resetezi acest profil la aspectul original? Toate modificările făcute se vor pierde. Profil resetat la aspectul original. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1a89a3a99..05bebf378 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -351,12 +351,8 @@ Количество привязок Выберите профиль Выберите стиль - Метки кнопок Оригинал Glass - Оригинал - Xbox - PlayStation Встроенный профиль — созданную копию можно редактировать. Имя профиля Профиль не выбран diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 030633a8a..9f62aecf2 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -361,12 +361,8 @@ Antal bindningar Välj profil Välj stil - Knappetiketter Original Glas - Original - Xbox - PlayStation Inbyggd profil — en kopia som du kan redigera har skapats. Profilnamn Ingen profil vald diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index ad4dbef72..3bb6b8616 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -361,12 +361,8 @@ จำนวนการผูกปุ่ม เลือกโปรไฟล์ เลือกสไตล์ - ป้ายกำกับปุ่ม ต้นฉบับ กระจก - ต้นฉบับ - Xbox - PlayStation โปรไฟล์ในตัว — สร้างสำเนาที่คุณสามารถแก้ไขได้แล้ว ชื่อโปรไฟล์ ไม่ได้เลือกโปรไฟล์ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 53699ce6c..d3a0d96a0 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -361,12 +361,8 @@ Bağlama Sayısı Profil Seç Stil Seç - Düğme Etiketleri Orijinal Cam - Orijinal - Xbox - PlayStation Yerleşik profil — düzenleyebileceğiniz bir kopya oluşturuldu. Profil Adı Seçili profil yok diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 701a1fc3d..973fa58a6 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1307,12 +1307,8 @@ Застарілий емулятор Steam Змушує емульований клієнт Steam повідомляти, що він офлайн, щоб гра не намагалася підключатися до мережі. Використовуйте для ігор, які зависають на мережевій перевірці Steam. Виберіть стиль - Мітки кнопок Оригінальний Glass - Оригінальний - Xbox - PlayStation Вбудований профіль — створено копію, яку можна редагувати. Скинути цей профіль до початкового макета? Усі внесені зміни буде втрачено. Профіль скинуто до початкового макета. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 84c703039..2da9ed261 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1301,12 +1301,8 @@ 旧版 Steam 模拟器 让模拟的 Steam 客户端报告为离线,这样游戏就不会尝试在线连接。适用于在 Steam 网络检查时卡住的游戏。 选择样式 - 按钮标签 原版 Glass - 原版 - Xbox - PlayStation 内置配置文件 - 已创建可编辑副本。 将此配置文件重置为原始布局?你所做的任何更改都会丢失。 配置文件已重置为原始布局。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 0102b4c29..ba47e54da 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1301,12 +1301,8 @@ 舊版 Steam 模擬器 讓模擬的 Steam 用戶端回報為離線,讓遊戲不會嘗試線上連線。適用於在 Steam 網路檢查時卡住的遊戲。 選擇樣式 - 按鈕標籤 原版 Glass - 原版 - Xbox - PlayStation 內建設定檔 - 已建立可編輯副本。 將此設定檔重設為原始版面配置?你所做的任何變更都會遺失。 設定檔已重設為原始版面配置。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d55a29024..65ce749e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -362,12 +362,12 @@ Number of Bindings Select Profile Select Style - Button Labels Original Glass - Original - Xbox - PlayStation + Slate + Halo + Glint + Color Theme Built-in profile — created a copy you can edit. Profile Name No profile selected diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 520031343..524620828 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -126,7 +126,7 @@ import com.winlator.cmod.runtime.input.controls.GestureProfile; import com.winlator.cmod.runtime.input.controls.GestureProfileManager; import com.winlator.cmod.runtime.input.controls.InputControlsManager; -import com.winlator.cmod.runtime.input.controls.LabelTheme; +import com.winlator.cmod.runtime.input.controls.AccentTheme; import com.winlator.cmod.runtime.input.controls.VisualStyle; import com.winlator.cmod.shared.math.Mathf; import com.winlator.cmod.shared.math.XForm; @@ -3971,19 +3971,23 @@ private void renderDrawerMenu() { } ArrayList styleNames = new ArrayList<>(); - styleNames.add(getString(R.string.input_controls_style_original)); + styleNames.add(getString(R.string.input_controls_style_slate)); styleNames.add(getString(R.string.input_controls_style_gamehub)); + styleNames.add(getString(R.string.input_controls_style_halo)); + styleNames.add(getString(R.string.input_controls_style_glint)); + styleNames.add(getString(R.string.input_controls_style_original)); VisualStyle currentStyle = inputControlsView != null && inputControlsView.getVisualStyle() != null - ? inputControlsView.getVisualStyle() : VisualStyle.ORIGINAL; + ? inputControlsView.getVisualStyle() : VisualStyle.SLATE; int selectedStyleIndex = currentStyle.ordinal(); - ArrayList labelThemeNames = new ArrayList<>(); - labelThemeNames.add(getString(R.string.input_controls_label_theme_default)); - labelThemeNames.add(getString(R.string.input_controls_label_theme_xbox)); - labelThemeNames.add(getString(R.string.input_controls_label_theme_playstation)); - LabelTheme currentLabelTheme = inputControlsView != null && inputControlsView.getLabelTheme() != null - ? inputControlsView.getLabelTheme() : LabelTheme.DEFAULT; - int selectedLabelThemeIndex = currentLabelTheme.ordinal(); + ArrayList accentThemeNames = new ArrayList<>(); + int selectedAccentThemeIndex = 0; + if (activeProfile != null) { + accentThemeNames.addAll(Arrays.asList(AccentTheme.displayNames())); + AccentTheme currentAccent = activeProfile.getAccentTheme() != null + ? activeProfile.getAccentTheme() : AccentTheme.MONO; + selectedAccentThemeIndex = currentAccent.ordinal(); + } List gestureProfileNames = new ArrayList<>(); int gestureSelectedIndex = 0; @@ -4050,8 +4054,8 @@ private void renderDrawerMenu() { inputSelectedIndex, styleNames, selectedStyleIndex, - labelThemeNames, - selectedLabelThemeIndex, + accentThemeNames, + selectedAccentThemeIndex, preferences.getBoolean("show_touchscreen_controls_enabled", false), isTapToClickEnabled, preferences.getFloat("overlay_opacity", InputControlsView.DEFAULT_OVERLAY_OPACITY), @@ -4578,13 +4582,7 @@ public void onInputControlsProfileSelected(int index) { if (index - 1 < profiles.size()) { ControlsProfile profile = profiles.get(index - 1); showInputControls(profile); - - if (profile.id != InputControlsManager.LEGACY_XBOX_PROFILE_ID && - profile.id != InputControlsManager.LEGACY_PS_PROFILE_ID && - profile.id != InputControlsManager.GAMEHUB_LAYOUT_BUILTIN_ID) { - if (inputControlsView != null) inputControlsView.setLabelTheme(LabelTheme.DEFAULT); - preferences.edit().putString("input_label_theme", LabelTheme.DEFAULT.name()).apply(); - } } + } } renderDrawerMenu(); } @@ -4599,29 +4597,14 @@ public void onInputControlsStyleSelected(int index) { } @Override - public void onInputControlsLabelThemeSelected(int index) { - LabelTheme[] all = LabelTheme.values(); + public void onInputControlsAccentThemeSelected(int index) { + AccentTheme[] all = AccentTheme.values(); if (index < 0 || index >= all.length) return; - LabelTheme chosen = all[index]; - - ControlsProfile currentProfile = inputControlsView != null ? inputControlsView.getProfile() : null; - int currentId = currentProfile != null ? currentProfile.id : -1; - - if (currentId != InputControlsManager.GAMEHUB_LAYOUT_BUILTIN_ID) { - if (chosen == LabelTheme.XBOX) { - ControlsProfile p = inputControlsManager.getProfile(InputControlsManager.LEGACY_XBOX_PROFILE_ID); - if (p != null) showInputControls(p); - } else if (chosen == LabelTheme.PLAYSTATION) { - ControlsProfile p = inputControlsManager.getProfile(InputControlsManager.LEGACY_PS_PROFILE_ID); - if (p != null) showInputControls(p); - } else if (chosen == LabelTheme.DEFAULT) { - ControlsProfile p = inputControlsManager.getProfile(InputControlsManager.VIRTUAL_GAMEPAD_BUILTIN_ID); - if (p != null) showInputControls(p); - } - } - - if (inputControlsView != null) inputControlsView.setLabelTheme(chosen); - preferences.edit().putString("input_label_theme", chosen.name()).apply(); + ControlsProfile profile = inputControlsView != null ? inputControlsView.getProfile() : null; + if (profile == null) return; + profile.setAccentTheme(all[index]); + profile.save(); + inputControlsView.invalidate(); renderDrawerMenu(); } @@ -7613,29 +7596,16 @@ private int selectedGestureProfileId() { return def != null ? def.id : 0; } - // Hide legacy label-only profiles unless one is already active. private ArrayList getVisibleControlsProfiles() { - ArrayList all = inputControlsManager != null + return inputControlsManager != null ? inputControlsManager.getProfiles(true) : new ArrayList<>(); - ControlsProfile active = inputControlsView != null ? inputControlsView.getProfile() : null; - ArrayList visible = new ArrayList<>(all.size()); - for (ControlsProfile p : all) { - if (InputControlsManager.isLegacyLabelOnlyProfile(p) - && (active == null || active.id != p.id)) { - continue; - } - visible.add(p); - } - return visible; } private void applyInputVisualStylePreferences() { if (inputControlsView == null || preferences == null) return; inputControlsView.setVisualStyle( - VisualStyle.fromPreference(preferences.getString("input_visual_style", VisualStyle.ORIGINAL.name()))); - inputControlsView.setLabelTheme( - LabelTheme.fromPreference(preferences.getString("input_label_theme", LabelTheme.DEFAULT.name()))); + VisualStyle.fromPreference(preferences.getString("input_visual_style", VisualStyle.SLATE.name()))); } private ControlsProfile resolvePreferredStartupProfile() { diff --git a/app/src/main/runtime/display/XServerDrawerMenu.kt b/app/src/main/runtime/display/XServerDrawerMenu.kt index 1cee605dd..c4cb6acc8 100644 --- a/app/src/main/runtime/display/XServerDrawerMenu.kt +++ b/app/src/main/runtime/display/XServerDrawerMenu.kt @@ -633,8 +633,8 @@ data class XServerDrawerState( val inputControlsSelectedProfileIndex: Int = 0, val inputControlsStyleNames: List = emptyList(), val inputControlsSelectedStyleIndex: Int = 0, - val inputControlsLabelThemeNames: List = emptyList(), - val inputControlsSelectedLabelThemeIndex: Int = 0, + val inputControlsAccentThemeNames: List = emptyList(), + val inputControlsSelectedAccentThemeIndex: Int = 0, val inputControlsShowOverlay: Boolean = false, val inputControlsTapToClick: Boolean = true, val inputControlsOverlayOpacity: Float = 0.4f, @@ -1056,7 +1056,7 @@ interface XServerDrawerActionListener { fun onInputControlsStyleSelected(index: Int) - fun onInputControlsLabelThemeSelected(index: Int) + fun onInputControlsAccentThemeSelected(index: Int) fun onInputControlsShowOverlayChanged(enabled: Boolean) @@ -1164,8 +1164,8 @@ fun buildXServerDrawerState( inputControlsSelectedProfileIndex: Int = 0, inputControlsStyleNames: List = emptyList(), inputControlsSelectedStyleIndex: Int = 0, - inputControlsLabelThemeNames: List = emptyList(), - inputControlsSelectedLabelThemeIndex: Int = 0, + inputControlsAccentThemeNames: List = emptyList(), + inputControlsSelectedAccentThemeIndex: Int = 0, inputControlsShowOverlay: Boolean = false, inputControlsTapToClick: Boolean = true, inputControlsOverlayOpacity: Float = 0.4f, @@ -1362,8 +1362,8 @@ fun buildXServerDrawerState( inputControlsSelectedProfileIndex = inputControlsSelectedProfileIndex, inputControlsStyleNames = inputControlsStyleNames, inputControlsSelectedStyleIndex = inputControlsSelectedStyleIndex, - inputControlsLabelThemeNames = inputControlsLabelThemeNames, - inputControlsSelectedLabelThemeIndex = inputControlsSelectedLabelThemeIndex, + inputControlsAccentThemeNames = inputControlsAccentThemeNames, + inputControlsSelectedAccentThemeIndex = inputControlsSelectedAccentThemeIndex, inputControlsShowOverlay = inputControlsShowOverlay, inputControlsTapToClick = inputControlsTapToClick, inputControlsOverlayOpacity = inputControlsOverlayOpacity, @@ -2827,13 +2827,13 @@ private fun InputControlsPaneContent( } } - if (state.inputControlsLabelThemeNames.isNotEmpty()) { + if (state.inputControlsAccentThemeNames.isNotEmpty()) { Column(verticalArrangement = Arrangement.spacedBy((8f * paneScale).dp)) { - PaneSectionLabel(stringResource(R.string.input_controls_select_label_theme)) + PaneSectionLabel(stringResource(R.string.input_controls_accent_theme)) InputControlsSimpleDropdown( - options = state.inputControlsLabelThemeNames, - selectedIndex = state.inputControlsSelectedLabelThemeIndex, - onSelected = listener::onInputControlsLabelThemeSelected, + options = state.inputControlsAccentThemeNames, + selectedIndex = state.inputControlsSelectedAccentThemeIndex, + onSelected = listener::onInputControlsAccentThemeSelected, ) } } diff --git a/app/src/main/runtime/input/ControlsEditorActivity.java b/app/src/main/runtime/input/ControlsEditorActivity.java index bcd8eb906..e4106ef4e 100644 --- a/app/src/main/runtime/input/ControlsEditorActivity.java +++ b/app/src/main/runtime/input/ControlsEditorActivity.java @@ -2,7 +2,10 @@ import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; import android.os.Bundle; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -22,11 +25,14 @@ import android.widget.Spinner; import android.widget.TextView; import androidx.activity.OnBackPressedCallback; +import androidx.preference.PreferenceManager; import com.winlator.cmod.R; +import com.winlator.cmod.runtime.input.controls.AccentTheme; import com.winlator.cmod.runtime.input.controls.Binding; import com.winlator.cmod.runtime.input.controls.ControlElement; import com.winlator.cmod.runtime.input.controls.ControlsProfile; import com.winlator.cmod.runtime.input.controls.InputControlsManager; +import com.winlator.cmod.runtime.input.controls.VisualStyle; import com.winlator.cmod.runtime.input.ui.InputControlsView; import com.winlator.cmod.shared.android.AppUtils; import com.winlator.cmod.shared.ui.toast.WinToast; @@ -54,6 +60,10 @@ public void onCreate(Bundle bundle) { inputControlsView.setInputControlsManager(new InputControlsManager(this)); inputControlsView.setEditMode(true); inputControlsView.setOverlayOpacity(0.6f); + inputControlsView.setVisualStyle( + VisualStyle.fromPreference( + PreferenceManager.getDefaultSharedPreferences(this) + .getString("input_visual_style", null))); profile = InputControlsManager.loadProfile( @@ -68,6 +78,8 @@ public void onCreate(Bundle bundle) { container.findViewById(R.id.BTRemoveElement).setOnClickListener(this); container.findViewById(R.id.BTElementSettings).setOnClickListener(this); container.findViewById(R.id.BTColorPicker).setOnClickListener(this); + container.findViewById(R.id.BTAccentTheme).setOnClickListener(this); + container.findViewById(R.id.BTVisualStyle).setOnClickListener(this); setupToolbarDragging(); @@ -109,7 +121,58 @@ public void onClick(View v) { showColorPicker(v); } else WinToast.show(this, R.string.input_controls_editor_no_element_selected); break; + case R.id.BTAccentTheme: + showAccentThemePicker(v); + break; + case R.id.BTVisualStyle: + showVisualStylePicker(v); + break; + } + } + + private void showVisualStylePicker(View anchorView) { + LinearLayout view = new LinearLayout(this); + view.setOrientation(LinearLayout.VERTICAL); + int padding = (int) UnitUtils.dpToPx(12); + view.setPadding(padding, padding, padding, padding); + + TextView title = new TextView(this); + title.setText(R.string.input_controls_select_style); + title.setTextColor(getColor(R.color.settings_text_primary)); + title.setTextSize(14); + title.setTypeface(null, Typeface.BOLD); + title.setPadding(0, 0, 0, (int) UnitUtils.dpToPx(8)); + view.addView(title); + + VisualStyle current = inputControlsView.getVisualStyle(); + final PopupWindow[] popup = new PopupWindow[1]; + String[] names = VisualStyle.displayNames(); + VisualStyle[] styles = VisualStyle.values(); + int rowPadding = (int) UnitUtils.dpToPx(6); + for (int i = 0; i < styles.length; i++) { + final VisualStyle style = styles[i]; + boolean selected = style == current; + TextView label = new TextView(this); + label.setText(names[i]); + label.setTextSize(13); + label.setTextColor(getColor(selected ? R.color.settings_accent : R.color.settings_text_primary)); + label.setTypeface(null, selected ? Typeface.BOLD : Typeface.NORMAL); + label.setPadding(rowPadding, rowPadding, rowPadding, rowPadding); + label.setOnClickListener( + v -> { + androidx.preference.PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putString("input_visual_style", style.name()) + .apply(); + inputControlsView.setVisualStyle(style); + if (popup[0] != null) popup[0].dismiss(); + }); + view.addView(label); } + + popup[0] = AppUtils.showPopupWindow(anchorView, view, 220, 0); + popup[0].setFocusable(true); + popup[0].update(); } private void showColorPicker(View anchorView) { @@ -193,6 +256,66 @@ public View getView(int position, View convertView, ViewGroup parent) { popupWindow.dismiss(); }); } + + private void showAccentThemePicker(View anchorView) { + LinearLayout view = new LinearLayout(this); + view.setOrientation(LinearLayout.VERTICAL); + int padding = (int) UnitUtils.dpToPx(12); + view.setPadding(padding, padding, padding, padding); + + TextView title = new TextView(this); + title.setText(R.string.input_controls_accent_theme); + title.setTextColor(getColor(R.color.settings_text_primary)); + title.setTextSize(14); + title.setTypeface(null, Typeface.BOLD); + title.setPadding(0, 0, 0, (int) UnitUtils.dpToPx(8)); + view.addView(title); + + AccentTheme current = profile.getAccentTheme() != null ? profile.getAccentTheme() : AccentTheme.MONO; + final PopupWindow[] popup = new PopupWindow[1]; + String[] names = AccentTheme.displayNames(); + AccentTheme[] themes = AccentTheme.values(); + int swatchSize = (int) UnitUtils.dpToPx(14); + int rowPadding = (int) UnitUtils.dpToPx(6); + for (int i = 0; i < themes.length; i++) { + final AccentTheme theme = themes[i]; + LinearLayout row = new LinearLayout(this); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setPadding(rowPadding, rowPadding, rowPadding, rowPadding); + + GradientDrawable dot = new GradientDrawable(); + dot.setShape(GradientDrawable.OVAL); + dot.setColor(theme.accent); + View swatch = new View(this); + LinearLayout.LayoutParams swatchParams = new LinearLayout.LayoutParams(swatchSize, swatchSize); + swatchParams.rightMargin = (int) UnitUtils.dpToPx(10); + swatch.setLayoutParams(swatchParams); + swatch.setBackground(dot); + row.addView(swatch); + + boolean selected = theme == current; + TextView label = new TextView(this); + label.setText(names[i]); + label.setTextSize(13); + label.setTextColor(getColor(selected ? R.color.settings_accent : R.color.settings_text_primary)); + label.setTypeface(null, selected ? Typeface.BOLD : Typeface.NORMAL); + row.addView(label); + + row.setOnClickListener( + v -> { + profile.setAccentTheme(theme); + profile.save(); + inputControlsView.invalidate(); + if (popup[0] != null) popup[0].dismiss(); + }); + view.addView(row); + } + + popup[0] = AppUtils.showPopupWindow(anchorView, view, 220, 0); + popup[0].setFocusable(true); + popup[0].update(); + } + private void showControlElementSettings(View anchorView) { final ControlElement element = inputControlsView.getSelectedElement(); View view = LayoutInflater.from(this).inflate(R.layout.control_element_settings, null); diff --git a/app/src/main/runtime/input/controls/AccentTheme.java b/app/src/main/runtime/input/controls/AccentTheme.java new file mode 100644 index 000000000..e0daf728d --- /dev/null +++ b/app/src/main/runtime/input/controls/AccentTheme.java @@ -0,0 +1,39 @@ +package com.winlator.cmod.runtime.input.controls; + +/** + * Named accent presets for the themed control styles. The preset only supplies the default accent: + * a per-element custom color always wins, then this accent. + */ +public enum AccentTheme { + MONO(0xFFE8ECF2), + AZURE(0xFF4DA3FF), + CYAN(0xFF37E1DC), + MINT(0xFF4DE3B2), + LIME(0xFFB9F542), + GOLD(0xFFFFC14D), + EMBER(0xFFFF9950), + CRIMSON(0xFFFF4655), + ROSE(0xFFFF6FB5), + VIOLET(0xFFA78BFA); + + public final int accent; + + AccentTheme(int accent) { + this.accent = accent; + } + + public static AccentTheme fromPreference(String name) { + if (name == null) return MONO; + try { + return AccentTheme.valueOf(name); + } catch (IllegalArgumentException e) { + return MONO; + } + } + + public static String[] displayNames() { + return new String[] { + "Mono", "Azure", "Cyan", "Mint", "Lime", "Gold", "Ember", "Crimson", "Rose", "Violet" + }; + } +} diff --git a/app/src/main/runtime/input/controls/ControlElement.java b/app/src/main/runtime/input/controls/ControlElement.java index 3611277fd..fba7ad151 100644 --- a/app/src/main/runtime/input/controls/ControlElement.java +++ b/app/src/main/runtime/input/controls/ControlElement.java @@ -3,9 +3,14 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.CornerPathEffect; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.RectF; @@ -395,11 +400,8 @@ private String getDisplayText() { return text; } Binding binding = getBindingAt(0); - // LabelTheme override (Xbox/PS glyphs and shoulder labels) is applied only when the user - // hasn't typed a custom label themselves. - LabelTheme theme = inputControlsView.getLabelTheme(); - String themed = theme != null ? theme.labelFor(binding) : null; - if (themed != null) return themed; + String bumper = bumperTriggerLabel(binding); + if (bumper != null) return bumper; String text = binding.toString().replace("NUMPAD ", "NP").replace("BUTTON ", ""); if (text.length() > 7) { String[] parts = text.split(" "); @@ -410,22 +412,34 @@ private String getDisplayText() { } /** - * Returns the effective accent color for this element after applying (in order): per-element - * customColor (highest priority — user explicit), LabelTheme color for the primary binding, or - * {@code -1} if no override applies (caller should fall back to the overlay primary color). + * Returns the per-element customColor (user explicit override), or {@code -1} if unset (caller + * should fall back to the theme accent or the overlay primary color). */ private int resolveAccentColor() { - if (customColor != -1) return customColor; - LabelTheme theme = inputControlsView.getLabelTheme(); - if (theme != null) { - int c = theme.colorFor(inputControlsView, getBindingAt(0)); - if (c != 0) return c; + return customColor; + } + + /** Bumper/trigger labels follow the standard gamepad naming; null for every other binding. */ + private static String bumperTriggerLabel(Binding binding) { + if (binding == null) return null; + switch (binding) { + case GAMEPAD_BUTTON_L1: + return "LB"; + case GAMEPAD_BUTTON_R1: + return "RB"; + case GAMEPAD_BUTTON_L2: + return "LT"; + case GAMEPAD_BUTTON_R2: + return "RT"; + default: + return null; } - return -1; } private String getBindingShortText(int index) { Binding binding = getBindingAt(index); + String bumper = bumperTriggerLabel(binding); + if (bumper != null) return bumper; String text = binding.toString().replace("NUMPAD ", "NP").replace("BUTTON ", "").replace("KEY_", "").replace("GAMEPAD_", ""); if (text.length() > 6) { String[] parts = text.split("_"); @@ -465,11 +479,88 @@ private boolean isEngaged() { return currentPointerId != -1 || (toggleSwitch && selected); } + // Shared draw caches. Drawing happens only on the UI thread, so static temps are safe. + private static Shader bloomShader; + private static Shader edgeShadeShader; + private static final Matrix shaderMatrix = new Matrix(); + private static final RectF tempRect = new RectF(); + private PorterDuffColorFilter cachedAccentFilter; + private int cachedAccentFilterColor = 1; + private CornerPathEffect cachedCornerEffect; + private float cachedCornerRadius = -1f; + + private static Shader getBloomShader() { + if (bloomShader == null) { + bloomShader = + new RadialGradient( + 0f, 0f, 1f, + new int[] {0x8CFFFFFF, 0x3EFFFFFF, 0x00FFFFFF}, + new float[] {0f, 0.6f, 1f}, + Shader.TileMode.CLAMP); + } + return bloomShader; + } + + private static Shader getEdgeShadeShader() { + if (edgeShadeShader == null) { + edgeShadeShader = + new RadialGradient(0f, 0f, 1f, 0x00000000, 0xFF000000, Shader.TileMode.CLAMP); + } + return edgeShadeShader; + } + + private static void placeShader(Shader shader, float cx, float cy, float r) { + shaderMatrix.reset(); + shaderMatrix.postScale(r, r); + shaderMatrix.postTranslate(cx, cy); + shader.setLocalMatrix(shaderMatrix); + } + + private PorterDuffColorFilter accentFilter(int color) { + if (cachedAccentFilterColor != color) { + cachedAccentFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN); + cachedAccentFilterColor = color; + } + return cachedAccentFilter; + } + + private CornerPathEffect cornerEffect(float radius) { + if (cachedCornerEffect == null || cachedCornerRadius != radius) { + cachedCornerEffect = new CornerPathEffect(radius); + cachedCornerRadius = radius; + } + return cachedCornerEffect; + } + + private int resolveThemedAccent() { + int c = resolveAccentColor(); + return c != -1 ? c : inputControlsView.getAccentTheme().accent; + } + + private void beginBloom(Paint paint, float cx, float cy, float r, int accent, float alpha) { + Shader bloom = getBloomShader(); + placeShader(bloom, cx, cy, r); + paint.setStyle(Paint.Style.FILL); + paint.setShader(bloom); + paint.setColorFilter(accentFilter(accent)); + paint.setAlpha((int) (255 * alpha)); + } + + private static void endBloom(Paint paint) { + paint.setShader(null); + paint.setColorFilter(null); + } + public void draw(Canvas canvas) { - if (inputControlsView.getVisualStyle() == VisualStyle.GAMEHUB) { + VisualStyle style = inputControlsView.getVisualStyle(); + if (style == VisualStyle.GAMEHUB) { drawGameHub(canvas); return; } + if (style != VisualStyle.ORIGINAL) { + drawClean(canvas, style); + return; + } int snappingSize = inputControlsView.getSnappingSize(); Paint paint = inputControlsView.getPaint(); float effectiveOpacity = inputControlsView.isEditMode() ? Math.max(0.15f, opacity) : opacity; @@ -875,6 +966,541 @@ public void draw(Canvas canvas) { } } + private float cleanCornerRadius(Rect bb) { + switch (shape) { + case ROUND_RECT: + return bb.height() * 0.5f; + case SQUARE: + return Math.min(bb.width(), bb.height()) * 0.28f; + case RECT: + return bb.height() * 0.22f; + default: + return 0f; + } + } + + private void drawCleanBody(Canvas canvas, Paint paint, Rect bb, float inset) { + if (shape == Shape.CIRCLE) { + canvas.drawCircle(bb.exactCenterX(), bb.exactCenterY(), bb.width() * 0.5f - inset, paint); + } else { + float r = Math.max(2f, cleanCornerRadius(bb) - inset); + canvas.drawRoundRect( + bb.left + inset, bb.top + inset, bb.right - inset, bb.bottom - inset, r, r, paint); + } + } + + /** + * Themed visual styles (SLATE, HALO, GLINT) — flat graphite body with the accent color carried + * by the outline, indicators and labels. Geometry (positions, bounding boxes, stick thumb, + * radial menu paths) matches the original code; only the look differs. + */ + private void drawClean(Canvas canvas, VisualStyle style) { + int snappingSize = inputControlsView.getSnappingSize(); + Paint paint = inputControlsView.getPaint(); + float effectiveOpacity = inputControlsView.isEditMode() ? Math.max(0.15f, opacity) : opacity; + float overlayOpacity = inputControlsView.getOverlayOpacity(); + float dim = overlayOpacity <= 0.4f + ? 0.28f + (overlayOpacity - 0.1f) * (0.5f / 0.3f) + : 0.78f + (overlayOpacity - 0.4f) * (0.22f / 0.6f); + float a = Mathf.clamp(dim, 0f, 1f) * effectiveOpacity; + boolean engaged = isEngaged(); + Rect boundingBox = getBoundingBox(); + int accent = resolveThemedAccent(); + int bodyColor = Color.argb((int) (150 * a), 0x14, 0x18, 0x1F); + float hairline = Math.max(1.5f, snappingSize * 0.09f * scale); + boolean halo = style == VisualStyle.HALO; + boolean glint = style == VisualStyle.GLINT; + + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setStrokeCap(Paint.Cap.BUTT); + + int frameColor; + float frameWidth; + if (halo) { + frameColor = ColorUtils.setAlphaComponent(accent, (int) ((engaged ? 250 : 215) * a)); + frameWidth = hairline * (engaged ? 2.8f : 1.9f); + } else if (glint) { + frameColor = engaged + ? ColorUtils.setAlphaComponent(accent, (int) (245 * a)) + : Color.argb((int) (55 * a), 255, 255, 255); + frameWidth = hairline * (engaged ? 1.8f : 1f); + } else { + frameColor = ColorUtils.setAlphaComponent(accent, (int) ((engaged ? 240 : 150) * a)); + frameWidth = hairline * (engaged ? 1.6f : 1f); + } + if (selected && resolveAccentColor() == -1) { + frameColor = + ColorUtils.setAlphaComponent(inputControlsView.getSecondaryColor(), (int) (235 * a)); + } + float frameInset = halo ? hairline * 2.5f : 0f; + int labelColor = (halo || engaged) + ? Color.argb((int) (240 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (235 * a)); + ColorFilter iconTint = + (halo || engaged) ? inputControlsView.getColorFilter() : accentFilter(accent); + + switch (type) { + case BUTTON: { + float cx = boundingBox.centerX(); + float cy = boundingBox.centerY(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(bodyColor); + drawCleanBody(canvas, paint, boundingBox, 0f); + if (engaged) { + float bloomRadius = Math.min(boundingBox.width(), boundingBox.height()) * 0.52f; + beginBloom(paint, cx, cy, bloomRadius, accent, a * 0.9f); + drawCleanBody(canvas, paint, boundingBox, 0f); + endBloom(paint); + } + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(frameWidth); + paint.setColor(frameColor); + drawCleanBody(canvas, paint, boundingBox, frameInset); + + if (glint && !engaged) { + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(hairline * 1.9f); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (220 * a))); + if (shape == Shape.CIRCLE) { + tempRect.set( + boundingBox.left + hairline, + boundingBox.top + hairline, + boundingBox.right - hairline, + boundingBox.bottom - hairline); + canvas.drawArc(tempRect, 62, 56, false, paint); + } else { + float lineY = boundingBox.bottom - hairline * 2.2f; + float halfLength = boundingBox.width() * 0.21f; + canvas.drawLine(cx - halfLength, lineY, cx + halfLength, lineY, paint); + } + paint.setStrokeCap(Paint.Cap.BUTT); + } + + if (iconId > 0) { + paint.setColor(labelColor); + drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, true, iconTint); + } else { + String label = getDisplayText(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(labelColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, label, boundingBox.width() - hairline * 4), + snappingSize * 2 * scale)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, x, (y - ((paint.descent() + paint.ascent()) * 0.5f)), paint); + } + break; + } + case D_PAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float base = Math.min(boundingBox.width(), boundingBox.height()); + float half = base * 0.5f; + float arm = base * 0.17f; + path.reset(); + path.moveTo(cx - arm, cy - half); + path.lineTo(cx + arm, cy - half); + path.lineTo(cx + arm, cy - arm); + path.lineTo(cx + half, cy - arm); + path.lineTo(cx + half, cy + arm); + path.lineTo(cx + arm, cy + arm); + path.lineTo(cx + arm, cy + half); + path.lineTo(cx - arm, cy + half); + path.lineTo(cx - arm, cy + arm); + path.lineTo(cx - half, cy + arm); + path.lineTo(cx - half, cy - arm); + path.lineTo(cx - arm, cy - arm); + path.close(); + + paint.setPathEffect(cornerEffect(base * 0.09f)); + paint.setStyle(Paint.Style.FILL); + paint.setColor(bodyColor); + canvas.drawPath(path, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(frameWidth); + paint.setColor(frameColor); + canvas.drawPath(path, paint); + paint.setPathEffect(null); + + boolean hasStates = engaged && states.length >= 4; + for (int i = 0; i < 4; i++) { + int dx = i == 1 ? 1 : i == 3 ? -1 : 0; + int dy = i == 2 ? 1 : i == 0 ? -1 : 0; + boolean hot = hasStates && states[i]; + if (hot) { + float bx = cx + dx * half * 0.55f; + float by = cy + dy * half * 0.55f; + beginBloom(paint, bx, by, base * 0.24f, accent, a); + canvas.drawCircle(bx, by, base * 0.24f, paint); + endBloom(paint); + } + float ax = cx + dx * half * 0.62f; + float ay = cy + dy * half * 0.62f; + float t = base * 0.055f; + path.reset(); + if (i == 0) { + path.moveTo(ax, ay - t); + path.lineTo(ax + 0.85f * t, ay + 0.65f * t); + path.lineTo(ax - 0.85f * t, ay + 0.65f * t); + } else if (i == 1) { + path.moveTo(ax + t, ay); + path.lineTo(ax - 0.65f * t, ay + 0.85f * t); + path.lineTo(ax - 0.65f * t, ay - 0.85f * t); + } else if (i == 2) { + path.moveTo(ax, ay + t); + path.lineTo(ax - 0.85f * t, ay - 0.65f * t); + path.lineTo(ax + 0.85f * t, ay - 0.65f * t); + } else { + path.moveTo(ax - t, ay); + path.lineTo(ax + 0.65f * t, ay - 0.85f * t); + path.lineTo(ax + 0.65f * t, ay + 0.85f * t); + } + path.close(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot + ? ColorUtils.setAlphaComponent(accent, (int) (240 * a)) + : Color.argb((int) (190 * a), 255, 255, 255)); + canvas.drawPath(path, paint); + } + + if (glint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 1.9f); + paint.setStrokeCap(Paint.Cap.ROUND); + float tickInset = hairline * 1.6f; + float span = arm * 0.45f; + for (int i = 0; i < 4; i++) { + boolean hot = hasStates && states[i]; + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((hot ? 245 : 170) * a))); + if (i == 0) { + canvas.drawLine(cx - span, cy - half + tickInset, cx + span, cy - half + tickInset, paint); + } else if (i == 1) { + canvas.drawLine(cx + half - tickInset, cy - span, cx + half - tickInset, cy + span, paint); + } else if (i == 2) { + canvas.drawLine(cx - span, cy + half - tickInset, cx + span, cy + half - tickInset, paint); + } else { + canvas.drawLine(cx - half + tickInset, cy - span, cx - half + tickInset, cy + span, paint); + } + } + paint.setStrokeCap(Paint.Cap.BUTT); + } + break; + } + case STICK: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float r = boundingBox.height() * 0.5f; + + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.argb((int) (120 * a), 0x14, 0x18, 0x1F)); + canvas.drawCircle(cx, cy, r, paint); + + float thumbX = getCurrentPosition().x; + float thumbY = getCurrentPosition().y; + float thumbRadius = snappingSize * 3.5f * scale; + + if (engaged) { + beginBloom(paint, thumbX, thumbY, thumbRadius * 1.6f, accent, a * 0.8f); + canvas.drawCircle(thumbX, thumbY, thumbRadius * 1.6f, paint); + endBloom(paint); + } + + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(frameWidth); + paint.setColor(frameColor); + canvas.drawCircle(cx, cy, r - frameInset, paint); + + float guideWidth = Math.max(2f, snappingSize * 0.14f * scale); + paint.setStrokeWidth(guideWidth); + if (glint && !engaged) { + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (110 * a))); + float arcRadius = r - hairline; + tempRect.set(cx - arcRadius, cy - arcRadius, cx + arcRadius, cy + arcRadius); + canvas.drawArc(tempRect, 62, 56, false, paint); + paint.setStrokeCap(Paint.Cap.BUTT); + } else { + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((engaged ? 220 : 110) * a))); + canvas.drawCircle(cx, cy, r * 0.58f, paint); + } + + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.argb((int) (200 * a), 0x1C, 0x22, 0x2B)); + canvas.drawCircle(thumbX, thumbY, thumbRadius, paint); + if (glint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(Color.argb((int) (55 * a), 255, 255, 255)); + canvas.drawCircle(thumbX, thumbY, thumbRadius, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (240 * a))); + canvas.drawCircle(thumbX, thumbY, Math.max(2.5f, snappingSize * 0.3f), paint); + } else { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * (halo ? 2.2f : 1.4f)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((engaged ? 245 : 150) * a))); + canvas.drawCircle(thumbX, thumbY, thumbRadius, paint); + } + break; + } + case TRACKPAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float radius = boundingBox.height() * 0.18f; + paint.setStyle(Paint.Style.FILL); + paint.setColor(bodyColor); + canvas.drawRoundRect( + boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + radius, radius, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(frameWidth); + paint.setColor(frameColor); + if (halo) { + float in = frameInset; + float fr = Math.max(2f, radius - in); + canvas.drawRoundRect( + boundingBox.left + in, boundingBox.top + in, + boundingBox.right - in, boundingBox.bottom - in, + fr, fr, paint); + } else { + canvas.drawRoundRect( + boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + radius, radius, paint); + } + if (glint && !engaged) { + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(hairline * 1.9f); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (220 * a))); + float lineY = boundingBox.bottom - hairline * 2.2f; + float halfLength = boundingBox.width() * 0.175f; + canvas.drawLine(cx - halfLength, lineY, cx + halfLength, lineY, paint); + paint.setStrokeCap(Paint.Cap.BUTT); + } + + float s = Math.min(boundingBox.width(), boundingBox.height()) * 0.45f; + float glyphW = 0.42f * s; + float glyphH = 0.62f * s; + float glyphRadius = glyphW * 0.5f; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(2f, s * 0.035f)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((engaged ? 220 : 110) * a))); + canvas.drawRoundRect( + cx - glyphW * 0.5f, cy - glyphH * 0.5f, cx + glyphW * 0.5f, cy + glyphH * 0.5f, + glyphRadius, glyphRadius, paint); + canvas.drawLine(cx, cy - 0.30f * glyphH, cx, cy - 0.02f * glyphH, paint); + break; + } + case RANGE_BUTTON: { + Range range = getRange(); + float rr = (orientation == 0 ? boundingBox.height() : boundingBox.width()) * 0.45f; + float elementSize = scroller.getElementSize(); + float minTextSize = snappingSize * 2 * scale; + float scrollOffset = scroller.getScrollOffset(); + byte[] rangeIndex = scroller.getRangeIndex(); + + paint.setStyle(Paint.Style.FILL); + paint.setColor(bodyColor); + canvas.drawRoundRect( + boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + rr, rr, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(frameWidth); + paint.setColor(frameColor); + canvas.drawRoundRect( + boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + rr, rr, paint); + + canvas.save(); + path.reset(); + path.addRoundRect( + boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + rr, rr, Path.Direction.CW); + canvas.clipPath(path); + + int pressedIndex = + currentPointerId != -1 && !scroller.isScrolling() ? scroller.getBindingIndex() : -1; + int dividerColor = Color.argb((int) (45 * a), 255, 255, 255); + float dividerWidth = Math.max(1f, snappingSize * 0.06f); + int hotTextColor = Color.argb((int) (240 * a), 255, 255, 255); + int cellTextColor = ColorUtils.setAlphaComponent(accent, (int) (225 * a)); + + if (orientation == 0) { + float lineTop = boundingBox.top + boundingBox.height() * 0.25f; + float lineBottom = boundingBox.bottom - boundingBox.height() * 0.25f; + float startX = boundingBox.left - (scrollOffset % elementSize); + + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(dividerWidth); + paint.setColor(dividerColor); + if (startX > boundingBox.left && startX < boundingBox.right) + canvas.drawLine(startX, lineTop, startX, lineBottom, paint); + + if (startX < boundingBox.right && startX + elementSize > boundingBox.left) { + boolean hot = index == pressedIndex; + if (hot) { + drawCleanHotCell( + canvas, paint, style, accent, a, hairline, + startX, boundingBox.top, startX + elementSize, boundingBox.bottom); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : cellTextColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, elementSize - hairline * 4), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + startX + elementSize * 0.5f, + (y - ((paint.descent() + paint.ascent()) * 0.5f)), + paint); + } + startX += elementSize; + } + } else { + float lineLeft = boundingBox.left + boundingBox.width() * 0.25f; + float lineRight = boundingBox.right - boundingBox.width() * 0.25f; + float startY = boundingBox.top - (scrollOffset % elementSize); + + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(dividerWidth); + paint.setColor(dividerColor); + if (startY > boundingBox.top && startY < boundingBox.bottom) + canvas.drawLine(lineLeft, startY, lineRight, startY, paint); + + if (startY < boundingBox.bottom && startY + elementSize > boundingBox.top) { + boolean hot = i % range.max == pressedIndex; + if (hot) { + drawCleanHotCell( + canvas, paint, style, accent, a, hairline, + boundingBox.left, startY, boundingBox.right, startY + elementSize); + } + String cellText = getRangeTextForIndex(range, i % range.max); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : cellTextColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, boundingBox.width() - hairline * 4), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + x, + startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), + paint); + } + startY += elementSize; + } + } + canvas.restore(); + break; + } + case RADIAL_MENU: { + float cx = boundingBox.centerX(); + float cy = boundingBox.centerY(); + float radius = boundingBox.width() * 0.5f; + + if (radialMenuExpanded && bindings.length > 0 && radius > 0) { + float innerRadius = radius + snappingSize * 0.5f; + float outerRadius = boundingBox.width() + (snappingSize * scale); + float angleStep = 360.0f / bindings.length; + + if (paths == null || paths.length != bindings.length) { + paths = new Path[bindings.length]; + RectF outerRect = new RectF(cx - outerRadius, cy - outerRadius, cx + outerRadius, cy + outerRadius); + RectF innerRect = new RectF(cx - innerRadius, cy - innerRadius, cx + innerRadius, cy + innerRadius); + + for (int i = 0; i < bindings.length; i++) { + float startAngle = -90.0f + i * angleStep; + paths[i] = new Path(); + paths[i].arcTo(outerRect, startAngle, angleStep, true); + paths[i].arcTo(innerRect, startAngle + angleStep, -angleStep, false); + paths[i].close(); + } + } + + if (paths != null && paths.length == bindings.length) { + for (int i = 0; i < bindings.length; i++) { + boolean active = i == activeRadialBindingIndex; + paint.setStyle(Paint.Style.FILL); + paint.setColor( + active ? ColorUtils.setAlphaComponent(accent, (int) (80 * a)) : bodyColor); + canvas.drawPath(paths[i], paint); + + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(Color.argb((int) (50 * a), 255, 255, 255)); + canvas.drawPath(paths[i], paint); + + float middleAngle = (float) Math.toRadians(-90.0f + i * angleStep + angleStep * 0.5f); + float labelRadius = (innerRadius + outerRadius) * 0.5f; + float labelX = (float) (cx + Math.cos(middleAngle) * labelRadius); + float labelY = (float) (cy + Math.sin(middleAngle) * labelRadius); + + String label = getBindingShortText(i); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? Color.argb((int) (240 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (215 * a))); + paint.setTextSize(snappingSize * 1.2f * scale); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, labelX, labelY - ((paint.descent() + paint.ascent()) * 0.5f), paint); + } + } + } + + paint.setStyle(Paint.Style.FILL); + paint.setColor(bodyColor); + canvas.drawCircle(cx, cy, radius, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(frameWidth); + paint.setColor(frameColor); + canvas.drawCircle(cx, cy, radius - frameInset, paint); + + paint.setColor(labelColor); + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), + iconId > 0 ? iconId : 34, true, iconTint); + break; + } + } + paint.setStrokeJoin(Paint.Join.MITER); + paint.setStrokeCap(Paint.Cap.BUTT); + } + + private void drawCleanHotCell( + Canvas canvas, Paint paint, VisualStyle style, int accent, float a, float hairline, + float left, float top, float right, float bottom) { + if (style == VisualStyle.GLINT) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 2.5f); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (240 * a))); + float cx = (left + right) * 0.5f; + float halfBar = (right - left) * 0.275f; + float lineY = bottom - (bottom - top) * 0.12f; + canvas.drawLine(cx - halfBar, lineY, cx + halfBar, lineY, paint); + paint.setStrokeCap(Paint.Cap.BUTT); + return; + } + tempRect.set(left + 4, top + 5, right - 4, bottom - 5); + float r = tempRect.height() * 0.5f; + if (style == VisualStyle.HALO) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 1.6f); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (235 * a))); + } else { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (70 * a))); + } + canvas.drawRoundRect(tempRect, r, r, paint); + } + /** * GameHub visual style — dark translucent glass body, light white rim, brighter rim & inner glow * when pressed, soft outer shadow. Used when the user picks the "GameHub" style. @@ -890,7 +1516,9 @@ private void drawGameHub(Canvas canvas) { boolean engaged = isEngaged(); Rect boundingBox = getBoundingBox(); - int accent = resolveAccentColor(); + int custom = resolveAccentColor(); + AccentTheme theme = inputControlsView.getAccentTheme(); + int accent = custom != -1 ? custom : (theme != AccentTheme.MONO ? theme.accent : -1); boolean hasAccent = accent != -1; // Anchored at 40% default; steeper below, gentle to full above. @@ -903,6 +1531,9 @@ private void drawGameHub(Canvas canvas) { int pressedStrokeAlpha = (int) (220 * gameHubDim * effectiveOpacity); int textAlpha = (int) (255 * gameHubDim * effectiveOpacity); int glassEdgeAlpha = (int) (75 * gameHubDim * effectiveOpacity); + // The edge shade is modulated by the body fill's alpha so the vignette keeps its subtle weight. + int glassShadeAlpha = glassEdgeAlpha * fillAlpha / 255; + int pressedGlassShadeAlpha = glassEdgeAlpha * pressedFillAlpha / 255; int fillColor = Color.argb(fillAlpha, 0, 0, 0); int strokeColor = hasAccent @@ -947,14 +1578,16 @@ private void drawGameHub(Canvas canvas) { } drawGameHubGlassOnPath( canvas, paint, path, cx, cy, - Math.max(boundingBox.width(), boundingBox.height()) * 0.5f, glassEdgeAlpha); + Math.max(boundingBox.width(), boundingBox.height()) * 0.5f, + engaged ? pressedGlassShadeAlpha : glassShadeAlpha); paint.setStyle(Paint.Style.STROKE); paint.setColor(engaged ? pressedStrokeColor : strokeColor); canvas.drawPath(path, paint); } else { drawGameHubShape(canvas, paint, boundingBox, fillColor, true); if (engaged) drawGameHubShape(canvas, paint, boundingBox, pressedFillColor, true); - drawGameHubGlassShape(canvas, paint, boundingBox, glassEdgeAlpha); + drawGameHubGlassShape( + canvas, paint, boundingBox, engaged ? pressedGlassShadeAlpha : glassShadeAlpha); paint.setStyle(Paint.Style.STROKE); paint.setColor(engaged ? pressedStrokeColor : strokeColor); drawGameHubShape(canvas, paint, boundingBox, 0, false); @@ -990,12 +1623,11 @@ private void drawGameHub(Canvas canvas) { paint.setColor(ringFill); canvas.drawCircle(cx, cy, ringRadius, paint); - if (glassEdgeAlpha > 0) { - paint.setShader(new RadialGradient( - cx, cy, ringRadius, - Color.argb(0, 0, 0, 0), Color.argb(glassEdgeAlpha, 0, 0, 0), - Shader.TileMode.CLAMP)); + if (glassShadeAlpha > 0) { + placeShader(getEdgeShadeShader(), cx, cy, ringRadius); + paint.setShader(getEdgeShadeShader()); paint.setStyle(Paint.Style.FILL); + paint.setAlpha(glassShadeAlpha); canvas.drawCircle(cx, cy, ringRadius, paint); paint.setShader(null); } @@ -1039,7 +1671,8 @@ private void drawGameHub(Canvas canvas) { if (glassEdgeAlpha > 0) { GameHubLayout.dpadArrowCenter(side, cx, cy, radius, arrowCenter); drawGameHubGlassOnPath( - canvas, paint, path, arrowCenter[0], arrowCenter[1], arrowGradR, glassEdgeAlpha); + canvas, paint, path, arrowCenter[0], arrowCenter[1], arrowGradR, + engaged ? pressedGlassShadeAlpha : glassShadeAlpha); } } GameHubLayout.buildDpadArrows(path, cx, cy, radius); @@ -1093,7 +1726,9 @@ private void drawGameHub(Canvas canvas) { paint.setColor(isSegmentEngaged ? pressedFillColor : fillColor); canvas.drawPath(paths[i], paint); - drawGameHubGlassOnPath(canvas, paint, paths[i], cx, cy, outerRadius, glassEdgeAlpha); + drawGameHubGlassOnPath( + canvas, paint, paths[i], cx, cy, outerRadius, + isSegmentEngaged ? pressedGlassShadeAlpha : glassShadeAlpha); paint.setStyle(Paint.Style.STROKE); paint.setColor(isSegmentEngaged ? pressedStrokeColor : strokeColor); @@ -1116,7 +1751,8 @@ private void drawGameHub(Canvas canvas) { drawGameHubShape(canvas, paint, boundingBox, fillColor, true); if (engaged) drawGameHubShape(canvas, paint, boundingBox, pressedFillColor, true); - drawGameHubGlassShape(canvas, paint, boundingBox, glassEdgeAlpha); + drawGameHubGlassShape( + canvas, paint, boundingBox, engaged ? pressedGlassShadeAlpha : glassShadeAlpha); paint.setStyle(Paint.Style.STROKE); paint.setColor(engaged ? pressedStrokeColor : strokeColor); drawGameHubShape(canvas, paint, boundingBox, 0, false); @@ -1139,7 +1775,9 @@ private void drawGameHub(Canvas canvas) { drawGameHubShape(canvas, paint, boundingBox, fillColor, true, Shape.ROUND_RECT); if (engaged) drawGameHubShape(canvas, paint, boundingBox, pressedFillColor, true, Shape.ROUND_RECT); - drawGameHubGlassShape(canvas, paint, boundingBox, glassEdgeAlpha, Shape.ROUND_RECT); + drawGameHubGlassShape( + canvas, paint, boundingBox, + engaged ? pressedGlassShadeAlpha : glassShadeAlpha, Shape.ROUND_RECT); paint.setStyle(Paint.Style.STROKE); paint.setColor(engaged ? pressedStrokeColor : strokeColor); drawGameHubShape(canvas, paint, boundingBox, 0, false, Shape.ROUND_RECT); @@ -1199,6 +1837,8 @@ private void drawGameHub(Canvas canvas) { drawOriginalLegacy(canvas); break; } + paint.setStrokeJoin(Paint.Join.MITER); + paint.setStrokeCap(Paint.Cap.BUTT); } private void drawGameHubShape(Canvas canvas, Paint paint, Rect bb, int color, boolean fill) { @@ -1240,11 +1880,10 @@ private void drawGameHubGlassShape(Canvas canvas, Paint paint, Rect bb, int edge float cx = bb.exactCenterX(); float cy = bb.exactCenterY(); float gradR = Math.max(bb.width(), bb.height()) * 0.5f; - paint.setShader(new RadialGradient( - cx, cy, gradR, - Color.argb(0, 0, 0, 0), Color.argb(edgeAlpha, 0, 0, 0), - Shader.TileMode.CLAMP)); + placeShader(getEdgeShadeShader(), cx, cy, gradR); + paint.setShader(getEdgeShadeShader()); paint.setStyle(Paint.Style.FILL); + paint.setAlpha(edgeAlpha); int snappingSize = inputControlsView.getSnappingSize(); switch (overrideShape) { case CIRCLE: @@ -1270,11 +1909,10 @@ private void drawGameHubGlassShape(Canvas canvas, Paint paint, Rect bb, int edge private void drawGameHubGlassOnPath( Canvas canvas, Paint paint, Path path, float cx, float cy, float gradR, int edgeAlpha) { if (edgeAlpha <= 0 || gradR <= 0) return; - paint.setShader(new RadialGradient( - cx, cy, gradR, - Color.argb(0, 0, 0, 0), Color.argb(edgeAlpha, 0, 0, 0), - Shader.TileMode.CLAMP)); + placeShader(getEdgeShadeShader(), cx, cy, gradR); + paint.setShader(getEdgeShadeShader()); paint.setStyle(Paint.Style.FILL); + paint.setAlpha(edgeAlpha); canvas.drawPath(path, paint); paint.setShader(null); } @@ -1294,26 +1932,32 @@ private void drawOriginalLegacy(Canvas canvas) { } } + private static final Rect iconSrcRect = new Rect(); + private static final Rect iconDstRect = new Rect(); + private void drawIcon(Canvas canvas, float cx, float cy, float width, float height, int iconId) { drawIcon(canvas, cx, cy, width, height, iconId, true); } private void drawIcon(Canvas canvas, float cx, float cy, float width, float height, int iconId, boolean automargin) { + drawIcon(canvas, cx, cy, width, height, iconId, automargin, inputControlsView.getColorFilter()); + } + + private void drawIcon(Canvas canvas, float cx, float cy, float width, float height, int iconId, boolean automargin, ColorFilter tint) { Bitmap icon = inputControlsView.getIcon((byte) iconId); if (icon == null) return; Paint paint = inputControlsView.getPaint(); - paint.setColorFilter(inputControlsView.getColorFilter()); + paint.setColorFilter(tint); int margin = automargin ? (int) (inputControlsSize() * (shape == Shape.CIRCLE || shape == Shape.SQUARE ? 2.0f : 1.0f) * scale) : 0; int halfSize = (int) ((Math.min(width, height) - margin) * 0.5f); - Rect srcRect = new Rect(0, 0, icon.getWidth(), icon.getHeight()); - Rect dstRect = - new Rect( - (int) (cx - halfSize), - (int) (cy - halfSize), - (int) (cx + halfSize), - (int) (cy + halfSize)); - canvas.drawBitmap(icon, srcRect, dstRect, paint); + iconSrcRect.set(0, 0, icon.getWidth(), icon.getHeight()); + iconDstRect.set( + (int) (cx - halfSize), + (int) (cy - halfSize), + (int) (cx + halfSize), + (int) (cy + halfSize)); + canvas.drawBitmap(icon, iconSrcRect, iconDstRect, paint); paint.setColorFilter(null); } diff --git a/app/src/main/runtime/input/controls/ControlsProfile.java b/app/src/main/runtime/input/controls/ControlsProfile.java index 1e4a0146a..5db68bf03 100644 --- a/app/src/main/runtime/input/controls/ControlsProfile.java +++ b/app/src/main/runtime/input/controls/ControlsProfile.java @@ -17,6 +17,7 @@ public class ControlsProfile implements Comparable { public final int id; private String name; private float cursorSpeed = 1.0f; + private AccentTheme accentTheme; private volatile List elements = new ArrayList<>(); private final ArrayList controllers = new ArrayList<>(); private boolean elementsLoaded = false; @@ -46,6 +47,14 @@ public void setCursorSpeed(float cursorSpeed) { this.cursorSpeed = cursorSpeed; } + public AccentTheme getAccentTheme() { + return accentTheme; + } + + public void setAccentTheme(AccentTheme accentTheme) { + this.accentTheme = accentTheme; + } + public boolean isVirtualGamepad() { if (!elementsLoaded && !virtualGamepad) { File file = getProfileFile(context, id); @@ -161,6 +170,7 @@ public void save() { data.put("id", id); data.put("name", name); data.put("cursorSpeed", Float.valueOf(cursorSpeed)); + if (accentTheme != null) data.put("accentTheme", accentTheme.name()); JSONArray elementsJSONArray = new JSONArray(); if (!elementsLoaded && file.isFile()) { @@ -205,16 +215,6 @@ public List getElements() { return Collections.unmodifiableList(elements); } - public int findColorForBinding(Binding binding) { - if (binding == null || binding == Binding.NONE) return -1; - for (ControlElement element : elements) { - for (Binding b : element.getBindings()) { - if (b == binding) return element.getCustomColor(); - } - } - return -1; - } - public int getElementCountFromFile() { File file = getProfileFile(context, id); if (file.isFile()) { diff --git a/app/src/main/runtime/input/controls/InputControlsManager.java b/app/src/main/runtime/input/controls/InputControlsManager.java index 7e59dfbdd..8d5144fb6 100644 --- a/app/src/main/runtime/input/controls/InputControlsManager.java +++ b/app/src/main/runtime/input/controls/InputControlsManager.java @@ -25,12 +25,12 @@ import org.json.JSONObject; public class InputControlsManager { - private static final int ASSET_PROFILE_SYNC_REVISION = 6; + private static final int ASSET_PROFILE_SYNC_REVISION = 7; public static final int LAST_BUILTIN_PROFILE_ID = 8; public static final int VIRTUAL_GAMEPAD_BUILTIN_ID = 3; public static final int GAMEHUB_LAYOUT_BUILTIN_ID = 7; - public static final int LEGACY_PS_PROFILE_ID = 4; - public static final int LEGACY_XBOX_PROFILE_ID = 5; + // Retired bundled layout ids whose installed copies are removed on sync. + private static final int[] RETIRED_PROFILE_IDS = {4, 5}; private final Context context; private ArrayList profiles; @@ -41,11 +41,6 @@ public static boolean isBuiltinProfile(ControlsProfile profile) { return profile != null && profile.id <= LAST_BUILTIN_PROFILE_ID; } - public static boolean isLegacyLabelOnlyProfile(ControlsProfile profile) { - if (profile == null) return false; - return profile.id == LEGACY_PS_PROFILE_ID || profile.id == LEGACY_XBOX_PROFILE_ID; - } - public InputControlsManager(Context context) { this.context = context; } @@ -124,6 +119,11 @@ private void copyAssetProfilesIfNeeded() { .putInt("inputcontrols_asset_sync_revision", ASSET_PROFILE_SYNC_REVISION) .apply(); + for (int id : RETIRED_PROFILE_IDS) { + ControlsProfile.getProfileFile(context, id).delete(); + getBackupFile(context, id).delete(); + } + try { AssetManager assetManager = context.getAssets(); String[] assetFiles = assetManager.list("inputcontrols/profiles"); @@ -312,6 +312,7 @@ public static ControlsProfile loadProfile(Context context, InputStream inStream) int profileId = 0; String profileName = null; float cursorSpeed = Float.NaN; + String accentTheme = null; int fieldsRead = 0; reader.beginObject(); @@ -327,6 +328,8 @@ public static ControlsProfile loadProfile(Context context, InputStream inStream) } else if (name.equals("cursorSpeed")) { cursorSpeed = (float) reader.nextDouble(); fieldsRead++; + } else if (name.equals("accentTheme")) { + accentTheme = reader.nextString(); } else { if (fieldsRead == 3) break; reader.skipValue(); @@ -337,6 +340,12 @@ public static ControlsProfile loadProfile(Context context, InputStream inStream) if (profileName != null) { profile.setName(profileName); profile.setCursorSpeed(cursorSpeed); + if (accentTheme != null) { + try { + profile.setAccentTheme(AccentTheme.valueOf(accentTheme)); + } catch (IllegalArgumentException ignored) { + } + } reader.close(); return profile; } diff --git a/app/src/main/runtime/input/controls/LabelTheme.java b/app/src/main/runtime/input/controls/LabelTheme.java deleted file mode 100644 index 73404a9e4..000000000 --- a/app/src/main/runtime/input/controls/LabelTheme.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.winlator.cmod.runtime.input.controls; - -import android.graphics.Color; -import com.winlator.cmod.runtime.input.ui.InputControlsView; - -public enum LabelTheme { - DEFAULT, - XBOX, - PLAYSTATION; - - public static LabelTheme fromPreference(String name) { - if (name == null) return DEFAULT; - try { - return LabelTheme.valueOf(name); - } catch (IllegalArgumentException e) { - return DEFAULT; - } - } - - public static String[] displayNames() { - return new String[] {"Original", "Xbox", "PlayStation"}; - } - - public int colorFor(InputControlsView view, Binding binding) { - if (this == DEFAULT || binding == null || view == null) return 0; - InputControlsManager manager = view.getInputControlsManager(); - if (manager == null) return 0; - - int profileId = -1; - if (this == XBOX) { - profileId = InputControlsManager.LEGACY_XBOX_PROFILE_ID; - } else if (this == PLAYSTATION) { - profileId = InputControlsManager.LEGACY_PS_PROFILE_ID; - } - - if (profileId != -1) { - ControlsProfile p = manager.getProfile(profileId); - if (p != null) { - if (!p.isElementsLoaded()) p.loadElements(view); - int color = p.findColorForBinding(binding); - if (color != -1) return color; - } - } - - switch (this) { - case XBOX: - switch (binding) { - case GAMEPAD_BUTTON_A: - return 0xFF22A348; // Xbox green - case GAMEPAD_BUTTON_B: - return 0xFFE13C3C; // Xbox red - case GAMEPAD_BUTTON_X: - return 0xFF1976D2; // Xbox blue - case GAMEPAD_BUTTON_Y: - return 0xFFE9B007; // Xbox yellow - default: - return 0; - } - case PLAYSTATION: - switch (binding) { - case GAMEPAD_BUTTON_A: - return 0xFF5C8DEC; // Cross — blue - case GAMEPAD_BUTTON_B: - return 0xFFE34A4A; // Circle — red - case GAMEPAD_BUTTON_X: - return 0xFFD66ED1; // Square — pink/magenta - case GAMEPAD_BUTTON_Y: - return 0xFF34BFA0; // Triangle — teal - default: - return 0; - } - default: - return 0; - } - } - - public String labelFor(Binding binding) { - if (this == DEFAULT || binding == null) return null; - switch (this) { - case XBOX: - switch (binding) { - case GAMEPAD_BUTTON_A: - return "A"; - case GAMEPAD_BUTTON_B: - return "B"; - case GAMEPAD_BUTTON_X: - return "X"; - case GAMEPAD_BUTTON_Y: - return "Y"; - case GAMEPAD_BUTTON_L1: - return "LB"; - case GAMEPAD_BUTTON_R1: - return "RB"; - case GAMEPAD_BUTTON_L2: - return "LT"; - case GAMEPAD_BUTTON_R2: - return "RT"; - case GAMEPAD_BUTTON_L3: - return "L3"; - case GAMEPAD_BUTTON_R3: - return "R3"; - case GAMEPAD_BUTTON_START: - return "≡"; - case GAMEPAD_BUTTON_SELECT: - return "❐"; - default: - return null; - } - case PLAYSTATION: - switch (binding) { - case GAMEPAD_BUTTON_A: - return "✕"; - case GAMEPAD_BUTTON_B: - return "○"; - case GAMEPAD_BUTTON_X: - return "▢"; - case GAMEPAD_BUTTON_Y: - return "△"; - case GAMEPAD_BUTTON_L1: - return "L1"; - case GAMEPAD_BUTTON_R1: - return "R1"; - case GAMEPAD_BUTTON_L2: - return "L2"; - case GAMEPAD_BUTTON_R2: - return "R2"; - case GAMEPAD_BUTTON_L3: - return "L3"; - case GAMEPAD_BUTTON_R3: - return "R3"; - case GAMEPAD_BUTTON_START: - return "≡"; - case GAMEPAD_BUTTON_SELECT: - return "❐"; - default: - return null; - } - default: - return null; - } - } - - public boolean overridesColor(InputControlsView view, Binding binding) { - return colorFor(view, binding) != 0; - } - - public static int safeColor(int themedColor, int fallback) { - return themedColor != 0 ? themedColor : fallback; - } -} diff --git a/app/src/main/runtime/input/controls/RangeScroller.java b/app/src/main/runtime/input/controls/RangeScroller.java index dd992e36f..7cef17463 100644 --- a/app/src/main/runtime/input/controls/RangeScroller.java +++ b/app/src/main/runtime/input/controls/RangeScroller.java @@ -14,6 +14,7 @@ public class RangeScroller { private float lastPosition; private long touchTime; private Binding binding = Binding.NONE; + private int bindingIndex = -1; private boolean isActionDown = false; private boolean scrolling = false; private Timer timer; @@ -44,16 +45,15 @@ public byte[] getRangeIndex() { return new byte[] {from, to}; } - private Binding getBindingByPosition(float x, float y) { - Rect boundingBox = element.getBoundingBox(); - ControlElement.Range range = element.getRange(); - float offset = - element.getOrientation() == 0 - ? x - boundingBox.left - currentOffset - : y - boundingBox.top - currentOffset; - int index = (int) Math.floor((offset / getElementSize()) % range.max); - if (index < 0) index = range.max + index; + public int getBindingIndex() { + return bindingIndex; + } + + public boolean isScrolling() { + return scrolling; + } + private static Binding bindingForIndex(ControlElement.Range range, int index) { switch (range) { case FROM_A_TO_Z: return Binding.valueOf("KEY_" + ((char) (65 + index))); @@ -68,6 +68,19 @@ private Binding getBindingByPosition(float x, float y) { } } + private Binding getBindingByPosition(float x, float y) { + Rect boundingBox = element.getBoundingBox(); + ControlElement.Range range = element.getRange(); + float offset = + element.getOrientation() == 0 + ? x - boundingBox.left - currentOffset + : y - boundingBox.top - currentOffset; + int index = (int) Math.floor((offset / getElementSize()) % range.max); + if (index < 0) index = range.max + index; + bindingIndex = index; + return bindingForIndex(range, index); + } + private boolean isTap() { return (System.currentTimeMillis() - touchTime) < TouchpadView.MAX_TAP_MILLISECONDS; } diff --git a/app/src/main/runtime/input/controls/VisualStyle.java b/app/src/main/runtime/input/controls/VisualStyle.java index 0c226297d..91ab0b244 100644 --- a/app/src/main/runtime/input/controls/VisualStyle.java +++ b/app/src/main/runtime/input/controls/VisualStyle.java @@ -1,28 +1,34 @@ package com.winlator.cmod.runtime.input.controls; /** - * Visual rendering style for on-screen virtual gamepad elements. + * Visual rendering style for on-screen virtual gamepad elements. Enum order is the picker order. * - *

ORIGINAL preserves the historic Winlator look (translucent white strokes, lightly filled when - * engaged). GAMEHUB mimics the dark glass aesthetic used by the GameHub launcher: dark translucent - * fill, light white rim, brighter rim and inner fill when pressed, with a soft drop shadow. + *

SLATE (default) is a flat graphite surface with an accent hairline outline and a bloom glow + * when pressed. GAMEHUB ("Glass") is a dark translucent glass: light rim, brighter rim and inner + * fill when pressed, soft drop shadow. HALO keeps a quiet base and lights an inset accent ring on + * press. GLINT is a neutral frame with a small accent indicator that sweeps into a full ring while + * held. ORIGINAL preserves the historic look (translucent white strokes, lightly filled when + * engaged) and is listed last. * *

The actual drawing branches on this enum inside {@link ControlElement#draw}. */ public enum VisualStyle { - ORIGINAL, - GAMEHUB; + SLATE, + GAMEHUB, + HALO, + GLINT, + ORIGINAL; public static VisualStyle fromPreference(String name) { - if (name == null) return ORIGINAL; + if (name == null) return SLATE; try { return VisualStyle.valueOf(name); } catch (IllegalArgumentException e) { - return ORIGINAL; + return SLATE; } } public static String[] displayNames() { - return new String[] {"Original", "GameHub"}; + return new String[] {"Slate", "Glass", "Halo", "Glint", "Original"}; } } diff --git a/app/src/main/runtime/input/ui/InputControlsView.java b/app/src/main/runtime/input/ui/InputControlsView.java index 89b0473ba..55a2cf046 100644 --- a/app/src/main/runtime/input/ui/InputControlsView.java +++ b/app/src/main/runtime/input/ui/InputControlsView.java @@ -36,6 +36,7 @@ import com.winlator.cmod.runtime.display.winhandler.WinHandler; import com.winlator.cmod.runtime.display.xserver.Pointer; import com.winlator.cmod.runtime.display.xserver.XServer; +import com.winlator.cmod.runtime.input.controls.AccentTheme; import com.winlator.cmod.runtime.input.controls.Binding; import com.winlator.cmod.runtime.input.controls.ControlElement; import com.winlator.cmod.runtime.input.controls.ControlsProfile; @@ -43,7 +44,6 @@ import com.winlator.cmod.runtime.input.controls.ExternalControllerBinding; import com.winlator.cmod.runtime.input.controls.GamepadState; import com.winlator.cmod.runtime.input.controls.InputControlsManager; -import com.winlator.cmod.runtime.input.controls.LabelTheme; import com.winlator.cmod.runtime.input.controls.VisualStyle; import com.winlator.cmod.shared.math.Mathf; import java.io.IOException; @@ -75,8 +75,7 @@ public class InputControlsView extends View { private volatile float mouseMoveOffsetX = 0f; private volatile float mouseMoveOffsetY = 0f; private boolean showTouchscreenControls = false; - private VisualStyle visualStyle = VisualStyle.ORIGINAL; - private LabelTheme labelTheme = LabelTheme.DEFAULT; + private VisualStyle visualStyle = VisualStyle.SLATE; private InputControlsManager inputControlsManager; private Handler timeoutHandler; // Reference to the activity's timeout handler @@ -187,29 +186,26 @@ public VisualStyle getVisualStyle() { } public void setVisualStyle(VisualStyle style) { - this.visualStyle = style != null ? style : VisualStyle.ORIGINAL; + this.visualStyle = style != null ? style : VisualStyle.SLATE; invalidate(); } /** Same as {@link #setVisualStyle} but without {@link #invalidate()}, for internal draw-time * fallbacks where requesting another redraw would loop. */ public void setVisualStyleSilent(VisualStyle style) { - this.visualStyle = style != null ? style : VisualStyle.ORIGINAL; + this.visualStyle = style != null ? style : VisualStyle.SLATE; } - public LabelTheme getLabelTheme() { - return labelTheme; + public AccentTheme getAccentTheme() { + ControlsProfile p = profile; + AccentTheme t = p != null ? p.getAccentTheme() : null; + return t != null ? t : AccentTheme.MONO; } public InputControlsManager getInputControlsManager() { return inputControlsManager; } - public void setLabelTheme(LabelTheme theme) { - this.labelTheme = theme != null ? theme : LabelTheme.DEFAULT; - invalidate(); - } - public int getSnappingSize() { return snappingSize; } From 760a39687c46f976d63e809902c5b013b4d6ce58 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Mon, 13 Jul 2026 21:48:40 +0000 Subject: [PATCH 2/8] Editor theme list scrolls; Original style accepts accent themes - Theme picker rows wrapped in a capped ScrollView so all presets stay reachable - Original style tints with the selected accent theme; Mono keeps the classic look --- .../main/runtime/input/ControlsEditorActivity.java | 14 +++++++++++++- .../runtime/input/controls/ControlElement.java | 4 ++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/runtime/input/ControlsEditorActivity.java b/app/src/main/runtime/input/ControlsEditorActivity.java index e4106ef4e..41453fbf2 100644 --- a/app/src/main/runtime/input/ControlsEditorActivity.java +++ b/app/src/main/runtime/input/ControlsEditorActivity.java @@ -21,6 +21,7 @@ import android.widget.LinearLayout; import android.widget.PopupWindow; import android.widget.RadioGroup; +import android.widget.ScrollView; import android.widget.SeekBar; import android.widget.Spinner; import android.widget.TextView; @@ -277,6 +278,8 @@ private void showAccentThemePicker(View anchorView) { AccentTheme[] themes = AccentTheme.values(); int swatchSize = (int) UnitUtils.dpToPx(14); int rowPadding = (int) UnitUtils.dpToPx(6); + LinearLayout rows = new LinearLayout(this); + rows.setOrientation(LinearLayout.VERTICAL); for (int i = 0; i < themes.length; i++) { final AccentTheme theme = themes[i]; LinearLayout row = new LinearLayout(this); @@ -308,9 +311,18 @@ private void showAccentThemePicker(View anchorView) { inputControlsView.invalidate(); if (popup[0] != null) popup[0].dismiss(); }); - view.addView(row); + rows.addView(row); } + ScrollView scroller = new ScrollView(this); + scroller.setVerticalScrollBarEnabled(true); + scroller.addView(rows); + int maxListHeight = + (int) Math.min(UnitUtils.dpToPx(250), getResources().getDisplayMetrics().heightPixels * 0.5f); + view.addView( + scroller, + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, maxListHeight)); + popup[0] = AppUtils.showPopupWindow(anchorView, view, 220, 0); popup[0].setFocusable(true); popup[0].update(); diff --git a/app/src/main/runtime/input/controls/ControlElement.java b/app/src/main/runtime/input/controls/ControlElement.java index fba7ad151..3a0a443ad 100644 --- a/app/src/main/runtime/input/controls/ControlElement.java +++ b/app/src/main/runtime/input/controls/ControlElement.java @@ -565,6 +565,10 @@ public void draw(Canvas canvas) { Paint paint = inputControlsView.getPaint(); float effectiveOpacity = inputControlsView.isEditMode() ? Math.max(0.15f, opacity) : opacity; int accent = resolveAccentColor(); + if (accent == -1) { + AccentTheme theme = inputControlsView.getAccentTheme(); + if (theme != AccentTheme.MONO) accent = theme.accent; + } int primaryColor = accent != -1 ? ColorUtils.setAlphaComponent(accent, (int) (Math.min(1.0f, inputControlsView.getOverlayOpacity() * 2.0f) * 255)) From 5e52227e957221debb7235466d6b99027de69bf6 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Mon, 13 Jul 2026 22:30:33 +0000 Subject: [PATCH 3/8] Add Shadow style, per-shortcut style/theme memory, Cyan default accent - Shadow: soft dark layered style with four-tile D-pad, accent flood on press - Style and color theme are remembered on the shortcut (.desktop extras) like the controls profile; global prefs act as the default; .icp stays layout-only - Editor pickers set the global defaults with live preview - Default accent theme is Cyan; Mono gives the classic white look - New style/theme strings added to all locales --- app/src/main/res/values-b+es+419/strings.xml | 5 + app/src/main/res/values-da/strings.xml | 5 + app/src/main/res/values-de/strings.xml | 5 + app/src/main/res/values-es/strings.xml | 5 + app/src/main/res/values-fi/strings.xml | 5 + app/src/main/res/values-fr/strings.xml | 5 + app/src/main/res/values-hi/strings.xml | 5 + app/src/main/res/values-it/strings.xml | 5 + app/src/main/res/values-ja/strings.xml | 5 + app/src/main/res/values-ko/strings.xml | 5 + app/src/main/res/values-no/strings.xml | 5 + app/src/main/res/values-pl/strings.xml | 5 + app/src/main/res/values-pt-rBR/strings.xml | 5 + app/src/main/res/values-pt/strings.xml | 5 + app/src/main/res/values-ro/strings.xml | 5 + app/src/main/res/values-ru/strings.xml | 5 + app/src/main/res/values-sv/strings.xml | 5 + app/src/main/res/values-th/strings.xml | 5 + app/src/main/res/values-tr/strings.xml | 5 + app/src/main/res/values-uk/strings.xml | 5 + app/src/main/res/values-zh-rCN/strings.xml | 5 + app/src/main/res/values-zh-rTW/strings.xml | 5 + app/src/main/res/values/strings.xml | 1 + .../display/XServerDisplayActivity.java | 49 ++- .../runtime/input/ControlsEditorActivity.java | 16 +- .../runtime/input/controls/AccentTheme.java | 4 +- .../input/controls/ControlElement.java | 412 ++++++++++++++++-- .../input/controls/ControlsProfile.java | 10 - .../input/controls/InputControlsManager.java | 9 - .../runtime/input/controls/VisualStyle.java | 8 +- .../runtime/input/ui/InputControlsView.java | 14 +- 31 files changed, 546 insertions(+), 87 deletions(-) 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 dd08f0471..12a261f9c 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -363,6 +363,11 @@ Seleccionar estilo Original Vidrio + Slate + Halo + Glint + Shadow + Tema de color Perfil integrado — se creó una copia que puedes editar. Nombre del perfil No hay ningún perfil seleccionado diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index bfa736c5a..ac5876c61 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1303,6 +1303,11 @@ Installeret sti: Vælg stil Original Glass + Slate + Halo + Glint + Shadow + Farvetema Indbygget profil - oprettede en kopi, du kan redigere. Nulstil denne profil til dens oprindelige layout? Alle ændringer, du har lavet, går tabt. Profil nulstillet til dens oprindelige layout. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d43b8af51..d6c8478f4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1303,6 +1303,11 @@ Installierter Pfad: Stil auswählen Original Glass + Slate + Halo + Glint + Shadow + Farbschema Integriertes Profil - eine bearbeitbare Kopie wurde erstellt. Dieses Profil auf sein ursprüngliches Layout zurücksetzen? Alle vorgenommenen Änderungen gehen verloren. Profil auf ursprüngliches Layout zurückgesetzt. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f69c090e4..0d96eb8da 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1303,6 +1303,11 @@ Ruta instalada: Seleccionar estilo Original Glass + Slate + Halo + Glint + Shadow + Tema de color Perfil integrado: se creó una copia que puedes editar. ¿Restablecer este perfil a su diseño original? Se perderán todos los cambios que hayas hecho. Perfil restablecido a su diseño original. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index b40b43692..8cdf1379e 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -363,6 +363,11 @@ Valitse tyyli Alkuperäinen Lasi + Slate + Halo + Glint + Shadow + Väriteema Sisäänrakennettu profiili – luotiin kopio, jota voit muokata. Profiilin nimi Ei valittua profiilia diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index df12b6b2f..f7c157a13 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1303,6 +1303,11 @@ Chemin installé : Sélectionner le style Original Glass + Slate + Halo + Glint + Shadow + Thème de couleur Profil intégré : une copie modifiable a été créée. Réinitialiser ce profil à sa disposition d\'origine ? Toutes vos modifications seront perdues. Profil réinitialisé à sa disposition d\'origine. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index b68672f9f..9960f0361 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -341,6 +341,11 @@ शैली चुनें मूल Glass + Slate + Halo + Glint + Shadow + रंग थीम अंतर्निहित प्रोफ़ाइल - एक प्रति बनाई गई जिसे आप संपादित कर सकते हैं। प्रोफ़ाइल नाम कोई प्रोफ़ाइल चयनित नहीं diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0858beeeb..50295b4b3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1303,6 +1303,11 @@ Percorso installato: Seleziona stile Originale Glass + Slate + Halo + Glint + Shadow + Tema colore Profilo integrato: è stata creata una copia modificabile. Ripristinare questo profilo al layout originale? Tutte le modifiche apportate andranno perse. Profilo ripristinato al layout originale. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e3a8caa24..c7e18d4fa 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -363,6 +363,11 @@ スタイルを選択 オリジナル Glass + Slate + Halo + Glint + Shadow + カラーテーマ 組み込みプロファイル — 編集可能なコピーを作成しました。 プロファイル名 プロファイルが選択されていません diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 085b244b0..d8f0c7107 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1304,6 +1304,11 @@ 스타일 선택 원본 Glass + Slate + Halo + Glint + Shadow + 색상 테마 기본 제공 프로필입니다. 편집할 수 있는 복사본을 만들었습니다. 이 프로필을 원래 레이아웃으로 초기화할까요? 변경 사항이 모두 손실됩니다. 프로필이 원래 레이아웃으로 초기화되었습니다. diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index da508fa3f..afbcb4d3a 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -363,6 +363,11 @@ Velg stil Original Glass + Slate + Halo + Glint + Shadow + Fargetema Innebygd profil – opprettet en kopi du kan redigere. Profilnavn Ingen profil valgt diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 7a21030b1..441218c2e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1309,6 +1309,11 @@ Zainstalowana ścieżka: Wybierz styl Oryginalny Glass + Slate + Halo + Glint + Shadow + Motyw kolorystyczny Wbudowany profil — utworzono kopię, którą możesz edytować. Zresetować ten profil do oryginalnego układu? Wszystkie wprowadzone zmiany zostaną utracone. Profil zresetowany do oryginalnego układu. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 522f0e5d4..fbc33c07b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1303,6 +1303,11 @@ Caminho instalado: Selecionar estilo Original Glass + Slate + Halo + Glint + Shadow + Tema de cores Perfil integrado - uma cópia editável foi criada. Redefinir este perfil para o layout original? Todas as alterações feitas serão perdidas. Perfil redefinido para o layout original. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 6d952538d..be2f90071 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -363,6 +363,11 @@ Selecionar estilo Original Vidro + Slate + Halo + Glint + Shadow + Tema de cores Perfil incorporado — foi criada uma cópia que pode editar. Nome do perfil Nenhum perfil selecionado diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index a4e7d19e8..6d15b656d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1303,6 +1303,11 @@ Cale instalata: Selectează stilul Original Glass + Slate + Halo + Glint + Shadow + Temă de culoare Profil integrat - a fost creată o copie editabilă. Resetezi acest profil la aspectul original? Toate modificările făcute se vor pierde. Profil resetat la aspectul original. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 05bebf378..ef3639d02 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -353,6 +353,11 @@ Выберите стиль Оригинал Glass + Slate + Halo + Glint + Shadow + Цветовая тема Встроенный профиль — созданную копию можно редактировать. Имя профиля Профиль не выбран diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 9f62aecf2..633a1984f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -363,6 +363,11 @@ Välj stil Original Glas + Slate + Halo + Glint + Shadow + Färgtema Inbyggd profil — en kopia som du kan redigera har skapats. Profilnamn Ingen profil vald diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 3bb6b8616..1514325ed 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -363,6 +363,11 @@ เลือกสไตล์ ต้นฉบับ กระจก + Slate + Halo + Glint + Shadow + ธีมสี โปรไฟล์ในตัว — สร้างสำเนาที่คุณสามารถแก้ไขได้แล้ว ชื่อโปรไฟล์ ไม่ได้เลือกโปรไฟล์ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d3a0d96a0..69b5d16e9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -363,6 +363,11 @@ Stil Seç Orijinal Cam + Slate + Halo + Glint + Shadow + Renk teması Yerleşik profil — düzenleyebileceğiniz bir kopya oluşturuldu. Profil Adı Seçili profil yok diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 973fa58a6..58bdfb2de 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1309,6 +1309,11 @@ Виберіть стиль Оригінальний Glass + Slate + Halo + Glint + Shadow + Колірна тема Вбудований профіль — створено копію, яку можна редагувати. Скинути цей профіль до початкового макета? Усі внесені зміни буде втрачено. Профіль скинуто до початкового макета. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2da9ed261..7052b3370 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1303,6 +1303,11 @@ 选择样式 原版 Glass + Slate + Halo + Glint + Shadow + 颜色主题 内置配置文件 - 已创建可编辑副本。 将此配置文件重置为原始布局?你所做的任何更改都会丢失。 配置文件已重置为原始布局。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ba47e54da..2cc64f335 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1303,6 +1303,11 @@ 選擇樣式 原版 Glass + Slate + Halo + Glint + Shadow + 顏色主題 內建設定檔 - 已建立可編輯副本。 將此設定檔重設為原始版面配置?你所做的任何變更都會遺失。 設定檔已重設為原始版面配置。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 65ce749e7..960116f34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -367,6 +367,7 @@ Slate Halo Glint + Shadow Color Theme Built-in profile — created a copy you can edit. Profile Name diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 524620828..cbcd7d7dc 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -3975,6 +3975,7 @@ private void renderDrawerMenu() { styleNames.add(getString(R.string.input_controls_style_gamehub)); styleNames.add(getString(R.string.input_controls_style_halo)); styleNames.add(getString(R.string.input_controls_style_glint)); + styleNames.add(getString(R.string.input_controls_style_shadow)); styleNames.add(getString(R.string.input_controls_style_original)); VisualStyle currentStyle = inputControlsView != null && inputControlsView.getVisualStyle() != null ? inputControlsView.getVisualStyle() : VisualStyle.SLATE; @@ -3982,11 +3983,9 @@ private void renderDrawerMenu() { ArrayList accentThemeNames = new ArrayList<>(); int selectedAccentThemeIndex = 0; - if (activeProfile != null) { + if (activeProfile != null && inputControlsView != null) { accentThemeNames.addAll(Arrays.asList(AccentTheme.displayNames())); - AccentTheme currentAccent = activeProfile.getAccentTheme() != null - ? activeProfile.getAccentTheme() : AccentTheme.MONO; - selectedAccentThemeIndex = currentAccent.ordinal(); + selectedAccentThemeIndex = inputControlsView.getAccentTheme().ordinal(); } List gestureProfileNames = new ArrayList<>(); @@ -4592,7 +4591,7 @@ public void onInputControlsStyleSelected(int index) { if (index < 0 || index >= all.length) return; VisualStyle chosen = all[index]; if (inputControlsView != null) inputControlsView.setVisualStyle(chosen); - preferences.edit().putString("input_visual_style", chosen.name()).apply(); + persistSelectedStyle(chosen); renderDrawerMenu(); } @@ -4600,11 +4599,9 @@ public void onInputControlsStyleSelected(int index) { public void onInputControlsAccentThemeSelected(int index) { AccentTheme[] all = AccentTheme.values(); if (index < 0 || index >= all.length) return; - ControlsProfile profile = inputControlsView != null ? inputControlsView.getProfile() : null; - if (profile == null) return; - profile.setAccentTheme(all[index]); - profile.save(); - inputControlsView.invalidate(); + AccentTheme chosen = all[index]; + if (inputControlsView != null) inputControlsView.setAccentTheme(chosen); + persistSelectedAccentTheme(chosen); renderDrawerMenu(); } @@ -7570,6 +7567,30 @@ private void persistSelectedProfile(ControlsProfile profile) { } } + // Style/theme picks follow the controlsProfile pattern: remembered on the shortcut when the + // session was launched from one, otherwise as the global default. + private void persistSelectedStyle(VisualStyle style) { + if (shortcut != null) { + if (!style.name().equals(shortcut.getExtra("controlsStyle", ""))) { + shortcut.putExtra("controlsStyle", style.name()); + shortcut.saveData(); + } + } else { + preferences.edit().putString("input_visual_style", style.name()).apply(); + } + } + + private void persistSelectedAccentTheme(AccentTheme theme) { + if (shortcut != null) { + if (!theme.name().equals(shortcut.getExtra("controlsAccentTheme", ""))) { + shortcut.putExtra("controlsAccentTheme", theme.name()); + shortcut.saveData(); + } + } else { + preferences.edit().putString("input_accent_theme", theme.name()).apply(); + } + } + private void pushSelectedGestureConfig() { try { int gid = selectedGestureProfileId(); @@ -7604,8 +7625,12 @@ private ArrayList getVisibleControlsProfiles() { private void applyInputVisualStylePreferences() { if (inputControlsView == null || preferences == null) return; - inputControlsView.setVisualStyle( - VisualStyle.fromPreference(preferences.getString("input_visual_style", VisualStyle.SLATE.name()))); + String style = shortcut != null ? shortcut.getExtra("controlsStyle", "") : ""; + if (style.isEmpty()) style = preferences.getString("input_visual_style", VisualStyle.SLATE.name()); + inputControlsView.setVisualStyle(VisualStyle.fromPreference(style)); + String theme = shortcut != null ? shortcut.getExtra("controlsAccentTheme", "") : ""; + if (theme.isEmpty()) theme = preferences.getString("input_accent_theme", AccentTheme.CYAN.name()); + inputControlsView.setAccentTheme(AccentTheme.fromPreference(theme)); } private ControlsProfile resolvePreferredStartupProfile() { diff --git a/app/src/main/runtime/input/ControlsEditorActivity.java b/app/src/main/runtime/input/ControlsEditorActivity.java index 41453fbf2..29386f941 100644 --- a/app/src/main/runtime/input/ControlsEditorActivity.java +++ b/app/src/main/runtime/input/ControlsEditorActivity.java @@ -65,6 +65,10 @@ public void onCreate(Bundle bundle) { VisualStyle.fromPreference( PreferenceManager.getDefaultSharedPreferences(this) .getString("input_visual_style", null))); + inputControlsView.setAccentTheme( + AccentTheme.fromPreference( + PreferenceManager.getDefaultSharedPreferences(this) + .getString("input_accent_theme", null))); profile = InputControlsManager.loadProfile( @@ -161,7 +165,7 @@ private void showVisualStylePicker(View anchorView) { label.setPadding(rowPadding, rowPadding, rowPadding, rowPadding); label.setOnClickListener( v -> { - androidx.preference.PreferenceManager.getDefaultSharedPreferences(this) + PreferenceManager.getDefaultSharedPreferences(this) .edit() .putString("input_visual_style", style.name()) .apply(); @@ -272,7 +276,7 @@ private void showAccentThemePicker(View anchorView) { title.setPadding(0, 0, 0, (int) UnitUtils.dpToPx(8)); view.addView(title); - AccentTheme current = profile.getAccentTheme() != null ? profile.getAccentTheme() : AccentTheme.MONO; + AccentTheme current = inputControlsView.getAccentTheme(); final PopupWindow[] popup = new PopupWindow[1]; String[] names = AccentTheme.displayNames(); AccentTheme[] themes = AccentTheme.values(); @@ -306,9 +310,11 @@ private void showAccentThemePicker(View anchorView) { row.setOnClickListener( v -> { - profile.setAccentTheme(theme); - profile.save(); - inputControlsView.invalidate(); + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putString("input_accent_theme", theme.name()) + .apply(); + inputControlsView.setAccentTheme(theme); if (popup[0] != null) popup[0].dismiss(); }); rows.addView(row); diff --git a/app/src/main/runtime/input/controls/AccentTheme.java b/app/src/main/runtime/input/controls/AccentTheme.java index e0daf728d..54fc6ce45 100644 --- a/app/src/main/runtime/input/controls/AccentTheme.java +++ b/app/src/main/runtime/input/controls/AccentTheme.java @@ -23,11 +23,11 @@ public enum AccentTheme { } public static AccentTheme fromPreference(String name) { - if (name == null) return MONO; + if (name == null) return CYAN; try { return AccentTheme.valueOf(name); } catch (IllegalArgumentException e) { - return MONO; + return CYAN; } } diff --git a/app/src/main/runtime/input/controls/ControlElement.java b/app/src/main/runtime/input/controls/ControlElement.java index 3a0a443ad..55c6be83a 100644 --- a/app/src/main/runtime/input/controls/ControlElement.java +++ b/app/src/main/runtime/input/controls/ControlElement.java @@ -557,6 +557,10 @@ public void draw(Canvas canvas) { drawGameHub(canvas); return; } + if (style == VisualStyle.SHADOW) { + drawShadow(canvas); + return; + } if (style != VisualStyle.ORIGINAL) { drawClean(canvas, style); return; @@ -970,6 +974,29 @@ public void draw(Canvas canvas) { } } + /** Builds an outward-pointing arrow triangle for D-pad direction {@code dir} (0=up..3=left). */ + private static void buildDpadArrowPath(Path p, float ax, float ay, float t, int dir) { + p.reset(); + if (dir == 0) { + p.moveTo(ax, ay - t); + p.lineTo(ax + 0.85f * t, ay + 0.65f * t); + p.lineTo(ax - 0.85f * t, ay + 0.65f * t); + } else if (dir == 1) { + p.moveTo(ax + t, ay); + p.lineTo(ax - 0.65f * t, ay + 0.85f * t); + p.lineTo(ax - 0.65f * t, ay - 0.85f * t); + } else if (dir == 2) { + p.moveTo(ax, ay + t); + p.lineTo(ax - 0.85f * t, ay - 0.65f * t); + p.lineTo(ax + 0.85f * t, ay - 0.65f * t); + } else { + p.moveTo(ax - t, ay); + p.lineTo(ax + 0.65f * t, ay - 0.85f * t); + p.lineTo(ax + 0.65f * t, ay + 0.85f * t); + } + p.close(); + } + private float cleanCornerRadius(Rect bb) { switch (shape) { case ROUND_RECT: @@ -1141,26 +1168,7 @@ private void drawClean(Canvas canvas, VisualStyle style) { } float ax = cx + dx * half * 0.62f; float ay = cy + dy * half * 0.62f; - float t = base * 0.055f; - path.reset(); - if (i == 0) { - path.moveTo(ax, ay - t); - path.lineTo(ax + 0.85f * t, ay + 0.65f * t); - path.lineTo(ax - 0.85f * t, ay + 0.65f * t); - } else if (i == 1) { - path.moveTo(ax + t, ay); - path.lineTo(ax - 0.65f * t, ay + 0.85f * t); - path.lineTo(ax - 0.65f * t, ay - 0.85f * t); - } else if (i == 2) { - path.moveTo(ax, ay + t); - path.lineTo(ax - 0.85f * t, ay - 0.65f * t); - path.lineTo(ax + 0.85f * t, ay - 0.65f * t); - } else { - path.moveTo(ax - t, ay); - path.lineTo(ax + 0.65f * t, ay - 0.85f * t); - path.lineTo(ax + 0.65f * t, ay + 0.85f * t); - } - path.close(); + buildDpadArrowPath(path, ax, ay, base * 0.055f, i); paint.setStyle(Paint.Style.FILL); paint.setColor(hot ? ColorUtils.setAlphaComponent(accent, (int) (240 * a)) @@ -1505,6 +1513,352 @@ private void drawCleanHotCell( canvas.drawRoundRect(tempRect, r, r, paint); } + private static final int SHADOW_BODY = 0xFF0A1422; + private static final int SHADOW_EDGE = 0xFF84A9D4; + + private void shadowSoftRect( + Canvas canvas, Paint paint, float l, float t, float r, float b, float radius, + boolean active, int accent, float a, int snappingSize) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.55f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((active ? 64 : 16) * a))); + canvas.drawRoundRect(l, t, r, b, radius, radius, paint); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.22f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) ((active ? 70 : 28) * a))); + canvas.drawRoundRect(l, t, r, b, radius, radius, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? ColorUtils.setAlphaComponent(accent, (int) (190 * a)) + : ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (135 * a))); + canvas.drawRoundRect(l, t, r, b, radius, radius, paint); + float inset = Math.max(1f, snappingSize * 0.35f * scale); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.08f * scale)); + paint.setColor(Color.argb((int) ((active ? 58 : 24) * a), 255, 255, 255)); + canvas.drawRoundRect( + l + inset, t + inset, r - inset, b - inset, + Math.max(1f, radius - inset), Math.max(1f, radius - inset), paint); + } + + private void shadowSoftCircle( + Canvas canvas, Paint paint, float cx, float cy, float r, + boolean active, int accent, float a, int snappingSize) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.55f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((active ? 72 : 18) * a))); + canvas.drawCircle(cx, cy, r, paint); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.2f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) ((active ? 74 : 30) * a))); + canvas.drawCircle(cx, cy, r, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? ColorUtils.setAlphaComponent(accent, (int) (190 * a)) + : ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (140 * a))); + canvas.drawCircle(cx, cy, r, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.08f * scale)); + paint.setColor(Color.argb((int) ((active ? 44 : 18) * a), 255, 255, 255)); + canvas.drawCircle(cx, cy, r * 0.88f, paint); + } + + /** + * SHADOW style — soft dark filled bodies built from layered translucent strokes: a wide accent + * halo, a light edge line, a dark fill and a thin inner highlight. Bodies flood with the accent + * while pressed. The D-pad renders as four separate tiles with per-direction feedback. + */ + private void drawShadow(Canvas canvas) { + int snappingSize = inputControlsView.getSnappingSize(); + Paint paint = inputControlsView.getPaint(); + float effectiveOpacity = inputControlsView.isEditMode() ? Math.max(0.15f, opacity) : opacity; + float overlayOpacity = inputControlsView.getOverlayOpacity(); + float dim = overlayOpacity <= 0.4f + ? 0.28f + (overlayOpacity - 0.1f) * (0.5f / 0.3f) + : 0.78f + (overlayOpacity - 0.4f) * (0.22f / 0.6f); + float a = Mathf.clamp(dim, 0f, 1f) * effectiveOpacity; + boolean engaged = isEngaged(); + Rect boundingBox = getBoundingBox(); + int accent = resolveThemedAccent(); + if (selected && resolveAccentColor() == -1) { + accent = ColorUtils.setAlphaComponent(inputControlsView.getSecondaryColor(), 255); + } + int labelColor = engaged + ? Color.argb((int) (245 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (235 * a)); + float minTextSize = snappingSize * 2 * scale; + + switch (type) { + case BUTTON: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + switch (shape) { + case CIRCLE: + shadowSoftCircle( + canvas, paint, cx, cy, boundingBox.width() * 0.5f, engaged, accent, a, snappingSize); + break; + case SQUARE: + shadowSoftRect( + canvas, paint, boundingBox.left, boundingBox.top, boundingBox.right, + boundingBox.bottom, snappingSize * 1.05f * scale, engaged, accent, a, snappingSize); + break; + default: + shadowSoftRect( + canvas, paint, boundingBox.left, boundingBox.top, boundingBox.right, + boundingBox.bottom, boundingBox.height() * 0.45f, engaged, accent, a, snappingSize); + break; + } + if (iconId > 0) { + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, true, + engaged ? inputControlsView.getColorFilter() : accentFilter(accent)); + } else { + String label = getDisplayText(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(labelColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, label, boundingBox.width() - snappingSize * 0.6f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, cx, (cy - ((paint.descent() + paint.ascent()) * 0.5f)), paint); + } + break; + } + case D_PAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float base = Math.min(boundingBox.width(), boundingBox.height()); + float piece = base * 0.30f; + float dist = piece * 1.15f; + float c = piece * 0.13f; + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (60 * a))); + canvas.drawRoundRect(cx - c, cy - c, cx + c, cy + c, c * 0.45f, c * 0.45f, paint); + boolean hasStates = engaged && states.length >= 4; + for (int i = 0; i < 4; i++) { + int dx = i == 1 ? 1 : i == 3 ? -1 : 0; + int dy = i == 2 ? 1 : i == 0 ? -1 : 0; + float px = cx + dx * dist; + float py = cy + dy * dist; + boolean hot = hasStates && states[i]; + shadowSoftRect( + canvas, paint, px - piece * 0.5f, py - piece * 0.5f, px + piece * 0.5f, + py + piece * 0.5f, piece * 0.28f, hot, accent, a, snappingSize); + buildDpadArrowPath(path, px, py, piece * 0.22f, i); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot + ? Color.argb((int) (255 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (220 * a))); + canvas.drawPath(path, paint); + } + break; + } + case STICK: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float r = boundingBox.height() * 0.5f; + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_BODY, (int) ((engaged ? 155 : 118) * a))); + canvas.drawCircle(cx, cy, r, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.2f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) ((engaged ? 62 : 26) * a))); + canvas.drawCircle(cx, cy, r * 0.96f, paint); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.5f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((engaged ? 72 : 20) * a))); + canvas.drawCircle(cx, cy, r * 0.98f, paint); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.22f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((engaged ? 235 : 205) * a))); + canvas.drawCircle(cx, cy, r * 0.52f, paint); + PointF pos = getCurrentPosition(); + float thumbRadius = snappingSize * 3.5f * scale; + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (190 * a))); + canvas.drawCircle(pos.x, pos.y, thumbRadius, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.12f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) (45 * a))); + canvas.drawCircle(pos.x, pos.y, thumbRadius * 0.96f, paint); + break; + } + case TRACKPAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + shadowSoftRect( + canvas, paint, boundingBox.left, boundingBox.top, boundingBox.right, + boundingBox.bottom, boundingBox.height() * 0.18f, engaged, accent, a, snappingSize); + float glyph = Math.min(boundingBox.width(), boundingBox.height()) * 0.45f; + float gw = glyph * 0.42f; + float gh = glyph * 0.62f; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1.5f, glyph * 0.035f)); + paint.setColor(ColorUtils.setAlphaComponent( + engaged ? Color.WHITE : accent, (int) ((engaged ? 220 : 95) * a))); + canvas.drawRoundRect( + cx - gw * 0.5f, cy - gh * 0.5f, cx + gw * 0.5f, cy + gh * 0.5f, + gw * 0.5f, gw * 0.5f, paint); + canvas.drawLine(cx, cy - gh * 0.30f, cx, cy - gh * 0.02f, paint); + break; + } + case RANGE_BUTTON: { + Range range = getRange(); + float rr = (orientation == 0 ? boundingBox.height() : boundingBox.width()) * 0.45f; + float elementSize = scroller.getElementSize(); + float scrollOffset = scroller.getScrollOffset(); + byte[] rangeIndex = scroller.getRangeIndex(); + shadowSoftRect( + canvas, paint, boundingBox.left, boundingBox.top, boundingBox.right, + boundingBox.bottom, rr, false, accent, a, snappingSize); + + canvas.save(); + path.reset(); + path.addRoundRect( + boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + rr, rr, Path.Direction.CW); + canvas.clipPath(path); + + int pressedIndex = + currentPointerId != -1 && !scroller.isScrolling() ? scroller.getBindingIndex() : -1; + int dividerColor = ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) (35 * a)); + float dividerWidth = Math.max(1f, snappingSize * 0.06f); + int hotTextColor = Color.argb((int) (245 * a), 255, 255, 255); + int cellTextColor = ColorUtils.setAlphaComponent(accent, (int) (225 * a)); + + if (orientation == 0) { + float lineTop = boundingBox.top + boundingBox.height() * 0.25f; + float lineBottom = boundingBox.bottom - boundingBox.height() * 0.25f; + float startX = boundingBox.left - (scrollOffset % elementSize); + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(dividerWidth); + paint.setColor(dividerColor); + if (startX > boundingBox.left && startX < boundingBox.right) + canvas.drawLine(startX, lineTop, startX, lineBottom, paint); + if (startX < boundingBox.right && startX + elementSize > boundingBox.left) { + boolean hot = index == pressedIndex; + if (hot) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (150 * a))); + canvas.drawRect( + startX, boundingBox.top, startX + elementSize, boundingBox.bottom, paint); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : cellTextColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, elementSize - snappingSize * 0.5f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + startX + elementSize * 0.5f, + (y - ((paint.descent() + paint.ascent()) * 0.5f)), + paint); + } + startX += elementSize; + } + } else { + float lineLeft = boundingBox.left + boundingBox.width() * 0.25f; + float lineRight = boundingBox.right - boundingBox.width() * 0.25f; + float startY = boundingBox.top - (scrollOffset % elementSize); + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(dividerWidth); + paint.setColor(dividerColor); + if (startY > boundingBox.top && startY < boundingBox.bottom) + canvas.drawLine(lineLeft, startY, lineRight, startY, paint); + if (startY < boundingBox.bottom && startY + elementSize > boundingBox.top) { + boolean hot = index == pressedIndex; + if (hot) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (150 * a))); + canvas.drawRect( + boundingBox.left, startY, boundingBox.right, startY + elementSize, paint); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : cellTextColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, boundingBox.width() - snappingSize * 0.5f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + x, + startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), + paint); + } + startY += elementSize; + } + } + canvas.restore(); + break; + } + case RADIAL_MENU: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float radius = boundingBox.width() * 0.5f; + + if (radialMenuExpanded && bindings.length > 0 && radius > 0) { + float innerRadius = radius + snappingSize * 0.5f; + float outerRadius = boundingBox.width() + (snappingSize * scale); + float angleStep = 360.0f / bindings.length; + + if (paths == null || paths.length != bindings.length) { + paths = new Path[bindings.length]; + RectF outerRect = new RectF(cx - outerRadius, cy - outerRadius, cx + outerRadius, cy + outerRadius); + RectF innerRect = new RectF(cx - innerRadius, cy - innerRadius, cx + innerRadius, cy + innerRadius); + for (int i = 0; i < bindings.length; i++) { + float startAngle = -90.0f + i * angleStep; + paths[i] = new Path(); + paths[i].arcTo(outerRect, startAngle, angleStep, true); + paths[i].arcTo(innerRect, startAngle + angleStep, -angleStep, false); + paths[i].close(); + } + } + + if (paths != null && paths.length == bindings.length) { + for (int i = 0; i < bindings.length; i++) { + boolean active = i == activeRadialBindingIndex; + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? ColorUtils.setAlphaComponent(accent, (int) (150 * a)) + : ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (135 * a))); + canvas.drawPath(paths[i], paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.12f)); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) (40 * a))); + canvas.drawPath(paths[i], paint); + + float middleAngle = (float) Math.toRadians(-90.0f + i * angleStep + angleStep * 0.5f); + float labelRadius = (innerRadius + outerRadius) * 0.5f; + float labelX = (float) (cx + Math.cos(middleAngle) * labelRadius); + float labelY = (float) (cy + Math.sin(middleAngle) * labelRadius); + String label = getBindingShortText(i); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? Color.argb((int) (240 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (215 * a))); + paint.setTextSize(snappingSize * 1.2f * scale); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, labelX, labelY - ((paint.descent() + paint.ascent()) * 0.5f), paint); + } + } + } + + shadowSoftCircle(canvas, paint, cx, cy, radius, engaged, accent, a, snappingSize); + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), + iconId > 0 ? iconId : 34, true, + engaged ? inputControlsView.getColorFilter() : accentFilter(accent)); + break; + } + } + } + /** * GameHub visual style — dark translucent glass body, light white rim, brighter rim & inner glow * when pressed, soft outer shadow. Used when the user picks the "GameHub" style. @@ -1837,9 +2191,6 @@ private void drawGameHub(Canvas canvas) { canvas.restore(); break; } - default: - drawOriginalLegacy(canvas); - break; } paint.setStrokeJoin(Paint.Join.MITER); paint.setStrokeCap(Paint.Cap.BUTT); @@ -1921,21 +2272,6 @@ private void drawGameHubGlassOnPath( paint.setShader(null); } - /** - * Fallback to the original draw routine for element types we don't customize in the GameHub - * style (RADIAL_MENU, RANGE_BUTTON). This temporarily switches the view's style back to ORIGINAL - * just for this draw call. - */ - private void drawOriginalLegacy(Canvas canvas) { - VisualStyle saved = inputControlsView.getVisualStyle(); - try { - inputControlsView.setVisualStyleSilent(VisualStyle.ORIGINAL); - draw(canvas); - } finally { - inputControlsView.setVisualStyleSilent(saved); - } - } - private static final Rect iconSrcRect = new Rect(); private static final Rect iconDstRect = new Rect(); diff --git a/app/src/main/runtime/input/controls/ControlsProfile.java b/app/src/main/runtime/input/controls/ControlsProfile.java index 5db68bf03..f134852c5 100644 --- a/app/src/main/runtime/input/controls/ControlsProfile.java +++ b/app/src/main/runtime/input/controls/ControlsProfile.java @@ -17,7 +17,6 @@ public class ControlsProfile implements Comparable { public final int id; private String name; private float cursorSpeed = 1.0f; - private AccentTheme accentTheme; private volatile List elements = new ArrayList<>(); private final ArrayList controllers = new ArrayList<>(); private boolean elementsLoaded = false; @@ -47,14 +46,6 @@ public void setCursorSpeed(float cursorSpeed) { this.cursorSpeed = cursorSpeed; } - public AccentTheme getAccentTheme() { - return accentTheme; - } - - public void setAccentTheme(AccentTheme accentTheme) { - this.accentTheme = accentTheme; - } - public boolean isVirtualGamepad() { if (!elementsLoaded && !virtualGamepad) { File file = getProfileFile(context, id); @@ -170,7 +161,6 @@ public void save() { data.put("id", id); data.put("name", name); data.put("cursorSpeed", Float.valueOf(cursorSpeed)); - if (accentTheme != null) data.put("accentTheme", accentTheme.name()); JSONArray elementsJSONArray = new JSONArray(); if (!elementsLoaded && file.isFile()) { diff --git a/app/src/main/runtime/input/controls/InputControlsManager.java b/app/src/main/runtime/input/controls/InputControlsManager.java index 8d5144fb6..bfab64126 100644 --- a/app/src/main/runtime/input/controls/InputControlsManager.java +++ b/app/src/main/runtime/input/controls/InputControlsManager.java @@ -312,7 +312,6 @@ public static ControlsProfile loadProfile(Context context, InputStream inStream) int profileId = 0; String profileName = null; float cursorSpeed = Float.NaN; - String accentTheme = null; int fieldsRead = 0; reader.beginObject(); @@ -328,8 +327,6 @@ public static ControlsProfile loadProfile(Context context, InputStream inStream) } else if (name.equals("cursorSpeed")) { cursorSpeed = (float) reader.nextDouble(); fieldsRead++; - } else if (name.equals("accentTheme")) { - accentTheme = reader.nextString(); } else { if (fieldsRead == 3) break; reader.skipValue(); @@ -340,12 +337,6 @@ public static ControlsProfile loadProfile(Context context, InputStream inStream) if (profileName != null) { profile.setName(profileName); profile.setCursorSpeed(cursorSpeed); - if (accentTheme != null) { - try { - profile.setAccentTheme(AccentTheme.valueOf(accentTheme)); - } catch (IllegalArgumentException ignored) { - } - } reader.close(); return profile; } diff --git a/app/src/main/runtime/input/controls/VisualStyle.java b/app/src/main/runtime/input/controls/VisualStyle.java index 91ab0b244..95fe9c804 100644 --- a/app/src/main/runtime/input/controls/VisualStyle.java +++ b/app/src/main/runtime/input/controls/VisualStyle.java @@ -7,8 +7,9 @@ * when pressed. GAMEHUB ("Glass") is a dark translucent glass: light rim, brighter rim and inner * fill when pressed, soft drop shadow. HALO keeps a quiet base and lights an inset accent ring on * press. GLINT is a neutral frame with a small accent indicator that sweeps into a full ring while - * held. ORIGINAL preserves the historic look (translucent white strokes, lightly filled when - * engaged) and is listed last. + * held. SHADOW is a soft dark filled look built from layered translucent strokes, with a + * four-tile D-pad, whose bodies flood with the accent while pressed. ORIGINAL preserves the + * historic look (translucent white strokes, lightly filled when engaged) and is listed last. * *

The actual drawing branches on this enum inside {@link ControlElement#draw}. */ @@ -17,6 +18,7 @@ public enum VisualStyle { GAMEHUB, HALO, GLINT, + SHADOW, ORIGINAL; public static VisualStyle fromPreference(String name) { @@ -29,6 +31,6 @@ public static VisualStyle fromPreference(String name) { } public static String[] displayNames() { - return new String[] {"Slate", "Glass", "Halo", "Glint", "Original"}; + return new String[] {"Slate", "Glass", "Halo", "Glint", "Shadow", "Original"}; } } diff --git a/app/src/main/runtime/input/ui/InputControlsView.java b/app/src/main/runtime/input/ui/InputControlsView.java index 55a2cf046..46c07633b 100644 --- a/app/src/main/runtime/input/ui/InputControlsView.java +++ b/app/src/main/runtime/input/ui/InputControlsView.java @@ -76,6 +76,7 @@ public class InputControlsView extends View { private volatile float mouseMoveOffsetY = 0f; private boolean showTouchscreenControls = false; private VisualStyle visualStyle = VisualStyle.SLATE; + private AccentTheme accentTheme = AccentTheme.CYAN; private InputControlsManager inputControlsManager; private Handler timeoutHandler; // Reference to the activity's timeout handler @@ -190,16 +191,13 @@ public void setVisualStyle(VisualStyle style) { invalidate(); } - /** Same as {@link #setVisualStyle} but without {@link #invalidate()}, for internal draw-time - * fallbacks where requesting another redraw would loop. */ - public void setVisualStyleSilent(VisualStyle style) { - this.visualStyle = style != null ? style : VisualStyle.SLATE; + public AccentTheme getAccentTheme() { + return accentTheme; } - public AccentTheme getAccentTheme() { - ControlsProfile p = profile; - AccentTheme t = p != null ? p.getAccentTheme() : null; - return t != null ? t : AccentTheme.MONO; + public void setAccentTheme(AccentTheme theme) { + this.accentTheme = theme != null ? theme : AccentTheme.CYAN; + invalidate(); } public InputControlsManager getInputControlsManager() { From 3f240da04ec105219c8d94f37d024168e9e95f0c Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Mon, 13 Jul 2026 23:43:57 +0000 Subject: [PATCH 4/8] Controls polish: refreshed No R-Stick, themed icons everywhere, Slate press glow, finer opacity steps - No R-Stick built-in rebuilt without baked-in colors so themes apply; installed copies refreshed once on update - Icon buttons (start/select etc.) tint with the accent in Original and Glass too - Slate pressed buttons illuminate the whole body under the bloom - Drawer style selector hidden until a profile is active, like the color theme - Overlay opacity slider moves in 5% steps --- .../inputcontrols/profiles/controls-8.icp | 2 +- .../display/XServerDisplayActivity.java | 14 +++++---- .../main/runtime/display/XServerDrawerMenu.kt | 2 +- .../input/controls/ControlElement.java | 31 ++++++++++++------- .../input/controls/InputControlsManager.java | 7 ++++- 5 files changed, 35 insertions(+), 21 deletions(-) diff --git a/app/src/main/assets/inputcontrols/profiles/controls-8.icp b/app/src/main/assets/inputcontrols/profiles/controls-8.icp index 8c5b6390c..5ff39be3d 100644 --- a/app/src/main/assets/inputcontrols/profiles/controls-8.icp +++ b/app/src/main/assets/inputcontrols/profiles/controls-8.icp @@ -1 +1 @@ -{"id":8,"name":"No R-Stick","cursorSpeed":1,"elements":[{"type":"BUTTON","shape":"ROUND_RECT","customColor":-12435134,"bindings":["GAMEPAD_BUTTON_L2","NONE","NONE","NONE"],"scale":1.25,"x":0.12871287763118744,"y":0.10775047540664673,"toggleSwitch":false,"text":"LT","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-12435134,"bindings":["GAMEPAD_BUTTON_R2","NONE","NONE","NONE"],"scale":1.25,"x":0.8807576298713684,"y":0.10775047540664673,"toggleSwitch":false,"text":"RT","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_L1","NONE","NONE","NONE"],"scale":1.05,"x":0.12871287763118744,"y":0.239130437374115,"toggleSwitch":false,"text":"LB","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_R1","NONE","NONE","NONE"],"scale":1.15,"x":0.8807576298713684,"y":0.239130437374115,"toggleSwitch":false,"text":"RB","iconId":0},{"type":"D_PAD","shape":"CIRCLE","customColor":-13948632,"bindings":["GAMEPAD_DPAD_UP","GAMEPAD_DPAD_RIGHT","GAMEPAD_DPAD_DOWN","GAMEPAD_DPAD_LEFT"],"scale":0.92,"x":0.3164012134075165,"y":0.7816635370254517,"toggleSwitch":false,"text":"","iconId":0},{"type":"STICK","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_LEFT_THUMB_UP","GAMEPAD_LEFT_THUMB_RIGHT","GAMEPAD_LEFT_THUMB_DOWN","GAMEPAD_LEFT_THUMB_LEFT"],"scale":1.08,"x":0.10891088843345642,"y":0.6304348111152649,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-16776961,"bindings":["GAMEPAD_BUTTON_X","NONE","NONE","NONE"],"scale":1,"x":0.7524752616882324,"y":0.760869562625885,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-256,"bindings":["GAMEPAD_BUTTON_Y","NONE","NONE","NONE"],"scale":1,"x":0.8118811845779419,"y":0.6304348111152649,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-16711936,"bindings":["GAMEPAD_BUTTON_A","NONE","NONE","NONE"],"scale":1,"x":0.8118811845779419,"y":0.8913043737411499,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-65536,"bindings":["GAMEPAD_BUTTON_B","NONE","NONE","NONE"],"scale":1,"x":0.8712871074676514,"y":0.760869562625885,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_SELECT","NONE","NONE","NONE"],"scale":0.9,"x":0.44468361139297485,"y":0.95652174949646,"toggleSwitch":false,"text":"","iconId":16},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_START","NONE","NONE","NONE"],"scale":0.9,"x":0.5445544719696045,"y":0.95652174949646,"toggleSwitch":false,"text":"","iconId":15},{"type":"BUTTON","shape":"CIRCLE","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_L3","NONE","NONE","NONE"],"scale":0.78,"x":0.1782178282737732,"y":0.8478260636329651,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-8355712,"bindings":["GAMEPAD_BUTTON_R3","NONE","NONE","NONE"],"scale":0.78,"x":0.9306930899620056,"y":0.54347825050354,"toggleSwitch":false,"text":"","iconId":0}]} \ No newline at end of file +{"id":8,"name":"No R-Stick","cursorSpeed":1,"elements":[{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_L2","NONE","NONE","NONE"],"scale":1.25,"x":0.12745098769664764,"y":0.10638298094272614,"toggleSwitch":false,"text":"LT","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_R2","NONE","NONE","NONE"],"scale":1.25,"x":0.8823529481887817,"y":0.10638298094272614,"toggleSwitch":false,"text":"RT","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_L1","NONE","NONE","NONE"],"scale":1.05,"x":0.12745098769664764,"y":0.23404255509376526,"toggleSwitch":false,"text":"LB","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_R1","NONE","NONE","NONE"],"scale":1.15,"x":0.8823529481887817,"y":0.23404255509376526,"toggleSwitch":false,"text":"RB","iconId":0},{"type":"D_PAD","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_DPAD_UP","GAMEPAD_DPAD_RIGHT","GAMEPAD_DPAD_DOWN","GAMEPAD_DPAD_LEFT"],"scale":0.92,"x":0.3235294222831726,"y":0.7872340679168701,"toggleSwitch":false,"text":"","iconId":0},{"type":"STICK","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_LEFT_THUMB_UP","GAMEPAD_LEFT_THUMB_RIGHT","GAMEPAD_LEFT_THUMB_DOWN","GAMEPAD_LEFT_THUMB_LEFT"],"scale":1.08,"x":0.10856935381889343,"y":0.6296296119689941,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_BUTTON_X","NONE","NONE","NONE"],"scale":1,"x":0.7549019455909729,"y":0.7659574747085571,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_BUTTON_Y","NONE","NONE","NONE"],"scale":1,"x":0.813725471496582,"y":0.6382978558540344,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_BUTTON_A","NONE","NONE","NONE"],"scale":1,"x":0.813725471496582,"y":0.8936170339584351,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_BUTTON_B","NONE","NONE","NONE"],"scale":1,"x":0.8725489974021912,"y":0.7659574747085571,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_SELECT","NONE","NONE","NONE"],"scale":0.9,"x":0.4444444477558136,"y":0.9550827145576477,"toggleSwitch":false,"text":"","iconId":16},{"type":"BUTTON","shape":"ROUND_RECT","customColor":-1,"bindings":["GAMEPAD_BUTTON_START","NONE","NONE","NONE"],"scale":0.9,"x":0.5439360737800598,"y":0.9550827145576477,"toggleSwitch":false,"text":"","iconId":15},{"type":"BUTTON","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_BUTTON_L3","NONE","NONE","NONE"],"scale":0.78,"x":0.19607843458652496,"y":0.8510638475418091,"toggleSwitch":false,"text":"","iconId":0},{"type":"BUTTON","shape":"CIRCLE","customColor":-1,"bindings":["GAMEPAD_BUTTON_R3","NONE","NONE","NONE"],"scale":0.78,"x":0.9215686321258545,"y":0.531126856803894,"toggleSwitch":false,"text":"","iconId":0}]} \ No newline at end of file diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index cbcd7d7dc..b521811c1 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -3971,12 +3971,14 @@ private void renderDrawerMenu() { } ArrayList styleNames = new ArrayList<>(); - styleNames.add(getString(R.string.input_controls_style_slate)); - styleNames.add(getString(R.string.input_controls_style_gamehub)); - styleNames.add(getString(R.string.input_controls_style_halo)); - styleNames.add(getString(R.string.input_controls_style_glint)); - styleNames.add(getString(R.string.input_controls_style_shadow)); - styleNames.add(getString(R.string.input_controls_style_original)); + if (activeProfile != null) { + styleNames.add(getString(R.string.input_controls_style_slate)); + styleNames.add(getString(R.string.input_controls_style_gamehub)); + styleNames.add(getString(R.string.input_controls_style_halo)); + styleNames.add(getString(R.string.input_controls_style_glint)); + styleNames.add(getString(R.string.input_controls_style_shadow)); + styleNames.add(getString(R.string.input_controls_style_original)); + } VisualStyle currentStyle = inputControlsView != null && inputControlsView.getVisualStyle() != null ? inputControlsView.getVisualStyle() : VisualStyle.SLATE; int selectedStyleIndex = currentStyle.ordinal(); diff --git a/app/src/main/runtime/display/XServerDrawerMenu.kt b/app/src/main/runtime/display/XServerDrawerMenu.kt index c4cb6acc8..c44e296ed 100644 --- a/app/src/main/runtime/display/XServerDrawerMenu.kt +++ b/app/src/main/runtime/display/XServerDrawerMenu.kt @@ -2850,7 +2850,7 @@ private fun InputControlsPaneContent( valueText = "${(state.inputControlsOverlayOpacity * 100).toInt()}%", value = state.inputControlsOverlayOpacity, valueRange = 0.1f..1.0f, - steps = 8, + steps = 17, onValueChange = listener::onInputControlsOverlayOpacityChanged, ) Spacer(Modifier.height(4.dp)) diff --git a/app/src/main/runtime/input/controls/ControlElement.java b/app/src/main/runtime/input/controls/ControlElement.java index 55c6be83a..40889077f 100644 --- a/app/src/main/runtime/input/controls/ControlElement.java +++ b/app/src/main/runtime/input/controls/ControlElement.java @@ -679,7 +679,9 @@ public void draw(Canvas canvas) { } if (iconId > 0) { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId); + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, true, + accent != -1 ? accentFilter(accent) : inputControlsView.getColorFilter()); } else { String text = getDisplayText(); paint.setTextSize( @@ -749,11 +751,10 @@ public void draw(Canvas canvas) { : primaryColor); canvas.drawCircle(cx, cy, radius, paint); - if (iconId > 0) { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId); - } else { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), 34); - } + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), + iconId > 0 ? iconId : 34, true, + accent != -1 ? accentFilter(accent) : inputControlsView.getColorFilter()); break; } case D_PAD: @@ -1078,6 +1079,11 @@ private void drawClean(Canvas canvas, VisualStyle style) { paint.setColor(bodyColor); drawCleanBody(canvas, paint, boundingBox, 0f); if (engaged) { + if (!halo && !glint) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (105 * a))); + drawCleanBody(canvas, paint, boundingBox, 0f); + } float bloomRadius = Math.min(boundingBox.width(), boundingBox.height()) * 0.52f; beginBloom(paint, cx, cy, bloomRadius, accent, a * 0.9f); drawCleanBody(canvas, paint, boundingBox, 0f); @@ -1952,7 +1958,9 @@ private void drawGameHub(Canvas canvas) { } if (iconId > 0) { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId); + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, true, + hasAccent ? accentFilter(accent) : inputControlsView.getColorFilter()); } else { String label = getDisplayText(); paint.setStyle(Paint.Style.FILL); @@ -2115,11 +2123,10 @@ private void drawGameHub(Canvas canvas) { paint.setColor(engaged ? pressedStrokeColor : strokeColor); drawGameHubShape(canvas, paint, boundingBox, 0, false); - if (iconId > 0) { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId); - } else { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), 34); - } + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), + iconId > 0 ? iconId : 34, true, + hasAccent ? accentFilter(accent) : inputControlsView.getColorFilter()); break; } case RANGE_BUTTON: { diff --git a/app/src/main/runtime/input/controls/InputControlsManager.java b/app/src/main/runtime/input/controls/InputControlsManager.java index bfab64126..d299794b3 100644 --- a/app/src/main/runtime/input/controls/InputControlsManager.java +++ b/app/src/main/runtime/input/controls/InputControlsManager.java @@ -25,12 +25,14 @@ import org.json.JSONObject; public class InputControlsManager { - private static final int ASSET_PROFILE_SYNC_REVISION = 7; + private static final int ASSET_PROFILE_SYNC_REVISION = 8; public static final int LAST_BUILTIN_PROFILE_ID = 8; public static final int VIRTUAL_GAMEPAD_BUILTIN_ID = 3; public static final int GAMEHUB_LAYOUT_BUILTIN_ID = 7; // Retired bundled layout ids whose installed copies are removed on sync. private static final int[] RETIRED_PROFILE_IDS = {4, 5}; + // Bundled layouts replaced in this revision: reinstall pristine copies once. + private static final int[] REFRESHED_PROFILE_IDS = {8}; private final Context context; private ArrayList profiles; @@ -123,6 +125,9 @@ private void copyAssetProfilesIfNeeded() { ControlsProfile.getProfileFile(context, id).delete(); getBackupFile(context, id).delete(); } + for (int id : REFRESHED_PROFILE_IDS) { + ControlsProfile.getProfileFile(context, id).delete(); + } try { AssetManager assetManager = context.getAssets(); From 2c5322e3a382151b3471de2ee6d01681640616ee Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Tue, 14 Jul 2026 00:08:28 +0000 Subject: [PATCH 5/8] Add Reticle and Neon draw styles - Reticle: open hairline linework with faceted frames, corner brackets that extend while pressed, tick-marked stick with deflection line, crosshair trackpad - Neon: hollow chamfered wireframes with a layered-stroke glow that ignites white-hot on press; no blur filters, draw paths stay allocation-free - Style names translated across all locales; descriptive comments trimmed to one line --- app/src/main/res/values-b+es+419/strings.xml | 8 +- app/src/main/res/values-da/strings.xml | 8 +- app/src/main/res/values-de/strings.xml | 8 +- app/src/main/res/values-es/strings.xml | 8 +- app/src/main/res/values-fi/strings.xml | 8 +- app/src/main/res/values-fr/strings.xml | 8 +- app/src/main/res/values-hi/strings.xml | 10 +- app/src/main/res/values-it/strings.xml | 10 +- app/src/main/res/values-ja/strings.xml | 10 +- app/src/main/res/values-ko/strings.xml | 10 +- app/src/main/res/values-no/strings.xml | 8 +- app/src/main/res/values-pl/strings.xml | 8 +- app/src/main/res/values-pt-rBR/strings.xml | 8 +- app/src/main/res/values-pt/strings.xml | 8 +- app/src/main/res/values-ro/strings.xml | 8 +- app/src/main/res/values-ru/strings.xml | 10 +- app/src/main/res/values-sv/strings.xml | 8 +- app/src/main/res/values-th/strings.xml | 10 +- app/src/main/res/values-tr/strings.xml | 10 +- app/src/main/res/values-uk/strings.xml | 10 +- app/src/main/res/values-zh-rCN/strings.xml | 10 +- app/src/main/res/values-zh-rTW/strings.xml | 10 +- app/src/main/res/values/strings.xml | 2 + .../display/XServerDisplayActivity.java | 5 +- .../runtime/input/controls/AccentTheme.java | 5 +- .../input/controls/ControlElement.java | 996 +++++++++++++++++- .../runtime/input/controls/VisualStyle.java | 18 +- 27 files changed, 1069 insertions(+), 153 deletions(-) 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 12a261f9c..89ae105f2 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -363,10 +363,12 @@ Seleccionar estilo Original Vidrio - Slate + Pizarra Halo - Glint - Shadow + Destello + Sombra + Retícula + Neón Tema de color Perfil integrado — se creó una copia que puedes editar. Nombre del perfil diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index ac5876c61..950d293af 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1303,10 +1303,12 @@ Installeret sti: Vælg stil Original Glass - Slate + Skifer Halo - Glint - Shadow + Glimt + Skygge + Sigtekryds + Neon Farvetema Indbygget profil - oprettede en kopi, du kan redigere. Nulstil denne profil til dens oprindelige layout? Alle ændringer, du har lavet, går tabt. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d6c8478f4..79a4c8c72 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1303,10 +1303,12 @@ Installierter Pfad: Stil auswählen Original Glass - Slate + Schiefer Halo - Glint - Shadow + Schimmer + Schatten + Fadenkreuz + Neon Farbschema Integriertes Profil - eine bearbeitbare Kopie wurde erstellt. Dieses Profil auf sein ursprüngliches Layout zurücksetzen? Alle vorgenommenen Änderungen gehen verloren. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 0d96eb8da..4da8ef24d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1303,10 +1303,12 @@ Ruta instalada: Seleccionar estilo Original Glass - Slate + Pizarra Halo - Glint - Shadow + Destello + Sombra + Retícula + Neón Tema de color Perfil integrado: se creó una copia que puedes editar. ¿Restablecer este perfil a su diseño original? Se perderán todos los cambios que hayas hecho. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 8cdf1379e..724e81453 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -363,10 +363,12 @@ Valitse tyyli Alkuperäinen Lasi - Slate + Liuske Halo - Glint - Shadow + Kimallus + Varjo + Ristikko + Neon Väriteema Sisäänrakennettu profiili – luotiin kopio, jota voit muokata. Profiilin nimi diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f7c157a13..8edf03c15 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1303,10 +1303,12 @@ Chemin installé : Sélectionner le style Original Glass - Slate + Ardoise Halo - Glint - Shadow + Éclat + Ombre + Réticule + Néon Thème de couleur Profil intégré : une copie modifiable a été créée. Réinitialiser ce profil à sa disposition d\'origine ? Toutes vos modifications seront perdues. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 9960f0361..bff626ce8 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -341,10 +341,12 @@ शैली चुनें मूल Glass - Slate - Halo - Glint - Shadow + स्लेट + हेलो + चमक + छाया + रेटिकल + नियॉन रंग थीम अंतर्निहित प्रोफ़ाइल - एक प्रति बनाई गई जिसे आप संपादित कर सकते हैं। प्रोफ़ाइल नाम diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 50295b4b3..033639b3a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1303,10 +1303,12 @@ Percorso installato: Seleziona stile Originale Glass - Slate - Halo - Glint - Shadow + Ardesia + Alone + Bagliore + Ombra + Reticolo + Neon Tema colore Profilo integrato: è stata creata una copia modificabile. Ripristinare questo profilo al layout originale? Tutte le modifiche apportate andranno perse. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c7e18d4fa..1f03ba184 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -363,10 +363,12 @@ スタイルを選択 オリジナル Glass - Slate - Halo - Glint - Shadow + スレート + ハロー + グリント + シャドウ + レティクル + ネオン カラーテーマ 組み込みプロファイル — 編集可能なコピーを作成しました。 プロファイル名 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d8f0c7107..58b78bef7 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1304,10 +1304,12 @@ 스타일 선택 원본 Glass - Slate - Halo - Glint - Shadow + 슬레이트 + 헤일로 + 글린트 + 섀도우 + 레티클 + 네온 색상 테마 기본 제공 프로필입니다. 편집할 수 있는 복사본을 만들었습니다. 이 프로필을 원래 레이아웃으로 초기화할까요? 변경 사항이 모두 손실됩니다. diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index afbcb4d3a..a06771a55 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -363,10 +363,12 @@ Velg stil Original Glass - Slate + Skifer Halo - Glint - Shadow + Glimt + Skygge + Retikkel + Neon Fargetema Innebygd profil – opprettet en kopi du kan redigere. Profilnavn diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 441218c2e..a39133c13 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1309,10 +1309,12 @@ Zainstalowana ścieżka: Wybierz styl Oryginalny Glass - Slate + Łupek Halo - Glint - Shadow + Błysk + Cień + Celownik + Neon Motyw kolorystyczny Wbudowany profil — utworzono kopię, którą możesz edytować. Zresetować ten profil do oryginalnego układu? Wszystkie wprowadzone zmiany zostaną utracone. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index fbc33c07b..efa34e1ea 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1303,10 +1303,12 @@ Caminho instalado: Selecionar estilo Original Glass - Slate + Ardósia Halo - Glint - Shadow + Brilho + Sombra + Retícula + Neon Tema de cores Perfil integrado - uma cópia editável foi criada. Redefinir este perfil para o layout original? Todas as alterações feitas serão perdidas. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index be2f90071..b59c1f83b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -363,10 +363,12 @@ Selecionar estilo Original Vidro - Slate + Ardósia Halo - Glint - Shadow + Brilho + Sombra + Retícula + Néon Tema de cores Perfil incorporado — foi criada uma cópia que pode editar. Nome do perfil diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 6d15b656d..0e7c37edb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1303,10 +1303,12 @@ Cale instalata: Selectează stilul Original Glass - Slate + Ardezie Halo - Glint - Shadow + Sclipire + Umbră + Reticul + Neon Temă de culoare Profil integrat - a fost creată o copie editabilă. Resetezi acest profil la aspectul original? Toate modificările făcute se vor pierde. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ef3639d02..ab8c17aeb 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -353,10 +353,12 @@ Выберите стиль Оригинал Glass - Slate - Halo - Glint - Shadow + Сланец + Ореол + Блик + Тень + Прицел + Неон Цветовая тема Встроенный профиль — созданную копию можно редактировать. Имя профиля diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 633a1984f..7c57796ff 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -363,10 +363,12 @@ Välj stil Original Glas - Slate + Skiffer Halo - Glint - Shadow + Glimt + Skugga + Hårkors + Neon Färgtema Inbyggd profil — en kopia som du kan redigera har skapats. Profilnamn diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 1514325ed..730a244aa 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -363,10 +363,12 @@ เลือกสไตล์ ต้นฉบับ กระจก - Slate - Halo - Glint - Shadow + สเลท + ฮาโล + ประกาย + เงา + เรติเคิล + นีออน ธีมสี โปรไฟล์ในตัว — สร้างสำเนาที่คุณสามารถแก้ไขได้แล้ว ชื่อโปรไฟล์ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 69b5d16e9..540088cef 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -363,10 +363,12 @@ Stil Seç Orijinal Cam - Slate - Halo - Glint - Shadow + Arduvaz + Hale + Parıltı + Gölge + Retikül + Neon Renk teması Yerleşik profil — düzenleyebileceğiniz bir kopya oluşturuldu. Profil Adı diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 58bdfb2de..ddd3829af 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1309,10 +1309,12 @@ Виберіть стиль Оригінальний Glass - Slate - Halo - Glint - Shadow + Сланець + Ореол + Відблиск + Тінь + Приціл + Неон Колірна тема Вбудований профіль — створено копію, яку можна редагувати. Скинути цей профіль до початкового макета? Усі внесені зміни буде втрачено. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7052b3370..100b43c72 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1303,10 +1303,12 @@ 选择样式 原版 Glass - Slate - Halo - Glint - Shadow + 石板 + 光环 + 微光 + 暗影 + 准星 + 霓虹 颜色主题 内置配置文件 - 已创建可编辑副本。 将此配置文件重置为原始布局?你所做的任何更改都会丢失。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 2cc64f335..69701d457 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1303,10 +1303,12 @@ 選擇樣式 原版 Glass - Slate - Halo - Glint - Shadow + 石板 + 光環 + 微光 + 暗影 + 準星 + 霓虹 顏色主題 內建設定檔 - 已建立可編輯副本。 將此設定檔重設為原始版面配置?你所做的任何變更都會遺失。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 960116f34..d8315bbe4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -368,6 +368,8 @@ Halo Glint Shadow + Reticle + Neon Color Theme Built-in profile — created a copy you can edit. Profile Name diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index b521811c1..a1b10b93f 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -3977,6 +3977,8 @@ private void renderDrawerMenu() { styleNames.add(getString(R.string.input_controls_style_halo)); styleNames.add(getString(R.string.input_controls_style_glint)); styleNames.add(getString(R.string.input_controls_style_shadow)); + styleNames.add(getString(R.string.input_controls_style_reticle)); + styleNames.add(getString(R.string.input_controls_style_neon)); styleNames.add(getString(R.string.input_controls_style_original)); } VisualStyle currentStyle = inputControlsView != null && inputControlsView.getVisualStyle() != null @@ -7569,8 +7571,7 @@ private void persistSelectedProfile(ControlsProfile profile) { } } - // Style/theme picks follow the controlsProfile pattern: remembered on the shortcut when the - // session was launched from one, otherwise as the global default. + // Style/theme picks persist like controlsProfile: on the shortcut if present, else globally. private void persistSelectedStyle(VisualStyle style) { if (shortcut != null) { if (!style.name().equals(shortcut.getExtra("controlsStyle", ""))) { diff --git a/app/src/main/runtime/input/controls/AccentTheme.java b/app/src/main/runtime/input/controls/AccentTheme.java index 54fc6ce45..7d3cc2ce0 100644 --- a/app/src/main/runtime/input/controls/AccentTheme.java +++ b/app/src/main/runtime/input/controls/AccentTheme.java @@ -1,9 +1,6 @@ package com.winlator.cmod.runtime.input.controls; -/** - * Named accent presets for the themed control styles. The preset only supplies the default accent: - * a per-element custom color always wins, then this accent. - */ +/** Accent presets for the themed control styles; a per-element custom color always wins. */ public enum AccentTheme { MONO(0xFFE8ECF2), AZURE(0xFF4DA3FF), diff --git a/app/src/main/runtime/input/controls/ControlElement.java b/app/src/main/runtime/input/controls/ControlElement.java index 40889077f..96f3d8301 100644 --- a/app/src/main/runtime/input/controls/ControlElement.java +++ b/app/src/main/runtime/input/controls/ControlElement.java @@ -411,10 +411,7 @@ private String getDisplayText() { } else return text; } - /** - * Returns the per-element customColor (user explicit override), or {@code -1} if unset (caller - * should fall back to the theme accent or the overlay primary color). - */ + /** Returns the per-element customColor, or {@code -1} if unset (fall back to theme accent). */ private int resolveAccentColor() { return customColor; } @@ -561,6 +558,14 @@ public void draw(Canvas canvas) { drawShadow(canvas); return; } + if (style == VisualStyle.RETICLE) { + drawReticle(canvas); + return; + } + if (style == VisualStyle.NEON) { + drawNeon(canvas); + return; + } if (style != VisualStyle.ORIGINAL) { drawClean(canvas, style); return; @@ -1021,11 +1026,7 @@ private void drawCleanBody(Canvas canvas, Paint paint, Rect bb, float inset) { } } - /** - * Themed visual styles (SLATE, HALO, GLINT) — flat graphite body with the accent color carried - * by the outline, indicators and labels. Geometry (positions, bounding boxes, stick thumb, - * radial menu paths) matches the original code; only the look differs. - */ + /** SLATE/HALO/GLINT — flat graphite bodies with the accent carried by outline, indicators and labels. */ private void drawClean(Canvas canvas, VisualStyle style) { int snappingSize = inputControlsView.getSnappingSize(); Paint paint = inputControlsView.getPaint(); @@ -1519,59 +1520,940 @@ private void drawCleanHotCell( canvas.drawRoundRect(tempRect, r, r, paint); } - private static final int SHADOW_BODY = 0xFF0A1422; - private static final int SHADOW_EDGE = 0xFF84A9D4; + private static void buildChamferRectPath(Path p, float l, float t, float r, float b, float c) { + p.reset(); + p.moveTo(l + c, t); + p.lineTo(r - c, t); + p.lineTo(r, t + c); + p.lineTo(r, b - c); + p.lineTo(r - c, b); + p.lineTo(l + c, b); + p.lineTo(l, b - c); + p.lineTo(l, t + c); + p.close(); + } - private void shadowSoftRect( - Canvas canvas, Paint paint, float l, float t, float r, float b, float radius, - boolean active, int accent, float a, int snappingSize) { - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(Math.max(1f, snappingSize * 0.55f * scale)); - paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((active ? 64 : 16) * a))); - canvas.drawRoundRect(l, t, r, b, radius, radius, paint); - paint.setStrokeWidth(Math.max(1f, snappingSize * 0.22f * scale)); - paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) ((active ? 70 : 28) * a))); - canvas.drawRoundRect(l, t, r, b, radius, radius, paint); - paint.setStyle(Paint.Style.FILL); - paint.setColor(active - ? ColorUtils.setAlphaComponent(accent, (int) (190 * a)) - : ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (135 * a))); - canvas.drawRoundRect(l, t, r, b, radius, radius, paint); - float inset = Math.max(1f, snappingSize * 0.35f * scale); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(Math.max(1f, snappingSize * 0.08f * scale)); - paint.setColor(Color.argb((int) ((active ? 58 : 24) * a), 255, 255, 255)); - canvas.drawRoundRect( - l + inset, t + inset, r - inset, b - inset, - Math.max(1f, radius - inset), Math.max(1f, radius - inset), paint); + /** Flat-top hexagon (vertices every 60 degrees starting at 0). */ + private static void buildHexagonPath(Path p, float cx, float cy, float r) { + p.reset(); + for (int i = 0; i < 6; i++) { + double angle = Math.toRadians(60 * i); + float px = cx + (float) (r * Math.cos(angle)); + float py = cy + (float) (r * Math.sin(angle)); + if (i == 0) p.moveTo(px, py); + else p.lineTo(px, py); + } + p.close(); } - private void shadowSoftCircle( - Canvas canvas, Paint paint, float cx, float cy, float r, - boolean active, int accent, float a, int snappingSize) { - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(Math.max(1f, snappingSize * 0.55f * scale)); - paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((active ? 72 : 18) * a))); - canvas.drawCircle(cx, cy, r, paint); - paint.setStrokeWidth(Math.max(1f, snappingSize * 0.2f * scale)); - paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) ((active ? 74 : 30) * a))); - canvas.drawCircle(cx, cy, r, paint); - paint.setStyle(Paint.Style.FILL); - paint.setColor(active - ? ColorUtils.setAlphaComponent(accent, (int) (190 * a)) - : ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (140 * a))); - canvas.drawCircle(cx, cy, r, paint); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(Math.max(1f, snappingSize * 0.08f * scale)); - paint.setColor(Color.argb((int) ((active ? 44 : 18) * a), 255, 255, 255)); - canvas.drawCircle(cx, cy, r * 0.88f, paint); + /** One faceted D-pad arm plate, built for "up" then rotated into place. */ + private static void buildReticleArmPath( + Path p, float cx, float cy, float half, float armHalf, float gap, float chamfer, int dir) { + p.reset(); + p.moveTo(cx - armHalf + chamfer, cy - half); + p.lineTo(cx + armHalf - chamfer, cy - half); + p.lineTo(cx + armHalf, cy - half + chamfer); + p.lineTo(cx + armHalf, cy - gap - armHalf); + p.lineTo(cx, cy - gap); + p.lineTo(cx - armHalf, cy - gap - armHalf); + p.lineTo(cx - armHalf, cy - half + chamfer); + p.close(); + if (dir != 0) { + shaderMatrix.setRotate(90f * dir, cx, cy); + p.transform(shaderMatrix); + } } - /** - * SHADOW style — soft dark filled bodies built from layered translucent strokes: a wide accent - * halo, a light edge line, a dark fill and a thin inner highlight. Bodies flood with the accent - * while pressed. The D-pad renders as four separate tiles with per-direction feedback. - */ + /** RETICLE — open hairline linework with faceted frames and accent brackets that extend while pressed. */ + private void drawReticle(Canvas canvas) { + int snappingSize = inputControlsView.getSnappingSize(); + Paint paint = inputControlsView.getPaint(); + float effectiveOpacity = inputControlsView.isEditMode() ? Math.max(0.15f, opacity) : opacity; + float overlayOpacity = inputControlsView.getOverlayOpacity(); + float dim = overlayOpacity <= 0.4f + ? 0.28f + (overlayOpacity - 0.1f) * (0.5f / 0.3f) + : 0.78f + (overlayOpacity - 0.4f) * (0.22f / 0.6f); + float a = Mathf.clamp(dim, 0f, 1f) * effectiveOpacity; + boolean engaged = isEngaged(); + Rect boundingBox = getBoundingBox(); + int accent = resolveThemedAccent(); + if (selected && resolveAccentColor() == -1) { + accent = ColorUtils.setAlphaComponent(inputControlsView.getSecondaryColor(), 255); + } + float hairline = Math.max(1.5f, snappingSize * 0.09f * scale); + float bracket = hairline * 2.2f; + int lineColor = Color.argb((int) (185 * a), 255, 255, 255); + int scrimColor = Color.argb((int) (24 * a), 0, 0, 0); + int accentIdle = ColorUtils.setAlphaComponent(accent, (int) (220 * a)); + int accentFull = ColorUtils.setAlphaComponent(accent, (int) (250 * a)); + int labelColor = engaged ? Color.argb((int) (245 * a), 255, 255, 255) : accentIdle; + int ghostFill = ColorUtils.setAlphaComponent(accent, (int) (46 * a)); + float minTextSize = snappingSize * 1.9f * scale; + paint.setStrokeCap(Paint.Cap.BUTT); + paint.setStrokeJoin(Paint.Join.MITER); + + switch (type) { + case BUTTON: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float w = boundingBox.width(); + float h = boundingBox.height(); + if (shape == Shape.CIRCLE) { + float r = w * 0.5f - hairline; + buildHexagonPath(path, cx, cy, r); + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawPath(path, paint); + if (engaged) { + paint.setColor(ghostFill); + canvas.drawPath(path, paint); + } + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(lineColor); + canvas.drawPath(path, paint); + if (engaged) { + buildHexagonPath(path, cx, cy, r - hairline * 3f); + paint.setColor(accentFull); + canvas.drawPath(path, paint); + } + float tick = r * (engaged ? 0.30f : 0.18f); + paint.setStrokeWidth(bracket * (engaged ? 1.3f : 1f)); + paint.setColor(engaged ? accentFull : accentIdle); + canvas.drawLine(cx - r, cy, cx - r - tick, cy, paint); + canvas.drawLine(cx + r, cy, cx + r + tick, cy, paint); + } else { + float chamfer = shape == Shape.SQUARE + ? snappingSize * 0.9f * scale + : h * 0.35f; + buildChamferRectPath( + path, boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + chamfer); + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawPath(path, paint); + if (engaged) { + paint.setColor(ghostFill); + canvas.drawPath(path, paint); + } + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(lineColor); + canvas.drawPath(path, paint); + float len = Math.min(w, h) * (engaged ? 0.34f : 0.22f); + paint.setStrokeWidth(engaged ? hairline * 3f : bracket); + paint.setColor(engaged ? accentFull : accentIdle); + if (shape == Shape.SQUARE) { + drawCornerBrackets(canvas, paint, boundingBox, len); + } else { + canvas.drawLine( + boundingBox.left, boundingBox.bottom - chamfer, + boundingBox.left + chamfer, boundingBox.bottom, paint); + canvas.drawLine( + boundingBox.left, boundingBox.top + chamfer, + boundingBox.left + chamfer, boundingBox.top, paint); + canvas.drawLine( + boundingBox.right, boundingBox.bottom - chamfer, + boundingBox.right - chamfer, boundingBox.bottom, paint); + canvas.drawLine( + boundingBox.right, boundingBox.top + chamfer, + boundingBox.right - chamfer, boundingBox.top, paint); + } + } + if (iconId > 0) { + drawIcon( + canvas, cx, cy, w, h, iconId, true, + engaged ? inputControlsView.getColorFilter() : accentFilter(accent)); + } else { + String label = getDisplayText(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(labelColor); + paint.setLetterSpacing(0.08f); + paint.setTextSize( + Math.min(getTextSizeForWidth(paint, label, w * 0.62f), minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, cx, (cy - ((paint.descent() + paint.ascent()) * 0.5f)), paint); + paint.setLetterSpacing(0f); + } + break; + } + case D_PAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float base = Math.min(boundingBox.width(), boundingBox.height()); + float half = base * 0.5f; + float armHalf = base * 0.15f; + float gap = base * 0.10f; + float chamfer = armHalf * 0.56f; + boolean hasStates = engaged && states.length >= 4; + for (int i = 0; i < 4; i++) { + boolean hot = hasStates && states[i]; + buildReticleArmPath(path, cx, cy, half, armHalf, gap, chamfer, i); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? ghostFill : scrimColor); + canvas.drawPath(path, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(lineColor); + canvas.drawPath(path, paint); + int dx = i == 1 ? 1 : i == 3 ? -1 : 0; + int dy = i == 2 ? 1 : i == 0 ? -1 : 0; + float pos = gap + 0.62f * (half - gap); + float chevSize = base * 0.045f; + paint.setStrokeWidth(bracket); + paint.setColor(hot ? Color.argb((int) (250 * a), 255, 255, 255) : accentIdle); + int chevrons = hot ? 2 : 1; + for (int k = 0; k < chevrons; k++) { + float off = pos + (k - (chevrons - 1) * 0.5f) * chevSize * 2.4f; + float ax = cx + dx * off; + float ay = cy + dy * off; + if (dy != 0) { + canvas.drawLine(ax - chevSize, ay - dy * chevSize, ax, ay + dy * chevSize, paint); + canvas.drawLine(ax + chevSize, ay - dy * chevSize, ax, ay + dy * chevSize, paint); + } else { + canvas.drawLine(ax - dx * chevSize, ay - chevSize, ax + dx * chevSize, ay, paint); + canvas.drawLine(ax - dx * chevSize, ay + chevSize, ax + dx * chevSize, ay, paint); + } + } + } + float dm = base * 0.045f; + path.reset(); + path.moveTo(cx, cy - dm); + path.lineTo(cx + dm, cy); + path.lineTo(cx, cy + dm); + path.lineTo(cx - dm, cy); + path.close(); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(accentIdle); + canvas.drawPath(path, paint); + break; + } + case STICK: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float r = boundingBox.height() * 0.5f; + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawCircle(cx, cy, r, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(lineColor); + canvas.drawCircle(cx, cy, r, paint); + PointF pos = getCurrentPosition(); + float dxp = pos.x - cx; + float dyp = pos.y - cy; + int hotTick = -1; + if (engaged && (dxp != 0f || dyp != 0f)) { + hotTick = Math.abs(dxp) >= Math.abs(dyp) ? (dxp >= 0 ? 1 : 3) : (dyp >= 0 ? 2 : 0); + } + float tickLen = snappingSize * 0.8f * scale; + paint.setStrokeWidth(bracket); + for (int i = 0; i < 4; i++) { + int dx = i == 1 ? 1 : i == 3 ? -1 : 0; + int dy = i == 2 ? 1 : i == 0 ? -1 : 0; + paint.setColor(i == hotTick ? accentFull : accentIdle); + canvas.drawLine( + cx + dx * (r - tickLen * 0.5f), cy + dy * (r - tickLen * 0.5f), + cx + dx * (r + tickLen * 0.5f), cy + dy * (r + tickLen * 0.5f), paint); + } + tempRect.set(cx - r * 0.55f, cy - r * 0.55f, cx + r * 0.55f, cy + r * 0.55f); + paint.setStrokeWidth(hairline); + paint.setColor(engaged ? accentFull : accentIdle); + for (int i = 0; i < 8; i++) { + canvas.drawArc(tempRect, i * 45f - 9f, 18f, false, paint); + } + if (engaged) { + paint.setColor(accentFull); + canvas.drawLine(cx, cy, pos.x, pos.y, paint); + } + float thumbR = snappingSize * 2.2f * scale; + buildHexagonPath(path, pos.x, pos.y, thumbR); + if (engaged) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (90 * a))); + canvas.drawPath(path, paint); + } + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(engaged ? accentFull : accentIdle); + canvas.drawPath(path, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(lineColor); + canvas.drawCircle(pos.x, pos.y, hairline, paint); + break; + } + case TRACKPAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float chamfer = boundingBox.height() * 0.22f; + buildChamferRectPath( + path, boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + chamfer); + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawPath(path, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(lineColor); + canvas.drawPath(path, paint); + float len = Math.min(boundingBox.width(), boundingBox.height()) * 0.22f; + paint.setStrokeWidth(bracket); + paint.setColor(accentIdle); + drawCornerBrackets(canvas, paint, boundingBox, len); + float gap = hairline * 2.2f; + float cross = snappingSize * 1.2f * scale; + paint.setStrokeWidth(hairline); + paint.setColor(engaged ? accentFull : accentIdle); + canvas.drawLine(cx - cross, cy, cx - gap, cy, paint); + canvas.drawLine(cx + gap, cy, cx + cross, cy, paint); + canvas.drawLine(cx, cy - cross, cx, cy - gap, paint); + canvas.drawLine(cx, cy + gap, cx, cy + cross, paint); + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(cx, cy, hairline, paint); + break; + } + case RANGE_BUTTON: { + Range range = getRange(); + float chamfer = (orientation == 0 ? boundingBox.height() : boundingBox.width()) * 0.30f; + float elementSize = scroller.getElementSize(); + float scrollOffset = scroller.getScrollOffset(); + byte[] rangeIndex = scroller.getRangeIndex(); + buildChamferRectPath( + path, boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + chamfer); + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawPath(path, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(lineColor); + canvas.drawPath(path, paint); + + canvas.save(); + canvas.clipPath(path); + int pressedIndex = + currentPointerId != -1 && !scroller.isScrolling() ? scroller.getBindingIndex() : -1; + int dividerColor = Color.argb((int) (70 * a), 255, 255, 255); + int hotTextColor = Color.argb((int) (245 * a), 255, 255, 255); + paint.setLetterSpacing(0.08f); + + if (orientation == 0) { + float lineTop = boundingBox.top + boundingBox.height() * 0.225f; + float lineBottom = boundingBox.bottom - boundingBox.height() * 0.225f; + float startX = boundingBox.left - (scrollOffset % elementSize); + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 0.8f); + paint.setColor(dividerColor); + if (startX > boundingBox.left && startX < boundingBox.right) + canvas.drawLine(startX, lineTop, startX, lineBottom, paint); + if (startX < boundingBox.right && startX + elementSize > boundingBox.left) { + boolean hot = index == pressedIndex; + if (hot) { + paint.setStrokeWidth(hairline * 2.5f); + paint.setColor(accentFull); + float uy = boundingBox.bottom - boundingBox.height() * 0.15f; + canvas.drawLine( + startX + elementSize * 0.2f, uy, startX + elementSize * 0.8f, uy, paint); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : accentIdle); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, elementSize - snappingSize * 0.5f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + startX + elementSize * 0.5f, + (y - ((paint.descent() + paint.ascent()) * 0.5f)), + paint); + } + startX += elementSize; + } + } else { + float lineLeft = boundingBox.left + boundingBox.width() * 0.225f; + float lineRight = boundingBox.right - boundingBox.width() * 0.225f; + float startY = boundingBox.top - (scrollOffset % elementSize); + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 0.8f); + paint.setColor(dividerColor); + if (startY > boundingBox.top && startY < boundingBox.bottom) + canvas.drawLine(lineLeft, startY, lineRight, startY, paint); + if (startY < boundingBox.bottom && startY + elementSize > boundingBox.top) { + boolean hot = index == pressedIndex; + if (hot) { + paint.setStrokeWidth(hairline * 2.5f); + paint.setColor(accentFull); + float ux = boundingBox.right - boundingBox.width() * 0.15f; + canvas.drawLine( + ux, startY + elementSize * 0.2f, ux, startY + elementSize * 0.8f, paint); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : accentIdle); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, boundingBox.width() - snappingSize * 0.5f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + x, + startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), + paint); + } + startY += elementSize; + } + } + paint.setLetterSpacing(0f); + canvas.restore(); + break; + } + case RADIAL_MENU: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float radius = boundingBox.width() * 0.5f; + + if (radialMenuExpanded && bindings.length > 0 && radius > 0) { + float innerRadius = radius + snappingSize * 0.5f; + float outerRadius = boundingBox.width() + (snappingSize * scale); + float angleStep = 360.0f / bindings.length; + + if (paths == null || paths.length != bindings.length) { + paths = new Path[bindings.length]; + RectF outerRect = new RectF(cx - outerRadius, cy - outerRadius, cx + outerRadius, cy + outerRadius); + RectF innerRect = new RectF(cx - innerRadius, cy - innerRadius, cx + innerRadius, cy + innerRadius); + for (int i = 0; i < bindings.length; i++) { + float startAngle = -90.0f + i * angleStep; + paths[i] = new Path(); + paths[i].arcTo(outerRect, startAngle, angleStep, true); + paths[i].arcTo(innerRect, startAngle + angleStep, -angleStep, false); + paths[i].close(); + } + } + + if (paths != null && paths.length == bindings.length) { + for (int i = 0; i < bindings.length; i++) { + boolean active = i == activeRadialBindingIndex; + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? ColorUtils.setAlphaComponent(accent, (int) (70 * a)) + : scrimColor); + canvas.drawPath(paths[i], paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(lineColor); + canvas.drawPath(paths[i], paint); + + float middleAngle = (float) Math.toRadians(-90.0f + i * angleStep + angleStep * 0.5f); + float labelRadius = (innerRadius + outerRadius) * 0.5f; + float labelX = (float) (cx + Math.cos(middleAngle) * labelRadius); + float labelY = (float) (cy + Math.sin(middleAngle) * labelRadius); + String label = getBindingShortText(i); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active ? Color.argb((int) (240 * a), 255, 255, 255) : accentIdle); + paint.setTextSize(snappingSize * 1.2f * scale); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, labelX, labelY - ((paint.descent() + paint.ascent()) * 0.5f), paint); + } + } + } + + buildHexagonPath(path, cx, cy, radius - hairline); + paint.setStyle(Paint.Style.FILL); + paint.setColor(engaged ? ghostFill : scrimColor); + canvas.drawPath(path, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(lineColor); + canvas.drawPath(path, paint); + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), + iconId > 0 ? iconId : 34, true, + engaged ? inputControlsView.getColorFilter() : accentFilter(accent)); + break; + } + } + } + + /** L-shaped corner brackets hugging the four corners of {@code bb}. */ + private static void drawCornerBrackets(Canvas canvas, Paint paint, Rect bb, float len) { + canvas.drawLine(bb.left, bb.top, bb.left + len, bb.top, paint); + canvas.drawLine(bb.left, bb.top, bb.left, bb.top + len, paint); + canvas.drawLine(bb.right, bb.top, bb.right - len, bb.top, paint); + canvas.drawLine(bb.right, bb.top, bb.right, bb.top + len, paint); + canvas.drawLine(bb.left, bb.bottom, bb.left + len, bb.bottom, paint); + canvas.drawLine(bb.left, bb.bottom, bb.left, bb.bottom - len, paint); + canvas.drawLine(bb.right, bb.bottom, bb.right - len, bb.bottom, paint); + canvas.drawLine(bb.right, bb.bottom, bb.right, bb.bottom - len, paint); + } + + private static final int NEON_SCRIM = 0xFF0A0A16; + + /** Layered-stroke glow around {@code p}: wide faint halos under a crisp core line. */ + private void neonStrokePath( + Canvas canvas, Paint paint, Path p, int color, float core, float a, boolean pressed) { + paint.setStyle(Paint.Style.STROKE); + paint.setColor(ColorUtils.setAlphaComponent(color, (int) (18 * a))); + paint.setStrokeWidth(core * 4.2f); + canvas.drawPath(p, paint); + paint.setColor(ColorUtils.setAlphaComponent(color, (int) (36 * a))); + paint.setStrokeWidth(core * 2.6f); + canvas.drawPath(p, paint); + paint.setColor(ColorUtils.setAlphaComponent(color, (int) (66 * a))); + paint.setStrokeWidth(core * 1.6f); + canvas.drawPath(p, paint); + if (pressed) { + paint.setColor(ColorUtils.setAlphaComponent(color, (int) (26 * a))); + paint.setStrokeWidth(core * 6f); + canvas.drawPath(p, paint); + } + paint.setColor(ColorUtils.setAlphaComponent(color, (int) (235 * a))); + paint.setStrokeWidth(core); + canvas.drawPath(p, paint); + if (pressed) { + paint.setColor(Color.argb((int) (235 * a), 255, 255, 255)); + paint.setStrokeWidth(core * 0.55f); + canvas.drawPath(p, paint); + } + } + + /** NEON — hollow chamfered wireframes with a layered-stroke glow that ignites white-hot on press. */ + private void drawNeon(Canvas canvas) { + int snappingSize = inputControlsView.getSnappingSize(); + Paint paint = inputControlsView.getPaint(); + float effectiveOpacity = inputControlsView.isEditMode() ? Math.max(0.15f, opacity) : opacity; + float overlayOpacity = inputControlsView.getOverlayOpacity(); + float dim = overlayOpacity <= 0.4f + ? 0.28f + (overlayOpacity - 0.1f) * (0.5f / 0.3f) + : 0.78f + (overlayOpacity - 0.4f) * (0.22f / 0.6f); + float a = Mathf.clamp(dim, 0f, 1f) * effectiveOpacity; + boolean engaged = isEngaged(); + Rect boundingBox = getBoundingBox(); + int accent = resolveThemedAccent(); + if (selected && resolveAccentColor() == -1) { + accent = ColorUtils.setAlphaComponent(inputControlsView.getSecondaryColor(), 255); + } + int secondary = ColorUtils.blendARGB(accent, Color.WHITE, 0.35f); + float core = Math.max(1.5f, snappingSize * 0.14f * scale); + int scrimColor = ColorUtils.setAlphaComponent(NEON_SCRIM, (int) (61 * a)); + int washColor = ColorUtils.setAlphaComponent(accent, (int) (33 * a)); + int labelColor = engaged + ? Color.argb((int) (245 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (235 * a)); + float minTextSize = snappingSize * 2 * scale; + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeJoin(Paint.Join.ROUND); + + switch (type) { + case BUTTON: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float w = boundingBox.width(); + float h = boundingBox.height(); + if (shape == Shape.CIRCLE) { + float r = w * 0.5f - core; + path.reset(); + path.addCircle(cx, cy, r, Path.Direction.CW); + } else { + float chamfer = shape == Shape.SQUARE + ? snappingSize * 0.8f * scale + : shape == Shape.RECT ? h * 0.25f : h * 0.30f; + buildChamferRectPath( + path, boundingBox.left + core, boundingBox.top + core, + boundingBox.right - core, boundingBox.bottom - core, chamfer); + } + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawPath(path, paint); + if (engaged) { + paint.setColor(washColor); + canvas.drawPath(path, paint); + } + neonStrokePath(canvas, paint, path, accent, core, a, engaged); + if (shape == Shape.CIRCLE) { + float r = w * 0.5f - core; + tempRect.set( + cx - r - core * 1.8f, cy - r - core * 1.8f, + cx + r + core * 1.8f, cy + r + core * 1.8f); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(core * 0.7f); + paint.setColor(ColorUtils.setAlphaComponent(secondary, (int) (150 * a))); + canvas.drawArc(tempRect, -80f, 60f, false, paint); + } + if (iconId > 0) { + drawIcon( + canvas, cx, cy, w, h, iconId, true, + engaged ? inputControlsView.getColorFilter() : accentFilter(accent)); + } else { + String label = getDisplayText(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(labelColor); + paint.setTextSize( + Math.min(getTextSizeForWidth(paint, label, w * 0.62f), minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, cx, (cy - ((paint.descent() + paint.ascent()) * 0.5f)), paint); + } + break; + } + case D_PAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float base = Math.min(boundingBox.width(), boundingBox.height()); + float half = base * 0.5f - core; + float arm = base * 0.17f; + float ch = base * 0.075f; + path.reset(); + path.moveTo(cx - arm, cy - half + ch); + path.lineTo(cx - arm + ch, cy - half); + path.lineTo(cx + arm - ch, cy - half); + path.lineTo(cx + arm, cy - half + ch); + path.lineTo(cx + arm, cy - arm); + path.lineTo(cx + half - ch, cy - arm); + path.lineTo(cx + half, cy - arm + ch); + path.lineTo(cx + half, cy + arm - ch); + path.lineTo(cx + half - ch, cy + arm); + path.lineTo(cx + arm, cy + arm); + path.lineTo(cx + arm, cy + half - ch); + path.lineTo(cx + arm - ch, cy + half); + path.lineTo(cx - arm + ch, cy + half); + path.lineTo(cx - arm, cy + half - ch); + path.lineTo(cx - arm, cy + arm); + path.lineTo(cx - half + ch, cy + arm); + path.lineTo(cx - half, cy + arm - ch); + path.lineTo(cx - half, cy - arm + ch); + path.lineTo(cx - half + ch, cy - arm); + path.lineTo(cx - arm, cy - arm); + path.close(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawPath(path, paint); + neonStrokePath(canvas, paint, path, accent, core, a, false); + + boolean hasStates = engaged && states.length >= 4; + float chev = base * 0.043f; + for (int i = 0; i < 4; i++) { + boolean hot = hasStates && states[i]; + int dx = i == 1 ? 1 : i == 3 ? -1 : 0; + int dy = i == 2 ? 1 : i == 0 ? -1 : 0; + float ax = cx + dx * half * 0.60f; + float ay = cy + dy * half * 0.60f; + paint.setStyle(Paint.Style.STROKE); + if (hot) { + paint.setStrokeWidth(core * 2.8f); + paint.setColor(ColorUtils.setAlphaComponent(secondary, (int) (90 * a))); + if (dy != 0) { + canvas.drawLine(ax - chev, ay - dy * chev, ax, ay + dy * chev, paint); + canvas.drawLine(ax + chev, ay - dy * chev, ax, ay + dy * chev, paint); + } else { + canvas.drawLine(ax - dx * chev, ay - chev, ax + dx * chev, ay, paint); + canvas.drawLine(ax - dx * chev, ay + chev, ax + dx * chev, ay, paint); + } + } + paint.setStrokeWidth(core); + paint.setColor(hot + ? Color.argb((int) (245 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(secondary, (int) (190 * a))); + if (dy != 0) { + canvas.drawLine(ax - chev, ay - dy * chev, ax, ay + dy * chev, paint); + canvas.drawLine(ax + chev, ay - dy * chev, ax, ay + dy * chev, paint); + } else { + canvas.drawLine(ax - dx * chev, ay - chev, ax + dx * chev, ay, paint); + canvas.drawLine(ax - dx * chev, ay + chev, ax + dx * chev, ay, paint); + } + } + float dm = base * 0.055f; + path.reset(); + path.moveTo(cx, cy - dm); + path.lineTo(cx + dm, cy); + path.lineTo(cx, cy + dm); + path.lineTo(cx - dm, cy); + path.close(); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(core * 0.7f); + paint.setColor(ColorUtils.setAlphaComponent(secondary, (int) (100 * a))); + canvas.drawPath(path, paint); + break; + } + case STICK: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float r = boundingBox.height() * 0.5f - core; + path.reset(); + path.addCircle(cx, cy, r, Path.Direction.CW); + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(NEON_SCRIM, (int) (46 * a))); + canvas.drawPath(path, paint); + neonStrokePath(canvas, paint, path, accent, core * 0.8f, a, false); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(core * 0.8f); + paint.setColor(ColorUtils.setAlphaComponent(secondary, (int) (165 * a))); + float tick = snappingSize * 0.5f * scale; + canvas.drawLine(cx, cy - r - tick, cx, cy - r + tick, paint); + canvas.drawLine(cx, cy + r - tick, cx, cy + r + tick, paint); + canvas.drawLine(cx - r - tick, cy, cx - r + tick, cy, paint); + canvas.drawLine(cx + r - tick, cy, cx + r + tick, cy, paint); + paint.setStrokeWidth(Math.max(1f, core * 0.5f)); + paint.setColor(ColorUtils.setAlphaComponent(secondary, (int) (150 * a))); + canvas.drawCircle(cx, cy, r * 0.55f, paint); + PointF pos = getCurrentPosition(); + float thumbR = snappingSize * 2.6f * scale; + path.reset(); + path.addCircle(pos.x, pos.y, thumbR, Path.Direction.CW); + paint.setStyle(Paint.Style.FILL); + paint.setColor(engaged ? washColor : scrimColor); + canvas.drawPath(path, paint); + neonStrokePath(canvas, paint, path, accent, core * 0.8f, a, engaged); + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.argb((int) (240 * a), 255, 255, 255)); + canvas.drawCircle(pos.x, pos.y, core, paint); + break; + } + case TRACKPAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float chamfer = boundingBox.height() * 0.20f; + buildChamferRectPath( + path, boundingBox.left + core, boundingBox.top + core, + boundingBox.right - core, boundingBox.bottom - core, chamfer); + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawPath(path, paint); + neonStrokePath(canvas, paint, path, accent, core, a, engaged); + float dm = snappingSize * 1.1f * scale; + path.reset(); + path.moveTo(cx, cy - dm); + path.lineTo(cx + dm, cy); + path.lineTo(cx, cy + dm); + path.lineTo(cx - dm, cy); + path.close(); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(core * 0.7f); + paint.setColor(ColorUtils.setAlphaComponent(secondary, (int) ((engaged ? 220 : 150) * a))); + canvas.drawPath(path, paint); + break; + } + case RANGE_BUTTON: { + Range range = getRange(); + float chamfer = (orientation == 0 ? boundingBox.height() : boundingBox.width()) * 0.30f; + float elementSize = scroller.getElementSize(); + float scrollOffset = scroller.getScrollOffset(); + byte[] rangeIndex = scroller.getRangeIndex(); + buildChamferRectPath( + path, boundingBox.left + core, boundingBox.top + core, + boundingBox.right - core, boundingBox.bottom - core, chamfer); + paint.setStyle(Paint.Style.FILL); + paint.setColor(scrimColor); + canvas.drawPath(path, paint); + neonStrokePath(canvas, paint, path, accent, core, a, false); + + canvas.save(); + canvas.clipPath(path); + int pressedIndex = + currentPointerId != -1 && !scroller.isScrolling() ? scroller.getBindingIndex() : -1; + int dividerColor = ColorUtils.setAlphaComponent(accent, (int) (80 * a)); + int hotTextColor = Color.argb((int) (245 * a), 255, 255, 255); + int cellTextColor = ColorUtils.setAlphaComponent(accent, (int) (220 * a)); + int hotFill = ColorUtils.setAlphaComponent(secondary, (int) (41 * a)); + + if (orientation == 0) { + float lineTop = boundingBox.top + boundingBox.height() * 0.2f; + float lineBottom = boundingBox.bottom - boundingBox.height() * 0.2f; + float startX = boundingBox.left - (scrollOffset % elementSize); + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, core * 0.4f)); + paint.setColor(dividerColor); + if (startX > boundingBox.left && startX < boundingBox.right) + canvas.drawLine(startX, lineTop, startX, lineBottom, paint); + if (startX < boundingBox.right && startX + elementSize > boundingBox.left) { + boolean hot = index == pressedIndex; + if (hot) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(hotFill); + canvas.drawRect( + startX, boundingBox.top, startX + elementSize, boundingBox.bottom, paint); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : cellTextColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, elementSize - snappingSize * 0.5f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + startX + elementSize * 0.5f, + (y - ((paint.descent() + paint.ascent()) * 0.5f)), + paint); + } + startX += elementSize; + } + } else { + float lineLeft = boundingBox.left + boundingBox.width() * 0.2f; + float lineRight = boundingBox.right - boundingBox.width() * 0.2f; + float startY = boundingBox.top - (scrollOffset % elementSize); + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, core * 0.4f)); + paint.setColor(dividerColor); + if (startY > boundingBox.top && startY < boundingBox.bottom) + canvas.drawLine(lineLeft, startY, lineRight, startY, paint); + if (startY < boundingBox.bottom && startY + elementSize > boundingBox.top) { + boolean hot = index == pressedIndex; + if (hot) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(hotFill); + canvas.drawRect( + boundingBox.left, startY, boundingBox.right, startY + elementSize, paint); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : cellTextColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, boundingBox.width() - snappingSize * 0.5f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + x, + startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), + paint); + } + startY += elementSize; + } + } + canvas.restore(); + break; + } + case RADIAL_MENU: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float radius = boundingBox.width() * 0.5f; + + if (radialMenuExpanded && bindings.length > 0 && radius > 0) { + float innerRadius = radius + snappingSize * 0.5f; + float outerRadius = boundingBox.width() + (snappingSize * scale); + float angleStep = 360.0f / bindings.length; + + if (paths == null || paths.length != bindings.length) { + paths = new Path[bindings.length]; + RectF outerRect = new RectF(cx - outerRadius, cy - outerRadius, cx + outerRadius, cy + outerRadius); + RectF innerRect = new RectF(cx - innerRadius, cy - innerRadius, cx + innerRadius, cy + innerRadius); + for (int i = 0; i < bindings.length; i++) { + float startAngle = -90.0f + i * angleStep; + paths[i] = new Path(); + paths[i].arcTo(outerRect, startAngle, angleStep, true); + paths[i].arcTo(innerRect, startAngle + angleStep, -angleStep, false); + paths[i].close(); + } + } + + if (paths != null && paths.length == bindings.length) { + for (int i = 0; i < bindings.length; i++) { + boolean active = i == activeRadialBindingIndex; + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? ColorUtils.setAlphaComponent(secondary, (int) (55 * a)) + : scrimColor); + canvas.drawPath(paths[i], paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, core * 0.5f)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (110 * a))); + canvas.drawPath(paths[i], paint); + + float middleAngle = (float) Math.toRadians(-90.0f + i * angleStep + angleStep * 0.5f); + float labelRadius = (innerRadius + outerRadius) * 0.5f; + float labelX = (float) (cx + Math.cos(middleAngle) * labelRadius); + float labelY = (float) (cy + Math.sin(middleAngle) * labelRadius); + String label = getBindingShortText(i); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? Color.argb((int) (240 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (215 * a))); + paint.setTextSize(snappingSize * 1.2f * scale); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, labelX, labelY - ((paint.descent() + paint.ascent()) * 0.5f), paint); + } + } + } + + path.reset(); + path.addCircle(cx, cy, radius - core, Path.Direction.CW); + paint.setStyle(Paint.Style.FILL); + paint.setColor(engaged ? washColor : scrimColor); + canvas.drawPath(path, paint); + neonStrokePath(canvas, paint, path, accent, core, a, engaged); + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), + iconId > 0 ? iconId : 34, true, + engaged ? inputControlsView.getColorFilter() : accentFilter(accent)); + break; + } + } + paint.setStrokeCap(Paint.Cap.BUTT); + paint.setStrokeJoin(Paint.Join.MITER); + } + + private static final int SHADOW_BODY = 0xFF0A1422; + private static final int SHADOW_EDGE = 0xFF84A9D4; + + private void shadowSoftRect( + Canvas canvas, Paint paint, float l, float t, float r, float b, float radius, + boolean active, int accent, float a, int snappingSize) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.55f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((active ? 64 : 16) * a))); + canvas.drawRoundRect(l, t, r, b, radius, radius, paint); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.22f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) ((active ? 70 : 28) * a))); + canvas.drawRoundRect(l, t, r, b, radius, radius, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? ColorUtils.setAlphaComponent(accent, (int) (190 * a)) + : ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (135 * a))); + canvas.drawRoundRect(l, t, r, b, radius, radius, paint); + float inset = Math.max(1f, snappingSize * 0.35f * scale); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.08f * scale)); + paint.setColor(Color.argb((int) ((active ? 58 : 24) * a), 255, 255, 255)); + canvas.drawRoundRect( + l + inset, t + inset, r - inset, b - inset, + Math.max(1f, radius - inset), Math.max(1f, radius - inset), paint); + } + + private void shadowSoftCircle( + Canvas canvas, Paint paint, float cx, float cy, float r, + boolean active, int accent, float a, int snappingSize) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.55f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((active ? 72 : 18) * a))); + canvas.drawCircle(cx, cy, r, paint); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.2f * scale)); + paint.setColor(ColorUtils.setAlphaComponent(SHADOW_EDGE, (int) ((active ? 74 : 30) * a))); + canvas.drawCircle(cx, cy, r, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? ColorUtils.setAlphaComponent(accent, (int) (190 * a)) + : ColorUtils.setAlphaComponent(SHADOW_BODY, (int) (140 * a))); + canvas.drawCircle(cx, cy, r, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, snappingSize * 0.08f * scale)); + paint.setColor(Color.argb((int) ((active ? 44 : 18) * a), 255, 255, 255)); + canvas.drawCircle(cx, cy, r * 0.88f, paint); + } + + /** SHADOW — soft dark layered-stroke bodies that flood with the accent while pressed; four-tile D-pad. */ private void drawShadow(Canvas canvas) { int snappingSize = inputControlsView.getSnappingSize(); Paint paint = inputControlsView.getPaint(); diff --git a/app/src/main/runtime/input/controls/VisualStyle.java b/app/src/main/runtime/input/controls/VisualStyle.java index 95fe9c804..796149da3 100644 --- a/app/src/main/runtime/input/controls/VisualStyle.java +++ b/app/src/main/runtime/input/controls/VisualStyle.java @@ -1,24 +1,14 @@ package com.winlator.cmod.runtime.input.controls; -/** - * Visual rendering style for on-screen virtual gamepad elements. Enum order is the picker order. - * - *

SLATE (default) is a flat graphite surface with an accent hairline outline and a bloom glow - * when pressed. GAMEHUB ("Glass") is a dark translucent glass: light rim, brighter rim and inner - * fill when pressed, soft drop shadow. HALO keeps a quiet base and lights an inset accent ring on - * press. GLINT is a neutral frame with a small accent indicator that sweeps into a full ring while - * held. SHADOW is a soft dark filled look built from layered translucent strokes, with a - * four-tile D-pad, whose bodies flood with the accent while pressed. ORIGINAL preserves the - * historic look (translucent white strokes, lightly filled when engaged) and is listed last. - * - *

The actual drawing branches on this enum inside {@link ControlElement#draw}. - */ +/** Rendering styles for the on-screen controls; enum order is the picker order and drawing branches on this in {@link ControlElement#draw}. */ public enum VisualStyle { SLATE, GAMEHUB, HALO, GLINT, SHADOW, + RETICLE, + NEON, ORIGINAL; public static VisualStyle fromPreference(String name) { @@ -31,6 +21,6 @@ public static VisualStyle fromPreference(String name) { } public static String[] displayNames() { - return new String[] {"Slate", "Glass", "Halo", "Glint", "Shadow", "Original"}; + return new String[] {"Slate", "Glass", "Halo", "Glint", "Shadow", "Reticle", "Neon", "Original"}; } } From bc3f13395a2ca32074321ddee278a637ba2ff24b Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Tue, 14 Jul 2026 00:22:32 +0000 Subject: [PATCH 6/8] Cache control labels; octagonal Neon buttons - Button, radial-menu and range-cell label strings are cached (invalidated on text/binding edits), making the draw path fully allocation-free - Neon circle buttons render as regular octagons to match the chamfered set --- .../input/controls/ControlElement.java | 91 ++++++++++++++----- 1 file changed, 70 insertions(+), 21 deletions(-) diff --git a/app/src/main/runtime/input/controls/ControlElement.java b/app/src/main/runtime/input/controls/ControlElement.java index 96f3d8301..2f16b3c7b 100644 --- a/app/src/main/runtime/input/controls/ControlElement.java +++ b/app/src/main/runtime/input/controls/ControlElement.java @@ -113,6 +113,8 @@ public static String[] names() { private PointF currentPosition; private PointF trackpadOrigin; private int customColor = -1; + private String displayTextCache; + private String[] bindingTextCache; private RangeScroller scroller; private CubicBezierInterpolator interpolator; private Object touchTime; @@ -151,6 +153,7 @@ private void reset() { boundingBoxNeedsUpdate = true; radialMenuExpanded = false; paths = null; + invalidateLabelCache(); } public Type getType() { @@ -175,6 +178,7 @@ public void setBindingCount(int bindingCount) { states = new boolean[bindingCount]; boundingBoxNeedsUpdate = true; paths = null; + invalidateLabelCache(); } public Shape getShape() { @@ -255,11 +259,13 @@ public void setBindingAt(int index, Binding binding) { } bindings[index] = binding; paths = null; + invalidateLabelCache(); } public void setBinding(Binding binding) { Arrays.fill(bindings, binding); paths = null; + invalidateLabelCache(); } public float getScale() { @@ -310,6 +316,7 @@ public String getText() { public void setText(String text) { this.text = text != null ? text : ""; + invalidateLabelCache(); } public byte getIconId() { @@ -395,20 +402,33 @@ private Rect computeBoundingBox() { } private String getDisplayText() { + if (displayTextCache != null) return displayTextCache; + String result; // Per-element text always wins (user explicit override). if (text != null && !text.isEmpty()) { - return text; + result = text; + } else { + Binding binding = getBindingAt(0); + String bumper = bumperTriggerLabel(binding); + if (bumper != null) { + result = bumper; + } else { + String s = binding.toString().replace("NUMPAD ", "NP").replace("BUTTON ", ""); + if (s.length() > 7) { + String[] parts = s.split(" "); + StringBuilder sb = new StringBuilder(); + for (String part : parts) sb.append(part.charAt(0)); + result = (binding.isMouse() ? "M" : "") + sb; + } else result = s; + } } - Binding binding = getBindingAt(0); - String bumper = bumperTriggerLabel(binding); - if (bumper != null) return bumper; - String text = binding.toString().replace("NUMPAD ", "NP").replace("BUTTON ", ""); - if (text.length() > 7) { - String[] parts = text.split(" "); - StringBuilder sb = new StringBuilder(); - for (String part : parts) sb.append(part.charAt(0)); - return (binding.isMouse() ? "M" : "") + sb; - } else return text; + displayTextCache = result; + return result; + } + + private void invalidateLabelCache() { + displayTextCache = null; + bindingTextCache = null; } /** Returns the per-element customColor, or {@code -1} if unset (fall back to theme accent). */ @@ -434,17 +454,30 @@ private static String bumperTriggerLabel(Binding binding) { } private String getBindingShortText(int index) { + String[] cache = bindingTextCache; + if (cache != null && index < cache.length && cache[index] != null) return cache[index]; Binding binding = getBindingAt(index); String bumper = bumperTriggerLabel(binding); - if (bumper != null) return bumper; - String text = binding.toString().replace("NUMPAD ", "NP").replace("BUTTON ", "").replace("KEY_", "").replace("GAMEPAD_", ""); - if (text.length() > 6) { - String[] parts = text.split("_"); - StringBuilder sb = new StringBuilder(); - for (String part : parts) if (!part.isEmpty()) sb.append(part.charAt(0)); - return (binding.isMouse() ? "M" : "") + sb.toString(); + String result; + if (bumper != null) { + result = bumper; + } else { + String text = binding.toString().replace("NUMPAD ", "NP").replace("BUTTON ", "").replace("KEY_", "").replace("GAMEPAD_", ""); + if (text.length() > 6) { + String[] parts = text.split("_"); + StringBuilder sb = new StringBuilder(); + for (String part : parts) if (!part.isEmpty()) sb.append(part.charAt(0)); + result = (binding.isMouse() ? "M" : "") + sb; + } else { + result = text.replace("_", " "); + } + } + if (cache == null || cache.length != bindings.length) { + cache = new String[bindings.length]; + bindingTextCache = cache; } - return text.replace("_", " "); + if (index < cache.length) cache[index] = result; + return result; } private static float getTextSizeForWidth(Paint paint, String text, float desiredWidth) { @@ -453,7 +486,24 @@ private static float getTextSizeForWidth(Paint paint, String text, float desired return testTextSize * desiredWidth / paint.measureText(text); } + private static final String[][] rangeTextCache = new String[Range.values().length][]; + private static String getRangeTextForIndex(Range range, int index) { + if (index < 0 || index >= range.max) return computeRangeText(range, index); + String[] cache = rangeTextCache[range.ordinal()]; + if (cache == null) { + cache = new String[range.max]; + rangeTextCache[range.ordinal()] = cache; + } + String cached = cache[index]; + if (cached == null) { + cached = computeRangeText(range, index); + cache[index] = cached; + } + return cached; + } + + private static String computeRangeText(Range range, int index) { String text = ""; switch (range) { case FROM_A_TO_Z: @@ -2059,8 +2109,7 @@ private void drawNeon(Canvas canvas) { float h = boundingBox.height(); if (shape == Shape.CIRCLE) { float r = w * 0.5f - core; - path.reset(); - path.addCircle(cx, cy, r, Path.Direction.CW); + buildChamferRectPath(path, cx - r, cy - r, cx + r, cy + r, r * 0.586f); } else { float chamfer = shape == Shape.SQUARE ? snappingSize * 0.8f * scale From 4370a1f6ce75de439caa56f6c5412ee38b3cf2e4 Mon Sep 17 00:00:00 2001 From: Xnick417x Date: Tue, 14 Jul 2026 05:03:32 +0000 Subject: [PATCH 7/8] Fix D-pad press visuals not repainting; themed arrows; theme picker icon - D-pad touches never triggered a redraw, so per-direction pressed states drew stale; the view now invalidates on touch-down and when a direction changes - Slate/Glint D-pad arrows draw in the accent color with bloom press feedback; Shadow keeps its solid accent flood; no white arrow flips - Editor theme button gets its own icon: eyedropper with a T --- .../main/res/drawable/pickerthemecontrol.xml | 11 +++++++++++ .../res/layout/controls_editor_activity.xml | 2 +- .../runtime/input/controls/ControlElement.java | 18 ++++++++++++------ 3 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 app/src/main/res/drawable/pickerthemecontrol.xml diff --git a/app/src/main/res/drawable/pickerthemecontrol.xml b/app/src/main/res/drawable/pickerthemecontrol.xml new file mode 100644 index 000000000..0d8a7ebe8 --- /dev/null +++ b/app/src/main/res/drawable/pickerthemecontrol.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout/controls_editor_activity.xml b/app/src/main/res/layout/controls_editor_activity.xml index 26eb62aa5..8bf55e637 100644 --- a/app/src/main/res/layout/controls_editor_activity.xml +++ b/app/src/main/res/layout/controls_editor_activity.xml @@ -69,7 +69,7 @@ + android:src="@drawable/pickerthemecontrol" /> Date: Tue, 14 Jul 2026 17:20:23 +0000 Subject: [PATCH 8/8] Add Lumina style; true two-tone Neon secondary; scrollable style list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lumina: frosted glass plates via static gradient shaders — silver rims, accent labels, glass-dome stick thumb, accent rim + strong bloom while pressed - Neon secondary marks now use a hue-rotated partner color instead of a white blend, giving every theme a real two-tone pairing - Editor style popup scrolls like the theme list --- app/src/main/res/values-b+es+419/strings.xml | 1 + app/src/main/res/values-da/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fi/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-hi/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-no/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt-rBR/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-th/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../display/XServerDisplayActivity.java | 1 + .../runtime/input/ControlsEditorActivity.java | 13 +- .../input/controls/ControlElement.java | 461 +++++++++++++++++- .../runtime/input/controls/VisualStyle.java | 3 +- 27 files changed, 498 insertions(+), 3 deletions(-) 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 89ae105f2..99ad64847 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -369,6 +369,7 @@ Sombra Retícula Neón + Lumina Tema de color Perfil integrado — se creó una copia que puedes editar. Nombre del perfil diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 950d293af..8aa98c504 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1309,6 +1309,7 @@ Installeret sti: Skygge Sigtekryds Neon + Lumina Farvetema Indbygget profil - oprettede en kopi, du kan redigere. Nulstil denne profil til dens oprindelige layout? Alle ændringer, du har lavet, går tabt. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 79a4c8c72..62a530bb3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1309,6 +1309,7 @@ Installierter Pfad: Schatten Fadenkreuz Neon + Lumina Farbschema Integriertes Profil - eine bearbeitbare Kopie wurde erstellt. Dieses Profil auf sein ursprüngliches Layout zurücksetzen? Alle vorgenommenen Änderungen gehen verloren. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4da8ef24d..f8ec9b596 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1309,6 +1309,7 @@ Ruta instalada: Sombra Retícula Neón + Lumina Tema de color Perfil integrado: se creó una copia que puedes editar. ¿Restablecer este perfil a su diseño original? Se perderán todos los cambios que hayas hecho. diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 724e81453..fa539167c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -369,6 +369,7 @@ Varjo Ristikko Neon + Lumina Väriteema Sisäänrakennettu profiili – luotiin kopio, jota voit muokata. Profiilin nimi diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8edf03c15..6a184076e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1309,6 +1309,7 @@ Chemin installé : Ombre Réticule Néon + Lumina Thème de couleur Profil intégré : une copie modifiable a été créée. Réinitialiser ce profil à sa disposition d\'origine ? Toutes vos modifications seront perdues. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index bff626ce8..9e2aaa132 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -347,6 +347,7 @@ छाया रेटिकल नियॉन + लुमिना रंग थीम अंतर्निहित प्रोफ़ाइल - एक प्रति बनाई गई जिसे आप संपादित कर सकते हैं। प्रोफ़ाइल नाम diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 033639b3a..260a62c88 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1309,6 +1309,7 @@ Percorso installato: Ombra Reticolo Neon + Lumina Tema colore Profilo integrato: è stata creata una copia modificabile. Ripristinare questo profilo al layout originale? Tutte le modifiche apportate andranno perse. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1f03ba184..dc43b24fb 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -369,6 +369,7 @@ シャドウ レティクル ネオン + ルミナ カラーテーマ 組み込みプロファイル — 編集可能なコピーを作成しました。 プロファイル名 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 58b78bef7..cfe378c18 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1310,6 +1310,7 @@ 섀도우 레티클 네온 + 루미나 색상 테마 기본 제공 프로필입니다. 편집할 수 있는 복사본을 만들었습니다. 이 프로필을 원래 레이아웃으로 초기화할까요? 변경 사항이 모두 손실됩니다. diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index a06771a55..b639af0bc 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -369,6 +369,7 @@ Skygge Retikkel Neon + Lumina Fargetema Innebygd profil – opprettet en kopi du kan redigere. Profilnavn diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a39133c13..14287a002 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1315,6 +1315,7 @@ Zainstalowana ścieżka: Cień Celownik Neon + Lumina Motyw kolorystyczny Wbudowany profil — utworzono kopię, którą możesz edytować. Zresetować ten profil do oryginalnego układu? Wszystkie wprowadzone zmiany zostaną utracone. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index efa34e1ea..37b984463 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1309,6 +1309,7 @@ Caminho instalado: Sombra Retícula Neon + Lumina Tema de cores Perfil integrado - uma cópia editável foi criada. Redefinir este perfil para o layout original? Todas as alterações feitas serão perdidas. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b59c1f83b..81bb6d1ef 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -369,6 +369,7 @@ Sombra Retícula Néon + Lumina Tema de cores Perfil incorporado — foi criada uma cópia que pode editar. Nome do perfil diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 0e7c37edb..2f1bee9db 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1309,6 +1309,7 @@ Cale instalata: Umbră Reticul Neon + Lumina Temă de culoare Profil integrat - a fost creată o copie editabilă. Resetezi acest profil la aspectul original? Toate modificările făcute se vor pierde. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ab8c17aeb..96b203547 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -359,6 +359,7 @@ Тень Прицел Неон + Люмина Цветовая тема Встроенный профиль — созданную копию можно редактировать. Имя профиля diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 7c57796ff..59022a5b8 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -369,6 +369,7 @@ Skugga Hårkors Neon + Lumina Färgtema Inbyggd profil — en kopia som du kan redigera har skapats. Profilnamn diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 730a244aa..41b08a83e 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -369,6 +369,7 @@ เงา เรติเคิล นีออน + ลูมินา ธีมสี โปรไฟล์ในตัว — สร้างสำเนาที่คุณสามารถแก้ไขได้แล้ว ชื่อโปรไฟล์ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 540088cef..9aec8be52 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -369,6 +369,7 @@ Gölge Retikül Neon + Lumina Renk teması Yerleşik profil — düzenleyebileceğiniz bir kopya oluşturuldu. Profil Adı diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ddd3829af..5113cc890 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1315,6 +1315,7 @@ Тінь Приціл Неон + Люміна Колірна тема Вбудований профіль — створено копію, яку можна редагувати. Скинути цей профіль до початкового макета? Усі внесені зміни буде втрачено. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 100b43c72..e9289c574 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1309,6 +1309,7 @@ 暗影 准星 霓虹 + 流光 颜色主题 内置配置文件 - 已创建可编辑副本。 将此配置文件重置为原始布局?你所做的任何更改都会丢失。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 69701d457..3985dac37 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1309,6 +1309,7 @@ 暗影 準星 霓虹 + 流光 顏色主題 內建設定檔 - 已建立可編輯副本。 將此設定檔重設為原始版面配置?你所做的任何變更都會遺失。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8315bbe4..813cad000 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -370,6 +370,7 @@ Shadow Reticle Neon + Lumina Color Theme Built-in profile — created a copy you can edit. Profile Name diff --git a/app/src/main/runtime/display/XServerDisplayActivity.java b/app/src/main/runtime/display/XServerDisplayActivity.java index 15991979b..ba9e39f04 100644 --- a/app/src/main/runtime/display/XServerDisplayActivity.java +++ b/app/src/main/runtime/display/XServerDisplayActivity.java @@ -3980,6 +3980,7 @@ private void renderDrawerMenu() { styleNames.add(getString(R.string.input_controls_style_shadow)); styleNames.add(getString(R.string.input_controls_style_reticle)); styleNames.add(getString(R.string.input_controls_style_neon)); + styleNames.add(getString(R.string.input_controls_style_lumina)); styleNames.add(getString(R.string.input_controls_style_original)); } VisualStyle currentStyle = inputControlsView != null && inputControlsView.getVisualStyle() != null diff --git a/app/src/main/runtime/input/ControlsEditorActivity.java b/app/src/main/runtime/input/ControlsEditorActivity.java index 29386f941..7f46ecab3 100644 --- a/app/src/main/runtime/input/ControlsEditorActivity.java +++ b/app/src/main/runtime/input/ControlsEditorActivity.java @@ -154,6 +154,8 @@ private void showVisualStylePicker(View anchorView) { String[] names = VisualStyle.displayNames(); VisualStyle[] styles = VisualStyle.values(); int rowPadding = (int) UnitUtils.dpToPx(6); + LinearLayout rows = new LinearLayout(this); + rows.setOrientation(LinearLayout.VERTICAL); for (int i = 0; i < styles.length; i++) { final VisualStyle style = styles[i]; boolean selected = style == current; @@ -172,9 +174,18 @@ private void showVisualStylePicker(View anchorView) { inputControlsView.setVisualStyle(style); if (popup[0] != null) popup[0].dismiss(); }); - view.addView(label); + rows.addView(label); } + ScrollView scroller = new ScrollView(this); + scroller.setVerticalScrollBarEnabled(true); + scroller.addView(rows); + int maxListHeight = + (int) Math.min(UnitUtils.dpToPx(250), getResources().getDisplayMetrics().heightPixels * 0.5f); + view.addView( + scroller, + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, maxListHeight)); + popup[0] = AppUtils.showPopupWindow(anchorView, view, 220, 0); popup[0].setFocusable(true); popup[0].update(); diff --git a/app/src/main/runtime/input/controls/ControlElement.java b/app/src/main/runtime/input/controls/ControlElement.java index b80e368b8..f3ddcbd8b 100644 --- a/app/src/main/runtime/input/controls/ControlElement.java +++ b/app/src/main/runtime/input/controls/ControlElement.java @@ -616,6 +616,10 @@ public void draw(Canvas canvas) { drawNeon(canvas); return; } + if (style == VisualStyle.LUMINA) { + drawLumina(canvas); + return; + } if (style != VisualStyle.ORIGINAL) { drawClean(canvas, style); return; @@ -2049,6 +2053,14 @@ private static void drawCornerBrackets(Canvas canvas, Paint paint, Rect bb, floa } private static final int NEON_SCRIM = 0xFF0A0A16; + private static final float[] hsvTemp = new float[3]; + + /** Contrasting partner color for the neon accent (hue rotated 150 degrees). */ + private static int neonSecondary(int accent) { + Color.colorToHSV(accent, hsvTemp); + hsvTemp[0] = (hsvTemp[0] + 150f) % 360f; + return Color.HSVToColor(Color.alpha(accent), hsvTemp); + } /** Layered-stroke glow around {@code p}: wide faint halos under a crisp core line. */ private void neonStrokePath( @@ -2094,7 +2106,7 @@ private void drawNeon(Canvas canvas) { if (selected && resolveAccentColor() == -1) { accent = ColorUtils.setAlphaComponent(inputControlsView.getSecondaryColor(), 255); } - int secondary = ColorUtils.blendARGB(accent, Color.WHITE, 0.35f); + int secondary = neonSecondary(accent); float core = Math.max(1.5f, snappingSize * 0.14f * scale); int scrimColor = ColorUtils.setAlphaComponent(NEON_SCRIM, (int) (61 * a)); int washColor = ColorUtils.setAlphaComponent(accent, (int) (33 * a)); @@ -2458,6 +2470,453 @@ private void drawNeon(Canvas canvas) { paint.setStrokeJoin(Paint.Join.MITER); } + private static final int LUMINA_RIM = 0xFFC7D0DA; + private static final int LUMINA_TEXT = 0xFFF4F7FA; + private static Shader luminaBodyShader; + private static Shader luminaShadeShader; + private static Shader luminaSpecularShader; + private static Shader luminaDomeShader; + + private static Shader getLuminaBodyShader() { + if (luminaBodyShader == null) { + luminaBodyShader = new android.graphics.LinearGradient( + 0f, 0f, 0f, 1f, 0xFF2E343C, 0xB512151A, Shader.TileMode.CLAMP); + } + return luminaBodyShader; + } + + private static Shader getLuminaShadeShader() { + if (luminaShadeShader == null) { + luminaShadeShader = new android.graphics.LinearGradient( + 0f, 0f, 0f, 1f, + new int[] {0x00000000, 0x00000000, 0x4D000000}, + new float[] {0f, 0.55f, 1f}, Shader.TileMode.CLAMP); + } + return luminaShadeShader; + } + + private static Shader getLuminaSpecularShader() { + if (luminaSpecularShader == null) { + luminaSpecularShader = new android.graphics.LinearGradient( + 0f, 0f, 0f, 1f, + new int[] {0xFFFFFFFF, 0x00FFFFFF, 0x00FFFFFF}, + new float[] {0f, 0.55f, 1f}, Shader.TileMode.CLAMP); + } + return luminaSpecularShader; + } + + private static Shader getLuminaDomeShader() { + if (luminaDomeShader == null) { + luminaDomeShader = new RadialGradient( + 0f, 0f, 1f, + new int[] {0x64FFFFFF, 0xB92E343C, 0xE112151A}, + new float[] {0f, 0.55f, 1f}, Shader.TileMode.CLAMP); + } + return luminaDomeShader; + } + + /** Draws the frosted plate passes (body gradient + bottom shade) for the element's shape. */ + private void luminaGlassBody(Canvas canvas, Paint paint, Rect bb, float a) { + paint.setStyle(Paint.Style.FILL); + placeShader(getLuminaBodyShader(), bb.left, bb.top, Math.max(1, bb.height())); + paint.setShader(getLuminaBodyShader()); + paint.setAlpha((int) (170 * a)); + drawCleanBody(canvas, paint, bb, 0f); + placeShader(getLuminaShadeShader(), bb.left, bb.top, Math.max(1, bb.height())); + paint.setShader(getLuminaShadeShader()); + paint.setAlpha((int) (255 * a)); + drawCleanBody(canvas, paint, bb, 0f); + paint.setShader(null); + } + + /** LUMINA — frosted glass plates: gradient body, silver hairline rim, top specular, accent light on press. */ + private void drawLumina(Canvas canvas) { + int snappingSize = inputControlsView.getSnappingSize(); + Paint paint = inputControlsView.getPaint(); + float effectiveOpacity = inputControlsView.isEditMode() ? Math.max(0.15f, opacity) : opacity; + float overlayOpacity = inputControlsView.getOverlayOpacity(); + float dim = overlayOpacity <= 0.4f + ? 0.28f + (overlayOpacity - 0.1f) * (0.5f / 0.3f) + : 0.78f + (overlayOpacity - 0.4f) * (0.22f / 0.6f); + float a = Mathf.clamp(dim, 0f, 1f) * effectiveOpacity; + boolean engaged = isEngaged(); + Rect boundingBox = getBoundingBox(); + int custom = resolveAccentColor(); + int accent = resolveThemedAccent(); + if (selected && custom == -1) { + accent = ColorUtils.setAlphaComponent(inputControlsView.getSecondaryColor(), 255); + } + float hairline = Math.max(1.5f, snappingSize * 0.11f * scale); + int rimIdle = custom != -1 ? ColorUtils.blendARGB(LUMINA_RIM, custom, 0.25f) : LUMINA_RIM; + int rimColor = engaged + ? ColorUtils.setAlphaComponent(accent, (int) (235 * a)) + : ColorUtils.setAlphaComponent(rimIdle, (int) (150 * a)); + int labelColor = engaged + ? Color.argb((int) (250 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (230 * a)); + float minTextSize = snappingSize * 2 * scale; + + switch (type) { + case BUTTON: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + luminaGlassBody(canvas, paint, boundingBox, a); + if (engaged) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (85 * a))); + drawCleanBody(canvas, paint, boundingBox, 0f); + beginBloom(paint, cx, cy, Math.min(boundingBox.width(), boundingBox.height()) * 0.58f, + accent, a); + drawCleanBody(canvas, paint, boundingBox, 0f); + endBloom(paint); + } + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 1.2f); + paint.setColor(rimColor); + drawCleanBody(canvas, paint, boundingBox, 0f); + placeShader(getLuminaSpecularShader(), boundingBox.left, boundingBox.top, + Math.max(1, boundingBox.height())); + paint.setShader(getLuminaSpecularShader()); + paint.setStrokeWidth(hairline * 1.3f); + paint.setAlpha((int) (150 * a)); + drawCleanBody(canvas, paint, boundingBox, hairline); + paint.setShader(null); + if (iconId > 0) { + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, true, + engaged ? inputControlsView.getColorFilter() : accentFilter(accent)); + } else { + String label = getDisplayText(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(labelColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, label, boundingBox.width() - snappingSize * 0.6f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, cx, (cy - ((paint.descent() + paint.ascent()) * 0.5f)), paint); + } + break; + } + case D_PAD: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float base = Math.min(boundingBox.width(), boundingBox.height()); + float half = base * 0.5f; + float arm = base * 0.17f; + path.reset(); + path.moveTo(cx - arm, cy - half); + path.lineTo(cx + arm, cy - half); + path.lineTo(cx + arm, cy - arm); + path.lineTo(cx + half, cy - arm); + path.lineTo(cx + half, cy + arm); + path.lineTo(cx + arm, cy + arm); + path.lineTo(cx + arm, cy + half); + path.lineTo(cx - arm, cy + half); + path.lineTo(cx - arm, cy + arm); + path.lineTo(cx - half, cy + arm); + path.lineTo(cx - half, cy - arm); + path.lineTo(cx - arm, cy - arm); + path.close(); + paint.setPathEffect(cornerEffect(base * 0.09f)); + paint.setStyle(Paint.Style.FILL); + placeShader(getLuminaBodyShader(), boundingBox.left, cy - half, Math.max(1f, base)); + paint.setShader(getLuminaBodyShader()); + paint.setAlpha((int) (170 * a)); + canvas.drawPath(path, paint); + placeShader(getLuminaShadeShader(), boundingBox.left, cy - half, Math.max(1f, base)); + paint.setShader(getLuminaShadeShader()); + paint.setAlpha((int) (255 * a)); + canvas.drawPath(path, paint); + paint.setShader(null); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 1.2f); + paint.setColor(rimColor); + canvas.drawPath(path, paint); + paint.setPathEffect(null); + + boolean hasStates = engaged && states.length >= 4; + float chev = base * 0.05f; + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeJoin(Paint.Join.ROUND); + for (int i = 0; i < 4; i++) { + boolean hot = hasStates && states[i]; + int dx = i == 1 ? 1 : i == 3 ? -1 : 0; + int dy = i == 2 ? 1 : i == 0 ? -1 : 0; + if (hot) { + float bx = cx + dx * half * 0.6f; + float by = cy + dy * half * 0.6f; + beginBloom(paint, bx, by, base * 0.26f, accent, a); + canvas.drawCircle(bx, by, base * 0.26f, paint); + endBloom(paint); + } + float ax = cx + dx * half * 0.62f; + float ay = cy + dy * half * 0.62f; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(2f, snappingSize * 0.16f * scale)); + paint.setColor(hot + ? Color.argb((int) (250 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(accent, (int) (160 * a))); + if (dy != 0) { + canvas.drawLine(ax - chev, ay + dy * chev, ax, ay - dy * chev * 0.4f, paint); + canvas.drawLine(ax + chev, ay + dy * chev, ax, ay - dy * chev * 0.4f, paint); + } else { + canvas.drawLine(ax + dx * chev, ay - chev, ax - dx * chev * 0.4f, ay, paint); + canvas.drawLine(ax + dx * chev, ay + chev, ax - dx * chev * 0.4f, ay, paint); + } + } + paint.setStrokeCap(Paint.Cap.BUTT); + paint.setStrokeJoin(Paint.Join.MITER); + break; + } + case STICK: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float r = boundingBox.height() * 0.5f; + paint.setStyle(Paint.Style.FILL); + placeShader(getLuminaBodyShader(), cx - r, cy - r, Math.max(1f, r * 2f)); + paint.setShader(getLuminaBodyShader()); + paint.setAlpha((int) (95 * a)); + canvas.drawCircle(cx, cy, r, paint); + paint.setShader(null); + paint.setStyle(Paint.Style.STROKE); + float bezel = Math.max(2f, snappingSize * 0.3f * scale); + paint.setStrokeWidth(bezel); + paint.setColor(ColorUtils.setAlphaComponent(LUMINA_RIM, (int) (55 * a))); + canvas.drawCircle(cx, cy, r * 0.97f, paint); + tempRect.set(cx - r * 0.97f, cy - r * 0.97f, cx + r * 0.97f, cy + r * 0.97f); + paint.setColor(Color.argb((int) (110 * a), 255, 255, 255)); + canvas.drawArc(tempRect, 150f, 180f, false, paint); + paint.setStrokeWidth(hairline * 1.3f); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((engaged ? 210 : 95) * a))); + canvas.drawCircle(cx, cy, r * 0.55f, paint); + PointF pos = getCurrentPosition(); + float thumbR = snappingSize * 3.5f * scale; + paint.setStyle(Paint.Style.FILL); + placeShader(getLuminaDomeShader(), + pos.x - thumbR * 0.35f, pos.y - thumbR * 0.38f, Math.max(1f, thumbR * 1.6f)); + paint.setShader(getLuminaDomeShader()); + paint.setAlpha((int) (235 * a)); + canvas.drawCircle(pos.x, pos.y, thumbR, paint); + paint.setShader(null); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 1.2f); + paint.setColor(engaged + ? ColorUtils.setAlphaComponent(accent, (int) (235 * a)) + : ColorUtils.setAlphaComponent(rimIdle, (int) (170 * a))); + canvas.drawCircle(pos.x, pos.y, thumbR, paint); + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.argb((int) (128 * a), 255, 255, 255)); + canvas.drawCircle(pos.x - thumbR * 0.35f, pos.y - thumbR * 0.38f, thumbR * 0.14f, paint); + break; + } + case TRACKPAD: { + luminaGlassBody(canvas, paint, boundingBox, a); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 1.2f); + paint.setColor(rimColor); + drawCleanBody(canvas, paint, boundingBox, 0f); + float len = Math.min(boundingBox.width(), boundingBox.height()) * 0.08f; + float ins = Math.min(boundingBox.width(), boundingBox.height()) * 0.06f; + paint.setStrokeWidth(hairline); + paint.setColor(engaged + ? ColorUtils.setAlphaComponent(accent, (int) (200 * a)) + : ColorUtils.setAlphaComponent(LUMINA_RIM, (int) (90 * a))); + canvas.drawLine(boundingBox.left + ins, boundingBox.top + ins, boundingBox.left + ins + len, boundingBox.top + ins, paint); + canvas.drawLine(boundingBox.left + ins, boundingBox.top + ins, boundingBox.left + ins, boundingBox.top + ins + len, paint); + canvas.drawLine(boundingBox.right - ins, boundingBox.top + ins, boundingBox.right - ins - len, boundingBox.top + ins, paint); + canvas.drawLine(boundingBox.right - ins, boundingBox.top + ins, boundingBox.right - ins, boundingBox.top + ins + len, paint); + canvas.drawLine(boundingBox.left + ins, boundingBox.bottom - ins, boundingBox.left + ins + len, boundingBox.bottom - ins, paint); + canvas.drawLine(boundingBox.left + ins, boundingBox.bottom - ins, boundingBox.left + ins, boundingBox.bottom - ins - len, paint); + canvas.drawLine(boundingBox.right - ins, boundingBox.bottom - ins, boundingBox.right - ins - len, boundingBox.bottom - ins, paint); + canvas.drawLine(boundingBox.right - ins, boundingBox.bottom - ins, boundingBox.right - ins, boundingBox.bottom - ins - len, paint); + break; + } + case RANGE_BUTTON: { + Range range = getRange(); + float rr = (orientation == 0 ? boundingBox.height() : boundingBox.width()) * 0.45f; + float elementSize = scroller.getElementSize(); + float scrollOffset = scroller.getScrollOffset(); + byte[] rangeIndex = scroller.getRangeIndex(); + luminaGlassBody(canvas, paint, boundingBox, a); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 1.2f); + paint.setColor(rimColor); + drawCleanBody(canvas, paint, boundingBox, 0f); + + canvas.save(); + path.reset(); + path.addRoundRect( + boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, + rr, rr, Path.Direction.CW); + canvas.clipPath(path); + int pressedIndex = + currentPointerId != -1 && !scroller.isScrolling() ? scroller.getBindingIndex() : -1; + int dividerColor = ColorUtils.setAlphaComponent(LUMINA_RIM, (int) (60 * a)); + float dividerWidth = Math.max(1f, snappingSize * 0.06f); + int hotTextColor = Color.argb((int) (250 * a), 255, 255, 255); + int cellTextColor = ColorUtils.setAlphaComponent(LUMINA_TEXT, (int) (205 * a)); + + if (orientation == 0) { + float lineTop = boundingBox.top + boundingBox.height() * 0.25f; + float lineBottom = boundingBox.bottom - boundingBox.height() * 0.25f; + float startX = boundingBox.left - (scrollOffset % elementSize); + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(dividerWidth); + paint.setColor(dividerColor); + if (startX > boundingBox.left && startX < boundingBox.right) + canvas.drawLine(startX, lineTop, startX, lineBottom, paint); + if (startX < boundingBox.right && startX + elementSize > boundingBox.left) { + boolean hot = index == pressedIndex; + if (hot) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (240 * a))); + float uy = boundingBox.bottom - boundingBox.height() * 0.16f; + tempRect.set( + startX + elementSize * 0.225f, uy, + startX + elementSize * 0.775f, uy + hairline * 2.2f); + canvas.drawRoundRect(tempRect, hairline, hairline, paint); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : cellTextColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, elementSize - snappingSize * 0.5f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + startX + elementSize * 0.5f, + (y - ((paint.descent() + paint.ascent()) * 0.5f)), + paint); + } + startX += elementSize; + } + } else { + float lineLeft = boundingBox.left + boundingBox.width() * 0.25f; + float lineRight = boundingBox.right - boundingBox.width() * 0.25f; + float startY = boundingBox.top - (scrollOffset % elementSize); + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + int index = i % range.max; + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(dividerWidth); + paint.setColor(dividerColor); + if (startY > boundingBox.top && startY < boundingBox.bottom) + canvas.drawLine(lineLeft, startY, lineRight, startY, paint); + if (startY < boundingBox.bottom && startY + elementSize > boundingBox.top) { + boolean hot = index == pressedIndex; + if (hot) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (240 * a))); + float ux = boundingBox.right - boundingBox.width() * 0.16f; + tempRect.set( + ux, startY + elementSize * 0.225f, + ux + hairline * 2.2f, startY + elementSize * 0.775f); + canvas.drawRoundRect(tempRect, hairline, hairline, paint); + } + String cellText = getRangeTextForIndex(range, index); + paint.setStyle(Paint.Style.FILL); + paint.setColor(hot ? hotTextColor : cellTextColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, cellText, boundingBox.width() - snappingSize * 0.5f), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + cellText, + x, + startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), + paint); + } + startY += elementSize; + } + } + canvas.restore(); + break; + } + case RADIAL_MENU: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float radius = boundingBox.width() * 0.5f; + + if (radialMenuExpanded && bindings.length > 0 && radius > 0) { + float innerRadius = radius + snappingSize * 0.5f; + float outerRadius = boundingBox.width() + (snappingSize * scale); + float angleStep = 360.0f / bindings.length; + + if (paths == null || paths.length != bindings.length) { + paths = new Path[bindings.length]; + RectF outerRect = new RectF(cx - outerRadius, cy - outerRadius, cx + outerRadius, cy + outerRadius); + RectF innerRect = new RectF(cx - innerRadius, cy - innerRadius, cx + innerRadius, cy + innerRadius); + for (int i = 0; i < bindings.length; i++) { + float startAngle = -90.0f + i * angleStep; + paths[i] = new Path(); + paths[i].arcTo(outerRect, startAngle, angleStep, true); + paths[i].arcTo(innerRect, startAngle + angleStep, -angleStep, false); + paths[i].close(); + } + } + + if (paths != null && paths.length == bindings.length) { + for (int i = 0; i < bindings.length; i++) { + boolean active = i == activeRadialBindingIndex; + paint.setStyle(Paint.Style.FILL); + if (active) { + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (90 * a))); + } else { + placeShader(getLuminaBodyShader(), cx - outerRadius, cy - outerRadius, + Math.max(1f, outerRadius * 2f)); + paint.setShader(getLuminaBodyShader()); + paint.setAlpha((int) (150 * a)); + } + canvas.drawPath(paths[i], paint); + paint.setShader(null); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline); + paint.setColor(ColorUtils.setAlphaComponent(LUMINA_RIM, (int) (80 * a))); + canvas.drawPath(paths[i], paint); + + float middleAngle = (float) Math.toRadians(-90.0f + i * angleStep + angleStep * 0.5f); + float labelRadius = (innerRadius + outerRadius) * 0.5f; + float labelX = (float) (cx + Math.cos(middleAngle) * labelRadius); + float labelY = (float) (cy + Math.sin(middleAngle) * labelRadius); + String label = getBindingShortText(i); + paint.setStyle(Paint.Style.FILL); + paint.setColor(active + ? Color.argb((int) (250 * a), 255, 255, 255) + : ColorUtils.setAlphaComponent(LUMINA_TEXT, (int) (205 * a))); + paint.setTextSize(snappingSize * 1.2f * scale); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText(label, labelX, labelY - ((paint.descent() + paint.ascent()) * 0.5f), paint); + } + } + } + + luminaGlassBody(canvas, paint, boundingBox, a); + if (engaged) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (85 * a))); + canvas.drawCircle(cx, cy, radius, paint); + beginBloom(paint, cx, cy, radius, accent, a); + canvas.drawCircle(cx, cy, radius, paint); + endBloom(paint); + } + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(hairline * 1.2f); + paint.setColor(rimColor); + canvas.drawCircle(cx, cy, radius, paint); + drawIcon( + canvas, cx, cy, boundingBox.width(), boundingBox.height(), + iconId > 0 ? iconId : 34, true, + engaged ? inputControlsView.getColorFilter() : accentFilter(accent)); + break; + } + } + } + private static final int SHADOW_BODY = 0xFF0A1422; private static final int SHADOW_EDGE = 0xFF84A9D4; diff --git a/app/src/main/runtime/input/controls/VisualStyle.java b/app/src/main/runtime/input/controls/VisualStyle.java index 796149da3..8282ebee5 100644 --- a/app/src/main/runtime/input/controls/VisualStyle.java +++ b/app/src/main/runtime/input/controls/VisualStyle.java @@ -9,6 +9,7 @@ public enum VisualStyle { SHADOW, RETICLE, NEON, + LUMINA, ORIGINAL; public static VisualStyle fromPreference(String name) { @@ -21,6 +22,6 @@ public static VisualStyle fromPreference(String name) { } public static String[] displayNames() { - return new String[] {"Slate", "Glass", "Halo", "Glint", "Shadow", "Reticle", "Neon", "Original"}; + return new String[] {"Slate", "Glass", "Halo", "Glint", "Shadow", "Reticle", "Neon", "Lumina", "Original"}; } }