Compare commits

...

215 Commits

Author SHA1 Message Date
galister 6dc76c881f update deps 2026-06-03 18:25:47 +09:00
galister 4b78e39af7
glyph upload fixes (#531)
* remove unused pass cache, cache imageviews instead

* glyph upload batching

* image upload batching (wip)
2026-06-03 17:29:31 +09:00
oo8dev 7f97dc884f
wayvr: Space gravity (#520)
* wayvr: Space gravity (wip)

* dash-frontend: dedicated "Space drag" settings tab, deduplicate overlay shift code in xr&vr, account for gravity shift for static overlays

* space reset & gravity conflict bugfix, add ground friction slider

* Space gravity UI improvements

* space gravity: floor level

* wayvr: space gravity: hide in openvr
2026-06-03 09:51:22 +02:00
art0007i 9806027e19
add watch toggle setting (#526)
* add watch toggle setting

* previous commit broke monado workaround, this fixes it

* use active_state to toggle watch

* use the other monado workaround

* fix whitespace

* cargo fmt
2026-06-03 16:22:09 +09:00
Hiina 066f033a91
fix: preserve screencopy transforms for rotated outputs (#530) 2026-06-03 15:55:50 +09:00
galister 1591466a8d fix some supported settings getting hidden 2026-05-17 18:02:47 +09:00
galister 901c18520e hide unsupported settings depending on backend 2026-05-17 12:46:54 +09:00
Sapphire e39085aa2b openvr: Hide lasers and unblock input when dashboard is open 2026-05-17 12:10:44 +09:00
Sapphire 48d58f61d2 openvr: Hide devices without battery status from battery panel 2026-05-17 12:10:44 +09:00
galister 8691cf99d2 clippy + fmt 2026-05-13 16:39:15 +09:00
galister 80bfb177be range sliders in settings 2026-05-13 16:38:15 +09:00
Aleksander 20743f91eb clippy 2026-05-12 21:59:54 +02:00
Aleksander 5cf7b986f3 wayvr: Remember skymap texture across restarts 2026-05-12 21:39:04 +02:00
Aleksander 3d71746ff2 dash-frontend: watch_view_angle setting (Closes #505), re-format language files 2026-05-12 21:21:04 +02:00
Snemby c7b16c5713 fix space rotate
the change in rotation is computed in the untransformed stage space. To produce the correct rotation change the quaternion needs to be rotated using the space offset
2026-05-13 00:59:27 +09:00
galister ed1d708802 mnd playspace: fetch offset on drag/rotate start 2026-05-12 22:58:42 +09:00
steve hocktail 557a0775c3
add a toast for uinput errors (#494)
* add a toast for uinput errors

this makes it more obvious if missing uinput permissions are the reason your keyboard and mouse aren't working.

* uinput toaster part 2: the cargo formattening

* increase uinput error toast's timeout to 30 seconds

---------

Co-authored-by: galister <22305755+galister@users.noreply.github.com>
2026-05-12 22:53:14 +09:00
galister 2309f89701
update issue template 2026-05-12 17:11:15 +09:00
oo8dev cdd658d4ea
wayvr: IPC: Fix dangling fds (#513) 2026-05-07 22:06:54 +02:00
Aleksander ec2e0501c1 wayvr: skip unsupported config files (Closes #510) 2026-05-03 19:35:49 +02:00
Aleksander f30c650bba wgui: add `EventResult::ConsumedExclusive` for Slider components
This prevents us from accidentally swipe-scrolling the content if the user tries to change a slider value.
2026-05-03 19:13:45 +02:00
Aleksander 45af52ddf9 wgui: Range sliders, fix swipe scrolling <-> slider conflict 2026-05-03 18:54:31 +02:00
galister 10b14794ed
update to upstream pipewire-rs (#508)
* update to upstream pipewire-rs

* CI to use 24.04 without no pipewire ppa

* fix build without openxr feature
2026-05-03 14:36:49 +09:00
oo8dev 6b81f07cc8
wgui: Swipe kinetic scrolling support (#504)
* wgui: use DeviceBitmask instead of usize

* wgui: Swipe logic works (wip)

* wgui: Kinetic scrolling, rubberband effect
2026-05-02 19:21:30 +02:00
galister 9734955ebb dont show skymap tab if not openxr 2026-04-23 19:58:02 +09:00
galister dd3108d423 new built-in sky shader 2026-04-23 19:14:05 +09:00
galister bf7276b2dd skybox downloader: apply button for downloaded skymaps 2026-04-23 19:14:05 +09:00
galister 90c4567505 shuffle some options around 2026-04-23 19:14:03 +09:00
galister 6cddb43b0f default skybox is shader skybox 2026-04-23 19:12:32 +09:00
Aleksander f3732c7087 wayvr: improve grid shader, dash-frontend: make pop-up title non-scrollable 2026-04-22 20:22:45 +02:00
Aleksander 07bd841bc6 dash-frontend: skymap browser ui improvements, wayvr: fix context menus not working 2026-04-22 19:03:47 +02:00
galister 9986b71b26 skybox selector 2026-04-22 19:09:26 +09:00
galister 16446f7808 Merge remote-tracking branch 'origin/feat-skybox-catalog' 2026-04-22 18:24:12 +09:00
galister 5615186654 revert: feat: swipe to type (#451) 2026-04-22 17:43:38 +09:00
galister 244822ffb3 add --no-autostart arg 2026-04-22 17:36:04 +09:00
galister 266b0fd507 reconfigure_environment_blend to handle chroma + skybox + alpha 2026-04-17 14:44:25 +09:00
Aleksander 0d8937ff80 dash-frontend: Dialog boxes, Skymap removal dialog, Skymap resolution selector dialog 2026-04-17 12:10:18 +09:00
Aleksander c59e195d94 dash-frontend: Re-work skymap list, show downloaded resolution pips 2026-04-17 12:10:18 +09:00
galister 994dffc4fb png → dds 2026-04-17 12:10:18 +09:00
Aleksander 701e41296b dash-frontend: Remote skymap downloader finished and fully working 2026-04-17 12:10:18 +09:00
Aleksander b0dba25f36 dash-frontend: File saving, close callbacks 2026-04-17 12:10:18 +09:00
Aleksander 1bee41aea9 dash-frontend: http_client: Progress callback, display download status 2026-04-17 12:10:18 +09:00
Aleksander 2035d9ba76 dash-frontend: ViewTrait, refactoring, `download_file::View` (wip) 2026-04-17 12:10:18 +09:00
Aleksander 24e3099b7b dash-frontend: remove dangling popups immediately 2026-04-17 12:10:18 +09:00
Aleksander aee5bf69da wgui: Simplify Popup usage 2026-04-17 12:10:18 +09:00
Aleksander 62881c08b3 rebase changes from feat/chroma-key 2026-04-17 12:10:18 +09:00
Aleksander 0c87d47ce6 wgui: Simplify usage and code, no more start_common(), Layout processes alterables 2026-04-17 12:10:18 +09:00
Aleksander 47bb5ab7a2 dash-frontend: skymap downloader dialog 2026-04-17 12:10:18 +09:00
Aleksander e5e798b599 dash-frontend: rename download_skymaps -> remote_skymap_list 2026-04-17 12:10:18 +09:00
Aleksander 68daa5b3df dash-frontend: skymap preview fetch (done), reformat Cargo.toml files 2026-04-17 12:10:18 +09:00
Aleksander f2dcdcc6c3 dash-frontend: list skymaps from the server (wip), loading spinner, wgui: animation bugfix 2026-04-17 12:10:18 +09:00
Aleksander a3460fbe70 dash-frontend: use SettingsTab trait for each settings tab 2026-04-17 12:10:18 +09:00
Aleksander 7f822b6787 dash-frontend: Skymap catalog fetch 2026-04-17 12:10:18 +09:00
Aleksander 1d8b22d992 dash-frontend: "Skybox" tab 2026-04-17 12:10:18 +09:00
galister 1238808d87 fix chroma_key_params not saving 2026-04-14 12:32:54 +09:00
Aleksander 26f5be36b6 dash-frontend: Chroma key settings 2026-04-11 13:48:49 +02:00
Aleksander 0c1b0906c1 wgui: Add `ColorSelector` component 2026-04-11 00:22:24 +02:00
galister 5eb258e310 update libmonado-rs & oxr chroma key 2026-04-10 20:01:39 +09:00
galister bec00c6f48 fmt 2026-04-10 14:47:36 +09:00
Diamond White db1675d399
feat: swipe to type (#451)
* start to integrate swipetype with keyboardstate

* successful debug implementation

* change to git dependency

* add manual trigger to app image build to test

* use latest swipeType lib to test including binaries

* use clipboard to paste swipe output

* use new prediction engine, move swipe type related logic to its own module

* update appimage bulid to support building ort runtime

* implement swipe selection ui in keyboard overlay

* only add motion data for the device that started the swipe

* process swipes on separate thread to not block ui

* add toggle to settings.rs

* default swipe to type config to false

* remove println, minor fix

* use correct key_dimensions

* use latest swipe_type library version

* optionally use wayvr compositor clipboard when pasting

* remove unused imports

* fix typo in log::error

* fix within key positioning

* run swipes on AltGr keycaps
2026-04-10 14:46:22 +09:00
Aleksander 18f99b5daf dash-frontend: settings: use generics instead of macros, separate each settings tab by a separate file 2026-04-03 00:11:26 +02:00
oo8dev 543f157895
Merge pull request #445 from wayvr-org/feat-monado-timings
feat: Monado debug timings graph
2026-04-02 17:57:46 +02:00
Aleksander 8ad31cfa28 Add `feat-monado-timings` build feature 2026-04-02 17:43:26 +02:00
Aleksander 80277e0c12 ParserState: add `realize_template`, fix sliders not being updated 2026-04-02 17:43:26 +02:00
Aleksander 0a4fc34fac dash-interface: fetch client IDs, toggle filter 2026-04-02 17:43:10 +02:00
Aleksander e035fb9d27 separate debug graphs by session ID, selectable session ID list 2026-04-02 17:43:10 +02:00
Aleksander 97fcde1a3b rebase 2026-04-02 17:43:10 +02:00
Aleksander c16c403966 display debug graphs 2026-04-02 17:43:10 +02:00
Aleksander d5148e2107 wgui: `ComponentBarGraph`, `WidgetCustomDraw` 2026-04-02 17:43:10 +02:00
Aleksander 0f6f344c97 protobuf parser, read metrics, move async executor to wlx_common 2026-04-02 17:43:10 +02:00
Aleksander 41c6f43a3c dash-frontend: Monado runtime: Tabs, add stub DebugTimings tab 2026-04-02 17:42:38 +02:00
Aleksander f44c4b78bc cargo clippy 2026-03-31 22:15:14 +02:00
Aleksander 5420efa4d3 wayvr: Fix default locale fallback not working properly (regression caused by PR #411) 2026-03-31 21:02:44 +02:00
Aleksander 89f43b95ef Update FUNDING.yml
[skip ci]
2026-03-28 16:57:40 +01:00
founta 00bc179655
Add space reset to openvr actions (#475)
* Add space reset to openvr actions

* remove additional reset state storage
2026-03-28 14:44:14 +09:00
oo8dev a4115f5e4b
Merge pull request #478 from wayvr-org/move-doc
docs: move out docs to a separate repo/website, fix typos
2026-03-25 18:39:59 +00:00
Aleksander 6c896bb956 fix typo 2026-03-25 19:39:20 +01:00
galister d9c8689c18
docs move: continuation
Updated links and references to the new GitHub organization.
2026-03-26 03:33:45 +09:00
Aleksander 5df6ce93f8 docs: move out docs to a separate repo/website, fix typos
[skip ci]
2026-03-24 00:19:27 +01:00
Earthgames ee4bb5ca29
Allow the checkbox to be themed (#462) 2026-03-23 12:24:16 +09:00
hare_ware b89ae4726c Revert "fix bindings for wmr controllers"
This reverts commit 285f6db134.
2026-03-23 12:23:01 +09:00
galister b0bbc25083 dont die if autostart app cant be started 2026-03-20 12:48:52 +09:00
Craig McLure 635df298fe Updated the pipewire reference in Cargo.lock 2026-03-18 22:22:00 +09:00
galister 786847cc2f update pipewire-rs to fix build on arch 2026-03-18 12:31:42 +09:00
Aleksander d9f89faf33 wayvr: "Fix floor" countdown timer and sound
- 5-second timeout sound with countdown timer
- sort locale JSON
- modify toast lerp amount
2026-03-16 13:47:51 +09:00
galister 769978560e oops 2026-03-16 13:46:23 +09:00
galister bebc39e766 dbus notify timeout is ms 2026-03-16 13:38:10 +09:00
oo8dev bb15a4e867
Merge pull request #466 from wlx-team/toast-mp3-update
update toast.mp3 sound
2026-03-14 12:23:30 +00:00
Aleksander 84015c7972 update toast.mp3 sound
[skip ci]
2026-03-14 13:20:48 +01:00
galister f2c34fba8f
sticky modifier keys explanation in README 2026-03-11 10:44:01 +09:00
Earthgames 1a74eb1507 Use a more common name 2026-03-08 01:18:35 +09:00
Earthgames 6b3f8a7b42 Only convert locale one time 2026-03-08 01:18:35 +09:00
Earthgames ad78fdab24 Hack in full lang codes
Add British
2026-03-08 01:18:35 +09:00
Earthgames 12ceebec6d Used locale in chrono 2026-03-08 01:18:35 +09:00
Earthgames 6f9ca5b3b7 Added documentation for default colors 2026-03-06 12:23:50 +09:00
oo8dev c54b211872
Merge pull request #460 from wlx-team/defaults-globals-refactor
wgui: separate globals from themes
2026-03-05 21:55:46 +00:00
Aleksander cb899235b4 wgui: separate globals and themes 2026-03-05 19:28:42 +01:00
Aleksander a83af3d833 translations 2026-03-05 12:42:04 +01:00
galister e8c8a67fc0
add wiki links to readme 2026-03-03 02:52:51 +09:00
Earthgames 2b4a0b6a0d Changed old link to the new one 2026-03-01 13:53:49 +09:00
galister 5ffa5e59bf skybox cleanups 2026-02-28 13:33:54 +09:00
galister ed8ca1e068 fix copy-paste fail causing stuck images 2026-02-28 13:33:27 +09:00
galister f21776b123 box CompositionLayerColorScaleBiasKHR 2026-02-28 13:10:02 +09:00
Torge Matthies ab3f873a38 fix grid opacity not updating live 2026-02-27 12:30:27 +09:00
oo8dev de6047107e
Merge pull request #450 from openglfreak/feat-grid-opacity
add option for grid opacity
2026-02-25 21:14:11 +01:00
Torge Matthies 39152b8548
add option for grid opacity 2026-02-25 20:59:17 +01:00
Kresny b517afc379
Rebind toggle_dashboard and show_hide (#449) 2026-02-25 14:58:30 +09:00
oo8dev b84d565b1e
Merge pull request #443 from wlx-team/feat-editbox
Editbox, wgui fixes, doc
2026-02-21 12:23:49 +01:00
Sapphire 46f3a54203 don't block inputs for headless sessions 2026-02-21 14:13:31 +09:00
Sapphire 4afaa6d048 reduce indentations in block_inputs 2026-02-21 14:13:31 +09:00
Sapphire 26f078d512 unblock inputs on exit 2026-02-21 14:13:31 +09:00
Aleksander 124d817752 fix logic bug in push_scissor_stack 2026-02-16 20:33:43 +01:00
Aleksander 2ddb419676 editbox keyboard input, scrollbars fix (scissoring broken atm), clippy 2026-02-14 22:20:51 +01:00
Aleksander 841241a7e6 wgui: Editbox basics, component focus, minor refactoring 2026-02-14 19:24:36 +01:00
Aleksander c902f0cd44 wgui: widgets.md: document tabs 2026-02-14 19:24:36 +01:00
Aleksander 756c42609d use SlotMap 2026-02-14 16:04:36 +01:00
Aleksander 4fc439abe2 uidev: fix color formats
Current implementation picks up the first available color format, which could be HDR (A2R10G10B10_UNORM_PACK32 in my case), but we don't have any color profiles. Stick back to SDR-only instead.
2026-02-14 10:18:41 +01:00
oo8dev 9685576272
Merge pull request #438 from wlx-team/feat-language-selector
Settings: Language selector, "Requires restart" info
2026-02-13 08:33:32 +01:00
galister d2f01c06ec
Add Open Collective link 2026-02-13 13:29:53 +09:00
Aleksander 94c36076af Language selector and "requires restart" info 2026-02-12 20:53:06 +01:00
galister a4134030fe Release 26.2.1 2026-02-11 02:58:18 +09:00
Aleksander ebacbf175a Update lang
[skip ci]
2026-02-10 17:59:27 +01:00
Sefa Eyeoglu 6c987b4492
Add Nix package to README (#432)
Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2026-02-10 21:29:01 +09:00
SpookySkeletons 8acb2236bd
fix: delete hand bindings from openxr_actions.json5 (#429)
* fix: delete hand bindings from openxr_actions.json5

Presumably fixes handtracking causing laser offsets to tear off of controllers when hands switch to and from handtrack.

* Comment out fallback controller
2026-02-10 17:20:35 +09:00
Sapphire 5dae6f8387
fix openxr aspect ratio calculation (#430) 2026-02-10 17:20:07 +09:00
Sapphire 5ab242f8e0
fix size of set buttons (#431) 2026-02-10 17:19:51 +09:00
galister fcbb5fab73 update workflow for vendor tarball 2026-02-09 02:38:16 +09:00
galister f9a57e58bb Release 26.2.0 2026-02-09 02:34:31 +09:00
Sapphire d713302d0d add option to block poses when using keyboard (OpenXR only) 2026-02-08 01:13:56 +09:00
Sapphire d398e6fb5a update libmonado and use better input blocking when available 2026-02-08 01:13:56 +09:00
Sapphire 903a6b351c
allow hotswitch between 12h and 24h clock for keyboard (#426) 2026-02-07 10:21:47 +09:00
galister b3e09b330f screen.frag: force alpha to 1.0 2026-02-06 10:56:33 +09:00
dependabot[bot] c63089b944 Bump time from 0.3.44 to 0.3.47
Bumps [time](https://github.com/time-rs/time) from 0.3.44 to 0.3.47.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.44...v0.3.47)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.47
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-06 09:55:25 +09:00
Sapphire ca770c1c05 disable grabbing for watch in default state
watch should only be grabbable in edit mode
2026-02-06 09:54:52 +09:00
galister ac65bca05a only write non-default attributes to state 2026-02-05 11:55:03 +09:00
Sapphire e61fd66210 remove libmonado active client workaround 2026-02-05 10:25:27 +09:00
Sapphire 1dc7d4340c better filtering for monado app switcher 2026-02-05 10:25:27 +09:00
galister a22bb2bf1a
KHR_composition_layer_color_scale_bias instead of raster alpha (#407) 2026-02-04 09:39:57 +09:00
dependabot[bot] e1c72fa446
Bump bytes from 1.11.0 to 1.11.1 (#419)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-04 09:39:21 +09:00
galister 676a96b2cd
fix KeySymbol font size 2026-02-02 12:56:20 +09:00
oo8dev 4821123c73
Merge pull request #417 from wlx-team/staging
dash-frontend: UX improvements
2026-01-28 10:47:37 +01:00
galister 32c8b7ae62 uidev: query surface format + color space from gpu 2026-01-28 11:25:18 +09:00
Aleksander d97bbfc796 dash-frontend: games: pagination 2026-01-27 19:49:27 +01:00
Aleksander 19083cfd83 dash-frontend: Top panel title 2026-01-27 19:49:27 +01:00
oo8dev 2c60196ead
Merge pull request #416 from wlx-team/staging
wgui: perf: Scissor render culling
2026-01-27 17:53:26 +01:00
Aleksander b17de39ef0 wgui: perf: scissor render culling
Greatly increases rendering performance and minimizes drawcall count for large lists.
2026-01-27 00:36:23 +01:00
ZokuTe 46e54db969
Fix: Refine locale matching logic to support region-specific tags (#411)
* wgui(i18n): Fix language matching logic

* wgui(i18n): Fix locale prefix match
2026-01-25 03:22:59 +00:00
galister 554029aacf fix dropdowns being stuck open; multi button events 2026-01-25 12:21:57 +09:00
galister a5146e0396 fix color bias behavior 2026-01-25 11:44:42 +09:00
galister 6960652379 reset playspace button in settings 2026-01-25 11:44:17 +09:00
galister 3ec1dd7f4e mouse-motion-only handsfree modes 2026-01-25 11:43:44 +09:00
oo8dev 157c88127f
Merge pull request #412 from wlx-team/staging
Update ovr_overlay
2026-01-24 15:24:45 +01:00
Aleksander 18e628f083 Update ovr_overlay 2026-01-24 14:32:40 +01:00
ZokuTe ab83d44a5e
i18n: add support for zh_CN locale (#409) 2026-01-23 01:30:40 +00:00
galister fa6492a422 fix build without openxr feature 2026-01-22 16:16:59 +09:00
galister 160e84120d fix monado no-layer bug 2026-01-22 01:14:04 +09:00
galister c222c25ddf angle fade (partial, need monado fix) 2026-01-22 01:00:44 +09:00
galister ee8391a45f refactor skybox & passthru realtime switching 2026-01-21 18:26:18 +09:00
Sapphire 3ac6f70725 allow toggling skybox during runtime 2026-01-21 03:47:20 +00:00
Sapphire d23b058d0e allow toggling passthrough during runtime 2026-01-21 03:47:20 +00:00
Orion 37fde4c642
scale grab offset together with the overlay/anchor (#404)
fixes #397
2026-01-21 02:22:12 +00:00
Sapphire 2ade8c542e
fix active modifier border color not sticking on unhover (#405) 2026-01-21 02:21:27 +00:00
Aleksander 03a1f449b5 Running games list (Closes #398) 2026-01-18 03:20:29 +00:00
Aleksander 7b3a2a1e48 UI gradient intensity slider, update wgui Defaults for dashboard, prepend ui-related config entries with `ui_` 2026-01-18 03:20:29 +00:00
Aleksander dabd122a1d translations, minor refactoring, wgui: gradient intensity in globals, dash-frontend: remove unused window_list, process_list and window_options 2026-01-18 03:20:29 +00:00
Orion 0181119393
input blocking improvements (#399)
* move input blocking hover logic into InteractionState

* add per-window input blocking setting

* OpenVR: per-hand input blocking

* move block_input setting into OverlayWindowState
2026-01-18 03:09:22 +00:00
Orion 2e1d07abc2
fix 2D scroll in apps spawned inside wayvr (#401) 2026-01-18 02:00:31 +00:00
Jay fe61319fc2
OSC: Change `::OscSend` format and restore String support to Button Actions. (#388)
* OscSend: support strings poc
- change value delimiter to `;`
- default convert values to String when not converted to another type

* add `_arg<n>="<value>"` format instead of `;` delimiter
that is, vrchat chatbox is controlled like `_press="::OscSend /chatbox/input" _arg0="${send}" _arg1="true" _arg2="true"` now

old format still supported as shorthand, with same "strings can't have spaces" limit as before. `_arg<n>` are appended after these.

* fix accidentally resetting `osc_args` to empty between formats

* update readme to document `::OscSend` paramter-form

* clean up, use `while let` instead of `while match`

* merge `parse_osc_value` into while condition

* remove second log from shortform arg parsing
2026-01-16 02:13:44 +00:00
galister da644c97d3
Fix readme formatting 2026-01-15 12:34:30 +00:00
galister b67914b956 expand vars in paths 2026-01-15 20:47:43 +09:00
Kresny e4caa1d6d4
Fix typos,improve clarity remove Homebrew in README.md (#392)
* Fix typos and improve clarity in README.md

Also replaced wlx with wayvr in build instructions

* Remove Homebrew
2026-01-15 09:13:03 +00:00
galister ea62be8ad7 openvr lines 2026-01-15 18:11:41 +09:00
galister 6ab1db9a76 fix openvr lines 2026-01-15 14:32:36 +09:00
galister cd0d49eaf6
steamvr is broken completely, not just steam link 2026-01-15 02:31:31 +00:00
galister a97ee2cee2 wayvrctl switch-set 2026-01-14 15:01:01 +09:00
galister 9d43068271 adjust mouse for stereo 2026-01-14 14:24:14 +09:00
oo8dev c7ebb7492e
Merge pull request #390 from wlx-team/staging
Tooltip improvements
2026-01-13 18:08:38 +01:00
Aleksander c5356724fd Tooltip fade-in animations, implement tooltip for CheckBox and Slider, use FnOnce for layout tasks 2026-01-13 17:51:35 +01:00
galister d02efc2657 always visible checkbox 2026-01-13 23:59:03 +09:00
galister 40cd532134 add help for handsfree mode 2026-01-13 20:00:43 +09:00
galister 869c0ba5e2 bindings for eye + pinch 2026-01-13 19:23:04 +09:00
galister fd24060d7b handsfree mode 2026-01-13 19:15:51 +09:00
Orion 1318f23947
task bar: toggle overlays on right click (#385) 2026-01-13 01:28:16 +00:00
galister d2385fd3b2 exported images to have dedicated allocations 2026-01-12 22:27:26 +09:00
Jay 7059a85742
Re-implement custom audio files support. (#379)
* reimplement custom audio loading for `toast` and `key_click` using `from_mp3`

* run cargo fmt

* `try_bytes_from_config`: output with `log::trace` when file cannot be opened

* rewrite `try_bytes_from_config`

* use file `read_to_end`, log categories

* edit `register_wgui_samples`

* rework audio functions for loading custom wgui sounds
oh hey, it works!

* clean up

* allow custom `startup` and `app_start` sounds
these sounds are loaded on-demand and never registered; this behaviour is unchanged

* edit `bytes_from_config` outputs

* use return value of `read_to_end` instead of `len()` for reported file size

* implement suggested changes
- remove now-unused `register_mp3_sample_from_assets` function
- replace implementation of `try_bytes_from_config`
  - missing the `real_path` context

* swap out `context`s for `inspect_err`s with closures to output `real_path`

* clean up `audio.rs`
- remove unused import
- unify capitalisation + quotes in output messages.
2026-01-12 12:42:03 +00:00
Orion 2e5dcd62c1
add missing test_button checks in button.rs (#384) 2026-01-12 04:16:29 +00:00
galister 72e64ad965
Update documentation for sprite and image widgets
Added emphasis on setting width and height for sprite and image widgets.
2026-01-12 04:07:59 +00:00
galister 0e004e7b3f fix some button actions missing filters 2026-01-12 12:17:30 +09:00
oo8dev 91bc46defe
Merge pull request #382 from wlx-team/feat-dash-tabbed-settings
dash-frontend: Tabbed settings, minor fixes
2026-01-11 19:45:26 +01:00
oo8dev b8f266ea6a
Merge pull request #383 from BERADQ/chinese/1-11
Add Chinese (Simplified) translation
2026-01-11 19:24:53 +01:00
galister a33d4ec9d0 log svg loads 2026-01-12 02:32:45 +09:00
ZokuTe 4c3500ea16
chore(i18n): Fix typo in Chinese (Simplified) translation
Co-authored-by: Amos Wong <8733840+amoshydra@users.noreply.github.com>
2026-01-12 01:10:33 +08:00
BERADQ bc1c993e17
chore(i18n): Add Chinese (Simplified) translation 2026-01-11 23:24:08 +08:00
galister 3889c9d2ea fix stereo on wayvr overlays 2026-01-11 23:38:35 +09:00
Aleksander 8eda46fb5e update logo 2026-01-11 15:05:23 +01:00
Aleksander a0451408aa wgui: fix possible panic 2026-01-11 14:59:26 +01:00
Aleksander 84d4fb7cc2 Tabbed settings (Closes #355) 2026-01-11 14:41:47 +01:00
amoshydra b48320c7bc feat: use contextual label for full frame 3d base on stereo mode
show Full-SBS when stereo is LeftRight or RightLeft
show Full-TAB when stereo is TopBottom
show Full-BAT when stereo is BottomTop
2026-01-11 12:03:45 +00:00
amoshydra ce5191ddd1 feat: add full frame 3d support
Related to #346
2026-01-11 12:03:45 +00:00
Aleksander 37e3ab1bda dash-frontend: Scrollable audio device list (Closes #353), wgui: fix scrollbar rendering 2026-01-11 11:11:01 +01:00
Aleksander 6b98d5f188 openxr: Fix 1.0 click threshold (Closes #374), wgi: less aggressive scrolling 2026-01-11 10:45:45 +01:00
oo8dev f059c5668a
Merge pull request #373 from wlx-team/staging
Add Italian translations
2026-01-10 21:41:53 +01:00
Aleksander e7fc7be950 Add Italian translations
[skip ci]
2026-01-10 21:40:38 +01:00
galister 6136fae885
Remove button label update instructions from README
Removed section about updating button label from stdout in README.
2026-01-10 17:15:41 +00:00
galister 9505352f37 Release 26.1.2 2026-01-10 22:52:10 +09:00
galister 07cb0e7ab8 fix icon loading in cache folder missing 2026-01-10 22:51:22 +09:00
galister 285f6db134 fix bindings for wmr controllers 2026-01-10 22:51:04 +09:00
galister c7757500b0 Release 26.1.1 2026-01-10 22:33:08 +09:00
galister 7f2d62b506 fix drm_exporter panic 2026-01-10 22:26:52 +09:00
galister 7deb2a30db previously wlxoverlay note 2026-01-10 21:20:42 +09:00
galister df7e9be13d update readme 2026-01-10 21:16:26 +09:00
oo8dev 1d605888a3
Update readme screenshot (#366)
[skip ci]
2026-01-10 12:11:28 +00:00
galister 1394c143ae wlx-capture GPL-3.0-only 2026-01-10 20:54:29 +09:00
galister fcba21ef53 also add license to main folder 2026-01-10 20:53:06 +09:00
galister 9a23fdc541 license fixes 2026-01-10 20:52:33 +09:00
303 changed files with 20975 additions and 7455 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
open_collective: wayvr-org

View File

@ -22,25 +22,28 @@ If this is a regression, please mention which version was working previously.
**Kernel version**:
**VR Runtime**:
- [ ] Monado/WiVRn
- [ ] SteamVR/ALVR
- [ ] Monado
- [ ] WiVRn
- [ ] ALVR
- [ ] SteamLink
- [ ] SteamVR (not SteamLink)
<!-- Run `vulkaninfo --summary` and paste the devices section from the bottom. -->
**GPU models and driver versions**:
## Overlay Logs
<!-- Start the overlay once more with the following environment variables:
RUST_BACKTRACE=full
RUST_LOG=debug
If your issue is graphical or crash or freeze, also add:
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation
<!-- Start the overlay from terminal, with additional environment variables:
Next, create the empty log file: echo > /tmp/wlx.log
AppImage:
RUST_BACKTRACE=full RUST_LOG=debug /path/to/WayVR.AppImage
Be sure to go and reproduce the issue once more, after these have been set.
System or AUR package:
RUST_BACKTRACE=full RUST_LOG=debug wayvr
Upload the log file from: /tmp/wlx.log
Reproduce the issue once more, while WayVR is running from the terminal.
Upload the log file from: /tmp/wayvr.log
-->

View File

@ -11,7 +11,7 @@ env:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./wayvr

View File

@ -15,7 +15,7 @@ env:
jobs:
build_appimage:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./wayvr

View File

@ -11,7 +11,7 @@ env:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./wayvr

View File

@ -11,7 +11,7 @@ env:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./wayvr

View File

@ -11,7 +11,7 @@ env:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./wayvr

View File

@ -11,7 +11,7 @@ env:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./wayvr

View File

@ -11,7 +11,7 @@ env:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./wayvr

View File

@ -13,7 +13,7 @@ env:
jobs:
make_release:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ./wayvr
@ -36,10 +36,9 @@ jobs:
cargo build --release
- name: Make tarball
run: |
pip install portage pycargoebuild
wget https://github.com/gentoo/gentoo/raw/refs/heads/master/metadata/license-mapping.conf
mkdir dist
pycargoebuild --distdir dist --license-mapping license-mapping.conf --crate-tarball --crate-tarball-path wayvr-crates.tar.xz
cargo vendor > vendor-config.toml
mv vendor-config.toml vendor
tar --xz -cf vendor.tar.xz vendor
- name: Create Release
id: create_release
uses: actions/create-release@v1
@ -81,12 +80,12 @@ jobs:
asset_name: WayVR-${{ github.ref_name }}-x86_64.AppImage
asset_content_type: application/octet-stream
- name: Upload crates tarball
- name: Upload vendor tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_KEY }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./wayvr/wayvr-crates.tar.xz
asset_name: WayVR-${{ github.ref_name }}-crates.tar.xz
asset_path: ./wayvr/vendor.tar.xz
asset_name: vendor.tar.xz
asset_content_type: application/x-gtar

View File

@ -1,7 +1,5 @@
#!/bin/sh
sudo add-apt-repository -syn universe
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
sudo apt-get update
sudo apt-get install -y fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libxkbcommon-x11-dev libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1 libwayland-dev libegl-dev libxcb-glx0 libxcb-glx0-dev
rustup update

2030
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,42 @@
[workspace]
resolver = "3"
members = [
"dash-frontend",
"scripts/prost_build",
"uidev",
"wayvr",
"wayvr-ipc",
"wayvrctl",
"wgui",
"wlx-capture",
"wlx-common",
]
[workspace.dependencies]
anyhow = "1.0.100"
clap = { version = "4.5.53", features = ["derive"] }
glam = { version = "0.30.9", features = ["mint", "serde"] }
idmap = "0.2.2"
idmap-derive = "0.2.22"
log = "0.4.29"
regex = "1.12.2"
rust-embed = "8.9.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.145"
slotmap = "1.1.1"
smol = "2.0.2"
strum = { version = "0.27.2", features = ["derive"] }
uuid = { version = "1.19.0", features = ["fast-rng", "v4", "serde"] }
vulkano = { version = "0.35.2", default-features = false, features = [
"macros",
] }
vulkano-shaders = "0.35.0"
wayland-client = { version = "0.31.11" }
xdg = "3.0.0"
[patch.crates-io]
vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928a56ce16b376037c1120f2b167678" }
[profile.dev]
opt-level = 1
debug = true
@ -19,39 +58,3 @@ incremental = true
[profile.release-with-debug]
inherits = "release"
debug = true
[workspace]
members = [
"uidev",
"wgui",
"wlx-common",
"wayvr",
"wlx-capture",
"dash-frontend",
"wayvr-ipc",
"wayvrctl",
]
resolver = "3"
[patch.crates-io]
vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928a56ce16b376037c1120f2b167678" }
[workspace.dependencies]
anyhow = "1.0.100"
glam = { version = "0.30.9", features = ["mint", "serde"] }
clap = { version = "4.5.53", features = ["derive"] }
xdg = "3.0.0"
idmap = "0.2.2"
idmap-derive = "0.2.22"
log = "0.4.29"
regex = "1.12.2"
rust-embed = "8.9.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.145"
slotmap = "1.1.1"
strum = { version = "0.27.2", features = ["derive"] }
vulkano = { version = "0.35.2", default-features = false, features = [
"macros",
] }
vulkano-shaders = "0.35.0"
wayland-client = { version = "0.31.11" }

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -1,20 +1,20 @@
![WayVR splash screen header](https://github.com/wlx-team/wayvr/blob/guide/wayvr-readme-header.webp?raw=true)
![WayVR splash screen header](https://github.com/wayvr-org/wayvr/blob/guide/wayvr-readme-header.webp?raw=true)
# WayVR
# WayVR (previously WlxOverlay-S)
A lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops, inspired by XSOverlay.
A lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops.
WayVR lets you to access your desktop screens while in VR.
WayVR lets you access your desktop screens while in VR, and even launch apps directly in VR.
In comparison to similar overlays, WayVR aims to run alongside VR games and experiences while having as little performance impact as possible. The UI appearance and rendering techniques are kept as simple and efficient as possible, while still allowing a high degree of customizability.
![Screenshot of WayVR being used as an OpenXR home environment](https://github.com/galister/wlx-overlay-s/blob/guide/wlx-s.png?raw=true)
![Screenshot of WayVR being used as an OpenXR home environment](https://github.com/wayvr-org/wayvr/blob/guide/wayvr-readme-screenshot.webp?raw=true)
## Join the Linux VR Community
We are available on either **Discord** or **Matrix space**:
[![LVRA Discord](https://img.shields.io/discord/1065291958328758352?style=for-the-badge&logo=discord)](https://discord.gg/EHAYe3tTYa) [![LVRA Matrix](https://img.shields.io/matrix/linux-vr-adventures:matrix.org?logo=matrix&style=for-the-badge)](https://matrix.to/#/#linux-vr-adventures:matrix.org)
[![LVRA Discord](https://img.shields.io/discord/1065291958328758352?style=for-the-badge/&logo=discord)](https://discord.gg/EHAYe3tTYa) [![LVRA Matrix](https://img.shields.io/matrix/linux-vr-adventures:matrix.org?logo=matrix&style=for-the-badge)](https://matrix.to/#/#linux-vr-adventures:matrix.org)
Questions/issues specific to WayVR will be handled in the `wayvr` chat room. Feel free to ask anything.
@ -24,14 +24,10 @@ Questions/issues specific to WayVR will be handled in the `wayvr` chat room. Fee
There are multiple ways to install WayVR:
1. AppImage: Download from [Releases](https://github.com/galister/wlx-overlay-s/releases)
1. AUR package: [wlx-overlay-s-git](https://aur.archlinux.org/packages/wlx-overlay-s-git)
1. Homebrew:
- Add AtomicXR tap: `brew tap matrixfurry.com/atomicxr https://tangled.sh/@matrixfurry.com/homebrew-atomicxr`
- Install WayVR: `brew install wlx-overlay-s`
1. [Building from source](https://github.com/galister/wlx-overlay-s/wiki/Building-from-Source).
1. AppImage: Download from [Releases](https://github.com/wayvr-org/wayvr/releases)
1. AUR package: [wayvr](https://aur.archlinux.org/packages/wayvr) or [wayvr-git](https://aur.archlinux.org/packages/wayvr-git)
1. Nix package: [wayvr](https://search.nixos.org/packages?channel=unstable&show=wayvr&query=wayvr) or [unstable package from nixpkgs-xr](https://github.com/nix-community/nixpkgs-xr)
1. [Docs: Building from source](https://wayvr.org/docs/basics/building-from-source/).
### General Setup
@ -44,7 +40,7 @@ There are multiple ways to install WayVR:
For users specifically running **SteamVR via Steam Flatpak**, follow these steps:
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
1. Grab the latest AppImage from [Releases](https://github.com/wayvr-org/wayvr/releases).
1. `WayVR-*.AppImage --appimage-extract`
1. `chmod +x squashfs-root/AppRun`
1. Move the newly created `squashfs-root` folder to a location accessible by the Steam Flatpak.
@ -60,15 +56,15 @@ For users specifically running **SteamVR via Steam Flatpak**, follow these steps
In case screens were selected in the wrong order:
- Go to Settings and press `Clear PipeWire tokens` and then `Restart software`
- Pay attention to your notifications, it tells you in which order to pick the screens.
- If notifications don't show, try start WayVR from the terminal and look for instructions in there.
- Pay attention to your notifications, which tell you in which order to pick the screens.
- If notifications don't show, try starting WayVR from the terminal and look for instructions in there.
**WiVRn users**: Select WayVR from the `Application` drop-down. If there's no such entry, select `Custom` and browse to your WayVR executable or AppImage.
**Envision users**: Go to the Plugins menu and select the WayVR plugin. This will download and run the AppImage version of the overlay.
In order to run a standalone installation (for instance from the AUR), create a bash script containing `wayvr --openxr --show` and then set this bash script as a custom Envision plugin.
To run a standalone installation (for instance, from the AUR), create a bash script containing `wayvr --openxr --show` and then set this bash script as a custom Envision plugin.
This will show a home environment with headset passthrough by default or a [customizable background](https://github.com/galister/wlx-overlay-s/wiki/OpenXR-Skybox)!
This will show a home environment with headset passthrough enabled by default or a [customizable background](https://wayvr.org/docs/various/openxr-skybox/)!
**SteamVR users**: WayVR will register itself for auto-start, so there is no need to start it every time. Disclaimer: SteamVR will sometimes disregard this and not start WayVR anyway.
@ -78,7 +74,7 @@ This will show a home environment with headset passthrough by default or a [cust
### Working Set
The working set consists of all currently selected overlays; screens, mirrors, keyboard, etc.
The working set consists of all currently selected overlays: screens, mirrors, keyboard, etc.
The working set appears in front of the headset when shown, and can be re-centered by hiding and showing again.
@ -108,19 +104,17 @@ Check your left wrist for the watch. The watch is the primary tool for controlli
The top of the watch shows device batteries, and the bottom shows your overlay controls.
Enter edit mode (leftmost button on bottom) to edit your overlay sets.
Enter edit mode (the leftmost button at the bottom) to edit your overlay sets.
While in edit mode, the watch can also be grabbed, and passed between your hands.
While in edit mode, the watch can also be grabbed and passed between your hands.
After grabbing, the watch will automatically attach to the hand that's opposite from the one that held it.
In edit mode, try hovering other overlays to see their advanced options!
![Watch usage guide](https://github.com/galister/wlx-overlay-s/blob/guide/wlx-watch.png)
In edit mode, try hovering over other overlays to see their advanced options!
### The screens
Hovering a pointer over a screen will move the mouse. If there are more than one pointers hovering a screen, the pointer that was last used to click will take precedence.
Hovering a pointer over a screen will move the mouse. If more than one pointer is hovering over a screen, the pointer that was last used to click will take precedence.
The click type depends on the laser color:
@ -134,29 +128,45 @@ The click type depends on the laser color:
Typing
- Use the BLUE laser when typing regularly.
- While using ORANGE laser, all keystrokes will have SHIFT applied.
- While using the ORANGE laser, all keystrokes will have SHIFT applied.
- Purple laser is customizable via the settings, no modifier by default.
**Modifier Keys** are sticky. They will remain pressed until a non-modifier key is pressed, the modifier gets toggled off, or the keyboard gets hidden.
**Modifier Keys are sticky**. They will remain pressed until either:
- a non-modifier key is pressed
- the modifier is toggled off by clicking again
- the keyboard is hidden (including via show-hide)
### Default Bindings
![Index Controller Bindings](https://github.com/galister/wlx-overlay-s/blob/guide/wlx-index.png)
![Index Controller Bindings](https://github.com/wayvr-org/wayvr/blob/guide/wlx-index.png)
![Touch Controller Bindings](https://github.com/galister/wlx-overlay-s/blob/guide/wlx-oculus.png)
![Touch Controller Bindings](https://github.com/wayvr-org/wayvr/blob/guide/wlx-oculus.png)
To customize bindings on OpenXR, refer to the [OpenXR Bindings wiki page](https://github.com/galister/wlx-overlay-s/wiki/OpenXR-Bindings).
### Changing Bindings
If your bindings are not supported, please reach out. \
SteamVR: Simply change the bindings from the SteamVR bindings section. If WayVR doesn't show up on the list, select any other title and then press back on the top left. (SteamVR is weird like that sometimes)
OpenXR (Monado/WiVRn): See [Docs: OpenXR Bindings](https://wayvr.org/docs/various/openxr-bindings/)
If your controllers are not supported, please reach out. \
We would like to work with you and include additional bindings.
## Customization
See these relevant wiki pages:
- For all available config options, check [Docs: Configuration](https://wayvr.org/docs/basics/configuration/)
- Looking to customize look & feel, or add functionality? See [Docs: Customization](https://wayvr.org/docs/basics/customization/)
- Looking to change the OpenXR background? See [Docs: OpenXR Skybox](https://wayvr.org/docs/various/openxr-skybox/)
## Troubleshooting
When an error is detected, we often print tips for fixing into the log file.
When an error is detected, we often print tips for fixing it into the log file.
Logs will be at `/tmp/wayvr.log` for most distros.
Check [here](https://github.com/galister/wlx-overlay-s/wiki/Troubleshooting) for tips.
Check [here](https://wayvr.org/docs/various/troubleshooting/) for tips.
## Known Issues
@ -165,8 +175,8 @@ Check [here](https://github.com/galister/wlx-overlay-s/wiki/Troubleshooting) for
If the mouse is moving on a completely different screen, the screens were likely selected in the wrong order:
- Go to Settings and press `Clear PipeWire tokens` and then `Restart software`
- Pay attention to your notifications, it tells you in which order to pick the screens.
- If notifications don't show, try start WayVR from the terminal and look for instructions in there.
- Pay attention to your notifications, which tell you in which order to pick the screens.
- If notifications don't show, try starting WayVR from the terminal and look for instructions in there.
COSMIC desktop:
@ -178,23 +188,23 @@ X11 users:
- DPI scaling is not supported and will mess with the mouse.
- Upright screens are not supported and will mess with the mouse.
### Screens are blank or black or frozen on Steam Link
### Screens are blank or black or frozen on SteamVR 2.14+
As of SteamVR version 2.14.x, PipeWire capture no longer works when using Steam Link.
We're unable to completely troubleshoot how and why Steam Link interferes with PipeWire, so consider the following workarounds for the time being:
We're unable to completely troubleshoot how and why SteamVR interferes with PipeWire, so consider the following workarounds for the time being:
- Use another streamer, such as WiVRn or ALVR
- Use another streamer, such as WiVRn or ALVR. Note that SteamVR on Linux is considered experimental by Valve themselves [disclaimer](https://github.com/ValveSoftware/SteamVR-for-Linux?tab=readme-ov-file#steamvr-release-notes-and-known-issues).
- If your desktop [supports ScreenCopy](https://wayland.app/protocols/wlr-screencopy-unstable-v1#compositor-support), go to Settings and set `Wayland capture method` to `ScreenCopy`
- If your desktop has an X11 mode, try using that
### Modifiers get stuck
Hiding the keyboard will un-press all of its buttons. Alternatively, go to Settings and use the `Restart software` button.
Hiding the keyboard will unpress all of its buttons. Alternatively, go to Settings and use the `Restart software` button.
### X11 limitations
- X11 capture can generally seem slow. This is because zero-copy GPU capture is not supported on the general X11 desktop. Consider trying Wayland.
- DPI scaling is not supported and may cause the mouse to not follow the laser properly.
- Upright screens are not supported and can cause the mouse to not follow the laser properly.
- Screen changes (connecting / disconnecting a display, resolution changes, etc) are not handled at runtime. Restart the overlay for these to take effect.
- Screen changes (connecting/disconnecting a display, resolution changes, etc) are not handled at runtime. Restart the overlay for these to take effect.

View File

@ -2,29 +2,32 @@
name = "dash-frontend"
version = "0.1.0"
edition = "2024"
description = "The dashboard frontend for WayVR"
license = "GPL-3.0-only"
authors = ["galister", "oo8dev"]
repository = "https://github.com/wlx-team/wayvr"
[dependencies]
wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
wgui = { path = "../wgui/" }
wlx-common = { path = "../wlx-common" }
anyhow.workspace = true
async-native-tls = "0.5.0"
chrono = "0.4.42"
glam = { workspace = true, features = ["mint", "serde"] }
http-body-util = "0.1.3"
hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
keyvalues-parser = { git = "https://codeberg.org/CosmicHarper/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
log.workspace = true
xdg.workspace = true
rust-embed.workspace = true
serde = { workspace = true, features = ["rc"] }
serde_json.workspace = true
strum.workspace = true
chrono = "0.4.42"
keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
smol = "2.0.2"
hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
http-body-util = "0.1.3"
async-native-tls = "0.5.0"
smol = { workspace = true }
smol-hyper = "0.1.1"
strum.workspace = true
uuid.workspace = true
wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
wgui = { path = "../wgui/" }
wlx-common = { path = "../wlx-common" }
xdg.workspace = true
[features]
default = ["monado" ]
default = ["monado"]
monado = []

674
dash-frontend/LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<path fill="currentColor" d="m10 18l-6-6l6-6l1.4 1.45L7.85 11H20v2H7.85l3.55 3.55z" />
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<path fill="currentColor" d="m14 18l-1.4-1.45L16.15 13H4v-2h12.15L12.6 7.45L14 6l6 6z" />
</svg>

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#33FF99" d="m9.55 18l-5.7-5.7l1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"/></svg>

After

Width:  |  Height:  |  Size: 295 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 22l-4.25-4.25l1.425-1.425L11 18.15V13H5.875L7.7 14.8l-1.45 1.45L2 12l4.225-4.225L7.65 9.2L5.85 11H11V5.85L9.175 7.675L7.75 6.25L12 2l4.25 4.25l-1.425 1.425L13 5.85V11h5.125L16.3 9.2l1.45-1.45L22 12l-4.25 4.25l-1.425-1.425L18.15 13H13v5.125l1.8-1.825l1.45 1.45z"/></svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#FF4455" d="M12 17q.425 0 .713-.288T13 16t-.288-.712T12 15t-.712.288T11 16t.288.713T12 17m-1-4h2V7h-2zm1 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/></svg>

After

Width:  |  Height:  |  Size: 574 B

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
sodipodi:docname="gravity.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="23.234375"
inkscape:cx="17.861466"
inkscape:cy="22.488231"
inkscape:window-width="1582"
inkscape:window-height="1302"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<defs
id="defs1" />
<!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<!-- Modified it a little bit, there's no "space gravity" icon available. -->
<path
fill="currentColor"
d="m 15.76786,18.274928 -1.391472,0.295932 -0.674684,-2.9751 -3.051267,-0.735557 -2.4062791,-3.292082 -0.8846145,1.824345 1.561484,2.213127 -1.1706157,0.808349 -1.9932655,-2.862487 1.752944,-3.7745856 Q 7.740355,9.2978686 8.2693798,9.1366908 8.7984057,8.9755132 9.3446883,8.9671549 9.9537305,8.9704882 10.547911,8.8283107 11.142091,8.6861403 11.667399,8.364791 12.192707,8.0434408 12.541821,7.5511898 12.890935,7.0589389 13.170167,6.5208564 l 1.228561,0.6477209 q -0.321651,0.575352 -0.696034,1.1164692 -0.374382,0.5411172 -0.890945,0.9726251 -0.466314,0.3718768 -0.998843,0.6112737 -0.532529,0.2393967 -1.117089,0.3386867 L 12,12 l 2.176682,-1.469824 4.352172,0.934365 -0.326914,1.35612 -3.582453,-0.734464 -2.11263,1.442217 2.281549,0.535655 z M 7.5517982,8.9044443 Q 7.0233673,9.1322023 6.4853273,8.9182692 5.9472884,8.7043353 5.7186139,8.1755407 5.4899403,7.6467461 5.7054294,7.108795 5.9209195,6.5708439 6.4475175,6.3423563 6.9741154,6.1138698 7.5149037,6.3288971 8.0556931,6.5439235 8.2807018,7.07126 8.5057095,7.5985974 8.2948017,8.1383704 8.0838948,8.6781445 7.5517982,8.9044443"
id="path1"
style="stroke-width:0.697484" />
<path
id="path2"
style="fill:none;stroke:currentColor;stroke-width:2.43525;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:0, 7.30575"
d="M 22.480952,12 C 22.480952,17.78847 17.78847,22.480952 12,22.480952 6.2115299,22.480952 1.5190477,17.78847 1.5190477,12 1.5190475,6.2115298 6.2115298,1.5190477 12,1.5190477 c 0.653263,0 1.292568,0.059766 1.912744,0.1741274 C 18.787854,2.5921561 22.480952,6.8647934 22.480952,12 Z"
sodipodi:nodetypes="ssssss" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8"/></svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE -->
<path fill="currentColor" d="M12 19q1.65 0 2.825-1.175T16 15v-4q0-1.65-1.175-2.825T12 7T9.175 8.175T8 11v4q0 1.65 1.175 2.825T12 19m-2-3h4v-2h-4zm0-4h4v-2h-4zm2 9q-1.625 0-3.012-.8T6.8 18H4v-2h2.1q-.075-.5-.088-1T6 14H4v-2h2q0-.5.012-1t.088-1H4V8h2.8q.35-.575.788-1.075T8.6 6.05L7 4.4L8.4 3l2.15 2.15q.7-.225 1.425-.225t1.425.225L15.6 3L17 4.4l-1.65 1.65q.575.375 1.038.862T17.2 8H20v2h-2.1q.075.5.088 1T18 12h2v2h-2q0 .5-.013 1t-.087 1H20v2h-2.8q-.8 1.4-2.187 2.2T12 21" />
</svg>

After

Width:  |  Height:  |  Size: 682 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zM17 6H7v13h10zM9 17h2V8H9zm4 0h2V8h-2zM7 6v13z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,110 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30"
height="30"
viewBox="0 0 7.9375004 7.9375004"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="dashboard_logo.svg"
inkscape:export-filename="dashboard_logo.png"
inkscape:export-xdpi="409.60001"
inkscape:export-ydpi="409.60001"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="16"
inkscape:cx="11.8125"
inkscape:cy="16.78125"
inkscape:window-width="1836"
inkscape:window-height="1185"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"
showgrid="true"
showguides="false">
<inkscape:grid
id="grid2"
units="mm"
originx="0"
originy="0"
spacingx="0.26458333"
spacingy="0.26458333"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#ad70ff;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
style="stop-color:#00ffff;stop-opacity:1;"
offset="0.99844205"
id="stop2" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="0"
y1="8.4666653"
x2="8.4664993"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(0.93751843,0.93750002)" />
</defs>
<g
inkscape:label="back"
inkscape:groupmode="layer"
id="layer1">
<rect
style="font-variation-settings:'wght' 700;fill:url(#linearGradient2);stroke-width:0.468754;stroke-linecap:round;stroke-linejoin:round"
id="rect1"
width="7.9380002"
height="7.9375"
x="0"
y="0"
rx="1.5874999"
ry="1.5874999" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1.5874998,3.7041664 v 1.3229167 h 1.3229166 v 1.3229165 l 1.3229165,-1e-7"
id="path3"
sodipodi:nodetypes="ccccc" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 1.5874998,2.6458332 V 1.5875 c 3.1749997,-2e-7 4.7624995,1.5874997 4.7624995,4.7624995 H 5.2916661"
id="path4"
sodipodi:nodetypes="cccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="front"
style="stroke-width:2.11667;stroke-dasharray:none" />
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -3,6 +3,7 @@
<theme>
<var key="side_size" value="48" />
<var key="top_size" value="40" />
<var key="side_sprite_size" value="26" />
<var key="side_button_size" value="48" />
</theme>
@ -25,13 +26,6 @@
</template>
<elements>
<div position="absolute" width="100%" justify_content="space_between">
<div />
<div padding="6">
<Button id="btn_close" sprite_src_builtin="dashboard/close.svg" color="#000000" border="2" border_color="~color_faded" />
</div>
</div>
<!-- left/right separator (menu and rest) -->
<div flex_direction="row" gap="8" width="100%" height="100%" padding="4" interactable="0">
<!-- LEFT MENU -->
@ -55,21 +49,34 @@
<SideButton id="btn_side_apps" src_builtin="dashboard/apps.svg" tooltip="APPLICATIONS" tooltip_side="right" />
<SideButton id="btn_side_games" src_builtin="dashboard/games.svg" tooltip="GAMES" tooltip_side="right" />
<SideButton id="btn_side_monado" src_builtin="dashboard/monado.svg" tooltip="MONADO_RUNTIME" tooltip_side="right" />
<!-- SideButton id="btn_side_processes" src_builtin="dashboard/window.svg" tooltip="PROCESSES" tooltip_side="right" /-->
<rectangle height="2" color="#FFFFFF33" width="~side_sprite_size" />
<SideButton id="btn_side_settings" src_builtin="dashboard/settings.svg" tooltip="SETTINGS" tooltip_side="right" />
</rectangle>
</div>
<!-- REST -->
<!-- content/bottom panel separator -->
<!-- top/content/bottom panel separator -->
<div
flex_direction="column"
gap="8"
width="100%"
height="100%"
overflow_x="scroll">
<!-- CONTENT -->
<!-- TOP PANEL -->
<div position="relative" width="100%" height="~top_size" min_height="~top_size" max_height="~top_size" justify_content="end">
<!-- Title bar -->
<div width="100%" align_items="center" justify_content="center">
<rectangle min_width="300" height="100%" consume_mouse_events="1" round="100%" color="~color_top_panel" align_items="center" justify_content="center" gap="8">
<sprite id="sprite_titlebar_icon" width="24" height="24" />
<label id="label_titlebar_title" weight="bold" size="16" text="Title" />
</rectangle>
</div>
<!-- Close button -->
<Button position="absolute" id="btn_close" color="~color_top_panel" round="100%" width="~top_size" height="~top_size" tooltip="CLOSE_WINDOW" tooltip_side="left">
<sprite src_builtin="dashboard/close.svg" width="32" height="32" />
</Button>
</div>
<!-- CONTENT -->
<!-- color and color2 alpha will be modified at runtime -->
<rectangle
id="rect_content"
@ -79,29 +86,26 @@
round="8"
flex_grow="1"
width="100%"
overflow_y="scroll"
overflow="scroll"
position="relative"
>
<!-- radial gradient -->
<rectangle
position="absolute" width="100%" height="100%"
gradient="radial" color="#44BBFF11" color2="#00000000" />
<div
id="content"
flex_direction="column"
overflow_x="scroll"
overflow_y="scroll"
padding_top="8"
padding_bottom="8"
padding_left="16"
padding_right="16"
gap="8"
width="100%"
min_height="100%"
>
<!-- filled-in at runtime -->
<div overflow_x="scroll" overflow_y="scroll" width="100%" height="100%">
<div
id="content"
flex_direction="column"
padding="16"
gap="8"
width="100%"
height="100%"
>
<!-- filled-in at runtime -->
</div>
</div>
<div position="absolute" id="popup_manager" width="100%" height="100%" />
</rectangle>
<!-- BOTTOM PANEL -->

View File

@ -11,10 +11,10 @@
gap="8"
round="8" />
<!-- src, text, translation -->
<!-- src_builtin, text, translation -->
<template name="GroupBoxTitle">
<div flex_direction="row" align_items="center" gap="8">
<sprite src="${src}" src_builtin="${src_builtin}" width="24" height="24" />
<sprite src_builtin="${src_builtin}" width="24" height="24" />
<label text="${text}" translation="${translation}" weight="bold" size="18" />
</div>
<rectangle color="#FFFFFF44" width="100%" height="2" />

View File

@ -0,0 +1,16 @@
<layout>
<template name="LoadingWithText">
<div id="root" width="100%" height="100%" align_items="center" justify_content="center">
<div flex_direction="row" gap="8" align_items="center" >
<sprite id="sprite_loading" src_builtin="dashboard/loading.svg" width="32" height="32"/>
<label translation="LOADING" weight="bold"/>
</div>
</div>
</template>
<template name="LoadingWithoutText">
<div id="root" width="100%" height="100%" align_items="center" justify_content="center">
<sprite id="sprite_loading" src_builtin="dashboard/loading.svg" width="32" height="32"/>
</div>
</template>
</layout>

View File

@ -1,5 +1,4 @@
<layout>
<include src="t_tab_title.xml" />
<include src="../theme.xml" />
<template name="AppEntry">
@ -25,7 +24,6 @@
</template>
<elements>
<TabTitle translation="APPLICATIONS" icon="dashboard/apps.svg" />
<!-- placeholders for now -->
<!--
<div gap="4" align_items="center">
@ -46,7 +44,6 @@
flex_direction="row"
flex_wrap="wrap"
gap="4"
overflow_y="scroll"
/>
</elements>
</layout>

View File

@ -1,8 +1,6 @@
<layout>
<include src="t_tab_title.xml" />
<elements>
<TabTitle translation="GAMES" icon="dashboard/games.svg" />
<div id="game_list_parent" align_items="center" />
<div id="running_games_list_parent" align_self="center" />
<div id="game_list_parent" align_items="center" flex_direction="column" gap="8" />
</elements>
</layout>

View File

@ -8,7 +8,7 @@
align_items="center"
flex_grow="1"
gap="24">
<sprite src_builtin="dashboard/wayvr_dashboard.svg" width="96" height="96" />
<sprite src_builtin="dashboard/wayvr_dashboard.png" width="96" height="96" />
<label id="label_hello" size="32" weight="bold" />
<!-- main button list -->
@ -16,9 +16,8 @@
<MenuButton id="btn_apps" icon="dashboard/apps.svg" translation="APPLICATIONS" />
<MenuButton id="btn_games" icon="dashboard/games.svg" translation="GAMES" />
<MenuButton id="btn_monado" icon="dashboard/monado.svg" text="Monado" />
<!--MenuButton id="btn_processes" icon="dashboard/window.svg" translation="PROCESSES" /-->
<MenuButton id="btn_settings" icon="dashboard/settings.svg" translation="SETTINGS" />
</div>
</div>
</elements>
</layout>
</layout>

View File

@ -1,40 +1,14 @@
<layout>
<include src="t_tab_title.xml" />
<include src="../t_group_box.xml" />
<!-- key: str, value: str -->
<template name="BoolFlag">
<div flex_direction="row" gap="4">
<label text="${key}" />
<label weight="bold" text="${value}" />
</div>
</template>
<!-- name, checked, flag_* -->
<template name="Cell">
<rectangle macro="group_box">
<CheckBox id="checkbox" text="${name}" checked="${checked}" />
<div flex_direction="row" gap="8">
<BoolFlag key="Active:" value="${flag_active}" />
<BoolFlag key="Focused:" value="${flag_focused}" />
<BoolFlag key="IO active:" value="${flag_io_active}" />
<BoolFlag key="Overlay:" value="${flag_overlay}" />
<BoolFlag key="Primary:" value="${flag_primary}" />
<BoolFlag key="Visible:" value="${flag_visible}" />
</div>
</rectangle>
</template>
<elements>
<TabTitle translation="MONADO_RUNTIME" icon="dashboard/monado.svg" />
<label translation="DISPLAY_BRIGHTNESS" />
<Slider id="slider_brightness" width="300" height="24" min_value="0" max_value="140" />
<label translation="LIST_OF_PROCESSES" />
<div id="list_parent" flex_direction="column" gap="8">
<!-- filled at runtime -->
<div gap="4">
<Tabs id="tabs">
<Tab name="general_settings" translation="GENERAL_SETTINGS" sprite_src_builtin="dashboard/settings.svg" />
<Tab name="process_list" translation="PROCESS_LIST" sprite_src_builtin="dashboard/cpu.svg" />
<Tab name="debug_timings" translation="DEBUG_INFO" sprite_src_builtin="dashboard/not_a_bug.svg" />
</Tabs>
<div gap="4" id="content" width="100%">
<!-- filled-in at runtime -->
</div>
</div>
</elements>
</layout>

View File

@ -0,0 +1,27 @@
<layout>
<include src="../t_group_box.xml" />
<!-- name, limit_min, limit_max -->
<template name="DebugGraph">
<rectangle flex_direction="column" align_items="center" gap="8" border="2" color="#ffffff11" color2="#ffffff22" gradient="vertical" border_color="#ffffff33" round="4" padding="4">
<label text="${name}" size="9" weight="bold" />
<BarGraph id="graph" width="180" height="70" unit="ms" limit_min="${limit_min}" limit_max="${limit_max}" capacity="50" />
</rectangle>
</template>
<!-- text -->
<template name="SessionButton">
<Button id="button" text="${text}" />
</template>
<elements>
<rectangle macro="group_box">
<GroupBoxTitle src_builtin="dashboard/apps.svg" text="Sessions" />
<div id="session_list_parent" flex_wrap="wrap" flex_direction="row" gap="8" />
<GroupBoxTitle src_builtin="dashboard/not_a_bug.svg" text="Debug timings" />
<div id="timings_parent" flex_wrap="wrap" flex_direction="row" gap="8">
<label text="Please select which session you want to monitor" />
</div>
</rectangle>
</elements>
</layout>

View File

@ -0,0 +1,56 @@
<layout>
<include src="../t_group_box.xml" />
<macro name="slider" width="100%" height="24"/>
<theme>
<var key="s_width" value="500"/>
</theme>
<elements>
<div flex_direction="column" gap="8">
<rectangle macro="group_box">
<GroupBoxTitle src_builtin="dashboard/settings.svg" translation="GENERAL_SETTINGS" />
<label translation="DISPLAY_BRIGHTNESS" />
<Slider id="slider_brightness" width="300" height="24" min_value="0" max_value="140" />
</rectangle>
<rectangle macro="group_box">
<GroupBoxTitle src_builtin="dashboard/settings.svg" translation="APP_SETTINGS.COLOR_KEYING" />
<div gap="8" width="~s_width">
<div gap="8" flex_direction="column" width="100%">
<label text="Color"/>
<ColorSelector id="cs_keying" height="24" width="100%"/>
</div>
<div gap="8" flex_direction="column" width="100%">
<label text="Despill"/>
<Slider id="slider_keying_despill" min_value="0" max_value="100" macro="slider"/>
</div>
<div gap="8" flex_direction="column" width="100%">
<label text="Curve (0 = disabled)"/>
<Slider id="slider_keying_curve" min_value="0" max_value="100" macro="slider"/>
</div>
</div>
<div gap="8" width="~s_width">
<div gap="8" flex_direction="column" width="100%">
<label text="Hue range"/>
<Slider id="slider_keying_hue_range" min_value="0" max_value="100" macro="slider"/>
</div>
<div gap="8" flex_direction="column" width="100%">
<label text="Saturation range"/>
<Slider id="slider_keying_saturation_range" min_value="0" max_value="100" macro="slider"/>
</div>
<div gap="8" flex_direction="column" width="100%">
<label text="Value range"/>
<Slider id="slider_keying_value_range" min_value="0" max_value="100" macro="slider"/>
</div>
</div>
</rectangle>
</div>
</elements>
</layout>

View File

@ -0,0 +1,35 @@
<layout>
<include src="../t_group_box.xml" />
<!-- key: str, value: str -->
<template name="BoolFlag">
<div flex_direction="row" gap="4">
<label text="${key}" />
<label weight="bold" text="${value}" />
</div>
</template>
<!-- name, checked, flag_* -->
<template name="Cell">
<rectangle macro="group_box">
<CheckBox id="checkbox" text="${name}" checked="${checked}" />
<div flex_direction="row" gap="8" flex_wrap="wrap">
<BoolFlag key="Active:" value="${flag_active}" />
<BoolFlag key="Focused:" value="${flag_focused}" />
<BoolFlag key="IO active:" value="${flag_io_active}" />
<BoolFlag key="Overlay:" value="${flag_overlay}" />
<BoolFlag key="Primary:" value="${flag_primary}" />
<BoolFlag key="Visible:" value="${flag_visible}" />
</div>
</rectangle>
</template>
<elements>
<rectangle macro="group_box">
<GroupBoxTitle src_builtin="dashboard/cpu.svg" translation="PROCESS_LIST" />
<div id="list_parent" flex_direction="column" gap="8">
<!-- filled at runtime -->
</div>
</rectangle>
</elements>
</layout>

View File

@ -1,10 +0,0 @@
<layout>
<include src="t_tab_title.xml" />
<elements>
<TabTitle translation="LIST_OF_WINDOWS" icon="dashboard/window.svg" />
<div id="window_list_parent" />
<TabTitle translation="LIST_OF_PROCESSES" icon="dashboard/cpu.svg" />
<div id="process_list_parent" />
</elements>
</layout>

View File

@ -1,5 +1,4 @@
<layout>
<include src="t_tab_title.xml" />
<include src="../t_group_box.xml" />
<include src="../t_dropdown_button.xml" />
@ -14,8 +13,13 @@
</template>
<template name="SliderSetting">
<label text="${text}" translation="${translation}" />
<Slider id="${id}" width="250" height="24" min_value="${min}" max_value="${max}" step="${step}" value="${value}" tooltip="${tooltip}" />
<Slider id="${id}" width="200" height="24" min_value="${min}" max_value="${max}" step="${step}" value="${value}" tooltip="${tooltip}" />
<label text="${text}" weight="bold" translation="${translation}" />
</template>
<template name="RangeSetting">
<Slider id="${id}" width="200" height="24" min_value="${min}" max_value="${max}" step="${step}" value="${value}" value2="${value2}" tooltip="${tooltip}" />
<label text="${text}" weight="bold" translation="${translation}" />
</template>
<template name="SelectSetting">
@ -28,7 +32,7 @@
</template>
<template name="DangerButton">
<Button id="${id}" color="#AA3333" height="32" tooltip="${translation}_HELP" padding="4" gap="8" >
<Button id="${id}" color="#AA3333" height="32" tooltip="${translation}_HELP" padding="4" gap="8">
<sprite src_builtin="${icon}" height="24" width="24" />
<label align="left" translation="${translation}" weight="bold" min_width="200" />
</Button>
@ -36,17 +40,30 @@
<template name="AutostartApp">
<div id="${id}_root" flex_direction="row">
<Button id="${id}" color="#AA3333" height="24" padding="4" margin_top="-2" margin_bottom="-2" >
<Button id="${id}" color="#AA3333" height="24" padding="4" margin_top="-2" margin_bottom="-2">
<sprite src_builtin="dashboard/close.svg" height="20" width="20" />
</Button>
<div padding_left="8" >
<label align="left" text="${text}" weight="bold" overflow="hidden"/>
<div padding_left="8">
<label align="left" text="${text}" weight="bold" overflow="hidden" />
</div>
</div>
</template>
<elements>
<TabTitle translation="SETTINGS" icon="dashboard/settings.svg" />
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" />
<div gap="4">
<Tabs id="tabs">
<Tab name="look_and_feel" translation="APP_SETTINGS.LOOK_AND_FEEL" sprite_src_builtin="dashboard/palette.svg" />
<Tab name="skybox" translation="APP_SETTINGS.SKYBOX" sprite_src_builtin="dashboard/globe.svg" />
<Tab name="features" translation="APP_SETTINGS.FEATURES" sprite_src_builtin="dashboard/options.svg" />
<Tab name="controls" translation="APP_SETTINGS.CONTROLS" sprite_src_builtin="dashboard/controller.svg" />
<Tab name="space_drag" translation="APP_SETTINGS.SPACE_DRAG" sprite_src_builtin="dashboard/drag.svg" />
<Tab name="misc" translation="APP_SETTINGS.MISC" sprite_src_builtin="dashboard/blocks.svg" />
<Tab name="autostart_apps" translation="APP_SETTINGS.AUTOSTART_APPS" sprite_src_builtin="dashboard/apps.svg" />
<Tab name="troubleshooting" translation="APP_SETTINGS.TROUBLESHOOTING" sprite_src_builtin="dashboard/cpu.svg" />
</Tabs>
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" width="100%">
<!-- filled-in at runtime -->
</div>
</div>
</elements>
</layout>

View File

@ -0,0 +1,14 @@
<layout>
<include src="../t_group_box.xml" />
<elements>
<div id="common_options_parent" flex_direction="column" gap="8"/>
<rectangle macro="group_box">
<GroupBoxTitle src_builtin="dashboard/gravity.svg" translation="APP_SETTINGS.SPACE_GRAVITY_GRAVITY" />
<div id="gravity_enabled_parent"/>
<div id="space_gravity_parent" flex_direction="column" gap="8">
<!-- filled-in at runtime -->
</div>
</rectangle>
</elements>
</layout>

View File

@ -1,9 +0,0 @@
<layout>
<!-- translation, icon -->
<template name="TabTitle">
<div gap="8" align_items="center">
<sprite src_builtin="${icon}" width="24" height="24" />
<label translation="${translation}" size="18" weight="bold" />
</div>
</template>
</layout>

View File

@ -2,6 +2,7 @@
<theme>
<var key="color_menu_dark" value="#0A0A0ACC" />
<var key="color_accent" value="#008cff" />
<var key="color_top_panel" value="#00000099" />
<var key="color_bottom_panel" value="#141e28" />
</theme>
</layout>

View File

@ -37,18 +37,18 @@
<Separator />
<label translation="APP_LAUNCHER.ASPECT_TITLE" />
<RadioGroup id="radio_orientation" flex_direction="row" gap="16">
<RadioBox translation="APP_LAUNCHER.ASPECT.WIDE" value="Wide" tooltip="16:9" checked="1" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SEMI_WIDE" value="SemiWide" tooltip="3:2" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SQUARE" value="Square" tooltip="1:1" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SEMI_TALL" value="SemiTall" tooltip="2:3" />
<RadioBox translation="APP_LAUNCHER.ASPECT.TALL" value="Tall" tooltip="9:16" />
<RadioBox translation="APP_LAUNCHER.ASPECT.WIDE" value="Wide" tooltip_str="16:9" checked="1" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SEMI_WIDE" value="SemiWide" tooltip_str="3:2" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SQUARE" value="Square" tooltip_str="1:1" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SEMI_TALL" value="SemiTall" tooltip_str="2:3" />
<RadioBox translation="APP_LAUNCHER.ASPECT.TALL" value="Tall" tooltip_str="9:16" />
</RadioGroup>
<!-- Separator /> // saved settings override this, so let's hide it for now
<label translation="APP_LAUNCHER.POS_TITLE" />
<RadioGroup id="radio_pos" flex_direction="row" gap="16">
<RadioBox translation="APP_LAUNCHER.POS.FLOATING" value="Floating" tooltip="APP_LAUNCHER.POS.FLOATING_HELP" />
<RadioBox translation="APP_LAUNCHER.POS.ANCHORED" value="Anchored" tooltip="APP_LAUNCHER.POS.ANCHORED_HELP" checked="1" />
<RadioBox translation="APP_LAUNCHER.POS.STATIC" value="Static" tooltip="APP_LAUNCHER.POS.STATIC_HELP" />
<RadioBox translation="APP_LAUNCHER.POS.FLOATING" value="Floating" tooltip_str="APP_LAUNCHER.POS.FLOATING_HELP" />
<RadioBox translation="APP_LAUNCHER.POS.ANCHORED" value="Anchored" tooltip_str="APP_LAUNCHER.POS.ANCHORED_HELP" checked="1" />
<RadioBox translation="APP_LAUNCHER.POS.STATIC" value="Static" tooltip_str="APP_LAUNCHER.POS.STATIC_HELP" />
</RadioGroup -->
<Separator />
<div flex_direction="row" justify_content="space_between" gap="16">
@ -61,4 +61,4 @@
</div>
</div>
</elements>
</layout>
</layout>

View File

@ -1,16 +1,16 @@
<layout>
<include src="../t_group_box.xml" />
<include src_builtin="../t_group_box.xml" />
<!-- device_name, device_icon -->
<template name="DeviceSlider">
<rectangle macro="group_box">
<div width="100%" align_items="center" justify_content="center" gap="8">
<sprite src="${device_icon}" width="16" height="16" />
<sprite src_builtin="${device_icon}" width="16" height="16" />
<label text="${device_name}" margin_right="8" size="12" weight="bold" />
</div>
<div width="100%" align_items="center">
<CheckBox id="checkbox" />
<Button sprite_src="${volume_icon}" id="btn_mute" width="32" />
<Button sprite_src_builtin="${volume_icon}" id="btn_mute" width="32" />
<Slider id="slider" flex_grow="1" height="16" min_value="0" max_value="150" margin_left="8" />
</div>
</rectangle>
@ -41,28 +41,30 @@
flex_grow="1"
id="${id}"
translation="${translation}"
sprite_src="${src}">
sprite_src_builtin="${src}">
</Button>
</template>
<elements>
<div id="devices" flex_direction="column" gap="4">
<div flex_direction="column" gap="4">
<div id="devices" flex_direction="column" gap="4" max_height="400" overflow_y="scroll">
</div>
</div>
<!-- bottom buttons -->
<div flex_direction="row" gap="4">
<Button
id="btn_auto"
sprite_src_builtin="dashboard/magic_wand.svg"
min_width="32"
tooltip="AUDIO.AUTO_SWITCH_TO_VR_AUDIO"
color="~color_accent"
tooltip_side="right" />
<!-- bottom buttons -->
<div flex_direction="row" gap="4">
<Button
id="btn_auto"
sprite_src_builtin="dashboard/magic_wand.svg"
min_width="32"
tooltip="AUDIO.AUTO_SWITCH_TO_VR_AUDIO"
color="~color_accent"
tooltip_side="right" />
<BottomButton id="btn_sinks" src_builtin="dashboard/volume.svg" translation="AUDIO.SPEAKERS" />
<BottomButton id="btn_sources" src_builtin="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" />
<BottomButton id="btn_cards" src_builtin="dashboard/cpu.svg" translation="AUDIO.CARDS" />
<BottomButton id="btn_sinks" src_builtin="dashboard/volume.svg" translation="AUDIO.SPEAKERS" />
<BottomButton id="btn_sources" src_builtin="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" />
<BottomButton id="btn_cards" src_builtin="dashboard/cpu.svg" translation="AUDIO.CARDS" />
</div>
</div>
</elements>
</layout>
</layout>

View File

@ -0,0 +1,15 @@
<layout>
<template name="DialogBoxButton">
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"/>
</template>
<elements>
<div flex_direction="column" align_items="center" justify_content="center" width="100%" gap="32">
<label id="label_message" size="18" weight="bold"/>
<div id="buttons" gap="8">
<!-- filled-in at runtime -->
</div>
</div>
</elements>
</layout>

View File

@ -0,0 +1,22 @@
<layout>
<include src="../t_separator.xml"/>
<template name="btn_close">
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="dashboard/check.svg"/>
</template>
<elements>
<div align_items="center" justify_content="center" width="100%">
<div id="content" flex_direction="column" gap="8" width="100%">
<label translation="DOWNLOADING_FILE" size="24" weight="bold"/>
<Separator/>
<label id="label_target_path" color="~color_text_translucent" />
<div gap="8" align_items="center">
<div id="loading_parent"/>
<label id="label_status"/>
</div>
</div>
</div>
</elements>
</layout>

View File

@ -1,7 +1,12 @@
<layout>
<include src="../t_group_box.xml" />
<elements>
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
<div flex_direction="column" padding_top="8" padding_bottom="8">
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
</div>
<div align_items="center" justify_content="center" gap="16">
<Button id="btn_prev" sprite_src_builtin="dashboard/arrow_left.svg" width="32" height="32" />
<label id="label_page" text="Page X" weight="bold" />
<Button id="btn_next" sprite_src_builtin="dashboard/arrow_right.svg" width="32" height="32" />
</div>
</elements>
</layout>

View File

@ -9,6 +9,7 @@
width="100%"
height="100%"
flex_direction="column"
position="absolute"
>
<!-- Top black bar -->
<rectangle
@ -32,14 +33,18 @@
</div>
</rectangle>
<!-- Content -->
<rectangle height="100%"
<rectangle
width="100%"
height="100%"
color="#010310fe"
color2="#051c55fc"
gradient="vertical"
padding="16"
id="content">
position="relative"
>
<div id="content" padding="16" width="100%" height="100%" position="absolute" overflow_y="scroll">
<!-- Content, filled-in at runtime -->
</div>
</rectangle>
</div>
</elements>
</layout>
</layout>

View File

@ -1,9 +0,0 @@
<layout>
<include src="../t_group_box.xml" />
<elements>
<rectangle macro="group_box" flex_direction="row" align_items="center">
<div id="list_parent" gap="8" flex_direction="column" flex_wrap="wrap" flex_grow="1" />
</rectangle>
</elements>
</layout>

View File

@ -0,0 +1,36 @@
<layout>
<!--
parameters:
"text"
"sprite"
ids:
"button"
-->
<template name="ResolutionButton">
<Button id="button" sprite_src_builtin="${sprite}" text="${text}"/>
</template>
<include src="../t_separator.xml"/>
<elements>
<div gap="8" flex_direction="column" min_width="100%" overflow_y="scroll">
<div gap="8" flex_direction="column" align_items="center">
<div id="resolution_buttons" gap="8" flex_direction="row">
<!-- filled-in at runtime -->
</div>
<image id="image" width="400" height="200" round="8" border="2" border_color="~color_accent"/>
<label id="label_author" weight="bold"/>
<label id="label_description" wrap="1"/>
<Separator/>
<!-- nerdy stuff below -->
<div gap="24" justify_content="center">
<label size="10" id="label_creation_date"/>
<label size="10" id="label_modification_date"/>
<label size="10" id="label_version"/>
</div>
</div>
</div>
</elements>
</layout>

View File

@ -0,0 +1,7 @@
<layout>
<elements>
<div id="list" gap="8" flex_wrap="wrap" align_self="baseline">
<!-- filled-in at runtime -->
</div>
</elements>
</layout>

View File

@ -0,0 +1,20 @@
<layout>
<include src="../t_group_box.xml" />
<template name="RunningGameCell">
<rectangle macro="group_box" flex_direction="row">
<Button id="btn_stop" sprite_src_builtin="dashboard/remove_circle.svg" tooltip="PROCESS.STOP" />
<Button id="btn_kill" sprite_src_builtin="dashboard/knife.svg" tooltip="PROCESS.FORCE_KILL" />
<label id="label_name" weight="bold" />
</rectangle>
</template>
<elements>
<div align_items="center" gap="8">
<Button id="btn_refresh" tooltip="REFRESH" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" />
<sprite src_builtin="dashboard/cpu.svg" width="24" height="24" />
<label translation="GAME_LIST.RUNNING_GAMES_LIST" />
</div>
<div id="list_parent" gap="8" />
</elements>
</layout>

View File

@ -0,0 +1,11 @@
<layout>
<elements>
<div flex_direction="column" gap="8" width="100%" align_items="center">
<div flex_direction="row" gap="4" align_self="end">
<Button id="btn_refresh" tooltip="RELOAD_FROM_DISK" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" />
<Button id="btn_download_skymaps" height="32" translation="APP_SETTINGS.BROWSE_ONLINE_CATALOG" sprite_src_builtin="dashboard/download.svg"/>
</div>
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" />
</div>
</elements>
</layout>

View File

@ -0,0 +1,40 @@
<layout>
<!--
ids:
"button"
"image_preview"
"label_title"
"label_desc"
-->
<template name="Cell">
<Button
id="button"
padding="8"
round="8"
flex_direction="column"
gap="4"
width="256"
align_items="center"
align_self="start">
<image id="image_preview" width="100%" height="128" round="6">
<!-- new_pass is required, because we need to render rectangles at the top of the image. Sorry. -->
<div new_pass="1" id="resolution_pips" gap="4" margin="6"/>
</image>
<label id="label_title" wrap="1" weight="bold"/>
<label id="label_author" wrap="1"/>
</Button>
</template>
<!--
params:
"color"
"text"
-->
<template name="ResolutionPip">
<rectangle color="${color}" padding_left="4" padding_right="4" padding_top="2" padding_bottom="2" round="3" align_self="start">
<label text="${text}" weight="bold" size="12" shadow="#000000" shadow_x="2" shadow_y="2"/>
</rectangle>
</template>
</layout>

View File

@ -1,153 +1,162 @@
{
"HOME_SCREEN": "Startbildschirm",
"MONADO_RUNTIME": "Monado-Laufzeitumgebung",
"APPLICATIONS": "Anwendungen",
"GAMES": "Spiele",
"SETTINGS": "Einstellungen",
"PROCESSES": "Prozesse",
"HELLO_USER": "Hallo, {USER}!",
"GENERAL_SETTINGS": "Allgemeine Einstellungen",
"APPLICATION_LAUNCHER": "Anwendung Launcher",
"APP_SETTINGS": {
"HIDE_USERNAME": "Benutzernamen ausblenden",
"OPAQUE_BACKGROUND": "Undurchsichtiger Hintergrund",
"WLX": {},
"LOOK_AND_FEEL": "Aussehen und Verhalten",
"HIDE_GRAB_HELP": "Greif-Hilfe ausblenden",
"ANIMATION_SPEED": "UI-Animationsgeschwindigkeit",
"ROUND_MULTIPLIER": "UI-Kantenrundung",
"USE_SKYBOX": "Skybox aktivieren",
"USE_PASSTHROUGH": "Passthrough aktivieren",
"CLOCK_12H": "12-Stunden-Uhr",
"FEATURES": "Funktionen",
"NOTIFICATIONS_ENABLED": "Benachrichtigungen aktivieren",
"NOTIFICATIONS_SOUND_ENABLED": "Benachrichtigungstöne",
"KEYBOARD_SOUND_ENABLED": "Tastengeräusche",
"SPACE_DRAG_MULTIPLIER": "Raum-Drag-Multiplikator",
"SPACE_DRAG_UNLOCKED": "Erlaube Space-Drag auf allen Achsen",
"SPACE_ROTATE_UNLOCKED": "Erlaube Drehungen in allen Achsen",
"BLOCK_GAME_INPUT": "Spieleingabe blockieren",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignoriere Watch beim Blockieren der Eingabe",
"CONTROLS": "Steuerung",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mausbewegung bei Triggerberührung",
"LEFT_HANDED_MOUSE": "Linkshändige Maus",
"ALLOW_SLIDING": "Stabinteraktion während des Greifens",
"INVERT_SCROLL_DIRECTION_X": "Horizontale Scrollrichtung umkehren",
"INVERT_SCROLL_DIRECTION_Y": "Vertikale Bildlaufrichtung umkehren",
"SCROLL_SPEED": "Scrollgeschwindigkeit",
"LONG_PRESS_DURATION": "Dauer für lange Drückvorgänge",
"POINTER_LERP_FACTOR": "Zeigerglättung",
"XR_CLICK_SENSITIVITY": "XR-Klicksensitivität",
"XR_CLICK_SENSITIVITY_RELEASE": "XR-Loslassempfindlichkeit",
"CLICK_FREEZE_TIME_MS": "Klick-Freeze-Zeit (ms)",
"MISC": "Verschiedenes",
"XWAYLAND_BY_DEFAULT": "Standardmäßig Apps im Kompatibilitätsmodus ausführen",
"UPRIGHT_SCREEN_FIX": "Bildschirm-Drehkorrektur",
"DOUBLE_CURSOR_FIX": "Doppelter Cursor Fix",
"SCREEN_RENDER_DOWN": "Bildschirm bei niedrigerer Auflösung rendern",
"UPRIGHT_SCREEN_FIX_HELP": "Behebt hochstehende Bildschirme auf einigen Desktops",
"DOUBLE_CURSOR_FIX_HELP": "Aktivieren Sie dies, wenn Sie 2 Cursor sehen",
"XR_CLICK_SENSITIVITY_HELP": "Analoge Trigger-Empfindlichkeit",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Muss niedriger als Klick sein",
"CLICK_FREEZE_TIME_MS_HELP": "Hilft bei der Präzision von Doppelklicks",
"LEFT_HANDED_MOUSE_HELP": "Verwenden Sie diese Option, wenn die Maustasten vertauscht sind",
"BLOCK_GAME_INPUT_HELP": "Blockiert alle Eingaben, wenn ein Overlay angefahren wird",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Blockiere die Eingabe nicht, wenn die Überwachung aktiviert ist",
"USE_SKYBOX_HELP": "Zeige einen Skybox, wenn keine Szenen-App oder Passthrough vorhanden ist",
"USE_PASSTHROUGH_HELP": "Aktiviere Passthrough, falls die XR-Laufzeitumgebung dies unterstützt",
"SCREEN_RENDER_DOWN_HELP": "Hilft bei Aliasing auf hochauflösenden Bildschirmen",
"SETS_ON_WATCH": "Sets auf der Watch",
"TROUBLESHOOTING": "Fehlerbehebung",
"CLEAR_SAVED_STATE": "Gespeicherten Zustand löschen",
"CLEAR_PIPEWIRE_TOKENS": "PipeWire Tokens löschen",
"DELETE_ALL_CONFIGS": "Konfiguration löschen",
"RESTART_SOFTWARE": "Software neu starten",
"CLEAR_SAVED_STATE_HELP": "Sets und Overlay-Positionen zurücksetzen",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Bildschirmauswahl beim nächsten Start abfragen",
"DELETE_ALL_CONFIGS_HELP": "Entfernen Sie alle Konfigurationsdateien aus conf.d",
"RESTART_SOFTWARE_HELP": "Einstellungen anwenden, die einen Neustart erfordern",
"CAPTURE_METHOD": "Wayland-Bildschirmaufnahme",
"CAPTURE_METHOD_HELP": "Versuchen Sie, dies zu ändern, wenn Sie\nschwarze oder fehlerhafte Bildschirme erleben",
"KEYBOARD_MIDDLE_CLICK": "Keyboard-Mittelklick",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifikator bei Eingabe mit violettem Laser",
"OPTION": {
"AUTO": "Automatisch",
"AUTO_HELP": "Bildschirmkopie GPU falls unterstützt,\nansonsten PipeWire GPU.",
"PIPEWIRE_HELP": "Schnelle GPU-Erfassung,\nStandard auf allen Desktops.",
"PW_FALLBACK_HELP": "Langsame Methode mit hoher CPU-Auslastung.\nVersuchen Sie es, falls PipeWire GPU nicht funktioniert.",
"SCREENCOPY_GPU_HELP": "Schnell, keine Bildschirmfreigabe-Popups.\nFunktioniert mit: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Langsam, keine Bildschirmfreigabe-Popups.\nFunktioniert mit: Hyprland, Niri, River, Sway"
},
"AUTOSTART_APPS": "Anwendungen, die beim Start ausgeführt werden sollen"
},
"HELLO": "Hallo!",
"AUDIO": {
"VOLUME": "Lautstärke",
"SETTINGS": "Audioeinstellungen",
"AUTO_SWITCH_TO_VR_AUDIO": "Automatisch auf VR-Audio umschalten",
"SPEAKERS": "Lautsprecher",
"MICROPHONES": "Mikrofone",
"CARDS": "Sound-Karten",
"SELECT_AUDIO_CARD_PROFILE": "Wählen Sie das Sound-Kartenprofil",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Keine VR-Lautsprecher gefunden. Schalten Sie diese manuell um.",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Kein VR-Mikrofon gefunden. Schalten Sie es manuell um.",
"FAILED_TO_SWITCH_MICROPHONE": "Fehler beim Wechseln des Mikrofons",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofon erfolgreich umgeschaltet",
"SPEAKERS_SET_SUCCESSFULLY": "Lautsprecher erfolgreich umgeschaltet"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Playspace neu zentrieren"
},
"LIST_OF_PROCESSES": "Prozessliste",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Auflösung"
},
"WIDTH": "Breite",
"HEIGHT": "Höhe",
"HIDE": "Verbergen",
"REMOVE": "Entfernen",
"SHOW": "Anzeigen",
"PROCESS_LIST": {
"NO_PROCESSES_FOUND": "Keine Prozesse gefunden",
"LOCATED_ON": "auf",
"TERMINATE_PROCESS_NAMED_X": "Prozess \"{PROCESS_NAME}\" beenden"
},
"FAILED_TO_LAUNCH_APPLICATION": "Fehler beim Starten der Anwendung:",
"NO_WINDOWS_FOUND": "Keine Fenster gefunden",
"WINDOW_OPTIONS": "Fensteroptionen",
"APPLICATION_STARTED": "Anwendung gestartet",
"LIST_OF_WINDOWS": "Fensterliste",
"CLOSE_WINDOW": "Fenster schließen",
"GAME_LIST": {
"NO_GAMES_FOUND": "Keine Spiele gefunden"
},
"TERMINATE_PROCESS": "Prozess beenden",
"GAME_LAUNCHED": "Spiel gestartet",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "Nativer Modus",
"CAGE": "Kompatibilitätsmodus (Cage)"
},
"RES_TITLE": "Auflösung",
"ASPECT_TITLE": "Seitenverhältnis",
"ASPECT": {
"WIDE": "Breit",
"SEMI_WIDE": "Halb-breit",
"SQUARE": "Quadratisch",
"SEMI_TALL": "Halbhoch",
"TALL": "Hoch"
},
"POS_TITLE": "Positionierung",
"POS": {
"FLOATING": "Schwebend",
"ANCHORED": "Verankert",
"STATIC": "Statisch",
"FLOATING_HELP": "Bewegt sich unabhängig, zentriert sich bei Anzeige neu.",
"ANCHORED_HELP": "Bleibt relativ zur Mittelmarkierung an Ort und Stelle.",
"STATIC_HELP": "Keine Set zugeordnet. Wird nicht zentriert."
},
"AUTOSTART": "Automatisch beim Start ausführen",
"LAUNCH": "Starten"
},
"DISPLAY_BRIGHTNESS": "Bildschirmhelligkeit"
}
"HOME_SCREEN": "Startbildschirm",
"MONADO_RUNTIME": "Monado-Laufzeitumgebung",
"APPLICATIONS": "Anwendungen",
"GAMES": "Spiele",
"SETTINGS": "Einstellungen",
"HELLO_USER": "Hallo, {USER}!",
"GENERAL_SETTINGS": "Allgemeine Einstellungen",
"APPLICATION_LAUNCHER": "Anwendung Launcher",
"APP_SETTINGS": {
"HIDE_USERNAME": "Benutzernamen ausblenden",
"OPAQUE_BACKGROUND": "Undurchsichtiger Hintergrund",
"LOOK_AND_FEEL": "Aussehen und Verhalten",
"HIDE_GRAB_HELP": "Greif-Hilfe ausblenden",
"ANIMATION_SPEED": "UI-Animationsgeschwindigkeit",
"ROUND_MULTIPLIER": "UI-Kantenrundung",
"USE_SKYBOX": "Skybox aktivieren",
"USE_PASSTHROUGH": "Passthrough aktivieren",
"CLOCK_12H": "12-Stunden-Uhr",
"FEATURES": "Funktionen",
"NOTIFICATIONS_ENABLED": "Benachrichtigungen aktivieren",
"NOTIFICATIONS_SOUND_ENABLED": "Benachrichtigungstöne",
"KEYBOARD_SOUND_ENABLED": "Tastengeräusche",
"SPACE_DRAG_MULTIPLIER": "Raum-Drag-Multiplikator",
"SPACE_DRAG_UNLOCKED": "Erlaube Space-Drag auf allen Achsen",
"SPACE_ROTATE_UNLOCKED": "Erlaube Drehungen in allen Achsen",
"BLOCK_GAME_INPUT": "Spieleingabe blockieren",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignoriere Watch beim Blockieren der Eingabe",
"CONTROLS": "Steuerung",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mausbewegung bei Triggerberührung",
"LEFT_HANDED_MOUSE": "Linkshändige Maus",
"ALLOW_SLIDING": "Stabinteraktion während des Greifens",
"INVERT_SCROLL_DIRECTION_X": "Horizontale Scrollrichtung umkehren",
"INVERT_SCROLL_DIRECTION_Y": "Vertikale Bildlaufrichtung umkehren",
"SCROLL_SPEED": "Scrollgeschwindigkeit",
"LONG_PRESS_DURATION": "Dauer für lange Drückvorgänge",
"POINTER_LERP_FACTOR": "Zeigerglättung",
"CLICK_FREEZE_TIME_MS": "Klick-Freeze-Zeit (ms)",
"MISC": "Verschiedenes",
"XWAYLAND_BY_DEFAULT": "Standardmäßig Apps im Kompatibilitätsmodus ausführen",
"UPRIGHT_SCREEN_FIX": "Bildschirm-Drehkorrektur",
"DOUBLE_CURSOR_FIX": "Doppelter Cursor Fix",
"SCREEN_RENDER_DOWN": "Bildschirm bei niedrigerer Auflösung rendern",
"UPRIGHT_SCREEN_FIX_HELP": "Behebt hochstehende Bildschirme auf einigen Desktops",
"DOUBLE_CURSOR_FIX_HELP": "Aktivieren Sie dies, wenn Sie 2 Cursor sehen",
"CLICK_FREEZE_TIME_MS_HELP": "Hilft bei der Präzision von Doppelklicks",
"LEFT_HANDED_MOUSE_HELP": "Verwenden Sie diese Option, wenn die Maustasten vertauscht sind",
"BLOCK_GAME_INPUT_HELP": "Blockiert alle Eingaben, wenn ein Overlay angefahren wird",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Blockiere die Eingabe nicht, wenn die Überwachung aktiviert ist",
"USE_SKYBOX_HELP": "Zeige einen Skybox, wenn keine Szenen-App oder Passthrough vorhanden ist",
"USE_PASSTHROUGH_HELP": "Aktiviere Passthrough, falls die XR-Laufzeitumgebung dies unterstützt",
"SCREEN_RENDER_DOWN_HELP": "Hilft bei Aliasing auf hochauflösenden Bildschirmen",
"SETS_ON_WATCH": "Sets auf der Watch",
"TROUBLESHOOTING": "Fehlerbehebung",
"CLEAR_SAVED_STATE": "Gespeicherten Zustand löschen",
"CLEAR_PIPEWIRE_TOKENS": "PipeWire Tokens löschen",
"DELETE_ALL_CONFIGS": "Konfiguration löschen",
"RESTART_SOFTWARE": "Software neu starten",
"CLEAR_SAVED_STATE_HELP": "Sets und Overlay-Positionen zurücksetzen",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Bildschirmauswahl beim nächsten Start abfragen",
"DELETE_ALL_CONFIGS_HELP": "Entfernen Sie alle Konfigurationsdateien aus conf.d",
"RESTART_SOFTWARE_HELP": "Einstellungen anwenden, die einen Neustart erfordern",
"CAPTURE_METHOD": "Wayland-Bildschirmaufnahme",
"CAPTURE_METHOD_HELP": "Versuchen Sie, dies zu ändern, wenn Sie\nschwarze oder fehlerhafte Bildschirme erleben",
"KEYBOARD_MIDDLE_CLICK": "Keyboard-Mittelklick",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifikator bei Eingabe mit violettem Laser",
"OPTION": {
"AUTO": "Automatisch",
"AUTO_HELP": "Bildschirmkopie GPU falls unterstützt,\nansonsten PipeWire GPU.",
"PIPEWIRE_HELP": "Schnelle GPU-Erfassung,\nStandard auf allen Desktops.",
"PW_FALLBACK_HELP": "Langsame Methode mit hoher CPU-Auslastung.\nVersuchen Sie es, falls PipeWire GPU nicht funktioniert.",
"SCREENCOPY_GPU_HELP": "Schnell, keine Bildschirmfreigabe-Popups.\nFunktioniert mit: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Langsam, keine Bildschirmfreigabe-Popups.\nFunktioniert mit: Hyprland, Niri, River, Sway",
"NONE": "Keine",
"HMD_PINCH": "HMD + Kneifen",
"EYE_PINCH": "Auge + Kneifen",
"EYE_ONLY": "Nur Auge",
"HMD_ONLY": "Nur HMD"
},
"AUTOSTART_APPS": "Anwendungen, die beim Start ausgeführt werden sollen",
"HANDSFREE_POINTER": "Freihändige Modus",
"HANDSFREE_POINTER_HELP": "Eingabe, die bei Bewegung\nder Controller verwendet wird, wenn diese nicht verfügbar sind.\nLinkes Kneifen greift, rechtes klickt.",
"UI_GRADIENT_INTENSITY": "UI-Verlaufsintensität",
"RESET_PLAYSPACE": "Spielbereich zurücksetzen",
"RESET_PLAYSPACE_HELP": "Den Abstand des Spielbereichs zurücksetzen.",
"BLOCK_POSES_ON_KBD_INTERACTION": "Posen beim Interagieren mit der Tastatur blockieren",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Verhindert, dass das Spiel Posen empfängt, wenn die Tastatur angefahren wird und „Spieleingabe blockieren“ aktiviert ist",
"LANGUAGE": "Sprache",
"REQUIRES_RESTART": "Erfordert Neustart",
"GRID_OPACITY": "Bodenraster-Undurchsichtigkeit",
"GRID_OPACITY_HELP": "Undurchsichtigkeit des Bodenrasters, wenn der Skybox aktiviert ist"
},
"HELLO": "Hallo!",
"AUDIO": {
"VOLUME": "Lautstärke",
"SETTINGS": "Audioeinstellungen",
"AUTO_SWITCH_TO_VR_AUDIO": "Automatisch auf VR-Audio umschalten",
"SPEAKERS": "Lautsprecher",
"MICROPHONES": "Mikrofone",
"CARDS": "Sound-Karten",
"SELECT_AUDIO_CARD_PROFILE": "Wählen Sie das Sound-Kartenprofil",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Keine VR-Lautsprecher gefunden. Schalten Sie diese manuell um.",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Kein VR-Mikrofon gefunden. Schalten Sie es manuell um.",
"FAILED_TO_SWITCH_MICROPHONE": "Fehler beim Wechseln des Mikrofons",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofon erfolgreich umgeschaltet",
"SPEAKERS_SET_SUCCESSFULLY": "Lautsprecher erfolgreich umgeschaltet"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Playspace neu zentrieren"
},
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Auflösung"
},
"WIDTH": "Breite",
"HEIGHT": "Höhe",
"HIDE": "Verbergen",
"REMOVE": "Entfernen",
"SHOW": "Anzeigen",
"FAILED_TO_LAUNCH_APPLICATION": "Fehler beim Starten der Anwendung:",
"APPLICATION_STARTED": "Anwendung gestartet",
"CLOSE_WINDOW": "Fenster schließen",
"GAME_LIST": {
"NO_GAMES_FOUND": "Keine Spiele gefunden",
"RUNNING_GAMES_LIST": "Liste der laufenden Spiele"
},
"TERMINATE_PROCESS": "Prozess beenden",
"GAME_LAUNCHED": "Spiel gestartet",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "Nativer Modus",
"CAGE": "Kompatibilitätsmodus (Cage)"
},
"RES_TITLE": "Auflösung",
"ASPECT_TITLE": "Seitenverhältnis",
"ASPECT": {
"WIDE": "Breit",
"SEMI_WIDE": "Halb-breit",
"SQUARE": "Quadratisch",
"SEMI_TALL": "Halbhoch",
"TALL": "Hoch"
},
"POS_TITLE": "Positionierung",
"POS": {
"FLOATING": "Schwebend",
"ANCHORED": "Verankert",
"STATIC": "Statisch",
"FLOATING_HELP": "Bewegt sich unabhängig, zentriert sich bei Anzeige neu.",
"ANCHORED_HELP": "Bleibt relativ zur Mittelmarkierung an Ort und Stelle.",
"STATIC_HELP": "Keine Set zugeordnet. Wird nicht zentriert."
},
"AUTOSTART": "Automatisch beim Start ausführen",
"LAUNCH": "Starten"
},
"DISPLAY_BRIGHTNESS": "Bildschirmhelligkeit",
"PROCESS_LIST": "Prozessliste",
"REFRESH": "Aktualisieren",
"PROCESS": {
"STOP": "Stopp",
"FORCE_KILL": "Erzwinge Beenden"
},
"DEBUG_INFO": "Debug-Informationen"
}

View File

@ -31,12 +31,18 @@
"APP_SETTINGS": {
"ALLOW_SLIDING": "Stick interaction during grab",
"ANIMATION_SPEED": "UI Animation speed",
"AUTOSTART_APPS": "Apps to run on startup",
"BLOCK_GAME_INPUT": "Block game input",
"BLOCK_GAME_INPUT_HELP": "Blocks all input when an overlay is hovered",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignore watch when blocking input",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Do not block input when watch is hovered",
"BLOCK_POSES_ON_KBD_INTERACTION": "Block poses when interacting with keyboard",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blocks the game from receiving poses when the keyboard is hovered and 'Block game input' is enabled",
"BROWSE_ONLINE_CATALOG": "Browse online catalog...",
"BROWSE_SKYMAPS": "Browse skymaps",
"CAPTURE_METHOD": "Wayland screen capture",
"CAPTURE_METHOD_HELP": "Try changing this if you are\nexperiencing black or glitchy screens",
"COLOR_KEYING": "Color keying",
"CLEAR_PIPEWIRE_TOKENS": "Clear PipeWire tokens",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Prompt for screen selection on next start",
"CLEAR_SAVED_STATE": "Clear saved state",
@ -49,8 +55,11 @@
"DELETE_ALL_CONFIGS_HELP": "Remove all configuration files from conf.d",
"DOUBLE_CURSOR_FIX": "Double cursor fix",
"DOUBLE_CURSOR_FIX_HELP": "Enable this if you see 2 cursors",
"ENABLED": "Enabled",
"FEATURES": "Features",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mouse move on trigger touch",
"HANDSFREE_POINTER": "Handsfree mode",
"HANDSFREE_POINTER_HELP": "Input to use when motion\ncontrollers are unavailable.\nLeft pinch is grab, right is click.",
"HIDE_GRAB_HELP": "Hide grab help",
"HIDE_USERNAME": "Hide username",
"INVERT_SCROLL_DIRECTION_X": "Invert horizontal scroll direction",
@ -58,6 +67,7 @@
"KEYBOARD_MIDDLE_CLICK": "Keyboard middle click",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier to use when typing\nwith purple laser",
"KEYBOARD_SOUND_ENABLED": "Keyboard sounds",
"LANGUAGE": "Language",
"LEFT_HANDED_MOUSE": "Left-handed mouse",
"LEFT_HANDED_MOUSE_HELP": "Use this if mouse buttons are swapped",
"LONG_PRESS_DURATION": "Long press duration",
@ -65,39 +75,66 @@
"MISC": "Miscellaneous",
"NOTIFICATIONS_ENABLED": "Enable notifications",
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
"NOT_SUPPORTED": "Not supported",
"NO_SKYMAPS_FOUND": "No skymaps found",
"OPAQUE_BACKGROUND": "Opaque background",
"OPTION": {
"AUTO": "Automatic",
"AUTO_HELP": "ScreenCopy GPU if supported,\notherwise PipeWire GPU.",
"EYE_ONLY": "Eye only",
"EYE_PINCH": "Eye + pinch",
"HMD_ONLY": "HMD only",
"HMD_PINCH": "HMD + pinch",
"NONE": "None",
"PIPEWIRE_HELP": "Fast GPU capture,\nstandard on all desktops.",
"PW_FALLBACK_HELP": "Slow method with high CPU usage.\nTry in case PipeWire GPU doesn't work",
"SCREENCOPY_GPU_HELP": "Fast, no screen share popups.\nWorks on: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Slow, no screen share popups.\nWorks on: Hyprland, Niri, River, Sway"
},
"POINTER_LERP_FACTOR": "Pointer smoothing",
"REQUIRES_RESTART": "Requires restart",
"RESET_PLAYSPACE": "Reset playspace",
"RESET_PLAYSPACE_HELP": "Clear the stage space offset.",
"RESTART_SOFTWARE": "Restart software",
"RESTART_SOFTWARE_HELP": "Apply settings that require a restart",
"ROUND_MULTIPLIER": "UI Edge roundness",
"SCREEN_RENDER_DOWN": "Render screen at lower resolution",
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
"SCROLL_SPEED": "Scroll speed",
"SELECT_VARIANT": "Select variant",
"ENABLE_WATCH": "Enable watch",
"SETS_ON_WATCH": "Sets on watch",
"SKYBOX": "Skybox",
"SKYMAP_ALREADY_DOWNLOADED": "This skymap is already downloaded. Select desired action.",
"SPACE_DRAG": "Space drag",
"SPACE_DRAG_MULTIPLIER": "Space drag multiplier",
"SPACE_DRAG_UNLOCKED": "Allow space drag on all axes",
"SPACE_GRAVITY_DAMPING": "Damping",
"SPACE_GRAVITY_DAMPING_HELP": "Artificial drag to slow down movement. 0.1 - high drag, 1.0 - no drag",
"SPACE_GRAVITY_FLING_STRENGTH": "Fling strength",
"SPACE_GRAVITY_FLING_STRENGTH_HELP": "Intensity multiplier of gravitational launch force after space-drag.\n0.0 - no movement at all, 2.0 - double intensity",
"SPACE_GRAVITY_GRAVITY": "Gravity",
"SPACE_GRAVITY_GRAVITY_HELP": "Amount of downwards force. 0.0 - no gravity",
"SPACE_GRAVITY_GROUND_FRICTION": "Ground friction",
"SPACE_GRAVITY_GROUND_FRICTION_HELP": "Amount of friction slowing you down if you're touching the ground.\n0.0 - no friction (just like on ice), 1.0 - rough surface",
"SPACE_GRAVITY_FLOOR_HEIGHT": "Floor height",
"SPACE_GRAVITY_FLOOR_HEIGHT_HELP": "The Y position where the floor is. Gravity stops when you reach this height.",
"SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes",
"TROUBLESHOOTING": "Troubleshooting",
"UI_GRADIENT_INTENSITY": "UI Gradient intensity",
"UPRIGHT_SCREEN_FIX": "Upright screen fix",
"UPRIGHT_SCREEN_FIX_HELP": "Fixes upright screens on some desktops",
"USE_PASSTHROUGH": "Enable passthrough",
"USE_PASSTHROUGH_HELP": "Allow passthrough if the XR runtime supports it",
"USE_SKYBOX": "Enable skybox",
"USE_SKYBOX_HELP": "Show a skybox if there's no scene app or passthrough",
"XR_CLICK_SENSITIVITY": "XR click sensitivity",
"XR_CLICK_SENSITIVITY_HELP": "Analog trigger sensitivity",
"XR_CLICK_SENSITIVITY_RELEASE": "XR release sensitivity",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Must be lower than click",
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default",
"AUTOSTART_APPS": "Apps to run on startup"
"WATCH_VIEW_ANGLE": "Watch view angles",
"WATCH_VIEW_ANGLE_HELP": "Control how the watch fades away",
"GRID_OPACITY": "Floor grid opacity",
"GRID_OPACITY_HELP": "Opacity of the floor grid when the skybox is enabled",
"XR_CLICK_SENSITIVITY": "XR trigger sensitivity",
"XR_CLICK_SENSITIVITY_HELP": "Press and release values for analog triggers",
"XWAYLAND_BY_DEFAULT": "Run apps in Compatibility mode by default"
},
"APPLICATION_LAUNCHER": "Application launcher",
"APPLICATION_STARTED": "Application started",
@ -117,11 +154,17 @@
"VOLUME": "Volume"
},
"CLOSE_WINDOW": "Close window",
"CREATION_DATE": "Creation date",
"DEBUG_INFO": "Debug info",
"DISPLAY_BRIGHTNESS": "Display brightness",
"DOWNLOADER": "Downloader",
"DOWNLOAD_AGAIN": "Download again",
"DOWNLOADING_FILE": "Downloading file...",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
"GAME_LAUNCHED": "Game launched",
"GAME_LIST": {
"NO_GAMES_FOUND": "No games found"
"NO_GAMES_FOUND": "No games found",
"RUNNING_GAMES_LIST": "List of running games"
},
"GAMES": "Games",
"GENERAL_SETTINGS": "General settings",
@ -130,23 +173,25 @@
"HELLO_USER": "Hello, {USER}!",
"HIDE": "Hide",
"HOME_SCREEN": "Home",
"LIST_OF_PROCESSES": "Process list",
"LIST_OF_WINDOWS": "Window list",
"MODIFICATION_DATE": "Modification date",
"MONADO_RUNTIME": "Monado runtime",
"NO_WINDOWS_FOUND": "No windows found",
"LOADING": "Loading...",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
},
"PROCESS_LIST": {
"LOCATED_ON": "on",
"NO_PROCESSES_FOUND": "No processes found",
"TERMINATE_PROCESS_NAMED_X": "Terminate process \"{PROCESS_NAME}\""
"PROCESS": {
"FORCE_KILL": "Force-kill",
"STOP": "Stop"
},
"PROCESSES": "Processes",
"PROCESS_LIST": "Process list",
"REFRESH": "Refresh",
"REMOVE": "Remove",
"APPLY": "Apply",
"RELOAD_FROM_DISK": "Reload from disk",
"SETTINGS": "Settings",
"SHOW": "Show",
"TARGET_PATH": "Target path",
"TERMINATE_PROCESS": "Terminate process",
"WIDTH": "Width",
"WINDOW_OPTIONS": "Window options"
"VERSION": "Version",
"WIDTH": "Width"
}

View File

@ -1,153 +1,162 @@
{
"HOME_SCREEN": "Inicio",
"MONADO_RUNTIME": "Monado tiempo de ejecución",
"APPLICATIONS": "Aplicaciones",
"GAMES": "Juegos",
"SETTINGS": "Ajustes",
"PROCESSES": "Procesos",
"HELLO_USER": "¡Hola, {USER}!",
"GENERAL_SETTINGS": "Ajustes generales",
"APPLICATION_LAUNCHER": "Lanzador de aplicaciones",
"APP_SETTINGS": {
"HIDE_USERNAME": "Ocultar nombre de usuario",
"OPAQUE_BACKGROUND": "Fondo opaco",
"WLX": {},
"LOOK_AND_FEEL": "Apariencia y estilo",
"HIDE_GRAB_HELP": "Ocultar ayuda para agarrar",
"ANIMATION_SPEED": "Velocidad de animación de la IU",
"ROUND_MULTIPLIER": "Redondeo de bordes de la IU",
"USE_SKYBOX": "Activar skybox",
"USE_PASSTHROUGH": "Activar passthrough",
"CLOCK_12H": "Reloj de 12 horas",
"FEATURES": "Funciones",
"NOTIFICATIONS_ENABLED": "Habilitar notificaciones",
"NOTIFICATIONS_SOUND_ENABLED": "Sonidos de notificación",
"KEYBOARD_SOUND_ENABLED": "Sonidos del teclado",
"SPACE_DRAG_MULTIPLIER": "Multiplicador de arrastre espacial",
"SPACE_DRAG_UNLOCKED": "Permitir arrastre del espacio en todos los ejes",
"SPACE_ROTATE_UNLOCKED": "Permitir rotación espacial en todos los ejes",
"BLOCK_GAME_INPUT": "Bloquear entrada del juego",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignorar watch al bloquear la entrada",
"CONTROLS": "Controles",
"FOCUS_FOLLOWS_MOUSE_MODE": "El movimiento del ratón en el disparador",
"LEFT_HANDED_MOUSE": "Ratón para zurdos",
"ALLOW_SLIDING": "Interacción con el stick durante el agarre",
"INVERT_SCROLL_DIRECTION_X": "Invertir dirección de desplazamiento horizontal",
"INVERT_SCROLL_DIRECTION_Y": "Invertir la dirección del desplazamiento vertical",
"SCROLL_SPEED": "Velocidad de desplazamiento",
"LONG_PRESS_DURATION": "Duración de la pulsación larga",
"POINTER_LERP_FACTOR": "Suavizado del puntero",
"XR_CLICK_SENSITIVITY": "Sensibilidad del clic XR",
"XR_CLICK_SENSITIVITY_RELEASE": "Sensibilidad de liberación de OpenXR",
"CLICK_FREEZE_TIME_MS": "Tiempo de congelación al hacer clic (ms)",
"MISC": "Miscelánea",
"XWAYLAND_BY_DEFAULT": "Ejecutar aplicaciones en modo de compatibilidad por defecto",
"UPRIGHT_SCREEN_FIX": "Corrección de pantalla vertical",
"DOUBLE_CURSOR_FIX": "Solución de doble cursor",
"SCREEN_RENDER_DOWN": "Renderizar pantalla a menor resolución",
"UPRIGHT_SCREEN_FIX_HELP": "Corrige pantallas en posición vertical en algunos escritorios",
"DOUBLE_CURSOR_FIX_HELP": "Habilita esto si ves 2 cursores",
"XR_CLICK_SENSITIVITY_HELP": "Sensibilidad del gatillo analógico",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Debe ser inferior a 'click'",
"CLICK_FREEZE_TIME_MS_HELP": "Ayuda con la precisión de los dobles clics",
"LEFT_HANDED_MOUSE_HELP": "Utilice esto si los botones del ratón están intercambiados",
"BLOCK_GAME_INPUT_HELP": "Bloquea toda la entrada cuando se pasa el cursor sobre un overlay",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "No bloquear la entrada cuando el cursor está sobre la ventana",
"USE_SKYBOX_HELP": "Mostrar una skybox si no hay una aplicación de escena o passthrough",
"USE_PASSTHROUGH_HELP": "Permitir passthrough si el entorno de ejecución XR lo admite",
"SCREEN_RENDER_DOWN_HELP": "Ayuda a reducir el aliasing en pantallas de alta resolución",
"SETS_ON_WATCH": "Conjuntos en el reloj",
"TROUBLESHOOTING": "Solución de problemas",
"CLEAR_SAVED_STATE": "Borrar estado guardado",
"CLEAR_PIPEWIRE_TOKENS": "Limpiar tokens de PipeWire",
"DELETE_ALL_CONFIGS": "Borrar configuración",
"RESTART_SOFTWARE": "Reiniciar software",
"CLEAR_SAVED_STATE_HELP": "Restablecer sets y posiciones de superposición",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Solicitar la selección de pantalla al iniciar la próxima vez",
"DELETE_ALL_CONFIGS_HELP": "Eliminar todos los archivos de configuración de conf.d",
"RESTART_SOFTWARE_HELP": "Aplicar la configuración que requiere un reinicio",
"CAPTURE_METHOD": "Captura de pantalla de Wayland",
"CAPTURE_METHOD_HELP": "Intente cambiar esta opción si\nexperimenta pantallas negras o con fallos",
"KEYBOARD_MIDDLE_CLICK": "Clic del botón central del teclado",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modificador para usar al escribir\ncon láser púrpura",
"OPTION": {
"AUTO": "Automático",
"AUTO_HELP": "Copiar pantalla usando la GPU si es compatible,\nde lo contrario, GPU de PipeWire.",
"PIPEWIRE_HELP": "Captura de GPU rápida,\nestándar en todos los escritorios.",
"PW_FALLBACK_HELP": "Método lento con alto uso de CPU.\nPruébalo si PipeWire GPU no funciona",
"SCREENCOPY_GPU_HELP": "Rápido, sin ventanas emergentes de uso compartido de pantalla.\nFunciona en: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Lento, sin ventanas emergentes de uso compartido de pantalla.\nFunciona en: Hyprland, Niri, River, Sway"
},
"AUTOSTART_APPS": "Aplicaciones a ejecutar al inicio"
},
"HELLO": "¡Hola!",
"AUDIO": {
"VOLUME": "Volumen",
"SETTINGS": "Configuración de audio",
"AUTO_SWITCH_TO_VR_AUDIO": "Conmutar automáticamente al audio VR",
"SPEAKERS": "Altavoces",
"MICROPHONES": "Micrófonos",
"CARDS": "Tarjetas",
"SELECT_AUDIO_CARD_PROFILE": "Seleccionar perfil de tarjeta de audio",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "No se encontraron altavoces VR. Actívelos manualmente.",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "No se encontró micrófono VR. Actívelo manualmente.",
"FAILED_TO_SWITCH_MICROPHONE": "No se pudo cambiar el micrófono",
"MICROPHONE_SET_SUCCESSFULLY": "Micrófono configurado correctamente",
"SPEAKERS_SET_SUCCESSFULLY": "Altavoces configurados correctamente"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Re-centrar espacio de juego"
},
"LIST_OF_PROCESSES": "Lista de procesos",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolución"
},
"WIDTH": "Ancho",
"HEIGHT": "Altura",
"HIDE": "Ocultar",
"REMOVE": "Eliminar",
"SHOW": "Mostrar",
"PROCESS_LIST": {
"NO_PROCESSES_FOUND": "No se encontraron procesos",
"LOCATED_ON": "en",
"TERMINATE_PROCESS_NAMED_X": "Terminar proceso \"{PROCESS_NAME}\""
},
"FAILED_TO_LAUNCH_APPLICATION": "No se pudo iniciar la aplicación:",
"NO_WINDOWS_FOUND": "No se encontraron ventanas",
"WINDOW_OPTIONS": "Opciones de ventana",
"APPLICATION_STARTED": "Aplicación iniciada",
"LIST_OF_WINDOWS": "Lista de ventanas",
"CLOSE_WINDOW": "Cerrar ventana",
"GAME_LIST": {
"NO_GAMES_FOUND": "No se encontraron juegos"
},
"TERMINATE_PROCESS": "Finalizar proceso",
"GAME_LAUNCHED": "Juego lanzado",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "Modo nativo",
"CAGE": "Modo de compatibilidad (Cage)"
},
"RES_TITLE": "Resolución",
"ASPECT_TITLE": "Aspecto",
"ASPECT": {
"WIDE": "Ancho",
"SEMI_WIDE": "Semi-ancho",
"SQUARE": "Cuadrado",
"SEMI_TALL": "Semi alto",
"TALL": "Alto"
},
"POS_TITLE": "Posicionamiento",
"POS": {
"FLOATING": "Flotante",
"ANCHORED": "Anclado",
"STATIC": "Estático",
"FLOATING_HELP": "Se mueve independientemente, se recentra al mostrarse.",
"ANCHORED_HELP": "Se mantiene en su lugar con respecto al marcador central.",
"STATIC_HELP": "No pertenece a ningún conjunto. No se recentra."
},
"AUTOSTART": "Ejecutar automáticamente al inicio",
"LAUNCH": "Iniciar"
},
"DISPLAY_BRIGHTNESS": "Brillo de la pantalla"
}
"HOME_SCREEN": "Inicio",
"MONADO_RUNTIME": "Monado tiempo de ejecución",
"APPLICATIONS": "Aplicaciones",
"GAMES": "Juegos",
"SETTINGS": "Ajustes",
"HELLO_USER": "¡Hola, {USER}!",
"GENERAL_SETTINGS": "Ajustes generales",
"APPLICATION_LAUNCHER": "Lanzador de aplicaciones",
"APP_SETTINGS": {
"HIDE_USERNAME": "Ocultar nombre de usuario",
"OPAQUE_BACKGROUND": "Fondo opaco",
"LOOK_AND_FEEL": "Apariencia y estilo",
"HIDE_GRAB_HELP": "Ocultar ayuda para agarrar",
"ANIMATION_SPEED": "Velocidad de animación de la IU",
"ROUND_MULTIPLIER": "Redondeo de bordes de la IU",
"USE_SKYBOX": "Activar skybox",
"USE_PASSTHROUGH": "Activar passthrough",
"CLOCK_12H": "Reloj de 12 horas",
"FEATURES": "Funciones",
"NOTIFICATIONS_ENABLED": "Habilitar notificaciones",
"NOTIFICATIONS_SOUND_ENABLED": "Sonidos de notificación",
"KEYBOARD_SOUND_ENABLED": "Sonidos del teclado",
"SPACE_DRAG_MULTIPLIER": "Multiplicador de arrastre espacial",
"SPACE_DRAG_UNLOCKED": "Permitir arrastre del espacio en todos los ejes",
"SPACE_ROTATE_UNLOCKED": "Permitir rotación espacial en todos los ejes",
"BLOCK_GAME_INPUT": "Bloquear entrada del juego",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignorar watch al bloquear la entrada",
"CONTROLS": "Controles",
"FOCUS_FOLLOWS_MOUSE_MODE": "El movimiento del ratón en el disparador",
"LEFT_HANDED_MOUSE": "Ratón para zurdos",
"ALLOW_SLIDING": "Interacción con el stick durante el agarre",
"INVERT_SCROLL_DIRECTION_X": "Invertir dirección de desplazamiento horizontal",
"INVERT_SCROLL_DIRECTION_Y": "Invertir la dirección del desplazamiento vertical",
"SCROLL_SPEED": "Velocidad de desplazamiento",
"LONG_PRESS_DURATION": "Duración de la pulsación larga",
"POINTER_LERP_FACTOR": "Suavizado del puntero",
"CLICK_FREEZE_TIME_MS": "Tiempo de congelación al hacer clic (ms)",
"MISC": "Miscelánea",
"XWAYLAND_BY_DEFAULT": "Ejecutar aplicaciones en modo de compatibilidad por defecto",
"UPRIGHT_SCREEN_FIX": "Corrección de pantalla vertical",
"DOUBLE_CURSOR_FIX": "Solución de doble cursor",
"SCREEN_RENDER_DOWN": "Renderizar pantalla a menor resolución",
"UPRIGHT_SCREEN_FIX_HELP": "Corrige pantallas en posición vertical en algunos escritorios",
"DOUBLE_CURSOR_FIX_HELP": "Habilita esto si ves 2 cursores",
"CLICK_FREEZE_TIME_MS_HELP": "Ayuda con la precisión de los dobles clics",
"LEFT_HANDED_MOUSE_HELP": "Utilice esto si los botones del ratón están intercambiados",
"BLOCK_GAME_INPUT_HELP": "Bloquea toda la entrada cuando se pasa el cursor sobre un overlay",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "No bloquear la entrada cuando el cursor está sobre la ventana",
"USE_SKYBOX_HELP": "Mostrar una skybox si no hay una aplicación de escena o passthrough",
"USE_PASSTHROUGH_HELP": "Permitir passthrough si el entorno de ejecución XR lo admite",
"SCREEN_RENDER_DOWN_HELP": "Ayuda a reducir el aliasing en pantallas de alta resolución",
"SETS_ON_WATCH": "Conjuntos en el reloj",
"TROUBLESHOOTING": "Solución de problemas",
"CLEAR_SAVED_STATE": "Borrar estado guardado",
"CLEAR_PIPEWIRE_TOKENS": "Limpiar tokens de PipeWire",
"DELETE_ALL_CONFIGS": "Borrar configuración",
"RESTART_SOFTWARE": "Reiniciar software",
"CLEAR_SAVED_STATE_HELP": "Restablecer sets y posiciones de superposición",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Solicitar la selección de pantalla al iniciar la próxima vez",
"DELETE_ALL_CONFIGS_HELP": "Eliminar todos los archivos de configuración de conf.d",
"RESTART_SOFTWARE_HELP": "Aplicar la configuración que requiere un reinicio",
"CAPTURE_METHOD": "Captura de pantalla de Wayland",
"CAPTURE_METHOD_HELP": "Intente cambiar esta opción si\nexperimenta pantallas negras o con fallos",
"KEYBOARD_MIDDLE_CLICK": "Clic del botón central del teclado",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modificador para usar al escribir\ncon láser púrpura",
"OPTION": {
"AUTO": "Automático",
"AUTO_HELP": "Copiar pantalla usando la GPU si es compatible,\nde lo contrario, GPU de PipeWire.",
"PIPEWIRE_HELP": "Captura de GPU rápida,\nestándar en todos los escritorios.",
"PW_FALLBACK_HELP": "Método lento con alto uso de CPU.\nPruébalo si PipeWire GPU no funciona",
"SCREENCOPY_GPU_HELP": "Rápido, sin ventanas emergentes de uso compartido de pantalla.\nFunciona en: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Lento, sin ventanas emergentes de uso compartido de pantalla.\nFunciona en: Hyprland, Niri, River, Sway",
"NONE": "Ninguno",
"HMD_PINCH": "HMD + pellizco",
"EYE_PINCH": "Ojo + pellizco",
"EYE_ONLY": "Solo ojo",
"HMD_ONLY": "Solo HMD"
},
"AUTOSTART_APPS": "Aplicaciones a ejecutar al inicio",
"HANDSFREE_POINTER": "Modo manos libres",
"HANDSFREE_POINTER_HELP": "Entrada a utilizar cuando no\nestén disponibles los mandos de movimiento.\nPellizco con la izquierda para agarrar, con la derecha para hacer clic.",
"UI_GRADIENT_INTENSITY": "Intensidad del degradado de la IU",
"RESET_PLAYSPACE": "Restablecer espacio de juego",
"RESET_PLAYSPACE_HELP": "Borrar el desplazamiento del espacio de juego.",
"BLOCK_POSES_ON_KBD_INTERACTION": "Bloquear poses al interactuar con el teclado",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Bloquea que el juego reciba poses cuando el teclado está sobre él y 'Bloquear entrada del juego' está habilitado",
"LANGUAGE": "Idioma",
"REQUIRES_RESTART": "Requiere reinicio",
"GRID_OPACITY": "Opacidad de la cuadrícula del suelo",
"GRID_OPACITY_HELP": "Opacidad de la cuadrícula del suelo cuando el skybox está habilitado"
},
"HELLO": "¡Hola!",
"AUDIO": {
"VOLUME": "Volumen",
"SETTINGS": "Configuración de audio",
"AUTO_SWITCH_TO_VR_AUDIO": "Conmutar automáticamente al audio VR",
"SPEAKERS": "Altavoces",
"MICROPHONES": "Micrófonos",
"CARDS": "Tarjetas",
"SELECT_AUDIO_CARD_PROFILE": "Seleccionar perfil de tarjeta de audio",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "No se encontraron altavoces VR. Actívelos manualmente.",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "No se encontró micrófono VR. Actívelo manualmente.",
"FAILED_TO_SWITCH_MICROPHONE": "No se pudo cambiar el micrófono",
"MICROPHONE_SET_SUCCESSFULLY": "Micrófono configurado correctamente",
"SPEAKERS_SET_SUCCESSFULLY": "Altavoces configurados correctamente"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Re-centrar espacio de juego"
},
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolución"
},
"WIDTH": "Ancho",
"HEIGHT": "Altura",
"HIDE": "Ocultar",
"REMOVE": "Eliminar",
"SHOW": "Mostrar",
"FAILED_TO_LAUNCH_APPLICATION": "No se pudo iniciar la aplicación:",
"APPLICATION_STARTED": "Aplicación iniciada",
"CLOSE_WINDOW": "Cerrar ventana",
"GAME_LIST": {
"NO_GAMES_FOUND": "No se encontraron juegos",
"RUNNING_GAMES_LIST": "Lista de juegos en ejecución"
},
"TERMINATE_PROCESS": "Finalizar proceso",
"GAME_LAUNCHED": "Juego lanzado",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "Modo nativo",
"CAGE": "Modo de compatibilidad (Cage)"
},
"RES_TITLE": "Resolución",
"ASPECT_TITLE": "Aspecto",
"ASPECT": {
"WIDE": "Ancho",
"SEMI_WIDE": "Semi-ancho",
"SQUARE": "Cuadrado",
"SEMI_TALL": "Semi alto",
"TALL": "Alto"
},
"POS_TITLE": "Posicionamiento",
"POS": {
"FLOATING": "Flotante",
"ANCHORED": "Anclado",
"STATIC": "Estático",
"FLOATING_HELP": "Se mueve independientemente, se recentra al mostrarse.",
"ANCHORED_HELP": "Se mantiene en su lugar con respecto al marcador central.",
"STATIC_HELP": "No pertenece a ningún conjunto. No se recentra."
},
"AUTOSTART": "Ejecutar automáticamente al inicio",
"LAUNCH": "Iniciar"
},
"DISPLAY_BRIGHTNESS": "Brillo de la pantalla",
"PROCESS_LIST": "Lista de procesos",
"REFRESH": "Actualizar",
"PROCESS": {
"STOP": "Detener",
"FORCE_KILL": "Forzar cierre"
},
"DEBUG_INFO": "Información de depuración"
}

View File

@ -0,0 +1,162 @@
{
"ACTIONS": {
"RECENTER_PLAYSPACE": "Recentra lo spazio di gioco"
},
"APP_LAUNCHER": {
"ASPECT": {
"SEMI_TALL": "Semi-alto",
"SEMI_WIDE": "Semi-ampio",
"SQUARE": "Quadrato",
"TALL": "Alto",
"WIDE": "Ampio"
},
"ASPECT_TITLE": "Proporzioni",
"AUTOSTART": "Avvia automaticamente all'avvio",
"LAUNCH": "Avvia",
"MODE": {
"CAGE": "Modalità compatibilità (Cage)",
"NATIVE": "Modalità nativa"
},
"POS": {
"ANCHORED": "Ancorato",
"ANCHORED_HELP": "Rimane fermo rispetto al marcatore centrale.",
"FLOATING": "Fluttuante",
"FLOATING_HELP": "Si muove in modo indipendente, si recentra quando viene mostrato.",
"STATIC": "Statico",
"STATIC_HELP": "Non fa parte di alcun set. Non recentra."
},
"POS_TITLE": "Posizionamento",
"RES_TITLE": "Risoluzione"
},
"APP_SETTINGS": {
"ALLOW_SLIDING": "Interazione tramite stick durante l'afferrare",
"ANIMATION_SPEED": "Velocità dell'animazione dell'interfaccia utente",
"BLOCK_GAME_INPUT": "Blocca l'input di gioco",
"BLOCK_GAME_INPUT_HELP": "Blocca tutti gli input quando il cursore è sopra un overlay",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignora il Watch quando si blocca l'input",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Non bloccare l'input quando il watch è evidenziato",
"CAPTURE_METHOD": "Acquisizione dello schermo Wayland",
"CAPTURE_METHOD_HELP": "Prova a modificare questa impostazione se riscontri schermate nere o con artefatti.",
"CLEAR_PIPEWIRE_TOKENS": "Cancella i token PipeWire",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Richiedi la selezione dello schermo all'avvio successivo",
"CLEAR_SAVED_STATE": "Cancella stato salvato",
"CLEAR_SAVED_STATE_HELP": "Ripristina set e posizioni degli overlay",
"CLICK_FREEZE_TIME_MS": "Tempo di congelamento del click (ms)",
"CLICK_FREEZE_TIME_MS_HELP": "Aiuta con la precisione dei doppi clic",
"CLOCK_12H": "Orologio a 12 ore",
"CONTROLS": "Controlli",
"DELETE_ALL_CONFIGS": "Elimina configurazione",
"DELETE_ALL_CONFIGS_HELP": "Rimuovi tutti i file di configurazione da conf.d",
"DOUBLE_CURSOR_FIX": "Correzione doppio cursore",
"DOUBLE_CURSOR_FIX_HELP": "Abilita questa opzione se vedi 2 cursori",
"FEATURES": "Funzionalità",
"FOCUS_FOLLOWS_MOUSE_MODE": "Movimento del mouse al tocco del grilletto",
"HIDE_GRAB_HELP": "Nascondi l'aiuto per l'afferrare",
"HIDE_USERNAME": "Nascondi nome utente",
"INVERT_SCROLL_DIRECTION_X": "Inverti direzione dello scorrimento orizzontale",
"INVERT_SCROLL_DIRECTION_Y": "Inverti direzione dello scorrimento verticale",
"KEYBOARD_MIDDLE_CLICK": "Click centrale del tasto",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modificatore da usare durante la digitazione\ncon laser viola",
"KEYBOARD_SOUND_ENABLED": "Suoni della tastiera",
"LEFT_HANDED_MOUSE": "Mouse per mancini",
"LEFT_HANDED_MOUSE_HELP": "Usalo se i pulsanti del mouse sono invertiti",
"LONG_PRESS_DURATION": "Durata pressione prolungata",
"LOOK_AND_FEEL": "Aspetto",
"MISC": "Varie",
"NOTIFICATIONS_ENABLED": "Abilita notifiche",
"NOTIFICATIONS_SOUND_ENABLED": "Suoni di notifica",
"OPAQUE_BACKGROUND": "Sfondo opaco",
"OPTION": {
"AUTO": "Automatico",
"AUTO_HELP": "Copia dello schermo tramite GPU se supportata,\naltrimenti tramite GPU PipeWire.",
"PIPEWIRE_HELP": "Acquisizione GPU veloce,\nstandard su tutti i desktop.",
"PW_FALLBACK_HELP": "Metodo lento con elevato utilizzo della CPU.\nProva in caso PipeWire GPU non funzioni",
"SCREENCOPY_GPU_HELP": "Veloce, senza popup di condivisione schermo.\nFunziona su: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Lento, nessuna finestra pop-up per la condivisione dello schermo.\nFunziona su: Hyprland, Niri, River, Sway",
"NONE": "Nessuno",
"HMD_PINCH": "HMD + pizzico",
"EYE_PINCH": "Occhio + pizzico",
"EYE_ONLY": "Solo occhio",
"HMD_ONLY": "Solo HMD"
},
"POINTER_LERP_FACTOR": "Smussamento puntatore",
"RESTART_SOFTWARE": "Riavvia il software",
"RESTART_SOFTWARE_HELP": "Applica impostazioni che richiedono un riavvio",
"ROUND_MULTIPLIER": "Arrotondamento bordi UI",
"SCREEN_RENDER_DOWN": "Rendering dello schermo a risoluzione inferiore",
"SCREEN_RENDER_DOWN_HELP": "Aiuta a ridurre l'aliasing su schermi ad alta risoluzione",
"SCROLL_SPEED": "Velocità di scorrimento",
"SETS_ON_WATCH": "Set sul Watch",
"SPACE_DRAG_MULTIPLIER": "Moltiplicatore space-drag",
"SPACE_DRAG_UNLOCKED": "Consenti lo space-drag su tutti gli assi",
"SPACE_ROTATE_UNLOCKED": "Consenti la rotazione dello spazio su tutti gli assi",
"TROUBLESHOOTING": "Risoluzione dei problemi",
"UPRIGHT_SCREEN_FIX": "Correzione schermo verticale",
"UPRIGHT_SCREEN_FIX_HELP": "Corregge schermi verticali su alcuni desktop",
"USE_PASSTHROUGH": "Abilita passthrough",
"USE_PASSTHROUGH_HELP": "Consenti il passthrough se supportato dal runtime XR",
"USE_SKYBOX": "Abilita skybox",
"USE_SKYBOX_HELP": "Mostra uno skybox se non c'è un'app di scena o passthrough",
"XWAYLAND_BY_DEFAULT": "Esegui le app in modalità Compatibilità per impostazione predefinita",
"AUTOSTART_APPS": "App da avviare all'avvio",
"HANDSFREE_POINTER": "Modalità a mani libere",
"HANDSFREE_POINTER_HELP": "Input da usare quando i\ncontroller di movimento non sono disponibili.\nPizzico sinistro per afferrare, destro per cliccare.",
"UI_GRADIENT_INTENSITY": "Intensità gradiente dell'interfaccia utente",
"RESET_PLAYSPACE": "Ripristina playspace",
"RESET_PLAYSPACE_HELP": "Cancella l'offset dello spazio di gioco.",
"BLOCK_POSES_ON_KBD_INTERACTION": "Blocca le pose durante l'interazione con la tastiera",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Impedisce al gioco di ricevere pose quando la tastiera è evidenziata e 'Blocca input di gioco' è abilitato",
"LANGUAGE": "Lingua",
"REQUIRES_RESTART": "Richiede riavvio",
"GRID_OPACITY": "Opacità della griglia del pavimento",
"GRID_OPACITY_HELP": "Opacità della griglia del pavimento quando lo skybox è abilitato"
},
"APPLICATION_LAUNCHER": "Lanciatore applicazioni",
"APPLICATION_STARTED": "Applicazione avviata",
"APPLICATIONS": "Applicazioni",
"AUDIO": {
"AUTO_SWITCH_TO_VR_AUDIO": "Passa automaticamente all'audio VR",
"CARDS": "Schede",
"FAILED_TO_SWITCH_MICROPHONE": "Impossibile cambiare microfono",
"MICROPHONE_SET_SUCCESSFULLY": "Microfono impostato con successo",
"MICROPHONES": "Microfoni",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Nessun microfono VR trovato. Attivalo manualmente.",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Nessun altoparlante VR trovato. Selezionali manualmente.",
"SELECT_AUDIO_CARD_PROFILE": "Seleziona profilo scheda audio",
"SETTINGS": "Impostazioni audio",
"SPEAKERS": "Altoparlanti",
"SPEAKERS_SET_SUCCESSFULLY": "Altoparlanti impostati correttamente",
"VOLUME": "Volume"
},
"CLOSE_WINDOW": "Chiudi finestra",
"DISPLAY_BRIGHTNESS": "Luminosità display",
"FAILED_TO_LAUNCH_APPLICATION": "Impossibile avviare l'applicazione:",
"GAME_LAUNCHED": "Gioco lanciato",
"GAME_LIST": {
"NO_GAMES_FOUND": "Nessun gioco trovato",
"RUNNING_GAMES_LIST": "Lista dei giochi in esecuzione"
},
"GAMES": "Giochi",
"GENERAL_SETTINGS": "Impostazioni generali",
"HEIGHT": "Altezza",
"HELLO": "Ciao!",
"HELLO_USER": "Ciao, {USER}!",
"HIDE": "Nascondi",
"HOME_SCREEN": "Home",
"MONADO_RUNTIME": "Runtime Monado",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Risoluzione"
},
"REMOVE": "Rimuovi",
"SETTINGS": "Impostazioni",
"SHOW": "Mostra",
"TERMINATE_PROCESS": "Termina processo",
"WIDTH": "Larghezza",
"PROCESS_LIST": "Elenco processi",
"REFRESH": "Aggiorna",
"PROCESS": {
"STOP": "Interrompi",
"FORCE_KILL": "Uccidi forzatamente"
},
"DEBUG_INFO": "Informazioni di debug"
}

View File

@ -1,153 +1,162 @@
{
"HOME_SCREEN": "ホーム",
"MONADO_RUNTIME": "「Monado」ランタイム",
"APPLICATIONS": "アプリ",
"GAMES": "ゲーム",
"SETTINGS": "設定",
"PROCESSES": "プロセス",
"HELLO_USER": "こんにちは、{USER}",
"GENERAL_SETTINGS": "全般設定",
"APPLICATION_LAUNCHER": "アプリケーションランチャー",
"APP_SETTINGS": {
"HIDE_USERNAME": "ユーザー名を非表示",
"OPAQUE_BACKGROUND": "不透明な背景",
"WLX": {},
"LOOK_AND_FEEL": "外観",
"HIDE_GRAB_HELP": "グリップ動作中にのヘルプを非表示",
"ANIMATION_SPEED": "UIアニメーション速度",
"ROUND_MULTIPLIER": "UI の丸み",
"USE_SKYBOX": "スカイボックスを有効",
"USE_PASSTHROUGH": "パススルーを有効",
"CLOCK_12H": "12時間制",
"FEATURES": "機能",
"NOTIFICATIONS_ENABLED": "通知を有効にする",
"NOTIFICATIONS_SOUND_ENABLED": "通知音",
"KEYBOARD_SOUND_ENABLED": "キーボード音",
"SPACE_DRAG_MULTIPLIER": "スペースドラッグ倍率",
"SPACE_DRAG_UNLOCKED": "全軸のスペースドラッグ",
"SPACE_ROTATE_UNLOCKED": "全軸のスペース回転",
"BLOCK_GAME_INPUT": "ゲームの入力をブロック",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "入力ブロック時に監視を無視",
"CONTROLS": "コントロール",
"FOCUS_FOLLOWS_MOUSE_MODE": "トリガータッチでマウス移動",
"LEFT_HANDED_MOUSE": "左利きマウス",
"ALLOW_SLIDING": "グリップ動作中にスティックを有効",
"INVERT_SCROLL_DIRECTION_X": "水平スクロール方向を反転",
"INVERT_SCROLL_DIRECTION_Y": "垂直スクロール方向を反転",
"SCROLL_SPEED": "スクロール速度",
"LONG_PRESS_DURATION": "長押し時間",
"POINTER_LERP_FACTOR": "ポインターのスムージング",
"XR_CLICK_SENSITIVITY": "XRクリック感度",
"XR_CLICK_SENSITIVITY_RELEASE": "XRリリース感度",
"CLICK_FREEZE_TIME_MS": "クリックで一時停止時間 (ms)",
"MISC": "その他",
"XWAYLAND_BY_DEFAULT": "アプリ実行のデフォルトは互換モード",
"UPRIGHT_SCREEN_FIX": "画面の縦向き修正",
"DOUBLE_CURSOR_FIX": "ダブルカーソル修正",
"SCREEN_RENDER_DOWN": "画面の解像度を縮小",
"UPRIGHT_SCREEN_FIX_HELP": "一部のデスクトップで縦向きの画面を修正",
"DOUBLE_CURSOR_FIX_HELP": "2つのカーソルが表示される場合は、これを有効にします",
"XR_CLICK_SENSITIVITY_HELP": "アナログトリガの感度",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "クリックより低くする必要があります",
"CLICK_FREEZE_TIME_MS_HELP": "ダブルクリックの精度向上に役立ちます",
"LEFT_HANDED_MOUSE_HELP": "マウスボタンが入れ替わっている場合に有効にします",
"BLOCK_GAME_INPUT_HELP": "オーバーレイ上にマウスカーソルがあるときに入力をブロックします",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "ウォッチがホバーされているときは入力をブロックしない",
"USE_SKYBOX_HELP": "シーンアプリまたはパススルーがない場合にスカイボックスを表示します",
"USE_PASSTHROUGH_HELP": "XRランタイムがサポートしていれば、パススルーを有効にします",
"SCREEN_RENDER_DOWN_HELP": "高解像度スクリーンでのエイリアシングを軽減します",
"SETS_ON_WATCH": "ウォッチのセット",
"TROUBLESHOOTING": "トラブルシューティング",
"CLEAR_SAVED_STATE": "保存された状態をクリア",
"CLEAR_PIPEWIRE_TOKENS": "PipeWire トークンをクリア",
"DELETE_ALL_CONFIGS": "設定を消去",
"RESTART_SOFTWARE": "ソフトウェアを再起動",
"CLEAR_SAVED_STATE_HELP": "セットとオーバーレイの位置をリセット",
"CLEAR_PIPEWIRE_TOKENS_HELP": "次の起動時に画面選択のプロンプトを表示",
"DELETE_ALL_CONFIGS_HELP": "conf.d 内のすべての設定ファイルを削除",
"RESTART_SOFTWARE_HELP": "再起動が必要な設定を適用",
"CAPTURE_METHOD": "Waylandスクリーンキャプチャ",
"CAPTURE_METHOD_HELP": "画面が黒くなる、または乱れる場合は、\nこの設定を変更してみてください。",
"KEYBOARD_MIDDLE_CLICK": "キーボードの中ボタンクリック",
"KEYBOARD_MIDDLE_CLICK_HELP": "紫色のレーザーで入力する際の修飾キー",
"OPTION": {
"AUTO": "自動",
"AUTO_HELP": "サポートされている場合はScreenCopy GPU、\nそうでない場合はPipeWire GPU。",
"PIPEWIRE_HELP": "GPU高速キャプチャ。\nすべてのデスクトップで標準です。",
"PW_FALLBACK_HELP": "CPU使用量が多い低速な方法です。\nPipeWire GPUが動作しない場合に試してください。",
"SCREENCOPY_GPU_HELP": "高速で、スクリーン共有ポップアップはありません。\n動作する環境: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "遅延あり、画面共有ポップアップなし。\n動作する環境: Hyprland, Niri, River, Sway"
},
"AUTOSTART_APPS": "起動時に実行するアプリ"
},
"HELLO": "こんにちは!",
"AUDIO": {
"VOLUME": "音量",
"SETTINGS": "オーディオ設定",
"AUTO_SWITCH_TO_VR_AUDIO": "VRオーディオに自動切り替え",
"SPEAKERS": "スピーカー",
"MICROPHONES": "マイク",
"CARDS": "カード",
"SELECT_AUDIO_CARD_PROFILE": "オーディオカードプロファイルを選択",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "VRスピーカーが見つかりませんでした。手動で切り替えてください。",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "VRマイクが見つかりませんでした。手動で切り替えてください。",
"FAILED_TO_SWITCH_MICROPHONE": "マイクの切り替えに失敗しました",
"MICROPHONE_SET_SUCCESSFULLY": "マイクの設定が完了しました",
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定しました"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "プレイスペースを再中央"
},
"LIST_OF_PROCESSES": "プロセスのリスト",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "解像度"
},
"WIDTH": "幅",
"HEIGHT": "高さ",
"HIDE": "隠す",
"REMOVE": "削除",
"SHOW": "表示",
"PROCESS_LIST": {
"NO_PROCESSES_FOUND": "プロセスが見つかりませんでした",
"LOCATED_ON": "に",
"TERMINATE_PROCESS_NAMED_X": "プロセス \"{PROCESS_NAME}\" を終了します"
},
"FAILED_TO_LAUNCH_APPLICATION": "アプリケーションの起動に失敗しました:",
"NO_WINDOWS_FOUND": "ウィンドウが見つかりませんでした",
"WINDOW_OPTIONS": "ウィンドウオプション",
"APPLICATION_STARTED": "アプリケーションが起動しました",
"LIST_OF_WINDOWS": "ウィンドウ一覧",
"CLOSE_WINDOW": "ウィンドウを閉じる",
"GAME_LIST": {
"NO_GAMES_FOUND": "ゲームが見つかりませんでした"
},
"TERMINATE_PROCESS": "プロセスを終了する",
"GAME_LAUNCHED": "ゲームが起動しました",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "ネイティブモード",
"CAGE": "互換モードCage"
},
"RES_TITLE": "解像度",
"ASPECT_TITLE": "アスペクト",
"ASPECT": {
"WIDE": "ワイド",
"SEMI_WIDE": "半広角",
"SQUARE": "正方形",
"SEMI_TALL": "半縦長",
"TALL": "縦長"
},
"POS_TITLE": "配置",
"POS": {
"FLOATING": "フローティング",
"ANCHORED": "固定",
"STATIC": "固定",
"FLOATING_HELP": "独立して移動し、表示時に中央に再配置されます。",
"ANCHORED_HELP": "中央マーカーに対して固定された位置に留まります。",
"STATIC_HELP": "どのセットにも属していません。リセンターされません。"
},
"AUTOSTART": "起動時に自動実行",
"LAUNCH": "起動"
},
"DISPLAY_BRIGHTNESS": "ディスプレイの明るさ"
}
"HOME_SCREEN": "ホーム",
"MONADO_RUNTIME": "「Monado」ランタイム",
"APPLICATIONS": "アプリ",
"GAMES": "ゲーム",
"SETTINGS": "設定",
"HELLO_USER": "こんにちは、{USER}",
"GENERAL_SETTINGS": "全般設定",
"APPLICATION_LAUNCHER": "アプリケーションランチャー",
"APP_SETTINGS": {
"HIDE_USERNAME": "ユーザー名を非表示",
"OPAQUE_BACKGROUND": "不透明な背景",
"LOOK_AND_FEEL": "外観",
"HIDE_GRAB_HELP": "グリップ動作中にのヘルプを非表示",
"ANIMATION_SPEED": "UIアニメーション速度",
"ROUND_MULTIPLIER": "UI の丸み",
"USE_SKYBOX": "スカイボックスを有効",
"USE_PASSTHROUGH": "パススルーを有効",
"CLOCK_12H": "12時間制",
"FEATURES": "機能",
"NOTIFICATIONS_ENABLED": "通知を有効にする",
"NOTIFICATIONS_SOUND_ENABLED": "通知音",
"KEYBOARD_SOUND_ENABLED": "キーボード音",
"SPACE_DRAG_MULTIPLIER": "スペースドラッグ倍率",
"SPACE_DRAG_UNLOCKED": "全軸のスペースドラッグ",
"SPACE_ROTATE_UNLOCKED": "全軸のスペース回転",
"BLOCK_GAME_INPUT": "ゲームの入力をブロック",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "入力ブロック時に監視を無視",
"CONTROLS": "コントロール",
"FOCUS_FOLLOWS_MOUSE_MODE": "トリガータッチでマウス移動",
"LEFT_HANDED_MOUSE": "左利きマウス",
"ALLOW_SLIDING": "グリップ動作中にスティックを有効",
"INVERT_SCROLL_DIRECTION_X": "水平スクロール方向を反転",
"INVERT_SCROLL_DIRECTION_Y": "垂直スクロール方向を反転",
"SCROLL_SPEED": "スクロール速度",
"LONG_PRESS_DURATION": "長押し時間",
"POINTER_LERP_FACTOR": "ポインターのスムージング",
"CLICK_FREEZE_TIME_MS": "クリックで一時停止時間 (ms)",
"MISC": "その他",
"XWAYLAND_BY_DEFAULT": "アプリ実行のデフォルトは互換モード",
"UPRIGHT_SCREEN_FIX": "画面の縦向き修正",
"DOUBLE_CURSOR_FIX": "ダブルカーソル修正",
"SCREEN_RENDER_DOWN": "画面の解像度を縮小",
"UPRIGHT_SCREEN_FIX_HELP": "一部のデスクトップで縦向きの画面を修正",
"DOUBLE_CURSOR_FIX_HELP": "2つのカーソルが表示される場合は、これを有効にします",
"CLICK_FREEZE_TIME_MS_HELP": "ダブルクリックの精度向上に役立ちます",
"LEFT_HANDED_MOUSE_HELP": "マウスボタンが入れ替わっている場合に有効にします",
"BLOCK_GAME_INPUT_HELP": "オーバーレイ上にマウスカーソルがあるときに入力をブロックします",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "ウォッチがホバーされているときは入力をブロックしない",
"USE_SKYBOX_HELP": "シーンアプリまたはパススルーがない場合にスカイボックスを表示します",
"USE_PASSTHROUGH_HELP": "XRランタイムがサポートしていれば、パススルーを有効にします",
"SCREEN_RENDER_DOWN_HELP": "高解像度スクリーンでのエイリアシングを軽減します",
"SETS_ON_WATCH": "ウォッチのセット",
"TROUBLESHOOTING": "トラブルシューティング",
"CLEAR_SAVED_STATE": "保存された状態をクリア",
"CLEAR_PIPEWIRE_TOKENS": "PipeWire トークンをクリア",
"DELETE_ALL_CONFIGS": "設定を消去",
"RESTART_SOFTWARE": "ソフトウェアを再起動",
"CLEAR_SAVED_STATE_HELP": "セットとオーバーレイの位置をリセット",
"CLEAR_PIPEWIRE_TOKENS_HELP": "次の起動時に画面選択のプロンプトを表示",
"DELETE_ALL_CONFIGS_HELP": "conf.d 内のすべての設定ファイルを削除",
"RESTART_SOFTWARE_HELP": "再起動が必要な設定を適用",
"CAPTURE_METHOD": "Waylandスクリーンキャプチャ",
"CAPTURE_METHOD_HELP": "画面が黒くなる、または乱れる場合は、\nこの設定を変更してみてください。",
"KEYBOARD_MIDDLE_CLICK": "キーボードの中ボタンクリック",
"KEYBOARD_MIDDLE_CLICK_HELP": "紫色のレーザーで入力する際の修飾キー",
"OPTION": {
"AUTO": "自動",
"AUTO_HELP": "サポートされている場合はScreenCopy GPU、\nそうでない場合はPipeWire GPU。",
"PIPEWIRE_HELP": "GPU高速キャプチャ。\nすべてのデスクトップで標準です。",
"PW_FALLBACK_HELP": "CPU使用量が多い低速な方法です。\nPipeWire GPUが動作しない場合に試してください。",
"SCREENCOPY_GPU_HELP": "高速で、スクリーン共有ポップアップはありません。\n動作する環境: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "遅延あり、画面共有ポップアップなし。\n動作する環境: Hyprland, Niri, River, Sway",
"NONE": "なし",
"HMD_PINCH": "HMD + ピンチ",
"EYE_PINCH": "つまんで目を合わせる",
"EYE_ONLY": "視野のみ",
"HMD_ONLY": "HMDのみ"
},
"AUTOSTART_APPS": "起動時に実行するアプリ",
"HANDSFREE_POINTER": "ハンズフリーモード",
"HANDSFREE_POINTER_HELP": "モーションコントローラーが利用できない場合の入力方法。\n左手のピンチは掴み、右手のピンチはクリックです。",
"UI_GRADIENT_INTENSITY": "UIグラデーションの強さ",
"RESET_PLAYSPACE": "プレイエリアをリセット",
"RESET_PLAYSPACE_HELP": "プレイエリアのオフセットをクリアします。",
"BLOCK_POSES_ON_KBD_INTERACTION": "キーボード操作時のポーズをブロック",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "キーボードがホバーされ、「ゲーム入力をブロック」が有効になっている場合、ゲームがポーズを受信することをブロックします",
"LANGUAGE": "言語",
"REQUIRES_RESTART": "再起動が必要です",
"GRID_OPACITY": "フロアグリッドの不透明度",
"GRID_OPACITY_HELP": "スカイボックスが有効なときの床グリッドの不透明度"
},
"HELLO": "こんにちは!",
"AUDIO": {
"VOLUME": "音量",
"SETTINGS": "オーディオ設定",
"AUTO_SWITCH_TO_VR_AUDIO": "VRオーディオに自動切り替え",
"SPEAKERS": "スピーカー",
"MICROPHONES": "マイク",
"CARDS": "カード",
"SELECT_AUDIO_CARD_PROFILE": "オーディオカードプロファイルを選択",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "VRスピーカーが見つかりませんでした。手動で切り替えてください。",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "VRマイクが見つかりませんでした。手動で切り替えてください。",
"FAILED_TO_SWITCH_MICROPHONE": "マイクの切り替えに失敗しました",
"MICROPHONE_SET_SUCCESSFULLY": "マイクの設定が完了しました",
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定しました"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "プレイスペースを再中央"
},
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "解像度"
},
"WIDTH": "幅",
"HEIGHT": "高さ",
"HIDE": "隠す",
"REMOVE": "削除",
"SHOW": "表示",
"FAILED_TO_LAUNCH_APPLICATION": "アプリケーションの起動に失敗しました:",
"APPLICATION_STARTED": "アプリケーションが起動しました",
"CLOSE_WINDOW": "ウィンドウを閉じる",
"GAME_LIST": {
"NO_GAMES_FOUND": "ゲームが見つかりませんでした",
"RUNNING_GAMES_LIST": "実行中のゲーム一覧"
},
"TERMINATE_PROCESS": "プロセスを終了する",
"GAME_LAUNCHED": "ゲームが起動しました",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "ネイティブモード",
"CAGE": "互換モードCage"
},
"RES_TITLE": "解像度",
"ASPECT_TITLE": "アスペクト",
"ASPECT": {
"WIDE": "ワイド",
"SEMI_WIDE": "半広角",
"SQUARE": "正方形",
"SEMI_TALL": "半縦長",
"TALL": "縦長"
},
"POS_TITLE": "配置",
"POS": {
"FLOATING": "フローティング",
"ANCHORED": "固定",
"STATIC": "固定",
"FLOATING_HELP": "独立して移動し、表示時に中央に再配置されます。",
"ANCHORED_HELP": "中央マーカーに対して固定された位置に留まります。",
"STATIC_HELP": "どのセットにも属していません。リセンターされません。"
},
"AUTOSTART": "起動時に自動実行",
"LAUNCH": "起動"
},
"DISPLAY_BRIGHTNESS": "ディスプレイの明るさ",
"PROCESS_LIST": "プロセスリスト",
"REFRESH": "更新",
"PROCESS": {
"STOP": "停止",
"FORCE_KILL": "強制終了"
},
"DEBUG_INFO": "デバッグ情報"
}

View File

@ -1,153 +1,163 @@
{
"ACTIONS": {
"RECENTER_PLAYSPACE": "Wycentruj przestrzeń"
},
"APP_SETTINGS": {
"HIDE_USERNAME": "Ukryj nazwę użytkownika",
"OPAQUE_BACKGROUND": "Nieprzezroczyste tło",
"WLX": {},
"LOOK_AND_FEEL": "Wygląd i działanie",
"HIDE_GRAB_HELP": "Ukryj pomoc dotyczącą chwytania",
"ANIMATION_SPEED": "Prędkość animacji UI",
"ROUND_MULTIPLIER": "Zaokrąglenie krawędzi UI",
"USE_SKYBOX": "Włącz niebo",
"USE_PASSTHROUGH": "Włącz passthrough",
"CLOCK_12H": "Zegar 12-godzinny",
"FEATURES": "Funkcje",
"NOTIFICATIONS_ENABLED": "Włącz powiadomienia",
"NOTIFICATIONS_SOUND_ENABLED": "Dźwięki powiadomień",
"KEYBOARD_SOUND_ENABLED": "Dźwięki klawiatury",
"SPACE_DRAG_MULTIPLIER": "Mnożnik przesuwania przestrzeni",
"SPACE_DRAG_UNLOCKED": "Pozwól na przesuwanie przestrzeni na wszystkich osiach",
"SPACE_ROTATE_UNLOCKED": "Pozwól na rotację przestrzeni na wszystkich osiach",
"BLOCK_GAME_INPUT": "Blokuj input z gry",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Nie blokuj inputu gry, gdy zegarek jest używany",
"CONTROLS": "Sterowanie",
"FOCUS_FOLLOWS_MOUSE_MODE": "Ruch myszą po dotknięciu spustu",
"LEFT_HANDED_MOUSE": "Myszka dla leworęcznych",
"ALLOW_SLIDING": "Interakcja z drążkami podczas chwytania",
"INVERT_SCROLL_DIRECTION_X": "Odwróć kierunek przewijania w poziomie",
"INVERT_SCROLL_DIRECTION_Y": "Odwróć kierunek przewijania w pionie",
"SCROLL_SPEED": "Prędkość przewijania",
"LONG_PRESS_DURATION": "Czas długiego przytrzymania",
"POINTER_LERP_FACTOR": "Wygładzanie wskaźnika",
"XR_CLICK_SENSITIVITY": "Czułość kliknięć XR",
"XR_CLICK_SENSITIVITY_RELEASE": "Czułość zwalniania XR",
"CLICK_FREEZE_TIME_MS": "Czas zamrożenia po kliknięciu (ms)",
"MISC": "Różne",
"XWAYLAND_BY_DEFAULT": "Uruchamiaj aplikacje domyślnie w trybie kompatybilności",
"UPRIGHT_SCREEN_FIX": "Naprawa pozycji ekranu",
"DOUBLE_CURSOR_FIX": "Naprawa podwójnego kursora",
"SCREEN_RENDER_DOWN": "Renderuj ekran w niższej rozdzielczości",
"UPRIGHT_SCREEN_FIX_HELP": "Naprawia pionowe ekrany na niektórych komputerach",
"DOUBLE_CURSOR_FIX_HELP": "Włącz to, jeśli widzisz 2 kursory",
"XR_CLICK_SENSITIVITY_HELP": "Czułość analogowego spustu",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Musi być niższa niż kliknięcie",
"CLICK_FREEZE_TIME_MS_HELP": "Pomaga w precyzji podwójnego kliknięcia",
"LEFT_HANDED_MOUSE_HELP": "Użyj tego, jeśli przyciski myszy są zamienione",
"BLOCK_GAME_INPUT_HELP": "Blokuje wszystkie dane wejściowe, gdy kursor najedzie na nakładkę",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Nie blokuj inputu, gdy wskaźnik jest najedzony",
"USE_SKYBOX_HELP": "Wyświetlaj niebo, jeśli nie ma aplikacji sceny lub passthrough",
"USE_PASSTHROUGH_HELP": "Pozwól na passthrough, jeśli runtime XR to obsługuje",
"SCREEN_RENDER_DOWN_HELP": "Pomaga redukować aliasing na ekranach o wysokiej rozdzielczości",
"SETS_ON_WATCH": "Lista zestawów na zegarku",
"TROUBLESHOOTING": "Rozwiązywanie problemów",
"CLEAR_SAVED_STATE": "Wyczyść zapisany stan",
"CLEAR_PIPEWIRE_TOKENS": "Wyczyść tokeny PipeWire",
"DELETE_ALL_CONFIGS": "Wyczyść konfigurację",
"RESTART_SOFTWARE": "Uruchom ponownie oprogramowanie",
"CLEAR_SAVED_STATE_HELP": "Zresetuj zestawy i pozycje nakładek",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Zapytaj o wybór ekranu przy następnym uruchomieniu",
"DELETE_ALL_CONFIGS_HELP": "Usuń wszystkie pliki konfiguracyjne z katalogu conf.d",
"RESTART_SOFTWARE_HELP": "Zastosuj ustawienia wymagające ponownego uruchomienia",
"CAPTURE_METHOD": "Przechwytywanie ekranu Wayland",
"CAPTURE_METHOD_HELP": "Spróbuj zmienić tę opcję, jeśli masz\nproblemy z czarnym lub migoczącym ekranem",
"KEYBOARD_MIDDLE_CLICK": "Środkowy przycisk myszy na klawiaturze",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modyfikator, który ma zostać użyty podczas pisania\nfioletową wiązką lasera",
"OPTION": {
"AUTO": "Automatycznie",
"AUTO_HELP": "Skopiuj ekran za pomocą GPU, jeśli obsługiwane,\nw przeciwnym razie GPU PipeWire.",
"PIPEWIRE_HELP": "Szybkie przechwytywanie GPU,\nstandard na wszystkich komputerach.",
"PW_FALLBACK_HELP": "Powolna metoda z wysokim użyciem procesora.\nWypróbuj w przypadku, gdy PipeWire GPU nie działa",
"SCREENCOPY_GPU_HELP": "Szybkie działanie, brak wyskakujących okien z informacją o udostępnianiu ekranu.\nDziała na: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Wolne, bez wyskakujących okienek udostępniania ekranu.\nDziała na: Hyprland, Niri, River, Sway"
},
"AUTOSTART_APPS": "Aplikacje do uruchomienia przy starcie"
},
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji",
"APPLICATIONS": "Aplikacje",
"AUDIO": {
"AUTO_SWITCH_TO_VR_AUDIO": "Automatyczne przełączanie na dźwięk VR",
"CARDS": "Karty",
"FAILED_TO_SWITCH_MICROPHONE": "Nie udało się przełączyć mikrofon",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofon ustawiono pomyślnie",
"MICROPHONES": "Mikrofony",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Brak mikrofonu VR. Włącz go ręcznie.",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Brak głośników VR. Włącz je ręcznie.",
"SELECT_AUDIO_CARD_PROFILE": "Wybierz profil karty dźwiękowej",
"SETTINGS": "Ustawienia dźwięku",
"SPEAKERS": "Głośniki",
"SPEAKERS_SET_SUCCESSFULLY": "Głośniki ustawiono pomyślnie",
"VOLUME": "Głośność"
},
"GAMES": "Gry",
"GENERAL_SETTINGS": "Ustawienia ogólne",
"HEIGHT": "Wysokość",
"HELLO": "Witaj!",
"HELLO_USER": "Witaj, {USER}!",
"HIDE": "Ukryj",
"HOME_SCREEN": "Ekran główny",
"LIST_OF_PROCESSES": "Lista procesów",
"MONADO_RUNTIME": "Środowisko Monado",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Rozdzielczość"
},
"REMOVE": "Usuń",
"SETTINGS": "Ustawienia",
"SHOW": "Pokaż",
"WIDTH": "Szerokość",
"PROCESSES": "Procesy",
"PROCESS_LIST": {
"NO_PROCESSES_FOUND": "Nie znaleziono procesów",
"LOCATED_ON": "na",
"TERMINATE_PROCESS_NAMED_X": "Zakończ proces \"{PROCESS_NAME}\""
},
"FAILED_TO_LAUNCH_APPLICATION": "Nie udało się uruchomić aplikacji:",
"NO_WINDOWS_FOUND": "Nie znaleziono okien",
"WINDOW_OPTIONS": "Opcje okna",
"APPLICATION_STARTED": "Aplikacja uruchomiona",
"LIST_OF_WINDOWS": "Lista okien",
"CLOSE_WINDOW": "Zamknij okno",
"GAME_LIST": {
"NO_GAMES_FOUND": "Nie znaleziono gier"
},
"TERMINATE_PROCESS": "Zakończ proces",
"GAME_LAUNCHED": "Gra uruchomiona",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "Tryb natywny",
"CAGE": "Tryb kompatybilności (Cage)"
},
"RES_TITLE": "Rozdzielczość",
"ASPECT_TITLE": "Proporcje",
"ASPECT": {
"WIDE": "Szeroki",
"SEMI_WIDE": "Półszeroki",
"SQUARE": "Kwadrat",
"SEMI_TALL": "Pół-wysoki",
"TALL": "Wysoki"
},
"POS_TITLE": "Pozycjonowanie",
"POS": {
"FLOATING": "Pływający",
"ANCHORED": "Przymocowane",
"STATIC": "Statyczny",
"FLOATING_HELP": "Porusza się niezależnie, wycentrowuje się po otwarciu.",
"ANCHORED_HELP": "Pozostaje nieruchoma względem centralnego znacznika.",
"STATIC_HELP": "Nie należy do żadnego zestawu. Nie wyśrodkowuje."
},
"AUTOSTART": "Uruchom automatycznie przy starcie",
"LAUNCH": "Uruchom"
},
"DISPLAY_BRIGHTNESS": "Jasność wyświetlacza"
}
"ACTIONS": {
"RECENTER_PLAYSPACE": "Wycentruj przestrzeń"
},
"APP_SETTINGS": {
"HIDE_USERNAME": "Ukryj nazwę użytkownika",
"OPAQUE_BACKGROUND": "Nieprzezroczyste tło",
"LOOK_AND_FEEL": "Wygląd i działanie",
"HIDE_GRAB_HELP": "Ukryj pomoc dotyczącą chwytania",
"ANIMATION_SPEED": "Prędkość animacji UI",
"ROUND_MULTIPLIER": "Zaokrąglenie krawędzi UI",
"USE_SKYBOX": "Włącz niebo",
"USE_PASSTHROUGH": "Włącz passthrough",
"CLOCK_12H": "Zegar 12-godzinny",
"FEATURES": "Funkcje",
"NOTIFICATIONS_ENABLED": "Włącz powiadomienia",
"NOTIFICATIONS_SOUND_ENABLED": "Dźwięki powiadomień",
"KEYBOARD_SOUND_ENABLED": "Dźwięki klawiatury",
"SPACE_DRAG_MULTIPLIER": "Mnożnik przesuwania przestrzeni",
"SPACE_DRAG_UNLOCKED": "Pozwól na przesuwanie przestrzeni na wszystkich osiach",
"SPACE_ROTATE_UNLOCKED": "Pozwól na rotację przestrzeni na wszystkich osiach",
"BLOCK_GAME_INPUT": "Blokuj input z gry",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Nie blokuj inputu gry, gdy zegarek jest używany",
"CONTROLS": "Sterowanie",
"FOCUS_FOLLOWS_MOUSE_MODE": "Ruch myszą po dotknięciu spustu",
"LEFT_HANDED_MOUSE": "Myszka dla leworęcznych",
"ALLOW_SLIDING": "Interakcja z drążkami podczas chwytania",
"INVERT_SCROLL_DIRECTION_X": "Odwróć kierunek przewijania w poziomie",
"INVERT_SCROLL_DIRECTION_Y": "Odwróć kierunek przewijania w pionie",
"SCROLL_SPEED": "Prędkość przewijania",
"LONG_PRESS_DURATION": "Czas długiego przytrzymania",
"POINTER_LERP_FACTOR": "Wygładzanie wskaźnika",
"CLICK_FREEZE_TIME_MS": "Czas zamrożenia po kliknięciu (ms)",
"XWAYLAND_BY_DEFAULT": "Uruchamiaj aplikacje domyślnie w trybie kompatybilności",
"UPRIGHT_SCREEN_FIX": "Naprawa pozycji ekranu",
"DOUBLE_CURSOR_FIX": "Naprawa podwójnego kursora",
"SCREEN_RENDER_DOWN": "Renderuj ekran w niższej rozdzielczości",
"UPRIGHT_SCREEN_FIX_HELP": "Naprawia pionowe ekrany na niektórych komputerach",
"DOUBLE_CURSOR_FIX_HELP": "Włącz to, jeśli widzisz 2 kursory",
"CLICK_FREEZE_TIME_MS_HELP": "Pomaga w precyzji podwójnego kliknięcia",
"LEFT_HANDED_MOUSE_HELP": "Użyj tego, jeśli przyciski myszy są zamienione",
"BLOCK_GAME_INPUT_HELP": "Blokuje wszystkie dane wejściowe, gdy kursor najedzie na nakładkę",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Nie blokuj inputu, gdy wskaźnik jest najedzony",
"USE_SKYBOX_HELP": "Wyświetlaj niebo, jeśli nie ma aplikacji sceny lub passthrough",
"USE_PASSTHROUGH_HELP": "Pozwól na passthrough, jeśli runtime XR to obsługuje",
"SCREEN_RENDER_DOWN_HELP": "Pomaga redukować aliasing na ekranach o wysokiej rozdzielczości",
"ENABLE_WATCH": "Włącz zegarek",
"SETS_ON_WATCH": "Lista zestawów na zegarku",
"TROUBLESHOOTING": "Rozwiązywanie problemów",
"CLEAR_SAVED_STATE": "Wyczyść zapisany stan",
"CLEAR_PIPEWIRE_TOKENS": "Wyczyść tokeny PipeWire",
"DELETE_ALL_CONFIGS": "Wyczyść konfigurację",
"RESTART_SOFTWARE": "Restartuj WayVR",
"CLEAR_SAVED_STATE_HELP": "Zresetuj zestawy i pozycje nakładek",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Zapytaj o wybór ekranu przy następnym uruchomieniu",
"DELETE_ALL_CONFIGS_HELP": "Usuń wszystkie pliki konfiguracyjne z katalogu conf.d",
"RESTART_SOFTWARE_HELP": "Zastosuj ustawienia wymagające ponownego uruchomienia",
"CAPTURE_METHOD": "Przechwytywanie ekranu Wayland",
"CAPTURE_METHOD_HELP": "Spróbuj zmienić tę opcję, jeśli masz\nproblemy z czarnym lub migoczącym ekranem",
"KEYBOARD_MIDDLE_CLICK": "Środkowy przycisk myszy na klawiaturze",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modyfikator, który ma zostać użyty podczas pisania\nfioletową wiązką lasera",
"OPTION": {
"AUTO": "Automatycznie",
"AUTO_HELP": "Skopiuj ekran za pomocą GPU, jeśli obsługiwane,\nw przeciwnym razie GPU PipeWire.",
"PIPEWIRE_HELP": "Szybkie przechwytywanie GPU,\nstandard na wszystkich komputerach.",
"PW_FALLBACK_HELP": "Powolna metoda z wysokim użyciem procesora.\nWypróbuj w przypadku, gdy PipeWire GPU nie działa",
"SCREENCOPY_GPU_HELP": "Szybkie działanie, brak wyskakujących okien z informacją o udostępnianiu ekranu.\nDziała na: Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "Wolne, bez wyskakujących okienek udostępniania ekranu.\nDziała na: Hyprland, Niri, River, Sway",
"NONE": "Brak",
"HMD_PINCH": "HMD + ściśnięcie placami",
"EYE_PINCH": "Ściśnięcie palcami + oko",
"EYE_ONLY": "Tylko oko",
"HMD_ONLY": "Tylko HMD"
},
"AUTOSTART_APPS": "Aplikacje auto-start",
"HANDSFREE_POINTER": "Tryb bez użycia rąk",
"HANDSFREE_POINTER_HELP": "Wejście do użycia, gdy kontrolery ruchu\nsą niedostępne. Lewy szczyptak to chwyt,\nprawy to kliknięcie.",
"UI_GRADIENT_INTENSITY": "Intensywność gradientu UI",
"RESET_PLAYSPACE": "Zresetuj przestrzeń gry",
"RESET_PLAYSPACE_HELP": "Wyczyść przesunięcie przestrzeni gry.",
"BLOCK_POSES_ON_KBD_INTERACTION": "Blokuj pozy podczas interakcji z klawiaturą",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blokuje odbieranie póz przez grę, gdy kursor myszy znajduje się nad klawiaturą i włączona jest opcja 'Blokuj dane wejściowe z gry'",
"LANGUAGE": "Język",
"REQUIRES_RESTART": "Wymaga restartu",
"MISC": "Różne",
"GRID_OPACITY": "Przezroczystość siatki podłogowej",
"GRID_OPACITY_HELP": "Przezroczystość siatki podłogowej, gdy włączony jest skybox"
},
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji",
"APPLICATIONS": "Aplikacje",
"AUDIO": {
"AUTO_SWITCH_TO_VR_AUDIO": "Automatyczne przełączanie na dźwięk VR",
"CARDS": "Karty",
"FAILED_TO_SWITCH_MICROPHONE": "Nie udało się przełączyć mikrofon",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofon ustawiono pomyślnie",
"MICROPHONES": "Mikrofony",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Brak mikrofonu VR. Włącz go ręcznie.",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Brak głośników VR. Włącz je ręcznie.",
"SELECT_AUDIO_CARD_PROFILE": "Wybierz profil karty dźwiękowej",
"SETTINGS": "Ustawienia dźwięku",
"SPEAKERS": "Głośniki",
"SPEAKERS_SET_SUCCESSFULLY": "Głośniki ustawiono pomyślnie",
"VOLUME": "Głośność"
},
"GAMES": "Gry",
"GENERAL_SETTINGS": "Ustawienia ogólne",
"HEIGHT": "Wysokość",
"HELLO": "Witaj!",
"HELLO_USER": "Witaj, {USER}!",
"HIDE": "Ukryj",
"HOME_SCREEN": "Ekran główny",
"MONADO_RUNTIME": "Środowisko Monado",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Rozdzielczość"
},
"REMOVE": "Usuń",
"SETTINGS": "Ustawienia",
"SHOW": "Pokaż",
"WIDTH": "Szerokość",
"FAILED_TO_LAUNCH_APPLICATION": "Nie udało się uruchomić aplikacji:",
"APPLICATION_STARTED": "Aplikacja uruchomiona",
"CLOSE_WINDOW": "Zamknij okno",
"GAME_LIST": {
"NO_GAMES_FOUND": "Nie znaleziono gier",
"RUNNING_GAMES_LIST": "Lista uruchomionych gier"
},
"TERMINATE_PROCESS": "Zakończ proces",
"GAME_LAUNCHED": "Gra uruchomiona",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "Tryb natywny",
"CAGE": "Tryb kompatybilności (Cage)"
},
"RES_TITLE": "Rozdzielczość",
"ASPECT_TITLE": "Proporcje",
"ASPECT": {
"WIDE": "Szeroki",
"SEMI_WIDE": "Półszeroki",
"SQUARE": "Kwadrat",
"SEMI_TALL": "Pół-wysoki",
"TALL": "Wysoki"
},
"POS_TITLE": "Pozycjonowanie",
"POS": {
"FLOATING": "Pływający",
"ANCHORED": "Przymocowane",
"STATIC": "Statyczny",
"FLOATING_HELP": "Porusza się niezależnie, wycentrowuje się po otwarciu.",
"ANCHORED_HELP": "Pozostaje nieruchoma względem centralnego znacznika.",
"STATIC_HELP": "Nie należy do żadnego zestawu. Nie wyśrodkowuje."
},
"AUTOSTART": "Uruchom automatycznie przy starcie",
"LAUNCH": "Uruchom"
},
"DISPLAY_BRIGHTNESS": "Jasność wyświetlacza",
"PROCESS_LIST": "Lista procesów",
"REFRESH": "Odśwież",
"PROCESS": {
"STOP": "Zatrzymaj",
"FORCE_KILL": "Wymuś zakończenie"
},
"DEBUG_INFO": "Informacje debugowania"
}

View File

@ -0,0 +1,162 @@
{
"ACTIONS": {
"RECENTER_PLAYSPACE": "重置游玩区中心"
},
"APP_LAUNCHER": {
"ASPECT": {
"SEMI_TALL": "略高",
"SEMI_WIDE": "略宽",
"SQUARE": "正方形",
"TALL": "高",
"WIDE": "宽"
},
"ASPECT_TITLE": "比例",
"AUTOSTART": "启动时自动运行",
"LAUNCH": "启动",
"MODE": {
"CAGE": "兼容模式 (Cage)",
"NATIVE": "原生模式"
},
"POS": {
"ANCHORED": "锚定",
"ANCHORED_HELP": "相对于中心标记保持固定。",
"FLOATING": "悬浮",
"FLOATING_HELP": "独立移动,显示时自动重置中心。",
"STATIC": "静态",
"STATIC_HELP": "不属于任何集合。不会重置中心。"
},
"POS_TITLE": "定位",
"RES_TITLE": "分辨率"
},
"APP_SETTINGS": {
"ALLOW_SLIDING": "抓取时允许摇杆交互",
"ANIMATION_SPEED": "UI 动画速度",
"BLOCK_GAME_INPUT": "屏蔽游戏输入",
"BLOCK_GAME_INPUT_HELP": "悬停在覆盖层上时屏蔽所有输入",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "屏蔽输入时忽略手表",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "悬停在手表上时不屏蔽输入",
"CAPTURE_METHOD": "Wayland 屏幕采集",
"CAPTURE_METHOD_HELP": "如果您遇到黑屏或花屏,请尝试更改此项",
"CLEAR_PIPEWIRE_TOKENS": "清除 PipeWire 令牌",
"CLEAR_PIPEWIRE_TOKENS_HELP": "下次启动时提示选择屏幕",
"CLEAR_SAVED_STATE": "清除保存的状态",
"CLEAR_SAVED_STATE_HELP": "重置集合与覆盖层位置",
"CLICK_FREEZE_TIME_MS": "点击冻结时间 (毫秒)",
"CLICK_FREEZE_TIME_MS_HELP": "有助于提高双击精度",
"CLOCK_12H": "12 小时制时钟",
"CONTROLS": "控制",
"DELETE_ALL_CONFIGS": "擦除配置",
"DELETE_ALL_CONFIGS_HELP": "从 conf.d 中删除所有配置文件",
"DOUBLE_CURSOR_FIX": "双光标修复",
"DOUBLE_CURSOR_FIX_HELP": "如果您看到两个光标,请启用此项",
"FEATURES": "功能",
"FOCUS_FOLLOWS_MOUSE_MODE": "触摸扳机时移动鼠标",
"HIDE_GRAB_HELP": "隐藏抓取帮助",
"HIDE_USERNAME": "隐藏用户名",
"INVERT_SCROLL_DIRECTION_X": "反转水平滚动方向",
"INVERT_SCROLL_DIRECTION_Y": "反转垂直滚动方向",
"KEYBOARD_MIDDLE_CLICK": "键盘中键点击",
"KEYBOARD_MIDDLE_CLICK_HELP": "使用紫色激光打字时的修饰键",
"KEYBOARD_SOUND_ENABLED": "键盘音效",
"LEFT_HANDED_MOUSE": "左手鼠标",
"LEFT_HANDED_MOUSE_HELP": "如果鼠标按钮反置,请使用此项",
"LONG_PRESS_DURATION": "长按持续时间",
"LOOK_AND_FEEL": "外观与体验",
"MISC": "杂项",
"NOTIFICATIONS_ENABLED": "启用通知",
"NOTIFICATIONS_SOUND_ENABLED": "通知音效",
"OPAQUE_BACKGROUND": "不透明背景",
"OPTION": {
"AUTO": "自动",
"AUTO_HELP": "如果支持,则使用 ScreenCopy GPU\n否则使用 PipeWire GPU。",
"PIPEWIRE_HELP": "快速 GPU 采集,\n所有桌面环境的标准方式。",
"PW_FALLBACK_HELP": "高 CPU 占用的慢速方法。\n在 PipeWire GPU 不起作用时尝试。",
"SCREENCOPY_GPU_HELP": "快速,无屏幕共享弹窗。\n支持Hyprland, Niri, River, Sway",
"SCREENCOPY_HELP": "慢速,无屏幕共享弹窗。\n支持Hyprland, Niri, River, Sway",
"NONE": "无",
"HMD_PINCH": "HMD + 捏合",
"EYE_PINCH": "眼睛 + 捏合",
"EYE_ONLY": "仅眼球",
"HMD_ONLY": "仅限头显"
},
"POINTER_LERP_FACTOR": "指针平滑",
"RESTART_SOFTWARE": "重启软件",
"RESTART_SOFTWARE_HELP": "应用需要重启的设置",
"ROUND_MULTIPLIER": "UI 边缘圆角",
"SCREEN_RENDER_DOWN": "以较低分辨率渲染屏幕",
"SCREEN_RENDER_DOWN_HELP": "有助于解决高分辨率屏幕的锯齿问题",
"SCROLL_SPEED": "滚动速度",
"SETS_ON_WATCH": "在手表上显示集合",
"SPACE_DRAG_MULTIPLIER": "空间拖拽倍率",
"SPACE_DRAG_UNLOCKED": "允许在所有轴上进行空间拖拽",
"SPACE_ROTATE_UNLOCKED": "允许在所有轴上进行空间旋转",
"TROUBLESHOOTING": "故障排除",
"UPRIGHT_SCREEN_FIX": "垂直屏幕修复",
"UPRIGHT_SCREEN_FIX_HELP": "修复某些桌面上的垂直屏幕问题",
"USE_PASSTHROUGH": "启用穿透",
"USE_PASSTHROUGH_HELP": "如果 XR 运行时支持,则允许穿透",
"USE_SKYBOX": "启用天空盒",
"USE_SKYBOX_HELP": "如果没有场景应用或穿透,则显示天空盒",
"XWAYLAND_BY_DEFAULT": "默认以兼容模式运行应用",
"AUTOSTART_APPS": "开机启动应用",
"HANDSFREE_POINTER": "免提模式",
"HANDSFREE_POINTER_HELP": "当运动控制器不可用时使用的输入。\n左手捏合为抓取右手为点击。",
"UI_GRADIENT_INTENSITY": "UI 渐变强度",
"RESET_PLAYSPACE": "重置游戏空间",
"RESET_PLAYSPACE_HELP": "清除舞台空间偏移。",
"BLOCK_POSES_ON_KBD_INTERACTION": "与键盘交互时阻止姿势",
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "当键盘悬停且启用“阻止游戏输入”时,阻止游戏接收姿势",
"LANGUAGE": "语言",
"REQUIRES_RESTART": "需要重启",
"GRID_OPACITY": "地面网格不透明度",
"GRID_OPACITY_HELP": "启用天空盒时地板网格的不透明度"
},
"APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用已启动",
"APPLICATIONS": "应用",
"AUDIO": {
"AUTO_SWITCH_TO_VR_AUDIO": "自动切换到 VR 音频",
"CARDS": "声卡",
"FAILED_TO_SWITCH_MICROPHONE": "切换麦克风失败",
"MICROPHONE_SET_SUCCESSFULLY": "麦克风设置成功",
"MICROPHONES": "麦克风",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "未找到 VR 麦克风。请手动切换。",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "未找到 VR 扬声器。请手动切换。",
"SELECT_AUDIO_CARD_PROFILE": "选择声卡配置",
"SETTINGS": "音频设置",
"SPEAKERS": "扬声器",
"SPEAKERS_SET_SUCCESSFULLY": "扬声器设置成功",
"VOLUME": "音量"
},
"CLOSE_WINDOW": "关闭窗口",
"DISPLAY_BRIGHTNESS": "显示亮度",
"FAILED_TO_LAUNCH_APPLICATION": "启动应用失败:",
"GAME_LAUNCHED": "游戏已启动",
"GAME_LIST": {
"NO_GAMES_FOUND": "未找到游戏",
"RUNNING_GAMES_LIST": "正在运行的游戏列表"
},
"GAMES": "游戏",
"GENERAL_SETTINGS": "通用设置",
"HEIGHT": "高度",
"HELLO": "你好!",
"HELLO_USER": "你好,{USER}",
"HIDE": "隐藏",
"HOME_SCREEN": "主页",
"MONADO_RUNTIME": "Monado 运行时",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "分辨率"
},
"REMOVE": "移除",
"SETTINGS": "设置",
"SHOW": "显示",
"TERMINATE_PROCESS": "终止进程",
"WIDTH": "宽度",
"PROCESS_LIST": "进程列表",
"REFRESH": "刷新",
"PROCESS": {
"STOP": "停止",
"FORCE_KILL": "强制关闭"
},
"DEBUG_INFO": "调试信息"
}

View File

@ -10,36 +10,42 @@ use wgui::{
i18n::Translation,
layout::{Layout, LayoutParams, LayoutUpdateParams, LayoutUpdateResult, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
renderer_vk::text::custom_glyph::CustomGlyphData,
task::Tasks,
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
theme::WguiTheme,
widget::{label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite},
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
};
use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep};
use wlx_common::{
async_executor::AsyncExecutor,
audio,
dash_interface::{BoxDashInterface, RecenterMode},
locale::WayVRLangProvider,
timestep::{self, Timestep},
};
use crate::{
assets,
tab::{
apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, settings::TabSettings,
Tab, TabType,
},
tab::{Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, settings::TabSettings},
util::{
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
various::AsyncExecutor,
},
views,
};
pub struct FrontendWidgets {
pub id_label_time: WidgetID,
pub id_rect_content: WidgetID,
id_label_time: WidgetID,
id_rect_content: WidgetID,
id_sprite_titlebar_icon: WidgetID,
id_label_titlebar_title: WidgetID,
}
pub type FrontendTasks = Tasks<FrontendTask>;
pub struct Frontend<T> {
pub layout: Layout,
globals: WguiGlobals,
pub globals: WguiGlobals,
pub interface: BoxDashInterface<T>,
@ -77,9 +83,11 @@ pub struct FrontendUpdateResult {
pub sounds_to_play: Vec<SoundType>,
}
pub struct InitParams<T> {
pub struct InitParams<'a, T> {
pub interface: BoxDashInterface<T>,
pub lang_provider: &'a WayVRLangProvider,
pub has_monado: bool,
pub theme: Rc<WguiTheme>,
}
#[derive(Clone)]
@ -93,7 +101,7 @@ pub enum FrontendTask {
SetTab(TabType),
RefreshClock,
RefreshBackground,
MountPopup(MountPopupParams),
MountPopupOnce(MountPopupOnceParams),
RefreshPopupManager,
ShowAudioSettings,
UpdateAudioSettingsView,
@ -104,7 +112,7 @@ pub enum FrontendTask {
}
impl<T: 'static> Frontend<T> {
pub fn new(params: InitParams<T>, data: &mut T) -> anyhow::Result<Frontend<T>> {
pub fn new(params: InitParams<T>) -> anyhow::Result<Frontend<T>> {
let mut assets = Box::new(assets::Asset {});
let font_binary_bold = assets.load_from_path_gzip("Quicksand-Bold.ttf.gz")?;
@ -113,7 +121,7 @@ impl<T: 'static> Frontend<T> {
let globals = WguiGlobals::new(
assets,
wgui::globals::Defaults::default(),
params.lang_provider,
&WguiFontConfig {
binaries: vec![&font_binary_regular, &font_binary_bold, &font_binary_light],
family_name_sans_serif: "Quicksand",
@ -129,7 +137,10 @@ impl<T: 'static> Frontend<T> {
path: AssetPath::BuiltIn("gui/dashboard.xml"),
extra: Default::default(),
},
&LayoutParams { resize_to_parent: true },
LayoutParams {
resize_to_parent: true,
theme: params.theme,
},
)?;
let id_popup_manager = state.get_widget_id("popup_manager")?;
@ -144,6 +155,8 @@ impl<T: 'static> Frontend<T> {
let id_label_time = state.get_widget_id("label_time")?;
let id_rect_content = state.get_widget_id("rect_content")?;
let id_sprite_titlebar_icon = state.get_widget_id("sprite_titlebar_icon")?;
let id_label_titlebar_title = state.get_widget_id("label_titlebar_title")?;
let timestep = Timestep::new(60.0);
@ -157,6 +170,8 @@ impl<T: 'static> Frontend<T> {
widgets: FrontendWidgets {
id_label_time,
id_rect_content,
id_sprite_titlebar_icon,
id_label_titlebar_title,
},
timestep,
interface: params.interface,
@ -169,8 +184,8 @@ impl<T: 'static> Frontend<T> {
};
// init some things first
frontend.update_background(data)?;
frontend.update_time(data)?;
frontend.tasks.push(FrontendTask::RefreshBackground);
frontend.tasks.push(FrontendTask::RefreshClock);
Frontend::register_widgets(&mut frontend)?;
@ -183,10 +198,19 @@ impl<T: 'static> Frontend<T> {
fn play_sound(&mut self, audio_system: &mut audio::AudioSystem, sound_type: SoundType) -> anyhow::Result<()> {
let mut assets = self.globals.assets_builtin();
let sample = audio::AudioSample::from_mp3(&assets.load_from_path(match sound_type {
let path = match sound_type {
SoundType::Startup => "sound/startup.mp3",
SoundType::Launch => "sound/app_start.mp3",
})?)?;
};
// try loading a custom sound; if one doesn't exist (or it failed to load), use the built-in asset
let sound_bytes = match audio::AudioSample::try_bytes_from_config(path) {
Ok(bytes) => bytes,
Err(_) => assets.load_from_path(path)?.into(),
};
let sample = audio::AudioSample::from_mp3(&sound_bytes)?;
audio_system.play_sample(&sample);
Ok(())
}
@ -198,8 +222,10 @@ impl<T: 'static> Frontend<T> {
self.process_task(&mut params, task)?;
}
let time_ms = timestep::get_micros() / 1000;
if let Some(mut tab) = self.current_tab.take() {
tab.update(self, params.data)?;
tab.update(self, time_ms as u32, params.data)?;
self.current_tab = Some(tab);
}
@ -237,7 +263,7 @@ impl<T: 'static> Frontend<T> {
{
// always 30 times per second
while self.timestep.on_tick() {
self.toast_manager.tick(&self.globals, &mut self.layout)?;
self.toast_manager.tick(&mut self.layout)?;
}
}
@ -253,13 +279,12 @@ impl<T: 'static> Frontend<T> {
}
fn update_time(&mut self, data: &mut T) -> anyhow::Result<()> {
let mut c = self.layout.start_common();
let mut common = c.common();
{
let Some(mut label) = common.state.widgets.get_as::<WidgetLabel>(self.widgets.id_label_time) else {
anyhow::bail!("");
};
let mut common = self.layout.common();
let mut label = common
.state
.widgets
.cast_as::<WidgetLabel>(self.widgets.id_label_time)?;
let now = chrono::Local::now();
let hours = now.hour();
@ -276,31 +301,26 @@ impl<T: 'static> Frontend<T> {
label.set_text(&mut common, Translation::from_raw_text(&text));
}
c.finish()?;
Ok(())
}
fn mount_popup(&mut self, params: MountPopupParams, data: &mut T) -> anyhow::Result<()> {
fn mount_popup_once(&mut self, params: MountPopupOnceParams, data: &mut T) -> anyhow::Result<()> {
let config = self.interface.general_config(data);
self.popup_manager.mount_popup(
self.globals.clone(),
&mut self.layout,
self.tasks.clone(),
params,
config,
)?;
self
.popup_manager
.mount_popup_once(&self.globals, &mut self.layout, &self.tasks, params, config)?;
Ok(())
}
fn refresh_popup_manager(&mut self) -> anyhow::Result<()> {
let mut c = self.layout.start_common();
self.popup_manager.refresh(c.common().alterables);
c.finish()?;
self.popup_manager.refresh(&mut self.layout.alterables);
Ok(())
}
fn update_background(&mut self, data: &mut T) -> anyhow::Result<()> {
self.layout.mark_redraw();
let Some(mut rect) = self
.layout
.state
@ -327,7 +347,7 @@ impl<T: 'static> Frontend<T> {
FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?,
FrontendTask::RefreshClock => self.update_time(params.data)?,
FrontendTask::RefreshBackground => self.update_background(params.data)?,
FrontendTask::MountPopup(popup_params) => self.mount_popup(popup_params, params.data)?,
FrontendTask::MountPopupOnce(popup_params) => self.mount_popup_once(popup_params, params.data)?,
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
@ -339,17 +359,51 @@ impl<T: 'static> Frontend<T> {
Ok(())
}
fn set_tab_title(&mut self, translation: &str, icon: &str) -> anyhow::Result<()> {
let mut common = self.layout.common();
{
let mut label = common
.state
.widgets
.cast_as::<WidgetLabel>(self.widgets.id_label_titlebar_title)?;
label.set_text(&mut common, Translation::from_translation_key(translation));
}
{
let mut sprite = common
.state
.widgets
.cast_as::<WidgetSprite>(self.widgets.id_sprite_titlebar_icon)?;
sprite.set_content(
common.alterables,
Some(CustomGlyphData::from_assets(&self.globals, AssetPath::BuiltIn(icon))?),
);
}
Ok(())
}
fn set_tab(&mut self, data: &mut T, tab_type: TabType) -> anyhow::Result<()> {
log::info!("Setting tab to {tab_type:?}");
let widget_content = self.state.fetch_widget(&self.layout.state, "content")?;
self.layout.remove_children(widget_content.id);
let (tab_translation, icon_path) = match tab_type {
TabType::Home => ("HOME_SCREEN", "dashboard/home.svg"),
TabType::Apps => ("APPLICATIONS", "dashboard/apps.svg"),
TabType::Games => ("GAMES", "dashboard/games.svg"),
TabType::Monado => ("MONADO_RUNTIME", "dashboard/monado.svg"),
TabType::Settings => ("SETTINGS", "dashboard/settings.svg"),
};
self.set_tab_title(tab_translation, icon_path)?;
let tab: Box<dyn Tab<T>> = match tab_type {
TabType::Home => Box::new(TabHome::new(self, widget_content.id, data)?),
TabType::Apps => Box::new(TabApps::new(self, widget_content.id, data)?),
TabType::Games => Box::new(TabGames::new(self, widget_content.id)?),
TabType::Monado => Box::new(TabMonado::new(self, widget_content.id)?),
TabType::Processes => Box::new(TabProcesses::new(self, widget_content.id)?),
TabType::Settings => Box::new(TabSettings::new(self, widget_content.id, data)?),
};
@ -393,12 +447,6 @@ impl<T: 'static> Frontend<T> {
FrontendTask::SetTab(TabType::Monado),
);
// "Processes" side button
// self.tasks.handle_button(
// &self.state.fetch_component_as::<ComponentButton>("btn_side_processes")?,
// FrontendTask::SetTab(TabType::Processes),
// );
// "Settings" side button
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_side_settings")?,
@ -426,7 +474,6 @@ impl<T: 'static> Frontend<T> {
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
self.window_audio_settings.open(&mut WguiWindowParams {
globals: &self.globals,
position: Vec2::new(64.0, 64.0),
layout: &mut self.layout,
extra: WguiWindowParamsExtra {
@ -466,7 +513,7 @@ impl<T: 'static> Frontend<T> {
}
fn action_recenter_playspace(&mut self, data: &mut T) -> anyhow::Result<()> {
self.interface.recenter_playspace(data)?;
self.interface.recenter_playspace(data, RecenterMode::Recenter)?;
Ok(())
}

View File

@ -9,26 +9,20 @@ use wgui::{
assets::AssetPath,
components::button::{ButtonClickCallback, ComponentButton},
globals::WguiGlobals,
i18n::Translation,
layout::{WidgetID, WidgetPair},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
};
use wlx_common::desktop_finder::DesktopEntry;
use crate::{
frontend::{Frontend, FrontendTask, FrontendTasks},
frontend::{Frontend, FrontendTasks},
tab::{Tab, TabType},
util::popup_manager::{MountPopupParams, PopupHandle},
views::{self, app_launcher},
util::popup_manager::PopupHolder,
views::{self},
};
enum Task {
CloseLauncher,
}
struct State {
view_launcher: Option<(PopupHandle, views::app_launcher::View)>,
view_launcher: PopupHolder<views::app_launcher::View>,
}
pub struct TabApps<T> {
@ -37,7 +31,6 @@ pub struct TabApps<T> {
state: Rc<RefCell<State>>,
app_list: AppList,
tasks: Tasks<Task>,
marker: PhantomData<T>,
}
@ -46,22 +39,14 @@ impl<T> Tab<T> for TabApps<T> {
TabType::Apps
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
let mut state = self.state.borrow_mut();
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
let state = self.state.borrow_mut();
for task in self.tasks.drain() {
match task {
Task::CloseLauncher => state.view_launcher = None,
}
}
self.app_list.tick(frontend, &self.state, &mut self.parser_state)?;
self
.app_list
.tick(frontend, &self.state, &self.tasks, &mut self.parser_state)?;
if let Some((_, launcher)) = &mut state.view_launcher {
launcher.update(&mut frontend.interface, data)?;
}
state
.view_launcher
.with_view_res(|view| view.update(&mut frontend.interface, data))?;
Ok(())
}
}
@ -79,40 +64,14 @@ fn on_app_click(
globals: WguiGlobals,
entry: DesktopEntry,
state: Rc<RefCell<State>>,
tasks: Tasks<Task>,
) -> ButtonClickCallback {
Box::new(move |_common, _evt| {
frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_raw_text(&entry.app_name),
on_content: {
// this is awful
let state = state.clone();
let entry = entry.clone();
let globals = globals.clone();
let frontend_tasks = frontend_tasks.clone();
let tasks = tasks.clone();
Rc::new(move |data| {
let on_launched = {
let tasks = tasks.clone();
Box::new(move || tasks.push(Task::CloseLauncher))
};
let view = app_launcher::View::new(app_launcher::Params {
entry: entry.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
config: data.config,
on_launched,
})?;
state.borrow_mut().view_launcher = Some((data.handle, view));
Ok(())
})
},
}));
Rc::new(move |_common, _evt| {
views::app_launcher::mount_popup(
frontend_tasks.clone(),
globals.clone(),
entry.clone(),
state.borrow_mut().view_launcher.clone(),
);
Ok(())
})
}
@ -128,8 +87,9 @@ fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
impl<T> TabApps<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone();
let tasks = Tasks::new();
let state = Rc::new(RefCell::new(State { view_launcher: None }));
let state = Rc::new(RefCell::new(State {
view_launcher: Default::default(),
}));
let parser_state = wgui::parser::parse_from_assets(&doc_params(globals.clone()), &mut frontend.layout, parent_id)?;
let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?;
@ -157,7 +117,6 @@ impl<T> TabApps<T> {
app_list,
parser_state,
state,
tasks,
marker: PhantomData,
})
}
@ -286,7 +245,7 @@ impl AppList {
let mut params = HashMap::<Rc<str>, Rc<str>>::new();
params.insert("text".into(), category_name.into());
parser_state.parse_template(
parser_state.realize_template(
doc_params,
"CategoryText",
&mut frontend.layout,
@ -318,7 +277,7 @@ impl AppList {
);
params.insert("name".into(), entry.app_name.clone());
let data = parser_state.parse_template(
let data = parser_state.realize_template(
doc_params,
"AppEntry",
&mut frontend.layout,
@ -334,11 +293,10 @@ impl AppList {
&mut self,
frontend: &mut Frontend<T>,
state: &Rc<RefCell<State>>,
tasks: &Tasks<Task>,
parser_state: &mut ParserState,
) -> anyhow::Result<()> {
// load 4 entries for a single frame at most
for _ in 0..4 {
// load 30 entries for a single frame at most
for _ in 0..30 {
if let Some(entry) = self.entries_to_mount.pop_front() {
let globals = frontend.layout.state.globals.clone();
let button = self.mount_entry(frontend, parser_state, &doc_params(globals.clone()), &entry)?;
@ -348,7 +306,6 @@ impl AppList {
globals.clone(),
entry.clone(),
state.clone(),
tasks.clone(),
));
} else {
break;

View File

@ -9,7 +9,8 @@ use wgui::{
use crate::{
frontend::Frontend,
tab::{Tab, TabType},
views::game_list,
util::steam_utils::SteamUtils,
views::{ViewTrait, ViewUpdateParams, game_list, running_games_list},
};
pub struct TabGames<T> {
@ -17,6 +18,7 @@ pub struct TabGames<T> {
pub state: ParserState,
view_game_list: game_list::View,
view_running_games_list: running_games_list::View,
marker: PhantomData<T>,
}
@ -25,17 +27,32 @@ impl<T> Tab<T> for TabGames<T> {
TabType::Games
}
fn update(&mut self, frontend: &mut Frontend<T>, _data: &mut T) -> anyhow::Result<()> {
self.view_game_list.update(&mut frontend.layout, &frontend.executor)?;
fn update(&mut self, frontend: &mut Frontend<T>, time_ms: u32, data: &mut T) -> anyhow::Result<()> {
let mut config_change_kind = None;
self.view_game_list.update(&mut ViewUpdateParams {
layout: &mut frontend.layout,
executor: &frontend.executor,
general_config: frontend.interface.general_config(data),
config_change_kind: &mut config_change_kind,
})?;
if let Some(kind) = config_change_kind {
frontend.interface.config_changed(data, kind);
}
self.view_running_games_list.update(&mut frontend.layout, time_ms)?;
Ok(())
}
}
impl<T> TabGames<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone();
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: frontend.layout.state.globals.clone(),
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/games.xml"),
extra: Default::default(),
},
@ -44,18 +61,31 @@ impl<T> TabGames<T> {
)?;
let game_list_parent = state.get_widget_id("game_list_parent")?;
let id_running_games_list_parent = state.get_widget_id("running_games_list_parent")?;
let mut steam_utils = SteamUtils::new()?;
let view_game_list = game_list::View::new(game_list::Params {
executor: frontend.executor.clone(),
frontend_tasks: frontend.tasks.clone(),
globals: frontend.layout.state.globals.clone(),
globals: globals.clone(),
layout: &mut frontend.layout,
parent_id: game_list_parent,
steam_utils: &steam_utils,
})?;
let view_running_games_list = running_games_list::View::new(running_games_list::Params {
globals: globals.clone(),
layout: &mut frontend.layout,
parent_id: id_running_games_list_parent,
steam_utils: &mut steam_utils,
frontend_tasks: frontend.tasks.clone(),
})?;
Ok(Self {
state,
view_game_list,
view_running_games_list,
marker: PhantomData,
})
}

View File

@ -59,9 +59,12 @@ impl<T> TabHome<T> {
parent_id,
)?;
let mut c = frontend.layout.start_common();
let widget_label = state.fetch_widget(&c.layout.state, "label_hello")?.widget;
configure_label_hello(&mut c.common(), widget_label, frontend.interface.general_config(data));
let widget_label = state.fetch_widget(&frontend.layout.state, "label_hello")?.widget;
configure_label_hello(
&mut frontend.layout.common(),
widget_label,
frontend.interface.general_config(data),
);
let btn_apps = state.fetch_component_as::<ComponentButton>("btn_apps")?;
let btn_games = state.fetch_component_as::<ComponentButton>("btn_games")?;

View File

@ -4,7 +4,6 @@ pub mod apps;
pub mod games;
pub mod home;
pub mod monado;
pub mod processes;
pub mod settings;
#[derive(Clone, Copy, Debug)]
@ -13,7 +12,6 @@ pub enum TabType {
Apps,
Games,
Monado,
Processes,
Settings,
}
@ -21,7 +19,7 @@ pub trait Tab<T> {
#[allow(dead_code)]
fn get_type(&self) -> TabType;
fn update(&mut self, _: &mut Frontend<T>, _: &mut T) -> anyhow::Result<()> {
fn update(&mut self, _frontend: &mut Frontend<T>, _time_ms: u32, _user_data: &mut T) -> anyhow::Result<()> {
Ok(())
}
}

View File

@ -2,24 +2,133 @@ use std::{collections::HashMap, marker::PhantomData, rc::Rc};
use wgui::{
assets::AssetPath,
components::{checkbox::ComponentCheckbox, slider::ComponentSlider},
components::{
bar_graph::{ComponentBarGraph, ValueCell},
button::ComponentButton,
checkbox::ComponentCheckbox,
color_selector::{ColorSelectorChangedCallback, ComponentColorSelector},
slider::{ComponentSlider, SliderValueChangedCallback},
tabs::ComponentTabs,
},
drawing::Color,
globals::WguiGlobals,
layout::WidgetID,
layout::{Layout, WidgetID},
parser::{self, Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
};
use wlx_common::dash_interface;
use wlx_common::{
config::GeneralConfig,
dash_interface::{self, ConfigChangeKind, MonadoDumpSessionFrame},
};
use crate::{
frontend::Frontend,
tab::{Tab, TabType},
};
#[derive(Debug)]
#[derive(Clone)]
enum TabNameEnum {
GeneralSettings,
ProcessList,
DebugTimings,
}
impl TabNameEnum {
fn from_string(s: &str) -> Option<Self> {
match s {
"general_settings" => Some(TabNameEnum::GeneralSettings),
"process_list" => Some(TabNameEnum::ProcessList),
"debug_timings" => Some(TabNameEnum::DebugTimings),
_ => None,
}
}
}
#[derive(Clone)]
enum Task {
Refresh,
FocusClient(String),
SetBrightness(f32),
SetTab(TabNameEnum),
// `ProcessList` tab
ProcessListRefresh,
ProcessListFocusClient(String),
// `DebugTimings` tab
DebugTimingsRefreshSessionList,
DebugTimingsSetSessionId(i64),
GeneralSettingsChromaUpdate,
}
struct SubtabProcessList {
id_list_parent: WidgetID,
state: ParserState,
cells: Vec<parser::ParserData>,
}
struct SubtabGeneralSettings {
#[allow(dead_code)]
state: ParserState,
slider_keying_curve: Rc<ComponentSlider>,
slider_keying_despill: Rc<ComponentSlider>,
slider_keying_hue_range: Rc<ComponentSlider>,
slider_keying_saturation_range: Rc<ComponentSlider>,
slider_keying_value_range: Rc<ComponentSlider>,
cs_keying: Rc<ComponentColorSelector>,
}
struct DebugGraph {
graph: Rc<ComponentBarGraph>,
}
struct DebugSessionList {
#[allow(dead_code)]
buttons: Vec<Rc<ComponentButton>>,
}
struct TimingsSession {
resolved_name: Option<String>,
last_frame: MonadoDumpSessionFrame,
}
struct Graphs {
predicted_display_time: DebugGraph,
predicted_frame_time: DebugGraph,
predicted_wake_up_time: DebugGraph,
predicted_gpu_done_time: DebugGraph,
predicted_display_period: DebugGraph,
display_time: DebugGraph,
when_predicted: DebugGraph,
when_wait_woke: DebugGraph,
when_begin: DebugGraph,
when_delivered: DebugGraph,
when_gpu_done: DebugGraph,
}
type SessionsMap = HashMap<i64 /* session id */, TimingsSession>;
struct SubtabDebugTimings {
#[allow(dead_code)]
state: ParserState,
graphs: Option<Graphs>,
session_list: DebugSessionList,
selected_session_id: Option<i64>,
id_sessions_list_parent: WidgetID,
id_timings_parent: WidgetID,
sessions: SessionsMap,
}
#[allow(dead_code)]
#[allow(clippy::large_enum_variant)]
enum Subtab {
Empty,
GeneralSettings(SubtabGeneralSettings),
ProcessList(SubtabProcessList),
DebugTimings(SubtabDebugTimings),
}
pub struct TabMonado<T> {
@ -29,10 +138,8 @@ pub struct TabMonado<T> {
marker: PhantomData<T>,
globals: WguiGlobals,
id_list_parent: WidgetID,
cells: Vec<parser::ParserData>,
id_content: WidgetID,
subtab: Subtab,
ticks: u32,
}
@ -42,18 +149,75 @@ impl<T> Tab<T> for TabMonado<T> {
TabType::Games
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::Refresh => self.refresh(frontend, data)?,
Task::FocusClient(name) => self.focus_client(frontend, data, name)?,
Task::ProcessListRefresh => {
if let Subtab::ProcessList(process_list) = &mut self.subtab {
process_list.refresh(frontend, data, &self.tasks)?;
}
}
Task::ProcessListFocusClient(client_name) => {
if let Subtab::ProcessList(process_list) = &mut self.subtab {
process_list.focus_client(frontend, data, client_name, &self.tasks)?;
}
}
Task::DebugTimingsRefreshSessionList => {
if let Subtab::DebugTimings(tab) = &mut self.subtab {
tab.refresh_session_list(&mut frontend.layout, &self.tasks)?;
}
}
Task::DebugTimingsSetSessionId(session_id) => {
if let Subtab::DebugTimings(tab) = &mut self.subtab {
tab.set_session_id(&mut frontend.layout, session_id)?;
}
}
Task::GeneralSettingsChromaUpdate => {
if let Subtab::GeneralSettings(tab) = &mut self.subtab {
tab.chroma_update(frontend.interface.general_config(data));
frontend
.interface
.config_changed(data, ConfigChangeKind::EnvironmentBlend);
}
}
Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness),
Task::SetTab(tab) => {
frontend.layout.remove_children(self.id_content);
match tab {
TabNameEnum::GeneralSettings => {
self.subtab = Subtab::GeneralSettings(SubtabGeneralSettings::new(
self.id_content,
frontend,
data,
&self.tasks,
)?)
}
TabNameEnum::ProcessList => {
self.tasks.push(Task::ProcessListRefresh);
self.subtab = Subtab::ProcessList(SubtabProcessList::new(self.id_content, frontend)?)
}
TabNameEnum::DebugTimings => {
self.subtab = Subtab::DebugTimings(SubtabDebugTimings::new(self.id_content, frontend, &self.tasks)?)
}
}
}
}
}
// every few seconds
if self.ticks.is_multiple_of(500) {
self.tasks.push(Task::Refresh);
match &mut self.subtab {
Subtab::Empty => {}
Subtab::GeneralSettings(_) => {}
Subtab::ProcessList(_) => {
// every few seconds
if let Subtab::ProcessList(_) = &self.subtab
&& self.ticks.is_multiple_of(500)
{
self.tasks.push(Task::ProcessListRefresh);
}
}
Subtab::DebugTimings(timings) => {
timings.update(&self.tasks, data, frontend)?;
}
}
self.ticks += 1;
@ -62,7 +226,7 @@ impl<T> Tab<T> for TabMonado<T> {
}
}
fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
fn doc_params_monado(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado.xml"),
@ -70,6 +234,30 @@ fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
}
}
fn doc_params_tab_process_list(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado_tab_process_list.xml"),
extra: Default::default(),
}
}
fn doc_params_tab_general_settings(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado_tab_general_settings.xml"),
extra: Default::default(),
}
}
fn doc_params_tab_debug_timings(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado_tab_debug_timings.xml"),
extra: Default::default(),
}
}
fn yesno(n: bool) -> &'static str {
match n {
true => "yes",
@ -77,29 +265,393 @@ fn yesno(n: bool) -> &'static str {
}
}
impl<T> TabMonado<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone();
let state = wgui::parser::parse_from_assets(&doc_params(&globals), &mut frontend.layout, parent_id)?;
const SLIDER_MULTIPLIER: f32 = 100.0;
let id_list_parent = state.get_widget_id("list_parent")?;
impl SubtabGeneralSettings {
fn new<T>(
parent_id: WidgetID,
frontend: &mut Frontend<T>,
data: &mut T,
tasks: &Tasks<Task>,
) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&doc_params_tab_general_settings(&frontend.globals),
&mut frontend.layout,
parent_id,
)?;
let tasks = Tasks::<Task>::new();
// get brightness
let slider_brightness = state.fetch_component_as::<ComponentSlider>("slider_brightness")?;
if let Some(brightness) = frontend.interface.monado_brightness_get(data) {
slider_brightness.set_value_primary(&mut frontend.layout.common(), brightness * 100.0);
tasks.push(Task::Refresh);
slider_brightness.on_value_changed({
let tasks = tasks.clone();
Box::new(move |_common, e| {
tasks.push(Task::SetBrightness(e.value / 100.0));
})
});
}
let config = frontend.interface.general_config(data);
let cs_keying = state.fetch_component_as::<ComponentColorSelector>("cs_keying")?;
let slider_keying_despill = state.fetch_component_as::<ComponentSlider>("slider_keying_despill")?;
let slider_keying_curve = state.fetch_component_as::<ComponentSlider>("slider_keying_curve")?;
let slider_keying_hue_range = state.fetch_component_as::<ComponentSlider>("slider_keying_hue_range")?;
let slider_keying_saturation_range =
state.fetch_component_as::<ComponentSlider>("slider_keying_saturation_range")?;
let slider_keying_value_range = state.fetch_component_as::<ComponentSlider>("slider_keying_value_range")?;
{
let mut common = frontend.layout.common();
// set initial values
let (rgb, range_h, range_s, range_v) = config.chroma_key_params.get_rgb_and_hsv_ranges();
slider_keying_curve.set_value_primary(&mut common, config.chroma_key_params.curve * SLIDER_MULTIPLIER);
slider_keying_despill.set_value_primary(&mut common, config.chroma_key_params.despill * SLIDER_MULTIPLIER);
slider_keying_hue_range.set_value_primary(&mut common, range_h * SLIDER_MULTIPLIER);
slider_keying_saturation_range.set_value_primary(&mut common, range_s * SLIDER_MULTIPLIER);
slider_keying_value_range.set_value_primary(&mut common, range_v * SLIDER_MULTIPLIER);
cs_keying.set_color(&mut common, rgb);
// prepare callbacks
fn get_slider_callback(tasks: &Tasks<Task>) -> SliderValueChangedCallback {
Box::new({
let tasks = tasks.clone();
move |_, _| {
tasks.push(Task::GeneralSettingsChromaUpdate);
}
})
}
fn get_color_selector_callback(tasks: &Tasks<Task>) -> ColorSelectorChangedCallback {
Box::new({
let tasks = tasks.clone();
move |_, _| {
tasks.push(Task::GeneralSettingsChromaUpdate);
}
})
}
slider_keying_curve.on_value_changed(get_slider_callback(tasks));
slider_keying_despill.on_value_changed(get_slider_callback(tasks));
slider_keying_hue_range.on_value_changed(get_slider_callback(tasks));
slider_keying_saturation_range.on_value_changed(get_slider_callback(tasks));
slider_keying_value_range.on_value_changed(get_slider_callback(tasks));
cs_keying.on_changed(get_color_selector_callback(tasks));
}
Ok(Self {
state,
marker: PhantomData,
slider_keying_curve,
slider_keying_despill,
slider_keying_hue_range,
slider_keying_saturation_range,
slider_keying_value_range,
cs_keying,
})
}
fn chroma_update(&mut self, config: &mut GeneralConfig) {
let val_curve = self.slider_keying_curve.get_value_primary();
let val_despill = self.slider_keying_despill.get_value_primary();
let val_range_h = self.slider_keying_hue_range.get_value_primary();
let val_range_s = self.slider_keying_saturation_range.get_value_primary();
let val_range_v = self.slider_keying_value_range.get_value_primary();
let val_rgb = self.cs_keying.get_color();
config.chroma_key_params.despill = val_despill / SLIDER_MULTIPLIER;
config.chroma_key_params.curve = val_curve / SLIDER_MULTIPLIER;
config.chroma_key_params.update_hsv_range_from_rgb(
val_rgb,
val_range_h / SLIDER_MULTIPLIER,
val_range_s / SLIDER_MULTIPLIER,
val_range_v / SLIDER_MULTIPLIER,
);
}
}
fn mount_sessions_list(
state: &mut ParserState,
layout: &mut Layout,
tasks: &Tasks<Task>,
id_parent: WidgetID,
sessions: &SessionsMap,
) -> anyhow::Result<DebugSessionList> {
let mut buttons = Vec::new();
let globals = layout.state.globals.clone();
layout.remove_children(id_parent);
for (session_id, session) in sessions {
let mut params = HashMap::new();
params.insert(
Rc::from("text"),
Rc::from(format!(
"{} (ID {})",
session.resolved_name.as_ref().map_or("Unknown", |s| s.as_str()),
session_id,
)),
);
let data = state.realize_template(
&doc_params_tab_debug_timings(&globals),
"SessionButton",
layout,
id_parent,
params,
)?;
let button = data.fetch_component_as::<ComponentButton>("button")?;
button.on_click({
let tasks = tasks.clone();
let session_id = *session_id;
Rc::new(move |_, _| {
tasks.push(Task::DebugTimingsSetSessionId(session_id));
Ok(())
})
});
buttons.push(button);
}
Ok(DebugSessionList { buttons })
}
fn mount_graph(
state: &mut ParserState,
layout: &mut Layout,
id_parent: WidgetID,
name: &'static str,
limits: (f32, f32),
) -> anyhow::Result<DebugGraph> {
let globals = layout.state.globals.clone();
let mut params = HashMap::new();
params.insert(Rc::from("name"), Rc::from(name));
params.insert(Rc::from("limit_min"), Rc::from(limits.0.to_string()));
params.insert(Rc::from("limit_max"), Rc::from(limits.1.to_string()));
let data = state.realize_template(
&doc_params_tab_debug_timings(&globals),
"DebugGraph",
layout,
id_parent,
params,
)?;
let graph = data.fetch_component_as::<ComponentBarGraph>("graph")?;
Ok(DebugGraph { graph })
}
fn ns_to_ms(ns: i64) -> f32 {
(ns / 1000) as f32 / 1000.0
}
impl SubtabDebugTimings {
fn new<T>(parent_id: WidgetID, frontend: &mut Frontend<T>, tasks: &Tasks<Task>) -> anyhow::Result<Self> {
let mut state = wgui::parser::parse_from_assets(
&doc_params_tab_debug_timings(&frontend.globals),
&mut frontend.layout,
parent_id,
)?;
let id_timings_parent = state.get_widget_id("timings_parent")?;
let id_sessions_list_parent = state.get_widget_id("session_list_parent")?;
let sessions = Default::default();
let session_list = mount_sessions_list(
&mut state,
&mut frontend.layout,
tasks,
globals,
id_sessions_list_parent,
&sessions,
)?;
Ok(Self {
state,
graphs: None,
session_list,
id_sessions_list_parent,
id_timings_parent,
sessions,
selected_session_id: None,
})
}
fn set_session_id(&mut self, layout: &mut Layout, session_id: i64) -> anyhow::Result<()> {
layout.remove_children(self.id_timings_parent);
let mut graph = |name: &'static str, limits: (f32, f32)| -> anyhow::Result<DebugGraph> {
mount_graph(&mut self.state, layout, self.id_timings_parent, name, limits)
};
// populate graphs
self.graphs = Some(Graphs {
predicted_display_time: graph("Predicted display time", (0.0, 30.0))?,
predicted_frame_time: graph("Predicted frame time", (0.0, 30.0))?,
predicted_wake_up_time: graph("Predicted wake-up time", (0.0, 30.0))?,
predicted_gpu_done_time: graph("Predicted GPU done time", (0.0, 30.0))?,
predicted_display_period: graph("Predicted display period", (0.0, 30.0))?,
display_time: graph("Display time", (0.0, 30.0))?,
when_predicted: graph("When predicted", (0.0, 30.0))?,
when_wait_woke: graph("When wait woke", (0.0, 30.0))?,
when_begin: graph("When begin", (0.0, 30.0))?,
when_delivered: graph("When delivered", (0.0, 30.0))?,
when_gpu_done: graph("When GPU done", (0.0, 30.0))?,
});
self.selected_session_id = Some(session_id);
Ok(())
}
fn refresh_session_list(&mut self, layout: &mut Layout, tasks: &Tasks<Task>) -> anyhow::Result<()> {
self.session_list = mount_sessions_list(
&mut self.state,
layout,
tasks,
self.id_sessions_list_parent,
&self.sessions,
)?;
Ok(())
}
fn update<T>(&mut self, tasks: &Tasks<Task>, data: &mut T, frontend: &mut Frontend<T>) -> anyhow::Result<()> {
if !frontend.interface.monado_metrics_set_enabled(data, true) {
return Ok(());
}
let frames = frontend.interface.monado_metrics_dump_session_frames(data);
if frames.is_empty() {
return Ok(());
}
let col_green = Color::new(0.0, 1.0, 0.0, 1.0);
for frame in frames {
//log::info!("{:?}", frame);
match self.sessions.get_mut(&frame.session_id) {
Some(session) => {
if let Some(graphs) = &mut self.graphs
&& let Some(selected_session_id) = self.selected_session_id
&& selected_session_id == frame.session_id
{
let predicted_display_time = ns_to_ms(session.last_frame.predicted_display_time_ns as i64);
let predicted_frame_time = ns_to_ms(frame.predicted_frame_time_ns as i64);
let predicted_wake_up_time =
ns_to_ms(frame.predicted_wake_up_time_ns as i64 - session.last_frame.predicted_wake_up_time_ns as i64);
let predicted_gpu_done_time =
ns_to_ms(frame.predicted_gpu_done_time_ns as i64 - session.last_frame.predicted_gpu_done_time_ns as i64);
let predicted_display_period = ns_to_ms(session.last_frame.predicted_display_period_ns as i64); // 6.944 ms for 144Hz
let display_time = ns_to_ms(frame.display_time_ns as i64 - session.last_frame.display_time_ns as i64);
let when_predicted = ns_to_ms(frame.when_predicted_ns as i64 - session.last_frame.when_predicted_ns as i64);
let when_wait_woke = ns_to_ms(frame.when_wait_woke_ns as i64 - session.last_frame.when_wait_woke_ns as i64);
let when_begin = ns_to_ms(frame.when_begin_ns as i64 - session.last_frame.when_begin_ns as i64);
let when_delivered = ns_to_ms(frame.when_delivered_ns as i64 - session.last_frame.when_delivered_ns as i64);
let when_gpu_done = ns_to_ms(frame.when_gpu_done_ns as i64 - session.last_frame.when_gpu_done_ns as i64);
graphs.predicted_display_time.graph.push_value(ValueCell {
value: predicted_display_time,
color: col_green,
});
graphs.predicted_frame_time.graph.push_value(ValueCell {
value: predicted_frame_time,
color: col_green,
});
graphs.predicted_wake_up_time.graph.push_value(ValueCell {
value: predicted_wake_up_time,
color: col_green,
});
graphs.predicted_gpu_done_time.graph.push_value(ValueCell {
value: predicted_gpu_done_time,
color: col_green,
});
graphs.predicted_display_period.graph.push_value(ValueCell {
value: predicted_display_period,
color: col_green,
});
graphs.display_time.graph.push_value(ValueCell {
value: display_time,
color: col_green,
});
graphs.when_predicted.graph.push_value(ValueCell {
value: when_predicted,
color: col_green,
});
graphs.when_wait_woke.graph.push_value(ValueCell {
value: when_wait_woke,
color: col_green,
});
graphs.when_begin.graph.push_value(ValueCell {
value: when_begin,
color: col_green,
});
graphs.when_delivered.graph.push_value(ValueCell {
value: when_delivered,
color: col_green,
});
graphs.when_gpu_done.graph.push_value(ValueCell {
value: when_gpu_done,
color: col_green,
});
}
session.last_frame = frame;
}
None => {
self.sessions.insert(
frame.session_id,
TimingsSession {
last_frame: frame,
resolved_name: None, /* TODO! find client ID from session ID */
},
);
tasks.push(Task::DebugTimingsRefreshSessionList);
}
}
}
frontend.layout.mark_redraw();
Ok(())
}
}
impl SubtabProcessList {
fn new<T>(parent_id: WidgetID, frontend: &mut Frontend<T>) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&doc_params_tab_process_list(&frontend.globals),
&mut frontend.layout,
parent_id,
)?;
let id_list_parent = state.get_widget_id("list_parent")?;
Ok(Self {
state,
id_list_parent,
ticks: 0,
cells: Vec::new(),
})
}
fn mount_client(&mut self, frontend: &mut Frontend<T>, client: &dash_interface::MonadoClient) -> anyhow::Result<()> {
fn mount_client(
&mut self,
layout: &mut Layout,
client: &dash_interface::MonadoClient,
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
let mut par = HashMap::<Rc<str>, Rc<str>>::new();
par.insert(
"checked".into(),
@ -109,7 +661,10 @@ impl<T> TabMonado<T> {
Rc::from("0")
},
);
par.insert("name".into(), client.name.clone().into());
par.insert(
"name".into(),
format!("{} (Client ID: {})", client.name, client.id).into(),
);
par.insert("flag_active".into(), yesno(client.is_active).into());
par.insert("flag_focused".into(), yesno(client.is_focused).into());
par.insert("flag_io_active".into(), yesno(client.is_io_active).into());
@ -117,21 +672,23 @@ impl<T> TabMonado<T> {
par.insert("flag_primary".into(), yesno(client.is_primary).into());
par.insert("flag_visible".into(), yesno(client.is_visible).into());
let state_cell = self.state.parse_template(
&doc_params(&self.globals),
let globals = layout.state.globals.clone();
let state_cell = self.state.realize_template(
&doc_params_tab_process_list(&globals),
"Cell",
&mut frontend.layout,
layout,
self.id_list_parent,
par,
)?;
let checkbox = state_cell.fetch_component_as::<ComponentCheckbox>("checkbox")?;
checkbox.on_toggle({
let tasks = self.tasks.clone();
let tasks = tasks.clone();
let client_name = client.name.clone();
Box::new(move |_common, e| {
if e.checked {
tasks.push(Task::FocusClient(client_name.clone()));
tasks.push(Task::ProcessListFocusClient(client_name.clone()));
}
Ok(())
})
@ -142,41 +699,63 @@ impl<T> TabMonado<T> {
Ok(())
}
fn refresh(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
fn focus_client<T>(
&mut self,
frontend: &mut Frontend<T>,
data: &mut T,
name: String,
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
frontend.interface.monado_client_focus(data, &name)?;
tasks.push(Task::ProcessListRefresh);
Ok(())
}
fn refresh<T>(&mut self, frontend: &mut Frontend<T>, data: &mut T, tasks: &Tasks<Task>) -> anyhow::Result<()> {
log::debug!("refreshing monado client list");
let clients = frontend.interface.monado_client_list(data)?;
let clients = frontend.interface.monado_client_list(data, true)?;
frontend.layout.remove_children(self.id_list_parent);
self.cells.clear();
for client in clients {
self.mount_client(frontend, &client)?;
}
// get brightness
let slider_brightness = self.state.fetch_component_as::<ComponentSlider>("slider_brightness")?;
if let Some(brightness) = frontend.interface.monado_brightness_get(data) {
let mut c = frontend.layout.start_common();
slider_brightness.set_value(&mut c.common(), brightness * 100.0);
c.finish()?;
slider_brightness.on_value_changed({
let tasks = self.tasks.clone();
Box::new(move |_common, e| {
tasks.push(Task::SetBrightness(e.value / 100.0));
Ok(())
})
});
self.mount_client(&mut frontend.layout, &client, tasks)?;
}
Ok(())
}
}
fn focus_client(&mut self, frontend: &mut Frontend<T>, data: &mut T, name: String) -> anyhow::Result<()> {
frontend.interface.monado_client_focus(data, &name)?;
self.tasks.push(Task::Refresh);
Ok(())
impl<T> TabMonado<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone();
let state = wgui::parser::parse_from_assets(&doc_params_monado(&globals), &mut frontend.layout, parent_id)?;
let id_content = state.get_widget_id("content")?;
let tabs = state.fetch_component_as::<ComponentTabs>("tabs")?;
let tasks = Tasks::<Task>::new();
tabs.on_select({
let tasks = tasks.clone();
Rc::new(move |_common, evt| {
if let Some(tab) = TabNameEnum::from_string(&evt.name) {
tasks.push(Task::SetTab(tab));
}
Ok(())
})
});
tasks.push(Task::SetTab(TabNameEnum::ProcessList));
Ok(Self {
state,
marker: PhantomData,
tasks,
id_content,
ticks: 0,
subtab: Subtab::Empty,
})
}
fn set_brightness(&mut self, frontend: &mut Frontend<T>, data: &mut T, brightness: f32) {

View File

@ -1,70 +0,0 @@
use std::marker::PhantomData;
use wgui::{
assets::AssetPath,
layout::WidgetID,
parser::{Fetchable, ParseDocumentParams, ParserState},
};
use crate::{
frontend::Frontend,
tab::{Tab, TabType},
views::{process_list, window_list},
};
pub struct TabProcesses<T> {
#[allow(dead_code)]
pub state: ParserState,
view_window_list: window_list::View,
view_process_list: process_list::View,
marker: PhantomData<T>,
}
impl<T> Tab<T> for TabProcesses<T> {
fn get_type(&self) -> TabType {
TabType::Games
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
self
.view_window_list
.update(&mut frontend.layout, &mut frontend.interface, data)?;
self
.view_process_list
.update(&mut frontend.layout, &mut frontend.interface, data)?;
Ok(())
}
}
impl<T> TabProcesses<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone();
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/processes.xml"),
extra: Default::default(),
},
&mut frontend.layout,
parent_id,
)?;
Ok(Self {
view_window_list: window_list::View::new(window_list::Params {
layout: &mut frontend.layout,
parent_id: state.get_widget_id("window_list_parent")?,
globals: globals.clone(),
frontend_tasks: frontend.tasks.clone(),
on_click: None,
})?,
view_process_list: process_list::View::new(process_list::Params {
layout: &mut frontend.layout,
parent_id: state.get_widget_id("process_list_parent")?,
globals,
})?,
state,
marker: PhantomData,
})
}
}

View File

@ -1,736 +0,0 @@
use std::{collections::HashMap, marker::PhantomData, rc::Rc, str::FromStr};
use glam::Vec2;
use strum::{AsRefStr, EnumProperty, EnumString, VariantArray};
use wgui::{
assets::AssetPath,
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
event::{CallbackDataCommon, EventAlterables},
i18n::Translation,
layout::{Layout, WidgetID},
log::LogErr,
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult},
};
use wlx_common::{config::GeneralConfig, config_io::ConfigRoot};
use crate::{
frontend::{Frontend, FrontendTask},
tab::{Tab, TabType},
};
enum Task {
UpdateBool(SettingType, bool),
UpdateFloat(SettingType, f32),
UpdateInt(SettingType, i32),
OpenContextMenu(Vec2, Vec<context_menu::Cell>),
ClearPipewireTokens,
ClearSavedState,
DeleteAllConfigs,
RestartSoftware,
RemoveAutostartApp(Rc<str>),
}
pub struct TabSettings<T> {
pub state: ParserState,
app_button_ids: Vec<Rc<str>>,
context_menu: ContextMenu,
tasks: Tasks<Task>,
marker: PhantomData<T>,
}
impl<T> Tab<T> for TabSettings<T> {
fn get_type(&self) -> TabType {
TabType::Settings
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
let config = frontend.interface.general_config(data);
let mut changed = false;
for task in self.tasks.drain() {
match task {
Task::UpdateBool(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task));
*setting.mut_bool(config) = n;
changed = true;
}
Task::UpdateFloat(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task));
*setting.mut_f32(config) = n;
changed = true;
}
Task::UpdateInt(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task));
*setting.mut_i32(config) = n;
changed = true;
}
Task::ClearPipewireTokens => {
let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("pw_tokens.yaml"))
.log_err("Could not remove pw_tokens.yaml");
}
Task::ClearSavedState => {
let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("zz-saved-state.json5"))
.log_err("Could not remove zz-saved-state.json5");
}
Task::DeleteAllConfigs => {
let path = ConfigRoot::Generic.get_conf_d_path();
std::fs::remove_dir_all(&path)?;
std::fs::create_dir(&path)?;
}
Task::RestartSoftware => {
frontend.interface.restart(data);
return Ok(());
}
Task::OpenContextMenu(position, cells) => {
self.context_menu.open(context_menu::OpenParams {
on_custom_attribs: None,
position,
blueprint: Blueprint::Cells(cells),
});
}
Task::RemoveAutostartApp(button_id) => {
if let (Some(idx), Ok(widget)) = (
self.app_button_ids.iter().position(|x| button_id.eq(x)),
self.state.get_widget_id(&format!("{button_id}_root")),
) {
self.app_button_ids.remove(idx);
config.autostart_apps.remove(idx);
frontend.layout.remove_widget(widget);
changed = true;
}
}
}
}
// Dropdown handling
if let TickResult::Action(name) = self.context_menu.tick(&mut frontend.layout, &mut self.state)? {
if let (Some(setting), Some(id), Some(value), Some(text), Some(translated)) = {
let mut s = name.splitn(5, ';');
(s.next(), s.next(), s.next(), s.next(), s.next())
} {
let mut label = self
.state
.fetch_widget_as::<WidgetLabel>(&frontend.layout.state, &format!("{id}_value"))?;
let mut alterables = EventAlterables::default();
let mut common = CallbackDataCommon {
alterables: &mut alterables,
state: &frontend.layout.state,
};
let translation = Translation {
text: text.into(),
translated: translated == "1",
};
label.set_text(&mut common, translation);
let setting = SettingType::from_str(setting).expect("Invalid Enum string");
setting.set_enum(config, value);
changed = true;
}
}
// Notify overlays of the change
if changed {
frontend.interface.config_changed(data);
}
Ok(())
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, AsRefStr, EnumString)]
enum SettingType {
AnimationSpeed,
RoundMultiplier,
InvertScrollDirectionX,
InvertScrollDirectionY,
ScrollSpeed,
LongPressDuration,
NotificationsEnabled,
NotificationsSoundEnabled,
KeyboardSoundEnabled,
UprightScreenFix,
DoubleCursorFix,
SetsOnWatch,
HideGrabHelp,
XrClickSensitivity,
XrClickSensitivityRelease,
AllowSliding,
ClickFreezeTimeMs,
FocusFollowsMouseMode,
LeftHandedMouse,
BlockGameInput,
BlockGameInputIgnoreWatch,
SpaceDragMultiplier,
UseSkybox,
UsePassthrough,
ScreenRenderDown,
PointerLerpFactor,
SpaceDragUnlocked,
SpaceRotateUnlocked,
Clock12h,
HideUsername,
OpaqueBackground,
XwaylandByDefault,
CaptureMethod,
KeyboardMiddleClick,
}
impl SettingType {
pub fn mut_bool<'a>(self, config: &'a mut GeneralConfig) -> &'a mut bool {
match self {
Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x,
Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y,
Self::NotificationsEnabled => &mut config.notifications_enabled,
Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled,
Self::KeyboardSoundEnabled => &mut config.keyboard_sound_enabled,
Self::UprightScreenFix => &mut config.upright_screen_fix,
Self::DoubleCursorFix => &mut config.double_cursor_fix,
Self::SetsOnWatch => &mut config.sets_on_watch,
Self::HideGrabHelp => &mut config.hide_grab_help,
Self::AllowSliding => &mut config.allow_sliding,
Self::FocusFollowsMouseMode => &mut config.focus_follows_mouse_mode,
Self::LeftHandedMouse => &mut config.left_handed_mouse,
Self::BlockGameInput => &mut config.block_game_input,
Self::BlockGameInputIgnoreWatch => &mut config.block_game_input_ignore_watch,
Self::UseSkybox => &mut config.use_skybox,
Self::UsePassthrough => &mut config.use_passthrough,
Self::ScreenRenderDown => &mut config.screen_render_down,
Self::SpaceDragUnlocked => &mut config.space_drag_unlocked,
Self::SpaceRotateUnlocked => &mut config.space_rotate_unlocked,
Self::Clock12h => &mut config.clock_12h,
Self::HideUsername => &mut config.hide_username,
Self::OpaqueBackground => &mut config.opaque_background,
Self::XwaylandByDefault => &mut config.xwayland_by_default,
_ => panic!("Requested bool for non-bool SettingType"),
}
}
pub fn mut_f32<'a>(self, config: &'a mut GeneralConfig) -> &'a mut f32 {
match self {
Self::AnimationSpeed => &mut config.animation_speed,
Self::RoundMultiplier => &mut config.round_multiplier,
Self::ScrollSpeed => &mut config.scroll_speed,
Self::LongPressDuration => &mut config.long_press_duration,
Self::XrClickSensitivity => &mut config.xr_click_sensitivity,
Self::XrClickSensitivityRelease => &mut config.xr_click_sensitivity_release,
Self::SpaceDragMultiplier => &mut config.space_drag_multiplier,
Self::PointerLerpFactor => &mut config.pointer_lerp_factor,
_ => panic!("Requested f32 for non-f32 SettingType"),
}
}
pub fn mut_i32<'a>(self, config: &'a mut GeneralConfig) -> &'a mut i32 {
match self {
Self::ClickFreezeTimeMs => &mut config.click_freeze_time_ms,
_ => panic!("Requested i32 for non-i32 SettingType"),
}
}
pub fn set_enum<'a>(self, config: &'a mut GeneralConfig, value: &str) {
match self {
Self::CaptureMethod => {
config.capture_method = wlx_common::config::CaptureMethod::from_str(value).expect("Invalid enum value!")
}
Self::KeyboardMiddleClick => {
config.keyboard_middle_click_mode =
wlx_common::config::AltModifier::from_str(value).expect("Invalid enum value!")
}
_ => panic!("Requested enum for non-enum SettingType"),
}
}
fn get_enum_title<'a>(self, config: &'a mut GeneralConfig) -> Translation {
match self {
Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method),
Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode),
_ => panic!("Requested enum for non-enum SettingType"),
}
}
fn get_enum_title_inner<E>(value: E) -> Translation
where
E: EnumProperty + AsRef<str>,
{
value
.get_str("Translation")
.map(|x| Translation::from_translation_key(x))
.or_else(|| value.get_str("Text").map(|x| Translation::from_raw_text(x)))
.unwrap_or_else(|| Translation::from_raw_text(value.as_ref()))
}
fn get_enum_tooltip_inner<E>(value: E) -> Option<Translation>
where
E: EnumProperty + AsRef<str>,
{
value.get_str("Tooltip").map(|x| Translation::from_translation_key(x))
}
/// Ok is translation, Err is raw text
fn get_translation(self) -> Result<&'static str, &'static str> {
match self {
Self::AnimationSpeed => Ok("APP_SETTINGS.ANIMATION_SPEED"),
Self::RoundMultiplier => Ok("APP_SETTINGS.ROUND_MULTIPLIER"),
Self::InvertScrollDirectionX => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_X"),
Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"),
Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"),
Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"),
Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"),
Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"),
Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"),
Self::UprightScreenFix => Ok("APP_SETTINGS.UPRIGHT_SCREEN_FIX"),
Self::DoubleCursorFix => Ok("APP_SETTINGS.DOUBLE_CURSOR_FIX"),
Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"),
Self::HideGrabHelp => Ok("APP_SETTINGS.HIDE_GRAB_HELP"),
Self::XrClickSensitivity => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"),
Self::XrClickSensitivityRelease => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE"),
Self::AllowSliding => Ok("APP_SETTINGS.ALLOW_SLIDING"),
Self::ClickFreezeTimeMs => Ok("APP_SETTINGS.CLICK_FREEZE_TIME_MS"),
Self::FocusFollowsMouseMode => Ok("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE"),
Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"),
Self::BlockGameInput => Ok("APP_SETTINGS.BLOCK_GAME_INPUT"),
Self::BlockGameInputIgnoreWatch => Ok("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH"),
Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"),
Self::UseSkybox => Ok("APP_SETTINGS.USE_SKYBOX"),
Self::UsePassthrough => Ok("APP_SETTINGS.USE_PASSTHROUGH"),
Self::ScreenRenderDown => Ok("APP_SETTINGS.SCREEN_RENDER_DOWN"),
Self::PointerLerpFactor => Ok("APP_SETTINGS.POINTER_LERP_FACTOR"),
Self::SpaceDragUnlocked => Ok("APP_SETTINGS.SPACE_DRAG_UNLOCKED"),
Self::SpaceRotateUnlocked => Ok("APP_SETTINGS.SPACE_ROTATE_UNLOCKED"),
Self::Clock12h => Ok("APP_SETTINGS.CLOCK_12H"),
Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"),
Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"),
Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"),
Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"),
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
}
}
fn get_tooltip(self) -> Option<&'static str> {
match self {
Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"),
Self::DoubleCursorFix => Some("APP_SETTINGS.DOUBLE_CURSOR_FIX_HELP"),
Self::XrClickSensitivity => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"),
Self::XrClickSensitivityRelease => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE_HELP"),
Self::FocusFollowsMouseMode => Some("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE_HELP"),
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
Self::BlockGameInput => Some("APP_SETTINGS.BLOCK_GAME_INPUT_HELP"),
Self::BlockGameInputIgnoreWatch => Some("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH_HELP"),
Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"),
Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"),
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"),
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
_ => None,
}
}
//TODO: incorporate this
fn requires_restart(self) -> bool {
match self {
Self::AnimationSpeed
| Self::RoundMultiplier
| Self::UprightScreenFix
| Self::DoubleCursorFix
| Self::SetsOnWatch
| Self::UseSkybox
| Self::UsePassthrough
| Self::ScreenRenderDown => true,
_ => false,
}
}
fn get_frontend_task(self) -> Option<FrontendTask> {
match self {
Self::Clock12h => Some(FrontendTask::RefreshClock),
Self::OpaqueBackground => Some(FrontendTask::RefreshBackground),
_ => None,
}
}
}
macro_rules! category {
($pe:expr, $root:expr, $translation:expr, $icon:expr) => {{
let id = $pe.idx.to_string();
$pe.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("translation"), Rc::from($translation));
params.insert(Rc::from("icon"), Rc::from($icon));
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
$pe
.parser_state
.instantiate_template($pe.doc_params, "SettingsGroupBox", $pe.layout, $root, params)?;
$pe.parser_state.get_widget_id(&id)
}};
}
macro_rules! checkbox {
($mp:expr, $root:expr, $setting:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let checked = if *$setting.mut_bool($mp.config) { "1" } else { "0" };
params.insert(Rc::from("checked"), Rc::from(checked));
$mp
.parser_state
.instantiate_template($mp.doc_params, "CheckBoxSetting", $mp.layout, $root, params)?;
let checkbox = $mp.parser_state.fetch_component_as::<ComponentCheckbox>(&id)?;
checkbox.on_toggle(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateBool($setting, e.checked));
Ok(())
}
}));
};
}
macro_rules! slider_f32 {
($mp:expr, $root:expr, $setting:expr, $min:expr, $max:expr, $step:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let value = $setting.mut_f32($mp.config).to_string();
params.insert(Rc::from("value"), Rc::from(value));
params.insert(Rc::from("min"), Rc::from($min.to_string()));
params.insert(Rc::from("max"), Rc::from($max.to_string()));
params.insert(Rc::from("step"), Rc::from($step.to_string()));
$mp
.parser_state
.instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, $root, params)?;
let slider = $mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateFloat($setting, e.value));
Ok(())
}
}));
};
}
macro_rules! slider_i32 {
($mp:expr, $root:expr, $setting:expr, $min:expr, $max:expr, $step:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let value = $setting.mut_i32($mp.config).to_string();
params.insert(Rc::from("value"), Rc::from(value));
params.insert(Rc::from("min"), Rc::from($min.to_string()));
params.insert(Rc::from("max"), Rc::from($max.to_string()));
params.insert(Rc::from("step"), Rc::from($step.to_string()));
$mp
.parser_state
.instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, $root, params)?;
let slider = $mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateInt($setting, e.value as i32));
Ok(())
}
}));
};
}
macro_rules! dropdown {
($mp:expr, $root:expr, $setting:expr, $options:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
$mp
.parser_state
.instantiate_template($mp.doc_params, "DropdownButton", $mp.layout, $root, params)?;
let setting_str = $setting.as_ref();
let title = $setting.get_enum_title($mp.config);
{
let mut label = $mp
.parser_state
.fetch_widget_as::<WidgetLabel>(&$mp.layout.state, &format!("{id}_value"))?;
label.set_text_simple(&mut $mp.layout.state.globals.get(), title);
}
let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
$options
.iter()
.filter_map(|item| {
if item.get_bool("Hidden").unwrap_or(false) {
return None;
}
let value = item.as_ref();
let title = SettingType::get_enum_title_inner(*item);
let tooltip = SettingType::get_enum_tooltip_inner(*item);
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
Some(context_menu::Cell {
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
title,
tooltip,
attribs: vec![],
})
})
.collect(),
));
Ok(())
}
}));
};
}
macro_rules! danger_button {
($mp:expr, $root:expr, $translation:expr, $icon:expr, $task:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
params.insert(Rc::from("translation"), Rc::from($translation));
params.insert(Rc::from("icon"), Rc::from($icon));
$mp
.parser_state
.instantiate_template($mp.doc_params, "DangerButton", $mp.layout, $root, params)?;
let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Box::new({
let tasks = $mp.tasks.clone();
move |_common, _e| {
tasks.push($task);
Ok(())
}
}));
};
}
macro_rules! autostart_app {
($mp:expr, $root:expr, $text:expr, $ids:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
params.insert(Rc::from("text"), Rc::from($text.as_str()));
$mp
.parser_state
.instantiate_template($mp.doc_params, "AutostartApp", $mp.layout, $root, params)?;
let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
let id: Rc<str> = Rc::from(id);
$ids.push(id.clone());
btn.on_click(Box::new({
let tasks = $mp.tasks.clone();
move |_common, _e| {
tasks.push(Task::RemoveAutostartApp(id.clone()));
Ok(())
}
}));
};
}
struct MacroParams<'a> {
layout: &'a mut Layout,
parser_state: &'a mut ParserState,
doc_params: &'a ParseDocumentParams<'a>,
config: &'a mut GeneralConfig,
tasks: Tasks<Task>,
idx: usize,
}
impl<T> TabSettings<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
let doc_params = ParseDocumentParams {
globals: frontend.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/settings.xml"),
extra: Default::default(),
};
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, &mut frontend.layout, parent_id)?;
let root = parser_state.get_widget_id("settings_root")?;
let mut mp = MacroParams {
layout: &mut frontend.layout,
parser_state: &mut parser_state,
doc_params: &doc_params,
config: frontend.interface.general_config(data),
tasks: Tasks::default(),
idx: 9001,
};
let c = category!(mp, root, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?;
checkbox!(mp, c, SettingType::OpaqueBackground);
checkbox!(mp, c, SettingType::HideUsername);
checkbox!(mp, c, SettingType::HideGrabHelp);
slider_f32!(mp, c, SettingType::AnimationSpeed, 0.5, 5.0, 0.1); // min, max, step
slider_f32!(mp, c, SettingType::RoundMultiplier, 0.5, 5.0, 0.1);
checkbox!(mp, c, SettingType::SetsOnWatch);
checkbox!(mp, c, SettingType::UseSkybox);
checkbox!(mp, c, SettingType::UsePassthrough);
checkbox!(mp, c, SettingType::Clock12h);
let c = category!(mp, root, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
checkbox!(mp, c, SettingType::NotificationsEnabled);
checkbox!(mp, c, SettingType::NotificationsSoundEnabled);
checkbox!(mp, c, SettingType::KeyboardSoundEnabled);
checkbox!(mp, c, SettingType::SpaceDragUnlocked);
checkbox!(mp, c, SettingType::SpaceRotateUnlocked);
slider_f32!(mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5);
checkbox!(mp, c, SettingType::BlockGameInput);
checkbox!(mp, c, SettingType::BlockGameInputIgnoreWatch);
let c = category!(mp, root, "APP_SETTINGS.CONTROLS", "dashboard/controller.svg")?;
dropdown!(
mp,
c,
SettingType::KeyboardMiddleClick,
wlx_common::config::AltModifier::VARIANTS
);
checkbox!(mp, c, SettingType::FocusFollowsMouseMode);
checkbox!(mp, c, SettingType::LeftHandedMouse);
checkbox!(mp, c, SettingType::AllowSliding);
checkbox!(mp, c, SettingType::InvertScrollDirectionX);
checkbox!(mp, c, SettingType::InvertScrollDirectionY);
slider_f32!(mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1);
slider_f32!(mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1);
slider_f32!(mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1);
slider_f32!(mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1);
slider_f32!(mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1);
slider_i32!(mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50);
let c = category!(mp, root, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
dropdown!(
mp,
c,
SettingType::CaptureMethod,
wlx_common::config::CaptureMethod::VARIANTS
);
checkbox!(mp, c, SettingType::XwaylandByDefault);
checkbox!(mp, c, SettingType::UprightScreenFix);
checkbox!(mp, c, SettingType::DoubleCursorFix);
checkbox!(mp, c, SettingType::ScreenRenderDown);
let mut app_button_ids = vec![];
if !mp.config.autostart_apps.is_empty() {
let c = category!(mp, root, "APP_SETTINGS.AUTOSTART_APPS", "dashboard/apps.svg")?;
for app in &mp.config.autostart_apps {
autostart_app!(mp, c, app.name, app_button_ids);
}
}
let c = category!(mp, root, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/cpu.svg")?;
danger_button!(
mp,
c,
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
"dashboard/display.svg",
Task::ClearPipewireTokens
);
danger_button!(
mp,
c,
"APP_SETTINGS.CLEAR_SAVED_STATE",
"dashboard/binary.svg",
Task::ClearSavedState
);
danger_button!(
mp,
c,
"APP_SETTINGS.DELETE_ALL_CONFIGS",
"dashboard/circle.svg",
Task::DeleteAllConfigs
);
danger_button!(
mp,
c,
"APP_SETTINGS.RESTART_SOFTWARE",
"dashboard/refresh.svg",
Task::RestartSoftware
);
Ok(Self {
app_button_ids,
tasks: mp.tasks,
state: parser_state,
marker: PhantomData,
context_menu: ContextMenu::default(),
})
}
}

View File

@ -0,0 +1,377 @@
use std::{collections::HashMap, rc::Rc};
use crate::tab::settings::{self, SettingType, Task, horiz_cell, mount_requires_restart};
use wgui::{
components::{
button::{ButtonClickEvent, ComponentButton},
checkbox::ComponentCheckbox,
slider::ComponentSlider,
},
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu,
};
use wlx_common::config::GeneralConfig;
pub fn options_category(
mp: &mut MacroParams,
parent: WidgetID,
translation: &str,
icon: &str,
) -> anyhow::Result<WidgetID> {
let id = mp.idx.to_string();
mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("translation"), Rc::from(translation));
params.insert(Rc::from("icon"), Rc::from(icon));
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
mp.parser_state
.instantiate_template(mp.doc_params, "SettingsGroupBox", mp.layout, parent, params)?;
mp.parser_state.get_widget_id(&id)
}
pub fn options_checkbox(mp: &mut MacroParams, parent: WidgetID, setting: SettingType) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let checked = if *setting.mut_bool(mp.config) { "1" } else { "0" };
params.insert(Rc::from("checked"), Rc::from(checked));
let id_cell = horiz_cell(mp.layout, parent)?;
mp.parser_state
.instantiate_template(mp.doc_params, "CheckBoxSetting", mp.layout, id_cell, params)?;
if setting.requires_restart() {
mount_requires_restart(mp.layout, id_cell)?;
}
let checkbox = mp.parser_state.fetch_component_as::<ComponentCheckbox>(&id)?;
checkbox.on_toggle(Box::new({
let tasks = mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateBool(setting, e.checked));
Ok(())
}
}));
Ok(())
}
pub fn options_slider_f32(
mp: &mut MacroParams,
parent: WidgetID,
setting: SettingType,
min: f32,
max: f32,
step: f32,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let value = setting.mut_f32(mp.config).to_string();
params.insert(Rc::from("value"), Rc::from(value));
params.insert(Rc::from("min"), Rc::from(min.to_string()));
params.insert(Rc::from("max"), Rc::from(max.to_string()));
params.insert(Rc::from("step"), Rc::from(step.to_string()));
let id_cell = horiz_cell(mp.layout, parent)?;
mp.parser_state
.instantiate_template(mp.doc_params, "SliderSetting", mp.layout, id_cell, params)?;
if setting.requires_restart() {
mount_requires_restart(mp.layout, id_cell)?;
}
let slider = mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
let tasks = mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateFloat(setting, e.value));
}
}));
Ok(())
}
pub fn options_range_f32(
mp: &mut MacroParams,
parent: WidgetID,
setting: SettingType,
setting2: SettingType,
min: f32,
max: f32,
step: f32,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let value = setting.mut_f32(mp.config).to_string();
let value2 = setting2.mut_f32(mp.config).to_string();
params.insert(Rc::from("value"), Rc::from(value));
params.insert(Rc::from("value2"), Rc::from(value2));
params.insert(Rc::from("min"), Rc::from(min.to_string()));
params.insert(Rc::from("max"), Rc::from(max.to_string()));
params.insert(Rc::from("step"), Rc::from(step.to_string()));
let id_cell = horiz_cell(mp.layout, parent)?;
mp.parser_state
.instantiate_template(mp.doc_params, "RangeSetting", mp.layout, id_cell, params)?;
if setting.requires_restart() {
mount_requires_restart(mp.layout, id_cell)?;
}
let slider = mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
let tasks = mp.tasks.clone();
move |_common, e| {
if matches!(e.index, wgui::components::slider::ValueIndex::Primary) {
tasks.push(Task::UpdateFloat(setting, e.value));
} else {
tasks.push(Task::UpdateFloat(setting2, e.value));
}
}
}));
Ok(())
}
pub fn options_slider_i32(
mp: &mut MacroParams,
parent: WidgetID,
setting: SettingType,
min: i32,
max: i32,
step: i32,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let id_cell = horiz_cell(mp.layout, parent)?;
let value = setting.mut_i32(mp.config).to_string();
params.insert(Rc::from("value"), Rc::from(value));
params.insert(Rc::from("min"), Rc::from(min.to_string()));
params.insert(Rc::from("max"), Rc::from(max.to_string()));
params.insert(Rc::from("step"), Rc::from(step.to_string()));
mp.parser_state
.instantiate_template(mp.doc_params, "SliderSetting", mp.layout, id_cell, params)?;
if setting.requires_restart() {
mount_requires_restart(mp.layout, id_cell)?;
}
let slider = mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
let tasks = mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateInt(setting, e.value as i32));
}
}));
Ok(())
}
pub fn options_dropdown<EnumType>(
mp: &mut MacroParams,
parent: WidgetID,
setting: &'static SettingType,
) -> anyhow::Result<()>
where
EnumType: strum::VariantArray + strum::EnumProperty + std::convert::AsRef<str> + Copy + 'static,
{
let id = mp.idx.to_string();
mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let id_cell = horiz_cell(mp.layout, parent)?;
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, id_cell, params)?;
if setting.requires_restart() {
mount_requires_restart(mp.layout, id_cell)?;
}
let setting_str = setting.as_ref();
let title = setting.get_enum_title(mp.config);
{
let mut label = mp
.parser_state
.fetch_widget_as::<WidgetLabel>(&mp.layout.state, &format!("{id}_value"))?;
label.set_text_simple(&mut mp.layout.state.globals.get(), title);
}
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Rc::new({
let tasks = mp.tasks.clone();
move |_common, e: ButtonClickEvent| {
tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
EnumType::VARIANTS
.iter()
.filter_map(|item| {
if item.get_bool("Hidden").unwrap_or(false) {
return None;
}
let value = item.as_ref();
let title = SettingType::get_enum_title_inner(*item);
let tooltip = SettingType::get_enum_tooltip_inner(*item);
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
Some(context_menu::Cell {
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
title,
tooltip,
attribs: vec![],
})
})
.collect(),
));
Ok(())
}
}));
Ok(())
}
pub fn options_danger_button(
mp: &mut MacroParams,
parent: WidgetID,
translation: &str,
icon: &str,
task: Task,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
params.insert(Rc::from("translation"), Rc::from(translation));
params.insert(Rc::from("icon"), Rc::from(icon));
mp.parser_state
.instantiate_template(mp.doc_params, "DangerButton", mp.layout, parent, params)?;
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Rc::new({
let tasks = mp.tasks.clone();
move |_common, _e| {
tasks.push(task.clone());
Ok(())
}
}));
Ok(())
}
pub fn options_autostart_app(
mp: &mut MacroParams,
parent: WidgetID,
text: &str,
ids: &mut Vec<Rc<str>>,
) -> anyhow::Result<()> {
let id = mp.idx.to_string();
mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
params.insert(Rc::from("text"), Rc::from(text));
mp.parser_state
.instantiate_template(mp.doc_params, "AutostartApp", mp.layout, parent, params)?;
let btn = mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
let id: Rc<str> = Rc::from(id);
ids.push(id.clone());
btn.on_click(Rc::new({
let tasks = mp.tasks.clone();
move |_common, _e| {
tasks.push(Task::RemoveAutostartApp(id.clone()));
Ok(())
}
}));
Ok(())
}
pub struct MacroParams<'a> {
pub layout: &'a mut Layout,
pub parser_state: &'a mut ParserState,
pub doc_params: &'a ParseDocumentParams<'a>,
pub config: &'a mut GeneralConfig,
pub tasks: Tasks<settings::Task>,
pub idx: usize,
}

View File

@ -0,0 +1,694 @@
use glam::Vec2;
use std::{marker::PhantomData, rc::Rc, str::FromStr};
use strum::{AsRefStr, EnumProperty, EnumString};
use wgui::{
assets::AssetPath,
components::tabs::ComponentTabs,
drawing,
event::StyleSetRequest,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
log::LogErr,
parser::{Fetchable, ParseDocumentParams, ParserState},
renderer_vk::text::{FontWeight, TextStyle},
taffy::{self, prelude::length},
task::Tasks,
widget::{
div::WidgetDiv,
label::{WidgetLabel, WidgetLabelParams},
},
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult},
};
use wlx_common::{
config::GeneralConfig,
config_io::ConfigRoot,
dash_interface::{ConfigChangeKind, InterfaceFeats, RecenterMode},
};
use crate::{
frontend::{Frontend, FrontendTask, FrontendTasks},
tab::{Tab, TabType, settings::macros::MacroParams},
views::ViewUpdateParams,
};
mod macros;
mod tab_autostart_apps;
mod tab_controls;
mod tab_features;
mod tab_look_and_feel;
mod tab_misc;
mod tab_skybox;
mod tab_space_drag;
mod tab_troubleshooting;
#[derive(Clone)]
enum TabNameEnum {
AutostartApps,
Controls,
Features,
LookAndFeel,
Misc,
Skybox,
SpaceDrag,
Troubleshooting,
}
impl TabNameEnum {
fn from_string(s: &str) -> Option<Self> {
match s {
"autostart_apps" => Some(TabNameEnum::AutostartApps),
"controls" => Some(TabNameEnum::Controls),
"features" => Some(TabNameEnum::Features),
"look_and_feel" => Some(TabNameEnum::LookAndFeel),
"misc" => Some(TabNameEnum::Misc),
"skybox" => Some(TabNameEnum::Skybox),
"space_drag" => Some(TabNameEnum::SpaceDrag),
"troubleshooting" => Some(TabNameEnum::Troubleshooting),
_ => None,
}
}
}
#[derive(Clone)]
enum Task {
ClearPipewireTokens,
ClearSavedState,
DeleteAllConfigs,
OpenContextMenu(Vec2, Vec<context_menu::Cell>),
RemoveAutostartApp(Rc<str>),
ResetPlayspace,
RestartSoftware,
SetTab(TabNameEnum),
SettingUpdated(SettingType),
UpdateBool(SettingType, bool),
UpdateFloat(SettingType, f32),
UpdateInt(SettingType, i32),
}
struct SettingsMountParams<'a> {
mp: &'a mut MacroParams<'a>,
frontend_tasks: &'a FrontendTasks,
id_parent: WidgetID,
feats: InterfaceFeats,
}
struct SettingUpdatedParams<'a> {
layout: &'a mut Layout,
config: &'a GeneralConfig,
setting_type: SettingType,
}
trait SettingsTab {
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
Ok(())
}
fn setting_updated(&mut self, _sup: &mut SettingUpdatedParams) -> anyhow::Result<()> {
Ok(())
}
}
pub struct TabSettings<T> {
pub state: ParserState,
app_button_ids: Vec<Rc<str>>,
context_menu: ContextMenu,
current_tab: Option<Box<dyn SettingsTab>>,
tasks: Tasks<Task>,
marker: PhantomData<T>,
frontend_tasks: FrontendTasks,
}
impl<T> Tab<T> for TabSettings<T> {
fn get_type(&self) -> TabType {
TabType::Settings
}
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
if let Some(tab) = &mut self.current_tab {
let mut config_change_kind = None;
tab.update(&mut ViewUpdateParams {
layout: &mut frontend.layout,
executor: &frontend.executor,
general_config: frontend.interface.general_config(data),
config_change_kind: &mut config_change_kind,
})?;
if let Some(kind) = config_change_kind {
frontend.interface.config_changed(data, kind);
}
}
let mut changed = None;
for task in self.tasks.drain() {
match task {
Task::SetTab(tab) => {
self.set_tab(frontend, data, tab)?;
}
Task::UpdateBool(setting, n) => {
self.tasks.push(Task::SettingUpdated(setting));
if let Some(task) = setting.get_frontend_task() {
frontend.tasks.push(task)
}
let config = frontend.interface.general_config(data);
*setting.mut_bool(config) = n;
changed = Some(setting.change_kind());
}
Task::UpdateFloat(setting, n) => {
self.tasks.push(Task::SettingUpdated(setting));
if let Some(task) = setting.get_frontend_task() {
frontend.tasks.push(task)
}
let config = frontend.interface.general_config(data);
*setting.mut_f32(config) = n;
changed = Some(setting.change_kind());
}
Task::UpdateInt(setting, n) => {
self.tasks.push(Task::SettingUpdated(setting));
if let Some(task) = setting.get_frontend_task() {
frontend.tasks.push(task)
}
let config = frontend.interface.general_config(data);
*setting.mut_i32(config) = n;
changed = Some(setting.change_kind());
}
Task::ClearPipewireTokens => {
let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("pw_tokens.yaml"))
.log_err("Could not remove pw_tokens.yaml");
}
Task::ClearSavedState => {
let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("zz-saved-state.json5"))
.log_err("Could not remove zz-saved-state.json5");
}
Task::DeleteAllConfigs => {
let path = ConfigRoot::Generic.get_conf_d_path();
std::fs::remove_dir_all(&path)?;
std::fs::create_dir(&path)?;
}
Task::ResetPlayspace => {
frontend.interface.recenter_playspace(data, RecenterMode::Reset)?;
return Ok(());
}
Task::RestartSoftware => {
frontend.interface.restart(data);
return Ok(());
}
Task::OpenContextMenu(position, cells) => {
self.context_menu.open(context_menu::OpenParams {
on_custom_attribs: None,
position,
blueprint: Blueprint::Cells(cells),
});
}
Task::RemoveAutostartApp(button_id) => {
if let (Some(idx), Ok(widget)) = (
self.app_button_ids.iter().position(|x| button_id.eq(x)),
self.state.get_widget_id(&format!("{button_id}_root")),
) {
self.app_button_ids.remove(idx);
let config = frontend.interface.general_config(data);
config.autostart_apps.remove(idx);
frontend.layout.remove_widget(widget);
changed = Some(ConfigChangeKind::OverlayConfig);
}
}
Task::SettingUpdated(setting) => {
if let Some(tab) = &mut self.current_tab {
tab.setting_updated(&mut SettingUpdatedParams {
layout: &mut frontend.layout,
config: frontend.interface.general_config(data),
setting_type: setting,
})?;
}
match setting {
SettingType::UiAnimationSpeed | SettingType::UiGradientIntensity | SettingType::UiRoundMultiplier => {
// todo: currently, wayvr restart is required to apply these changes (WguiTheme is Rc)
}
_ => { /* do nothing */ }
}
}
}
}
// Dropdown handling
if let TickResult::Action(name) = self.context_menu.tick(&mut frontend.layout, &mut self.state)?
&& let (Some(setting), Some(id), Some(value), Some(text), Some(translated)) = {
let mut s = name.splitn(5, ';');
(s.next(), s.next(), s.next(), s.next(), s.next())
} {
let mut common = frontend.layout.common();
let mut label = self
.state
.fetch_widget_as::<WidgetLabel>(common.state, &format!("{id}_value"))?;
let translation = Translation {
text: text.into(),
translated: translated == "1",
};
label.set_text(&mut common, translation);
let setting = SettingType::from_str(setting).expect("Invalid Enum string");
let config = frontend.interface.general_config(data);
setting.set_enum(config, value);
changed = Some(ConfigChangeKind::OverlayConfig);
}
// Notify overlays of the change
if let Some(changed) = changed {
frontend.interface.config_changed(data, changed);
}
Ok(())
}
}
// Sorted alphabetically
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, Eq, PartialEq, AsRefStr, EnumString)]
enum SettingType {
AllowSliding,
BlockGameInput,
BlockGameInputIgnoreWatch,
BlockPosesOnKbdInteraction,
CaptureMethod,
ClickFreezeTimeMs,
Clock12h,
DoubleCursorFix,
FocusFollowsMouseMode,
GridOpacity,
HandsfreePointer,
HideGrabHelp,
HideUsername,
InvertScrollDirectionX,
InvertScrollDirectionY,
KeyboardMiddleClick,
KeyboardSoundEnabled,
Language,
LeftHandedMouse,
LongPressDuration,
NotificationsEnabled,
NotificationsSoundEnabled,
OpaqueBackground,
PointerLerpFactor,
ScreenRenderDown,
ScrollSpeed,
EnableWatch,
SetsOnWatch,
SpaceDragMultiplier,
SpaceDragUnlocked,
SpaceGravityDamping,
SpaceGravityEnabled,
SpaceGravityFlingStrength,
SpaceGravityGravity,
SpaceGravityGroundFriction,
SpaceGravityFloorHeight,
SpaceRotateUnlocked,
UiAnimationSpeed,
UiGradientIntensity,
UiRoundMultiplier,
UprightScreenFix,
UsePassthrough,
UseSkybox,
WatchViewAngleMax,
WatchViewAngleMin,
XrClickSensitivity,
XrClickSensitivityRelease,
XwaylandByDefault,
}
impl SettingType {
pub fn change_kind(self) -> ConfigChangeKind {
match self {
Self::UseSkybox | Self::UsePassthrough => ConfigChangeKind::EnvironmentBlend,
_ => ConfigChangeKind::OverlayConfig,
}
}
pub fn mut_bool(self, config: &mut GeneralConfig) -> &mut bool {
match self {
Self::AllowSliding => &mut config.allow_sliding,
Self::BlockGameInput => &mut config.block_game_input,
Self::BlockGameInputIgnoreWatch => &mut config.block_game_input_ignore_watch,
Self::BlockPosesOnKbdInteraction => &mut config.block_poses_on_kbd_interaction,
Self::Clock12h => &mut config.clock_12h,
Self::DoubleCursorFix => &mut config.double_cursor_fix,
Self::EnableWatch => &mut config.enable_watch,
Self::FocusFollowsMouseMode => &mut config.focus_follows_mouse_mode,
Self::HideGrabHelp => &mut config.hide_grab_help,
Self::HideUsername => &mut config.hide_username,
Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x,
Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y,
Self::KeyboardSoundEnabled => &mut config.keyboard_sound_enabled,
Self::LeftHandedMouse => &mut config.left_handed_mouse,
Self::NotificationsEnabled => &mut config.notifications_enabled,
Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled,
Self::OpaqueBackground => &mut config.opaque_background,
Self::ScreenRenderDown => &mut config.screen_render_down,
Self::SetsOnWatch => &mut config.sets_on_watch,
Self::SpaceDragUnlocked => &mut config.space_drag_unlocked,
Self::SpaceGravityEnabled => &mut config.space_gravity_enabled,
Self::SpaceRotateUnlocked => &mut config.space_rotate_unlocked,
Self::UprightScreenFix => &mut config.upright_screen_fix,
Self::UsePassthrough => &mut config.use_passthrough,
Self::UseSkybox => &mut config.use_skybox,
Self::XwaylandByDefault => &mut config.xwayland_by_default,
_ => panic!("Requested bool for non-bool SettingType"),
}
}
pub fn mut_f32(self, config: &mut GeneralConfig) -> &mut f32 {
match self {
Self::GridOpacity => &mut config.grid_opacity,
Self::LongPressDuration => &mut config.long_press_duration,
Self::PointerLerpFactor => &mut config.pointer_lerp_factor,
Self::ScrollSpeed => &mut config.scroll_speed,
Self::SpaceDragMultiplier => &mut config.space_drag_multiplier,
Self::SpaceGravityDamping => &mut config.space_gravity_damping,
Self::SpaceGravityFlingStrength => &mut config.space_gravity_fling_strength,
Self::SpaceGravityGravity => &mut config.space_gravity_gravity,
Self::SpaceGravityGroundFriction => &mut config.space_gravity_ground_friction,
Self::SpaceGravityFloorHeight => &mut config.space_gravity_floor_height,
Self::UiAnimationSpeed => &mut config.ui_animation_speed,
Self::UiGradientIntensity => &mut config.ui_gradient_intensity,
Self::UiRoundMultiplier => &mut config.ui_round_multiplier,
Self::WatchViewAngleMax => &mut config.watch_view_angle_max,
Self::WatchViewAngleMin => &mut config.watch_view_angle_min,
Self::XrClickSensitivity => &mut config.xr_click_sensitivity,
Self::XrClickSensitivityRelease => &mut config.xr_click_sensitivity_release,
_ => panic!("Requested f32 for non-f32 SettingType"),
}
}
pub fn mut_i32(self, config: &mut GeneralConfig) -> &mut i32 {
match self {
Self::ClickFreezeTimeMs => &mut config.click_freeze_time_ms,
_ => panic!("Requested i32 for non-i32 SettingType"),
}
}
pub fn set_enum(self, config: &mut GeneralConfig, value: &str) {
match self {
Self::CaptureMethod => {
config.capture_method = wlx_common::config::CaptureMethod::from_str(value).expect("Invalid enum value!")
}
Self::KeyboardMiddleClick => {
config.keyboard_middle_click_mode =
wlx_common::config::AltModifier::from_str(value).expect("Invalid enum value!")
}
Self::HandsfreePointer => {
config.handsfree_pointer = wlx_common::config::HandsfreePointer::from_str(value).expect("Invalid enum value!")
}
Self::Language => {
config.language = Some(wlx_common::locale::Language::from_str(value).expect("Invalid enum value!"))
}
_ => panic!("Requested enum for non-enum SettingType"),
}
}
fn get_enum_title(self, config: &mut GeneralConfig) -> Translation {
match self {
Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method),
Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode),
Self::HandsfreePointer => Self::get_enum_title_inner(config.handsfree_pointer),
Self::Language => match &config.language {
Some(lang) => Self::get_enum_title_inner(*lang),
None => Translation::from_translation_key("APP_SETTINGS.OPTION.AUTO"),
},
_ => panic!("Requested enum for non-enum SettingType"),
}
}
fn get_enum_title_inner<E>(value: E) -> Translation
where
E: EnumProperty + AsRef<str>,
{
value
.get_str("Translation")
.map(Translation::from_translation_key)
.or_else(|| value.get_str("Text").map(Translation::from_raw_text))
.unwrap_or_else(|| Translation::from_raw_text(value.as_ref()))
}
fn get_enum_tooltip_inner<E>(value: E) -> Option<Translation>
where
E: EnumProperty + AsRef<str>,
{
value.get_str("Tooltip").map(Translation::from_translation_key)
}
/// Ok is translation, Err is raw text
/// `match` sorted alphabetically
fn get_translation(self) -> Result<&'static str, &'static str> {
match self {
Self::AllowSliding => Ok("APP_SETTINGS.ALLOW_SLIDING"),
Self::BlockGameInput => Ok("APP_SETTINGS.BLOCK_GAME_INPUT"),
Self::BlockGameInputIgnoreWatch => Ok("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH"),
Self::BlockPosesOnKbdInteraction => Ok("APP_SETTINGS.BLOCK_POSES_ON_KBD_INTERACTION"),
Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"),
Self::ClickFreezeTimeMs => Ok("APP_SETTINGS.CLICK_FREEZE_TIME_MS"),
Self::Clock12h => Ok("APP_SETTINGS.CLOCK_12H"),
Self::DoubleCursorFix => Ok("APP_SETTINGS.DOUBLE_CURSOR_FIX"),
Self::FocusFollowsMouseMode => Ok("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE"),
Self::GridOpacity => Ok("APP_SETTINGS.GRID_OPACITY"),
Self::HandsfreePointer => Ok("APP_SETTINGS.HANDSFREE_POINTER"),
Self::HideGrabHelp => Ok("APP_SETTINGS.HIDE_GRAB_HELP"),
Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"),
Self::InvertScrollDirectionX => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_X"),
Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"),
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"),
Self::Language => Ok("APP_SETTINGS.LANGUAGE"),
Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"),
Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"),
Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"),
Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"),
Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"),
Self::PointerLerpFactor => Ok("APP_SETTINGS.POINTER_LERP_FACTOR"),
Self::ScreenRenderDown => Ok("APP_SETTINGS.SCREEN_RENDER_DOWN"),
Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"),
Self::EnableWatch => Ok("APP_SETTINGS.ENABLE_WATCH"),
Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"),
Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"),
Self::SpaceDragUnlocked => Ok("APP_SETTINGS.SPACE_DRAG_UNLOCKED"),
Self::SpaceGravityDamping => Ok("APP_SETTINGS.SPACE_GRAVITY_DAMPING"),
Self::SpaceGravityEnabled => Ok("APP_SETTINGS.ENABLED"),
Self::SpaceGravityFlingStrength => Ok("APP_SETTINGS.SPACE_GRAVITY_FLING_STRENGTH"),
Self::SpaceGravityGravity => Ok("APP_SETTINGS.SPACE_GRAVITY_GRAVITY"),
Self::SpaceGravityGroundFriction => Ok("APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION"),
Self::SpaceGravityFloorHeight => Ok("APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT"),
Self::SpaceRotateUnlocked => Ok("APP_SETTINGS.SPACE_ROTATE_UNLOCKED"),
Self::UiAnimationSpeed => Ok("APP_SETTINGS.ANIMATION_SPEED"),
Self::UiGradientIntensity => Ok("APP_SETTINGS.UI_GRADIENT_INTENSITY"),
Self::UiRoundMultiplier => Ok("APP_SETTINGS.ROUND_MULTIPLIER"),
Self::UprightScreenFix => Ok("APP_SETTINGS.UPRIGHT_SCREEN_FIX"),
Self::UsePassthrough => Ok("APP_SETTINGS.USE_PASSTHROUGH"),
Self::UseSkybox => Ok("APP_SETTINGS.USE_SKYBOX"),
Self::WatchViewAngleMax => Ok("APP_SETTINGS.WATCH_VIEW_ANGLE"),
Self::WatchViewAngleMin => Ok("APP_SETTINGS.WATCH_VIEW_ANGLE"),
Self::XrClickSensitivity => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"),
Self::XrClickSensitivityRelease => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"),
Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"),
}
}
/// `match` sorted alphabetically
fn get_tooltip(self) -> Option<&'static str> {
match self {
Self::BlockGameInput => Some("APP_SETTINGS.BLOCK_GAME_INPUT_HELP"),
Self::BlockGameInputIgnoreWatch => Some("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH_HELP"),
Self::BlockPosesOnKbdInteraction => Some("APP_SETTINGS.BLOCK_POSES_ON_KBD_INTERACTION_HELP"),
Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"),
Self::DoubleCursorFix => Some("APP_SETTINGS.DOUBLE_CURSOR_FIX_HELP"),
Self::GridOpacity => Some("APP_SETTINGS.GRID_OPACITY_HELP"),
Self::HandsfreePointer => Some("APP_SETTINGS.HANDSFREE_POINTER_HELP"),
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::SpaceGravityDamping => Some("APP_SETTINGS.SPACE_GRAVITY_DAMPING_HELP"),
Self::SpaceGravityFlingStrength => Some("APP_SETTINGS.SPACE_GRAVITY_FLING_STRENGTH_HELP"),
Self::SpaceGravityFloorHeight => Some("APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT_HELP"),
Self::SpaceGravityGravity => Some("APP_SETTINGS.SPACE_GRAVITY_GRAVITY_HELP"),
Self::SpaceGravityGroundFriction => Some("APP_SETTINGS.SPACE_GRAVITY_GROUND_FRICTION_HELP"),
Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"),
Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"),
Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"),
Self::WatchViewAngleMax => Some("APP_SETTINGS.WATCH_VIEW_ANGLE_HELP"),
Self::WatchViewAngleMin => Some("APP_SETTINGS.WATCH_VIEW_ANGLE_HELP"),
Self::XrClickSensitivity => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"),
Self::XrClickSensitivityRelease => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"),
_ => None,
}
}
fn requires_restart(self) -> bool {
matches!(
self,
Self::UiAnimationSpeed
| Self::UiRoundMultiplier
| Self::UiGradientIntensity
| Self::UprightScreenFix
| Self::DoubleCursorFix
| Self::Language
| Self::CaptureMethod
)
}
fn get_frontend_task(self) -> Option<FrontendTask> {
match self {
Self::Clock12h => Some(FrontendTask::RefreshClock),
Self::OpaqueBackground => Some(FrontendTask::RefreshBackground),
_ => None,
}
}
}
// creates a simple div with horizontal, centered flow
fn horiz_cell(layout: &mut Layout, parent: WidgetID) -> anyhow::Result<WidgetID> {
let (pair, _) = layout.add_child(
parent,
WidgetDiv::create(),
taffy::Style {
flex_direction: taffy::FlexDirection::Row,
align_items: Some(taffy::AlignItems::Center),
gap: length(8.0),
..Default::default()
},
)?;
Ok(pair.id)
}
fn mount_requires_restart(layout: &mut Layout, parent: WidgetID) -> anyhow::Result<()> {
let content = Translation::from_translation_key("APP_SETTINGS.REQUIRES_RESTART");
let label = WidgetLabel::create(
&mut layout.state,
WidgetLabelParams {
content,
style: TextStyle {
wrap: false,
color: Some(drawing::Color::new(1.0, 0.5, 0.5, 1.0)),
weight: Some(FontWeight::Bold),
size: Some(10.0),
..Default::default()
},
},
);
layout.add_child(parent, label, Default::default())?;
Ok(())
}
fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/settings.xml"),
extra: Default::default(),
}
}
impl<T> TabSettings<T> {
fn set_tab(&mut self, frontend: &mut Frontend<T>, data: &mut T, name: TabNameEnum) -> anyhow::Result<()> {
let root = self.state.get_widget_id("settings_root")?;
frontend.layout.remove_children(root);
let globals = frontend.layout.state.globals.clone();
self.current_tab = None;
let feats = frontend.interface.get_feats(data);
let mut mp = MacroParams {
layout: &mut frontend.layout,
parser_state: &mut self.state,
doc_params: &doc_params(&globals),
config: frontend.interface.general_config(data),
tasks: self.tasks.clone(),
idx: 9001,
};
let settings_mount_params = SettingsMountParams {
mp: &mut mp,
id_parent: root,
frontend_tasks: &self.frontend_tasks,
feats,
};
match name {
TabNameEnum::LookAndFeel => {
self.current_tab = Some(Box::new(tab_look_and_feel::State::mount(settings_mount_params)?));
}
TabNameEnum::Features => {
self.current_tab = Some(Box::new(tab_features::State::mount(settings_mount_params)?));
}
TabNameEnum::SpaceDrag => {
self.current_tab = Some(Box::new(tab_space_drag::State::mount(settings_mount_params)?));
}
TabNameEnum::Controls => {
self.current_tab = Some(Box::new(tab_controls::State::mount(settings_mount_params)?));
}
TabNameEnum::Misc => {
self.current_tab = Some(Box::new(tab_misc::State::mount(settings_mount_params)?));
}
TabNameEnum::AutostartApps => {
self.current_tab = Some(Box::new(tab_autostart_apps::State::mount(
settings_mount_params,
&mut self.app_button_ids,
)?));
}
TabNameEnum::Troubleshooting => {
self.current_tab = Some(Box::new(tab_troubleshooting::State::mount(settings_mount_params)?));
}
TabNameEnum::Skybox => {
self.current_tab = Some(Box::new(tab_skybox::State::mount(settings_mount_params)?));
}
}
Ok(())
}
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
let doc_params = ParseDocumentParams {
globals: frontend.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/settings.xml"),
extra: Default::default(),
};
let parser_state = wgui::parser::parse_from_assets(&doc_params, &mut frontend.layout, parent_id)?;
let tasks = Tasks::default();
let tabs = parser_state.fetch_component_as::<ComponentTabs>("tabs")?;
if !frontend.interface.get_feats(data).openxr {
let skybox_btn = tabs.get_tab_button("skybox").unwrap();
frontend
.layout
.common()
.alterables
.set_style(skybox_btn.get_rect(), StyleSetRequest::Display(taffy::Display::None));
}
tabs.on_select({
let tasks = tasks.clone();
Rc::new(move |_common, evt| {
if let Some(tab) = TabNameEnum::from_string(&evt.name) {
tasks.push(Task::SetTab(tab));
}
Ok(())
})
});
tasks.push(Task::SetTab(TabNameEnum::LookAndFeel));
Ok(Self {
app_button_ids: Vec::new(),
tasks,
state: parser_state,
marker: PhantomData,
context_menu: ContextMenu::default(),
current_tab: None,
frontend_tasks: frontend.tasks.clone(),
})
}
}

View File

@ -0,0 +1,33 @@
use std::rc::Rc;
use crate::tab::settings::{
SettingsMountParams, SettingsTab,
macros::{options_autostart_app, options_category},
};
pub struct State {}
impl SettingsTab for State {}
impl State {
pub fn mount(par: SettingsMountParams, app_button_ids: &mut Vec<Rc<str>>) -> anyhow::Result<State> {
*app_button_ids = Vec::new();
if !par.mp.config.autostart_apps.is_empty() {
let c = options_category(
par.mp,
par.id_parent,
"APP_SETTINGS.AUTOSTART_APPS",
"dashboard/apps.svg",
)?;
// todo: prevent clone
let autostart_apps = par.mp.config.autostart_apps.clone();
for app in autostart_apps {
options_autostart_app(par.mp, c, &app.name, app_button_ids)?;
}
}
Ok(State {})
}
}

View File

@ -0,0 +1,46 @@
use crate::tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{
options_category, options_checkbox, options_dropdown, options_range_f32, options_slider_f32, options_slider_i32,
},
};
pub struct State {}
impl SettingsTab for State {}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(
par.mp,
par.id_parent,
"APP_SETTINGS.CONTROLS",
"dashboard/controller.svg",
)?;
options_dropdown::<wlx_common::config::AltModifier>(par.mp, c, &SettingType::KeyboardMiddleClick)?;
options_dropdown::<wlx_common::config::HandsfreePointer>(par.mp, c, &SettingType::HandsfreePointer)?;
options_checkbox(par.mp, c, SettingType::FocusFollowsMouseMode)?;
options_checkbox(par.mp, c, SettingType::LeftHandedMouse)?;
options_checkbox(par.mp, c, SettingType::AllowSliding)?;
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionX)?;
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionY)?;
options_slider_f32(par.mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?;
options_slider_f32(par.mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?;
if par.feats.openxr {
options_slider_f32(par.mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?;
options_range_f32(
par.mp,
c,
SettingType::XrClickSensitivityRelease,
SettingType::XrClickSensitivity,
0.1,
0.9,
0.1,
)?;
}
options_slider_i32(par.mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?;
Ok(State {})
}
}

View File

@ -0,0 +1,37 @@
use crate::tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox, options_range_f32, options_slider_f32},
};
pub struct State {}
impl SettingsTab for State {}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?;
options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?;
if !par.feats.openxr || par.feats.monado {
// monado or openvr
options_checkbox(par.mp, c, SettingType::BlockGameInput)?;
options_checkbox(par.mp, c, SettingType::BlockGameInputIgnoreWatch)?;
}
if par.feats.monado {
// monado-only
options_checkbox(par.mp, c, SettingType::BlockPosesOnKbdInteraction)?;
}
options_range_f32(
par.mp,
c,
SettingType::WatchViewAngleMin,
SettingType::WatchViewAngleMax,
0.1,
1.0,
0.1,
)?;
Ok(State {})
}
}

View File

@ -0,0 +1,30 @@
use crate::tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox, options_dropdown, options_slider_f32},
};
pub struct State {}
impl SettingsTab for State {}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(
par.mp,
par.id_parent,
"APP_SETTINGS.LOOK_AND_FEEL",
"dashboard/palette.svg",
)?;
options_dropdown::<wlx_common::locale::Language>(par.mp, c, &SettingType::Language)?;
options_checkbox(par.mp, c, SettingType::OpaqueBackground)?;
options_checkbox(par.mp, c, SettingType::HideUsername)?;
options_checkbox(par.mp, c, SettingType::HideGrabHelp)?;
options_slider_f32(par.mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1)?; // min, max, step
options_slider_f32(par.mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05)?; // min, max, step
options_slider_f32(par.mp, c, SettingType::UiRoundMultiplier, 0.1, 5.0, 0.1)?;
options_checkbox(par.mp, c, SettingType::EnableWatch)?;
options_checkbox(par.mp, c, SettingType::SetsOnWatch)?;
options_checkbox(par.mp, c, SettingType::Clock12h)?;
Ok(State {})
}
}

View File

@ -0,0 +1,20 @@
use crate::tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox, options_dropdown},
};
pub struct State {}
impl SettingsTab for State {}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
options_dropdown::<wlx_common::config::CaptureMethod>(par.mp, c, &SettingType::CaptureMethod)?;
options_checkbox(par.mp, c, SettingType::XwaylandByDefault)?;
options_checkbox(par.mp, c, SettingType::UprightScreenFix)?;
options_checkbox(par.mp, c, SettingType::DoubleCursorFix)?;
options_checkbox(par.mp, c, SettingType::ScreenRenderDown)?;
Ok(State {})
}
}

View File

@ -0,0 +1,70 @@
use wgui::{assets::AssetPath, i18n::Translation, layout::Layout, task::Tasks};
use crate::{
frontend::FrontendTasks,
tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox, options_slider_f32},
},
util::{popup_manager::PopupHolder, wgui_simple},
views::{self, ViewUpdateParams, skymap_list},
};
#[derive(Clone)]
enum Task {
ShowSkymapList,
}
pub struct State {
popup_skymap_list: PopupHolder<skymap_list::View>,
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
}
impl SettingsTab for State {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
self.popup_skymap_list.update(par)?;
for task in self.tasks.drain() {
match task {
Task::ShowSkymapList => self.show_skymap_list(par.layout),
}
}
Ok(())
}
}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
let id_category = options_category(par.mp, par.id_parent, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?;
options_checkbox(par.mp, id_category, SettingType::UseSkybox)?;
options_checkbox(par.mp, id_category, SettingType::UsePassthrough)?;
options_slider_f32(par.mp, id_category, SettingType::GridOpacity, 0.0, 1.0, 0.05)?;
let tasks = Tasks::<Task>::new();
// "Browse skymaps" button
wgui_simple::create_button(wgui_simple::CreateButtonParams {
id_parent: id_category,
layout: par.mp.layout,
content: Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"),
icon_builtin: AssetPath::BuiltIn("dashboard/globe.svg"),
on_click: tasks.get_button_click_callback(Task::ShowSkymapList),
})?;
Ok(Self {
popup_skymap_list: Default::default(),
frontend_tasks: par.frontend_tasks.clone(),
tasks,
})
}
fn show_skymap_list(&mut self, layout: &mut Layout) {
views::skymap_list::mount_popup(
self.frontend_tasks.clone(),
layout.state.globals.clone(),
self.popup_skymap_list.clone(),
);
}
}

View File

@ -0,0 +1,138 @@
use wgui::{
assets::AssetPath,
i18n::Translation,
layout::{Layout, LayoutTask, WidgetID},
parser::{Fetchable, ParseDocumentParams},
};
use crate::{
tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox, options_slider_f32},
},
util::wgui_simple,
};
pub struct State {
id_space_gravity_parent: WidgetID,
}
fn set_visible(parent: WidgetID, layout: &mut Layout, n: bool) {
layout.tasks.push(LayoutTask::SetWidgetVisible(parent, n));
}
impl SettingsTab for State {
fn setting_updated(&mut self, sup: &mut super::SettingUpdatedParams) -> anyhow::Result<()> {
if sup.setting_type == SettingType::SpaceGravityEnabled {
set_visible(
self.id_space_gravity_parent,
sup.layout,
sup.config.space_gravity_enabled,
);
}
Ok(())
}
}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.SPACE_DRAG", "dashboard/drag.svg")?;
let globals = par.mp.layout.state.globals.clone();
let tab_state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals,
path: AssetPath::BuiltIn("gui/tab/settings_tab_space_drag.xml"),
extra: Default::default(),
},
par.mp.layout,
c,
)?;
let id_common_options_parent = tab_state.get_widget_id("common_options_parent")?;
let id_gravity_enabled_parent = tab_state.get_widget_id("gravity_enabled_parent")?;
let id_space_gravity_parent = tab_state.get_widget_id("space_gravity_parent")?;
if !par.feats.openxr || par.feats.monado {
// monado or openvr
options_checkbox(par.mp, id_common_options_parent, SettingType::SpaceDragUnlocked)?;
options_slider_f32(
par.mp,
id_common_options_parent,
SettingType::SpaceDragMultiplier,
-10.0,
10.0,
0.5,
)?;
}
if par.feats.monado {
// openvr can only ever rotate yaw
options_checkbox(par.mp, id_common_options_parent, SettingType::SpaceRotateUnlocked)?;
}
if par.feats.monado {
/* space gravity section */
options_checkbox(par.mp, id_gravity_enabled_parent, SettingType::SpaceGravityEnabled)?;
options_slider_f32(
par.mp,
id_space_gravity_parent,
SettingType::SpaceGravityGravity,
0.0,
10.0,
0.5,
)?;
options_slider_f32(
par.mp,
id_space_gravity_parent,
SettingType::SpaceGravityDamping,
0.1,
1.0,
0.01,
)?;
options_slider_f32(
par.mp,
id_space_gravity_parent,
SettingType::SpaceGravityFlingStrength,
0.0,
3.0,
0.1,
)?;
options_slider_f32(
par.mp,
id_space_gravity_parent,
SettingType::SpaceGravityGroundFriction,
0.0,
1.0,
0.01,
)?;
options_slider_f32(
par.mp,
id_space_gravity_parent,
SettingType::SpaceGravityFloorHeight,
-5.0,
5.0,
0.1,
)?;
} else {
wgui_simple::create_label(
par.mp.layout,
id_gravity_enabled_parent,
Translation::from_translation_key("APP_SETTINGS.NOT_SUPPORTED"),
)?;
}
set_visible(
id_space_gravity_parent,
par.mp.layout,
par.mp.config.space_gravity_enabled,
);
Ok(State {
id_space_gravity_parent,
})
}
}

View File

@ -0,0 +1,55 @@
use crate::tab::settings::{
SettingsMountParams, SettingsTab, Task,
macros::{options_category, options_danger_button},
};
pub struct State {}
impl SettingsTab for State {}
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
let c = options_category(
par.mp,
par.id_parent,
"APP_SETTINGS.TROUBLESHOOTING",
"dashboard/cpu.svg",
)?;
options_danger_button(
par.mp,
c,
"APP_SETTINGS.RESET_PLAYSPACE",
"dashboard/recenter.svg",
Task::ResetPlayspace,
)?;
options_danger_button(
par.mp,
c,
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
"dashboard/display.svg",
Task::ClearPipewireTokens,
)?;
options_danger_button(
par.mp,
c,
"APP_SETTINGS.CLEAR_SAVED_STATE",
"dashboard/binary.svg",
Task::ClearSavedState,
)?;
options_danger_button(
par.mp,
c,
"APP_SETTINGS.DELETE_ALL_CONFIGS",
"dashboard/circle.svg",
Task::DeleteAllConfigs,
)?;
options_danger_button(
par.mp,
c,
"APP_SETTINGS.RESTART_SOFTWARE",
"dashboard/refresh.svg",
Task::RestartSoftware,
)?;
Ok(State {})
}
}

View File

@ -1,8 +1,7 @@
use crate::util::{networking::http_client, steam_utils::AppID};
use anyhow::Context;
use serde::Deserialize;
use wlx_common::cache_dir;
use crate::util::{http_client, steam_utils::AppID, various::AsyncExecutor};
use wlx_common::{async_executor::AsyncExecutor, cache_dir};
pub struct CoverArt {
// can be empty in case if data couldn't be fetched (use a fallback image then)
@ -24,7 +23,7 @@ pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Re
app_id
);
match http_client::get(&executor, &url).await {
match http_client::get_simple(&executor, &url).await {
Ok(response) => {
log::info!("Success");
cache_dir::set_data(&cache_file_path, &response.data).await?;
@ -69,7 +68,7 @@ async fn get_app_details_json_internal(
// Fetch from Steam API
log::info!("Fetching app detail ID {}", app_id);
let url = format!("https://store.steampowered.com/api/appdetails?appids={}", app_id);
let response = http_client::get(&executor, &url).await?;
let response = http_client::get_simple(&executor, &url).await?;
let res_utf8 = String::from_utf8(response.data)?;
let root = serde_json::from_str::<serde_json::Value>(&res_utf8)?;
let body = root.get(&app_id).context("invalid body")?;

View File

@ -1,7 +1,7 @@
pub mod cached_fetcher;
pub mod http_client;
pub mod networking;
pub mod pactl_wrapper;
pub mod popup_manager;
pub mod steam_utils;
pub mod toast_manager;
pub mod various;
pub mod wgui_simple;

View File

@ -13,16 +13,38 @@ use smol::{net::TcpStream, prelude::*};
use std::convert::TryInto;
use std::pin::Pin;
use std::task::{Context, Poll};
use crate::util::various::AsyncExecutor;
use wlx_common::async_executor::AsyncExecutor;
pub struct HttpClientResponse {
pub data: Vec<u8>,
}
pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> {
log::info!("fetching URL \"{}\"", url);
impl HttpClientResponse {
pub fn into_json<T>(self) -> anyhow::Result<T>
where
T: for<'a> serde::Deserialize<'a>,
{
let utf8 = str::from_utf8(&self.data)?;
Ok(serde_json::from_str::<T>(utf8)?)
}
}
let url: hyper::Uri = url.try_into()?;
pub struct ProgressFuncData {
pub bytes_downloaded: u64,
pub file_size: u64,
}
pub type ProgressFunc = Box<dyn Fn(ProgressFuncData)>;
pub struct GetParams<'a> {
pub executor: &'a AsyncExecutor,
pub url: &'a str,
pub on_progress: Option<ProgressFunc>,
}
pub async fn get(params: GetParams<'_>) -> anyhow::Result<HttpClientResponse> {
log::info!("fetching URL \"{}\"", params.url);
let url: hyper::Uri = params.url.try_into()?;
let req = Request::builder()
.header(
hyper::header::HOST,
@ -31,23 +53,55 @@ pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClie
.uri(url)
.body(Empty::new())?;
let resp = fetch(executor, req).await?;
let resp = fetch(params.executor, req).await?;
if !resp.status().is_success() {
// non-200 HTTP response
anyhow::bail!("non-200 HTTP response: {}", resp.status().as_str());
}
let body = BodyStream::new(resp.into_body())
let mut bytes_downloaded: u64 = 0;
let mut file_size: u64 = 1;
let (parts, body) = resp.into_parts();
// that's a pretty interesting way to get file size :]
if let Some(val) = parts.headers.get("Content-Length")
&& let Ok(str) = val.to_str()
&& let Ok(s) = str.parse()
{
file_size = s;
}
let mut on_progress = params.on_progress;
let data = BodyStream::new(body)
.try_fold(Vec::new(), |mut body, chunk| {
if let Some(chunk) = chunk.data_ref() {
bytes_downloaded += chunk.len() as u64;
body.extend_from_slice(chunk);
if let Some(on_progress) = &mut on_progress {
on_progress(ProgressFuncData {
bytes_downloaded,
file_size,
})
}
}
Ok(body)
})
.await?;
Ok(HttpClientResponse { data: body })
Ok(HttpClientResponse { data })
}
pub async fn get_simple(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> {
get(GetParams {
executor,
url,
on_progress: None,
})
.await
}
async fn fetch(

View File

@ -0,0 +1,16 @@
use std::rc::Rc;
use wgui::{globals::WguiGlobals, renderer_vk::text::custom_glyph::CustomGlyphData};
use wlx_common::async_executor::AsyncExecutor;
use crate::util::networking::http_client;
pub async fn fetch_to_glyph_data(
globals: &WguiGlobals,
executor: &AsyncExecutor,
url: &str,
) -> anyhow::Result<(CustomGlyphData, Rc<Vec<u8>>)> {
let res = http_client::get_simple(executor, url).await?;
let glyph_data = CustomGlyphData::from_bytes_raster(globals, url, &res.data)?;
Ok((glyph_data, Rc::new(res.data)))
}

View File

@ -0,0 +1,6 @@
pub mod http_client;
pub mod image_fetch;
pub mod skymap_catalog;
// pub const WAYVR_ROOT_URL: &str = "https://wayvr.org";
pub const WAYVR_SKYMAPS_ROOT: &str = "https://wayvr.org/skymaps";

View File

@ -0,0 +1,189 @@
use crate::util::networking::{self, WAYVR_SKYMAPS_ROOT, http_client};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use wlx_common::{async_executor::AsyncExecutor, config_io};
pub type SkymapUuid = uuid::Uuid;
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
pub enum SkymapResolution {
Res2k,
Res4k,
Res8k,
}
impl SkymapResolution {
pub const fn get_display_str(&self) -> &'static str {
match self {
SkymapResolution::Res2k => "2K (2 MiB VRAM)",
SkymapResolution::Res4k => "4K (8 MiB VRAM)",
SkymapResolution::Res8k => "8K (33 MiB VRAM)",
}
}
pub const fn get_display_str_simple(&self) -> &'static str {
match self {
SkymapResolution::Res2k => "2K",
SkymapResolution::Res4k => "4K",
SkymapResolution::Res8k => "8K",
}
}
pub fn from_display_str_simple(text: &str) -> Option<SkymapResolution> {
match text {
"2K" => Some(SkymapResolution::Res2k),
"4K" => Some(SkymapResolution::Res4k),
"8K" => Some(SkymapResolution::Res8k),
_ => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SkymapCatalogEntryFiles {
pub size_8k: Option<String>, // "my_skymap_8k.dds"
pub size_4k: Option<String>, // "my_skymap_4k.dds"
pub size_2k: String, // we should have *at least* this
pub preview: String,
}
impl SkymapCatalogEntryFiles {
pub fn get_url_preview(&self) -> String {
format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, self.preview)
}
pub fn get_filename_from_res(&self, res: SkymapResolution) -> Option<String> {
match res {
SkymapResolution::Res2k => Some(&self.size_2k),
SkymapResolution::Res4k => self.size_4k.as_ref(),
SkymapResolution::Res8k => self.size_8k.as_ref(),
}
.map(|raw_filename| {
// sanitize filename, do not allow "../" just in case
PathBuf::from(raw_filename)
.file_name()
.map(|s| String::from(s.to_string_lossy()))
})?
}
// example result: "https://wayvr.org/skymaps/files/my_skymap_8k.dds"
pub fn get_url_from_res(&self, res: SkymapResolution) -> Option<String> {
let filename = self.get_filename_from_res(res)?;
Some(format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, filename))
}
pub fn get_preview_path(&self) -> PathBuf {
config_io::get_skymaps_root().join(&self.preview)
}
pub fn save_preview_to_file(&self, data: &[u8]) -> anyhow::Result<()> {
std::fs::write(self.get_preview_path(), data)?;
Ok(())
}
pub fn remove_preview_file(&self) {
let _dont_care = std::fs::remove_file(self.get_preview_path());
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SkymapCatalogEntry {
pub uuid: SkymapUuid,
pub created_at: String,
pub modified_at: String,
pub version: u32,
pub name: String,
pub description: String,
pub author: String,
pub files: SkymapCatalogEntryFiles,
}
impl SkymapCatalogEntry {
pub fn get_destination_path(&self, resolution: SkymapResolution) -> Option<PathBuf> {
let filename = self.files.get_filename_from_res(resolution)?;
Some(config_io::get_skymaps_root().join(filename))
}
pub fn get_destination_metadata_path(&self) -> PathBuf {
config_io::get_skymaps_root().join(format!("{}.json", self.uuid))
}
pub fn is_downloaded(&self, resolution: SkymapResolution) -> anyhow::Result<bool> {
let Some(full_path) = self.get_destination_path(resolution) else {
return Ok(false);
};
Ok(std::fs::exists(full_path)?)
}
pub fn has_any_downloaded(&self) -> bool {
self.is_downloaded(SkymapResolution::Res2k).unwrap_or(false)
|| self.is_downloaded(SkymapResolution::Res4k).unwrap_or(false)
|| self.is_downloaded(SkymapResolution::Res8k).unwrap_or(false)
}
pub fn remove_file(&self, resolution: SkymapResolution) {
let Some(full_path) = self.get_destination_path(resolution) else {
return;
};
let _dont_care = std::fs::remove_file(full_path);
}
pub fn save_metadata(&self) -> anyhow::Result<()> {
let json = serde_json::to_string_pretty(self)?;
std::fs::write(self.get_destination_metadata_path(), json)?;
Ok(())
}
pub fn remove_metadata(&self) {
let _dont_care = std::fs::remove_file(self.get_destination_metadata_path());
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct SkymapCatalog {
pub version: u32,
pub r#type: String,
pub entries: Vec<SkymapCatalogEntry>,
}
impl SkymapCatalog {
fn validate(&self) -> anyhow::Result<()> {
if self.version != 1 {
anyhow::bail!("Unsupported version");
}
if self.r#type != "wayvr_skymaps" {
anyhow::bail!("Unsupported type");
}
Ok(())
}
}
pub async fn request_catalog(executor: &AsyncExecutor) -> anyhow::Result<SkymapCatalog> {
log::info!("Fetching skymap list");
let res = http_client::get_simple(executor, &format!("{}/catalog.json", networking::WAYVR_SKYMAPS_ROOT)).await?;
let catalog = res.into_json::<SkymapCatalog>()?;
catalog.validate()?;
Ok(catalog)
}
pub fn get_entries_from_disk() -> anyhow::Result<Vec<SkymapCatalogEntry>> {
let mut entries = Vec::<SkymapCatalogEntry>::new();
let skymaps_root = config_io::get_skymaps_root();
for uuid in config_io::get_skymaps_uuids().unwrap_or_default() {
let metadata_path = skymaps_root.join(format!("{}.json", uuid));
let Ok(data) = std::fs::read_to_string(metadata_path) else {
continue;
};
let entry = serde_json::from_str::<SkymapCatalogEntry>(&data)?;
entries.push(entry);
}
Ok(entries)
}

View File

@ -6,17 +6,19 @@ use std::{
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
event::{EventAlterables, StyleSetRequest},
event::EventAlterables,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, LayoutTask, LayoutTasks, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
taffy::Display,
widget::label::WidgetLabel,
};
use wlx_common::config::GeneralConfig;
use crate::frontend::{FrontendTask, FrontendTasks};
use crate::{
frontend::{FrontendTask, FrontendTasks},
views::{ViewTrait, ViewUpdateParams},
};
pub struct PopupManagerParams {
pub parent_id: WidgetID,
@ -34,15 +36,136 @@ pub struct MountedPopup {
frontend_tasks: FrontendTasks,
}
#[derive(Default)]
struct MountedPopupState {
mounted_popup: Option<MountedPopup>,
closed_callback: Option<PopupClosedCallback>,
}
#[derive(Clone)]
#[derive(Default, Clone)]
pub struct PopupHandle {
state: Rc<RefCell<MountedPopupState>>,
}
struct PopupHolderState<ViewType: ViewTrait> {
popup_handle: PopupHandle,
view: Option<ViewType>,
on_view_close: Option<Box<dyn FnOnce()>>,
}
// we can't use #[derive(Default)] due to the fact that ViewType can't be Default.
impl<ViewType: ViewTrait> Default for PopupHolderState<ViewType> {
fn default() -> Self {
Self {
popup_handle: Default::default(),
view: None,
on_view_close: None,
}
}
}
pub struct PopupHolder<ViewType: ViewTrait> {
state: Rc<RefCell<PopupHolderState<ViewType>>>,
}
impl<ViewType: ViewTrait> Default for PopupHolder<ViewType> {
fn default() -> Self {
Self {
state: Rc::new(RefCell::new(PopupHolderState::default())),
}
}
}
impl<ViewType: ViewTrait> PopupHolderState<ViewType> {
fn close(&mut self) {
if self.view.is_some() {
self.view = None;
if let Some(on_close) = self.on_view_close.take() {
on_close();
}
}
self.popup_handle.close();
}
}
impl<ViewType: ViewTrait> Drop for PopupHolderState<ViewType> {
fn drop(&mut self) {
self.close();
}
}
// we can't derive(Clone) due to the fact that ViewType is non-cloneable
impl<ViewType: ViewTrait> Clone for PopupHolder<ViewType> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
}
}
}
impl<ViewType: ViewTrait> PopupHolder<ViewType> {
pub fn update(&self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
let mut state = self.state.borrow_mut();
let Some(view) = &mut state.view else {
return Ok(());
};
view.update(par)
}
pub fn set_view(&self, handle: PopupHandle, view: ViewType, on_view_close: Option<Box<dyn FnOnce()>>) {
let mut state = self.state.borrow_mut();
state.view = Some(view);
state.popup_handle = handle;
state.on_view_close = on_view_close;
}
// Get underlying ViewType object in a closure and return its value
// example usage:
//
// ```rs
// holder.with_view(|view| {
// view.foo();
// })
// ```
//
pub fn with_view<F, R>(&self, f: F) -> Option<R>
where
F: FnOnce(&mut ViewType) -> R,
{
let mut state = self.state.borrow_mut();
state.view.as_mut().map(f)
}
// Same as with_view, but the closure expects a simple anyhow::Result<()> type
pub fn with_view_res<F>(&self, f: F) -> anyhow::Result<()>
where
F: FnOnce(&mut ViewType) -> anyhow::Result<()>,
{
if let Some(res) = self.with_view(f) {
return res;
}
Ok(())
}
pub fn get_close_callback(&self, layout: &Layout) -> Box<dyn FnOnce()>
where
ViewType: 'static,
{
let layout_tasks = layout.tasks.clone();
let weak_state = Rc::downgrade(&self.state);
Box::new(move || {
// we can't borrow State here yet, dispatch it.
layout_tasks.push(LayoutTask::Dispatch(Box::new(move |_common| {
if let Some(state) = weak_state.upgrade() {
state.borrow_mut().close();
}
Ok(())
})));
})
}
}
impl PopupHandle {
pub fn close(&self) {
self.state.borrow_mut().mounted_popup = None; // Drop will be called
@ -61,10 +184,24 @@ pub struct PopupContentFuncData<'a> {
pub id_content: WidgetID,
}
type PopupClosedCallback = Box<dyn FnOnce()>;
type OnContentCallback = Box<dyn FnOnce(PopupContentFuncData) -> anyhow::Result<PopupClosedCallback>>;
// we need to implement Clone here, but the underlying function can be called only once.
// on_content will be cleared after the first call
#[derive(Clone)]
pub struct MountPopupParams {
pub title: Translation,
pub on_content: Rc<dyn Fn(PopupContentFuncData) -> anyhow::Result<()>>,
pub struct MountPopupOnceParams {
title: Translation,
on_content: Rc<RefCell<Option<OnContentCallback>>>,
}
impl MountPopupOnceParams {
pub fn new(title: Translation, on_content: OnContentCallback) -> Self {
Self {
title,
on_content: Rc::new(RefCell::new(Some(on_content))),
}
}
}
impl Drop for MountedPopup {
@ -78,25 +215,23 @@ impl State {
fn refresh_stack(&mut self, alterables: &mut EventAlterables) {
// show only the topmost popup
self.popup_stack.retain(|weak| {
let Some(popup) = weak.upgrade() else {
return false;
let retain = {
let Some(popup) = weak.upgrade() else {
return false;
};
popup.borrow_mut().mounted_popup.is_some()
};
popup.borrow_mut().mounted_popup.is_some()
if !retain {
log::debug!("removing popup from popup_stack");
}
retain
});
for (idx, popup) in self.popup_stack.iter().enumerate() {
let popup = popup.upgrade().unwrap(); // safe
let popup = popup.borrow_mut();
let mounted_popup = popup.mounted_popup.as_ref().unwrap(); // safe;
alterables.set_style(
mounted_popup.id_root,
StyleSetRequest::Display(if idx == self.popup_stack.len() - 1 {
Display::Flex
} else {
Display::None
}),
);
alterables.set_widget_visible(mounted_popup.id_root, idx == self.popup_stack.len() - 1);
}
}
}
@ -116,16 +251,13 @@ impl PopupManager {
state.refresh_stack(alterables);
}
/// Mount a new popup on top of the existing popup stack.
/// Only the topmost popup is visible.
pub fn mount_popup(
&mut self,
globals: WguiGlobals,
fn mount_popup_prepare(
&self,
globals: &WguiGlobals,
layout: &mut Layout,
frontend_tasks: FrontendTasks,
params: MountPopupParams,
config: &GeneralConfig,
) -> anyhow::Result<()> {
frontend_tasks: &FrontendTasks,
popup_title: &Translation,
) -> anyhow::Result<(PopupHandle, WidgetID /* content widget ID */)> {
let doc_params = &ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/popup_window.xml"),
@ -138,7 +270,7 @@ impl PopupManager {
{
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&layout.state, "popup_title")?;
label_title.set_text_simple(&mut globals.get(), params.title);
label_title.set_text_simple(&mut globals.get(), popup_title.clone());
}
let but_back = state.fetch_component_as::<ComponentButton>("but_back")?;
@ -152,6 +284,7 @@ impl PopupManager {
let mounted_popup_state = MountedPopupState {
mounted_popup: Some(mounted_popup),
closed_callback: None,
};
let popup_handle = PopupHandle {
@ -159,28 +292,56 @@ impl PopupManager {
};
let mut state = self.state.borrow_mut();
log::debug!("pushing popup to popup_stack");
state.popup_stack.push(Rc::downgrade(&popup_handle.state));
but_back.on_click({
let popup_handle = Rc::downgrade(&popup_handle.state);
Box::new(move |_common, _evt| {
if let Some(popup_handle) = popup_handle.upgrade() {
popup_handle.borrow_mut().mounted_popup = None; // will call Drop
Rc::new(move |_common, _evt| {
if let Some(popup_handle) = popup_handle.upgrade()
&& let Some(closed_callback) = {
let mut state = popup_handle.borrow_mut();
state.mounted_popup = None; // will call Drop
state.closed_callback.take()
} {
log::debug!("closed_callback called");
closed_callback();
}
Ok(())
})
});
frontend_tasks.push(FrontendTask::RefreshPopupManager);
Ok((popup_handle, id_content))
}
/// Mount a new popup on top of the existing popup stack.
/// Only the topmost popup is visible.
pub fn mount_popup_once(
&mut self,
globals: &WguiGlobals,
layout: &mut Layout,
frontend_tasks: &FrontendTasks,
params: MountPopupOnceParams,
config: &GeneralConfig,
) -> anyhow::Result<()> {
let mut func = params.on_content.borrow_mut();
let Some(on_content_func) = func.take() else {
anyhow::bail!("mount_popup_once called more than once");
};
let (popup_handle, id_content) = self.mount_popup_prepare(globals, layout, frontend_tasks, &params.title)?;
// mount user-set popup content
(*params.on_content)(PopupContentFuncData {
let closed_callback = on_content_func(PopupContentFuncData {
layout,
handle: popup_handle.clone(),
id_content,
config,
})?;
popup_handle.state.borrow_mut().closed_callback = Some(closed_callback);
Ok(())
}
}

View File

@ -2,6 +2,7 @@ use keyvalues_parser::{Obj, Vdf};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Clone)]
pub struct SteamUtils {
steam_root: PathBuf,
}
@ -32,7 +33,10 @@ pub struct AppManifest {
pub last_played: Option<u64>, // unix timestamp
}
// TODO @oo8dev: game sort methods
#[allow(dead_code)]
pub enum GameSortMethod {
None,
NameAsc,
NameDesc,
PlayDateDesc,
@ -119,6 +123,12 @@ struct AppEntry {
pub app_id: AppID,
}
pub fn launch(app_id: &AppID) -> anyhow::Result<()> {
log::info!("Launching Steam game with AppID {}", app_id);
call_steam(&format!("steam://rungameid/{}", app_id))?;
Ok(())
}
pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
log::info!("Stopping Steam game with AppID {}", app_id);
@ -137,12 +147,6 @@ pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
Ok(())
}
pub fn launch(app_id: &AppID) -> anyhow::Result<()> {
log::info!("Launching Steam game with AppID {}", app_id);
call_steam(&format!("steam://rungameid/{}", app_id))?;
Ok(())
}
#[derive(Serialize)]
pub struct RunningGame {
pub app_id: AppID,
@ -281,6 +285,7 @@ impl SteamUtils {
.collect();
match sort_method {
GameSortMethod::None => {}
GameSortMethod::NameAsc => {
games.sort_by(|a, b| a.name.cmp(&b.name));
}

View File

@ -5,7 +5,6 @@ use wgui::{
animation::{Animation, AnimationEasing},
components::tooltip::{TOOLTIP_BORDER_COLOR, TOOLTIP_COLOR},
drawing::Color,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, LayoutTask, LayoutTasks, WidgetID},
renderer_vk::{
@ -61,15 +60,7 @@ impl ToastManager {
}
}
fn mount_toast(
&self,
globals: &WguiGlobals,
layout: &mut Layout,
state: &mut State,
content: Translation,
) -> anyhow::Result<()> {
let mut globals = globals.get();
fn mount_toast(&self, layout: &mut Layout, state: &mut State, content: Translation) -> anyhow::Result<()> {
let (root, _) = layout.add_topmost_child(
WidgetDiv::create(),
taffy::Style {
@ -110,27 +101,24 @@ impl ToastManager {
},
)?;
let (label, _) = layout.add_child(
rect.id,
WidgetLabel::create(
&mut globals,
WidgetLabelParams {
content,
style: TextStyle {
weight: Some(FontWeight::Bold),
align: Some(HorizontalAlign::Center),
wrap: true,
..Default::default()
},
let label = WidgetLabel::create(
&mut layout.state,
WidgetLabelParams {
content,
style: TextStyle {
weight: Some(FontWeight::Bold),
align: Some(HorizontalAlign::Center),
wrap: true,
..Default::default()
},
),
taffy::Style { ..Default::default() },
)?;
},
);
let (label, _) = layout.add_child(rect.id, label, taffy::Style { ..Default::default() })?;
// show-up animation
layout.animations.add(Animation::new(
rect.id,
(TOAST_DURATION_TICKS as f32 * globals.defaults.animation_mult) as u32,
(TOAST_DURATION_TICKS as f32 * layout.state.theme.animation_mult) as u32,
AnimationEasing::Linear,
Box::new(move |common, data| {
let pos_showup = AnimationEasing::OutQuint.interpolate((data.pos * 4.0).min(1.0));
@ -161,7 +149,7 @@ impl ToastManager {
Ok(())
}
pub fn tick(&mut self, globals: &WguiGlobals, layout: &mut Layout) -> anyhow::Result<()> {
pub fn tick(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
let mut state = self.state.borrow_mut();
if state.timeout > 0 {
state.timeout -= 1;
@ -176,7 +164,7 @@ impl ToastManager {
state.timeout = TOAST_DURATION_TICKS;
// mount next
if let Some(content) = state.queue.pop_front() {
self.mount_toast(globals, layout, &mut state, content)?;
self.mount_toast(layout, &mut state, content)?;
}
}

View File

@ -1,77 +0,0 @@
use std::{path::PathBuf, rc::Rc, str::FromStr};
use wgui::{
assets::{AssetPath, AssetPathOwned},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
renderer_vk::text::custom_glyph::CustomGlyphData,
taffy::{self, prelude::length},
widget::{
label::{WidgetLabel, WidgetLabelParams},
sprite::{WidgetSprite, WidgetSpriteParams},
},
};
use wlx_common::desktop_finder;
pub type AsyncExecutor = Rc<smol::LocalExecutor<'static>>;
// the compiler wants to scream
#[allow(irrefutable_let_patterns)]
pub fn get_desktop_file_icon_path(desktop_file: &desktop_finder::DesktopEntry) -> AssetPathOwned {
/*
FIXME: why is the compiler complaining about trailing irrefutable patterns there?!?!
looking at the PathBuf::from_str implementation, it always returns Ok() and it's inline, maybe that's why.
*/
if let Some(icon) = &desktop_file.icon_path
&& let Ok(path) = PathBuf::from_str(icon)
{
return AssetPathOwned::File(path);
}
AssetPathOwned::BuiltIn(PathBuf::from_str("dashboard/terminal.svg").unwrap())
}
pub fn mount_simple_label(
globals: &WguiGlobals,
layout: &mut Layout,
parent_id: WidgetID,
translation: Translation,
) -> anyhow::Result<()> {
layout.add_child(
parent_id,
WidgetLabel::create(
&mut globals.get(),
WidgetLabelParams {
content: translation,
..Default::default()
},
),
taffy::Style::default(),
)?;
Ok(())
}
pub fn mount_simple_sprite_square(
globals: &WguiGlobals,
layout: &mut Layout,
parent_id: WidgetID,
size_px: f32,
path: AssetPath,
) -> anyhow::Result<()> {
layout.add_child(
parent_id,
WidgetSprite::create(WidgetSpriteParams {
glyph_data: Some(CustomGlyphData::from_assets(globals, path)?),
..Default::default()
}),
taffy::Style {
size: taffy::Size {
width: length(size_px),
height: length(size_px),
},
..Default::default()
},
)?;
Ok(())
}

View File

@ -0,0 +1,159 @@
use glam::{Mat4, Vec2};
use wgui::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
components::{self, button::ButtonClickCallback},
drawing,
i18n::Translation,
layout::{Layout, LayoutTask, WidgetID},
parser::{Fetchable, ParseDocumentParams},
renderer_vk::{
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
util::centered_matrix,
},
taffy::{self, prelude::length},
widget::{
ConstructEssentials,
label::{WidgetLabel, WidgetLabelParams},
sprite::{WidgetSprite, WidgetSpriteParams},
},
};
pub struct CreateButtonParams<'a> {
pub id_parent: WidgetID,
pub layout: &'a mut Layout,
pub content: Translation,
pub icon_builtin: AssetPath<'a>,
pub on_click: ButtonClickCallback,
}
pub fn create_button(par: CreateButtonParams) -> anyhow::Result<()> {
let (_, button) = components::button::construct(
&mut ConstructEssentials {
layout: par.layout,
parent: par.id_parent,
},
components::button::Params {
text: Some(par.content),
sprite_src: Some(par.icon_builtin),
..Default::default()
},
)?;
button.on_click(par.on_click);
Ok(())
}
#[allow(dead_code)]
pub fn create_label(layout: &mut Layout, id_parent: WidgetID, content: Translation) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut layout.state,
WidgetLabelParams {
content,
style: TextStyle {
wrap: true,
..Default::default()
},
},
);
layout.add_child(id_parent, label, Default::default())?;
Ok(())
}
pub fn create_label_error(layout: &mut Layout, parent: WidgetID, content: String) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut layout.state,
WidgetLabelParams {
content: Translation::from_raw_text_string(content),
style: TextStyle {
wrap: true,
color: Some(drawing::Color::new(1.0, 0.5, 0.0, 1.0)),
weight: Some(FontWeight::Bold),
..Default::default()
},
},
);
layout.add_child(parent, label, Default::default())?;
Ok(())
}
pub fn create_icon(layout: &mut Layout, id_parent: WidgetID, size: Vec2, path: AssetPath) -> anyhow::Result<WidgetID> {
let widget_sprite = WidgetSprite::create(WidgetSpriteParams {
color: None,
glyph_data: Some(CustomGlyphData::from_assets(&layout.state.globals, path)?),
});
let size = taffy::Size {
width: length(size.x),
height: length(size.y),
};
let (widget, _) = layout.add_child(
id_parent,
widget_sprite,
taffy::Style {
min_size: size,
max_size: size,
size,
..Default::default()
},
)?;
Ok(widget.id)
}
pub struct CreateLoadingParams<'a> {
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub with_text: bool,
}
pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result<WidgetID> {
let doc_params = ParseDocumentParams {
globals: par.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/t_loading.xml"),
extra: Default::default(),
};
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.parent_id)?;
let data = parser_state.realize_template(
&doc_params,
if par.with_text {
"LoadingWithText"
} else {
"LoadingWithoutText"
},
par.layout,
par.parent_id,
Default::default(),
)?;
let id_root = data.get_widget_id("root")?;
let id_sprite_loading = data.get_widget_id("sprite_loading")?;
par.layout.animations.add(Animation::new(
id_sprite_loading,
60 * 30, /* spin it for 30 seconds at most */
AnimationEasing::Linear,
Box::new(move |common, data| {
// spin it
data.data.transform = centered_matrix(data.widget_boundary.size, &Mat4::from_rotation_z(data.pos * 400.0));
if data.pos == 1.0 {
// remove the spinner, do not waste energy
common
.alterables
.tasks
.push(LayoutTask::RemoveWidget(id_sprite_loading));
}
common.alterables.mark_redraw();
}),
));
Ok(id_root)
}

View File

@ -14,7 +14,11 @@ use wgui::{
};
use wlx_common::{config::GeneralConfig, dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
use crate::frontend::{FrontendTask, FrontendTasks, SoundType};
use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType},
util::popup_manager::{MountPopupOnceParams, PopupHolder},
views::{ViewTrait, ViewUpdateParams},
};
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum PosMode {
@ -50,7 +54,6 @@ enum CompositorMode {
enum Task {
SetCompositor(CompositorMode),
SetRes(ResMode),
SetPos(PosMode),
SetOrientation(OrientationMode),
SetAutoStart(bool),
Launch,
@ -67,7 +70,7 @@ struct LaunchParams<'a, T> {
interface: &'a mut BoxDashInterface<T>,
auto_start: bool,
data: &'a mut T,
on_launched: &'a dyn Fn(),
on_launched: Option<Box<dyn FnOnce()>>,
}
pub struct View {
@ -92,7 +95,7 @@ pub struct View {
auto_start: bool,
on_launched: Box<dyn Fn()>,
on_launched: Option<Box<dyn FnOnce()>>,
}
pub struct Params<'a> {
@ -102,7 +105,13 @@ pub struct Params<'a> {
pub parent_id: WidgetID,
pub config: &'a GeneralConfig,
pub frontend_tasks: &'a FrontendTasks,
pub on_launched: Box<dyn Fn()>,
pub on_launched: Box<dyn FnOnce()>,
}
impl ViewTrait for View {
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
Ok(())
}
}
impl View {
@ -156,7 +165,7 @@ impl View {
} else {
CompositorMode::Native
};
radio_compositor.set_value(compositor_mode.as_ref())?;
radio_compositor.set_value_simple(compositor_mode.as_ref())?;
tasks.push(Task::SetCompositor(compositor_mode));
let res_mode = ResMode::Res1080;
@ -180,7 +189,7 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
CompositorMode::from_str(&*v)
CompositorMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for compositor: '{v}'. Valid values are: {:?}",
@ -199,7 +208,7 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
ResMode::from_str(&*v)
ResMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for resolution: '{v}'. Valid values are: {:?}",
@ -237,7 +246,7 @@ impl View {
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
OrientationMode::from_str(&*v)
OrientationMode::from_str(&v)
.inspect_err(|_| {
log::error!(
"Invalid value for orientation: '{v}'. Valid values are: {:?}",
@ -281,7 +290,7 @@ impl View {
entry: params.entry,
frontend_tasks: params.frontend_tasks.clone(),
globals: params.globals.clone(),
on_launched: params.on_launched,
on_launched: Some(params.on_launched),
})
}
@ -295,7 +304,6 @@ impl View {
match task {
Task::SetCompositor(mode) => self.compositor_mode = mode,
Task::SetRes(mode) => self.res_mode = mode,
Task::SetPos(mode) => self.pos_mode = mode,
Task::SetOrientation(mode) => self.orientation_mode = mode,
Task::SetAutoStart(auto_start) => self.auto_start = auto_start,
Task::Launch => self.action_launch(interface, data),
@ -318,7 +326,7 @@ impl View {
auto_start: self.auto_start,
interface,
data,
on_launched: &self.on_launched,
on_launched: self.on_launched.take(),
});
}
@ -336,7 +344,7 @@ impl View {
))));
}
fn launch<T>(params: LaunchParams<T>) -> anyhow::Result<()> {
fn launch<T>(mut params: LaunchParams<T>) -> anyhow::Result<()> {
let mut env = Vec::<String>::new();
if params.compositor_mode == CompositorMode::Native {
@ -392,7 +400,9 @@ impl View {
params.frontend_tasks.push(FrontendTask::PlaySound(SoundType::Launch));
(*params.on_launched)();
if let Some(on_launched) = params.on_launched.take() {
on_launched();
}
// we're done!
Ok(())
@ -422,3 +432,26 @@ impl View {
[width as u32, height as u32]
}
}
pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, entry: DesktopEntry, popup: PopupHolder<View>) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_raw_text(&entry.app_name),
Box::new(move |data| {
let on_launched = popup.get_close_callback(data.layout);
let view = View::new(Params {
entry: entry.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
config: data.config,
on_launched,
})?;
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
)));
}

View File

@ -110,8 +110,7 @@ struct MultiSelectorParams<'a> {
}
fn mount_multi_selector(params: MultiSelectorParams) -> anyhow::Result<()> {
let globals = params.ess.layout.state.globals.clone();
let accent_color = globals.get().defaults.accent_color;
let accent_color = params.ess.layout.state.theme.accent_color;
for cell in params.cells {
let highlighted = cell.key == params.def_cell;
@ -131,7 +130,7 @@ fn mount_multi_selector(params: MultiSelectorParams) -> anyhow::Result<()> {
button.on_click({
let on_click = params.on_click.clone();
let key = cell.key.clone();
Box::new(move |_, _| {
Rc::new(move |_, _| {
(*on_click)(key.as_str());
Ok(())
})
@ -610,7 +609,7 @@ impl View {
fn handle_func_button_click(&self, task: ViewTask) -> ButtonClickCallback {
let tasks = self.tasks.clone();
let on_update = self.on_update.clone();
Box::new(move |_common, _evt| {
Rc::new(move |_common, _evt| {
tasks.push(task.clone());
(*on_update)();
Ok(())
@ -654,11 +653,6 @@ impl View {
}
fn update_button_highlights(&self, layout: &mut Layout) -> anyhow::Result<()> {
let defaults = self.globals.defaults();
let mut c = layout.start_common();
let mut common = c.common();
let num: u8 = match &self.mode {
CurrentMode::Sinks => 0,
CurrentMode::Sources => 1,
@ -666,22 +660,21 @@ impl View {
CurrentMode::CardProfileSelector(_) => 255,
};
let mut com = layout.common();
let mut perform = |btn_num: u8, btn: &Rc<ComponentButton>| {
btn.set_color(
&mut common,
if num == btn_num {
defaults.accent_color
} else {
defaults.button_color
},
);
let color = if num == btn_num {
com.state.theme.accent_color
} else {
com.state.theme.button_color
};
btn.set_color(&mut com, color);
};
perform(0, &self.btn_sinks);
perform(1, &self.btn_sources);
perform(2, &self.btn_cards);
c.finish()?;
Ok(())
}
@ -755,21 +748,20 @@ impl View {
let data = self
.state
.parse_template(&doc_params(&self.globals), "Card", params.layout, self.id_devices, par)?;
.realize_template(&doc_params(&self.globals), "Card", params.layout, self.id_devices, par)?;
let btn_card = data.fetch_component_as::<ComponentButton>("btn_card")?;
btn_card.on_click({
let tasks = self.tasks.clone();
let card = params.card.clone();
let on_update = self.on_update.clone();
Box::new(move |_common, _evt| {
Rc::new(move |_common, _evt| {
tasks.push(ViewTask::SetMode(CurrentMode::CardProfileSelector(card.clone())));
(*on_update)();
Ok(())
})
});
log::info!("mount card TODO: {}", params.card.name);
Ok(())
}
@ -799,7 +791,7 @@ impl View {
},
);
let data = self.state.parse_template(
let data = self.state.realize_template(
&doc_params(&self.globals),
"DeviceSlider",
params.layout,
@ -807,14 +799,12 @@ impl View {
par,
)?;
let mut c = params.layout.start_common();
let mut common = c.common();
let mut common = params.layout.common();
let checkbox = data.fetch_component_as::<ComponentCheckbox>("checkbox")?;
let btn_mute = data.fetch_component_as::<ComponentButton>("btn_mute")?;
let slider = data.fetch_component_as::<ComponentSlider>("slider")?;
slider.set_value(&mut common, params.control.on_volume_request()? / VOLUME_MULT);
slider.set_value_primary(&mut common, params.control.on_volume_request()? / VOLUME_MULT);
checkbox.set_checked(&mut common, params.checked);
@ -829,21 +819,21 @@ impl View {
slider.on_value_changed({
let control = params.control.clone();
Box::new(move |_common, event| {
control.on_volume_change(event.value * VOLUME_MULT)?;
Ok(())
if let Err(e) = control.on_volume_change(event.value * VOLUME_MULT) {
log::error!("{:?}", e);
debug_assert!(false);
};
})
});
btn_mute.on_click({
let control = params.control.clone();
Box::new(move |_common, _event| {
Rc::new(move |_common, _event| {
control.on_mute_toggle()?;
Ok(())
})
});
c.finish()?;
Ok(())
}
@ -946,7 +936,7 @@ impl View {
layout.remove_children(self.id_devices);
{
let data = self.state.parse_template(
let data = self.state.realize_template(
&doc_params(&self.globals),
"SelectAudioProfileText",
layout,
@ -957,7 +947,7 @@ impl View {
btn_back.on_click({
let tasks = self.tasks.clone();
let on_update = self.on_update.clone();
Box::new(move |_, _| {
Rc::new(move |_, _| {
tasks.push(ViewTask::SetMode(CurrentMode::Cards));
(*on_update)();
Ok(())

View File

@ -0,0 +1,127 @@
use std::{collections::HashMap, rc::Rc};
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::popup_manager::{MountPopupOnceParams, PopupHolder},
views::{ViewTrait, ViewUpdateParams},
};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::label::WidgetLabel,
};
pub struct ButtonEntry {
pub content: Translation, // button text
pub icon: &'static str, // sprite_src_builtin
pub action: &'static str, // action name (will be passed into on_action_click)
}
pub struct Params {
pub globals: WguiGlobals,
pub entries: Vec<ButtonEntry>,
pub message: Translation,
pub on_action_click: Box<dyn FnOnce(&'static str)>,
}
#[derive(Clone)]
enum Task {
ActionClicked(&'static str),
}
pub struct View {
tasks: Tasks<Task>,
#[allow(dead_code)]
parser_state: ParserState,
on_action_click: Option<Box<dyn FnOnce(&'static str)>>,
on_close_request: Option<Box<dyn FnOnce()>>,
}
fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/dialog_box.xml"),
extra: Default::default(),
}
}
impl ViewTrait for View {
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::ActionClicked(action) => {
if let Some(func) = self.on_action_click.take() {
func(action);
}
if let Some(on_close) = self.on_close_request.take() {
on_close();
}
}
}
}
Ok(())
}
}
impl View {
pub fn new(
layout: &mut Layout,
id_parent: WidgetID,
on_close_request: Box<dyn FnOnce()>,
par: Params,
) -> anyhow::Result<Self> {
let tasks = Tasks::<Task>::new();
let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?;
let id_buttons = parser_state.get_widget_id("buttons")?;
{
let label_message = parser_state.fetch_widget(&layout.state, "label_message")?.widget;
label_message
.cast::<WidgetLabel>()?
.set_text(&mut layout.common(), par.message);
}
for entry in par.entries {
let mut t_par = HashMap::<Rc<str>, Rc<str>>::new();
t_par.insert(Rc::from("icon"), Rc::from(entry.icon));
let data =
parser_state.realize_template(&doc_params(&par.globals), "DialogBoxButton", layout, id_buttons, t_par)?;
let button = data.fetch_component_as::<ComponentButton>("btn")?;
button.set_text(&mut layout.common(), entry.content.clone());
button.on_click(tasks.get_button_click_callback(Task::ActionClicked(entry.action)));
}
Ok(Self {
tasks,
parser_state,
on_action_click: Some(par.on_action_click),
on_close_request: Some(on_close_request),
})
}
}
pub fn mount_popup(popup: PopupHolder<View>, frontend_tasks: FrontendTasks, params: Params) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_raw_text("Info"),
Box::new(move |data| {
let on_close_request = popup.get_close_callback(data.layout);
let view = View::new(data.layout, data.id_content, on_close_request, params)?;
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
)));
}

View File

@ -0,0 +1,265 @@
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
networking::http_client::{self, ProgressFuncData},
popup_manager::{MountPopupOnceParams, PopupHolder},
wgui_simple,
},
views::{ViewTrait, ViewUpdateParams},
};
use glam::Vec2;
use std::path::PathBuf;
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::label::WidgetLabel,
};
use wlx_common::async_executor::AsyncExecutor;
pub struct Params {
pub globals: WguiGlobals,
pub executor: AsyncExecutor,
pub target_path: PathBuf,
pub url: String,
pub on_downloaded: Box<dyn FnOnce()>,
}
#[derive(Clone)]
enum Task {
StartDownload(/*url*/ String, /*target path*/ PathBuf),
SetStatusText(String),
ShowIconSuccess,
ShowIconError,
Close,
}
pub struct View {
globals: WguiGlobals,
tasks: Tasks<Task>,
executor: AsyncExecutor,
#[allow(dead_code)]
parser_state: ParserState,
id_label_status: WidgetID,
id_loading_parent: WidgetID,
id_content: WidgetID,
on_close_request: Option<Box<dyn FnOnce()>>,
on_downloaded: Option<Box<dyn FnOnce()>>,
}
fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/download_file.xml"),
extra: Default::default(),
}
}
impl ViewTrait for View {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::StartDownload(url, path) => {
if let Some(on_downloaded) = self.on_downloaded.take() {
self
.executor
.spawn(View::download(
self.tasks.clone(),
self.executor.clone(),
url,
path,
on_downloaded,
))
.detach();
}
}
Task::SetStatusText(text) => {
let widgets = &mut par.layout.state.widgets;
widgets
.fetch(self.id_label_status)?
.cast::<WidgetLabel>()?
.set_text(&mut par.layout.common(), Translation::from_raw_text_string(text));
}
Task::ShowIconSuccess => {
par.layout.remove_children(self.id_loading_parent);
wgui_simple::create_icon(
par.layout,
self.id_loading_parent,
Vec2::splat(32.0),
AssetPath::BuiltIn("dashboard/check.svg"),
)?;
// "Close window" button
self
.parser_state
.realize_template(
&doc_params(&self.globals),
"btn_close",
par.layout,
self.id_content,
Default::default(),
)?
.fetch_component_as::<ComponentButton>("btn")?
.on_click(self.tasks.get_button_click_callback(Task::Close));
}
Task::ShowIconError => {
par.layout.remove_children(self.id_loading_parent);
wgui_simple::create_icon(
par.layout,
self.id_loading_parent,
Vec2::splat(32.0),
AssetPath::BuiltIn("dashboard/error.svg"),
)?;
}
Task::Close => {
if let Some(on_close) = self.on_close_request.take() {
on_close();
}
}
}
}
Ok(())
}
}
fn handle_async_result<T, E>(error_reason: &'static str, tasks: &Tasks<Task>, result: anyhow::Result<T, E>) -> Option<T>
where
E: std::fmt::Debug,
{
match result {
Ok(res) => Some(res),
Err(e) => {
tasks.push(Task::ShowIconError);
tasks.push(Task::SetStatusText(format!("{}: {:?}", error_reason, e)));
None
}
}
}
impl View {
pub fn new(
layout: &mut Layout,
id_parent: WidgetID,
on_close_request: Box<dyn FnOnce()>,
par: Params,
) -> anyhow::Result<Self> {
let tasks = Tasks::<Task>::new();
let parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?;
let id_label_status = parser_state.get_widget_id("label_status")?;
let id_content = parser_state.get_widget_id("content")?;
let id_loading_parent = parser_state.get_widget_id("loading_parent")?;
wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
parent_id: id_loading_parent,
layout,
with_text: false,
})?;
let str_target_path = par.globals.i18n().translate("TARGET_PATH");
{
let label_target_path = parser_state.fetch_widget(&layout.state, "label_target_path")?.widget;
label_target_path.cast::<WidgetLabel>()?.set_text(
&mut layout.common(),
Translation::from_raw_text_string(format!("{}: {}", str_target_path, par.target_path.display())),
);
}
tasks.push(Task::StartDownload(par.url, par.target_path));
Ok(Self {
tasks,
globals: par.globals.clone(),
executor: par.executor.clone(),
parser_state,
id_label_status,
id_loading_parent,
id_content,
on_close_request: Some(on_close_request),
on_downloaded: Some(par.on_downloaded),
})
}
async fn download(
tasks: Tasks<Task>,
executor: AsyncExecutor,
url: String,
target_path: PathBuf,
on_downloaded: Box<dyn FnOnce()>,
) -> Option<()> {
tasks.push(Task::SetStatusText(String::from("Connecting to the server...")));
// start downloading from the server with progress reporting
let res = handle_async_result(
"Download failed",
&tasks,
http_client::get(http_client::GetParams {
executor: &executor,
url: &url,
on_progress: Some(Box::new({
let tasks = tasks.clone();
move |data: ProgressFuncData| {
tasks.push(Task::SetStatusText(format!(
"{}/{} KiB ({}%)",
data.bytes_downloaded / 1024,
data.file_size / 1024,
(data.bytes_downloaded as f32 / data.file_size as f32 * 100.0).round()
)))
}
})),
})
.await,
)?;
tasks.push(Task::SetStatusText(String::from("Writing to file...")));
// create skymaps directory if it doesn't exist yet
if let Some(parent) = target_path.parent() {
handle_async_result(
"Directory creation failed",
&tasks,
smol::fs::create_dir_all(parent).await,
)?;
}
handle_async_result(
"File write failed",
&tasks,
smol::fs::write(target_path, res.data).await,
)?;
tasks.push(Task::SetStatusText(String::from("Download finished")));
tasks.push(Task::ShowIconSuccess);
on_downloaded();
None
}
}
pub fn mount_popup(
popup: PopupHolder<View>,
frontend_tasks: FrontendTasks,
on_view_close: Box<dyn FnOnce()>,
params: Params,
) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_translation_key("DOWNLOADER"),
Box::new(move |data| {
let on_close_request = popup.get_close_callback(data.layout);
let view = View::new(data.layout, data.id_content, on_close_request, params)?;
popup.set_view(data.handle, view, Some(on_view_close));
Ok(popup.get_close_callback(data.layout))
}),
)));
}

View File

@ -10,7 +10,7 @@ use wgui::{
drawing::{self, GradientMode},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID, WidgetPair},
layout::{Layout, WidgetID},
renderer_vk::text::{FontWeight, HorizontalAlign, TextShadow, TextStyle, custom_glyph::CustomGlyphData},
taffy::{
self, AlignItems, AlignSelf, JustifyContent, JustifySelf,
@ -25,11 +25,12 @@ use wgui::{
util::WLength,
},
};
use wlx_common::async_executor::AsyncExecutor;
use crate::util::{
cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID},
various::AsyncExecutor,
wgui_simple,
};
pub struct ViewCommon {
@ -47,8 +48,8 @@ pub struct Params<'a, 'b> {
pub struct View {
pub button: Rc<ComponentButton>,
pair: WidgetPair,
id_image_parent: WidgetID,
id_loading: WidgetID,
app_name: String,
app_id: AppID,
}
@ -99,15 +100,9 @@ impl View {
Ok(())
}
fn mount_placeholder_text(
&self,
globals: &WguiGlobals,
layout: &mut Layout,
parent: WidgetID,
text: &str,
) -> anyhow::Result<()> {
fn mount_placeholder_text(&self, layout: &mut Layout, parent: WidgetID, text: &str) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut globals.get(),
&mut layout.state,
WidgetLabelParams {
content: Translation::from_raw_text(text),
style: TextStyle {
@ -150,11 +145,13 @@ impl View {
layout: &mut Layout,
cover_art: &CoverArt,
) -> anyhow::Result<()> {
layout.remove_widget(self.id_loading);
if cover_art.compressed_image_data.is_empty() {
// mount placeholder
let img = view_common.get_placeholder_image()?.clone();
self.mount_image(layout, &img)?;
self.mount_placeholder_text(&view_common.globals, layout, self.id_image_parent, &self.app_name)?;
self.mount_placeholder_text(layout, self.id_image_parent, &self.app_name)?;
} else {
// mount image
let path = format!("app:{:?}", self.app_id);
@ -278,6 +275,12 @@ impl View {
rect_gradient_style(taffy::AlignSelf::End, 0.05),
)?;
let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
layout: params.ess.layout,
parent_id: image_parent.id,
with_text: false,
})?;
// request cover image data from the internet or disk cache
params
.executor
@ -289,11 +292,11 @@ impl View {
.detach();
Ok(View {
pair: widget_button,
button,
id_image_parent: image_parent.id,
app_name: params.manifest.name.clone(),
app_id: params.manifest.app_id.clone(),
id_loading,
})
}
}

View File

@ -4,10 +4,10 @@ use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType},
util::{
cached_fetcher::{self, CoverArt},
popup_manager::{MountPopupOnceParams, PopupHolder},
steam_utils::{self, AppID, AppManifest},
various::AsyncExecutor,
},
views::game_cover,
views::{ViewTrait, ViewUpdateParams, game_cover},
};
use wgui::{
assets::AssetPath,
@ -19,6 +19,7 @@ use wgui::{
task::Tasks,
widget::{ConstructEssentials, label::WidgetLabel},
};
use wlx_common::async_executor::AsyncExecutor;
#[derive(Clone)]
enum Task {
@ -34,13 +35,14 @@ pub struct Params<'a> {
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub frontend_tasks: &'a FrontendTasks,
pub on_launched: Box<dyn Fn()>,
pub on_launched: Box<dyn FnOnce()>,
}
pub struct View {
#[allow(dead_code)]
state: ParserState,
tasks: Tasks<Task>,
on_launched: Box<dyn Fn()>,
on_launched: Option<Box<dyn FnOnce()>>,
frontend_tasks: FrontendTasks,
game_cover_view_common: game_cover::ViewCommon,
@ -48,6 +50,30 @@ pub struct View {
app_id: AppID,
}
impl ViewTrait for View {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::FillAppDetails(details) => self.action_fill_app_details(par.layout, details)?,
Task::Launch => self.action_launch(),
Task::SetCoverArt(cover_art) => {
let _ = self
.view_cover
.set_cover_art(&mut self.game_cover_view_common, par.layout, &cover_art);
}
}
}
}
Ok(())
}
}
impl View {
async fn fetch_details(executor: AsyncExecutor, tasks: Tasks<Task>, app_id: AppID) {
let Some(details) = cached_fetcher::get_app_details_json(executor, app_id).await else {
@ -104,7 +130,7 @@ impl View {
Ok(Self {
state,
tasks,
on_launched: params.on_launched,
on_launched: Some(params.on_launched),
frontend_tasks: params.frontend_tasks.clone(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
view_cover,
@ -112,43 +138,20 @@ impl View {
})
}
pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::FillAppDetails(details) => self.action_fill_app_details(layout, details)?,
Task::Launch => self.action_launch(),
Task::SetCoverArt(cover_art) => {
let _ = self
.view_cover
.set_cover_art(&mut self.game_cover_view_common, layout, &cover_art);
}
}
}
}
Ok(())
}
fn action_fill_app_details(
&mut self,
layout: &mut Layout,
mut details: cached_fetcher::AppDetailsJSONData,
) -> anyhow::Result<()> {
let mut c = layout.start_common();
{
let label_author = self.state.fetch_widget(&c.layout.state, "label_author")?.widget;
let label_description = self.state.fetch_widget(&c.layout.state, "label_description")?.widget;
let mut c = layout.common();
let label_author = self.state.fetch_widget(c.state, "label_author")?.widget;
let label_description = self.state.fetch_widget(c.state, "label_description")?.widget;
if let Some(developer) = details.developers.pop() {
label_author
.cast::<WidgetLabel>()?
.set_text(&mut c.common(), Translation::from_raw_text_string(developer));
.set_text(&mut c, Translation::from_raw_text_string(developer));
}
let desc = if let Some(desc) = &details.short_description {
@ -162,11 +165,10 @@ impl View {
if let Some(desc) = desc {
label_description
.cast::<WidgetLabel>()?
.set_text(&mut c.common(), Translation::from_raw_text(desc));
.set_text(&mut c, Translation::from_raw_text(desc));
}
}
c.finish()?;
Ok(())
}
@ -190,6 +192,37 @@ impl View {
}
}
(*self.on_launched)();
if let Some(on_launched) = self.on_launched.take() {
on_launched();
}
}
}
pub fn mount_popup(
frontend_tasks: FrontendTasks,
executor: AsyncExecutor,
globals: WguiGlobals,
manifest: AppManifest,
popup: PopupHolder<View>,
) {
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_raw_text(&manifest.name),
Box::new(move |data| {
let on_launched = popup.get_close_callback(data.layout);
let view = View::new(Params {
manifest: manifest.clone(),
executor: executor.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
on_launched,
})?;
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
)));
}

View File

@ -1,7 +1,8 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use std::{collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
@ -12,24 +13,26 @@ use wgui::{
label::{WidgetLabel, WidgetLabelParams},
},
};
use wlx_common::async_executor::AsyncExecutor;
use crate::{
frontend::{FrontendTask, FrontendTasks},
frontend::FrontendTasks,
util::{
cached_fetcher::CoverArt,
popup_manager::{MountPopupParams, PopupHandle},
steam_utils::{self, AppID, AppManifest, SteamUtils},
various::AsyncExecutor,
popup_manager::PopupHolder,
steam_utils::{self, AppID, SteamUtils},
},
views::{self, game_cover, game_launcher},
views::{self, ViewTrait, ViewUpdateParams, game_cover},
};
#[derive(Clone)]
enum Task {
AppManifestClicked(steam_utils::AppManifest),
SetCoverArt(AppID, Rc<CoverArt>),
CloseLauncher,
Refresh,
LoadManifests,
FillPage(u32),
PrevPage,
NextPage,
}
pub struct Params<'a> {
@ -38,15 +41,13 @@ pub struct Params<'a> {
pub frontend_tasks: FrontendTasks,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub steam_utils: &'a SteamUtils,
}
pub struct Cell {
const MAX_GAMES_PER_PAGE: u32 = 30;
pub struct GameCoverCell {
view_cover: game_cover::View,
manifest: AppManifest,
}
struct State {
view_launcher: Option<(PopupHandle, views::game_launcher::View)>,
}
pub struct View {
@ -56,11 +57,39 @@ pub struct View {
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
id_list_parent: WidgetID,
steam_utils: steam_utils::SteamUtils,
cells: HashMap<AppID, Cell>,
game_cover_view_common: game_cover::ViewCommon,
executor: AsyncExecutor,
state: Rc<RefCell<State>>,
mounted_game_covers: HashMap<AppID, GameCoverCell>,
all_manifests: Vec<steam_utils::AppManifest>,
cur_page: u32,
page_count: u32,
id_label_page: WidgetID,
view_launcher: PopupHolder<views::game_launcher::View>,
steam_utils: SteamUtils,
}
impl ViewTrait for View {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::LoadManifests => self.load_manifests(),
Task::FillPage(page_idx) => self.fill_page(par.layout, par.executor, page_idx)?,
Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(par.layout, app_id, cover_art),
Task::PrevPage => self.page_prev(),
Task::NextPage => self.page_next(),
}
}
}
self.view_launcher.update(par)?;
Ok(())
}
}
impl View {
@ -73,12 +102,21 @@ impl View {
let parser_state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
let list_parent = parser_state.fetch_widget(&params.layout.state, "list_parent")?;
let id_label_page = parser_state.get_widget_id("label_page")?;
let tasks = Tasks::new();
let steam_utils = SteamUtils::new()?;
tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_prev")?,
Task::PrevPage,
);
tasks.push(Task::Refresh);
tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_next")?,
Task::NextPage,
);
tasks.push(Task::LoadManifests);
Ok(Self {
parser_state,
@ -86,51 +124,27 @@ impl View {
frontend_tasks: params.frontend_tasks,
globals: params.globals.clone(),
id_list_parent: list_parent.id,
steam_utils,
cells: HashMap::new(),
mounted_game_covers: HashMap::new(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
state: Rc::new(RefCell::new(State { view_launcher: None })),
executor: params.executor,
all_manifests: Vec::new(),
cur_page: 0,
page_count: 0,
id_label_page,
view_launcher: Default::default(),
steam_utils: params.steam_utils.clone(),
})
}
pub fn update(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::Refresh => self.refresh(layout, executor)?,
Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(layout, app_id, cover_art),
Task::CloseLauncher => self.state.borrow_mut().view_launcher = None,
}
}
}
let mut state = self.state.borrow_mut();
if let Some((_, view)) = &mut state.view_launcher {
view.update(layout)?;
}
Ok(())
}
}
pub struct Games {
manifests: Vec<steam_utils::AppManifest>,
}
fn fill_game_list(
ess: &mut ConstructEssentials,
executor: &AsyncExecutor,
cells: &mut HashMap<AppID, Cell>,
games: &Games,
mounted_game_covers: &mut HashMap<AppID, GameCoverCell>,
manifests: &[steam_utils::AppManifest],
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
for manifest in &games.manifests {
for manifest in manifests {
let on_loaded = {
let app_id = manifest.app_id.clone();
let tasks = tasks.clone();
@ -150,81 +164,108 @@ fn fill_game_list(
view_cover.button.on_click({
let tasks = tasks.clone();
let manifest = manifest.clone();
Box::new(move |_, _| {
Rc::new(move |_, _| {
tasks.push(Task::AppManifestClicked(manifest.clone()));
Ok(())
})
});
cells.insert(
manifest.app_id.clone(),
Cell {
view_cover,
manifest: manifest.clone(),
},
);
mounted_game_covers.insert(manifest.app_id.clone(), GameCoverCell { view_cover });
}
Ok(())
}
impl View {
fn game_list(&self) -> anyhow::Result<Games> {
let manifests = self
fn load_manifests(&mut self) {
match self
.steam_utils
.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?;
Ok(Games { manifests })
.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)
{
Ok(manifests) => {
self.page_count = (manifests.len() as u32 + MAX_GAMES_PER_PAGE) / MAX_GAMES_PER_PAGE;
self.all_manifests = manifests;
self.tasks.push(Task::FillPage(0));
}
Err(e) => {
log::error!("Failed to list installed games: {e:?}");
}
}
}
fn refresh(&mut self, layout: &mut Layout, executor: &AsyncExecutor) -> anyhow::Result<()> {
layout.remove_children(self.id_list_parent);
self.cells.clear();
let mut text: Option<Translation> = None;
match self.game_list() {
Ok(list) => {
if list.manifests.is_empty() {
text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))
} else {
fill_game_list(
&mut ConstructEssentials {
layout,
parent: self.id_list_parent,
},
executor,
&mut self.cells,
&list,
&self.tasks,
)?
}
}
Err(e) => text = Some(Translation::from_raw_text(&format!("Error: {:?}", e))),
fn page_prev(&mut self) {
if self.cur_page == 0 {
return;
}
self.cur_page -= 1;
self.tasks.push(Task::FillPage(self.cur_page));
}
fn page_next(&mut self) {
if self.cur_page >= self.page_count - 1 {
return;
}
self.cur_page += 1;
self.tasks.push(Task::FillPage(self.cur_page));
}
fn fill_page(&mut self, layout: &mut Layout, executor: &AsyncExecutor, page_idx: u32) -> anyhow::Result<()> {
layout.remove_children(self.id_list_parent);
self.mounted_game_covers.clear();
let idx_from = (page_idx * MAX_GAMES_PER_PAGE).min(self.all_manifests.len() as u32);
let idx_to = ((page_idx + 1) * MAX_GAMES_PER_PAGE).min(self.all_manifests.len() as u32);
let page_manifests = &self.all_manifests[idx_from as usize..idx_to as usize];
let mut text: Option<Translation> = None;
if page_manifests.is_empty() {
text = Some(Translation::from_translation_key("GAME_LIST.NO_GAMES_FOUND"))
}
// set page text
{
let mut c = layout.common();
let mut widget = c.state.widgets.cast_as::<WidgetLabel>(self.id_label_page)?;
widget.set_text(
&mut c,
Translation::from_raw_text_string(format!("{}/{}", self.cur_page + 1, self.page_count)),
);
}
fill_game_list(
&mut ConstructEssentials {
layout,
parent: self.id_list_parent,
},
executor,
&mut self.mounted_game_covers,
page_manifests,
&self.tasks,
)?;
if let Some(text) = text.take() {
layout.add_child(
self.id_list_parent,
WidgetLabel::create(
&mut self.globals.get(),
WidgetLabelParams {
content: text,
..Default::default()
},
),
Default::default(),
)?;
let label = WidgetLabel::create(
&mut layout.state,
WidgetLabelParams {
content: text,
..Default::default()
},
);
layout.add_child(self.id_list_parent, label, Default::default())?;
}
Ok(())
}
fn set_cover_art(&mut self, layout: &mut Layout, app_id: AppID, cover_art: Rc<CoverArt>) {
let Some(cell) = &mut self.cells.get_mut(&app_id) else {
let Some(cover) = &mut self.mounted_game_covers.get_mut(&app_id) else {
return;
};
if let Err(e) = cell
if let Err(e) = cover
.view_cover
.set_cover_art(&mut self.game_cover_view_common, layout, &cover_art)
{
@ -233,36 +274,13 @@ impl View {
}
fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> {
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_raw_text(&manifest.name),
on_content: {
let state = self.state.clone();
let tasks = self.tasks.clone();
let executor = self.executor.clone();
let globals = self.globals.clone();
let frontend_tasks = self.frontend_tasks.clone();
Rc::new(move |data| {
let on_launched = {
let tasks = tasks.clone();
Box::new(move || tasks.push(Task::CloseLauncher))
};
let view = game_launcher::View::new(game_launcher::Params {
manifest: manifest.clone(),
executor: executor.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
on_launched,
})?;
state.borrow_mut().view_launcher = Some((data.handle, view));
Ok(())
})
},
}));
views::game_launcher::mount_popup(
self.frontend_tasks.clone(),
self.executor.clone(),
self.globals.clone(),
manifest,
self.view_launcher.clone(),
);
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More