Compare commits

...
Author SHA1 Message Date
galister db777d7937 XSHM & PipeWire CPU capture fixes 2026-10-23 21:11:33 +09:00
galister f94c0c4ace desktop_finder rework (#697)
* desktop_finder rework

* fix args for wayvrctl process-launch
2026-10-23 19:29:25 +09:00
Aleksander 5f4c97794c wayvr: prevent alpha from going NaN again 2026-10-22 18:04:40 +02:00
Aleksander d9288a877c dash-frontend: refactoring: deduplicate dropdown code 2026-10-21 19:57:14 +02:00
galister dff4b0067a thumbstick axes fix 2026-10-21 14:29:48 +09:00
oo8dev adf852971f dash-frontend: whisper GPU device selector (Closes #680) (#692) 2026-10-18 17:19:10 +02:00
Aleksander 074d2de7aa wgui: bugfix: layout is recomputed every frame (tick/render loop) (Closes #690) 2026-10-18 12:37:49 +02:00
diffy e8d9a35714 openxr: pivot unlocked space rotate on the hand (#689)
* openxr: pivot unlocked space rotate on the hand

* openxr: don't start space drag during space rotate
2026-10-18 16:53:05 +09:00
diffy 36f9fab5c6 apply ui_gradient_intensity to theme (#688) 2026-10-17 13:16:03 +09:00
diffy 058792fc8e wlx-capture: destroy dmabuf params after create_immed (#687) 2026-10-17 13:15:48 +09:00
Dandrid ef934e0b02 chore: Swap away from using deprecated associated constants (#686) 2026-10-17 13:05:56 +09:00
galister c42e992a62 support both systemctl and loginctl 2026-10-15 13:20:31 +09:00
galister 41d7a0b3d2 try fix appimage with multi-desktop files 2026-10-14 13:22:25 +09:00
galister 471e4be946 show toast when starting hidden 2026-10-14 13:02:22 +09:00
galister 26afa57fe3 include headless desktop entry 2026-10-14 12:51:33 +09:00
galister d120617abe openvr: hide set & dash when showing bindings 2026-10-14 12:39:49 +09:00
galister 351b24533c openvr: button to open bindings 2026-10-14 12:09:52 +09:00
galister 09ed96542f openvr: fix space drag interrupting itself 2026-10-14 11:44:36 +09:00
oo8devandgalister 9b89622ff9 Power options, context menu icons (#681)
* dash-frontend: Power options (Closes #640), Context menu: refactor: positioning

* dash-frontend: Power options

* systemctl → loginctl

Co-authored-by: galister <22305755+galister@users.noreply.github.com>

---------

Co-authored-by: galister <22305755+galister@users.noreply.github.com>
2026-10-13 14:06:47 +09:00
83 changed files with 1584 additions and 648 deletions

No files matched your search

@@ -21,6 +21,9 @@ 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
+118 -3
View File
@@ -689,6 +689,12 @@ dependencies = [
"wyz",
]
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -729,6 +735,17 @@ dependencies = [
"piper",
]
[[package]]
name = "bstr"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
dependencies = [
"memchr",
"regex-automata",
"serde_core",
]
[[package]]
name = "built"
version = "0.8.1"
@@ -1401,6 +1418,7 @@ dependencies = [
"rust-embed",
"serde",
"serde_json",
"shell-words",
"smol",
"strum",
"ureq",
@@ -1921,6 +1939,21 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "freedesktop-desktop-entry"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a69ad6111a2b572cb41e0f3542d546386e841013cee7c45d705b61a15a87dd"
dependencies = [
"bstr",
"gettext-rs",
"log",
"memchr",
"thiserror 2.0.18",
"unicase",
"xdg 3.0.0",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
@@ -2100,6 +2133,26 @@ dependencies = [
"rand_core 0.10.1",
]
[[package]]
name = "gettext-rs"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df5e3fa8c077b15fdfa8ce130e2bc73d663b8aaa6d51b71421b8659980b1d80"
dependencies = [
"gettext-sys",
"locale_config",
]
[[package]]
name = "gettext-sys"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4caa7e4460b196107cc1bb2629488e8b6dbb7f34f6683ea6753c4ac22f590785"
dependencies = [
"cc",
"temp-dir",
]
[[package]]
name = "gif"
version = "0.14.2"
@@ -2869,6 +2922,19 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "locale_config"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934"
dependencies = [
"lazy_static",
"objc",
"objc-foundation",
"regex",
"winapi",
]
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -2930,6 +2996,15 @@ dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "matchers"
version = "0.2.0"
@@ -3221,6 +3296,26 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -3528,6 +3623,15 @@ dependencies = [
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.37.3"
@@ -4669,8 +4773,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rxscreen"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbab13f83503a8272b7150d487494992cf6c59c299dcaf5d8ffac75bad9fc96"
source = "git+https://github.com/galister/rxscreen.git?rev=7197e1de6622b65859e6f2060b5750185e0dad2a#7197e1de6622b65859e6f2060b5750185e0dad2a"
dependencies = [
"libc",
]
@@ -5343,6 +5446,12 @@ version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "temp-dir"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964"
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -5693,6 +5802,12 @@ dependencies = [
"wlx-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
@@ -6739,6 +6854,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"freedesktop-desktop-entry",
"glam",
"glob",
"identicons-svg",
@@ -6746,7 +6862,6 @@ dependencies = [
"idmap-derive",
"log",
"rodio",
"rust-ini",
"serde",
"serde_json",
"serde_json5",
+1 -1
View File
@@ -32,6 +32,7 @@ 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"
@@ -44,7 +45,6 @@ wayland-protocols-misc = { version = "0.3.12" }
winit = "0.30.12"
xdg = "3.0.0"
[profile.dev]
opt-level = 1
debug = true
+1
View File
@@ -19,6 +19,7 @@ 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
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs id="defs1" />
<path fill="currentColor" d="m 10,12 c 0,-1.104569 0.895431,-2 2,-2 1.104569,0 2,0.895431 2,2 0,1.104569 -0.895431,2 -2,2 -1.104569,0 -2,-0.895431 -2,-2" id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 422 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 585 B

+13
View File
@@ -6,6 +6,16 @@
<var key="side_button_size" value="48" />
</vars>
<blueprint name="context_menu_power_options">
<context_menu>
<cell translation="APP_SETTINGS.RESTART_WAYVR" action="restart" sprite_src_builtin="@/dashboard/refresh.svg"/>
<cell translation="APP_SETTINGS.CLOSE_WAYVR" action="close" sprite_src_builtin="@/dashboard/close.svg"/>
<separator/>
<cell translation="POWER_OPTIONS.SUSPEND" action="suspend" sprite_src_builtin="@/dashboard/sleep.svg"/>
<cell translation="POWER_OPTIONS.SHUTDOWN" action="shutdown" sprite_src_builtin="@/dashboard/power.svg"/>
</context_menu>
</blueprint>
<template name="SideButton">
<Button
id="${id}"
@@ -136,6 +146,9 @@
<!-- Left bottom side -->
<div margin_left="8">
<Button id="btn_power" color="background(transparent)" border_color="background(transparent)" tooltip="POWER_OPTIONS.TITLE" tooltip_side="top">
<sprite src_builtin="@/dashboard/power.svg" width="24" height="24" margin="8" />
</Button>
<Button id="btn_audio" color="background(transparent)" border_color="background(transparent)" tooltip="AUDIO.VOLUME" tooltip_side="top">
<sprite src_builtin="@/dashboard/volume.svg" width="24" height="24" margin="8" />
</Button>
+4 -1
View File
@@ -1,6 +1,9 @@
<layout version="2">
<template name="DialogBoxButton">
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"/>
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"
text_size="16"
padding_left="16" padding_right="16"
padding_top="8" padding_bottom="8"/>
</template>
<elements>
+16 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Nutze aktuellen Playspace für die Playspace-Reset-Belegung.",
"RESET_PLAYSPACE_CENTER": "Playspace-Zentrum zurücksetzen",
"RESET_PLAYSPACE_CENTER_HELP": "Vorheriges Playspace-Zentrum zurücksetzen.",
"RESTART_SOFTWARE": "Software neu starten",
"RESTART_SOFTWARE_HELP": "Einstellungen übernehmen, die einen Neustart erfordern",
"ROUND_MULTIPLIER": "Abrundung der UI-Kanten",
"SCREEN_RENDER_DOWN": "Bildschirm mit niedrigerer Auflösung rendern",
"SCREEN_RENDER_DOWN_HELP": "Hilft gegen Treppeneffekte bei hochauflösenden Bildschirmen",
@@ -263,7 +261,11 @@
"TITLE": "Diagnose",
"ENABLE_REBAR": "Aktiviere resizable bar in den BIOS-Einstellungen für optimale VR-Performance"
},
"TUTORIAL": "Tutorial"
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "WayVR neu starten",
"CLOSE_WAYVR": "WayVR schließen",
"SPEECH_TO_TEXT": "Sprache zu Text",
"WHISPER_GPU_SELECT": "GPU auswählen"
},
"APPLICATION_LAUNCHER": "Programmstarter",
"APPLICATION_STARTED": "Anwendung gestartet",
@@ -368,5 +370,15 @@
"REMOVE_FROM_FAVOURITES": "Aus angehefteten Anwendungen entfernen",
"EMPTY": "Leer",
"INSTALLED": "Installiert",
"NOT_INSTALLED": "Nicht installiert"
"NOT_INSTALLED": "Nicht installiert",
"POWER_OPTIONS": {
"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?"
},
"YES": "Ja",
"NO": "Nein"
}
+12 -2
View File
@@ -208,8 +208,8 @@ APP_SETTINGS.SAVE_PLAYSPACE_CENTER,Save playspace center,"Button in Settings > S
APP_SETTINGS.SAVE_PLAYSPACE_CENTER_HELP,Use current playspace for reset playspace binding.,"Tooltip for the Save playspace center button. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESET_PLAYSPACE_CENTER,Reset playspace center,"Button in Settings > Space Drag tab. Clears the previously saved playspace center. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESET_PLAYSPACE_CENTER_HELP,Clear previously set playspace center.,"Tooltip for the Reset playspace center button. ""Playspace"" was previously translated as ""${DEFINITIONS.PLAYSPACE}""."
APP_SETTINGS.RESTART_SOFTWARE,Restart software,Button in Settings > Troubleshooting tab. Restarts the WayVR software to apply settings that require a restart.
APP_SETTINGS.RESTART_SOFTWARE_HELP,Apply settings that require a restart,Tooltip for the Restart software button.
APP_SETTINGS.RESTART_WAYVR,Restart WayVR,Button in Settings which restarts WayVR
APP_SETTINGS.CLOSE_WAYVR,Close WayVR,Button in Settings which closes WayVR
APP_SETTINGS.ROUND_MULTIPLIER,UI Edge roundness,Slider in Settings > Look & Feel tab. Controls how rounded the corners of UI elements appear (range 0.1-5.0).
APP_SETTINGS.SCREEN_RENDER_DOWN,Render screen at lower resolution,Checkbox in Settings > Miscellaneous tab. Renders the captured desktop screen at a lower resolution to improve performance or reduce aliasing.
APP_SETTINGS.SCREEN_RENDER_DOWN_HELP,Helps with aliasing on high-res screens,Tooltip for the lower-resolution screen render setting.
@@ -259,6 +259,8 @@ 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}""."
@@ -336,6 +338,12 @@ MODIFICATION_DATE,Modification date,Column label in the remote skymap downloader
MONADO_RUNTIME,Monado runtime,Tab label in the dashboard sidebar for the Monado runtime tab.
LOADING,Loading...,Label in the loading spinner template used across the UI.
POPUP_ADD_DISPLAY.RESOLUTION,Resolution,Label for the resolution field in the add display popup.
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.
@@ -357,3 +365,5 @@ REMOVE_FROM_FAVOURITES,Remove from pinned apps,"Label on the unpin section in th
EMPTY,Empty,Label shown in the pinned apps section when there are no pinned apps.
INSTALLED,"Installed","Installation flag (installed/not installed)"
NOT_INSTALLED,"Not installed","Installation flag (installed/not installed)"
YES,"Yes","Just a simple 'Yes' message, button in the dialog box"
NO,"No","Just a simple 'No' message, button in the dialog box"
1 DEFINITIONS.ANCHORED Anchored A movement option that makes this window move together with the rest of the workspace. Emphasize the "moving together" part as opposed to "not moving at all".
208 APP_SETTINGS.SPACE_GRAVITY_GRAVITY_HELP Amount of downwards force. 0.0 - no gravity Tooltip for the gravity setting.
209 APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION Ground friction Setting in the space gravity physics panel. Friction when touching the ground (0.0 = ice-like, 1.0 = rough).
210 APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION_HELP Amount of friction slowing you down if you're touching the ground. 0.0 - no friction (just like on ice), 1.0 - rough surface Tooltip for the ground friction setting.
211 APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT Floor height Setting in the space gravity physics panel. The Y position where the virtual floor is.
212 APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT_HELP The Y position where the floor is. Gravity stops when you reach this height. Tooltip for the floor height setting.
213 APP_SETTINGS.SPACE_ROTATE_UNLOCKED Allow space rotate on all axes Checkbox in Settings > Space Drag tab. Unlocks space rotate to work on all axes. "Space" in this context refers to "Playspace" which was translated as "${DEFINITIONS.PLAYSPACE}".
214 APP_SETTINGS.ROTATIONS HMD rotations Number of full 360-degree rotations of the HMD since startup. Shown in Settings > Statistics.
215 APP_SETTINGS.SESSION_TIME Session time Time since WayVR started. Shown in Settings > Statistics.
259 DONATE.THANK_YOU_WAYVR DONATE.SUPPORT_US Thank you from the WayVR Team! Support us Thank you message displayed on the donate tab. Button tooltip on the home screen donate button.
260 DONATE.BUTTON_TOOLTIP DONATE.DESCRIPTION Donate via Open Collective We're a small team of 2 developers building WayVR in our free time. If you enjoy using this software, please consider supporting us - every donation helps us keep going. Tooltip on the donate button on the donate tab. Description text on the donate tab explaining the project.
261 DONATE.CURRENT_SUPPORTERS DONATE.THANK_YOU_WAYVR Current Supporters Thank you from the WayVR Team! Section header on the donate tab listing supporters. Thank you message displayed on the donate tab.
262 DONATE.BUTTON_TOOLTIP Donate via Open Collective Tooltip on the donate button on the donate tab.
263 DONATE.CURRENT_SUPPORTERS Current Supporters Section header on the donate tab listing supporters.
264 DONATE.BROUGHT_TO_YOU_VARIANTS_OFTEN Supported by {USER}|Special thanks to {USER}|Sponsored by {USER}|Backed by {USER}|Driven by community members like {USER}|Made possible by {USER}|Sustained by {USER}|Contributed by {USER}|Funded by {USER} This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it.
265 DONATE.BROUGHT_TO_YOU_VARIANTS_RARE Coffee provided by {USER}|Fuelled by {USER}|Shoutout to {USER}!|Keeping us going: {USER}|Helping us grow: {USER}|Keeping the servers running: {USER}|With gratitude to {USER}|Powered by {USER}|Supported by friends like {USER} This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it (rare versions).
266 DONATE.BROUGHT_TO_YOU_VARIANTS_VERY_RARE Supported by legends like {USER}|Caffeine supplied by {USER}|A true legend: {USER}|Our hero of the day: {USER}|An absolute superstar: {USER}|Paying for the electricity bill: {USER}|The savior of our sanity: {USER}|Leading the way: {USER}|Caffeine-to-code converter provided by {USER}|Keeping the production stable: {USER}|The one and only: {USER}|Passing dmabuf buffers: {USER} This text will show up a random person from donations page - line below this string will display the supporter user. There are many variants of it (rare versions).
338 NO No Just a simple 'No' message, button in the dialog box
339
340
341
342
343
344
345
346
347
348
349
365
366
367
368
369
+15 -3
View File
@@ -211,8 +211,8 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Use current playspace for reset playspace binding.",
"RESET_PLAYSPACE_CENTER": "Reset playspace center",
"RESET_PLAYSPACE_CENTER_HELP": "Clear previously set playspace center.",
"RESTART_SOFTWARE": "Restart software",
"RESTART_SOFTWARE_HELP": "Apply settings that require a restart",
"RESTART_WAYVR": "Restart WayVR",
"CLOSE_WAYVR": "Close WayVR",
"ROUND_MULTIPLIER": "UI Edge roundness",
"SCREEN_RENDER_DOWN": "Render screen at lower resolution",
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
@@ -258,6 +258,8 @@
},
"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",
@@ -346,6 +348,14 @@
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
},
"POWER_OPTIONS": {
"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?"
},
"PROCESS": {
"FORCE_KILL": "Force-kill",
"STOP": "Stop"
@@ -368,5 +378,7 @@
"REMOVE_FROM_FAVOURITES": "Remove from pinned apps",
"EMPTY": "Empty",
"INSTALLED": "Installed",
"NOT_INSTALLED": "Not installed"
"NOT_INSTALLED": "Not installed",
"YES": "Yes",
"NO": "No"
}
+16 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Usa el espacio de juego actual para el comando de reiniciar espacio de juego.",
"RESET_PLAYSPACE_CENTER": "Reiniciar centro del espacio de juego",
"RESET_PLAYSPACE_CENTER_HELP": "Restablece el centro del espacio de juego definido previamente.",
"RESTART_SOFTWARE": "Reiniciar software",
"RESTART_SOFTWARE_HELP": "Aplicar ajustes que requieren reiniciar",
"ROUND_MULTIPLIER": "Redondeo de bordes de la UI",
"SCREEN_RENDER_DOWN": "Renderizar pantalla a resolución menor",
"SCREEN_RENDER_DOWN_HELP": "Ayuda con el aliasing en pantallas de alta resolución",
@@ -263,7 +261,11 @@
"TITLE": "Diagnósticos",
"ENABLE_REBAR": "Activa [resizable bar] en la configuración de la BIOS para un rendimiento óptimo de VR"
},
"TUTORIAL": "Tutorial"
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "Reiniciar WayVR",
"CLOSE_WAYVR": "Cerrar WayVR",
"SPEECH_TO_TEXT": "Dictado por voz",
"WHISPER_GPU_SELECT": "Seleccionar GPU"
},
"APPLICATION_LAUNCHER": "Lanzador de aplicaciones",
"APPLICATION_STARTED": "Aplicación iniciada",
@@ -368,5 +370,15 @@
"REMOVE_FROM_FAVOURITES": "Quitar de aplicaciones fijadas",
"EMPTY": "Vacío",
"INSTALLED": "Instalado",
"NOT_INSTALLED": "No instalado"
"NOT_INSTALLED": "No instalado",
"POWER_OPTIONS": {
"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?"
},
"YES": "Sí",
"NO": "No"
}
+16 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Usa il playspace corrente per il comando di reset del playspace.",
"RESET_PLAYSPACE_CENTER": "Resetta centro playspace",
"RESET_PLAYSPACE_CENTER_HELP": "Rimuovi il centro del playspace impostato precedentemente.",
"RESTART_SOFTWARE": "Riavvia software",
"RESTART_SOFTWARE_HELP": "Applica le impostazioni che richiedono il riavvio",
"ROUND_MULTIPLIER": "Arrotondamento bordi UI",
"SCREEN_RENDER_DOWN": "Rendering schermo a risoluzione inferiore",
"SCREEN_RENDER_DOWN_HELP": "Riduce l'effetto aliasing sugli schermi ad alta risoluzione",
@@ -263,7 +261,11 @@
"TITLE": "Diagnostica",
"ENABLE_REBAR": "Abilita Resizable BAR nelle impostazioni del BIOS per prestazioni VR ottimali"
},
"TUTORIAL": "Tutorial"
"TUTORIAL": "Tutorial",
"RESTART_WAYVR": "Riavvia WayVR",
"CLOSE_WAYVR": "Chiudi WayVR",
"SPEECH_TO_TEXT": "Speech-to-text",
"WHISPER_GPU_SELECT": "Seleziona GPU"
},
"APPLICATION_LAUNCHER": "Avvio applicazioni",
"APPLICATION_STARTED": "Applicazione avviata",
@@ -368,5 +370,15 @@
"REMOVE_FROM_FAVOURITES": "Rimuovi dalle app fissate",
"EMPTY": "Vuoto",
"INSTALLED": "Installato",
"NOT_INSTALLED": "Non installato"
"NOT_INSTALLED": "Non installato",
"POWER_OPTIONS": {
"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?"
},
"YES": "Sì",
"NO": "No"
}
+16 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "現在のプレイスペースを、プレイスペースリセットのバインディングに使用する。",
"RESET_PLAYSPACE_CENTER": "プレイスペースの中心をリセット",
"RESET_PLAYSPACE_CENTER_HELP": "設定済みのプレイスペースの中心点をリセットする。",
"RESTART_SOFTWARE": "ソフトウェアを再起動",
"RESTART_SOFTWARE_HELP": "再起動が必要な設定を適用する",
"ROUND_MULTIPLIER": "UIの角の丸み",
"SCREEN_RENDER_DOWN": "低解像度で画面をレンダリング",
"SCREEN_RENDER_DOWN_HELP": "高解像度ディスプレイでのジャギーを軽減する",
@@ -263,7 +261,11 @@
"TITLE": "診断",
"ENABLE_REBAR": "VRパフォーマンスを最適化するため、BIOS設定でresizable BARを有効にして下さい"
},
"TUTORIAL": "チュートリアル"
"TUTORIAL": "チュートリアル",
"RESTART_WAYVR": "WayVRを再起動",
"CLOSE_WAYVR": "WayVRを閉じる",
"SPEECH_TO_TEXT": "音声入力",
"WHISPER_GPU_SELECT": "GPUを選択"
},
"APPLICATION_LAUNCHER": "アプリランチャー",
"APPLICATION_STARTED": "アプリが起動しました",
@@ -368,5 +370,15 @@
"REMOVE_FROM_FAVOURITES": "固定済みアプリから外す",
"EMPTY": "なし",
"INSTALLED": "インストール済み",
"NOT_INSTALLED": "未インストール"
"NOT_INSTALLED": "未インストール",
"POWER_OPTIONS": {
"TITLE": "電源オプション",
"SHUTDOWN": "シャットダウン",
"SUSPEND": "サスペンド",
"SHUTDOWN_ARE_YOU_SURE": "コンピューターをシャットダウンしてもよろしいですか?",
"NOT_SUPPORTED": "電源オプションには systemd または elogind が必要です。",
"SUSPEND_ARE_YOU_SURE": "コンピューターをスリープ状態にしますか?"
},
"YES": "はい",
"NO": "いいえ"
}
+16 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "Użyj aktualnej przestrzeni gry dla przypisania resetowania przestrzeni.",
"RESET_PLAYSPACE_CENTER": "Zresetuj centrum przestrzeni gry",
"RESET_PLAYSPACE_CENTER_HELP": "Resetuj aktualny środek przestrzeni gry.",
"RESTART_SOFTWARE": "Uruchom ponownie program",
"RESTART_SOFTWARE_HELP": "Zastosuj ustawienia wymagające restartu",
"ROUND_MULTIPLIER": "Zaokrąglenie krawędzi UI",
"SCREEN_RENDER_DOWN": "Renderuj ekran w niższej rozdzielczości",
"SCREEN_RENDER_DOWN_HELP": "Pomaga zredukować aliasing na ekranach o wysokiej rozdzielczości",
@@ -263,7 +261,11 @@
"TITLE": "Diagnostyka",
"ENABLE_REBAR": "Włącz resizable BAR w ustawieniach BIOS, aby uzyskać optymalną wydajność VR"
},
"TUTORIAL": "Samouczek"
"TUTORIAL": "Samouczek",
"RESTART_WAYVR": "Zrestartuj WayVR",
"CLOSE_WAYVR": "Zamknij WayVR",
"SPEECH_TO_TEXT": "Zamiana mowy na tekst",
"WHISPER_GPU_SELECT": "Wybierz GPU"
},
"APPLICATION_LAUNCHER": "Launcher aplikacji",
"APPLICATION_STARTED": "Uruchomiono aplikację",
@@ -368,5 +370,15 @@
"REMOVE_FROM_FAVOURITES": "Usuń z przypiętych aplikacji",
"EMPTY": "Puste",
"INSTALLED": "Zainstalowano",
"NOT_INSTALLED": "Nie zainstalowano"
"NOT_INSTALLED": "Nie zainstalowano",
"POWER_OPTIONS": {
"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?"
},
"YES": "Tak",
"NO": "Nie"
}
+16 -4
View File
@@ -197,8 +197,6 @@
"SAVE_PLAYSPACE_CENTER_HELP": "将当前的活动空间设置为重置活动空间的绑定点。",
"RESET_PLAYSPACE_CENTER": "重置活动空间中心",
"RESET_PLAYSPACE_CENTER_HELP": "重置之前设置的活动空间中心。",
"RESTART_SOFTWARE": "重启软件",
"RESTART_SOFTWARE_HELP": "应用需要重启才能生效的设置",
"ROUND_MULTIPLIER": "UI 边缘圆角",
"SCREEN_RENDER_DOWN": "低分辨率渲染屏幕",
"SCREEN_RENDER_DOWN_HELP": "有助于减轻高分辨率屏幕上的锯齿现象",
@@ -263,7 +261,11 @@
"TITLE": "诊断",
"ENABLE_REBAR": "请在 BIOS 设置中启用 Resizable BAR 以获得最佳 VR 性能"
},
"TUTORIAL": "教程"
"TUTORIAL": "教程",
"RESTART_WAYVR": "重启 WayVR",
"CLOSE_WAYVR": "关闭 WayVR",
"SPEECH_TO_TEXT": "语音转文本",
"WHISPER_GPU_SELECT": "选择 GPU"
},
"APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用程序已启动",
@@ -368,5 +370,15 @@
"REMOVE_FROM_FAVOURITES": "从固定应用中移除",
"EMPTY": "暂无",
"INSTALLED": "已安装",
"NOT_INSTALLED": "未安装"
"NOT_INSTALLED": "未安装",
"POWER_OPTIONS": {
"TITLE": "电源选项",
"SHUTDOWN": "关机",
"SUSPEND": "暂停",
"SHUTDOWN_ARE_YOU_SURE": "确定要关机吗?",
"NOT_SUPPORTED": "电源选项需要 systemd 或 elogind。",
"SUSPEND_ARE_YOU_SURE": "确定要让计算机进入睡眠状态吗?"
},
"YES": "确定",
"NO": "否"
}
+123 -6
View File
@@ -1,11 +1,11 @@
use std::{path::PathBuf, rc::Rc};
use std::{path::PathBuf, process::Command, rc::Rc};
use chrono::Timelike;
use glam::Vec2;
use strum::EnumCount;
use wgui::{
assets::{AssetPathRef, AssetProvider},
components::button::ComponentButton,
components::{ComponentTrait, button::ComponentButton},
event::{CallbackDataCommon, StyleSetRequest},
font_config::WguiFontConfig,
globals::WguiGlobals,
@@ -17,12 +17,15 @@ use wgui::{
task::Tasks,
theme::WguiTheme,
widget::{label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite},
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
windowing::{
context_menu::{self, ContextMenu},
window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
},
};
use wlx_common::{
async_executor::AsyncExecutor,
audio,
dash_interface::{BoxDashInterface, ConfigChangeKind, DashPlayspaceTask},
dash_interface::{BoxDashInterface, ConfigChangeKind, DashBackendTask, DashPlayspaceTask},
locale::WayVRLangProvider,
palette::load_palette,
timestep::{self, Timestep},
@@ -35,10 +38,11 @@ use crate::{
settings::TabSettings, welcome::TabWelcome,
},
util::{
popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams},
is_exec_installed,
popup_manager::{MountPopupOnceParams, PopupHolder, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
},
views,
views::{self, ViewUpdateParams},
};
pub struct FrontendWidgets {
@@ -74,9 +78,11 @@ pub struct Frontend<T> {
toast_manager: ToastManager,
timestep: Timestep,
sounds_to_play: Vec<SoundType>,
context_menu_power: ContextMenu,
window_audio_settings: WguiWindow,
view_audio_settings: Option<views::audio_settings::View>,
popup_dialog_box: PopupHolder<views::dialog_box::View>,
}
pub struct FrontendUpdateParams<'a, T> {
@@ -116,13 +122,16 @@ pub enum FrontendTask {
MountPopupOnce(MountPopupOnceParams),
RefreshPopupManager,
ShowAudioSettings,
ShowPowerSettings(WidgetID /* source widget */),
UpdateAudioSettingsView,
RecenterPlayspace,
PushToast(Translation),
PlaySound(SoundType),
OpenURL(Rc<str>),
RunCommand(String /* program */, Vec<String> /* args */),
HideDashboard,
MarkTutorialGraduated,
BackendTask(DashBackendTask),
}
impl<T: 'static> Frontend<T> {
@@ -202,9 +211,11 @@ impl<T: 'static> Frontend<T> {
popup_manager,
toast_manager,
window_audio_settings: WguiWindow::default(),
context_menu_power: Default::default(),
view_audio_settings: None,
executor: params.executor,
sounds_to_play: Vec::new(),
popup_dialog_box: Default::default(),
};
// init some things first
@@ -275,12 +286,68 @@ 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",
};
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),
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));
}
}),
},
);
}
fn process_power_action(&mut self, action: &str, data: &mut T) {
match action {
"restart" => {
self.interface.restart(data);
}
"close" => {
self.interface.close(data);
}
"suspend" => {
self.power_action_confirm(PowerAction::Suspend);
}
"shutdown" => {
self.power_action_confirm(PowerAction::Poweroff);
}
_ => {
unreachable!()
}
}
}
fn tick(&mut self, params: FrontendUpdateParams<T>) -> anyhow::Result<FrontendUpdateResult> {
// fixme: timer events instead of this thing
if self.ticks.is_multiple_of(1000) {
self.update_time(params.data)?;
}
// TODO: refactor: simplify update function (no configs or T are required in most cases)
let mut config_change_kind = None;
self.popup_dialog_box.update(&mut ViewUpdateParams {
layout: &mut self.layout,
executor: &self.executor,
general_config: self.interface.general_config(params.data),
config_change_kind: &mut config_change_kind,
})?;
if let context_menu::TickResult::Action(action) = self.context_menu_power.tick(&mut self.layout, &mut self.state)? {
self.process_power_action(&action, params.data);
}
{
// always 30 times per second
while self.timestep.on_tick() {
@@ -371,13 +438,16 @@ impl<T: 'static> Frontend<T> {
FrontendTask::MountPopupOnce(popup_params) => self.mount_popup_once(popup_params, params.data)?,
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
FrontendTask::ShowPowerSettings(pos) => self.action_show_power_settings(pos)?,
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
FrontendTask::RecenterPlayspace => self.action_recenter_playspace(params.data)?,
FrontendTask::PushToast(content) => self.toast_manager.push(content),
FrontendTask::PlaySound(sound_type) => self.queue_play_sound(sound_type),
FrontendTask::HideDashboard => self.action_hide_dashboard(params.data),
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(())
}
@@ -503,6 +573,11 @@ impl<T: 'static> Frontend<T> {
FrontendTask::ShowAudioSettings,
);
let btn_power = self.state.fetch_component_as::<ComponentButton>("btn_power")?;
self
.tasks
.handle_button(&btn_power, FrontendTask::ShowPowerSettings(btn_power.base().get_id()));
// "Recenter playspace" bottom bar button
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_recenter")?,
@@ -512,6 +587,18 @@ impl<T: 'static> Frontend<T> {
Ok(())
}
fn action_show_power_settings(&mut self, source_id: WidgetID) -> anyhow::Result<()> {
self.context_menu_power.open(context_menu::OpenParams {
on_custom_attribs: None,
position: source_id.into(),
blueprint: context_menu::Blueprint::Template {
template_name: "context_menu_power_options".into(),
template_params: Default::default(),
},
});
Ok(())
}
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
self.window_audio_settings.open(&mut WguiWindowParams {
position: Vec2::new(64.0, 64.0),
@@ -576,4 +663,34 @@ impl<T: 'static> Frontend<T> {
url
))));
}
fn action_run_command(&mut self, command: String, args: Vec<String>) {
if let Err(e) = Command::new(command).args(args).spawn() {
self
.tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Failed to launch command: {e}"
))))
};
}
}
#[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"))
}
+14 -11
View File
@@ -61,7 +61,7 @@ impl<T> Tab<T> for TabApps<T> {
}
}
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [DesktopEntry]) -> Option<&'a DesktopEntry> {
fn find_entry_from_app_name<'a>(app_id: &str, entries: &'a [Rc<DesktopEntry>]) -> Option<&'a Rc<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, &DesktopEntry)>::new();
let mut pinned_desktop_entries = Vec::<(PinnedApp, Rc<DesktopEntry>)>::new();
// collect pinned desktop entries
{
@@ -83,7 +83,7 @@ impl<T> TabApps<T> {
continue;
};
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry));
pinned_desktop_entries.push((pinned_app.clone(), desktop_entry.clone()));
}
// 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,
desktop_entry.as_ref(),
Some(tooltip_string),
)?;
@@ -139,8 +139,8 @@ impl<T> TabApps<T> {
struct AppList {
//data: Vec<ParserData>,
all_entries: Vec<DesktopEntry>,
entries_to_mount: VecDeque<DesktopEntry>,
all_entries: Vec<Rc<DesktopEntry>>,
entries_to_mount: VecDeque<Rc<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: DesktopEntry,
entry: Rc<DesktopEntry>,
state: Rc<RefCell<State>>,
pinned_app: Option<PinnedApp>,
) -> ButtonClickCallback {
@@ -307,14 +307,17 @@ fn get_category_name_score(name: &str) -> u8 {
Scores::Unknown as u8
}
fn get_best_category_name(categories: &[Rc<str>]) -> Option<&Rc<str>> {
fn get_best_category_name<S>(categories: &[S]) -> Option<&str>
where
S: AsRef<str>,
{
let mut best_score: u8 = 0;
let mut best_category: Option<&Rc<str>> = None;
let mut best_category: Option<&str> = None;
for cat in categories {
let score = get_category_name_score(cat);
let score = get_category_name_score(cat.as_ref());
if score > best_score {
best_category = Some(cat);
best_category = Some(cat.as_ref());
best_score = score;
}
}
+57 -66
View File
@@ -1,16 +1,14 @@
use std::rc::Rc;
use crate::tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart};
use crate::{
tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart},
util::wgui_simple,
};
use wgui::{
components::{
button::{ButtonClickEvent, ComponentButton},
checkbox::ComponentCheckbox,
slider::ComponentSlider,
},
components::{button::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};
@@ -297,7 +295,6 @@ where
mp.idx += 1;
let mut params = TemplateParams::new();
params.insert("id", &id);
match setting.get_translation() {
Ok(translation) => params.insert("translation", translation),
@@ -310,69 +307,63 @@ where
let id_cell = horiz_cell(mp.layout, parent)?;
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, id_cell, params)?;
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))),
})?;
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.unwrap_or_default(),
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 {
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
title,
tooltip,
attribs: vec![],
})
})
.collect(),
));
Ok(())
}
}));
Ok(())
}
+17 -12
View File
@@ -1,4 +1,3 @@
use glam::Vec2;
use std::{marker::PhantomData, rc::Rc, str::FromStr};
use strum::{AsRefStr, EnumProperty, EnumString};
use wgui::{
@@ -81,11 +80,10 @@ pub(crate) enum Task {
ClearPipewireTokens,
ClearSavedState,
DeleteAllConfigs,
OpenContextMenu(Vec2, Vec<context_menu::Cell>),
OpenContextMenu(context_menu::Position, Vec<context_menu::Cell>),
RemoveAutostartApp(Rc<str>),
ResetPlayspace,
ResetPlayspaceCenter,
RestartSoftware,
SavePlayspaceCenter,
SetTab(TabNameEnum),
SettingUpdated(SettingType),
@@ -143,6 +141,7 @@ pub struct TabSettings<T> {
tasks: Tasks<Task>,
marker: PhantomData<T>,
frontend_tasks: FrontendTasks,
feats: InterfaceFeats,
}
impl<T> Tab<T> for TabSettings<T> {
@@ -231,10 +230,6 @@ impl<T> Tab<T> for TabSettings<T> {
frontend.interface.playspace_task(data, DashPlayspaceTask::Reset)?;
return Ok(());
}
Task::RestartSoftware => {
frontend.interface.restart(data);
return Ok(());
}
Task::OpenContextMenu(position, cells) => {
self.context_menu.open(context_menu::OpenParams {
on_custom_attribs: None,
@@ -703,10 +698,14 @@ 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);
let globals = frontend.layout.state.globals.clone();
self.current_tab = None;
let globals = frontend.layout.state.globals.clone();
let feats = frontend.interface.get_feats(data);
let vulkan_devices = if matches!(name, TabNameEnum::Features) {
Some(frontend.interface.enumerate_vulkan_devices(data)?)
} else {
None
};
let mut mp = MacroParams {
layout: &mut frontend.layout,
@@ -715,14 +714,14 @@ impl<T> TabSettings<T> {
config: frontend.interface.general_config(data),
tasks: self.tasks.clone(),
idx: 9001,
feats,
feats: self.feats,
};
let settings_mount_params = SettingsMountParams {
mp: &mut mp,
id_parent: root,
frontend_tasks: &self.frontend_tasks,
feats,
feats: self.feats,
};
match name {
@@ -730,7 +729,10 @@ 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)?));
self.current_tab = Some(Box::new(tab_features::State::mount(
settings_mount_params,
vulkan_devices.unwrap(), /* safe */
)?));
}
TabNameEnum::SpaceDrag => {
self.current_tab = Some(Box::new(tab_space_drag::State::mount(settings_mount_params)?));
@@ -796,6 +798,8 @@ 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,
@@ -804,6 +808,7 @@ impl<T> TabSettings<T> {
context_menu: ContextMenu::default(),
current_tab: None,
frontend_tasks: frontend.tasks.clone(),
feats,
})
}
}
+23 -6
View File
@@ -3,7 +3,10 @@ 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;
@@ -18,7 +21,8 @@ use crate::{
#[derive(Clone)]
enum Task {
OpenInputProfiles,
OpenInputProfilesDash,
OpenInputProfilesBackend,
}
pub struct State {
@@ -34,13 +38,23 @@ impl SettingsTab for State {
for task in self.tasks.drain() {
match task {
Task::OpenInputProfiles => {
Task::OpenInputProfilesDash => {
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);
}
}
}
@@ -53,6 +67,7 @@ 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;
@@ -71,7 +86,11 @@ fn create_input_profiles_button(
btn.on_click(Rc::new({
let tasks = tasks.clone();
move |_common, _e| {
tasks.push(Task::OpenInputProfiles);
if xr_backend.is_open_vr() {
tasks.push(Task::OpenInputProfilesBackend);
} else {
tasks.push(Task::OpenInputProfilesDash);
}
Ok(())
}
}));
@@ -92,9 +111,7 @@ impl State {
"APP_SETTINGS.CONTROLS",
"@/dashboard/controller.svg",
)?;
if par.feats.xr_backend.is_open_xr() {
create_input_profiles_button(par.mp, c, tasks.clone(), &popup)?;
}
create_input_profiles_button(par.mp, c, tasks.clone(), &popup, par.feats.xr_backend)?;
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,27 +1,17 @@
use wgui::layout::WidgetID;
use crate::tab::settings::{
SettingsMountParams, SettingsTab,
macros::{MacroParams, options_category, options_diagnostics_drm_card, options_diagnostics_row},
use crate::{
tab::settings::{
SettingsMountParams, SettingsTab,
macros::{MacroParams, options_category, options_diagnostics_drm_card, options_diagnostics_row},
},
util::is_exec_installed,
};
pub struct State {}
impl SettingsTab for State {}
// works good enough
fn is_exec_installed(exec: &str) -> bool {
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c");
cmd.arg(format!("command -v {}", exec));
let Ok(res) = cmd.output() else {
return false;
};
res.status.success()
}
#[derive(Default)]
struct LspciInfo {
// slot: String,
+128 -72
View File
@@ -1,7 +1,6 @@
use std::rc::Rc;
use wgui::{
components::button::{ButtonClickEvent, ComponentButton},
globals::WguiGlobals,
i18n::Translation,
layout::WidgetID,
@@ -9,7 +8,7 @@ use wgui::{
parser::{Fetchable, TemplateParams},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu::{self},
windowing::context_menu,
};
use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind};
@@ -21,6 +20,7 @@ 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,41 +104,60 @@ impl SettingsTab for State {
layout: &mut wgui::layout::Layout,
state: &mut wgui::parser::ParserState,
) -> anyhow::Result<()> {
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"))?;
label.set_text(&mut common, Translation::from_raw_text(model.display_name));
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(());
};
config.whisper_model = model.file_name.into();
if !whisper_model_path(model.file_name).exists() {
self.show_whisper_model_dialog_box_download(model)?;
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);
}
} 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));
// 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" => {
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()),
);
change_kind.replace(ConfigChangeKind::Other);
}
_ => unreachable!(),
};
Ok(())
}
}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
pub fn mount(par: SettingsMountParams, vulkan_devices: Vec<String>) -> 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();
@@ -151,7 +170,9 @@ 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)?;
@@ -287,6 +308,48 @@ 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;
@@ -307,56 +370,49 @@ 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");
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_translation);
}
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 {
action_name: Some(format!(";{id};{}", item.file_name).into()),
title: Translation::from_raw_text_string(format!(
"{}{}",
item.display_name,
if downloaded_models.contains(&idx) { " ✅" } else { "" }
)),
tooltip: None,
attribs: vec![],
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()
})
.collect::<Vec<_>>();
} 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<_>>();
cells.insert(
0,
context_menu::Cell {
action_name: Some(format!(";{id};").into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
tooltip: None,
attribs: vec![],
},
);
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()
}),
);
parent_tasks.push(ParentTask::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
cells,
));
Ok(())
}
}));
Ok(())
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))),
})
}
@@ -36,13 +36,6 @@ impl State {
"@/dashboard/circle.svg",
Task::DeleteAllConfigs,
)?;
options_danger_button(
par.mp,
c,
"APP_SETTINGS.RESTART_SOFTWARE",
"@/dashboard/refresh.svg",
Task::RestartSoftware,
)?;
Ok(State {})
}
}
+13
View File
@@ -7,3 +7,16 @@ pub mod steam_utils;
pub mod toast_manager;
pub mod wgui_simple;
pub mod whisper;
// works good enough
pub fn is_exec_installed(exec: &str) -> bool {
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c");
cmd.arg(format!("command -v {}", exec));
let Ok(res) = cmd.output() else {
return false;
};
res.status.success()
}
+10 -5
View File
@@ -81,7 +81,7 @@ impl BindingsDropdown for XrInputComponent {
pub struct ParsedOpenXrInputPath {
pub side: XrInputSide,
pub subpath: XrInputSubpathKind,
pub component: XrInputComponent,
pub component: Option<XrInputComponent>,
}
impl TryFrom<&str> for ParsedOpenXrInputPath {
@@ -101,13 +101,18 @@ impl TryFrom<&str> for ParsedOpenXrInputPath {
bail!("missing input prefix");
}
let (identifier, component) = rest.rsplit_once('/').context("missing identifier or component")?;
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),
};
if identifier.is_empty() || component.is_empty() {
bail!("identifier or component empty");
if identifier.is_empty() {
bail!("identifier empty");
}
let component = XrInputComponent::try_from(component).context("bad component")?;
let subpath = XrInputSubpathKind::try_from(identifier).context("bad subpath")?;
Ok(Self {
+43 -2
View File
@@ -1,12 +1,16 @@
use glam::{Mat4, Vec2};
use std::rc::Rc;
use wgui::{
animation::{Animation, AnimationDuration, AnimationEasing},
assets::AssetPathRef,
color::WguiColorName,
components::{self, button::ButtonClickCallback},
components::{
self,
button::{ButtonClickCallback, ButtonClickEvent, ComponentButton},
},
i18n::Translation,
layout::{Layout, LayoutTask, WidgetID},
parser::{Fetchable, ParseDocumentParams},
parser::{Fetchable, ParseDocumentParams, ParserState, TemplateParams},
renderer_vk::{
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
util::centered_matrix,
@@ -17,6 +21,7 @@ use wgui::{
label::{WidgetLabel, WidgetLabelParams},
sprite::{WidgetSprite, WidgetSpriteParams},
},
windowing::context_menu,
};
pub struct CreateButtonParams<'a> {
@@ -166,3 +171,39 @@ 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(())
}
+21 -19
View File
@@ -53,7 +53,7 @@ struct LaunchParams<'a, T> {
pub struct View {
#[allow(dead_code)]
state: ParserState,
entry: DesktopEntry,
entry: Rc<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: DesktopEntry,
pub entry: Rc<DesktopEntry>,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub config: &'a GeneralConfig,
@@ -119,10 +119,8 @@ impl View {
{
let mut label_exec = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_exec")?;
label_exec.set_text_simple(
&mut params.globals.get(),
Translation::from_raw_text_string(format!("{} {}", params.entry.exec_path, params.entry.exec_args)),
);
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));
}
let tasks = Tasks::new();
@@ -406,14 +404,19 @@ impl View {
env.push("ELECTRON_OZONE_PLATFORM_HINT=wayland".into());
}
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 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 pos_mode = match params.pos_mode {
@@ -423,7 +426,7 @@ impl View {
};
let mut userdata = HashMap::new();
userdata.insert("desktop-entry".to_string(), serde_json::to_string(params.application)?);
userdata.insert("desktop-entry".into(), serde_json::to_string(params.application)?);
let resolution = Self::calculate_resolution(params.res_mode, params.orientation_mode);
@@ -432,12 +435,11 @@ impl View {
params.auto_start,
WvrProcessLaunchParams {
env,
exec,
argv,
name: params.application.app_name.to_string(),
args,
resolution,
pos_mode,
icon: params.application.icon_path.as_ref().map(|x| x.as_ref().to_string()),
icon: params.application.icon_path.as_deref().map(String::from),
userdata,
},
)?;
@@ -486,7 +488,7 @@ impl View {
pub fn mount_popup(
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
entry: DesktopEntry,
entry: Rc<DesktopEntry>,
popup: PopupHolder<View>,
on_app_pins_changed: Box<dyn Fn()>,
pinned_app: Option<PinnedApp>,
+106 -105
View File
@@ -3,17 +3,13 @@ use std::rc::Rc;
use strum::EnumProperty;
use wgui::{
assets::AssetPathRef,
components::{
button::{ButtonClickEvent, ComponentButton},
slider::ComponentSlider,
},
components::{button::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::{
@@ -39,7 +35,7 @@ use crate::{
enum Task {
Save,
Cancel,
OpenContextMenu(glam::Vec2, Vec<context_menu::Cell>),
OpenContextMenu(context_menu::Position, Vec<context_menu::Cell>),
UpdateThreshold(Rc<str>, XrInputSide, usize, f32),
AddChordMember(Rc<str>, XrInputSide),
RemoveChordMember(Rc<str>, XrInputSide, usize),
@@ -162,6 +158,7 @@ impl ViewTrait for View {
"subpath" => {
apply_subpath(
action_binding_mut(action_mut, side),
action_name,
side,
value,
self.controller_profile,
@@ -617,7 +614,7 @@ fn chord_member_controls(
output_side,
member_idx,
available_components,
parsed.as_ref().map(|x| x.component),
parsed.and_then(|x| x.component),
)?;
let tasks = mp.tasks.clone();
@@ -638,7 +635,7 @@ fn chord_member_controls(
})?;
// put threshold slider on its own row
if parsed.as_ref().is_some_and(|x| x.component.is_analog()) {
if parsed.and_then(|x| x.component).is_some_and(|x| x.is_analog()) {
chord_threshold_slider(mp, parent, action.clone(), output_side, member_idx, member.threshold())?;
}
@@ -666,13 +663,6 @@ 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()
@@ -689,23 +679,33 @@ 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.as_ref().map(|x| x.component),
current.and_then(|x| x.component),
)? {
return Ok(());
}
clicks_dropdown(mp, parent, par.action.clone(), par.click_type)?;
if let Some(component) = current.as_ref().map(|x| x.component)
if let Some(component) = current.and_then(|x| x.component)
&& component.is_analog()
&& &**par.action != "scroll"
// hax
{
threshold_slider(mp, parent, par.action.clone(), par.side, par.threshold)?;
}
@@ -804,20 +804,21 @@ fn chord_hand_dropdown(
let cells = [XrInputSide::Left, XrInputSide::Right]
.into_iter()
.filter(|side| profile.find_userpath(*side).is_some())
.map(|side| context_menu::Cell {
action_name: Some(
format!(
"chord_hand;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
side.as_ref()
)
.into(),
),
title: side_translation(side),
tooltip: None,
attribs: vec![],
.map(|side| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_hand;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
side.as_ref()
)
.into(),
),
title: side_translation(side),
..Default::default()
})
})
.collect();
@@ -842,20 +843,21 @@ fn chord_subpath_dropdown(
.unwrap_or_else(|| Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"));
let cells = available
.iter()
.map(|subpath| context_menu::Cell {
action_name: Some(
format!(
"chord_subpath;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
subpath.as_ref()
)
.into(),
),
title: subpath.translation(),
tooltip: None,
attribs: vec![],
.map(|subpath| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_subpath;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
subpath.as_ref()
)
.into(),
),
title: subpath.translation(),
..Default::default()
})
})
.collect();
@@ -885,20 +887,21 @@ fn chord_component_dropdown(
.unwrap_or_else(|| Translation::from_raw_text_rc(Default::default()));
let cells = available
.iter()
.map(|component| context_menu::Cell {
action_name: Some(
format!(
"chord_comp;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
component.as_ref()
)
.into(),
),
title: component.translation(),
tooltip: None,
attribs: vec![],
.map(|component| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(
format!(
"chord_comp;{};{};{};{}",
action,
output_side.as_ref(),
member_idx,
component.as_ref()
)
.into(),
),
title: component.translation(),
..Default::default()
})
})
.collect();
@@ -1003,23 +1006,23 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
) -> anyhow::Result<()> {
let mut cells = available
.iter()
.map(|item| context_menu::Cell {
action_name: Some(item.action_str(&action, side)),
title: item.translation(),
tooltip: None,
attribs: vec![],
.map(|item| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(item.action_str(&action, side).into()),
title: item.translation(),
..Default::default()
})
})
.collect::<Vec<_>>();
if let Some(action_str) = B::clear_str(&action, side) {
cells.insert(
0,
context_menu::Cell {
action_name: Some(action_str),
context_menu::Cell::Button(context_menu::Button {
action_name: Some(action_str.into()),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
tooltip: None,
attribs: vec![],
},
..Default::default()
}),
);
}
@@ -1029,42 +1032,29 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
fn create_dropdown_with_cells(
mp: &mut MacroParams,
parent: WidgetID,
mut params: TemplateParams,
params: TemplateParams,
current_text: Translation,
cells: Vec<context_menu::Cell>,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
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.unwrap_or_default(),
cells.clone(),
));
Ok(())
}
}));
Ok(())
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))),
})
}
fn apply_subpath(
side_mut: &mut Option<OneOrMany<String>>,
action_name: &str,
side_str: &str,
subpath_str: &str,
profile: &XrControllerProfile,
@@ -1075,6 +1065,12 @@ 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;
};
@@ -1087,11 +1083,12 @@ fn apply_subpath(
return;
};
let mut parsed_compo = parsed.component;
if !subpath_obj.components.contains(&parsed_compo) {
parsed_compo = *subpath_obj.components.first().unwrap();
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()
}
parsed_compo
} else {
*subpath_obj.components.first().unwrap()
};
@@ -1226,7 +1223,7 @@ fn apply_chord_hand(member: &mut OpenXrInputChordMember, side: XrInputSide, prof
};
let component = parsed
.map(|parsed| parsed.component)
.and_then(|parsed| parsed.component)
.filter(|component| subpath.components.contains(component))
.unwrap_or(subpath.components[0]);
let path = format!(
@@ -1256,8 +1253,12 @@ fn apply_chord_subpath(
return;
};
let component = if subpath_obj.components.contains(&parsed.component) {
parsed.component
let component = if let Some(comp) = parsed.component {
if subpath_obj.components.contains(&comp) {
comp
} else {
first_component
}
} else {
first_component
};
+22 -3
View File
@@ -32,6 +32,21 @@ enum Task {
ActionClicked(&'static str),
}
pub fn get_entries_yes_no() -> Vec<ButtonEntry> {
vec![
ButtonEntry {
content: Translation::from_translation_key("YES"),
icon: "@/dashboard/check.svg",
action: "yes",
},
ButtonEntry {
content: Translation::from_translation_key("NO"),
icon: "@/dashboard/cancel.svg",
action: "no",
},
]
}
pub struct View {
tasks: Tasks<Task>,
@@ -52,6 +67,13 @@ fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
impl ViewTrait for View {
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
self.tick();
Ok(())
}
}
impl View {
pub fn tick(&mut self) {
for task in self.tasks.drain() {
match task {
Task::ActionClicked(action) => {
@@ -65,11 +87,8 @@ impl ViewTrait for View {
}
}
}
Ok(())
}
}
impl View {
pub fn new(
layout: &mut Layout,
id_parent: WidgetID,
+2 -2
View File
@@ -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 => {
+8 -5
View File
@@ -1,5 +1,5 @@
use glam::{Vec2, vec2};
use std::rc::Rc;
use std::{rc::Rc, sync::Arc};
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, WGfxClearMode},
gfx::{CommandBufferUsage, PresentStatus, SwapchainError, WGfx, WGfxClearMode},
renderer_vk::{self},
};
use winit::{
@@ -48,13 +48,16 @@ fn init_logging() {
.init();
}
fn load_testbed(audio: &mut Box<dyn audio::AudioProvider>) -> anyhow::Result<Box<dyn Testbed>> {
fn load_testbed(
gfx: &Arc<WGfx>,
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()?),
"dashboard" => Box::new(TestbedDashboard::new(gfx.clone())?),
"" => Box::new(TestbedGeneric::new(assets)?),
_ => Box::new(TestbedAny::new(assets, &name)?),
})
@@ -75,7 +78,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(&mut audio)?;
let mut testbed = load_testbed(&gfx, &mut audio)?;
let mut mouse = Vec2::ZERO;
+5 -4
View File
@@ -1,8 +1,8 @@
use std::rc::Rc;
use std::{rc::Rc, sync::Arc};
use crate::testbed::{Testbed, TestbedUpdateParams};
use dash_frontend::frontend::{self, FrontendUpdateParams};
use wgui::{layout::Layout, theme::WguiTheme};
use wgui::{gfx::WGfx, i18n::Locale, layout::Layout, theme::WguiTheme};
use wlx_common::{
async_executor::AsyncExecutor, dash_interface_emulated::DashInterfaceEmulated,
locale::WayVRLangProvider,
@@ -14,9 +14,10 @@ pub struct TestbedDashboard {
}
impl TestbedDashboard {
pub fn new() -> anyhow::Result<Self> {
let interface = DashInterfaceEmulated::new();
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
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();
+6 -4
View File
@@ -33,7 +33,7 @@ use wlx_common::locale::WayVRLangProvider;
#[derive(Clone)]
pub enum TestbedTask {
ShowPopup,
ShowContextMenu(Vec2),
ShowContextMenu(context_menu::Position),
}
struct Data {
@@ -196,7 +196,7 @@ impl TestbedGeneric {
button_context_menu.on_click({
let tasks = testbed.tasks.clone();
Rc::new(move |_common, m| {
tasks.push(TestbedTask::ShowContextMenu(m.boundary.bottom_left()));
tasks.push(TestbedTask::ShowContextMenu(m.boundary.into()));
Ok(())
})
});
@@ -212,7 +212,9 @@ impl TestbedGeneric {
) -> anyhow::Result<()> {
match task {
TestbedTask::ShowPopup => self.show_popup(params, data)?,
TestbedTask::ShowContextMenu(position) => self.show_context_menu(params, data, *position),
TestbedTask::ShowContextMenu(position) => {
self.show_context_menu(params, data, position.clone())
}
}
Ok(())
@@ -251,7 +253,7 @@ impl TestbedGeneric {
&mut self,
_params: &mut TestbedUpdateParams,
data: &mut Data,
position: Vec2,
position: context_menu::Position,
) {
data.context_menu.open(context_menu::OpenParams {
on_custom_attribs: Some(Rc::new(move |custom_attribs| {
+1 -2
View File
@@ -49,9 +49,8 @@ pub enum HandsfreeParams {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WvrProcessLaunchParams {
pub name: String,
pub exec: String,
pub argv: Vec<String>,
pub env: Vec<String>,
pub args: String,
pub icon: Option<String>,
pub resolution: [u32; 2],
pub pos_mode: PositionMode,
+6 -4
View File
@@ -143,7 +143,11 @@
"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!"
"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!"
},
"WATCH": {
"ADD_NEW_SET": "Neues Set hinzufügen",
@@ -177,7 +181,5 @@
"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."
}
}
+4 -2
View File
@@ -114,8 +114,10 @@ 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}”
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.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
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
1 DEFINITIONS.ANCHORED Anchored A movement option that makes this window move together with the rest of the workspace. Emphasize the “moving together” part as opposed to “not moving at all”.
114 STEAMVR_DASH_TOAST_TITLE TOAST.STEAMVR_DASH_TITLE SteamVR Dashboard Open Title of toast shown when SteamVR dashboard is visible
115 STEAMVR_DASH_TOAST_TEXT TOAST.STEAMVR_DASH_TEXT Close SteamVR dashboard to start using WayVR. Body of toast shown when SteamVR dashboard is visible
116 TOAST.DEFAULT_TITLE TOAST.WAYVR_RUNNING_TITLE Notification WayVR is running! Default title of notifications Title of toast shown when WayVR is started but its windows are hidden by default
117 TOAST.EMPTY_SET TOAST.WAYVR_RUNNING_TEXT Empty set! Check your wrist for the watch, or use show-hide binding to get started! Title of a pop-up when the user switches to an empty set. “Set” was previously translated as “${DEFINITIONS.SET}” Body of toast shown when WayVR is started but its windows are hidden by default
118 TOAST.ERROR TOAST.DEFAULT_TITLE Error Notification Default title for error pop-ups Default title of notifications
119 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}”
120 TOAST.ERROR Error Default title for error pop-ups
121 TOAST.FIXING_FLOOR_IN_X_SECS Fixing floor in {SECONDS} second(s)... A countdown pop-up for automatically adjusting floor height. Keep the {SECONDS} part intact as it’s a parameter.
122 TOAST.LAST_EXISTING_SET This is the last existing set. Notification body explaining that the set cannot be removed because this is the last existing set. “Set” was previously translated as “${DEFINITIONS.SET}”
123 TOAST.LETS_ADD_OVERLAYS Let's add some overlays from the taskbar! Instructions prompting the user to add overlays. “Taskbar” was previously translated as “${DEFINITIONS.TASKBAR}”, “Overlay” was previously translated as “${DEFINITIONS.OVERLAY}”
+4 -2
View File
@@ -133,6 +133,10 @@
"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",
@@ -145,8 +149,6 @@
"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",
+6 -4
View File
@@ -143,7 +143,11 @@
"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!"
"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!"
},
"WATCH": {
"ADD_NEW_SET": "Añadir un set nuevo",
@@ -177,7 +181,5 @@
"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."
}
}
+6 -4
View File
@@ -143,7 +143,11 @@
"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!"
"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!"
},
"WATCH": {
"ADD_NEW_SET": "Aggiungi un nuovo set",
@@ -177,7 +181,5 @@
"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."
}
}
+6 -4
View File
@@ -143,7 +143,11 @@
"NO_SET_SELECTED": "セットが選択されていません。",
"ONE_CONTROLLER_ON_FLOOR": "コントローラーを片方、床に置いてください!",
"NO_KEYMAPS_CONFIGURED": "キーボードレイアウトが設定されていない",
"NO_KEYMAPS_CONFIGURED_HELP": "キーボードレイアウトの設定方法は wayvr.org を確認してね!"
"NO_KEYMAPS_CONFIGURED_HELP": "キーボードレイアウトの設定方法は wayvr.org を確認してね!",
"STEAMVR_DASH_TITLE": "SteamVRダッシュボード展開中",
"STEAMVR_DASH_TEXT": "WayVRを使用するには、SteamVRダッシュボードを閉じてください。",
"WAYVR_RUNNING_TITLE": "WayVR 実行中!",
"WAYVR_RUNNING_TEXT": "手首のウォッチを確認するか、表示・非表示のバインドを使用して開始してください!"
},
"WATCH": {
"ADD_NEW_SET": "新しいセットを追加",
@@ -177,7 +181,5 @@
"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ダッシュボードを閉じてください。"
}
}
+6 -4
View File
@@ -143,7 +143,11 @@
"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!"
"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ąć!"
},
"WATCH": {
"ADD_NEW_SET": "Dodaj nowy zestaw",
@@ -177,7 +181,5 @@
"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."
}
}
+6 -4
View File
@@ -143,7 +143,11 @@
"NO_SET_SELECTED": "未选择套装。",
"ONE_CONTROLLER_ON_FLOOR": "将一只控制器放在地面上!",
"NO_KEYMAPS_CONFIGURED": "尚未配置键盘布局",
"NO_KEYMAPS_CONFIGURED_HELP": "请访问 wayvr.org 了解如何设置键盘布局!"
"NO_KEYMAPS_CONFIGURED_HELP": "请访问 wayvr.org 了解如何设置键盘布局!",
"STEAMVR_DASH_TITLE": "SteamVR 控制面板已开启",
"STEAMVR_DASH_TEXT": "关闭 SteamVR 仪表板以开始使用 WayVR。",
"WAYVR_RUNNING_TITLE": "WayVR 正在运行!",
"WAYVR_RUNNING_TEXT": "查看手腕上的手表,或使用显示/隐藏快捷键开始!"
},
"WATCH": {
"ADD_NEW_SET": "添加新套装",
@@ -177,7 +181,5 @@
"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。"
}
}
+4
View File
@@ -138,6 +138,10 @@ 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(
+19 -2
View File
@@ -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(
"STEAMVR_DASH_TOAST_TITLE",
"TOAST.STEAMVR_DASH_TITLE",
)),
Translation::from_translation_key("STEAMVR_DASH_TOAST_TEXT"),
Translation::from_translation_key("TOAST.STEAMVR_DASH_TEXT"),
)
.with_timeout(300.0) // we'll zero this if dash gets hidden
.submit(&mut app);
@@ -227,6 +227,16 @@ 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);
}
}
}
@@ -260,6 +270,13 @@ 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
View File
@@ -1,4 +1,3 @@
use core::f32;
use std::sync::Arc;
use glam::Vec4;
-9
View File
@@ -339,15 +339,6 @@ 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 {
+12
View File
@@ -179,6 +179,18 @@ 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
+26 -19
View File
@@ -21,7 +21,7 @@ struct MoverData<T> {
pub(super) struct PlayspaceMover {
drag: Option<MoverData<Vec3A>>,
rotate: Option<MoverData<Quat>>,
rotate: Option<MoverData<Affine3A>>,
gravity: SpaceGravity,
playspace_state: PlayspaceState,
}
@@ -93,32 +93,37 @@ impl PlayspaceMover {
return;
}
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();
let new_hand = data.pose * app.input_state.pointers[data.hand].raw_pose;
let before_pose = data.pose;
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)
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(),
);
} 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),
);
Affine3A::from_rotation_y(rel_y)
};
let offset = (space_transform.transform_vector3a(app.input_state.hmd.translation)
- app.input_state.hmd.translation)
* -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;
space_transform.translation = offset;
data.pose *= space_transform;
data.pose.matrix3 = Mat3A::from_quat(Quat::from_affine3(&data.pose).normalize());
}
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);
}
@@ -126,6 +131,8 @@ 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() {
@@ -144,7 +151,7 @@ impl PlayspaceMover {
return;
};
let hand_pose = Quat::from_affine3(&(pose * pointer.raw_pose)).normalize();
let hand_pose = pose * pointer.raw_pose;
self.rotate = Some(MoverData {
pose,
hand: i,
+1
View File
@@ -56,6 +56,7 @@ pub enum InputTask {
#[allow(dead_code)]
pub enum OpenVrTask {
ColorGain(ColorChannel, f32),
ShowBindingsUI,
}
#[cfg(feature = "openxr")]
+2
View File
@@ -189,6 +189,7 @@ 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,
@@ -265,6 +266,7 @@ 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,
+1 -1
View File
@@ -244,7 +244,7 @@ pub(super) fn setup_custom_button<S: 'static>(
template_name: template_name.clone(),
template_params: template_params.clone(),
},
position: data.metadata.get_mouse_pos_absolute().unwrap(), //want panic
position: data.metadata.get_mouse_pos_absolute().unwrap().into(), // want panic
});
Ok(EventResult::Consumed)
}
+22 -19
View File
@@ -6,6 +6,7 @@ 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};
@@ -95,10 +96,6 @@ 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()
@@ -226,22 +223,28 @@ impl Connection {
serial: ipc::Serial,
packet_params: packet_client::WvrProcessLaunchParams,
) -> anyhow::Result<()> {
let args_vec = gen_args_vec(&packet_params.args);
let env_vec = gen_env_vec(&packet_params.env);
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 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());
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());
send_packet(
&mut self.conn,
+32 -5
View File
@@ -4,6 +4,7 @@ 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::{
@@ -20,7 +21,7 @@ use wgui::{
widget::EventResult,
};
use wlx_common::{
dash_interface::{self, ConfigChangeKind, DashInterface, DashPlayspaceTask},
dash_interface::{self, ConfigChangeKind, DashBackendTask, DashInterface, DashPlayspaceTask},
locale::WayVRLangProvider,
overlays::{BackendAttrib, BackendAttribValue},
};
@@ -43,7 +44,7 @@ use crate::{
},
},
config::{none_if_0, save_settings},
ipc::ipc_server::{gen_args_vec, gen_env_vec},
ipc::ipc_server::gen_env_vec,
state::AppState,
subsystem::hid::WheelDelta,
windowing::{
@@ -391,6 +392,10 @@ 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
@@ -436,7 +441,11 @@ impl DashInterface<AppState> for DashInterfaceLive {
) -> anyhow::Result<WvrProcessHandle> {
let wvr_server = app.wvr_server.as_mut().unwrap();
let args_vec = gen_args_vec(&params.args);
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 env_vec = gen_env_vec(&params.env);
if auto_start {
@@ -447,8 +456,8 @@ impl DashInterface<AppState> for DashInterfaceLive {
wvr_server
.spawn_process(
&params.name,
&params.exec,
&args_vec,
exec_path,
&exec_args,
&env_vec,
params.resolution,
params.pos_mode,
@@ -517,6 +526,20 @@ 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,
@@ -564,6 +587,10 @@ impl DashInterface<AppState> for DashInterfaceLive {
}
}
fn close(&mut self, _data: &mut AppState) {
RUNNING.store(false, Ordering::Relaxed);
}
fn restart(&mut self, _data: &mut AppState) {
RUNNING.store(false, Ordering::Relaxed);
RESTART.store(true, Ordering::Relaxed);
+63 -19
View File
@@ -31,6 +31,9 @@ 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>>,
@@ -664,6 +667,17 @@ 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
@@ -679,15 +693,23 @@ fn upload_image(
return None;
}
};
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;
}
};
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;
}
};
if let Err(e) = cmd_xfer.build_and_execute_now() {
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()
};
if let Err(e) = result {
log::error!("{}: Could not execute upload: {:?}", me.name, e);
return None;
}
@@ -757,8 +779,22 @@ pub(super) fn receive_callback(me: &WlxCaptureIn, frame: WlxFrame) -> Option<Wlx
}
};
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)?;
// 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,
)?;
Some(WlxCaptureOut {
image: me.gfx.create_image_view(image).ok()?,
@@ -811,6 +847,8 @@ pub(super) enum MainThreadFrame {
Cpu {
format: FrameFormat,
data: Vec<u8>,
offset: usize,
stride: usize,
mouse: Option<MouseMeta>,
},
Dmabuf {
@@ -870,12 +908,16 @@ 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(
@@ -910,22 +952,24 @@ 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) => {
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()
// 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,
)
};
Some(MainThreadFrame::Cpu {
format: frame.format,
data,
data: pixels.to_vec(),
offset: frame.offset,
stride: frame.stride,
mouse: frame.mouse,
})
}
+7 -2
View File
@@ -107,6 +107,8 @@ 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) => {
@@ -230,8 +232,11 @@ 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)
.log_err("Error while starting Whisper engine")
app.whisper_sst = match WhisperStt::new(
model_path,
app.session.config.whisper_gpu,
)
.log_err("Error while starting Whisper engine")
{
Ok(x) => Some(x),
Err(e) => {
+2 -2
View File
@@ -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,
..Default::default()
gradient_intensity: session.config.ui_gradient_intensity,
};
let dbus = DbusConnector::default();
let ipc_server = ipc_server::WayVRServer::new()?;
let mut desktop_finder = DesktopFinder::new();
let mut desktop_finder = DesktopFinder::new(params.wgui_globals.i18n().get_locale());
desktop_finder.refresh();
#[cfg(feature = "pipewire")]
+4 -2
View File
@@ -116,8 +116,10 @@ pub enum PttProgress {
}
impl WhisperStt {
pub fn new(model_path: impl AsRef<Path>) -> Result<Self, WhisperSttError> {
Self::init(WhisperSttConfig::new(model_path))
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 init(config: WhisperSttConfig) -> Result<Self, WhisperSttError> {
+6 -2
View File
@@ -320,8 +320,12 @@ impl OverlayWindowConfig {
return;
}
let to_hmd = (state.transform.translation - app.input_state.hmd.translation).normalize();
let watch_normal = state.transform.transform_vector3a(Vec3A::NEG_Z).normalize();
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 dot = to_hmd.dot(watch_normal);
let range = (app.session.config.watch_view_angle_max
+10
View File
@@ -0,0 +1,10 @@
[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
View File
@@ -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"
+2 -4
View File
@@ -107,13 +107,12 @@ pub async fn wvr_process_terminate(
#[allow(clippy::too_many_arguments)]
pub async fn wvr_process_launch(
state: &mut WayVRClientState,
exec: String,
argv: Vec<String>,
name: String,
env: Vec<String>,
resolution: [u32; 2],
pos_mode: PositionMode,
icon: Option<String>,
args: String,
userdata: HashMap<String, String>,
) {
handle_result(
@@ -123,9 +122,8 @@ pub async fn wvr_process_launch(
state.serial_generator.increment_get(),
packet_client::WvrProcessLaunchParams {
env,
exec,
argv,
name,
args,
resolution,
pos_mode,
icon,
+8 -10
View File
@@ -157,13 +157,12 @@ 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,
args,
command,
} => {
let env = env.split(",").map(|s| s.to_string()).collect::<Vec<_>>();
let resolution = resolution
@@ -181,13 +180,12 @@ async fn run_once(state: &mut WayVRClientState, args: Args) -> anyhow::Result<()
wvr_process_launch(
state,
exec,
command,
name,
env,
resolution,
pos_mode,
icon,
args,
HashMap::new(),
)
.await;
@@ -316,17 +314,17 @@ enum Subcommands {
/// Environment variables, separated by comma
#[arg(short, long, default_value = "")]
env: String,
/// Executable to run
exec: String,
#[arg(default_value = "1920x1080")]
#[arg(short, long, 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>,
/// Arguments to pass to executable
#[arg(default_value = "")]
args: String,
/// Executable and optional arguments
#[arg(required = true, num_args = 1..)]
command: Vec<String>,
},
/// Trigger haptics on the user's controller
Haptics {
+17 -3
View File
@@ -1,7 +1,21 @@
<layout version="2">
<!-- text: str -->
<template name="Cell">
<Button id="button" text="${text}" tooltip_str="${tooltip_str}" weight="bold" border="0" padding="4" color="background(transparent)" height="24" justify_content="start" />
<macro name="ContextMenuCell" tooltip_str="${tooltip_str}" weight="bold" border="0" padding_left="4" padding_right="4" color="background(transparent)" justify_content="start" min_height="28"/>
<!--
text
-->
<template name="CellButton">
<Button id="button" text="${text}" macro="ContextMenuCell"/>
</template>
<!--
text, sprite_src_builtin
-->
<template name="CellButtonSprite">
<Button id="button" macro="ContextMenuCell" gap="8">
<sprite src_builtin="${sprite_src_builtin}" width="24" height="24"/>
<label text="${text}" weight="bold"/>
</Button>
</template>
<template name="Separator">
+1 -1
View File
@@ -1,4 +1,4 @@
use std::{cell::RefCell, f32, rc::Rc};
use std::{cell::RefCell, rc::Rc};
use glam::{Mat4, Vec2, Vec3};
use taffy::prelude::{length, percent};
+18 -3
View File
@@ -139,6 +139,17 @@ 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,
@@ -149,9 +160,13 @@ 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(&staging.raw, &image.raw, [0, 0, 0], [width, height, 1]);
self.raw.copy_buffer_to_image_with_row_length(
&staging.raw,
&image.raw,
[0, 0, 0],
[width, height, 1],
buffer_row_length,
);
self.keepalive.push(Box::new(staging));
self.keepalive.push(Box::new(image.clone()));
Ok(image)
+4
View File
@@ -462,4 +462,8 @@ impl WGfx {
}
cmd::CommandBufferBuilder::new(self.clone(), QueueType::Transfer, usage, PhantomData)
}
pub fn enumerate_devices(&self) -> anyhow::Result<Vec<String>> {
self.raw.enumerate_devices()
}
}
+31 -1
View File
@@ -1687,6 +1687,25 @@ 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 {
@@ -2475,11 +2494,22 @@ 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(0)
.buffer_row_length(buffer_row_length)
.buffer_image_height(0)
.image_subresource(vk::ImageSubresourceLayers {
aspect_mask: vk::ImageAspectFlags::COLOR,
+10
View File
@@ -69,6 +69,16 @@ 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
View File
@@ -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.content_size,
None => self.prev_size,
};
if !self.state.tree.dirty(self.tree_root_node)? && self.prev_size == size {
+8 -2
View File
@@ -374,11 +374,15 @@ impl ParserState {
'children: for child in el_context_menu.children() {
match child.tag_name().name() {
"" => {}
"separator" => {
cells.push(context_menu::Cell::Separator);
}
"cell" => {
let mut title: Option<Translation> = None;
let mut tooltip: Option<Translation> = None;
let mut action_name: Option<Rc<str>> = None;
let mut attribs = Vec::<AttribPair>::new();
let mut sprite_src_builtin: Option<Rc<str>> = None;
for attrib in child.attributes() {
let (key, value) = (attrib.name(), attrib.value());
@@ -389,6 +393,7 @@ impl ParserState {
"tooltip" => tooltip = Some(Translation::from_translation_key(value)),
"tooltip_str" => tooltip = Some(Translation::from_raw_text(value)),
"action" => action_name = Some(value.into()),
"sprite_src_builtin" => sprite_src_builtin = Some(value.into()),
"skip" => {
let resolved = replace_vars(value, template_params);
//FIXME: this is always empty
@@ -406,12 +411,13 @@ impl ParserState {
}
let title = title.context("No text/translation provided")?;
cells.push(context_menu::Cell {
cells.push(context_menu::Cell::Button(context_menu::Button {
title,
tooltip,
action_name,
sprite_src_builtin,
attribs,
});
}));
}
other => {
anyhow::bail!("{:?}: unexpected <{other}> tag", self.xml_path.get_path().display());
+114 -29
View File
@@ -1,9 +1,10 @@
use crate::{
assets::AssetPathRef,
components::{ComponentTrait, button::ComponentButton},
drawing::Boundary,
globals::WguiGlobals,
i18n::Translation,
layout::Layout,
layout::{Layout, WidgetID},
parser::{self, Fetchable, ParserState, TemplateParams},
task::Tasks,
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
@@ -11,14 +12,21 @@ use crate::{
use glam::Vec2;
use std::rc::Rc;
#[derive(Clone)]
pub struct Cell {
#[derive(Default, Clone)]
pub struct Button {
pub title: Translation,
pub tooltip: Option<Translation>,
pub action_name: Option<Rc<str>>,
pub sprite_src_builtin: Option<Rc<str>>,
pub attribs: Vec<parser::AttribPair>,
}
#[derive(Clone)]
pub enum Cell {
Button(Button),
Separator,
}
pub enum Blueprint {
Cells(Vec<Cell>),
Template {
@@ -27,9 +35,47 @@ pub enum Blueprint {
},
}
#[derive(Clone)]
pub enum Position {
Unspecified,
Absolute(Vec2),
Widget(WidgetID),
Boundary(Boundary),
}
// absolute position
impl From<Vec2> for Position {
fn from(value: Vec2) -> Self {
Self::Absolute(value)
}
}
impl From<Option<Vec2>> for Position {
fn from(value: Option<Vec2>) -> Self {
match value {
Some(v) => Self::Absolute(v),
None => Self::Unspecified,
}
}
}
// relative to widget center
impl From<WidgetID> for Position {
fn from(value: WidgetID) -> Self {
Self::Widget(value)
}
}
// relative to absolute rectangle center
impl From<Boundary> for Position {
fn from(value: Boundary) -> Self {
Self::Boundary(value)
}
}
pub struct OpenParams {
pub on_custom_attribs: Option<parser::OnCustomAttribsFunc>,
pub position: Vec2,
pub position: Position,
pub blueprint: Blueprint,
}
@@ -89,14 +135,24 @@ impl ContextMenu {
Blueprint::Cells(cells) => cells,
};
let absolute_pos = match params.position {
Position::Unspecified => Vec2::new(0.0, 0.0),
Position::Absolute(pos) => pos,
Position::Boundary(boundary) => boundary.center(),
Position::Widget(widget_id) => {
let widget = layout.state.widgets.fetch(widget_id)?;
widget.state().data.cached_absolute_boundary.center()
}
};
let root_size = layout.state.get_widget_size(layout.content_root_widget);
let (window_pos, placement) = if params.position.y < root_size.y / 2.0 {
(params.position, WguiWindowPlacement::TopLeft)
let (window_pos, placement) = if absolute_pos.y < root_size.y / 2.0 {
(absolute_pos, WguiWindowPlacement::TopLeft)
} else {
// invert y axis (position.y is counted from the bottom)
(
Vec2::new(params.position.x, root_size.y - params.position.y),
Vec2::new(absolute_pos.x, root_size.y - absolute_pos.y),
WguiWindowPlacement::BottomLeft,
)
};
@@ -121,33 +177,62 @@ impl ContextMenu {
let id_buttons = inner_parser.get_widget_id("buttons")?;
for (idx, cell) in cells.iter().enumerate() {
let mut par = TemplateParams::new();
par.insert_rc("text", cell.title.generate(&mut globals.i18n()));
if let Some(tooltip) = cell.tooltip.as_ref() {
par.insert_rc("tooltip_str", tooltip.generate(&mut globals.i18n()));
let mut has_sprites = false;
for cell in &cells {
if let Cell::Button(button) = cell
&& button.sprite_src_builtin.is_some()
{
has_sprites = true;
}
}
let mut data_cell = inner_parser.parse_template_only(&doc_params, "Cell", layout, id_buttons, par)?;
for cell in &cells {
match cell {
Cell::Button(cell) => {
let mut par = TemplateParams::new();
par.insert_rc("text", cell.title.generate(&mut globals.i18n()));
let button = data_cell.fetch_component_as::<ComponentButton>("button")?;
let button_id = button.base().get_id();
parser_state.data.take_results_from(&mut data_cell);
self
.tasks
.handle_button(&button, Task::ActionClicked(cell.action_name.clone()));
if has_sprites {
par.insert_rc(
"sprite_src_builtin",
cell
.sprite_src_builtin
.clone()
.unwrap_or_else(|| Rc::from("@/dashboard/dot.svg")),
);
}
if let Some(tooltip) = &cell.tooltip {
par.insert_rc("tooltip_str", tooltip.generate(&mut globals.i18n()));
}
if let Some(c) = params.on_custom_attribs.as_ref() {
(*c)(parser::CustomAttribsInfo {
pairs: &cell.attribs,
parent_id: id_buttons,
widget_id: button_id,
widgets: &layout.state.widgets,
});
}
let mut data_cell = inner_parser.parse_template_only(
&doc_params,
if has_sprites { "CellButtonSprite" } else { "CellButton" },
layout,
id_buttons,
par,
)?;
if idx < cells.len() - 1 {
inner_parser.realize_template(&doc_params, "Separator", layout, id_buttons, Default::default())?;
let button = data_cell.fetch_component_as::<ComponentButton>("button")?;
let button_id = button.base().get_id();
parser_state.data.take_results_from(&mut data_cell);
self
.tasks
.handle_button(&button, Task::ActionClicked(cell.action_name.clone()));
if let Some(c) = params.on_custom_attribs.as_ref() {
(*c)(parser::CustomAttribsInfo {
pairs: &cell.attribs,
parent_id: id_buttons,
widget_id: button_id,
widgets: &layout.state.widgets,
});
}
}
Cell::Separator => {
inner_parser.realize_template(&doc_params, "Separator", layout, id_buttons, Default::default())?;
}
}
}
Ok(())
+3 -1
View File
@@ -31,7 +31,9 @@ wayland-protocols = {
drm-fourcc = "2.2.0"
rxscreen = {
version = "0.1.7",
git = "https://github.com/galister/rxscreen.git",
rev = "7197e1de6622b65859e6f2060b5750185e0dad2a",
# version = "0.1.7",
features = [
"mouse",
"shm",
+2
View File
@@ -128,6 +128,8 @@ pub struct MemPtrFrame {
pub format: FrameFormat,
pub ptr: usize,
pub size: usize,
pub offset: usize,
pub stride: usize,
pub mouse: Option<MouseMeta>,
}
+16 -2
View File
@@ -376,10 +376,24 @@ where
}
}
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: datas[0].as_raw().data as _,
size: datas[0].chunk().size() as _,
ptr: raw.data as _,
size,
offset,
stride: chunk.stride() as _,
mouse: mouse_meta,
};
+1
View File
@@ -262,6 +262,7 @@ where
&client.queue_handle,
(),
);
params.destroy();
log::trace!("{name}: ScreenCopy with Dmabuf");
// copy_with_damage seems to not work here
+2
View File
@@ -109,6 +109,8 @@ 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 {
+1 -1
View File
@@ -24,6 +24,7 @@ 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 = {
@@ -31,6 +32,5 @@ rodio = {
default-features = false,
features = ["hound", "mp3", "playback", "recording"]
}
rust-ini = "0.21.3"
walkdir = "2.5.0"
wgui = { path = "../wgui/" }
+3
View File
@@ -533,6 +533,9 @@ 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>,
+9 -1
View File
@@ -45,6 +45,11 @@ pub enum DashPlayspaceTask {
ResetCenter,
}
#[derive(Clone, Copy)]
pub enum DashBackendTask {
ShowBindingsUI,
}
#[derive(Clone, Copy)]
pub struct InterfaceFeats {
pub xr_backend: XrBackend,
@@ -92,14 +97,17 @@ 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, mode: DashPlayspaceTask) -> anyhow::Result<()>;
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 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);
fn restart(&mut self, data: &mut T);
fn close(&mut self, data: &mut 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)]
+19 -9
View File
@@ -1,12 +1,15 @@
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, DashInterface, DashPlayspaceTask},
dash_interface::{self, ConfigChangeKind, DashBackendTask, DashInterface, DashPlayspaceTask},
desktop_finder::DesktopFinder,
};
@@ -66,10 +69,11 @@ pub struct DashInterfaceEmulated {
brightness: f32,
debug_frame_id: u64,
debug_counter: u64,
gfx: Arc<WGfx>,
}
impl DashInterfaceEmulated {
pub fn new() -> Self {
pub fn new(gfx: Arc<WGfx>, locale: &Locale) -> Self {
let mut processes = DenseSlotMap::<EmuProcessHandle, EmuProcess>::default();
let process_handle = processes.insert(EmuProcess {
name: String::from("My app"),
@@ -81,7 +85,7 @@ impl DashInterfaceEmulated {
visible: true,
});
let mut desktop_finder = DesktopFinder::new();
let mut desktop_finder = DesktopFinder::new(locale);
desktop_finder.refresh();
// Use serde defaults
@@ -129,17 +133,16 @@ impl DashInterfaceEmulated {
brightness: 1.0,
debug_counter: 0,
debug_frame_id: 0,
gfx,
}
}
}
impl Default for DashInterfaceEmulated {
fn default() -> Self {
Self::new()
}
}
impl DashInterface<()> for DashInterfaceEmulated {
fn enumerate_vulkan_devices(&mut self, _: &mut ()) -> anyhow::Result<Vec<String>> {
self.gfx.enumerate_devices()
}
fn window_list(&mut self, _: &mut ()) -> anyhow::Result<Vec<WvrWindow>> {
Ok(self.windows.iter().map(|(handle, w)| w.to(handle)).collect())
}
@@ -219,6 +222,11 @@ 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
}
@@ -231,6 +239,8 @@ impl DashInterface<()> for DashInterfaceEmulated {
fn restart(&mut self, _data: &mut ()) {}
fn close(&mut self, _data: &mut ()) {}
fn toggle_dashboard(&mut self, _data: &mut ()) {}
fn get_feats(&mut self, _data: &mut ()) -> dash_interface::InterfaceFeats {
+67 -86
View File
@@ -10,37 +10,35 @@ use std::{
time::Instant,
};
use crate::cache_dir;
use crate::{cache_dir, locale::Language};
use anyhow::Context;
use ini::Ini;
use freedesktop_desktop_entry::DesktopEntry as FreedesktopDesktopEntry;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use wgui::i18n::Locale;
struct DesktopEntryOwned {
app_id: String,
exec_path: String,
exec_args: String,
app_name: String,
icon_path: Option<String>,
categories: Vec<String>,
app_id: Box<str>,
exec_argv: Box<[Box<str>]>,
app_name: Box<str>,
icon_path: Option<Box<str>>,
categories: Box<[Box<str>]>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DesktopEntry {
pub exec_path: Rc<str>,
pub exec_args: Rc<str>,
pub exec_argv: Rc<[Rc<str>]>,
pub app_id: Rc<str>,
pub app_name: Rc<str>,
pub icon_path: Option<Rc<str>>,
pub categories: Vec<Rc<str>>,
pub categories: Rc<[Rc<str>]>,
}
impl From<DesktopEntryOwned> for DesktopEntry {
fn from(value: DesktopEntryOwned) -> Self {
Self {
app_id: value.app_id.into(),
exec_path: value.exec_path.into(),
exec_args: value.exec_args.into(),
exec_argv: value.exec_argv.into_iter().map(|x| x.into()).collect(),
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(),
@@ -60,17 +58,18 @@ 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, DesktopEntry>,
entry_cache: HashMap<String, Rc<DesktopEntry>>,
bg_task: Option<JoinHandle<HashMap<String, DesktopEntryOwned>>>,
}
impl DesktopFinder {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
pub fn new(locale: &Locale) -> Self {
let xdg = xdg::BaseDirectories::new();
let mut app_folders = vec![];
@@ -107,11 +106,21 @@ 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,
@@ -146,79 +155,53 @@ impl DesktopFinder {
}
let file_name = entry.file_name().to_string_lossy();
let Some(app_id) = Path::new(entry.file_name())
.file_stem()
.map(|x| x.to_string_lossy().to_string())
else {
continue;
};
let file_path = entry.path().display().to_string();
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,
let desktop = match FreedesktopDesktopEntry::from_path(entry.path(), Some(&params.langs)) {
Ok(desktop) => desktop,
Err(e) => {
log::debug!("Failed to read INI for .desktop file {}: {:?}, skipping", file_path, e);
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
continue;
}
};
let Some(section) = ini.section(Some("Desktop Entry")) else {
if desktop.groups.desktop_entry().is_none() {
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
}
if let Some(x) = section.get("Terminal")
&& x == "true"
{
// prefer entries from earlier data directories
let app_id = desktop.id();
if known_files.contains(app_id) {
continue;
}
if let Some(x) = section.get("NoDisplay")
&& x.eq_ignore_ascii_case("true")
{
continue; // This application is hidden
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
continue;
}
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 {
let Some(exec) = desktop.exec() else {
log::debug!("Failed to get desktop entry Exec for file {}, skipping", file_path);
continue;
};
for entry in &CMD_BLOCKLIST {
if exec.contains(entry) {
continue 'entries;
}
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
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 exec_argv = desktop
.parse_exec_with_uris(&[], &params.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 Some(app_name) = section.get("Name") else {
let Some(app_name) = desktop.name(&params.langs) else {
log::debug!(
"Failed to get desktop entry application name for file {}, skipping",
file_path
@@ -226,33 +209,31 @@ impl DesktopFinder {
continue;
};
let icon_path = section
.get("Icon")
let icon_path = desktop
.icon()
.and_then(|icon_name| Self::find_icon(&params, icon_name))
.or_else(|| Self::create_icon(&file_name).ok());
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));
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());
}
}
known_files.insert(file_name.to_string());
known_files.insert(app_id.to_owned());
entries.insert(
app_id.clone(),
app_id.to_owned(),
DesktopEntryOwned {
app_id,
app_name: String::from(app_name),
exec_path: String::from(exec_path),
exec_args: exec_args.join(" "),
icon_path,
categories: vec_categories,
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(),
},
);
}
@@ -331,15 +312,15 @@ impl DesktopFinder {
self.entry_cache.clear();
for (app_id, entry) in entries {
self.entry_cache.insert(app_id, entry.into());
self.entry_cache.insert(app_id, Rc::new(entry.into()));
}
}
pub fn get_cached_entry(&self, app_id: &str) -> Option<&DesktopEntry> {
pub fn get_cached_entry(&self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
self.entry_cache.get(app_id)
}
pub fn find_entries(&mut self) -> HashMap<String, DesktopEntry> {
pub fn find_entries(&mut self) -> HashMap<String, Rc<DesktopEntry>> {
self.wait_for_entries();
self.entry_cache.clone()
}
+110 -1
View File
@@ -92,6 +92,112 @@ 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
@@ -100,7 +206,10 @@ pub fn load_xr_input_profiles() -> Vec<OpenXrInputProfile> {
};
match serde_json5::from_str::<Vec<OpenXrInputProfile>>(&conf) {
Ok(override_profiles) => {
Ok(mut override_profiles) => {
for new in override_profiles.iter_mut() {
normalize_profile(new);
}
for new in override_profiles {
if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) {
profiles[i] = new;