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
run: |
../.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
run: |
cd ../wayvrctl
@@ -80,24 +71,14 @@ jobs:
asset_name: wayvrctl
asset_content_type: application/octet-stream
- name: Upload AppImage (Full)
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)
- 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-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
- 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
cargo build --release --no-default-features --features=openvr,openxr,wayland,x11,osc
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
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
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]
inherits = "release"
debug = true
@@ -26,6 +37,7 @@ resolver = "3"
anyhow = "1.0.100"
glam = { version = "0.30.9", features = ["mint", "serde"] }
clap = { version = "4.5.53", features = ["derive"] }
xdg = "3.0.0"
idmap = "0.2.2"
idmap-derive = "0.2.22"
log = "0.4.29"
@@ -34,6 +46,7 @@ rust-embed = "8.9.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0.145"
slotmap = "1.1.1"
strum = { version = "0.27.2", features = ["derive"] }
vulkano = { version = "0.35.2", default-features = false, features = [
"macros",
] }

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.**
In case screens were selected in the wrong order:
- `rm ~/.config/wlxoverlay/conf.d/pw_tokens.yaml` then restart
- 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.
**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
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).
- DPI scaling is 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
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.
Hiding the keyboard will un-press all of its buttons. Alternatively, go to Settings and use the `Restart software` button.
### X11 limitations

View File

@@ -4,14 +4,27 @@ version = "0.1.0"
edition = "2024"
[dependencies]
anyhow.workspace = true
wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
wgui = { path = "../wgui/" }
wlx-common = { path = "../wlx-common" }
anyhow.workspace = true
glam = { workspace = true, features = ["mint", "serde"] }
log.workspace = true
xdg.workspace = true
rust-embed.workspace = true
chrono = "0.4.42"
gio = "0.21.5"
gtk = "0.18.2"
serde.workspace = true
serde = { workspace = true, features = ["rc"] }
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"
color="#44444400"
hover_color="#333333ff"
border_color="#00000000"
hover_border_color="#555555ff"
tooltip="${tooltip}"
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>
</template>
<elements>
<!-- background for testing -->
<!-- <rectangle position="absolute" color="#333333" width="100%" height="100%" /> -->
<!-- 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 -->
<div id="menu"
width="~size_size"
width="~side_size"
min_width="~side_size"
max_width="~side_size"
height="100%"
@@ -46,13 +44,13 @@
align_items="center"
gap="4"
>
<SideButton id="btn_side_home" src="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_games" src="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_processes" src="dashboard/window.svg" tooltip="PROCESSES" 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_builtin="dashboard/apps.svg" tooltip="APPLICATIONS" tooltip_side="right" />
<SideButton id="btn_side_games" src_builtin="dashboard/games.svg" tooltip="GAMES" tooltip_side="right" />
<SideButton id="btn_side_monado" src_builtin="dashboard/monado.svg" tooltip="MONADO_RUNTIME" tooltip_side="right" />
<SideButton id="btn_side_processes" src_builtin="dashboard/window.svg" tooltip="PROCESSES" tooltip_side="right" />
<rectangle height="2" color="#FFFFFF33" width="~side_sprite_size" />
<SideButton id="btn_side_settings" src="dashboard/settings.svg" tooltip="SETTINGS" tooltip_side="right" />
<SideButton id="btn_side_settings" src_builtin="dashboard/settings.svg" tooltip="SETTINGS" tooltip_side="right" />
</rectangle>
</div>
<!-- REST -->
@@ -69,7 +67,7 @@
<rectangle
id="rect_content"
color2="#0d131a00"
color="#24417900"
color="#252f5300"
gradient="vertical"
round="8"
flex_grow="1"
@@ -79,7 +77,7 @@
<!-- radial gradient -->
<rectangle
position="absolute" width="100%" height="100%"
gradient="radial" color="#44BBFF22" color2="#00000000" />
gradient="radial" color="#44BBFF11" color2="#00000000" />
<div
id="content"
@@ -101,6 +99,7 @@
</rectangle>
<!-- BOTTOM PANEL -->
<rectangle
consume_mouse_events="1"
width="100%"
height="48"
min_height="48"
@@ -124,16 +123,16 @@
<!-- top shine -->
<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>
<!-- Left bottom side -->
<div margin_left="8">
<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 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>
</div>
@@ -145,4 +144,4 @@
</div>
</div>
</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 -->
<template name="GroupBoxTitle">
<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" />
</div>
<rectangle color="#FFFFFF44" width="100%" height="2" />

View File

@@ -15,7 +15,7 @@
align_items="center"
justify_content="center"
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}" />
</div>
</Button>

View File

@@ -1,40 +1,50 @@
<layout>
<include src="t_tab_title.xml" />
<include src="../theme.xml" />
<template name="AppEntry">
<Button
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>
<sprite src="${src}" src_ext="${src_ext}" width="64" height="64" />
</div>
<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>
</Button>
</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>
<TabTitle translation="APPLICATIONS" icon="dashboard/apps.svg" />
<!-- placeholders for now -->
<!--
<div gap="4" align_items="center">
<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 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>
<sprite src="dashboard/search.svg" width="24" height="24" />
<!-- placeholder editbox -->
<sprite src_builtin="dashboard/search.svg" width="24" height="24" />
<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" />
</rectangle>
</div>
-->
<div
id="app_list_parent"
flex_direction="row"
flex_wrap="wrap"
justify_content="center"
gap="4"
overflow_y="scroll"
/>

View File

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

View File

@@ -8,7 +8,7 @@
align_items="center"
flex_grow="1"
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" />
<!-- main button list -->

View File

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

View File

@@ -2,6 +2,9 @@
<include src="t_tab_title.xml" />
<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>
</layout>

View File

@@ -1,61 +1,41 @@
<layout>
<include src="t_tab_title.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>
<TabTitle translation="SETTINGS" icon="dashboard/settings.svg" />
<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>
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" />
</elements>
</layout>
</layout>

View File

@@ -2,7 +2,7 @@
<!-- translation, icon -->
<template name="TabTitle">
<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" />
</div>
</template>

View File

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

View File

@@ -29,7 +29,7 @@
<template name="SelectAudioProfileText">
<div align_items="center" gap="8">
<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>
<label translation="AUDIO.SELECT_AUDIO_CARD_PROFILE" size="14" weight="bold" />
</div>
@@ -54,15 +54,15 @@
<div flex_direction="row" gap="4">
<Button
id="btn_auto"
sprite_src="dashboard/magic_wand.svg"
sprite_src_builtin="dashboard/magic_wand.svg"
min_width="32"
tooltip="AUDIO.AUTO_SWITCH_TO_VR_AUDIO"
color="~color_accent"
tooltip_side="right" />
<BottomButton id="btn_sinks" src="dashboard/volume.svg" translation="AUDIO.SPEAKERS" />
<BottomButton id="btn_sources" src="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" />
<BottomButton id="btn_cards" src="dashboard/cpu.svg" translation="AUDIO.CARDS" />
<BottomButton id="btn_sinks" src_builtin="dashboard/volume.svg" translation="AUDIO.SPEAKERS" />
<BottomButton id="btn_sources" src_builtin="dashboard/microphone.svg" translation="AUDIO.MICROPHONES" />
<BottomButton id="btn_cards" src_builtin="dashboard/cpu.svg" translation="AUDIO.CARDS" />
</div>
</elements>
</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
position="relative"
color="#000000"
round="4"
width="100%" height="48"
>
<!-- 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 -->
<div gap="16" align_items="center">
<!-- Back button -->
<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>
<!-- Title -->
@@ -34,9 +33,9 @@
</rectangle>
<!-- Content -->
<rectangle width="100%" height="100%"
color="#010310ee"
color2="#062a5eee"
<rectangle height="100%"
color="#010310fe"
color2="#051c55fc"
gradient="vertical"
padding="16"
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",
"MONADO_RUNTIME": "Monado-Laufzeitumgebung",
"MONADO_RUNTIME": "Monado-Laufzeitumgebung",
"APPLICATIONS": "Anwendungen",
"GAMES": "Spiele",
"SETTINGS": "Einstellungen",
@@ -11,21 +11,65 @@
"APP_SETTINGS": {
"HIDE_USERNAME": "Benutzernamen ausblenden",
"OPAQUE_BACKGROUND": "Undurchsichtiger Hintergrund",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "Standardmäßig in XWayland-Modus ausführen",
"WLX_OVERLAY_S_SETTINGS": "WlxOverlay-S Einstellungen",
"HEADSET_SETTINGS": "Headset-Einstellungen",
"BRIGHTNESS": "Helligkeit",
"WLX": {
"NOTIFICATIONS_ENABLED": "Benachrichtigungen aktiviert",
"NOTIFICATIONS_SOUND_ENABLED": "Benachrichtigungssound aktiviert",
"KEYBOARD_SOUND_ENABLED": "Tastaturgeräusch aktiviert",
"BLOCK_GAME_INPUT": "Spielsteuerung blockieren",
"SPACE_DRAG_MULTIPLIER": "Raum-Drag-Multiplikator",
"SPACE_DRAG_ROTATION_ENABLED": "Rotation im Space-Drag aktivieren",
"SHOW_SKYBOX": "Skybox anzeigen",
"ENABLE_PASSTHROUGH": "Passthrough aktivieren"
},
"RESTART_SOFTWARE": "Software neu starten"
"WLX": {},
"LOOK_AND_FEEL": "Aussehen und Verhalten",
"HIDE_GRAB_HELP": "Greif-Hilfe ausblenden",
"ANIMATION_SPEED": "UI-Animationsgeschwindigkeit",
"ROUND_MULTIPLIER": "UI-Kantenrundung",
"USE_SKYBOX": "Skybox aktivieren",
"USE_PASSTHROUGH": "Passthrough aktivieren",
"CLOCK_12H": "12-Stunden-Uhr",
"FEATURES": "Funktionen",
"NOTIFICATIONS_ENABLED": "Benachrichtigungen aktivieren",
"NOTIFICATIONS_SOUND_ENABLED": "Benachrichtigungstöne",
"KEYBOARD_SOUND_ENABLED": "Tastengeräusche",
"SPACE_DRAG_MULTIPLIER": "Raum-Drag-Multiplikator",
"SPACE_DRAG_UNLOCKED": "Erlaube Space-Drag auf allen Achsen",
"SPACE_ROTATE_UNLOCKED": "Erlaube Drehungen in allen Achsen",
"BLOCK_GAME_INPUT": "Spieleingabe blockieren",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignoriere Watch beim Blockieren der Eingabe",
"CONTROLS": "Steuerung",
"FOCUS_FOLLOWS_MOUSE_MODE": "Mausbewegung bei Triggerberührung",
"LEFT_HANDED_MOUSE": "Linkshändige Maus",
"ALLOW_SLIDING": "Stabinteraktion während des Greifens",
"INVERT_SCROLL_DIRECTION_X": "Horizontale Scrollrichtung umkehren",
"INVERT_SCROLL_DIRECTION_Y": "Vertikale Bildlaufrichtung umkehren",
"SCROLL_SPEED": "Scrollgeschwindigkeit",
"LONG_PRESS_DURATION": "Dauer für lange Drückvorgänge",
"POINTER_LERP_FACTOR": "Zeigerglättung",
"XR_CLICK_SENSITIVITY": "XR-Klicksensitivität",
"XR_CLICK_SENSITIVITY_RELEASE": "XR-Loslassempfindlichkeit",
"CLICK_FREEZE_TIME_MS": "Klick-Freeze-Zeit (ms)",
"MISC": "Verschiedenes",
"XWAYLAND_BY_DEFAULT": "Standardmäßig Apps im Kompatibilitätsmodus ausführen",
"UPRIGHT_SCREEN_FIX": "Bildschirm-Drehkorrektur",
"DOUBLE_CURSOR_FIX": "Doppelter Cursor Fix",
"SCREEN_RENDER_DOWN": "Bildschirm bei niedrigerer Auflösung rendern",
"UPRIGHT_SCREEN_FIX_HELP": "Behebt hochstehende Bildschirme auf einigen Desktops",
"DOUBLE_CURSOR_FIX_HELP": "Aktivieren Sie dies, wenn Sie 2 Cursor sehen",
"XR_CLICK_SENSITIVITY_HELP": "Analoge Trigger-Empfindlichkeit",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Muss niedriger als Klick sein",
"CLICK_FREEZE_TIME_MS_HELP": "Hilft bei der Präzision von Doppelklicks",
"LEFT_HANDED_MOUSE_HELP": "Verwenden Sie diese Option, wenn die Maustasten vertauscht sind",
"BLOCK_GAME_INPUT_HELP": "Blockiert alle Eingaben, wenn ein Overlay angefahren wird",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Blockiere die Eingabe nicht, wenn die Überwachung aktiviert ist",
"USE_SKYBOX_HELP": "Zeige einen Skybox, wenn keine Szenen-App oder Passthrough vorhanden ist",
"USE_PASSTHROUGH_HELP": "Aktiviere Passthrough, falls die XR-Laufzeitumgebung dies unterstützt",
"SCREEN_RENDER_DOWN_HELP": "Hilft bei Aliasing auf hochauflösenden Bildschirmen",
"SETS_ON_WATCH": "Sets auf der Watch",
"TROUBLESHOOTING": "Fehlerbehebung",
"CLEAR_SAVED_STATE": "Gespeicherten Zustand löschen",
"CLEAR_PIPEWIRE_TOKENS": "PipeWire Tokens löschen",
"DELETE_ALL_CONFIGS": "Konfiguration löschen",
"RESTART_SOFTWARE": "Software neu starten",
"CLEAR_SAVED_STATE_HELP": "Sets und Overlay-Positionen zurücksetzen",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Bildschirmauswahl beim nächsten Start abfragen",
"DELETE_ALL_CONFIGS_HELP": "Entfernen Sie alle Konfigurationsdateien aus conf.d",
"RESTART_SOFTWARE_HELP": "Einstellungen anwenden, die einen Neustart erfordern",
"CAPTURE_METHOD": "Wayland-Bildschirmaufnahme",
"CAPTURE_METHOD_HELP": "Versuchen Sie, dies zu ändern, wenn Sie\nschwarze oder fehlerhafte Bildschirme erleben",
"KEYBOARD_MIDDLE_CLICK": "Keyboard-Mittelklick",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifikator bei Eingabe mit violettem Laser"
},
"HELLO": "Hallo!",
"AUDIO": {
@@ -40,10 +84,61 @@
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Kein VR-Mikrofon gefunden. Schalten Sie es manuell um.",
"FAILED_TO_SWITCH_MICROPHONE": "Fehler beim Wechseln des Mikrofons",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofon erfolgreich umgeschaltet",
"SPEAKERS_SET_SUCCESSFULLY": "Lautsprecher erfolgreich umgeschaltet",
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "Gerät gefunden und initialisiert, aber nicht umgeschaltet"
"SPEAKERS_SET_SUCCESSFULLY": "Lautsprecher erfolgreich umgeschaltet"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Playspace neu zentrieren"
}
},
"LIST_OF_PROCESSES": "Prozessliste",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Auflösung"
},
"WIDTH": "Breite",
"HEIGHT": "Höhe",
"HIDE": "Verbergen",
"REMOVE": "Entfernen",
"SHOW": "Anzeigen",
"PROCESS_LIST": {
"NO_PROCESSES_FOUND": "Keine Prozesse gefunden",
"LOCATED_ON": "auf",
"TERMINATE_PROCESS_NAMED_X": "Prozess \"{PROCESS_NAME}\" beenden"
},
"FAILED_TO_LAUNCH_APPLICATION": "Fehler beim Starten der Anwendung:",
"NO_WINDOWS_FOUND": "Keine Fenster gefunden",
"WINDOW_OPTIONS": "Fensteroptionen",
"APPLICATION_STARTED": "Anwendung gestartet",
"LIST_OF_WINDOWS": "Fensterliste",
"CLOSE_WINDOW": "Fenster schließen",
"GAME_LIST": {
"NO_GAMES_FOUND": "Keine Spiele gefunden"
},
"TERMINATE_PROCESS": "Prozess beenden",
"GAME_LAUNCHED": "Spiel gestartet",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "Nativer Modus",
"CAGE": "Kompatibilitätsmodus (Cage)"
},
"RES_TITLE": "Auflösung",
"ASPECT_TITLE": "Seitenverhältnis",
"ASPECT": {
"WIDE": "Breit",
"SEMI_WIDE": "Halb-breit",
"SQUARE": "Quadratisch",
"SEMI_TALL": "Halbhoch",
"TALL": "Hoch"
},
"POS_TITLE": "Positionierung",
"POS": {
"FLOATING": "Schwebend",
"ANCHORED": "Verankert",
"STATIC": "Statisch",
"FLOATING_HELP": "Bewegt sich unabhängig, zentriert sich bei Anzeige neu.",
"ANCHORED_HELP": "Bleibt relativ zur Mittelmarkierung an Ort und Stelle.",
"STATIC_HELP": "Keine Set zugeordnet. Wird nicht zentriert."
},
"AUTOSTART": "Automatisch beim Start ausführen",
"LAUNCH": "Starten"
},
"DISPLAY_BRIGHTNESS": "Bildschirmhelligkeit"
}

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": {
"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",
"MONADO_RUNTIME": "Monado tiempo de ejecución",
"MONADO_RUNTIME": "Monado tiempo de ejecución",
"APPLICATIONS": "Aplicaciones",
"GAMES": "Juegos",
"SETTINGS": "Ajustes",
@@ -11,21 +11,65 @@
"APP_SETTINGS": {
"HIDE_USERNAME": "Ocultar nombre de usuario",
"OPAQUE_BACKGROUND": "Fondo opaco",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "Ejecutar en modo XWayland por defecto",
"WLX_OVERLAY_S_SETTINGS": "Configuración de WlxOverlay-S",
"HEADSET_SETTINGS": "Configuración del casco",
"BRIGHTNESS": "Brillo",
"WLX": {
"NOTIFICATIONS_ENABLED": "Notificaciones activadas",
"NOTIFICATIONS_SOUND_ENABLED": "Sonido de notificaciones activado",
"KEYBOARD_SOUND_ENABLED": "Sonido del teclado activado",
"BLOCK_GAME_INPUT": "Bloquear entrada del juego",
"SPACE_DRAG_MULTIPLIER": "Multiplicador de movimiento por arrastre",
"SPACE_DRAG_ROTATION_ENABLED": "Habilitar rotación en space-drag",
"SHOW_SKYBOX": "Mostrar cielo",
"ENABLE_PASSTHROUGH": "Habilitar Passthrough"
},
"RESTART_SOFTWARE": "Reiniciar software"
"WLX": {},
"LOOK_AND_FEEL": "Apariencia y estilo",
"HIDE_GRAB_HELP": "Ocultar ayuda para agarrar",
"ANIMATION_SPEED": "Velocidad de animación de la IU",
"ROUND_MULTIPLIER": "Redondeo de bordes de la IU",
"USE_SKYBOX": "Activar skybox",
"USE_PASSTHROUGH": "Activar passthrough",
"CLOCK_12H": "Reloj de 12 horas",
"FEATURES": "Funciones",
"NOTIFICATIONS_ENABLED": "Habilitar notificaciones",
"NOTIFICATIONS_SOUND_ENABLED": "Sonidos de notificación",
"KEYBOARD_SOUND_ENABLED": "Sonidos del teclado",
"SPACE_DRAG_MULTIPLIER": "Multiplicador de arrastre espacial",
"SPACE_DRAG_UNLOCKED": "Permitir arrastre del espacio en todos los ejes",
"SPACE_ROTATE_UNLOCKED": "Permitir rotación espacial en todos los ejes",
"BLOCK_GAME_INPUT": "Bloquear entrada del juego",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Ignorar watch al bloquear la entrada",
"CONTROLS": "Controles",
"FOCUS_FOLLOWS_MOUSE_MODE": "El movimiento del ratón en el disparador",
"LEFT_HANDED_MOUSE": "Ratón para zurdos",
"ALLOW_SLIDING": "Interacción con el stick durante el agarre",
"INVERT_SCROLL_DIRECTION_X": "Invertir dirección de desplazamiento horizontal",
"INVERT_SCROLL_DIRECTION_Y": "Invertir la dirección del desplazamiento vertical",
"SCROLL_SPEED": "Velocidad de desplazamiento",
"LONG_PRESS_DURATION": "Duración de la pulsación larga",
"POINTER_LERP_FACTOR": "Suavizado del puntero",
"XR_CLICK_SENSITIVITY": "Sensibilidad del clic XR",
"XR_CLICK_SENSITIVITY_RELEASE": "Sensibilidad de liberación de OpenXR",
"CLICK_FREEZE_TIME_MS": "Tiempo de congelación al hacer clic (ms)",
"MISC": "Miscelánea",
"XWAYLAND_BY_DEFAULT": "Ejecutar aplicaciones en modo de compatibilidad por defecto",
"UPRIGHT_SCREEN_FIX": "Corrección de pantalla vertical",
"DOUBLE_CURSOR_FIX": "Solución de doble cursor",
"SCREEN_RENDER_DOWN": "Renderizar pantalla a menor resolución",
"UPRIGHT_SCREEN_FIX_HELP": "Corrige pantallas en posición vertical en algunos escritorios",
"DOUBLE_CURSOR_FIX_HELP": "Habilita esto si ves 2 cursores",
"XR_CLICK_SENSITIVITY_HELP": "Sensibilidad del gatillo analógico",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Debe ser inferior a 'click'",
"CLICK_FREEZE_TIME_MS_HELP": "Ayuda con la precisión de los dobles clics",
"LEFT_HANDED_MOUSE_HELP": "Utilice esto si los botones del ratón están intercambiados",
"BLOCK_GAME_INPUT_HELP": "Bloquea toda la entrada cuando se pasa el cursor sobre un overlay",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "No bloquear la entrada cuando el cursor está sobre la ventana",
"USE_SKYBOX_HELP": "Mostrar una skybox si no hay una aplicación de escena o passthrough",
"USE_PASSTHROUGH_HELP": "Permitir passthrough si el entorno de ejecución XR lo admite",
"SCREEN_RENDER_DOWN_HELP": "Ayuda a reducir el aliasing en pantallas de alta resolución",
"SETS_ON_WATCH": "Conjuntos en el reloj",
"TROUBLESHOOTING": "Solución de problemas",
"CLEAR_SAVED_STATE": "Borrar estado guardado",
"CLEAR_PIPEWIRE_TOKENS": "Limpiar tokens de PipeWire",
"DELETE_ALL_CONFIGS": "Borrar configuración",
"RESTART_SOFTWARE": "Reiniciar software",
"CLEAR_SAVED_STATE_HELP": "Restablecer sets y posiciones de superposición",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Solicitar la selección de pantalla al iniciar la próxima vez",
"DELETE_ALL_CONFIGS_HELP": "Eliminar todos los archivos de configuración de conf.d",
"RESTART_SOFTWARE_HELP": "Aplicar la configuración que requiere un reinicio",
"CAPTURE_METHOD": "Captura de pantalla de Wayland",
"CAPTURE_METHOD_HELP": "Intente cambiar esta opción si\nexperimenta pantallas negras o con fallos",
"KEYBOARD_MIDDLE_CLICK": "Clic del botón central del teclado",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modificador para usar al escribir\ncon láser púrpura"
},
"HELLO": "¡Hola!",
"AUDIO": {
@@ -40,10 +84,61 @@
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "No se encontró micrófono VR. Actívelo manualmente.",
"FAILED_TO_SWITCH_MICROPHONE": "No se pudo cambiar el micrófono",
"MICROPHONE_SET_SUCCESSFULLY": "Micrófono configurado correctamente",
"SPEAKERS_SET_SUCCESSFULLY": "Altavoces configurados correctamente",
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "Dispositivo encontrado e inicializado, pero no cambiado"
"SPEAKERS_SET_SUCCESSFULLY": "Altavoces configurados correctamente"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Re-centrar espacio de juego"
}
}
},
"LIST_OF_PROCESSES": "Lista de procesos",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolución"
},
"WIDTH": "Ancho",
"HEIGHT": "Altura",
"HIDE": "Ocultar",
"REMOVE": "Eliminar",
"SHOW": "Mostrar",
"PROCESS_LIST": {
"NO_PROCESSES_FOUND": "No se encontraron procesos",
"LOCATED_ON": "en",
"TERMINATE_PROCESS_NAMED_X": "Terminar proceso \"{PROCESS_NAME}\""
},
"FAILED_TO_LAUNCH_APPLICATION": "No se pudo iniciar la aplicación:",
"NO_WINDOWS_FOUND": "No se encontraron ventanas",
"WINDOW_OPTIONS": "Opciones de ventana",
"APPLICATION_STARTED": "Aplicación iniciada",
"LIST_OF_WINDOWS": "Lista de ventanas",
"CLOSE_WINDOW": "Cerrar ventana",
"GAME_LIST": {
"NO_GAMES_FOUND": "No se encontraron juegos"
},
"TERMINATE_PROCESS": "Finalizar proceso",
"GAME_LAUNCHED": "Juego lanzado",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "Modo nativo",
"CAGE": "Modo de compatibilidad (Cage)"
},
"RES_TITLE": "Resolución",
"ASPECT_TITLE": "Aspecto",
"ASPECT": {
"WIDE": "Ancho",
"SEMI_WIDE": "Semi-ancho",
"SQUARE": "Cuadrado",
"SEMI_TALL": "Semi alto",
"TALL": "Alto"
},
"POS_TITLE": "Posicionamiento",
"POS": {
"FLOATING": "Flotante",
"ANCHORED": "Anclado",
"STATIC": "Estático",
"FLOATING_HELP": "Se mueve independientemente, se recentra al mostrarse.",
"ANCHORED_HELP": "Se mantiene en su lugar con respecto al marcador central.",
"STATIC_HELP": "No pertenece a ningún conjunto. No se recentra."
},
"AUTOSTART": "Ejecutar automáticamente al inicio",
"LAUNCH": "Iniciar"
},
"DISPLAY_BRIGHTNESS": "Brillo de la pantalla"
}

View File

@@ -1,7 +1,7 @@
{
"HOME_SCREEN": "ホーム",
"MONADO_RUNTIME": "「Monado」ランタイム",
"APPLICATIONS": "アプリケーション",
"APPLICATIONS": "アプリ",
"GAMES": "ゲーム",
"SETTINGS": "設定",
"PROCESSES": "プロセス",
@@ -11,21 +11,65 @@
"APP_SETTINGS": {
"HIDE_USERNAME": "ユーザー名を表示しない",
"OPAQUE_BACKGROUND": "不透明な背景",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "XWaylandモードでデフォルトで実行する",
"WLX_OVERLAY_S_SETTINGS": "WlxOverlay-Sの設定",
"HEADSET_SETTINGS": "ヘッドセット設定",
"BRIGHTNESS": "明るさ",
"WLX": {
"NOTIFICATIONS_ENABLED": "通知",
"NOTIFICATIONS_SOUND_ENABLED": "通知音",
"KEYBOARD_SOUND_ENABLED": "キーボード音",
"BLOCK_GAME_INPUT": "ゲーム入力をブロック",
"SPACE_DRAG_MULTIPLIER": "スペースドラッグ乗数",
"SPACE_DRAG_ROTATION_ENABLED": "スペースドラッグでの回転",
"SHOW_SKYBOX": "スカイボックス",
"ENABLE_PASSTHROUGH": "パススルー"
},
"RESTART_SOFTWARE": "ソフトウェアを再起動"
"WLX": {},
"LOOK_AND_FEEL": "外観",
"HIDE_GRAB_HELP": "掴み操作のヘルプを非表示にする",
"ANIMATION_SPEED": "UIアニメーション速度",
"ROUND_MULTIPLIER": "UI エッジの丸み",
"USE_SKYBOX": "スカイボックスを有効にする",
"USE_PASSTHROUGH": "パススルーを有効にする",
"CLOCK_12H": "12時間制",
"FEATURES": "機能",
"NOTIFICATIONS_ENABLED": "通知を有効にする",
"NOTIFICATIONS_SOUND_ENABLED": "通知音",
"KEYBOARD_SOUND_ENABLED": "キーボード音",
"SPACE_DRAG_MULTIPLIER": "スペースドラッグ倍率",
"SPACE_DRAG_UNLOCKED": "全ての軸でのスペースドラッグを許可",
"SPACE_ROTATE_UNLOCKED": "全軸でのスペース回転を許可する",
"BLOCK_GAME_INPUT": "ゲームからの入力をブロック",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "入力ブロック時に監視を無視",
"CONTROLS": "コントロール",
"FOCUS_FOLLOWS_MOUSE_MODE": "トリガー操作でマウス移動",
"LEFT_HANDED_MOUSE": "左利きマウス",
"ALLOW_SLIDING": "掴んでいる間のスティック操作を許可する",
"INVERT_SCROLL_DIRECTION_X": "水平スクロール方向を反転",
"INVERT_SCROLL_DIRECTION_Y": "垂直方向のスクロール方向を反転",
"SCROLL_SPEED": "スクロール速度",
"LONG_PRESS_DURATION": "長押し時間",
"POINTER_LERP_FACTOR": "ポインタのスムージング",
"XR_CLICK_SENSITIVITY": "XRクリック感度",
"XR_CLICK_SENSITIVITY_RELEASE": "XRリリース感度",
"CLICK_FREEZE_TIME_MS": "クリックで一時停止時間 (ms)",
"MISC": "その他",
"XWAYLAND_BY_DEFAULT": "互換モードでアプリをデフォルトで実行",
"UPRIGHT_SCREEN_FIX": "画面の縦向き修正",
"DOUBLE_CURSOR_FIX": "ダブルカーソル修正",
"SCREEN_RENDER_DOWN": "低い解像度で画面をレンダリングする",
"UPRIGHT_SCREEN_FIX_HELP": "一部のデスクトップで縦向きの画面を修正",
"DOUBLE_CURSOR_FIX_HELP": "2つのカーソルが表示される場合は、これを有効にします",
"XR_CLICK_SENSITIVITY_HELP": "アナログトリガの感度",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "クリックより低くする必要があります",
"CLICK_FREEZE_TIME_MS_HELP": "ダブルクリックの精度向上に役立ちます",
"LEFT_HANDED_MOUSE_HELP": "マウスボタンが入れ替わっている場合に有効にします",
"BLOCK_GAME_INPUT_HELP": "オーバーレイ上にマウスカーソルがあるときに入力をブロックします",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "ウォッチがホバーされているときは入力をブロックしない",
"USE_SKYBOX_HELP": "シーンアプリまたはパススルーがない場合にスカイボックスを表示します",
"USE_PASSTHROUGH_HELP": "XRランタイムがサポートしていれば、パススルーを有効にします",
"SCREEN_RENDER_DOWN_HELP": "高解像度スクリーンでのエイリアシングを軽減します",
"SETS_ON_WATCH": "ウォッチのセット",
"TROUBLESHOOTING": "トラブルシューティング",
"CLEAR_SAVED_STATE": "保存された状態をクリア",
"CLEAR_PIPEWIRE_TOKENS": "PipeWire トークンをクリア",
"DELETE_ALL_CONFIGS": "設定を消去",
"RESTART_SOFTWARE": "ソフトウェアを再起動",
"CLEAR_SAVED_STATE_HELP": "セットとオーバーレイの位置をリセット",
"CLEAR_PIPEWIRE_TOKENS_HELP": "次の起動時に画面選択のプロンプトを表示",
"DELETE_ALL_CONFIGS_HELP": "conf.d 内のすべての設定ファイルを削除します",
"RESTART_SOFTWARE_HELP": "再起動が必要な設定を適用する",
"CAPTURE_METHOD": "Waylandスクリーンキャプチャ",
"CAPTURE_METHOD_HELP": "画面が黒くなる、または乱れる場合は、\nこの設定を変更してみてください。",
"KEYBOARD_MIDDLE_CLICK": "キーボードの中ボタンクリック",
"KEYBOARD_MIDDLE_CLICK_HELP": "紫色のレーザーで入力する際の修飾キー"
},
"HELLO": "こんにちは!",
"AUDIO": {
@@ -40,10 +84,61 @@
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "VRマイクが見つかりませんでした。手動で切り替えてください。",
"FAILED_TO_SWITCH_MICROPHONE": "マイクの切り替えに失敗しました",
"MICROPHONE_SET_SUCCESSFULLY": "マイクの設定が完了しました",
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定しました",
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "デバイスが見つかり、初期化されましたが、切り替えられていません"
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定しました"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "プレイスペースを再中央"
}
},
"LIST_OF_PROCESSES": "プロセスのリスト",
"POPUP_ADD_DISPLAY": {
"RESOLUTION": "解像度"
},
"WIDTH": "幅",
"HEIGHT": "高さ",
"HIDE": "隠す",
"REMOVE": "削除",
"SHOW": "表示",
"PROCESS_LIST": {
"NO_PROCESSES_FOUND": "プロセスが見つかりませんでした",
"LOCATED_ON": "に",
"TERMINATE_PROCESS_NAMED_X": "プロセス \"{PROCESS_NAME}\" を終了します"
},
"FAILED_TO_LAUNCH_APPLICATION": "アプリケーションの起動に失敗しました:",
"NO_WINDOWS_FOUND": "ウィンドウが見つかりませんでした",
"WINDOW_OPTIONS": "ウィンドウオプション",
"APPLICATION_STARTED": "アプリケーションが起動しました",
"LIST_OF_WINDOWS": "ウィンドウ一覧",
"CLOSE_WINDOW": "ウィンドウを閉じる",
"GAME_LIST": {
"NO_GAMES_FOUND": "ゲームが見つかりませんでした"
},
"TERMINATE_PROCESS": "プロセスを終了する",
"GAME_LAUNCHED": "ゲームが起動しました",
"APP_LAUNCHER": {
"MODE": {
"NATIVE": "ネイティブモード",
"CAGE": "互換モードCage"
},
"RES_TITLE": "解像度",
"ASPECT_TITLE": "アスペクト",
"ASPECT": {
"WIDE": "ワイド",
"SEMI_WIDE": "半広角",
"SQUARE": "正方形",
"SEMI_TALL": "半縦長",
"TALL": "縦長"
},
"POS_TITLE": "配置",
"POS": {
"FLOATING": "フローティング",
"ANCHORED": "固定",
"STATIC": "固定",
"FLOATING_HELP": "独立して移動し、表示時に中央に再配置されます。",
"ANCHORED_HELP": "中央マーカーに対して固定された位置に留まります。",
"STATIC_HELP": "どのセットにも属していません。リセンターされません。"
},
"AUTOSTART": "起動時に自動実行",
"LAUNCH": "起動"
},
"DISPLAY_BRIGHTNESS": "ディスプレイの明るさ"
}

View File

@@ -1,49 +1,144 @@
{
"HOME_SCREEN": "Ekran główny",
"MONADO_RUNTIME": "Środowisko Monado",
"APPLICATIONS": "Aplikacje",
"GAMES": "Gry",
"SETTINGS": "Ustawienia",
"PROCESSES": "Procesy",
"HELLO_USER": "Witaj, {USER}!",
"GENERAL_SETTINGS": "Ustawienia ogólne",
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji",
"APP_SETTINGS": {
"HIDE_USERNAME": "Ukryj nazwę użytkownika",
"OPAQUE_BACKGROUND": "Nieprzezroczyste tło",
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "Uruchom domyślnie w trybie XWayland",
"WLX_OVERLAY_S_SETTINGS": "Ustawienia wlx-overlay-s",
"HEADSET_SETTINGS": "Ustawienia HMD",
"BRIGHTNESS": "Jasność",
"WLX": {
"NOTIFICATIONS_ENABLED": "Powiadomienia",
"NOTIFICATIONS_SOUND_ENABLED": "Dźwięk powiadomień",
"KEYBOARD_SOUND_ENABLED": "Dźwięki klawiatury",
"BLOCK_GAME_INPUT": "Zablokuj sterowanie grą podczas używania Wlx",
"SPACE_DRAG_MULTIPLIER": "Mnożnik space-drag",
"SPACE_DRAG_ROTATION_ENABLED": "Włącz rotację w space-drag",
"SHOW_SKYBOX": "Pokaż skybox",
"ENABLE_PASSTHROUGH": "Włącz passthrough"
},
"RESTART_SOFTWARE": "Uruchom ponownie oprogramowanie"
},
"HELLO": "Witaj!",
"AUDIO": {
"VOLUME": "Głośność",
"SETTINGS": "Ustawienia dźwięku",
"AUTO_SWITCH_TO_VR_AUDIO": "Automatyczne przełączanie na dźwięk VR",
"SPEAKERS": "Głośniki",
"MICROPHONES": "Mikrofony",
"CARDS": "Karty",
"SELECT_AUDIO_CARD_PROFILE": "Wybierz profil karty dźwiękowej",
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "Brak głośników VR. Włącz je ręcznie.",
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "Brak mikrofonu VR. Włącz go ręcznie.",
"FAILED_TO_SWITCH_MICROPHONE": "Nie udało się przełączyć mikrofon",
"MICROPHONE_SET_SUCCESSFULLY": "Mikrofon ustawiono pomyślnie",
"SPEAKERS_SET_SUCCESSFULLY": "Głośniki ustawiono pomyślnie",
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "Urządzenie znalezione i zainicjalizowane, ale nie przełączone"
},
"ACTIONS": {
"RECENTER_PLAYSPACE": "Wycentruj przestrzeń"
}
"ACTIONS": {
"RECENTER_PLAYSPACE": "Wycentruj przestrzeń"
},
"APP_SETTINGS": {
"HIDE_USERNAME": "Ukryj nazwę użytkownika",
"OPAQUE_BACKGROUND": "Nieprzezroczyste tło",
"WLX": {},
"LOOK_AND_FEEL": "Wygląd i działanie",
"HIDE_GRAB_HELP": "Ukryj pomoc dotyczącą chwytania",
"ANIMATION_SPEED": "Prędkość animacji UI",
"ROUND_MULTIPLIER": "Zaokrąglenie krawędzi UI",
"USE_SKYBOX": "Włącz niebo",
"USE_PASSTHROUGH": "Włącz passthrough",
"CLOCK_12H": "Zegar 12-godzinny",
"FEATURES": "Funkcje",
"NOTIFICATIONS_ENABLED": "Włącz powiadomienia",
"NOTIFICATIONS_SOUND_ENABLED": "Dźwięki powiadomień",
"KEYBOARD_SOUND_ENABLED": "Dźwięki klawiatury",
"SPACE_DRAG_MULTIPLIER": "Mnożnik przesuwania przestrzeni",
"SPACE_DRAG_UNLOCKED": "Pozwól na przesuwanie przestrzeni na wszystkich osiach",
"SPACE_ROTATE_UNLOCKED": "Pozwól na rotację przestrzeni na wszystkich osiach",
"BLOCK_GAME_INPUT": "Blokuj input z gry",
"BLOCK_GAME_INPUT_IGNORE_WATCH": "Nie blokuj inputu gry, gdy zegarek jest używany",
"CONTROLS": "Sterowanie",
"FOCUS_FOLLOWS_MOUSE_MODE": "Ruch myszą po dotknięciu spustu",
"LEFT_HANDED_MOUSE": "Myszka dla leworęcznych",
"ALLOW_SLIDING": "Interakcja z drążkami podczas chwytania",
"INVERT_SCROLL_DIRECTION_X": "Odwróć kierunek przewijania w poziomie",
"INVERT_SCROLL_DIRECTION_Y": "Odwróć kierunek przewijania w pionie",
"SCROLL_SPEED": "Prędkość przewijania",
"LONG_PRESS_DURATION": "Czas długiego przytrzymania",
"POINTER_LERP_FACTOR": "Wygładzanie wskaźnika",
"XR_CLICK_SENSITIVITY": "Czułość kliknięć XR",
"XR_CLICK_SENSITIVITY_RELEASE": "Czułość zwalniania XR",
"CLICK_FREEZE_TIME_MS": "Czas zamrożenia po kliknięciu (ms)",
"MISC": "Różne",
"XWAYLAND_BY_DEFAULT": "Uruchamiaj aplikacje domyślnie w trybie kompatybilności",
"UPRIGHT_SCREEN_FIX": "Naprawa pozycji ekranu",
"DOUBLE_CURSOR_FIX": "Naprawa podwójnego kursora",
"SCREEN_RENDER_DOWN": "Renderuj ekran w niższej rozdzielczości",
"UPRIGHT_SCREEN_FIX_HELP": "Naprawia pionowe ekrany na niektórych komputerach",
"DOUBLE_CURSOR_FIX_HELP": "Włącz to, jeśli widzisz 2 kursory",
"XR_CLICK_SENSITIVITY_HELP": "Czułość analogowego spustu",
"XR_CLICK_SENSITIVITY_RELEASE_HELP": "Musi być niższa niż kliknięcie",
"CLICK_FREEZE_TIME_MS_HELP": "Pomaga w precyzji podwójnego kliknięcia",
"LEFT_HANDED_MOUSE_HELP": "Użyj tego, jeśli przyciski myszy są zamienione",
"BLOCK_GAME_INPUT_HELP": "Blokuje wszystkie dane wejściowe, gdy kursor najedzie na nakładkę",
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Nie blokuj inputu, gdy wskaźnik jest najedzony",
"USE_SKYBOX_HELP": "Wyświetlaj niebo, jeśli nie ma aplikacji sceny lub passthrough",
"USE_PASSTHROUGH_HELP": "Pozwól na passthrough, jeśli runtime XR to obsługuje",
"SCREEN_RENDER_DOWN_HELP": "Pomaga redukować aliasing na ekranach o wysokiej rozdzielczości",
"SETS_ON_WATCH": "Lista zestawów na zegarku",
"TROUBLESHOOTING": "Rozwiązywanie problemów",
"CLEAR_SAVED_STATE": "Wyczyść zapisany stan",
"CLEAR_PIPEWIRE_TOKENS": "Wyczyść tokeny PipeWire",
"DELETE_ALL_CONFIGS": "Wyczyść konfigurację",
"RESTART_SOFTWARE": "Uruchom ponownie oprogramowanie",
"CLEAR_SAVED_STATE_HELP": "Zresetuj zestawy i pozycje nakładek",
"CLEAR_PIPEWIRE_TOKENS_HELP": "Zapytaj o wybór ekranu przy następnym uruchomieniu",
"DELETE_ALL_CONFIGS_HELP": "Usuń wszystkie pliki konfiguracyjne z katalogu conf.d",
"RESTART_SOFTWARE_HELP": "Zastosuj ustawienia wymagające ponownego uruchomienia",
"CAPTURE_METHOD": "Przechwytywanie ekranu Wayland",
"CAPTURE_METHOD_HELP": "Spróbuj zmienić tę opcję, jeśli masz\nproblemy z czarnym lub migoczącym ekranem",
"KEYBOARD_MIDDLE_CLICK": "Środkowy przycisk myszy na klawiaturze",
"KEYBOARD_MIDDLE_CLICK_HELP": "Modyfikator, który ma zostać użyty podczas pisania\nfioletową wiązką lasera"
},
"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 glam::Vec2;
@@ -8,23 +8,24 @@ use wgui::{
font_config::WguiFontConfig,
globals::WguiGlobals,
i18n::Translation,
layout::{LayoutParams, RcLayout, WidgetID},
layout::{Layout, LayoutParams, LayoutUpdateParams, LayoutUpdateResult, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
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::{
assets, settings,
assets,
tab::{
Tab, TabParams, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses,
settings::TabSettings,
apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, settings::TabSettings,
Tab, TabType,
},
task::Tasks,
util::{
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
toast_manager::ToastManager,
various::AsyncExecutor,
},
views,
};
@@ -36,16 +37,19 @@ pub struct FrontendWidgets {
pub type FrontendTasks = Tasks<FrontendTask>;
pub struct Frontend {
pub layout: RcLayout,
pub struct Frontend<T> {
pub layout: Layout,
globals: WguiGlobals,
pub settings: Box<dyn settings::SettingsIO>,
pub interface: BoxDashInterface<T>,
// async runtime executor
pub executor: AsyncExecutor,
#[allow(dead_code)]
state: ParserState,
current_tab: Option<Box<dyn Tab>>,
current_tab: Option<Box<dyn Tab<T>>>,
pub tasks: FrontendTasks,
@@ -55,16 +59,34 @@ pub struct Frontend {
popup_manager: PopupManager,
toast_manager: ToastManager,
timestep: Timestep,
sounds_to_play: Vec<SoundType>,
window_audio_settings: WguiWindow,
view_audio_settings: Option<views::audio_settings::View>,
}
pub struct InitParams {
pub settings: Box<dyn settings::SettingsIO>,
pub struct FrontendUpdateParams<'a, T> {
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)]
pub enum FrontendTask {
@@ -77,10 +99,11 @@ pub enum FrontendTask {
UpdateAudioSettingsView,
RecenterPlayspace,
PushToast(Translation),
PlaySound(SoundType),
}
impl Frontend {
pub fn new(params: InitParams) -> anyhow::Result<(RcFrontend, RcLayout)> {
impl<T: 'static> Frontend<T> {
pub fn new(params: InitParams<T>, data: &mut T) -> anyhow::Result<Frontend<T>> {
let mut assets = Box::new(assets::Asset {});
let font_binary_bold = assets.load_from_path_gzip("Quicksand-Bold.ttf.gz")?;
@@ -115,19 +138,16 @@ impl Frontend {
let toast_manager = ToastManager::new();
let rc_layout = layout.as_rc();
let tasks = FrontendTasks::new();
tasks.push(FrontendTask::SetTab(TabType::Home));
let id_label_time = state.get_widget_id("label_time")?;
let id_rect_content = state.get_widget_id("rect_content")?;
let mut timestep = Timestep::new();
timestep.set_tps(30.0); // 30 ticks per second
let timestep = Timestep::new(60.0);
let frontend = Self {
layout: rc_layout.clone(),
let mut frontend = Self {
layout,
state,
current_tab: None,
globals,
@@ -138,60 +158,101 @@ impl Frontend {
id_rect_content,
},
timestep,
settings: params.settings,
interface: params.interface,
popup_manager,
toast_manager,
window_audio_settings: WguiWindow::default(),
view_audio_settings: None,
executor: Rc::new(smol::LocalExecutor::new()),
sounds_to_play: Vec::new(),
};
// init some things first
frontend.update_background()?;
frontend.update_time()?;
frontend.update_background(data)?;
frontend.update_time(data)?;
let res = Rc::new(RefCell::new(frontend));
Frontend::register_widgets(&mut frontend)?;
Frontend::register_widgets(&res)?;
Ok((res, rc_layout))
Ok(frontend)
}
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();
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;
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(())
}
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
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
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<()> {
let mut layout = self.layout.borrow_mut();
let mut c = layout.start_common();
fn update_time(&mut self, data: &mut T) -> anyhow::Result<()> {
let mut c = self.layout.start_common();
let mut common = c.common();
{
@@ -203,12 +264,12 @@ impl Frontend {
let hours = now.hour();
let minutes = now.minute();
let text: String = if !self.settings.get().general.am_pm_clock {
format!("{hours:02}:{minutes:02}")
} else {
let text: String = if self.interface.general_config(data).clock_12h {
let hours_ampm = (hours + 11) % 12 + 1;
let suffix = if hours >= 12 { "PM" } else { "AM" };
format!("{hours_ampm:02}:{minutes:02} {suffix}")
} else {
format!("{hours:02}:{minutes:02}")
};
label.set_text(&mut common, Translation::from_raw_text(&text));
@@ -218,26 +279,29 @@ impl Frontend {
Ok(())
}
fn mount_popup(&mut self, params: MountPopupParams) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut();
self
.popup_manager
.mount_popup(self.globals.clone(), &mut layout, self.tasks.clone(), params)?;
fn mount_popup(&mut self, params: MountPopupParams, data: &mut T) -> anyhow::Result<()> {
let config = self.interface.general_config(data);
self.popup_manager.mount_popup(
self.globals.clone(),
&mut self.layout,
self.tasks.clone(),
params,
config,
)?;
Ok(())
}
fn refresh_popup_manager(&mut self) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut();
let mut c = layout.start_common();
let mut c = self.layout.start_common();
self.popup_manager.refresh(c.common().alterables);
c.finish()?;
Ok(())
}
fn update_background(&self) -> anyhow::Result<()> {
let layout = self.layout.borrow_mut();
let Some(mut rect) = layout
fn update_background(&mut self, data: &mut T) -> anyhow::Result<()> {
let Some(mut rect) = self
.layout
.state
.widgets
.get_as::<WidgetRectangle>(self.widgets.id_rect_content)
@@ -245,10 +309,10 @@ impl Frontend {
anyhow::bail!("");
};
let (alpha1, alpha2) = if !self.settings.get().general.opaque_background {
(0.8666, 0.9333)
} else {
let (alpha1, alpha2) = if self.interface.general_config(data).opaque_background {
(1.0, 1.0)
} else {
(0.8666, 0.9333)
};
rect.params.color.a = alpha1;
@@ -257,47 +321,34 @@ impl Frontend {
Ok(())
}
pub fn get_layout(&self) -> &RcLayout {
&self.layout
}
fn process_task(&mut self, rc_this: &RcFrontend, task: FrontendTask) -> anyhow::Result<()> {
fn process_task(&mut self, params: &mut FrontendUpdateParams<T>, task: FrontendTask) -> anyhow::Result<()> {
match task {
FrontendTask::SetTab(tab_type) => self.set_tab(tab_type, rc_this)?,
FrontendTask::RefreshClock => self.update_time()?,
FrontendTask::RefreshBackground => self.update_background()?,
FrontendTask::MountPopup(params) => self.mount_popup(params)?,
FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?,
FrontendTask::RefreshClock => self.update_time(params.data)?,
FrontendTask::RefreshBackground => self.update_background(params.data)?,
FrontendTask::MountPopup(popup_params) => self.mount_popup(popup_params, params.data)?,
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
FrontendTask::ShowAudioSettings => self.action_show_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::PlaySound(sound_type) => self.queue_play_sound(sound_type),
};
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:?}");
let mut layout = self.layout.borrow_mut();
let widget_content = self.state.fetch_widget(&layout.state, "content")?;
layout.remove_children(widget_content.id);
let widget_content = self.state.fetch_widget(&self.layout.state, "content")?;
self.layout.remove_children(widget_content.id);
let tab_params = TabParams {
globals: &self.globals,
layout: &mut layout,
parent_id: widget_content.id,
frontend: rc_this,
//frontend_widgets: &self.widgets,
settings: self.settings.get_mut(),
};
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)?),
let tab: Box<dyn Tab<T>> = match tab_type {
TabType::Home => Box::new(TabHome::new(self, widget_content.id, data)?),
TabType::Apps => Box::new(TabApps::new(self, widget_content.id, data)?),
TabType::Games => Box::new(TabGames::new(self, widget_content.id)?),
TabType::Monado => Box::new(TabMonado::new(self, widget_content.id)?),
TabType::Processes => Box::new(TabProcesses::new(self, widget_content.id)?),
TabType::Settings => Box::new(TabSettings::new(self, widget_content.id, data)?),
};
self.current_tab = Some(tab);
@@ -305,61 +356,44 @@ impl Frontend {
Ok(())
}
pub fn register_button_task(this_rc: RcFrontend, btn: &Rc<ComponentButton>, task: FrontendTask) {
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();
fn register_widgets(&mut self) -> anyhow::Result<()> {
// ################################
// SIDE BUTTONS
// ################################
// "Home" side button
Frontend::register_button_task(
rc_this.clone(),
&this.state.fetch_component_as::<ComponentButton>("btn_side_home")?,
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_side_home")?,
FrontendTask::SetTab(TabType::Home),
);
// "Apps" side button
Frontend::register_button_task(
rc_this.clone(),
&this.state.fetch_component_as::<ComponentButton>("btn_side_apps")?,
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_side_apps")?,
FrontendTask::SetTab(TabType::Apps),
);
// "Games" side button
Frontend::register_button_task(
rc_this.clone(),
&this.state.fetch_component_as::<ComponentButton>("btn_side_games")?,
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_side_games")?,
FrontendTask::SetTab(TabType::Games),
);
// "Monado side button"
Frontend::register_button_task(
rc_this.clone(),
&this.state.fetch_component_as::<ComponentButton>("btn_side_monado")?,
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_side_monado")?,
FrontendTask::SetTab(TabType::Monado),
);
// "Processes" side button
Frontend::register_button_task(
rc_this.clone(),
&this.state.fetch_component_as::<ComponentButton>("btn_side_processes")?,
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_side_processes")?,
FrontendTask::SetTab(TabType::Processes),
);
// "Settings" side button
Frontend::register_button_task(
rc_this.clone(),
&this.state.fetch_component_as::<ComponentButton>("btn_side_settings")?,
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_side_settings")?,
FrontendTask::SetTab(TabType::Settings),
);
@@ -368,16 +402,14 @@ impl Frontend {
// ################################
// "Audio" bottom bar button
Frontend::register_button_task(
rc_this.clone(),
&this.state.fetch_component_as::<ComponentButton>("btn_audio")?,
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_audio")?,
FrontendTask::ShowAudioSettings,
);
// "Recenter playspace" bottom bar button
Frontend::register_button_task(
rc_this.clone(),
&this.state.fetch_component_as::<ComponentButton>("btn_recenter")?,
self.tasks.handle_button(
&self.state.fetch_component_as::<ComponentButton>("btn_recenter")?,
FrontendTask::RecenterPlayspace,
);
@@ -385,16 +417,15 @@ impl Frontend {
}
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
let mut layout = self.layout.borrow_mut();
self.window_audio_settings.open(&mut WguiWindowParams {
globals: self.globals.clone(),
globals: &self.globals,
position: Vec2::new(64.0, 64.0),
layout: &mut layout,
title: Translation::from_translation_key("AUDIO.SETTINGS"),
layout: &mut self.layout,
extra: WguiWindowParamsExtra {
fixed_width: Some(400.0),
placement: WguiWindowPlacement::BottomLeft,
close_if_clicked_outside: true,
title: Some(Translation::from_translation_key("AUDIO.SETTINGS")),
..Default::default()
},
})?;
@@ -404,7 +435,7 @@ impl Frontend {
self.view_audio_settings = Some(views::audio_settings::View::new(views::audio_settings::Params {
globals: self.globals.clone(),
frontend_tasks: self.tasks.clone(),
layout: &mut layout,
layout: &mut self.layout,
parent_id: content.id,
on_update: {
let tasks = self.tasks.clone();
@@ -421,14 +452,13 @@ impl Frontend {
return Ok(());
};
let mut layout = self.layout.borrow_mut();
view.update(&mut layout)?;
view.update(&mut self.layout)?;
Ok(())
}
fn action_recenter_playspace(&mut self) -> anyhow::Result<()> {
log::info!("todo");
fn action_recenter_playspace(&mut self, data: &mut T) -> anyhow::Result<()> {
self.interface.recenter_playspace(data)?;
Ok(())
}
}

View File

@@ -1,8 +1,6 @@
mod assets;
pub mod frontend;
pub mod settings;
mod tab;
mod task;
mod util;
mod various;
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::{
assets::AssetPath,
components::button::{ButtonClickCallback, ComponentButton},
globals::WguiGlobals,
i18n::Translation,
layout::WidgetPair,
layout::{WidgetID, WidgetPair},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
};
use wlx_common::desktop_finder::DesktopEntry;
use crate::{
frontend::{FrontendTask, RcFrontend},
tab::{Tab, TabParams, TabType},
util::{
self,
desktop_finder::DesktopEntry,
popup_manager::{MountPopupParams, PopupHandle},
},
frontend::{Frontend, FrontendTask, FrontendTasks},
tab::{Tab, TabType},
util::popup_manager::{MountPopupParams, PopupHandle},
views::{self, app_launcher},
};
enum Task {
CloseLauncher,
}
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)]
pub parser_state: ParserState,
parser_state: ParserState,
#[allow(dead_code)]
state: Rc<RefCell<State>>,
#[allow(dead_code)]
entries: Vec<DesktopEntry>,
#[allow(dead_code)]
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 {
TabType::Apps
}
}
#[derive(Default)]
struct AppList {
//data: Vec<ParserData>,
}
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
let mut state = self.state.borrow_mut();
// called after the user clicks any desktop entry
fn on_app_click(
frontend: RcFrontend,
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,
})?;
for task in self.tasks.drain() {
match task {
Task::CloseLauncher => state.view_launcher = None,
}
}
state.borrow_mut().launcher = Some((data.handle, view));
Ok(())
})
},
}));
Ok(())
})
}
self
.app_list
.tick(frontend, &self.state, &self.tasks, &mut self.parser_state)?;
impl TabApps {
pub fn new(mut tab_params: TabParams) -> anyhow::Result<Self> {
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);
if let Some((_, launcher)) = &mut state.view_launcher {
launcher.update(&mut frontend.interface, data)?;
}
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::{
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)]
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 {
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 {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
impl<T> TabGames<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
globals: frontend.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/games.xml"),
extra: Default::default(),
},
params.layout,
params.parent_id,
&mut frontend.layout,
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::{
assets::AssetPath,
components::button::ComponentButton,
event::CallbackDataCommon,
i18n::Translation,
layout::Widget,
layout::{Widget, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
widget::label::WidgetLabel,
};
use wlx_common::config::GeneralConfig;
use crate::{
frontend::{Frontend, FrontendTask},
settings,
tab::{Tab, TabParams, TabType},
tab::{Tab, TabType},
various,
};
pub struct TabHome {
pub struct TabHome<T> {
#[allow(dead_code)]
pub state: ParserState,
marker: PhantomData<T>,
}
impl Tab for TabHome {
impl<T> Tab<T> for TabHome<T> {
fn get_type(&self) -> TabType {
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();
// first character as uppercase
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);
}
let translated = if !settings.home_screen.hide_username {
let translated = if !config.hide_username {
common.i18n().translate_and_replace("HELLO_USER", ("{USER}", &username))
} else {
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));
}
impl TabHome {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
impl<T> TabHome<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
globals: frontend.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/home.xml"),
extra: Default::default(),
},
params.layout,
params.parent_id,
&mut frontend.layout,
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;
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_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_settings = state.fetch_component_as::<ComponentButton>("btn_settings")?;
let frontend = params.frontend;
Frontend::register_button_task(frontend.clone(), &btn_apps, FrontendTask::SetTab(TabType::Apps));
Frontend::register_button_task(frontend.clone(), &btn_games, FrontendTask::SetTab(TabType::Games));
Frontend::register_button_task(frontend.clone(), &btn_monado, FrontendTask::SetTab(TabType::Monado));
Frontend::register_button_task(
frontend.clone(),
&btn_processes,
FrontendTask::SetTab(TabType::Processes),
);
Frontend::register_button_task(frontend.clone(), &btn_settings, FrontendTask::SetTab(TabType::Settings));
let tasks = &mut frontend.tasks;
tasks.handle_button(&btn_apps, FrontendTask::SetTab(TabType::Apps));
tasks.handle_button(&btn_games, FrontendTask::SetTab(TabType::Games));
tasks.handle_button(&btn_monado, FrontendTask::SetTab(TabType::Monado));
tasks.handle_button(&btn_processes, FrontendTask::SetTab(TabType::Processes));
tasks.handle_button(&btn_settings, FrontendTask::SetTab(TabType::Settings));
Ok(Self { state })
Ok(Self {
state,
marker: PhantomData,
})
}
}

View File

@@ -1,9 +1,4 @@
use wgui::{
globals::WguiGlobals,
layout::{Layout, WidgetID},
};
use crate::frontend::RcFrontend;
use crate::frontend::Frontend;
pub mod apps;
pub mod games;
@@ -22,15 +17,11 @@ pub enum TabType {
Settings,
}
pub struct TabParams<'a> {
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 {
pub trait Tab<T> {
#[allow(dead_code)]
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::{
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};
pub struct TabMonado {
#[allow(dead_code)]
pub state: ParserState,
#[derive(Debug)]
enum Task {
Refresh,
FocusClient(String),
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 {
TabType::Games
}
}
impl TabMonado {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/monado.xml"),
extra: Default::default(),
},
params.layout,
params.parent_id,
)?;
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
for task in self.tasks.drain() {
match task {
Task::Refresh => self.refresh(frontend, data)?,
Task::FocusClient(name) => self.focus_client(frontend, data, name)?,
Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness),
}
}
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::{
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)]
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 {
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 {
pub fn new(params: TabParams) -> anyhow::Result<Self> {
impl<T> TabProcesses<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone();
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/processes.xml"),
extra: Default::default(),
},
params.layout,
params.parent_id,
&mut frontend.layout,
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::{
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},
task::Tasks,
widget::label::WidgetLabel,
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult},
};
use wlx_common::{config::GeneralConfig, config_io::ConfigRoot};
use crate::{
frontend::{Frontend, FrontendTask},
settings,
tab::{Tab, TabParams, TabType},
tab::{Tab, TabType},
};
pub struct TabSettings {
#[allow(dead_code)]
pub state: ParserState,
enum Task {
UpdateBool(SettingType, bool),
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 {
TabType::Settings
}
}
fn init_setting_checkbox(
params: &mut TabParams,
checkbox: Rc<ComponentCheckbox>,
fetch_callback: fn(&mut settings::Settings) -> &mut bool,
change_callback: Option<fn(&mut Frontend, bool)>,
) -> anyhow::Result<()> {
let mut c = params.layout.start_common();
checkbox.set_checked(&mut c.common(), *fetch_callback(params.settings));
let rc_frontend = params.frontend.clone();
checkbox.on_toggle(Box::new(move |_common, e| {
let mut frontend = rc_frontend.borrow_mut();
*fetch_callback(frontend.settings.get_mut()) = e.checked;
if let Some(change_callback) = &change_callback {
change_callback(&mut frontend, e.checked);
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
let config = frontend.interface.general_config(data);
let mut changed = false;
for task in self.tasks.drain() {
match task {
Task::UpdateBool(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task));
*setting.mut_bool(config) = n;
changed = true;
}
Task::UpdateFloat(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task));
*setting.mut_f32(config) = n;
changed = true;
}
Task::UpdateInt(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task));
*setting.mut_i32(config) = n;
changed = true;
}
Task::ClearPipewireTokens => {
let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("pw_tokens.yaml"))
.log_err("Could not remove pw_tokens.yaml");
}
Task::ClearSavedState => {
let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("zz-saved-state.json5"))
.log_err("Could not remove zz-saved-state.json5");
}
Task::DeleteAllConfigs => {
let path = ConfigRoot::Generic.get_conf_d_path();
std::fs::remove_dir_all(&path)?;
std::fs::create_dir(&path)?;
}
Task::RestartSoftware => {
frontend.interface.restart(data);
return Ok(());
}
Task::OpenContextMenu(position, cells) => {
self.context_menu.open(context_menu::OpenParams {
on_custom_attribs: None,
position,
blueprint: Blueprint::Cells(cells),
});
}
}
}
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(())
}));
c.finish()?;
Ok(())
}
impl TabSettings {
pub fn new(mut params: TabParams) -> anyhow::Result<Self> {
let state = wgui::parser::parse_from_assets(
&ParseDocumentParams {
globals: params.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/settings.xml"),
extra: Default::default(),
},
params.layout,
params.parent_id,
)?;
init_setting_checkbox(
&mut params,
state.data.fetch_component_as::<ComponentCheckbox>("cb_hide_username")?,
|settings| &mut settings.home_screen.hide_username,
None,
)?;
init_setting_checkbox(
&mut params,
state.data.fetch_component_as::<ComponentCheckbox>("cb_am_pm_clock")?,
|settings| &mut settings.general.am_pm_clock,
Some(|frontend, _| {
frontend.tasks.push(FrontendTask::RefreshClock);
}),
)?;
init_setting_checkbox(
&mut params,
state
.data
.fetch_component_as::<ComponentCheckbox>("cb_opaque_background")?,
|settings| &mut settings.general.opaque_background,
Some(|frontend, _| {
frontend.tasks.push(FrontendTask::RefreshBackground);
}),
)?;
init_setting_checkbox(
&mut params,
state
.data
.fetch_component_as::<ComponentCheckbox>("cb_xwayland_by_default")?,
|settings| &mut settings.tweaks.xwayland_by_default,
None,
)?;
Ok(Self { state })
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Copy, AsRefStr, EnumString)]
enum SettingType {
AnimationSpeed,
RoundMultiplier,
InvertScrollDirectionX,
InvertScrollDirectionY,
ScrollSpeed,
LongPressDuration,
NotificationsEnabled,
NotificationsSoundEnabled,
KeyboardSoundEnabled,
UprightScreenFix,
DoubleCursorFix,
SetsOnWatch,
HideGrabHelp,
XrClickSensitivity,
XrClickSensitivityRelease,
AllowSliding,
ClickFreezeTimeMs,
FocusFollowsMouseMode,
LeftHandedMouse,
BlockGameInput,
BlockGameInputIgnoreWatch,
SpaceDragMultiplier,
UseSkybox,
UsePassthrough,
ScreenRenderDown,
PointerLerpFactor,
SpaceDragUnlocked,
SpaceRotateUnlocked,
Clock12h,
HideUsername,
OpaqueBackground,
XwaylandByDefault,
CaptureMethod,
KeyboardMiddleClick,
}
impl SettingType {
pub fn mut_bool<'a>(self, config: &'a mut GeneralConfig) -> &'a mut bool {
match self {
Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x,
Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y,
Self::NotificationsEnabled => &mut config.notifications_enabled,
Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled,
Self::KeyboardSoundEnabled => &mut config.keyboard_sound_enabled,
Self::UprightScreenFix => &mut config.upright_screen_fix,
Self::DoubleCursorFix => &mut config.double_cursor_fix,
Self::SetsOnWatch => &mut config.sets_on_watch,
Self::HideGrabHelp => &mut config.hide_grab_help,
Self::AllowSliding => &mut config.allow_sliding,
Self::FocusFollowsMouseMode => &mut config.focus_follows_mouse_mode,
Self::LeftHandedMouse => &mut config.left_handed_mouse,
Self::BlockGameInput => &mut config.block_game_input,
Self::BlockGameInputIgnoreWatch => &mut config.block_game_input_ignore_watch,
Self::UseSkybox => &mut config.use_skybox,
Self::UsePassthrough => &mut config.use_passthrough,
Self::ScreenRenderDown => &mut config.screen_render_down,
Self::SpaceDragUnlocked => &mut config.space_drag_unlocked,
Self::SpaceRotateUnlocked => &mut config.space_rotate_unlocked,
Self::Clock12h => &mut config.clock_12h,
Self::HideUsername => &mut config.hide_username,
Self::OpaqueBackground => &mut config.opaque_background,
Self::XwaylandByDefault => &mut config.xwayland_by_default,
_ => panic!("Requested bool for non-bool SettingType"),
}
}
pub fn mut_f32<'a>(self, config: &'a mut GeneralConfig) -> &'a mut f32 {
match self {
Self::AnimationSpeed => &mut config.animation_speed,
Self::RoundMultiplier => &mut config.round_multiplier,
Self::ScrollSpeed => &mut config.scroll_speed,
Self::LongPressDuration => &mut config.long_press_duration,
Self::XrClickSensitivity => &mut config.xr_click_sensitivity,
Self::XrClickSensitivityRelease => &mut config.xr_click_sensitivity_release,
Self::SpaceDragMultiplier => &mut config.space_drag_multiplier,
Self::PointerLerpFactor => &mut config.pointer_lerp_factor,
_ => panic!("Requested f32 for non-f32 SettingType"),
}
}
pub fn mut_i32<'a>(self, config: &'a mut GeneralConfig) -> &'a mut i32 {
match self {
Self::ClickFreezeTimeMs => &mut config.click_freeze_time_ms,
_ => panic!("Requested i32 for non-i32 SettingType"),
}
}
pub fn set_enum<'a>(self, config: &'a mut GeneralConfig, value: &str) {
match self {
Self::CaptureMethod => {
config.capture_method = wlx_common::config::CaptureMethod::from_str(value).expect("Invalid enum value!")
}
Self::KeyboardMiddleClick => {
config.keyboard_middle_click_mode =
wlx_common::config::AltModifier::from_str(value).expect("Invalid enum value!")
}
_ => panic!("Requested enum for non-enum SettingType"),
}
}
fn get_enum_title<'a>(self, config: &'a mut GeneralConfig) -> Translation {
match self {
Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method),
Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode),
_ => panic!("Requested enum for non-enum SettingType"),
}
}
fn get_enum_title_inner<E>(value: E) -> Translation
where
E: EnumProperty + AsRef<str>,
{
value
.get_str("Translation")
.map(|x| Translation::from_translation_key(x))
.or_else(|| value.get_str("Text").map(|x| Translation::from_raw_text(x)))
.unwrap_or_else(|| Translation::from_raw_text(value.as_ref()))
}
fn get_enum_tooltip_inner<E>(value: E) -> Option<Translation>
where
E: EnumProperty + AsRef<str>,
{
value.get_str("Tooltip").map(|x| Translation::from_translation_key(x))
}
/// Ok is translation, Err is raw text
fn get_translation(self) -> Result<&'static str, &'static str> {
match self {
Self::AnimationSpeed => Ok("APP_SETTINGS.ANIMATION_SPEED"),
Self::RoundMultiplier => Ok("APP_SETTINGS.ROUND_MULTIPLIER"),
Self::InvertScrollDirectionX => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_X"),
Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"),
Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"),
Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"),
Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"),
Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"),
Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"),
Self::UprightScreenFix => Ok("APP_SETTINGS.UPRIGHT_SCREEN_FIX"),
Self::DoubleCursorFix => Ok("APP_SETTINGS.DOUBLE_CURSOR_FIX"),
Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"),
Self::HideGrabHelp => Ok("APP_SETTINGS.HIDE_GRAB_HELP"),
Self::XrClickSensitivity => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"),
Self::XrClickSensitivityRelease => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE"),
Self::AllowSliding => Ok("APP_SETTINGS.ALLOW_SLIDING"),
Self::ClickFreezeTimeMs => Ok("APP_SETTINGS.CLICK_FREEZE_TIME_MS"),
Self::FocusFollowsMouseMode => Ok("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE"),
Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"),
Self::BlockGameInput => Ok("APP_SETTINGS.BLOCK_GAME_INPUT"),
Self::BlockGameInputIgnoreWatch => Ok("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH"),
Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"),
Self::UseSkybox => Ok("APP_SETTINGS.USE_SKYBOX"),
Self::UsePassthrough => Ok("APP_SETTINGS.USE_PASSTHROUGH"),
Self::ScreenRenderDown => Ok("APP_SETTINGS.SCREEN_RENDER_DOWN"),
Self::PointerLerpFactor => Ok("APP_SETTINGS.POINTER_LERP_FACTOR"),
Self::SpaceDragUnlocked => Ok("APP_SETTINGS.SPACE_DRAG_UNLOCKED"),
Self::SpaceRotateUnlocked => Ok("APP_SETTINGS.SPACE_ROTATE_UNLOCKED"),
Self::Clock12h => Ok("APP_SETTINGS.CLOCK_12H"),
Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"),
Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"),
Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"),
Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"),
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
}
}
fn get_tooltip(self) -> Option<&'static str> {
match self {
Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"),
Self::DoubleCursorFix => Some("APP_SETTINGS.DOUBLE_CURSOR_FIX_HELP"),
Self::XrClickSensitivity => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"),
Self::XrClickSensitivityRelease => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE_HELP"),
Self::FocusFollowsMouseMode => Some("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE_HELP"),
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
Self::BlockGameInput => Some("APP_SETTINGS.BLOCK_GAME_INPUT_HELP"),
Self::BlockGameInputIgnoreWatch => Some("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH_HELP"),
Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"),
Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"),
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"),
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
_ => None,
}
}
//TODO: incorporate this
fn requires_restart(self) -> bool {
match self {
Self::AnimationSpeed
| Self::RoundMultiplier
| Self::UprightScreenFix
| Self::DoubleCursorFix
| Self::SetsOnWatch
| Self::UseSkybox
| Self::UsePassthrough
| Self::ScreenRenderDown => true,
_ => false,
}
}
fn get_frontend_task(self) -> Option<FrontendTask> {
match self {
Self::Clock12h => Some(FrontendTask::RefreshClock),
Self::OpaqueBackground => Some(FrontendTask::RefreshBackground),
_ => None,
}
}
}
macro_rules! category {
($pe:expr, $root:expr, $translation:expr, $icon:expr) => {{
let id = $pe.idx.to_string();
$pe.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("translation"), Rc::from($translation));
params.insert(Rc::from("icon"), Rc::from($icon));
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
$pe
.parser_state
.instantiate_template($pe.doc_params, "SettingsGroupBox", $pe.layout, $root, params)?;
$pe.parser_state.get_widget_id(&id)
}};
}
macro_rules! checkbox {
($mp:expr, $root:expr, $setting:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let checked = if *$setting.mut_bool($mp.config) { "1" } else { "0" };
params.insert(Rc::from("checked"), Rc::from(checked));
$mp
.parser_state
.instantiate_template($mp.doc_params, "CheckBoxSetting", $mp.layout, $root, params)?;
let checkbox = $mp.parser_state.fetch_component_as::<ComponentCheckbox>(&id)?;
checkbox.on_toggle(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateBool($setting, e.checked));
Ok(())
}
}));
};
}
macro_rules! slider_f32 {
($mp:expr, $root:expr, $setting:expr, $min:expr, $max:expr, $step:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let value = $setting.mut_f32($mp.config).to_string();
params.insert(Rc::from("value"), Rc::from(value));
params.insert(Rc::from("min"), Rc::from($min.to_string()));
params.insert(Rc::from("max"), Rc::from($max.to_string()));
params.insert(Rc::from("step"), Rc::from($step.to_string()));
$mp
.parser_state
.instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, $root, params)?;
let slider = $mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateFloat($setting, e.value));
Ok(())
}
}));
};
}
macro_rules! slider_i32 {
($mp:expr, $root:expr, $setting:expr, $min:expr, $max:expr, $step:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
let value = $setting.mut_i32($mp.config).to_string();
params.insert(Rc::from("value"), Rc::from(value));
params.insert(Rc::from("min"), Rc::from($min.to_string()));
params.insert(Rc::from("max"), Rc::from($max.to_string()));
params.insert(Rc::from("step"), Rc::from($step.to_string()));
$mp
.parser_state
.instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, $root, params)?;
let slider = $mp.parser_state.fetch_component_as::<ComponentSlider>(&id)?;
slider.on_value_changed(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::UpdateInt($setting, e.value as i32));
Ok(())
}
}));
};
}
macro_rules! dropdown {
($mp:expr, $root:expr, $setting:expr, $options:expr) => {
let id = $mp.idx.to_string();
$mp.idx += 1;
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
match $setting.get_translation() {
Ok(translation) => params.insert(Rc::from("translation"), translation.into()),
Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()),
};
if let Some(tooltip) = $setting.get_tooltip() {
params.insert(Rc::from("tooltip"), Rc::from(tooltip));
}
$mp
.parser_state
.instantiate_template($mp.doc_params, "DropdownButton", $mp.layout, $root, params)?;
let setting_str = $setting.as_ref();
let title = $setting.get_enum_title($mp.config);
{
let mut label = $mp
.parser_state
.fetch_widget_as::<WidgetLabel>(&$mp.layout.state, &format!("{id}_value"))?;
label.set_text_simple(&mut $mp.layout.state.globals.get(), title);
}
let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Box::new({
let tasks = $mp.tasks.clone();
move |_common, e| {
tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(),
$options
.iter()
.filter_map(|item| {
if item.get_bool("Hidden").unwrap_or(false) {
return None;
}
let value = item.as_ref();
let title = SettingType::get_enum_title_inner(*item);
let tooltip = SettingType::get_enum_tooltip_inner(*item);
let text = &title.text;
let translated = if title.translated { "1" } else { "0" };
Some(context_menu::Cell {
action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()),
title,
tooltip,
attribs: vec![],
})
})
.collect(),
));
Ok(())
}
}));
};
}
macro_rules! 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 popup_manager;
pub mod steam_utils;
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
#[serde(rename = "device.nick")]
pub device_nick: String, // HD-Audio Generic
pub device_nick: Option<String>, // HD-Audio Generic
}
#[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)?;
// 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)
}

View File

@@ -14,6 +14,7 @@ use wgui::{
taffy::Display,
widget::label::WidgetLabel,
};
use wlx_common::config::GeneralConfig;
use crate::frontend::{FrontendTask, FrontendTasks};
@@ -55,6 +56,7 @@ pub struct PopupManager {
pub struct PopupContentFuncData<'a> {
pub layout: &'a mut Layout,
pub config: &'a GeneralConfig,
pub handle: PopupHandle,
pub id_content: WidgetID,
}
@@ -122,6 +124,7 @@ impl PopupManager {
layout: &mut Layout,
frontend_tasks: FrontendTasks,
params: MountPopupParams,
config: &GeneralConfig,
) -> anyhow::Result<()> {
let doc_params = &ParseDocumentParams {
globals: globals.clone(),
@@ -175,6 +178,7 @@ impl PopupManager {
layout,
handle: popup_handle.clone(),
id_content,
config,
})?;
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,
layout::{Layout, LayoutTask, LayoutTasks, WidgetID},
renderer_vk::{
text::{FontWeight, TextStyle},
text::{FontWeight, HorizontalAlign, TextStyle},
util::centered_matrix,
},
taffy::{
self,
prelude::{length, percent},
prelude::{auto, length, percent},
},
widget::{
div::WidgetDiv,
@@ -47,7 +47,7 @@ impl Drop for MountedToast {
}
}
const TOAST_DURATION_TICKS: u32 = 90;
const TOAST_DURATION_TICKS: u32 = 150;
impl ToastManager {
pub fn new() -> Self {
@@ -102,6 +102,10 @@ impl ToastManager {
top: length(8.0),
bottom: length(8.0),
},
max_size: taffy::Size {
width: length(400.0),
height: auto(),
},
..Default::default()
},
)?;
@@ -114,6 +118,8 @@ impl ToastManager {
content,
style: TextStyle {
weight: Some(FontWeight::Bold),
align: Some(HorizontalAlign::Center),
wrap: true,
..Default::default()
},
},
@@ -124,7 +130,7 @@ impl ToastManager {
// show-up animation
layout.animations.add(Animation::new(
rect.id,
160, // does not use anim_mult
(TOAST_DURATION_TICKS as f32 * globals.defaults.animation_mult) as u32,
AnimationEasing::Linear,
Box::new(move |common, data| {
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 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));
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<()> {
if !self.needs_tick {
return Ok(());
}
let mut state = self.state.borrow_mut();
if state.timeout > 0 {
state.timeout -= 1;
}
if !self.needs_tick {
return Ok(());
}
if state.timeout == 0 {
state.toast = None;
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::{
assets::AssetPath,
components::{button::ComponentButton, checkbox::ComponentCheckbox, radio_group::ComponentRadioGroup},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
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 {
#[allow(dead_code)]
pub state: ParserState,
//entry: DesktopEntry,
state: ParserState,
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 globals: WguiGlobals,
pub globals: &'a WguiGlobals,
pub entry: DesktopEntry,
pub layout: &'a mut Layout,
pub parent_id: WidgetID,
pub config: &'a GeneralConfig,
pub frontend_tasks: &'a FrontendTasks,
pub on_launched: Box<dyn Fn()>,
}
impl View {
@@ -33,12 +114,34 @@ impl View {
};
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")?;
// app icon
if let Some(icon_path) = &params.entry.icon_path {
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(
doc_params,
"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")?;
label_title.set_text_simple(
@@ -56,8 +268,157 @@ impl View {
);
Ok(Self {
//entry: params.entry,
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,
layout::{Layout, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks,
widget::ConstructEssentials,
};
use crate::{
frontend::{FrontendTask, FrontendTasks},
task::Tasks,
util::pactl_wrapper,
util::pactl_wrapper::{self},
};
#[derive(Clone)]
@@ -172,7 +172,8 @@ fn does_string_mention_hmd_sink(input: &str) -> bool {
lwr.contains("index") || // Valve hardware
lwr.contains("oculus") || // 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 {
@@ -180,7 +181,8 @@ fn does_string_mention_hmd_source(input: &str) -> bool {
lwr.contains("hmd") || // generic hmd name detected
lwr.contains("valve") || // Valve hardware
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 {
@@ -401,7 +403,30 @@ struct MountDeviceSliderParams<'a> {
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(
globals: &WguiGlobals,
frontend_tasks: &FrontendTasks,
card: &pactl_wrapper::Card,
profile_name: &str,
@@ -424,32 +449,32 @@ fn switch_sink_card(
}
if sink_found {
frontend_tasks.push(FrontendTask::PushToast(Translation::from_translation_key(
format!("[AUDIO.SPEAKERS_SET_SUCCESSFULLY]: {}", name.name).as_str(),
)));
push_popup_speakers_set_successfully(globals, frontend_tasks, &name.name);
} else {
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(())
}
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) {
Ok(()) => {
frontend_tasks.push(FrontendTask::PushToast(Translation::from_translation_key(
format!(
"[AUDIO.MICROPHONE_SET_SUCCESSFULLY]: {}",
if let Some(card_name) = &source.properties.card_name {
card_name
} else {
&source.description
}
)
.as_str(),
)));
push_popup_microphone_set_successfully(
globals,
frontend_tasks,
if let Some(card_name) = &source.properties.card_name {
card_name
} else {
&source.description
},
);
Ok(())
}
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 mut switched = false;
for source in &sources {
if is_source_mentioning_hmd(source) {
switch_source(frontend_tasks, source)?;
switch_source(globals, frontend_tasks, source)?;
switched = true;
break;
}
@@ -529,10 +554,20 @@ fn get_best_profile_from_array<'a>(arr: &[CardPriorityResult<'a>]) -> Option<Car
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 sinks = pactl_wrapper::list_sinks()?;
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 {
if !is_card_mentioning_hmd(card) {
continue;
@@ -545,7 +580,7 @@ fn switch_to_vr_speakers(frontend_tasks: &FrontendTasks) -> anyhow::Result<()> {
if !best_profiles.is_empty() {
let best_profile = get_best_profile_from_array(&best_profiles).unwrap();
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(());
}
@@ -556,7 +591,7 @@ fn switch_to_vr_speakers(frontend_tasks: &FrontendTasks) -> anyhow::Result<()> {
if !name.is_vr {
continue;
}
switch_sink_card(frontend_tasks, card, profile_name, &name)?;
switch_sink_card(globals, frontend_tasks, card, profile_name, &name)?;
return Ok(());
}
}
@@ -684,8 +719,8 @@ impl View {
pactl_wrapper::set_card_profile(c.card.index, &c.profile_name)?;
}
ViewTask::AutoSwitch => {
switch_to_vr_microphone(&self.frontend_tasks)?;
switch_to_vr_speakers(&self.frontend_tasks)?;
switch_to_vr_speakers(&self.globals, &self.frontend_tasks)?;
switch_to_vr_microphone(&self.globals, &self.frontend_tasks)?;
self.tasks.push(ViewTask::Remount);
}
}
@@ -745,8 +780,14 @@ impl View {
par.insert("device_name".into(), disp.name.as_str().into());
par.insert("device_icon".into(), disp.icon_path.into());
} 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_icon".into(), "dashboard/binary.svg".into());
par.insert("device_icon".into(), icon_path.into());
}
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 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"
edition = "2024"

View File

@@ -5,12 +5,16 @@ Glossary:
- wlx-overlay-s: The name of this software (also called WlxOverlay-S)
- 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
- Monado: A VR compositor
- OpenVR: API made by Valve
- OpenXR: API made by Khronos
- OSC: OpenSoundControl
- 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
- 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-shaders = { workspace = true }
dash-frontend = { path = "../dash-frontend/" }
wlx-common = { path = "../wlx-common" }

View File

@@ -9,6 +9,15 @@
align_self="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>
<rectangle position="absolute" width="100%" height="100%" color="#1e3a3eee" />
<div
@@ -25,8 +34,8 @@
<rectangle macro="rect">
<label id="label_current_option" text="Click any of these buttons" size="20" weight="bold" />
<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_aqua" text="Aqua button" width="150" height="32" color="#00FFFF" tooltip="I'm at the bottom" tooltip_side="bottom" />
<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_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" />
</div>
<div gap="4">
@@ -50,6 +59,11 @@
</div>
</rectangle>
<rectangle macro="rect">
<label text="Context menu test" />
<Button id="button_context_menu" text="Show context menu" />
</rectangle>
<rectangle macro="rect">
<label text="visibility test" weight="bold" />
<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