Compare commits

...
Author SHA1 Message Date
galister 03d534188e fix dashboard spawning too far 2026-10-29 03:05:18 +09:00
galister e1fc831934 update smithay 2026-10-29 03:05:18 +09:00
galister e2fa349021 not rendered spam 2026-10-29 03:05:18 +09:00
galister 86689759c2 wvr_server: diag logs 2026-10-29 03:05:18 +09:00
galister 75175a2264 cursor fixes 2026-10-29 03:05:18 +09:00
galister 69b930141d cursor from surface 2026-10-29 00:04:27 +09:00
galister 5ef2fe7be3 floating is default 2026-10-28 23:32:29 +09:00
galister 73d41680ff clippy 2026-10-28 23:23:05 +09:00
galister c955afd466 xdg-icon support 2026-10-28 23:23:05 +09:00
galister fb971a7999 window persistence 2026-10-28 21:46:00 +09:00
galister 1d0a86ee65 rework floating 2026-10-28 18:39:47 +09:00
galister f1ed8cd11c fix closed apps losing overlay state 2026-10-28 18:39:47 +09:00
galister 6e8643e566 fix headless mode dummy screen & restart propagation 2026-10-28 18:39:47 +09:00
galister 8de6380ad9 watch desktop entry changes 2026-10-28 18:39:47 +09:00
galister a65b33fed2 openvr: fix scrolling 2026-10-28 14:41:12 +09:00
galister 786903efd5 wvr_server: process matching tweaks 2026-10-28 14:37:24 +09:00
Aleksander 466960d32a Add contribution instructions regarding LLM usage policy [skip ci] 2026-10-27 12:13:34 +09:00
Aleksander abafddefd8 add Swedish translation strings 2026-10-24 23:31:49 +02:00
Jonatan Nyberg 9b1fc2774a Add Swedish template to the translation tool (#701) 2026-10-24 22:48:43 +02:00
galister c539ff6a90 XSHM & PipeWire CPU capture fixes (#698) 2026-10-23 21:13:08 +09:00
64 changed files with 3522 additions and 689 deletions

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
# Contributing
Contributions are warmly welcome. Before you start, please read the entire document carefully.
## AI Policy
Do not use LLMs to generate substantial pieces of code. You need to fully understand every line of code you change. No exceptions.
Commits, issues and pull requests must be made by humans. Co-authoring by AI is not allowed.
Looking to create an issue on the project, such as a feature request or bug report? Focus on writing what you know for sure in simple terms. We'll figure it out from there.
Usage of Gen-AI for visuals and audio is prohibited.
The only exception is LLM-translated strings generated by the scripts/translator tool.
## Basics
Before committing, make sure to format your Rust code by running:
```
cargo fmt
```
You should also check for any compiler warnings too:
```
cargo check
```
or even better:
```
cargo clippy
```
## Large contributions
If you want to submit a relatively big pull requests, it's highly advisable to create a separate issue request for it. That way we can discuss the approach and sort out the details first.
## Adding translations
Look at `scripts/translator/run.sh` for more information.
## Licensing
WayVR is developed and published under [GPL-3 License](https://www.gnu.org/licenses/gpl-3.0.en.html).
Generated
+116 -53
View File
@@ -213,15 +213,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e149dc73cd30538307e7ffa2acd3d2221148eaeed4871f246657b1c3eaa1cbd2"
[[package]]
name = "approx"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278"
dependencies = [
"num-traits",
]
[[package]]
name = "aquamarine"
version = "0.1.12"
@@ -430,12 +421,6 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atomic_float"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a"
[[package]]
name = "atty"
version = "0.2.14"
@@ -812,14 +797,13 @@ dependencies = [
[[package]]
name = "calloop"
version = "0.14.4"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7"
checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e"
dependencies = [
"bitflags 2.13.0",
"polling",
"rustix 1.1.4",
"slab",
"tracing",
]
@@ -841,7 +825,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
dependencies = [
"calloop 0.14.4",
"calloop 0.14.5",
"rustix 1.1.4",
"wayland-backend",
"wayland-client",
@@ -896,16 +880,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "cgmath"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317"
dependencies = [
"approx",
"num-traits",
]
[[package]]
name = "chacha20"
version = "0.10.1"
@@ -1410,7 +1384,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"glam",
"glam 0.30.10",
"keyvalues-parser",
"log",
"lz4_flex",
@@ -1643,6 +1617,26 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "enumset"
version = "1.1.13"
@@ -2179,6 +2173,12 @@ dependencies = [
"serde_core",
]
[[package]]
name = "glam"
version = "0.33.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37bfe73dc8ec21f54d181e5a60554327ec3f1c600c455a75ccbfc65d2106a6c4"
[[package]]
name = "glob"
version = "0.3.3"
@@ -2479,6 +2479,26 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
[[package]]
name = "inotify"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592"
dependencies = [
"bitflags 2.13.0",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
dependencies = [
"libc",
]
[[package]]
name = "input"
version = "0.9.1"
@@ -4125,7 +4145,7 @@ version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
dependencies = [
"heck 0.4.1",
"heck 0.5.0",
"itertools 0.14.0",
"log",
"multimap",
@@ -4531,6 +4551,18 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "reis"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b967ec6489a42067a20724f11987bb96178ea796873cbe1afd25fb93ee6f85f"
dependencies = [
"calloop 0.14.5",
"enumflags2",
"log",
"rustix 1.1.4",
]
[[package]]
name = "resvg"
version = "0.47.0"
@@ -4773,8 +4805,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",
]
@@ -5109,27 +5140,27 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "smithay"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "740cea6927892bc182d5bf70c8f79806c8bc9f68f2fb96e55a30be171b63af98"
source = "git+https://github.com/Smithay/smithay.git?rev=0d66849533b475c40c0c461ec182d0f939424452#0d66849533b475c40c0c461ec182d0f939424452"
dependencies = [
"appendlist",
"ash",
"atomic_float",
"bitflags 2.13.0",
"calloop 0.14.4",
"cgmath",
"calloop 0.14.5",
"cursor-icon",
"downcast-rs",
"drm-fourcc",
"encoding_rs",
"errno",
"glam 0.33.12",
"indexmap 2.14.0",
"libc",
"portable-atomic",
"profiling",
"rand 0.9.4",
"rand 0.10.2",
"reis",
"rustix 1.1.4",
"scopeguard",
"sha2",
"siphasher",
"smallvec",
"tempfile",
"thiserror 2.0.18",
@@ -5138,8 +5169,8 @@ dependencies = [
"wayland-protocols-misc",
"wayland-protocols-wlr",
"wayland-server",
"x11rb",
"xkbcommon",
"x11rb 0.14.0",
"xkbcommon 0.9.0",
]
[[package]]
@@ -5175,7 +5206,7 @@ checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
dependencies = [
"bitflags 2.13.0",
"bytemuck",
"calloop 0.14.4",
"calloop 0.14.5",
"calloop-wayland-source 0.4.1",
"cursor-icon",
"libc",
@@ -5193,7 +5224,7 @@ dependencies = [
"wayland-protocols-misc",
"wayland-protocols-wlr",
"wayland-scanner",
"xkbcommon",
"xkbcommon 0.8.0",
"xkeysym",
]
@@ -5460,7 +5491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.52.0",
@@ -5793,7 +5824,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"dash-frontend",
"glam",
"glam 0.30.10",
"log",
"rust-embed",
"tracing-subscriber",
@@ -6211,7 +6242,7 @@ version = "26.8.0"
dependencies = [
"anyhow",
"bytes",
"calloop 0.14.4",
"calloop 0.14.5",
"calloop-wayland-source 0.4.1",
"chrono",
"chrono-tz",
@@ -6221,9 +6252,10 @@ dependencies = [
"dbus",
"evdev",
"futures",
"glam",
"glam 0.30.10",
"idmap",
"idmap-derive",
"image",
"image_dds",
"input",
"input-linux",
@@ -6263,6 +6295,7 @@ dependencies = [
"wayland-client",
"wayland-protocols",
"wayland-protocols-misc",
"wayland-scanner",
"wayvr-ipc",
"wgui",
"whisper-rs",
@@ -6271,7 +6304,7 @@ dependencies = [
"wlx-common",
"xcb",
"xdg 3.0.0",
"xkbcommon",
"xkbcommon 0.8.0",
]
[[package]]
@@ -6352,7 +6385,7 @@ dependencies = [
"dav1d-sys",
"etagere",
"flate2",
"glam",
"glam 0.30.10",
"image",
"log",
"lru",
@@ -6813,7 +6846,7 @@ dependencies = [
"web-time",
"windows-sys 0.52.0",
"x11-dl",
"x11rb",
"x11rb 0.13.2",
"xkbcommon-dl",
]
@@ -6856,11 +6889,13 @@ dependencies = [
"anyhow",
"chrono",
"freedesktop-desktop-entry",
"glam",
"glam 0.30.10",
"glob",
"identicons-svg",
"idmap",
"idmap-derive",
"inotify",
"libc",
"log",
"rodio",
"serde",
@@ -6907,7 +6942,18 @@ dependencies = [
"libloading 0.8.9",
"once_cell",
"rustix 1.1.4",
"x11rb-protocol",
"x11rb-protocol 0.13.2",
]
[[package]]
name = "x11rb"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a8885a854a8bfdf87a301e53e41b17c5f8f33639903131338b997b1eb614f44"
dependencies = [
"gethostname",
"rustix 1.1.4",
"x11rb-protocol 0.14.0",
]
[[package]]
@@ -6916,6 +6962,12 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "x11rb-protocol"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acf4d1bc32aa46eec18caa634ec3cf4c05bfa151f12b93b510b15190f69a1ca8"
[[package]]
name = "xcb"
version = "1.7.0"
@@ -6958,6 +7010,17 @@ dependencies = [
"xkeysym",
]
[[package]]
name = "xkbcommon"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a974f48060a14e95705c01f24ad9c3345022f4d97441b8a36beb7ed5c4a02d"
dependencies = [
"libc",
"memmap2",
"xkeysym",
]
[[package]]
name = "xkbcommon-dl"
version = "0.4.2"
+1
View File
@@ -23,6 +23,7 @@ dbus = { version = "0.9.12" }
glam = { version = "0.30.9", features = ["mint", "serde"] }
idmap = "0.2.2"
idmap-derive = "0.2.22"
image = { version = "0.25.10", default-features = false, features = ["png"] }
interprocess = { version = "2.2.3" }
libc = "0.2.178"
log = "0.4.29"
+384
View File
@@ -0,0 +1,384 @@
{
"DEFINITIONS": {
"ANCHORED": "Förankrad",
"FLOATING": "Flytande",
"STATIC": "Stationär",
"SET": "Set",
"WATCH": "Klocka",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"SETTINGS": "Inställningar",
"KEYBOARD": "Tangentbord",
"TASKBAR": "Uppgiftsfält",
"FEATURES": "Funktioner",
"OVERLAY": "Overlay",
"SKYMAP": "Skymap",
"PLAYSPACE": "Playspace",
"D_PAD": "D-pad"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Centrera om playspace"
},
"APP_LAUNCHER": {
"ASPECT": {
"SEMI_TALL": "Halvhög",
"SEMI_WIDE": "Halvbredd",
"SQUARE": "Kvadratisk",
"TALL": "Hög",
"WIDE": "Bredbild"
},
"ASPECT_TITLE": "Bildförhållande",
"AUTOSTART": "Kör automatiskt vid start",
"LAUNCH": "Starta",
"MODE": {
"CAGE": "Kompatibilitetsläge (Cage)",
"NATIVE": "Nativt läge"
},
"RES_TITLE": "Upplösning"
},
"APP_SETTINGS": {
"ALLOW_SLIDING": "Stick-interaktion vid grepp",
"ANIMATION_SPEED": "Hastighet för UI-animationer",
"AUTOSTART_APPS": "Appar som ska köras vid start",
"BLOCK_GAME_INPUT": "Blockera spelindata",
"BLOCK_GAME_INPUT_HELP": "Blockerar all input när en overlay hovras över",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignorera klockan vid blockering av input",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Blockera inte inmatning när klockan är markerad",
"BLOCK_POSES_ON_KBD_INTERACTION": "Blockera poser vid interaktion med tangentbord",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blockerar spelets mottagning av poser när tangentbordet hovras över och 'Blockera spel-input' är aktiverat",
"BINDINGS": {
"COMP": {
"CLICK": "Klick",
"FORCE": "Tryckkraft",
"TOUCH": "Beröring",
"VALUE": "Värde",
"PROXIMITY": "Närhet",
"X_AXIS": "X-axel",
"Y_AXIS": "Y-axel"
},
"TYPE": {
"BUMPER": "Bumper",
"DPAD_UP": "D-pad upp",
"DPAD_DOWN": "D-pad ned",
"DPAD_LEFT": "D-pad vänster",
"DPAD_RIGHT": "D-pad höger",
"JOYSTICK": "Joystick",
"MENU": "Meny",
"SHOULDER": "Axel",
"SQUEEZE": "Grepp",
"SYSTEM": "System",
"THUMBREST": "Tumstöd",
"THUMBSTICK": "Thumbstick",
"TRACKPAD": "Trackpad",
"TRIGGER": "Trigger",
"VIEW": "Visa"
},
"CLICK": {
"ANY": "Vilken som helst",
"DOUBLE": "Dubbelklicka",
"TRIPLE": "Trippelklick",
"TYPE": "Antal tryck"
},
"ACTION": {
"CLICK": "Klicka*",
"GRAB": "Greppa*",
"ALT_CLICK": "Anpassat shell-kommando (ställ in alt_click i config)",
"SHOW_HIDE": "Visa, dölj, centrera om*",
"TOGGLE_DASHBOARD": "Växla instrumentpanel",
"SPACE_DRAG": "Dra playspace",
"SPACE_ROTATE": "Rotera playspace",
"SPACE_RESET": "Återställ Playspace",
"CLICK_MODIFIER_RIGHT": "Högerklick-modifierare",
"CLICK_MODIFIER_MIDDLE": "Modifierare för mittklick",
"MOVE_MOUSE": "Flytta mus (om avstängd som standard)",
"SCROLL": "Scrolla*"
},
"LEFT": "Vänster",
"RIGHT": "Höger",
"CHORD": "Kombination",
"CHORD_ADD_INPUT": "Lägg till inmatning för kombination",
"CHORD_INPUT_HAND": "Inmatningshand",
"COMPONENT": "Aktiveringstyp",
"SUBPATH": "Aktiverande kontroll",
"THRESHOLD": "Aktivera vid övre värdet\nAvaktivera vid nedre värdet"
},
"BROWSE_ONLINE_CATALOG": "Bläddra i onlinekatalogen...",
"BROWSE_SKYMAPS": "Bläddra bland skymaps",
"CAPTURE_METHOD": "Metod för skärmavbildning",
"CAPTURE_METHOD_HELP": "Prova att ändra detta om du\nupplever svarta eller glitchiga skärmar",
"COLOR_KEYING": "Färgnyckelning",
"COLOR_PALETTE": "Färgpalett",
"COLOR_PALETTE_HELP": "Klicka för att bläddra bland färgpaletter.\nSe webbplatsen för att skapa egna!",
"COLOR_PALETTES": "Färgpaletter",
"COLOR_PALETTE_CURRENT": "Nuvarande aktivt tema.",
"COLOR_PALETTE_CUSTOM_URL": "Lägg till ett eget anpassat tema!",
"COLOR_PALETTE_CUSTOM_URL2": "Klicka för att öppna webbmanualen.",
"COLOR_PALETTE_ACTIVATE": "Klicka för att använda detta tema.",
"CLEAR_PIPEWIRE_TOKENS": "Rensa PipeWire-tokens",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Fråga om skärmval vid nästa start",
"CLEAR_SAVED_STATE": "Återställ sparade inställningar",
"CLEAR_SAVED_STATE_HELP": "Återställ sets och overlay-positioner",
"CLICK_FREEZE_TIME_MS": "Klickfrysningstid (ms)",
"CLICK_FREEZE_TIME_MS_HELP": "Hjälper till med precision vid dubbelklick",
"CLOCK_12H": "12-timmarsklocka",
"CONTROLS": "Kontroller",
"DELETE_ALL_CONFIGS": "Rensa konfiguration",
"DELETE_ALL_CONFIGS_HELP": "Ta bort alla konfigurationsfiler från conf.d",
"DOUBLE_CURSOR_FIX": "Fix för dubbla markörer",
"DOUBLE_CURSOR_FIX_HELP": "Aktivera om du ser två markörer",
"DEFAULT_OVERLAY_SCALE": "Standardstorlek för overlay",
"DEFAULT_OVERLAY_SCALE_HELP": "Skapa nya overlays i den här skalan.",
"DEFAULT_CURVATURE": "Standardkurvatur för overlay",
"DEFAULT_OPACITY": "Standardopacitet för overlay",
"DEFAULT_POSITIONING": "Standardposition för overlay",
"OVERLAY_DEFAULTS_HELP": "Gäller endast nya overlays",
"ENABLED": "Aktiverad",
"FEATURES": "Funktioner",
"FOCUS_FOLLOWS_MOUSE_MODE": "Musrörelse vid trigger-tryck",
"HANDSFREE_POINTER": "Handsfree-läge",
"HANDSFREE_POINTER_HELP": "Inmatning som används när rörelsekontroller saknas.\nVänster nyp är greppa, höger är klicka.",
"HANDSFREE_ALT_TAB": "Sikte för app-växlare",
"HANDSFREE_ALT_TAB_HELP": "Håll in Super+X, sikta på önskat fönster och släpp sedan för att byta fokus.",
"DIAGNOSTICS": {
"CPU_ACCESSIBLE_VRAM": "CPU-tillgängligt VRAM: {NUM} MiB",
"TITLE": "Diagnostik",
"ENABLE_REBAR": "Aktivera [resizable bar] i BIOS-inställningarna för optimal VR-prestanda"
},
"DIAGNOSTICS_REASON": {
"PACTL": "Krävs för kontroll av högtalare/mikrofon och byte av ljudkort",
"CAGE": "Rekommenderas vid inkompatibilitet med vissa X11-applikationer (kompatibilitetsläge)",
"XWAYLAND_SATELLITE": "Rekommenderas om du vill köra X11 (Xwayland)-applikationer i nativt läge",
"XDG_OPEN": "Krävs för att öppna URL:er eller Steam-spel"
},
"HIDE_GRAB_HELP": "Dölj grepphjälp",
"HIDE_USERNAME": "Dölj användarnamn",
"INPUT_EMULATION_METHOD": "Metod för inmatningssimulering",
"INPUT_EMULATION_METHOD_HELP": "Prova att ändra detta om mus- eller\ntangentbordindata inte registreras.",
"INPUT_CAPTURE_METHOD": "Metod för inmatningsfångst",
"INPUT_CAPTURE_METHOD_HELP": "Möjliggör användning av fysisk kringutrustning i WayVR.\n Super+Z: Växla input mellan WayVR och skrivbordet\n Super+X: Växla mellan WayVR-appar",
"INPUT_PROFILES": "Ändra styrinställningar",
"INPUT_PROFILES_HELP": "OpenXR-kontrollbindningar",
"INVERT_SCROLL_DIRECTION_X": "Invertera horisontell scrollriktning",
"INVERT_SCROLL_DIRECTION_Y": "Invertera vertikal rullningsriktning",
"LENS_SEPARATION_MM": "Linsavstånd (mm)",
"KEYBOARD_MIDDLE_CLICK": "Tangentbordets mittklick",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier som används vid skrivning\nmed lila laser",
"KEYBOARD_SOUND_ENABLED": "Tangentbordsljud",
"LANGUAGE": "Språk",
"LEFT_HANDED_MOUSE": "Vänstermus",
"LEFT_HANDED_MOUSE_HELP": "Används om musknapparna är ombytta",
"LONG_PRESS_DURATION": "Långtryckstid",
"LOOK_AND_FEEL": "Utseende och känsla",
"MISC": "Övrigt",
"MIDDLE_GRABS_SINGLE": "Modifierare för mittklick greppar enskilda objekt",
"MIDDLE_GRABS_SINGLE_HELP": "Att greppa med modifieraren för mittklick (lila laser)\ngreppar enskilda overlays istället för hela setet",
"NOTIFICATIONS_ENABLED": "Aktivera aviseringar",
"NOTIFICATIONS_SOUND_ENABLED": "Ljud för aviseringar",
"NOT_SUPPORTED": "Ej stöds",
"NO_SKYMAPS_FOUND": "Inga skymaps hittades",
"OPAQUE_BACKGROUND": "Opak bakgrund",
"OPTION": {
"ANCHORED_HELP": "Följer centermarkören. Rekommenderas.",
"FLOATING_HELP": "Flyttas oberoende, centreras om.\nLätt att tappa bort; rekommenderas ej.",
"STATIC_HELP": "Håller sig på plats i förhållande till världen.",
"AUTO": "Automatisk",
"AUTO_HELP": "ScreenCopy GPU om det stöds,\nannars PipeWire GPU.",
"EYE_ONLY": "Ögonlaser",
"EYE_PINCH": "Öga + pinch",
"HMD_ONLY": "HMD-laser",
"HMD_PINCH": "HMD + pinch",
"NONE": "Ingen",
"NONE_INPUT_HELP": "Emulera inte mus och tangentbord.\nInteraktion med skärmar blir inte möjlig.",
"NONE_INPUT_CAPTURE_HELP": "Fånga inte mus och tangentbord.\nDu kommer inte att kunna använda fysisk mus\neller tangentbord i appar som startas i WayVR.",
"PIPEWIRE_HELP": "Snabb GPU-avbildning,\nstandard för alla skrivbord.",
"PW_FALLBACK_HELP": "Långsam metod med hög CPU-belastning.\nProva om PipeWire GPU inte fungerar",
"SCREENCOPY_GPU_HELP": "Snabb, inga popup-fönster för skärmdelning.\nFungerar på: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Långsam, inga popup-fönster för skärmdelning.\nFungerar på: Hyprland, Niri, River, Sway",
"UINPUT": "Enhetsemulering",
"UINPUT_HELP": "Använder modulen uinput för att emulera en enhet.\nFungerar generellt bra på de flesta skrivbord.",
"EVDEV_HELP": "Ta kontroll över inmatningsenheter för exklusiv användning.\nAnvändaren måste tillhöra gruppen 'input'.",
"WL_VIRTUAL": "Wayland virtuell inmatning",
"WL_VIRTUAL_HELP": "Skickar händelser till Wayland-kompositorn via\nzwlr_virtual_pointer och zwp_virtual_keyboard.\nFungerar endast på vissa skrivbord.\nFungerar INTE på KDE, GNOME eller COSMIC."
},
"POINTER_ACCELERATION": "Musacceleration",
"POINTER_ACCELERATION_HELP": "På: Adaptiv musacceleration\nAv: Konstant hastighet",
"POINTER_LERP_FACTOR": "Utjämning av pekare",
"POINTER_SPEED": "Mushastighet",
"REQUIRES_RESTART": "Kräver omstart",
"RESET_PLAYSPACE": "Återställ playspace",
"RESET_PLAYSPACE_HELP": "Nollställ offset för playspace.",
"SAVE_PLAYSPACE_CENTER": "Spara playspace-center",
"SAVE_PLAYSPACE_CENTER_HELP": "Använd nuvarande playspace för bindningen \"återställ playspace\".",
"RESET_PLAYSPACE_CENTER": "Återställ playspace-center",
"RESET_PLAYSPACE_CENTER_HELP": "Rensa tidigare inställd mittpunkt för playspace.",
"RESTART_WAYVR": "Starta om WayVR",
"CLOSE_WAYVR": "Stäng WayVR",
"ROUND_MULTIPLIER": "UI-kanternas rundning",
"SCREEN_RENDER_DOWN": "Rendera skärm i lägre upplösning",
"SCREEN_RENDER_DOWN_HELP": "Hjälper mot aliasing på skärmar med hög upplösning",
"SCROLL_SPEED": "Scrollningshastighet",
"SELECT_VARIANT": "Välj variant",
"SNAP_ANGLE_DEG": "Snapvinkel för fönster (pitch & roll)",
"SNAP_ANGLE_DEG_HELP": "Greppade fönster snäpps till denna vinkel på pitch- och rollaxeln.",
"ENABLE_WATCH": "Aktivera klockan",
"SETS_ON_WATCH": "Set på klockan",
"SKYBOX": "Skymap",
"SKYMAP_ALREADY_DOWNLOADED": "Denna skymap är redan nedladdad. Välj åtgärd.",
"SPACE_DRAG": "Playspace",
"SPACE_DRAG_MULTIPLIER": "Multiplikator för space-drag",
"SPACE_DRAG_AFFECTS_WORLD": "Lämna overlays kvar vid flytt av playspace",
"SPACE_DRAG_UNLOCKED": "Tillåt space-drag på alla axlar",
"SPACE_GRAVITY_DAMPING": "Dämpning",
"SPACE_GRAVITY_DAMPING_HELP": "Artificiell dämpning för att sakta ner rörelser. 0,1 – hög dämpning, 1,0 – ingen dämpning",
"SPACE_GRAVITY_FLING_STRENGTH": "Kaststyrka",
"SPACE_GRAVITY_FLING_STRENGTH_HELP": "Intensitetsmultiplikator för gravitationskraften efter space-drag.\n0.0 - ingen rörelse alls, 2.0 - dubbel intensitet",
"SPACE_GRAVITY_GRAVITY": "Gravitation",
"SPACE_GRAVITY_GRAVITY_HELP": "Mängd nedåtgående kraft. 0.0 - ingen gravitation",
"SPACE_GRAVITY_GROUND_FRICTION": "Markfriktion",
"SPACE_GRAVITY_GROUND_FRICTION_HELP": "Mängden friktion som saktar ner dig när du rör vid marken.\n0.0 - ingen friktion (som på is), 1.0 - sträv yta",
"SPACE_GRAVITY_FLOOR_HEIGHT": "Golvhöjd",
"SPACE_GRAVITY_FLOOR_HEIGHT_HELP": "Y-positionen för golvet. Gravitationen upphör när denna höjd nås.",
"SPACE_ROTATE_UNLOCKED": "Tillåt rotation av playspace på alla axlar",
"ROTATIONS": "HMD-rotationer",
"SESSION_TIME": "Sessionstid",
"STATISTICS": "Statistik",
"TROUBLESHOOTING": "Felsökning",
"UI_GRADIENT_INTENSITY": "Intensitet för UI-gradient",
"UPRIGHT_SCREEN_FIX": "Fix för upprätt skärm",
"UPRIGHT_SCREEN_FIX_HELP": "Fixar upprätt ställda skärmar på vissa skrivbord",
"USE_PASSTHROUGH": "Aktivera passthrough",
"USE_PASSTHROUGH_HELP": "Tillåt passthrough om XR-runtime stöder det",
"USE_SKYBOX": "Visa skymap",
"USE_SKYBOX_HELP": "Visa en skymap om ingen scenapp eller passthrough används",
"WATCH_VIEW_ANGLE": "Klockans synvinklar",
"WATCH_VIEW_ANGLE_HELP": "Styr hur klockan tonas ut",
"WHISPER": {
"NEED_TO_DOWNLOAD_MODEL": "Den valda modellen måste laddas ner.\nVill du ladda ner den nu?",
"REMOVE_UNUSED_MODELS": "Du stänger av tal-till-text-funktionen.\nVill du ta bort nedladdade Whisper-modeller för att frigöra utrymme?"
},
"WHISPER_MODEL": "Tal-till-text-modell",
"WHISPER_MODEL_HELP": "Whisper-modell som ska användas.\nStörre modeller ger mer precisa\nresultat, men förbrukar mer VRAM.",
"SPEECH_TO_TEXT": "Tal till text",
"WHISPER_GPU_SELECT": "Välj GPU",
"TUTORIAL": "Handbok",
"GRID_OPACITY": "Golvgridens opacitet",
"GRID_OPACITY_HELP": "Opacitet för golvgridet när skymap är aktiverat",
"XWAYLAND_BY_DEFAULT": "Kör appar i kompatibilitetsläge som standard",
"SAVE": "Spara",
"CANCEL": "Avbryt"
},
"APPLICATION_LAUNCHER": "App-startare",
"APPLICATION_STARTED": "Applikationen startades",
"APPLICATIONS": "Applikationer",
"AUDIO": {
"SWITCH_TO_THIS_DEVICE": "Byt till den här enheten",
"AUTO_SWITCH_TO_VR_AUDIO": "Byt automatiskt till VR-ljud",
"CARDS": "Kort",
"FAILED_TO_SWITCH_MICROPHONE": "Kunde inte byta mikrofon",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofonen har ändrats",
"MICROPHONES": "Mikrofoner",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Ingen VR-mikrofon hittades. Byt manuellt.",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Inga VR-högtalare hittades. Byt manuellt.",
"SELECT_AUDIO_CARD_PROFILE": "Välj ljudkortsprofil",
"SETTINGS": "Ljudinställningar",
"SPEAKERS": "Högtalare",
"SPEAKERS_SET_SUCCESSFULLY": "Högtalarna har ändrats",
"VOLUME": "Volym"
},
"CLOSE_WINDOW": "Stäng fönster",
"CREATION_DATE": "Skapad datum",
"DEBUG_INFO": "Debuginfo",
"DISPLAY_BRIGHTNESS": "Skärmljusstyrka",
"DONATE": {
"SUPPORT_US": "Stöd oss",
"DESCRIPTION": "Vi är ett litet team på två utvecklare som bygger WayVR på vår fritid. Om du gillar att använda programvaran, överväg att stötta oss – varje donation hjälper oss att fortsätta.",
"THANK_YOU_WAYVR": "Tack från WayVR-teamet!",
"BUTTON_TOOLTIP": "Donera via Open Collective",
"CURRENT_SUPPORTERS": "Nuvarande supportrar",
"BROUGHT_TO_YOU_VARIANTS_OFTEN": "Stöds av {USER}|Särskilt tack till {USER}|Sponsras av {USER}|Backas av {USER}|Drivs av community-medlemmar som {USER}|Möjliggjorts av {USER}|Upprätthålls av {USER}|Bidrag från {USER}|Finansierat av {USER}",
"BROUGHT_TO_YOU_VARIANTS_RARE": "Kaffe sponsrat av {USER}|Drivs av {USER}|Hälsningar till {USER}!|Håller oss igång: {USER}|Hjälper oss att växa: {USER}|Håller servrarna igång: {USER}|Med tacksamhet till {USER}|Drivs av {USER}|Stöds av vänner som {USER}",
"BROUGHT_TO_YOU_VARIANTS_VERY_RARE": "Stöttas av legender som {USER}|Koffein sponsrat av {USER}|En sann legend: {USER}|Dagens hjälte: {USER}|En total superstjärna: {USER}|Betalar elräkningen: {USER}|Räddaren av vårt förstånd: {USER}|Visar vägen: {USER}|Koffein-till-kod-omvandlare tillhandahållen av {USER}|Håller produktionen stabil: {USER}|Den ende och ende: {USER}|Skickar dmabuf-buffertar: {USER}"
},
"DOWNLOAD": "Ladda ner",
"DOWNLOADER": "Nedladdare",
"WELCOME": {
"ALL_SET": "Nu är du redo!",
"BASICS": "Dessa grunder gör att du enkelt kan börja använda WayVR.",
"CENTER_MARKER": "Centreringsmarkören visas framför dig vid omcentrering. Placera overlays runt den!",
"DOUBLE_PRESS_TO_TOGGLE": "Dubbeltryck B (på Index) eller Y (på Meta) för att visa, centrera om eller dölja ditt set. Prova nu.",
"FEATURES_DOCUMENTATION": "Det finns många andra funktioner värda att anpassa. För mer information, se dokumentationen på",
"FINISH_TUTORIAL": "Avsluta handledningen",
"GRAB_BOTH_HANDS": "Greppa med båda händerna om du vill flytta ett specifikt overlay i ditt set.",
"GRAB_ONE_HAND": "Greppa med en hand för att flytta ditt set.",
"GREETING": "Hej!",
"GUIDE_INTRO": "Låt oss guida dig genom dina första steg.",
"LASER_COLORS": "Laserfärger",
"LASER_FUNCTIONALITY": "Mycket av funktionaliteten i WayVR beror på vilken laserfärg som används för att interagera med ett UI-element. Du behöver inte trycka på knapparna, det räcker att röra vid dem!",
"LAST_PAGE_INFO": "Denna handledning slutar visas när du når den sista sidan.",
"LOOK_AT_LEFT_WRIST": "Titta på din vänstra handled. Det är din klocka.",
"MIDDLE_CLICK_MODE_PURPLE_LASER": "Mittenklick-läge: Lila laser",
"MOVING_YOUR_SET": "Flytta ditt set",
"NEXT_BUTTON": "Tryck på nästa-knappen nedan för att komma igång.",
"REGULAR_MODE_BLUE_LASER": "Vanligt läge: Blå laser",
"REPOSITION_THIS_WINDOW_BY_GRABBING": "Flytta det här fönstret med kontrollens grip-funktion.",
"RIGHT_CLICK_MODE_ORANGE_LASER": "Högerklicksläge: Orange laser",
"THANKS_FOR_INSTALLING": "Tack för att du installerade WayVR!",
"WATCH": "Klocka",
"WORKING_SET": "Aktivt Set"
},
"DOWNLOAD_AGAIN": "Ladda ner igen",
"DOWNLOADING_FILE": "Laddar ner fil...",
"FAILED_TO_LAUNCH_APPLICATION": "Kunde inte starta applikationen:",
"GAME_LAUNCHED": "Spelet har startats",
"GAME_LIST": {
"NO_GAMES_FOUND": "Inga spel hittades",
"RUNNING_GAMES_LIST": "Lista över körande spel"
},
"GAMES": "Spel",
"GENERAL_SETTINGS": "Allmänna inställningar",
"GETTING_STARTED": "Kom igång",
"HEIGHT": "Höjd",
"HELLO": "Hej!",
"HELLO_USER": "Hej, {USER}!",
"HIDE": "Dölj",
"HOME_SCREEN": "Hem",
"MODIFICATION_DATE": "Ändringsdatum",
"MONADO_RUNTIME": "Monado runtime",
"LOADING": "Laddar...",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Upplösning"
},
"POWER_OPTIONS": {
"TITLE": "Strömalternativ",
"SHUTDOWN": "Stäng av",
"SUSPEND": "Viloläge",
"NOT_SUPPORTED": "Strömalternativ kräver systemd eller elogind.",
"SHUTDOWN_ARE_YOU_SURE": "Är du säker på att du vill stänga av datorn?",
"SUSPEND_ARE_YOU_SURE": "Är du säker på att du vill försätta datorn i viloläge?"
},
"PROCESS": {
"FORCE_KILL": "Tvinga avslut",
"STOP": "Stoppa"
},
"PROCESS_LIST": "Processlista",
"REFRESH": "Uppdatera",
"REMOVE": "Ta bort",
"APPLY": "Verkställ",
"RELOAD_FROM_DISK": "Ladda om från disk",
"SETTINGS": "Inställningar",
"SHOW": "Visa",
"TARGET_PATH": "Mål sökväg",
"TERMINATE_PROCESS": "Avsluta process",
"VERSION": "Version",
"WIDTH": "Bredd",
"PINNED_APPS": "Fästa applikationer",
"SAVED_TO_FAVOURITES": "Sparad i fästa applikationer",
"ADD_TO_FAVOURITES": "Lägg till i fästa applikationer",
"ADD_TO_FAVOURITES_HINT": "Spara förinställning och fäst den i app-menyn",
"REMOVE_FROM_FAVOURITES": "Ta bort från fästa applikationer",
"EMPTY": "Tomt",
"INSTALLED": "Installerad",
"NOT_INSTALLED": "Ej installerad",
"YES": "Ja",
"NO": "Nej"
}
+1 -1
View File
@@ -166,7 +166,7 @@ impl View {
// TODO: configurable defaults ?
let mut res_mode = AppResMode::Res1080;
let mut orientation_mode = AppOrientationMode::Wide;
let pos_mode = AppPosMode::Anchored;
let pos_mode = AppPosMode::Floating;
if let Some(pinned_app) = &params.pinned_app {
res_mode = pinned_app.res_mode;
+2 -2
View File
@@ -1008,7 +1008,7 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
.iter()
.map(|item| {
context_menu::Cell::Button(context_menu::Button {
action_name: Some(item.action_str(&action, side).into()),
action_name: Some(item.action_str(&action, side)),
title: item.translation(),
..Default::default()
})
@@ -1019,7 +1019,7 @@ fn create_dropdown<B: 'static + BindingsDropdown>(
cells.insert(
0,
context_menu::Cell::Button(context_menu::Button {
action_name: Some(action_str.into()),
action_name: Some(action_str),
title: Translation::from_translation_key("APP_SETTINGS.OPTION.NONE"),
..Default::default()
}),
+34 -4
View File
@@ -1,6 +1,32 @@
#!/bin/bash
# Do not run this script if you don't have at least 32 GiB of system memory.
# This is the translation tool intended for machine-translating strings.
#
# ###########################
# # Adding new translations #
# ###########################
#
# If you want to add a new language, follow these steps:
#
# 1. Include the language in `wlx-common/src/locale.rs`:
# - add your entry in `enum Language`
# - add your entry in Language::code() function
# - add your language code in Language::add_codes() function
#
# 2. Create a new template file
# - Add your language code in `scripts/translator/templates/<lang code>.json`
# (you can copy one of nearby files and adapt them accordingly)
#
# 3. Modify this very file
# - Go to the bottom and add your language code entry here too
#
# 4. Run the script (it will hint you which LANG_PATHs are supported)
#
# ############################################
# # Running machine translations (local LLM) #
# ############################################
#
# Do not run this script if you don't have at least ~16 GiB of free system memory.
# Submitting PRs that lack translated strings is okay if you don't meet the
# system requirements to run this script, or if you simply prefer not to; we are
# regularly updating the missing translation strings anyway.
@@ -8,15 +34,18 @@
# Base language: English (en.json)
#
# This tool expects llama.cpp server being present (llama-server) at port 8080.
#
# Currently we're using Gemma 4 for machine translations.
# Model URL:
# https://huggingface.co/unsloth/gemma-4-31B-it-GGUF
# Recommended quantization: Q4_K_M
#
set -e
cd "$(dirname "$0")"
bun install
# model URL:
# https://huggingface.co/unsloth/gemma-4-31B-it-GGUF
# you can specify your own if you want to.
if [ -z "${MODEL}" ]; then
export MODEL="gemma-4-31B-it-Q4_K_M"
fi
@@ -31,4 +60,5 @@ TEMPLATE="de" bun main.ts
TEMPLATE="ja" bun main.ts
TEMPLATE="es" bun main.ts
TEMPLATE="it" bun main.ts
TEMPLATE="sv" bun main.ts
TEMPLATE="zh_CN" bun main.ts
+20
View File
@@ -0,0 +1,20 @@
{
"full_name": "Swedish",
"examples": [
{
"key": "BAR.OPACITY",
"en": "Opacity",
"translated": "Opacitet"
},
{
"key": "PANEL.CLOSE",
"en": "Close panel",
"translated": "Stäng panelen"
},
{
"key": "SETTINGS.LEFT_HANDED_MODE",
"en": "Left-handed mode",
"translated": "Vänsterhänt läge"
}
]
}
+5 -2
View File
@@ -54,7 +54,8 @@ strum.workspace = true
tracing-subscriber.workspace = true
uuid = { workspace = true }
wayland-client = { workspace = true }
wayland-protocols = { workspace = true }
wayland-protocols = { workspace = true, features = ["server", "staging"] }
wayland-scanner = "0.31.10"
wayland-protocols-misc = { workspace = true }
winit = { workspace = true, optional = true }
xdg.workspace = true
@@ -63,6 +64,7 @@ bytes = { workspace = true }
chrono-tz = "0.10.4"
config = "0.15.25"
futures = "0.3.32"
image.workspace = true
image_dds = {
version = "0.7.2",
default-features = false,
@@ -96,7 +98,8 @@ rosc = { version = "0.11.4", optional = true }
serde_yaml = "0.9.33"
signal-hook = "0.4.4"
smithay = {
version = "0.7.0",
git = "https://github.com/Smithay/smithay.git",
rev = "0d66849533b475c40c0c461ec182d0f939424452",
default-features = false,
features = [
"backend_vulkan",
@@ -0,0 +1,333 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="xdg_session_management_v1">
<copyright>
Copyright 2018 Mike Blumenkrantz
Copyright 2018 Samsung Electronics Co., Ltd
Copyright 2018 Red Hat Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="Protocol for managing application sessions">
This description provides a high-level overview of the interplay between
the interfaces defined this protocol. For details, see the protocol
specification.
The xdg_session_manager protocol declares interfaces necessary to
allow clients to restore toplevel state from previous executions. The
xdg_session_manager_v1.get_session request can be used to obtain a
xdg_session_v1 resource representing the state of a set of toplevels.
Clients may obtain the session string to use in future calls through
the xdg_session_v1.created event. Compositors will use this string
as an identifiable token for future runs, possibly storing data about
the related toplevels in persistent storage. Clients that wish to
track sessions in multiple environments may use the $XDG_CURRENT_DESKTOP
environment variable.
Toplevels are managed through the xdg_session_v1.add_toplevel and
xdg_session_v1.remove_toplevel pair of requests. Clients will explicitly
request a toplevel to be restored according to prior state through the
xdg_session_v1.restore_toplevel request before the toplevel is mapped.
Compositors may store session information up to any arbitrary level, and
apply any limits and policies to the amount of data stored and its lifetime.
Clients must account for missing sessions and partial session restoration.
Warning! The protocol described in this file is currently in the testing
phase. Backward compatible changes may be added together with the
corresponding interface version bump. Backward incompatible changes can
only be done by creating a new major version of the extension.
</description>
<interface name="xdg_session_manager_v1" version="1">
<description summary="manage sessions for applications">
The xdg_session_manager_v1 interface defines base requests for creating and
managing a session for an application. Sessions persist across application
and compositor restarts unless explicitly destroyed. A session is created
for the purpose of maintaining an application's xdg_toplevel surfaces
across compositor or application restarts. The compositor should remember
as many states as possible for surfaces in a given session, but there is
no requirement for which states must be remembered.
Policies such as cache eviction are declared an implementation detail of
the compositor. Clients should account for no longer existing sessions.
</description>
<enum name="error">
<entry name="in_use" summary="a requested session is already in use"
value="1"/>
<entry name="invalid_session_id" summary="invalid session identifier"
value="2"/>
<entry name="invalid_reason" summary="invalid reason" value="3"/>
</enum>
<enum name="reason">
<description summary="reason for getting a session">
The reason may determine in what way a session restores the window
management state of associated toplevels.
For example newly launched applications might be launched on the active
workspace with restored size and position, while a recovered
application might restore additional state such as active workspace and
stacking order.
</description>
<entry name="launch" value="1">
<description summary="an app is newly launched">
A new app instance is launched, for example from an app launcher.
</description>
</entry>
<entry name="recover" value="2">
<description summary="an app recovered">
An app instance is recovering from for example a compositor or app crash.
</description>
</entry>
<entry name="session_restore" value="3">
<description summary="an app restored">
An app instance is restored, for example part of a restored session, or
restored from having been temporarily terminated due to resource
constraints.
</description>
</entry>
</enum>
<request name="destroy" type="destructor">
<description summary="Destroy this object">
Destroy the manager object. The existing session objects will be
unaffected.
</description>
</request>
<request name="get_session">
<description summary="create or restore a session">
Create a session object corresponding to either an existing session
identified by the given session identifier string or a new session.
While the session object exists, the session is considered to be "in
use".
If an identifier string represents a session that is currently actively
in use by the same client, an 'in_use' error is raised. If some
other client is currently using the same session, the new session will
replace managing the associated state.
If the reason is not a valid enum entry, the 'invalid_reason' protocol
error is raised.
NULL is passed to initiate a new session. If a session_id is passed
which does not represent a valid session, the compositor treats it as if
NULL had been passed.
The session id string must be UTF-8 encoded. It is also limited by the
maximum length of wayland messages (around 4KB). The 'invalid_session_id'
protocol error will be raised if an invalid string is provided.
A client is allowed to have any number of in use sessions at the same
time.
</description>
<arg name="id" type="new_id" interface="xdg_session_v1"/>
<arg name="reason" type="uint" enum="reason"
summary="reason for session"/>
<arg name="session_id" type="string"
summary="the session to restore"
allow-null="true"/>
</request>
</interface>
<interface name="xdg_session_v1" version="1">
<description summary="A session for an application">
A xdg_session_v1 object represents a session for an application. While the
object exists, all surfaces which have been added to the session will
have states stored by the compositor which can be reapplied at a later
time. Two sessions cannot exist for the same identifier string.
States for surfaces added to a session are automatically updated by the
compositor when they are changed.
</description>
<enum name="error">
<entry name="name_in_use"
summary="toplevel name is already in use"
value="1"/>
<entry name="already_mapped"
summary="toplevel was already mapped when restored"
value="2"/>
<entry name="invalid_name"
summary="provided toplevel name is invalid"
value="3"/>
<entry name="already_added"
summary="toplevel already added"
value="4"/>
</enum>
<request name="destroy" type="destructor">
<description summary="Destroy the session">
Destroy a session object, preserving the current state but not continuing
to make further updates if state changes occur. This makes the associated
xdg_toplevel_session_v1 objects inert.
</description>
</request>
<request name="remove" type="destructor">
<description summary="Remove the session">
Remove the session, making it no longer available for restoration. A
compositor should in response to this request remove the data related to
this session from its storage.
</description>
</request>
<request name="add_toplevel">
<description summary="add a new surface to the session">
Attempt to add a given surface to the session. The passed name is used
to identify what window is being restored, and may be used to store
window specific state within the session.
The name given to the toplevel must not correspond to any previously
existing toplevel names in the session. If the name matches an already
known toplevel name in the session, a 'name_in_use' protocol error will
be raised.
The toplevel object must not be added more than once to any session
created by the client, otherwise the 'already_added' protocol error
will be raised.
This request will return a xdg_toplevel_session_v1 for later
manipulation. As this resource is created from an empty initial state,
compositors must not emit a xdg_toplevel_session_v1.restored event for
resources created through this request.
The name string must be UTF-8 encoded. It is also limited by the maximum
length of wayland messages (around 4KB). The 'invalid_name' protocol
error will be raised if an invalid string is provided.
</description>
<arg name="id" type="new_id" interface="xdg_toplevel_session_v1"/>
<arg name="toplevel" type="object" interface="xdg_toplevel"/>
<arg name="name" type="string" summary="name identifying the toplevel"/>
</request>
<request name="restore_toplevel">
<description summary="restore a surface state">
Inform the compositor that the toplevel associated with the passed name
should have its window management state restored.
If the toplevel name was previously granted to another xdg_toplevel,
the 'name_in_use' protocol error will be raised.
The toplevel object must not be added more than once to any session
created by the client, otherwise the 'already_added' protocol error
will be raised.
This request must be called prior to the first commit on the associated
wl_surface after creating the toplevel, otherwise an 'already_mapped'
error is raised.
As part of the initial configure sequence, if the toplevel was
successfully restored, a xdg_toplevel_session_v1.restored event is
emitted. If the toplevel name was not known in the session, this request
will be equivalent to the xdg_toplevel_session_v1.add_toplevel request,
and no such event will be emitted. See the xdg_toplevel_session_v1.restored
event for further details.
The name string must be UTF-8 encoded. It is also limited by the maximum
length of wayland messages (around 4KB). The 'invalid_name' protocol
error will be raised if an invalid string is provided.
</description>
<arg name="id" type="new_id" interface="xdg_toplevel_session_v1"/>
<arg name="toplevel" type="object" interface="xdg_toplevel"/>
<arg name="name" type="string" summary="name identifying the toplevel"/>
</request>
<request name="remove_toplevel">
<description summary="remove a surface from the session">
Remove a specified surface from the session and render any related
xdg_toplevel_session_v1 object inert. The compositor should remove any
data related to the toplevel in the corresponding session from its internal
storage.
The window is specified by its name in the session. The name string
must be encoded in UTF-8, and it is limited in size by the maximum
length of wayland messages (around 4KB).
</description>
<arg name="name" type="string" summary="name identifying the toplevel"/>
</request>
<event name="created">
<description summary="newly-created session id">
Emitted at most once some time after getting a new session object. It
means that no previous state was restored, and a new session was created.
The passed id can be persistently stored and used to restore previous
sessions.
</description>
<arg name="session_id" type="string"/>
</event>
<event name="restored">
<description summary="the session has been restored">
Emitted at most once some time after getting a new session object. It
means that previous state was at least partially restored. The same id
can again be used to restore previous sessions.
</description>
</event>
<event name="replaced">
<description summary="the session has been replaced">
Emitted at most once, if the session was taken over by some other
client. When this happens, the session and all its toplevel session
objects become inert, and should be destroyed.
</description>
</event>
</interface>
<interface name="xdg_toplevel_session_v1" version="1">
<description summary="A session for an application">
A xdg_toplevel_session_v1 resource acts as a handle for the given
toplevel in the session. It allows for receiving events after a
toplevel state was restored, and has the requests to manage them.
</description>
<request name="destroy" type="destructor">
<description summary="Destroy the object">
Destroy the object. This has no effect over window management of the
associated toplevel.
</description>
</request>
<request name="rename">
<description summary="change the name of toplevel session">
Renames the toplevel session. The new name can be used in subsequent requests
to identify this session object. The state associated with this toplevel
session will be preserved.
If the xdg_session_v1 already contains a toplevel with the specified name,
the 'name_in_use' protocol error will be raised.
</description>
<arg name="name" type="string" summary="new name to identify the toplevel"/>
</request>
<event name="restored">
<description summary="a toplevel's session has been restored">
The "restored" event is emitted prior to the first
xdg_toplevel.configure for the toplevel. It will only be emitted after
xdg_session_v1.restore_toplevel, and the initial empty surface state has
been applied, and it indicates that the surface's session is being
restored with this configure event.
</description>
</event>
</interface>
</protocol>
+2 -2
View File
@@ -43,14 +43,14 @@
"MOVE_PRESS_AND_DRAG": "Bewegen (drücken & ziehen)",
"OPACITY": "Undurchsichtigkeit",
"POS_ANCHORED": "Verankert: Bewegt sich gemeinsam mit dem Set. Standard.",
"POS_FLOATING": "Schwebend: Bewegt sich unabhängig, zentriert sich beim Anzeigen neu.",
"POS_HAND_L": "Folgt der linken Hand.",
"POS_HAND_R": "Rechter Hand folgen.",
"POS_HMD": "Folgt dem HMD.",
"POS_STATIC": "Statisch: Nicht Teil eines Sets, kein Recenter.",
"POSITIONING": "Positionierung",
"RESIZE_PRESS_AND_DRAG": "Größe ändern (drücken & ziehen)",
"RESIZE_CORNER": "Zum Vergrößern ziehen"
"RESIZE_CORNER": "Zum Vergrößern ziehen",
"POS_FLOATING": "Schwebend: Bewegt sich unabhängig, wird mit dem Anker gespeichert."
},
"DEFINITIONS": {
"ANCHORED": "Verankert",
+1 -1
View File
@@ -63,7 +63,7 @@ the screens were likely selected wrong. See readme.",Tooltip to explain possible
EDIT_MODE.MOVE_PRESS_AND_DRAG,Move (press & drag),Tooltip hinting to move the window by pressing and dragging with the pointer
EDIT_MODE.OPACITY,Opacity,Title. Adjust transparency of the window
EDIT_MODE.POS_ANCHORED,Anchored: Moves together with set. Default.,"Tooltip for a an option that changes movement/positioning behavior. “Anchored” was previously translated as “${DEFINITIONS.ANCHORED}”, “Set” was previously translated as “${DEFINITIONS.SET}”"
EDIT_MODE.POS_FLOATING,"Floating: Moves independently, recenters when shown.",Tooltip for a an option that changes movement/positioning behavior. “Floating” was previously translated as “${DEFINITIONS.FLOATING}”
EDIT_MODE.POS_FLOATING,"Floating: Moves independently, Saves with the anchor.",Tooltip for a an option that changes movement/positioning behavior. “Floating” was previously translated as “${DEFINITIONS.FLOATING}”
EDIT_MODE.POS_HAND_L,Follow the left hand.,Tooltip for a an option that changes movement/positioning behavior.
EDIT_MODE.POS_HAND_R,Follow the right hand.,Tooltip for a an option that changes movement/positioning behavior.
EDIT_MODE.POS_HMD,Follow the HMD.,Tooltip for a an option that changes movement/positioning behavior.
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”.
63 EDIT_MODE.POS_ANCHORED Anchored: Moves together with set. Default. Tooltip for a an option that changes movement/positioning behavior. “Anchored” was previously translated as “${DEFINITIONS.ANCHORED}”, “Set” was previously translated as “${DEFINITIONS.SET}”
64 EDIT_MODE.POS_FLOATING Floating: Moves independently, recenters when shown. Floating: Moves independently, Saves with the anchor. Tooltip for a an option that changes movement/positioning behavior. “Floating” was previously translated as “${DEFINITIONS.FLOATING}”
65 EDIT_MODE.POS_HAND_L Follow the left hand. Tooltip for a an option that changes movement/positioning behavior.
66 EDIT_MODE.POS_HAND_R Follow the right hand. Tooltip for a an option that changes movement/positioning behavior.
67 EDIT_MODE.POS_HMD Follow the HMD. Tooltip for a an option that changes movement/positioning behavior.
68 EDIT_MODE.POS_STATIC Static: Not part of any set, no recenter. Tooltip for a an option that changes movement/positioning behavior. “Static” was previously translated as “${DEFINITIONS.STATIC}”, “Set” was previously translated as “${DEFINITIONS.SET}”
69 EDIT_MODE.POSITIONING Positioning Title for positioning modes for the window
+1 -1
View File
@@ -73,7 +73,7 @@
"MOVE_PRESS_AND_DRAG": "Move (press & drag)",
"OPACITY": "Opacity",
"POS_ANCHORED": "Anchored: Moves together with set. Default.",
"POS_FLOATING": "Floating: Moves independently, recenters when shown.",
"POS_FLOATING": "Floating: Moves independently, Saves with the anchor.",
"POS_HAND_L": "Follow the left hand.",
"POS_HAND_R": "Follow the right hand.",
"POS_HMD": "Follow the HMD.",
+2 -2
View File
@@ -43,14 +43,14 @@
"MOVE_PRESS_AND_DRAG": "Mover (presiona y arrastra)",
"OPACITY": "Opacidad",
"POS_ANCHORED": "Anclado: Se mueve junto con el set. Por defecto.",
"POS_FLOATING": "Flotante: Se mueve independientemente, se recentra al mostrarse.",
"POS_HAND_L": "Sigue la mano izquierda.",
"POS_HAND_R": "Sigue la mano derecha.",
"POS_HMD": "Sigue el HMD.",
"POS_STATIC": "Estático: No pertenece a ningún set, sin recentrar.",
"POSITIONING": "Posicionamiento",
"RESIZE_PRESS_AND_DRAG": "Redimensionar (presiona y arrastra)",
"RESIZE_CORNER": "Arrastrar para redimensionar"
"RESIZE_CORNER": "Arrastrar para redimensionar",
"POS_FLOATING": "Flotante: Se mueve independientemente, se guarda con el ancla."
},
"DEFINITIONS": {
"ANCHORED": "Anclado",
+2 -2
View File
@@ -43,14 +43,14 @@
"MOVE_PRESS_AND_DRAG": "Sposta (premi e trascina)",
"OPACITY": "Opacità",
"POS_ANCHORED": "Ancorato: Si muove insieme al set. Predefinito.",
"POS_FLOATING": "Fluttuante: si muove indipendentemente, si ricentra quando appare.",
"POS_HAND_L": "Segui la mano sinistra.",
"POS_HAND_R": "Segui la mano destra.",
"POS_HMD": "Segue l'HMD.",
"POS_STATIC": "Statico: non fa parte di alcun set, nessun ricentramento.",
"POSITIONING": "Posizionamento",
"RESIZE_PRESS_AND_DRAG": "Ridimensiona (premi e trascina)",
"RESIZE_CORNER": "Trascina per ridimensionare"
"RESIZE_CORNER": "Trascina per ridimensionare",
"POS_FLOATING": "Fluttuante: si muove indipendentemente, salva con l'ancora."
},
"DEFINITIONS": {
"ANCHORED": "Ancorato",
+2 -2
View File
@@ -42,7 +42,6 @@
"LOCK_INTERACTION": "インタラクションをロック",
"MOVE_PRESS_AND_DRAG": "移動(押しながらドラッグ)",
"OPACITY": "不透明度",
"POS_FLOATING": "フローティング:独立して移動し、表示時に中心位置がリセットされる。",
"POS_HAND_L": "左手に追従",
"POS_HAND_R": "右手に従う",
"POS_HMD": "HMDに従う",
@@ -50,7 +49,8 @@
"POSITIONING": "配置",
"RESIZE_PRESS_AND_DRAG": "リサイズ (押し込みながらドラッグ)",
"RESIZE_CORNER": "ドラッグしてリサイズ",
"POS_ANCHORED": "セット追従:セットと一緒に移動。デフォルト。"
"POS_ANCHORED": "セット追従:セットと一緒に移動。デフォルト。",
"POS_FLOATING": "フローティング:独立して移動し、アンカーと共に保存される。"
},
"DEFINITIONS": {
"ANCHORED": "セット追従",
+2 -2
View File
@@ -43,14 +43,14 @@
"MOVE_PRESS_AND_DRAG": "Przesuń (przyciśnij i przeciągnij)",
"OPACITY": "Przezroczystość",
"POS_ANCHORED": "Przytwierdzone: Przesuwa się wraz z zestawem. Domyślnie.",
"POS_FLOATING": "Pływający: przesuwa się niezależnie, centruje się po wyświetleniu.",
"POS_HAND_L": "Podążaj za lewą ręką.",
"POS_HAND_R": "Podążaj za prawą ręką.",
"POS_HMD": "Podążaj za HMD.",
"POS_STATIC": "Stacjonarny: Poza jakimkolwiek zestawem, brak recentrowania.",
"POSITIONING": "Pozycjonowanie",
"RESIZE_PRESS_AND_DRAG": "Zmień rozmiar (naciśnij i przeciągnij)",
"RESIZE_CORNER": "Przeciągnij, aby zmienić rozmiar"
"RESIZE_CORNER": "Przeciągnij, aby zmienić rozmiar",
"POS_FLOATING": "Pływający: przesuwa się niezależnie, zapisuje wraz z kotwicą."
},
"DEFINITIONS": {
"ANCHORED": "Przytwierdzone",
+185
View File
@@ -0,0 +1,185 @@
{
"DEFINITIONS": {
"ANCHORED": "Förankrad",
"FLOATING": "Flytande",
"STATIC": "Statisk",
"SET": "Set",
"WATCH": "Klocka",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"MIRROR": "Spegling",
"SETTINGS": "Inställningar",
"KEYBOARD": "Tangentbord",
"TASKBAR": "Uppgiftspanel",
"FEATURES": "Funktioner",
"OVERLAY": "Overlay"
},
"ANCHOR": {
"CENTER": "Center"
},
"BAR": {
"ADD_MIRROR": "Lägg till ny spegling-overlay",
"ADD_NEW_SET": "Lägg till ny set",
"ADD_PASSTHRU": "Lägg till ny passthrough-utskärning",
"CLOSE_APP": "Stäng appen",
"CLOSE_OVERLAY": "Stäng {$DEFINITIONS.OVERLAY}",
"DELETE_CURRENT_SET": "Ta bort nuvarande set",
"EDIT_MODE_TOGGLE": "Växla redigeringsläge",
"FORCE_CLOSE_APP": "Tvinga stäng app",
"HANDSFREE": {
"EYE_ONLY": "Ögonsiktet",
"EYE_TRACKING": "Öga + pinch",
"HMD": "HMD + pinch",
"HMD_ONLY": "HMD-laser",
"NONE": "Av",
"TITLE": "Händsfree-läge"
},
"RELOAD_FROM_DISK": "Ladda om XML från disk",
"RESET_POSITION": "Återställ position",
"TOGGLE_VISIBILITY": "Växla synlighet"
},
"DEFAULT": "Standard",
"DISABLED": "Inaktiverat",
"DONE": "Klar",
"EDIT_MODE": {
"ADJUST_CURVATURE": "Justera krökning",
"ALIGN_TO_HMD": "Rikta mot HMD",
"ALPHA_BLEND_MODE": "Alfablendingsläge",
"ANGLE_FADE": "Vinkeltoning",
"ANGLE_FADE_HELP": "Tona ut när den inte är vänd mot HMD",
"BLENDING_ADDITIVE": "Additiv blandning",
"BLOCK_INPUT": "Blockera spel-input",
"CURVATURE": "Krökning",
"DELETE": "Håll inne för att ta bort från nuvarande set",
"DISABLE_GRAB": "Inaktivera grepp",
"GLOBAL": "Alltid synlig",
"HINT_POINT_WINDOW": "Peka på ett fönster för att ändra dess parametrar.\nNär du är klar, lämna redigeringsläget via knappen till höger.",
"INTERPOLATION": "Interpolation",
"KEYBOARD": "Tangentbord",
"LEAVE": "Lämna redigeringsläge",
"LOCK_INTERACTION": "Lås interaktion",
"MOUSE": {
"FLIP180": "Speglad 180°",
"FLIP270": "Speglad 270°",
"FLIP90": "Speglad 90°",
"FLIPPED": "Speglad",
"NORMAL": "Normal",
"ROTATE180": "Roterad 180°",
"ROTATE270": "Roterad 270°",
"ROTATE90": "Roterad 90°",
"TITLE": "Musfixar",
"WRONG_SCREEN_SELECTION_HELP": "Om markören rör sig på en helt annan skärm\nhar skärmarna troligtvis valts fel. Se readme."
},
"MOVE_PRESS_AND_DRAG": "Flytta (tryck & dra)",
"OPACITY": "Opacitet",
"POS_ANCHORED": "Förankrad: Flyttas tillsammans med set. Standard.",
"POS_HAND_L": "Följ vänster hand.",
"POS_HAND_R": "Följ höger hand.",
"POS_HMD": "Följ HMD.",
"POS_STATIC": "Statisk: Inte en del av något set, ingen återcentrering.",
"POSITIONING": "Positionering",
"RESIZE_PRESS_AND_DRAG": "Ändra storlek (tryck & dra)",
"RESIZE_CORNER": "Dra för att ändra storlek",
"STEREO_3D_MODE": {
"ADJUST_MOUSE": "Justera mus",
"FULL_FRAME": "Full 3D",
"FULL_FRAME_BAT": "Full-BAT",
"FULL_FRAME_SBS": "Full-SBS",
"FULL_FRAME_TAB": "Full-TAB",
"SPLIT_BOTTOM_TOP": "NEDRE→ÖVRE",
"SPLIT_LEFT_RIGHT": "VÄNSTRER→HÖGER",
"SPLIT_RIGHT_LEFT": "HÖGER→VÄNSTER",
"SPLIT_TOP_BOTTOM": "TOPP→BOTTEN",
"TITLE": "3D-stereoläge"
},
"POS_FLOATING": "Flytande: Flyttas oberoende, sparas med ankaret."
},
"ALTTAB": {
"RELEASE_TO_FOCUS": "Sikta på en app och släpp Super för att fokusera på den.",
"WHILE_AIMING": "Vid sikte",
"CLICK": "VR-pekarklick",
"GRABALL": "Flytta hela setet",
"GRAB": "Flytta enskild overlay",
"DASH": "Växla WayVR Dashboard",
"EDIT": "Växla redigeringsläge",
"KEYBOARD": "Växla tangentbord",
"SET": "Byt set",
"RECENTER": "Visa/dölj/centrera",
"GENERAL_BINDS": "Allmänna bindningar",
"TOGGLE_CAPTURE": "Återlämna kontrollen till skrivbordet",
"FOCUS_MODE": "Fokusläge (håll in Super)",
"CLOSE": "Stäng fokuserad app",
"MOUSE": "Klick och skroll med fysisk mus översätts även till VR-pekaren"
},
"GRAB": {
"ADJUST_DISTANCE": "Justera avstånd",
"ADJUST_SIZE": "Justera storlek",
"GRABBING_ANCHORED": "Förankrade överlägg rör sig med setet. Separera ett enskilt fönster genom att greppa det med den andra handen samtidigt som du håller i setet.",
"GRABBING_ANCHORED_EDIT": "Det här överlägget kommer att förbli förankrat vid centreringsmarkören.",
"GRABBING_FLOATING": "Det här överlägget är flytande och kommer att stanna på plats, om det inte centreras om.",
"GRABBING_FOLLOW": "Detta overlay följer den enhet den är kopplad till.",
"GRABBING_STATIC": "Det här överlägget är statiskt och kommer att stanna kvar på platsen, oavsett om du centrerar om.",
"GRABBING_WATCH": "För att byta hand, flytta klockan framför dig och greppa den med den andra handen.",
"UNRESTRICTED_MOVEMENT": "Obegränsad rörelse"
},
"SCREENCAST": {
"PENDING": "Vänligen vänta...",
"QUEUED": "Väntar på att en annan overlays skärmdelningsförfrågan ska godkännas.",
"WAITING_FOR_USER_DESC": "Kontrollera skrivbordet efter ett popup-fönster för skärmdelning.\nVälj följande:",
"WAITING_FOR_USER": "Kontrollera din skrivbordsdisplay för ett popup-fönster om skärmdelning.",
"FAIL": "Kunde inte initiera källan för skärmöverföring."
},
"TOAST": {
"TITLE_SCREENCAST_FAIL": "Kunde inte starta källan för skärmdelning!",
"CANNOT_ADD_SET": "Kunde inte lägga till set!",
"CANNOT_REMOVE_SET": "Kan inte ta bort set!",
"STEAMVR_DASH_TITLE": "SteamVR-kontrollpanelen är öppen",
"STEAMVR_DASH_TEXT": "Stäng SteamVR-panelen för att börja använda WayVR.",
"WAYVR_RUNNING_TITLE": "WayVR körs!",
"WAYVR_RUNNING_TEXT": "Kolla handleden för klockan, eller använd bindningen för visa/dölj för att komma igång!",
"DEFAULT_TITLE": "Avisering",
"EMPTY_SET": "Tomt set!",
"ERROR": "Fel",
"FIXING_FLOOR_IN_X_SECS": "Justerar golvet om {SECONDS} sekunder...",
"LAST_EXISTING_SET": "Detta är det sista befintliga setet.",
"LETS_ADD_OVERLAYS": "Låt oss lägga till några overlays från uppgiftspanelen!",
"MAXIMUM_SETS_REACHED": "Maximalt antal set har uppnåtts.",
"NO_KEYMAPS_CONFIGURED": "Inga tangentbordslayouts konfigurerade",
"NO_KEYMAPS_CONFIGURED_HELP": "Kolla wayvr.org för att se hur du ställer in dina tangentbordslayouter!",
"NO_SET_SELECTED": "Inget Set är valt.",
"ONE_CONTROLLER_ON_FLOOR": "Placera en kontroller på golvet!"
},
"WATCH": {
"ADD_NEW_SET": "Lägg till ett nytt set",
"CLEANUP_MIRRORS": "Ta bort speglingar som\ninte är synliga just nu",
"DASHBOARD": "Kontrollpanel",
"EDIT_MODE": "Redigeringsläge",
"FIX_FLOOR": "Justera golvnivå",
"LONG_PRESS_TO_DELETE_SET": "Håll för att ta bort",
"RECENTER": "Centrera playspace",
"SWITCH_TO_SET": "Byt till set",
"TOGGLE_FOR_CURRENT_SET": "Växla för nuvarande set"
},
"WHISPER": {
"TRANSCRIBE": "Håll ned för att transkribera text",
"PASTE": "Kopiera transkribering till urklipp",
"PASTE_AND_GO": "Kopiera transkribering och skicka Ctrl-V till fokuserad app",
"SEND_OSC": "Skicka transkription till OSC-chatt",
"UNLOAD_AND_CLOSE": "Avlasta modell och stäng",
"MODEL_NOT_DOWNLOADED": "Tal-till-text är inte konfigurerat!",
"DOWNLOAD_GUIDANCE": "Konfigurera tal-till-text under inställningar, funktioner!",
"INITIAL_TEXT": "Håll inne mikroknappen för att börja transkribera...",
"INIT_ERROR": "Fel vid start av Whisper-motorn",
"ENTER_KEY": "Enter-tangent"
},
"NOTIFICATION": {
"COUD_NOT_CONNECT_TO_VR_RUNTIME": "Kunde inte ansluta till VR-runtime.",
"NOT_RUNNING_XR_VR": "Se till att Monado, WiVRn eller SteamVR är igång.",
"NOT_RUNNING_VR": "Se till att Monado, WiVRn eller SteamVR är igång.",
"NOT_RUNNING_XR": "Se till att Monado eller WiVRn är igång.",
"INVALID_ARGS": "Kontrollera dina startargument.",
"SELECT_SCREEN_CAST_FOR": "Välj skärmdelning för display {DISPLAY}",
"CANNOT_CAPTURE_KBD_MOUSE": "Kunde inte initiera fångst av tangentbord/mus!\nKontrollera att din användare tillhör gruppen 'input'.",
"WE_ARE_GRABBING": "WayVR styr nu ditt tangentbord och din mus!\nTryck Super+Z för att släppa."
}
}
+2 -2
View File
@@ -43,14 +43,14 @@
"MOVE_PRESS_AND_DRAG": "移动 (按下并拖拽)",
"OPACITY": "不透明度",
"POS_ANCHORED": "同步移动:随套装一起移动。默认。",
"POS_FLOATING": "悬浮:独立移动,显示时自动居中。",
"POS_HAND_L": "跟随左手",
"POS_HAND_R": "跟随右手。",
"POS_HMD": "跟随 HMD",
"POS_STATIC": "静态:不属于任何套装,不重新居中。",
"POSITIONING": "位置设定",
"RESIZE_PRESS_AND_DRAG": "调整大小(按住并拖动)",
"RESIZE_CORNER": "拖动以调整大小"
"RESIZE_CORNER": "拖动以调整大小",
"POS_FLOATING": "悬浮:独立移动,随锚点保存。"
},
"DEFINITIONS": {
"ANCHORED": "同步移动",
+5 -5
View File
@@ -874,11 +874,11 @@ fn start_grab(par: StartGrabParams) {
// - not in editmode
// - not using grab_float
// - grabbing with one hand. (grabbing with the 2nd hand will grab the individual overlay instead)
let grab_anchor = !par.edit_mode
&& !par.grab_float
&& !app.anchor_grabbed
&& matches!(state.positioning, Positioning::Anchored)
&& !(app.session.config.middle_grabs_single
let grab_anchor = !(par.edit_mode
|| par.grab_float
|| app.anchor_grabbed
|| !matches!(state.positioning, Positioning::Anchored)
|| app.session.config.middle_grabs_single
&& pointer.interaction.mode == PointerMode::Middle);
let relative_grab_transform = if grab_anchor {
+1 -1
View File
@@ -270,7 +270,7 @@ impl OverlayWindowData<OpenVrOverlayData> {
};
let Some(view) = self.data.image_view.as_ref() else {
log::debug!("{}: Not rendered", self.config.name);
log::trace!("{}: Not rendered", self.config.name);
return;
};
+2 -2
View File
@@ -723,7 +723,7 @@ fn for_each_path(spec: Option<&OneOrMany<String>>, mut f: impl FnMut(&str)) {
}
}
fn button_actions(profile: &OpenXrInputProfile) -> [Option<&OpenXrInputAction>; 11] {
const fn button_actions(profile: &OpenXrInputProfile) -> [Option<&OpenXrInputAction>; 11] {
[
profile.click.as_ref(),
profile.alt_click.as_ref(),
@@ -925,7 +925,7 @@ fn add_button_bindings(
for (side, hand) in hands.iter_mut().enumerate() {
for binding in build_button_bindings(action, side, profile_index, physical_input_map) {
field(&mut **hand).add_binding(binding);
field(hand).add_binding(binding);
}
}
}
+23 -27
View File
@@ -133,34 +133,30 @@ impl PlayspaceMover {
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() {
if pointer.now.space_rotate {
let Ok(pose) = monado
.ipc
.get_reference_space_offset(ReferenceSpaceType::Stage)
.log_err("Could not get reference space offset.")
.map(|p| {
Affine3A::from_rotation_translation(
p.orientation.into(),
p.position.into(),
)
})
else {
return;
};
let hand_pose = pose * pointer.raw_pose;
self.rotate = Some(MoverData {
pose,
hand: i,
hand_pose,
});
self.drag = None;
log::info!("Start space rotate");
}
// start space rotate
for (i, pointer) in app.input_state.pointers.iter().enumerate() {
if pointer.now.space_rotate {
let Ok(pose) = monado
.ipc
.get_reference_space_offset(ReferenceSpaceType::Stage)
.log_err("Could not get reference space offset.")
.map(|p| {
Affine3A::from_rotation_translation(p.orientation.into(), p.position.into())
})
else {
return;
}
};
let hand_pose = pose * pointer.raw_pose;
self.rotate = Some(MoverData {
pose,
hand: i,
hand_pose,
});
self.drag = None;
log::info!("Start space rotate");
return;
}
}
+9 -9
View File
@@ -148,7 +148,7 @@ impl WlxSwapchain {
struct RetiredSwapchain {
// keep alive until completion is signaled
_swapchain: WlxSwapchain,
swapchain: WlxSwapchain,
completion: Option<GpuCompletionMarker>,
}
@@ -162,7 +162,7 @@ impl SwapchainRetirementQueue {
/// queue the swapchain for non-blocking retirement
pub(super) fn retire(&mut self, swapchain: WlxSwapchain, gfx: &Arc<WGfx>) {
self.retired.push(RetiredSwapchain {
_swapchain: swapchain,
swapchain,
completion: None,
});
@@ -177,7 +177,7 @@ impl SwapchainRetirementQueue {
return Ok(());
}
retired._swapchain.ensure_image_released()?;
retired.swapchain.ensure_image_released()?;
retired.completion = Some(gfx.submit_graphics_completion_marker()?);
Ok(())
}
@@ -185,12 +185,12 @@ impl SwapchainRetirementQueue {
pub(super) fn collect(&mut self, gfx: &Arc<WGfx>) {
let mut idx = 0;
while idx < self.retired.len() {
if self.retired[idx].completion.is_none() {
if let Err(e) = Self::arm(&mut self.retired[idx], gfx) {
log::warn!("Failed re-arming retired OpenXR swapchain: {e:#}");
idx += 1;
continue;
}
if self.retired[idx].completion.is_none()
&& let Err(e) = Self::arm(&mut self.retired[idx], gfx)
{
log::warn!("Failed re-arming retired OpenXR swapchain: {e:#}");
idx += 1;
continue;
}
let complete = match self.retired[idx]
+8
View File
@@ -1,6 +1,7 @@
use std::{
cmp,
collections::{BinaryHeap, VecDeque},
sync::Arc,
sync::atomic::{self, AtomicUsize},
time::Instant,
};
@@ -10,6 +11,7 @@ use strum::EnumIs;
use crate::{
backend::input,
backend::wayvr as wvr,
state::AppState,
windowing::{
OverlayID, OverlaySelector,
@@ -109,6 +111,8 @@ pub enum SpawnPos {
Spread,
/// Spawn relative to a different overlay
Parent(OverlaySelector),
/// Resolve parent or app-id at the point of spawn
WlDefault(wvr::window::WindowHandle, Option<OverlayID>),
}
pub enum GlobalChange {
@@ -138,6 +142,10 @@ pub enum OverlayTask {
Drop(OverlaySelector),
DropImmediate(OverlaySelector),
DropExpired(OverlayID),
SetOverlayName(OverlayID, Arc<str>, bool, bool),
ForgetWindowState(OverlayID, Arc<str>),
RenameWindowState(Arc<str>, Arc<str>),
ClearWindowState(Arc<str>),
}
pub type GlobalTask = dyn FnOnce(&mut AppState) + Send;
+37 -26
View File
@@ -10,10 +10,13 @@ use anyhow::Context;
use glam::{DVec2, Vec2};
use slotmap::DenseSlotMap;
use smithay::{
backend::input::{Axis, AxisSource, ButtonState, Keycode},
backend::input::{Axis, AxisSource, ButtonState, InputTime, Keycode},
input::{
keyboard::KeyboardHandle,
pointer::{AxisFrame, ButtonEvent, MotionEvent, PointerHandle, RelativeMotionEvent},
pointer::{
AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent, PointerHandle,
RelativeMotionEvent,
},
},
reexports::wayland_server::{self, protocol::wl_surface::WlSurface},
utils::{Logical, Point, SerialCounter},
@@ -21,8 +24,6 @@ use smithay::{
use wgui::log::LogErr;
use xkbcommon::xkb;
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
use super::{
ProcessWayVREnv,
comp::{self, ClientState},
@@ -32,6 +33,7 @@ use super::{
pub struct WayVRClient {
pub client: wayland_server::Client,
pub pid: u32,
pub process: process::ProcessHandle,
}
pub struct WayVRCompositor {
@@ -160,16 +162,17 @@ impl WayVRCompositor {
let process_env = get_wayvr_env_from_pid(credentials.pid)?;
// Find suitable auth key from the process list
for (_, process) in processes {
if let process::Process::Managed(process) = &process
for (handle, process) in processes.iter() {
if let process::Process::Managed(process) = process
&& let Some(auth_key) = &process_env.display_auth
{
// Find process with matching auth key
if process.auth_key.as_str() == auth_key {
// Add client
// Associate client now, instead of trying to match env later
self.add_client(WayVRClient {
client,
pid: credentials.pid as u32,
process: handle,
});
return Ok(());
}
@@ -178,18 +181,15 @@ impl WayVRCompositor {
// This is a new process which we didn't met before.
// Treat external processes exclusively (spawned by the user or external program)
log::warn!(
"External process ID {} connected to this Wayland server",
credentials.pid
);
self.state
.wayvr_tasks
.send(WayVRTask::NewExternalProcess(ExternalProcessRequest {
env: process_env,
client,
pid: credentials.pid as u32,
}));
let pid = credentials.pid as u32;
log::warn!("External process ID {pid} connected to this Wayland server");
let process =
processes.insert(process::Process::External(process::ExternalProcess { pid }));
self.add_client(WayVRClient {
client,
pid,
process,
});
Ok(())
}
@@ -239,7 +239,7 @@ impl WayVRCompositor {
Keycode::new(virtual_key),
state,
self.serial_counter.next_serial(),
super::time::get_millis() as u32,
InputTime::from_millis(super::time::get_millis() as u32),
|_, _, _| smithay::input::keyboard::FilterResult::Forward,
);
}
@@ -253,7 +253,7 @@ impl WayVRCompositor {
keycode,
smithay::backend::input::KeyState::Released,
self.serial_counter.next_serial(),
super::time::get_millis() as u32,
InputTime::from_millis(super::time::get_millis() as u32),
|_, _, _| smithay::input::keyboard::FilterResult::Forward,
);
}
@@ -286,13 +286,15 @@ impl WayVRCompositor {
(surface, focus_location)
});
let previous_focus = self.seat_pointer.current_focus();
let time = InputTime::from_millis(super::time::get_millis() as u32);
self.seat_pointer.motion(
&mut self.state,
focus.clone(),
&MotionEvent {
location,
serial: self.serial_counter.next_serial(),
time: super::time::get_millis() as u32,
time,
},
);
self.seat_pointer.relative_motion(
@@ -301,11 +303,18 @@ impl WayVRCompositor {
&RelativeMotionEvent {
delta,
delta_unaccel,
utime: super::time::get_millis(),
time,
},
);
self.seat_pointer.frame(&mut self.state);
self.reset_cursor_on_focus_change(previous_focus);
}
fn reset_cursor_on_focus_change(&mut self, previous_focus: Option<WlSurface>) {
if previous_focus != self.seat_pointer.current_focus() {
self.state.cursor_image = CursorImageStatus::default_named();
}
}
pub fn send_pointer_button(&mut self, index: super::MouseIndex, pressed: bool) {
@@ -316,13 +325,14 @@ impl WayVRCompositor {
};
let serial = self.serial_counter.next_serial();
let time = super::time::get_millis() as u32;
let time = InputTime::from_millis(super::time::get_millis() as u32);
let button = match index {
super::MouseIndex::Left => 0x110,
super::MouseIndex::Center => 0x112,
super::MouseIndex::Right => 0x111,
};
let previous_focus = self.seat_pointer.current_focus();
self.seat_pointer.button(
&mut self.state,
&ButtonEvent {
@@ -334,13 +344,14 @@ impl WayVRCompositor {
);
self.seat_pointer.frame(&mut self.state);
self.reset_cursor_on_focus_change(previous_focus);
}
pub fn send_pointer_axis_wheel_raw(&mut self, delta: super::WheelDelta) {
// 15 logical axis units for one wheel detent of 120 v120 units
const AXIS_VALUE_PER_DETENT: f64 = 15.0;
let time = super::time::get_millis() as u32;
let time = InputTime::from_millis(super::time::get_millis() as u32);
let v120_x = delta.x as i32;
let v120_y = delta.y as i32;
@@ -374,7 +385,7 @@ impl WayVRCompositor {
}
pub fn send_pointer_axis_wheel_accumulated(&mut self, delta: super::WheelDelta) {
let time = super::time::get_millis() as u32;
let time = InputTime::from_millis(super::time::get_millis() as u32);
let multiplier = 32.0;
let delta_x = (delta.x * multiplier) as i32;
+163 -46
View File
@@ -6,20 +6,20 @@ use smithay::desktop::{
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy,
find_popup_root_surface, get_popup_toplevel_coords,
};
use smithay::input::dnd::{DnDGrab, DndGrabHandler, DndTarget, GrabType, Source};
use smithay::input::pointer::{CursorImageStatus, Focus};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::rustix::fs::{OFlags, fcntl_setfl};
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::XdgToplevel;
use smithay::reexports::wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration;
use smithay::reexports::wayland_server::Resource;
use smithay::reexports::wayland_server::backend::ObjectId;
use smithay::reexports::wayland_server::protocol::{
wl_buffer, wl_callback, wl_data_source, wl_output, wl_seat,
};
use smithay::reexports::wayland_server::protocol::{wl_buffer, wl_callback, wl_output, wl_seat};
use smithay::reexports::wayland_server::{self, DisplayHandle};
use smithay::utils::{Logical, Rectangle, Serial, Size};
use smithay::utils::{Logical, Point, Rectangle, Serial, Size};
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::compositor::{self, BufferAssignment, SurfaceAttributes, send_surface_state};
use smithay::wayland::dmabuf::{
@@ -27,10 +27,11 @@ use smithay::wayland::dmabuf::{
};
use smithay::wayland::fractional_scale::with_fractional_scale;
use smithay::wayland::output::OutputHandler;
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::relative_pointer::RelativePointerManagerState;
use smithay::wayland::selection::data_device::{
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
set_data_device_focus, set_data_device_selection,
DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
set_data_device_selection,
};
use smithay::wayland::selection::{self, SelectionHandler};
use smithay::wayland::selection::{
@@ -46,12 +47,8 @@ use smithay::wayland::shell::xdg::{
use smithay::wayland::shm::{ShmHandler, ShmState, shm_format_to_fourcc, with_buffer_contents};
use smithay::wayland::single_pixel_buffer::get_single_pixel_buffer;
use smithay::wayland::viewporter::ViewporterState;
use smithay::{
delegate_compositor, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_ext_data_control, delegate_kde_decoration, delegate_output,
delegate_primary_selection, delegate_relative_pointer, delegate_seat, delegate_shm,
delegate_single_pixel_buffer, delegate_viewporter, delegate_xdg_decoration, delegate_xdg_shell,
};
use smithay::wayland::xdg_toplevel_icon::{XdgToplevelIconHandler, XdgToplevelIconManager};
use smithay::wayland::xdg_toplevel_tag::{XdgToplevelTagHandler, XdgToplevelTagManager};
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::Write;
@@ -62,6 +59,8 @@ use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
use wayland_server::protocol::wl_surface::WlSurface;
use wlx_capture::DrmFourcc;
use super::icon::{self, ToplevelIconSnapshot};
use super::session::SessionRegistry;
use crate::backend::wayvr::image_importer::ImageImporter;
use crate::backend::wayvr::{SurfaceBufWithImage, WAYVR_SCREEN_RES, time};
use crate::ipc::event_queue::SyncEventQueue;
@@ -74,6 +73,14 @@ pub struct Application {
pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
pub compositor: compositor::CompositorState,
pub xdg_shell: XdgShellState,
#[allow(dead_code)]
pub xdg_toplevel_tags: XdgToplevelTagManager,
#[allow(dead_code)]
pub xdg_toplevel_icons: XdgToplevelIconManager,
pub pending_icon_changes: HashMap<ObjectId, ToplevelIconSnapshot>,
pub current_icons: HashMap<ObjectId, ToplevelIconSnapshot>,
pub sessions: SessionRegistry,
pub toplevel_tags: HashMap<ObjectId, Arc<str>>,
pub seat: Seat<Application>,
pub seat_state: SeatState<Application>,
pub shm: ShmState,
@@ -235,7 +242,7 @@ impl Application {
}
}
fn drm_format_has_alpha(format: DrmFourcc) -> bool {
const fn drm_format_has_alpha(format: DrmFourcc) -> bool {
matches!(
format,
DrmFourcc::Abgr8888 | DrmFourcc::Argb8888 | DrmFourcc::Abgr2101010
@@ -256,6 +263,43 @@ impl compositor::CompositorHandler for Application {
#[allow(clippy::significant_drop_tightening)]
fn commit(&mut self, surface: &WlSurface) {
if let Some(icon) = self.pending_icon_changes.remove(&surface.id()) {
if icon.is_empty() {
self.current_icons.remove(&surface.id());
} else {
self.current_icons.insert(surface.id(), icon);
}
if let Some(toplevel) = self
.xdg_shell
.toplevel_surfaces()
.iter()
.find(|t| t.wl_surface() == surface)
{
self.wayvr_tasks
.send(WayVRTask::IconChange(toplevel.clone()));
}
}
if let Some(toplevel) = self
.xdg_shell
.toplevel_surfaces()
.iter()
.find(|toplevel| toplevel.wl_surface() == surface)
{
// first commit is special in terms of restoring session
if self
.sessions
.committed_toplevels
.insert(toplevel.xdg_toplevel().id())
{
log::debug!(
"first toplevel commit, surface: {:?}, toplevel: {:?}",
surface.id(),
toplevel.xdg_toplevel().id()
);
self.wayvr_tasks
.send(WayVRTask::FirstToplevelCommit(toplevel.clone()));
}
}
self.popups_commit(surface);
smithay::wayland::compositor::with_states(surface, |states| {
@@ -397,33 +441,75 @@ impl BufferHandler for Application {
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
}
impl ClientDndGrabHandler for Application {
fn started(
impl PointerConstraintsHandler for Application {}
impl WaylandDndGrabHandler for Application {
fn dnd_requested<S: Source>(
&mut self,
_source: Option<wl_data_source::WlDataSource>,
source: S,
icon: Option<WlSurface>,
_seat: Seat<Self>,
seat: Seat<Self>,
serial: Serial,
type_: GrabType,
) {
let pointer = self.pointer_button_owner.unwrap_or(self.vr_pointer);
self.dnd_pointer = Some(pointer);
self.wayvr_tasks
.send(WayVRTask::DndStarted { icon, pointer });
}
fn dropped(&mut self, _target: Option<WlSurface>, _validated: bool, _seat: Seat<Self>) {
match type_ {
GrabType::Pointer => {
let Some(pointer) = seat.get_pointer() else {
log::warn!("dnd_requested: seat has no pointer");
return;
};
let Some(start_data) = pointer.grab_start_data() else {
log::warn!("dnd_requested: no pointer grab start data");
return;
};
pointer.set_grab(
self,
DnDGrab::new_pointer(&self.display_handle, start_data, source, seat),
serial,
Focus::Keep,
);
}
GrabType::Touch => {
let Some(touch) = seat.get_touch() else {
log::warn!("dnd_requested: seat has no touch");
return;
};
let Some(start_data) = touch.grab_start_data() else {
log::warn!("dnd_requested: no touch grab start data");
return;
};
touch.set_grab(
self,
DnDGrab::new_touch(&self.display_handle, start_data, source, seat),
serial,
);
}
}
}
}
impl DndGrabHandler for Application {
fn dropped(
&mut self,
_target: Option<DndTarget<'_, Self>>,
_validated: bool,
_seat: Seat<Self>,
_location: Point<f64, Logical>,
) {
self.dnd_pointer = None;
self.pointer_button_owner = None;
self.wayvr_tasks.send(WayVRTask::DndDropped);
}
}
impl ServerDndGrabHandler for Application {
fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat<Self>) {}
}
impl DataDeviceHandler for Application {
fn data_device_state(&self) -> &DataDeviceState {
&self.data_device
fn data_device_state(&mut self) -> &mut DataDeviceState {
&mut self.data_device
}
}
@@ -489,6 +575,11 @@ impl XdgShellHandler for Application {
fn new_toplevel(&mut self, surface: ToplevelSurface) {
let wl_surface = surface.wl_surface();
log::debug!(
"new_toplevel:, surface: {:?}, toplevel: {:?}",
wl_surface.id(),
surface.xdg_toplevel().id()
);
self.send_initial_surface_state(wl_surface);
if let Some(client) = wl_surface.client() {
self.wayvr_tasks
@@ -497,6 +588,10 @@ impl XdgShellHandler for Application {
}
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
self.pending_icon_changes.remove(&surface.wl_surface().id());
self.current_icons.remove(&surface.wl_surface().id());
self.toplevel_tags.remove(&surface.xdg_toplevel().id());
self.sessions.forget_toplevel(&surface);
self.output.leave(surface.wl_surface());
self.no_server_decorations
.remove(&surface.wl_surface().id());
@@ -653,6 +748,16 @@ impl XdgShellHandler for Application {
}
}
fn app_id_changed(&mut self, surface: ToplevelSurface) {
self.wayvr_tasks
.send(WayVRTask::WindowIdentityChanged(surface));
}
fn parent_changed(&mut self, surface: ToplevelSurface) {
self.wayvr_tasks
.send(WayVRTask::WindowIdentityChanged(surface));
}
fn title_changed(&mut self, surface: ToplevelSurface) {
if let Some(client) = surface.wl_surface().client() {
self.wayvr_tasks
@@ -661,6 +766,32 @@ impl XdgShellHandler for Application {
}
}
impl XdgToplevelIconHandler for Application {
fn set_icon(&mut self, toplevel: XdgToplevel, surface: WlSurface) {
log::debug!(
"set_icon, surface: {:?}, toplevel: {:?}",
surface.id(),
toplevel.id()
);
let icon = icon::snapshot_pending_icon(&surface);
self.pending_icon_changes.insert(surface.id(), icon);
}
}
impl XdgToplevelTagHandler for Application {
fn set_tag(&mut self, toplevel: XdgToplevel, tag: String) {
if tag.is_empty() {
self.toplevel_tags.remove(&toplevel.id());
} else {
self.toplevel_tags.insert(toplevel.id(), tag.into());
}
if let Some(surface) = self.xdg_shell.get_toplevel(&toplevel) {
self.wayvr_tasks
.send(WayVRTask::WindowIdentityChanged(surface));
}
}
}
impl ShmHandler for Application {
fn shm_state(&self) -> &ShmState {
&self.shm
@@ -695,20 +826,20 @@ impl DmabufHandler for Application {
}
impl PrimarySelectionHandler for Application {
fn primary_selection_state(&self) -> &PrimarySelectionState {
&self.primary_selection_state
fn primary_selection_state(&mut self) -> &mut PrimarySelectionState {
&mut self.primary_selection_state
}
}
impl selection_wlr::DataControlHandler for Application {
fn data_control_state(&self) -> &selection_wlr::DataControlState {
&self.wlr_data_control_state
fn data_control_state(&mut self) -> &mut selection_wlr::DataControlState {
&mut self.wlr_data_control_state
}
}
impl selection_ext::DataControlHandler for Application {
fn data_control_state(&self) -> &selection_ext::DataControlState {
&self.ext_data_control_state
fn data_control_state(&mut self) -> &mut selection_ext::DataControlState {
&mut self.ext_data_control_state
}
}
impl XdgDecorationHandler for Application {
@@ -763,21 +894,7 @@ impl KdeDecorationHandler for Application {
}
}
delegate_dmabuf!(Application);
delegate_xdg_shell!(Application);
delegate_compositor!(Application);
delegate_viewporter!(Application);
delegate_shm!(Application);
delegate_seat!(Application);
delegate_data_device!(Application);
delegate_output!(Application);
delegate_primary_selection!(Application);
delegate_data_control!(Application);
delegate_ext_data_control!(Application);
delegate_xdg_decoration!(Application);
delegate_kde_decoration!(Application);
delegate_single_pixel_buffer!(Application);
delegate_relative_pointer!(Application);
smithay::delegate_dispatch2!(Application);
const fn wl_transform_to_frame_transform(
transform: wl_output::Transform,
+7 -18
View File
@@ -1,14 +1,11 @@
use glam::{Affine2, Vec2};
use smithay::{
desktop::PopupManager,
desktop::{PopupKind, PopupManager},
reexports::wayland_server::{Resource, backend::ObjectId, protocol::wl_surface::WlSurface},
utils::{Logical, Point},
wayland::{
compositor::{
SUBSURFACE_ROLE, SubsurfaceCachedState, SurfaceAttributes, TraversalAction,
with_states, with_surface_tree_upward,
},
shell::xdg::XdgPopupSurfaceData,
wayland::compositor::{
SUBSURFACE_ROLE, SubsurfaceCachedState, SurfaceAttributes, TraversalAction, with_states,
with_surface_tree_upward,
},
};
use std::{ops::RangeInclusive, rc::Rc, sync::Arc};
@@ -383,17 +380,9 @@ pub fn build_hit_context(
let mut popup_roots = Vec::new();
for (popup, point) in PopupManager::popups_for_surface(toplevel) {
let configured = with_states(popup.wl_surface(), |states| {
states
.data_map
.get::<XdgPopupSurfaceData>()
.unwrap()
.lock()
.unwrap()
.configured
});
if !configured {
if let PopupKind::Xdg(popup) = &popup
&& !popup.is_initial_configure_sent()
{
continue;
}
+126
View File
@@ -0,0 +1,126 @@
use smithay::{
reexports::wayland_server::{
Resource,
backend::ObjectId,
protocol::{wl_shm, wl_surface::WlSurface},
},
wayland::{
compositor::with_states, shm::with_buffer_contents,
xdg_toplevel_icon::ToplevelIconCachedState,
},
};
use std::{collections::HashMap, sync::Arc};
use wlx_common::desktop_finder::DesktopFinder;
#[derive(Debug, Default)]
pub struct ToplevelIconSnapshot {
name: Option<Arc<str>>,
png_path: Option<Arc<str>>,
}
impl ToplevelIconSnapshot {
pub fn resolve(&self, finder: &DesktopFinder) -> Option<Arc<str>> {
self.png_path.clone().or_else(|| {
self.name
.as_deref()
.and_then(|name| finder.resolve_icon(name))
.map(Into::into)
})
}
pub const fn is_empty(&self) -> bool {
self.name.is_none() && self.png_path.is_none()
}
}
pub fn toplevel_icon(
surface: &WlSurface,
icons: &HashMap<ObjectId, ToplevelIconSnapshot>,
finder: &DesktopFinder,
) -> Option<Arc<str>> {
icons
.get(&surface.id())
.and_then(|icon| icon.resolve(finder))
}
pub fn snapshot_pending_icon(surface: &WlSurface) -> ToplevelIconSnapshot {
let (name, buffers) = with_states(surface, |states| {
let mut icon = states.cached_state.get::<ToplevelIconCachedState>();
let pending = icon.pending();
(
pending.icon_name().map(Arc::<str>::from),
pending.buffers().to_vec(),
)
});
for (buffer, _scale) in buffers {
let mut png_path = None;
let _ = with_buffer_contents(&buffer, |data, size, info| {
let data = unsafe { std::slice::from_raw_parts(data, size) };
let (w, h) = (info.width as usize, info.height as usize);
if info.width <= 0
|| info.height <= 0
|| info.stride <= 0
|| info.offset < 0
|| w != h
|| w > 1024
{
return;
}
let stride = info.stride as usize;
let offset = info.offset as usize;
if stride < w * 4
|| offset
.checked_add((h - 1).saturating_mul(stride) + w * 4)
.is_none_or(|end| end > size)
{
return;
}
let has_alpha = match info.format {
wl_shm::Format::Argb8888 => true,
wl_shm::Format::Xrgb8888 => false,
_ => return,
};
let mut rgba = vec![0u8; w * h * 4];
for y in 0..h {
for x in 0..w {
let pos = offset + y * stride + x * 4;
let dst = (y * w + x) * 4;
rgba[dst] = data[pos + 2];
rgba[dst + 1] = data[pos + 1];
rgba[dst + 2] = data[pos];
rgba[dst + 3] = if has_alpha { data[pos + 3] } else { 255 };
}
}
let path = wlx_common::cache_dir::get_path(&format!(
"icons/wayland-{}.png",
uuid::Uuid::new_v4()
));
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if image::save_buffer_with_format(
&path,
&rgba,
w as u32,
h as u32,
image::ColorType::Rgba8,
image::ImageFormat::Png,
)
.is_ok()
{
png_path = Some(path.to_string_lossy().into_owned().into());
}
});
if let Some(png_path) = png_path {
return ToplevelIconSnapshot {
name,
png_path: Some(png_path),
};
}
}
ToplevelIconSnapshot {
name,
png_path: None,
}
}
+388 -70
View File
@@ -1,15 +1,19 @@
pub mod client;
mod comp;
pub mod hit_test;
mod icon;
mod image_importer;
mod input_capture;
pub mod process;
mod session;
mod session_protocol;
mod time;
pub mod window;
use anyhow::Context;
use comp::Application;
use glam::{DVec2, Vec2};
use session::{SessionRegistry, app_key, tag_key};
use slotmap::{DenseSlotMap, SecondaryMap};
use smallvec::SmallVec;
use smithay::{
@@ -19,7 +23,7 @@ use smithay::{
reexports::{
wayland_protocols_misc::server_decoration::server::org_kde_kwin_server_decoration_manager as kde_decoration,
wayland_server::{
self,
self, Resource,
backend::ClientId,
protocol::{wl_buffer, wl_surface::WlSurface},
},
@@ -118,22 +122,19 @@ pub struct ProcessWayVREnv {
pub display_name: Option<String>, // Externally spawned process by a user script
}
#[derive(Clone)]
pub struct ExternalProcessRequest {
#[allow(dead_code)]
pub env: ProcessWayVREnv,
pub client: wayland_server::Client,
pub pid: u32,
}
#[derive(Clone)]
pub enum WayVRTask {
NewToplevel(ClientId, ToplevelSurface),
FirstToplevelCommit(ToplevelSurface),
DropToplevel(ClientId, ToplevelSurface),
MinimizeRequest(ClientId, ToplevelSurface),
TitleChange(ClientId, ToplevelSurface),
IconChange(ToplevelSurface),
WindowIdentityChanged(ToplevelSurface),
FreezeWindowState(ToplevelSurface, Arc<str>),
RenameWindowState(Arc<str>, Arc<str>),
ForgetWindowState(Arc<str>),
VisibilityChange(window::WindowHandle, bool),
NewExternalProcess(ExternalProcessRequest),
ProcessTerminationRequest(process::ProcessHandle, KillSignal),
CloseWindowRequest(window::WindowHandle),
DndStarted {
@@ -173,16 +174,16 @@ pub enum PointerFocusTarget {
None,
}
pub enum TickTask {
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
}
const KEY_REPEAT_DELAY: i32 = 500;
const KEY_REPEAT_RATE: i32 = 50;
const WAYVR_SCREEN_RES: [i32; 2] = [2560, 1440];
impl WvrServerState {
pub fn new(gfx: Arc<WGfx>, gfx_extras: &WGfxExtras) -> anyhow::Result<Self> {
pub fn new(
gfx: Arc<WGfx>,
gfx_extras: &WGfxExtras,
config: &GeneralConfig,
) -> anyhow::Result<Self> {
const fn filter_allow_any(_: &wayland_server::Client) -> bool {
true
}
@@ -191,6 +192,26 @@ impl WvrServerState {
let dh = display.handle();
let compositor = compositor::CompositorState::new::<Application>(&dh);
let xdg_shell = XdgShellState::new::<Application>(&dh);
let _session_global = dh.create_global::<Application,
session_protocol::server::xdg_session_manager_v1::XdgSessionManagerV1, _>(1, ());
let xdg_toplevel_tags =
smithay::wayland::xdg_toplevel_tag::XdgToplevelTagManager::new::<Application>(&dh);
let mut xdg_toplevel_icons =
smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager::new::<Application>(&dh);
xdg_toplevel_icons.add_icon_size(64);
if std::env::var("WAYVR_DISABLE_TOPLEVEL_ICONS")
.ok()
.as_deref()
.is_some_and(|value| !value.is_empty() && value != "0")
{
dh.disable_global::<Application>(xdg_toplevel_icons.global());
}
let stored_keys: HashSet<Arc<str>> = config
.sets
.iter()
.flat_map(|set| set.overlays.keys().chain(set.hidden_overlays.keys()))
.cloned()
.collect();
let mut seat_state = SeatState::new();
let shm = ShmState::new::<Application>(&dh, Vec::new());
let data_device = DataDeviceState::new::<Application>(&dh);
@@ -223,6 +244,7 @@ impl WvrServerState {
subpixel: smithay::output::Subpixel::None,
make: String::from("Completely Legit"),
model: String::from("Virtual WayVR Display"),
serial_number: String::new(),
},
);
@@ -282,6 +304,12 @@ impl WvrServerState {
display_handle: dh,
compositor,
xdg_shell,
xdg_toplevel_tags,
xdg_toplevel_icons,
pending_icon_changes: HashMap::new(),
current_icons: HashMap::new(),
sessions: SessionRegistry::new(stored_keys),
toplevel_tags: HashMap::new(),
seat,
seat_state,
shm,
@@ -325,26 +353,29 @@ impl WvrServerState {
#[allow(clippy::too_many_lines)]
#[allow(clippy::significant_drop_tightening)]
pub fn tick_events(app: &mut AppState) -> anyhow::Result<Vec<TickTask>> {
let mut tasks: Vec<TickTask> = Vec::new();
pub fn tick_events(app: &mut AppState) -> anyhow::Result<()> {
let Some(wvr_server) = app.wvr_server.as_mut() else {
return Ok(tasks);
return Ok(());
};
app.ipc_server.tick(&mut ipc_server::TickParams {
wvr_server,
input_state: &app.input_state,
tasks: &mut tasks,
signals: &app.wayvr_signals,
hid_wrapper: &mut app.hid_provider,
});
// Tick all child processes
// Keep launch records while wl client is attached
let attached_processes: HashSet<_> = wvr_server
.manager
.clients
.iter()
.map(|client| client.process)
.collect();
let mut to_remove: SmallVec<[process::ProcessHandle; 2]> = SmallVec::new();
for (handle, process) in &mut wvr_server.processes {
if !process.is_running() {
if !process.is_running() && !attached_processes.contains(&handle) {
to_remove.push(handle);
}
}
@@ -356,10 +387,11 @@ impl WvrServerState {
while let Some(task) = wvr_server.tasks.read() {
match task {
WayVRTask::NewExternalProcess(req) => {
tasks.push(TickTask::NewExternalProcess(req));
}
WayVRTask::NewToplevel(client_id, toplevel) => {
log::debug!(
"NewToplevel: client: {client_id:?}, toplevel: {:?}",
toplevel.xdg_toplevel().id()
);
let toplevel = Rc::new(toplevel);
// Attach newly created toplevel surfaces to displays
@@ -368,15 +400,14 @@ impl WvrServerState {
continue;
}
let Some(process_handle) =
process::find_by_pid(&wvr_server.processes, client.pid)
else {
let process_handle = client.process;
if !wvr_server.processes.contains_key(process_handle) {
log::error!(
"WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.",
"WayVR window creation failed: Process for PID {} was removed after the client connected.",
client.pid
);
continue;
};
}
let output_bounds = wvr_server.manager.state.output_logical_size();
@@ -428,6 +459,29 @@ impl WvrServerState {
),
};
let (xdg_title, app_id): (Option<String>, Option<String>) =
with_states(toplevel.wl_surface(), |states| {
states.data_map.get::<XdgToplevelSurfaceData>().map_or(
(None, None),
|t| {
let t = t.lock().unwrap();
(t.title.clone(), t.app_id.clone())
},
)
});
let app_id: Option<Arc<str>> =
app_id.filter(|s| !s.is_empty()).map(Into::into);
let parent = toplevel.parent().and_then(|surface| {
wvr_server
.wm
.windows
.iter()
.find(|(_, window)| *window.toplevel.wl_surface() == surface)
.map(|(handle, _)| handle)
});
let (name, restore_on_spawn) =
wvr_server.window_identity(&toplevel, app_id.as_deref(), parent, None);
let window_handle = wvr_server.wm.create_window(CreateWindowParams {
toplevel: toplevel.clone(),
process: process_handle,
@@ -436,6 +490,10 @@ impl WvrServerState {
max_size,
size_x: fallback_size.w as _,
size_y: fallback_size.h as _,
app_id: app_id.clone(),
parent,
name: name.clone(),
restore_on_spawn,
});
toplevel.with_pending_state(|state| {
@@ -444,42 +502,41 @@ impl WvrServerState {
state.size = Some(fallback_size);
state.states.set(xdg_toplevel::State::Activated);
});
toplevel.send_configure();
// xdg-shell requires an empty commit before initial configure
if wvr_server
.manager
.state
.sessions
.committed_toplevels
.contains(&toplevel.xdg_toplevel().id())
{
wvr_server.send_first_configure(&toplevel);
}
let mut title: Arc<str> = fallback_title
.unwrap_or_else(|| format!("P{}", client.pid))
.into();
.map(Arc::from)
.or_else(|| name.clone())
.unwrap_or_else(|| format!("P{}", client.pid).into());
let spawn_pos = wvr_server
.last_process_overlay(process_handle)
.map_or(SpawnPos::Spread, |oid| {
SpawnPos::Parent(OverlaySelector::Id(oid))
});
let spawn_pos = SpawnPos::WlDefault(
window_handle,
wvr_server.last_process_overlay(process_handle),
);
let mut icon = icon;
// Try to get title from xdg_toplevel, unless it's running in cage
if !is_cage {
let mut needs_title = true;
let (xdg_title, app_id): (Option<String>, Option<String>) =
with_states(toplevel.wl_surface(), |states| {
states.data_map.get::<XdgToplevelSurfaceData>().map_or(
(None, None),
|t| {
let t = t.lock().unwrap();
(t.title.clone(), t.app_id.clone())
},
)
});
if let Some(xdg_title) = xdg_title {
needs_title = false;
title = xdg_title.into();
}
// Try to get title & icon from desktop entry
if let Some(app_id) = app_id
if let Some(app_id) = app_id.as_deref()
&& let Some(desktop_entry) =
app.desktop_finder.get_cached_entry(&app_id)
app.desktop_finder.get_cached_entry(app_id)
{
if needs_title {
title = desktop_entry.app_name.as_ref().into();
@@ -492,6 +549,13 @@ impl WvrServerState {
}
}
let icon = icon::toplevel_icon(
toplevel.wl_surface(),
&wvr_server.manager.state.current_icons,
&app.desktop_finder,
)
.or(icon);
// Fall back to identicon
let icon = match icon {
Some(icon) => icon,
@@ -502,8 +566,34 @@ impl WvrServerState {
OverlaySelector::Nothing,
spawn_pos,
Box::new(move |app: &mut AppState| {
let (identity, restore_on_spawn) = app
.wvr_server
.as_ref()
.and_then(|wvr| wvr.wm.windows.get(window_handle))
.map_or((None, false), |w| {
(w.name.clone(), w.restore_on_spawn)
});
let persistent = identity.is_some();
let name =
identity.unwrap_or_else(|| window_handle.transient_name());
let icon = app
.wvr_server
.as_ref()
.and_then(|wvr| {
wvr.wm.windows.get(window_handle).and_then(|window| {
icon::toplevel_icon(
window.toplevel.wl_surface(),
&wvr.manager.state.current_icons,
&app.desktop_finder,
)
})
})
.unwrap_or(icon);
create_wl_window_overlay(
name,
title,
persistent,
restore_on_spawn,
app,
window_handle,
icon,
@@ -517,6 +607,16 @@ impl WvrServerState {
)));
}
}
WayVRTask::FirstToplevelCommit(toplevel) => {
let has_window = wvr_server.wm.find_window_handle(&toplevel).is_some();
log::debug!(
"FirstToplevelCommit toplevel: {:?}, has_window: {has_window}",
toplevel.xdg_toplevel().id()
);
if has_window {
wvr_server.send_first_configure(&toplevel);
}
}
WayVRTask::DropToplevel(client_id, toplevel) => {
for client in &wvr_server.manager.clients {
if client.client.id() != client_id {
@@ -557,9 +657,81 @@ impl WvrServerState {
}
}
if let Some(key) = wvr_server
.wm
.windows
.get(window_handle)
.and_then(|w| w.name.clone())
{
wvr_server.manager.state.sessions.stored_keys.insert(key);
}
wvr_server.wm.remove_window(window_handle);
}
}
WayVRTask::WindowIdentityChanged(toplevel) => {
let Some(handle) = wvr_server.wm.find_window_handle(&toplevel) else {
continue;
};
let app_id: Option<Arc<str>> = with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|d| {
d.lock()
.unwrap()
.app_id
.as_deref()
.filter(|s| !s.is_empty())
.map(Into::into)
})
});
let parent = toplevel.parent().and_then(|surface| {
wvr_server
.wm
.windows
.iter()
.find(|(_, w)| *w.toplevel.wl_surface() == surface)
.map(|(id, _)| id)
});
let (key, restore) = wvr_server.window_identity(
&toplevel,
app_id.as_deref(),
parent,
Some(handle),
);
if let Some(window) = wvr_server.wm.windows.get_mut(handle) {
window.app_id = app_id;
window.parent = parent;
if window.name != key || window.restore_on_spawn != restore {
window.name.clone_from(&key);
window.restore_on_spawn = restore;
if let Some(oid) = wvr_server.window_to_overlay.get(&handle) {
let persistent = key.is_some();
let name = key.unwrap_or_else(|| handle.transient_name());
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::SetOverlayName(
*oid, name, persistent, restore,
)));
}
}
}
}
WayVRTask::FreezeWindowState(toplevel, key) => {
if let Some(handle) = wvr_server.wm.find_window_handle(&toplevel)
&& let Some(oid) = wvr_server.window_to_overlay.get(&handle)
{
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::ForgetWindowState(*oid, key)));
}
}
WayVRTask::RenameWindowState(old, new) => {
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::RenameWindowState(old, new)));
}
WayVRTask::ForgetWindowState(key) => {
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::ClearWindowState(key)));
}
WayVRTask::VisibilityChange(window_handle, visible) => {
if let Some(oid) = wvr_server.window_to_overlay.get(&window_handle) {
app.tasks
@@ -594,6 +766,46 @@ impl WvrServerState {
}
}
}
WayVRTask::IconChange(toplevel) => {
if let Some(handle) = wvr_server.wm.find_window_handle(&toplevel)
&& let Some(oid) = wvr_server.window_to_overlay.get(&handle)
{
let icon = icon::toplevel_icon(
toplevel.wl_surface(),
&wvr_server.manager.state.current_icons,
&app.desktop_finder,
);
let fallback = wvr_server
.wm
.windows
.get(handle)
.and_then(|w| w.app_id.as_deref())
.and_then(|id| app.desktop_finder.get_cached_entry(id))
.and_then(|entry| entry.icon_path.as_ref())
.map(|s| Arc::<str>::from(s.as_ref()));
let icon = icon.or(fallback).or_else(|| {
let title =
with_states(toplevel.wl_surface(), |states| {
states.data_map.get::<XdgToplevelSurfaceData>().and_then(
|data| data.lock().ok().and_then(|data| data.title.clone()),
)
})
.unwrap_or_else(|| "window".to_owned());
DesktopFinder::create_icon(&title).ok().map(Into::into)
});
if let Some(icon) = icon {
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Id(*oid),
Box::new(move |app, owc| {
let _ = owc.backend.notify(
app,
OverlayEventData::WvrCommand(WvrCommand::SetIcon(icon)),
);
}),
)));
}
}
}
WayVRTask::TitleChange(client_id, toplevel) => {
for client in &wvr_server.manager.clients {
if client.client.id() != client_id {
@@ -601,19 +813,33 @@ impl WvrServerState {
}
let Some(window_handle) = wvr_server.wm.find_window_handle(&toplevel)
else {
log::warn!("MinimizeRequest: Couldn't find matching window handle");
log::warn!("TitleChange: Couldn't find matching window handle");
continue;
};
if let Some(oid) = wvr_server.window_to_overlay.get(&window_handle) {
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Id(*oid),
Box::new(|app, owc| {
let _ = owc.backend.notify(
app,
OverlayEventData::WvrCommand(WvrCommand::ReloadTitle),
);
}),
)));
let title: Option<Arc<str>> =
with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| {
data.lock().ok().and_then(|data| data.title.clone())
})
.filter(|title| !title.is_empty())
.map(Into::into)
});
if let Some(title) = title {
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Id(*oid),
Box::new(move |app, owc| {
owc.title = Some(title);
let _ = owc.backend.notify(
app,
OverlayEventData::WvrCommand(WvrCommand::ReloadTitle),
);
}),
)));
}
}
}
}
@@ -669,7 +895,7 @@ impl WvrServerState {
Box::new(move |app| {
create_drag_item(app, icon, pointer)
.inspect_err(|e| {
log::warn!("Could not create drag item overlay: {e:?}")
log::warn!("Could not create drag item overlay: {e:?}");
})
.ok()
}),
@@ -703,7 +929,7 @@ impl WvrServerState {
wvr_server.ticks += 1;
Ok(tasks)
Ok(())
}
pub fn config_changed(&mut self, config: &GeneralConfig) {
@@ -746,6 +972,74 @@ impl WvrServerState {
.copied()
}
/// session restore send as part of first configure
fn send_first_configure(&self, toplevel: &ToplevelSurface) {
if toplevel.is_initial_configure_sent() {
return;
}
if let Some(s) = self.manager.state.sessions.get_toplevel(toplevel)
&& s.restore_requested
&& self.manager.state.sessions.stored_keys.contains(&s.key)
&& let Some(resource) = s.restore_resource.as_ref()
{
resource.restored();
}
let serial = toplevel.send_configure();
log::debug!(
"Initial configure sent: to toplevel: {:?}, serial: {serial:?}",
toplevel.xdg_toplevel().id()
);
}
/// Identifier key based on xdg session or app_id+tags or app_id+pid
fn window_identity(
&self,
toplevel: &ToplevelSurface,
app_id: Option<&str>,
parent: Option<window::WindowHandle>,
except: Option<window::WindowHandle>,
) -> (Option<Arc<str>>, bool) {
let session = self.manager.state.sessions.get_toplevel(toplevel);
let tag = self
.manager
.state
.toplevel_tags
.get(&toplevel.xdg_toplevel().id());
let identity = session
.map(|s| (s.key.clone(), s.restore_requested))
.or_else(|| {
app_id
.zip(tag)
.map(|(app_id, tag)| (tag_key(app_id, tag), true))
})
.or_else(|| {
let app_id = app_id?;
if parent.is_some()
|| self.wm.windows.iter().any(|(id, w)| {
Some(id) != except
&& w.parent.is_none()
&& w.app_id.as_deref() == Some(app_id)
})
{
None
} else {
Some((app_key(app_id), true))
}
});
match identity {
Some((key, restore))
if !self
.wm
.windows
.iter()
.any(|(id, w)| Some(id) != except && w.name.as_ref() == Some(&key)) =>
{
(Some(key), restore)
}
_ => (None, false),
}
}
pub fn process_removed(&mut self, tasks: &mut TaskContainer, process: process::ProcessHandle) {
let mut to_remove = vec![];
@@ -1080,13 +1374,41 @@ impl WvrServerState {
}
/// Use HidWrapper::set_input_focus instead!!!
pub const fn set_input_focus(&mut self, has_focus: bool) {
pub fn set_input_focus(&mut self, has_focus: bool) {
if self.has_input_focus && !has_focus {
let pos = self
.wm
.mouse
.as_ref()
.map_or(DVec2::ZERO, |mouse| mouse.pos);
self.manager
.send_mouse_move(None, pos, DVec2::ZERO, DVec2::ZERO);
self.manager.state.cursor_image = CursorImageStatus::default_named();
}
self.has_input_focus = has_focus;
if !has_focus {
self.wm.mouse = None;
}
}
pub fn leave_overlay(&mut self, window: window::WindowHandle, pointer: usize) {
if self.manager.state.vr_pointer != pointer || self.dnd_active_for_pointer(pointer) {
return;
}
let Some(mouse) = self
.wm
.mouse
.as_ref()
.filter(|mouse| mouse.hover_window == window)
else {
return;
};
let pos = mouse.pos;
self.manager
.send_mouse_move(None, pos, DVec2::ZERO, DVec2::ZERO);
self.wm.mouse = None;
}
pub const fn get_focused_window(&self) -> Option<window::WindowHandle> {
if !self.has_input_focus {
return None;
@@ -1226,7 +1548,7 @@ impl WvrServerState {
self.manager.send_pointer_button(index, pressed);
}
pub fn set_vr_pointer(&mut self, pointer: usize) {
pub const fn set_vr_pointer(&mut self, pointer: usize) {
self.manager.state.vr_pointer = pointer;
}
@@ -1241,7 +1563,7 @@ impl WvrServerState {
}
}
pub fn dnd_active(&self) -> bool {
pub const fn dnd_active(&self) -> bool {
self.manager.state.dnd_pointer.is_some()
}
@@ -1307,11 +1629,6 @@ impl WvrServerState {
self.manager.state.set_clipboard_text(content);
}
pub fn add_external_process(&mut self, pid: u32) -> process::ProcessHandle {
self.processes
.insert(process::Process::External(process::ExternalProcess { pid }))
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_process(
&mut self,
@@ -1468,6 +1785,7 @@ impl SurfaceBufWithImage {
let mut guard = surface_data.cached_state.get::<ViewportCachedState>();
let viewport = *guard.current();
drop(guard);
let Some(src) = viewport.src else {
return [0.0, 0.0, 1.0, 1.0];
};
+14 -64
View File
@@ -1,6 +1,6 @@
use std::{collections::HashMap, io::Read, sync::Arc};
use std::{collections::HashMap, sync::Arc};
use slotmap::{DenseSlotMap, new_key_type};
use slotmap::new_key_type;
use wayvr_ipc::{packet_client, packet_server};
#[derive(Debug)]
@@ -87,30 +87,15 @@ impl Process {
impl Drop for WayVRProcess {
fn drop(&mut self) {
log::info!(
"Sending SIGTERM (graceful exit) to process {} ({})",
self.child.id(),
self.exec_path.as_str()
);
self.kill(libc::SIGTERM);
}
}
fn get_process_env_value(pid: i32, key: &str) -> anyhow::Result<Option<String>> {
let path = format!("/proc/{pid}/environ");
let mut env_data = String::new();
std::fs::File::open(path)?.read_to_string(&mut env_data)?;
let lines: Vec<&str> = env_data.split('\0').filter(|s| !s.is_empty()).collect();
for line in lines {
if let Some(cell) = line.split_once('=')
&& cell.0 == key
{
return Ok(Some(String::from(cell.1)));
if self.is_running() {
log::info!(
"Sending SIGTERM (graceful exit) to process {} ({})",
self.child.id(),
self.exec_path.as_str()
);
self.kill(libc::SIGTERM);
}
}
Ok(None)
}
impl WayVRProcess {
@@ -127,9 +112,11 @@ impl WayVRProcess {
}
fn kill(&mut self, signal: i32) {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, signal);
if self.is_running() {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, signal);
}
}
}
@@ -179,43 +166,6 @@ new_key_type! {
pub struct ProcessHandle;
}
pub fn find_by_pid(
processes: &DenseSlotMap<ProcessHandle, Process>,
pid: u32,
) -> Option<ProcessHandle> {
log::debug!("Finding process with PID {pid}");
for (handle, process) in processes {
match &process {
Process::Managed(wayvr_process) => {
if wayvr_process.child.id() == pid {
return Some(handle);
}
}
Process::External(external_process) => {
if external_process.pid == pid {
return Some(handle);
}
}
}
}
log::debug!("Finding by PID failed, trying WAYVR_DISPLAY_AUTH...");
if let Ok(Some(value)) = get_process_env_value(pid as i32, "WAYVR_DISPLAY_AUTH") {
for (handle, process) in processes {
if let Process::Managed(wayvr_process) = &process
&& wayvr_process.auth_key == value
{
return Some(handle);
}
}
}
log::debug!("Process find with PID {pid} failed");
None
}
impl ProcessHandle {
pub fn from_packet(handle: packet_server::WvrProcessHandle) -> Self {
Self::from(slotmap::KeyData::from_ffi(handle.user))
+419
View File
@@ -0,0 +1,419 @@
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use smithay::{
reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
backend::ObjectId,
},
wayland::shell::xdg::ToplevelSurface,
};
use super::{
WayVRTask,
comp::Application,
session_protocol::server::{
xdg_session_manager_v1::{self, XdgSessionManagerV1},
xdg_session_v1::{self, XdgSessionV1},
xdg_toplevel_session_v1::{self, XdgToplevelSessionV1},
},
};
const SESSION_PREFIX: &str = "wayvr/session/";
pub(super) fn session_key(session: &str, name: &str) -> Arc<str> {
format!("{SESSION_PREFIX}{}:{session}/{name}", session.len()).into()
}
fn session_prefix(session: &str) -> String {
format!("{SESSION_PREFIX}{}:{session}/", session.len())
}
pub(super) fn tag_key(app_id: &str, tag: &str) -> Arc<str> {
format!("wayvr/tag/{}:{app_id}/{}:{tag}", app_id.len(), tag.len()).into()
}
pub(super) fn app_key(app_id: &str) -> Arc<str> {
format!("wayvr/app/{}:{app_id}", app_id.len()).into()
}
pub struct WindowSession {
pub key: Arc<str>,
pub restore_requested: bool,
pub restore_resource: Option<XdgToplevelSessionV1>,
session_id: String,
}
#[derive(Default)]
pub struct SessionRegistry {
sessions: HashMap<String, Option<XdgSessionV1>>,
toplevels: HashMap<ObjectId, WindowSession>,
pub stored_keys: HashSet<Arc<str>>,
pub committed_toplevels: HashSet<ObjectId>,
}
impl SessionRegistry {
pub fn new(saved_keys: HashSet<Arc<str>>) -> Self {
Self {
stored_keys: saved_keys,
..Default::default()
}
}
pub fn get_toplevel(&self, toplevel: &ToplevelSurface) -> Option<&WindowSession> {
self.toplevels.get(&toplevel.xdg_toplevel().id())
}
pub fn forget_toplevel(&mut self, toplevel: &ToplevelSurface) {
self.toplevels.remove(&toplevel.xdg_toplevel().id());
self.committed_toplevels
.remove(&toplevel.xdg_toplevel().id());
}
}
impl Application {
fn detach_session(&mut self, session_id: &str, forget: bool) {
let detached: Vec<_> = self
.sessions
.toplevels
.iter()
.filter(|(_, w)| w.session_id == session_id)
.map(|(id, w)| (id.clone(), w.key.clone()))
.collect();
for (id, key) in detached {
self.sessions.toplevels.remove(&id);
let surface = self
.xdg_shell
.toplevel_surfaces()
.iter()
.find(|surface| surface.xdg_toplevel().id() == id)
.cloned();
if forget {
self.sessions.stored_keys.remove(&key);
} else {
self.sessions.stored_keys.insert(key.clone());
if let Some(surface) = &surface {
self.wayvr_tasks
.send(WayVRTask::FreezeWindowState(surface.clone(), key.clone()));
}
}
if let Some(surface) = surface {
self.wayvr_tasks
.send(WayVRTask::WindowIdentityChanged(surface));
}
if forget {
self.wayvr_tasks.send(WayVRTask::ForgetWindowState(key));
}
}
}
fn remove_session_toplevel(&mut self, session_id: &str, name: &str) {
let key = session_key(session_id, name);
let live = self
.sessions
.toplevels
.iter()
.find(|(_, w)| w.session_id == session_id && w.key == key)
.map(|(id, _)| id.clone());
if let Some(id) = live {
self.sessions.toplevels.remove(&id);
if let Some(surface) = self
.xdg_shell
.toplevel_surfaces()
.iter()
.find(|surface| surface.xdg_toplevel().id() == id)
.cloned()
{
self.wayvr_tasks
.send(WayVRTask::WindowIdentityChanged(surface));
}
}
self.sessions.stored_keys.remove(&key);
self.wayvr_tasks.send(WayVRTask::ForgetWindowState(key));
}
}
impl GlobalDispatch<XdgSessionManagerV1, ()> for Application {
fn bind(
_state: &mut Self,
_dh: &DisplayHandle,
_client: &Client,
resource: New<XdgSessionManagerV1>,
_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
data_init.init(resource, ());
}
}
impl Dispatch<XdgSessionManagerV1, ()> for Application {
fn request(
state: &mut Self,
client: &Client,
resource: &XdgSessionManagerV1,
request: xdg_session_manager_v1::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
match request {
xdg_session_manager_v1::Request::GetSession {
id,
reason,
session_id,
} => {
if let WEnum::Unknown(_) = reason {
resource.post_error(
xdg_session_manager_v1::Error::InvalidReason,
"unrecognized session reason",
);
return;
}
let known = session_id.as_ref().is_some_and(|token| {
state.sessions.sessions.contains_key(token)
|| state
.sessions
.stored_keys
.iter()
.any(|key| key.starts_with(session_prefix(token).as_str()))
});
let token = if known {
session_id.unwrap()
} else {
uuid::Uuid::new_v4().to_string()
};
if let Some(Some(old)) = state.sessions.sessions.get(&token) {
if old.client().is_some_and(|c| c.id() == client.id()) {
resource.post_error(
xdg_session_manager_v1::Error::InUse,
"session is already in use by this client",
);
return;
}
old.replaced();
state.detach_session(&token, false);
}
let session_resource = data_init.init(id, token.clone());
if known {
session_resource.restored();
} else {
session_resource.created(token.clone());
}
state
.sessions
.sessions
.insert(token, Some(session_resource));
}
xdg_session_manager_v1::Request::Destroy => {}
}
}
}
impl Dispatch<XdgSessionV1, String> for Application {
fn request(
state: &mut Self,
_client: &Client,
resource: &XdgSessionV1,
request: xdg_session_v1::Request,
session_id: &String,
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
if state
.sessions
.sessions
.get(session_id)
.and_then(Option::as_ref)
!= Some(resource)
{
return;
}
let restore_requested = matches!(&request, xdg_session_v1::Request::RestoreToplevel { .. });
match request {
xdg_session_v1::Request::AddToplevel { id, toplevel, name }
| xdg_session_v1::Request::RestoreToplevel { id, toplevel, name } => {
let key = session_key(session_id, &name);
if state
.sessions
.toplevels
.values()
.any(|w| w.session_id == *session_id && w.key == key)
|| (!restore_requested && state.sessions.stored_keys.contains(&key))
{
resource.post_error(
xdg_session_v1::Error::NameInUse,
"toplevel name already in use",
);
return;
}
let toplevel_id = toplevel.id();
if state.sessions.toplevels.contains_key(&toplevel_id) {
resource.post_error(
xdg_session_v1::Error::AlreadyAdded,
"toplevel already belongs to a session",
);
return;
}
if restore_requested && state.sessions.committed_toplevels.contains(&toplevel_id) {
resource.post_error(
xdg_session_v1::Error::AlreadyMapped,
"restore_toplevel after first commit",
);
return;
}
let can_restore = restore_requested && state.sessions.stored_keys.contains(&key);
let session_resource = data_init.init(id, ());
state.sessions.toplevels.insert(
toplevel_id,
WindowSession {
key,
session_id: session_id.clone(),
restore_requested: can_restore,
restore_resource: Some(session_resource),
},
);
if let Some(surface) = state.xdg_shell.get_toplevel(&toplevel) {
state
.wayvr_tasks
.send(WayVRTask::WindowIdentityChanged(surface));
}
}
xdg_session_v1::Request::RemoveToplevel { name } => {
state.remove_session_toplevel(session_id, &name);
}
xdg_session_v1::Request::Destroy => {
state.detach_session(session_id, false);
state.sessions.sessions.insert(session_id.clone(), None);
}
xdg_session_v1::Request::Remove => {
state.detach_session(session_id, true);
let prefix = session_prefix(session_id);
let keys: Vec<_> = state
.sessions
.stored_keys
.iter()
.filter(|key| key.starts_with(prefix.as_str()))
.cloned()
.collect();
for key in keys {
state.sessions.stored_keys.remove(&key);
state.wayvr_tasks.send(WayVRTask::ForgetWindowState(key));
}
state.sessions.sessions.remove(session_id);
}
}
}
fn destroyed(
state: &mut Self,
_client: smithay::reexports::wayland_server::backend::ClientId,
resource: &XdgSessionV1,
session_id: &String,
) {
if state
.sessions
.sessions
.get(session_id)
.and_then(Option::as_ref)
== Some(resource)
{
state.detach_session(session_id, false);
state.sessions.sessions.insert(session_id.clone(), None);
}
}
}
impl Dispatch<XdgToplevelSessionV1, ()> for Application {
fn request(
state: &mut Self,
_client: &Client,
resource: &XdgToplevelSessionV1,
request: xdg_toplevel_session_v1::Request,
_data: &(),
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
match request {
xdg_toplevel_session_v1::Request::Destroy => {
for w in state.sessions.toplevels.values_mut() {
if w.restore_resource.as_ref() == Some(resource) {
w.restore_resource = None;
}
}
}
xdg_toplevel_session_v1::Request::Rename { name } => {
let entry = state
.sessions
.toplevels
.iter()
.find(|(_, w)| w.restore_resource.as_ref() == Some(resource))
.map(|(id, w)| (id.clone(), w.session_id.clone(), w.key.clone()));
let Some((id, session_id, old_key)) = entry else {
return;
};
let new_key = session_key(&session_id, &name);
if old_key == new_key {
return;
}
if state.sessions.stored_keys.contains(&new_key)
|| state
.sessions
.toplevels
.values()
.any(|w| w.session_id == session_id && w.key == new_key)
{
if let Some(Some(owner)) = state.sessions.sessions.get(&session_id) {
owner.post_error(
xdg_session_v1::Error::NameInUse,
"toplevel name already in use",
);
}
return;
}
if let Some(w) = state.sessions.toplevels.get_mut(&id) {
w.key = new_key.clone();
}
if state.sessions.stored_keys.remove(&old_key) {
state.sessions.stored_keys.insert(new_key.clone());
}
state
.wayvr_tasks
.send(WayVRTask::RenameWindowState(old_key, new_key));
if let Some(surface) = state
.xdg_shell
.toplevel_surfaces()
.iter()
.find(|s| s.xdg_toplevel().id() == id)
.cloned()
{
state
.wayvr_tasks
.send(WayVRTask::WindowIdentityChanged(surface));
}
}
}
}
fn destroyed(
state: &mut Self,
_client: smithay::reexports::wayland_server::backend::ClientId,
resource: &XdgToplevelSessionV1,
_data: &(),
) {
for w in state.sessions.toplevels.values_mut() {
if w.restore_resource.as_ref() == Some(resource) {
w.restore_resource = None;
}
}
}
}
@@ -0,0 +1,23 @@
#[allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
#[allow(non_upper_case_globals, non_snake_case, unused_imports)]
#[allow(clippy::all)]
pub mod server {
use smithay::reexports::wayland_server;
use wayland_protocols::xdg::shell::server::xdg_toplevel;
use wayland_server::protocol::*;
pub mod __interfaces {
use super::wayland_server;
use wayland_protocols::xdg::shell::server::__interfaces::{
XDG_TOPLEVEL_INTERFACE, xdg_toplevel_interface,
};
use wayland_server::backend as wayland_backend;
use wayland_server::protocol::__interfaces::*;
wayland_scanner::generate_interfaces!("protocols/xdg-session-management-v1.xml");
}
use self::__interfaces::{
XDG_SESSION_MANAGER_V1_INTERFACE, XDG_SESSION_V1_INTERFACE,
XDG_TOPLEVEL_SESSION_V1_INTERFACE,
};
wayland_scanner::generate_server_code!("protocols/xdg-session-management-v1.xml");
}
+25 -1
View File
@@ -1,4 +1,4 @@
use std::rc::Rc;
use std::{rc::Rc, sync::Arc};
use glam::DVec2;
use slotmap::{DenseSlotMap, new_key_type};
@@ -24,6 +24,10 @@ pub struct Window {
pub visible: bool,
pub toplevel: Rc<ToplevelSurface>,
pub process: process::ProcessHandle,
pub app_id: Option<Arc<str>>,
pub parent: Option<WindowHandle>,
pub name: Option<Arc<str>>,
pub restore_on_spawn: bool,
pub pending_configure: Option<PendingConfigure>,
}
@@ -35,6 +39,10 @@ impl Window {
bounds: Size<i32, Logical>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
app_id: Option<Arc<str>>,
parent: Option<WindowHandle>,
name: Option<Arc<str>>,
restore_on_spawn: bool,
) -> Self {
Self {
bounds,
@@ -45,6 +53,10 @@ impl Window {
visible: true,
toplevel,
process,
app_id,
parent,
name,
restore_on_spawn,
pending_configure: None,
}
}
@@ -148,6 +160,10 @@ pub struct CreateWindowParams {
pub max_size: Size<i32, Logical>,
pub size_x: u32,
pub size_y: u32,
pub app_id: Option<Arc<str>>,
pub parent: Option<WindowHandle>,
pub name: Option<Arc<str>>,
pub restore_on_spawn: bool,
}
impl WindowManager {
@@ -175,6 +191,10 @@ impl WindowManager {
par.bounds,
par.min_size,
par.max_size,
par.app_id,
par.parent,
par.name,
par.restore_on_spawn,
);
window.remember_committed_size(Size::new(par.size_x as i32, par.size_y as i32), None);
self.windows.insert(window)
@@ -199,4 +219,8 @@ impl WindowHandle {
user: self.0.as_ffi(),
}
}
pub fn transient_name(self) -> Arc<str> {
format!("wayvr/volatile/{}", self.as_packet().user).into()
}
}
+7 -3
View File
@@ -9,9 +9,9 @@ use wayvr_ipc::packet_client::WvrProcessLaunchParams;
use wlx_common::{
astr_containers::AStrMap,
config::{
AltModifier, CaptureMethod, ChromaKeyParams, DefaultPositioning, GeneralConfig,
HandsfreeAltTab, HandsfreePointer, InputCaptureMethod, InputEmulationMethod, PinnedApp,
SerializedWindowSet,
AltModifier, CONFIG_VERSION, CaptureMethod, ChromaKeyParams, DefaultPositioning,
GeneralConfig, HandsfreeAltTab, HandsfreePointer, InputCaptureMethod, InputEmulationMethod,
PinnedApp, SerializedWindowSet,
},
config_io,
locale::Language,
@@ -200,6 +200,7 @@ pub struct AutoSettings {
pub default_opacity: f32,
pub default_positioning: DefaultPositioning,
pub middle_grabs_single: bool,
pub config_version: u32,
}
fn get_settings_path() -> PathBuf {
@@ -277,6 +278,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
default_opacity: config.default_opacity,
default_positioning: config.default_positioning,
middle_grabs_single: config.middle_grabs_single,
config_version: CONFIG_VERSION,
};
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
@@ -294,6 +296,7 @@ pub struct AutoState {
pub spawn_overlays: Vec<Arc<str>>,
pub last_set: u32,
pub attribs: AStrMap<Vec<BackendAttribValue>>,
pub state_version: u32,
}
fn get_state_path() -> PathBuf {
@@ -308,6 +311,7 @@ pub fn save_state(config: &GeneralConfig) -> anyhow::Result<()> {
last_set: config.last_set,
spawn_overlays: config.spawn_overlays.clone(),
attribs: config.attribs.clone(),
state_version: CONFIG_VERSION,
};
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic
+1 -1
View File
@@ -116,7 +116,7 @@ impl OverlayList {
}
params.insert_rc("idx", i.to_string().into());
params.insert("name", meta.name.as_ref());
params.insert("name", meta.title.as_deref().unwrap_or(&meta.name));
parser_state
.instantiate_template(doc_params, template, layout, root, params)?;
let overlay_button = parser_state
+3 -27
View File
@@ -1,38 +1,17 @@
use std::sync::Arc;
use crate::backend::wayvr::{self, WayVRTask, WvrServerState};
use crate::backend::wayvr::{WayVRTask, WvrServerState};
use wayvr_ipc::packet_client::{HandsfreeMode, HandsfreeParams};
use wayvr_ipc::packet_server::PacketServer;
use wlx_common::config::HandsfreePointer;
use crate::{
backend::{
self,
task::{InputTask, OverlayTask, TaskType, ToggleMode},
},
backend::task::{InputTask, OverlayTask, TaskType, ToggleMode},
ipc::{signal::WayVRSignal, window_attrib, window_state},
state::AppState,
windowing::{OverlaySelector, manager::OverlayWindowManager},
};
fn process_tick_tasks(
tick_tasks: Vec<backend::wayvr::TickTask>,
server_state: &mut WvrServerState,
) {
for tick_task in tick_tasks {
match tick_task {
backend::wayvr::TickTask::NewExternalProcess(request) => {
log::info!("Registering external process with PID {}", request.pid);
server_state.add_external_process(request.pid);
server_state.manager.add_client(wayvr::client::WayVRClient {
client: request.client,
pid: request.pid,
});
}
}
}
}
pub fn tick_events<O>(
app: &mut AppState,
overlays: &mut OverlayWindowManager<O>,
@@ -135,10 +114,7 @@ where
}
}
let tick_tasks = WvrServerState::tick_events(app)?;
if let Some(wayvr_server) = app.wvr_server.as_mut() {
process_tick_tasks(tick_tasks, wayvr_server);
}
WvrServerState::tick_events(app)?;
Ok(())
}
-2
View File
@@ -89,8 +89,6 @@ fn read_payload(conn: &mut local_socket::Stream, size: u32) -> anyhow::Result<Op
pub struct TickParams<'a> {
pub wvr_server: &'a mut WvrServerState,
#[allow(dead_code)]
pub tasks: &'a mut Vec<wayvr::TickTask>,
pub signals: &'a SyncEventQueue<WayVRSignal>,
pub input_state: &'a InputState,
pub hid_wrapper: &'a mut HidWrapper,
+38 -19
View File
@@ -180,43 +180,37 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
setup_signal_hooks()?;
let mut used_backend = None;
auto_run(args, &mut used_backend);
auto_run(&args, &mut used_backend);
if RESTART.load(Ordering::Relaxed) {
log::warn!("Restarting...");
let exe = std::env::current_exe()?;
let mut args = vec!["--replace", "--show"];
match used_backend {
Some(XrBackend::OpenXR) => args.push("--openxr"),
Some(XrBackend::OpenVR) => args.push("--openvr"),
_ => {}
}
let _ = Command::new(exe).args(args).spawn();
let _ = Command::new(exe)
.args(restart_args(&args, used_backend))
.spawn();
}
Ok(())
}
#[allow(unused_mut, clippy::similar_names)]
fn auto_run(args: Args, used_backend: &mut Option<XrBackend>) {
fn auto_run(args: &Args, out_backend: &mut Option<XrBackend>) {
let mut tried_xr = false;
let mut tried_vr = false;
loop {
#[cfg(feature = "openxr")]
if !args_get_openvr(&args) {
if !args_get_openvr(args) {
use crate::backend::{BackendError, openxr::openxr_run};
tried_xr = true;
match openxr_run(&args, init_run_params(args.no_audio)) {
match openxr_run(args, init_run_params(args.no_audio)) {
Ok(()) => {
used_backend.replace(XrBackend::OpenXR);
out_backend.replace(XrBackend::OpenXR);
return;
}
Err(BackendError::NotSupported) => (),
Err(e) => {
used_backend.replace(XrBackend::OpenXR);
out_backend.replace(XrBackend::OpenXR);
log::error!("{e:?}");
return;
}
@@ -224,17 +218,17 @@ fn auto_run(args: Args, used_backend: &mut Option<XrBackend>) {
}
#[cfg(feature = "openvr")]
if !args_get_openxr(&args) {
if !args_get_openxr(args) {
use crate::backend::{BackendError, openvr::openvr_run};
tried_vr = true;
match openvr_run(&args, init_run_params(args.no_audio)) {
match openvr_run(args, init_run_params(args.no_audio)) {
Ok(()) => {
used_backend.replace(XrBackend::OpenVR);
out_backend.replace(XrBackend::OpenVR);
return;
}
Err(BackendError::NotSupported) => (),
Err(e) => {
used_backend.replace(XrBackend::OpenVR);
out_backend.replace(XrBackend::OpenVR);
log::error!("{e:?}");
return;
}
@@ -303,6 +297,31 @@ const fn args_get_openxr(args: &Args) -> bool {
ret
}
fn restart_args(args: &Args, backend: Option<XrBackend>) -> Vec<&'static str> {
let mut flags = vec!["--replace", "--show"];
match backend {
Some(XrBackend::OpenXR) => flags.push("--openxr"),
Some(XrBackend::OpenVR) => flags.push("--openvr"),
None => {}
}
if args.headless {
flags.push("--headless");
}
if args.no_autostart {
flags.push("--no-autostart");
}
if args.install {
flags.push("--install");
}
if args.no_audio {
flags.push("--no-audio");
}
flags
}
#[allow(clippy::single_match_else)]
fn setup_signal_hooks() -> anyhow::Result<()> {
let mut signals = Signals::new([SIGINT, SIGTERM, SIGUSR1])?;
+2 -2
View File
@@ -332,7 +332,7 @@ fn tutorial_spawn_effect(app: &mut AppState) {
realign(&mut transform, hmd, 1.0, app.session.config.snap_angle_deg);
owc.active_state = Some(OverlayWindowState {
saved_transform: Some(Affine3A::from_translation(vec3(0., -0.1, -0.9))),
saved_transform: Some(Affine3A::from_translation(vec3(0., -0.1, -0.1))),
transform,
grabbable: true,
interactable: true,
@@ -370,7 +370,7 @@ pub fn create_dash_frontend(app: &mut AppState) -> anyhow::Result<OverlayWindowC
Ok(OverlayWindowConfig {
name: DASH_NAME.into(),
default_state: OverlayWindowState {
transform: Affine3A::from_translation(vec3(0., 0., -0.9)),
transform: Affine3A::from_translation(vec3(0., 0., -0.1)),
grabbable: true,
interactable: true,
positioning: Positioning::Floating,
+115 -33
View File
@@ -8,8 +8,8 @@ use glam::Affine3A;
use smallvec::{SmallVec, smallvec};
use wgui::{
gfx::{
Buffer, BufferUsage, CommandBufferUsage, Filter, Format, Image, ImageView, Scissor,
Vert2Uv, WGfx,
AttachmentBlend, BlendFactor, BlendOp, Buffer, BufferUsage, CommandBufferUsage, Filter,
Format, Image, ImageView, Scissor, Vert2Uv, WGfx,
cmd::WGfxClearMode,
pass::WGfxPass,
pipeline::{WGfxPipeline, WPipelineCreateInfo},
@@ -29,7 +29,10 @@ use crate::{
windowing::backend::{FrameMeta, RenderResources},
};
const CURSOR_SIZE: f32 = 16. / 1440.;
const CURSOR_SIZE: f32 = 64. / 1440.;
// synchronize queue shared by the WlxCapture worker threads
static CAPTURE_QUEUE_MUTEX: Mutex<()> = Mutex::new(());
struct BufPass {
pass: WGfxPass<Vert2Uv>,
@@ -39,6 +42,8 @@ struct BufPass {
/// A render pipeline that supports mouse + stereo
pub struct ScreenPipeline {
mouse: BufPass,
mouse_pipeline: Arc<WGfxPipeline<Vert2Uv>>,
default_cursor_image: Arc<ImageView>,
pass: SmallVec<[BufPass; 2]>,
pipeline: Arc<WGfxPipeline<Vert2Uv>>,
extentf: [f32; 2],
@@ -107,6 +112,22 @@ impl ScreenPipeline {
cmd_xfer.build_and_execute_now()?;
let fallback_image = app.gfx.create_image_view(fallback_image)?;
let mouse_pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap(),
app.gfx_extras.shaders.get("frag_wayland").unwrap(),
WPipelineCreateInfo::new(app.gfx.surface_format())
.use_updatable_descriptors([0])
.use_blend(AttachmentBlend {
src_color_blend_factor: BlendFactor::One,
dst_color_blend_factor: BlendFactor::OneMinusSrcAlpha,
color_blend_op: BlendOp::Add,
src_alpha_blend_factor: BlendFactor::One,
dst_alpha_blend_factor: BlendFactor::OneMinusSrcAlpha,
alpha_blend_op: BlendOp::Add,
}),
)?;
let (mouse, default_cursor_image) =
Self::create_mouse_pass(app, mouse_pipeline.clone(), extentf, offsetf)?;
let mut me = Self {
pass: smallvec![Self::create_pass(
app,
@@ -115,7 +136,9 @@ impl ScreenPipeline {
extentf,
offsetf,
)?],
mouse: Self::create_mouse_pass(app, pipeline.clone(), extentf, offsetf)?,
mouse,
mouse_pipeline,
default_cursor_image,
pipeline,
extentf,
offsetf,
@@ -144,6 +167,7 @@ impl ScreenPipeline {
/// Select normalized rect from backing image
pub fn set_source_uv(&mut self, source_uv: [f32; 4]) {
#[allow(clippy::float_cmp)]
if self.source_uv != source_uv {
self.source_uv = source_uv;
self.pass.clear();
@@ -193,7 +217,8 @@ impl ScreenPipeline {
self.transform = transform;
self.pass.clear();
self.mouse = Self::create_mouse_pass(app, self.pipeline.clone(), extentf, offsetf)?;
(self.mouse, self.default_cursor_image) =
Self::create_mouse_pass(app, self.mouse_pipeline.clone(), extentf, offsetf)?;
Ok(())
}
@@ -227,7 +252,7 @@ impl ScreenPipeline {
pipeline: Arc<WGfxPipeline<Vert2Uv>>,
extentf: [f32; 2],
offsetf: [f32; 2],
) -> anyhow::Result<BufPass> {
) -> anyhow::Result<(BufPass, Arc<ImageView>)> {
#[rustfmt::skip]
let mouse_bytes = [
0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff,
@@ -248,7 +273,7 @@ impl ScreenPipeline {
.gfx
.empty_buffer(BufferUsage::TRANSFER_DST | BufferUsage::VERTEX_BUFFER, 4)?;
let set0 = pipeline.uniform_sampler(0, view, Filter::Nearest)?;
let set0 = pipeline.uniform_sampler(0, view.clone(), Filter::Nearest)?;
let pass = pipeline.create_pass(
extentf,
offsetf,
@@ -260,7 +285,7 @@ impl ScreenPipeline {
)?;
cmd_xfer.build_and_execute_now()?;
Ok(BufPass { pass, buf_vert })
Ok((BufPass { pass, buf_vert }, view))
}
pub fn render(
@@ -272,7 +297,7 @@ impl ScreenPipeline {
) -> anyhow::Result<()> {
self.render_screen(image, app, rdr)?;
if let Some(mouse) = mouse {
self.render_mouse(mouse, rdr)?;
self.render_mouse(mouse, rdr, None)?;
}
Ok(())
}
@@ -302,10 +327,26 @@ impl ScreenPipeline {
&mut self,
mouse: &MouseMeta,
rdr: &mut RenderResources,
cursor: Option<(&Arc<ImageView>, [f32; 2], [f32; 2])>,
) -> anyhow::Result<()> {
let image = cursor.map_or(&self.default_cursor_image, |(image, _, _)| image);
self.mouse
.pass
.update_sampler(0, image.clone(), Filter::Nearest)?;
for cmd_buf in &mut rdr.cmd_bufs {
let size = CURSOR_SIZE * self.extentf[1];
let half_size = size * 0.5;
let (width, height, hotspot_x, hotspot_y) =
if let Some((_, dimensions, hotspot)) = cursor {
let scale = size / dimensions[0].max(dimensions[1]).max(1.0);
(
dimensions[0] * scale,
dimensions[1] * scale,
hotspot[0] * scale,
hotspot[1] * scale,
)
} else {
(size, size, size * 0.5, size * 0.5)
};
let (x_scale, y_scale) = if self.stereo_adjust_mouse {
match self.stereo {
@@ -321,10 +362,10 @@ impl ScreenPipeline {
&self.mouse.buf_vert,
self.extentf[0],
self.extentf[1],
mouse.x.mul_add(self.extentf[0] * x_scale, -half_size),
mouse.y.mul_add(self.extentf[1] * y_scale, -half_size),
size,
size,
mouse.x.mul_add(self.extentf[0] * x_scale, -hotspot_x),
mouse.y.mul_add(self.extentf[1] * y_scale, -hotspot_y),
width,
height,
)?;
cmd_buf.run_ref(&self.mouse.pass)?;
@@ -664,6 +705,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 +731,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 +817,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 +885,8 @@ pub(super) enum MainThreadFrame {
Cpu {
format: FrameFormat,
data: Vec<u8>,
offset: usize,
stride: usize,
mouse: Option<MouseMeta>,
},
Dmabuf {
@@ -870,12 +946,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 +990,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,
})
}
+130 -38
View File
@@ -3,13 +3,13 @@ use std::{mem, ops::RangeInclusive, rc::Rc, sync::Arc};
use glam::{Affine2, Affine3A, Quat, Vec3, vec2, vec3};
use slotmap::Key;
use smithay::{
desktop::PopupManager,
input::pointer::CursorImageStatus,
desktop::{PopupKind, PopupManager},
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
reexports::wayland_server::Resource,
utils::{Logical, Size},
utils::{IsAlive, Logical, Size},
wayland::{
compositor::with_states,
shell::xdg::{SurfaceCachedState, XdgPopupSurfaceData, XdgToplevelSurfaceData},
shell::xdg::{SurfaceCachedState, XdgToplevelSurfaceData},
},
};
use wayvr_ipc::packet_client::PositionMode;
@@ -70,6 +70,7 @@ use crate::{
#[derive(Clone)]
pub enum WvrCommand {
ReloadTitle,
SetIcon(Arc<str>),
CloseWindow,
KillProcess(KillSignal),
}
@@ -88,6 +89,9 @@ const BLEND_WAYLAND_ALPHA: AttachmentBlend = AttachmentBlend {
pub fn create_wl_window_overlay(
name: Arc<str>,
title: Arc<str>,
persistent: bool,
restore_on_spawn: bool,
app: &mut AppState,
window: wayvr::window::WindowHandle,
icon: Arc<str>,
@@ -109,7 +113,10 @@ pub fn create_wl_window_overlay(
.is_some_and(Window::resizable);
Ok(OverlayWindowConfig {
name: name.clone(),
name,
title: Some(title.clone()),
persistent,
restore_on_spawn,
default_state: OverlayWindowState {
grabbable: true,
interactable: true,
@@ -131,7 +138,7 @@ pub fn create_wl_window_overlay(
category: OverlayCategory::WayVR,
show_on_spawn: true,
..OverlayWindowConfig::from_backend(Box::new(WvrWindowBackend::new(
name,
title,
app,
window,
icon,
@@ -142,7 +149,7 @@ pub fn create_wl_window_overlay(
}
pub struct WvrWindowBackend {
name: Arc<str>,
title: Arc<str>,
icon: Arc<str>,
pipeline: Option<ScreenPipeline>,
subsurface_pipeline_opaque: Arc<WGfxPipeline<Vert2Uv>>,
@@ -156,6 +163,8 @@ pub struct WvrWindowBackend {
just_resumed: bool,
meta: Option<FrameMeta>,
mouse: Option<MouseMeta>,
cursor_revision: Option<(Arc<ImageView>, [i32; 2], [i32; 2])>,
cursor_status: CursorImageStatus,
stereo: Option<StereoMode>,
stereo_full_frame: bool,
stereo_adjust_mouse: bool,
@@ -176,7 +185,7 @@ pub struct WvrWindowBackend {
impl WvrWindowBackend {
fn new(
name: Arc<str>,
title: Arc<str>,
app: &mut AppState,
window: wayvr::window::WindowHandle,
icon: Arc<str>,
@@ -243,7 +252,7 @@ impl WvrWindowBackend {
NewGuiPanelParams {
resize_to_parent: true,
on_custom_attrib: Some(on_custom_attrib),
extra_vars: [("title".into(), name.as_ref().into())].into(),
extra_vars: [("title".into(), title.as_ref().into())].into(),
..Default::default()
},
)?;
@@ -251,7 +260,7 @@ impl WvrWindowBackend {
panel.update_layout(app)?;
Ok(Self {
name,
title,
icon,
pipeline: None,
window,
@@ -265,6 +274,8 @@ impl WvrWindowBackend {
just_resumed: false,
meta: None,
mouse: None,
cursor_revision: None,
cursor_status: CursorImageStatus::default_named(),
stereo: app
.feats
.xr_backend
@@ -390,6 +401,7 @@ impl WvrWindowBackend {
}
fn update_title(&mut self, new_title: Rc<str>) {
self.title = new_title.as_ref().into();
let mut common = CallbackDataCommon {
state: &self.panel.layout.state,
alterables: &mut self.panel.layout.alterables,
@@ -527,7 +539,11 @@ impl WvrWindowBackend {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
.and_then(|data| {
data.lock()
.ok()
.and_then(|data| data.last_acked.as_ref().map(|c| c.serial))
})
});
if committed == clamped {
@@ -568,7 +584,7 @@ impl OverlayBackend for WvrWindowBackend {
else {
log::debug!(
"{:?}: WayVR overlay without matching window entry",
self.name
self.title
);
return Ok(ShouldRender::Unable);
};
@@ -585,20 +601,12 @@ impl OverlayBackend for WvrWindowBackend {
let surfaces = collect_rendered_surface_tree(toplevel.wl_surface());
let mut popup_roots = Vec::new();
let mut popups = Vec::new();
let mut popup_trees = Vec::new();
for (popup, point) in PopupManager::popups_for_surface(toplevel.wl_surface()) {
let configured = with_states(popup.wl_surface(), |states| {
states
.data_map
.get::<XdgPopupSurfaceData>()
.unwrap()
.lock()
.unwrap()
.configured
});
if !configured {
if let PopupKind::Xdg(popup) = &popup
&& !popup.is_initial_configure_sent()
{
continue;
}
@@ -609,13 +617,15 @@ impl OverlayBackend for WvrWindowBackend {
surface_origin: vec2(popup_origin.x as f32, popup_origin.y as f32),
});
popups.extend(collect_rendered_surface_tree_at(
popup_trees.push(collect_rendered_surface_tree_at(
popup.wl_surface(),
popup_origin,
true,
));
}
let popups: Vec<RenderedSurface> = popup_trees.into_iter().rev().flatten().collect();
let mut tree_dirty = false;
if let Some(wvr_server) = app.wvr_server.as_mut() {
@@ -653,10 +663,11 @@ impl OverlayBackend for WvrWindowBackend {
let source_uv = surf.source_uv(states);
let mut xdg_state = states.cached_state.get::<SurfaceCachedState>();
let window_geometry = xdg_state.current().geometry;
drop(xdg_state);
Some((surf, surface_size, source_uv, window_geometry))
})
else {
log::trace!("{}: no buffer for wl_surface", self.name);
log::trace!("{}: no buffer for wl_surface", self.title);
return Ok(ShouldRender::Unable);
};
@@ -691,9 +702,9 @@ impl OverlayBackend for WvrWindowBackend {
}
let inner_extent = meta.extent;
let committed_window_extent = window_geometry
.map(|geometry| [geometry.size.w.max(1) as u32, geometry.size.h.max(1) as u32])
.unwrap_or(inner_extent);
let committed_window_extent = window_geometry.map_or(inner_extent, |geometry| {
[geometry.size.w.max(1) as u32, geometry.size.h.max(1) as u32]
});
self.sync_committed_toplevel_size(app, committed_window_extent);
if self.server_side_decorations {
@@ -752,7 +763,48 @@ impl OverlayBackend for WvrWindowBackend {
y: (m.pos.y as f32) / (inner_extent[1] as f32),
});
let dirty = self.mouse != mouse || rendered_surfaces_dirty(&self.popups, &popups);
let cursor_status = {
let state = &mut app.wvr_server.as_mut().unwrap().manager.state;
if matches!(
&state.cursor_image,
CursorImageStatus::Surface(surface) if !surface.alive()
) {
state.cursor_image = CursorImageStatus::default_named();
}
if mouse.is_some() {
state.cursor_image.clone()
} else {
CursorImageStatus::default_named()
}
};
let cursor_revision = match &cursor_status {
CursorImageStatus::Surface(surface) if surface.alive() => {
with_states(surface, |states| {
let image = SurfaceBufWithImage::get_from_surface(states)?;
let size = image.logical_size(states);
let hotspot = states
.data_map
.get::<CursorImageSurfaceData>()?
.lock()
.ok()?
.hotspot;
Some((image.image, [size.w, size.h], [hotspot.x, hotspot.y]))
})
}
_ => None,
};
let cursor_dirty = self.cursor_status != cursor_status
|| match (&self.cursor_revision, &cursor_revision) {
(Some((a, asize, ah)), Some((b, bsize, bh))) => {
!Arc::ptr_eq(a, b) || asize != bsize || ah != bh
}
(None, None) => false,
_ => true,
};
self.cursor_status = cursor_status;
self.cursor_revision = cursor_revision;
let dirty =
cursor_dirty || self.mouse != mouse || rendered_surfaces_dirty(&self.popups, &popups);
if !self.scrolling {
self.mouse = mouse;
}
@@ -771,7 +823,7 @@ impl OverlayBackend for WvrWindowBackend {
{
log::trace!(
"{}: new {} image",
self.name,
self.title,
if surf.dmabuf { "DMA-buf" } else { "SHM" }
);
self.cur_image = Some(surf.image);
@@ -817,12 +869,40 @@ impl OverlayBackend for WvrWindowBackend {
if let Some(wvr_server) = app.wvr_server.as_mut() {
let state = &mut wvr_server.manager.state;
match state.cursor_image {
CursorImageStatus::Hidden => {}
CursorImageStatus::Named(_) | CursorImageStatus::Surface(_) => {
// TODO: properly render surface?
if let Some(mouse) = self.mouse.as_ref() {
self.pipeline.as_mut().unwrap().render_mouse(mouse, rdr)?;
if let Some(mouse) = self.mouse.as_ref() {
match state.cursor_image.clone() {
CursorImageStatus::Hidden => {}
CursorImageStatus::Surface(surface) if surface.alive() => {
let cursor = with_states(&surface, |states| {
let image = SurfaceBufWithImage::get_from_surface(states)?;
let size = image.logical_size(states);
let hotspot = states
.data_map
.get::<CursorImageSurfaceData>()?
.lock()
.ok()?
.hotspot;
Some((
image.image,
[size.w as f32, size.h as f32],
[hotspot.x as f32, hotspot.y as f32],
))
});
if let Some((image, size, hotspot)) = cursor {
self.pipeline.as_mut().unwrap().render_mouse(
mouse,
rdr,
Some((&image, size, hotspot)),
)?;
state.send_frame_callbacks_for_surface_id(&surface.id());
}
// a live cursor surface with no buffer makes an invisible cursor
}
CursorImageStatus::Named(_) | CursorImageStatus::Surface(_) => {
self.pipeline
.as_mut()
.unwrap()
.render_mouse(mouse, rdr, None)?;
}
}
}
@@ -857,6 +937,9 @@ impl OverlayBackend for WvrWindowBackend {
let wvr_server = app.wvr_server.as_mut().unwrap(); //never None
wvr_server.overlay_added(oid, self.window);
}
OverlayEventData::WvrCommand(WvrCommand::SetIcon(icon)) => {
self.icon = icon;
}
OverlayEventData::WvrCommand(WvrCommand::ReloadTitle) => {
let wvr_server = app.wvr_server.as_mut().unwrap(); //never None
if let Some(window) = wvr_server.wm.windows.get(self.window) {
@@ -926,6 +1009,12 @@ impl OverlayBackend for WvrWindowBackend {
Some(WvrHitTarget::Panel(hit2)) => {
if let Some(wvr_server) = app.wvr_server.as_mut() {
wvr_server.clear_dnd_focus(hit.pointer);
wvr_server.send_mouse_move(
PointerFocusTarget::None,
ctx.unclamped_client_pos_from_hit(hit),
self.window,
false,
);
}
self.panel_hovered = true;
self.panel.on_hover(app, &hit2)
@@ -996,6 +1085,9 @@ impl OverlayBackend for WvrWindowBackend {
self.panel.on_left(app, pointer);
self.panel_hovered = false;
}
if let Some(wvr_server) = app.wvr_server.as_mut() {
wvr_server.leave_overlay(self.window, pointer);
}
}
fn on_pointer(&mut self, app: &mut state::AppState, hit: &input::PointerHit, pressed: bool) {
@@ -1161,7 +1253,7 @@ impl OverlayBackend for WvrWindowBackend {
match value {
BackendAttribValue::Stereo(new) => {
if let Some(stereo) = self.stereo.as_mut() {
log::debug!("{}: stereo: {stereo:?} → {new:?}", self.name);
log::debug!("{}: stereo: {stereo:?} → {new:?}", self.title);
*stereo = new;
if let Some(meta) = self.meta {
let _ = self.apply_extent(app, &meta);
+6 -10
View File
@@ -223,26 +223,22 @@
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/right/input/thumbstick"
},
{
+6 -10
View File
@@ -141,26 +141,22 @@
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/right/input/thumbstick"
}
]
+6 -10
View File
@@ -107,26 +107,22 @@
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"position" : {
"output" : "/actions/default/in/scroll"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"mode" : "joystick",
"parameters" : {},
"path" : "/user/hand/right/input/joystick"
}
]
+1 -1
View File
@@ -105,7 +105,7 @@ impl AppState {
let wvr_signals = SyncEventQueue::new();
let wvr_server = {
let mut maybe_wvr = WvrServerState::new(gfx.clone(), &gfx_extras)
let mut maybe_wvr = WvrServerState::new(gfx.clone(), &gfx_extras, &session.config)
.log_err("Could not initialize WayVR Server")
.ok();
if let Some(wvr) = maybe_wvr.as_mut() {
+1
View File
@@ -127,6 +127,7 @@ macro_rules! attrib_value {
pub struct OverlayMeta {
pub id: OverlayID,
pub name: Arc<str>,
pub title: Option<Arc<str>>,
pub category: OverlayCategory,
pub visible: bool,
pub icon: Option<Arc<str>>,
+309 -44
View File
@@ -1,6 +1,7 @@
use std::{
collections::{HashMap, VecDeque},
rc::Rc,
sync::Arc,
sync::atomic::Ordering,
};
@@ -9,9 +10,11 @@ use glam::{Affine3A, Vec3, Vec3A};
use slotmap::{Key, SecondaryMap, SlotMap};
use wgui::{i18n::Translation, log::LogErr};
use wlx_common::{
DesktopBackend,
astr_containers::{AStrMap, AStrMapExt},
config::SerializedWindowSet,
overlays::{BackendAttrib, BackendAttribValue, ToastTopic},
windowing::{OverlayWindowState, Positioning},
};
use crate::{
@@ -84,6 +87,8 @@ where
};
if headless {
app.screens.clear();
app.feats.desktop_backend = DesktopBackend::Headless;
log::info!("Running in headless mode; keyboard will be en-US");
} else {
// create one window set for each screen.
@@ -92,12 +97,12 @@ where
match create_screens(app) {
Ok((data, backend)) => {
app.feats.desktop_backend = backend;
let last_idx = data.screens.len() - 1;
let screen_count = data.screens.len();
for (idx, (meta, mut config)) in data.screens.into_iter().enumerate() {
config.show_on_spawn = true;
me.add(OverlayWindowData::from_config(config), app);
if idx < last_idx {
if idx + 1 < screen_count {
me.sets.push(OverlayWindowSet::default());
me.switch_to_set(app, Some(me.current_set.unwrap() + 1), false);
}
@@ -243,6 +248,7 @@ where
_ => {}
}
let key = o.config.name.clone();
let parent_set = if o.config.global {
&mut self.global_set
} else {
@@ -254,8 +260,8 @@ where
parent_set
.hidden_overlays
.arc_set(o.config.name.clone(), active_state);
} else if let Some(state) = parent_set.hidden_overlays.arc_rm(&o.config.name) {
.arc_set(key.clone(), active_state);
} else if let Some(state) = parent_set.hidden_overlays.arc_rm(&key) {
let o = &mut self.overlays[id];
log::debug!("{}: toggle on", o.config.name);
o.config.dirty = true;
@@ -387,9 +393,23 @@ where
OverlayTask::Modify(sel, f) => {
if let Some(o) = self.mut_by_selector(&sel) {
let was_visible = o.config.is_active();
let old_title = o.config.title.clone();
let old_icon = match o.config.backend.get_attrib(BackendAttrib::Icon) {
Some(BackendAttribValue::Icon(icon)) => Some(icon),
_ => None,
};
f(app, &mut o.config);
let new_icon = match o.config.backend.get_attrib(BackendAttrib::Icon) {
Some(BackendAttribValue::Icon(icon)) => Some(icon),
_ => None,
};
let title_changed = old_title != o.config.title || old_icon != new_icon;
let visibility_changed = was_visible != o.config.is_active();
if was_visible != o.config.is_active() {
if title_changed {
self.overlays_changed(app)?;
}
if visibility_changed {
let _ = self.visible_overlays_changed(app);
}
} else {
@@ -400,65 +420,52 @@ where
self.spawn_overlay(app, sel, spawn_pos, f);
}
OverlayTask::Drop(sel) => {
let (id, name) = match &sel {
OverlaySelector::Id(id) => {
let id = *id;
let name = self.overlays.get(id).map(|o| o.config.name.clone());
(Some(id), name)
}
OverlaySelector::Name(name) => {
let id = self.lookup(name);
(id, Some(name.clone()))
}
_ => (None, None),
};
let id = self.id_by_selector(&sel);
if let Some(o) = self.mut_by_selector(&sel)
&& o.birthframe < FRAME_COUNTER.load(Ordering::Relaxed)
&& let Some(o) = self.remove_by_selector(&sel, app)
{
log::debug!("Dropping overlay {}", o.config.name);
if let (Some(id), Some(name)) = (id, name) {
self.remove_saved_state(id, &name);
if let Some(id) = id {
self.on_overlay_dropped(id, &o);
}
self.dropped_overlays.push_back(o);
}
}
OverlayTask::DropImmediate(sel) => {
let (id, name) = match &sel {
OverlaySelector::Id(id) => {
let id = *id;
let name = self.overlays.get(id).map(|o| o.config.name.clone());
(Some(id), name)
}
OverlaySelector::Name(name) => {
let id = self.lookup(name);
(id, Some(name.clone()))
}
_ => (None, None),
};
let id = self.id_by_selector(&sel);
if let Some(o) = self.remove_by_selector(&sel, app) {
log::debug!("Dropping overlay {} immediately", o.config.name);
if let (Some(id), Some(name)) = (id, name) {
self.remove_saved_state(id, &name);
if let Some(id) = id {
self.on_overlay_dropped(id, &o);
}
self.dropped_overlays.push_back(o);
}
}
OverlayTask::SetOverlayName(id, name, persistent, restore_on_spawn) => {
self.set_window_name(app, id, name, persistent, restore_on_spawn);
}
OverlayTask::ForgetWindowState(id, key) => {
self.freeze_window_state(id, &key);
}
OverlayTask::RenameWindowState(old, new) => {
self.rename_window_state(app, &old, &new);
}
OverlayTask::ClearWindowState(key) => {
self.clear_window_state(app, &key);
}
OverlayTask::DropExpired(id) => {
let should_drop = self
.overlays
.get(id)
.and_then(|overlay| overlay.config.lifetime.as_ref())
.is_some_and(|lifetime| lifetime.is_expired());
.is_some_and(super::window::OverlayLifetime::is_expired);
if should_drop
&& let Some(o) = self.remove_by_selector(&OverlaySelector::Id(id), app)
{
log::debug!("Dropping expired overlay {}", o.config.name);
let name = o.config.name.clone();
self.remove_saved_state(id, &name);
self.on_overlay_dropped(id, &o);
self.dropped_overlays.push_back(o);
}
}
@@ -532,14 +539,195 @@ impl<T> OverlayWindowManager<T> {
self.dropped_overlays.pop_front()
}
fn remove_saved_state(&mut self, id: OverlayID, name: &str) {
fn on_overlay_dropped(&mut self, id: OverlayID, overlay: &OverlayWindowData<T>) {
let name = &overlay.config.name;
let remember_position =
overlay.config.category == OverlayCategory::WayVR && overlay.config.can_persist();
// keep removed overlays by name
for set in &mut self.sets {
let state = set.overlays.remove(id);
let hidden_state = set.hidden_overlays.arc_rm(name);
if remember_position {
if let Some(state) = state.or(hidden_state) {
set.inactive_overlays.arc_set(name.clone(), state);
}
} else {
set.inactive_overlays.arc_rm(name);
}
}
let state = self.global_set.overlays.remove(id);
let hidden_state = self.global_set.hidden_overlays.arc_rm(name);
if remember_position {
if let Some(state) = state.or(hidden_state) {
self.global_set
.inactive_overlays
.arc_set(name.clone(), state);
}
if let Some(state) = &overlay.config.active_state {
let set = if overlay.config.global {
&mut self.global_set
} else {
&mut self.sets[self.current_set.unwrap_or(self.restore_set)]
};
set.inactive_overlays.arc_set(name.clone(), state.clone());
}
} else {
// transient overlays (toasts, etc)
self.global_set.inactive_overlays.arc_rm(name);
}
}
/// Set the name of an existing window, restoring the state associated with the new name
fn set_window_name(
&mut self,
app: &mut AppState,
id: OverlayID,
name: Arc<str>,
persistent: bool,
restore_on_spawn: bool,
) {
let Some(overlay) = self.overlays.get(id) else {
return;
};
if overlay.config.category != OverlayCategory::WayVR
|| (overlay.config.name == name
&& overlay.config.persistent == persistent
&& overlay.config.restore_on_spawn == restore_on_spawn)
{
return;
}
if self
.overlays
.iter()
.any(|(other_id, other)| other_id != id && other.config.name == name)
{
log::debug!("Ignoring duplicate WayVR window name {name}");
return;
}
let old_name = overlay.config.name.clone();
let global = overlay.config.global;
if old_name != name {
for set in &mut self.sets {
if let Some(state) = set.hidden_overlays.arc_rm(&old_name) {
set.hidden_overlays.arc_set(name.clone(), state);
}
}
if let Some(state) = self.global_set.hidden_overlays.arc_rm(&old_name) {
self.global_set.hidden_overlays.arc_set(name.clone(), state);
}
}
{
let config = &mut self.overlays[id].config;
config.name = name.clone();
config.persistent = persistent;
config.restore_on_spawn = restore_on_spawn;
}
if let Err(err) = self.overlays_changed(app) {
log::error!("Unable to update overlay name: {err:?}");
}
if !persistent || !restore_on_spawn {
return;
}
// apply to all saved states
if global {
if let Some(state) = self.global_set.inactive_overlays.arc_rm(&name) {
let o = &mut self.overlays[id];
o.config.active_state = Some(state);
o.config.reset(app, false);
o.config.dirty = true;
}
} else {
for (i, set) in self.sets.iter_mut().enumerate() {
let Some(state) = set.inactive_overlays.arc_rm(&name) else {
continue;
};
if self.current_set == Some(i) {
let o = &mut self.overlays[id];
o.config.active_state = Some(state);
o.config.reset(app, false);
o.config.dirty = true;
} else {
set.overlays.insert(id, state);
}
}
}
}
fn freeze_window_state(&mut self, id: OverlayID, name: &Arc<str>) {
let Some(overlay) = self.overlays.get(id) else {
return;
};
if let Some(state) = overlay.config.active_state.clone() {
let set = if overlay.config.global {
&mut self.global_set
} else {
&mut self.sets[self.current_set.unwrap_or(self.restore_set)]
};
set.inactive_overlays.arc_set(name.clone(), state);
} else {
for set in &mut self.sets {
if let Some(state) = set.overlays.get(id) {
set.inactive_overlays.arc_set(name.clone(), state.clone());
}
}
if let Some(state) = self.global_set.overlays.get(id) {
self.global_set
.inactive_overlays
.arc_set(name.clone(), state.clone());
}
}
}
fn rename_window_state(&mut self, app: &mut AppState, old: &Arc<str>, new: &Arc<str>) {
if old == new {
return;
}
for set in &mut self.sets {
if let Some(state) = set.inactive_overlays.arc_rm(old) {
set.inactive_overlays.arc_set(new.clone(), state);
}
if let Some(state) = set.hidden_overlays.arc_rm(old) {
set.hidden_overlays.arc_set(new.clone(), state);
}
}
if let Some(state) = self.global_set.inactive_overlays.arc_rm(old) {
self.global_set
.inactive_overlays
.arc_set(new.clone(), state);
}
if let Some(state) = self.global_set.hidden_overlays.arc_rm(old) {
self.global_set.hidden_overlays.arc_set(new.clone(), state);
}
if let Some(attrs) = app.session.config.attribs.arc_rm(old) {
app.session.config.attribs.arc_set(new.clone(), attrs);
}
for set in &mut app.session.config.sets {
if let Some(state) = set.overlays.remove(old.as_ref()) {
set.overlays.insert(new.clone(), state);
}
if let Some(state) = set.hidden_overlays.remove(old.as_ref()) {
set.hidden_overlays.insert(new.clone(), state);
}
}
}
fn clear_window_state(&mut self, app: &mut AppState, name: &str) {
for set in &mut self.sets {
set.overlays.retain(|k, _| k != id);
set.inactive_overlays.arc_rm(name);
set.hidden_overlays.arc_rm(name);
}
self.global_set.inactive_overlays.arc_rm(name);
self.global_set.hidden_overlays.arc_rm(name);
app.session.config.attribs.arc_rm(name);
for set in &mut app.session.config.sets {
set.overlays.remove(name);
set.hidden_overlays.remove(name);
}
}
pub fn persist_layout(&mut self, app: &mut AppState) {
@@ -561,7 +749,11 @@ impl<T> OverlayWindowManager<T> {
.overlays
.iter()
.filter_map(|(k, v)| {
let n = self.overlays.get(k).map(|o| o.config.name.clone())?;
let o = self.overlays.get(k)?;
if !o.config.can_persist() {
return None;
}
let n = o.config.name.clone();
Some((n, v.clone()))
})
.collect();
@@ -604,6 +796,7 @@ impl<T> OverlayWindowManager<T> {
for o in self.overlays.values() {
if o.config.global
&& let Some(state) = &o.config.active_state
&& o.config.can_persist()
{
global_overlays.insert(o.config.name.clone(), state.clone());
}
@@ -628,7 +821,7 @@ impl<T> OverlayWindowManager<T> {
.filter_map(|a| o.config.backend.get_attrib(*a))
.filter(|val| !val.is_default())
.collect();
if !attrs.is_empty() {
if !attrs.is_empty() && o.config.can_persist() {
app.session
.config
.attribs
@@ -647,6 +840,29 @@ impl<T> OverlayWindowManager<T> {
return;
}
// floating transforms in older states are relative to hmd, not anchor
if app.session.config.state_version < 1 {
let clear_legacy_floating = |state: &mut OverlayWindowState| {
if matches!(state.positioning, Positioning::Floating) {
state.saved_transform = None;
}
};
for set in &mut app.session.config.sets {
for state in set
.overlays
.values_mut()
.chain(set.hidden_overlays.values_mut())
{
clear_legacy_floating(state);
}
}
for state in app.session.config.global_set.values_mut() {
clear_legacy_floating(state);
}
app.session.config.state_version = 1;
}
// only safe to load when current_set is None
if self.current_set.is_some() {
self.switch_to_set(app, None, false);
@@ -973,9 +1189,13 @@ impl<T> OverlayWindowManager<T> {
overlay.config.name
);
overlay.config.name = format!("{}_2", overlay.config.name).into();
if overlay.config.category == OverlayCategory::WayVR {
overlay.config.persistent = false;
}
}
let name = overlay.config.name.clone();
let restore = overlay.config.can_persist() && overlay.config.restore_on_spawn;
let global = overlay.config.global;
let internal = matches!(overlay.config.category, OverlayCategory::Internal);
let show_on_spawn = overlay.config.show_on_spawn;
@@ -985,6 +1205,14 @@ impl<T> OverlayWindowManager<T> {
if !global {
for (i, set) in self.sets.iter_mut().enumerate() {
if !restore {
// Explicitly new session window: replace any old slot instead of
// accidentally restoring it on a later relaunch.
if self.overlays[oid].config.can_persist() {
set.inactive_overlays.arc_rm(&name);
}
continue;
}
let Some(state) = set.inactive_overlays.arc_rm(&name) else {
continue;
};
@@ -999,7 +1227,7 @@ impl<T> OverlayWindowManager<T> {
log::debug!("loaded state for {name} to set {i}");
}
}
} else if let Some(state) = self.global_set.inactive_overlays.arc_rm(&name) {
} else if restore && let Some(state) = self.global_set.inactive_overlays.arc_rm(&name) {
let o = &mut self.overlays[oid];
o.config.active_state = Some(state);
o.config.reset(app, false);
@@ -1007,7 +1235,11 @@ impl<T> OverlayWindowManager<T> {
log::debug!("loaded state for {name} from global_set!");
}
let saved_attribs = app.session.config.attribs.arc_get(&name).cloned();
let saved_attribs = if restore {
app.session.config.attribs.arc_get(&name).cloned()
} else {
None
};
if let Some(attribs) = saved_attribs {
for value in attribs {
self.overlays[oid].config.backend.set_attrib(app, value);
@@ -1049,6 +1281,38 @@ impl<T> OverlayWindowManager<T> {
SpawnPos::Parent(parent) => {
let _ = self.offset_spawn_from_parent(app, oid, &parent);
}
SpawnPos::WlDefault(window, fallback_pid) => {
let Some((parent, has_app_id)) = app.wvr_server.as_ref().and_then(|wvr| {
wvr.wm.windows.get(window).map(|w| {
(
w.parent.and_then(|p| wvr.get_overlay_id(p)),
w.app_id.is_some(),
)
})
}) else {
return;
};
let parent_id = parent.or_else(|| {
if has_app_id {
None
} else {
// legacy pid-based matching
fallback_pid.filter(|candidate| *candidate != oid)
}
});
if let Some(parent_id) = parent_id
&& self
.offset_spawn_from_parent(app, oid, &OverlaySelector::Id(parent_id))
.is_some()
{
return;
}
if let Some(parent_id) = self.spread_parent_for(oid) {
let _ =
self.offset_spawn_from_parent(app, oid, &OverlaySelector::Id(parent_id));
}
}
}
}
@@ -1213,6 +1477,7 @@ impl<T> OverlayWindowManager<T> {
meta.push(OverlayMeta {
id,
name: data.config.name.clone(),
title: data.config.title.clone(),
category: data.config.category,
visible: data.config.is_active(),
icon,
+16 -5
View File
@@ -108,6 +108,10 @@ impl OverlayLifetime {
pub struct OverlayWindowConfig {
pub name: Arc<str>,
/// Optional presentation title
pub title: Option<Arc<str>>,
pub persistent: bool,
pub restore_on_spawn: bool,
pub backend: Box<dyn OverlayBackend>,
/// The default state to show when the overlay is newly spawned.
pub default_state: OverlayWindowState,
@@ -136,9 +140,16 @@ pub struct OverlayWindowConfig {
}
impl OverlayWindowConfig {
pub const fn can_persist(&self) -> bool {
self.persistent
}
pub fn from_backend(backend: Box<dyn OverlayBackend>) -> Self {
Self {
name: "".into(),
title: None,
persistent: true,
restore_on_spawn: true,
backend,
default_state: OverlayWindowState {
transform: Affine3A::from_translation(Vec3::NEG_Z),
@@ -275,8 +286,8 @@ impl OverlayWindowConfig {
app.input_state.pointers[hand as usize].pose * cur_transform,
lerp,
),
Positioning::Anchored => (app.anchor * cur_transform, 1.0),
Positioning::Static | Positioning::Floating => (state.transform, 1.0), // STAGE space
Positioning::Anchored | Positioning::Floating => (app.anchor * cur_transform, 1.0),
Positioning::Static => (state.transform, 1.0), // STAGE space
};
state.transform = match lerp {
@@ -347,9 +358,9 @@ impl OverlayWindowConfig {
.unwrap_or(self.default_state.transform);
let parent_transform = match state.positioning {
Positioning::Floating | Positioning::FollowHead { .. } => app.input_state.hmd,
Positioning::FollowHead { .. } => app.input_state.hmd,
Positioning::FollowHand { hand, .. } => app.input_state.pointers[hand as usize].pose,
Positioning::Anchored => app.anchor,
Positioning::Anchored | Positioning::Floating => app.anchor,
Positioning::Static => {
if hard_reset {
app.input_state.hmd
@@ -437,7 +448,7 @@ pub fn scalar_scale(a: &Affine3A) -> f32 {
pub fn save_transform(state: &mut OverlayWindowState, app: &mut AppState) {
let parent_transform = match state.positioning {
Positioning::Floating => snap_upright(app.input_state.hmd, Vec3A::Y),
Positioning::Floating => app.anchor,
Positioning::FollowHead { .. } => app.input_state.hmd,
Positioning::FollowHand { hand, .. } => app.input_state.pointers[hand as usize].pose,
Positioning::Anchored => snap_upright(app.anchor, Vec3A::Y),
+6 -11
View File
@@ -24,17 +24,12 @@ raw-window-handle = "0.6"
cosmic-text = "0.19.0"
etagere = "0.3.0"
flate2 = "1.1.9"
image = {
version = "0.25.10",
default-features = false,
features = [
"gif",
"jpeg",
"png",
"rayon",
"webp",
]
}
image = { workspace = true, features = [
"gif",
"jpeg",
"rayon",
"webp",
] }
lru = "0.18.1"
ouroboros = "0.18.5"
parking_lot = "0.12.5"
+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)
+1 -1
View File
@@ -218,7 +218,7 @@ impl WGfx {
/// Submit a non-blocking completion marker to the queue
///
/// It uses an ALL_COMMANDS to ALL_COMMANDS barrier, so its fence doesn't
/// It uses an `ALL_COMMANDS` to `ALL_COMMANDS` barrier, so its fence doesn't
/// complete until all earlier work is finished.
pub fn submit_graphics_completion_marker(&self) -> anyhow::Result<GpuCompletionMarker> {
Ok(GpuCompletionMarker {
+12 -1
View File
@@ -2494,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,
+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,
};
+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 {
+2
View File
@@ -15,6 +15,7 @@ chrono.workspace = true
glam.workspace = true
idmap = { workspace = true, features = ["serde"] }
idmap-derive.workspace = true
libc.workspace = true
log.workspace = true
serde = { workspace = true, features = ["rc"] }
serde_json.workspace = true
@@ -34,3 +35,4 @@ rodio = {
}
walkdir = "2.5.0"
wgui = { path = "../wgui/" }
inotify = { version = "0.11.5", default-features = false }
+11 -3
View File
@@ -14,6 +14,8 @@ use crate::{
windowing::OverlayWindowState,
};
pub const CONFIG_VERSION: u32 = 1;
pub type PwTokenMap = AStrMap<String>;
pub type SerializedWindowStates = HashMap<Arc<str>, OverlayWindowState>;
@@ -87,12 +89,12 @@ pub enum InputCaptureMethod {
#[derive(Default, Clone, Copy, Serialize, Deserialize, AsRefStr, EnumString, EnumProperty, VariantArray)]
pub enum DefaultPositioning {
#[default]
#[strum(props(Translation = "DEFINITIONS.ANCHORED", Tooltip = "APP_SETTINGS.OPTION.ANCHORED_HELP"))]
Anchored,
#[strum(props(Translation = "DEFINITIONS.FLOATING", Tooltip = "APP_SETTINGS.OPTION.FLOATING_HELP",))]
Floating,
#[strum(props(Translation = "DEFINITIONS.ANCHORED", Tooltip = "APP_SETTINGS.OPTION.ANCHORED_HELP"))]
Anchored,
#[strum(props(Translation = "DEFINITIONS.STATIC", Tooltip = "APP_SETTINGS.OPTION.STATIC_HELP"))]
Static,
}
@@ -562,4 +564,10 @@ pub struct GeneralConfig {
#[serde(default)]
pub middle_grabs_single: bool,
#[serde(default)]
pub state_version: u32,
#[serde(default)]
pub config_version: u32,
}
+203 -115
View File
@@ -3,14 +3,14 @@ use std::{
ffi::OsStr,
fmt::Debug,
fs::exists,
path::Path,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
thread::JoinHandle,
time::Instant,
};
use crate::{cache_dir, locale::Language};
use crate::{cache_dir, desktop_watcher::DesktopWatcher, locale::Language};
use anyhow::Context;
use freedesktop_desktop_entry::DesktopEntry as FreedesktopDesktopEntry;
use serde::{Deserialize, Serialize};
@@ -57,14 +57,32 @@ const CATEGORY_TYPE_BLOCKLIST: [&str; 1] = ["ConsoleOnly"];
struct DesktopFinderParams {
size_preferences: Vec<&'static OsStr>,
icon_folders: Vec<String>,
app_folders: Vec<String>,
app_folders: Vec<PathBuf>,
langs: Vec<String>,
}
struct CachedFileOwned {
root_index: usize,
entry: DesktopEntryOwned,
}
struct CachedFile {
root_index: usize,
entry: Rc<DesktopEntry>,
}
struct CacheBuild {
files: HashMap<PathBuf, CachedFileOwned>,
watcher: Option<DesktopWatcher>,
}
pub struct DesktopFinder {
params: Arc<DesktopFinderParams>,
// include all desktop files, so that removing a higher-prio one will reveal the lower-prio underneath
files: HashMap<PathBuf, CachedFile>,
entry_cache: HashMap<String, Rc<DesktopEntry>>,
bg_task: Option<JoinHandle<HashMap<String, DesktopEntryOwned>>>,
watcher: Option<DesktopWatcher>,
bg_task: Option<JoinHandle<CacheBuild>>,
}
impl DesktopFinder {
@@ -76,13 +94,8 @@ impl DesktopFinder {
let mut icon_folders = vec![];
if let Some(data_home) = xdg.get_data_home() {
app_folders.push(data_home.join("applications").to_string_lossy().to_string());
app_folders.push(
data_home
.join("flatpak/exports/share/applications")
.to_string_lossy()
.to_string(),
);
app_folders.push(data_home.join("applications"));
app_folders.push(data_home.join("flatpak/exports/share/applications"));
icon_folders.push(data_home.join("icons").to_string_lossy().to_string());
icon_folders.push(
data_home
@@ -97,7 +110,7 @@ impl DesktopFinder {
// /usr/share and such
for data_dir in xdg.get_data_dirs() {
app_folders.push(data_dir.join("applications").to_string_lossy().to_string());
app_folders.push(data_dir.join("applications"));
icon_folders.push(data_dir.join("icons").to_string_lossy().to_string());
}
@@ -122,126 +135,117 @@ impl DesktopFinder {
size_preferences,
langs,
}),
files: HashMap::new(),
entry_cache: HashMap::new(),
watcher: None,
bg_task: None,
}
}
fn build_cache(params: Arc<DesktopFinderParams>) -> HashMap<String, DesktopEntryOwned> {
let start = Instant::now();
fn read_single(params: &DesktopFinderParams, path: &Path) -> Option<DesktopEntryOwned> {
let file_path = path.display();
let file_name = path.file_name()?.to_string_lossy();
let desktop = match FreedesktopDesktopEntry::from_path(path, Some(&params.langs)) {
Ok(desktop) => desktop,
Err(e) => {
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
return None;
}
};
let mut known_files = HashSet::new();
let mut entries = HashMap::<String, DesktopEntryOwned>::new();
if desktop.groups.desktop_entry().is_none() {
log::debug!("Failed to get [Desktop Entry] section for file {}, skipping", file_path);
return None;
}
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
return None;
}
let exec = desktop.exec()?;
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
return None;
}
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);
return None;
}
let app_name = desktop.name(&params.langs)?;
let mut vec_categories = Vec::<Box<str>>::new();
for cat in desktop.categories().unwrap_or_default() {
if CATEGORY_TYPE_BLOCKLIST.contains(&cat) {
return None;
}
if !cat.is_empty() {
vec_categories.push(cat.into());
}
}
let icon_path = desktop
.icon()
.and_then(|icon_name| Self::find_icon(params, icon_name))
.or_else(|| Self::create_icon(&file_name).ok());
Some(DesktopEntryOwned {
app_id: desktop.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(),
})
}
fn build_cache_full(params: Arc<DesktopFinderParams>) -> CacheBuild {
let start = Instant::now();
// install watch before scan so edits during scan remain in inotify event queue
let watcher = match DesktopWatcher::new(&params.app_folders) {
Ok(watcher) => Some(watcher),
Err(err) => {
log::warn!("Could not watch .desktop files: {err}");
None
}
};
let mut files = HashMap::new();
let icons_folder = cache_dir::get_path("icons");
if !std::fs::exists(&icons_folder).unwrap_or(false) {
let _ = std::fs::create_dir(&icons_folder);
}
for path in &params.app_folders {
log::debug!("Searching desktop entries in path {}", path);
'entries: for entry in WalkDir::new(path)
for (root_index, root) in params.app_folders.iter().enumerate() {
log::debug!("Searching desktop entries in path {}", root.display());
for entry in WalkDir::new(root)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| !e.file_type().is_dir())
.filter_map(Result::ok)
.filter(|entry| !entry.file_type().is_dir())
{
let Some(extension) = Path::new(entry.file_name()).extension() else {
continue;
};
if extension != "desktop" {
continue; // ignore, go on
}
let file_name = entry.file_name().to_string_lossy();
let file_path = entry.path().display().to_string();
let desktop = match FreedesktopDesktopEntry::from_path(entry.path(), Some(&params.langs)) {
Ok(desktop) => desktop,
Err(e) => {
log::debug!("Failed to read .desktop file {}: {:?}, skipping", file_path, e);
continue;
}
};
if desktop.groups.desktop_entry().is_none() {
log::debug!("Failed to get [Desktop Entry] section for file {}, skipping", file_path);
let path = entry.path();
if path.extension() != Some(OsStr::new("desktop")) || files.contains_key(path) {
continue;
}
// prefer entries from earlier data directories
let app_id = desktop.id();
if known_files.contains(app_id) {
continue;
}
if desktop.only_show_in().is_some() || desktop.terminal() || desktop.no_display() || desktop.hidden() {
continue;
}
let Some(exec) = desktop.exec() else {
log::debug!("Failed to get desktop entry Exec for file {}, skipping", file_path);
continue;
};
if CMD_BLOCKLIST.iter().any(|blocked| exec.contains(blocked)) {
continue;
}
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) = desktop.name(&params.langs) else {
log::debug!(
"Failed to get desktop entry application name for file {}, skipping",
file_path
if let Some(parsed) = Self::read_single(&params, path) {
files.insert(
path.to_path_buf(),
CachedFileOwned {
root_index,
entry: parsed,
},
);
continue;
};
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::<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(app_id.to_owned());
entries.insert(
app_id.to_owned(),
DesktopEntryOwned {
app_id: app_id.into(),
app_name: app_name.into(),
exec_argv,
icon_path: icon_path.map(|i| i.into_boxed_str()),
categories: vec_categories.into_boxed_slice(),
},
);
}
}
log::debug!("Desktop entry & icon scan took {:?}", start.elapsed());
CacheBuild { files, watcher }
}
entries
pub fn resolve_icon(&self, icon_name: &str) -> Option<String> {
Self::find_icon(&self.params, icon_name)
}
fn find_icon(params: &DesktopFinderParams, icon_name: &str) -> Option<String> {
@@ -306,29 +310,113 @@ impl DesktopFinder {
return;
};
let Ok(entries) = bg_task.join() else {
let Ok(CacheBuild { files, watcher }) = bg_task.join() else {
log::warn!("Desktop entry scan thread panicked");
return;
};
self.files = files
.into_iter()
.map(|(path, file)| {
(
path,
CachedFile {
root_index: file.root_index,
entry: Rc::new(file.entry.into()),
},
)
})
.collect();
self.watcher = watcher;
let mut files: Vec<_> = self.files.iter().collect();
files.sort_by(|(path_a, a), (path_b, b)| a.root_index.cmp(&b.root_index).then_with(|| path_a.cmp(path_b)));
self.entry_cache.clear();
for (app_id, entry) in entries {
self.entry_cache.insert(app_id, Rc::new(entry.into()));
for (_, file) in files {
self
.entry_cache
.entry(file.entry.app_id.to_string())
.or_insert_with(|| Rc::clone(&file.entry));
}
}
pub fn get_cached_entry(&self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
fn update_app_id(&mut self, app_id: &str) {
let best = self
.files
.iter()
.filter(|(_, file)| file.entry.app_id.as_ref() == app_id)
.min_by(|(path_a, a), (path_b, b)| a.root_index.cmp(&b.root_index).then_with(|| path_a.cmp(path_b)))
.map(|(_, file)| Rc::clone(&file.entry));
if let Some(best) = best {
self.entry_cache.insert(app_id.to_owned(), best);
} else {
self.entry_cache.remove(app_id);
}
}
fn sync_file_changes(&mut self) {
self.wait_for_entries();
let Some(watcher) = self.watcher.as_mut() else {
return;
};
let changes = watcher.collect_changes();
if changes.overflowed {
log::warn!("Desktop file inotify event queue overflowed; rebuilding cache");
self.refresh();
self.wait_for_entries();
return;
}
let mut affected_ids = HashSet::new();
if !changes.removed_directories.is_empty() {
self.files.retain(|path, file| {
let removed = changes
.removed_directories
.iter()
.any(|directory| path.starts_with(directory));
if removed {
affected_ids.insert(file.entry.app_id.to_string());
}
!removed
});
}
for path in changes.files {
if let Some(previous) = self.files.remove(&path) {
affected_ids.insert(previous.entry.app_id.to_string());
}
let root_index = self.params.app_folders.iter().position(|root| path.starts_with(root));
if let Some(root_index) = root_index
&& let Some(entry) = Self::read_single(&self.params, &path)
{
affected_ids.insert(entry.app_id.to_string());
self.files.insert(
path,
CachedFile {
root_index,
entry: Rc::new(entry.into()),
},
);
}
}
for app_id in affected_ids {
self.update_app_id(&app_id);
}
}
pub fn get_cached_entry(&mut self, app_id: &str) -> Option<&Rc<DesktopEntry>> {
self.sync_file_changes();
self.entry_cache.get(app_id)
}
pub fn find_entries(&mut self) -> HashMap<String, Rc<DesktopEntry>> {
self.wait_for_entries();
self.sync_file_changes();
self.entry_cache.clone()
}
pub fn refresh(&mut self) {
let bg_task = std::thread::spawn({
let params = self.params.clone();
move || Self::build_cache(params)
move || Self::build_cache_full(params)
});
self.bg_task = Some(bg_task);
}
+208
View File
@@ -0,0 +1,208 @@
use std::{
collections::{HashMap, HashSet},
ffi::OsStr,
io,
path::{Path, PathBuf},
};
use inotify::{EventMask, Inotify, WatchDescriptor, WatchMask};
#[derive(Default)]
pub(crate) struct DesktopChanges {
pub files: HashSet<PathBuf>,
pub removed_directories: HashSet<PathBuf>,
pub overflowed: bool,
}
pub(crate) struct DesktopWatcher {
inotify: Inotify,
roots: Vec<PathBuf>,
by_wd: HashMap<WatchDescriptor, HashSet<PathBuf>>,
by_path: HashMap<PathBuf, WatchDescriptor>,
}
impl DesktopWatcher {
pub fn new(roots: &[PathBuf]) -> io::Result<Self> {
let mut watcher = Self {
inotify: Inotify::init()?,
roots: roots.to_vec(),
by_wd: HashMap::new(),
by_path: HashMap::new(),
};
for root in roots {
for ancestor in root.ancestors().skip(1) {
if ancestor.is_dir() {
watcher.watch_directory(ancestor);
}
}
watcher.watch_tree(root, None);
}
Ok(watcher)
}
fn is_related_directory(&self, path: &Path) -> bool {
self
.roots
.iter()
.any(|root| path.starts_with(root) || root.starts_with(path))
}
fn is_application_file(&self, path: &Path) -> bool {
path.extension() == Some(OsStr::new("desktop")) && self.roots.iter().any(|root| path.starts_with(root))
}
fn watch_directory(&mut self, path: &Path) {
if self.by_path.contains_key(path) || !path.is_dir() {
return;
}
let mask = WatchMask::CREATE
| WatchMask::DELETE
| WatchMask::MOVED_FROM
| WatchMask::MOVED_TO
| WatchMask::MODIFY
| WatchMask::CLOSE_WRITE
| WatchMask::ATTRIB
| WatchMask::DELETE_SELF
| WatchMask::MOVE_SELF
| WatchMask::ONLYDIR;
let wd = match self.inotify.watches().add(path, mask) {
Ok(wd) => wd,
Err(err) => {
log::warn!("Unable to watch desktop directory {}: {err}", path.display());
return;
}
};
self.by_wd.entry(wd.clone()).or_default().insert(path.to_path_buf());
self.by_path.insert(path.to_path_buf(), wd);
}
fn watch_tree(&mut self, directory: &Path, mut existing_files: Option<&mut HashSet<PathBuf>>) {
let mut pending = vec![directory.to_path_buf()];
while let Some(dir) = pending.pop() {
if !self.is_related_directory(&dir) || !dir.is_dir() {
continue;
}
self.watch_directory(&dir);
let children = match std::fs::read_dir(&dir) {
Ok(children) => children,
Err(err) => {
log::debug!("Cannot read desktop directory {}: {err}", dir.display());
continue;
}
};
for child in children.flatten() {
let path = child.path();
match child.file_type() {
Ok(kind) if kind.is_dir() => {
if self.is_related_directory(&path) {
pending.push(path);
}
}
Ok(_) => {
if self.is_application_file(&path)
&& let Some(files) = existing_files.as_deref_mut()
{
files.insert(path);
}
}
Err(_) => {}
}
}
}
}
fn unwatch_tree(&mut self, directory: &Path) {
let descendants: Vec<_> = self
.by_path
.iter()
.filter(|(path, _)| path.starts_with(directory))
.map(|(path, wd)| (path.clone(), wd.clone()))
.collect();
for (path, wd) in descendants {
self.by_path.remove(&path);
if let Some(paths) = self.by_wd.get_mut(&wd) {
paths.remove(&path);
if paths.is_empty() {
self.by_wd.remove(&wd);
let _ = self.inotify.watches().remove(wd);
}
}
}
}
pub fn collect_changes(&mut self) -> DesktopChanges {
let mut changes = DesktopChanges::default();
let mut buffer = [0u8; 64 * 1024];
loop {
let events = match self.inotify.read_events(&mut buffer) {
Ok(events) => events,
Err(err) if err.kind() == io::ErrorKind::WouldBlock => break,
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
Err(err) => {
log::warn!("Unable to read desktop directory inotify events: {err}");
changes.overflowed = true;
break;
}
};
for event in events {
if event.mask.contains(EventMask::Q_OVERFLOW) {
changes.overflowed = true;
continue;
}
if event.mask.contains(EventMask::IGNORED) {
if let Some(paths) = self.by_wd.remove(&event.wd) {
for path in paths {
self.by_path.remove(&path);
}
}
continue;
}
let Some(directories) = self.by_wd.get(&event.wd).cloned() else {
continue;
};
for directory in directories {
let path = match event.name {
Some(name) => directory.join(name),
None => directory,
};
if event.mask.intersects(EventMask::DELETE_SELF | EventMask::MOVE_SELF) {
changes.removed_directories.insert(path.clone());
self.unwatch_tree(&path);
} else if event.mask.contains(EventMask::ISDIR) {
if event.mask.intersects(EventMask::DELETE | EventMask::MOVED_FROM) {
changes.removed_directories.insert(path.clone());
self.unwatch_tree(&path);
}
if event.mask.intersects(EventMask::CREATE | EventMask::MOVED_TO) {
self.watch_tree(&path, Some(&mut changes.files));
}
} else if self.is_application_file(&path) {
changes.files.insert(path);
}
}
}
}
changes
}
}
+1
View File
@@ -11,6 +11,7 @@ pub mod dash_interface;
pub mod dash_interface_emulated;
pub mod data_dir;
pub mod desktop_finder;
mod desktop_watcher;
pub mod locale;
pub mod openxr_actions;
pub mod openxr_bindings_schema;
+4 -1
View File
@@ -23,6 +23,8 @@ pub enum Language {
ChineseSimplified,
#[strum(props(Text = "Español"))]
Spanish,
#[strum(props(Text = "Svenska"))]
Swedish,
}
impl Language {
@@ -36,6 +38,7 @@ impl Language {
Language::Italian => "it_IT",
Language::ChineseSimplified => "zh_CN",
Language::Spanish => "es_ES",
Language::Swedish => "sv_SE",
}
}
@@ -44,7 +47,7 @@ impl Language {
}
pub const fn all_codes() -> &'static [&'static str] {
&["en", "pl", "ja", "de", "it", "zh_CN", "es"]
&["en", "pl", "ja", "de", "it", "zh_CN", "es", "sv"]
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ use crate::{common::LeftRight, config::DefaultPositioning};
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub enum Positioning {
/// Stays in place, recenters relative to HMD
/// Freely grabbed, but stored relative to the anchor and moves with it.
#[default]
Floating,
/// Stays in place, recenters relative to anchor. Follows anchor during anchor grab.