mirror of
https://github.com/wayvr-org/wayvr.git
synced 2026-10-28 13:02:53 +01:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b34595e489 | ||
|
|
330cf1bdeb | ||
|
|
19b7d85682 |
No files matched your search
@@ -21,9 +21,6 @@ export LDAI_UPDATE_INFORMATION="gh-releases-zsync|wayvr-org|wayvr|latest|WayVR-*
|
||||
|
||||
echo "Packaging AppImage for ${APPIMAGE_ARCH}"
|
||||
|
||||
# linuxdeploy doesn't like multiple -d's being passed
|
||||
install -Dm 0644 wayvr-headless.desktop "${APPDIR}/usr/share/applications/wayvr-headless.desktop"
|
||||
|
||||
"./${LINUXDEPLOY}" \
|
||||
-dwayvr.desktop \
|
||||
-iwayvr.png \
|
||||
|
||||
Generated
+586
-933
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -32,7 +32,6 @@ rust-embed = "8.9.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
serde_json5 = "0.2.1"
|
||||
shell-words = "1.1.1"
|
||||
slotmap = "1.1.1"
|
||||
smallvec = "1.15.2"
|
||||
smol = "2.0.2"
|
||||
@@ -45,6 +44,7 @@ wayland-protocols-misc = { version = "0.3.12" }
|
||||
winit = "0.30.12"
|
||||
xdg = "3.0.0"
|
||||
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
debug = true
|
||||
|
||||
@@ -19,7 +19,6 @@ log.workspace = true
|
||||
rust-embed.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json.workspace = true
|
||||
shell-words.workspace = true
|
||||
smol = { workspace = true }
|
||||
strum.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M17 10q-.825 0-1.412-.575T15 8V4q0-.85.588-1.425T17 2q.85 0 1.425.575T19 4v4q0 .85-.575 1.425T17 10M5 22q-.825 0-1.412-.587T3 20V4q0-.825.588-1.412T5 2h8v2H5v16h11v-2h2v2q0 .825-.587 1.413T16 22zm2-4v-2h7v2zm0-3v-2h5v2zm11 1h-2v-2.6q-1.925-.35-3.213-1.862T11.5 8h2q0 1.45 1.025 2.475T17 11.5q1.475 0 2.488-1.025T20.5 8h2q0 2.025-1.275 3.538T18 13.4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 585 B |
@@ -263,9 +263,7 @@
|
||||
},
|
||||
"TUTORIAL": "Tutorial",
|
||||
"RESTART_WAYVR": "WayVR neu starten",
|
||||
"CLOSE_WAYVR": "WayVR schließen",
|
||||
"SPEECH_TO_TEXT": "Sprache zu Text",
|
||||
"WHISPER_GPU_SELECT": "GPU auswählen"
|
||||
"CLOSE_WAYVR": "WayVR schließen"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Programmstarter",
|
||||
"APPLICATION_STARTED": "Anwendung gestartet",
|
||||
@@ -375,9 +373,7 @@
|
||||
"TITLE": "Energieoptionen",
|
||||
"SHUTDOWN": "Herunterfahren",
|
||||
"SUSPEND": "Ruhezustand",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Bist du sicher, dass du den Computer ausschalten willst?",
|
||||
"NOT_SUPPORTED": "Energieoptionen erfordern systemd oder elogind.",
|
||||
"SUSPEND_ARE_YOU_SURE": "Bist du sicher, dass du den Computer in den Ruhezustand versetzen willst?"
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Bist du sicher, dass du den Computer ausschalten willst?"
|
||||
},
|
||||
"YES": "Ja",
|
||||
"NO": "Nein"
|
||||
|
||||
@@ -259,8 +259,6 @@ APP_SETTINGS.WHISPER_MODEL,Speech-to-text model,Dropdown label in Settings > Fea
|
||||
APP_SETTINGS.WHISPER_MODEL_HELP,"Whisper model to use.
|
||||
Larger models produce more accurate
|
||||
results, but consume more VRAM.",Tooltip for the speech-to-text model dropdown.
|
||||
APP_SETTINGS.SPEECH_TO_TEXT,"Speech to text","Category header in Settings"
|
||||
APP_SETTINGS.WHISPER_GPU_SELECT,"Select GPU","A list of GPUs the user can select from"
|
||||
APP_SETTINGS.TUTORIAL,Tutorial,Tooltip for tutorial button in the main screen.
|
||||
APP_SETTINGS.GRID_OPACITY,Floor grid opacity,Slider in Settings > Skybox tab. Controls the opacity of the floor grid when skybox is enabled.
|
||||
APP_SETTINGS.GRID_OPACITY_HELP,Opacity of the floor grid when the skymap is enabled,"Tooltip for the floor grid opacity slider. ""Skymap"" was previously translated as ""${DEFINITIONS.SKYMAP}""."
|
||||
@@ -341,9 +339,7 @@ POPUP_ADD_DISPLAY.RESOLUTION,Resolution,Label for the resolution field in the ad
|
||||
POWER_OPTIONS.TITLE,"Power options","A button which allows the user to perform a specific power action"
|
||||
POWER_OPTIONS.SHUTDOWN,"Shut down","One of the power options: shuts down the computer"
|
||||
POWER_OPTIONS.SUSPEND,"Suspend","One of the power options: suspends the computer"
|
||||
POWER_OPTIONS.NOT_SUPPORTED,"Power options require systemd or elogind.","Notification to the user that power options (shutdown and suspend) are not available. 'systemd' and 'elogind' are package names and should not be translated."
|
||||
POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE,"Are you sure you want to turn off your computer?","Question dialog box before shutting down the computer"
|
||||
POWER_OPTIONS.SUSPEND_ARE_YOU_SURE,"Are you sure you want to put your computer to sleep?","Question dialog box before putting the computer in sleep mode"
|
||||
PROCESS.FORCE_KILL,Force-kill,Tooltip on the kill button in the running games list.
|
||||
PROCESS.STOP,Stop,Tooltip on the stop button in the running games list.
|
||||
PROCESS_LIST,Process list,Tab name and group box title in the Monado runtime process list section.
|
||||
|
||||
|
@@ -258,8 +258,6 @@
|
||||
},
|
||||
"WHISPER_MODEL": "Speech-to-text model",
|
||||
"WHISPER_MODEL_HELP": "Whisper model to use.\nLarger models produce more accurate\nresults, but consume more VRAM.",
|
||||
"SPEECH_TO_TEXT": "Speech to text",
|
||||
"WHISPER_GPU_SELECT": "Select GPU",
|
||||
"TUTORIAL": "Tutorial",
|
||||
"GRID_OPACITY": "Floor grid opacity",
|
||||
"GRID_OPACITY_HELP": "Opacity of the floor grid when the skymap is enabled",
|
||||
@@ -352,9 +350,7 @@
|
||||
"TITLE": "Power options",
|
||||
"SHUTDOWN": "Shut down",
|
||||
"SUSPEND": "Suspend",
|
||||
"NOT_SUPPORTED": "Power options require systemd or elogind.",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Are you sure you want to turn off your computer?",
|
||||
"SUSPEND_ARE_YOU_SURE": "Are you sure you want to put your computer to sleep?"
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Are you sure you want to turn off your computer?"
|
||||
},
|
||||
"PROCESS": {
|
||||
"FORCE_KILL": "Force-kill",
|
||||
|
||||
@@ -263,9 +263,7 @@
|
||||
},
|
||||
"TUTORIAL": "Tutorial",
|
||||
"RESTART_WAYVR": "Reiniciar WayVR",
|
||||
"CLOSE_WAYVR": "Cerrar WayVR",
|
||||
"SPEECH_TO_TEXT": "Dictado por voz",
|
||||
"WHISPER_GPU_SELECT": "Seleccionar GPU"
|
||||
"CLOSE_WAYVR": "Cerrar WayVR"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Lanzador de aplicaciones",
|
||||
"APPLICATION_STARTED": "Aplicación iniciada",
|
||||
@@ -375,9 +373,7 @@
|
||||
"TITLE": "Opciones de energía",
|
||||
"SHUTDOWN": "Apagar",
|
||||
"SUSPEND": "Suspender",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "¿Seguro que quieres apagar el ordenador?",
|
||||
"NOT_SUPPORTED": "Las opciones de energía requieren systemd o elogind.",
|
||||
"SUSPEND_ARE_YOU_SURE": "¿Estás seguro de que quieres suspender el equipo?"
|
||||
"SHUTDOWN_ARE_YOU_SURE": "¿Seguro que quieres apagar el ordenador?"
|
||||
},
|
||||
"YES": "Sí",
|
||||
"NO": "No"
|
||||
|
||||
@@ -263,9 +263,7 @@
|
||||
},
|
||||
"TUTORIAL": "Tutorial",
|
||||
"RESTART_WAYVR": "Riavvia WayVR",
|
||||
"CLOSE_WAYVR": "Chiudi WayVR",
|
||||
"SPEECH_TO_TEXT": "Speech-to-text",
|
||||
"WHISPER_GPU_SELECT": "Seleziona GPU"
|
||||
"CLOSE_WAYVR": "Chiudi WayVR"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Avvio applicazioni",
|
||||
"APPLICATION_STARTED": "Applicazione avviata",
|
||||
@@ -375,9 +373,7 @@
|
||||
"TITLE": "Opzioni di alimentazione",
|
||||
"SHUTDOWN": "Spegni",
|
||||
"SUSPEND": "Sospendi",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Sei sicuro di voler spegnere il computer?",
|
||||
"NOT_SUPPORTED": "Le opzioni di alimentazione richiedono systemd o elogind.",
|
||||
"SUSPEND_ARE_YOU_SURE": "Sei sicuro di voler mettere il computer in sospensione?"
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Sei sicuro di voler spegnere il computer?"
|
||||
},
|
||||
"YES": "Sì",
|
||||
"NO": "No"
|
||||
|
||||
@@ -263,9 +263,7 @@
|
||||
},
|
||||
"TUTORIAL": "チュートリアル",
|
||||
"RESTART_WAYVR": "WayVRを再起動",
|
||||
"CLOSE_WAYVR": "WayVRを閉じる",
|
||||
"SPEECH_TO_TEXT": "音声入力",
|
||||
"WHISPER_GPU_SELECT": "GPUを選択"
|
||||
"CLOSE_WAYVR": "WayVRを閉じる"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "アプリランチャー",
|
||||
"APPLICATION_STARTED": "アプリが起動しました",
|
||||
@@ -375,9 +373,7 @@
|
||||
"TITLE": "電源オプション",
|
||||
"SHUTDOWN": "シャットダウン",
|
||||
"SUSPEND": "サスペンド",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "コンピューターをシャットダウンしてもよろしいですか?",
|
||||
"NOT_SUPPORTED": "電源オプションには systemd または elogind が必要です。",
|
||||
"SUSPEND_ARE_YOU_SURE": "コンピューターをスリープ状態にしますか?"
|
||||
"SHUTDOWN_ARE_YOU_SURE": "コンピューターをシャットダウンしてもよろしいですか?"
|
||||
},
|
||||
"YES": "はい",
|
||||
"NO": "いいえ"
|
||||
|
||||
@@ -263,9 +263,7 @@
|
||||
},
|
||||
"TUTORIAL": "Samouczek",
|
||||
"RESTART_WAYVR": "Zrestartuj WayVR",
|
||||
"CLOSE_WAYVR": "Zamknij WayVR",
|
||||
"SPEECH_TO_TEXT": "Zamiana mowy na tekst",
|
||||
"WHISPER_GPU_SELECT": "Wybierz GPU"
|
||||
"CLOSE_WAYVR": "Zamknij WayVR"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "Launcher aplikacji",
|
||||
"APPLICATION_STARTED": "Uruchomiono aplikację",
|
||||
@@ -375,9 +373,7 @@
|
||||
"TITLE": "Opcje zasilania",
|
||||
"SHUTDOWN": "Wyłącz",
|
||||
"SUSPEND": "Uśpij",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Czy na pewno chcesz wyłączyć komputer?",
|
||||
"NOT_SUPPORTED": "Opcje zasilania wymagają systemd lub elogind.",
|
||||
"SUSPEND_ARE_YOU_SURE": "Czy na pewno chcesz uśpić komputer?"
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Czy na pewno chcesz wyłączyć komputer?"
|
||||
},
|
||||
"YES": "Tak",
|
||||
"NO": "Nie"
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
{
|
||||
"DEFINITIONS": {
|
||||
"ANCHORED": "Förankrad",
|
||||
"FLOATING": "Flytande",
|
||||
"STATIC": "Stationär",
|
||||
"SET": "Set",
|
||||
"WATCH": "Klocka",
|
||||
"DASHBOARD": "Kontrollpanel",
|
||||
"EDIT_MODE": "Redigeringsläge",
|
||||
"SETTINGS": "Inställningar",
|
||||
"KEYBOARD": "Tangentbord",
|
||||
"TASKBAR": "Uppgiftsfält",
|
||||
"FEATURES": "Funktioner",
|
||||
"OVERLAY": "Overlay",
|
||||
"SKYMAP": "Skymap",
|
||||
"PLAYSPACE": "Playspace",
|
||||
"D_PAD": "D-pad"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"RECENTER_PLAYSPACE": "Centrera om playspace"
|
||||
},
|
||||
"APP_LAUNCHER": {
|
||||
"ASPECT": {
|
||||
"SEMI_TALL": "Halvhög",
|
||||
"SEMI_WIDE": "Halvbredd",
|
||||
"SQUARE": "Kvadratisk",
|
||||
"TALL": "Hög",
|
||||
"WIDE": "Bredbild"
|
||||
},
|
||||
"ASPECT_TITLE": "Bildförhållande",
|
||||
"AUTOSTART": "Kör automatiskt vid start",
|
||||
"LAUNCH": "Starta",
|
||||
"MODE": {
|
||||
"CAGE": "Kompatibilitetsläge (Cage)",
|
||||
"NATIVE": "Nativt läge"
|
||||
},
|
||||
"RES_TITLE": "Upplösning"
|
||||
},
|
||||
"APP_SETTINGS": {
|
||||
"ALLOW_SLIDING": "Stick-interaktion vid grepp",
|
||||
"ANIMATION_SPEED": "Hastighet för UI-animationer",
|
||||
"AUTOSTART_APPS": "Appar som ska köras vid start",
|
||||
"BLOCK_GAME_INPUT": "Blockera spelindata",
|
||||
"BLOCK_GAME_INPUT_HELP": "Blockerar all input när en overlay hovras över",
|
||||
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignorera klockan vid blockering av input",
|
||||
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Blockera inte inmatning när klockan är markerad",
|
||||
"BLOCK_POSES_ON_KBD_INTERACTION": "Blockera poser vid interaktion med tangentbord",
|
||||
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blockerar spelets mottagning av poser när tangentbordet hovras över och 'Blockera spel-input' är aktiverat",
|
||||
"BINDINGS": {
|
||||
"COMP": {
|
||||
"CLICK": "Klick",
|
||||
"FORCE": "Tryckkraft",
|
||||
"TOUCH": "Beröring",
|
||||
"VALUE": "Värde",
|
||||
"PROXIMITY": "Närhet",
|
||||
"X_AXIS": "X-axel",
|
||||
"Y_AXIS": "Y-axel"
|
||||
},
|
||||
"TYPE": {
|
||||
"BUMPER": "Bumper",
|
||||
"DPAD_UP": "D-pad upp",
|
||||
"DPAD_DOWN": "D-pad ned",
|
||||
"DPAD_LEFT": "D-pad vänster",
|
||||
"DPAD_RIGHT": "D-pad höger",
|
||||
"JOYSTICK": "Joystick",
|
||||
"MENU": "Meny",
|
||||
"SHOULDER": "Axel",
|
||||
"SQUEEZE": "Grepp",
|
||||
"SYSTEM": "System",
|
||||
"THUMBREST": "Tumstöd",
|
||||
"THUMBSTICK": "Thumbstick",
|
||||
"TRACKPAD": "Trackpad",
|
||||
"TRIGGER": "Trigger",
|
||||
"VIEW": "Visa"
|
||||
},
|
||||
"CLICK": {
|
||||
"ANY": "Vilken som helst",
|
||||
"DOUBLE": "Dubbelklicka",
|
||||
"TRIPLE": "Trippelklick",
|
||||
"TYPE": "Antal tryck"
|
||||
},
|
||||
"ACTION": {
|
||||
"CLICK": "Klicka*",
|
||||
"GRAB": "Greppa*",
|
||||
"ALT_CLICK": "Anpassat shell-kommando (ställ in alt_click i config)",
|
||||
"SHOW_HIDE": "Visa, dölj, centrera om*",
|
||||
"TOGGLE_DASHBOARD": "Växla instrumentpanel",
|
||||
"SPACE_DRAG": "Dra playspace",
|
||||
"SPACE_ROTATE": "Rotera playspace",
|
||||
"SPACE_RESET": "Återställ Playspace",
|
||||
"CLICK_MODIFIER_RIGHT": "Högerklick-modifierare",
|
||||
"CLICK_MODIFIER_MIDDLE": "Modifierare för mittklick",
|
||||
"MOVE_MOUSE": "Flytta mus (om avstängd som standard)",
|
||||
"SCROLL": "Scrolla*"
|
||||
},
|
||||
"LEFT": "Vänster",
|
||||
"RIGHT": "Höger",
|
||||
"CHORD": "Kombination",
|
||||
"CHORD_ADD_INPUT": "Lägg till inmatning för kombination",
|
||||
"CHORD_INPUT_HAND": "Inmatningshand",
|
||||
"COMPONENT": "Aktiveringstyp",
|
||||
"SUBPATH": "Aktiverande kontroll",
|
||||
"THRESHOLD": "Aktivera vid övre värdet\nAvaktivera vid nedre värdet"
|
||||
},
|
||||
"BROWSE_ONLINE_CATALOG": "Bläddra i onlinekatalogen...",
|
||||
"BROWSE_SKYMAPS": "Bläddra bland skymaps",
|
||||
"CAPTURE_METHOD": "Metod för skärmavbildning",
|
||||
"CAPTURE_METHOD_HELP": "Prova att ändra detta om du\nupplever svarta eller glitchiga skärmar",
|
||||
"COLOR_KEYING": "Färgnyckelning",
|
||||
"COLOR_PALETTE": "Färgpalett",
|
||||
"COLOR_PALETTE_HELP": "Klicka för att bläddra bland färgpaletter.\nSe webbplatsen för att skapa egna!",
|
||||
"COLOR_PALETTES": "Färgpaletter",
|
||||
"COLOR_PALETTE_CURRENT": "Nuvarande aktivt tema.",
|
||||
"COLOR_PALETTE_CUSTOM_URL": "Lägg till ett eget anpassat tema!",
|
||||
"COLOR_PALETTE_CUSTOM_URL2": "Klicka för att öppna webbmanualen.",
|
||||
"COLOR_PALETTE_ACTIVATE": "Klicka för att använda detta tema.",
|
||||
"CLEAR_PIPEWIRE_TOKENS": "Rensa PipeWire-tokens",
|
||||
"CLEAR_PIPEWIRE_TOKENS_HELP": "Fråga om skärmval vid nästa start",
|
||||
"CLEAR_SAVED_STATE": "Återställ sparade inställningar",
|
||||
"CLEAR_SAVED_STATE_HELP": "Återställ sets och overlay-positioner",
|
||||
"CLICK_FREEZE_TIME_MS": "Klickfrysningstid (ms)",
|
||||
"CLICK_FREEZE_TIME_MS_HELP": "Hjälper till med precision vid dubbelklick",
|
||||
"CLOCK_12H": "12-timmarsklocka",
|
||||
"CONTROLS": "Kontroller",
|
||||
"DELETE_ALL_CONFIGS": "Rensa konfiguration",
|
||||
"DELETE_ALL_CONFIGS_HELP": "Ta bort alla konfigurationsfiler från conf.d",
|
||||
"DOUBLE_CURSOR_FIX": "Fix för dubbla markörer",
|
||||
"DOUBLE_CURSOR_FIX_HELP": "Aktivera om du ser två markörer",
|
||||
"DEFAULT_OVERLAY_SCALE": "Standardstorlek för overlay",
|
||||
"DEFAULT_OVERLAY_SCALE_HELP": "Skapa nya overlays i den här skalan.",
|
||||
"DEFAULT_CURVATURE": "Standardkurvatur för overlay",
|
||||
"DEFAULT_OPACITY": "Standardopacitet för overlay",
|
||||
"DEFAULT_POSITIONING": "Standardposition för overlay",
|
||||
"OVERLAY_DEFAULTS_HELP": "Gäller endast nya overlays",
|
||||
"ENABLED": "Aktiverad",
|
||||
"FEATURES": "Funktioner",
|
||||
"FOCUS_FOLLOWS_MOUSE_MODE": "Musrörelse vid trigger-tryck",
|
||||
"HANDSFREE_POINTER": "Handsfree-läge",
|
||||
"HANDSFREE_POINTER_HELP": "Inmatning som används när rörelsekontroller saknas.\nVänster nyp är greppa, höger är klicka.",
|
||||
"HANDSFREE_ALT_TAB": "Sikte för app-växlare",
|
||||
"HANDSFREE_ALT_TAB_HELP": "Håll in Super+X, sikta på önskat fönster och släpp sedan för att byta fokus.",
|
||||
"DIAGNOSTICS": {
|
||||
"CPU_ACCESSIBLE_VRAM": "CPU-tillgängligt VRAM: {NUM} MiB",
|
||||
"TITLE": "Diagnostik",
|
||||
"ENABLE_REBAR": "Aktivera [resizable bar] i BIOS-inställningarna för optimal VR-prestanda"
|
||||
},
|
||||
"DIAGNOSTICS_REASON": {
|
||||
"PACTL": "Krävs för kontroll av högtalare/mikrofon och byte av ljudkort",
|
||||
"CAGE": "Rekommenderas vid inkompatibilitet med vissa X11-applikationer (kompatibilitetsläge)",
|
||||
"XWAYLAND_SATELLITE": "Rekommenderas om du vill köra X11 (Xwayland)-applikationer i nativt läge",
|
||||
"XDG_OPEN": "Krävs för att öppna URL:er eller Steam-spel"
|
||||
},
|
||||
"HIDE_GRAB_HELP": "Dölj grepphjälp",
|
||||
"HIDE_USERNAME": "Dölj användarnamn",
|
||||
"INPUT_EMULATION_METHOD": "Metod för inmatningssimulering",
|
||||
"INPUT_EMULATION_METHOD_HELP": "Prova att ändra detta om mus- eller\ntangentbordindata inte registreras.",
|
||||
"INPUT_CAPTURE_METHOD": "Metod för inmatningsfångst",
|
||||
"INPUT_CAPTURE_METHOD_HELP": "Möjliggör användning av fysisk kringutrustning i WayVR.\n Super+Z: Växla input mellan WayVR och skrivbordet\n Super+X: Växla mellan WayVR-appar",
|
||||
"INPUT_PROFILES": "Ändra styrinställningar",
|
||||
"INPUT_PROFILES_HELP": "OpenXR-kontrollbindningar",
|
||||
"INVERT_SCROLL_DIRECTION_X": "Invertera horisontell scrollriktning",
|
||||
"INVERT_SCROLL_DIRECTION_Y": "Invertera vertikal rullningsriktning",
|
||||
"LENS_SEPARATION_MM": "Linsavstånd (mm)",
|
||||
"KEYBOARD_MIDDLE_CLICK": "Tangentbordets mittklick",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier som används vid skrivning\nmed lila laser",
|
||||
"KEYBOARD_SOUND_ENABLED": "Tangentbordsljud",
|
||||
"LANGUAGE": "Språk",
|
||||
"LEFT_HANDED_MOUSE": "Vänstermus",
|
||||
"LEFT_HANDED_MOUSE_HELP": "Används om musknapparna är ombytta",
|
||||
"LONG_PRESS_DURATION": "Långtryckstid",
|
||||
"LOOK_AND_FEEL": "Utseende och känsla",
|
||||
"MISC": "Övrigt",
|
||||
"MIDDLE_GRABS_SINGLE": "Modifierare för mittklick greppar enskilda objekt",
|
||||
"MIDDLE_GRABS_SINGLE_HELP": "Att greppa med modifieraren för mittklick (lila laser)\ngreppar enskilda overlays istället för hela setet",
|
||||
"NOTIFICATIONS_ENABLED": "Aktivera aviseringar",
|
||||
"NOTIFICATIONS_SOUND_ENABLED": "Ljud för aviseringar",
|
||||
"NOT_SUPPORTED": "Ej stöds",
|
||||
"NO_SKYMAPS_FOUND": "Inga skymaps hittades",
|
||||
"OPAQUE_BACKGROUND": "Opak bakgrund",
|
||||
"OPTION": {
|
||||
"ANCHORED_HELP": "Följer centermarkören. Rekommenderas.",
|
||||
"FLOATING_HELP": "Flyttas oberoende, centreras om.\nLätt att tappa bort; rekommenderas ej.",
|
||||
"STATIC_HELP": "Håller sig på plats i förhållande till världen.",
|
||||
"AUTO": "Automatisk",
|
||||
"AUTO_HELP": "ScreenCopy GPU om det stöds,\nannars PipeWire GPU.",
|
||||
"EYE_ONLY": "Ögonlaser",
|
||||
"EYE_PINCH": "Öga + pinch",
|
||||
"HMD_ONLY": "HMD-laser",
|
||||
"HMD_PINCH": "HMD + pinch",
|
||||
"NONE": "Ingen",
|
||||
"NONE_INPUT_HELP": "Emulera inte mus och tangentbord.\nInteraktion med skärmar blir inte möjlig.",
|
||||
"NONE_INPUT_CAPTURE_HELP": "Fånga inte mus och tangentbord.\nDu kommer inte att kunna använda fysisk mus\neller tangentbord i appar som startas i WayVR.",
|
||||
"PIPEWIRE_HELP": "Snabb GPU-avbildning,\nstandard för alla skrivbord.",
|
||||
"PW_FALLBACK_HELP": "Långsam metod med hög CPU-belastning.\nProva om PipeWire GPU inte fungerar",
|
||||
"SCREENCOPY_GPU_HELP": "Snabb, inga popup-fönster för skärmdelning.\nFungerar på: Hyprland, Niri, River, Sway",
|
||||
"SCREENCOPY_HELP": "Långsam, inga popup-fönster för skärmdelning.\nFungerar på: Hyprland, Niri, River, Sway",
|
||||
"UINPUT": "Enhetsemulering",
|
||||
"UINPUT_HELP": "Använder modulen uinput för att emulera en enhet.\nFungerar generellt bra på de flesta skrivbord.",
|
||||
"EVDEV_HELP": "Ta kontroll över inmatningsenheter för exklusiv användning.\nAnvändaren måste tillhöra gruppen 'input'.",
|
||||
"WL_VIRTUAL": "Wayland virtuell inmatning",
|
||||
"WL_VIRTUAL_HELP": "Skickar händelser till Wayland-kompositorn via\nzwlr_virtual_pointer och zwp_virtual_keyboard.\nFungerar endast på vissa skrivbord.\nFungerar INTE på KDE, GNOME eller COSMIC."
|
||||
},
|
||||
"POINTER_ACCELERATION": "Musacceleration",
|
||||
"POINTER_ACCELERATION_HELP": "På: Adaptiv musacceleration\nAv: Konstant hastighet",
|
||||
"POINTER_LERP_FACTOR": "Utjämning av pekare",
|
||||
"POINTER_SPEED": "Mushastighet",
|
||||
"REQUIRES_RESTART": "Kräver omstart",
|
||||
"RESET_PLAYSPACE": "Återställ playspace",
|
||||
"RESET_PLAYSPACE_HELP": "Nollställ offset för playspace.",
|
||||
"SAVE_PLAYSPACE_CENTER": "Spara playspace-center",
|
||||
"SAVE_PLAYSPACE_CENTER_HELP": "Använd nuvarande playspace för bindningen \"återställ playspace\".",
|
||||
"RESET_PLAYSPACE_CENTER": "Återställ playspace-center",
|
||||
"RESET_PLAYSPACE_CENTER_HELP": "Rensa tidigare inställd mittpunkt för playspace.",
|
||||
"RESTART_WAYVR": "Starta om WayVR",
|
||||
"CLOSE_WAYVR": "Stäng WayVR",
|
||||
"ROUND_MULTIPLIER": "UI-kanternas rundning",
|
||||
"SCREEN_RENDER_DOWN": "Rendera skärm i lägre upplösning",
|
||||
"SCREEN_RENDER_DOWN_HELP": "Hjälper mot aliasing på skärmar med hög upplösning",
|
||||
"SCROLL_SPEED": "Scrollningshastighet",
|
||||
"SELECT_VARIANT": "Välj variant",
|
||||
"SNAP_ANGLE_DEG": "Snapvinkel för fönster (pitch & roll)",
|
||||
"SNAP_ANGLE_DEG_HELP": "Greppade fönster snäpps till denna vinkel på pitch- och rollaxeln.",
|
||||
"ENABLE_WATCH": "Aktivera klockan",
|
||||
"SETS_ON_WATCH": "Set på klockan",
|
||||
"SKYBOX": "Skymap",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "Denna skymap är redan nedladdad. Välj åtgärd.",
|
||||
"SPACE_DRAG": "Playspace",
|
||||
"SPACE_DRAG_MULTIPLIER": "Multiplikator för space-drag",
|
||||
"SPACE_DRAG_AFFECTS_WORLD": "Lämna overlays kvar vid flytt av playspace",
|
||||
"SPACE_DRAG_UNLOCKED": "Tillåt space-drag på alla axlar",
|
||||
"SPACE_GRAVITY_DAMPING": "Dämpning",
|
||||
"SPACE_GRAVITY_DAMPING_HELP": "Artificiell dämpning för att sakta ner rörelser. 0,1 – hög dämpning, 1,0 – ingen dämpning",
|
||||
"SPACE_GRAVITY_FLING_STRENGTH": "Kaststyrka",
|
||||
"SPACE_GRAVITY_FLING_STRENGTH_HELP": "Intensitetsmultiplikator för gravitationskraften efter space-drag.\n0.0 - ingen rörelse alls, 2.0 - dubbel intensitet",
|
||||
"SPACE_GRAVITY_GRAVITY": "Gravitation",
|
||||
"SPACE_GRAVITY_GRAVITY_HELP": "Mängd nedåtgående kraft. 0.0 - ingen gravitation",
|
||||
"SPACE_GRAVITY_GROUND_FRICTION": "Markfriktion",
|
||||
"SPACE_GRAVITY_GROUND_FRICTION_HELP": "Mängden friktion som saktar ner dig när du rör vid marken.\n0.0 - ingen friktion (som på is), 1.0 - sträv yta",
|
||||
"SPACE_GRAVITY_FLOOR_HEIGHT": "Golvhöjd",
|
||||
"SPACE_GRAVITY_FLOOR_HEIGHT_HELP": "Y-positionen för golvet. Gravitationen upphör när denna höjd nås.",
|
||||
"SPACE_ROTATE_UNLOCKED": "Tillåt rotation av playspace på alla axlar",
|
||||
"ROTATIONS": "HMD-rotationer",
|
||||
"SESSION_TIME": "Sessionstid",
|
||||
"STATISTICS": "Statistik",
|
||||
"TROUBLESHOOTING": "Felsökning",
|
||||
"UI_GRADIENT_INTENSITY": "Intensitet för UI-gradient",
|
||||
"UPRIGHT_SCREEN_FIX": "Fix för upprätt skärm",
|
||||
"UPRIGHT_SCREEN_FIX_HELP": "Fixar upprätt ställda skärmar på vissa skrivbord",
|
||||
"USE_PASSTHROUGH": "Aktivera passthrough",
|
||||
"USE_PASSTHROUGH_HELP": "Tillåt passthrough om XR-runtime stöder det",
|
||||
"USE_SKYBOX": "Visa skymap",
|
||||
"USE_SKYBOX_HELP": "Visa en skymap om ingen scenapp eller passthrough används",
|
||||
"WATCH_VIEW_ANGLE": "Klockans synvinklar",
|
||||
"WATCH_VIEW_ANGLE_HELP": "Styr hur klockan tonas ut",
|
||||
"WHISPER": {
|
||||
"NEED_TO_DOWNLOAD_MODEL": "Den valda modellen måste laddas ner.\nVill du ladda ner den nu?",
|
||||
"REMOVE_UNUSED_MODELS": "Du stänger av tal-till-text-funktionen.\nVill du ta bort nedladdade Whisper-modeller för att frigöra utrymme?"
|
||||
},
|
||||
"WHISPER_MODEL": "Tal-till-text-modell",
|
||||
"WHISPER_MODEL_HELP": "Whisper-modell som ska användas.\nStörre modeller ger mer precisa\nresultat, men förbrukar mer VRAM.",
|
||||
"SPEECH_TO_TEXT": "Tal till text",
|
||||
"WHISPER_GPU_SELECT": "Välj GPU",
|
||||
"TUTORIAL": "Handbok",
|
||||
"GRID_OPACITY": "Golvgridens opacitet",
|
||||
"GRID_OPACITY_HELP": "Opacitet för golvgridet när skymap är aktiverat",
|
||||
"XWAYLAND_BY_DEFAULT": "Kör appar i kompatibilitetsläge som standard",
|
||||
"SAVE": "Spara",
|
||||
"CANCEL": "Avbryt"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "App-startare",
|
||||
"APPLICATION_STARTED": "Applikationen startades",
|
||||
"APPLICATIONS": "Applikationer",
|
||||
"AUDIO": {
|
||||
"SWITCH_TO_THIS_DEVICE": "Byt till den här enheten",
|
||||
"AUTO_SWITCH_TO_VR_AUDIO": "Byt automatiskt till VR-ljud",
|
||||
"CARDS": "Kort",
|
||||
"FAILED_TO_SWITCH_MICROPHONE": "Kunde inte byta mikrofon",
|
||||
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofonen har ändrats",
|
||||
"MICROPHONES": "Mikrofoner",
|
||||
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Ingen VR-mikrofon hittades. Byt manuellt.",
|
||||
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Inga VR-högtalare hittades. Byt manuellt.",
|
||||
"SELECT_AUDIO_CARD_PROFILE": "Välj ljudkortsprofil",
|
||||
"SETTINGS": "Ljudinställningar",
|
||||
"SPEAKERS": "Högtalare",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "Högtalarna har ändrats",
|
||||
"VOLUME": "Volym"
|
||||
},
|
||||
"CLOSE_WINDOW": "Stäng fönster",
|
||||
"CREATION_DATE": "Skapad datum",
|
||||
"DEBUG_INFO": "Debuginfo",
|
||||
"DISPLAY_BRIGHTNESS": "Skärmljusstyrka",
|
||||
"DONATE": {
|
||||
"SUPPORT_US": "Stöd oss",
|
||||
"DESCRIPTION": "Vi är ett litet team på två utvecklare som bygger WayVR på vår fritid. Om du gillar att använda programvaran, överväg att stötta oss – varje donation hjälper oss att fortsätta.",
|
||||
"THANK_YOU_WAYVR": "Tack från WayVR-teamet!",
|
||||
"BUTTON_TOOLTIP": "Donera via Open Collective",
|
||||
"CURRENT_SUPPORTERS": "Nuvarande supportrar",
|
||||
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Stöds av {USER}|Särskilt tack till {USER}|Sponsras av {USER}|Backas av {USER}|Drivs av community-medlemmar som {USER}|Möjliggjorts av {USER}|Upprätthålls av {USER}|Bidrag från {USER}|Finansierat av {USER}",
|
||||
"BROUGHT_TO_YOU_VARIANTS_RARE": "Kaffe sponsrat av {USER}|Drivs av {USER}|Hälsningar till {USER}!|Håller oss igång: {USER}|Hjälper oss att växa: {USER}|Håller servrarna igång: {USER}|Med tacksamhet till {USER}|Drivs av {USER}|Stöds av vänner som {USER}",
|
||||
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "Stöttas av legender som {USER}|Koffein sponsrat av {USER}|En sann legend: {USER}|Dagens hjälte: {USER}|En total superstjärna: {USER}|Betalar elräkningen: {USER}|Räddaren av vårt förstånd: {USER}|Visar vägen: {USER}|Koffein-till-kod-omvandlare tillhandahållen av {USER}|Håller produktionen stabil: {USER}|Den ende och ende: {USER}|Skickar dmabuf-buffertar: {USER}"
|
||||
},
|
||||
"DOWNLOAD": "Ladda ner",
|
||||
"DOWNLOADER": "Nedladdare",
|
||||
"WELCOME": {
|
||||
"ALL_SET": "Nu är du redo!",
|
||||
"BASICS": "Dessa grunder gör att du enkelt kan börja använda WayVR.",
|
||||
"CENTER_MARKER": "Centreringsmarkören visas framför dig vid omcentrering. Placera overlays runt den!",
|
||||
"DOUBLE_PRESS_TO_TOGGLE": "Dubbeltryck B (på Index) eller Y (på Meta) för att visa, centrera om eller dölja ditt set. Prova nu.",
|
||||
"FEATURES_DOCUMENTATION": "Det finns många andra funktioner värda att anpassa. För mer information, se dokumentationen på",
|
||||
"FINISH_TUTORIAL": "Avsluta handledningen",
|
||||
"GRAB_BOTH_HANDS": "Greppa med båda händerna om du vill flytta ett specifikt overlay i ditt set.",
|
||||
"GRAB_ONE_HAND": "Greppa med en hand för att flytta ditt set.",
|
||||
"GREETING": "Hej!",
|
||||
"GUIDE_INTRO": "Låt oss guida dig genom dina första steg.",
|
||||
"LASER_COLORS": "Laserfärger",
|
||||
"LASER_FUNCTIONALITY": "Mycket av funktionaliteten i WayVR beror på vilken laserfärg som används för att interagera med ett UI-element. Du behöver inte trycka på knapparna, det räcker att röra vid dem!",
|
||||
"LAST_PAGE_INFO": "Denna handledning slutar visas när du når den sista sidan.",
|
||||
"LOOK_AT_LEFT_WRIST": "Titta på din vänstra handled. Det är din klocka.",
|
||||
"MIDDLE_CLICK_MODE_PURPLE_LASER": "Mittenklick-läge: Lila laser",
|
||||
"MOVING_YOUR_SET": "Flytta ditt set",
|
||||
"NEXT_BUTTON": "Tryck på nästa-knappen nedan för att komma igång.",
|
||||
"REGULAR_MODE_BLUE_LASER": "Vanligt läge: Blå laser",
|
||||
"REPOSITION_THIS_WINDOW_BY_GRABBING": "Flytta det här fönstret med kontrollens grip-funktion.",
|
||||
"RIGHT_CLICK_MODE_ORANGE_LASER": "Högerklicksläge: Orange laser",
|
||||
"THANKS_FOR_INSTALLING": "Tack för att du installerade WayVR!",
|
||||
"WATCH": "Klocka",
|
||||
"WORKING_SET": "Aktivt Set"
|
||||
},
|
||||
"DOWNLOAD_AGAIN": "Ladda ner igen",
|
||||
"DOWNLOADING_FILE": "Laddar ner fil...",
|
||||
"FAILED_TO_LAUNCH_APPLICATION": "Kunde inte starta applikationen:",
|
||||
"GAME_LAUNCHED": "Spelet har startats",
|
||||
"GAME_LIST": {
|
||||
"NO_GAMES_FOUND": "Inga spel hittades",
|
||||
"RUNNING_GAMES_LIST": "Lista över körande spel"
|
||||
},
|
||||
"GAMES": "Spel",
|
||||
"GENERAL_SETTINGS": "Allmänna inställningar",
|
||||
"GETTING_STARTED": "Kom igång",
|
||||
"HEIGHT": "Höjd",
|
||||
"HELLO": "Hej!",
|
||||
"HELLO_USER": "Hej, {USER}!",
|
||||
"HIDE": "Dölj",
|
||||
"HOME_SCREEN": "Hem",
|
||||
"MODIFICATION_DATE": "Ändringsdatum",
|
||||
"MONADO_RUNTIME": "Monado runtime",
|
||||
"LOADING": "Laddar...",
|
||||
"POPUP_ADD_DISPLAY": {
|
||||
"RESOLUTION": "Upplösning"
|
||||
},
|
||||
"POWER_OPTIONS": {
|
||||
"TITLE": "Strömalternativ",
|
||||
"SHUTDOWN": "Stäng av",
|
||||
"SUSPEND": "Viloläge",
|
||||
"NOT_SUPPORTED": "Strömalternativ kräver systemd eller elogind.",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "Är du säker på att du vill stänga av datorn?",
|
||||
"SUSPEND_ARE_YOU_SURE": "Är du säker på att du vill försätta datorn i viloläge?"
|
||||
},
|
||||
"PROCESS": {
|
||||
"FORCE_KILL": "Tvinga avslut",
|
||||
"STOP": "Stoppa"
|
||||
},
|
||||
"PROCESS_LIST": "Processlista",
|
||||
"REFRESH": "Uppdatera",
|
||||
"REMOVE": "Ta bort",
|
||||
"APPLY": "Verkställ",
|
||||
"RELOAD_FROM_DISK": "Ladda om från disk",
|
||||
"SETTINGS": "Inställningar",
|
||||
"SHOW": "Visa",
|
||||
"TARGET_PATH": "Mål sökväg",
|
||||
"TERMINATE_PROCESS": "Avsluta process",
|
||||
"VERSION": "Version",
|
||||
"WIDTH": "Bredd",
|
||||
"PINNED_APPS": "Fästa applikationer",
|
||||
"SAVED_TO_FAVOURITES": "Sparad i fästa applikationer",
|
||||
"ADD_TO_FAVOURITES": "Lägg till i fästa applikationer",
|
||||
"ADD_TO_FAVOURITES_HINT": "Spara förinställning och fäst den i app-menyn",
|
||||
"REMOVE_FROM_FAVOURITES": "Ta bort från fästa applikationer",
|
||||
"EMPTY": "Tomt",
|
||||
"INSTALLED": "Installerad",
|
||||
"NOT_INSTALLED": "Ej installerad",
|
||||
"YES": "Ja",
|
||||
"NO": "Nej"
|
||||
}
|
||||
@@ -263,9 +263,7 @@
|
||||
},
|
||||
"TUTORIAL": "教程",
|
||||
"RESTART_WAYVR": "重启 WayVR",
|
||||
"CLOSE_WAYVR": "关闭 WayVR",
|
||||
"SPEECH_TO_TEXT": "语音转文本",
|
||||
"WHISPER_GPU_SELECT": "选择 GPU"
|
||||
"CLOSE_WAYVR": "关闭 WayVR"
|
||||
},
|
||||
"APPLICATION_LAUNCHER": "应用启动器",
|
||||
"APPLICATION_STARTED": "应用程序已启动",
|
||||
@@ -375,9 +373,7 @@
|
||||
"TITLE": "电源选项",
|
||||
"SHUTDOWN": "关机",
|
||||
"SUSPEND": "暂停",
|
||||
"SHUTDOWN_ARE_YOU_SURE": "确定要关机吗?",
|
||||
"NOT_SUPPORTED": "电源选项需要 systemd 或 elogind。",
|
||||
"SUSPEND_ARE_YOU_SURE": "确定要让计算机进入睡眠状态吗?"
|
||||
"SHUTDOWN_ARE_YOU_SURE": "确定要关机吗?"
|
||||
},
|
||||
"YES": "确定",
|
||||
"NO": "否"
|
||||
|
||||
@@ -25,7 +25,7 @@ use wgui::{
|
||||
use wlx_common::{
|
||||
async_executor::AsyncExecutor,
|
||||
audio,
|
||||
dash_interface::{BoxDashInterface, ConfigChangeKind, DashBackendTask, DashPlayspaceTask},
|
||||
dash_interface::{BoxDashInterface, ConfigChangeKind, DashPlayspaceTask},
|
||||
locale::WayVRLangProvider,
|
||||
palette::load_palette,
|
||||
timestep::{self, Timestep},
|
||||
@@ -131,7 +131,6 @@ pub enum FrontendTask {
|
||||
RunCommand(String /* program */, Vec<String> /* args */),
|
||||
HideDashboard,
|
||||
MarkTutorialGraduated,
|
||||
BackendTask(DashBackendTask),
|
||||
}
|
||||
|
||||
impl<T: 'static> Frontend<T> {
|
||||
@@ -286,23 +285,21 @@ impl<T: 'static> Frontend<T> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn power_action_confirm(&mut self, power_action: PowerAction) {
|
||||
let translation_key = match power_action {
|
||||
PowerAction::Suspend => "POWER_OPTIONS.SUSPEND_ARE_YOU_SURE",
|
||||
PowerAction::Poweroff => "POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE",
|
||||
};
|
||||
|
||||
fn action_poweroff_confirm(&mut self) {
|
||||
let tasks = self.tasks.clone();
|
||||
views::dialog_box::mount_popup(
|
||||
self.popup_dialog_box.clone(),
|
||||
self.tasks.clone(),
|
||||
views::dialog_box::Params {
|
||||
globals: self.globals.clone(),
|
||||
message: Translation::from_translation_key(translation_key),
|
||||
message: Translation::from_translation_key("POWER_OPTIONS.SHUTDOWN_ARE_YOU_SURE"),
|
||||
entries: views::dialog_box::get_entries_yes_no(),
|
||||
on_action_click: Box::new(move |action| {
|
||||
if action == "yes" {
|
||||
tasks.push(task_for_power_action(power_action));
|
||||
tasks.push(FrontendTask::RunCommand(
|
||||
String::from("loginctl"),
|
||||
vec![String::from("poweroff")],
|
||||
));
|
||||
}
|
||||
}),
|
||||
},
|
||||
@@ -310,6 +307,14 @@ impl<T: 'static> Frontend<T> {
|
||||
}
|
||||
|
||||
fn process_power_action(&mut self, action: &str, data: &mut T) {
|
||||
if !is_exec_installed("loginctl") {
|
||||
// TODO: add support for other init systems?
|
||||
self.tasks.push(FrontendTask::PushToast(Translation::from_raw_text(
|
||||
"loginctl is not installed",
|
||||
)));
|
||||
return;
|
||||
}
|
||||
|
||||
match action {
|
||||
"restart" => {
|
||||
self.interface.restart(data);
|
||||
@@ -318,10 +323,13 @@ impl<T: 'static> Frontend<T> {
|
||||
self.interface.close(data);
|
||||
}
|
||||
"suspend" => {
|
||||
self.power_action_confirm(PowerAction::Suspend);
|
||||
self.tasks.push(FrontendTask::RunCommand(
|
||||
String::from("loginctl"),
|
||||
vec![String::from("suspend")],
|
||||
));
|
||||
}
|
||||
"shutdown" => {
|
||||
self.power_action_confirm(PowerAction::Poweroff);
|
||||
self.action_poweroff_confirm();
|
||||
}
|
||||
_ => {
|
||||
unreachable!()
|
||||
@@ -447,7 +455,6 @@ impl<T: 'static> Frontend<T> {
|
||||
FrontendTask::OpenURL(url) => self.action_open_url(url),
|
||||
FrontendTask::RunCommand(program, args) => self.action_run_command(program, args),
|
||||
FrontendTask::MarkTutorialGraduated => self.action_tutorial_graduated(params.data),
|
||||
FrontendTask::BackendTask(task) => self.interface.backend_task(params.data, task)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
@@ -674,23 +681,3 @@ impl<T: 'static> Frontend<T> {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PowerAction {
|
||||
Suspend,
|
||||
Poweroff,
|
||||
}
|
||||
|
||||
pub fn task_for_power_action(action: PowerAction) -> FrontendTask {
|
||||
for cmd in ["systemctl", "loginctl"] {
|
||||
if is_exec_installed(cmd) {
|
||||
let (command, args) = match action {
|
||||
PowerAction::Suspend => (cmd.to_string(), vec!["suspend".to_string()]),
|
||||
PowerAction::Poweroff => (cmd.to_string(), vec!["poweroff".to_string()]),
|
||||
};
|
||||
log::debug!("Command line for {action:?}: {command} {}", args.join(" "));
|
||||
return FrontendTask::RunCommand(command, args);
|
||||
}
|
||||
}
|
||||
FrontendTask::PushToast(Translation::from_translation_key("POWER_OPTIONS.NOT_SUPPORTED"))
|
||||
}
|
||||
@@ -61,7 +61,7 @@ impl<T> Tab<T> for TabApps<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [Rc<DesktopEntry>]) -> Option<&'a Rc<DesktopEntry>> {
|
||||
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [DesktopEntry]) -> Option<&'a DesktopEntry> {
|
||||
entries.iter().find(|&entry| *entry.app_id == *app_id)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ impl<T> TabApps<T> {
|
||||
|
||||
let mut stale_entries = Vec::<Rc<str>>::new();
|
||||
|
||||
let mut pinned_desktop_entries = Vec::<(PinnedApp, Rc<DesktopEntry>)>::new();
|
||||
let mut pinned_desktop_entries = Vec::<(PinnedApp, &DesktopEntry)>::new();
|
||||
|
||||
// collect pinned desktop entries
|
||||
{
|
||||
@@ -83,7 +83,7 @@ impl<T> TabApps<T> {
|
||||
continue;
|
||||
};
|
||||
|
||||
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry.clone()));
|
||||
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry));
|
||||
}
|
||||
// cleanup:
|
||||
// remove non-existent app ids from pinned apps
|
||||
@@ -119,7 +119,7 @@ impl<T> TabApps<T> {
|
||||
&mut state.parser_state,
|
||||
&doc_params(frontend.globals.clone()),
|
||||
self.pinned_apps_parent,
|
||||
desktop_entry.as_ref(),
|
||||
desktop_entry,
|
||||
Some(tooltip_string),
|
||||
)?;
|
||||
|
||||
@@ -139,8 +139,8 @@ impl<T> TabApps<T> {
|
||||
|
||||
struct AppList {
|
||||
//data: Vec<ParserData>,
|
||||
all_entries: Vec<Rc<DesktopEntry>>,
|
||||
entries_to_mount: VecDeque<Rc<DesktopEntry>>,
|
||||
all_entries: Vec<DesktopEntry>,
|
||||
entries_to_mount: VecDeque<DesktopEntry>,
|
||||
list_parent: WidgetPair,
|
||||
prev_category_name: String,
|
||||
}
|
||||
@@ -150,7 +150,7 @@ fn on_app_click(
|
||||
frontend_tasks: FrontendTasks,
|
||||
tasks: Tasks<Task>,
|
||||
globals: WguiGlobals,
|
||||
entry: Rc<DesktopEntry>,
|
||||
entry: DesktopEntry,
|
||||
state: Rc<RefCell<State>>,
|
||||
pinned_app: Option<PinnedApp>,
|
||||
) -> ButtonClickCallback {
|
||||
@@ -307,17 +307,14 @@ fn get_category_name_score(name: &str) -> u8 {
|
||||
Scores::Unknown as u8
|
||||
}
|
||||
|
||||
fn get_best_category_name<S>(categories: &[S]) -> Option<&str>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
fn get_best_category_name(categories: &[Rc<str>]) -> Option<&Rc<str>> {
|
||||
let mut best_score: u8 = 0;
|
||||
let mut best_category: Option<&str> = None;
|
||||
let mut best_category: Option<&Rc<str>> = None;
|
||||
|
||||
for cat in categories {
|
||||
let score = get_category_name_score(cat.as_ref());
|
||||
let score = get_category_name_score(cat);
|
||||
if score > best_score {
|
||||
best_category = Some(cat.as_ref());
|
||||
best_category = Some(cat);
|
||||
best_score = score;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart},
|
||||
util::wgui_simple,
|
||||
};
|
||||
use crate::tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart};
|
||||
use wgui::{
|
||||
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
|
||||
components::{
|
||||
button::{ButtonClickEvent, ComponentButton},
|
||||
checkbox::ComponentCheckbox,
|
||||
slider::ComponentSlider,
|
||||
},
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
windowing::context_menu,
|
||||
};
|
||||
use wlx_common::{DesktopBackend, XrBackend, config::GeneralConfig, dash_interface::InterfaceFeats};
|
||||
@@ -295,6 +297,7 @@ where
|
||||
mp.idx += 1;
|
||||
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("id", &id);
|
||||
|
||||
match setting.get_translation() {
|
||||
Ok(translation) => params.insert("translation", translation),
|
||||
@@ -307,63 +310,69 @@ where
|
||||
|
||||
let id_cell = horiz_cell(mp.layout, parent)?;
|
||||
|
||||
let setting_str = setting.as_ref();
|
||||
let title = setting.get_enum_title(mp.config);
|
||||
|
||||
let feats = mp.feats;
|
||||
let cells = EnumType::VARIANTS
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
if item.get_bool("Hidden").unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
if item
|
||||
.get_str("Backend")
|
||||
.is_some_and(|x| XrBackend::try_from(x).unwrap() != feats.xr_backend)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if item
|
||||
.get_str("Desktop")
|
||||
.is_some_and(|x| DesktopBackend::try_from(x).unwrap() != feats.desktop_backend)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let value = item.as_ref();
|
||||
let title = SettingType::get_enum_title_inner(*item);
|
||||
let tooltip = SettingType::get_enum_tooltip_inner(*item);
|
||||
|
||||
let text = &title.text;
|
||||
let translated = if title.translated { "1" } else { "0" };
|
||||
let action = format!("{setting_str};{id};{value};{text};{translated}");
|
||||
|
||||
Some(context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(action.into()),
|
||||
title,
|
||||
tooltip,
|
||||
..Default::default()
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tasks = mp.tasks.clone();
|
||||
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
|
||||
layout: mp.layout,
|
||||
parser_state: mp.parser_state,
|
||||
doc_params: mp.doc_params,
|
||||
parent: id_cell,
|
||||
id,
|
||||
params,
|
||||
current_text: title,
|
||||
cells,
|
||||
on_open: Box::new(move |pos, cells| tasks.push(Task::OpenContextMenu(pos, cells))),
|
||||
})?;
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, id_cell, params)?;
|
||||
|
||||
if setting.requires_restart() {
|
||||
mount_requires_restart(mp.layout, id_cell)?;
|
||||
}
|
||||
|
||||
let setting_str = setting.as_ref();
|
||||
let title = setting.get_enum_title(mp.config);
|
||||
|
||||
{
|
||||
let mut label = mp
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
|
||||
label.set_text_simple(&mut mp.layout.state.globals.get(), title);
|
||||
}
|
||||
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let feats = mp.feats;
|
||||
let tasks = mp.tasks.clone();
|
||||
move |_common, e: ButtonClickEvent| {
|
||||
tasks.push(Task::OpenContextMenu(
|
||||
e.mouse_pos_absolute.into(),
|
||||
EnumType::VARIANTS
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
if item.get_bool("Hidden").unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
if item
|
||||
.get_str("Backend")
|
||||
.is_some_and(|x| XrBackend::try_from(x).unwrap() != feats.xr_backend)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if item
|
||||
.get_str("Desktop")
|
||||
.is_some_and(|x| DesktopBackend::try_from(x).unwrap() != feats.desktop_backend)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let value = item.as_ref();
|
||||
let title = SettingType::get_enum_title_inner(*item);
|
||||
let tooltip = SettingType::get_enum_tooltip_inner(*item);
|
||||
|
||||
let text = &title.text;
|
||||
let translated = if title.translated { "1" } else { "0" };
|
||||
|
||||
Some(context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
|
||||
title,
|
||||
tooltip,
|
||||
..Default::default()
|
||||
}))
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,6 @@ pub struct TabSettings<T> {
|
||||
tasks: Tasks<Task>,
|
||||
marker: PhantomData<T>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
feats: InterfaceFeats,
|
||||
}
|
||||
|
||||
impl<T> Tab<T> for TabSettings<T> {
|
||||
@@ -698,14 +697,10 @@ impl<T> TabSettings<T> {
|
||||
fn set_tab(&mut self, frontend: &mut Frontend<T>, data: &mut T, name: TabNameEnum) -> anyhow::Result<()> {
|
||||
let root = self.state.get_widget_id("settings_root")?;
|
||||
frontend.layout.remove_children(root);
|
||||
self.current_tab = None;
|
||||
let globals = frontend.layout.state.globals.clone();
|
||||
self.current_tab = None;
|
||||
|
||||
let vulkan_devices = if matches!(name, TabNameEnum::Features) {
|
||||
Some(frontend.interface.enumerate_vulkan_devices(data)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let feats = frontend.interface.get_feats(data);
|
||||
|
||||
let mut mp = MacroParams {
|
||||
layout: &mut frontend.layout,
|
||||
@@ -714,14 +709,14 @@ impl<T> TabSettings<T> {
|
||||
config: frontend.interface.general_config(data),
|
||||
tasks: self.tasks.clone(),
|
||||
idx: 9001,
|
||||
feats: self.feats,
|
||||
feats,
|
||||
};
|
||||
|
||||
let settings_mount_params = SettingsMountParams {
|
||||
mp: &mut mp,
|
||||
id_parent: root,
|
||||
frontend_tasks: &self.frontend_tasks,
|
||||
feats: self.feats,
|
||||
feats,
|
||||
};
|
||||
|
||||
match name {
|
||||
@@ -729,10 +724,7 @@ impl<T> TabSettings<T> {
|
||||
self.current_tab = Some(Box::new(tab_look_and_feel::State::mount(settings_mount_params)?));
|
||||
}
|
||||
TabNameEnum::Features => {
|
||||
self.current_tab = Some(Box::new(tab_features::State::mount(
|
||||
settings_mount_params,
|
||||
vulkan_devices.unwrap(), /* safe */
|
||||
)?));
|
||||
self.current_tab = Some(Box::new(tab_features::State::mount(settings_mount_params)?));
|
||||
}
|
||||
TabNameEnum::SpaceDrag => {
|
||||
self.current_tab = Some(Box::new(tab_space_drag::State::mount(settings_mount_params)?));
|
||||
@@ -798,8 +790,6 @@ impl<T> TabSettings<T> {
|
||||
|
||||
tasks.push(Task::SetTab(TabNameEnum::LookAndFeel));
|
||||
|
||||
let feats = frontend.interface.get_feats(data);
|
||||
|
||||
Ok(Self {
|
||||
app_button_ids: Vec::new(),
|
||||
tasks,
|
||||
@@ -808,7 +798,6 @@ impl<T> TabSettings<T> {
|
||||
context_menu: ContextMenu::default(),
|
||||
current_tab: None,
|
||||
frontend_tasks: frontend.tasks.clone(),
|
||||
feats,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,7 @@ use wgui::parser::TemplateParams;
|
||||
use wgui::{
|
||||
components::button::ComponentButton, globals::WguiGlobals, layout::WidgetID, parser::Fetchable, task::Tasks,
|
||||
};
|
||||
use wlx_common::XrBackend;
|
||||
use wlx_common::dash_interface::DashBackendTask;
|
||||
|
||||
use crate::frontend::FrontendTask;
|
||||
use crate::tab::settings::{UpdateExtra, horiz_cell, mount_requires_restart};
|
||||
use crate::util::popup_manager::PopupHolder;
|
||||
|
||||
@@ -21,8 +18,7 @@ use crate::{
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
OpenInputProfilesDash,
|
||||
OpenInputProfilesBackend,
|
||||
OpenInputProfiles,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
@@ -38,23 +34,13 @@ impl SettingsTab for State {
|
||||
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::OpenInputProfilesDash => {
|
||||
Task::OpenInputProfiles => {
|
||||
input_profiles::mount_popup(
|
||||
self.frontend_tasks.clone(),
|
||||
self.globals.clone(),
|
||||
self.popup_input_profiles.clone(),
|
||||
);
|
||||
}
|
||||
Task::OpenInputProfilesBackend => {
|
||||
self
|
||||
.frontend_tasks
|
||||
.push(FrontendTask::BackendTask(DashBackendTask::ShowBindingsUI));
|
||||
|
||||
// overlay sort order doesn't work on steamvr,
|
||||
// so the dashboard would be drawn over the bindings UI
|
||||
// while the bindings UI has input focus
|
||||
self.frontend_tasks.push(FrontendTask::HideDashboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +53,6 @@ fn create_input_profiles_button(
|
||||
parent: WidgetID,
|
||||
tasks: Tasks<Task>,
|
||||
_popup: &PopupHolder<input_profiles::View>,
|
||||
xr_backend: XrBackend,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
@@ -86,11 +71,7 @@ fn create_input_profiles_button(
|
||||
btn.on_click(Rc::new({
|
||||
let tasks = tasks.clone();
|
||||
move |_common, _e| {
|
||||
if xr_backend.is_open_vr() {
|
||||
tasks.push(Task::OpenInputProfilesBackend);
|
||||
} else {
|
||||
tasks.push(Task::OpenInputProfilesDash);
|
||||
}
|
||||
tasks.push(Task::OpenInputProfiles);
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
@@ -111,7 +92,9 @@ impl State {
|
||||
"APP_SETTINGS.CONTROLS",
|
||||
"@/dashboard/controller.svg",
|
||||
)?;
|
||||
create_input_profiles_button(par.mp, c, tasks.clone(), &popup, par.feats.xr_backend)?;
|
||||
if par.feats.xr_backend.is_open_xr() {
|
||||
create_input_profiles_button(par.mp, c, tasks.clone(), &popup)?;
|
||||
}
|
||||
options_dropdown::<wlx_common::config::AltModifier>(par.mp, c, &SettingType::KeyboardMiddleClick)?;
|
||||
options_dropdown::<wlx_common::config::HandsfreePointer>(par.mp, c, &SettingType::HandsfreePointer)?;
|
||||
options_dropdown::<wlx_common::config::HandsfreeAltTab>(par.mp, c, &SettingType::HandsfreeAltTab)?;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use wgui::{
|
||||
components::button::{ButtonClickEvent, ComponentButton},
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::WidgetID,
|
||||
@@ -8,7 +9,7 @@ use wgui::{
|
||||
parser::{Fetchable, TemplateParams},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
windowing::context_menu,
|
||||
windowing::context_menu::{self},
|
||||
};
|
||||
use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind};
|
||||
|
||||
@@ -20,7 +21,6 @@ use crate::{
|
||||
},
|
||||
util::{
|
||||
popup_manager::PopupHolder,
|
||||
wgui_simple,
|
||||
whisper::{
|
||||
WHISPER_MODELS, WhisperModel, whisper_any_models_downloaded, whisper_delete_all_models, whisper_model_from_name,
|
||||
whisper_model_path,
|
||||
@@ -104,60 +104,41 @@ impl SettingsTab for State {
|
||||
layout: &mut wgui::layout::Layout,
|
||||
state: &mut wgui::parser::ParserState,
|
||||
) -> anyhow::Result<()> {
|
||||
let (Some(_), Some(_type), Some(id), Some(value)) = ({
|
||||
let mut s = action.splitn(4, ';');
|
||||
(s.next(), s.next(), s.next(), s.next())
|
||||
}) else {
|
||||
debug_assert!(false);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match _type {
|
||||
"whisper_model" => {
|
||||
let model_name = value;
|
||||
if !model_name.is_empty()
|
||||
&& let Some(model) = whisper_model_from_name(model_name)
|
||||
{
|
||||
let mut common = layout.common();
|
||||
let mut label = state.fetch_widget_as::<WidgetLabel>(common.state, &format!("{id}_value"))?;
|
||||
label.set_text(&mut common, Translation::from_raw_text(model.display_name));
|
||||
|
||||
config.whisper_model = model.file_name.into();
|
||||
if !whisper_model_path(model.file_name).exists() {
|
||||
self.show_whisper_model_dialog_box_download(model)?;
|
||||
} else {
|
||||
change_kind.replace(ConfigChangeKind::Other);
|
||||
}
|
||||
} else {
|
||||
config.whisper_model = "".into();
|
||||
change_kind.replace(ConfigChangeKind::Other);
|
||||
// re-init the whole tab
|
||||
if whisper_any_models_downloaded().unwrap_or_default() {
|
||||
self.show_whisper_model_dialog_box_cleanup()?;
|
||||
} else {
|
||||
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
|
||||
}
|
||||
}
|
||||
}
|
||||
"whisper_gpu" => {
|
||||
if let (Some(_), Some(id), Some(model_name)) = {
|
||||
let mut s = action.splitn(3, ';');
|
||||
(s.next(), s.next(), s.next())
|
||||
} {
|
||||
if !model_name.is_empty()
|
||||
&& let Some(model) = whisper_model_from_name(model_name)
|
||||
{
|
||||
let mut common = layout.common();
|
||||
let mut label = state.fetch_widget_as::<WidgetLabel>(common.state, &format!("{id}_value"))?;
|
||||
config.whisper_gpu = value.parse::<u32>().unwrap();
|
||||
label.set_text(
|
||||
&mut common,
|
||||
Translation::from_raw_text_string(config.whisper_gpu.to_string()),
|
||||
);
|
||||
label.set_text(&mut common, Translation::from_raw_text(model.display_name));
|
||||
|
||||
config.whisper_model = model.file_name.into();
|
||||
if !whisper_model_path(model.file_name).exists() {
|
||||
self.show_whisper_model_dialog_box_download(model)?;
|
||||
} else {
|
||||
change_kind.replace(ConfigChangeKind::Other);
|
||||
}
|
||||
} else {
|
||||
config.whisper_model = "".into();
|
||||
change_kind.replace(ConfigChangeKind::Other);
|
||||
// re-init the whole tab
|
||||
if whisper_any_models_downloaded().unwrap_or_default() {
|
||||
self.show_whisper_model_dialog_box_cleanup()?;
|
||||
} else {
|
||||
self.parent_tasks.push(ParentTask::SetTab(TabNameEnum::Features));
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams, vulkan_devices: Vec<String>) -> anyhow::Result<State> {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
|
||||
let tasks = Tasks::<Task>::new();
|
||||
let popup_download = PopupHolder::<views::download_file::View>::default();
|
||||
let popup_dialog = PopupHolder::<views::dialog_box::View>::default();
|
||||
@@ -170,9 +151,7 @@ impl State {
|
||||
)?;
|
||||
|
||||
if par.feats.whisper {
|
||||
let c = options_category(par.mp, c, "APP_SETTINGS.SPEECH_TO_TEXT", "@/dashboard/stt.svg")?;
|
||||
whisper_models_dropdown(par.mp, c)?;
|
||||
whisper_gpu_select_dropdown(par.mp, c, vulkan_devices)?;
|
||||
}
|
||||
|
||||
options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
|
||||
@@ -308,48 +287,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn whisper_gpu_select_dropdown(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
vulkan_devices: Vec<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
|
||||
let parent = horiz_cell(mp.layout, parent)?;
|
||||
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("translation", "APP_SETTINGS.WHISPER_GPU_SELECT");
|
||||
|
||||
let current_text = Translation::from_raw_text_string(mp.config.whisper_gpu.to_string());
|
||||
|
||||
let cells = vulkan_devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, name)| !name.contains("llvmpipe"))
|
||||
.map(|(idx, name)| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!(";whisper_gpu;{};{}", id, idx).into()),
|
||||
title: Translation::from_raw_text_string(name.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tasks = mp.tasks.clone();
|
||||
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
|
||||
layout: mp.layout,
|
||||
parser_state: mp.parser_state,
|
||||
doc_params: mp.doc_params,
|
||||
parent,
|
||||
id,
|
||||
params,
|
||||
current_text,
|
||||
cells,
|
||||
on_open: Box::new(move |pos, cells| tasks.push(ParentTask::OpenContextMenu(pos, cells))),
|
||||
})
|
||||
}
|
||||
|
||||
fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
@@ -370,49 +307,57 @@ fn whisper_models_dropdown(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Re
|
||||
});
|
||||
|
||||
let mut params = TemplateParams::new();
|
||||
params.insert("id", &id);
|
||||
params.insert("translation", "APP_SETTINGS.WHISPER_MODEL");
|
||||
params.insert("tooltip", "APP_SETTINGS.WHISPER_MODEL_HELP");
|
||||
|
||||
let mut cells = WHISPER_MODELS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, item)| {
|
||||
if downloaded_models.contains(&idx) {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!(";whisper_model;{id};{}", item.file_name).into()),
|
||||
title: Translation::from_raw_text(item.display_name),
|
||||
sprite_src_builtin: Some("@/dashboard/check.svg".into()),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!(";whisper_model;{id};{}", item.file_name).into()),
|
||||
title: Translation::from_raw_text(item.display_name),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
|
||||
|
||||
cells.insert(
|
||||
0,
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!(";whisper_model;{id};").into()),
|
||||
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
{
|
||||
let mut label = mp
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
|
||||
label.set_text_simple(&mut mp.layout.state.globals.get(), current_translation);
|
||||
}
|
||||
|
||||
let tasks = mp.tasks.clone();
|
||||
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
|
||||
layout: mp.layout,
|
||||
parser_state: mp.parser_state,
|
||||
doc_params: mp.doc_params,
|
||||
parent,
|
||||
id,
|
||||
params,
|
||||
current_text: current_translation,
|
||||
cells,
|
||||
on_open: Box::new(move |pos, cells| tasks.push(ParentTask::OpenContextMenu(pos, cells))),
|
||||
})
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let parent_tasks = mp.tasks.clone();
|
||||
move |_common, e: ButtonClickEvent| {
|
||||
let mut cells = WHISPER_MODELS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, item)| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!(";{id};{}", item.file_name).into()),
|
||||
title: Translation::from_raw_text(item.display_name),
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
sprite_src_builtin: if downloaded_models.contains(&idx) {
|
||||
Some("@/dashboard/check.svg".into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cells.insert(
|
||||
0,
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(format!(";{id};").into()),
|
||||
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
|
||||
tooltip: None,
|
||||
attribs: vec![],
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
parent_tasks.push(ParentTask::OpenContextMenu(e.mouse_pos_absolute.into(), cells));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -81,7 +81,7 @@ impl BindingsDropdown for XrInputComponent {
|
||||
pub struct ParsedOpenXrInputPath {
|
||||
pub side: XrInputSide,
|
||||
pub subpath: XrInputSubpathKind,
|
||||
pub component: Option<XrInputComponent>,
|
||||
pub component: XrInputComponent,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ParsedOpenXrInputPath {
|
||||
@@ -101,18 +101,13 @@ impl TryFrom<&str> for ParsedOpenXrInputPath {
|
||||
bail!("missing input prefix");
|
||||
}
|
||||
|
||||
let (identifier, component) = match rest.rsplit_once('/') {
|
||||
Some((ident, comp)) if !comp.is_empty() => {
|
||||
let comp = XrInputComponent::try_from(comp).context("bad component")?;
|
||||
(ident, Some(comp))
|
||||
}
|
||||
_ => (rest, None),
|
||||
};
|
||||
let (identifier, component) = rest.rsplit_once('/').context("missing identifier or component")?;
|
||||
|
||||
if identifier.is_empty() {
|
||||
bail!("identifier empty");
|
||||
if identifier.is_empty() || component.is_empty() {
|
||||
bail!("identifier or component empty");
|
||||
}
|
||||
|
||||
let component = XrInputComponent::try_from(component).context("bad component")?;
|
||||
let subpath = XrInputSubpathKind::try_from(identifier).context("bad subpath")?;
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
use glam::{Mat4, Vec2};
|
||||
use std::rc::Rc;
|
||||
use wgui::{
|
||||
animation::{Animation, AnimationDuration, AnimationEasing},
|
||||
assets::AssetPathRef,
|
||||
color::WguiColorName,
|
||||
components::{
|
||||
self,
|
||||
button::{ButtonClickCallback, ButtonClickEvent, ComponentButton},
|
||||
},
|
||||
components::{self, button::ButtonClickCallback},
|
||||
i18n::Translation,
|
||||
layout::{Layout, LayoutTask, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
|
||||
parser::{Fetchable, ParseDocumentParams},
|
||||
renderer_vk::{
|
||||
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
|
||||
util::centered_matrix,
|
||||
@@ -21,7 +17,6 @@ use wgui::{
|
||||
label::{WidgetLabel, WidgetLabelParams},
|
||||
sprite::{WidgetSprite, WidgetSpriteParams},
|
||||
},
|
||||
windowing::context_menu,
|
||||
};
|
||||
|
||||
pub struct CreateButtonParams<'a> {
|
||||
@@ -171,39 +166,3 @@ pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result<WidgetID> {
|
||||
|
||||
Ok(id_root)
|
||||
}
|
||||
|
||||
pub struct MountDropdownParams<'a> {
|
||||
pub layout: &'a mut Layout,
|
||||
pub parser_state: &'a mut ParserState,
|
||||
pub doc_params: &'a ParseDocumentParams<'a>,
|
||||
pub parent: WidgetID,
|
||||
pub id: String,
|
||||
pub params: TemplateParams,
|
||||
pub current_text: Translation,
|
||||
pub cells: Vec<context_menu::Cell>,
|
||||
pub on_open: Box<dyn Fn(context_menu::Position, Vec<context_menu::Cell>)>,
|
||||
}
|
||||
|
||||
pub fn mount_dropdown(par: MountDropdownParams) -> anyhow::Result<()> {
|
||||
let mut params = par.params;
|
||||
params.insert("id", &par.id);
|
||||
|
||||
par
|
||||
.parser_state
|
||||
.instantiate_template(par.doc_params, "DropdownButton", par.layout, par.parent, params)?;
|
||||
|
||||
{
|
||||
let mut label = par
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&par.layout.state, &format!("{}_value", par.id))?;
|
||||
label.set_text_simple(&mut par.layout.state.globals.get(), par.current_text);
|
||||
}
|
||||
|
||||
let btn = par.parser_state.fetch_component_as::<ComponentButton>(&par.id)?;
|
||||
btn.on_click(Rc::new(move |_common, e: ButtonClickEvent| {
|
||||
(par.on_open)(e.mouse_pos_absolute.into(), par.cells.clone());
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -53,7 +53,7 @@ struct LaunchParams<'a, T> {
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
state: ParserState,
|
||||
entry: Rc<DesktopEntry>,
|
||||
entry: DesktopEntry,
|
||||
tasks: Tasks<Task>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
@@ -80,7 +80,7 @@ pub struct View {
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: &'a WguiGlobals,
|
||||
pub entry: Rc<DesktopEntry>,
|
||||
pub entry: DesktopEntry,
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub config: &'a GeneralConfig,
|
||||
@@ -119,8 +119,10 @@ impl View {
|
||||
{
|
||||
let mut label_exec = state.fetch_widget_as::<WidgetLabel>(¶ms.layout.state, "label_exec")?;
|
||||
|
||||
let command = shell_words::join(params.entry.exec_argv.iter().map(|x| x.as_ref()));
|
||||
label_exec.set_text_simple(&mut params.globals.get(), Translation::from_raw_text_string(command));
|
||||
label_exec.set_text_simple(
|
||||
&mut params.globals.get(),
|
||||
Translation::from_raw_text_string(format!("{} {}", params.entry.exec_path, params.entry.exec_args)),
|
||||
);
|
||||
}
|
||||
|
||||
let tasks = Tasks::new();
|
||||
@@ -404,19 +406,14 @@ impl View {
|
||||
env.push("ELECTRON_OZONE_PLATFORM_HINT=wayland".into());
|
||||
}
|
||||
|
||||
let argv = match params.compositor_mode {
|
||||
AppCompositorMode::Cage => ["cage", "--"]
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(params.application.exec_argv.iter().map(|x| x.as_ref()))
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
AppCompositorMode::Native => params
|
||||
.application
|
||||
.exec_argv
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
let args = match params.compositor_mode {
|
||||
AppCompositorMode::Cage => format!("-- {} {}", params.application.exec_path, params.application.exec_args),
|
||||
AppCompositorMode::Native => params.application.exec_args.to_string(),
|
||||
};
|
||||
|
||||
let exec = match params.compositor_mode {
|
||||
AppCompositorMode::Cage => "cage".to_string(),
|
||||
AppCompositorMode::Native => params.application.exec_path.to_string(),
|
||||
};
|
||||
|
||||
let pos_mode = match params.pos_mode {
|
||||
@@ -426,7 +423,7 @@ impl View {
|
||||
};
|
||||
|
||||
let mut userdata = HashMap::new();
|
||||
userdata.insert("desktop-entry".into(), serde_json::to_string(params.application)?);
|
||||
userdata.insert("desktop-entry".to_string(), serde_json::to_string(params.application)?);
|
||||
|
||||
let resolution = Self::calculate_resolution(params.res_mode, params.orientation_mode);
|
||||
|
||||
@@ -435,11 +432,12 @@ impl View {
|
||||
params.auto_start,
|
||||
WvrProcessLaunchParams {
|
||||
env,
|
||||
argv,
|
||||
exec,
|
||||
name: params.application.app_name.to_string(),
|
||||
args,
|
||||
resolution,
|
||||
pos_mode,
|
||||
icon: params.application.icon_path.as_deref().map(String::from),
|
||||
icon: params.application.icon_path.as_ref().map(|x| x.as_ref().to_string()),
|
||||
userdata,
|
||||
},
|
||||
)?;
|
||||
@@ -488,7 +486,7 @@ impl View {
|
||||
pub fn mount_popup(
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
entry: Rc<DesktopEntry>,
|
||||
entry: DesktopEntry,
|
||||
popup: PopupHolder<View>,
|
||||
on_app_pins_changed: Box<dyn Fn()>,
|
||||
pinned_app: Option<PinnedApp>,
|
||||
|
||||
@@ -3,13 +3,17 @@ use std::rc::Rc;
|
||||
use strum::EnumProperty;
|
||||
use wgui::{
|
||||
assets::AssetPathRef,
|
||||
components::{button::ComponentButton, slider::ComponentSlider},
|
||||
components::{
|
||||
button::{ButtonClickEvent, ComponentButton},
|
||||
slider::ComponentSlider,
|
||||
},
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
log::LogErr,
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
windowing::context_menu::{self, TickResult},
|
||||
};
|
||||
use wlx_common::{
|
||||
@@ -158,7 +162,6 @@ impl ViewTrait for View {
|
||||
"subpath" => {
|
||||
apply_subpath(
|
||||
action_binding_mut(action_mut, side),
|
||||
action_name,
|
||||
side,
|
||||
value,
|
||||
self.controller_profile,
|
||||
@@ -614,7 +617,7 @@ fn chord_member_controls(
|
||||
output_side,
|
||||
member_idx,
|
||||
available_components,
|
||||
parsed.and_then(|x| x.component),
|
||||
parsed.as_ref().map(|x| x.component),
|
||||
)?;
|
||||
|
||||
let tasks = mp.tasks.clone();
|
||||
@@ -635,7 +638,7 @@ fn chord_member_controls(
|
||||
})?;
|
||||
|
||||
// put threshold slider on its own row
|
||||
if parsed.and_then(|x| x.component).is_some_and(|x| x.is_analog()) {
|
||||
if parsed.as_ref().is_some_and(|x| x.component.is_analog()) {
|
||||
chord_threshold_slider(mp, parent, action.clone(), output_side, member_idx, member.threshold())?;
|
||||
}
|
||||
|
||||
@@ -663,6 +666,13 @@ fn input_controls_for_hand(mp: &mut MacroParams, par: InputControlsForHandParams
|
||||
|
||||
let parent = horiz_cell(mp.layout, par.parent)?;
|
||||
|
||||
let available_components: Rc<[XrInputComponent]> = current
|
||||
.as_ref()
|
||||
.and_then(|par| user_path.find_subpath(par.subpath))
|
||||
.map(|subp| subp.components)
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
|
||||
let available_subpaths: Rc<[XrInputSubpathKind]> = user_path
|
||||
.paths
|
||||
.iter()
|
||||
@@ -679,33 +689,23 @@ fn input_controls_for_hand(mp: &mut MacroParams, par: InputControlsForHandParams
|
||||
current.as_ref().map(|x| x.subpath),
|
||||
)?;
|
||||
|
||||
if &**par.action == "scroll" {
|
||||
clicks_dropdown(mp, parent, par.action.clone(), par.click_type)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let available_components: Rc<[XrInputComponent]> = current
|
||||
.as_ref()
|
||||
.and_then(|par| user_path.find_subpath(par.subpath))
|
||||
.map(|subp| subp.components)
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
|
||||
if !component_dropdown(
|
||||
mp,
|
||||
parent,
|
||||
par.action.clone(),
|
||||
par.side,
|
||||
available_components,
|
||||
current.and_then(|x| x.component),
|
||||
current.as_ref().map(|x| x.component),
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
clicks_dropdown(mp, parent, par.action.clone(), par.click_type)?;
|
||||
|
||||
if let Some(component) = current.and_then(|x| x.component)
|
||||
if let Some(component) = current.as_ref().map(|x| x.component)
|
||||
&& component.is_analog()
|
||||
&& &**par.action != "scroll"
|
||||
// hax
|
||||
{
|
||||
threshold_slider(mp, parent, par.action.clone(), par.side, par.threshold)?;
|
||||
}
|
||||
@@ -1008,7 +1008,7 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
|
||||
.iter()
|
||||
.map(|item| {
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(item.action_str(&action, side).into()),
|
||||
action_name: Some(item.action_str(&action, side)),
|
||||
title: item.translation(),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -1019,7 +1019,7 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
|
||||
cells.insert(
|
||||
0,
|
||||
context_menu::Cell::Button(context_menu::Button {
|
||||
action_name: Some(action_str.into()),
|
||||
action_name: Some(action_str),
|
||||
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -1032,29 +1032,39 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
|
||||
fn create_dropdown_with_cells(
|
||||
mp: &mut MacroParams,
|
||||
parent: WidgetID,
|
||||
params: TemplateParams,
|
||||
mut params: TemplateParams,
|
||||
current_text: Translation,
|
||||
cells: Vec<context_menu::Cell>,
|
||||
) -> anyhow::Result<()> {
|
||||
let id = mp.idx.to_string();
|
||||
mp.idx += 1;
|
||||
let tasks = mp.tasks.clone();
|
||||
wgui_simple::mount_dropdown(wgui_simple::MountDropdownParams {
|
||||
layout: mp.layout,
|
||||
parser_state: mp.parser_state,
|
||||
doc_params: mp.doc_params,
|
||||
parent,
|
||||
id,
|
||||
params,
|
||||
current_text,
|
||||
cells,
|
||||
on_open: Box::new(move |pos, cells| tasks.push(Task::OpenContextMenu(pos, cells))),
|
||||
})
|
||||
params.insert("id", &id);
|
||||
|
||||
mp.parser_state
|
||||
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
|
||||
|
||||
{
|
||||
let mut label = mp
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
|
||||
label.set_text_simple(&mut mp.layout.state.globals.get(), current_text);
|
||||
}
|
||||
|
||||
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
|
||||
btn.on_click(Rc::new({
|
||||
let tasks = mp.tasks.clone();
|
||||
let cells = cells.clone();
|
||||
move |_common, e: ButtonClickEvent| {
|
||||
tasks.push(Task::OpenContextMenu(e.mouse_pos_absolute.into(), cells.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_subpath(
|
||||
side_mut: &mut Option<OneOrMany<String>>,
|
||||
action_name: &str,
|
||||
side_str: &str,
|
||||
subpath_str: &str,
|
||||
profile: &XrControllerProfile,
|
||||
@@ -1065,12 +1075,6 @@ fn apply_subpath(
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if action_name == "scroll" {
|
||||
*side_mut = Some(OneOrMany::One(format!("/user/hand/{side_str}/input/{subpath_str}")));
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(subpath_obj) = profile.find_userpath(side).and_then(|p| p.find_subpath(subpath)) else {
|
||||
return;
|
||||
};
|
||||
@@ -1083,12 +1087,11 @@ fn apply_subpath(
|
||||
return;
|
||||
};
|
||||
|
||||
let parsed_compo = parsed.component.unwrap_or(*subpath_obj.components.first().unwrap());
|
||||
if subpath_obj.components.contains(&parsed_compo) {
|
||||
parsed_compo
|
||||
} else {
|
||||
*subpath_obj.components.first().unwrap()
|
||||
let mut parsed_compo = parsed.component;
|
||||
if !subpath_obj.components.contains(&parsed_compo) {
|
||||
parsed_compo = *subpath_obj.components.first().unwrap();
|
||||
}
|
||||
parsed_compo
|
||||
} else {
|
||||
*subpath_obj.components.first().unwrap()
|
||||
};
|
||||
@@ -1223,7 +1226,7 @@ fn apply_chord_hand(member: &mut OpenXrInputChordMember, side: XrInputSide, prof
|
||||
};
|
||||
|
||||
let component = parsed
|
||||
.and_then(|parsed| parsed.component)
|
||||
.map(|parsed| parsed.component)
|
||||
.filter(|component| subpath.components.contains(component))
|
||||
.unwrap_or(subpath.components[0]);
|
||||
let path = format!(
|
||||
@@ -1253,12 +1256,8 @@ fn apply_chord_subpath(
|
||||
return;
|
||||
};
|
||||
|
||||
let component = if let Some(comp) = parsed.component {
|
||||
if subpath_obj.components.contains(&comp) {
|
||||
comp
|
||||
} else {
|
||||
first_component
|
||||
}
|
||||
let component = if subpath_obj.components.contains(&parsed.component) {
|
||||
parsed.component
|
||||
} else {
|
||||
first_component
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ impl ViewTrait for View {
|
||||
par.layout,
|
||||
self.id_loading_parent,
|
||||
Vec2::splat(32.0),
|
||||
AssetPathRef::BuiltIn("dashboard/check.svg"),
|
||||
AssetPathRef::BuiltIn("@/dashboard/check.svg"),
|
||||
)?;
|
||||
|
||||
// "Close window" button
|
||||
@@ -114,7 +114,7 @@ impl ViewTrait for View {
|
||||
par.layout,
|
||||
self.id_loading_parent,
|
||||
Vec2::splat(32.0),
|
||||
AssetPathRef::BuiltIn("dashboard/error.svg"),
|
||||
AssetPathRef::BuiltIn("@/dashboard/error.svg"),
|
||||
)?;
|
||||
}
|
||||
Task::Close => {
|
||||
|
||||
@@ -31,5 +31,4 @@ TEMPLATE="de" bun main.ts
|
||||
TEMPLATE="ja" bun main.ts
|
||||
TEMPLATE="es" bun main.ts
|
||||
TEMPLATE="it" bun main.ts
|
||||
TEMPLATE="sv" bun main.ts
|
||||
TEMPLATE="zh_CN" bun main.ts
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"full_name": "Swedish",
|
||||
"examples": [
|
||||
{
|
||||
"key": "BAR.OPACITY",
|
||||
"en": "Opacity",
|
||||
"translated": "Opacitet"
|
||||
},
|
||||
{
|
||||
"key": "PANEL.CLOSE",
|
||||
"en": "Close panel",
|
||||
"translated": "Stäng panelen"
|
||||
},
|
||||
{
|
||||
"key": "SETTINGS.LEFT_HANDED_MODE",
|
||||
"en": "Left-handed mode",
|
||||
"translated": "Vänsterhänt läge"
|
||||
}
|
||||
]
|
||||
}
|
||||
+5
-8
@@ -1,5 +1,5 @@
|
||||
use glam::{Vec2, vec2};
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use std::rc::Rc;
|
||||
use testbed::{Testbed, testbed_any::TestbedAny};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
@@ -8,7 +8,7 @@ use tracing_subscriber::util::SubscriberInitExt;
|
||||
use vulkan::init_window;
|
||||
use wgui::{
|
||||
event::{MouseButtonEvent, MouseButtonIndex, MouseMotionEvent, MouseWheelEvent},
|
||||
gfx::{CommandBufferUsage, PresentStatus, SwapchainError, WGfx, WGfxClearMode},
|
||||
gfx::{CommandBufferUsage, PresentStatus, SwapchainError, WGfxClearMode},
|
||||
renderer_vk::{self},
|
||||
};
|
||||
use winit::{
|
||||
@@ -48,16 +48,13 @@ fn init_logging() {
|
||||
.init();
|
||||
}
|
||||
|
||||
fn load_testbed(
|
||||
gfx: &Arc<WGfx>,
|
||||
audio: &mut Box<dyn audio::AudioProvider>,
|
||||
) -> anyhow::Result<Box<dyn Testbed>> {
|
||||
fn load_testbed(audio: &mut Box<dyn audio::AudioProvider>) -> anyhow::Result<Box<dyn Testbed>> {
|
||||
let mut assets = Box::new(assets::Asset {});
|
||||
audio.register_wgui_samples(assets.as_mut())?;
|
||||
|
||||
let name = std::env::var("TESTBED").unwrap_or_default();
|
||||
Ok(match name.as_str() {
|
||||
"dashboard" => Box::new(TestbedDashboard::new(gfx.clone())?),
|
||||
"dashboard" => Box::new(TestbedDashboard::new()?),
|
||||
"" => Box::new(TestbedGeneric::new(assets)?),
|
||||
_ => Box::new(TestbedAny::new(assets, &name)?),
|
||||
})
|
||||
@@ -78,7 +75,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut scale = window.scale_factor() as f32;
|
||||
|
||||
let mut audio: Box<dyn audio::AudioProvider> = Box::new(audio::RealAudioProvider::new());
|
||||
let mut testbed = load_testbed(&gfx, &mut audio)?;
|
||||
let mut testbed = load_testbed(&mut audio)?;
|
||||
|
||||
let mut mouse = Vec2::ZERO;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::testbed::{Testbed, TestbedUpdateParams};
|
||||
use dash_frontend::frontend::{self, FrontendUpdateParams};
|
||||
use wgui::{gfx::WGfx, i18n::Locale, layout::Layout, theme::WguiTheme};
|
||||
use wgui::{layout::Layout, theme::WguiTheme};
|
||||
use wlx_common::{
|
||||
async_executor::AsyncExecutor, dash_interface_emulated::DashInterfaceEmulated,
|
||||
locale::WayVRLangProvider,
|
||||
@@ -14,10 +14,9 @@ pub struct TestbedDashboard {
|
||||
}
|
||||
|
||||
impl TestbedDashboard {
|
||||
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let interface = DashInterfaceEmulated::new();
|
||||
let lang_provider = WayVRLangProvider::default();
|
||||
let locale = Locale::from_env(&lang_provider);
|
||||
let interface = DashInterfaceEmulated::new(gfx, &locale);
|
||||
let palette_name = std::env::var("PALETTE").unwrap_or_else(|_| "Default".to_string());
|
||||
let executor = wlx_common::async_executor::create_local();
|
||||
|
||||
|
||||
@@ -49,8 +49,9 @@ pub enum HandsfreeParams {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WvrProcessLaunchParams {
|
||||
pub name: String,
|
||||
pub argv: Vec<String>,
|
||||
pub exec: String,
|
||||
pub env: Vec<String>,
|
||||
pub args: String,
|
||||
pub icon: Option<String>,
|
||||
pub resolution: [u32; 2],
|
||||
pub pos_mode: PositionMode,
|
||||
|
||||
@@ -143,11 +143,7 @@
|
||||
"NO_SET_SELECTED": "Kein Set ausgewählt.",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Einen Controller auf den Boden legen!",
|
||||
"NO_KEYMAPS_CONFIGURED": "Keine Tastaturbelegungen konfiguriert",
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "Besuch wayvr.org, um zu erfahren, wie du deine Tastaturlayouts einrichtest!",
|
||||
"STEAMVR_DASH_TITLE": "SteamVR Dashboard geöffnet",
|
||||
"STEAMVR_DASH_TEXT": "SteamVR-Dashboard schließen, um WayVR zu nutzen.",
|
||||
"WAYVR_RUNNING_TITLE": "WayVR läuft!",
|
||||
"WAYVR_RUNNING_TEXT": "Schau auf dein Handgelenk für die Watch oder nutze die Ein/Aus-Belegung, um zu starten!"
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "Besuch wayvr.org, um zu erfahren, wie du deine Tastaturlayouts einrichtest!"
|
||||
},
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "Neues Set hinzufügen",
|
||||
@@ -181,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "Screen-Cast für Display {DISPLAY} auswählen",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "Tastatur/Maus-Erfassung konnte nicht initialisiert werden!\nPrüfe, ob dein Benutzer in der Gruppe 'input' ist.",
|
||||
"WE_ARE_GRABBING": "WayVR steuert jetzt deine Tastatur und Maus!\nDrücke Super+Z, um die Steuerung freizugeben."
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "SteamVR Dashboard geöffnet",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Schließe das SteamVR-Dashboard, um WayVR zu nutzen."
|
||||
}
|
||||
@@ -114,10 +114,8 @@ SCREENCAST.FAIL,Could not initialize screen cast source.,Error message on a wind
|
||||
TOAST.TITLE_SCREENCAST_FAIL,Could not start screen cast source!,Error message on a window that normally displays screen casts.
|
||||
TOAST.CANNOT_ADD_SET,Cannot add set!,Error message telling the user the set could not be added. “Set” was previously translated as “${DEFINITIONS.SET}”
|
||||
TOAST.CANNOT_REMOVE_SET,Cannot remove set!,Error message telling the user the set could not be removed. “Set” was previously translated as “${DEFINITIONS.SET}”
|
||||
TOAST.STEAMVR_DASH_TITLE,SteamVR Dashboard Open,Title of toast shown when SteamVR dashboard is visible
|
||||
TOAST.STEAMVR_DASH_TEXT,Close SteamVR dashboard to start using WayVR.,Body of toast shown when SteamVR dashboard is visible
|
||||
TOAST.WAYVR_RUNNING_TITLE,WayVR is running!,Title of toast shown when WayVR is started but its windows are hidden by default
|
||||
TOAST.WAYVR_RUNNING_TEXT,"Check your wrist for the watch, or use show-hide binding to get started!",Body of toast shown when WayVR is started but its windows are hidden by default
|
||||
STEAMVR_DASH_TOAST_TITLE,SteamVR Dashboard Open,Title of toast shown when SteamVR dashboard is visible
|
||||
STEAMVR_DASH_TOAST_TEXT,Close SteamVR dashboard to start using WayVR.,Body of toast shown when SteamVR dashboard is visible
|
||||
TOAST.DEFAULT_TITLE,Notification,Default title of notifications
|
||||
TOAST.EMPTY_SET,Empty set!,Title of a pop-up when the user switches to an empty set. “Set” was previously translated as “${DEFINITIONS.SET}”
|
||||
TOAST.ERROR,Error,Default title for error pop-ups
|
||||
|
||||
|
@@ -133,10 +133,6 @@
|
||||
"TITLE_SCREENCAST_FAIL": "Could not start screen cast source!",
|
||||
"CANNOT_ADD_SET": "Cannot add set!",
|
||||
"CANNOT_REMOVE_SET": "Cannot remove set!",
|
||||
"STEAMVR_DASH_TITLE": "SteamVR Dashboard Open",
|
||||
"STEAMVR_DASH_TEXT": "Close SteamVR dashboard to start using WayVR.",
|
||||
"WAYVR_RUNNING_TITLE": "WayVR is running!",
|
||||
"WAYVR_RUNNING_TEXT": "Check your wrist for the watch, or use show-hide binding to get started!",
|
||||
"DEFAULT_TITLE": "Notification",
|
||||
"EMPTY_SET": "Empty set!",
|
||||
"ERROR": "Error",
|
||||
@@ -149,6 +145,8 @@
|
||||
"NO_SET_SELECTED": "No set is selected.",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Place one controller on the floor!"
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "SteamVR Dashboard Open",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Close SteamVR dashboard to start using WayVR.",
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "Add a new set",
|
||||
"CLEANUP_MIRRORS": "Remove mirrors that are\nnot currently visible",
|
||||
|
||||
@@ -143,11 +143,7 @@
|
||||
"NO_SET_SELECTED": "No hay ningún set seleccionado.",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "¡Coloca un controlador en el suelo!",
|
||||
"NO_KEYMAPS_CONFIGURED": "Sin distribuciones de teclado configuradas",
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "¡Consulta wayvr.org para configurar tus diseños de teclado!",
|
||||
"STEAMVR_DASH_TITLE": "Panel de SteamVR abierto",
|
||||
"STEAMVR_DASH_TEXT": "Cierra el tablero de SteamVR para empezar a usar WayVR.",
|
||||
"WAYVR_RUNNING_TITLE": "¡WayVR está ejecutándose!",
|
||||
"WAYVR_RUNNING_TEXT": "¡Mira tu muñeca para ver el reloj o usa el mando de mostrar/ocultar para empezar!"
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "¡Consulta wayvr.org para configurar tus diseños de teclado!"
|
||||
},
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "Añadir un set nuevo",
|
||||
@@ -181,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "Seleccionar transmisión de pantalla para la pantalla {DISPLAY}",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "¡No se pudo inicializar la captura de teclado/ratón!\nComprueba que tu usuario esté en el grupo 'input'.",
|
||||
"WE_ARE_GRABBING": "¡WayVR tiene el control de tu teclado y ratón!\nPulsa Super+Z para liberarlos."
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "Panel de SteamVR abierto",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Cierra el tablero de SteamVR para empezar a usar WayVR."
|
||||
}
|
||||
@@ -143,11 +143,7 @@
|
||||
"NO_SET_SELECTED": "Nessun set selezionato.",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Appoggia un controller sul pavimento!",
|
||||
"NO_KEYMAPS_CONFIGURED": "Nessuna disposizione tastiera configurata",
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "Consulta wayvr.org per configurare i layout della tastiera!",
|
||||
"STEAMVR_DASH_TITLE": "Dashboard SteamVR Aperta",
|
||||
"STEAMVR_DASH_TEXT": "Chiudi il pannello di SteamVR per iniziare a usare WayVR.",
|
||||
"WAYVR_RUNNING_TITLE": "WayVR è attivo!",
|
||||
"WAYVR_RUNNING_TEXT": "Controlla il polso per l'orologio, o usa il comando show-hide per iniziare!"
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "Consulta wayvr.org per configurare i layout della tastiera!"
|
||||
},
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "Aggiungi un nuovo set",
|
||||
@@ -181,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "Seleziona la trasmissione schermo per il display {DISPLAY}",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "Impossibile inizializzare la cattura di tastiera/mouse!\nControlla che il tuo utente sia nel gruppo 'input'.",
|
||||
"WE_ARE_GRABBING": "WayVR sta controllando tastiera e mouse!\nPremi Super+Z per rilasciare il controllo."
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "Dashboard di SteamVR aperta",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Chiudi il pannello di SteamVR per iniziare a usare WayVR."
|
||||
}
|
||||
@@ -143,11 +143,7 @@
|
||||
"NO_SET_SELECTED": "セットが選択されていません。",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "コントローラーを片方、床に置いてください!",
|
||||
"NO_KEYMAPS_CONFIGURED": "キーボードレイアウトが設定されていない",
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "キーボードレイアウトの設定方法は wayvr.org を確認してね!",
|
||||
"STEAMVR_DASH_TITLE": "SteamVRダッシュボード展開中",
|
||||
"STEAMVR_DASH_TEXT": "WayVRを使用するには、SteamVRダッシュボードを閉じてください。",
|
||||
"WAYVR_RUNNING_TITLE": "WayVR 実行中!",
|
||||
"WAYVR_RUNNING_TEXT": "手首のウォッチを確認するか、表示・非表示のバインドを使用して開始してください!"
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "キーボードレイアウトの設定方法は wayvr.org を確認してね!"
|
||||
},
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "新しいセットを追加",
|
||||
@@ -181,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "ディスプレイ {DISPLAY} のスクリーンキャストを選択してください",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "キーボード/マウスのキャプチャを初期化できませんでした!\nユーザーが 'input' グループに所属しているか確認してください。",
|
||||
"WE_ARE_GRABBING": "WayVR がキーボードとマウスを制御しています。\n解除するには、Super+Zを押してください。"
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "SteamVRダッシュボード展開中",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "WayVRを使用するには、SteamVRダッシュボードを閉じてください。"
|
||||
}
|
||||
@@ -143,11 +143,7 @@
|
||||
"NO_SET_SELECTED": "Żaden zestaw nie jest wybrany.",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Połóż jeden kontroler na podłodze!",
|
||||
"NO_KEYMAPS_CONFIGURED": "Brak skonfigurowanych układów klawiatury",
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "Odwiedź stronę wayvr.org, aby dowiedzieć się, jak skonfigurować układy klawiatury!",
|
||||
"STEAMVR_DASH_TITLE": "Panel SteamVR otwarty",
|
||||
"STEAMVR_DASH_TEXT": "Zamknij pulpit SteamVR, aby zacząć korzystać z WayVR.",
|
||||
"WAYVR_RUNNING_TITLE": "WayVR działa!",
|
||||
"WAYVR_RUNNING_TEXT": "Spójrz na nadgarstek, by zobaczyć zegarek, lub użyj przycisku pokaż/ukryj, aby zacząć!"
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "Odwiedź stronę wayvr.org, aby dowiedzieć się, jak skonfigurować układy klawiatury!"
|
||||
},
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "Dodaj nowy zestaw",
|
||||
@@ -181,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "Wybierz przesyłanie ekranu dla wyświetlacza {DISPLAY}",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "Nie można zainicjować przechwytywania klawiatury/myszy!\nSprawdź, czy Twój użytkownik należy do grupy 'input'.",
|
||||
"WE_ARE_GRABBING": "WayVR przejął kontrolę nad klawiaturą i myszą!\nNaciśnij Super+Z, aby zwolnić."
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "Panel SteamVR otwarty",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "Zamknij pulpit SteamVR, aby zacząć korzystać z WayVR."
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
{
|
||||
"DEFINITIONS": {
|
||||
"ANCHORED": "Förankrad",
|
||||
"FLOATING": "Flytande",
|
||||
"STATIC": "Statisk",
|
||||
"SET": "Set",
|
||||
"WATCH": "Klocka",
|
||||
"DASHBOARD": "Kontrollpanel",
|
||||
"EDIT_MODE": "Redigeringsläge",
|
||||
"MIRROR": "Spegling",
|
||||
"SETTINGS": "Inställningar",
|
||||
"KEYBOARD": "Tangentbord",
|
||||
"TASKBAR": "Uppgiftspanel",
|
||||
"FEATURES": "Funktioner",
|
||||
"OVERLAY": "Overlay"
|
||||
},
|
||||
"ANCHOR": {
|
||||
"CENTER": "Center"
|
||||
},
|
||||
"BAR": {
|
||||
"ADD_MIRROR": "Lägg till ny spegling-overlay",
|
||||
"ADD_NEW_SET": "Lägg till ny set",
|
||||
"ADD_PASSTHRU": "Lägg till ny passthrough-utskärning",
|
||||
"CLOSE_APP": "Stäng appen",
|
||||
"CLOSE_OVERLAY": "Stäng {$DEFINITIONS.OVERLAY}",
|
||||
"DELETE_CURRENT_SET": "Ta bort nuvarande set",
|
||||
"EDIT_MODE_TOGGLE": "Växla redigeringsläge",
|
||||
"FORCE_CLOSE_APP": "Tvinga stäng app",
|
||||
"HANDSFREE": {
|
||||
"EYE_ONLY": "Ögonsiktet",
|
||||
"EYE_TRACKING": "Öga + pinch",
|
||||
"HMD": "HMD + pinch",
|
||||
"HMD_ONLY": "HMD-laser",
|
||||
"NONE": "Av",
|
||||
"TITLE": "Händsfree-läge"
|
||||
},
|
||||
"RELOAD_FROM_DISK": "Ladda om XML från disk",
|
||||
"RESET_POSITION": "Återställ position",
|
||||
"TOGGLE_VISIBILITY": "Växla synlighet"
|
||||
},
|
||||
"DEFAULT": "Standard",
|
||||
"DISABLED": "Inaktiverat",
|
||||
"DONE": "Klar",
|
||||
"EDIT_MODE": {
|
||||
"ADJUST_CURVATURE": "Justera krökning",
|
||||
"ALIGN_TO_HMD": "Rikta mot HMD",
|
||||
"ALPHA_BLEND_MODE": "Alfablendingsläge",
|
||||
"ANGLE_FADE": "Vinkeltoning",
|
||||
"ANGLE_FADE_HELP": "Tona ut när den inte är vänd mot HMD",
|
||||
"BLENDING_ADDITIVE": "Additiv blandning",
|
||||
"BLOCK_INPUT": "Blockera spel-input",
|
||||
"CURVATURE": "Krökning",
|
||||
"DELETE": "Håll inne för att ta bort från nuvarande set",
|
||||
"DISABLE_GRAB": "Inaktivera grepp",
|
||||
"GLOBAL": "Alltid synlig",
|
||||
"HINT_POINT_WINDOW": "Peka på ett fönster för att ändra dess parametrar.\nNär du är klar, lämna redigeringsläget via knappen till höger.",
|
||||
"INTERPOLATION": "Interpolation",
|
||||
"KEYBOARD": "Tangentbord",
|
||||
"LEAVE": "Lämna redigeringsläge",
|
||||
"LOCK_INTERACTION": "Lås interaktion",
|
||||
"MOUSE": {
|
||||
"FLIP180": "Speglad 180°",
|
||||
"FLIP270": "Speglad 270°",
|
||||
"FLIP90": "Speglad 90°",
|
||||
"FLIPPED": "Speglad",
|
||||
"NORMAL": "Normal",
|
||||
"ROTATE180": "Roterad 180°",
|
||||
"ROTATE270": "Roterad 270°",
|
||||
"ROTATE90": "Roterad 90°",
|
||||
"TITLE": "Musfixar",
|
||||
"WRONG_SCREEN_SELECTION_HELP": "Om markören rör sig på en helt annan skärm\nhar skärmarna troligtvis valts fel. Se readme."
|
||||
},
|
||||
"MOVE_PRESS_AND_DRAG": "Flytta (tryck & dra)",
|
||||
"OPACITY": "Opacitet",
|
||||
"POS_ANCHORED": "Förankrad: Flyttas tillsammans med set. Standard.",
|
||||
"POS_FLOATING": "Flytande: Flyttas oberoende, centreras om vid visning.",
|
||||
"POS_HAND_L": "Följ vänster hand.",
|
||||
"POS_HAND_R": "Följ höger hand.",
|
||||
"POS_HMD": "Följ HMD.",
|
||||
"POS_STATIC": "Statisk: Inte en del av något set, ingen återcentrering.",
|
||||
"POSITIONING": "Positionering",
|
||||
"RESIZE_PRESS_AND_DRAG": "Ändra storlek (tryck & dra)",
|
||||
"RESIZE_CORNER": "Dra för att ändra storlek",
|
||||
"STEREO_3D_MODE": {
|
||||
"ADJUST_MOUSE": "Justera mus",
|
||||
"FULL_FRAME": "Full 3D",
|
||||
"FULL_FRAME_BAT": "Full-BAT",
|
||||
"FULL_FRAME_SBS": "Full-SBS",
|
||||
"FULL_FRAME_TAB": "Full-TAB",
|
||||
"SPLIT_BOTTOM_TOP": "NEDRE→ÖVRE",
|
||||
"SPLIT_LEFT_RIGHT": "VÄNSTRER→HÖGER",
|
||||
"SPLIT_RIGHT_LEFT": "HÖGER→VÄNSTER",
|
||||
"SPLIT_TOP_BOTTOM": "TOPP→BOTTEN",
|
||||
"TITLE": "3D-stereoläge"
|
||||
}
|
||||
},
|
||||
"ALTTAB": {
|
||||
"RELEASE_TO_FOCUS": "Sikta på en app och släpp Super för att fokusera på den.",
|
||||
"WHILE_AIMING": "Vid sikte",
|
||||
"CLICK": "VR-pekarklick",
|
||||
"GRABALL": "Flytta hela setet",
|
||||
"GRAB": "Flytta enskild overlay",
|
||||
"DASH": "Växla WayVR Dashboard",
|
||||
"EDIT": "Växla redigeringsläge",
|
||||
"KEYBOARD": "Växla tangentbord",
|
||||
"SET": "Byt set",
|
||||
"RECENTER": "Visa/dölj/centrera",
|
||||
"GENERAL_BINDS": "Allmänna bindningar",
|
||||
"TOGGLE_CAPTURE": "Återlämna kontrollen till skrivbordet",
|
||||
"FOCUS_MODE": "Fokusläge (håll in Super)",
|
||||
"CLOSE": "Stäng fokuserad app",
|
||||
"MOUSE": "Klick och skroll med fysisk mus översätts även till VR-pekaren"
|
||||
},
|
||||
"GRAB": {
|
||||
"ADJUST_DISTANCE": "Justera avstånd",
|
||||
"ADJUST_SIZE": "Justera storlek",
|
||||
"GRABBING_ANCHORED": "Förankrade överlägg rör sig med setet. Separera ett enskilt fönster genom att greppa det med den andra handen samtidigt som du håller i setet.",
|
||||
"GRABBING_ANCHORED_EDIT": "Det här överlägget kommer att förbli förankrat vid centreringsmarkören.",
|
||||
"GRABBING_FLOATING": "Det här överlägget är flytande och kommer att stanna på plats, om det inte centreras om.",
|
||||
"GRABBING_FOLLOW": "Detta overlay följer den enhet den är kopplad till.",
|
||||
"GRABBING_STATIC": "Det här överlägget är statiskt och kommer att stanna kvar på platsen, oavsett om du centrerar om.",
|
||||
"GRABBING_WATCH": "För att byta hand, flytta klockan framför dig och greppa den med den andra handen.",
|
||||
"UNRESTRICTED_MOVEMENT": "Obegränsad rörelse"
|
||||
},
|
||||
"SCREENCAST": {
|
||||
"PENDING": "Vänligen vänta...",
|
||||
"QUEUED": "Väntar på att en annan overlays skärmdelningsförfrågan ska godkännas.",
|
||||
"WAITING_FOR_USER_DESC": "Kontrollera skrivbordet efter ett popup-fönster för skärmdelning.\nVälj följande:",
|
||||
"WAITING_FOR_USER": "Kontrollera din skrivbordsdisplay för ett popup-fönster om skärmdelning.",
|
||||
"FAIL": "Kunde inte initiera källan för skärmöverföring."
|
||||
},
|
||||
"TOAST": {
|
||||
"TITLE_SCREENCAST_FAIL": "Kunde inte starta källan för skärmdelning!",
|
||||
"CANNOT_ADD_SET": "Kunde inte lägga till set!",
|
||||
"CANNOT_REMOVE_SET": "Kan inte ta bort set!",
|
||||
"STEAMVR_DASH_TITLE": "SteamVR-kontrollpanelen är öppen",
|
||||
"STEAMVR_DASH_TEXT": "Stäng SteamVR-panelen för att börja använda WayVR.",
|
||||
"WAYVR_RUNNING_TITLE": "WayVR körs!",
|
||||
"WAYVR_RUNNING_TEXT": "Kolla handleden för klockan, eller använd bindningen för visa/dölj för att komma igång!",
|
||||
"DEFAULT_TITLE": "Avisering",
|
||||
"EMPTY_SET": "Tomt set!",
|
||||
"ERROR": "Fel",
|
||||
"FIXING_FLOOR_IN_X_SECS": "Justerar golvet om {SECONDS} sekunder...",
|
||||
"LAST_EXISTING_SET": "Detta är det sista befintliga setet.",
|
||||
"LETS_ADD_OVERLAYS": "Låt oss lägga till några overlays från uppgiftspanelen!",
|
||||
"MAXIMUM_SETS_REACHED": "Maximalt antal set har uppnåtts.",
|
||||
"NO_KEYMAPS_CONFIGURED": "Inga tangentbordslayouts konfigurerade",
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "Kolla wayvr.org för att se hur du ställer in dina tangentbordslayouter!",
|
||||
"NO_SET_SELECTED": "Inget Set är valt.",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "Placera en kontroller på golvet!"
|
||||
},
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "Lägg till ett nytt set",
|
||||
"CLEANUP_MIRRORS": "Ta bort speglingar som\ninte är synliga just nu",
|
||||
"DASHBOARD": "Kontrollpanel",
|
||||
"EDIT_MODE": "Redigeringsläge",
|
||||
"FIX_FLOOR": "Justera golvnivå",
|
||||
"LONG_PRESS_TO_DELETE_SET": "Håll för att ta bort",
|
||||
"RECENTER": "Centrera playspace",
|
||||
"SWITCH_TO_SET": "Byt till set",
|
||||
"TOGGLE_FOR_CURRENT_SET": "Växla för nuvarande set"
|
||||
},
|
||||
"WHISPER": {
|
||||
"TRANSCRIBE": "Håll ned för att transkribera text",
|
||||
"PASTE": "Kopiera transkribering till urklipp",
|
||||
"PASTE_AND_GO": "Kopiera transkribering och skicka Ctrl-V till fokuserad app",
|
||||
"SEND_OSC": "Skicka transkription till OSC-chatt",
|
||||
"UNLOAD_AND_CLOSE": "Avlasta modell och stäng",
|
||||
"MODEL_NOT_DOWNLOADED": "Tal-till-text är inte konfigurerat!",
|
||||
"DOWNLOAD_GUIDANCE": "Konfigurera tal-till-text under inställningar, funktioner!",
|
||||
"INITIAL_TEXT": "Håll inne mikroknappen för att börja transkribera...",
|
||||
"INIT_ERROR": "Fel vid start av Whisper-motorn",
|
||||
"ENTER_KEY": "Enter-tangent"
|
||||
},
|
||||
"NOTIFICATION": {
|
||||
"COUD_NOT_CONNECT_TO_VR_RUNTIME": "Kunde inte ansluta till VR-runtime.",
|
||||
"NOT_RUNNING_XR_VR": "Se till att Monado, WiVRn eller SteamVR är igång.",
|
||||
"NOT_RUNNING_VR": "Se till att Monado, WiVRn eller SteamVR är igång.",
|
||||
"NOT_RUNNING_XR": "Se till att Monado eller WiVRn är igång.",
|
||||
"INVALID_ARGS": "Kontrollera dina startargument.",
|
||||
"SELECT_SCREEN_CAST_FOR": "Välj skärmdelning för display {DISPLAY}",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "Kunde inte initiera fångst av tangentbord/mus!\nKontrollera att din användare tillhör gruppen 'input'.",
|
||||
"WE_ARE_GRABBING": "WayVR styr nu ditt tangentbord och din mus!\nTryck Super+Z för att släppa."
|
||||
}
|
||||
}
|
||||
@@ -143,11 +143,7 @@
|
||||
"NO_SET_SELECTED": "未选择套装。",
|
||||
"ONE_CONTROLLER_ON_FLOOR": "将一只控制器放在地面上!",
|
||||
"NO_KEYMAPS_CONFIGURED": "尚未配置键盘布局",
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "请访问 wayvr.org 了解如何设置键盘布局!",
|
||||
"STEAMVR_DASH_TITLE": "SteamVR 控制面板已开启",
|
||||
"STEAMVR_DASH_TEXT": "关闭 SteamVR 仪表板以开始使用 WayVR。",
|
||||
"WAYVR_RUNNING_TITLE": "WayVR 正在运行!",
|
||||
"WAYVR_RUNNING_TEXT": "查看手腕上的手表,或使用显示/隐藏快捷键开始!"
|
||||
"NO_KEYMAPS_CONFIGURED_HELP": "请访问 wayvr.org 了解如何设置键盘布局!"
|
||||
},
|
||||
"WATCH": {
|
||||
"ADD_NEW_SET": "添加新套装",
|
||||
@@ -181,5 +177,7 @@
|
||||
"SELECT_SCREEN_CAST_FOR": "为显示器 {DISPLAY} 选择屏幕投射",
|
||||
"CANNOT_CAPTURE_KBD_MOUSE": "无法初始化键盘/鼠标捕获!\n请检查您的用户是否在 'input' 组中。",
|
||||
"WE_ARE_GRABBING": "WayVR 正在接管你的键盘和鼠标!\n按 Super+Z 释放。"
|
||||
}
|
||||
},
|
||||
"STEAMVR_DASH_TOAST_TITLE": "SteamVR 控制面板已开启",
|
||||
"STEAMVR_DASH_TOAST_TEXT": "关闭 SteamVR 仪表板以开始使用 WayVR。"
|
||||
}
|
||||
@@ -138,10 +138,6 @@ impl OpenVrInputSource {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_bindings(&mut self, input: &mut InputManager) {
|
||||
let _ = input.open_binding_ui(None, None, self.hands[0].input_hnd, false);
|
||||
}
|
||||
|
||||
pub fn haptics(&mut self, input: &mut InputManager, hand: usize, haptics: &Haptics) {
|
||||
let action_handle = self.hands[hand].haptics_hnd;
|
||||
let _ = input.trigger_haptic_vibration_action(
|
||||
|
||||
@@ -214,9 +214,9 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
let id = Toast::new(
|
||||
ToastTopic::Priority,
|
||||
Some(Translation::from_translation_key(
|
||||
"TOAST.STEAMVR_DASH_TITLE",
|
||||
"STEAMVR_DASH_TOAST_TITLE",
|
||||
)),
|
||||
Translation::from_translation_key("TOAST.STEAMVR_DASH_TEXT"),
|
||||
Translation::from_translation_key("STEAMVR_DASH_TOAST_TEXT"),
|
||||
)
|
||||
.with_timeout(300.0) // we'll zero this if dash gets hidden
|
||||
.submit(&mut app);
|
||||
@@ -227,16 +227,6 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
dashboard_hidden = true;
|
||||
if let Some(id) = dash_toast_id {
|
||||
Toast::expire_now(&mut app, id);
|
||||
} else if !args.show {
|
||||
Toast::new(
|
||||
ToastTopic::Priority,
|
||||
Some(Translation::from_translation_key(
|
||||
"TOAST.WAYVR_RUNNING_TITLE",
|
||||
)),
|
||||
Translation::from_translation_key("TOAST.WAYVR_RUNNING_TEXT"),
|
||||
)
|
||||
.with_timeout(5.0)
|
||||
.submit(&mut app);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,13 +260,6 @@ pub fn openvr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
OpenVrTask::ColorGain(channel, value) => {
|
||||
let _ = adjust_gain(&mut settings_mgr, channel, value);
|
||||
}
|
||||
OpenVrTask::ShowBindingsUI => {
|
||||
// hide current set because overlay sort order is broken
|
||||
// and will cover the bindings UI while not taking input
|
||||
overlays.switch_to_set(&mut app, None, false);
|
||||
|
||||
input_source.open_bindings(&mut input_mgr);
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "openxr")]
|
||||
TaskType::OpenXR(_) => {}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use core::f32;
|
||||
use std::sync::Arc;
|
||||
|
||||
use glam::Vec4;
|
||||
|
||||
@@ -339,6 +339,15 @@ impl PlayspaceMover {
|
||||
);
|
||||
self.universe = new_universe;
|
||||
}
|
||||
|
||||
if self.drag.is_some() {
|
||||
log::info!("Space drag interrupted by external change");
|
||||
self.drag = None;
|
||||
}
|
||||
if self.rotate.is_some() {
|
||||
log::info!("Space rotate interrupted by external change");
|
||||
self.rotate = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_universe(&self) -> ETrackingUniverseOrigin {
|
||||
|
||||
@@ -179,18 +179,6 @@ pub fn openxr_run(args: &Args, params: RunParams) -> Result<(), BackendError> {
|
||||
// declared last so rust drops this before all long-lived XR objects
|
||||
let _gpu_idle_before_xr_drop = OpenXrGpuIdleGuard(app.gfx.clone());
|
||||
|
||||
if !args.show {
|
||||
Toast::new(
|
||||
ToastTopic::Priority,
|
||||
Some(Translation::from_translation_key(
|
||||
"TOAST.WAYVR_RUNNING_TITLE",
|
||||
)),
|
||||
Translation::from_translation_key("TOAST.WAYVR_RUNNING_TEXT"),
|
||||
)
|
||||
.with_timeout(5.0)
|
||||
.submit(&mut app);
|
||||
}
|
||||
|
||||
'main_loop: loop {
|
||||
let now = Instant::now();
|
||||
app.delta_time = (now.duration_since(last_frame_time).as_secs_f32()).clamp(0.001, 0.2); // 5 - 1000 fps
|
||||
|
||||
@@ -21,7 +21,7 @@ struct MoverData<T> {
|
||||
|
||||
pub(super) struct PlayspaceMover {
|
||||
drag: Option<MoverData<Vec3A>>,
|
||||
rotate: Option<MoverData<Affine3A>>,
|
||||
rotate: Option<MoverData<Quat>>,
|
||||
gravity: SpaceGravity,
|
||||
playspace_state: PlayspaceState,
|
||||
}
|
||||
@@ -93,37 +93,32 @@ impl PlayspaceMover {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_hand = data.pose * app.input_state.pointers[data.hand].raw_pose;
|
||||
let before_pose = data.pose;
|
||||
let pose_quat = Quat::from_affine3(&data.pose).normalize();
|
||||
let new_hand = (pose_quat
|
||||
* Quat::from_affine3(&app.input_state.pointers[data.hand].raw_pose))
|
||||
.normalize();
|
||||
|
||||
if app.session.config.space_rotate_unlocked {
|
||||
// pivot on the hand so the body orbits it
|
||||
let pose = new_hand * data.hand_pose.inverse() * data.pose;
|
||||
data.pose = Affine3A::from_rotation_translation(
|
||||
Quat::from_affine3(&pose).normalize(),
|
||||
pose.translation.into(),
|
||||
);
|
||||
let dq = (pose_quat.conjugate() * (new_hand * data.hand_pose.conjugate()) * pose_quat)
|
||||
.normalize();
|
||||
let mut space_transform = if app.session.config.space_rotate_unlocked {
|
||||
Affine3A::from_quat(dq)
|
||||
} else {
|
||||
let pose_quat = Quat::from_affine3(&data.pose).normalize();
|
||||
let dq = (pose_quat.conjugate()
|
||||
* Quat::from_affine3(&(new_hand * data.hand_pose.inverse()))
|
||||
* pose_quat)
|
||||
.normalize();
|
||||
let rel_y = f32::atan2(
|
||||
2.0 * dq.y.mul_add(dq.w, dq.x * dq.z),
|
||||
2.0f32.mul_add(dq.w.mul_add(dq.w, dq.x * dq.x), -1.0),
|
||||
);
|
||||
|
||||
let mut space_transform = Affine3A::from_rotation_y(rel_y);
|
||||
space_transform.translation = (space_transform
|
||||
.transform_vector3a(app.input_state.hmd.translation)
|
||||
- app.input_state.hmd.translation)
|
||||
* -1.0;
|
||||
Affine3A::from_rotation_y(rel_y)
|
||||
};
|
||||
let offset = (space_transform.transform_vector3a(app.input_state.hmd.translation)
|
||||
- app.input_state.hmd.translation)
|
||||
* -1.0;
|
||||
|
||||
data.pose *= space_transform;
|
||||
data.pose.matrix3 = Mat3A::from_quat(Quat::from_affine3(&data.pose).normalize());
|
||||
}
|
||||
space_transform.translation = offset;
|
||||
|
||||
let before_pose = data.pose;
|
||||
data.pose *= space_transform;
|
||||
data.pose.matrix3 = Mat3A::from_quat(Quat::from_affine3(&data.pose).normalize());
|
||||
if !app.session.config.space_drag_affects_world {
|
||||
playspace_common::shift_world(overlays, &mut app.anchor, &before_pose, &data.pose);
|
||||
}
|
||||
@@ -131,8 +126,6 @@ impl PlayspaceMover {
|
||||
|
||||
apply_offset(data.pose, &mut monado.ipc);
|
||||
self.rotate = Some(data);
|
||||
// drag and rotate can share a button
|
||||
return;
|
||||
} else {
|
||||
// start space rotate
|
||||
for (i, pointer) in app.input_state.pointers.iter().enumerate() {
|
||||
@@ -151,7 +144,7 @@ impl PlayspaceMover {
|
||||
return;
|
||||
};
|
||||
|
||||
let hand_pose = pose * pointer.raw_pose;
|
||||
let hand_pose = Quat::from_affine3(&(pose * pointer.raw_pose)).normalize();
|
||||
self.rotate = Some(MoverData {
|
||||
pose,
|
||||
hand: i,
|
||||
|
||||
@@ -56,7 +56,6 @@ pub enum InputTask {
|
||||
#[allow(dead_code)]
|
||||
pub enum OpenVrTask {
|
||||
ColorGain(ColorChannel, f32),
|
||||
ShowBindingsUI,
|
||||
}
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
|
||||
@@ -114,8 +114,6 @@ impl ImageImporter {
|
||||
num_planes: dmabuf.num_planes(),
|
||||
planes: Default::default(),
|
||||
mouse: None,
|
||||
acquire_sync: Default::default(),
|
||||
lease: None,
|
||||
};
|
||||
|
||||
for (i, handle) in dmabuf.handles().enumerate() {
|
||||
@@ -131,7 +129,7 @@ impl ImageImporter {
|
||||
frame.planes[i].stride = stride as _;
|
||||
}
|
||||
|
||||
let image = self.gfx.dmabuf_texture(&frame)?;
|
||||
let image = self.gfx.dmabuf_texture(frame)?;
|
||||
log::info!("DMA-buf import: creating image view");
|
||||
let image_view = self.gfx.create_image_view(image)?;
|
||||
log::info!("DMA-buf import: image view created");
|
||||
|
||||
@@ -189,7 +189,6 @@ pub struct AutoSettings {
|
||||
pub input_emulation_method: InputEmulationMethod,
|
||||
pub tutorial_graduated: bool,
|
||||
pub whisper_model: Arc<str>,
|
||||
pub whisper_gpu: u32,
|
||||
pub default_overlay_scale: f32,
|
||||
pub color_palette: Arc<str>,
|
||||
pub snap_angle_deg: f32,
|
||||
@@ -266,7 +265,6 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
|
||||
input_emulation_method: config.input_emulation_method,
|
||||
tutorial_graduated: config.tutorial_graduated,
|
||||
whisper_model: config.whisper_model.clone(),
|
||||
whisper_gpu: config.whisper_gpu,
|
||||
default_overlay_scale: config.default_overlay_scale,
|
||||
color_palette: config.color_palette.clone(),
|
||||
snap_angle_deg: config.snap_angle_deg,
|
||||
|
||||
@@ -4,11 +4,11 @@ use wgui::gfx::{DmabufImportInfo, DmabufPlaneLayout, Format, Image, ImageView, W
|
||||
use wlx_capture::{DrmFormat, DrmFourcc, DrmModifier, frame::DmabufFrame};
|
||||
|
||||
pub trait WGfxDmabuf {
|
||||
fn dmabuf_texture(&self, frame: &DmabufFrame) -> anyhow::Result<Arc<Image>>;
|
||||
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>>;
|
||||
}
|
||||
|
||||
impl WGfxDmabuf for WGfx {
|
||||
fn dmabuf_texture(&self, frame: &DmabufFrame) -> anyhow::Result<Arc<Image>> {
|
||||
fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result<Arc<Image>> {
|
||||
let format = fourcc_to_vk(frame.format.drm_format.code)?;
|
||||
let modifier = (!matches!(frame.format.drm_format.modifier, DrmModifier::Invalid))
|
||||
.then(|| u64::from(frame.format.drm_format.modifier));
|
||||
|
||||
@@ -8,8 +8,6 @@ use wgui::gfx::{
|
||||
BufferUsage, BuiltCommandBuffer, Format, ShaderModule, Vert2Buf, Vert2Uv, WGfx, WGfxCreateInfo,
|
||||
};
|
||||
use wlx_capture::DrmFormat;
|
||||
#[cfg(feature = "pipewire")]
|
||||
use wlx_capture::syncobj::DrmRenderNode;
|
||||
|
||||
use crate::shaders::{
|
||||
frag_color, frag_grid, frag_screen, frag_simple, frag_sky, frag_srgb, frag_wayland, vert_quad,
|
||||
@@ -22,8 +20,6 @@ pub struct WGfxExtras {
|
||||
pub drm_formats: Arc<[DrmFormat]>,
|
||||
pub quad_verts: Vert2Buf,
|
||||
pub drm_device: Option<(i64, i64)>,
|
||||
#[cfg(feature = "pipewire")]
|
||||
pub drm_render_node: Option<DrmRenderNode>,
|
||||
}
|
||||
|
||||
impl WGfxExtras {
|
||||
@@ -59,32 +55,19 @@ impl WGfxExtras {
|
||||
];
|
||||
let quad_verts = gfx.new_buffer(BufferUsage::VERTEX_BUFFER, &vertices)?;
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
let drm_device = gfx.device_info().drm_render_node.map(|(major, minor)| {
|
||||
log::info!("DRM render device: {major} {minor}");
|
||||
(i64::from(major), i64::from(minor))
|
||||
});
|
||||
#[cfg(feature = "pipewire")]
|
||||
if drm_device.is_none() {
|
||||
log::warn!("No DRM device.");
|
||||
}
|
||||
#[cfg(feature = "pipewire")]
|
||||
let drm_render_node = drm_device.and_then(|(maj, min)| {
|
||||
use wgui::log::LogErr;
|
||||
use wlx_capture::syncobj::open_render_node;
|
||||
|
||||
open_render_node(maj as u32, min as u32)
|
||||
.log_err("Could not open DRM render node")
|
||||
.ok()
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
shaders,
|
||||
drm_formats,
|
||||
quad_verts,
|
||||
drm_device,
|
||||
#[cfg(feature = "pipewire")]
|
||||
drm_render_node,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+19
-22
@@ -6,7 +6,6 @@ use crate::{
|
||||
backend::input::InputState,
|
||||
ipc::{event_queue::SyncEventQueue, signal::WayVRSignal},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use bytes::BufMut;
|
||||
use glam::Vec3A;
|
||||
use interprocess::local_socket::{self, ToNsName, traits::Listener};
|
||||
@@ -96,6 +95,10 @@ pub struct TickParams<'a> {
|
||||
pub hid_wrapper: &'a mut HidWrapper,
|
||||
}
|
||||
|
||||
pub fn gen_args_vec(input: &str) -> Vec<&str> {
|
||||
input.split_whitespace().collect()
|
||||
}
|
||||
|
||||
pub fn gen_env_vec(input: &[String]) -> Vec<(&str, &str)> {
|
||||
input
|
||||
.iter()
|
||||
@@ -223,28 +226,22 @@ impl Connection {
|
||||
serial: ipc::Serial,
|
||||
packet_params: packet_client::WvrProcessLaunchParams,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = {
|
||||
let mut argv_iter = packet_params.argv.iter().map(String::as_str);
|
||||
let exec_path = argv_iter
|
||||
.next()
|
||||
.context("No executable for process_launch")?;
|
||||
let exec_args = argv_iter.collect::<Box<_>>();
|
||||
let env_vec = gen_env_vec(&packet_params.env);
|
||||
let args_vec = gen_args_vec(&packet_params.args);
|
||||
let env_vec = gen_env_vec(&packet_params.env);
|
||||
|
||||
params.wvr_server.spawn_process(
|
||||
&packet_params.name,
|
||||
exec_path,
|
||||
&exec_args,
|
||||
&env_vec,
|
||||
packet_params.resolution,
|
||||
packet_params.pos_mode,
|
||||
None,
|
||||
packet_params.icon.as_deref(),
|
||||
packet_params.userdata,
|
||||
)
|
||||
}
|
||||
.map(ProcessHandle::as_packet)
|
||||
.map_err(|e| e.to_string());
|
||||
let res = params.wvr_server.spawn_process(
|
||||
&packet_params.name,
|
||||
&packet_params.exec,
|
||||
&args_vec,
|
||||
&env_vec,
|
||||
packet_params.resolution,
|
||||
packet_params.pos_mode,
|
||||
None,
|
||||
packet_params.icon.as_deref(),
|
||||
packet_params.userdata,
|
||||
);
|
||||
|
||||
let res = res.map(ProcessHandle::as_packet).map_err(|e| e.to_string());
|
||||
|
||||
send_packet(
|
||||
&mut self.conn,
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use dash_frontend::frontend::{self, FrontendTask, FrontendUpdateParams};
|
||||
use glam::{Affine2, Affine3A, Quat, Vec2, vec2, vec3};
|
||||
use wayvr_ipc::{
|
||||
@@ -21,7 +20,7 @@ use wgui::{
|
||||
widget::EventResult,
|
||||
};
|
||||
use wlx_common::{
|
||||
dash_interface::{self, ConfigChangeKind, DashBackendTask, DashInterface, DashPlayspaceTask},
|
||||
dash_interface::{self, ConfigChangeKind, DashInterface, DashPlayspaceTask},
|
||||
locale::WayVRLangProvider,
|
||||
overlays::{BackendAttrib, BackendAttribValue},
|
||||
};
|
||||
@@ -44,7 +43,7 @@ use crate::{
|
||||
},
|
||||
},
|
||||
config::{none_if_0, save_settings},
|
||||
ipc::ipc_server::gen_env_vec,
|
||||
ipc::ipc_server::{gen_args_vec, gen_env_vec},
|
||||
state::AppState,
|
||||
subsystem::hid::WheelDelta,
|
||||
windowing::{
|
||||
@@ -392,10 +391,6 @@ impl DashInterfaceLive {
|
||||
}
|
||||
|
||||
impl DashInterface<AppState> for DashInterfaceLive {
|
||||
fn enumerate_vulkan_devices(&mut self, data: &mut AppState) -> anyhow::Result<Vec<String>> {
|
||||
data.gfx.enumerate_devices()
|
||||
}
|
||||
|
||||
fn window_list(&mut self, app: &mut AppState) -> anyhow::Result<Vec<WvrWindow>> {
|
||||
let wvr_server = app.wvr_server.as_mut().unwrap();
|
||||
Ok(wvr_server
|
||||
@@ -441,11 +436,7 @@ impl DashInterface<AppState> for DashInterfaceLive {
|
||||
) -> anyhow::Result<WvrProcessHandle> {
|
||||
let wvr_server = app.wvr_server.as_mut().unwrap();
|
||||
|
||||
let mut argv_iter = params.argv.iter().map(String::as_str);
|
||||
let exec_path = argv_iter
|
||||
.next()
|
||||
.context("No executable for process_launch")?;
|
||||
let exec_args = argv_iter.collect::<Box<_>>();
|
||||
let args_vec = gen_args_vec(¶ms.args);
|
||||
let env_vec = gen_env_vec(¶ms.env);
|
||||
|
||||
if auto_start {
|
||||
@@ -456,8 +447,8 @@ impl DashInterface<AppState> for DashInterfaceLive {
|
||||
wvr_server
|
||||
.spawn_process(
|
||||
¶ms.name,
|
||||
exec_path,
|
||||
&exec_args,
|
||||
¶ms.exec,
|
||||
&args_vec,
|
||||
&env_vec,
|
||||
params.resolution,
|
||||
params.pos_mode,
|
||||
@@ -526,20 +517,6 @@ impl DashInterface<AppState> for DashInterfaceLive {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
fn backend_task(&mut self, app: &mut AppState, mode: DashBackendTask) -> anyhow::Result<()> {
|
||||
#[cfg(feature = "openvr")]
|
||||
if app.feats.xr_backend.is_open_vr() {
|
||||
use crate::backend::task::OpenVrTask;
|
||||
let task = match mode {
|
||||
DashBackendTask::ShowBindingsUI => OpenVrTask::ShowBindingsUI,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
app.tasks.enqueue(TaskType::OpenVR(task));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn desktop_finder<'a>(
|
||||
&'a mut self,
|
||||
data: &'a mut AppState,
|
||||
|
||||
@@ -48,12 +48,10 @@ pub(super) enum CaptureType {
|
||||
|
||||
pub struct ScreenBackend {
|
||||
name: Arc<str>,
|
||||
// frames mut come before `capture` to maintain drop order
|
||||
cur_frame: Option<WlxCaptureOut>,
|
||||
pending_frame: Option<WlxCaptureOut>,
|
||||
capture_type: CaptureType,
|
||||
capture: Box<dyn WlxCapture<WlxCaptureIn, WlxCaptureOut>>,
|
||||
pipeline: Option<ScreenPipeline>,
|
||||
cur_frame: Option<WlxCaptureOut>,
|
||||
meta: Option<FrameMeta>,
|
||||
mouse_transform: Affine2,
|
||||
interaction_transform: Option<Affine2>,
|
||||
@@ -79,7 +77,6 @@ impl ScreenBackend {
|
||||
capture,
|
||||
pipeline: None,
|
||||
cur_frame: None,
|
||||
pending_frame: None,
|
||||
meta: None,
|
||||
mouse_transform: Affine2::ZERO,
|
||||
interaction_transform: None,
|
||||
@@ -207,26 +204,7 @@ impl OverlayBackend for ScreenBackend {
|
||||
return Ok(ShouldRender::Unable);
|
||||
}
|
||||
|
||||
// don't submit the new image until the producer signaled
|
||||
if let Some(mut frame) = self.capture.receive()
|
||||
&& (self.pending_frame.is_none() || matches!(frame.is_ready(), Ok(true)))
|
||||
{
|
||||
self.pending_frame = Some(frame);
|
||||
}
|
||||
let pending_ready = match self.pending_frame.as_mut() {
|
||||
Some(frame) => match frame.is_ready() {
|
||||
Ok(ready) => ready,
|
||||
Err(e) => {
|
||||
log::warn!("{}: failed to check DMA-buf acquire fence: {e}", self.name);
|
||||
self.pending_frame = None;
|
||||
false
|
||||
}
|
||||
},
|
||||
None => false,
|
||||
};
|
||||
|
||||
if pending_ready {
|
||||
let frame = self.pending_frame.take().unwrap();
|
||||
if let Some(frame) = self.capture.receive() {
|
||||
let stereo = self.stereo.unwrap_or(StereoMode::None);
|
||||
let mut meta = frame.get_frame_meta(&app.session.config, stereo);
|
||||
|
||||
|
||||
@@ -31,9 +31,6 @@ use crate::{
|
||||
|
||||
const CURSOR_SIZE: f32 = 16. / 1440.;
|
||||
|
||||
// synchronize queue shared by the WlxCapture worker threads
|
||||
static CAPTURE_QUEUE_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct BufPass {
|
||||
pass: WGfxPass<Vert2Uv>,
|
||||
buf_vert: Arc<Buffer<Vert2Uv>>,
|
||||
@@ -558,15 +555,9 @@ pub(super) struct WlxCaptureOut {
|
||||
pub(super) image: Arc<ImageView>,
|
||||
pub(super) format: FrameFormat,
|
||||
pub(super) mouse: Option<MouseMeta>,
|
||||
pub(super) acquire_sync: wlx_frame::FrameAcquireSync,
|
||||
pub(super) _lease: Option<wlx_frame::FrameLease>,
|
||||
}
|
||||
|
||||
impl WlxCaptureOut {
|
||||
pub(super) fn is_ready(&mut self) -> std::io::Result<bool> {
|
||||
self.acquire_sync.check_ready()
|
||||
}
|
||||
|
||||
pub(super) fn get_frame_meta(&self, config: &GeneralConfig, stereo: StereoMode) -> FrameMeta {
|
||||
FrameMeta {
|
||||
clear: WGfxClearMode::DontCare,
|
||||
@@ -673,17 +664,6 @@ fn upload_image(
|
||||
height: u32,
|
||||
format: Format,
|
||||
data: &[u8],
|
||||
) -> Option<Arc<Image>> {
|
||||
upload_image_with_row_length(me, width, height, format, data, 0)
|
||||
}
|
||||
|
||||
fn upload_image_with_row_length(
|
||||
me: &WlxCaptureIn,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: Format,
|
||||
data: &[u8],
|
||||
buffer_row_length: u32,
|
||||
) -> Option<Arc<Image>> {
|
||||
let cmd_result = if me.use_capture_queue {
|
||||
me.gfx
|
||||
@@ -699,23 +679,15 @@ fn upload_image_with_row_length(
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let image =
|
||||
match cmd_xfer.upload_image_with_row_length(width, height, format, data, buffer_row_length)
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
log::error!("{}: Could not create vkImage: {:?}", me.name, e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let result = if me.use_capture_queue {
|
||||
let _guard = CAPTURE_QUEUE_MUTEX.lock().unwrap(); // mutex poison only
|
||||
cmd_xfer.build_and_execute_now()
|
||||
} else {
|
||||
cmd_xfer.build_and_execute_now()
|
||||
let image = match cmd_xfer.upload_image(width, height, format, data) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
log::error!("{}: Could not create vkImage: {:?}", me.name, e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if let Err(e) = result {
|
||||
|
||||
if let Err(e) = cmd_xfer.build_and_execute_now() {
|
||||
log::error!("{}: Could not execute upload: {:?}", me.name, e);
|
||||
return None;
|
||||
}
|
||||
@@ -725,23 +697,18 @@ fn upload_image_with_row_length(
|
||||
|
||||
pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<WlxCaptureOut> {
|
||||
match frame {
|
||||
WlxFrame::Dmabuf(mut frame) => {
|
||||
WlxFrame::Dmabuf(frame) => {
|
||||
if !frame.is_valid() {
|
||||
log::error!("{}: Invalid frame", me.name);
|
||||
return None;
|
||||
}
|
||||
log::trace!("{}: New DMA-buf frame", me.name);
|
||||
let format = frame.format;
|
||||
let lease = frame.lease.clone();
|
||||
let acquire_sync = std::mem::take(&mut frame.acquire_sync);
|
||||
|
||||
match me.gfx.dmabuf_texture(&frame) {
|
||||
match me.gfx.dmabuf_texture(frame) {
|
||||
Ok(image) => Some(WlxCaptureOut {
|
||||
image: me.gfx.create_image_view(image).ok()?,
|
||||
format,
|
||||
mouse: None,
|
||||
acquire_sync,
|
||||
_lease: lease,
|
||||
}),
|
||||
Err(e) => {
|
||||
log::error!("{}: Failed to create DMA-buf vkImage: {}", me.name, e);
|
||||
@@ -777,8 +744,6 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
|
||||
image: me.gfx.create_image_view(image).ok()?,
|
||||
format: frame.format,
|
||||
mouse: None,
|
||||
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
|
||||
_lease: None,
|
||||
})
|
||||
}
|
||||
WlxFrame::MemPtr(frame) => {
|
||||
@@ -792,29 +757,13 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
|
||||
}
|
||||
};
|
||||
|
||||
// SAFETY: the MemPtr params & allocation and must be valid and must outlive the callback
|
||||
let pixels = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
(frame.ptr as *const u8).add(frame.offset),
|
||||
frame.size - frame.offset,
|
||||
)
|
||||
};
|
||||
let buffer_row_length = (frame.stride / 4) as u32;
|
||||
let image = upload_image_with_row_length(
|
||||
me,
|
||||
frame.format.width,
|
||||
frame.format.height,
|
||||
format,
|
||||
pixels,
|
||||
buffer_row_length,
|
||||
)?;
|
||||
let data = unsafe { std::slice::from_raw_parts(frame.ptr as *const u8, frame.size) };
|
||||
let image = upload_image(me, frame.format.width, frame.format.height, format, data)?;
|
||||
|
||||
Some(WlxCaptureOut {
|
||||
image: me.gfx.create_image_view(image).ok()?,
|
||||
format: frame.format,
|
||||
mouse: frame.mouse,
|
||||
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
|
||||
_lease: None,
|
||||
})
|
||||
}
|
||||
WlxFrame::Implicit(transform) => {
|
||||
@@ -838,8 +787,6 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
|
||||
image,
|
||||
format,
|
||||
mouse: None,
|
||||
acquire_sync: wlx_frame::FrameAcquireSync::Ready,
|
||||
_lease: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -864,8 +811,6 @@ pub(super) enum MainThreadFrame {
|
||||
Cpu {
|
||||
format: FrameFormat,
|
||||
data: Vec<u8>,
|
||||
offset: usize,
|
||||
stride: usize,
|
||||
mouse: Option<MouseMeta>,
|
||||
},
|
||||
Dmabuf {
|
||||
@@ -925,16 +870,12 @@ where
|
||||
MainThreadFrame::Cpu {
|
||||
format,
|
||||
data,
|
||||
offset,
|
||||
stride,
|
||||
mouse,
|
||||
} => {
|
||||
let frame = wlx_frame::MemPtrFrame {
|
||||
format,
|
||||
ptr: data.as_ptr() as usize,
|
||||
size: data.len(),
|
||||
offset,
|
||||
stride,
|
||||
mouse,
|
||||
};
|
||||
receive_callback(
|
||||
@@ -969,24 +910,22 @@ fn receive_callback_dummy(_: &DmaExporterProxy, frame: WlxFrame) -> Option<MainT
|
||||
Some(MainThreadFrame::Cpu {
|
||||
format: frame.format,
|
||||
data: map.as_slice().to_vec(),
|
||||
offset: frame.plane.offset as _,
|
||||
stride: (frame.format.width as usize).checked_mul(4)?,
|
||||
mouse: frame.mouse,
|
||||
})
|
||||
}
|
||||
WlxFrame::MemPtr(frame) => {
|
||||
// SAFETY: Allocation must be valid & must outlive the callback
|
||||
let pixels = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
(frame.ptr as *const u8).add(frame.offset),
|
||||
frame.size - frame.offset,
|
||||
)
|
||||
let data = if frame.size == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
if frame.ptr == 0 {
|
||||
log::error!("Main-thread CPU capture received a null MemPtr frame");
|
||||
return None;
|
||||
}
|
||||
unsafe { std::slice::from_raw_parts(frame.ptr as *const u8, frame.size) }.to_vec()
|
||||
};
|
||||
Some(MainThreadFrame::Cpu {
|
||||
format: frame.format,
|
||||
data: pixels.to_vec(),
|
||||
offset: frame.offset,
|
||||
stride: frame.stride,
|
||||
data,
|
||||
mouse: frame.mouse,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -344,14 +344,8 @@ fn check(mut par: CheckParams, finalize_fn: ScreenCastFinalizeFn) {
|
||||
par.app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
||||
OverlaySelector::Name(par.name.clone()),
|
||||
Box::new(move |app, owc| {
|
||||
let capture = new_wlx_capture!(
|
||||
app,
|
||||
PipewireCapture::new(
|
||||
par.name.clone(),
|
||||
node_id,
|
||||
app.gfx_extras.drm_render_node.clone()
|
||||
)
|
||||
);
|
||||
let capture =
|
||||
new_wlx_capture!(app, PipewireCapture::new(par.name.clone(), node_id));
|
||||
|
||||
owc.backend =
|
||||
finalize_fn(par.name, par.logical_pos, par.logical_size, capture, app);
|
||||
|
||||
@@ -107,8 +107,6 @@ fn start_progress_state(
|
||||
let mut st = st.borrow_mut();
|
||||
let mut update_text = false;
|
||||
|
||||
common.alterables.mark_redraw();
|
||||
|
||||
while let Ok(msg) = progress_rx.try_recv() {
|
||||
match msg {
|
||||
PttProgress::VuVolume(volume) => {
|
||||
@@ -232,11 +230,8 @@ pub fn create_whisper(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig>
|
||||
let model_path = data_dir::get_path("whisper")
|
||||
.join(app.session.config.whisper_model.as_ref());
|
||||
if model_path.is_file() {
|
||||
app.whisper_sst = match WhisperStt::new(
|
||||
model_path,
|
||||
app.session.config.whisper_gpu,
|
||||
)
|
||||
.log_err("Error while starting Whisper engine")
|
||||
app.whisper_sst = match WhisperStt::new(model_path)
|
||||
.log_err("Error while starting Whisper engine")
|
||||
{
|
||||
Ok(x) => Some(x),
|
||||
Err(e) => {
|
||||
|
||||
+2
-2
@@ -172,14 +172,14 @@ impl AppState {
|
||||
let mut theme = WguiTheme {
|
||||
animation_mult: 1. / session.config.ui_animation_speed,
|
||||
rounding_mult: session.config.ui_round_multiplier,
|
||||
gradient_intensity: session.config.ui_gradient_intensity,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let dbus = DbusConnector::default();
|
||||
|
||||
let ipc_server = ipc_server::WayVRServer::new()?;
|
||||
|
||||
let mut desktop_finder = DesktopFinder::new(params.wgui_globals.i18n().get_locale());
|
||||
let mut desktop_finder = DesktopFinder::new();
|
||||
desktop_finder.refresh();
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
|
||||
@@ -116,10 +116,8 @@ pub enum PttProgress {
|
||||
}
|
||||
|
||||
impl WhisperStt {
|
||||
pub fn new(model_path: impl AsRef<Path>, gpu_idx: u32) -> Result<Self, WhisperSttError> {
|
||||
let mut conf = WhisperSttConfig::new(model_path);
|
||||
conf.gpu_device = gpu_idx as i32;
|
||||
Self::init(conf)
|
||||
pub fn new(model_path: impl AsRef<Path>) -> Result<Self, WhisperSttError> {
|
||||
Self::init(WhisperSttConfig::new(model_path))
|
||||
}
|
||||
|
||||
pub fn init(config: WhisperSttConfig) -> Result<Self, WhisperSttError> {
|
||||
|
||||
@@ -320,12 +320,8 @@ impl OverlayWindowConfig {
|
||||
return;
|
||||
}
|
||||
|
||||
let to_hmd =
|
||||
(state.transform.translation - app.input_state.hmd.translation).normalize_or_zero();
|
||||
let watch_normal = state
|
||||
.transform
|
||||
.transform_vector3a(Vec3A::NEG_Z)
|
||||
.normalize_or_zero();
|
||||
let to_hmd = (state.transform.translation - app.input_state.hmd.translation).normalize();
|
||||
let watch_normal = state.transform.transform_vector3a(Vec3A::NEG_Z).normalize();
|
||||
let dot = to_hmd.dot(watch_normal);
|
||||
|
||||
let range = (app.session.config.watch_view_angle_max
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=WayVR (Headless)
|
||||
Comment=Your way to enjoy VR on Linux
|
||||
Exec=wayvr --headless
|
||||
Icon=wayvr
|
||||
Terminal=false
|
||||
Categories=Utility;X-WiVRn-VR;
|
||||
X-XR-Plugin=true
|
||||
X-XR-Plugin-Exec=wayvr --openxr --headless
|
||||
+1
-1
@@ -15,7 +15,7 @@ clap.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
shell-words.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
env_logger = "0.11.11"
|
||||
shell-words = "1.1.1"
|
||||
@@ -107,12 +107,13 @@ pub async fn wvr_process_terminate(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn wvr_process_launch(
|
||||
state: &mut WayVRClientState,
|
||||
argv: Vec<String>,
|
||||
exec: String,
|
||||
name: String,
|
||||
env: Vec<String>,
|
||||
resolution: [u32; 2],
|
||||
pos_mode: PositionMode,
|
||||
icon: Option<String>,
|
||||
args: String,
|
||||
userdata: HashMap<String, String>,
|
||||
) {
|
||||
handle_result(
|
||||
@@ -122,8 +123,9 @@ pub async fn wvr_process_launch(
|
||||
state.serial_generator.increment_get(),
|
||||
packet_client::WvrProcessLaunchParams {
|
||||
env,
|
||||
argv,
|
||||
exec,
|
||||
name,
|
||||
args,
|
||||
resolution,
|
||||
pos_mode,
|
||||
icon,
|
||||
|
||||
+10
-8
@@ -157,12 +157,13 @@ async fn run_once(state: &mut WayVRClientState, args: Args) -> anyhow::Result<()
|
||||
wvr_process_terminate(state, handle).await;
|
||||
}
|
||||
Subcommands::ProcessLaunch {
|
||||
exec,
|
||||
name,
|
||||
env,
|
||||
resolution,
|
||||
pos,
|
||||
icon,
|
||||
command,
|
||||
args,
|
||||
} => {
|
||||
let env = env.split(",").map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
let resolution = resolution
|
||||
@@ -180,12 +181,13 @@ async fn run_once(state: &mut WayVRClientState, args: Args) -> anyhow::Result<()
|
||||
|
||||
wvr_process_launch(
|
||||
state,
|
||||
command,
|
||||
exec,
|
||||
name,
|
||||
env,
|
||||
resolution,
|
||||
pos_mode,
|
||||
icon,
|
||||
args,
|
||||
HashMap::new(),
|
||||
)
|
||||
.await;
|
||||
@@ -314,17 +316,17 @@ enum Subcommands {
|
||||
/// Environment variables, separated by comma
|
||||
#[arg(short, long, default_value = "")]
|
||||
env: String,
|
||||
#[arg(short, long, default_value = "1920x1080")]
|
||||
/// Executable to run
|
||||
exec: String,
|
||||
#[arg(default_value = "1920x1080")]
|
||||
resolution: String,
|
||||
/// Default positioning
|
||||
#[arg(short, long, default_value = "anchored")]
|
||||
pos: PosModeEnum,
|
||||
/// Absolute path to the app icon
|
||||
#[arg(short, long, default_value = "")]
|
||||
icon: Option<String>,
|
||||
/// Executable and optional arguments
|
||||
#[arg(required = true, num_args = 1..)]
|
||||
command: Vec<String>,
|
||||
/// Arguments to pass to executable
|
||||
#[arg(default_value = "")]
|
||||
args: String,
|
||||
},
|
||||
/// Trigger haptics on the user's controller
|
||||
Haptics {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{cell::RefCell, f32, rc::Rc};
|
||||
|
||||
use glam::{Mat4, Vec2, Vec3};
|
||||
use taffy::prelude::{length, percent};
|
||||
|
||||
+3
-18
@@ -139,17 +139,6 @@ impl CommandBufferBuilder<CmdBufGfx> {
|
||||
|
||||
impl CommandBufferBuilder<CmdBufXfer> {
|
||||
pub fn upload_image(&mut self, width: u32, height: u32, format: Format, data: &[u8]) -> anyhow::Result<Arc<Image>> {
|
||||
self.upload_image_with_row_length(width, height, format, data, 0)
|
||||
}
|
||||
|
||||
pub fn upload_image_with_row_length(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
format: Format,
|
||||
data: &[u8],
|
||||
buffer_row_length: u32,
|
||||
) -> anyhow::Result<Arc<Image>> {
|
||||
let image = self.graphics.create_image(ImageCreateInfo::new_2d(
|
||||
width,
|
||||
height,
|
||||
@@ -160,13 +149,9 @@ impl CommandBufferBuilder<CmdBufXfer> {
|
||||
.graphics
|
||||
.empty_buffer::<u8>(BufferUsage::TRANSFER_SRC, data.len() as u64)?;
|
||||
staging.write()?.copy_from_slice(data);
|
||||
self.raw.copy_buffer_to_image_with_row_length(
|
||||
&staging.raw,
|
||||
&image.raw,
|
||||
[0, 0, 0],
|
||||
[width, height, 1],
|
||||
buffer_row_length,
|
||||
);
|
||||
self
|
||||
.raw
|
||||
.copy_buffer_to_image(&staging.raw, &image.raw, [0, 0, 0], [width, height, 1]);
|
||||
self.keepalive.push(Box::new(staging));
|
||||
self.keepalive.push(Box::new(image.clone()));
|
||||
Ok(image)
|
||||
|
||||
@@ -462,8 +462,4 @@ impl WGfx {
|
||||
}
|
||||
cmd::CommandBufferBuilder::new(self.clone(), QueueType::Transfer, usage, PhantomData)
|
||||
}
|
||||
|
||||
pub fn enumerate_devices(&self) -> anyhow::Result<Vec<String>> {
|
||||
self.raw.enumerate_devices()
|
||||
}
|
||||
}
|
||||
+1
-31
@@ -1687,25 +1687,6 @@ impl Context {
|
||||
external_images: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enumerate_devices(&self) -> anyhow::Result<Vec<String>> {
|
||||
let devices = unsafe { self.instance.enumerate_physical_devices() }
|
||||
.map_err(|e| anyhow!("vkEnumeratePhysicalDevices failed: {e:?}"))?;
|
||||
|
||||
let devices: Vec<String> = devices
|
||||
.iter()
|
||||
.map(|dev| unsafe {
|
||||
let mut id = vk::PhysicalDeviceIDProperties::default();
|
||||
let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut id);
|
||||
self.instance.get_physical_device_properties2(*dev, &mut props2);
|
||||
CStr::from_ptr(props2.properties.device_name.as_ptr())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Context {
|
||||
@@ -2494,22 +2475,11 @@ impl RawCommandBuffer {
|
||||
image: &Arc<RawImage>,
|
||||
offset: [u32; 3],
|
||||
extent: [u32; 3],
|
||||
) {
|
||||
self.copy_buffer_to_image_with_row_length(buffer, image, offset, extent, 0);
|
||||
}
|
||||
|
||||
pub(super) fn copy_buffer_to_image_with_row_length(
|
||||
&mut self,
|
||||
buffer: &RawBuffer,
|
||||
image: &Arc<RawImage>,
|
||||
offset: [u32; 3],
|
||||
extent: [u32; 3],
|
||||
buffer_row_length: u32,
|
||||
) {
|
||||
self.transition_image(image, ImageLayout::TransferDst);
|
||||
let regions = [vk::BufferImageCopy::default()
|
||||
.buffer_offset(0)
|
||||
.buffer_row_length(buffer_row_length)
|
||||
.buffer_row_length(0)
|
||||
.buffer_image_height(0)
|
||||
.image_subresource(vk::ImageSubresourceLayers {
|
||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||
|
||||
@@ -69,16 +69,6 @@ pub trait LangsList {
|
||||
}
|
||||
|
||||
impl Locale {
|
||||
#[must_use]
|
||||
/// Returns a clone of `self` without `region`
|
||||
pub fn without_region(&self) -> Locale {
|
||||
Locale {
|
||||
lang: self.lang.clone(),
|
||||
region: None,
|
||||
matched: self.matched.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn match_locale<'o>(
|
||||
default_lang: &'static str,
|
||||
lang: &str,
|
||||
|
||||
+1
-1
@@ -660,7 +660,7 @@ impl Layout {
|
||||
pub fn recompute_layout(&mut self, new_size: Option<Vec2>) -> anyhow::Result<()> {
|
||||
let size = match new_size {
|
||||
Some(size) => Vec2::new(size.x.round(), size.y.round()),
|
||||
None => self.prev_size,
|
||||
None => self.content_size,
|
||||
};
|
||||
|
||||
if !self.state.tree.dirty(self.tree_root_node)? && self.prev_size == size {
|
||||
|
||||
@@ -16,7 +16,6 @@ idmap = { workspace = true }
|
||||
libc.workspace = true
|
||||
log = { workspace = true }
|
||||
pipewire = { workspace = true, optional = true }
|
||||
timeline_syncobj = { git = "https://github.com/Schmarni-Dev/timeline_syncobj", rev = "7f67e49ebf466487cf35138fc108618092acb9e8", optional = true }
|
||||
slotmap.workspace = true
|
||||
wayland-client = { workspace = true, optional = true }
|
||||
wayland-protocols = {
|
||||
@@ -32,9 +31,7 @@ wayland-protocols = {
|
||||
|
||||
drm-fourcc = "2.2.0"
|
||||
rxscreen = {
|
||||
git = "https://github.com/galister/rxscreen.git",
|
||||
rev = "7197e1de6622b65859e6f2060b5750185e0dad2a",
|
||||
# version = "0.1.7",
|
||||
version = "0.1.7",
|
||||
features = [
|
||||
"mouse",
|
||||
"shm",
|
||||
@@ -47,7 +44,7 @@ smithay-client-toolkit = { version = "0.20.0", optional = true }
|
||||
[features]
|
||||
default = ["pipewire", "wayland"]
|
||||
egl = []
|
||||
pipewire = ["dep:dbus", "dep:pipewire", "dep:timeline_syncobj", "wayland"]
|
||||
pipewire = ["dep:dbus", "dep:pipewire", "wayland"]
|
||||
wayland = [
|
||||
"dep:smithay-client-toolkit",
|
||||
"dep:wayland-client",
|
||||
|
||||
@@ -30,6 +30,17 @@ let capture = PipewireCapture::new(
|
||||
);
|
||||
```
|
||||
|
||||
### Wlr-Dmabuf Setup
|
||||
```rust
|
||||
let wl = WlxClient::new();
|
||||
|
||||
// select desired screen
|
||||
let output_id = wl.outputs.keys().first().unwrap();
|
||||
|
||||
let mut capture = WlrDmabufCapture::new(wl, *output_id).unwrap();
|
||||
```
|
||||
|
||||
|
||||
### XSHM Setup
|
||||
```rust
|
||||
let monitors = XshmCapture::get_monitors();
|
||||
|
||||
+1
-112
@@ -1,10 +1,4 @@
|
||||
use std::{
|
||||
os::fd::{AsRawFd, OwnedFd, RawFd},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
use timeline_syncobj::timeline_syncobj::TimelineSyncObj;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier};
|
||||
|
||||
@@ -75,8 +69,6 @@ pub struct DmabufFrame {
|
||||
pub num_planes: usize,
|
||||
pub planes: [FramePlane; 4],
|
||||
pub mouse: Option<MouseMeta>,
|
||||
pub acquire_sync: FrameAcquireSync,
|
||||
pub lease: Option<FrameLease>,
|
||||
}
|
||||
|
||||
impl DmabufFrame {
|
||||
@@ -136,8 +128,6 @@ pub struct MemPtrFrame {
|
||||
pub format: FrameFormat,
|
||||
pub ptr: usize,
|
||||
pub size: usize,
|
||||
pub offset: usize,
|
||||
pub stride: usize,
|
||||
pub mouse: Option<MouseMeta>,
|
||||
}
|
||||
|
||||
@@ -147,107 +137,6 @@ pub struct MouseMeta {
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FrameLease(Arc<FrameLeaseInner>);
|
||||
|
||||
struct FrameLeaseInner(Box<dyn Fn() + Send + Sync>);
|
||||
|
||||
impl FrameLease {
|
||||
pub fn new(f: impl Fn() + Send + Sync + 'static) -> Self {
|
||||
Self(Arc::new(FrameLeaseInner(Box::new(f))))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FrameLeaseInner {
|
||||
fn drop(&mut self) {
|
||||
(self.0)();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub enum FrameAcquireSync {
|
||||
#[default]
|
||||
/// Frame is ready, no sync needed
|
||||
Ready,
|
||||
/// Implicit sync using polled FDs
|
||||
Implicit { fds: Arc<[OwnedFd]> },
|
||||
#[cfg(feature = "pipewire")]
|
||||
/// Explicit sync using DRM SyncObj
|
||||
Explicit {
|
||||
timeline: Arc<TimelineSyncObj>,
|
||||
point: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl FrameAcquireSync {
|
||||
pub fn check_ready(&mut self) -> std::io::Result<bool> {
|
||||
match self {
|
||||
FrameAcquireSync::Ready => Ok(true),
|
||||
FrameAcquireSync::Implicit { fds } => {
|
||||
if fds.is_empty() {
|
||||
return Err(std::io::Error::other("implicit DMA-buf without planes"));
|
||||
}
|
||||
for plane_fd in fds.iter() {
|
||||
let fd = plane_fd.as_raw_fd();
|
||||
let mut pfd = libc::pollfd {
|
||||
fd,
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
let result = unsafe { libc::poll(&mut pfd, 1, 0) };
|
||||
if result < 0 {
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.kind() == std::io::ErrorKind::Interrupted {
|
||||
return Ok(false);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
if result == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
if pfd.revents & (libc::POLLNVAL | libc::POLLHUP) != 0 {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"DMA-buf plane fd {fd} poll returned with {:#x}",
|
||||
pfd.revents
|
||||
)));
|
||||
}
|
||||
if pfd.revents & libc::POLLERR != 0 {
|
||||
static WARNED: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
log::error!(
|
||||
"Could not poll implicit acquire fence. Falling back to unsynchronized behavior. Visual artifacts may happen."
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if pfd.revents & libc::POLLIN == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
*self = FrameAcquireSync::Ready;
|
||||
Ok(true)
|
||||
}
|
||||
#[cfg(feature = "pipewire")]
|
||||
FrameAcquireSync::Explicit { timeline, point } => {
|
||||
if *point == 0 {
|
||||
return Ok(true);
|
||||
}
|
||||
if timeline
|
||||
.query_signaled_point()
|
||||
.map(|signaled| signaled >= *point)
|
||||
.map_err(|e| std::io::Error::from_raw_os_error(e.raw_os_error()))?
|
||||
{
|
||||
*self = FrameAcquireSync::Ready;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DmaExporter {
|
||||
fn next_frame(
|
||||
&mut self,
|
||||
|
||||
@@ -6,12 +6,12 @@ use frame::WlxFrame;
|
||||
|
||||
pub mod frame;
|
||||
|
||||
#[cfg(feature = "pipewire")]
|
||||
pub mod syncobj;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub mod wayland;
|
||||
|
||||
#[cfg(feature = "wlr")]
|
||||
pub mod wlr_dmabuf;
|
||||
|
||||
#[cfg(feature = "wlr")]
|
||||
pub mod wlr_screencopy;
|
||||
|
||||
|
||||
+141
-338
@@ -1,9 +1,5 @@
|
||||
use std::any::Any;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc;
|
||||
use std::thread::JoinHandle;
|
||||
@@ -14,9 +10,8 @@ use drm_fourcc::DrmModifier;
|
||||
use pipewire as pw;
|
||||
use pipewire::context::ContextRc;
|
||||
use pipewire::main_loop::MainLoopRc;
|
||||
use pipewire::spa::buffer::meta::{
|
||||
MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform, MetaVideoTransformValue,
|
||||
};
|
||||
use pipewire::spa::buffer::meta::MetaVideoTransformValue;
|
||||
use pipewire::spa::buffer::meta::{MetaCursor, MetaHeader, MetaHeaderFlags, MetaVideoTransform};
|
||||
use pipewire::stream::StreamRc;
|
||||
use pw::spa;
|
||||
|
||||
@@ -26,7 +21,7 @@ use pw::stream::{Stream, StreamFlags};
|
||||
use spa::buffer::DataType;
|
||||
use spa::param::ParamType;
|
||||
use spa::param::video::VideoFormat;
|
||||
use spa::param::video::{VideoFlags, VideoInfoRaw};
|
||||
use spa::param::video::VideoInfoRaw;
|
||||
use spa::pod::ChoiceValue;
|
||||
use spa::pod::Pod;
|
||||
use spa::pod::serialize::GenError;
|
||||
@@ -34,24 +29,13 @@ use spa::pod::{Object, Property, PropertyFlags, Value};
|
||||
use spa::utils::Choice;
|
||||
use spa::utils::ChoiceEnum;
|
||||
use spa::utils::ChoiceFlags;
|
||||
use timeline_syncobj::render_node::DrmRenderNode;
|
||||
use wayland_client::backend::smallvec::SmallVec;
|
||||
|
||||
use super::raw_buffer::DequeuedBuffer;
|
||||
use crate::WlxCapture;
|
||||
use crate::frame::FrameFormat;
|
||||
use crate::frame::MouseMeta;
|
||||
use crate::frame::Transform;
|
||||
use crate::frame::WlxFrame;
|
||||
use crate::frame::{
|
||||
DmabufFrame, FrameAcquireSync, FrameLease, FramePlane, MemFdFrame, MemPtrFrame,
|
||||
};
|
||||
use crate::pipewire::raw_buffer::{HeldBuffers, MAX_PW_BUFFERS};
|
||||
use crate::syncobj::import_timeline;
|
||||
|
||||
const SPA_DATA_SYNCOBJ: u32 = 5;
|
||||
const SPA_META_SYNC_TIMELINE: u32 = 9;
|
||||
const SPA_PARAM_BUFFERS_META_TYPE: u32 = 7;
|
||||
use crate::frame::{DmabufFrame, FramePlane, MemFdFrame, MemPtrFrame};
|
||||
|
||||
#[derive(Default)]
|
||||
struct StreamData {
|
||||
@@ -64,7 +48,6 @@ pub enum PwChangeRequest {
|
||||
Pause,
|
||||
Resume,
|
||||
Stop,
|
||||
ReleaseBuffer(usize),
|
||||
}
|
||||
|
||||
struct CaptureData<R>
|
||||
@@ -83,20 +66,18 @@ where
|
||||
data: Option<CaptureData<R>>,
|
||||
node_id: u32,
|
||||
handle: Option<JoinHandle<Result<(), Error>>>,
|
||||
drm_render_node: Option<DrmRenderNode>,
|
||||
}
|
||||
|
||||
impl<R> PipewireCapture<R>
|
||||
where
|
||||
R: Any + Send,
|
||||
{
|
||||
pub fn new(name: Arc<str>, node_id: u32, drm_render_node: Option<DrmRenderNode>) -> Self {
|
||||
pub fn new(name: Arc<str>, node_id: u32) -> Self {
|
||||
PipewireCapture {
|
||||
name,
|
||||
data: None,
|
||||
node_id,
|
||||
handle: None,
|
||||
drm_render_node,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +87,7 @@ where
|
||||
R: Any + Send,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
if let Some(data) = self.data.take() {
|
||||
drop(data.rx_frame);
|
||||
if let Some(data) = &self.data {
|
||||
let _ = data.tx_ctrl.send(PwChangeRequest::Stop);
|
||||
}
|
||||
if let Some(handle) = self.handle.take() {
|
||||
@@ -127,15 +107,10 @@ where
|
||||
user_data: U,
|
||||
receive_callback: fn(&U, WlxFrame) -> Option<R>,
|
||||
) {
|
||||
let (tx_frame, rx_frame) = mpsc::sync_channel(4);
|
||||
let (tx_frame, rx_frame) = mpsc::sync_channel(2);
|
||||
let (tx_ctrl, rx_ctrl) = pw::channel::channel();
|
||||
|
||||
self.data = Some(CaptureData {
|
||||
tx_ctrl: tx_ctrl.clone(),
|
||||
rx_frame,
|
||||
});
|
||||
|
||||
let drm_render_node = self.drm_render_node.clone();
|
||||
self.data = Some(CaptureData { tx_ctrl, rx_frame });
|
||||
|
||||
self.handle = Some(std::thread::spawn({
|
||||
let name = self.name.clone();
|
||||
@@ -148,11 +123,9 @@ where
|
||||
node_id,
|
||||
formats,
|
||||
tx_frame,
|
||||
tx_ctrl,
|
||||
rx_ctrl,
|
||||
user_data,
|
||||
receive_callback,
|
||||
drm_render_node,
|
||||
)
|
||||
}
|
||||
}));
|
||||
@@ -202,12 +175,10 @@ fn main_loop<U, R>(
|
||||
name: Arc<str>,
|
||||
node_id: u32,
|
||||
dmabuf_formats: Vec<DrmFormat>,
|
||||
tx_frame: mpsc::SyncSender<R>,
|
||||
tx_ctrl: pw::channel::Sender<PwChangeRequest>,
|
||||
rx_ctrl: pw::channel::Receiver<PwChangeRequest>,
|
||||
sender: mpsc::SyncSender<R>,
|
||||
receiver: pw::channel::Receiver<PwChangeRequest>,
|
||||
user_data: U,
|
||||
receive_callback: fn(&U, WlxFrame) -> Option<R>,
|
||||
drm_render_node: Option<DrmRenderNode>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
U: Any,
|
||||
@@ -218,8 +189,6 @@ where
|
||||
let context = ContextRc::new(&main_loop, None)?;
|
||||
let core = context.connect_rc(None)?;
|
||||
|
||||
let held_buffers = Rc::new(RefCell::new(HeldBuffers::default()));
|
||||
|
||||
let stream = StreamRc::new(
|
||||
core,
|
||||
&name,
|
||||
@@ -248,7 +217,6 @@ where
|
||||
})
|
||||
.param_changed({
|
||||
let name = name.clone();
|
||||
let support_explicit_sync = drm_render_node.is_some();
|
||||
move |stream, format, id, param| {
|
||||
let Some(param) = param else {
|
||||
return;
|
||||
@@ -266,278 +234,172 @@ where
|
||||
format.drm_format.code = spa_to_fourcc(info.format());
|
||||
format.drm_format.modifier = DrmModifier::from(info.modifier());
|
||||
|
||||
let dmabuf = info.flags().contains(VideoFlags::MODIFIER);
|
||||
let kind = if dmabuf { "DMA-buf" } else { "SHM" };
|
||||
let kind = if info.modifier() != 0 {
|
||||
"DMA-buf"
|
||||
} else {
|
||||
"SHM"
|
||||
};
|
||||
|
||||
log::info!("{}: got {} video format:", name, kind);
|
||||
log::info!(" format: {} ({:?})", info.format().as_raw(), info.format());
|
||||
log::info!(" size: {}x{}", info.size().width, info.size().height);
|
||||
log::info!(" modifier: {}", info.modifier());
|
||||
let Ok(params_bytes) = obj_to_bytes(get_buffer_params()) else {
|
||||
log::warn!("{}: failed to serialize buffer params", name);
|
||||
return;
|
||||
};
|
||||
let Some(params_pod) = Pod::from_bytes(¶ms_bytes) else {
|
||||
log::warn!("{}: failed to deserialize buffer params", name);
|
||||
return;
|
||||
};
|
||||
|
||||
let mut param_bytes = Vec::new();
|
||||
if dmabuf && support_explicit_sync {
|
||||
param_bytes.push(obj_to_bytes(get_sync_buffer_params()).unwrap());
|
||||
// order defines preference, we prefer explicit sync
|
||||
param_bytes.push(
|
||||
obj_to_bytes(get_meta_object(
|
||||
SPA_META_SYNC_TIMELINE,
|
||||
std::mem::size_of::<MetaSyncTimeline>(),
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
param_bytes.push(obj_to_bytes(get_buffer_params()).unwrap());
|
||||
param_bytes.push(
|
||||
obj_to_bytes(get_meta_object(
|
||||
spa::sys::SPA_META_Header,
|
||||
std::mem::size_of::<spa::sys::spa_meta_header>(),
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
param_bytes.push(
|
||||
obj_to_bytes(get_meta_object(
|
||||
spa::sys::SPA_META_VideoTransform,
|
||||
std::mem::size_of::<spa::sys::spa_meta_videotransform>(),
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
let header_bytes = obj_to_bytes(get_meta_object(
|
||||
spa::sys::SPA_META_Header,
|
||||
std::mem::size_of::<spa::sys::spa_meta_header>(),
|
||||
))
|
||||
.unwrap(); // want panic
|
||||
let header_pod = Pod::from_bytes(&header_bytes).unwrap(); // want panic
|
||||
|
||||
let mut pods: Vec<&Pod> = param_bytes
|
||||
.iter()
|
||||
.map(|bytes| Pod::from_bytes(bytes).unwrap())
|
||||
.collect();
|
||||
let xform_bytes = obj_to_bytes(get_meta_object(
|
||||
spa::sys::SPA_META_VideoTransform,
|
||||
std::mem::size_of::<spa::sys::spa_meta_videotransform>(),
|
||||
))
|
||||
.unwrap(); // want panic
|
||||
let xform_pod = Pod::from_bytes(&xform_bytes).unwrap(); // want panic
|
||||
|
||||
let mut pods = [params_pod, header_pod, xform_pod];
|
||||
if let Err(e) = stream.update_params(&mut pods) {
|
||||
log::error!("{}: failed to update params: {}", name, e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.remove_buffer({
|
||||
let held_buffers = held_buffers.clone();
|
||||
let name = name.clone();
|
||||
move |_, _, ptr| {
|
||||
if let Some(buffer) = NonNull::new(ptr)
|
||||
&& held_buffers.borrow_mut().on_removed(buffer)
|
||||
{
|
||||
log::warn!("{name}: PipeWire removed a still-leased buffer");
|
||||
}
|
||||
}
|
||||
})
|
||||
.process({
|
||||
let name = name.clone();
|
||||
let sync_device = drm_render_node.clone();
|
||||
let held_buffers = held_buffers.clone();
|
||||
let tx_ctrl = tx_ctrl.clone();
|
||||
let u = user_data;
|
||||
move |stream, format| {
|
||||
let mut new_buffer = None;
|
||||
while let Some(buffer) = DequeuedBuffer::dequeue(stream) {
|
||||
new_buffer = Some(buffer);
|
||||
}
|
||||
let Some(buffer) = new_buffer else { return };
|
||||
|
||||
if let Some(header) = buffer.find_meta::<MetaHeader>()
|
||||
&& header.flags().contains(MetaHeaderFlags::CORRUPTED)
|
||||
{
|
||||
log::warn!("{name}: PipeWire buffer is corrupt");
|
||||
return;
|
||||
let mut maybe_buffer = None;
|
||||
// discard all but the newest frame
|
||||
while let Some(buffer) = stream.dequeue_buffer() {
|
||||
maybe_buffer = Some(buffer);
|
||||
}
|
||||
|
||||
if let Some(transform) = buffer.find_meta::<MetaVideoTransform>() {
|
||||
format.transform = match transform.transform() {
|
||||
MetaVideoTransformValue::NONE => Transform::Normal,
|
||||
MetaVideoTransformValue::ROTATED90 => Transform::Rotated90,
|
||||
MetaVideoTransformValue::ROTATED180 => Transform::Rotated180,
|
||||
MetaVideoTransformValue::ROTATED270 => Transform::Rotated270,
|
||||
MetaVideoTransformValue::FLIPPED => Transform::Flipped,
|
||||
MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90,
|
||||
MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180,
|
||||
MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270,
|
||||
_ => Transform::Undefined,
|
||||
};
|
||||
}
|
||||
if let Some(mut buffer) = maybe_buffer {
|
||||
if let Some(header) = buffer.find_meta::<MetaHeader>()
|
||||
&& header.flags().contains(MetaHeaderFlags::CORRUPTED)
|
||||
{
|
||||
log::warn!("{}: PipeWire buffer is corrupt.", name);
|
||||
return;
|
||||
}
|
||||
|
||||
let mouse_meta = buffer
|
||||
.find_meta::<MetaCursor>()
|
||||
.filter(|c| c.is_valid())
|
||||
.map(|c| MouseMeta {
|
||||
x: c.position().x as f32 / format.width as f32,
|
||||
y: c.position().y as f32 / format.height as f32,
|
||||
});
|
||||
|
||||
let acquire_point = buffer
|
||||
.find_meta::<MetaSyncTimeline>()
|
||||
.map(|meta| meta.acquire_point);
|
||||
|
||||
let datas = buffer.datas_mut();
|
||||
if datas.is_empty() {
|
||||
log::debug!("{name}: empty PipeWire frame");
|
||||
return;
|
||||
}
|
||||
let is_explicit = datas.len() >= 3
|
||||
&& datas[datas.len() - 2].type_().as_raw() == SPA_DATA_SYNCOBJ
|
||||
&& datas[datas.len() - 1].type_().as_raw() == SPA_DATA_SYNCOBJ;
|
||||
if is_explicit != acquire_point.is_some() {
|
||||
log::warn!("{name}: inconsistent SyncTimeline metadata / SyncObj FD layout");
|
||||
return;
|
||||
}
|
||||
let acquire_fd = is_explicit.then(|| datas[datas.len() - 2].fd());
|
||||
let datas = if is_explicit {
|
||||
&datas[..datas.len() - 2]
|
||||
} else {
|
||||
datas
|
||||
};
|
||||
if datas.is_empty()
|
||||
|| datas.len() > 4
|
||||
|| datas
|
||||
.iter()
|
||||
.any(|d| d.type_().as_raw() == SPA_DATA_SYNCOBJ)
|
||||
{
|
||||
log::warn!("{name}: invalid PipeWire plane layout");
|
||||
return;
|
||||
}
|
||||
let data_type = datas[0].type_();
|
||||
if datas.iter().any(|d| d.type_() != data_type) {
|
||||
log::warn!("{name}: mixed PipeWire data types");
|
||||
return;
|
||||
}
|
||||
|
||||
match datas[0].type_() {
|
||||
DataType::DmaBuf => {
|
||||
let mut planes: SmallVec<[FramePlane; 4]> = datas
|
||||
.iter()
|
||||
.map(|p| FramePlane {
|
||||
fd: Some(p.as_raw().fd as _),
|
||||
offset: p.chunk().offset(),
|
||||
stride: p.chunk().stride(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let acquire_sync = if is_explicit {
|
||||
let Some(device) = sync_device.as_ref() else {
|
||||
log::error!("{name}: explicit sync without DRM timeline support");
|
||||
return;
|
||||
};
|
||||
let acquire_point = acquire_point.unwrap();
|
||||
let fd = acquire_fd.unwrap();
|
||||
let acquire = match import_timeline(device, fd as RawFd) {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
log::error!("{name}: acquire SyncObj import failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
FrameAcquireSync::Explicit {
|
||||
timeline: acquire,
|
||||
point: acquire_point,
|
||||
}
|
||||
} else {
|
||||
let mut fds = Vec::with_capacity(planes.len());
|
||||
for (index, plane) in planes.iter_mut().enumerate() {
|
||||
let fd = plane.fd.unwrap(); // always Some
|
||||
// duplicate the fd so that we can poll it for acquire sync
|
||||
let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
|
||||
if duplicate < 0 {
|
||||
log::error!(
|
||||
"{name}: failed to duplicate DMA-buf plane {index} FD {fd}: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
return;
|
||||
}
|
||||
fds.push(unsafe { OwnedFd::from_raw_fd(duplicate) });
|
||||
plane.fd = Some(duplicate);
|
||||
}
|
||||
FrameAcquireSync::Implicit { fds: fds.into() }
|
||||
if let Some(transform) = buffer.find_meta::<MetaVideoTransform>() {
|
||||
format.transform = match transform.transform() {
|
||||
MetaVideoTransformValue::NONE => Transform::Normal,
|
||||
MetaVideoTransformValue::ROTATED90 => Transform::Rotated90,
|
||||
MetaVideoTransformValue::ROTATED180 => Transform::Rotated180,
|
||||
MetaVideoTransformValue::ROTATED270 => Transform::Rotated270,
|
||||
MetaVideoTransformValue::FLIPPED => Transform::Flipped,
|
||||
MetaVideoTransformValue::FLIPPED90 => Transform::Flipped90,
|
||||
MetaVideoTransformValue::FLIPPED180 => Transform::Flipped180,
|
||||
MetaVideoTransformValue::FLIPPED270 => Transform::Flipped270,
|
||||
_ => Transform::Undefined,
|
||||
};
|
||||
log::debug!("{}: Transform: {:?}", name, format.transform);
|
||||
}
|
||||
|
||||
let lease_id = held_buffers.borrow_mut().insert(buffer);
|
||||
let tx = tx_ctrl.clone();
|
||||
let lease = FrameLease::new(move || {
|
||||
let _ = tx.send(PwChangeRequest::ReleaseBuffer(lease_id));
|
||||
let mouse_meta = buffer
|
||||
.find_meta::<MetaCursor>()
|
||||
.filter(|c| c.is_valid())
|
||||
.map(|c| MouseMeta {
|
||||
x: c.position().x as f32 / format.width as f32,
|
||||
y: c.position().y as f32 / format.height as f32,
|
||||
});
|
||||
|
||||
let mut dmabuf = DmabufFrame {
|
||||
format: *format,
|
||||
num_planes: planes.len(),
|
||||
planes: Default::default(),
|
||||
mouse: mouse_meta,
|
||||
acquire_sync,
|
||||
lease: Some(lease),
|
||||
};
|
||||
let datas = buffer.datas_mut();
|
||||
if datas.is_empty() {
|
||||
log::debug!("{}: no data", name);
|
||||
return;
|
||||
}
|
||||
|
||||
dmabuf.planes[..planes.len()].copy_from_slice(&planes[..planes.len()]);
|
||||
let planes: Vec<FramePlane> = datas
|
||||
.iter()
|
||||
.map(|p| FramePlane {
|
||||
fd: Some(p.as_raw().fd as _),
|
||||
offset: p.chunk().offset(),
|
||||
stride: p.chunk().stride(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(result) = receive_callback(&u, WlxFrame::Dmabuf(dmabuf)) {
|
||||
match tx_frame.try_send(result) {
|
||||
Ok(()) | Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{name}: frame receiver disconnected");
|
||||
let _ = stream.disconnect();
|
||||
match datas[0].type_() {
|
||||
DataType::DmaBuf => {
|
||||
let mut dmabuf = DmabufFrame {
|
||||
format: *format,
|
||||
num_planes: planes.len(),
|
||||
mouse: mouse_meta,
|
||||
planes: Default::default(),
|
||||
};
|
||||
dmabuf.planes[..planes.len()].copy_from_slice(&planes[..planes.len()]);
|
||||
|
||||
let frame = WlxFrame::Dmabuf(dmabuf);
|
||||
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match sender.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DataType::MemFd => {
|
||||
let memfd = MemFdFrame {
|
||||
format: *format,
|
||||
plane: FramePlane {
|
||||
fd: Some(datas[0].as_raw().fd as _),
|
||||
offset: datas[0].chunk().offset(),
|
||||
stride: datas[0].chunk().stride(),
|
||||
},
|
||||
mouse: mouse_meta,
|
||||
};
|
||||
DataType::MemFd => {
|
||||
let memfd = MemFdFrame {
|
||||
format: *format,
|
||||
plane: FramePlane {
|
||||
fd: Some(datas[0].as_raw().fd as _),
|
||||
offset: datas[0].chunk().offset(),
|
||||
stride: datas[0].chunk().stride(),
|
||||
},
|
||||
mouse: mouse_meta,
|
||||
};
|
||||
|
||||
let frame = WlxFrame::MemFd(memfd);
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match tx_frame.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
let frame = WlxFrame::MemFd(memfd);
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match sender.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DataType::MemPtr => {
|
||||
let raw = datas[0].as_raw();
|
||||
let chunk = datas[0].chunk();
|
||||
let offset = chunk.offset() as usize;
|
||||
let chunk_size = chunk.size() as usize;
|
||||
let Some(size) = offset.checked_add(chunk_size) else {
|
||||
log::error!("{name}: PipeWire chunk range overflow");
|
||||
return;
|
||||
};
|
||||
if size > raw.maxsize as usize {
|
||||
log::error!("{name}: PipeWire chunk extends beyond allocation");
|
||||
return;
|
||||
}
|
||||
let memptr = MemPtrFrame {
|
||||
format: *format,
|
||||
ptr: raw.data as _,
|
||||
size,
|
||||
offset,
|
||||
stride: chunk.stride() as _,
|
||||
mouse: mouse_meta,
|
||||
};
|
||||
DataType::MemPtr => {
|
||||
let memptr = MemPtrFrame {
|
||||
format: *format,
|
||||
ptr: datas[0].as_raw().data as _,
|
||||
size: datas[0].chunk().size() as _,
|
||||
mouse: mouse_meta,
|
||||
};
|
||||
|
||||
let frame = WlxFrame::MemPtr(memptr);
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match tx_frame.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
let frame = WlxFrame::MemPtr(memptr);
|
||||
if let Some(r) = receive_callback(&u, frame) {
|
||||
match sender.try_send(r) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected, stopping stream", name);
|
||||
let _ = stream.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("Received invalid frame data type ({:?})", datas[0].type_())
|
||||
}
|
||||
}
|
||||
_ => log::error!("Received invalid frame data type ({:?})", datas[0].type_()),
|
||||
}
|
||||
|
||||
// DMA-buf frames transfer ownership to held_buffers,
|
||||
// requeued when their lease expires
|
||||
}
|
||||
})
|
||||
.register()?;
|
||||
@@ -572,10 +434,9 @@ where
|
||||
params.as_mut_slice(),
|
||||
)?;
|
||||
|
||||
let _receiver = rx_ctrl.attach(main_loop.loop_(), {
|
||||
let _receiver = receiver.attach(main_loop.loop_(), {
|
||||
let name = name.clone();
|
||||
let main_loop = main_loop.clone();
|
||||
let held_buffers = held_buffers.clone();
|
||||
|
||||
move |req| {
|
||||
log::debug!("{name}: request pipewire stream to {req:?}");
|
||||
@@ -590,9 +451,6 @@ where
|
||||
.set_active(true)
|
||||
.inspect_err(|e| log::warn!("Could not {req:?} pipewire stream: {e:?}"));
|
||||
}
|
||||
PwChangeRequest::ReleaseBuffer(id) => {
|
||||
held_buffers.borrow_mut().requeue(&stream, id);
|
||||
}
|
||||
PwChangeRequest::Stop => {
|
||||
main_loop.quit();
|
||||
}
|
||||
@@ -619,25 +477,7 @@ fn get_buffer_params() -> Object {
|
||||
| (1 << DataType::MemPtr.as_raw())
|
||||
| (1 << DataType::DmaBuf.as_raw());
|
||||
|
||||
get_buffer_params_with_data_types(data_types)
|
||||
}
|
||||
|
||||
fn get_buffer_params_with_data_types(data_types: i32) -> Object {
|
||||
// since we hold onto buffers until rendering is done,
|
||||
// fewer than 4 buffers may produce stalls
|
||||
let buffers = Property {
|
||||
key: spa::sys::SPA_PARAM_BUFFERS_buffers,
|
||||
flags: PropertyFlags::empty(),
|
||||
value: Value::Choice(ChoiceValue::Int(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Range {
|
||||
default: 6,
|
||||
min: 4,
|
||||
max: MAX_PW_BUFFERS,
|
||||
},
|
||||
))),
|
||||
};
|
||||
let data_types = Property {
|
||||
let property = Property {
|
||||
key: spa::sys::SPA_PARAM_BUFFERS_dataType,
|
||||
flags: PropertyFlags::empty(),
|
||||
value: Value::Int(data_types),
|
||||
@@ -646,35 +486,10 @@ fn get_buffer_params_with_data_types(data_types: i32) -> Object {
|
||||
spa::pod::object!(
|
||||
spa::utils::SpaTypes::ObjectParamBuffers,
|
||||
spa::param::ParamType::Buffers,
|
||||
buffers,
|
||||
data_types,
|
||||
property,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_sync_buffer_params() -> Object {
|
||||
let mut obj = get_buffer_params_with_data_types(1 << DataType::DmaBuf.as_raw());
|
||||
obj.properties.push(Property {
|
||||
key: spa::sys::SPA_PARAM_BUFFERS_blocks,
|
||||
flags: PropertyFlags::empty(),
|
||||
value: Value::Choice(ChoiceValue::Int(Choice(
|
||||
ChoiceFlags::empty(),
|
||||
ChoiceEnum::Range {
|
||||
default: 3,
|
||||
// when using sync timeline, we want at least 1 image plane + 2 syncobj fds
|
||||
min: 3,
|
||||
// up to 4 image planes
|
||||
max: 6,
|
||||
},
|
||||
))),
|
||||
});
|
||||
obj.properties.push(Property {
|
||||
key: SPA_PARAM_BUFFERS_META_TYPE,
|
||||
flags: PropertyFlags::MANDATORY,
|
||||
value: Value::Int(1_i32 << SPA_META_SYNC_TIMELINE),
|
||||
});
|
||||
obj
|
||||
}
|
||||
|
||||
fn get_meta_object(key: u32, size: usize) -> Object {
|
||||
let meta_type_property = Property {
|
||||
key: spa::sys::SPA_PARAM_META_type,
|
||||
@@ -812,15 +627,3 @@ fn spa_to_fourcc(spa: VideoFormat) -> DrmFourcc {
|
||||
_ => panic!("Unsupported format"),
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct MetaSyncTimeline {
|
||||
_flags: u32,
|
||||
_padding: u32,
|
||||
acquire_point: u64,
|
||||
_release_point: u64,
|
||||
}
|
||||
|
||||
impl spa::buffer::meta::Metadata for MetaSyncTimeline {
|
||||
const META_TYPE: u32 = SPA_META_SYNC_TIMELINE;
|
||||
}
|
||||
@@ -26,7 +26,6 @@ slotmap::new_key_type! {
|
||||
|
||||
pub mod capture;
|
||||
mod dbus_screencast;
|
||||
mod raw_buffer;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PipewireStream {
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
use std::{marker::PhantomData, mem, ptr::NonNull};
|
||||
|
||||
use pipewire::{
|
||||
self as pw,
|
||||
spa::{self, buffer::meta::Metadata},
|
||||
stream::Stream,
|
||||
};
|
||||
|
||||
pub(super) struct DequeuedBuffer<'a> {
|
||||
stream: &'a Stream,
|
||||
ptr: NonNull<pw::sys::pw_buffer>,
|
||||
_not_send: PhantomData<std::rc::Rc<()>>,
|
||||
}
|
||||
|
||||
impl<'a> DequeuedBuffer<'a> {
|
||||
pub(super) fn dequeue(stream: &'a Stream) -> Option<Self> {
|
||||
let ptr = NonNull::new(unsafe { stream.dequeue_raw_buffer() })?;
|
||||
Some(Self {
|
||||
stream,
|
||||
ptr,
|
||||
_not_send: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
fn spa(&self) -> Option<NonNull<spa::sys::spa_buffer>> {
|
||||
NonNull::new(unsafe { self.ptr.read().buffer })
|
||||
}
|
||||
|
||||
pub(super) fn datas_mut(&self) -> &'a mut [pw::spa::buffer::Data] {
|
||||
let Some(buffer) = self.spa() else {
|
||||
return &mut [];
|
||||
};
|
||||
unsafe {
|
||||
let n_datas = buffer.read().n_datas;
|
||||
if n_datas > 0
|
||||
&& let Some(datas) = NonNull::new(buffer.read().datas)
|
||||
{
|
||||
let datas = datas.as_ptr() as *mut pw::spa::buffer::Data;
|
||||
std::slice::from_raw_parts_mut(datas, usize::try_from(n_datas).unwrap())
|
||||
} else {
|
||||
&mut []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn find_meta<T>(&self) -> Option<&'a T>
|
||||
where
|
||||
T: Metadata,
|
||||
{
|
||||
let buffer = self.spa()?;
|
||||
unsafe {
|
||||
if buffer.read().n_metas != 0 {
|
||||
match T::META_TYPE {
|
||||
pw::spa::sys::SPA_META_VideoDamage => {
|
||||
let meta_data =
|
||||
pw::spa::sys::spa_buffer_find_meta(buffer.as_ptr(), T::META_TYPE)
|
||||
as *const T;
|
||||
if !meta_data.is_null() {
|
||||
return Some(&*meta_data);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let meta_data = pw::spa::sys::spa_buffer_find_meta_data(
|
||||
buffer.as_ptr(),
|
||||
T::META_TYPE,
|
||||
std::mem::size_of::<T>(),
|
||||
) as *const T;
|
||||
if !meta_data.is_null() {
|
||||
return Some(&*meta_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const MAX_PW_BUFFERS: i32 = 8;
|
||||
|
||||
impl Drop for DequeuedBuffer<'_> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { self.stream.queue_raw_buffer(self.ptr.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct HeldBuffers {
|
||||
buffers: Vec<NonNull<pw::sys::pw_buffer>>,
|
||||
}
|
||||
|
||||
impl HeldBuffers {
|
||||
pub(super) fn insert<'a>(&mut self, buffer: DequeuedBuffer<'a>) -> usize {
|
||||
self.buffers.push(buffer.ptr);
|
||||
let ret_val = buffer.ptr.as_ptr() as usize;
|
||||
mem::forget(buffer);
|
||||
ret_val
|
||||
}
|
||||
|
||||
pub(super) fn on_removed(&mut self, buffer: NonNull<pw::sys::pw_buffer>) -> bool {
|
||||
let before_count = self.buffers.len();
|
||||
self.buffers.retain(|ptr| ptr.as_ptr() != buffer.as_ptr());
|
||||
before_count != self.buffers.len()
|
||||
}
|
||||
|
||||
pub(super) fn requeue(&mut self, stream: &pw::stream::Stream, id: usize) {
|
||||
self.buffers.retain(|ptr| {
|
||||
if ptr.as_ptr() as usize == id {
|
||||
unsafe { stream.queue_raw_buffer(ptr.as_ptr()) };
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HeldBuffers {
|
||||
fn drop(&mut self) {
|
||||
log::warn!("{} checked-out buffers at shutdown", self.buffers.len());
|
||||
self.buffers.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use std::{
|
||||
fs, io,
|
||||
os::{
|
||||
fd::{AsFd, AsRawFd, BorrowedFd, RawFd},
|
||||
unix::fs::{FileTypeExt, MetadataExt},
|
||||
},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub use timeline_syncobj::render_node::DrmRenderNode;
|
||||
use timeline_syncobj::timeline_syncobj::TimelineSyncObj;
|
||||
|
||||
const DRM_CAP_SYNCOBJ: u64 = 0x13;
|
||||
const DRM_CAP_SYNCOBJ_TIMELINE: u64 = 0x14;
|
||||
|
||||
#[repr(C)]
|
||||
struct DrmGetCap {
|
||||
capability: u64,
|
||||
value: u64,
|
||||
}
|
||||
|
||||
const DRM_IOCTL_GET_CAP: libc::c_ulong = ((3_u64 << 30)
|
||||
| ((std::mem::size_of::<DrmGetCap>() as u64) << 16)
|
||||
| ((b'd' as u64) << 8)
|
||||
| 0x0c) as libc::c_ulong;
|
||||
|
||||
fn get_cap(node: &DrmRenderNode, capability: u64) -> io::Result<u64> {
|
||||
let mut arg = DrmGetCap {
|
||||
capability,
|
||||
value: 0,
|
||||
};
|
||||
let ret = unsafe { libc::ioctl(node.as_fd().as_raw_fd(), DRM_IOCTL_GET_CAP, &mut arg) };
|
||||
if ret < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(arg.value)
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_timeline(node: &DrmRenderNode) -> bool {
|
||||
[DRM_CAP_SYNCOBJ, DRM_CAP_SYNCOBJ_TIMELINE]
|
||||
.into_iter()
|
||||
.all(|cap| get_cap(node, cap).is_ok_and(|value| value != 0))
|
||||
}
|
||||
|
||||
pub fn open_render_node(render_major: u32, render_minor: u32) -> io::Result<DrmRenderNode> {
|
||||
if !(128..=255).contains(&render_minor) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
format!("Unsupported DRM render minor: {render_minor}"),
|
||||
));
|
||||
}
|
||||
|
||||
let path = format!("/dev/dri/renderD{render_minor}");
|
||||
let metadata = fs::metadata(&path)?;
|
||||
|
||||
// verify that device actually matches the provided minor/major
|
||||
if !metadata.file_type().is_char_device()
|
||||
|| metadata.rdev() != libc::makedev(render_major, render_minor)
|
||||
{
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("{path} does not match DRM device {render_major}:{render_minor}"),
|
||||
));
|
||||
}
|
||||
|
||||
let node = DrmRenderNode::new(render_minor.into())
|
||||
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))?;
|
||||
|
||||
if !supports_timeline(&node) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
format!("{path} does not support DRM syncobj timelines"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
pub(crate) fn import_timeline(node: &DrmRenderNode, fd: RawFd) -> io::Result<Arc<TimelineSyncObj>> {
|
||||
if fd < 0 {
|
||||
return Err(io::Error::from_raw_os_error(libc::EBADF));
|
||||
}
|
||||
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
|
||||
TimelineSyncObj::import(node, fd)
|
||||
.map(Arc::new)
|
||||
.map_err(|e| io::Error::from_raw_os_error(e.raw_os_error()))
|
||||
}
|
||||
@@ -11,7 +11,10 @@ use smithay_client_toolkit::reexports::{
|
||||
zxdg_output_manager_v1::ZxdgOutputManagerV1,
|
||||
zxdg_output_v1::{self, ZxdgOutputV1},
|
||||
},
|
||||
protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
|
||||
protocols_wlr::{
|
||||
export_dmabuf::v1::client::zwlr_export_dmabuf_manager_v1::ZwlrExportDmabufManagerV1,
|
||||
screencopy::v1::client::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
|
||||
},
|
||||
};
|
||||
|
||||
use wayland_client::{
|
||||
@@ -58,6 +61,7 @@ pub struct WlxOutput {
|
||||
pub struct WlxClient {
|
||||
pub connection: Arc<Connection>,
|
||||
pub xdg_output_mgr: ZxdgOutputManagerV1,
|
||||
pub maybe_wlr_dmabuf_mgr: Option<ZwlrExportDmabufManagerV1>,
|
||||
pub maybe_wlr_screencopy_mgr: Option<ZwlrScreencopyManagerV1>,
|
||||
pub maybe_zwp_linux_dmabuf: Option<ZwpLinuxDmabufV1>,
|
||||
pub wl_seat: WlSeat,
|
||||
@@ -89,6 +93,7 @@ impl WlxClient {
|
||||
.bind(&qh, 4..=9, ())
|
||||
.expect(WlSeat::interface().name),
|
||||
wl_shm: globals.bind(&qh, 1..=1, ()).expect(WlShm::interface().name),
|
||||
maybe_wlr_dmabuf_mgr: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
maybe_wlr_screencopy_mgr: globals.bind(&qh, 3..=3, ()).ok(),
|
||||
maybe_zwp_linux_dmabuf: globals.bind(&qh, 4..=4, ()).ok(),
|
||||
outputs: IdMap::new(),
|
||||
@@ -393,6 +398,18 @@ impl Dispatch<ZxdgOutputManagerV1, ()> for WlxClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrExportDmabufManagerV1, ()> for WlxClient {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
_proxy: &ZwlrExportDmabufManagerV1,
|
||||
_event: <ZwlrExportDmabufManagerV1 as Proxy>::Event,
|
||||
_data: &(),
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrScreencopyManagerV1, ()> for WlxClient {
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::VecDeque,
|
||||
os::fd::{FromRawFd, IntoRawFd, OwnedFd, RawFd},
|
||||
sync::mpsc,
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use drm_fourcc::{DrmFormat, DrmFourcc, DrmModifier};
|
||||
use smithay_client_toolkit::reexports::protocols_wlr::export_dmabuf::v1::client::zwlr_export_dmabuf_frame_v1::{self, ZwlrExportDmabufFrameV1};
|
||||
use wayland_client::{Connection, QueueHandle, Dispatch, Proxy};
|
||||
|
||||
use crate::{
|
||||
WlxCapture,
|
||||
frame::{DmabufFrame, FrameFormat, FramePlane, WlxFrame},
|
||||
wayland::WlxClient,
|
||||
};
|
||||
|
||||
use log::{debug, warn};
|
||||
|
||||
struct CaptureData<U, R>
|
||||
where
|
||||
U: Any,
|
||||
R: Any,
|
||||
{
|
||||
sender: mpsc::SyncSender<WlxFrame>,
|
||||
receiver: mpsc::Receiver<WlxFrame>,
|
||||
user_data: U,
|
||||
receive_callback: fn(&U, WlxFrame) -> Option<R>,
|
||||
}
|
||||
|
||||
pub struct WlrDmabufCapture<U, R>
|
||||
where
|
||||
U: Any + Send,
|
||||
R: Any + Send,
|
||||
{
|
||||
output_id: u32,
|
||||
wl: Option<Box<WlxClient>>,
|
||||
handle: Option<JoinHandle<Box<WlxClient>>>,
|
||||
data: Option<CaptureData<U, R>>,
|
||||
fds: VecDeque<RawFd>,
|
||||
}
|
||||
|
||||
impl<U, R> WlrDmabufCapture<U, R>
|
||||
where
|
||||
U: Any + Send,
|
||||
R: Any + Send,
|
||||
{
|
||||
pub fn new(wl: WlxClient, output_id: u32) -> Self {
|
||||
Self {
|
||||
output_id,
|
||||
wl: Some(Box::new(wl)),
|
||||
handle: None,
|
||||
data: None,
|
||||
fds: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, R> WlxCapture<U, R> for WlrDmabufCapture<U, R>
|
||||
where
|
||||
U: Any + Send,
|
||||
R: Any + Send,
|
||||
{
|
||||
fn init(
|
||||
&mut self,
|
||||
_: &[DrmFormat],
|
||||
user_data: U,
|
||||
receive_callback: fn(&U, WlxFrame) -> Option<R>,
|
||||
) {
|
||||
debug_assert!(self.wl.is_some());
|
||||
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel::<WlxFrame>(2);
|
||||
self.data = Some(CaptureData {
|
||||
sender,
|
||||
receiver,
|
||||
user_data,
|
||||
receive_callback,
|
||||
});
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.data.is_some()
|
||||
}
|
||||
fn supports_dmbuf(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn receive(&mut self) -> Option<R> {
|
||||
if let Some(data) = self.data.as_ref()
|
||||
&& let Some(WlxFrame::Dmabuf(last)) = data.receiver.try_iter().last()
|
||||
{
|
||||
// this is the only protocol that requires us to manually close the FD
|
||||
while self.fds.len() > 6 * last.num_planes {
|
||||
// safe unwrap
|
||||
let _ = unsafe { OwnedFd::from_raw_fd(self.fds.pop_back().unwrap()) };
|
||||
}
|
||||
for p in 0..last.num_planes {
|
||||
if let Some(fd) = last.planes[p].fd {
|
||||
self.fds.push_front(fd);
|
||||
}
|
||||
}
|
||||
return (data.receive_callback)(&data.user_data, WlxFrame::Dmabuf(last));
|
||||
}
|
||||
None
|
||||
}
|
||||
fn pause(&mut self) {}
|
||||
fn resume(&mut self) {
|
||||
self.receive(); // clear old frames
|
||||
}
|
||||
fn request_new_frame(&mut self) {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
if handle.is_finished() {
|
||||
self.wl = Some(handle.join().unwrap()); // safe to unwrap because we checked is_finished
|
||||
} else {
|
||||
self.handle = Some(handle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(wl) = self.wl.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.handle = Some(std::thread::spawn({
|
||||
let sender = self
|
||||
.data
|
||||
.as_ref()
|
||||
.expect("must call init once before request_new_frame")
|
||||
.sender
|
||||
.clone();
|
||||
let output_id = self.output_id;
|
||||
move || request_dmabuf_frame(wl, output_id, sender)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Request a new DMA-Buf frame using the wlr-export-dmabuf protocol.
|
||||
fn request_dmabuf_frame(
|
||||
client: Box<WlxClient>,
|
||||
output_id: u32,
|
||||
sender: mpsc::SyncSender<WlxFrame>,
|
||||
) -> Box<WlxClient> {
|
||||
let Some(dmabuf_manager) = client.maybe_wlr_dmabuf_mgr.as_ref() else {
|
||||
return client;
|
||||
};
|
||||
|
||||
let Some(output) = client.outputs.get(output_id) else {
|
||||
return client;
|
||||
};
|
||||
|
||||
let transform = output.transform;
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel::<zwlr_export_dmabuf_frame_v1::Event>(16);
|
||||
let name = output.name.clone();
|
||||
|
||||
let _ = dmabuf_manager.capture_output(1, &output.wl_output, &client.queue_handle, tx.clone());
|
||||
|
||||
let mut client = client;
|
||||
client.dispatch();
|
||||
|
||||
let mut frame = None;
|
||||
|
||||
rx.try_iter().for_each(|event| match event {
|
||||
zwlr_export_dmabuf_frame_v1::Event::Frame {
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
mod_high,
|
||||
mod_low,
|
||||
num_objects,
|
||||
..
|
||||
} => {
|
||||
let mut new_frame = DmabufFrame {
|
||||
format: FrameFormat {
|
||||
width,
|
||||
height,
|
||||
drm_format: DrmFormat {
|
||||
code: match DrmFourcc::try_from(format) {
|
||||
Ok(code) => code,
|
||||
Err(e) => {
|
||||
log::error!("Unrecognized fourcc: {e:?}");
|
||||
return;
|
||||
}
|
||||
},
|
||||
modifier: DrmModifier::Invalid,
|
||||
},
|
||||
transform,
|
||||
},
|
||||
mouse: None,
|
||||
num_planes: num_objects as _,
|
||||
planes: Default::default(),
|
||||
};
|
||||
new_frame.format.set_mod(mod_high, mod_low);
|
||||
frame = Some(new_frame);
|
||||
}
|
||||
zwlr_export_dmabuf_frame_v1::Event::Object {
|
||||
index,
|
||||
fd,
|
||||
offset,
|
||||
stride,
|
||||
..
|
||||
} => {
|
||||
let Some(ref mut frame) = frame else {
|
||||
return;
|
||||
};
|
||||
frame.planes[index as usize] = FramePlane {
|
||||
fd: Some(fd.into_raw_fd()),
|
||||
offset,
|
||||
stride: stride as _,
|
||||
};
|
||||
}
|
||||
zwlr_export_dmabuf_frame_v1::Event::Ready { .. } => {
|
||||
let Some(frame) = frame.take() else {
|
||||
return;
|
||||
};
|
||||
debug!("DMA-Buf frame captured");
|
||||
let frame = WlxFrame::Dmabuf(frame);
|
||||
match sender.try_send(frame) {
|
||||
Ok(_) => (),
|
||||
Err(mpsc::TrySendError::Full(_)) => (),
|
||||
Err(mpsc::TrySendError::Disconnected(_)) => {
|
||||
log::warn!("{}: disconnected", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
zwlr_export_dmabuf_frame_v1::Event::Cancel { .. } => {
|
||||
warn!("DMA-Buf frame capture cancelled");
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrExportDmabufFrameV1, mpsc::SyncSender<zwlr_export_dmabuf_frame_v1::Event>>
|
||||
for WlxClient
|
||||
{
|
||||
fn event(
|
||||
_state: &mut Self,
|
||||
proxy: &ZwlrExportDmabufFrameV1,
|
||||
event: <ZwlrExportDmabufFrameV1 as Proxy>::Event,
|
||||
data: &mpsc::SyncSender<zwlr_export_dmabuf_frame_v1::Event>,
|
||||
_conn: &Connection,
|
||||
_qhandle: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
zwlr_export_dmabuf_frame_v1::Event::Ready { .. }
|
||||
| zwlr_export_dmabuf_frame_v1::Event::Cancel { .. } => {
|
||||
proxy.destroy();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = data.send(event).or_else(|err| {
|
||||
warn!("Failed to send DMA-Buf frame event: {}", err);
|
||||
Ok::<(), mpsc::SendError<zwlr_export_dmabuf_frame_v1::Event>>(())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -262,7 +262,6 @@ where
|
||||
&client.queue_handle,
|
||||
(),
|
||||
);
|
||||
params.destroy();
|
||||
|
||||
log::trace!("{name}: ScreenCopy with Dmabuf");
|
||||
// copy_with_damage seems to not work here
|
||||
|
||||
@@ -109,8 +109,6 @@ where
|
||||
transform: Transform::Normal,
|
||||
},
|
||||
ptr: unsafe { image.as_ptr() as _ },
|
||||
offset: 0,
|
||||
stride: image.bytes_per_line() as _,
|
||||
size,
|
||||
mouse: d.root_mouse_position().and_then(|root_pos| {
|
||||
monitor.mouse_to_local(root_pos).map(|(x, y)| MouseMeta {
|
||||
|
||||
@@ -24,7 +24,6 @@ smol.workspace = true
|
||||
strum.workspace = true
|
||||
xdg.workspace = true
|
||||
|
||||
freedesktop-desktop-entry = "0.8.4"
|
||||
glob = "0.3.3"
|
||||
identicons-svg = "0.1.0"
|
||||
rodio = {
|
||||
@@ -32,5 +31,6 @@ rodio = {
|
||||
default-features = false,
|
||||
features = ["hound", "mp3", "playback", "recording"]
|
||||
}
|
||||
rust-ini = "0.21.3"
|
||||
walkdir = "2.5.0"
|
||||
wgui = { path = "../wgui/" }
|
||||
@@ -533,9 +533,6 @@ pub struct GeneralConfig {
|
||||
#[serde(default)]
|
||||
pub whisper_model: Arc<str>,
|
||||
|
||||
#[serde(default)]
|
||||
pub whisper_gpu: u32,
|
||||
|
||||
#[serde(default = "def_default")]
|
||||
pub color_palette: Arc<str>,
|
||||
|
||||
|
||||
@@ -45,11 +45,6 @@ pub enum DashPlayspaceTask {
|
||||
ResetCenter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum DashBackendTask {
|
||||
ShowBindingsUI,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct InterfaceFeats {
|
||||
pub xr_backend: XrBackend,
|
||||
@@ -97,8 +92,7 @@ pub trait DashInterface<T> {
|
||||
fn monado_brightness_set(&mut self, data: &mut T, brightness: f32) -> Option<()>;
|
||||
fn monado_metrics_set_enabled(&mut self, data: &mut T, enabled: bool) -> bool;
|
||||
fn monado_metrics_dump_session_frames(&mut self, data: &mut T) -> Vec<MonadoDumpSessionFrame>;
|
||||
fn playspace_task(&mut self, data: &mut T, task: DashPlayspaceTask) -> anyhow::Result<()>;
|
||||
fn backend_task(&mut self, data: &mut T, task: DashBackendTask) -> anyhow::Result<()>;
|
||||
fn playspace_task(&mut self, data: &mut T, mode: DashPlayspaceTask) -> anyhow::Result<()>;
|
||||
fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder;
|
||||
fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig;
|
||||
fn config_changed(&mut self, data: &mut T, kind: ConfigChangeKind);
|
||||
@@ -107,7 +101,6 @@ pub trait DashInterface<T> {
|
||||
fn toggle_dashboard(&mut self, data: &mut T);
|
||||
fn get_feats(&mut self, data: &mut T) -> InterfaceFeats;
|
||||
fn hmd_stats(&mut self, data: &mut T) -> HmdStats;
|
||||
fn enumerate_vulkan_devices(&mut self, data: &mut T) -> anyhow::Result<Vec<String>>;
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use slotmap::{DenseSlotMap, new_key_type};
|
||||
use wayvr_ipc::{
|
||||
packet_client::WvrProcessLaunchParams,
|
||||
packet_server::{WvrProcess, WvrProcessHandle, WvrWindow, WvrWindowHandle},
|
||||
};
|
||||
use wgui::{gfx::WGfx, i18n::Locale};
|
||||
|
||||
use crate::{
|
||||
config::GeneralConfig,
|
||||
dash_interface::{self, ConfigChangeKind, DashBackendTask, DashInterface, DashPlayspaceTask},
|
||||
dash_interface::{self, ConfigChangeKind, DashInterface, DashPlayspaceTask},
|
||||
desktop_finder::DesktopFinder,
|
||||
};
|
||||
|
||||
@@ -69,11 +66,10 @@ pub struct DashInterfaceEmulated {
|
||||
brightness: f32,
|
||||
debug_frame_id: u64,
|
||||
debug_counter: u64,
|
||||
gfx: Arc<WGfx>,
|
||||
}
|
||||
|
||||
impl DashInterfaceEmulated {
|
||||
pub fn new(gfx: Arc<WGfx>, locale: &Locale) -> Self {
|
||||
pub fn new() -> Self {
|
||||
let mut processes = DenseSlotMap::<EmuProcessHandle, EmuProcess>::default();
|
||||
let process_handle = processes.insert(EmuProcess {
|
||||
name: String::from("My app"),
|
||||
@@ -85,7 +81,7 @@ impl DashInterfaceEmulated {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
let mut desktop_finder = DesktopFinder::new(locale);
|
||||
let mut desktop_finder = DesktopFinder::new();
|
||||
desktop_finder.refresh();
|
||||
|
||||
// Use serde defaults
|
||||
@@ -133,16 +129,17 @@ impl DashInterfaceEmulated {
|
||||
brightness: 1.0,
|
||||
debug_counter: 0,
|
||||
debug_frame_id: 0,
|
||||
gfx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DashInterface<()> for DashInterfaceEmulated {
|
||||
fn enumerate_vulkan_devices(&mut self, _: &mut ()) -> anyhow::Result<Vec<String>> {
|
||||
self.gfx.enumerate_devices()
|
||||
impl Default for DashInterfaceEmulated {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DashInterface<()> for DashInterfaceEmulated {
|
||||
fn window_list(&mut self, _: &mut ()) -> anyhow::Result<Vec<WvrWindow>> {
|
||||
Ok(self.windows.iter().map(|(handle, w)| w.to(handle)).collect())
|
||||
}
|
||||
@@ -222,11 +219,6 @@ impl DashInterface<()> for DashInterfaceEmulated {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backend_task(&mut self, _: &mut (), _: DashBackendTask) -> anyhow::Result<()> {
|
||||
// stub!
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn desktop_finder<'a>(&'a mut self, _: &'a mut ()) -> &'a mut DesktopFinder {
|
||||
&mut self.desktop_finder
|
||||
}
|
||||
|
||||
@@ -10,35 +10,37 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::{cache_dir, locale::Language};
|
||||
use crate::cache_dir;
|
||||
use anyhow::Context;
|
||||
use freedesktop_desktop_entry::DesktopEntry as FreedesktopDesktopEntry;
|
||||
use ini::Ini;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use walkdir::WalkDir;
|
||||
use wgui::i18n::Locale;
|
||||
|
||||
struct DesktopEntryOwned {
|
||||
app_id: Box<str>,
|
||||
exec_argv: Box<[Box<str>]>,
|
||||
app_name: Box<str>,
|
||||
icon_path: Option<Box<str>>,
|
||||
categories: Box<[Box<str>]>,
|
||||
app_id: String,
|
||||
exec_path: String,
|
||||
exec_args: String,
|
||||
app_name: String,
|
||||
icon_path: Option<String>,
|
||||
categories: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DesktopEntry {
|
||||
pub exec_argv: Rc<[Rc<str>]>,
|
||||
pub exec_path: Rc<str>,
|
||||
pub exec_args: Rc<str>,
|
||||
pub app_id: Rc<str>,
|
||||
pub app_name: Rc<str>,
|
||||
pub icon_path: Option<Rc<str>>,
|
||||
pub categories: Rc<[Rc<str>]>,
|
||||
pub categories: Vec<Rc<str>>,
|
||||
}
|
||||
|
||||
impl From<DesktopEntryOwned> for DesktopEntry {
|
||||
fn from(value: DesktopEntryOwned) -> Self {
|
||||
Self {
|
||||
app_id: value.app_id.into(),
|
||||
exec_argv: value.exec_argv.into_iter().map(|x| x.into()).collect(),
|
||||
exec_path: value.exec_path.into(),
|
||||
exec_args: value.exec_args.into(),
|
||||
app_name: value.app_name.into(),
|
||||
icon_path: value.icon_path.map(|x| x.into()),
|
||||
categories: value.categories.into_iter().map(|x| x.into()).collect(),
|
||||
@@ -58,18 +60,17 @@ struct DesktopFinderParams {
|
||||
size_preferences: Vec<&'static OsStr>,
|
||||
icon_folders: Vec<String>,
|
||||
app_folders: Vec<String>,
|
||||
langs: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct DesktopFinder {
|
||||
params: Arc<DesktopFinderParams>,
|
||||
entry_cache: HashMap<String, Rc<DesktopEntry>>,
|
||||
entry_cache: HashMap<String, DesktopEntry>,
|
||||
bg_task: Option<JoinHandle<HashMap<String, DesktopEntryOwned>>>,
|
||||
}
|
||||
|
||||
impl DesktopFinder {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new(locale: &Locale) -> Self {
|
||||
pub fn new() -> Self {
|
||||
let xdg = xdg::BaseDirectories::new();
|
||||
|
||||
let mut app_folders = vec![];
|
||||
@@ -106,21 +107,11 @@ impl DesktopFinder {
|
||||
.map(OsStr::new)
|
||||
.collect();
|
||||
|
||||
let langs = {
|
||||
let main_lang = locale.without_region().to_string();
|
||||
if main_lang == Language::get_default().code() {
|
||||
vec![main_lang]
|
||||
} else {
|
||||
vec![main_lang, Language::get_default().code().to_string()]
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
params: Arc::new(DesktopFinderParams {
|
||||
app_folders,
|
||||
icon_folders,
|
||||
size_preferences,
|
||||
langs,
|
||||
}),
|
||||
entry_cache: HashMap::new(),
|
||||
bg_task: None,
|
||||
@@ -155,53 +146,79 @@ impl DesktopFinder {
|
||||
}
|
||||
|
||||
let file_name = entry.file_name().to_string_lossy();
|
||||
let file_path = entry.path().display().to_string();
|
||||
let Some(app_id) = Path::new(entry.file_name())
|
||||
.file_stem()
|
||||
.map(|x| x.to_string_lossy().to_string())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let desktop = match FreedesktopDesktopEntry::from_path(entry.path(), Some(¶ms.langs)) {
|
||||
Ok(desktop) => desktop,
|
||||
if known_files.contains(file_name.as_ref()) {
|
||||
// as per xdg spec, user entries of the same filename will override system entries
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_path = format!("{}/{}", path, file_name);
|
||||
|
||||
let ini = match Ini::load_from_file(&file_path) {
|
||||
Ok(ini) => ini,
|
||||
Err(e) => {
|
||||
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
|
||||
log::debug!("Failed to read INI for .desktop file {}: {:?}, skipping", file_path, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if desktop.groups.desktop_entry().is_none() {
|
||||
let Some(section) = ini.section(Some("Desktop Entry")) else {
|
||||
log::debug!("Failed to get [Desktop Entry] section for file {}, skipping", file_path);
|
||||
continue;
|
||||
};
|
||||
|
||||
if section.contains_key("OnlyShowIn") {
|
||||
continue; // probably XFCE, KDE, GNOME or other DE-specific stuff
|
||||
}
|
||||
|
||||
// prefer entries from earlier data directories
|
||||
let app_id = desktop.id();
|
||||
if known_files.contains(app_id) {
|
||||
if let Some(x) = section.get("Terminal")
|
||||
&& x == "true"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
|
||||
continue;
|
||||
if let Some(x) = section.get("NoDisplay")
|
||||
&& x.eq_ignore_ascii_case("true")
|
||||
{
|
||||
continue; // This application is hidden
|
||||
}
|
||||
|
||||
let Some(exec) = desktop.exec() else {
|
||||
if let Some(x) = section.get("Hidden")
|
||||
&& x.eq_ignore_ascii_case("true")
|
||||
{
|
||||
continue; // This application is hidden
|
||||
}
|
||||
|
||||
let Some(exec) = section.get("Exec") else {
|
||||
log::debug!("Failed to get desktop entry Exec for file {}, skipping", file_path);
|
||||
continue;
|
||||
};
|
||||
|
||||
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
|
||||
continue;
|
||||
for entry in &CMD_BLOCKLIST {
|
||||
if exec.contains(entry) {
|
||||
continue 'entries;
|
||||
}
|
||||
}
|
||||
|
||||
let exec_argv = desktop
|
||||
.parse_exec_with_uris(&[], ¶ms.langs)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|x| x.into_boxed_str())
|
||||
.collect::<Box<[_]>>();
|
||||
|
||||
if exec_argv.is_empty() {
|
||||
log::debug!("Invalid Exec in desktop file {}, skipping", file_path);
|
||||
continue;
|
||||
let (exec_path, exec_args) = match exec.split_once(" ") {
|
||||
Some((left, right)) => (
|
||||
left,
|
||||
right
|
||||
.split(" ")
|
||||
.filter(|arg| !arg.starts_with('%')) // exclude arguments like "%f"
|
||||
.map(String::from)
|
||||
.collect(),
|
||||
),
|
||||
None => (exec, Vec::new()),
|
||||
};
|
||||
|
||||
let Some(app_name) = desktop.name(¶ms.langs) else {
|
||||
let Some(app_name) = section.get("Name") else {
|
||||
log::debug!(
|
||||
"Failed to get desktop entry application name for file {}, skipping",
|
||||
file_path
|
||||
@@ -209,31 +226,33 @@ impl DesktopFinder {
|
||||
continue;
|
||||
};
|
||||
|
||||
let icon_path = desktop
|
||||
.icon()
|
||||
let icon_path = section
|
||||
.get("Icon")
|
||||
.and_then(|icon_name| Self::find_icon(¶ms, icon_name))
|
||||
.or_else(|| Self::create_icon(&file_name).ok());
|
||||
|
||||
let mut vec_categories = Vec::<Box<str>>::new();
|
||||
for cat in desktop.categories().unwrap_or_default() {
|
||||
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
|
||||
continue 'entries;
|
||||
}
|
||||
if !cat.is_empty() {
|
||||
vec_categories.push(cat.into());
|
||||
let mut vec_categories = Vec::<String>::new();
|
||||
|
||||
if let Some(categories) = section.get("Categories") {
|
||||
for cat in categories.split(";") {
|
||||
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
|
||||
continue 'entries;
|
||||
}
|
||||
vec_categories.push(String::from(cat));
|
||||
}
|
||||
}
|
||||
|
||||
known_files.insert(app_id.to_owned());
|
||||
known_files.insert(file_name.to_string());
|
||||
|
||||
entries.insert(
|
||||
app_id.to_owned(),
|
||||
app_id.clone(),
|
||||
DesktopEntryOwned {
|
||||
app_id: app_id.into(),
|
||||
app_name: app_name.into(),
|
||||
exec_argv,
|
||||
icon_path: icon_path.map(|i| i.into_boxed_str()),
|
||||
categories: vec_categories.into_boxed_slice(),
|
||||
app_id,
|
||||
app_name: String::from(app_name),
|
||||
exec_path: String::from(exec_path),
|
||||
exec_args: exec_args.join(" "),
|
||||
icon_path,
|
||||
categories: vec_categories,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -312,15 +331,15 @@ impl DesktopFinder {
|
||||
|
||||
self.entry_cache.clear();
|
||||
for (app_id, entry) in entries {
|
||||
self.entry_cache.insert(app_id, Rc::new(entry.into()));
|
||||
self.entry_cache.insert(app_id, entry.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cached_entry(&self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
|
||||
pub fn get_cached_entry(&self, app_id: &str) -> Option<&DesktopEntry> {
|
||||
self.entry_cache.get(app_id)
|
||||
}
|
||||
|
||||
pub fn find_entries(&mut self) -> HashMap<String, Rc<DesktopEntry>> {
|
||||
pub fn find_entries(&mut self) -> HashMap<String, DesktopEntry> {
|
||||
self.wait_for_entries();
|
||||
self.entry_cache.clone()
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ pub enum Language {
|
||||
ChineseSimplified,
|
||||
#[strum(props(Text = "Español"))]
|
||||
Spanish,
|
||||
#[strum(props(Text = "Svenska"))]
|
||||
Swedish,
|
||||
}
|
||||
|
||||
impl Language {
|
||||
@@ -38,7 +36,6 @@ impl Language {
|
||||
Language::Italian => "it_IT",
|
||||
Language::ChineseSimplified => "zh_CN",
|
||||
Language::Spanish => "es_ES",
|
||||
Language::Swedish => "sv_SE",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +44,7 @@ impl Language {
|
||||
}
|
||||
|
||||
pub const fn all_codes() -> &'static [&'static str] {
|
||||
&["en", "pl", "ja", "de", "it", "zh_CN", "es", "sv"]
|
||||
&["en", "pl", "ja", "de", "it", "zh_CN", "es"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,112 +92,6 @@ pub struct OpenXrInputProfile {
|
||||
pub haptic: Option<OpenXrInputAction>,
|
||||
}
|
||||
|
||||
fn strip_axis_component(path: &str) -> String {
|
||||
if let Some((prefix, suffix)) = path.rsplit_once('/')
|
||||
&& (suffix == "x" || suffix == "y")
|
||||
&& (prefix.ends_with("/thumbstick") || prefix.ends_with("/trackpad") || prefix.ends_with("/joystick"))
|
||||
{
|
||||
return prefix.to_owned();
|
||||
}
|
||||
path.to_owned()
|
||||
}
|
||||
|
||||
// TODO: remove this after thumbstick issue is not a thing anymore
|
||||
fn normalize_profile(profile: &mut OpenXrInputProfile) {
|
||||
if let Some(action) = profile.pose.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.click.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.grab.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.alt_click.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.show_hide.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.toggle_dashboard.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.space_drag.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.space_rotate.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.space_reset.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.click_modifier_right.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.click_modifier_middle.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.move_mouse.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.scroll.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
if let Some(action) = profile.haptic.as_mut() {
|
||||
normalize_action(action);
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_action(action: &mut OpenXrInputAction) {
|
||||
let process_one_or_many = |opt: &mut Option<OneOrMany<String>>| {
|
||||
if let Some(paths) = opt {
|
||||
match paths {
|
||||
OneOrMany::One(p) => {
|
||||
let normalized = strip_axis_component(p);
|
||||
if normalized != *p {
|
||||
*p = normalized;
|
||||
}
|
||||
}
|
||||
OneOrMany::Many(ps) => {
|
||||
for p in ps.iter_mut() {
|
||||
let normalized = strip_axis_component(p);
|
||||
if normalized != *p {
|
||||
*p = normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
process_one_or_many(&mut action.left);
|
||||
process_one_or_many(&mut action.right);
|
||||
process_one_or_many(&mut action.handsfree);
|
||||
|
||||
let process_chord = |members: &mut Option<Vec<OpenXrInputChordMember>>| {
|
||||
if let Some(m) = members {
|
||||
for member in m.iter_mut() {
|
||||
let path = member.path().to_owned();
|
||||
let normalized = strip_axis_component(&path);
|
||||
if normalized != path {
|
||||
let threshold = member.threshold();
|
||||
*member = if threshold.is_some() {
|
||||
OpenXrInputChordMember::Detailed {
|
||||
path: normalized,
|
||||
threshold,
|
||||
}
|
||||
} else {
|
||||
OpenXrInputChordMember::Path(normalized)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
process_chord(&mut action.left_chord);
|
||||
process_chord(&mut action.right_chord);
|
||||
}
|
||||
|
||||
pub fn load_xr_input_profiles() -> Vec<OpenXrInputProfile> {
|
||||
let mut profiles: Vec<OpenXrInputProfile> = serde_json5::from_str(DEFAULT_XR_INPUT_PROFILES).unwrap(); // want panic
|
||||
|
||||
@@ -206,10 +100,7 @@ pub fn load_xr_input_profiles() -> Vec<OpenXrInputProfile> {
|
||||
};
|
||||
|
||||
match serde_json5::from_str::<Vec<OpenXrInputProfile>>(&conf) {
|
||||
Ok(mut override_profiles) => {
|
||||
for new in override_profiles.iter_mut() {
|
||||
normalize_profile(new);
|
||||
}
|
||||
Ok(override_profiles) => {
|
||||
for new in override_profiles {
|
||||
if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) {
|
||||
profiles[i] = new;
|
||||
|
||||
Reference in new issue
Block a user