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/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/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/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 88b387e6d..8bf55e637 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..89ae105f2 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,15 @@ Número de asignaciones Seleccionar perfil Seleccionar estilo - Etiquetas de botones Original Vidrio - Original - Xbox - PlayStation + Pizarra + Halo + Destello + Sombra + Retícula + Neón + 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 535cf118c..950d293af 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1301,12 +1301,15 @@ 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 + Skifer + Halo + 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. 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..79a4c8c72 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1301,12 +1301,15 @@ 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 + Schiefer + Halo + 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. 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..4da8ef24d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1301,12 +1301,15 @@ 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 + Pizarra + Halo + 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. 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..724e81453 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -361,12 +361,15 @@ Sidontojen määrä Valitse profiili Valitse tyyli - Painikkeiden nimiöt Alkuperäinen Lasi - Alkuperäinen - Xbox - PlayStation + Liuske + Halo + Kimallus + Varjo + Ristikko + Neon + 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 2afa57152..8edf03c15 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1301,12 +1301,15 @@ 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 + Ardoise + Halo + É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. 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..bff626ce8 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -339,12 +339,15 @@ बाइंडिंग की संख्या प्रोफ़ाइल चुनें शैली चुनें - बटन लेबल मूल 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..033639b3a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1301,12 +1301,15 @@ 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 + 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. 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..1f03ba184 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -361,12 +361,15 @@ バインドの数 プロファイルを選択 スタイルを選択 - ボタンラベル オリジナル 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..58b78bef7 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1302,12 +1302,15 @@ 레거시 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..a06771a55 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -361,12 +361,15 @@ Antall tilordninger Velg profil Velg stil - Knappeetiketter Original Glass - Original - Xbox - PlayStation + Skifer + Halo + Glimt + Skygge + Retikkel + Neon + 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 1098113cf..a39133c13 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1307,12 +1307,15 @@ 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 + Łupek + Halo + 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. 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..efa34e1ea 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1301,12 +1301,15 @@ 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 + Ardósia + Halo + 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. 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..b59c1f83b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -361,12 +361,15 @@ Número de associações Selecionar perfil Selecionar estilo - Etiquetas dos botões Original Vidro - Original - Xbox - PlayStation + Ardósia + Halo + Brilho + Sombra + Retícula + Néon + 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 a60b885a9..0e7c37edb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1301,12 +1301,15 @@ 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 + Ardezie + Halo + 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. 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..ab8c17aeb 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -351,12 +351,15 @@ Количество привязок Выберите профиль Выберите стиль - Метки кнопок Оригинал 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..7c57796ff 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -361,12 +361,15 @@ Antal bindningar Välj profil Välj stil - Knappetiketter Original Glas - Original - Xbox - PlayStation + Skiffer + Halo + Glimt + Skugga + Hårkors + Neon + 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 ad4dbef72..730a244aa 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -361,12 +361,15 @@ จำนวนการผูกปุ่ม เลือกโปรไฟล์ เลือกสไตล์ - ป้ายกำกับปุ่ม ต้นฉบับ กระจก - ต้นฉบับ - Xbox - PlayStation + สเลท + ฮาโล + ประกาย + เงา + เรติเคิล + นีออน + ธีมสี โปรไฟล์ในตัว — สร้างสำเนาที่คุณสามารถแก้ไขได้แล้ว ชื่อโปรไฟล์ ไม่ได้เลือกโปรไฟล์ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 53699ce6c..540088cef 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -361,12 +361,15 @@ Bağlama Sayısı Profil Seç Stil Seç - Düğme Etiketleri Orijinal Cam - Orijinal - Xbox - PlayStation + Arduvaz + Hale + Parıltı + Gölge + Retikül + Neon + 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 701a1fc3d..ddd3829af 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1307,12 +1307,15 @@ Застарілий емулятор 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..100b43c72 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1301,12 +1301,15 @@ 旧版 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..69701d457 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1301,12 +1301,15 @@ 舊版 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 b35be140a..c5f5cbe0d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -362,12 +362,15 @@ Number of Bindings Select Profile Select Style - Button Labels Original Glass - Original - Xbox - PlayStation + Slate + Halo + Glint + Shadow + Reticle + Neon + 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 c8508b6f3..15991979b 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; @@ -3972,19 +3972,26 @@ private void renderDrawerMenu() { } ArrayList styleNames = new ArrayList<>(); - styleNames.add(getString(R.string.input_controls_style_original)); - styleNames.add(getString(R.string.input_controls_style_gamehub)); + 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_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 - ? 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 && inputControlsView != null) { + accentThemeNames.addAll(Arrays.asList(AccentTheme.displayNames())); + selectedAccentThemeIndex = inputControlsView.getAccentTheme().ordinal(); + } List gestureProfileNames = new ArrayList<>(); int gestureSelectedIndex = 0; @@ -4051,8 +4058,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), @@ -4579,13 +4586,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(); } @@ -4595,34 +4596,17 @@ 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(); } @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(); + AccentTheme chosen = all[index]; + if (inputControlsView != null) inputControlsView.setAccentTheme(chosen); + persistSelectedAccentTheme(chosen); renderDrawerMenu(); } @@ -7588,6 +7572,29 @@ private void persistSelectedProfile(ControlsProfile profile) { } } + // 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", ""))) { + 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(); @@ -7614,29 +7621,20 @@ 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()))); + 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/display/XServerDrawerMenu.kt b/app/src/main/runtime/display/XServerDrawerMenu.kt index 1cee605dd..c44e296ed 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, ) } } @@ -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/ControlsEditorActivity.java b/app/src/main/runtime/input/ControlsEditorActivity.java index bcd8eb906..29386f941 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; @@ -18,15 +21,19 @@ 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; 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 +61,14 @@ 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))); + inputControlsView.setAccentTheme( + AccentTheme.fromPreference( + PreferenceManager.getDefaultSharedPreferences(this) + .getString("input_accent_theme", null))); profile = InputControlsManager.loadProfile( @@ -68,6 +83,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,9 +126,60 @@ 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 -> { + 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) { final ControlElement element = inputControlsView.getSelectedElement(); View view = LayoutInflater.from(this).inflate(R.layout.color_picker_popup, null); @@ -193,6 +261,79 @@ 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 = inputControlsView.getAccentTheme(); + 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); + 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); + 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 -> { + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putString("input_accent_theme", theme.name()) + .apply(); + inputControlsView.setAccentTheme(theme); + if (popup[0] != null) popup[0].dismiss(); + }); + 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(); + } + 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..7d3cc2ce0 --- /dev/null +++ b/app/src/main/runtime/input/controls/AccentTheme.java @@ -0,0 +1,36 @@ +package com.winlator.cmod.runtime.input.controls; + +/** Accent presets for the themed control styles; a per-element custom color always wins. */ +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 CYAN; + try { + return AccentTheme.valueOf(name); + } catch (IllegalArgumentException e) { + return CYAN; + } + } + + 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..b80e368b8 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; @@ -108,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; @@ -146,6 +153,7 @@ private void reset() { boundingBoxNeedsUpdate = true; radialMenuExpanded = false; paths = null; + invalidateLabelCache(); } public Type getType() { @@ -170,6 +178,7 @@ public void setBindingCount(int bindingCount) { states = new boolean[bindingCount]; boundingBoxNeedsUpdate = true; paths = null; + invalidateLabelCache(); } public Shape getShape() { @@ -250,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() { @@ -305,6 +316,7 @@ public String getText() { public void setText(String text) { this.text = text != null ? text : ""; + invalidateLabelCache(); } public byte getIconId() { @@ -390,50 +402,82 @@ 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); - // 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 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; } - /** - * 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). - */ + private void invalidateLabelCache() { + displayTextCache = null; + bindingTextCache = null; + } + + /** Returns the per-element customColor, or {@code -1} if unset (fall back to theme accent). */ 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) { + String[] cache = bindingTextCache; + if (cache != null && index < cache.length && cache[index] != null) return cache[index]; Binding binding = getBindingAt(index); - 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 bumper = bumperTriggerLabel(binding); + 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("_", " "); + } } - return text.replace("_", " "); + if (cache == null || cache.length != bindings.length) { + cache = new String[bindings.length]; + bindingTextCache = cache; + } + if (index < cache.length) cache[index] = result; + return result; } private static float getTextSizeForWidth(Paint paint, String text, float desiredWidth) { @@ -442,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: @@ -465,15 +526,108 @@ 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.SHADOW) { + 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; + } int snappingSize = inputControlsView.getSnappingSize(); 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)) @@ -580,7 +734,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( @@ -650,11 +806,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: @@ -759,119 +914,1887 @@ public void draw(Canvas canvas) { startX += elementSize; } - paint.setStyle(Paint.Style.STROKE); - paint.setColor(oldColor); - canvas.restore(); - } else { - float lineLeft = boundingBox.left + strokeWidth * 0.5f; - float lineRight = boundingBox.right - strokeWidth * 0.5f; - float startY = boundingBox.top; - canvas.drawRoundRect( - boundingBox.left, - startY, - boundingBox.right, - boundingBox.bottom, - radius, - radius, - paint); + paint.setStyle(Paint.Style.STROKE); + paint.setColor(oldColor); + canvas.restore(); + } else { + float lineLeft = boundingBox.left + strokeWidth * 0.5f; + float lineRight = boundingBox.right - strokeWidth * 0.5f; + float startY = boundingBox.top; + canvas.drawRoundRect( + boundingBox.left, + startY, + boundingBox.right, + boundingBox.bottom, + radius, + radius, + paint); + + canvas.save(); + path.addRoundRect( + boundingBox.left, + startY, + boundingBox.right, + boundingBox.bottom, + radius, + radius, + Path.Direction.CW); + canvas.clipPath(path); + startY -= scrollOffset % elementSize; + + for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { + paint.setStyle(Paint.Style.STROKE); + paint.setColor(oldColor); + + if (startY > boundingBox.top && startY < boundingBox.bottom) + canvas.drawLine(lineLeft, startY, lineRight, startY, paint); + String text = getRangeTextForIndex(range, i); + + if (startY < boundingBox.bottom && startY + elementSize > boundingBox.top) { + paint.setStyle(Paint.Style.FILL); + paint.setColor(primaryColor); + paint.setTextSize( + Math.min( + getTextSizeForWidth(paint, text, boundingBox.width() - strokeWidth * 2), + minTextSize)); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + text, + x, + startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), + paint); + } + startY += elementSize; + } + + paint.setStyle(Paint.Style.STROKE); + paint.setColor(oldColor); + canvas.restore(); + } + break; + } + case STICK: + { + int cx = boundingBox.centerX(); // Fixed outer circle center + int cy = boundingBox.centerY(); // Fixed outer circle center + int oldColor = paint.getColor(); + + // Draw the outer circle (base of the stick) + canvas.drawCircle(cx, cy, boundingBox.height() * 0.5f, paint); + + // Draw the inner thumbstick (current position based on gyroscope movement) + float thumbstickX = getCurrentPosition().x; + float thumbstickY = getCurrentPosition().y; + + short thumbRadius = (short) (snappingSize * 3.5f * scale); + int engagedAlpha = isEngaged() ? 120 : 50; + paint.setStyle(Paint.Style.FILL); + paint.setColor(ColorUtils.setAlphaComponent(primaryColor, engagedAlpha)); + canvas.drawCircle(thumbstickX, thumbstickY, thumbRadius, paint); // Draw thumbstick + + // Draw the thumbstick border + paint.setStyle(Paint.Style.STROKE); + paint.setColor(oldColor); + canvas.drawCircle(thumbstickX, thumbstickY, thumbRadius + strokeWidth * 0.5f, paint); + break; + } + + case TRACKPAD: + { + float radius = boundingBox.height() * 0.15f; + canvas.drawRoundRect( + boundingBox.left, + boundingBox.top, + boundingBox.right, + boundingBox.bottom, + radius, + radius, + paint); + float offset = strokeWidth * 2.5f; + float innerStrokeWidth = strokeWidth * 2; + float innerHeight = boundingBox.height() - offset * 2; + radius = + (innerHeight / boundingBox.height()) * radius + - (innerStrokeWidth * 0.5f + strokeWidth * 0.5f); + paint.setStrokeWidth(innerStrokeWidth); + canvas.drawRoundRect( + boundingBox.left + offset, + boundingBox.top + offset, + boundingBox.right - offset, + boundingBox.bottom - offset, + radius, + radius, + paint); + break; + } + } + } + + /** 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: + 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); + } + } + + /** 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(); + 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) { + 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); + 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; + buildDpadArrowPath(path, ax, ay, base * 0.055f, i); + paint.setStyle(Paint.Style.FILL); + if (halo) { + paint.setColor(hot + ? ColorUtils.setAlphaComponent(accent, (int) (240 * a)) + : Color.argb((int) (190 * a), 255, 255, 255)); + } else { + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) ((hot ? 250 : 220) * a))); + } + 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); + } + + 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(); + } + + /** 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(); + } + + /** 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); + } + } + + /** 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; + buildChamferRectPath(path, cx - r, cy - r, cx + r, cy + r, r * 0.586f); + } 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); + } - canvas.save(); - path.addRoundRect( - boundingBox.left, - startY, - boundingBox.right, - boundingBox.bottom, - radius, - radius, - Path.Direction.CW); - canvas.clipPath(path); - startY -= scrollOffset % elementSize; + private static final int SHADOW_BODY = 0xFF0A1422; + private static final int SHADOW_EDGE = 0xFF84A9D4; - for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { - paint.setStyle(Paint.Style.STROKE); - paint.setColor(oldColor); + 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); + } - if (startY > boundingBox.top && startY < boundingBox.bottom) - canvas.drawLine(lineLeft, startY, lineRight, startY, paint); - String text = getRangeTextForIndex(range, i); + 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); + } - if (startY < boundingBox.bottom && startY + elementSize > boundingBox.top) { + /** 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(); + 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(ColorUtils.setAlphaComponent(accent, (int) ((hot ? 250 : 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(primaryColor); - paint.setTextSize( - Math.min( - getTextSizeForWidth(paint, text, boundingBox.width() - strokeWidth * 2), - minTextSize)); - paint.setTextAlign(Paint.Align.CENTER); - canvas.drawText( - text, - x, - startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), - paint); + paint.setColor(ColorUtils.setAlphaComponent(accent, (int) (150 * a))); + canvas.drawRect( + startX, boundingBox.top, startX + elementSize, boundingBox.bottom, paint); } - startY += elementSize; + 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.setColor(oldColor); - canvas.restore(); + 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; } - break; } - case STICK: - { - int cx = boundingBox.centerX(); // Fixed outer circle center - int cy = boundingBox.centerY(); // Fixed outer circle center - int oldColor = paint.getColor(); + canvas.restore(); + break; + } + case RADIAL_MENU: { + float cx = boundingBox.exactCenterX(); + float cy = boundingBox.exactCenterY(); + float radius = boundingBox.width() * 0.5f; - // Draw the outer circle (base of the stick) - canvas.drawCircle(cx, cy, boundingBox.height() * 0.5f, paint); + 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; - // Draw the inner thumbstick (current position based on gyroscope movement) - float thumbstickX = getCurrentPosition().x; - float thumbstickY = getCurrentPosition().y; + 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(); + } + } - short thumbRadius = (short) (snappingSize * 3.5f * scale); - int engagedAlpha = isEngaged() ? 120 : 50; - paint.setStyle(Paint.Style.FILL); - paint.setColor(ColorUtils.setAlphaComponent(primaryColor, engagedAlpha)); - canvas.drawCircle(thumbstickX, thumbstickY, thumbRadius, paint); // Draw thumbstick + 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); - // Draw the thumbstick border - paint.setStyle(Paint.Style.STROKE); - paint.setColor(oldColor); - canvas.drawCircle(thumbstickX, thumbstickY, thumbRadius + strokeWidth * 0.5f, paint); - break; + 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); + } + } } - case TRACKPAD: - { - float radius = boundingBox.height() * 0.15f; - canvas.drawRoundRect( - boundingBox.left, - boundingBox.top, - boundingBox.right, - boundingBox.bottom, - radius, - radius, - paint); - float offset = strokeWidth * 2.5f; - float innerStrokeWidth = strokeWidth * 2; - float innerHeight = boundingBox.height() - offset * 2; - radius = - (innerHeight / boundingBox.height()) * radius - - (innerStrokeWidth * 0.5f + strokeWidth * 0.5f); - paint.setStrokeWidth(innerStrokeWidth); - canvas.drawRoundRect( - boundingBox.left + offset, - boundingBox.top + offset, - boundingBox.right - offset, - boundingBox.bottom - offset, - radius, - radius, - paint); - break; - } + 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; + } } } @@ -890,7 +2813,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 +2828,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,21 +2875,25 @@ 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); } 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); @@ -990,12 +2922,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 +2970,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 +3025,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,16 +3050,16 @@ 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); - 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: { @@ -1139,7 +3073,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); @@ -1195,10 +3131,9 @@ private void drawGameHub(Canvas canvas) { canvas.restore(); break; } - default: - 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 +3175,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,50 +3204,40 @@ 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); } - /** - * 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(); 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); } @@ -1430,6 +3354,7 @@ public boolean handleTouchDown(int pointerId, float x, float y) { if (trackpadOrigin == null) trackpadOrigin = new PointF(); trackpadOrigin.set(x, y); } + inputControlsView.invalidate(); return handleTouchMove(pointerId, x, y); } } else return false; @@ -1619,13 +3544,16 @@ public boolean handleTouchMove(int pointerId, float x, float y) { deltaX <= -DPAD_DEAD_ZONE }; + boolean statesChanged = false; for (byte i = 0; i < 4; i++) { float value = i == 1 || i == 3 ? deltaX : deltaY; Binding binding = getBindingAt(i); boolean state = binding.isMouseMove() ? (states[i] || states[(i + 2) % 4]) : states[i]; inputControlsView.handleInputEvent(binding, state, value); + if (this.states[i] != state) statesChanged = true; this.states[i] = state; } + if (statesChanged) inputControlsView.invalidate(); } return true; diff --git a/app/src/main/runtime/input/controls/ControlsProfile.java b/app/src/main/runtime/input/controls/ControlsProfile.java index 1e4a0146a..f134852c5 100644 --- a/app/src/main/runtime/input/controls/ControlsProfile.java +++ b/app/src/main/runtime/input/controls/ControlsProfile.java @@ -205,16 +205,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..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 = 6; + 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; - 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}; + // 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; @@ -41,11 +43,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 +121,14 @@ 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(); + } + for (int id : REFRESHED_PROFILE_IDS) { + ControlsProfile.getProfileFile(context, id).delete(); + } + try { AssetManager assetManager = context.getAssets(); String[] assetFiles = assetManager.list("inputcontrols/profiles"); 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..796149da3 100644 --- a/app/src/main/runtime/input/controls/VisualStyle.java +++ b/app/src/main/runtime/input/controls/VisualStyle.java @@ -1,28 +1,26 @@ package com.winlator.cmod.runtime.input.controls; -/** - * Visual rendering style for on-screen virtual gamepad elements. - * - *

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. - * - *

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 { - ORIGINAL, - GAMEHUB; + SLATE, + GAMEHUB, + HALO, + GLINT, + SHADOW, + RETICLE, + NEON, + 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", "Shadow", "Reticle", "Neon", "Original"}; } } diff --git a/app/src/main/runtime/input/ui/InputControlsView.java b/app/src/main/runtime/input/ui/InputControlsView.java index 89b0473ba..46c07633b 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,8 @@ 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 AccentTheme accentTheme = AccentTheme.CYAN; private InputControlsManager inputControlsManager; private Handler timeoutHandler; // Reference to the activity's timeout handler @@ -187,29 +187,23 @@ 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; + public AccentTheme getAccentTheme() { + return accentTheme; } - public LabelTheme getLabelTheme() { - return labelTheme; + public void setAccentTheme(AccentTheme theme) { + this.accentTheme = theme != null ? theme : AccentTheme.CYAN; + invalidate(); } public InputControlsManager getInputControlsManager() { return inputControlsManager; } - public void setLabelTheme(LabelTheme theme) { - this.labelTheme = theme != null ? theme : LabelTheme.DEFAULT; - invalidate(); - } - public int getSnappingSize() { return snappingSize; }