dash-frontend (#287)

* Slider: `show_value`, hide tooltip on button press

[skip ci]

* DashInterface, DashInterfaceEmulated

* `display_list`, `add_display` views (wip)

[skip ci]

* `add_display::View` done

* `display_options::View` done

* `process_list::View` done

* Merge remote-tracking branch 'origin/main' into next-dash-interface

[skip ci]

* tooltip wrap, clippy

[skip ci]

* App launcher

[skip ci]

* toast_manager: fix delay, add vulkan feat

[skip ci]

* smithay deps egl → vk

* rewrite built-in wayland compositor egl → vulkan

* move `add_display::View` -> `add_window::View` & `display_options::View` -> `window_options::VIew`, remove displays logic and replace it with window ones

[skip ci]

* Merge remote-tracking branch 'origin/wlvk' into next-dash-interface

[skip ci]

* when is maybeuninit not a bad idea?

* wayland_server: make frame callbacks, release buffers. logging

* wayvrctl process-launch args fix

* wayland_server: fix mouse

* `game_list::View` (wip)

[skip ci]

* `~/.cache/` IO

[skip ci]

* HTTP client, game cover art fetcher, game list image display, use `smol::LocalExecutor` for async runtime

* wayvr overlay removal

* WayVRData → WayVRState in the RefCell

* rearrange deps, add dash

* add back regex for build

* [skip ci] refactor dash?

* remove RcFrontend & RcLayout

[skip ci]

* todo in wrong place

* enjoy dashboard in vr

* Placeholder cover arts

[skip ci]

* fix DashFrontend not updating

[skip ci]

* fix animations, fix SlotMap dirty widget panic, set gui scale, set dash to 1080p

[skip ci]

* wgui to use srgb

* fix srgb in uidev mode, tweak colors a bit

[skip ci]

* dash-frontend: Detect and switch to WiVRn speakers

[skip ci]

* Game launcher (wip), wgui refactor

[skip ci]

* get rid of wayvr refcell

* clippy

* use freedesktop instead of gtk

* glob-based icon discovery but very slow

* background app entry finder

* cached image loading

* cached image loading: auto cleanup

* app entry cache build log level

* bump freedesktop to include flatpak search paths

* Nice Word Wrap™ for apps.xml

* overhaul desktop finder

* Game launcher (fully functional)

* app_launcher: fix exec label

* ShouldRender::Should if mouse moved

* generics + DashInterface impl (#334)

* fix uidev build

* make apps think that they are fullscreen

* fix cage

* implement xdg_popup support

* RadioBox & RadioGroup

* AppLauncher radio boxes

* app launcher res & orientation functionality

* fix scaling

* fix wrong default for res_mode

* typo

* separator placement

* poc window decorations

* move audio system to wlx-common, compress audio data, sample player

* dash and wgui sounds

* decor mouse fix

* decor improvements

* battery percentage sign ("%") in watch

* update decor.xml, fix max_size

* decor mouse hover & leave

* decor window close

* fallback identicons

* wayvr window size from res. remove decor tooltips

* wip: add bar to keyboard

* bar design

* tweak ui, clippy, modify desktop finder blacklist

* do not keep startup sound in memory

* tweak watch ui, load application list gradually (prevent lag)

* bar functionality

* tooltip raw text, inline translation fallback support

* bar app icons & tooltips

* bar dropdown backend logic

* fix wayvrctl

* fmt

* bar: dash button

* add ::OverlayReset

* add ::CustomOverlayReload

* bring back ToggleDashboard keybind support

* wgui: windowing: `close_if_clicked_outside` support, context menus

* ticking context menu

* on_custom_attribs Box → Rc

* context menu custom attribs

* dash-frontend: application list grouping

* checkbox sounds, app launch sounds

* settings implementation

* fix uidev build

* batteries: hide % if more than 3 devices

* ::OverlayToggle to not reset overlay on show

* update lang

* settings ui changes

* fallback fonts

* remove old gh actions

* app categories

* fix set_stereo

* pass template_params to context_menu

* refactor context_menu to only require parser_state on tick

* working bar context menus + kbd downsize

* fix hidden overlays all popping up after restart

* context to use release → press; cleanups

* list helpers

* watch rework

* reverse sets_on_watch

* settings tab buttons; autorestart

* tweak keycaps

* fix bar doing out of date on keymap change

* settings saving

* fix context menus, reload-from-disk

* fix force close not force closing

* burger menu + fix crash after removing set

* dropdown for capture_method + random tweaks

* support _format on clock time label (#344)

use _display as format string directly
remove log message

update docs

* more useful parser warnings + cleanups

* reduce warings, xml fixes

* keyboard middle click setting; docs, readme & logs

* app autostart

* implement spawn positioning

* rearrange settings

* update lang, update description.txt

* sort json

* Monado app switcher, lang update

* Update Monado IPC, display brightness slider

* fmt

* zwlr_screencopy v3 support

* remove wayvr feature

* fix features

---------

Co-authored-by: galister <22305755+galister@users.noreply.github.com>
Co-authored-by: Tayou <git@tayou.org>
This commit is contained in:
galister
2026-01-09 12:03:37 +00:00
committed by GitHub
288 changed files with 13836 additions and 8436 deletions

View File

@@ -1,41 +0,0 @@
name: Build AppImage (with WayVR Dashboard)
on:
push:
branches:
- 'main'
- 'staging'
env:
APPDIR: WlxOverlay-S-Full.AppDir
CARGO_TERM_COLOR: always
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
jobs:
build_appimage:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./wlx-overlay-s
steps:
- uses: actions/checkout@v3
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Prepare Environment
run: |
../.github/workflows/scripts/appimage_prepare_env.sh
- name: Cargo Build
run: |
../.github/workflows/scripts/appimage_build_wlx_full.sh
- name: Build WayVR Dashboard
run: |
../.github/workflows/scripts/appimage_build_wayvr_dashboard.sh
- name: Package AppImage
run: |
../.github/workflows/scripts/appimage_package_full.sh
- name: Upload AppImage
uses: actions/upload-artifact@v4
with:
name: WlxOverlay-S-Full-${{ github.ref_name }}-x86_64.AppImage
path: ./wlx-overlay-s/WlxOverlay-S-Full-x86_64.AppImage

View File

@@ -1,28 +0,0 @@
name: Check Wayland+OpenXR+OpenVR+WayVR
on:
pull_request:
#branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
jobs:
build:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./wlx-overlay-s
steps:
- uses: actions/checkout@v4
- name: Setup sccache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Prepare Environment
run: |
../.github/workflows/scripts/appimage_prepare_env.sh
- name: Build
run: cargo build --verbose --no-default-features --features=wayland,openxr,openvr,wayvr
- name: Run tests
run: cargo test --verbose --no-default-features --features=wayland,openxr,openvr,wayvr

View File

@@ -30,15 +30,6 @@ jobs:
- name: Package AppImage - name: Package AppImage
run: | run: |
../.github/workflows/scripts/appimage_package.sh ../.github/workflows/scripts/appimage_package.sh
- name: Cargo Build Full
run: |
../.github/workflows/scripts/appimage_build_wlx_full.sh
- name: Build WayVR Dashboard
run: |
../.github/workflows/scripts/appimage_build_wayvr_dashboard.sh
- name: Package AppImage
run: |
../.github/workflows/scripts/appimage_package_full.sh
- name: Build Wayvrctl - name: Build Wayvrctl
run: | run: |
cd ../wayvrctl cd ../wayvrctl
@@ -80,24 +71,14 @@ jobs:
asset_name: wayvrctl asset_name: wayvrctl
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload AppImage (Full) - name: Upload AppImage
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_KEY }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./wlx-overlay-s/WlxOverlay-S-Full-x86_64.AppImage
asset_name: WlxOverlay-S-${{ github.ref_name }}-Full-x86_64.AppImage
asset_content_type: application/octet-stream
- name: Upload AppImage (Slim)
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_KEY }} GITHUB_TOKEN: ${{ secrets.RELEASE_KEY }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./wlx-overlay-s/WlxOverlay-S-x86_64.AppImage asset_path: ./wlx-overlay-s/WlxOverlay-S-x86_64.AppImage
asset_name: WlxOverlay-S-${{ github.ref_name }}-Slim-x86_64.AppImage asset_name: WlxOverlay-S-${{ github.ref_name }}-x86_64.AppImage
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload crates tarball - name: Upload crates tarball

View File

@@ -1,38 +0,0 @@
#!/bin/sh
WAYVR_DASHBOARD_PATH="/tmp/wayvr-dashboard"
MAIN_DIR=$(realpath $(pwd))
# built wayvr-dashboard binary executable path
DASH_PATH="${WAYVR_DASHBOARD_PATH}/temp/wayvr-dashboard"
git clone --depth=1 https://github.com/olekolek1000/wayvr-dashboard.git ${WAYVR_DASHBOARD_PATH}
cd ${WAYVR_DASHBOARD_PATH}
.github/workflows/build.sh
# See https://github.com/olekolek1000/wayvr-dashboard/blob/master/.github/workflows/appimage.sh
cd ${MAIN_DIR}
cd ${APPDIR}
# Fix webkit
echo "Copying webkit runtime executables"
# Copy runtime executables
find -L /usr/lib /usr/libexec -name WebKitNetworkProcess -exec mkdir -p . ';' -exec cp -v --parents '{}' . ';' || true
find -L /usr/lib /usr/libexec -name WebKitWebProcess -exec mkdir -p . ';' -exec cp -v --parents '{}' . ';' || true
find -L /usr/lib /usr/libexec -name libwebkit2gtkinjectedbundle.so -exec mkdir -p . ';' -exec cp --parents '{}' . ';' || true
echo "Patching webkit lib"
# Patch libwebkit .so file: Replace 4 bytes containing "/usr" into "././". Required!
TARGET_WEBKIT_SO="./usr/lib/libwebkit2gtk-4.1.so.0"
cp /usr/lib/x86_64-linux-gnu/libwebkit2gtk-4.1.so.0 ${TARGET_WEBKIT_SO}
sed -i -e "s|/usr|././|g" "${TARGET_WEBKIT_SO}"
cd ${MAIN_DIR}
chmod +x ${DASH_PATH}
# Put resulting executable into wlx AppDir
cp ${DASH_PATH} ${APPDIR}/usr/bin/wayvr-dashboard

View File

@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
cargo build --release --no-default-features --features=openvr,openxr,wayland,x11,osc cargo build --release
chmod +x ../target/release/wlx-overlay-s chmod +x ../target/release/wlx-overlay-s
cp ../target/release/wlx-overlay-s ${APPDIR}/usr/bin cp ../target/release/wlx-overlay-s ${APPDIR}/usr/bin

View File

@@ -1,4 +0,0 @@
#!/bin/sh
cargo build --release
chmod +x ../target/release/wlx-overlay-s
cp ../target/release/wlx-overlay-s ${APPDIR}/usr/bin

View File

@@ -1,4 +0,0 @@
#!/bin/sh
export VERSION=$GITHUB_REF_NAME
./linuxdeploy-x86_64.AppImage -dwlx-overlay-s.desktop -iwlx-overlay-s.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
mv WlxOverlay-S-$VERSION-x86_64.AppImage WlxOverlay-S-Full-x86_64.AppImage

1272
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,17 @@ strip = "none"
debug-assertions = true debug-assertions = true
incremental = true incremental = true
# to be used in case if you don't want debug features
# (faster incremental compilation, about 15x smaller binary size compared to dev)
# --profile=plain
[profile.plain]
inherits = "dev"
opt-level = 1
debug = false
strip = true
debug-assertions = true
incremental = true
[profile.release-with-debug] [profile.release-with-debug]
inherits = "release" inherits = "release"
debug = true debug = true
@@ -26,6 +37,7 @@ resolver = "3"
anyhow = "1.0.100" anyhow = "1.0.100"
glam = { version = "0.30.9", features = ["mint", "serde"] } glam = { version = "0.30.9", features = ["mint", "serde"] }
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
xdg = "3.0.0"
idmap = "0.2.2" idmap = "0.2.2"
idmap-derive = "0.2.22" idmap-derive = "0.2.22"
log = "0.4.29" log = "0.4.29"
@@ -34,6 +46,7 @@ rust-embed = "8.9.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"
slotmap = "1.1.1" slotmap = "1.1.1"
strum = { version = "0.27.2", features = ["derive"] }
vulkano = { version = "0.35.2", default-features = false, features = [ vulkano = { version = "0.35.2", default-features = false, features = [
"macros", "macros",
] } ] }

View File

@@ -56,8 +56,9 @@ For users specifically running **SteamVR via Steam Flatpak**, follow these steps
**When the screen share pop-up appears, check your notifications or the terminal and select the screens in the order it requests.** **When the screen share pop-up appears, check your notifications or the terminal and select the screens in the order it requests.**
In case screens were selected in the wrong order: In case screens were selected in the wrong order:
- Go to Settings and press `Clear PipeWire tokens` and then `Restart software`
- `rm ~/.config/wlxoverlay/conf.d/pw_tokens.yaml` then restart - Pay attention to your notifications, it tells you in which order to pick the screens.
- If notifications don't show, try start Wlx from the terminal and look for instructions in there.
**WiVRn users**: Select WlxOverlay-S from the `Application` drop-down. If there's no such entry, select `Custom` and browse to your WlxOverlay-S executable or AppImage. **WiVRn users**: Select WlxOverlay-S from the `Application` drop-down. If there's no such entry, select `Custom` and browse to your WlxOverlay-S executable or AppImage.
@@ -161,29 +162,31 @@ Check [here](https://github.com/galister/wlx-overlay-s/wiki/Troubleshooting) for
### Mouse is not where it should be ### Mouse is not where it should be
X11 users: 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 Wlx from the terminal and look for instructions in there.
COSMIC destkop:
- Due to limitations with COSMIC, the mouse can only move on a single display.
X11 users:
- Might be dealing with a [Phantom Monitor](https://wiki.archlinux.org/title/Xrandr#Disabling_phantom_monitor). - Might be dealing with a [Phantom Monitor](https://wiki.archlinux.org/title/Xrandr#Disabling_phantom_monitor).
- DPI scaling is not supported and will mess with the mouse. - DPI scaling is not supported and will mess with the mouse.
- Upright screens are not supported and will mess with the mouse. - Upright screens are not supported and will mess with the mouse.
Other desktops: The screens may have been selected in the wrong order, see [First Start](#first-start). ### Screens are blank or black or frozen on Steam Link
### Crashes, blank screens As of SteamVR version 2.14.x, PipeWire capture no longer works when using Steam Link.
There are some driver-desktop combinations that don't play nice with DMA-buf capture. We're unable to completely troubleshoot how and why Steam Link interferes with PipeWire, so consider the following workarounds for the time being:
- Use another streamer, such as WiVRn or ALVR
- 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
Disabling DMA-buf capture is a good first step to try when encountering an app crash or gpu driver reset. ### Modifiers get stuck
```bash Hiding the keyboard will un-press all of its buttons. Alternatively, go to Settings and use the `Restart software` button.
echo 'capture_method: pw_fallback' > ~/.config/wlxoverlay/conf.d/pw_fallback.yaml
```
Without DMA-buf capture, capturing screens takes CPU power, so let's try and not show too many screens at the same time.
### Modifiers get stuck, mouse clicks stop working on KDE Plasma
We are not sure what causes this, but it only happens on KDE Plasma. Restarting the overlay fixes this.
### X11 limitations ### X11 limitations

View File

@@ -4,14 +4,27 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow.workspace = true wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
wgui = { path = "../wgui/" } wgui = { path = "../wgui/" }
wlx-common = { path = "../wlx-common" }
anyhow.workspace = true
glam = { workspace = true, features = ["mint", "serde"] } glam = { workspace = true, features = ["mint", "serde"] }
log.workspace = true log.workspace = true
xdg.workspace = true
rust-embed.workspace = true rust-embed.workspace = true
chrono = "0.4.42" serde = { workspace = true, features = ["rc"] }
gio = "0.21.5"
gtk = "0.18.2"
serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
wlx-common = { path = "../wlx-common" } 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-hyper = "0.1.1"
[features]
default = ["monado" ]
monado = []

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 18H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 258 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16.67 4H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 248 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 17H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 258 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 15H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 258 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 14H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 258 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 13H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 258 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 12H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 258 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 10H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 258 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 9H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 257 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M16 8H8V6h8m.67-2H15V2H9v2H7.33A1.33 1.33 0 0 0 6 5.33v15.34C6 21.4 6.6 22 7.33 22h9.34A1.33 1.33 0 0 0 18 20.67V5.33C18 4.6 17.4 4 16.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 257 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M23.05 11h-3V4l-5 10h3v8M12 18H4l.05-12h8m.67-2h-1.67V2h-6v2H3.38a1.33 1.33 0 0 0-1.33 1.33v15.34c0 .73.6 1.33 1.33 1.33h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.72 4" />
</svg>

Before

Width:  |  Height:  |  Size: 296 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M23 11h-3V4l-5 10h3v8M12.67 4H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 270 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M23.05 11h-3V4l-5 10h3v8m-6-5h-8V6h8m.67-2h-1.67V2h-6v2H3.38a1.33 1.33 0 0 0-1.33 1.33v15.34c0 .73.6 1.33 1.33 1.33h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.72 4" />
</svg>

Before

Width:  |  Height:  |  Size: 291 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M12 15H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4M23 11h-3V4l-5 10h3v8z" />
</svg>

Before

Width:  |  Height:  |  Size: 281 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M13 4h-2V2H5v2H3c-.6 0-1 .4-1 1v16c0 .6.4 1 1 1h10c.6 0 1-.4 1-1V5c0-.6-.4-1-1-1m-1 10.5H4V6h8zM23 11h-3V4l-5 10h3v8" />
</svg>

Before

Width:  |  Height:  |  Size: 234 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M23 11h-3V4l-5 10h3v8m-6-9H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 279 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M12 11H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4M23 11h-3V4l-5 10h3v8z" />
</svg>

Before

Width:  |  Height:  |  Size: 281 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M12 10H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4M23 11h-3V4l-5 10h3v8z" />
</svg>

Before

Width:  |  Height:  |  Size: 281 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M23 11h-3V4l-5 10h3v8M12 9H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 279 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="white" d="M23 11h-3V4l-5 10h3v8M12 8H4V6h8m.67-2H11V2H5v2H3.33A1.33 1.33 0 0 0 2 5.33v15.34C2 21.4 2.6 22 3.33 22h9.34c.73 0 1.33-.6 1.33-1.33V5.33A1.33 1.33 0 0 0 12.67 4" />
</svg>

Before

Width:  |  Height:  |  Size: 279 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 Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE -->
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 4a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1zM3 14h12a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v12" />
</svg>

After

Width:  |  Height:  |  Size: 469 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="M11 18q-1.65 0-2.825-1.175T7 14q-2.5.025-4.25-1.737T1 8q0-.825.588-1.412T3 6h4V5H5V3h6v2H9v1h3.175q.4 0 .763.15t.637.425l8.375 8.375Q23 16 23 17.45t-1.05 2.5t-2.5 1.05t-2.5-1.05L15 18zm0-4H8.975q0 .825.588 1.413T11 16h2zm1.175-6H3q0 1.65 1.175 2.825T7 12h4.825l6.55 6.55q.45.45 1.088.45t1.087-.45t.45-1.088t-.45-1.087zm0 0" />
</svg>

After

Width:  |  Height:  |  Size: 563 B

View File

@@ -0,0 +1 @@
../../../wlx-overlay-s/src/assets/keyboard/down.svg

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from IconaMoon by Dariush Habibpour - https://creativecommons.org/licenses/by/4.0/ -->
<g fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 8h9m4 0h3m-9 8h9M4 16h3" />
<circle cx="9" cy="16" r="2" />
<circle cx="15" cy="8" r="2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 413 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 22q-2.05 0-3.875-.788t-3.187-2.15t-2.15-3.187T2 12q0-2.075.813-3.9t2.2-3.175T8.25 2.788T12.2 2q2 0 3.775.688t3.113 1.9t2.125 2.875T22 11.05q0 2.875-1.75 4.413T16 17h-1.85q-.225 0-.312.125t-.088.275q0 .3.375.863t.375 1.287q0 1.25-.687 1.85T12 22m-5.5-9q.65 0 1.075-.425T8 11.5t-.425-1.075T6.5 10t-1.075.425T5 11.5t.425 1.075T6.5 13m3-4q.65 0 1.075-.425T11 7.5t-.425-1.075T9.5 6t-1.075.425T8 7.5t.425 1.075T9.5 9m5 0q.65 0 1.075-.425T16 7.5t-.425-1.075T14.5 6t-1.075.425T13 7.5t.425 1.075T14.5 9m3 4q.65 0 1.075-.425T19 11.5t-.425-1.075T17.5 10t-1.075.425T16 11.5t.425 1.075T17.5 13M12 20q.225 0 .363-.125t.137-.325q0-.35-.375-.825T11.75 17.3q0-1.05.725-1.675T14.25 15H16q1.65 0 2.825-.962T20 11.05q0-3.025-2.312-5.038T12.2 4Q8.8 4 6.4 6.325T4 12q0 3.325 2.338 5.663T12 20" />
</svg>

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="180"
height="180"
viewBox="0 0 180 180"
version="1.1"
id="svg1"
xml:space="preserve"
sodipodi:docname="wivrn_head_symbolic.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
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"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
id="namedview1"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="3.3305556"
inkscape:cx="62.602168"
inkscape:cy="88.27356"
inkscape:window-width="1969"
inkscape:window-height="1165"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><title
id="title1">WiVRn Wyvern yippe</title><defs
id="defs1"><linearGradient
id="linearGradient19"><stop
style="stop-color:#e6aa00;stop-opacity:1;"
offset="0"
id="stop19" /><stop
style="stop-color:#eae69a;stop-opacity:1;"
offset="1"
id="stop20" /></linearGradient><linearGradient
id="linearGradient17"><stop
style="stop-color:#e6aa00;stop-opacity:1;"
offset="0"
id="stop17" /><stop
style="stop-color:#eae69a;stop-opacity:1;"
offset="1"
id="stop18" /></linearGradient><linearGradient
id="linearGradient12"><stop
style="stop-color:#e09100;stop-opacity:1;"
offset="0"
id="stop12" /><stop
style="stop-color:#eae69a;stop-opacity:1;"
offset="1"
id="stop15" /></linearGradient><linearGradient
id="linearGradient4"><stop
style="stop-color:#001b42;stop-opacity:1;"
offset="0"
id="stop4" /><stop
style="stop-color:#000308;stop-opacity:1;"
offset="1"
id="stop5" /></linearGradient><linearGradient
xlink:href="#linearGradient4"
id="linearGradient3"
x1="-9.9182129e-05"
y1="255.99979"
x2="512.00006"
y2="255.99979"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-2.1362305e-4,-511.99978)" /><linearGradient
xlink:href="#linearGradient4"
id="linearGradient5"
x1="-9.9182129e-05"
y1="255.99979"
x2="512.00006"
y2="255.99979"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-2.1362305e-4,-511.99978)" /><linearGradient
xlink:href="#linearGradient12"
id="linearGradient15"
x1="231.79434"
y1="216.66031"
x2="-107.19677"
y2="-122.3308"
gradientUnits="userSpaceOnUse" /><linearGradient
xlink:href="#linearGradient17"
id="linearGradient18"
x1="287.30957"
y1="58.948975"
x2="239.39442"
y2="11.033832"
gradientUnits="userSpaceOnUse" /><linearGradient
xlink:href="#linearGradient19"
id="linearGradient20"
x1="96.295563"
y1="279.51785"
x2="18.726246"
y2="200.55717"
gradientUnits="userSpaceOnUse" /><linearGradient
xlink:href="#linearGradient19"
id="linearGradient22"
x1="513.62122"
y1="177.14229"
x2="399.88248"
y2="79.645096"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1063376,0.29644226,-0.29644226,1.1063376,-31.100051,-154.59176)" /></defs><g
id="night-sky"
style="display:none"
transform="matrix(0.35156237,0,0,0.35156237,6.6787018e-5,-2.0283465e-4)"><rect
style="opacity:1;fill:url(#linearGradient5);stroke:url(#linearGradient3);stroke-width:109.387;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="rect1"
width="402.61319"
height="402.61276"
x="54.693188"
y="-457.30637"
transform="rotate(90)" /><path
style="opacity:1;fill:url(#linearGradient15);fill-opacity:1;stroke:#000000;stroke-width:25.2217;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path12"
d="M 219.20311,114.5513 107.70982,111.45267 41.950025,201.54153 10.443676,94.547585 -95.556813,59.845289 -3.5355221,-3.181977 -3.2876299,-114.71803 85.091005,-46.67708 191.2447,-80.907857 153.84441,24.171031 Z"
transform="matrix(0.35683517,0,0,0.35683517,53.99049,62.985431)" /><path
style="opacity:1;fill:url(#linearGradient20);fill-opacity:1;stroke:#000000;stroke-width:9;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path16"
d="m 91.923883,258.09397 -24.559389,-2.01745 -15.585017,19.08772 -5.670558,-23.98079 -22.969535,-8.92381 21.054792,-12.80349 1.389064,-24.60293 18.683135,16.0678 23.828023,-6.28165 -9.50798,22.73394 z" /><path
style="opacity:1;fill:url(#linearGradient18);fill-opacity:1;stroke:#000000;stroke-width:9;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="path17"
d="m 282.84271,38.890874 -13.87372,2.657982 -5.67564,12.935685 -6.8151,-12.373325 -14.05644,-1.400506 9.66175,-10.305117 -3.01172,-13.801245 12.7864,6.004413 12.19509,-7.129133 -1.75932,14.016048 z" /><path
style="opacity:1;fill:url(#linearGradient22);fill-opacity:1;stroke:#000000;stroke-width:9;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 380.70776,184.75225 c -6.12682,2.5958 78.08431,26.29978 99.99397,-55.46822 21.04184,-78.529228 -65.63535,-99.651425 -59.9966,-93.381543 23.8872,26.560922 27.24809,55.867376 20.10618,82.193633 -7.64406,28.1771 -28.65811,53.33337 -60.10355,66.65613 z"
id="path20" /></g><g
id="wivrn"
transform="matrix(0.35156237,0,0,0.35156237,6.6787018e-5,-2.0283465e-4)"
style="display:inline"><g
id="layer1"
transform="matrix(2.8444455,0,0,2.8444455,-189.84471,-30.499434)"
style="stroke-width:14.99999998;stroke-dasharray:none"><path
style="fill:none;stroke:#ffffff;stroke-width:14.99999998;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 92.534742,122.93881 c -2.905994,7.38008 -4.746438,21.30199 8.633538,32.72739 7.57609,6.46936 20.38912,11.86588 32.48415,13.44224 9.58171,1.24879 26.70701,1.21338 32.90963,0.63191 11.80031,-1.10625 34.50568,-10.07679 44.543,-20.83318"
id="path14-7" /><path
style="fill:none;stroke:#ffffff;stroke-width:14.99999998;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 188.46876,110.26038 -5.21956,9.63246 -22.58647,-0.33215 c -6.90616,-0.0316 -7.30738,-7.07013 -13.85556,-7.07013 l -25.10134,0.80665 c -4.79251,0.18981 -5.81439,7.39991 -11.05598,7.96931 l -18.885326,1.4259 -12.960463,-22.50447 C 76.839174,95.928995 77.695768,91.866643 80.139132,87.863715 L 95.797837,62.429456 c 2.039918,-2.678813 4.159493,-5.023837 8.321053,-5.435523 l 74.41968,-3.237827 c 5.0906,-0.295214 8.73208,1.737803 11.00526,5.250985 l 11.20838,18.604622 -0.009,-0.01397"
id="path18-6" /><path
style="fill:none;stroke:#ffffff;stroke-width:14.99999998;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 134.47003,55.383809 c 4.67387,-5.765243 7.35483,-13.381057 7.35483,-13.381057 0,0 3.55879,6.714253 4.17565,12.147342"
id="path42-8" /><path
style="fill:none;stroke:#ffffff;stroke-width:14.99999998;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 188.84836,57.969864 c 7.2018,-9.344387 17.89541,-13.953904 27.09427,-16.702596 8.57089,-2.561049 14.57571,-4.987318 19.50218,-12.716749 3.16826,13.836148 -0.44802,26.39734 -3.84686,34.544005 -3.23674,7.758106 -11.52712,19.359828 -19.83097,24.294686"
id="path43-8" /><path
style="fill:none;stroke:#ffffff;stroke-width:14.99999998;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 200.75221,77.611713 c 9.05609,7.170997 12.00082,10.65054 15.89737,19.107608"
id="path44-5" /><path
style="fill:none;stroke:#ffffff;stroke-width:14.99999998;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 211.10506,148.90717 c 18.09388,-17.60049 9.68681,-45.03368 5.54452,-52.187849 -5.07761,-8.122981 -5.86539,-15.251314 -17.01296,-19.574554 l -10.98381,32.405743 v 0 c 10.01156,2.93429 24.20554,12.33526 29.85215,19.21559"
id="path45-0" /></g></g><metadata
id="metadata1"><rdf:RDF><cc:Work
rdf:about=""><dc:title>WiVRn Wyvern yippe</dc:title><dc:date>1/25/25</dc:date><dc:creator><cc:Agent><dc:title>Yaya, y.a.y.a on Discord.</dc:title></cc:Agent></dc:creator><cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" /></cc:Work><cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/"><cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" /><cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" /><cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" /><cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" /><cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /><cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" /></cc:License></rdf:RDF></metadata></svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -15,23 +15,21 @@
height="~side_button_size" height="~side_button_size"
color="#44444400" color="#44444400"
hover_color="#333333ff" hover_color="#333333ff"
border_color="#00000000"
hover_border_color="#555555ff" hover_border_color="#555555ff"
tooltip="${tooltip}" tooltip="${tooltip}"
tooltip_side="${tooltip_side}" tooltip_side="${tooltip_side}"
> >
<sprite src="${src}" width="~side_sprite_size" height="~side_sprite_size" /> <sprite src_builtin="${src_builtin}" width="~side_sprite_size" height="~side_sprite_size" />
</Button> </Button>
</template> </template>
<elements> <elements>
<!-- background for testing -->
<!-- <rectangle position="absolute" color="#333333" width="100%" height="100%" /> -->
<!-- left/right separator (menu and rest) --> <!-- left/right separator (menu and rest) -->
<div flex_direction="row" gap="8" width="100%" height="100%"> <div flex_direction="row" gap="8" width="100%" height="100%" padding="4" interactable="0">
<!-- LEFT MENU --> <!-- LEFT MENU -->
<div id="menu" <div id="menu"
width="~size_size" width="~side_size"
min_width="~side_size" min_width="~side_size"
max_width="~side_size" max_width="~side_size"
height="100%" height="100%"
@@ -46,13 +44,13 @@
align_items="center" align_items="center"
gap="4" gap="4"
> >
<SideButton id="btn_side_home" src="dashboard/wayvr_dashboard_mono.svg" tooltip="HOME_SCREEN" tooltip_side="right" /> <SideButton id="btn_side_home" src_builtin="dashboard/wayvr_dashboard_mono.svg" tooltip="HOME_SCREEN" tooltip_side="right" />
<SideButton id="btn_side_apps" src="dashboard/apps.svg" tooltip="APPLICATIONS" tooltip_side="right" /> <SideButton id="btn_side_apps" src_builtin="dashboard/apps.svg" tooltip="APPLICATIONS" tooltip_side="right" />
<SideButton id="btn_side_games" src="dashboard/games.svg" tooltip="GAMES" tooltip_side="right" /> <SideButton id="btn_side_games" src_builtin="dashboard/games.svg" tooltip="GAMES" tooltip_side="right" />
<SideButton id="btn_side_monado" src="dashboard/monado.svg" tooltip="MONADO_RUNTIME" 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="dashboard/window.svg" tooltip="PROCESSES" 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" /> <rectangle height="2" color="#FFFFFF33" width="~side_sprite_size" />
<SideButton id="btn_side_settings" src="dashboard/settings.svg" tooltip="SETTINGS" tooltip_side="right" /> <SideButton id="btn_side_settings" src_builtin="dashboard/settings.svg" tooltip="SETTINGS" tooltip_side="right" />
</rectangle> </rectangle>
</div> </div>
<!-- REST --> <!-- REST -->
@@ -69,7 +67,7 @@
<rectangle <rectangle
id="rect_content" id="rect_content"
color2="#0d131a00" color2="#0d131a00"
color="#24417900" color="#252f5300"
gradient="vertical" gradient="vertical"
round="8" round="8"
flex_grow="1" flex_grow="1"
@@ -79,7 +77,7 @@
<!-- radial gradient --> <!-- radial gradient -->
<rectangle <rectangle
position="absolute" width="100%" height="100%" position="absolute" width="100%" height="100%"
gradient="radial" color="#44BBFF22" color2="#00000000" /> gradient="radial" color="#44BBFF11" color2="#00000000" />
<div <div
id="content" id="content"
@@ -101,6 +99,7 @@
</rectangle> </rectangle>
<!-- BOTTOM PANEL --> <!-- BOTTOM PANEL -->
<rectangle <rectangle
consume_mouse_events="1"
width="100%" width="100%"
height="48" height="48"
min_height="48" min_height="48"
@@ -124,16 +123,16 @@
<!-- top shine --> <!-- top shine -->
<div position="absolute" width="100%" height="100%" justify_content="center"> <div position="absolute" width="100%" height="100%" justify_content="center">
<rectangle position="absolute" width="99%" height="2" color="#FFFFFF66" round="4" /> <rectangle position="absolute" width="99%" height="2" color="#FFFFFF22" round="4" />
</div> </div>
<!-- Left bottom side --> <!-- Left bottom side -->
<div margin_left="8"> <div margin_left="8">
<Button id="btn_audio" color="#FFFFFF00" border_color="#FFFFFF00" tooltip="AUDIO.VOLUME" tooltip_side="top"> <Button id="btn_audio" color="#FFFFFF00" border_color="#FFFFFF00" tooltip="AUDIO.VOLUME" tooltip_side="top">
<sprite src="dashboard/volume.svg" width="24" height="24" margin="8" /> <sprite src_builtin="dashboard/volume.svg" width="24" height="24" margin="8" />
</Button> </Button>
<Button id="btn_recenter" color="#FFFFFF00" border_color="#FFFFFF00" tooltip="ACTIONS.RECENTER_PLAYSPACE" tooltip_side="top"> <Button id="btn_recenter" color="#FFFFFF00" border_color="#FFFFFF00" tooltip="ACTIONS.RECENTER_PLAYSPACE" tooltip_side="top">
<sprite src="dashboard/recenter.svg" width="24" height="24" margin="8" /> <sprite src_builtin="dashboard/recenter.svg" width="24" height="24" margin="8" />
</Button> </Button>
</div> </div>
@@ -145,4 +144,4 @@
</div> </div>
</div> </div>
</elements> </elements>
</layout> </layout>

View File

@@ -0,0 +1,27 @@
<layout>
<include src="theme.xml" />
<macro name="dropdown_button"
flex_direction="row"
border="2"
color="#00000055"
border_color="#FFFFFF66"
justify_content="space_between" />
<!-- id, text, translation, tooltip -->
<template name="DropdownButton">
<label text="${text}" translation="${translation}" />
<Button id="${id}" height="32" tooltip="${tooltip}" >
<div padding_left="8" padding_right="8" min_width="200">
<label id="${id}_value" weight="bold" />
</div>
<div gap="2">
<div padding_top="4" padding_bottom="4">
<rectangle width="2" height="100%" color="#FFFFFF66" />
</div>
<sprite margin_left="-4" width="30" height="30" color="~color_text" src_builtin="dashboard/down.svg" />
</div>
</Button>
</template>
</layout>

View File

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

View File

@@ -15,7 +15,7 @@
align_items="center" align_items="center"
justify_content="center" justify_content="center"
flex_direction="column"> flex_direction="column">
<sprite src="${icon}" width="32" height="32" /> <sprite src_builtin="${icon}" width="32" height="32" />
<label weight="bold" size="18" text="${text}" translation="${translation}" /> <label weight="bold" size="18" text="${text}" translation="${translation}" />
</div> </div>
</Button> </Button>

View File

@@ -1,40 +1,50 @@
<layout> <layout>
<include src="t_tab_title.xml" /> <include src="t_tab_title.xml" />
<include src="../theme.xml" />
<template name="AppEntry"> <template name="AppEntry">
<Button <Button
id="button" width="116" max_width="140" min_height="100" flex_grow="1" id="button" width="116" max_width="140" min_height="100" flex_grow="1"
flex_direction="column" overflow="visible" align_items="center" justify_content="center" gap="8"> flex_direction="column" overflow="visible" align_items="center" justify_content="center" gap="4"
color="#3385FF10"
>
<div> <div>
<sprite src="${src}" src_ext="${src_ext}" width="64" height="64" /> <sprite src="${src}" src_ext="${src_ext}" width="64" height="64" />
</div> </div>
<div align_items="center" justify_content="center"> <div align_items="center" justify_content="center">
<label weight="bold" text="${name}" size="12" /> <label width="116" weight="bold" text="${name}" size="12" wrap="1" align="center" padding_left="16" padding_right="16" />
</div> </div>
</Button> </Button>
</template> </template>
<template name="CategoryText">
<rectangle width="100%" flex_direction="column" round="8" border="2" gradient="vertical" color2="#4477FF09" color="#5588FF10" border_color="#FFFFFF00">
<label margin="8" text="${text}" weight="bold" size="22" />
<rectangle height="2" color="~color_accent" margin_left="4" margin_right="4" />
</rectangle>
</template>
<elements> <elements>
<TabTitle translation="APPLICATIONS" icon="dashboard/apps.svg" /> <TabTitle translation="APPLICATIONS" icon="dashboard/apps.svg" />
<!-- placeholders for now --> <!-- placeholders for now -->
<!--
<div gap="4" align_items="center"> <div gap="4" align_items="center">
<Button width="48" height="38"> <Button width="48" height="38">
<sprite src="dashboard/alphabetical.svg" width="24" height="24" /> <sprite src_builtin="dashboard/alphabetical.svg" width="24" height="24" />
</Button> </Button>
<Button width="48" height="38"> <Button width="48" height="38">
<sprite src="dashboard/category_search.svg" width="24" height="24" /> <sprite src_builtin="dashboard/category_search.svg" width="24" height="24" />
</Button> </Button>
<sprite src="dashboard/search.svg" width="24" height="24" /> <sprite src_builtin="dashboard/search.svg" width="24" height="24" />
<!-- placeholder editbox -->
<rectangle flex_grow="1" height="100%" color="#1d2e51" border_color="#294774" border="2" round="4" align_items="center" padding_left="12"> <rectangle flex_grow="1" height="100%" color="#1d2e51" border_color="#294774" border="2" round="4" align_items="center" padding_left="12">
<label text="Search" color="#FFFFFF88" weight="bold" /> <label text="Search" color="#FFFFFF88" weight="bold" />
</rectangle> </rectangle>
</div> </div>
-->
<div <div
id="app_list_parent" id="app_list_parent"
flex_direction="row" flex_direction="row"
flex_wrap="wrap" flex_wrap="wrap"
justify_content="center"
gap="4" gap="4"
overflow_y="scroll" overflow_y="scroll"
/> />

View File

@@ -3,5 +3,6 @@
<elements> <elements>
<TabTitle translation="GAMES" icon="dashboard/games.svg" /> <TabTitle translation="GAMES" icon="dashboard/games.svg" />
<div id="game_list_parent" align_items="center" />
</elements> </elements>
</layout> </layout>

View File

@@ -8,7 +8,7 @@
align_items="center" align_items="center"
flex_grow="1" flex_grow="1"
gap="24"> gap="24">
<sprite src="dashboard/wayvr_dashboard.svg" width="96" height="96" /> <sprite src_builtin="dashboard/wayvr_dashboard.svg" width="96" height="96" />
<label id="label_hello" size="32" weight="bold" /> <label id="label_hello" size="32" weight="bold" />
<!-- main button list --> <!-- main button list -->

View File

@@ -1,7 +1,40 @@
<layout> <layout>
<include src="t_tab_title.xml" /> <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> <elements>
<TabTitle translation="MONADO_RUNTIME" icon="dashboard/monado.svg" /> <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>
</elements> </elements>
</layout> </layout>

View File

@@ -2,6 +2,9 @@
<include src="t_tab_title.xml" /> <include src="t_tab_title.xml" />
<elements> <elements>
<TabTitle translation="PROCESSES" icon="dashboard/window.svg" /> <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> </elements>
</layout> </layout>

View File

@@ -1,61 +1,41 @@
<layout> <layout>
<include src="t_tab_title.xml" /> <include src="t_tab_title.xml" />
<include src="../t_group_box.xml" /> <include src="../t_group_box.xml" />
<include src="../t_dropdown_button.xml" />
<template name="SettingsGroupBox">
<rectangle macro="group_box" id="${id}" flex_grow="1">
<GroupBoxTitle translation="${translation}" src_builtin="${icon}" />
</rectangle>
</template>
<template name="CheckBoxSetting">
<CheckBox id="${id}" text="${text}" translation="${translation}" checked="${checked}" tooltip="${tooltip}" />
</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}" />
</template>
<template name="SelectSetting">
<label text="${text}" translation="${translation}" tooltip="${tooltip}" />
<RadioGroup id="${id}" />
</template>
<template name="SelectOption">
<RadioBox text="${text}" translation="${translation}" value="${value}" tooltip="${tooltip}" />
</template>
<template name="DangerButton">
<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>
</template>
<elements> <elements>
<TabTitle translation="SETTINGS" icon="dashboard/settings.svg" /> <TabTitle translation="SETTINGS" icon="dashboard/settings.svg" />
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" />
<div flex_wrap="wrap" justify_content="stretch" gap="4">
<!-- Home screen -->
<rectangle macro="group_box">
<GroupBoxTitle translation="HOME_SCREEN" src="dashboard/wayvr_dashboard.svg" />
<CheckBox id="cb_hide_username" translation="APP_SETTINGS.HIDE_USERNAME" />
</rectangle>
<!-- General settings -->
<rectangle macro="group_box">
<GroupBoxTitle translation="GENERAL_SETTINGS" src="dashboard/settings.svg" />
<CheckBox id="cb_am_pm_clock" text="AM/PM clock" />
<CheckBox id="cb_opaque_background" translation="APP_SETTINGS.OPAQUE_BACKGROUND" />
</rectangle>
<!-- Application launcher -->
<rectangle macro="group_box">
<GroupBoxTitle translation="APPLICATION_LAUNCHER" src="dashboard/apps.svg" />
<CheckBox id="cb_xwayland_by_default" translation="APP_SETTINGS.RUN_IN_XWAYLAND_MODE_BY_DEFAULT" />
</rectangle>
<!-- headset settings -->
<rectangle macro="group_box">
<GroupBoxTitle translation="APP_SETTINGS.HEADSET_SETTINGS" src="dashboard/vr.svg" />
<label translation="APP_SETTINGS.BRIGHTNESS" />
<Slider width="100" height="24" min_value="0.0" max_value="100.0" />
</rectangle>
<!-- wlx-overlay-s settings -->
<rectangle macro="group_box">
<GroupBoxTitle translation="APP_SETTINGS.WLX_OVERLAY_S_SETTINGS" src="dashboard/vr.svg" />
<CheckBox translation="APP_SETTINGS.WLX.NOTIFICATIONS_ENABLED" />
<CheckBox translation="APP_SETTINGS.WLX.NOTIFICATIONS_SOUND_ENABLED" />
<CheckBox translation="APP_SETTINGS.WLX.KEYBOARD_SOUND_ENABLED" />
<CheckBox translation="APP_SETTINGS.WLX.BLOCK_GAME_INPUT" />
<label translation="APP_SETTINGS.WLX.SPACE_DRAG_MULTIPLIER" />
<Slider width="100" height="24" min_value="0.0" max_value="3.0" />
<CheckBox translation="APP_SETTINGS.WLX.SPACE_DRAG_ROTATION_ENABLED" />
<CheckBox translation="APP_SETTINGS.WLX.SHOW_SKYBOX" />
<CheckBox translation="APP_SETTINGS.WLX.ENABLE_PASSTHROUGH" />
</rectangle>
</div>
<div>
<!-- TODO: icon support in buttons -->
<Button color="#AA3333" height="32">
<div margin_left="8" margin_right="8" gap="4" align_items="center">
<sprite src="dashboard/refresh.svg" width="24" height="24" />
<label weight="bold" translation="APP_SETTINGS.RESTART_SOFTWARE" />
</div>
</Button>
</div>
</elements> </elements>
</layout> </layout>

View File

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

View File

@@ -2,38 +2,62 @@
<template name="Subtext"> <template name="Subtext">
<div flex_direction="row" gap="8"> <div flex_direction="row" gap="8">
<label weight="bold" text="${title}" /> <label weight="bold" text="${title}" />
<label text="foo" /> <label id="${label_id}" />
</div> </div>
</template> </template>
<template name="ApplicationIcon"> <template name="ApplicationIcon">
<sprite src_ext="${path}" width="128" height="128" /> <sprite src_ext="${path}" width="96" height="96" />
</template> </template>
<include src="../t_separator.xml" /> <include src="../t_separator.xml" />
<include src="../t_group_box.xml" /> <include src="../t_group_box.xml" />
<elements> <elements>
<div flex_direction="row" gap="16" width="100%"> <div flex_direction="row" gap="16" flex_grow="1">
<rectangle macro="group_box" id="icon_parent" height="100%" padding="8" color="#0033aa66" color2="#00000022" gradient="vertical" justify_content="center"> <rectangle macro="group_box" id="icon_parent" padding="16" color="#0033aa66" color2="#00000022" gradient="vertical" justify_content="center">
</rectangle> </rectangle>
<div flex_direction="column" gap="8" width="100%" align_items="baseline"> <div flex_direction="column" gap="8" flex_grow="1">
<label id="label_title" weight="bold" size="32" /> <label id="label_title" weight="bold" size="32" overflow="hidden" />
<Subtext title="Exec:" /> <Subtext label_id="label_exec" overflow="hidden" />
<Subtext title="Args:" />
<Separator /> <Separator />
<CheckBox text="Run in X11 mode (cage)" /> <RadioGroup id="radio_compositor" flex_direction="row" gap="16">
<CheckBox text="Run in Wayland mode" checked="1" /> <RadioBox translation="APP_LAUNCHER.MODE.NATIVE" value="Native" checked="1" />
<RadioBox translation="APP_LAUNCHER.MODE.CAGE" value="Cage" /> <!-- TODO: tooltips -->
</RadioGroup>
<Separator /> <Separator />
<Button color="#44ce22FF" padding_top="4" padding_bottom="4" round="8" padding_right="12"> <label translation="APP_LAUNCHER.RES_TITLE" />
<sprite src="dashboard/play.svg" width="32" height="32" /> <RadioGroup id="radio_res" flex_direction="row" gap="16">
<label text="Launch embedded" weight="bold" size="17" shadow="#00000099" /> <RadioBox text="1440p" value="Res1440" />
</Button> <RadioBox text="1080p" value="Res1080" checked="1" />
<RadioBox text="720p" value="Res720" />
<RadioBox text="480p" value="Res480" />
</RadioGroup>
<Separator /> <Separator />
<rectangle macro="group_box"> <label translation="APP_LAUNCHER.ASPECT_TITLE" />
<label size="16" weight="bold" text="Or launch it detached" /> <RadioGroup id="radio_orientation" flex_direction="row" gap="16">
</rectangle> <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" />
</RadioGroup>
<Separator />
<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" />
</RadioGroup>
<Separator />
<div flex_direction="row" justify_content="space_between" gap="16">
<CheckBox id="cb_autostart" translation="APP_LAUNCHER.AUTOSTART" />
<Button id="btn_launch" align_self="baseline" color="#44ce22FF" padding_top="4" padding_bottom="4" round="8" padding_right="12" min_height="40">
<sprite src_builtin="dashboard/play.svg" width="32" height="32" />
<label translation="APP_LAUNCHER.LAUNCH" weight="bold" size="17" shadow="#00000099" />
</Button>
</div>
</div> </div>
</div> </div>
</elements> </elements>

View File

@@ -29,7 +29,7 @@
<template name="SelectAudioProfileText"> <template name="SelectAudioProfileText">
<div align_items="center" gap="8"> <div align_items="center" gap="8">
<Button width="48" height="32" id="btn_back"> <Button width="48" height="32" id="btn_back">
<sprite src="dashboard/back.svg" width="24" height="24" /> <sprite src_builtin="dashboard/back.svg" width="24" height="24" />
</Button> </Button>
<label translation="AUDIO.SELECT_AUDIO_CARD_PROFILE" size="14" weight="bold" /> <label translation="AUDIO.SELECT_AUDIO_CARD_PROFILE" size="14" weight="bold" />
</div> </div>
@@ -54,15 +54,15 @@
<div flex_direction="row" gap="4"> <div flex_direction="row" gap="4">
<Button <Button
id="btn_auto" id="btn_auto"
sprite_src="dashboard/magic_wand.svg" sprite_src_builtin="dashboard/magic_wand.svg"
min_width="32" min_width="32"
tooltip="AUDIO.AUTO_SWITCH_TO_VR_AUDIO" tooltip="AUDIO.AUTO_SWITCH_TO_VR_AUDIO"
color="~color_accent" color="~color_accent"
tooltip_side="right" /> tooltip_side="right" />
<BottomButton id="btn_sinks" src="dashboard/volume.svg" translation="AUDIO.SPEAKERS" /> <BottomButton id="btn_sinks" src_builtin="dashboard/volume.svg" translation="AUDIO.SPEAKERS" />
<BottomButton id="btn_sources" src="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" /> <BottomButton id="btn_sources" src_builtin="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" />
<BottomButton id="btn_cards" src="dashboard/cpu.svg" translation="AUDIO.CARDS" /> <BottomButton id="btn_cards" src_builtin="dashboard/cpu.svg" translation="AUDIO.CARDS" />
</div> </div>
</elements> </elements>
</layout> </layout>

View File

@@ -0,0 +1,19 @@
<layout>
<elements>
<div flex_direction="row" gap="16" align_items="center">
<div id="cover_art_parent" />
<div flex_direction="column" gap="16">
<label id="label_title" weight="bold" size="32" />
<div flex_direction="row" gap="8">
<label text="by" />
<label weight="bold" id="label_author" text="Unknown" />
</div>
<label id="label_description" wrap="1" text="No description available" />
<Button id="btn_launch" align_self="baseline" color="#44ce22FF" padding_top="4" padding_bottom="4" round="8" padding_right="12" min_width="200" min_height="40">
<sprite src_builtin="dashboard/play.svg" width="32" height="32" />
<label text="Launch" weight="bold" size="17" shadow="#00000099" />
</Button>
</div>
</div>
</elements>
</layout>

View File

@@ -0,0 +1,7 @@
<layout>
<include src="../t_group_box.xml" />
<elements>
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" justify_content="center" />
</elements>
</layout>

View File

@@ -14,18 +14,17 @@
<rectangle <rectangle
position="relative" position="relative"
color="#000000" color="#000000"
round="4"
width="100%" height="48" width="100%" height="48"
> >
<!-- Shine effect at the top --> <!-- Shine effect at the top -->
<rectangle position="absolute" width="100%" height="2" round="4" color="#ffffff55" /> <rectangle position="absolute" width="100%" height="2" round="4" color="#ffffff22" />
<!-- Top bar contents --> <!-- Top bar contents -->
<div gap="16" align_items="center"> <div gap="16" align_items="center">
<!-- Back button --> <!-- Back button -->
<Button id="but_back" width="48" height="48" color="#ffffff00" border_color="#ffffff00"> <Button id="but_back" width="48" height="48" color="#ffffff00" border_color="#ffffff00">
<sprite src="dashboard/back.svg" width="24" height="24" /> <sprite src_builtin="dashboard/back.svg" width="24" height="24" />
</Button> </Button>
<!-- Title --> <!-- Title -->
@@ -34,9 +33,9 @@
</rectangle> </rectangle>
<!-- Content --> <!-- Content -->
<rectangle width="100%" height="100%" <rectangle height="100%"
color="#010310ee" color="#010310fe"
color2="#062a5eee" color2="#051c55fc"
gradient="vertical" gradient="vertical"
padding="16" padding="16"
id="content"> id="content">

View File

@@ -0,0 +1,9 @@
<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,9 @@
<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="row" flex_wrap="wrap" flex_grow="1" />
</rectangle>
</elements>
</layout>

View File

@@ -0,0 +1,16 @@
<layout>
<include src="../t_group_box.xml" />
<elements>
<div gap="16" flex_direction="column" width="100%" justify_self="center" align_items="center" justify_content="center">
<rectangle macro="group_box" align_items="center">
<div id="window_parent" />
<div gap="8">
<Button id="btn_show_hide" text="showhide" sprite_src_builtin="dashboard/eye.svg" />
<Button id="btn_close" translation="CLOSE_WINDOW" sprite_src_builtin="dashboard/remove_circle.svg" />
<Button id="btn_kill" translation="TERMINATE_PROCESS" sprite_src_builtin="dashboard/remove_circle.svg" />
</div>
</rectangle>
</div>
</elements>
</layout>

View File

@@ -1,6 +1,6 @@
{ {
"HOME_SCREEN": "Startbildschirm", "HOME_SCREEN": "Startbildschirm",
"MONADO_RUNTIME": "Monado-Laufzeitumgebung", "MONADO_RUNTIME": "Monado-Laufzeitumgebung",
"APPLICATIONS": "Anwendungen", "APPLICATIONS": "Anwendungen",
"GAMES": "Spiele", "GAMES": "Spiele",
"SETTINGS": "Einstellungen", "SETTINGS": "Einstellungen",
@@ -11,21 +11,65 @@
"APP_SETTINGS": { "APP_SETTINGS": {
"HIDE_USERNAME": "Benutzernamen ausblenden", "HIDE_USERNAME": "Benutzernamen ausblenden",
"OPAQUE_BACKGROUND": "Undurchsichtiger Hintergrund", "OPAQUE_BACKGROUND": "Undurchsichtiger Hintergrund",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "Standardmäßig in XWayland-Modus ausführen", "WLX": {},
"WLX_OVERLAY_S_SETTINGS": "WlxOverlay-S Einstellungen", "LOOK_AND_FEEL": "Aussehen und Verhalten",
"HEADSET_SETTINGS": "Headset-Einstellungen", "HIDE_GRAB_HELP": "Greif-Hilfe ausblenden",
"BRIGHTNESS": "Helligkeit", "ANIMATION_SPEED": "UI-Animationsgeschwindigkeit",
"WLX": { "ROUND_MULTIPLIER": "UI-Kantenrundung",
"NOTIFICATIONS_ENABLED": "Benachrichtigungen aktiviert", "USE_SKYBOX": "Skybox aktivieren",
"NOTIFICATIONS_SOUND_ENABLED": "Benachrichtigungssound aktiviert", "USE_PASSTHROUGH": "Passthrough aktivieren",
"KEYBOARD_SOUND_ENABLED": "Tastaturgeräusch aktiviert", "CLOCK_12H": "12-Stunden-Uhr",
"BLOCK_GAME_INPUT": "Spielsteuerung blockieren", "FEATURES": "Funktionen",
"SPACE_DRAG_MULTIPLIER": "Raum-Drag-Multiplikator", "NOTIFICATIONS_ENABLED": "Benachrichtigungen aktivieren",
"SPACE_DRAG_ROTATION_ENABLED": "Rotation im Space-Drag aktivieren", "NOTIFICATIONS_SOUND_ENABLED": "Benachrichtigungstöne",
"SHOW_SKYBOX": "Skybox anzeigen", "KEYBOARD_SOUND_ENABLED": "Tastengeräusche",
"ENABLE_PASSTHROUGH": "Passthrough aktivieren" "SPACE_DRAG_MULTIPLIER": "Raum-Drag-Multiplikator",
}, "SPACE_DRAG_UNLOCKED": "Erlaube Space-Drag auf allen Achsen",
"RESTART_SOFTWARE": "Software neu starten" "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"
}, },
"HELLO": "Hallo!", "HELLO": "Hallo!",
"AUDIO": { "AUDIO": {
@@ -40,10 +84,61 @@
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Kein VR-Mikrofon gefunden. Schalten Sie es 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", "FAILED_TO_SWITCH_MICROPHONE": "Fehler beim Wechseln des Mikrofons",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofon erfolgreich umgeschaltet", "MICROPHONE_SET_SUCCESSFULLY": "Mikrofon erfolgreich umgeschaltet",
"SPEAKERS_SET_SUCCESSFULLY": "Lautsprecher erfolgreich umgeschaltet", "SPEAKERS_SET_SUCCESSFULLY": "Lautsprecher erfolgreich umgeschaltet"
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "Gerät gefunden und initialisiert, aber nicht umgeschaltet"
}, },
"ACTIONS": { "ACTIONS": {
"RECENTER_PLAYSPACE": "Playspace neu zentrieren" "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"
} }

View File

@@ -1,49 +1,151 @@
{ {
"HOME_SCREEN": "Home",
"MONADO_RUNTIME": "„Monado” runtime",
"APPLICATIONS": "Applications",
"GAMES": "Games",
"SETTINGS": "Settings",
"PROCESSES": "Processes",
"HELLO_USER": "Hello, {USER}!",
"HELLO": "Hello!",
"GENERAL_SETTINGS": "General settings",
"APPLICATION_LAUNCHER": "Application launcher",
"APP_SETTINGS": {
"RESTART_SOFTWARE": "Restart software",
"HIDE_USERNAME": "Hide username",
"OPAQUE_BACKGROUND": "Opaque background",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "Run in XWayland mode by default",
"WLX_OVERLAY_S_SETTINGS": "WlxOverlay-S settings",
"HEADSET_SETTINGS": "Headset settings",
"BRIGHTNESS": "Brightness",
"WLX": {
"NOTIFICATIONS_ENABLED": "Notifications enabled",
"NOTIFICATIONS_SOUND_ENABLED": "Notifications sound enabled",
"KEYBOARD_SOUND_ENABLED": "Keyboard sound enabled",
"BLOCK_GAME_INPUT": "Block game input",
"SPACE_DRAG_MULTIPLIER": "Space-drag multiplier",
"SPACE_DRAG_ROTATION_ENABLED": "Enable rotation in space-drag",
"SHOW_SKYBOX": "Show skybox",
"ENABLE_PASSTHROUGH": "Enable passthrough"
}
},
"AUDIO": {
"SELECT_AUDIO_CARD_PROFILE": "Select audio card profile",
"SETTINGS": "Audio settings",
"VOLUME": "Volume",
"AUTO_SWITCH_TO_VR_AUDIO": "Auto-switch to VR audio",
"SPEAKERS": "Speakers",
"MICROPHONES": "Microphones",
"CARDS": "Cards",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "No VR speakers found. Switch them manually.",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "No VR microphone found. Switch it manually.",
"FAILED_TO_SWITCH_MICROPHONE": "Failed to switch microphone",
"MICROPHONE_SET_SUCCESSFULLY": "Microphone set successfully",
"SPEAKERS_SET_SUCCESSFULLY": "Speakers set successfully",
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "Device found and initialized, but not switched"
},
"ACTIONS": { "ACTIONS": {
"RECENTER_PLAYSPACE": "Re-center playspace" "RECENTER_PLAYSPACE": "Re-center playspace"
} },
"APP_LAUNCHER": {
"ASPECT": {
"SEMI_TALL": "Semi-tall",
"SEMI_WIDE": "Semi-wide",
"SQUARE": "Square",
"TALL": "Tall",
"WIDE": "Wide"
},
"ASPECT_TITLE": "Aspect",
"AUTOSTART": "Run automatically on startup",
"LAUNCH": "Launch",
"MODE": {
"CAGE": "Compatibility mode (Cage)",
"NATIVE": "Native mode"
},
"POS": {
"ANCHORED": "Anchored",
"ANCHORED_HELP": "Stays in place relative to center marker.",
"FLOATING": "Floating",
"FLOATING_HELP": "Moves independently, recenters on show.",
"STATIC": "Static",
"STATIC_HELP": "Not part of any set. Does not recenter."
},
"POS_TITLE": "Positioning",
"RES_TITLE": "Resolution"
},
"APP_SETTINGS": {
"ALLOW_SLIDING": "Stick interaction during grab",
"ANIMATION_SPEED": "UI Animation speed",
"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",
"CAPTURE_METHOD": "Wayland screen capture",
"CAPTURE_METHOD_HELP": "Try changing this if you are\nexperiencing black or glitchy screens",
"CLEAR_PIPEWIRE_TOKENS": "Clear PipeWire tokens",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Prompt for screen selection on next start",
"CLEAR_SAVED_STATE": "Clear saved state",
"CLEAR_SAVED_STATE_HELP": "Reset sets & overlay positions",
"CLICK_FREEZE_TIME_MS": "Click freeze time (ms)",
"CLICK_FREEZE_TIME_MS_HELP": "Helps with double-click precision",
"CLOCK_12H": "12-hour clock",
"CONTROLS": "Controls",
"DELETE_ALL_CONFIGS": "Wipe configuration",
"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",
"FEATURES": "Features",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mouse move on trigger touch",
"HIDE_GRAB_HELP": "Hide grab help",
"HIDE_USERNAME": "Hide username",
"INVERT_SCROLL_DIRECTION_X": "Invert horizontal scroll direction",
"INVERT_SCROLL_DIRECTION_Y": "Invert vertical scroll direction",
"KEYBOARD_MIDDLE_CLICK": "Keyboard middle click",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier to use when typing\nwith purple laser",
"KEYBOARD_SOUND_ENABLED": "Keyboard sounds",
"LEFT_HANDED_MOUSE": "Left-handed mouse",
"LEFT_HANDED_MOUSE_HELP": "Use this if mouse buttons are swapped",
"LONG_PRESS_DURATION": "Long press duration",
"LOOK_AND_FEEL": "Look & Feel",
"MISC": "Miscellaneous",
"NOTIFICATIONS_ENABLED": "Enable notifications",
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
"OPAQUE_BACKGROUND": "Opaque background",
"OPTION": {
"AUTO": "Automatic",
"AUTO_HELP": "ScreenCopy GPU if supported,\notherwise PipeWire GPU.",
"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",
"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",
"SETS_ON_WATCH": "Sets on watch",
"SPACE_DRAG_MULTIPLIER": "Space drag multiplier",
"SPACE_DRAG_UNLOCKED": "Allow space drag on all axes",
"SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes",
"TROUBLESHOOTING": "Troubleshooting",
"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"
},
"APPLICATION_LAUNCHER": "Application launcher",
"APPLICATION_STARTED": "Application started",
"APPLICATIONS": "Applications",
"AUDIO": {
"AUTO_SWITCH_TO_VR_AUDIO": "Auto-switch to VR audio",
"CARDS": "Cards",
"FAILED_TO_SWITCH_MICROPHONE": "Failed to switch microphone",
"MICROPHONE_SET_SUCCESSFULLY": "Microphone set successfully",
"MICROPHONES": "Microphones",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "No VR microphone found. Switch it manually.",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "No VR speakers found. Switch them manually.",
"SELECT_AUDIO_CARD_PROFILE": "Select audio card profile",
"SETTINGS": "Audio settings",
"SPEAKERS": "Speakers",
"SPEAKERS_SET_SUCCESSFULLY": "Speakers set successfully",
"VOLUME": "Volume"
},
"CLOSE_WINDOW": "Close window",
"DISPLAY_BRIGHTNESS": "Display brightness",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
"GAME_LAUNCHED": "Game launched",
"GAME_LIST": {
"NO_GAMES_FOUND": "No games found"
},
"GAMES": "Games",
"GENERAL_SETTINGS": "General settings",
"HEIGHT": "Height",
"HELLO": "Hello!",
"HELLO_USER": "Hello, {USER}!",
"HIDE": "Hide",
"HOME_SCREEN": "Home",
"LIST_OF_PROCESSES": "Process list",
"LIST_OF_WINDOWS": "Window list",
"MONADO_RUNTIME": "Monado runtime",
"NO_WINDOWS_FOUND": "No windows found",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution"
},
"PROCESS_LIST": {
"LOCATED_ON": "on",
"NO_PROCESSES_FOUND": "No processes found",
"TERMINATE_PROCESS_NAMED_X": "Terminate process \"{PROCESS_NAME}\""
},
"PROCESSES": "Processes",
"REMOVE": "Remove",
"SETTINGS": "Settings",
"SHOW": "Show",
"TERMINATE_PROCESS": "Terminate process",
"WIDTH": "Width",
"WINDOW_OPTIONS": "Window options"
} }

View File

@@ -1,6 +1,6 @@
{ {
"HOME_SCREEN": "Inicio", "HOME_SCREEN": "Inicio",
"MONADO_RUNTIME": "Monado tiempo de ejecución", "MONADO_RUNTIME": "Monado tiempo de ejecución",
"APPLICATIONS": "Aplicaciones", "APPLICATIONS": "Aplicaciones",
"GAMES": "Juegos", "GAMES": "Juegos",
"SETTINGS": "Ajustes", "SETTINGS": "Ajustes",
@@ -11,21 +11,65 @@
"APP_SETTINGS": { "APP_SETTINGS": {
"HIDE_USERNAME": "Ocultar nombre de usuario", "HIDE_USERNAME": "Ocultar nombre de usuario",
"OPAQUE_BACKGROUND": "Fondo opaco", "OPAQUE_BACKGROUND": "Fondo opaco",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "Ejecutar en modo XWayland por defecto", "WLX": {},
"WLX_OVERLAY_S_SETTINGS": "Configuración de WlxOverlay-S", "LOOK_AND_FEEL": "Apariencia y estilo",
"HEADSET_SETTINGS": "Configuración del casco", "HIDE_GRAB_HELP": "Ocultar ayuda para agarrar",
"BRIGHTNESS": "Brillo", "ANIMATION_SPEED": "Velocidad de animación de la IU",
"WLX": { "ROUND_MULTIPLIER": "Redondeo de bordes de la IU",
"NOTIFICATIONS_ENABLED": "Notificaciones activadas", "USE_SKYBOX": "Activar skybox",
"NOTIFICATIONS_SOUND_ENABLED": "Sonido de notificaciones activado", "USE_PASSTHROUGH": "Activar passthrough",
"KEYBOARD_SOUND_ENABLED": "Sonido del teclado activado", "CLOCK_12H": "Reloj de 12 horas",
"BLOCK_GAME_INPUT": "Bloquear entrada del juego", "FEATURES": "Funciones",
"SPACE_DRAG_MULTIPLIER": "Multiplicador de movimiento por arrastre", "NOTIFICATIONS_ENABLED": "Habilitar notificaciones",
"SPACE_DRAG_ROTATION_ENABLED": "Habilitar rotación en space-drag", "NOTIFICATIONS_SOUND_ENABLED": "Sonidos de notificación",
"SHOW_SKYBOX": "Mostrar cielo", "KEYBOARD_SOUND_ENABLED": "Sonidos del teclado",
"ENABLE_PASSTHROUGH": "Habilitar Passthrough" "SPACE_DRAG_MULTIPLIER": "Multiplicador de arrastre espacial",
}, "SPACE_DRAG_UNLOCKED": "Permitir arrastre del espacio en todos los ejes",
"RESTART_SOFTWARE": "Reiniciar software" "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"
}, },
"HELLO": "¡Hola!", "HELLO": "¡Hola!",
"AUDIO": { "AUDIO": {
@@ -40,10 +84,61 @@
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "No se encontró micrófono VR. Actívelo 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", "FAILED_TO_SWITCH_MICROPHONE": "No se pudo cambiar el micrófono",
"MICROPHONE_SET_SUCCESSFULLY": "Micrófono configurado correctamente", "MICROPHONE_SET_SUCCESSFULLY": "Micrófono configurado correctamente",
"SPEAKERS_SET_SUCCESSFULLY": "Altavoces configurados correctamente", "SPEAKERS_SET_SUCCESSFULLY": "Altavoces configurados correctamente"
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "Dispositivo encontrado e inicializado, pero no cambiado"
}, },
"ACTIONS": { "ACTIONS": {
"RECENTER_PLAYSPACE": "Re-centrar espacio de juego" "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"
}

View File

@@ -1,7 +1,7 @@
{ {
"HOME_SCREEN": "ホーム", "HOME_SCREEN": "ホーム",
"MONADO_RUNTIME": "「Monado」ランタイム", "MONADO_RUNTIME": "「Monado」ランタイム",
"APPLICATIONS": "アプリケーション", "APPLICATIONS": "アプリ",
"GAMES": "ゲーム", "GAMES": "ゲーム",
"SETTINGS": "設定", "SETTINGS": "設定",
"PROCESSES": "プロセス", "PROCESSES": "プロセス",
@@ -11,21 +11,65 @@
"APP_SETTINGS": { "APP_SETTINGS": {
"HIDE_USERNAME": "ユーザー名を表示しない", "HIDE_USERNAME": "ユーザー名を表示しない",
"OPAQUE_BACKGROUND": "不透明な背景", "OPAQUE_BACKGROUND": "不透明な背景",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "XWaylandモードでデフォルトで実行する", "WLX": {},
"WLX_OVERLAY_S_SETTINGS": "WlxOverlay-Sの設定", "LOOK_AND_FEEL": "外観",
"HEADSET_SETTINGS": "ヘッドセット設定", "HIDE_GRAB_HELP": "掴み操作のヘルプを非表示にする",
"BRIGHTNESS": "明るさ", "ANIMATION_SPEED": "UIアニメーション速度",
"WLX": { "ROUND_MULTIPLIER": "UI エッジの丸み",
"NOTIFICATIONS_ENABLED": "通知", "USE_SKYBOX": "スカイボックスを有効にする",
"NOTIFICATIONS_SOUND_ENABLED": "通知音", "USE_PASSTHROUGH": "パススルーを有効にする",
"KEYBOARD_SOUND_ENABLED": "キーボード音", "CLOCK_12H": "12時間制",
"BLOCK_GAME_INPUT": "ゲーム入力をブロック", "FEATURES": "機能",
"SPACE_DRAG_MULTIPLIER": "スペースドラッグ乗数", "NOTIFICATIONS_ENABLED": "通知を有効にする",
"SPACE_DRAG_ROTATION_ENABLED": "スペースドラッグでの回転", "NOTIFICATIONS_SOUND_ENABLED": "通知音",
"SHOW_SKYBOX": "スカイボックス", "KEYBOARD_SOUND_ENABLED": "キーボード音",
"ENABLE_PASSTHROUGH": "パススルー" "SPACE_DRAG_MULTIPLIER": "スペースドラッグ倍率",
}, "SPACE_DRAG_UNLOCKED": "全ての軸でのスペースドラッグを許可",
"RESTART_SOFTWARE": "ソフトウェアを再起動" "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": "紫色のレーザーで入力する際の修飾キー"
}, },
"HELLO": "こんにちは!", "HELLO": "こんにちは!",
"AUDIO": { "AUDIO": {
@@ -40,10 +84,61 @@
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "VRマイクが見つかりませんでした。手動で切り替えてください。", "NO_VR_MICROPHONE_SWITCH_MANUALLY": "VRマイクが見つかりませんでした。手動で切り替えてください。",
"FAILED_TO_SWITCH_MICROPHONE": "マイクの切り替えに失敗しました", "FAILED_TO_SWITCH_MICROPHONE": "マイクの切り替えに失敗しました",
"MICROPHONE_SET_SUCCESSFULLY": "マイクの設定が完了しました", "MICROPHONE_SET_SUCCESSFULLY": "マイクの設定が完了しました",
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定しました", "SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定しました"
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "デバイスが見つかり、初期化されましたが、切り替えられていません"
}, },
"ACTIONS": { "ACTIONS": {
"RECENTER_PLAYSPACE": "プレイスペースを再中央" "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": "ディスプレイの明るさ"
} }

View File

@@ -1,49 +1,144 @@
{ {
"HOME_SCREEN": "Ekran główny", "ACTIONS": {
"MONADO_RUNTIME": "Środowisko Monado", "RECENTER_PLAYSPACE": "Wycentruj przestrzeń"
"APPLICATIONS": "Aplikacje", },
"GAMES": "Gry", "APP_SETTINGS": {
"SETTINGS": "Ustawienia", "HIDE_USERNAME": "Ukryj nazwę użytkownika",
"PROCESSES": "Procesy", "OPAQUE_BACKGROUND": "Nieprzezroczyste tło",
"HELLO_USER": "Witaj, {USER}!", "WLX": {},
"GENERAL_SETTINGS": "Ustawienia ogólne", "LOOK_AND_FEEL": "Wygląd i działanie",
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji", "HIDE_GRAB_HELP": "Ukryj pomoc dotyczącą chwytania",
"APP_SETTINGS": { "ANIMATION_SPEED": "Prędkość animacji UI",
"HIDE_USERNAME": "Ukryj nazwę użytkownika", "ROUND_MULTIPLIER": "Zaokrąglenie krawędzi UI",
"OPAQUE_BACKGROUND": "Nieprzezroczyste tło", "USE_SKYBOX": "Włącz niebo",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "Uruchom domyślnie w trybie XWayland", "USE_PASSTHROUGH": "Włącz passthrough",
"WLX_OVERLAY_S_SETTINGS": "Ustawienia wlx-overlay-s", "CLOCK_12H": "Zegar 12-godzinny",
"HEADSET_SETTINGS": "Ustawienia HMD", "FEATURES": "Funkcje",
"BRIGHTNESS": "Jasność", "NOTIFICATIONS_ENABLED": "Włącz powiadomienia",
"WLX": { "NOTIFICATIONS_SOUND_ENABLED": "Dźwięki powiadomień",
"NOTIFICATIONS_ENABLED": "Powiadomienia", "KEYBOARD_SOUND_ENABLED": "Dźwięki klawiatury",
"NOTIFICATIONS_SOUND_ENABLED": "Dźwięk powiadomień", "SPACE_DRAG_MULTIPLIER": "Mnożnik przesuwania przestrzeni",
"KEYBOARD_SOUND_ENABLED": "Dźwięki klawiatury", "SPACE_DRAG_UNLOCKED": "Pozwól na przesuwanie przestrzeni na wszystkich osiach",
"BLOCK_GAME_INPUT": "Zablokuj sterowanie grą podczas używania Wlx", "SPACE_ROTATE_UNLOCKED": "Pozwól na rotację przestrzeni na wszystkich osiach",
"SPACE_DRAG_MULTIPLIER": "Mnożnik space-drag", "BLOCK_GAME_INPUT": "Blokuj input z gry",
"SPACE_DRAG_ROTATION_ENABLED": "Włącz rotację w space-drag", "BLOCK_GAME_INPUT_IGNORE_WATCH": "Nie blokuj inputu gry, gdy zegarek jest używany",
"SHOW_SKYBOX": "Pokaż skybox", "CONTROLS": "Sterowanie",
"ENABLE_PASSTHROUGH": "Włącz passthrough" "FOCUS_FOLLOWS_MOUSE_MODE": "Ruch myszą po dotknięciu spustu",
}, "LEFT_HANDED_MOUSE": "Myszka dla leworęcznych",
"RESTART_SOFTWARE": "Uruchom ponownie oprogramowanie" "ALLOW_SLIDING": "Interakcja z drążkami podczas chwytania",
}, "INVERT_SCROLL_DIRECTION_X": "Odwróć kierunek przewijania w poziomie",
"HELLO": "Witaj!", "INVERT_SCROLL_DIRECTION_Y": "Odwróć kierunek przewijania w pionie",
"AUDIO": { "SCROLL_SPEED": "Prędkość przewijania",
"VOLUME": "Głośność", "LONG_PRESS_DURATION": "Czas długiego przytrzymania",
"SETTINGS": "Ustawienia dźwięku", "POINTER_LERP_FACTOR": "Wygładzanie wskaźnika",
"AUTO_SWITCH_TO_VR_AUDIO": "Automatyczne przełączanie na dźwięk VR", "XR_CLICK_SENSITIVITY": "Czułość kliknięć XR",
"SPEAKERS": "Głośniki", "XR_CLICK_SENSITIVITY_RELEASE": "Czułość zwalniania XR",
"MICROPHONES": "Mikrofony", "CLICK_FREEZE_TIME_MS": "Czas zamrożenia po kliknięciu (ms)",
"CARDS": "Karty", "MISC": "Różne",
"SELECT_AUDIO_CARD_PROFILE": "Wybierz profil karty dźwiękowej", "XWAYLAND_BY_DEFAULT": "Uruchamiaj aplikacje domyślnie w trybie kompatybilności",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Brak głośników VR. Włącz je ręcznie.", "UPRIGHT_SCREEN_FIX": "Naprawa pozycji ekranu",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Brak mikrofonu VR. Włącz go ręcznie.", "DOUBLE_CURSOR_FIX": "Naprawa podwójnego kursora",
"FAILED_TO_SWITCH_MICROPHONE": "Nie udało się przełączyć mikrofon", "SCREEN_RENDER_DOWN": "Renderuj ekran w niższej rozdzielczości",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofon ustawiono pomyślnie", "UPRIGHT_SCREEN_FIX_HELP": "Naprawia pionowe ekrany na niektórych komputerach",
"SPEAKERS_SET_SUCCESSFULLY": "Głośniki ustawiono pomyślnie", "DOUBLE_CURSOR_FIX_HELP": "Włącz to, jeśli widzisz 2 kursory",
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "Urządzenie znalezione i zainicjalizowane, ale nie przełączone" "XR_CLICK_SENSITIVITY_HELP": "Czułość analogowego spustu",
}, "XR_CLICK_SENSITIVITY_RELEASE_HELP": "Musi być niższa niż kliknięcie",
"ACTIONS": { "CLICK_FREEZE_TIME_MS_HELP": "Pomaga w precyzji podwójnego kliknięcia",
"RECENTER_PLAYSPACE": "Wycentruj przestrzeń" "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"
},
"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"
} }

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,4 @@
use std::{cell::RefCell, path::PathBuf, rc::Rc}; use std::{path::PathBuf, rc::Rc};
use chrono::Timelike; use chrono::Timelike;
use glam::Vec2; use glam::Vec2;
@@ -8,23 +8,24 @@ use wgui::{
font_config::WguiFontConfig, font_config::WguiFontConfig,
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{LayoutParams, RcLayout, WidgetID}, layout::{Layout, LayoutParams, LayoutUpdateParams, LayoutUpdateResult, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::{label::WidgetLabel, rectangle::WidgetRectangle}, widget::{label::WidgetLabel, rectangle::WidgetRectangle},
windowing::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement}, windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
}; };
use wlx_common::timestep::Timestep; use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep};
use crate::{ use crate::{
assets, settings, assets,
tab::{ tab::{
Tab, TabParams, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, settings::TabSettings,
settings::TabSettings, Tab, TabType,
}, },
task::Tasks,
util::{ util::{
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams}, popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
toast_manager::ToastManager, toast_manager::ToastManager,
various::AsyncExecutor,
}, },
views, views,
}; };
@@ -36,16 +37,19 @@ pub struct FrontendWidgets {
pub type FrontendTasks = Tasks<FrontendTask>; pub type FrontendTasks = Tasks<FrontendTask>;
pub struct Frontend { pub struct Frontend<T> {
pub layout: RcLayout, pub layout: Layout,
globals: WguiGlobals, globals: WguiGlobals,
pub settings: Box<dyn settings::SettingsIO>, pub interface: BoxDashInterface<T>,
// async runtime executor
pub executor: AsyncExecutor,
#[allow(dead_code)] #[allow(dead_code)]
state: ParserState, state: ParserState,
current_tab: Option<Box<dyn Tab>>, current_tab: Option<Box<dyn Tab<T>>>,
pub tasks: FrontendTasks, pub tasks: FrontendTasks,
@@ -55,16 +59,34 @@ pub struct Frontend {
popup_manager: PopupManager, popup_manager: PopupManager,
toast_manager: ToastManager, toast_manager: ToastManager,
timestep: Timestep, timestep: Timestep,
sounds_to_play: Vec<SoundType>,
window_audio_settings: WguiWindow, window_audio_settings: WguiWindow,
view_audio_settings: Option<views::audio_settings::View>, view_audio_settings: Option<views::audio_settings::View>,
} }
pub struct InitParams { pub struct FrontendUpdateParams<'a, T> {
pub settings: Box<dyn settings::SettingsIO>, pub data: &'a mut T,
pub width: f32,
pub height: f32,
pub timestep_alpha: f32,
} }
pub type RcFrontend = Rc<RefCell<Frontend>>; pub struct FrontendUpdateResult {
pub layout_result: LayoutUpdateResult,
pub sounds_to_play: Vec<SoundType>,
}
pub struct InitParams<T> {
pub interface: BoxDashInterface<T>,
pub has_monado: bool,
}
#[derive(Clone)]
pub enum SoundType {
Startup,
Launch,
}
#[derive(Clone)] #[derive(Clone)]
pub enum FrontendTask { pub enum FrontendTask {
@@ -77,10 +99,11 @@ pub enum FrontendTask {
UpdateAudioSettingsView, UpdateAudioSettingsView,
RecenterPlayspace, RecenterPlayspace,
PushToast(Translation), PushToast(Translation),
PlaySound(SoundType),
} }
impl Frontend { impl<T: 'static> Frontend<T> {
pub fn new(params: InitParams) -> anyhow::Result<(RcFrontend, RcLayout)> { pub fn new(params: InitParams<T>, data: &mut T) -> anyhow::Result<Frontend<T>> {
let mut assets = Box::new(assets::Asset {}); let mut assets = Box::new(assets::Asset {});
let font_binary_bold = assets.load_from_path_gzip("Quicksand-Bold.ttf.gz")?; let font_binary_bold = assets.load_from_path_gzip("Quicksand-Bold.ttf.gz")?;
@@ -115,19 +138,16 @@ impl Frontend {
let toast_manager = ToastManager::new(); let toast_manager = ToastManager::new();
let rc_layout = layout.as_rc();
let tasks = FrontendTasks::new(); let tasks = FrontendTasks::new();
tasks.push(FrontendTask::SetTab(TabType::Home)); tasks.push(FrontendTask::SetTab(TabType::Home));
let id_label_time = state.get_widget_id("label_time")?; let id_label_time = state.get_widget_id("label_time")?;
let id_rect_content = state.get_widget_id("rect_content")?; let id_rect_content = state.get_widget_id("rect_content")?;
let mut timestep = Timestep::new(); let timestep = Timestep::new(60.0);
timestep.set_tps(30.0); // 30 ticks per second
let frontend = Self { let mut frontend = Self {
layout: rc_layout.clone(), layout,
state, state,
current_tab: None, current_tab: None,
globals, globals,
@@ -138,60 +158,101 @@ impl Frontend {
id_rect_content, id_rect_content,
}, },
timestep, timestep,
settings: params.settings, interface: params.interface,
popup_manager, popup_manager,
toast_manager, toast_manager,
window_audio_settings: WguiWindow::default(), window_audio_settings: WguiWindow::default(),
view_audio_settings: None, view_audio_settings: None,
executor: Rc::new(smol::LocalExecutor::new()),
sounds_to_play: Vec::new(),
}; };
// init some things first // init some things first
frontend.update_background()?; frontend.update_background(data)?;
frontend.update_time()?; frontend.update_time(data)?;
let res = Rc::new(RefCell::new(frontend)); Frontend::register_widgets(&mut frontend)?;
Frontend::register_widgets(&res)?; Ok(frontend)
Ok((res, rc_layout))
} }
pub fn update(&mut self, rc_this: &RcFrontend, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> { fn queue_play_sound(&mut self, sound_type: SoundType) {
self.sounds_to_play.push(sound_type);
}
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 {
SoundType::Startup => "sound/startup.mp3",
SoundType::Launch => "sound/app_start.mp3",
})?)?;
audio_system.play_sample(&sample);
Ok(())
}
pub fn update(&mut self, mut params: FrontendUpdateParams<T>) -> anyhow::Result<FrontendUpdateResult> {
let mut tasks = self.tasks.drain(); let mut tasks = self.tasks.drain();
while let Some(task) = tasks.pop_front() { while let Some(task) = tasks.pop_front() {
self.process_task(rc_this, task)?; self.process_task(&mut params, task)?;
} }
self.tick(width, height, timestep_alpha)?; if let Some(mut tab) = self.current_tab.take() {
tab.update(self, params.data)?;
self.current_tab = Some(tab);
}
// process async runtime tasks
while self.executor.try_tick() {}
let res = self.tick(params)?;
self.ticks += 1; self.ticks += 1;
Ok(res)
}
pub fn process_update(
&mut self,
res: FrontendUpdateResult,
audio_system: &mut audio::AudioSystem,
audio_sample_player: &mut audio::SamplePlayer,
) -> anyhow::Result<()> {
for sound_type in res.sounds_to_play {
self.play_sound(audio_system, sound_type)?;
}
audio_sample_player.play_wgui_samples(audio_system, res.layout_result.sounds_to_play);
Ok(()) Ok(())
} }
fn tick(&mut self, width: f32, height: f32, timestep_alpha: f32) -> anyhow::Result<()> { fn tick(&mut self, params: FrontendUpdateParams<T>) -> anyhow::Result<FrontendUpdateResult> {
// fixme: timer events instead of this thing // fixme: timer events instead of this thing
if self.ticks.is_multiple_of(1000) { if self.ticks.is_multiple_of(1000) {
self.update_time()?; self.update_time(params.data)?;
} }
{ {
let mut layout = self.layout.borrow_mut();
// always 30 times per second // always 30 times per second
while self.timestep.on_tick() { while self.timestep.on_tick() {
self.toast_manager.tick(&self.globals, &mut layout)?; self.toast_manager.tick(&self.globals, &mut self.layout)?;
} }
layout.update(Vec2::new(width, height), timestep_alpha)?;
} }
Ok(()) let layout_result = self.layout.update(&mut LayoutUpdateParams {
size: Vec2::new(params.width, params.height),
timestep_alpha: params.timestep_alpha,
})?;
Ok(FrontendUpdateResult {
layout_result,
sounds_to_play: std::mem::take(&mut self.sounds_to_play),
})
} }
fn update_time(&self) -> anyhow::Result<()> { fn update_time(&mut self, data: &mut T) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut(); let mut c = self.layout.start_common();
let mut c = layout.start_common();
let mut common = c.common(); let mut common = c.common();
{ {
@@ -203,12 +264,12 @@ impl Frontend {
let hours = now.hour(); let hours = now.hour();
let minutes = now.minute(); let minutes = now.minute();
let text: String = if !self.settings.get().general.am_pm_clock { let text: String = if self.interface.general_config(data).clock_12h {
format!("{hours:02}:{minutes:02}")
} else {
let hours_ampm = (hours + 11) % 12 + 1; let hours_ampm = (hours + 11) % 12 + 1;
let suffix = if hours >= 12 { "PM" } else { "AM" }; let suffix = if hours >= 12 { "PM" } else { "AM" };
format!("{hours_ampm:02}:{minutes:02} {suffix}") format!("{hours_ampm:02}:{minutes:02} {suffix}")
} else {
format!("{hours:02}:{minutes:02}")
}; };
label.set_text(&mut common, Translation::from_raw_text(&text)); label.set_text(&mut common, Translation::from_raw_text(&text));
@@ -218,26 +279,29 @@ impl Frontend {
Ok(()) Ok(())
} }
fn mount_popup(&mut self, params: MountPopupParams) -> anyhow::Result<()> { fn mount_popup(&mut self, params: MountPopupParams, data: &mut T) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut(); let config = self.interface.general_config(data);
self
.popup_manager self.popup_manager.mount_popup(
.mount_popup(self.globals.clone(), &mut layout, self.tasks.clone(), params)?; self.globals.clone(),
&mut self.layout,
self.tasks.clone(),
params,
config,
)?;
Ok(()) Ok(())
} }
fn refresh_popup_manager(&mut self) -> anyhow::Result<()> { fn refresh_popup_manager(&mut self) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut(); let mut c = self.layout.start_common();
let mut c = layout.start_common();
self.popup_manager.refresh(c.common().alterables); self.popup_manager.refresh(c.common().alterables);
c.finish()?; c.finish()?;
Ok(()) Ok(())
} }
fn update_background(&self) -> anyhow::Result<()> { fn update_background(&mut self, data: &mut T) -> anyhow::Result<()> {
let layout = self.layout.borrow_mut(); let Some(mut rect) = self
.layout
let Some(mut rect) = layout
.state .state
.widgets .widgets
.get_as::<WidgetRectangle>(self.widgets.id_rect_content) .get_as::<WidgetRectangle>(self.widgets.id_rect_content)
@@ -245,10 +309,10 @@ impl Frontend {
anyhow::bail!(""); anyhow::bail!("");
}; };
let (alpha1, alpha2) = if !self.settings.get().general.opaque_background { let (alpha1, alpha2) = if self.interface.general_config(data).opaque_background {
(0.8666, 0.9333)
} else {
(1.0, 1.0) (1.0, 1.0)
} else {
(0.8666, 0.9333)
}; };
rect.params.color.a = alpha1; rect.params.color.a = alpha1;
@@ -257,47 +321,34 @@ impl Frontend {
Ok(()) Ok(())
} }
pub fn get_layout(&self) -> &RcLayout { fn process_task(&mut self, params: &mut FrontendUpdateParams<T>, task: FrontendTask) -> anyhow::Result<()> {
&self.layout
}
fn process_task(&mut self, rc_this: &RcFrontend, task: FrontendTask) -> anyhow::Result<()> {
match task { match task {
FrontendTask::SetTab(tab_type) => self.set_tab(tab_type, rc_this)?, FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?,
FrontendTask::RefreshClock => self.update_time()?, FrontendTask::RefreshClock => self.update_time(params.data)?,
FrontendTask::RefreshBackground => self.update_background()?, FrontendTask::RefreshBackground => self.update_background(params.data)?,
FrontendTask::MountPopup(params) => self.mount_popup(params)?, FrontendTask::MountPopup(popup_params) => self.mount_popup(popup_params, params.data)?,
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?, FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?, FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?, FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
FrontendTask::RecenterPlayspace => self.action_recenter_playspace()?, FrontendTask::RecenterPlayspace => self.action_recenter_playspace(params.data)?,
FrontendTask::PushToast(content) => self.toast_manager.push(content), FrontendTask::PushToast(content) => self.toast_manager.push(content),
FrontendTask::PlaySound(sound_type) => self.queue_play_sound(sound_type),
}; };
Ok(()) Ok(())
} }
fn set_tab(&mut self, tab_type: TabType, rc_this: &RcFrontend) -> anyhow::Result<()> { fn set_tab(&mut self, data: &mut T, tab_type: TabType) -> anyhow::Result<()> {
log::info!("Setting tab to {tab_type:?}"); log::info!("Setting tab to {tab_type:?}");
let mut layout = self.layout.borrow_mut(); let widget_content = self.state.fetch_widget(&self.layout.state, "content")?;
let widget_content = self.state.fetch_widget(&layout.state, "content")?; self.layout.remove_children(widget_content.id);
layout.remove_children(widget_content.id);
let tab_params = TabParams { let tab: Box<dyn Tab<T>> = match tab_type {
globals: &self.globals, TabType::Home => Box::new(TabHome::new(self, widget_content.id, data)?),
layout: &mut layout, TabType::Apps => Box::new(TabApps::new(self, widget_content.id, data)?),
parent_id: widget_content.id, TabType::Games => Box::new(TabGames::new(self, widget_content.id)?),
frontend: rc_this, TabType::Monado => Box::new(TabMonado::new(self, widget_content.id)?),
//frontend_widgets: &self.widgets, TabType::Processes => Box::new(TabProcesses::new(self, widget_content.id)?),
settings: self.settings.get_mut(), TabType::Settings => Box::new(TabSettings::new(self, widget_content.id, data)?),
};
let tab: Box<dyn Tab> = match tab_type {
TabType::Home => Box::new(TabHome::new(tab_params)?),
TabType::Apps => Box::new(TabApps::new(tab_params)?),
TabType::Games => Box::new(TabGames::new(tab_params)?),
TabType::Monado => Box::new(TabMonado::new(tab_params)?),
TabType::Processes => Box::new(TabProcesses::new(tab_params)?),
TabType::Settings => Box::new(TabSettings::new(tab_params)?),
}; };
self.current_tab = Some(tab); self.current_tab = Some(tab);
@@ -305,61 +356,44 @@ impl Frontend {
Ok(()) Ok(())
} }
pub fn register_button_task(this_rc: RcFrontend, btn: &Rc<ComponentButton>, task: FrontendTask) { fn register_widgets(&mut self) -> anyhow::Result<()> {
btn.on_click({
Box::new(move |_common, _evt| {
this_rc.borrow_mut().tasks.push(task.clone());
Ok(())
})
});
}
fn register_widgets(rc_this: &RcFrontend) -> anyhow::Result<()> {
let this = rc_this.borrow_mut();
// ################################ // ################################
// SIDE BUTTONS // SIDE BUTTONS
// ################################ // ################################
// "Home" side button // "Home" side button
Frontend::register_button_task( self.tasks.handle_button(
rc_this.clone(), &self.state.fetch_component_as::<ComponentButton>("btn_side_home")?,
&this.state.fetch_component_as::<ComponentButton>("btn_side_home")?,
FrontendTask::SetTab(TabType::Home), FrontendTask::SetTab(TabType::Home),
); );
// "Apps" side button // "Apps" side button
Frontend::register_button_task( self.tasks.handle_button(
rc_this.clone(), &self.state.fetch_component_as::<ComponentButton>("btn_side_apps")?,
&this.state.fetch_component_as::<ComponentButton>("btn_side_apps")?,
FrontendTask::SetTab(TabType::Apps), FrontendTask::SetTab(TabType::Apps),
); );
// "Games" side button // "Games" side button
Frontend::register_button_task( self.tasks.handle_button(
rc_this.clone(), &self.state.fetch_component_as::<ComponentButton>("btn_side_games")?,
&this.state.fetch_component_as::<ComponentButton>("btn_side_games")?,
FrontendTask::SetTab(TabType::Games), FrontendTask::SetTab(TabType::Games),
); );
// "Monado side button" // "Monado side button"
Frontend::register_button_task( self.tasks.handle_button(
rc_this.clone(), &self.state.fetch_component_as::<ComponentButton>("btn_side_monado")?,
&this.state.fetch_component_as::<ComponentButton>("btn_side_monado")?,
FrontendTask::SetTab(TabType::Monado), FrontendTask::SetTab(TabType::Monado),
); );
// "Processes" side button // "Processes" side button
Frontend::register_button_task( self.tasks.handle_button(
rc_this.clone(), &self.state.fetch_component_as::<ComponentButton>("btn_side_processes")?,
&this.state.fetch_component_as::<ComponentButton>("btn_side_processes")?,
FrontendTask::SetTab(TabType::Processes), FrontendTask::SetTab(TabType::Processes),
); );
// "Settings" side button // "Settings" side button
Frontend::register_button_task( self.tasks.handle_button(
rc_this.clone(), &self.state.fetch_component_as::<ComponentButton>("btn_side_settings")?,
&this.state.fetch_component_as::<ComponentButton>("btn_side_settings")?,
FrontendTask::SetTab(TabType::Settings), FrontendTask::SetTab(TabType::Settings),
); );
@@ -368,16 +402,14 @@ impl Frontend {
// ################################ // ################################
// "Audio" bottom bar button // "Audio" bottom bar button
Frontend::register_button_task( self.tasks.handle_button(
rc_this.clone(), &self.state.fetch_component_as::<ComponentButton>("btn_audio")?,
&this.state.fetch_component_as::<ComponentButton>("btn_audio")?,
FrontendTask::ShowAudioSettings, FrontendTask::ShowAudioSettings,
); );
// "Recenter playspace" bottom bar button // "Recenter playspace" bottom bar button
Frontend::register_button_task( self.tasks.handle_button(
rc_this.clone(), &self.state.fetch_component_as::<ComponentButton>("btn_recenter")?,
&this.state.fetch_component_as::<ComponentButton>("btn_recenter")?,
FrontendTask::RecenterPlayspace, FrontendTask::RecenterPlayspace,
); );
@@ -385,16 +417,15 @@ impl Frontend {
} }
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> { fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut();
self.window_audio_settings.open(&mut WguiWindowParams { self.window_audio_settings.open(&mut WguiWindowParams {
globals: self.globals.clone(), globals: &self.globals,
position: Vec2::new(64.0, 64.0), position: Vec2::new(64.0, 64.0),
layout: &mut layout, layout: &mut self.layout,
title: Translation::from_translation_key("AUDIO.SETTINGS"),
extra: WguiWindowParamsExtra { extra: WguiWindowParamsExtra {
fixed_width: Some(400.0), fixed_width: Some(400.0),
placement: WguiWindowPlacement::BottomLeft, placement: WguiWindowPlacement::BottomLeft,
close_if_clicked_outside: true,
title: Some(Translation::from_translation_key("AUDIO.SETTINGS")),
..Default::default() ..Default::default()
}, },
})?; })?;
@@ -404,7 +435,7 @@ impl Frontend {
self.view_audio_settings = Some(views::audio_settings::View::new(views::audio_settings::Params { self.view_audio_settings = Some(views::audio_settings::View::new(views::audio_settings::Params {
globals: self.globals.clone(), globals: self.globals.clone(),
frontend_tasks: self.tasks.clone(), frontend_tasks: self.tasks.clone(),
layout: &mut layout, layout: &mut self.layout,
parent_id: content.id, parent_id: content.id,
on_update: { on_update: {
let tasks = self.tasks.clone(); let tasks = self.tasks.clone();
@@ -421,14 +452,13 @@ impl Frontend {
return Ok(()); return Ok(());
}; };
let mut layout = self.layout.borrow_mut(); view.update(&mut self.layout)?;
view.update(&mut layout)?;
Ok(()) Ok(())
} }
fn action_recenter_playspace(&mut self) -> anyhow::Result<()> { fn action_recenter_playspace(&mut self, data: &mut T) -> anyhow::Result<()> {
log::info!("todo"); self.interface.recenter_playspace(data)?;
Ok(()) Ok(())
} }
} }

View File

@@ -1,8 +1,6 @@
mod assets; mod assets;
pub mod frontend; pub mod frontend;
pub mod settings;
mod tab; mod tab;
mod task;
mod util; mod util;
mod various; mod various;
mod views; mod views;

View File

@@ -1,42 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize)]
pub struct HomeScreen {
pub hide_username: bool,
}
#[derive(Default, Serialize, Deserialize)]
pub struct General {
pub am_pm_clock: bool,
pub opaque_background: bool,
}
#[derive(Default, Serialize, Deserialize)]
pub struct Tweaks {
pub xwayland_by_default: bool,
}
#[derive(Default, Serialize, Deserialize)]
pub struct Settings {
pub home_screen: HomeScreen,
pub general: General,
pub tweaks: Tweaks,
}
impl Settings {
pub fn save(&self) -> String {
serde_json::to_string_pretty(&self).unwrap() /* want panic */
}
pub fn load(input: &str) -> anyhow::Result<Settings> {
Ok(serde_json::from_str::<Settings>(input)?)
}
}
pub trait SettingsIO {
fn get_mut(&mut self) -> &mut Settings;
fn get(&self) -> &Settings;
fn save_to_disk(&mut self);
fn read_from_disk(&mut self);
fn mark_as_dirty(&mut self);
}

View File

@@ -1,181 +1,360 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc}; use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
marker::PhantomData,
rc::Rc,
};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::{ButtonClickCallback, ComponentButton}, components::button::{ButtonClickCallback, ComponentButton},
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::WidgetPair, layout::{WidgetID, WidgetPair},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
}; };
use wlx_common::desktop_finder::DesktopEntry;
use crate::{ use crate::{
frontend::{FrontendTask, RcFrontend}, frontend::{Frontend, FrontendTask, FrontendTasks},
tab::{Tab, TabParams, TabType}, tab::{Tab, TabType},
util::{ util::popup_manager::{MountPopupParams, PopupHandle},
self,
desktop_finder::DesktopEntry,
popup_manager::{MountPopupParams, PopupHandle},
},
views::{self, app_launcher}, views::{self, app_launcher},
}; };
enum Task {
CloseLauncher,
}
struct State { struct State {
launcher: Option<(PopupHandle, views::app_launcher::View)>, view_launcher: Option<(PopupHandle, views::app_launcher::View)>,
} }
pub struct TabApps { pub struct TabApps<T> {
#[allow(dead_code)] #[allow(dead_code)]
pub parser_state: ParserState, parser_state: ParserState,
#[allow(dead_code)]
state: Rc<RefCell<State>>, state: Rc<RefCell<State>>,
#[allow(dead_code)]
entries: Vec<DesktopEntry>,
#[allow(dead_code)]
app_list: AppList, app_list: AppList,
tasks: Tasks<Task>,
marker: PhantomData<T>,
} }
impl Tab for TabApps { impl<T> Tab<T> for TabApps<T> {
fn get_type(&self) -> TabType { fn get_type(&self) -> TabType {
TabType::Apps TabType::Apps
} }
}
#[derive(Default)] fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
struct AppList { let mut state = self.state.borrow_mut();
//data: Vec<ParserData>,
}
// called after the user clicks any desktop entry for task in self.tasks.drain() {
fn on_app_click( match task {
frontend: RcFrontend, Task::CloseLauncher => state.view_launcher = None,
globals: WguiGlobals, }
entry: DesktopEntry, }
state: Rc<RefCell<State>>,
) -> ButtonClickCallback {
Box::new(move |_common, _evt| {
frontend
.borrow_mut()
.tasks
.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_raw_text(&entry.app_name),
on_content: {
let state = state.clone();
let entry = entry.clone();
let globals = globals.clone();
Rc::new(move |data| {
let view = app_launcher::View::new(app_launcher::Params {
entry: entry.clone(),
globals: globals.clone(),
layout: data.layout,
parent_id: data.id_content,
})?;
state.borrow_mut().launcher = Some((data.handle, view)); self
Ok(()) .app_list
}) .tick(frontend, &self.state, &self.tasks, &mut self.parser_state)?;
},
}));
Ok(())
})
}
impl TabApps { if let Some((_, launcher)) = &mut state.view_launcher {
pub fn new(mut tab_params: TabParams) -> anyhow::Result<Self> { launcher.update(&mut frontend.interface, data)?;
let doc_params = &ParseDocumentParams {
globals: tab_params.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/apps.xml"),
extra: Default::default(),
};
gtk::init()?;
let entries = util::desktop_finder::find_entries()?;
let frontend = tab_params.frontend.clone();
let globals = tab_params.globals.clone();
let state = Rc::new(RefCell::new(State { launcher: None }));
let mut parser_state = wgui::parser::parse_from_assets(doc_params, tab_params.layout, tab_params.parent_id)?;
let app_list_parent = parser_state.fetch_widget(&tab_params.layout.state, "app_list_parent")?;
let mut app_list = AppList::default();
app_list.mount_entries(
&entries,
&mut parser_state,
doc_params,
&mut tab_params,
&app_list_parent,
|button, entry| {
// Set up the click handler for the app button
button.on_click(on_app_click(
frontend.clone(),
globals.clone(),
entry.clone(),
state.clone(),
));
},
)?;
Ok(Self {
app_list,
parser_state,
entries,
state,
})
}
}
impl AppList {
fn mount_entry(
&mut self,
parser_state: &mut ParserState,
doc_params: &ParseDocumentParams,
params: &mut TabParams,
list_parent: &WidgetPair,
entry: &DesktopEntry,
) -> anyhow::Result<Rc<ComponentButton>> {
let mut template_params = HashMap::new();
// entry icon
template_params.insert(
Rc::from("src_ext"),
entry
.icon_path
.as_ref()
.map_or_else(|| Rc::from(""), |icon_path| Rc::from(icon_path.as_str())),
);
// entry fallback (question mark) icon
template_params.insert(
Rc::from("src"),
if entry.icon_path.is_none() {
Rc::from("dashboard/terminal.svg")
} else {
Rc::from("")
},
);
template_params.insert(Rc::from("name"), Rc::from(entry.app_name.as_str()));
let data = parser_state.parse_template(doc_params, "AppEntry", params.layout, list_parent.id, template_params)?;
data.fetch_component_as::<ComponentButton>("button")
}
fn mount_entries(
&mut self,
entries: &[DesktopEntry],
parser_state: &mut ParserState,
doc_params: &ParseDocumentParams,
params: &mut TabParams,
list_parent: &WidgetPair,
on_button: impl Fn(Rc<ComponentButton>, &DesktopEntry),
) -> anyhow::Result<()> {
for entry in entries {
let button = self.mount_entry(parser_state, doc_params, params, list_parent, entry)?;
on_button(button, entry);
} }
Ok(()) Ok(())
} }
} }
struct AppList {
//data: Vec<ParserData>,
entries_to_mount: VecDeque<DesktopEntry>,
list_parent: WidgetPair,
prev_category_name: String,
}
// called after the user clicks any desktop entry
fn on_app_click(
frontend_tasks: FrontendTasks,
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(())
})
},
}));
Ok(())
})
}
fn doc_params(globals: WguiGlobals) -> ParseDocumentParams<'static> {
ParseDocumentParams {
globals,
path: AssetPath::BuiltIn("gui/tab/apps.xml"),
extra: Default::default(),
}
}
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 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")?;
let mut entries_sorted: Vec<_> = frontend
.interface
.desktop_finder(data)
.find_entries()
.into_values()
.collect();
entries_sorted.sort_by(|a, b| {
let cat_name_a = get_category_name(a);
let cat_name_b = get_category_name(b);
cat_name_a.cmp(cat_name_b)
});
let app_list = AppList {
entries_to_mount: entries_sorted.drain(..).collect(),
list_parent: app_list_parent,
prev_category_name: String::new(),
};
Ok(Self {
app_list,
parser_state,
state,
tasks,
marker: PhantomData,
})
}
}
enum Scores {
Empty,
Unknown,
XFooBar, // X-something
Xfce,
Gnome,
Kde,
Gtk,
Qt,
Settings,
Application,
System,
Utility,
FileTools,
Filesystem,
FileManager,
Graphics,
Office,
Game,
VR, // best score (of course!)
}
fn get_category_name_score(name: &str) -> u8 {
if name.starts_with("X-") {
return Scores::XFooBar as u8;
}
match name {
"" => {
return Scores::Empty as u8;
}
"VR" => {
return Scores::VR as u8;
}
"Game" => {
return Scores::Game as u8;
}
"FileManager" => {
return Scores::FileManager as u8;
}
"Utility" => {
return Scores::Utility as u8;
}
"FileTools" => {
return Scores::FileTools as u8;
}
"Filesystem" => {
return Scores::Filesystem as u8;
}
"System" => {
return Scores::System as u8;
}
"Office" => {
return Scores::Office as u8;
}
"Settings" => {
return Scores::Settings as u8;
}
"Application" => {
return Scores::Application as u8;
}
"GTK" => {
return Scores::Gtk as u8;
}
"Qt" => {
return Scores::Qt as u8;
}
"XFCE" => {
return Scores::Xfce as u8;
}
"GNOME" => {
return Scores::Gnome as u8;
}
"KDE" => {
return Scores::Kde as u8;
}
"Graphics" => {
return Scores::Graphics as u8;
}
_ => {}
}
Scores::Unknown as u8
}
fn get_best_category_name(categories: &[Rc<str>]) -> Option<&Rc<str>> {
let mut best_score: u8 = 0;
let mut best_category: Option<&Rc<str>> = None;
for cat in categories {
let score = get_category_name_score(cat);
if score > best_score {
best_category = Some(cat);
best_score = score;
}
}
best_category
}
fn get_category_name(entry: &DesktopEntry) -> &str {
//log::info!("{:?}", entry.categories);
match get_best_category_name(&entry.categories) {
Some(cat) => cat,
None => "Other",
}
}
impl AppList {
fn mount_entry<T>(
&mut self,
frontend: &mut Frontend<T>,
parser_state: &mut ParserState,
doc_params: &ParseDocumentParams,
entry: &DesktopEntry,
) -> anyhow::Result<Rc<ComponentButton>> {
let category_name = get_category_name(entry);
if category_name != self.prev_category_name {
self.prev_category_name = String::from(category_name);
let mut params = HashMap::<Rc<str>, Rc<str>>::new();
params.insert("text".into(), category_name.into());
parser_state.parse_template(
doc_params,
"CategoryText",
&mut frontend.layout,
self.list_parent.id,
params,
)?;
}
{
let mut params = HashMap::new();
// entry icon
params.insert(
"src_ext".into(),
entry
.icon_path
.as_ref()
.map_or_else(|| "".into(), |icon_path| icon_path.clone()),
);
// entry fallback (question mark) icon
params.insert(
"src".into(),
if entry.icon_path.is_none() {
"dashboard/terminal.svg".into()
} else {
"".into()
},
);
params.insert("name".into(), entry.app_name.clone());
let data = parser_state.parse_template(
doc_params,
"AppEntry",
&mut frontend.layout,
self.list_parent.id,
params,
)?;
data.fetch_component_as::<ComponentButton>("button")
}
}
fn tick<T>(
&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 {
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)?;
button.on_click(on_app_click(
frontend.tasks.clone(),
globals.clone(),
entry.clone(),
state.clone(),
tasks.clone(),
));
} else {
break;
}
}
Ok(())
}
}

View File

@@ -1,33 +1,62 @@
use std::marker::PhantomData;
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
parser::{ParseDocumentParams, ParserState}, layout::WidgetID,
parser::{Fetchable, ParseDocumentParams, ParserState},
}; };
use crate::tab::{Tab, TabParams, TabType}; use crate::{
frontend::Frontend,
tab::{Tab, TabType},
views::game_list,
};
pub struct TabGames { pub struct TabGames<T> {
#[allow(dead_code)] #[allow(dead_code)]
pub state: ParserState, pub state: ParserState,
view_game_list: game_list::View,
marker: PhantomData<T>,
} }
impl Tab for TabGames { impl<T> Tab<T> for TabGames<T> {
fn get_type(&self) -> TabType { fn get_type(&self) -> TabType {
TabType::Games 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)?;
Ok(())
}
} }
impl TabGames { impl<T> TabGames<T> {
pub fn new(params: TabParams) -> anyhow::Result<Self> { pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets( let state = wgui::parser::parse_from_assets(
&ParseDocumentParams { &ParseDocumentParams {
globals: params.globals.clone(), globals: frontend.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/games.xml"), path: AssetPath::BuiltIn("gui/tab/games.xml"),
extra: Default::default(), extra: Default::default(),
}, },
params.layout, &mut frontend.layout,
params.parent_id, parent_id,
)?; )?;
Ok(Self { state }) let game_list_parent = state.get_widget_id("game_list_parent")?;
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(),
layout: &mut frontend.layout,
parent_id: game_list_parent,
})?;
Ok(Self {
state,
view_game_list,
marker: PhantomData,
})
} }
} }

View File

@@ -1,32 +1,35 @@
use std::marker::PhantomData;
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton, components::button::ComponentButton,
event::CallbackDataCommon, event::CallbackDataCommon,
i18n::Translation, i18n::Translation,
layout::Widget, layout::{Widget, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
widget::label::WidgetLabel, widget::label::WidgetLabel,
}; };
use wlx_common::config::GeneralConfig;
use crate::{ use crate::{
frontend::{Frontend, FrontendTask}, frontend::{Frontend, FrontendTask},
settings, tab::{Tab, TabType},
tab::{Tab, TabParams, TabType},
various, various,
}; };
pub struct TabHome { pub struct TabHome<T> {
#[allow(dead_code)] #[allow(dead_code)]
pub state: ParserState, pub state: ParserState,
marker: PhantomData<T>,
} }
impl Tab for TabHome { impl<T> Tab<T> for TabHome<T> {
fn get_type(&self) -> TabType { fn get_type(&self) -> TabType {
TabType::Home TabType::Home
} }
} }
fn configure_label_hello(common: &mut CallbackDataCommon, label_hello: Widget, settings: &settings::Settings) { fn configure_label_hello(common: &mut CallbackDataCommon, label_hello: Widget, config: &GeneralConfig) {
let mut username = various::get_username(); let mut username = various::get_username();
// first character as uppercase // first character as uppercase
if let Some(first) = username.chars().next() { if let Some(first) = username.chars().next() {
@@ -34,31 +37,31 @@ fn configure_label_hello(common: &mut CallbackDataCommon, label_hello: Widget, s
username.replace_range(0..1, &first); username.replace_range(0..1, &first);
} }
let translated = if !settings.home_screen.hide_username { let translated = if !config.hide_username {
common.i18n().translate_and_replace("HELLO_USER", ("{USER}", &username)) common.i18n().translate_and_replace("HELLO_USER", ("{USER}", &username))
} else { } else {
common.i18n().translate("HELLO").to_string() common.i18n().translate("HELLO").to_string()
}; };
let mut label_hello = label_hello.get_as_mut::<WidgetLabel>().unwrap(); let mut label_hello = label_hello.get_as::<WidgetLabel>().unwrap();
label_hello.set_text(common, Translation::from_raw_text(&translated)); label_hello.set_text(common, Translation::from_raw_text(&translated));
} }
impl TabHome { impl<T> TabHome<T> {
pub fn new(params: TabParams) -> anyhow::Result<Self> { pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets( let state = wgui::parser::parse_from_assets(
&ParseDocumentParams { &ParseDocumentParams {
globals: params.globals.clone(), globals: frontend.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/home.xml"), path: AssetPath::BuiltIn("gui/tab/home.xml"),
extra: Default::default(), extra: Default::default(),
}, },
params.layout, &mut frontend.layout,
params.parent_id, parent_id,
)?; )?;
let mut c = params.layout.start_common(); let mut c = frontend.layout.start_common();
let widget_label = state.fetch_widget(&c.layout.state, "label_hello")?.widget; let widget_label = state.fetch_widget(&c.layout.state, "label_hello")?.widget;
configure_label_hello(&mut c.common(), widget_label, params.settings); configure_label_hello(&mut c.common(), widget_label, frontend.interface.general_config(data));
let btn_apps = state.fetch_component_as::<ComponentButton>("btn_apps")?; let btn_apps = state.fetch_component_as::<ComponentButton>("btn_apps")?;
let btn_games = state.fetch_component_as::<ComponentButton>("btn_games")?; let btn_games = state.fetch_component_as::<ComponentButton>("btn_games")?;
@@ -66,17 +69,16 @@ impl TabHome {
let btn_processes = state.fetch_component_as::<ComponentButton>("btn_processes")?; let btn_processes = state.fetch_component_as::<ComponentButton>("btn_processes")?;
let btn_settings = state.fetch_component_as::<ComponentButton>("btn_settings")?; let btn_settings = state.fetch_component_as::<ComponentButton>("btn_settings")?;
let frontend = params.frontend; let tasks = &mut frontend.tasks;
Frontend::register_button_task(frontend.clone(), &btn_apps, FrontendTask::SetTab(TabType::Apps)); tasks.handle_button(&btn_apps, FrontendTask::SetTab(TabType::Apps));
Frontend::register_button_task(frontend.clone(), &btn_games, FrontendTask::SetTab(TabType::Games)); tasks.handle_button(&btn_games, FrontendTask::SetTab(TabType::Games));
Frontend::register_button_task(frontend.clone(), &btn_monado, FrontendTask::SetTab(TabType::Monado)); tasks.handle_button(&btn_monado, FrontendTask::SetTab(TabType::Monado));
Frontend::register_button_task( tasks.handle_button(&btn_processes, FrontendTask::SetTab(TabType::Processes));
frontend.clone(), tasks.handle_button(&btn_settings, FrontendTask::SetTab(TabType::Settings));
&btn_processes,
FrontendTask::SetTab(TabType::Processes),
);
Frontend::register_button_task(frontend.clone(), &btn_settings, FrontendTask::SetTab(TabType::Settings));
Ok(Self { state }) Ok(Self {
state,
marker: PhantomData,
})
} }
} }

View File

@@ -1,9 +1,4 @@
use wgui::{ use crate::frontend::Frontend;
globals::WguiGlobals,
layout::{Layout, WidgetID},
};
use crate::frontend::RcFrontend;
pub mod apps; pub mod apps;
pub mod games; pub mod games;
@@ -22,15 +17,11 @@ pub enum TabType {
Settings, Settings,
} }
pub struct TabParams<'a> { pub trait Tab<T> {
pub globals: &'a WguiGlobals,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub frontend: &'a RcFrontend,
pub settings: &'a mut crate::settings::Settings,
}
pub trait Tab {
#[allow(dead_code)] #[allow(dead_code)]
fn get_type(&self) -> TabType; fn get_type(&self) -> TabType;
fn update(&mut self, _: &mut Frontend<T>, _: &mut T) -> anyhow::Result<()> {
Ok(())
}
} }

View File

@@ -1,33 +1,185 @@
use std::{collections::HashMap, marker::PhantomData, rc::Rc};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
parser::{ParseDocumentParams, ParserState}, components::{checkbox::ComponentCheckbox, slider::ComponentSlider},
globals::WguiGlobals,
layout::WidgetID,
parser::{self, Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
};
use wlx_common::dash_interface;
use crate::{
frontend::Frontend,
tab::{Tab, TabType},
}; };
use crate::tab::{Tab, TabParams, TabType}; #[derive(Debug)]
enum Task {
pub struct TabMonado { Refresh,
#[allow(dead_code)] FocusClient(String),
pub state: ParserState, SetBrightness(f32),
} }
impl Tab for TabMonado { pub struct TabMonado<T> {
#[allow(dead_code)]
state: ParserState,
tasks: Tasks<Task>,
marker: PhantomData<T>,
globals: WguiGlobals,
id_list_parent: WidgetID,
cells: Vec<parser::ParserData>,
ticks: u32,
}
impl<T> Tab<T> for TabMonado<T> {
fn get_type(&self) -> TabType { fn get_type(&self) -> TabType {
TabType::Games TabType::Games
} }
}
impl TabMonado { fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
pub fn new(params: TabParams) -> anyhow::Result<Self> { for task in self.tasks.drain() {
let state = wgui::parser::parse_from_assets( match task {
&ParseDocumentParams { Task::Refresh => self.refresh(frontend, data)?,
globals: params.globals.clone(), Task::FocusClient(name) => self.focus_client(frontend, data, name)?,
path: AssetPath::BuiltIn("gui/tab/monado.xml"), Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness),
extra: Default::default(), }
}, }
params.layout,
params.parent_id,
)?;
Ok(Self { state }) // every few seconds
if self.ticks.is_multiple_of(500) {
self.tasks.push(Task::Refresh);
}
self.ticks += 1;
Ok(())
}
}
fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado.xml"),
extra: Default::default(),
}
}
fn yesno(n: bool) -> &'static str {
match n {
true => "yes",
false => "no",
}
}
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)?;
let id_list_parent = state.get_widget_id("list_parent")?;
let tasks = Tasks::<Task>::new();
tasks.push(Task::Refresh);
Ok(Self {
state,
marker: PhantomData,
tasks,
globals,
id_list_parent,
ticks: 0,
cells: Vec::new(),
})
}
fn mount_client(&mut self, frontend: &mut Frontend<T>, client: &dash_interface::MonadoClient) -> anyhow::Result<()> {
let mut par = HashMap::<Rc<str>, Rc<str>>::new();
par.insert(
"checked".into(),
if client.is_primary {
Rc::from("1")
} else {
Rc::from("0")
},
);
par.insert("name".into(), client.name.clone().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());
par.insert("flag_overlay".into(), yesno(client.is_overlay).into());
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),
"Cell",
&mut frontend.layout,
self.id_list_parent,
par,
)?;
let checkbox = state_cell.fetch_component_as::<ComponentCheckbox>("checkbox")?;
checkbox.on_toggle({
let tasks = self.tasks.clone();
let client_name = client.name.clone();
Box::new(move |_common, e| {
if e.checked {
tasks.push(Task::FocusClient(client_name.clone()));
}
Ok(())
})
});
self.cells.push(state_cell);
Ok(())
}
fn refresh(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
log::debug!("refreshing monado client list");
let clients = frontend.interface.monado_client_list(data)?;
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(())
})
});
}
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(())
}
fn set_brightness(&mut self, frontend: &mut Frontend<T>, data: &mut T, brightness: f32) {
frontend.interface.monado_brightness_set(data, brightness);
} }
} }

View File

@@ -1,33 +1,70 @@
use std::marker::PhantomData;
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
parser::{ParseDocumentParams, ParserState}, layout::WidgetID,
parser::{Fetchable, ParseDocumentParams, ParserState},
}; };
use crate::tab::{Tab, TabParams, TabType}; use crate::{
frontend::Frontend,
tab::{Tab, TabType},
views::{process_list, window_list},
};
pub struct TabProcesses { pub struct TabProcesses<T> {
#[allow(dead_code)] #[allow(dead_code)]
pub state: ParserState, pub state: ParserState,
view_window_list: window_list::View,
view_process_list: process_list::View,
marker: PhantomData<T>,
} }
impl Tab for TabProcesses { impl<T> Tab<T> for TabProcesses<T> {
fn get_type(&self) -> TabType { fn get_type(&self) -> TabType {
TabType::Games 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 TabProcesses { impl<T> TabProcesses<T> {
pub fn new(params: TabParams) -> anyhow::Result<Self> { 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( let state = wgui::parser::parse_from_assets(
&ParseDocumentParams { &ParseDocumentParams {
globals: params.globals.clone(), globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/processes.xml"), path: AssetPath::BuiltIn("gui/tab/processes.xml"),
extra: Default::default(), extra: Default::default(),
}, },
params.layout, &mut frontend.layout,
params.parent_id, parent_id,
)?; )?;
Ok(Self { state }) 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,103 +1,685 @@
use std::rc::Rc; use std::{collections::HashMap, marker::PhantomData, rc::Rc, str::FromStr};
use glam::Vec2;
use strum::{AsRefStr, EnumProperty, EnumString, VariantArray};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::checkbox::ComponentCheckbox, components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
event::{CallbackDataCommon, EventAlterables},
i18n::Translation,
layout::{Layout, WidgetID},
log::LogErr,
parser::{Fetchable, ParseDocumentParams, ParserState}, 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::{ use crate::{
frontend::{Frontend, FrontendTask}, frontend::{Frontend, FrontendTask},
settings, tab::{Tab, TabType},
tab::{Tab, TabParams, TabType},
}; };
pub struct TabSettings { enum Task {
#[allow(dead_code)] UpdateBool(SettingType, bool),
pub state: ParserState, UpdateFloat(SettingType, f32),
UpdateInt(SettingType, i32),
OpenContextMenu(Vec2, Vec<context_menu::Cell>),
ClearPipewireTokens,
ClearSavedState,
DeleteAllConfigs,
RestartSoftware,
} }
impl Tab for TabSettings { pub struct TabSettings<T> {
#[allow(dead_code)]
pub state: ParserState,
context_menu: ContextMenu,
tasks: Tasks<Task>,
marker: PhantomData<T>,
}
impl<T> Tab<T> for TabSettings<T> {
fn get_type(&self) -> TabType { fn get_type(&self) -> TabType {
TabType::Settings TabType::Settings
} }
}
fn init_setting_checkbox( fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
params: &mut TabParams, let config = frontend.interface.general_config(data);
checkbox: Rc<ComponentCheckbox>, let mut changed = false;
fetch_callback: fn(&mut settings::Settings) -> &mut bool, for task in self.tasks.drain() {
change_callback: Option<fn(&mut Frontend, bool)>, match task {
) -> anyhow::Result<()> { Task::UpdateBool(setting, n) => {
let mut c = params.layout.start_common(); setting.get_frontend_task().map(|task| frontend.tasks.push(task));
*setting.mut_bool(config) = n;
checkbox.set_checked(&mut c.common(), *fetch_callback(params.settings)); changed = true;
let rc_frontend = params.frontend.clone(); }
checkbox.on_toggle(Box::new(move |_common, e| { Task::UpdateFloat(setting, n) => {
let mut frontend = rc_frontend.borrow_mut(); setting.get_frontend_task().map(|task| frontend.tasks.push(task));
*fetch_callback(frontend.settings.get_mut()) = e.checked; *setting.mut_f32(config) = n;
changed = true;
if let Some(change_callback) = &change_callback { }
change_callback(&mut frontend, e.checked); 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),
});
}
}
} }
frontend.settings.mark_as_dirty(); // 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(()) Ok(())
})); }
}
c.finish()?;
Ok(()) #[allow(clippy::enum_variant_names)]
} #[derive(Clone, Copy, AsRefStr, EnumString)]
enum SettingType {
impl TabSettings { AnimationSpeed,
pub fn new(mut params: TabParams) -> anyhow::Result<Self> { RoundMultiplier,
let state = wgui::parser::parse_from_assets( InvertScrollDirectionX,
&ParseDocumentParams { InvertScrollDirectionY,
globals: params.globals.clone(), ScrollSpeed,
path: AssetPath::BuiltIn("gui/tab/settings.xml"), LongPressDuration,
extra: Default::default(), NotificationsEnabled,
}, NotificationsSoundEnabled,
params.layout, KeyboardSoundEnabled,
params.parent_id, UprightScreenFix,
)?; DoubleCursorFix,
SetsOnWatch,
init_setting_checkbox( HideGrabHelp,
&mut params, XrClickSensitivity,
state.data.fetch_component_as::<ComponentCheckbox>("cb_hide_username")?, XrClickSensitivityRelease,
|settings| &mut settings.home_screen.hide_username, AllowSliding,
None, ClickFreezeTimeMs,
)?; FocusFollowsMouseMode,
LeftHandedMouse,
init_setting_checkbox( BlockGameInput,
&mut params, BlockGameInputIgnoreWatch,
state.data.fetch_component_as::<ComponentCheckbox>("cb_am_pm_clock")?, SpaceDragMultiplier,
|settings| &mut settings.general.am_pm_clock, UseSkybox,
Some(|frontend, _| { UsePassthrough,
frontend.tasks.push(FrontendTask::RefreshClock); ScreenRenderDown,
}), PointerLerpFactor,
)?; SpaceDragUnlocked,
SpaceRotateUnlocked,
init_setting_checkbox( Clock12h,
&mut params, HideUsername,
state OpaqueBackground,
.data XwaylandByDefault,
.fetch_component_as::<ComponentCheckbox>("cb_opaque_background")?, CaptureMethod,
|settings| &mut settings.general.opaque_background, KeyboardMiddleClick,
Some(|frontend, _| { }
frontend.tasks.push(FrontendTask::RefreshBackground);
}), impl SettingType {
)?; pub fn mut_bool<'a>(self, config: &'a mut GeneralConfig) -> &'a mut bool {
match self {
init_setting_checkbox( Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x,
&mut params, Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y,
state Self::NotificationsEnabled => &mut config.notifications_enabled,
.data Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled,
.fetch_component_as::<ComponentCheckbox>("cb_xwayland_by_default")?, Self::KeyboardSoundEnabled => &mut config.keyboard_sound_enabled,
|settings| &mut settings.tweaks.xwayland_by_default, Self::UprightScreenFix => &mut config.upright_screen_fix,
None, Self::DoubleCursorFix => &mut config.double_cursor_fix,
)?; Self::SetsOnWatch => &mut config.sets_on_watch,
Self::HideGrabHelp => &mut config.hide_grab_help,
Ok(Self { state }) 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! 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(())
}
}));
};
}
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 c = category!(mp, root, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/blocks.svg")?;
button!(
mp,
c,
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
"dashboard/display.svg",
Task::ClearPipewireTokens
);
button!(
mp,
c,
"APP_SETTINGS.CLEAR_SAVED_STATE",
"dashboard/binary.svg",
Task::ClearSavedState
);
button!(
mp,
c,
"APP_SETTINGS.DELETE_ALL_CONFIGS",
"dashboard/circle.svg",
Task::DeleteAllConfigs
);
button!(
mp,
c,
"APP_SETTINGS.RESTART_SOFTWARE",
"dashboard/refresh.svg",
Task::RestartSoftware
);
Ok(Self {
tasks: mp.tasks,
state: parser_state,
marker: PhantomData,
context_menu: ContextMenu::default(),
})
} }
} }

View File

@@ -1,27 +0,0 @@
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
#[derive(Clone)]
pub struct Tasks<TaskType>(Rc<RefCell<VecDeque<TaskType>>>)
where
TaskType: Clone;
impl<TaskType: Clone + 'static> Tasks<TaskType> {
pub fn new() -> Self {
Self(Rc::new(RefCell::new(VecDeque::new())))
}
pub fn push(&self, task: TaskType) {
self.0.borrow_mut().push_back(task);
}
pub fn drain(&mut self) -> VecDeque<TaskType> {
let mut tasks = self.0.borrow_mut();
std::mem::take(&mut *tasks)
}
}
impl<TaskType: Clone + 'static> Default for Tasks<TaskType> {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,103 @@
use anyhow::Context;
use serde::Deserialize;
use wlx_common::cache_dir;
use crate::util::{http_client, steam_utils::AppID, various::AsyncExecutor};
pub struct CoverArt {
// can be empty in case if data couldn't be fetched (use a fallback image then)
pub compressed_image_data: Vec<u8>,
}
pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Result<CoverArt> {
let cache_file_path = format!("cover_arts/{}.bin", app_id);
// check if file already exists in cache directory
if let Some(data) = cache_dir::get_data(&cache_file_path).await {
return Ok(CoverArt {
compressed_image_data: data,
});
}
let url = format!(
"https://shared.steamstatic.com/store_item_assets/steam/apps/{}/library_600x900.jpg",
app_id
);
match http_client::get(&executor, &url).await {
Ok(response) => {
log::info!("Success");
cache_dir::set_data(&cache_file_path, &response.data).await?;
Ok(CoverArt {
compressed_image_data: response.data,
})
}
Err(e) => {
// fetch failed, write an empty file
log::error!("CoverArtFetcher: failed fetch for AppID {}: {}", app_id, e);
cache_dir::set_data(&cache_file_path, &[]).await?;
Ok(CoverArt {
compressed_image_data: Vec::new(),
})
}
}
}
#[derive(Deserialize, Clone)]
pub struct AppDetailsJSONData {
#[allow(dead_code)]
pub r#type: String, // "game"
#[allow(dead_code)]
pub name: String, // "Half-Life 3"
#[allow(dead_code)]
pub is_free: Option<bool>, // "false"
pub detailed_description: Option<String>, //
pub short_description: Option<String>, //
pub developers: Vec<String>, // ["Valve"]
}
async fn get_app_details_json_internal(
executor: AsyncExecutor,
cache_file_path: &str,
app_id: AppID,
) -> anyhow::Result<AppDetailsJSONData> {
// check if file already exists in cache directory
if let Some(data) = cache_dir::get_data(cache_file_path).await {
return Ok(serde_json::from_value(serde_json::from_slice(&data)?)?);
}
// 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 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")?;
if !body.get("success").is_some_and(|v| v.as_bool().unwrap_or(false)) {
anyhow::bail!("Failed");
}
let data = body.get("data").context("data null")?;
let data_bytes = serde_json::to_vec(&data)?;
let app_details: AppDetailsJSONData = serde_json::from_value(data.clone())?;
// cache for future use
cache_dir::set_data(cache_file_path, &data_bytes).await?;
Ok(app_details)
}
pub async fn get_app_details_json(executor: AsyncExecutor, app_id: AppID) -> Option<AppDetailsJSONData> {
let cache_file_path = format!("app_details/{}.json", app_id);
match get_app_details_json_internal(executor, &cache_file_path, app_id).await {
Ok(r) => Some(r),
Err(e) => {
log::error!("Failed to get app details: {:?}", e);
let _ = cache_dir::set_data(&cache_file_path, &[]).await; // write empty data
None
}
}
}

View File

@@ -1,131 +0,0 @@
use gio::prelude::{AppInfoExt, IconExt};
use gtk::traits::IconThemeExt;
#[derive(Debug, Clone)]
#[allow(dead_code)] // TODO: remove this
pub struct DesktopEntry {
pub exec_path: String,
pub exec_args: Vec<String>,
pub app_name: String,
pub icon_path: Option<String>,
pub categories: Vec<String>,
}
#[allow(dead_code)] // TODO: remove this
pub struct EntrySearchCell {
pub exec_path: String,
pub exec_args: Vec<String>,
pub app_name: String,
pub icon_name: Option<String>,
pub categories: Vec<String>,
}
const CMD_BLACKLIST: [&str; 1] = [
"lsp-plugins", // LSP Plugins collection. They clutter the application list a lot
];
const CATEGORY_TYPE_BLACKLIST: [&str; 5] = ["GTK", "Qt", "X-XFCE", "X-Bluetooth", "ConsoleOnly"];
pub fn find_entries() -> anyhow::Result<Vec<DesktopEntry>> {
let Some(icon_theme) = gtk::IconTheme::default() else {
anyhow::bail!("Failed to get current icon theme information");
};
let mut res = Vec::<DesktopEntry>::new();
let info = gio::AppInfo::all();
log::debug!("app entry count {}", info.len());
'outer: for app_entry in info {
let Some(app_entry_id) = app_entry.id() else {
log::warn!(
"failed to get desktop entry ID for application named \"{}\"",
app_entry.name()
);
continue;
};
let Some(desktop_app) = gio::DesktopAppInfo::new(&app_entry_id) else {
log::warn!(
"failed to find desktop app file from application named \"{}\"",
app_entry.name()
);
continue;
};
if desktop_app.is_nodisplay() || desktop_app.is_hidden() {
continue;
}
let Some(cmd) = desktop_app.commandline() else {
continue;
};
let name = String::from(desktop_app.name());
let exec = String::from(cmd.to_string_lossy());
for blacklisted in CMD_BLACKLIST {
if exec.contains(blacklisted) {
continue 'outer;
}
}
let (exec_path, exec_args) = match exec.split_once(" ") {
Some((left, right)) => (
String::from(left),
right
.split(" ")
.filter(|arg| !arg.starts_with('%')) // exclude arguments like "%f"
.map(String::from)
.collect(),
),
None => (exec, Vec::new()),
};
let icon_path = match desktop_app.icon() {
Some(icon) => {
if let Some(icon_str) = icon.to_string() {
if let Some(s_icon) = icon_theme.lookup_icon(&icon_str, 128, gtk::IconLookupFlags::GENERIC_FALLBACK) {
s_icon.filename().map(|p| String::from(p.to_string_lossy()))
} else {
None
}
} else {
None
}
}
None => None,
};
let categories: Vec<String> = match desktop_app.categories() {
Some(categories) => categories
.split(";")
.filter(|s| !s.is_empty())
.filter(|s| {
for b in CATEGORY_TYPE_BLACKLIST {
if *s == b {
return false;
}
}
true
})
.map(String::from)
.collect(),
None => Vec::new(),
};
let entry = DesktopEntry {
app_name: name,
categories,
exec_path,
exec_args,
icon_path,
};
res.push(entry);
}
Ok(res)
}

View File

@@ -0,0 +1,134 @@
//
// example smol+hyper usage derived from
// https://github.com/smol-rs/smol/blob/master/examples/hyper-client.rs
// under Apache-2.0 + MIT license.
// Repository URL: https://github.com/smol-rs/smol
//
use anyhow::Context as _;
use async_native_tls::TlsStream;
use http_body_util::{BodyStream, Empty};
use hyper::Request;
use smol::{net::TcpStream, prelude::*};
use std::convert::TryInto;
use std::pin::Pin;
use std::task::{Context, Poll};
use crate::util::various::AsyncExecutor;
pub struct HttpClientResponse {
pub data: Vec<u8>,
}
pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> {
log::info!("fetching URL \"{}\"", url);
let url: hyper::Uri = url.try_into()?;
let req = Request::builder()
.header(
hyper::header::HOST,
url.authority().context("invalid authority")?.clone().as_str(),
)
.uri(url)
.body(Empty::new())?;
let resp = fetch(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())
.try_fold(Vec::new(), |mut body, chunk| {
if let Some(chunk) = chunk.data_ref() {
body.extend_from_slice(chunk);
}
Ok(body)
})
.await?;
Ok(HttpClientResponse { data: body })
}
async fn fetch(
ex: &AsyncExecutor,
req: hyper::Request<http_body_util::Empty<&'static [u8]>>,
) -> anyhow::Result<hyper::Response<hyper::body::Incoming>> {
let io = {
let host = req.uri().host().context("cannot parse host")?;
match req.uri().scheme_str() {
Some("http") => {
let stream = {
let port = req.uri().port_u16().unwrap_or(80);
smol::net::TcpStream::connect((host, port)).await?
};
SmolStream::Plain(stream)
}
Some("https") => {
// In case of HTTPS, establish a secure TLS connection first.
let stream = {
let port = req.uri().port_u16().unwrap_or(443);
smol::net::TcpStream::connect((host, port)).await?
};
let stream = async_native_tls::connect(host, stream).await?;
SmolStream::Tls(stream)
}
scheme => anyhow::bail!("unsupported scheme: {:?}", scheme),
}
};
// Spawn the HTTP/1 connection.
let (mut sender, conn) = hyper::client::conn::http1::handshake(smol_hyper::rt::FuturesIo::new(io)).await?;
ex.spawn(async move {
if let Err(e) = conn.await {
println!("Connection failed: {:?}", e);
}
})
.detach();
// Get the result
let result = sender.send_request(req).await?;
Ok(result)
}
/// A TCP or TCP+TLS connection.
enum SmolStream {
/// A plain TCP connection.
Plain(TcpStream),
/// A TCP connection secured by TLS.
Tls(TlsStream<TcpStream>),
}
impl AsyncRead for SmolStream {
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<smol::io::Result<usize>> {
match &mut *self {
SmolStream::Plain(stream) => Pin::new(stream).poll_read(cx, buf),
SmolStream::Tls(stream) => Pin::new(stream).poll_read(cx, buf),
}
}
}
impl AsyncWrite for SmolStream {
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<smol::io::Result<usize>> {
match &mut *self {
SmolStream::Plain(stream) => Pin::new(stream).poll_write(cx, buf),
SmolStream::Tls(stream) => Pin::new(stream).poll_write(cx, buf),
}
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<smol::io::Result<()>> {
match &mut *self {
SmolStream::Plain(stream) => Pin::new(stream).poll_close(cx),
SmolStream::Tls(stream) => Pin::new(stream).poll_close(cx),
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<smol::io::Result<()>> {
match &mut *self {
SmolStream::Plain(stream) => Pin::new(stream).poll_flush(cx),
SmolStream::Tls(stream) => Pin::new(stream).poll_flush(cx),
}
}
}

View File

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

View File

@@ -68,7 +68,7 @@ pub struct CardProperties {
pub device_name: String, // alsa_card.pci-0000_0c_00.4 pub device_name: String, // alsa_card.pci-0000_0c_00.4
#[serde(rename = "device.nick")] #[serde(rename = "device.nick")]
pub device_nick: String, // HD-Audio Generic pub device_nick: Option<String>, // HD-Audio Generic
} }
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize, Debug)]
@@ -296,7 +296,12 @@ pub fn list_cards() -> anyhow::Result<Vec<Card>> {
let mut cards: Vec<Card> = serde_json::from_str(json_str)?; let mut cards: Vec<Card> = serde_json::from_str(json_str)?;
// exclude card which has "Loopback" in name // exclude card which has "Loopback" in name
cards.retain(|card| card.properties.device_nick != "Loopback"); cards.retain(|card| {
if let Some(nick) = &card.properties.device_nick {
return nick != "Loopback";
};
true
});
Ok(cards) Ok(cards)
} }

View File

@@ -14,6 +14,7 @@ use wgui::{
taffy::Display, taffy::Display,
widget::label::WidgetLabel, widget::label::WidgetLabel,
}; };
use wlx_common::config::GeneralConfig;
use crate::frontend::{FrontendTask, FrontendTasks}; use crate::frontend::{FrontendTask, FrontendTasks};
@@ -55,6 +56,7 @@ pub struct PopupManager {
pub struct PopupContentFuncData<'a> { pub struct PopupContentFuncData<'a> {
pub layout: &'a mut Layout, pub layout: &'a mut Layout,
pub config: &'a GeneralConfig,
pub handle: PopupHandle, pub handle: PopupHandle,
pub id_content: WidgetID, pub id_content: WidgetID,
} }
@@ -122,6 +124,7 @@ impl PopupManager {
layout: &mut Layout, layout: &mut Layout,
frontend_tasks: FrontendTasks, frontend_tasks: FrontendTasks,
params: MountPopupParams, params: MountPopupParams,
config: &GeneralConfig,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let doc_params = &ParseDocumentParams { let doc_params = &ParseDocumentParams {
globals: globals.clone(), globals: globals.clone(),
@@ -175,6 +178,7 @@ impl PopupManager {
layout, layout,
handle: popup_handle.clone(), handle: popup_handle.clone(),
id_content, id_content,
config,
})?; })?;
Ok(()) Ok(())

View File

@@ -0,0 +1,297 @@
use keyvalues_parser::{Obj, Vdf};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
pub struct SteamUtils {
steam_root: PathBuf,
}
fn get_steam_root() -> anyhow::Result<PathBuf> {
let home = PathBuf::from(std::env::var("HOME")?);
let steam_paths: [&str; 3] = [
".steam/steam",
".steam/debian-installation",
".var/app/com.valvesoftware.Steam/data/Steam",
];
let Some(steam_path) = steam_paths.iter().map(|path| home.join(path)).find(|p| p.exists()) else {
anyhow::bail!("Couldn't find Steam installation in search paths");
};
Ok(steam_path)
}
pub type AppID = String;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppManifest {
pub app_id: AppID,
pub run_game_id: AppID,
pub name: String,
pub raw_state_flags: u64, // documentation: https://github.com/lutris/lutris/blob/master/docs/steam.rst
pub last_played: Option<u64>, // unix timestamp
}
pub enum GameSortMethod {
NameAsc,
NameDesc,
PlayDateDesc,
}
fn get_obj_first<'a>(obj: &'a Obj<'_>, key: &str) -> Option<&'a Obj<'a>> {
obj.get(key)?.first()?.get_obj()
}
fn get_str_first<'a>(obj: &'a Obj<'_>, key: &str) -> Option<&'a str> {
obj.get(key)?.first()?.get_str()
}
fn vdf_parse_libraryfolders<'a>(vdf_root: &'a Vdf<'a>) -> Option<Vec<AppEntry>> {
let obj_libraryfolders = vdf_root.value.get_obj()?;
let mut res = Vec::<AppEntry>::new();
let mut num = 0;
loop {
let Some(library_folder) = get_obj_first(obj_libraryfolders, format!("{}", num).as_str()) else {
// no more libraries to find
break;
};
let Some(apps) = get_obj_first(library_folder, "apps") else {
// no apps?
num += 1;
continue;
};
let Some(path) = get_str_first(library_folder, "path") else {
// no path?
num += 1;
continue;
};
//log::trace!("path: {}", path);
res.extend(
apps
.iter()
.filter_map(|item| item.0.parse::<u64>().ok())
.map(|app_id| AppEntry {
app_id: app_id.to_string(),
root_path: String::from(path),
}),
);
num += 1;
}
Some(res)
}
fn vdf_parse_appstate<'a>(app_id: AppID, vdf_root: &'a Vdf<'a>) -> Option<AppManifest> {
let app_state_obj = vdf_root.value.get_obj()?;
let name = app_state_obj.get("name")?.first()?.get_str()?;
let raw_state_flags = app_state_obj
.get("StateFlags")?
.first()?
.get_str()?
.parse::<u64>()
.ok()?;
let last_played = match app_state_obj.get("LastPlayed") {
Some(s) => Some(s.first()?.get_str()?.parse::<u64>().ok()?),
None => None,
};
Some(AppManifest {
app_id: app_id.clone(),
run_game_id: app_id,
name: String::from(name),
raw_state_flags,
last_played,
})
}
struct AppEntry {
pub root_path: String,
pub app_id: AppID,
}
pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> {
log::info!("Stopping Steam game with AppID {}", app_id);
for game in list_running_games()? {
if game.app_id != app_id {
continue;
}
log::info!("Killing process with PID {} and its children", game.pid);
let _ = std::process::Command::new("pkill")
.arg(if force_kill { "-9" } else { "-15" })
.arg("-P")
.arg(format!("{}", game.pid))
.spawn()?;
}
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,
pub pid: i32,
}
pub fn list_running_games() -> anyhow::Result<Vec<RunningGame>> {
let mut res = Vec::<RunningGame>::new();
let entries = std::fs::read_dir("/proc")?;
for entry in entries.into_iter().flatten() {
let path_cmdline = entry.path().join("cmdline");
let Ok(cmdline) = std::fs::read(path_cmdline) else {
continue;
};
let proc_file_name = entry.file_name();
let Some(pid) = proc_file_name.to_str() else {
continue;
};
let Ok(pid) = pid.parse::<i32>() else {
continue;
};
let args: Vec<&str> = cmdline
.split(|byte| *byte == 0x00)
.filter_map(|arg| std::str::from_utf8(arg).ok())
.collect();
let mut has_steam_launch = false;
for arg in &args {
if *arg == "SteamLaunch" {
has_steam_launch = true;
break;
}
}
if !has_steam_launch {
continue;
}
// Running game process found. Parse AppID
for arg in &args {
let pat = "AppId=";
let Some(pos) = arg.find(pat) else {
continue;
};
if pos != 0 {
continue;
}
let Some((_, second)) = arg.split_at_checked(pat.len()) else {
continue;
};
let Ok(app_id_num) = second.parse::<u64>() else {
continue;
};
// AppID found. Add it to the list
res.push(RunningGame {
app_id: app_id_num.to_string(),
pid,
});
break;
}
}
Ok(res)
}
fn call_steam(arg: &str) -> anyhow::Result<()> {
match std::process::Command::new("xdg-open").arg(arg).spawn() {
Ok(_) => Ok(()),
Err(_) => {
std::process::Command::new("steam").arg(arg).spawn()?;
Ok(())
}
}
}
impl SteamUtils {
fn get_dir_steamapps(&self) -> PathBuf {
self.steam_root.join("steamapps")
}
pub fn new() -> anyhow::Result<Self> {
let steam_root = get_steam_root()?;
Ok(Self { steam_root })
}
fn get_app_manifest(&self, app_entry: &AppEntry) -> anyhow::Result<AppManifest> {
let manifest_path =
PathBuf::from(&app_entry.root_path).join(format!("steamapps/appmanifest_{}.acf", app_entry.app_id));
let vdf_data = std::fs::read_to_string(manifest_path)?;
let vdf_root = keyvalues_parser::Vdf::parse(&vdf_data)?;
let Some(manifest) = vdf_parse_appstate(app_entry.app_id.clone(), &vdf_root) else {
anyhow::bail!("Failed to parse AppState");
};
Ok(manifest)
}
pub fn list_installed_games(&self, sort_method: GameSortMethod) -> anyhow::Result<Vec<AppManifest>> {
let path = self.get_dir_steamapps().join("libraryfolders.vdf");
let vdf_data = std::fs::read_to_string(path)?;
let vdf_root = keyvalues_parser::Vdf::parse(&vdf_data)?;
let Some(apps) = vdf_parse_libraryfolders(&vdf_root) else {
anyhow::bail!("Failed to fetch installed Steam apps");
};
let mut games: Vec<AppManifest> = apps
.iter()
.filter_map(|app_entry| {
let manifest = match self.get_app_manifest(app_entry) {
Ok(manifest) => manifest,
Err(e) => {
log::warn!(
"Failed to get app manifest for AppID {}: {}. This entry won't show.",
app_entry.app_id,
e
);
return None;
}
};
Some(manifest)
})
.collect();
match sort_method {
GameSortMethod::NameAsc => {
games.sort_by(|a, b| a.name.cmp(&b.name));
}
GameSortMethod::NameDesc => {
games.sort_by(|a, b| b.name.cmp(&a.name));
}
GameSortMethod::PlayDateDesc => {
games.sort_by(|a, b| b.last_played.cmp(&a.last_played));
}
}
Ok(games)
}
}

View File

@@ -9,12 +9,12 @@ use wgui::{
i18n::Translation, i18n::Translation,
layout::{Layout, LayoutTask, LayoutTasks, WidgetID}, layout::{Layout, LayoutTask, LayoutTasks, WidgetID},
renderer_vk::{ renderer_vk::{
text::{FontWeight, TextStyle}, text::{FontWeight, HorizontalAlign, TextStyle},
util::centered_matrix, util::centered_matrix,
}, },
taffy::{ taffy::{
self, self,
prelude::{length, percent}, prelude::{auto, length, percent},
}, },
widget::{ widget::{
div::WidgetDiv, div::WidgetDiv,
@@ -47,7 +47,7 @@ impl Drop for MountedToast {
} }
} }
const TOAST_DURATION_TICKS: u32 = 90; const TOAST_DURATION_TICKS: u32 = 150;
impl ToastManager { impl ToastManager {
pub fn new() -> Self { pub fn new() -> Self {
@@ -102,6 +102,10 @@ impl ToastManager {
top: length(8.0), top: length(8.0),
bottom: length(8.0), bottom: length(8.0),
}, },
max_size: taffy::Size {
width: length(400.0),
height: auto(),
},
..Default::default() ..Default::default()
}, },
)?; )?;
@@ -114,6 +118,8 @@ impl ToastManager {
content, content,
style: TextStyle { style: TextStyle {
weight: Some(FontWeight::Bold), weight: Some(FontWeight::Bold),
align: Some(HorizontalAlign::Center),
wrap: true,
..Default::default() ..Default::default()
}, },
}, },
@@ -124,7 +130,7 @@ impl ToastManager {
// show-up animation // show-up animation
layout.animations.add(Animation::new( layout.animations.add(Animation::new(
rect.id, rect.id,
160, // does not use anim_mult (TOAST_DURATION_TICKS as f32 * globals.defaults.animation_mult) as u32,
AnimationEasing::Linear, AnimationEasing::Linear,
Box::new(move |common, data| { Box::new(move |common, data| {
let pos_showup = AnimationEasing::OutQuint.interpolate((data.pos * 4.0).min(1.0)); let pos_showup = AnimationEasing::OutQuint.interpolate((data.pos * 4.0).min(1.0));
@@ -132,7 +138,7 @@ impl ToastManager {
let scale = AnimationEasing::OutBack.interpolate((data.pos * 4.0).min(1.0)); let scale = AnimationEasing::OutBack.interpolate((data.pos * 4.0).min(1.0));
{ {
let mtx = Mat4::from_translation(Vec3::new(0.0, (1.0 - pos_showup) * 100.0, 0.0)) let mtx = Mat4::from_translation(Vec3::new(0.0, (1.0 - pos_showup) * 20.0, 0.0))
* Mat4::from_scale(Vec3::new(scale, scale, 1.0)); * Mat4::from_scale(Vec3::new(scale, scale, 1.0));
data.data.transform = centered_matrix(data.widget_boundary.size, &mtx); data.data.transform = centered_matrix(data.widget_boundary.size, &mtx);
} }
@@ -156,16 +162,15 @@ impl ToastManager {
} }
pub fn tick(&mut self, globals: &WguiGlobals, layout: &mut Layout) -> anyhow::Result<()> { pub fn tick(&mut self, globals: &WguiGlobals, layout: &mut Layout) -> anyhow::Result<()> {
if !self.needs_tick {
return Ok(());
}
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
if state.timeout > 0 { if state.timeout > 0 {
state.timeout -= 1; state.timeout -= 1;
} }
if !self.needs_tick {
return Ok(());
}
if state.timeout == 0 { if state.timeout == 0 {
state.toast = None; state.toast = None;
state.timeout = TOAST_DURATION_TICKS; state.timeout = TOAST_DURATION_TICKS;

View File

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

@@ -1,27 +1,108 @@
use std::{collections::HashMap, rc::Rc}; use std::{collections::HashMap, rc::Rc, str::FromStr};
use strum::{AsRefStr, EnumString, VariantNames};
use wayvr_ipc::packet_client::{PositionMode, WvrProcessLaunchParams};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::{button::ComponentButton, checkbox::ComponentCheckbox, radio_group::ComponentRadioGroup},
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::label::WidgetLabel, widget::label::WidgetLabel,
}; };
use wlx_common::{config::GeneralConfig, dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
use crate::util::desktop_finder::DesktopEntry; use crate::frontend::{FrontendTask, FrontendTasks, SoundType};
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum PosMode {
Floating,
Anchored,
Static,
}
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum ResMode {
Res1440,
Res1080,
Res720,
Res480,
}
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum OrientationMode {
Wide,
SemiWide,
Square,
SemiTall,
Tall,
}
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum CompositorMode {
Cage,
Native,
}
#[derive(Clone)]
enum Task {
SetCompositor(CompositorMode),
SetRes(ResMode),
SetPos(PosMode),
SetOrientation(OrientationMode),
SetAutoStart(bool),
Launch,
}
struct LaunchParams<'a, T> {
application: &'a DesktopEntry,
compositor_mode: CompositorMode,
pos_mode: PosMode,
res_mode: ResMode,
orientation_mode: OrientationMode,
globals: &'a WguiGlobals,
frontend_tasks: &'a FrontendTasks,
interface: &'a mut BoxDashInterface<T>,
auto_start: bool,
data: &'a mut T,
on_launched: &'a dyn Fn(),
}
pub struct View { pub struct View {
#[allow(dead_code)] #[allow(dead_code)]
pub state: ParserState, state: ParserState,
//entry: DesktopEntry, entry: DesktopEntry,
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
#[allow(dead_code)]
radio_compositor: Rc<ComponentRadioGroup>,
#[allow(dead_code)]
radio_res: Rc<ComponentRadioGroup>,
#[allow(dead_code)]
radio_orientation: Rc<ComponentRadioGroup>,
compositor_mode: CompositorMode,
pos_mode: PosMode,
res_mode: ResMode,
orientation_mode: OrientationMode,
auto_start: bool,
on_launched: Box<dyn Fn()>,
} }
pub struct Params<'a> { pub struct Params<'a> {
pub globals: WguiGlobals, pub globals: &'a WguiGlobals,
pub entry: DesktopEntry, pub entry: DesktopEntry,
pub layout: &'a mut Layout, pub layout: &'a mut Layout,
pub parent_id: WidgetID, pub parent_id: WidgetID,
pub config: &'a GeneralConfig,
pub frontend_tasks: &'a FrontendTasks,
pub on_launched: Box<dyn Fn()>,
} }
impl View { impl View {
@@ -33,12 +114,34 @@ impl View {
}; };
let mut state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?; let mut state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
let radio_compositor = state.fetch_component_as::<ComponentRadioGroup>("radio_compositor")?;
let radio_res = state.fetch_component_as::<ComponentRadioGroup>("radio_res")?;
let radio_pos = state.fetch_component_as::<ComponentRadioGroup>("radio_pos")?;
let radio_orientation = state.fetch_component_as::<ComponentRadioGroup>("radio_orientation")?;
let cb_autostart = state.fetch_component_as::<ComponentCheckbox>("cb_autostart")?;
let btn_launch = state.fetch_component_as::<ComponentButton>("btn_launch")?;
{
let mut label_exec = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_exec")?;
label_exec.set_text_simple(
&mut params.globals.get(),
Translation::from_raw_text_string(format!("{} {}", params.entry.exec_path, params.entry.exec_args)),
);
}
let tasks = Tasks::new();
tasks.handle_button(&btn_launch, Task::Launch);
let id_icon_parent = state.get_widget_id("icon_parent")?; let id_icon_parent = state.get_widget_id("icon_parent")?;
// app icon // app icon
if let Some(icon_path) = &params.entry.icon_path { if let Some(icon_path) = &params.entry.icon_path {
let mut template_params: HashMap<Rc<str>, Rc<str>> = HashMap::new(); let mut template_params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
template_params.insert("path".into(), icon_path.as_str().into()); template_params.insert("path".into(), icon_path.clone());
state.instantiate_template( state.instantiate_template(
doc_params, doc_params,
"ApplicationIcon", "ApplicationIcon",
@@ -48,6 +151,115 @@ impl View {
)?; )?;
} }
let compositor_mode = if params.config.xwayland_by_default {
CompositorMode::Cage
} else {
CompositorMode::Native
};
radio_compositor.set_value(compositor_mode.as_ref())?;
tasks.push(Task::SetCompositor(compositor_mode));
let res_mode = ResMode::Res1080;
// TODO: configurable defaults ?
//radio_res.set_value(res_mode.as_ref())?;
//tasks.push(Task::SetRes(res_mode));
let orientation_mode = OrientationMode::Wide;
// TODO: configurable defaults ?
//radio_orientation.set_value(orientation_mode.as_ref())?;
//tasks.push(Task::SetOrientation(orientation_mode));
let pos_mode = PosMode::Anchored;
// TODO: configurable defaults ?
//radio_pos.set_value(pos_mode.as_ref())?;
//tasks.push(Task::SetPos(pos_mode));
let auto_start = false;
radio_compositor.on_value_changed({
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
CompositorMode::from_str(&*v)
.inspect_err(|_| {
log::error!(
"Invalid value for compositor: '{v}'. Valid values are: {:?}",
ResMode::VARIANTS
)
})
.ok()
}) {
tasks.push(Task::SetCompositor(mode));
}
Ok(())
})
});
radio_res.on_value_changed({
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
ResMode::from_str(&*v)
.inspect_err(|_| {
log::error!(
"Invalid value for resolution: '{v}'. Valid values are: {:?}",
ResMode::VARIANTS
)
})
.ok()
}) {
tasks.push(Task::SetRes(mode));
}
Ok(())
})
});
radio_pos.on_value_changed({
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
PosMode::from_str(&*v)
.inspect_err(|_| {
log::error!(
"Invalid value for position: '{v}'. Valid values are: {:?}",
PosMode::VARIANTS
)
})
.ok()
}) {
tasks.push(Task::SetPos(mode));
}
Ok(())
})
});
radio_orientation.on_value_changed({
let tasks = tasks.clone();
Box::new(move |_, ev| {
if let Some(mode) = ev.value.and_then(|v| {
OrientationMode::from_str(&*v)
.inspect_err(|_| {
log::error!(
"Invalid value for orientation: '{v}'. Valid values are: {:?}",
OrientationMode::VARIANTS
)
})
.ok()
}) {
tasks.push(Task::SetOrientation(mode));
}
Ok(())
})
});
cb_autostart.on_toggle({
let tasks = tasks.clone();
Box::new(move |_, ev| {
tasks.push(Task::SetAutoStart(ev.checked));
Ok(())
})
});
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_title")?; let mut label_title = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_title")?;
label_title.set_text_simple( label_title.set_text_simple(
@@ -56,8 +268,157 @@ impl View {
); );
Ok(Self { Ok(Self {
//entry: params.entry,
state, state,
tasks,
radio_compositor,
radio_res,
radio_orientation,
compositor_mode,
pos_mode,
res_mode,
orientation_mode,
auto_start,
entry: params.entry,
frontend_tasks: params.frontend_tasks.clone(),
globals: params.globals.clone(),
on_launched: params.on_launched,
}) })
} }
pub fn update<T>(&mut self, interface: &mut BoxDashInterface<T>, data: &mut T) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
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),
}
}
}
Ok(())
}
fn action_launch<T>(&mut self, interface: &mut BoxDashInterface<T>, data: &mut T) {
View::try_launch(LaunchParams {
application: &self.entry,
frontend_tasks: &self.frontend_tasks,
globals: &self.globals,
compositor_mode: self.compositor_mode,
res_mode: self.res_mode,
pos_mode: self.pos_mode,
orientation_mode: self.orientation_mode,
auto_start: self.auto_start,
interface,
data,
on_launched: &self.on_launched,
});
}
fn try_launch<T>(params: LaunchParams<T>) {
let globals = params.globals.clone();
let frontend_tasks = params.frontend_tasks.clone();
// launch app itself
let Err(e) = View::launch(params) else { return };
let str_failed = globals.i18n().translate("FAILED_TO_LAUNCH_APPLICATION");
frontend_tasks.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"{} {:?}",
str_failed, e
))));
}
fn launch<T>(params: LaunchParams<T>) -> anyhow::Result<()> {
let mut env = Vec::<String>::new();
if params.compositor_mode == CompositorMode::Native {
// This list could be larger, feel free to expand it
env.push("QT_QPA_PLATFORM=wayland".into());
env.push("GDK_BACKEND=wayland".into());
env.push("SDL_VIDEODRIVER=wayland".into());
env.push("XDG_SESSION_TYPE=wayland".into());
env.push("ELECTRON_OZONE_PLATFORM_HINT=wayland".into());
}
let args = match params.compositor_mode {
CompositorMode::Cage => format!("-- {} {}", params.application.exec_path, params.application.exec_args),
CompositorMode::Native => params.application.exec_args.to_string(),
};
let exec = match params.compositor_mode {
CompositorMode::Cage => "cage".to_string(),
CompositorMode::Native => params.application.exec_path.to_string(),
};
let pos_mode = match params.pos_mode {
PosMode::Floating => PositionMode::Float,
PosMode::Anchored => PositionMode::Anchor,
PosMode::Static => PositionMode::Static,
};
let mut userdata = HashMap::new();
userdata.insert("desktop-entry".to_string(), serde_json::to_string(params.application)?);
let resolution = Self::calculate_resolution(params.res_mode, params.orientation_mode);
params.interface.process_launch(
params.data,
params.auto_start,
WvrProcessLaunchParams {
env,
exec,
name: params.application.app_name.to_string(),
args,
resolution,
pos_mode,
icon: params.application.icon_path.as_ref().map(|x| x.as_ref().to_string()),
userdata,
},
)?;
params
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_translation_key(
"APPLICATION_STARTED",
)));
params.frontend_tasks.push(FrontendTask::PlaySound(SoundType::Launch));
(*params.on_launched)();
// we're done!
Ok(())
}
fn calculate_resolution(res_mode: ResMode, orientation_mode: OrientationMode) -> [u32; 2] {
let total_pixels = match res_mode {
ResMode::Res1440 => 2560 * 1440,
ResMode::Res1080 => 1920 * 1080,
ResMode::Res720 => 1280 * 720,
ResMode::Res480 => 854 * 480,
};
let (ratio_w, ratio_h) = match orientation_mode {
OrientationMode::Wide => (16, 9),
OrientationMode::SemiWide => (3, 2),
OrientationMode::Square => (1, 1),
OrientationMode::SemiTall => (2, 3),
OrientationMode::Tall => (9, 16),
};
let k = ((total_pixels as f64) / (ratio_w * ratio_h) as f64).sqrt();
let width = (ratio_w as f64 * k).round() as u64;
let height = (ratio_h as f64 * k).round() as u64;
[width as u32, height as u32]
}
} }

View File

@@ -12,13 +12,13 @@ use wgui::{
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::ConstructEssentials, widget::ConstructEssentials,
}; };
use crate::{ use crate::{
frontend::{FrontendTask, FrontendTasks}, frontend::{FrontendTask, FrontendTasks},
task::Tasks, util::pactl_wrapper::{self},
util::pactl_wrapper,
}; };
#[derive(Clone)] #[derive(Clone)]
@@ -172,7 +172,8 @@ fn does_string_mention_hmd_sink(input: &str) -> bool {
lwr.contains("index") || // Valve hardware lwr.contains("index") || // Valve hardware
lwr.contains("oculus") || // Oculus lwr.contains("oculus") || // Oculus
lwr.contains("rift") || // Also Oculus lwr.contains("rift") || // Also Oculus
lwr.contains("beyond") // Bigscreen Beyond lwr.contains("beyond") || // Bigscreen Beyond
lwr.contains("wivrn") // WiVRn
} }
fn does_string_mention_hmd_source(input: &str) -> bool { fn does_string_mention_hmd_source(input: &str) -> bool {
@@ -180,7 +181,8 @@ fn does_string_mention_hmd_source(input: &str) -> bool {
lwr.contains("hmd") || // generic hmd name detected lwr.contains("hmd") || // generic hmd name detected
lwr.contains("valve") || // Valve hardware lwr.contains("valve") || // Valve hardware
lwr.contains("oculus") || // Oculus lwr.contains("oculus") || // Oculus
lwr.contains("beyond") // Bigscreen Beyond lwr.contains("beyond") || // Bigscreen Beyond
lwr.contains("wivrn") // WiVRn
} }
fn is_card_mentioning_hmd(card: &pactl_wrapper::Card) -> bool { fn is_card_mentioning_hmd(card: &pactl_wrapper::Card) -> bool {
@@ -401,7 +403,30 @@ struct MountDeviceSliderParams<'a> {
alt_desc: String, alt_desc: String,
} }
fn push_popup_speakers_set_successfully(globals: &WguiGlobals, frontend_tasks: &FrontendTasks, name: &str) {
frontend_tasks.push(FrontendTask::PushToast(Translation::from_translation_key(
format!(
"{}: {}",
globals.i18n().translate("AUDIO.SPEAKERS_SET_SUCCESSFULLY"),
name
)
.as_str(),
)));
}
fn push_popup_microphone_set_successfully(globals: &WguiGlobals, frontend_tasks: &FrontendTasks, name: &str) {
frontend_tasks.push(FrontendTask::PushToast(Translation::from_translation_key(
format!(
"{}: {}",
globals.i18n().translate("AUDIO.MICROPHONE_SET_SUCCESSFULLY"),
name
)
.as_str(),
)));
}
fn switch_sink_card( fn switch_sink_card(
globals: &WguiGlobals,
frontend_tasks: &FrontendTasks, frontend_tasks: &FrontendTasks,
card: &pactl_wrapper::Card, card: &pactl_wrapper::Card,
profile_name: &str, profile_name: &str,
@@ -424,32 +449,32 @@ fn switch_sink_card(
} }
if sink_found { if sink_found {
frontend_tasks.push(FrontendTask::PushToast(Translation::from_translation_key( push_popup_speakers_set_successfully(globals, frontend_tasks, &name.name);
format!("[AUDIO.SPEAKERS_SET_SUCCESSFULLY]: {}", name.name).as_str(),
)));
} else { } else {
frontend_tasks.push(FrontendTask::PushToast(Translation::from_translation_key( frontend_tasks.push(FrontendTask::PushToast(Translation::from_translation_key(
format!("[AUDIO.DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED]: {}", name.name).as_str(), format!("Card found ({}), but no matching speakers found", name.name).as_str(),
))); )));
} }
Ok(()) Ok(())
} }
fn switch_source(frontend_tasks: &FrontendTasks, source: &pactl_wrapper::Source) -> anyhow::Result<()> { fn switch_source(
globals: &WguiGlobals,
frontend_tasks: &FrontendTasks,
source: &pactl_wrapper::Source,
) -> anyhow::Result<()> {
match pactl_wrapper::set_default_source(source.index) { match pactl_wrapper::set_default_source(source.index) {
Ok(()) => { Ok(()) => {
frontend_tasks.push(FrontendTask::PushToast(Translation::from_translation_key( push_popup_microphone_set_successfully(
format!( globals,
"[AUDIO.MICROPHONE_SET_SUCCESSFULLY]: {}", frontend_tasks,
if let Some(card_name) = &source.properties.card_name { if let Some(card_name) = &source.properties.card_name {
card_name card_name
} else { } else {
&source.description &source.description
} },
) );
.as_str(),
)));
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
@@ -459,13 +484,13 @@ fn switch_source(frontend_tasks: &FrontendTasks, source: &pactl_wrapper::Source)
} }
} }
fn switch_to_vr_microphone(frontend_tasks: &FrontendTasks) -> anyhow::Result<()> { fn switch_to_vr_microphone(globals: &WguiGlobals, frontend_tasks: &FrontendTasks) -> anyhow::Result<()> {
let sources = pactl_wrapper::list_sources()?; let sources = pactl_wrapper::list_sources()?;
let mut switched = false; let mut switched = false;
for source in &sources { for source in &sources {
if is_source_mentioning_hmd(source) { if is_source_mentioning_hmd(source) {
switch_source(frontend_tasks, source)?; switch_source(globals, frontend_tasks, source)?;
switched = true; switched = true;
break; break;
} }
@@ -529,10 +554,20 @@ fn get_best_profile_from_array<'a>(arr: &[CardPriorityResult<'a>]) -> Option<Car
res res
} }
fn switch_to_vr_speakers(frontend_tasks: &FrontendTasks) -> anyhow::Result<()> { fn switch_to_vr_speakers(globals: &WguiGlobals, frontend_tasks: &FrontendTasks) -> anyhow::Result<()> {
let cards = pactl_wrapper::list_cards()?; let cards = pactl_wrapper::list_cards()?;
let sinks = pactl_wrapper::list_sinks()?;
let mut best_profiles = Vec::new(); let mut best_profiles = Vec::new();
// Check for WiVRn presence
for sink in sinks {
if sink.name.contains("wivrn") {
pactl_wrapper::set_default_sink(sink.index)?;
push_popup_speakers_set_successfully(globals, frontend_tasks, "WiVRn");
return Ok(());
}
}
for card in &cards { for card in &cards {
if !is_card_mentioning_hmd(card) { if !is_card_mentioning_hmd(card) {
continue; continue;
@@ -545,7 +580,7 @@ fn switch_to_vr_speakers(frontend_tasks: &FrontendTasks) -> anyhow::Result<()> {
if !best_profiles.is_empty() { if !best_profiles.is_empty() {
let best_profile = get_best_profile_from_array(&best_profiles).unwrap(); let best_profile = get_best_profile_from_array(&best_profiles).unwrap();
let name = get_profile_display_name(&best_profile.name, best_profile.card); let name = get_profile_display_name(&best_profile.name, best_profile.card);
switch_sink_card(frontend_tasks, best_profile.card, &best_profile.name, &name)?; switch_sink_card(globals, frontend_tasks, best_profile.card, &best_profile.name, &name)?;
return Ok(()); return Ok(());
} }
@@ -556,7 +591,7 @@ fn switch_to_vr_speakers(frontend_tasks: &FrontendTasks) -> anyhow::Result<()> {
if !name.is_vr { if !name.is_vr {
continue; continue;
} }
switch_sink_card(frontend_tasks, card, profile_name, &name)?; switch_sink_card(globals, frontend_tasks, card, profile_name, &name)?;
return Ok(()); return Ok(());
} }
} }
@@ -684,8 +719,8 @@ impl View {
pactl_wrapper::set_card_profile(c.card.index, &c.profile_name)?; pactl_wrapper::set_card_profile(c.card.index, &c.profile_name)?;
} }
ViewTask::AutoSwitch => { ViewTask::AutoSwitch => {
switch_to_vr_microphone(&self.frontend_tasks)?; switch_to_vr_speakers(&self.globals, &self.frontend_tasks)?;
switch_to_vr_speakers(&self.frontend_tasks)?; switch_to_vr_microphone(&self.globals, &self.frontend_tasks)?;
self.tasks.push(ViewTask::Remount); self.tasks.push(ViewTask::Remount);
} }
} }
@@ -745,8 +780,14 @@ impl View {
par.insert("device_name".into(), disp.name.as_str().into()); par.insert("device_name".into(), disp.name.as_str().into());
par.insert("device_icon".into(), disp.icon_path.into()); par.insert("device_icon".into(), disp.icon_path.into());
} else { } else {
let icon_path = if params.alt_desc.contains("WiVRn") {
"dashboard/wivrn_head_symbolic.svg"
} else {
"dashboard/binary.svg"
};
par.insert("device_name".into(), params.alt_desc.into()); par.insert("device_name".into(), params.alt_desc.into());
par.insert("device_icon".into(), "dashboard/binary.svg".into()); par.insert("device_icon".into(), icon_path.into());
} }
par.insert( par.insert(

View File

@@ -0,0 +1,317 @@
use std::rc::Rc;
use wgui::{
assets::AssetPath,
components::{
self,
button::ComponentButton,
tooltip::{TooltipInfo, TooltipSide},
},
drawing::{self, GradientMode},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID, WidgetPair},
renderer_vk::text::{FontWeight, HorizontalAlign, TextShadow, TextStyle, custom_glyph::CustomGlyphData},
taffy::{
self, AlignItems, AlignSelf, JustifyContent, JustifySelf,
prelude::{auto, length, percent},
},
widget::{
ConstructEssentials,
div::WidgetDiv,
image::{WidgetImage, WidgetImageParams},
label::{WidgetLabel, WidgetLabelParams},
rectangle,
util::WLength,
},
};
use crate::util::{
cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID},
various::AsyncExecutor,
};
pub struct ViewCommon {
img_placeholder: Option<CustomGlyphData>,
globals: WguiGlobals,
}
pub struct Params<'a, 'b> {
pub ess: &'a mut ConstructEssentials<'b>,
pub executor: &'a AsyncExecutor,
pub manifest: &'a steam_utils::AppManifest,
pub scale: f32,
pub on_loaded: Box<dyn FnOnce(CoverArt)>,
}
pub struct View {
pub button: Rc<ComponentButton>,
pair: WidgetPair,
id_image_parent: WidgetID,
app_name: String,
app_id: AppID,
}
const BORDER_COLOR_DEFAULT: drawing::Color = drawing::Color::new(0.0, 0.0, 0.0, 0.35);
const BORDER_COLOR_HOVERED: drawing::Color = drawing::Color::new(1.0, 1.0, 1.0, 1.0);
const GAME_COVER_SIZE_X: f32 = 140.0;
const GAME_COVER_SIZE_Y: f32 = 210.0;
impl View {
async fn request_cover_image(
executor: AsyncExecutor,
manifest: steam_utils::AppManifest,
on_loaded: Box<dyn FnOnce(CoverArt)>,
) {
let cover_art = match cached_fetcher::request_image(executor, manifest.app_id.clone()).await {
Ok(cover_art) => cover_art,
Err(e) => {
log::error!("request_cover_image failed: {:?}", e);
return;
}
};
on_loaded(cover_art)
}
fn mount_image(&self, layout: &mut Layout, glyph: &CustomGlyphData) -> anyhow::Result<()> {
let image = WidgetImage::create(WidgetImageParams {
round: WLength::Units(10.0),
glyph_data: Some(glyph.clone()),
..Default::default()
});
let (a, _) = layout.add_child(
self.id_image_parent,
image,
taffy::Style {
size: taffy::Size {
width: percent(1.0),
height: percent(1.0),
},
..Default::default()
},
)?;
a.widget.state().flags.new_pass = true;
Ok(())
}
fn mount_placeholder_text(
&self,
globals: &WguiGlobals,
layout: &mut Layout,
parent: WidgetID,
text: &str,
) -> anyhow::Result<()> {
let label = WidgetLabel::create(
&mut globals.get(),
WidgetLabelParams {
content: Translation::from_raw_text(text),
style: TextStyle {
weight: Some(FontWeight::Bold),
wrap: true,
size: Some(16.0),
align: Some(HorizontalAlign::Center),
shadow: Some(TextShadow {
color: drawing::Color::new(0.0, 0.0, 0.0, 1.0),
x: 2.0,
y: 2.0,
}),
..Default::default()
},
},
);
layout.add_child(
parent,
label,
taffy::Style {
position: taffy::Position::Absolute,
align_self: Some(AlignSelf::Baseline),
justify_self: Some(JustifySelf::Center),
margin: taffy::Rect {
top: length(32.0),
bottom: auto(),
left: auto(),
right: auto(),
},
..Default::default()
},
)?;
Ok(())
}
pub fn set_cover_art(
&mut self,
view_common: &mut ViewCommon,
layout: &mut Layout,
cover_art: &CoverArt,
) -> anyhow::Result<()> {
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)?;
} else {
// mount image
let path = format!("app:{:?}", self.app_id);
let glyph =
match CustomGlyphData::from_bytes_raster(&view_common.globals, &path, &cover_art.compressed_image_data) {
Ok(c) => c,
Err(e) => {
log::warn!("failed to decode cover art image: {:?}", e);
return Ok(());
}
};
self.mount_image(layout, &glyph)?;
}
Ok(())
}
pub fn new(params: Params) -> anyhow::Result<Self> {
let (widget_button, button) = components::button::construct(
params.ess,
components::button::Params {
color: Some(drawing::Color::new(1.0, 1.0, 1.0, 0.0)),
border_color: Some(BORDER_COLOR_DEFAULT),
hover_border_color: Some(BORDER_COLOR_HOVERED),
round: WLength::Units(12.0),
border: 2.0,
tooltip: Some(TooltipInfo {
side: TooltipSide::Bottom,
text: Translation::from_raw_text(&params.manifest.name),
}),
style: taffy::Style {
position: taffy::Position::Relative,
align_items: Some(taffy::AlignItems::Center),
justify_content: Some(taffy::JustifyContent::Center),
size: taffy::Size {
width: length(GAME_COVER_SIZE_X * params.scale),
height: length(GAME_COVER_SIZE_Y * params.scale),
},
..Default::default()
},
..Default::default()
},
)?;
let (image_parent, _) = params.ess.layout.add_child(
widget_button.id,
WidgetDiv::create(),
taffy::Style {
position: taffy::Position::Absolute,
size: taffy::Size {
width: percent(1.0),
height: percent(1.0),
},
padding: taffy::Rect::length(2.0),
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
..Default::default()
},
)?;
let rect_gradient = |color: drawing::Color, color2: drawing::Color| {
rectangle::WidgetRectangle::create(rectangle::WidgetRectangleParams {
color,
color2,
round: WLength::Units(12.0),
gradient: GradientMode::Vertical,
..Default::default()
})
};
let rect_gradient_style = |align_self: taffy::AlignSelf, height: f32| taffy::Style {
position: taffy::Position::Absolute,
align_self: Some(align_self),
size: taffy::Size {
width: percent(1.0),
height: percent(height),
},
..Default::default()
};
// top shine
let (top_shine, _) = params.ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(1.0, 1.0, 1.0, 0.2),
drawing::Color::new(1.0, 1.0, 1.0, 0.02),
),
rect_gradient_style(taffy::AlignSelf::Baseline, 0.05),
)?;
// not optimal, this forces us to create a new pass for every created cover art just to overlay various rectangles at the top of the image cover art
top_shine.widget.state().flags.new_pass = true;
// top white gradient
params.ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(1.0, 1.0, 1.0, 0.15),
drawing::Color::new(1.0, 1.0, 1.0, 0.0),
),
rect_gradient_style(taffy::AlignSelf::Baseline, 0.5),
)?;
// bottom black gradient
params.ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(0.0, 0.0, 0.0, 0.0),
drawing::Color::new(0.0, 0.0, 0.0, 0.25),
),
rect_gradient_style(taffy::AlignSelf::End, 0.5),
)?;
// bottom shadow
params.ess.layout.add_child(
widget_button.id,
rect_gradient(
drawing::Color::new(0.0, 0.0, 0.0, 0.1),
drawing::Color::new(0.0, 0.0, 0.0, 0.9),
),
rect_gradient_style(taffy::AlignSelf::End, 0.05),
)?;
// request cover image data from the internet or disk cache
params
.executor
.spawn(View::request_cover_image(
params.executor.clone(),
params.manifest.clone(),
Box::new(params.on_loaded),
))
.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(),
})
}
}
impl ViewCommon {
pub fn new(globals: WguiGlobals) -> Self {
Self {
globals,
img_placeholder: None,
}
}
fn get_placeholder_image(&mut self) -> anyhow::Result<&CustomGlyphData> {
if self.img_placeholder.is_none() {
let c = CustomGlyphData::from_assets(&self.globals, AssetPath::BuiltIn("dashboard/placeholder_cover.png"))?;
self.img_placeholder = Some(c);
}
Ok(self.img_placeholder.as_ref().unwrap()) // safe
}
}

View File

@@ -0,0 +1,195 @@
use std::rc::Rc;
use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType},
util::{
cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID, AppManifest},
various::AsyncExecutor,
},
views::game_cover,
};
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::{ConstructEssentials, label::WidgetLabel},
};
#[derive(Clone)]
enum Task {
FillAppDetails(cached_fetcher::AppDetailsJSONData),
SetCoverArt(Rc<CoverArt>),
Launch,
}
pub struct Params<'a> {
pub globals: &'a WguiGlobals,
pub executor: AsyncExecutor,
pub manifest: AppManifest,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub frontend_tasks: &'a FrontendTasks,
pub on_launched: Box<dyn Fn()>,
}
pub struct View {
#[allow(dead_code)]
state: ParserState,
tasks: Tasks<Task>,
on_launched: Box<dyn Fn()>,
frontend_tasks: FrontendTasks,
game_cover_view_common: game_cover::ViewCommon,
view_cover: game_cover::View,
app_id: AppID,
}
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 {
return;
};
tasks.push(Task::FillAppDetails(details));
}
pub fn new(params: Params) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/view/game_launcher.xml"),
extra: Default::default(),
};
let state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
{
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "label_title")?;
label_title.set_text_simple(
&mut params.globals.get(),
Translation::from_raw_text(&params.manifest.name),
);
}
let tasks = Tasks::new();
// fetch details from the web
let fut = View::fetch_details(params.executor.clone(), tasks.clone(), params.manifest.app_id.clone());
params.executor.spawn(fut).detach();
let id_cover_art_parent = state.get_widget_id("cover_art_parent")?;
let btn_launch = state.fetch_component_as::<ComponentButton>("btn_launch")?;
tasks.handle_button(&btn_launch, Task::Launch);
let view_cover = game_cover::View::new(game_cover::Params {
ess: &mut ConstructEssentials {
layout: params.layout,
parent: id_cover_art_parent,
},
executor: &params.executor,
manifest: &params.manifest,
on_loaded: {
let tasks = tasks.clone();
Box::new(move |cover_art| {
tasks.push(Task::SetCoverArt(Rc::new(cover_art)));
})
},
scale: 1.5,
})?;
Ok(Self {
state,
tasks,
on_launched: params.on_launched,
frontend_tasks: params.frontend_tasks.clone(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
view_cover,
app_id: params.manifest.app_id.clone(),
})
}
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;
if let Some(developer) = details.developers.pop() {
label_author
.cast::<WidgetLabel>()?
.set_text(&mut c.common(), Translation::from_raw_text_string(developer));
}
let desc = if let Some(desc) = &details.short_description {
Some(desc)
} else if let Some(desc) = &details.detailed_description {
Some(desc)
} else {
None
};
if let Some(desc) = desc {
label_description
.cast::<WidgetLabel>()?
.set_text(&mut c.common(), Translation::from_raw_text(desc));
}
}
c.finish()?;
Ok(())
}
fn action_launch(&mut self) {
match steam_utils::launch(&self.app_id) {
Ok(_) => {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_translation_key(
"GAME_LAUNCHED",
)));
self.frontend_tasks.push(FrontendTask::PlaySound(SoundType::Launch));
}
Err(e) => {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Failed to launch: {:?}",
e
))));
}
}
(*self.on_launched)();
}
}

View File

@@ -0,0 +1,269 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use wgui::{
assets::AssetPath,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::{
ConstructEssentials,
label::{WidgetLabel, WidgetLabelParams},
},
};
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
cached_fetcher::CoverArt,
popup_manager::{MountPopupParams, PopupHandle},
steam_utils::{self, AppID, AppManifest, SteamUtils},
various::AsyncExecutor,
},
views::{self, game_cover, game_launcher},
};
#[derive(Clone)]
enum Task {
AppManifestClicked(steam_utils::AppManifest),
SetCoverArt(AppID, Rc<CoverArt>),
CloseLauncher,
Refresh,
}
pub struct Params<'a> {
pub globals: WguiGlobals,
pub executor: AsyncExecutor,
pub frontend_tasks: FrontendTasks,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
}
pub struct Cell {
view_cover: game_cover::View,
manifest: AppManifest,
}
struct State {
view_launcher: Option<(PopupHandle, views::game_launcher::View)>,
}
pub struct View {
#[allow(dead_code)]
parser_state: ParserState,
tasks: Tasks<Task>,
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>>,
}
impl View {
pub fn new(params: Params) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/view/game_list.xml"),
extra: Default::default(),
};
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 tasks = Tasks::new();
let steam_utils = SteamUtils::new()?;
tasks.push(Task::Refresh);
Ok(Self {
parser_state,
tasks,
frontend_tasks: params.frontend_tasks,
globals: params.globals.clone(),
id_list_parent: list_parent.id,
steam_utils,
cells: 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,
})
}
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,
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
for manifest in &games.manifests {
let on_loaded = {
let app_id = manifest.app_id.clone();
let tasks = tasks.clone();
Box::new(move |cover_art: CoverArt| {
tasks.push(Task::SetCoverArt(app_id, Rc::from(cover_art)));
})
};
let view_cover = game_cover::View::new(game_cover::Params {
ess,
executor,
manifest,
on_loaded,
scale: 1.0,
})?;
view_cover.button.on_click({
let tasks = tasks.clone();
let manifest = manifest.clone();
Box::new(move |_, _| {
tasks.push(Task::AppManifestClicked(manifest.clone()));
Ok(())
})
});
cells.insert(
manifest.app_id.clone(),
Cell {
view_cover,
manifest: manifest.clone(),
},
);
}
Ok(())
}
impl View {
fn game_list(&self) -> anyhow::Result<Games> {
let manifests = self
.steam_utils
.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)?;
Ok(Games { manifests })
}
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))),
}
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(),
)?;
}
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 {
return;
};
if let Err(e) = cell
.view_cover
.set_cover_art(&mut self.game_cover_view_common, layout, &cover_art)
{
log::error!("{:?}", e);
};
}
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(())
})
},
}));
Ok(())
}
}

View File

@@ -1,2 +1,8 @@
pub mod app_launcher; pub mod app_launcher;
pub mod audio_settings; pub mod audio_settings;
pub mod game_cover;
pub mod game_launcher;
pub mod game_list;
pub mod process_list;
pub mod window_list;
pub mod window_options;

View File

@@ -0,0 +1,255 @@
use std::rc::Rc;
use wayvr_ipc::packet_server::{self};
use wgui::{
assets::AssetPath,
components::{
self,
button::ComponentButton,
tooltip::{TooltipInfo, TooltipSide},
},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
taffy::{self, prelude::length},
task::Tasks,
widget::{
ConstructEssentials,
div::WidgetDiv,
label::{WidgetLabel, WidgetLabelParams},
},
};
use wlx_common::{dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
use crate::util::{self, various::get_desktop_file_icon_path};
#[derive(Clone)]
enum Task {
Refresh,
TerminateProcess(packet_server::WvrProcess),
}
pub struct Params<'a> {
pub globals: WguiGlobals,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
}
pub struct View {
#[allow(dead_code)]
pub parser_state: ParserState,
tasks: Tasks<Task>,
globals: WguiGlobals,
id_list_parent: WidgetID,
}
impl View {
pub fn new(params: Params) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/view/process_list.xml"),
extra: Default::default(),
};
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 tasks = Tasks::new();
tasks.push(Task::Refresh);
Ok(Self {
parser_state,
tasks,
globals: params.globals,
id_list_parent: list_parent.id,
})
}
pub fn update<T>(
&mut self,
layout: &mut Layout,
interface: &mut BoxDashInterface<T>,
data: &mut T,
) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::Refresh => self.refresh(layout, interface, data)?,
Task::TerminateProcess(process) => self.action_terminate_process(interface, data, process)?,
}
}
}
Ok(())
}
}
fn get_desktop_entry_from_process(process: &packet_server::WvrProcess) -> Option<DesktopEntry> {
// TODO: refactor this after we ditch old wayvr-dashboard completely
let Some(dfile_str) = process.userdata.get("desktop-entry") else {
return None;
};
let Ok(desktop_file) = serde_json::from_str::<DesktopEntry>(dfile_str) else {
debug_assert!(false); // invalid json???
return None;
};
Some(desktop_file)
}
struct ProcessEntryResult {
btn_terminate: Rc<ComponentButton>,
}
fn construct_process_entry(
ess: &mut ConstructEssentials,
globals: &WguiGlobals,
process: &packet_server::WvrProcess,
) -> anyhow::Result<ProcessEntryResult> {
let (cell, _) = ess.layout.add_child(
ess.parent,
WidgetDiv::create(),
taffy::Style {
flex_direction: taffy::FlexDirection::Row,
align_items: Some(taffy::AlignItems::Center),
gap: length(8.0),
..Default::default()
},
)?;
let text_terminate_process = Translation::from_raw_text_string(globals.i18n().translate_and_replace(
"PROCESS_LIST.TERMINATE_PROCESS_NAMED_X",
("{PROCESS_NAME}", &process.name),
));
//"Terminate process" button
let (_, btn_terminate) = components::button::construct(
&mut ConstructEssentials {
layout: ess.layout,
parent: cell.id,
},
components::button::Params {
sprite_src: Some(AssetPath::BuiltIn("dashboard/remove_circle.svg")),
tooltip: Some(TooltipInfo {
text: text_terminate_process,
side: TooltipSide::Right,
}),
..Default::default()
},
)?;
if let Some(desktop_file) = get_desktop_entry_from_process(process) {
// desktop file icon and process name
util::various::mount_simple_sprite_square(
globals,
ess.layout,
cell.id,
24.0,
get_desktop_file_icon_path(&desktop_file).as_ref(),
)?;
util::various::mount_simple_label(
globals,
ess.layout,
cell.id,
Translation::from_raw_text_rc(desktop_file.app_name.clone()),
)?;
} else {
// just show a process name
util::various::mount_simple_label(
globals,
ess.layout,
cell.id,
Translation::from_raw_text_string(process.name.clone()),
)?;
}
Ok(ProcessEntryResult { btn_terminate })
}
fn fill_process_list(
globals: &WguiGlobals,
ess: &mut ConstructEssentials,
tasks: &Tasks<Task>,
list: &Vec<packet_server::WvrProcess>,
) -> anyhow::Result<()> {
for process_entry in list {
let entry_res = construct_process_entry(ess, globals, process_entry)?;
entry_res.btn_terminate.on_click({
let tasks = tasks.clone();
let entry = process_entry.clone();
Box::new(move |_, _| {
tasks.push(Task::TerminateProcess(entry.clone()));
Ok(())
})
});
}
Ok(())
}
impl View {
fn refresh<T>(
&mut self,
layout: &mut Layout,
interface: &mut BoxDashInterface<T>,
data: &mut T,
) -> anyhow::Result<()> {
layout.remove_children(self.id_list_parent);
let mut text: Option<Translation> = None;
match interface.process_list(data) {
Ok(list) => {
if list.is_empty() {
text = Some(Translation::from_translation_key("PROCESS_LIST.NO_PROCESSES_FOUND"))
} else {
fill_process_list(
&self.globals,
&mut ConstructEssentials {
layout,
parent: self.id_list_parent,
},
&self.tasks,
&list,
)?;
}
}
Err(e) => text = Some(Translation::from_raw_text(&format!("Error: {:?}", e))),
}
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(),
)?;
}
Ok(())
}
fn action_terminate_process<T>(
&mut self,
interface: &mut BoxDashInterface<T>,
data: &mut T,
process: packet_server::WvrProcess,
) -> anyhow::Result<()> {
interface.process_terminate(data, process.handle)?;
self.tasks.push(Task::Refresh);
Ok(())
}
}

View File

@@ -0,0 +1,290 @@
use std::{cell::RefCell, rc::Rc};
use wayvr_ipc::packet_server::{self, WvrWindowHandle};
use wgui::{
assets::AssetPath,
components::{self, button::ComponentButton},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID, WidgetPair},
parser::{Fetchable, ParseDocumentParams, ParserState},
renderer_vk::text::{FontWeight, HorizontalAlign, TextStyle},
taffy::{self, prelude::length},
task::Tasks,
widget::{
ConstructEssentials,
label::{WidgetLabel, WidgetLabelParams},
},
};
use wlx_common::dash_interface::BoxDashInterface;
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::popup_manager::{MountPopupParams, PopupHandle},
views::window_options,
};
#[derive(Clone)]
enum Task {
WindowClicked(packet_server::WvrWindow),
WindowOptionsFinish,
Refresh,
}
pub struct Params<'a> {
pub globals: WguiGlobals,
pub frontend_tasks: FrontendTasks,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub on_click: Option<Box<dyn Fn(WvrWindowHandle)>>,
}
struct State {
view_window_options: Option<(PopupHandle, window_options::View)>,
}
pub struct View {
#[allow(dead_code)]
pub parser_state: ParserState,
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
globals: WguiGlobals,
state: Rc<RefCell<State>>,
id_list_parent: WidgetID,
on_click: Option<Box<dyn Fn(WvrWindowHandle)>>,
}
impl View {
pub fn new(params: Params) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/view/window_list.xml"),
extra: Default::default(),
};
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 tasks = Tasks::new();
tasks.push(Task::Refresh);
let state = Rc::new(RefCell::new(State {
view_window_options: None,
}));
Ok(Self {
parser_state,
tasks,
frontend_tasks: params.frontend_tasks,
globals: params.globals.clone(),
state,
id_list_parent: list_parent.id,
on_click: params.on_click,
})
}
pub fn update<T>(
&mut self,
layout: &mut Layout,
interface: &mut BoxDashInterface<T>,
data: &mut T,
) -> anyhow::Result<()> {
loop {
let tasks = self.tasks.drain();
if tasks.is_empty() {
break;
}
for task in tasks {
match task {
Task::WindowClicked(display) => self.action_window_clicked(display)?,
Task::WindowOptionsFinish => self.action_window_options_finish(),
Task::Refresh => self.refresh(layout, interface, data)?,
}
}
}
let mut state = self.state.borrow_mut();
if let Some((_, view)) = &mut state.view_window_options {
view.update(layout, interface, data)?;
}
Ok(())
}
}
pub fn construct_window_button<T>(
ess: &mut ConstructEssentials,
interface: &mut BoxDashInterface<T>,
data: &mut T,
globals: &WguiGlobals,
window: &packet_server::WvrWindow,
) -> anyhow::Result<(WidgetPair, Rc<ComponentButton>)> {
let aspect = window.size_x as f32 / window.size_y as f32;
let height = 96.0;
let width = height * aspect;
let accent_color = globals.defaults().accent_color;
let (widget_button, button) = components::button::construct(
ess,
components::button::Params {
color: Some(accent_color.with_alpha(0.2)),
border_color: Some(accent_color),
style: taffy::Style {
align_items: Some(taffy::AlignItems::Center),
justify_content: Some(taffy::JustifyContent::Center),
size: taffy::Size {
width: length(width),
height: length(height),
},
..Default::default()
},
..Default::default()
},
)?;
let process_name = match interface.process_get(data, window.process_handle.clone()) {
Some(process) => process.name.clone(),
None => String::from("Unknown"),
};
let label_name = WidgetLabel::create(
&mut globals.get(),
WidgetLabelParams {
content: Translation::from_raw_text(&process_name),
style: TextStyle {
weight: Some(FontWeight::Bold),
wrap: true,
align: Some(HorizontalAlign::Center),
..Default::default()
},
},
);
let label_resolution = WidgetLabel::create(
&mut globals.get(),
WidgetLabelParams {
content: Translation::from_raw_text(""),
..Default::default()
},
);
ess.layout.add_child(widget_button.id, label_name, Default::default())?;
ess
.layout
.add_child(widget_button.id, label_resolution, Default::default())?;
Ok((widget_button, button))
}
fn fill_window_list<T>(
globals: &WguiGlobals,
ess: &mut ConstructEssentials,
interface: &mut BoxDashInterface<T>,
data: &mut T,
list: Vec<packet_server::WvrWindow>,
tasks: &Tasks<Task>,
) -> anyhow::Result<()> {
for entry in list {
let (_, button) = construct_window_button(ess, interface, data, globals, &entry)?;
button.on_click({
let tasks = tasks.clone();
Box::new(move |_, _| {
tasks.push(Task::WindowClicked(entry.clone()));
Ok(())
})
});
}
Ok(())
}
impl View {
fn action_window_options_finish(&mut self) {
self.state.borrow_mut().view_window_options = None;
self.tasks.push(Task::Refresh);
}
fn refresh<T>(
&mut self,
layout: &mut Layout,
interface: &mut BoxDashInterface<T>,
data: &mut T,
) -> anyhow::Result<()> {
layout.remove_children(self.id_list_parent);
let mut text: Option<Translation> = None;
match interface.window_list(data) {
Ok(list) => {
if list.is_empty() {
text = Some(Translation::from_translation_key("NO_WINDOWS_FOUND"))
} else {
fill_window_list(
&self.globals,
&mut ConstructEssentials {
layout,
parent: self.id_list_parent,
},
interface,
data,
list,
&self.tasks,
)?
}
}
Err(e) => text = Some(Translation::from_raw_text(&format!("Error: {:?}", e))),
}
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(),
)?;
}
Ok(())
}
fn action_window_clicked(&mut self, window: packet_server::WvrWindow) -> anyhow::Result<()> {
if let Some(on_click) = &mut self.on_click {
(*on_click)(window.handle);
} else {
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_translation_key("WINDOW_OPTIONS"),
on_content: {
let frontend_tasks = self.frontend_tasks.clone();
let globals = self.globals.clone();
let state = self.state.clone();
let tasks = self.tasks.clone();
//TODO
Rc::new(move |data| {
// state.borrow_mut().view_window_options = Some((
// data.handle,
// window_options::View::new(window_options::Params {
// globals: globals.clone(),
// layout: data.layout,
// parent_id: data.id_content,
// on_submit: tasks.make_callback(Task::WindowOptionsFinish),
// window: window.clone(),
// frontend_tasks: frontend_tasks.clone(),
// })?,
// ));
Ok(())
})
},
}));
}
Ok(())
}
}

View File

@@ -0,0 +1,162 @@
use anyhow::Context;
use std::rc::Rc;
use wayvr_ipc::packet_server;
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::ConstructEssentials,
};
use wlx_common::dash_interface::BoxDashInterface;
use crate::{
frontend::{FrontendTask, FrontendTasks},
views::window_list::construct_window_button,
};
#[derive(Clone)]
enum Task {
SetVisible(bool),
Kill,
Close,
}
pub struct View {
#[allow(dead_code)]
pub state: ParserState,
tasks: Tasks<Task>,
frontend_tasks: FrontendTasks,
window: packet_server::WvrWindow,
on_submit: Rc<dyn Fn()>,
}
pub struct Params<'a, T> {
pub globals: WguiGlobals,
pub frontend_tasks: FrontendTasks,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub on_submit: Rc<dyn Fn()>,
pub window: packet_server::WvrWindow,
pub interface: &'a mut BoxDashInterface<T>,
pub data: &'a mut T,
}
impl View {
pub fn new<T>(params: Params<T>) -> anyhow::Result<Self> {
let doc_params = &ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/view/window_options.xml"),
extra: Default::default(),
};
let state = wgui::parser::parse_from_assets(doc_params, params.layout, params.parent_id)?;
let tasks = Tasks::new();
let window_parent = state.get_widget_id("window_parent")?;
let btn_close = state.fetch_component_as::<ComponentButton>("btn_close")?;
let btn_kill = state.fetch_component_as::<ComponentButton>("btn_kill")?;
let btn_show_hide = state.fetch_component_as::<ComponentButton>("btn_show_hide")?;
construct_window_button(
&mut ConstructEssentials {
layout: params.layout,
parent: window_parent,
},
params.interface,
params.data,
&params.globals,
&params.window,
)?;
{
let mut c = params.layout.start_common();
btn_show_hide.set_text(
&mut c.common(),
Translation::from_translation_key(if params.window.visible { "HIDE" } else { "SHOW" }),
);
c.finish()?;
}
tasks.handle_button(&btn_close, Task::Close);
tasks.handle_button(&btn_kill, Task::Kill);
tasks.handle_button(&btn_show_hide, Task::SetVisible(!params.window.visible));
Ok(Self {
state,
tasks,
window: params.window,
frontend_tasks: params.frontend_tasks,
on_submit: params.on_submit,
})
}
pub fn update<T>(
&mut self,
_layout: &mut Layout,
interface: &mut BoxDashInterface<T>,
data: &mut T,
) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::SetVisible(v) => self.action_set_visible(interface, data, v),
Task::Close => self.action_close(interface, data),
Task::Kill => self.action_kill(interface, data),
}
}
Ok(())
}
}
impl View {
fn action_set_visible<T>(&mut self, interface: &mut BoxDashInterface<T>, data: &mut T, visible: bool) {
if let Err(e) = interface.window_set_visible(data, self.window.handle.clone(), visible) {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Failed to set window visibility: {:?}",
e
))));
};
(*self.on_submit)();
}
fn action_close<T>(&mut self, interface: &mut BoxDashInterface<T>, data: &mut T) {
if let Err(e) = interface.window_request_close(data, self.window.handle.clone()) {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Failed to close window: {:?}",
e
))));
};
(*self.on_submit)();
}
fn action_kill_process<T>(&mut self, interface: &mut BoxDashInterface<T>, data: &mut T) -> anyhow::Result<()> {
let process = interface
.process_get(data, self.window.process_handle.clone())
.context("Process not found")?;
interface.process_terminate(data, process.handle)?;
Ok(())
}
fn action_kill<T>(&mut self, interface: &mut BoxDashInterface<T>, data: &mut T) {
if let Err(e) = self.action_kill_process(interface, data) {
self
.frontend_tasks
.push(FrontendTask::PushToast(Translation::from_raw_text_string(format!(
"Failed to kill process: {:?}",
e
))));
};
(*self.on_submit)();
}
}

View File

@@ -1 +1,2 @@
style_edition = "2024" style_edition = "2024"
edition = "2024"

View File

@@ -5,12 +5,16 @@ Glossary:
- wlx-overlay-s: The name of this software (also called WlxOverlay-S) - wlx-overlay-s: The name of this software (also called WlxOverlay-S)
- WayVR: A Wayland compositor intended to be used in VR - WayVR: A Wayland compositor intended to be used in VR
- WayVR Dashboard: An application (and game) launcher which is displayed in front of the user - WayVR Dashboard: An application (and game) launcher which is displayed in front of the user
- Monado: A VR compositor
- OpenVR: API made by Valve - OpenVR: API made by Valve
- OpenXR: API made by Khronos - OpenXR: API made by Khronos
- OSC: OpenSoundControl - OSC: OpenSoundControl
- Playspace: A designated real area where users can interact with the virtual environment - Playspace: A designated real area where users can interact with the virtual environment
- Space-drag: A feature which allows the user to move their HMD origin position via a controller - Space-drag: A feature which allows the user to move their HMD origin position via a controller
- Passthrough: Some headsets have built-in cameras for displaying image on the HMD screen - Passthrough: Some headsets have built-in cameras for displaying image on the HMD screen
End of glossary; - Display: A virtual monitor located in WayVR environment. It has a specific resolution and it contains virtual windows.
- Set: A list of virtual overlays you can interact with
- Watch: An overlay anchored to your left hand in which you can display a dashboard, see your current date and time or toggle your sets.
End of glossary.
You will be given the input in the code blocks which needs to be translated to {TARGET_LANG} language. Write only the result in codeblocks, do not explain. Keep any newlines and other important formatting-required identifiers as-is. You will be given the input in the code blocks which needs to be translated to {TARGET_LANG} language. Write only the result in codeblocks, do not explain. Keep any newlines and other important formatting-required identifiers as-is, in the same state.

View File

@@ -15,3 +15,4 @@ winit = "0.30.12"
vulkano = { workspace = true } vulkano = { workspace = true }
vulkano-shaders = { workspace = true } vulkano-shaders = { workspace = true }
dash-frontend = { path = "../dash-frontend/" } dash-frontend = { path = "../dash-frontend/" }
wlx-common = { path = "../wlx-common" }

View File

@@ -9,6 +9,15 @@
align_self="baseline" align_self="baseline"
align_items="baseline" /> align_items="baseline" />
<blueprint name="my_context_menu">
<context_menu>
<cell translation="TESTBED.HELLO_WORLD" action="first" _custom1="foo" />
<cell text="Second button" action="second" _custom1="bar" />
<cell text="Third button" action="third" />
<cell text="Foobar test test test" action="foobar" />
</context_menu>
</blueprint>
<elements> <elements>
<rectangle position="absolute" width="100%" height="100%" color="#1e3a3eee" /> <rectangle position="absolute" width="100%" height="100%" color="#1e3a3eee" />
<div <div
@@ -25,8 +34,8 @@
<rectangle macro="rect"> <rectangle macro="rect">
<label id="label_current_option" text="Click any of these buttons" size="20" weight="bold" /> <label id="label_current_option" text="Click any of these buttons" size="20" weight="bold" />
<div gap="4"> <div gap="4">
<Button id="button_red" text="Red button" width="150" height="32" color="#FF0000" tooltip="I'm at the top" tooltip_side="top" /> <Button id="button_red" text="Red button" width="150" height="32" color="#FF0000" tooltip_str="I'm at the top" tooltip_side="top" />
<Button id="button_aqua" text="Aqua button" width="150" height="32" color="#00FFFF" tooltip="I'm at the bottom" tooltip_side="bottom" /> <Button id="button_aqua" text="Aqua button" width="150" height="32" color="#00FFFF" tooltip_str="I'm at the bottom" tooltip_side="bottom" />
<Button id="button_yellow" text="Yellow button" width="150" height="32" color="#FFFF00" tooltip="TESTBED.HELLO_WORLD" tooltip_side="right" /> <Button id="button_yellow" text="Yellow button" width="150" height="32" color="#FFFF00" tooltip="TESTBED.HELLO_WORLD" tooltip_side="right" />
</div> </div>
<div gap="4"> <div gap="4">
@@ -50,6 +59,11 @@
</div> </div>
</rectangle> </rectangle>
<rectangle macro="rect">
<label text="Context menu test" />
<Button id="button_context_menu" text="Show context menu" />
</rectangle>
<rectangle macro="rect"> <rectangle macro="rect">
<label text="visibility test" weight="bold" /> <label text="visibility test" weight="bold" />
<CheckBox id="cb_visible" height="24" text="visible" /> <CheckBox id="cb_visible" height="24" text="visible" />

View File

@@ -0,0 +1 @@
../../../wlx-overlay-s/src/assets/sound/wgui_button_press.mp3

View File

@@ -0,0 +1 @@
../../../wlx-overlay-s/src/assets/sound/wgui_button_release.mp3

View File

@@ -0,0 +1 @@
../../../wlx-overlay-s/src/assets/sound/wgui_checkbox_check.mp3

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