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>
41
.github/workflows/build-full-appimage.yml
vendored
@@ -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
|
||||
@@ -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
|
||||
23
.github/workflows/make-release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
13
Cargo.toml
@@ -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",
|
||||
] }
|
||||
|
||||
35
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
3
dash-frontend/assets/dashboard/blocks.svg
Normal 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 |
3
dash-frontend/assets/dashboard/controller.svg
Normal 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 |
1
dash-frontend/assets/dashboard/down.svg
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../wlx-overlay-s/src/assets/keyboard/down.svg
|
||||
7
dash-frontend/assets/dashboard/options.svg
Normal 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 |
3
dash-frontend/assets/dashboard/palette.svg
Normal 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 |
BIN
dash-frontend/assets/dashboard/placeholder_cover.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
169
dash-frontend/assets/dashboard/wivrn_head_symbolic.svg
Normal 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 |
@@ -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>
|
||||
|
||||
27
dash-frontend/assets/gui/t_dropdown_button.xml
Normal 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>
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
|
||||
<elements>
|
||||
<TabTitle translation="GAMES" icon="dashboard/games.svg" />
|
||||
<div id="game_list_parent" align_items="center" />
|
||||
</elements>
|
||||
</layout>
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
19
dash-frontend/assets/gui/view/game_launcher.xml
Normal 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>
|
||||
7
dash-frontend/assets/gui/view/game_list.xml
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
9
dash-frontend/assets/gui/view/process_list.xml
Normal 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>
|
||||
9
dash-frontend/assets/gui/view/window_list.xml
Normal 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>
|
||||
16
dash-frontend/assets/gui/view/window_options.xml
Normal 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>
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "ディスプレイの明るさ"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
BIN
dash-frontend/assets/sound/app_start.mp3
Normal file
BIN
dash-frontend/assets/sound/startup.mp3
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
mod assets;
|
||||
pub mod frontend;
|
||||
pub mod settings;
|
||||
mod tab;
|
||||
mod task;
|
||||
mod util;
|
||||
mod various;
|
||||
mod views;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
103
dash-frontend/src/util/cached_fetcher.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
134
dash-frontend/src/util/http_client.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
297
dash-frontend/src/util/steam_utils.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
77
dash-frontend/src/util/various.rs
Normal 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(())
|
||||
}
|
||||
@@ -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>(¶ms.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) = ¶ms.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>(¶ms.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]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
317
dash-frontend/src/views/game_cover.rs
Normal 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(¶ms.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
|
||||
}
|
||||
}
|
||||
195
dash-frontend/src/views/game_launcher.rs
Normal 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>(¶ms.layout.state, "label_title")?;
|
||||
label_title.set_text_simple(
|
||||
&mut params.globals.get(),
|
||||
Translation::from_raw_text(¶ms.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: ¶ms.executor,
|
||||
manifest: ¶ms.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)();
|
||||
}
|
||||
}
|
||||
269
dash-frontend/src/views/game_list.rs
Normal 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(¶ms.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(())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
255
dash-frontend/src/views/process_list.rs
Normal 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(¶ms.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(())
|
||||
}
|
||||
}
|
||||
290
dash-frontend/src/views/window_list.rs
Normal 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(¶ms.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(())
|
||||
}
|
||||
}
|
||||
162
dash-frontend/src/views/window_options.rs
Normal 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,
|
||||
¶ms.globals,
|
||||
¶ms.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)();
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
style_edition = "2024"
|
||||
edition = "2024"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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" />
|
||||
|
||||
1
uidev/assets/sound/wgui_button_press.mp3
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../wlx-overlay-s/src/assets/sound/wgui_button_press.mp3
|
||||
1
uidev/assets/sound/wgui_button_release.mp3
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../wlx-overlay-s/src/assets/sound/wgui_button_release.mp3
|
||||
1
uidev/assets/sound/wgui_checkbox_check.mp3
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../wlx-overlay-s/src/assets/sound/wgui_checkbox_check.mp3
|
||||