mirror of https://github.com/wayvr-org/wayvr.git
Merge branch 'main' into uinput-toast
This commit is contained in:
commit
f6cd9ad90c
|
|
@ -22,25 +22,28 @@ If this is a regression, please mention which version was working previously.
|
|||
**Kernel version**:
|
||||
|
||||
**VR Runtime**:
|
||||
- [ ] Monado/WiVRn
|
||||
- [ ] SteamVR/ALVR
|
||||
- [ ] Monado
|
||||
- [ ] WiVRn
|
||||
- [ ] ALVR
|
||||
- [ ] SteamLink
|
||||
- [ ] SteamVR (not SteamLink)
|
||||
|
||||
<!-- Run `vulkaninfo --summary` and paste the devices section from the bottom. -->
|
||||
**GPU models and driver versions**:
|
||||
|
||||
## Overlay Logs
|
||||
|
||||
<!-- Start the overlay once more with the following environment variables:
|
||||
RUST_BACKTRACE=full
|
||||
RUST_LOG=debug
|
||||
If your issue is graphical or crash or freeze, also add:
|
||||
VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation
|
||||
<!-- Start the overlay from terminal, with additional environment variables:
|
||||
|
||||
Next, create the empty log file: echo > /tmp/wlx.log
|
||||
AppImage:
|
||||
RUST_BACKTRACE=full RUST_LOG=debug /path/to/WayVR.AppImage
|
||||
|
||||
Be sure to go and reproduce the issue once more, after these have been set.
|
||||
System or AUR package:
|
||||
RUST_BACKTRACE=full RUST_LOG=debug wayvr
|
||||
|
||||
Upload the log file from: /tmp/wlx.log
|
||||
Reproduce the issue once more, while WayVR is running from the terminal.
|
||||
|
||||
Upload the log file from: /tmp/wayvr.log
|
||||
|
||||
-->
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./wayvr
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
name: Build AppImage
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./wayvr
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./wayvr
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./wayvr
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./wayvr
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./wayvr
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ env:
|
|||
|
||||
jobs:
|
||||
make_release:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./wayvr
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
#!/bin/sh
|
||||
|
||||
sudo add-apt-repository -syn universe
|
||||
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libxkbcommon-x11-0 libxkbcommon-x11-dev libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1 libwayland-dev libegl-dev libxcb-glx0 libxcb-glx0-dev
|
||||
rustup update
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
77
Cargo.toml
77
Cargo.toml
|
|
@ -1,3 +1,42 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"dash-frontend",
|
||||
"scripts/prost_build",
|
||||
"uidev",
|
||||
"wayvr",
|
||||
"wayvr-ipc",
|
||||
"wayvrctl",
|
||||
"wgui",
|
||||
"wlx-capture",
|
||||
"wlx-common",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
glam = { version = "0.30.9", features = ["mint", "serde"] }
|
||||
idmap = "0.2.2"
|
||||
idmap-derive = "0.2.22"
|
||||
log = "0.4.29"
|
||||
regex = "1.12.2"
|
||||
rust-embed = "8.9.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
slotmap = "1.1.1"
|
||||
smol = "2.0.2"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
uuid = { version = "1.19.0", features = ["fast-rng", "v4", "serde"] }
|
||||
vulkano = { version = "0.35.2", default-features = false, features = [
|
||||
"macros",
|
||||
] }
|
||||
vulkano-shaders = "0.35.0"
|
||||
wayland-client = { version = "0.31.11" }
|
||||
xdg = "3.0.0"
|
||||
|
||||
[patch.crates-io]
|
||||
vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928a56ce16b376037c1120f2b167678" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
debug = true
|
||||
|
|
@ -19,41 +58,3 @@ incremental = true
|
|||
[profile.release-with-debug]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"uidev",
|
||||
"wgui",
|
||||
"wlx-common",
|
||||
"wayvr",
|
||||
"wlx-capture",
|
||||
"dash-frontend",
|
||||
"wayvr-ipc",
|
||||
"wayvrctl",
|
||||
"scripts/prost_build",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[patch.crates-io]
|
||||
vulkano = { git = "https://github.com/galister/vulkano.git", rev = "cf7f92867928a56ce16b376037c1120f2b167678" }
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
smol = "2.0.2"
|
||||
glam = { version = "0.30.9", features = ["mint", "serde"] }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
xdg = "3.0.0"
|
||||
idmap = "0.2.2"
|
||||
idmap-derive = "0.2.22"
|
||||
log = "0.4.29"
|
||||
regex = "1.12.2"
|
||||
rust-embed = "8.9.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
slotmap = "1.1.1"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
vulkano = { version = "0.35.2", default-features = false, features = [
|
||||
"macros",
|
||||
] }
|
||||
vulkano-shaders = "0.35.0"
|
||||
wayland-client = { version = "0.31.11" }
|
||||
|
|
|
|||
|
|
@ -8,26 +8,25 @@ authors = ["galister", "oo8dev"]
|
|||
repository = "https://github.com/wlx-team/wayvr"
|
||||
|
||||
[dependencies]
|
||||
wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
|
||||
wgui = { path = "../wgui/" }
|
||||
wlx-common = { path = "../wlx-common" }
|
||||
|
||||
anyhow.workspace = true
|
||||
async-native-tls = "0.5.0"
|
||||
chrono = "0.4.42"
|
||||
glam = { workspace = true, features = ["mint", "serde"] }
|
||||
http-body-util = "0.1.3"
|
||||
hyper = { version = "1.8.1", features = ["client", "http1", "http2"] }
|
||||
keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
|
||||
log.workspace = true
|
||||
xdg.workspace = true
|
||||
rust-embed.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
chrono = "0.4.42"
|
||||
keyvalues-parser = { git = "https://github.com/CosmicHorrorDev/vdf-rs.git", rev = "fc6dcbea9eb13cacb98dea40063f6f56cde6e145" }
|
||||
smol = { workspace = true }
|
||||
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"
|
||||
strum.workspace = true
|
||||
uuid.workspace = true
|
||||
wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
|
||||
wgui = { path = "../wgui/" }
|
||||
wlx-common = { path = "../wlx-common" }
|
||||
xdg.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["monado"]
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 9.8 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#33FF99" d="m9.55 18l-5.7-5.7l1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 295 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"/></svg>
|
||||
|
After Width: | Height: | Size: 360 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#FF4455" d="M12 17q.425 0 .713-.288T13 16t-.288-.712T12 15t-.712.288T11 16t.288.713T12 17m-1-4h2V7h-2zm1 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/></svg>
|
||||
|
After Width: | Height: | Size: 574 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8"/></svg>
|
||||
|
After Width: | Height: | Size: 286 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zM17 6H7v13h10zM9 17h2V8H9zm4 0h2V8h-2zM7 6v13z"/></svg>
|
||||
|
After Width: | Height: | Size: 360 B |
|
|
@ -86,7 +86,8 @@
|
|||
round="8"
|
||||
flex_grow="1"
|
||||
width="100%"
|
||||
overflow_y="scroll"
|
||||
overflow="scroll"
|
||||
position="relative"
|
||||
>
|
||||
<!-- radial gradient -->
|
||||
<rectangle
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<layout>
|
||||
<template name="LoadingWithText">
|
||||
<div id="root" width="100%" height="100%" align_items="center" justify_content="center">
|
||||
<div flex_direction="row" gap="8" align_items="center" >
|
||||
<sprite id="sprite_loading" src_builtin="dashboard/loading.svg" width="32" height="32"/>
|
||||
<label translation="LOADING" weight="bold"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="LoadingWithoutText">
|
||||
<div id="root" width="100%" height="100%" align_items="center" justify_content="center">
|
||||
<sprite id="sprite_loading" src_builtin="dashboard/loading.svg" width="32" height="32"/>
|
||||
</div>
|
||||
</template>
|
||||
</layout>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
|
||||
<div gap="8" flex_direction="column" width="100%">
|
||||
<label text="Curve"/>
|
||||
<label text="Curve (0 = disabled)"/>
|
||||
<Slider id="slider_keying_curve" min_value="0" max_value="100" macro="slider"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
<div gap="4">
|
||||
<Tabs id="tabs">
|
||||
<Tab name="look_and_feel" translation="APP_SETTINGS.LOOK_AND_FEEL" sprite_src_builtin="dashboard/palette.svg" />
|
||||
<Tab name="skybox" translation="APP_SETTINGS.SKYBOX" sprite_src_builtin="dashboard/globe.svg" />
|
||||
<Tab name="features" translation="APP_SETTINGS.FEATURES" sprite_src_builtin="dashboard/options.svg" />
|
||||
<Tab name="controls" translation="APP_SETTINGS.CONTROLS" sprite_src_builtin="dashboard/controller.svg" />
|
||||
<Tab name="misc" translation="APP_SETTINGS.MISC" sprite_src_builtin="dashboard/blocks.svg" />
|
||||
|
|
@ -59,4 +60,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
</layout>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<layout>
|
||||
<template name="DialogBoxButton">
|
||||
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="${icon}"/>
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
<div flex_direction="column" align_items="center" justify_content="center" width="100%" gap="32">
|
||||
<label id="label_message" size="18" weight="bold"/>
|
||||
|
||||
<div id="buttons" gap="8">
|
||||
<!-- filled-in at runtime -->
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<layout>
|
||||
<include src="../t_separator.xml"/>
|
||||
|
||||
<template name="btn_close">
|
||||
<Button id="btn" translation="CLOSE_WINDOW" align_self="start" sprite_src_builtin="dashboard/check.svg"/>
|
||||
</template>
|
||||
|
||||
<elements>
|
||||
<div align_items="center" justify_content="center" width="100%">
|
||||
<div id="content" flex_direction="column" gap="8" width="100%">
|
||||
<label translation="DOWNLOADING_FILE" size="24" weight="bold"/>
|
||||
<Separator/>
|
||||
<label id="label_target_path" color="~color_text_translucent" />
|
||||
|
||||
<div gap="8" align_items="center">
|
||||
<div id="loading_parent"/>
|
||||
<label id="label_status"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
width="100%"
|
||||
height="100%"
|
||||
flex_direction="column"
|
||||
position="absolute"
|
||||
>
|
||||
<!-- Top black bar -->
|
||||
<rectangle
|
||||
|
|
@ -32,14 +33,18 @@
|
|||
</div>
|
||||
</rectangle>
|
||||
|
||||
<!-- Content -->
|
||||
<rectangle height="100%"
|
||||
<rectangle
|
||||
width="100%"
|
||||
height="100%"
|
||||
color="#010310fe"
|
||||
color2="#051c55fc"
|
||||
gradient="vertical"
|
||||
padding="16"
|
||||
id="content">
|
||||
position="relative"
|
||||
>
|
||||
<div id="content" padding="16" width="100%" height="100%" position="absolute" overflow_y="scroll">
|
||||
<!-- Content, filled-in at runtime -->
|
||||
</div>
|
||||
</rectangle>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
</layout>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<layout>
|
||||
|
||||
<!--
|
||||
parameters:
|
||||
"text"
|
||||
"sprite"
|
||||
|
||||
ids:
|
||||
"button"
|
||||
-->
|
||||
<template name="ResolutionButton">
|
||||
<Button id="button" sprite_src_builtin="${sprite}" text="${text}"/>
|
||||
</template>
|
||||
|
||||
<include src="../t_separator.xml"/>
|
||||
|
||||
<elements>
|
||||
<div gap="8" flex_direction="column" min_width="100%" overflow_y="scroll">
|
||||
<div gap="8" flex_direction="column" align_items="center">
|
||||
<div id="resolution_buttons" gap="8" flex_direction="row">
|
||||
<!-- filled-in at runtime -->
|
||||
</div>
|
||||
<image id="image" width="400" height="200" round="8" border="2" border_color="~color_accent"/>
|
||||
<label id="label_author" weight="bold"/>
|
||||
<label id="label_description" wrap="1"/>
|
||||
<Separator/>
|
||||
<!-- nerdy stuff below -->
|
||||
<div gap="24" justify_content="center">
|
||||
<label size="10" id="label_creation_date"/>
|
||||
<label size="10" id="label_modification_date"/>
|
||||
<label size="10" id="label_version"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<layout>
|
||||
<elements>
|
||||
<div id="list" gap="8" flex_wrap="wrap" align_self="baseline">
|
||||
<!-- filled-in at runtime -->
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<layout>
|
||||
<elements>
|
||||
<div flex_direction="column" gap="8" width="100%" align_items="center">
|
||||
<div flex_direction="row" gap="4" align_self="end">
|
||||
<Button id="btn_refresh" tooltip="RELOAD_FROM_DISK" width="32" height="32" sprite_src_builtin="dashboard/refresh.svg" />
|
||||
<Button id="btn_download_skymaps" height="32" translation="APP_SETTINGS.BROWSE_ONLINE_CATALOG" sprite_src_builtin="dashboard/download.svg"/>
|
||||
</div>
|
||||
<div id="list_parent" gap="8" flex_direction="row" flex_wrap="wrap" />
|
||||
</div>
|
||||
</elements>
|
||||
</layout>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<layout>
|
||||
|
||||
<!--
|
||||
ids:
|
||||
"button"
|
||||
"image_preview"
|
||||
"label_title"
|
||||
"label_desc"
|
||||
-->
|
||||
<template name="Cell">
|
||||
<Button
|
||||
id="button"
|
||||
padding="8"
|
||||
round="8"
|
||||
flex_direction="column"
|
||||
gap="4"
|
||||
width="256"
|
||||
align_items="center"
|
||||
align_self="start">
|
||||
<image id="image_preview" width="100%" height="128" round="6">
|
||||
<!-- new_pass is required, because we need to render rectangles at the top of the image. Sorry. -->
|
||||
<div new_pass="1" id="resolution_pips" gap="4" margin="6"/>
|
||||
</image>
|
||||
<label id="label_title" wrap="1" weight="bold"/>
|
||||
<label id="label_author" wrap="1"/>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
|
||||
params:
|
||||
"color"
|
||||
"text"
|
||||
-->
|
||||
<template name="ResolutionPip">
|
||||
<rectangle color="${color}" padding_left="4" padding_right="4" padding_top="2" padding_bottom="2" round="3" align_self="start">
|
||||
<label text="${text}" weight="bold" size="12" shadow="#000000" shadow_x="2" shadow_y="2"/>
|
||||
</rectangle>
|
||||
</template>
|
||||
</layout>
|
||||
|
|
@ -38,6 +38,8 @@
|
|||
"BLOCK_GAME_INPUT_IGNORE_WATCH_HELP": "Do not block input when watch is hovered",
|
||||
"BLOCK_POSES_ON_KBD_INTERACTION": "Block poses when interacting with keyboard",
|
||||
"BLOCK_POSES_ON_KBD_INTERACTION_HELP": "Blocks the game from receiving poses when the keyboard is hovered and 'Block game input' is enabled",
|
||||
"BROWSE_ONLINE_CATALOG": "Browse online catalog...",
|
||||
"BROWSE_SKYMAPS": "Browse skymaps",
|
||||
"CAPTURE_METHOD": "Wayland screen capture",
|
||||
"CAPTURE_METHOD_HELP": "Try changing this if you are\nexperiencing black or glitchy screens",
|
||||
"COLOR_KEYING": "Color keying",
|
||||
|
|
@ -64,14 +66,13 @@
|
|||
"KEYBOARD_MIDDLE_CLICK": "Keyboard middle click",
|
||||
"KEYBOARD_MIDDLE_CLICK_HELP": "Modifier to use when typing\nwith purple laser",
|
||||
"KEYBOARD_SOUND_ENABLED": "Keyboard sounds",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED": "Keyboard swipe to type",
|
||||
"KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP": "Only works for English! Results will be drastically worse on non qwerty keyboards.",
|
||||
"LANGUAGE": "Language",
|
||||
"LEFT_HANDED_MOUSE": "Left-handed mouse",
|
||||
"LEFT_HANDED_MOUSE_HELP": "Use this if mouse buttons are swapped",
|
||||
"LONG_PRESS_DURATION": "Long press duration",
|
||||
"LOOK_AND_FEEL": "Look & Feel",
|
||||
"MISC": "Miscellaneous",
|
||||
"NO_SKYMAPS_FOUND": "No skymaps found",
|
||||
"NOTIFICATIONS_ENABLED": "Enable notifications",
|
||||
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
|
||||
"OPAQUE_BACKGROUND": "Opaque background",
|
||||
|
|
@ -98,7 +99,10 @@
|
|||
"SCREEN_RENDER_DOWN": "Render screen at lower resolution",
|
||||
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
|
||||
"SCROLL_SPEED": "Scroll speed",
|
||||
"SELECT_VARIANT": "Select variant",
|
||||
"SETS_ON_WATCH": "Sets on watch",
|
||||
"SKYBOX": "Skybox",
|
||||
"SKYMAP_ALREADY_DOWNLOADED": "This skymap is already downloaded. Select desired action.",
|
||||
"SPACE_DRAG_MULTIPLIER": "Space drag multiplier",
|
||||
"SPACE_DRAG_UNLOCKED": "Allow space drag on all axes",
|
||||
"SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes",
|
||||
|
|
@ -136,8 +140,12 @@
|
|||
"VOLUME": "Volume"
|
||||
},
|
||||
"CLOSE_WINDOW": "Close window",
|
||||
"CREATION_DATE": "Creation date",
|
||||
"DEBUG_INFO": "Debug info",
|
||||
"DISPLAY_BRIGHTNESS": "Display brightness",
|
||||
"DOWNLOADER": "Downloader",
|
||||
"DOWNLOAD_AGAIN": "Download again",
|
||||
"DOWNLOADING_FILE": "Downloading file...",
|
||||
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
|
||||
"GAME_LAUNCHED": "Game launched",
|
||||
"GAME_LIST": {
|
||||
|
|
@ -151,7 +159,9 @@
|
|||
"HELLO_USER": "Hello, {USER}!",
|
||||
"HIDE": "Hide",
|
||||
"HOME_SCREEN": "Home",
|
||||
"MODIFICATION_DATE": "Modification date",
|
||||
"MONADO_RUNTIME": "Monado runtime",
|
||||
"LOADING": "Loading...",
|
||||
"POPUP_ADD_DISPLAY": {
|
||||
"RESOLUTION": "Resolution"
|
||||
},
|
||||
|
|
@ -162,8 +172,12 @@
|
|||
"PROCESS_LIST": "Process list",
|
||||
"REFRESH": "Refresh",
|
||||
"REMOVE": "Remove",
|
||||
"APPLY": "Apply",
|
||||
"RELOAD_FROM_DISK": "Reload from disk",
|
||||
"SETTINGS": "Settings",
|
||||
"SHOW": "Show",
|
||||
"TARGET_PATH": "Target path",
|
||||
"TERMINATE_PROCESS": "Terminate process",
|
||||
"VERSION": "Version",
|
||||
"WIDTH": "Width"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use crate::{
|
|||
assets,
|
||||
tab::{Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, settings::TabSettings},
|
||||
util::{
|
||||
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams},
|
||||
popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams},
|
||||
toast_manager::ToastManager,
|
||||
},
|
||||
views,
|
||||
|
|
@ -101,7 +101,7 @@ pub enum FrontendTask {
|
|||
SetTab(TabType),
|
||||
RefreshClock,
|
||||
RefreshBackground,
|
||||
MountPopup(MountPopupParams),
|
||||
MountPopupOnce(MountPopupOnceParams),
|
||||
RefreshPopupManager,
|
||||
ShowAudioSettings,
|
||||
UpdateAudioSettingsView,
|
||||
|
|
@ -279,10 +279,8 @@ impl<T: 'static> Frontend<T> {
|
|||
}
|
||||
|
||||
fn update_time(&mut self, data: &mut T) -> anyhow::Result<()> {
|
||||
let mut c = self.layout.start_common();
|
||||
let mut common = c.common();
|
||||
|
||||
{
|
||||
let mut common = self.layout.common();
|
||||
let mut label = common
|
||||
.state
|
||||
.widgets
|
||||
|
|
@ -303,27 +301,20 @@ impl<T: 'static> Frontend<T> {
|
|||
label.set_text(&mut common, Translation::from_raw_text(&text));
|
||||
}
|
||||
|
||||
c.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_popup(&mut self, params: MountPopupParams, data: &mut T) -> anyhow::Result<()> {
|
||||
fn mount_popup_once(&mut self, params: MountPopupOnceParams, data: &mut T) -> anyhow::Result<()> {
|
||||
let config = self.interface.general_config(data);
|
||||
|
||||
self.popup_manager.mount_popup(
|
||||
self.globals.clone(),
|
||||
&mut self.layout,
|
||||
self.tasks.clone(),
|
||||
params,
|
||||
config,
|
||||
)?;
|
||||
self
|
||||
.popup_manager
|
||||
.mount_popup_once(&self.globals, &mut self.layout, &self.tasks, params, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn refresh_popup_manager(&mut self) -> anyhow::Result<()> {
|
||||
let mut c = self.layout.start_common();
|
||||
self.popup_manager.refresh(c.common().alterables);
|
||||
c.finish()?;
|
||||
self.popup_manager.refresh(&mut self.layout.alterables);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -356,7 +347,7 @@ impl<T: 'static> Frontend<T> {
|
|||
FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?,
|
||||
FrontendTask::RefreshClock => self.update_time(params.data)?,
|
||||
FrontendTask::RefreshBackground => self.update_background(params.data)?,
|
||||
FrontendTask::MountPopup(popup_params) => self.mount_popup(popup_params, params.data)?,
|
||||
FrontendTask::MountPopupOnce(popup_params) => self.mount_popup_once(popup_params, params.data)?,
|
||||
FrontendTask::RefreshPopupManager => self.refresh_popup_manager()?,
|
||||
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
|
||||
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
|
||||
|
|
@ -369,8 +360,7 @@ impl<T: 'static> Frontend<T> {
|
|||
}
|
||||
|
||||
fn set_tab_title(&mut self, translation: &str, icon: &str) -> anyhow::Result<()> {
|
||||
let mut c = self.layout.start_common();
|
||||
let mut common = c.common();
|
||||
let mut common = self.layout.common();
|
||||
|
||||
{
|
||||
let mut label = common
|
||||
|
|
@ -386,12 +376,11 @@ impl<T: 'static> Frontend<T> {
|
|||
.widgets
|
||||
.cast_as::<WidgetSprite>(self.widgets.id_sprite_titlebar_icon)?;
|
||||
sprite.set_content(
|
||||
&mut common,
|
||||
common.alterables,
|
||||
Some(CustomGlyphData::from_assets(&self.globals, AssetPath::BuiltIn(icon))?),
|
||||
);
|
||||
}
|
||||
|
||||
c.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ use wgui::{
|
|||
assets::AssetPath,
|
||||
components::button::{ButtonClickCallback, ComponentButton},
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{WidgetID, WidgetPair},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
task::Tasks,
|
||||
|
|
@ -17,18 +16,17 @@ use wgui::{
|
|||
use wlx_common::desktop_finder::DesktopEntry;
|
||||
|
||||
use crate::{
|
||||
frontend::{Frontend, FrontendTask, FrontendTasks},
|
||||
frontend::{Frontend, FrontendTasks},
|
||||
tab::{Tab, TabType},
|
||||
util::popup_manager::{MountPopupParams, PopupHandle},
|
||||
views::{self, app_launcher},
|
||||
util::popup_manager::PopupHolder,
|
||||
views::{self},
|
||||
};
|
||||
|
||||
enum Task {
|
||||
CloseLauncher,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
enum Task {}
|
||||
|
||||
struct State {
|
||||
view_launcher: Option<(PopupHandle, views::app_launcher::View)>,
|
||||
view_launcher: PopupHolder<views::app_launcher::View>,
|
||||
}
|
||||
|
||||
pub struct TabApps<T> {
|
||||
|
|
@ -47,21 +45,17 @@ impl<T> Tab<T> for TabApps<T> {
|
|||
}
|
||||
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
let state = self.state.borrow_mut();
|
||||
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::CloseLauncher => state.view_launcher = None,
|
||||
}
|
||||
match task {}
|
||||
}
|
||||
|
||||
self
|
||||
.app_list
|
||||
.tick(frontend, &self.state, &self.tasks, &mut self.parser_state)?;
|
||||
self.app_list.tick(frontend, &self.state, &mut self.parser_state)?;
|
||||
|
||||
if let Some((_, launcher)) = &mut state.view_launcher {
|
||||
launcher.update(&mut frontend.interface, data)?;
|
||||
}
|
||||
state
|
||||
.view_launcher
|
||||
.with_view_res(|view| view.update(&mut frontend.interface, data))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -79,40 +73,14 @@ fn on_app_click(
|
|||
globals: WguiGlobals,
|
||||
entry: DesktopEntry,
|
||||
state: Rc<RefCell<State>>,
|
||||
tasks: Tasks<Task>,
|
||||
) -> ButtonClickCallback {
|
||||
Rc::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(())
|
||||
})
|
||||
},
|
||||
}));
|
||||
views::app_launcher::mount_popup(
|
||||
frontend_tasks.clone(),
|
||||
globals.clone(),
|
||||
entry.clone(),
|
||||
state.borrow_mut().view_launcher.clone(),
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
|
@ -129,7 +97,9 @@ impl<T> TabApps<T> {
|
|||
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
|
||||
let globals = frontend.layout.state.globals.clone();
|
||||
let tasks = Tasks::new();
|
||||
let state = Rc::new(RefCell::new(State { view_launcher: None }));
|
||||
let state = Rc::new(RefCell::new(State {
|
||||
view_launcher: Default::default(),
|
||||
}));
|
||||
|
||||
let parser_state = wgui::parser::parse_from_assets(&doc_params(globals.clone()), &mut frontend.layout, parent_id)?;
|
||||
let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?;
|
||||
|
|
@ -334,7 +304,6 @@ impl AppList {
|
|||
&mut self,
|
||||
frontend: &mut Frontend<T>,
|
||||
state: &Rc<RefCell<State>>,
|
||||
tasks: &Tasks<Task>,
|
||||
parser_state: &mut ParserState,
|
||||
) -> anyhow::Result<()> {
|
||||
// load 4 entries for a single frame at most
|
||||
|
|
@ -348,7 +317,6 @@ impl AppList {
|
|||
globals.clone(),
|
||||
entry.clone(),
|
||||
state.clone(),
|
||||
tasks.clone(),
|
||||
));
|
||||
} else {
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
frontend::Frontend,
|
||||
tab::{Tab, TabType},
|
||||
util::steam_utils::SteamUtils,
|
||||
views::{game_list, running_games_list},
|
||||
views::{ViewTrait, ViewUpdateParams, game_list, running_games_list},
|
||||
};
|
||||
|
||||
pub struct TabGames<T> {
|
||||
|
|
@ -19,7 +19,6 @@ pub struct TabGames<T> {
|
|||
|
||||
view_game_list: game_list::View,
|
||||
view_running_games_list: running_games_list::View,
|
||||
steam_utils: SteamUtils,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
|
|
@ -28,10 +27,20 @@ impl<T> Tab<T> for TabGames<T> {
|
|||
TabType::Games
|
||||
}
|
||||
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, time_ms: u32, _data: &mut T) -> anyhow::Result<()> {
|
||||
self
|
||||
.view_game_list
|
||||
.update(&mut frontend.layout, &mut self.steam_utils, &frontend.executor)?;
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, time_ms: u32, data: &mut T) -> anyhow::Result<()> {
|
||||
let mut config_change_kind = None;
|
||||
|
||||
self.view_game_list.update(&mut ViewUpdateParams {
|
||||
layout: &mut frontend.layout,
|
||||
executor: &frontend.executor,
|
||||
general_config: frontend.interface.general_config(data),
|
||||
config_change_kind: &mut config_change_kind,
|
||||
})?;
|
||||
|
||||
if let Some(kind) = config_change_kind {
|
||||
frontend.interface.config_changed(data, kind);
|
||||
}
|
||||
|
||||
self.view_running_games_list.update(&mut frontend.layout, time_ms)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -54,16 +63,17 @@ impl<T> TabGames<T> {
|
|||
let game_list_parent = state.get_widget_id("game_list_parent")?;
|
||||
let id_running_games_list_parent = state.get_widget_id("running_games_list_parent")?;
|
||||
|
||||
let mut steam_utils = SteamUtils::new()?;
|
||||
|
||||
let view_game_list = game_list::View::new(game_list::Params {
|
||||
executor: frontend.executor.clone(),
|
||||
frontend_tasks: frontend.tasks.clone(),
|
||||
globals: globals.clone(),
|
||||
layout: &mut frontend.layout,
|
||||
parent_id: game_list_parent,
|
||||
steam_utils: &steam_utils,
|
||||
})?;
|
||||
|
||||
let mut steam_utils = SteamUtils::new()?;
|
||||
|
||||
let view_running_games_list = running_games_list::View::new(running_games_list::Params {
|
||||
globals: globals.clone(),
|
||||
layout: &mut frontend.layout,
|
||||
|
|
@ -77,7 +87,6 @@ impl<T> TabGames<T> {
|
|||
view_game_list,
|
||||
view_running_games_list,
|
||||
marker: PhantomData,
|
||||
steam_utils,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,9 +59,12 @@ impl<T> TabHome<T> {
|
|||
parent_id,
|
||||
)?;
|
||||
|
||||
let mut c = frontend.layout.start_common();
|
||||
let widget_label = state.fetch_widget(&c.layout.state, "label_hello")?.widget;
|
||||
configure_label_hello(&mut c.common(), widget_label, frontend.interface.general_config(data));
|
||||
let widget_label = state.fetch_widget(&frontend.layout.state, "label_hello")?.widget;
|
||||
configure_label_hello(
|
||||
&mut frontend.layout.common(),
|
||||
widget_label,
|
||||
frontend.interface.general_config(data),
|
||||
);
|
||||
|
||||
let btn_apps = state.fetch_component_as::<ComponentButton>("btn_apps")?;
|
||||
let btn_games = state.fetch_component_as::<ComponentButton>("btn_games")?;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use wgui::{
|
|||
};
|
||||
use wlx_common::{
|
||||
config::GeneralConfig,
|
||||
dash_interface::{self, MonadoDumpSessionFrame},
|
||||
dash_interface::{self, ConfigChangeKind, MonadoDumpSessionFrame},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -175,7 +175,9 @@ impl<T> Tab<T> for TabMonado<T> {
|
|||
Task::GeneralSettingsChromaUpdate => {
|
||||
if let Subtab::GeneralSettings(tab) = &mut self.subtab {
|
||||
tab.chroma_update(frontend.interface.general_config(data));
|
||||
frontend.interface.config_changed(data);
|
||||
frontend
|
||||
.interface
|
||||
.config_changed(data, ConfigChangeKind::EnvironmentBlend);
|
||||
}
|
||||
}
|
||||
Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness),
|
||||
|
|
@ -281,9 +283,7 @@ impl SubtabGeneralSettings {
|
|||
// get brightness
|
||||
let slider_brightness = 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.set_value_primary(&mut frontend.layout.common(), brightness * 100.0);
|
||||
|
||||
slider_brightness.on_value_changed({
|
||||
let tasks = tasks.clone();
|
||||
|
|
@ -304,16 +304,15 @@ impl SubtabGeneralSettings {
|
|||
let slider_keying_value_range = state.fetch_component_as::<ComponentSlider>("slider_keying_value_range")?;
|
||||
|
||||
{
|
||||
let mut lc = frontend.layout.start_common();
|
||||
let mut common = lc.common();
|
||||
let mut common = frontend.layout.common();
|
||||
|
||||
// set initial values
|
||||
let (rgb, range_h, range_s, range_v) = config.chroma_key_params.get_rgb_and_hsv_ranges();
|
||||
slider_keying_curve.set_value(&mut common, config.chroma_key_params.curve * SLIDER_MULTIPLIER);
|
||||
slider_keying_despill.set_value(&mut common, config.chroma_key_params.despill * SLIDER_MULTIPLIER);
|
||||
slider_keying_hue_range.set_value(&mut common, range_h * SLIDER_MULTIPLIER);
|
||||
slider_keying_saturation_range.set_value(&mut common, range_s * SLIDER_MULTIPLIER);
|
||||
slider_keying_value_range.set_value(&mut common, range_v * SLIDER_MULTIPLIER);
|
||||
slider_keying_curve.set_value_primary(&mut common, config.chroma_key_params.curve * SLIDER_MULTIPLIER);
|
||||
slider_keying_despill.set_value_primary(&mut common, config.chroma_key_params.despill * SLIDER_MULTIPLIER);
|
||||
slider_keying_hue_range.set_value_primary(&mut common, range_h * SLIDER_MULTIPLIER);
|
||||
slider_keying_saturation_range.set_value_primary(&mut common, range_s * SLIDER_MULTIPLIER);
|
||||
slider_keying_value_range.set_value_primary(&mut common, range_v * SLIDER_MULTIPLIER);
|
||||
cs_keying.set_color(&mut common, rgb);
|
||||
|
||||
// prepare callbacks
|
||||
|
|
@ -340,8 +339,6 @@ impl SubtabGeneralSettings {
|
|||
slider_keying_saturation_range.on_value_changed(get_slider_callback(&tasks));
|
||||
slider_keying_value_range.on_value_changed(get_slider_callback(&tasks));
|
||||
cs_keying.on_changed(get_color_selector_callback(&tasks));
|
||||
|
||||
lc.finish()?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
|
|
@ -356,11 +353,11 @@ impl SubtabGeneralSettings {
|
|||
}
|
||||
|
||||
fn chroma_update(&mut self, config: &mut GeneralConfig) {
|
||||
let val_curve = self.slider_keying_curve.get_value();
|
||||
let val_despill = self.slider_keying_despill.get_value();
|
||||
let val_range_h = self.slider_keying_hue_range.get_value();
|
||||
let val_range_s = self.slider_keying_saturation_range.get_value();
|
||||
let val_range_v = self.slider_keying_value_range.get_value();
|
||||
let val_curve = self.slider_keying_curve.get_value_primary();
|
||||
let val_despill = self.slider_keying_despill.get_value_primary();
|
||||
let val_range_h = self.slider_keying_hue_range.get_value_primary();
|
||||
let val_range_s = self.slider_keying_saturation_range.get_value_primary();
|
||||
let val_range_v = self.slider_keying_value_range.get_value_primary();
|
||||
let val_rgb = self.cs_keying.get_color();
|
||||
|
||||
config.chroma_key_params.despill = val_despill / SLIDER_MULTIPLIER;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use wgui::{
|
|||
assets::AssetPath,
|
||||
components::tabs::ComponentTabs,
|
||||
drawing,
|
||||
event::{CallbackDataCommon, EventAlterables},
|
||||
event::StyleSetRequest,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
|
|
@ -20,11 +20,16 @@ use wgui::{
|
|||
},
|
||||
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult},
|
||||
};
|
||||
use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode};
|
||||
use wlx_common::{
|
||||
config::GeneralConfig,
|
||||
config_io::ConfigRoot,
|
||||
dash_interface::{ConfigChangeKind, RecenterMode},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
frontend::{Frontend, FrontendTask},
|
||||
frontend::{Frontend, FrontendTask, FrontendTasks},
|
||||
tab::{Tab, TabType, settings::macros::MacroParams},
|
||||
views::ViewUpdateParams,
|
||||
};
|
||||
|
||||
mod macros;
|
||||
|
|
@ -33,6 +38,7 @@ mod tab_controls;
|
|||
mod tab_features;
|
||||
mod tab_look_and_feel;
|
||||
mod tab_misc;
|
||||
mod tab_skybox;
|
||||
mod tab_troubleshooting;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -43,6 +49,7 @@ enum TabNameEnum {
|
|||
Misc,
|
||||
AutostartApps,
|
||||
Troubleshooting,
|
||||
Skybox,
|
||||
}
|
||||
|
||||
impl TabNameEnum {
|
||||
|
|
@ -54,6 +61,7 @@ impl TabNameEnum {
|
|||
"misc" => Some(TabNameEnum::Misc),
|
||||
"autostart_apps" => Some(TabNameEnum::AutostartApps),
|
||||
"troubleshooting" => Some(TabNameEnum::Troubleshooting),
|
||||
"skybox" => Some(TabNameEnum::Skybox),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -75,14 +83,29 @@ enum Task {
|
|||
SetTab(TabNameEnum),
|
||||
}
|
||||
|
||||
struct SettingsMountParams<'a> {
|
||||
mp: &'a mut MacroParams<'a>,
|
||||
frontend_tasks: &'a FrontendTasks,
|
||||
id_parent: WidgetID,
|
||||
}
|
||||
|
||||
trait SettingsTab {
|
||||
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TabSettings<T> {
|
||||
pub state: ParserState,
|
||||
|
||||
app_button_ids: Vec<Rc<str>>,
|
||||
context_menu: ContextMenu,
|
||||
|
||||
current_tab: Option<Box<dyn SettingsTab>>,
|
||||
|
||||
tasks: Tasks<Task>,
|
||||
marker: PhantomData<T>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
}
|
||||
|
||||
impl<T> Tab<T> for TabSettings<T> {
|
||||
|
|
@ -91,7 +114,22 @@ impl<T> Tab<T> for TabSettings<T> {
|
|||
}
|
||||
|
||||
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
|
||||
let mut changed = false;
|
||||
if let Some(tab) = &mut self.current_tab {
|
||||
let mut config_change_kind = None;
|
||||
|
||||
tab.update(&mut ViewUpdateParams {
|
||||
layout: &mut frontend.layout,
|
||||
executor: &frontend.executor,
|
||||
general_config: frontend.interface.general_config(data),
|
||||
config_change_kind: &mut config_change_kind,
|
||||
})?;
|
||||
|
||||
if let Some(kind) = config_change_kind {
|
||||
frontend.interface.config_changed(data, kind);
|
||||
}
|
||||
}
|
||||
|
||||
let mut changed = None;
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::SetTab(tab) => {
|
||||
|
|
@ -104,7 +142,7 @@ impl<T> Tab<T> for TabSettings<T> {
|
|||
}
|
||||
let config = frontend.interface.general_config(data);
|
||||
*setting.mut_bool(config) = n;
|
||||
changed = true;
|
||||
changed = Some(setting.change_kind());
|
||||
}
|
||||
Task::UpdateFloat(setting, n) => {
|
||||
self.tasks.push(Task::SettingUpdated(setting));
|
||||
|
|
@ -113,7 +151,7 @@ impl<T> Tab<T> for TabSettings<T> {
|
|||
}
|
||||
let config = frontend.interface.general_config(data);
|
||||
*setting.mut_f32(config) = n;
|
||||
changed = true;
|
||||
changed = Some(setting.change_kind());
|
||||
}
|
||||
Task::UpdateInt(setting, n) => {
|
||||
self.tasks.push(Task::SettingUpdated(setting));
|
||||
|
|
@ -122,7 +160,7 @@ impl<T> Tab<T> for TabSettings<T> {
|
|||
}
|
||||
let config = frontend.interface.general_config(data);
|
||||
*setting.mut_i32(config) = n;
|
||||
changed = true;
|
||||
changed = Some(setting.change_kind());
|
||||
}
|
||||
Task::ClearPipewireTokens => {
|
||||
let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("pw_tokens.yaml"))
|
||||
|
|
@ -161,7 +199,7 @@ impl<T> Tab<T> for TabSettings<T> {
|
|||
let config = frontend.interface.general_config(data);
|
||||
config.autostart_apps.remove(idx);
|
||||
frontend.layout.remove_widget(widget);
|
||||
changed = true;
|
||||
changed = Some(ConfigChangeKind::OverlayConfig);
|
||||
}
|
||||
}
|
||||
Task::SettingUpdated(setting) => match setting {
|
||||
|
|
@ -179,15 +217,10 @@ impl<T> Tab<T> for TabSettings<T> {
|
|||
let mut s = name.splitn(5, ';');
|
||||
(s.next(), s.next(), s.next(), s.next(), s.next())
|
||||
} {
|
||||
let mut common = frontend.layout.common();
|
||||
let mut label = self
|
||||
.state
|
||||
.fetch_widget_as::<WidgetLabel>(&frontend.layout.state, &format!("{id}_value"))?;
|
||||
|
||||
let mut alterables = EventAlterables::default();
|
||||
let mut common = CallbackDataCommon {
|
||||
alterables: &mut alterables,
|
||||
state: &frontend.layout.state,
|
||||
};
|
||||
.fetch_widget_as::<WidgetLabel>(&common.state, &format!("{id}_value"))?;
|
||||
|
||||
let translation = Translation {
|
||||
text: text.into(),
|
||||
|
|
@ -199,12 +232,12 @@ impl<T> Tab<T> for TabSettings<T> {
|
|||
let setting = SettingType::from_str(setting).expect("Invalid Enum string");
|
||||
let config = frontend.interface.general_config(data);
|
||||
setting.set_enum(config, value);
|
||||
changed = true;
|
||||
changed = Some(ConfigChangeKind::OverlayConfig);
|
||||
}
|
||||
|
||||
// Notify overlays of the change
|
||||
if changed {
|
||||
frontend.interface.config_changed(data);
|
||||
if let Some(changed) = changed {
|
||||
frontend.interface.config_changed(data, changed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -231,7 +264,6 @@ enum SettingType {
|
|||
InvertScrollDirectionY,
|
||||
KeyboardMiddleClick,
|
||||
KeyboardSoundEnabled,
|
||||
KeyboardSwipeToTypeEnabled,
|
||||
Language,
|
||||
LeftHandedMouse,
|
||||
LongPressDuration,
|
||||
|
|
@ -258,6 +290,13 @@ enum SettingType {
|
|||
}
|
||||
|
||||
impl SettingType {
|
||||
pub fn change_kind(self) -> ConfigChangeKind {
|
||||
match self {
|
||||
Self::UseSkybox | Self::UsePassthrough => ConfigChangeKind::EnvironmentBlend,
|
||||
_ => ConfigChangeKind::OverlayConfig,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mut_bool(self, config: &mut GeneralConfig) -> &mut bool {
|
||||
match self {
|
||||
Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x,
|
||||
|
|
@ -284,7 +323,6 @@ impl SettingType {
|
|||
Self::HideUsername => &mut config.hide_username,
|
||||
Self::OpaqueBackground => &mut config.opaque_background,
|
||||
Self::XwaylandByDefault => &mut config.xwayland_by_default,
|
||||
Self::KeyboardSwipeToTypeEnabled => &mut config.keyboard_swipe_to_type_enabled,
|
||||
_ => panic!("Requested bool for non-bool SettingType"),
|
||||
}
|
||||
}
|
||||
|
|
@ -383,7 +421,6 @@ impl SettingType {
|
|||
Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"),
|
||||
Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"),
|
||||
Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"),
|
||||
Self::KeyboardSwipeToTypeEnabled => Ok("APP_SETTINGS.KEYBOARD_SWIPE_TO_TYPE_ENABLED"),
|
||||
Self::Language => Ok("APP_SETTINGS.LANGUAGE"),
|
||||
Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"),
|
||||
Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"),
|
||||
|
|
@ -420,7 +457,6 @@ impl SettingType {
|
|||
Self::GridOpacity => Some("APP_SETTINGS.GRID_OPACITY_HELP"),
|
||||
Self::HandsfreePointer => Some("APP_SETTINGS.HANDSFREE_POINTER_HELP"),
|
||||
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
|
||||
Self::KeyboardSwipeToTypeEnabled => Some("APP_SETTINGS.KEYBOARD_SWIPE_TO_TYPE_ENABLED_HELP"),
|
||||
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
|
||||
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
|
||||
Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"),
|
||||
|
|
@ -440,7 +476,6 @@ impl SettingType {
|
|||
| Self::UiGradientIntensity
|
||||
| Self::UprightScreenFix
|
||||
| Self::DoubleCursorFix
|
||||
| Self::ScreenRenderDown
|
||||
| Self::Language
|
||||
| Self::CaptureMethod
|
||||
)
|
||||
|
|
@ -504,6 +539,7 @@ impl<T> TabSettings<T> {
|
|||
let root = self.state.get_widget_id("settings_root")?;
|
||||
frontend.layout.remove_children(root);
|
||||
let globals = frontend.layout.state.globals.clone();
|
||||
self.current_tab = None;
|
||||
|
||||
let mut mp = MacroParams {
|
||||
layout: &mut frontend.layout,
|
||||
|
|
@ -514,31 +550,43 @@ impl<T> TabSettings<T> {
|
|||
idx: 9001,
|
||||
};
|
||||
|
||||
let settings_mount_params = SettingsMountParams {
|
||||
mp: &mut mp,
|
||||
id_parent: root,
|
||||
frontend_tasks: &self.frontend_tasks,
|
||||
};
|
||||
|
||||
match name {
|
||||
TabNameEnum::LookAndFeel => {
|
||||
tab_look_and_feel::mount(&mut mp, root)?;
|
||||
self.current_tab = Some(Box::new(tab_look_and_feel::State::mount(settings_mount_params)?));
|
||||
}
|
||||
TabNameEnum::Features => {
|
||||
tab_features::mount(&mut mp, root)?;
|
||||
self.current_tab = Some(Box::new(tab_features::State::mount(settings_mount_params)?));
|
||||
}
|
||||
TabNameEnum::Controls => {
|
||||
tab_controls::mount(&mut mp, root)?;
|
||||
self.current_tab = Some(Box::new(tab_controls::State::mount(settings_mount_params)?));
|
||||
}
|
||||
TabNameEnum::Misc => {
|
||||
tab_misc::mount(&mut mp, root)?;
|
||||
self.current_tab = Some(Box::new(tab_misc::State::mount(settings_mount_params)?));
|
||||
}
|
||||
TabNameEnum::AutostartApps => {
|
||||
tab_autostart_apps::mount(&mut mp, root, &mut self.app_button_ids)?;
|
||||
self.current_tab = Some(Box::new(tab_autostart_apps::State::mount(
|
||||
settings_mount_params,
|
||||
&mut self.app_button_ids,
|
||||
)?));
|
||||
}
|
||||
TabNameEnum::Troubleshooting => {
|
||||
tab_troubleshooting::mount(&mut mp, root)?;
|
||||
self.current_tab = Some(Box::new(tab_troubleshooting::State::mount(settings_mount_params)?));
|
||||
}
|
||||
TabNameEnum::Skybox => {
|
||||
self.current_tab = Some(Box::new(tab_skybox::State::mount(settings_mount_params)?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, _data: &mut T) -> anyhow::Result<Self> {
|
||||
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"),
|
||||
|
|
@ -548,6 +596,16 @@ impl<T> TabSettings<T> {
|
|||
let parser_state = wgui::parser::parse_from_assets(&doc_params, &mut frontend.layout, parent_id)?;
|
||||
let tasks = Tasks::default();
|
||||
let tabs = parser_state.fetch_component_as::<ComponentTabs>("tabs")?;
|
||||
|
||||
if !frontend.interface.get_feats(data).openxr {
|
||||
let skybox_btn = tabs.get_tab_button("skybox").unwrap();
|
||||
frontend
|
||||
.layout
|
||||
.common()
|
||||
.alterables
|
||||
.set_style(skybox_btn.get_rect(), StyleSetRequest::Display(taffy::Display::None));
|
||||
}
|
||||
|
||||
tabs.on_select({
|
||||
let tasks = tasks.clone();
|
||||
Rc::new(move |_common, evt| {
|
||||
|
|
@ -566,6 +624,8 @@ impl<T> TabSettings<T> {
|
|||
state: parser_state,
|
||||
marker: PhantomData,
|
||||
context_menu: ContextMenu::default(),
|
||||
current_tab: None,
|
||||
frontend_tasks: frontend.tasks.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,33 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use crate::tab::settings::macros::{MacroParams, options_autostart_app, options_category};
|
||||
use wgui::layout::WidgetID;
|
||||
use crate::tab::settings::{
|
||||
SettingsMountParams, SettingsTab,
|
||||
macros::{options_autostart_app, options_category},
|
||||
};
|
||||
|
||||
pub fn mount(mp: &mut MacroParams, parent: WidgetID, app_button_ids: &mut Vec<Rc<str>>) -> anyhow::Result<()> {
|
||||
*app_button_ids = Vec::new();
|
||||
pub struct State {}
|
||||
|
||||
if !mp.config.autostart_apps.is_empty() {
|
||||
let c = options_category(mp, parent, "APP_SETTINGS.AUTOSTART_APPS", "dashboard/apps.svg")?;
|
||||
impl SettingsTab for State {}
|
||||
|
||||
// todo: prevent clone
|
||||
let autostart_apps = mp.config.autostart_apps.clone();
|
||||
for app in autostart_apps {
|
||||
options_autostart_app(mp, c, &app.name, app_button_ids)?;
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams, app_button_ids: &mut Vec<Rc<str>>) -> anyhow::Result<State> {
|
||||
*app_button_ids = Vec::new();
|
||||
|
||||
if !par.mp.config.autostart_apps.is_empty() {
|
||||
let c = options_category(
|
||||
par.mp,
|
||||
par.id_parent,
|
||||
"APP_SETTINGS.AUTOSTART_APPS",
|
||||
"dashboard/apps.svg",
|
||||
)?;
|
||||
|
||||
// todo: prevent clone
|
||||
let autostart_apps = par.mp.config.autostart_apps.clone();
|
||||
for app in autostart_apps {
|
||||
options_autostart_app(par.mp, c, &app.name, app_button_ids)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(State {})
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,33 @@
|
|||
use crate::tab::settings::{
|
||||
SettingType,
|
||||
macros::{MacroParams, options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32},
|
||||
SettingType, SettingsMountParams, SettingsTab,
|
||||
macros::{options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32},
|
||||
};
|
||||
use wgui::layout::WidgetID;
|
||||
|
||||
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
let c = options_category(mp, parent, "APP_SETTINGS.CONTROLS", "dashboard/controller.svg")?;
|
||||
options_dropdown::<wlx_common::config::AltModifier>(mp, c, &SettingType::KeyboardMiddleClick)?;
|
||||
options_dropdown::<wlx_common::config::HandsfreePointer>(mp, c, &SettingType::HandsfreePointer)?;
|
||||
options_checkbox(mp, c, SettingType::FocusFollowsMouseMode)?;
|
||||
options_checkbox(mp, c, SettingType::LeftHandedMouse)?;
|
||||
options_checkbox(mp, c, SettingType::AllowSliding)?;
|
||||
options_checkbox(mp, c, SettingType::InvertScrollDirectionX)?;
|
||||
options_checkbox(mp, c, SettingType::InvertScrollDirectionY)?;
|
||||
options_slider_f32(mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?;
|
||||
options_slider_f32(mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?;
|
||||
options_slider_f32(mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?;
|
||||
options_slider_f32(mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1)?;
|
||||
options_slider_f32(mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1)?;
|
||||
options_slider_i32(mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?;
|
||||
Ok(())
|
||||
pub struct State {}
|
||||
|
||||
impl SettingsTab for State {}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
|
||||
let c = options_category(
|
||||
par.mp,
|
||||
par.id_parent,
|
||||
"APP_SETTINGS.CONTROLS",
|
||||
"dashboard/controller.svg",
|
||||
)?;
|
||||
options_dropdown::<wlx_common::config::AltModifier>(par.mp, c, &SettingType::KeyboardMiddleClick)?;
|
||||
options_dropdown::<wlx_common::config::HandsfreePointer>(par.mp, c, &SettingType::HandsfreePointer)?;
|
||||
options_checkbox(par.mp, c, SettingType::FocusFollowsMouseMode)?;
|
||||
options_checkbox(par.mp, c, SettingType::LeftHandedMouse)?;
|
||||
options_checkbox(par.mp, c, SettingType::AllowSliding)?;
|
||||
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionX)?;
|
||||
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionY)?;
|
||||
options_slider_f32(par.mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?;
|
||||
options_slider_f32(par.mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?;
|
||||
options_slider_f32(par.mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?;
|
||||
options_slider_f32(par.mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1)?;
|
||||
options_slider_f32(par.mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1)?;
|
||||
options_slider_i32(par.mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?;
|
||||
Ok(State {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
use crate::tab::settings::{
|
||||
SettingType,
|
||||
macros::{MacroParams, options_category, options_checkbox, options_slider_f32},
|
||||
SettingType, SettingsMountParams, SettingsTab,
|
||||
macros::{options_category, options_checkbox, options_slider_f32},
|
||||
};
|
||||
use wgui::layout::WidgetID;
|
||||
|
||||
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
let c = options_category(mp, parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
|
||||
options_checkbox(mp, c, SettingType::NotificationsEnabled)?;
|
||||
options_checkbox(mp, c, SettingType::NotificationsSoundEnabled)?;
|
||||
options_checkbox(mp, c, SettingType::KeyboardSoundEnabled)?;
|
||||
options_checkbox(mp, c, SettingType::KeyboardSwipeToTypeEnabled)?;
|
||||
options_checkbox(mp, c, SettingType::SpaceDragUnlocked)?;
|
||||
options_checkbox(mp, c, SettingType::SpaceRotateUnlocked)?;
|
||||
options_slider_f32(mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5)?;
|
||||
options_checkbox(mp, c, SettingType::BlockGameInput)?;
|
||||
options_checkbox(mp, c, SettingType::BlockGameInputIgnoreWatch)?;
|
||||
options_checkbox(mp, c, SettingType::BlockPosesOnKbdInteraction)?;
|
||||
Ok(())
|
||||
pub struct State {}
|
||||
|
||||
impl SettingsTab for State {}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
|
||||
let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
|
||||
options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
|
||||
options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?;
|
||||
options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?;
|
||||
options_checkbox(par.mp, c, SettingType::SpaceDragUnlocked)?;
|
||||
options_checkbox(par.mp, c, SettingType::SpaceRotateUnlocked)?;
|
||||
options_slider_f32(par.mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5)?;
|
||||
options_checkbox(par.mp, c, SettingType::BlockGameInput)?;
|
||||
options_checkbox(par.mp, c, SettingType::BlockGameInputIgnoreWatch)?;
|
||||
options_checkbox(par.mp, c, SettingType::BlockPosesOnKbdInteraction)?;
|
||||
Ok(State {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,29 @@
|
|||
use crate::tab::settings::{
|
||||
SettingType,
|
||||
macros::{MacroParams, options_category, options_checkbox, options_dropdown, options_slider_f32},
|
||||
SettingType, SettingsMountParams, SettingsTab,
|
||||
macros::{options_category, options_checkbox, options_dropdown, options_slider_f32},
|
||||
};
|
||||
use wgui::layout::WidgetID;
|
||||
|
||||
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
let c = options_category(mp, parent, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?;
|
||||
options_dropdown::<wlx_common::locale::Language>(mp, c, &SettingType::Language)?;
|
||||
options_checkbox(mp, c, SettingType::OpaqueBackground)?;
|
||||
options_checkbox(mp, c, SettingType::HideUsername)?;
|
||||
options_checkbox(mp, c, SettingType::HideGrabHelp)?;
|
||||
options_slider_f32(mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1)?; // min, max, step
|
||||
options_slider_f32(mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05)?; // min, max, step
|
||||
options_slider_f32(mp, c, SettingType::UiRoundMultiplier, 0.5, 5.0, 0.1)?;
|
||||
options_checkbox(mp, c, SettingType::SetsOnWatch)?;
|
||||
options_checkbox(mp, c, SettingType::UseSkybox)?;
|
||||
options_slider_f32(mp, c, SettingType::GridOpacity, 0.0, 1.0, 0.05)?; // min, max, step
|
||||
options_checkbox(mp, c, SettingType::UsePassthrough)?;
|
||||
options_checkbox(mp, c, SettingType::Clock12h)?;
|
||||
Ok(())
|
||||
pub struct State {}
|
||||
|
||||
impl SettingsTab for State {}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
|
||||
let c = options_category(
|
||||
par.mp,
|
||||
par.id_parent,
|
||||
"APP_SETTINGS.LOOK_AND_FEEL",
|
||||
"dashboard/palette.svg",
|
||||
)?;
|
||||
options_dropdown::<wlx_common::locale::Language>(par.mp, c, &SettingType::Language)?;
|
||||
options_checkbox(par.mp, c, SettingType::OpaqueBackground)?;
|
||||
options_checkbox(par.mp, c, SettingType::HideUsername)?;
|
||||
options_checkbox(par.mp, c, SettingType::HideGrabHelp)?;
|
||||
options_slider_f32(par.mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1)?; // min, max, step
|
||||
options_slider_f32(par.mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05)?; // min, max, step
|
||||
options_slider_f32(par.mp, c, SettingType::UiRoundMultiplier, 0.5, 5.0, 0.1)?;
|
||||
options_checkbox(par.mp, c, SettingType::SetsOnWatch)?;
|
||||
options_checkbox(par.mp, c, SettingType::Clock12h)?;
|
||||
Ok(State {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
use crate::tab::settings::{
|
||||
SettingType,
|
||||
macros::{MacroParams, options_category, options_checkbox, options_dropdown},
|
||||
SettingType, SettingsMountParams, SettingsTab,
|
||||
macros::{options_category, options_checkbox, options_dropdown},
|
||||
};
|
||||
use wgui::layout::WidgetID;
|
||||
|
||||
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
let c = options_category(mp, parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
|
||||
options_dropdown::<wlx_common::config::CaptureMethod>(mp, c, &SettingType::CaptureMethod)?;
|
||||
options_checkbox(mp, c, SettingType::XwaylandByDefault)?;
|
||||
options_checkbox(mp, c, SettingType::UprightScreenFix)?;
|
||||
options_checkbox(mp, c, SettingType::DoubleCursorFix)?;
|
||||
options_checkbox(mp, c, SettingType::ScreenRenderDown)?;
|
||||
Ok(())
|
||||
pub struct State {}
|
||||
|
||||
impl SettingsTab for State {}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
|
||||
let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
|
||||
options_dropdown::<wlx_common::config::CaptureMethod>(par.mp, c, &SettingType::CaptureMethod)?;
|
||||
options_checkbox(par.mp, c, SettingType::XwaylandByDefault)?;
|
||||
options_checkbox(par.mp, c, SettingType::UprightScreenFix)?;
|
||||
options_checkbox(par.mp, c, SettingType::DoubleCursorFix)?;
|
||||
options_checkbox(par.mp, c, SettingType::ScreenRenderDown)?;
|
||||
Ok(State {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
use wgui::{assets::AssetPath, i18n::Translation, layout::Layout, task::Tasks};
|
||||
|
||||
use crate::{
|
||||
frontend::FrontendTasks,
|
||||
tab::settings::{
|
||||
SettingType, SettingsMountParams, SettingsTab,
|
||||
macros::{options_category, options_checkbox, options_slider_f32},
|
||||
},
|
||||
util::{popup_manager::PopupHolder, wgui_simple},
|
||||
views::{self, ViewUpdateParams, skymap_list},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
ShowSkymapList,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
popup_skymap_list: PopupHolder<skymap_list::View>,
|
||||
tasks: Tasks<Task>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
}
|
||||
|
||||
impl SettingsTab for State {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
self.popup_skymap_list.update(par)?;
|
||||
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::ShowSkymapList => self.show_skymap_list(par.layout),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
|
||||
let id_category = options_category(par.mp, par.id_parent, "APP_SETTINGS.SKYBOX", "dashboard/globe.svg")?;
|
||||
options_checkbox(par.mp, id_category, SettingType::UseSkybox)?;
|
||||
options_checkbox(par.mp, id_category, SettingType::UsePassthrough)?;
|
||||
options_slider_f32(par.mp, id_category, SettingType::GridOpacity, 0.0, 1.0, 0.05)?;
|
||||
|
||||
let tasks = Tasks::<Task>::new();
|
||||
|
||||
// "Browse skymaps" button
|
||||
wgui_simple::create_button(wgui_simple::CreateButtonParams {
|
||||
id_parent: id_category,
|
||||
layout: par.mp.layout,
|
||||
content: Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"),
|
||||
icon_builtin: AssetPath::BuiltIn("dashboard/globe.svg"),
|
||||
on_click: tasks.get_button_click_callback(Task::ShowSkymapList),
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
popup_skymap_list: Default::default(),
|
||||
frontend_tasks: par.frontend_tasks.clone(),
|
||||
tasks,
|
||||
})
|
||||
}
|
||||
|
||||
fn show_skymap_list(&mut self, layout: &mut Layout) {
|
||||
views::skymap_list::mount_popup(
|
||||
self.frontend_tasks.clone(),
|
||||
layout.state.globals.clone(),
|
||||
self.popup_skymap_list.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +1,55 @@
|
|||
use crate::tab::settings::{
|
||||
Task,
|
||||
macros::{MacroParams, options_category, options_danger_button},
|
||||
SettingsMountParams, SettingsTab, Task,
|
||||
macros::{options_category, options_danger_button},
|
||||
};
|
||||
use wgui::layout::WidgetID;
|
||||
|
||||
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> {
|
||||
let c = options_category(mp, parent, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/cpu.svg")?;
|
||||
options_danger_button(
|
||||
mp,
|
||||
c,
|
||||
"APP_SETTINGS.RESET_PLAYSPACE",
|
||||
"dashboard/recenter.svg",
|
||||
Task::ResetPlayspace,
|
||||
)?;
|
||||
options_danger_button(
|
||||
mp,
|
||||
c,
|
||||
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
|
||||
"dashboard/display.svg",
|
||||
Task::ClearPipewireTokens,
|
||||
)?;
|
||||
options_danger_button(
|
||||
mp,
|
||||
c,
|
||||
"APP_SETTINGS.CLEAR_SAVED_STATE",
|
||||
"dashboard/binary.svg",
|
||||
Task::ClearSavedState,
|
||||
)?;
|
||||
options_danger_button(
|
||||
mp,
|
||||
c,
|
||||
"APP_SETTINGS.DELETE_ALL_CONFIGS",
|
||||
"dashboard/circle.svg",
|
||||
Task::DeleteAllConfigs,
|
||||
)?;
|
||||
options_danger_button(
|
||||
mp,
|
||||
c,
|
||||
"APP_SETTINGS.RESTART_SOFTWARE",
|
||||
"dashboard/refresh.svg",
|
||||
Task::RestartSoftware,
|
||||
)?;
|
||||
Ok(())
|
||||
pub struct State {}
|
||||
|
||||
impl SettingsTab for State {}
|
||||
|
||||
impl State {
|
||||
pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
|
||||
let c = options_category(
|
||||
par.mp,
|
||||
par.id_parent,
|
||||
"APP_SETTINGS.TROUBLESHOOTING",
|
||||
"dashboard/cpu.svg",
|
||||
)?;
|
||||
options_danger_button(
|
||||
par.mp,
|
||||
c,
|
||||
"APP_SETTINGS.RESET_PLAYSPACE",
|
||||
"dashboard/recenter.svg",
|
||||
Task::ResetPlayspace,
|
||||
)?;
|
||||
options_danger_button(
|
||||
par.mp,
|
||||
c,
|
||||
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
|
||||
"dashboard/display.svg",
|
||||
Task::ClearPipewireTokens,
|
||||
)?;
|
||||
options_danger_button(
|
||||
par.mp,
|
||||
c,
|
||||
"APP_SETTINGS.CLEAR_SAVED_STATE",
|
||||
"dashboard/binary.svg",
|
||||
Task::ClearSavedState,
|
||||
)?;
|
||||
options_danger_button(
|
||||
par.mp,
|
||||
c,
|
||||
"APP_SETTINGS.DELETE_ALL_CONFIGS",
|
||||
"dashboard/circle.svg",
|
||||
Task::DeleteAllConfigs,
|
||||
)?;
|
||||
options_danger_button(
|
||||
par.mp,
|
||||
c,
|
||||
"APP_SETTINGS.RESTART_SOFTWARE",
|
||||
"dashboard/refresh.svg",
|
||||
Task::RestartSoftware,
|
||||
)?;
|
||||
Ok(State {})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
use crate::util::{networking::http_client, steam_utils::AppID};
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
use wlx_common::{async_executor::AsyncExecutor, cache_dir};
|
||||
|
||||
use crate::util::{http_client, steam_utils::AppID};
|
||||
|
||||
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>,
|
||||
|
|
@ -24,7 +23,7 @@ pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Re
|
|||
app_id
|
||||
);
|
||||
|
||||
match http_client::get(&executor, &url).await {
|
||||
match http_client::get_simple(&executor, &url).await {
|
||||
Ok(response) => {
|
||||
log::info!("Success");
|
||||
cache_dir::set_data(&cache_file_path, &response.data).await?;
|
||||
|
|
@ -69,7 +68,7 @@ async fn get_app_details_json_internal(
|
|||
// Fetch from Steam API
|
||||
log::info!("Fetching app detail ID {}", app_id);
|
||||
let url = format!("https://store.steampowered.com/api/appdetails?appids={}", app_id);
|
||||
let response = http_client::get(&executor, &url).await?;
|
||||
let response = http_client::get_simple(&executor, &url).await?;
|
||||
let res_utf8 = String::from_utf8(response.data)?;
|
||||
let root = serde_json::from_str::<serde_json::Value>(&res_utf8)?;
|
||||
let body = root.get(&app_id).context("invalid body")?;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
pub mod cached_fetcher;
|
||||
pub mod http_client;
|
||||
pub mod networking;
|
||||
pub mod pactl_wrapper;
|
||||
pub mod popup_manager;
|
||||
pub mod steam_utils;
|
||||
|
|
|
|||
|
|
@ -18,10 +18,33 @@ pub struct HttpClientResponse {
|
|||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> {
|
||||
log::info!("fetching URL \"{}\"", url);
|
||||
impl HttpClientResponse {
|
||||
pub fn as_json<T>(self) -> anyhow::Result<T>
|
||||
where
|
||||
T: for<'a> serde::Deserialize<'a>,
|
||||
{
|
||||
let utf8 = str::from_utf8(&self.data)?;
|
||||
Ok(serde_json::from_str::<T>(utf8)?)
|
||||
}
|
||||
}
|
||||
|
||||
let url: hyper::Uri = url.try_into()?;
|
||||
pub struct ProgressFuncData {
|
||||
pub bytes_downloaded: u64,
|
||||
pub file_size: u64,
|
||||
}
|
||||
|
||||
pub type ProgressFunc = Box<dyn Fn(ProgressFuncData)>;
|
||||
|
||||
pub struct GetParams<'a> {
|
||||
pub executor: &'a AsyncExecutor,
|
||||
pub url: &'a str,
|
||||
pub on_progress: Option<ProgressFunc>,
|
||||
}
|
||||
|
||||
pub async fn get(params: GetParams<'_>) -> anyhow::Result<HttpClientResponse> {
|
||||
log::info!("fetching URL \"{}\"", params.url);
|
||||
|
||||
let url: hyper::Uri = params.url.try_into()?;
|
||||
let req = Request::builder()
|
||||
.header(
|
||||
hyper::header::HOST,
|
||||
|
|
@ -30,23 +53,56 @@ pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClie
|
|||
.uri(url)
|
||||
.body(Empty::new())?;
|
||||
|
||||
let resp = fetch(executor, req).await?;
|
||||
let resp = fetch(params.executor, req).await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
// non-200 HTTP response
|
||||
anyhow::bail!("non-200 HTTP response: {}", resp.status().as_str());
|
||||
}
|
||||
|
||||
let body = BodyStream::new(resp.into_body())
|
||||
let mut bytes_downloaded: u64 = 0;
|
||||
let mut file_size: u64 = 1;
|
||||
|
||||
let (parts, body) = resp.into_parts();
|
||||
|
||||
// that's a pretty interesting way to get file size :]
|
||||
if let Some(val) = parts.headers.get("Content-Length") {
|
||||
if let Ok(str) = val.to_str() {
|
||||
if let Ok(s) = str.parse() {
|
||||
file_size = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut on_progress = params.on_progress;
|
||||
|
||||
let data = BodyStream::new(body)
|
||||
.try_fold(Vec::new(), |mut body, chunk| {
|
||||
if let Some(chunk) = chunk.data_ref() {
|
||||
bytes_downloaded += chunk.len() as u64;
|
||||
body.extend_from_slice(chunk);
|
||||
|
||||
if let Some(on_progress) = &mut on_progress {
|
||||
on_progress(ProgressFuncData {
|
||||
bytes_downloaded,
|
||||
file_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(body)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(HttpClientResponse { data: body })
|
||||
Ok(HttpClientResponse { data })
|
||||
}
|
||||
|
||||
pub async fn get_simple(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> {
|
||||
get(GetParams {
|
||||
executor,
|
||||
url,
|
||||
on_progress: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch(
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use wgui::{globals::WguiGlobals, renderer_vk::text::custom_glyph::CustomGlyphData};
|
||||
use wlx_common::async_executor::AsyncExecutor;
|
||||
|
||||
use crate::util::networking::http_client;
|
||||
|
||||
pub async fn fetch_to_glyph_data(
|
||||
globals: &WguiGlobals,
|
||||
executor: &AsyncExecutor,
|
||||
url: &str,
|
||||
) -> anyhow::Result<(CustomGlyphData, Rc<Vec<u8>>)> {
|
||||
let res = http_client::get_simple(executor, url).await?;
|
||||
let glyph_data = CustomGlyphData::from_bytes_raster(globals, url, &res.data)?;
|
||||
Ok((glyph_data, Rc::new(res.data)))
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
pub mod http_client;
|
||||
pub mod image_fetch;
|
||||
pub mod skymap_catalog;
|
||||
|
||||
// pub const WAYVR_ROOT_URL: &'static str = "https://wayvr.org";
|
||||
pub const WAYVR_SKYMAPS_ROOT: &'static str = "https://wayvr.org/skymaps";
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
use crate::util::networking::{self, WAYVR_SKYMAPS_ROOT, http_client};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use wlx_common::{async_executor::AsyncExecutor, config_io};
|
||||
pub type SkymapUuid = uuid::Uuid;
|
||||
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
|
||||
pub enum SkymapResolution {
|
||||
Res2k,
|
||||
Res4k,
|
||||
Res8k,
|
||||
}
|
||||
|
||||
impl SkymapResolution {
|
||||
pub const fn get_display_str(&self) -> &'static str {
|
||||
match self {
|
||||
SkymapResolution::Res2k => "2K (2 MiB VRAM)",
|
||||
SkymapResolution::Res4k => "4K (8 MiB VRAM)",
|
||||
SkymapResolution::Res8k => "8K (33 MiB VRAM)",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn get_display_str_simple(&self) -> &'static str {
|
||||
match self {
|
||||
SkymapResolution::Res2k => "2K",
|
||||
SkymapResolution::Res4k => "4K",
|
||||
SkymapResolution::Res8k => "8K",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_display_str_simple(text: &str) -> Option<SkymapResolution> {
|
||||
match text {
|
||||
"2K" => Some(SkymapResolution::Res2k),
|
||||
"4K" => Some(SkymapResolution::Res4k),
|
||||
"8K" => Some(SkymapResolution::Res8k),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SkymapCatalogEntryFiles {
|
||||
pub size_8k: Option<String>, // "my_skymap_8k.dds"
|
||||
pub size_4k: Option<String>, // "my_skymap_4k.dds"
|
||||
pub size_2k: String, // we should have *at least* this
|
||||
pub preview: String,
|
||||
}
|
||||
|
||||
impl SkymapCatalogEntryFiles {
|
||||
pub fn get_url_preview(&self) -> String {
|
||||
format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, self.preview)
|
||||
}
|
||||
|
||||
pub fn get_filename_from_res(&self, res: SkymapResolution) -> Option<String> {
|
||||
match res {
|
||||
SkymapResolution::Res2k => Some(&self.size_2k),
|
||||
SkymapResolution::Res4k => self.size_4k.as_ref(),
|
||||
SkymapResolution::Res8k => self.size_8k.as_ref(),
|
||||
}
|
||||
.map(|raw_filename| {
|
||||
// sanitize filename, do not allow "../" just in case
|
||||
PathBuf::from(raw_filename)
|
||||
.file_name()
|
||||
.map(|s| String::from(s.to_string_lossy()))
|
||||
})?
|
||||
}
|
||||
|
||||
// example result: "https://wayvr.org/skymaps/files/my_skymap_8k.dds"
|
||||
pub fn get_url_from_res(&self, res: SkymapResolution) -> Option<String> {
|
||||
let Some(filename) = self.get_filename_from_res(res) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(format!("{}/files/{}", WAYVR_SKYMAPS_ROOT, filename))
|
||||
}
|
||||
|
||||
pub fn get_preview_path(&self) -> PathBuf {
|
||||
config_io::get_skymaps_root().join(&self.preview)
|
||||
}
|
||||
|
||||
pub fn save_preview_to_file(&self, data: &[u8]) -> anyhow::Result<()> {
|
||||
std::fs::write(self.get_preview_path(), data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_preview_file(&self) {
|
||||
let _dont_care = std::fs::remove_file(self.get_preview_path());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SkymapCatalogEntry {
|
||||
pub uuid: SkymapUuid,
|
||||
pub created_at: String,
|
||||
pub modified_at: String,
|
||||
pub version: u32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub author: String,
|
||||
pub files: SkymapCatalogEntryFiles,
|
||||
}
|
||||
|
||||
impl SkymapCatalogEntry {
|
||||
pub fn get_destination_path(&self, resolution: SkymapResolution) -> Option<PathBuf> {
|
||||
let Some(filename) = self.files.get_filename_from_res(resolution) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(config_io::get_skymaps_root().join(filename))
|
||||
}
|
||||
|
||||
pub fn get_destination_metadata_path(&self) -> PathBuf {
|
||||
config_io::get_skymaps_root().join(format!("{}.json", self.uuid))
|
||||
}
|
||||
|
||||
pub fn is_downloaded(&self, resolution: SkymapResolution) -> anyhow::Result<bool> {
|
||||
let Some(full_path) = self.get_destination_path(resolution) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
Ok(std::fs::exists(full_path)?)
|
||||
}
|
||||
|
||||
pub fn has_any_downloaded(&self) -> bool {
|
||||
self.is_downloaded(SkymapResolution::Res2k).unwrap_or(false)
|
||||
|| self.is_downloaded(SkymapResolution::Res4k).unwrap_or(false)
|
||||
|| self.is_downloaded(SkymapResolution::Res8k).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn remove_file(&self, resolution: SkymapResolution) {
|
||||
let Some(full_path) = self.get_destination_path(resolution) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _dont_care = std::fs::remove_file(full_path);
|
||||
}
|
||||
|
||||
pub fn save_metadata(&self) -> anyhow::Result<()> {
|
||||
let json = serde_json::to_string_pretty(self)?;
|
||||
std::fs::write(self.get_destination_metadata_path(), json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_metadata(&self) {
|
||||
let _dont_care = std::fs::remove_file(self.get_destination_metadata_path());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct SkymapCatalog {
|
||||
pub version: u32,
|
||||
pub r#type: String,
|
||||
pub entries: Vec<SkymapCatalogEntry>,
|
||||
}
|
||||
|
||||
impl SkymapCatalog {
|
||||
fn validate(&self) -> anyhow::Result<()> {
|
||||
if self.version != 1 {
|
||||
anyhow::bail!("Unsupported version");
|
||||
}
|
||||
|
||||
if self.r#type != "wayvr_skymaps" {
|
||||
anyhow::bail!("Unsupported type");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_catalog(executor: &AsyncExecutor) -> anyhow::Result<SkymapCatalog> {
|
||||
log::info!("Fetching skymap list");
|
||||
|
||||
let res = http_client::get_simple(executor, &format!("{}/catalog.json", networking::WAYVR_SKYMAPS_ROOT)).await?;
|
||||
let catalog = res.as_json::<SkymapCatalog>()?;
|
||||
catalog.validate()?;
|
||||
|
||||
Ok(catalog)
|
||||
}
|
||||
|
||||
pub fn get_entries_from_disk() -> anyhow::Result<Vec<SkymapCatalogEntry>> {
|
||||
let mut entries = Vec::<SkymapCatalogEntry>::new();
|
||||
|
||||
let skymaps_root = config_io::get_skymaps_root();
|
||||
|
||||
for uuid in config_io::get_skymaps_uuids().unwrap_or_default() {
|
||||
let metadata_path = skymaps_root.join(format!("{}.json", uuid));
|
||||
let Ok(data) = std::fs::read_to_string(metadata_path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let entry = serde_json::from_str::<SkymapCatalogEntry>(&data)?;
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
|
@ -16,7 +16,10 @@ use wgui::{
|
|||
};
|
||||
use wlx_common::config::GeneralConfig;
|
||||
|
||||
use crate::frontend::{FrontendTask, FrontendTasks};
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
views::{ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
|
||||
pub struct PopupManagerParams {
|
||||
pub parent_id: WidgetID,
|
||||
|
|
@ -34,15 +37,140 @@ pub struct MountedPopup {
|
|||
frontend_tasks: FrontendTasks,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MountedPopupState {
|
||||
mounted_popup: Option<MountedPopup>,
|
||||
closed_callback: Option<PopupClosedCallback>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct PopupHandle {
|
||||
state: Rc<RefCell<MountedPopupState>>,
|
||||
}
|
||||
|
||||
struct PopupHolderState<ViewType: ViewTrait> {
|
||||
popup_handle: PopupHandle,
|
||||
view: Option<ViewType>,
|
||||
on_view_close: Option<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
// we can't use #[derive(Default)] due to the fact that ViewType can't be Default.
|
||||
impl<ViewType: ViewTrait> Default for PopupHolderState<ViewType> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
popup_handle: Default::default(),
|
||||
view: None,
|
||||
on_view_close: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PopupHolder<ViewType: ViewTrait> {
|
||||
state: Rc<RefCell<PopupHolderState<ViewType>>>,
|
||||
}
|
||||
|
||||
impl<ViewType: ViewTrait> Default for PopupHolder<ViewType> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Rc::new(RefCell::new(PopupHolderState::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ViewType: ViewTrait> PopupHolderState<ViewType> {
|
||||
fn close(&mut self) {
|
||||
if self.view.is_some() {
|
||||
self.view = None;
|
||||
if let Some(on_close) = self.on_view_close.take() {
|
||||
on_close();
|
||||
}
|
||||
}
|
||||
self.popup_handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl<ViewType: ViewTrait> Drop for PopupHolderState<ViewType> {
|
||||
fn drop(&mut self) {
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
|
||||
// we can't derive(Clone) due to the fact that ViewType is non-cloneable
|
||||
impl<ViewType: ViewTrait> Clone for PopupHolder<ViewType> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
state: self.state.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ViewType: ViewTrait> PopupHolder<ViewType> {
|
||||
pub fn update(&self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
let Some(view) = &mut state.view else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
view.update(par)
|
||||
}
|
||||
|
||||
pub fn set_view(&self, handle: PopupHandle, view: ViewType, on_view_close: Option<Box<dyn FnOnce()>>) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.view = Some(view);
|
||||
state.popup_handle = handle;
|
||||
state.on_view_close = on_view_close;
|
||||
}
|
||||
|
||||
// Get underlying ViewType object in a closure and return its value
|
||||
// example usage:
|
||||
//
|
||||
// ```rs
|
||||
// holder.with_view(|view| {
|
||||
// view.foo();
|
||||
// })
|
||||
// ```
|
||||
//
|
||||
pub fn with_view<F, R>(&self, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&mut ViewType) -> R,
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(view) = state.view.as_mut() {
|
||||
Some(f(view))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Same as with_view, but the closure expects a simple anyhow::Result<()> type
|
||||
pub fn with_view_res<F>(&self, f: F) -> anyhow::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut ViewType) -> anyhow::Result<()>,
|
||||
{
|
||||
if let Some(res) = self.with_view(f) {
|
||||
return res;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_close_callback(&self, layout: &Layout) -> Box<dyn FnOnce()>
|
||||
where
|
||||
ViewType: 'static,
|
||||
{
|
||||
let layout_tasks = layout.tasks.clone();
|
||||
let weak_state = Rc::downgrade(&self.state);
|
||||
Box::new(move || {
|
||||
// we can't borrow State here yet, dispatch it.
|
||||
layout_tasks.push(LayoutTask::Dispatch(Box::new(move |_common| {
|
||||
if let Some(state) = weak_state.upgrade() {
|
||||
state.borrow_mut().close();
|
||||
}
|
||||
Ok(())
|
||||
})));
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PopupHandle {
|
||||
pub fn close(&self) {
|
||||
self.state.borrow_mut().mounted_popup = None; // Drop will be called
|
||||
|
|
@ -61,10 +189,26 @@ pub struct PopupContentFuncData<'a> {
|
|||
pub id_content: WidgetID,
|
||||
}
|
||||
|
||||
type PopupClosedCallback = Box<dyn FnOnce()>;
|
||||
|
||||
// we need to implement Clone here, but the underlying function can be called only once.
|
||||
// on_content will be cleared after the first call
|
||||
#[derive(Clone)]
|
||||
pub struct MountPopupParams {
|
||||
pub title: Translation,
|
||||
pub on_content: Rc<dyn Fn(PopupContentFuncData) -> anyhow::Result<()>>,
|
||||
pub struct MountPopupOnceParams {
|
||||
title: Translation,
|
||||
on_content: Rc<RefCell<Option<Box<dyn FnOnce(PopupContentFuncData) -> anyhow::Result<PopupClosedCallback>>>>>,
|
||||
}
|
||||
|
||||
impl MountPopupOnceParams {
|
||||
pub fn new(
|
||||
title: Translation,
|
||||
on_content: Box<dyn FnOnce(PopupContentFuncData) -> anyhow::Result<PopupClosedCallback>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
title,
|
||||
on_content: Rc::new(RefCell::new(Some(on_content))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MountedPopup {
|
||||
|
|
@ -78,10 +222,16 @@ impl State {
|
|||
fn refresh_stack(&mut self, alterables: &mut EventAlterables) {
|
||||
// show only the topmost popup
|
||||
self.popup_stack.retain(|weak| {
|
||||
let Some(popup) = weak.upgrade() else {
|
||||
return false;
|
||||
let retain = {
|
||||
let Some(popup) = weak.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
popup.borrow_mut().mounted_popup.is_some()
|
||||
};
|
||||
popup.borrow_mut().mounted_popup.is_some()
|
||||
if !retain {
|
||||
log::debug!("removing popup from popup_stack");
|
||||
}
|
||||
retain
|
||||
});
|
||||
|
||||
for (idx, popup) in self.popup_stack.iter().enumerate() {
|
||||
|
|
@ -116,16 +266,13 @@ impl PopupManager {
|
|||
state.refresh_stack(alterables);
|
||||
}
|
||||
|
||||
/// Mount a new popup on top of the existing popup stack.
|
||||
/// Only the topmost popup is visible.
|
||||
pub fn mount_popup(
|
||||
&mut self,
|
||||
globals: WguiGlobals,
|
||||
fn mount_popup_prepare(
|
||||
&self,
|
||||
globals: &WguiGlobals,
|
||||
layout: &mut Layout,
|
||||
frontend_tasks: FrontendTasks,
|
||||
params: MountPopupParams,
|
||||
config: &GeneralConfig,
|
||||
) -> anyhow::Result<()> {
|
||||
frontend_tasks: &FrontendTasks,
|
||||
popup_title: &Translation,
|
||||
) -> anyhow::Result<(PopupHandle, WidgetID /* content widget ID */)> {
|
||||
let doc_params = &ParseDocumentParams {
|
||||
globals: globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/popup_window.xml"),
|
||||
|
|
@ -138,7 +285,7 @@ impl PopupManager {
|
|||
|
||||
{
|
||||
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&layout.state, "popup_title")?;
|
||||
label_title.set_text_simple(&mut globals.get(), params.title);
|
||||
label_title.set_text_simple(&mut globals.get(), popup_title.clone());
|
||||
}
|
||||
|
||||
let but_back = state.fetch_component_as::<ComponentButton>("but_back")?;
|
||||
|
|
@ -152,6 +299,7 @@ impl PopupManager {
|
|||
|
||||
let mounted_popup_state = MountedPopupState {
|
||||
mounted_popup: Some(mounted_popup),
|
||||
closed_callback: None,
|
||||
};
|
||||
|
||||
let popup_handle = PopupHandle {
|
||||
|
|
@ -159,28 +307,57 @@ impl PopupManager {
|
|||
};
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
log::debug!("pushing popup to popup_stack");
|
||||
state.popup_stack.push(Rc::downgrade(&popup_handle.state));
|
||||
|
||||
but_back.on_click({
|
||||
let popup_handle = Rc::downgrade(&popup_handle.state);
|
||||
Rc::new(move |_common, _evt| {
|
||||
if let Some(popup_handle) = popup_handle.upgrade() {
|
||||
popup_handle.borrow_mut().mounted_popup = None; // will call Drop
|
||||
if let Some(closed_callback) = {
|
||||
let mut state = popup_handle.borrow_mut();
|
||||
state.mounted_popup = None; // will call Drop
|
||||
state.closed_callback.take()
|
||||
} {
|
||||
log::debug!("closed_callback called");
|
||||
closed_callback();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
frontend_tasks.push(FrontendTask::RefreshPopupManager);
|
||||
Ok((popup_handle, id_content))
|
||||
}
|
||||
|
||||
/// Mount a new popup on top of the existing popup stack.
|
||||
/// Only the topmost popup is visible.
|
||||
pub fn mount_popup_once(
|
||||
&mut self,
|
||||
globals: &WguiGlobals,
|
||||
layout: &mut Layout,
|
||||
frontend_tasks: &FrontendTasks,
|
||||
params: MountPopupOnceParams,
|
||||
config: &GeneralConfig,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut func = params.on_content.borrow_mut();
|
||||
let Some(on_content_func) = func.take() else {
|
||||
anyhow::bail!("mount_popup_once called more than once");
|
||||
};
|
||||
|
||||
let (popup_handle, id_content) = self.mount_popup_prepare(globals, layout, frontend_tasks, ¶ms.title)?;
|
||||
|
||||
// mount user-set popup content
|
||||
(*params.on_content)(PopupContentFuncData {
|
||||
let closed_callback = on_content_func(PopupContentFuncData {
|
||||
layout,
|
||||
handle: popup_handle.clone(),
|
||||
id_content,
|
||||
config,
|
||||
})?;
|
||||
|
||||
popup_handle.state.borrow_mut().closed_callback = Some(closed_callback);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use keyvalues_parser::{Obj, Vdf};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SteamUtils {
|
||||
steam_root: PathBuf,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,52 @@
|
|||
use glam::{Mat4, Vec2};
|
||||
use wgui::{
|
||||
animation::{Animation, AnimationEasing},
|
||||
assets::AssetPath,
|
||||
components::{self, button::ButtonClickCallback},
|
||||
drawing,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
renderer_vk::text::TextStyle,
|
||||
widget::label::{WidgetLabel, WidgetLabelParams},
|
||||
layout::{Layout, LayoutTask, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams},
|
||||
renderer_vk::{
|
||||
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
|
||||
util::centered_matrix,
|
||||
},
|
||||
taffy::{self, prelude::length},
|
||||
widget::{
|
||||
ConstructEssentials,
|
||||
label::{WidgetLabel, WidgetLabelParams},
|
||||
sprite::{WidgetSprite, WidgetSpriteParams},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct CreateButtonParams<'a> {
|
||||
pub id_parent: WidgetID,
|
||||
pub layout: &'a mut Layout,
|
||||
pub content: Translation,
|
||||
pub icon_builtin: AssetPath<'a>,
|
||||
pub on_click: ButtonClickCallback,
|
||||
}
|
||||
|
||||
pub fn create_button(par: CreateButtonParams) -> anyhow::Result<()> {
|
||||
let (_, button) = components::button::construct(
|
||||
&mut ConstructEssentials {
|
||||
layout: par.layout,
|
||||
parent: par.id_parent,
|
||||
},
|
||||
components::button::Params {
|
||||
text: Some(par.content),
|
||||
sprite_src: Some(par.icon_builtin),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
button.on_click(par.on_click);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> {
|
||||
pub fn create_label(layout: &mut Layout, id_parent: WidgetID, content: Translation) -> anyhow::Result<()> {
|
||||
let label = WidgetLabel::create(
|
||||
&mut layout.state,
|
||||
WidgetLabelParams {
|
||||
|
|
@ -18,7 +58,102 @@ pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation)
|
|||
},
|
||||
);
|
||||
|
||||
layout.add_child(id_parent, label, Default::default())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_label_error(layout: &mut Layout, parent: WidgetID, content: String) -> anyhow::Result<()> {
|
||||
let label = WidgetLabel::create(
|
||||
&mut layout.state,
|
||||
WidgetLabelParams {
|
||||
content: Translation::from_raw_text_string(content),
|
||||
style: TextStyle {
|
||||
wrap: true,
|
||||
color: Some(drawing::Color::new(1.0, 0.5, 0.0, 1.0)),
|
||||
weight: Some(FontWeight::Bold),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
layout.add_child(parent, label, Default::default())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_icon(layout: &mut Layout, id_parent: WidgetID, size: Vec2, path: AssetPath) -> anyhow::Result<WidgetID> {
|
||||
let widget_sprite = WidgetSprite::create(WidgetSpriteParams {
|
||||
color: None,
|
||||
glyph_data: Some(CustomGlyphData::from_assets(&layout.state.globals, path)?),
|
||||
});
|
||||
|
||||
let size = taffy::Size {
|
||||
width: length(size.x),
|
||||
height: length(size.y),
|
||||
};
|
||||
|
||||
let (widget, _) = layout.add_child(
|
||||
id_parent,
|
||||
widget_sprite,
|
||||
taffy::Style {
|
||||
min_size: size.clone(),
|
||||
max_size: size.clone(),
|
||||
size: size.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(widget.id)
|
||||
}
|
||||
|
||||
pub struct CreateLoadingParams<'a> {
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub with_text: bool,
|
||||
}
|
||||
|
||||
pub fn create_loading(par: CreateLoadingParams) -> anyhow::Result<WidgetID> {
|
||||
let doc_params = ParseDocumentParams {
|
||||
globals: par.layout.state.globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/t_loading.xml"),
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, par.layout, par.parent_id)?;
|
||||
|
||||
let data = parser_state.realize_template(
|
||||
&doc_params,
|
||||
if par.with_text {
|
||||
"LoadingWithText"
|
||||
} else {
|
||||
"LoadingWithoutText"
|
||||
},
|
||||
par.layout,
|
||||
par.parent_id,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
let id_root = data.get_widget_id("root")?;
|
||||
let id_sprite_loading = data.get_widget_id("sprite_loading")?;
|
||||
|
||||
par.layout.animations.add(Animation::new(
|
||||
id_sprite_loading,
|
||||
60 * 30, /* spin it for 30 seconds at most */
|
||||
AnimationEasing::Linear,
|
||||
Box::new(move |common, data| {
|
||||
// spin it
|
||||
data.data.transform = centered_matrix(data.widget_boundary.size, &Mat4::from_rotation_z(data.pos * 400.0));
|
||||
if data.pos == 1.0 {
|
||||
// remove the spinner, do not waste energy
|
||||
common
|
||||
.alterables
|
||||
.tasks
|
||||
.push(LayoutTask::RemoveWidget(id_sprite_loading));
|
||||
}
|
||||
common.alterables.mark_redraw();
|
||||
}),
|
||||
));
|
||||
|
||||
Ok(id_root)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ use wgui::{
|
|||
};
|
||||
use wlx_common::{config::GeneralConfig, dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
|
||||
|
||||
use crate::frontend::{FrontendTask, FrontendTasks, SoundType};
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks, SoundType},
|
||||
util::popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
views::{ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
|
||||
enum PosMode {
|
||||
|
|
@ -66,7 +70,7 @@ struct LaunchParams<'a, T> {
|
|||
interface: &'a mut BoxDashInterface<T>,
|
||||
auto_start: bool,
|
||||
data: &'a mut T,
|
||||
on_launched: &'a dyn Fn(),
|
||||
on_launched: Option<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
|
|
@ -91,7 +95,7 @@ pub struct View {
|
|||
|
||||
auto_start: bool,
|
||||
|
||||
on_launched: Box<dyn Fn()>,
|
||||
on_launched: Option<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
pub struct Params<'a> {
|
||||
|
|
@ -101,7 +105,13 @@ pub struct Params<'a> {
|
|||
pub parent_id: WidgetID,
|
||||
pub config: &'a GeneralConfig,
|
||||
pub frontend_tasks: &'a FrontendTasks,
|
||||
pub on_launched: Box<dyn Fn()>,
|
||||
pub on_launched: Box<dyn FnOnce()>,
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
|
|
@ -280,7 +290,7 @@ impl View {
|
|||
entry: params.entry,
|
||||
frontend_tasks: params.frontend_tasks.clone(),
|
||||
globals: params.globals.clone(),
|
||||
on_launched: params.on_launched,
|
||||
on_launched: Some(params.on_launched),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +326,7 @@ impl View {
|
|||
auto_start: self.auto_start,
|
||||
interface,
|
||||
data,
|
||||
on_launched: &self.on_launched,
|
||||
on_launched: self.on_launched.take(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -334,7 +344,7 @@ impl View {
|
|||
))));
|
||||
}
|
||||
|
||||
fn launch<T>(params: LaunchParams<T>) -> anyhow::Result<()> {
|
||||
fn launch<T>(mut params: LaunchParams<T>) -> anyhow::Result<()> {
|
||||
let mut env = Vec::<String>::new();
|
||||
|
||||
if params.compositor_mode == CompositorMode::Native {
|
||||
|
|
@ -390,7 +400,9 @@ impl View {
|
|||
|
||||
params.frontend_tasks.push(FrontendTask::PlaySound(SoundType::Launch));
|
||||
|
||||
(*params.on_launched)();
|
||||
if let Some(on_launched) = params.on_launched.take() {
|
||||
on_launched();
|
||||
}
|
||||
|
||||
// we're done!
|
||||
Ok(())
|
||||
|
|
@ -420,3 +432,26 @@ impl View {
|
|||
[width as u32, height as u32]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, entry: DesktopEntry, popup: PopupHolder<View>) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_raw_text(&entry.app_name),
|
||||
Box::new(move |data| {
|
||||
let on_launched = popup.get_close_callback(data.layout);
|
||||
let view = View::new(Params {
|
||||
entry: entry.clone(),
|
||||
globals: &globals,
|
||||
layout: data.layout,
|
||||
parent_id: data.id_content,
|
||||
frontend_tasks: &frontend_tasks,
|
||||
config: data.config,
|
||||
on_launched,
|
||||
})?;
|
||||
|
||||
popup.set_view(data.handle, view, None);
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -653,9 +653,6 @@ impl View {
|
|||
}
|
||||
|
||||
fn update_button_highlights(&self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
let mut c = layout.start_common();
|
||||
let mut common = c.common();
|
||||
|
||||
let num: u8 = match &self.mode {
|
||||
CurrentMode::Sinks => 0,
|
||||
CurrentMode::Sources => 1,
|
||||
|
|
@ -663,20 +660,21 @@ impl View {
|
|||
CurrentMode::CardProfileSelector(_) => 255,
|
||||
};
|
||||
|
||||
let mut com = layout.common();
|
||||
|
||||
let mut perform = |btn_num: u8, btn: &Rc<ComponentButton>| {
|
||||
let color = if num == btn_num {
|
||||
common.state.theme.accent_color
|
||||
com.state.theme.accent_color
|
||||
} else {
|
||||
common.state.theme.button_color
|
||||
com.state.theme.button_color
|
||||
};
|
||||
btn.set_color(&mut common, color);
|
||||
btn.set_color(&mut com, color);
|
||||
};
|
||||
|
||||
perform(0, &self.btn_sinks);
|
||||
perform(1, &self.btn_sources);
|
||||
perform(2, &self.btn_cards);
|
||||
|
||||
c.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -801,14 +799,12 @@ impl View {
|
|||
par,
|
||||
)?;
|
||||
|
||||
let mut c = params.layout.start_common();
|
||||
let mut common = c.common();
|
||||
|
||||
let mut common = params.layout.common();
|
||||
let checkbox = data.fetch_component_as::<ComponentCheckbox>("checkbox")?;
|
||||
let btn_mute = data.fetch_component_as::<ComponentButton>("btn_mute")?;
|
||||
let slider = data.fetch_component_as::<ComponentSlider>("slider")?;
|
||||
|
||||
slider.set_value(&mut common, params.control.on_volume_request()? / VOLUME_MULT);
|
||||
slider.set_value_primary(&mut common, params.control.on_volume_request()? / VOLUME_MULT);
|
||||
|
||||
checkbox.set_checked(&mut common, params.checked);
|
||||
|
||||
|
|
@ -838,8 +834,6 @@ impl View {
|
|||
})
|
||||
});
|
||||
|
||||
c.finish()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
views::{ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
|
||||
pub struct ButtonEntry {
|
||||
pub content: Translation, // button text
|
||||
pub icon: &'static str, // sprite_src_builtin
|
||||
pub action: &'static str, // action name (will be passed into on_action_click)
|
||||
}
|
||||
|
||||
pub struct Params {
|
||||
pub globals: WguiGlobals,
|
||||
pub entries: Vec<ButtonEntry>,
|
||||
pub message: Translation,
|
||||
pub on_action_click: Box<dyn FnOnce(&'static str)>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
ActionClicked(&'static str),
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
tasks: Tasks<Task>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
parser_state: ParserState,
|
||||
|
||||
on_action_click: Option<Box<dyn FnOnce(&'static str)>>,
|
||||
on_close_request: Option<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
|
||||
ParseDocumentParams {
|
||||
globals: globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/dialog_box.xml"),
|
||||
extra: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, _par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::ActionClicked(action) => {
|
||||
if let Some(func) = self.on_action_click.take() {
|
||||
func(action);
|
||||
}
|
||||
|
||||
if let Some(on_close) = self.on_close_request.take() {
|
||||
on_close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(
|
||||
layout: &mut Layout,
|
||||
id_parent: WidgetID,
|
||||
on_close_request: Box<dyn FnOnce()>,
|
||||
par: Params,
|
||||
) -> anyhow::Result<Self> {
|
||||
let tasks = Tasks::<Task>::new();
|
||||
|
||||
let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?;
|
||||
let id_buttons = parser_state.get_widget_id("buttons")?;
|
||||
|
||||
{
|
||||
let label_message = parser_state.fetch_widget(&layout.state, "label_message")?.widget;
|
||||
label_message
|
||||
.cast::<WidgetLabel>()?
|
||||
.set_text(&mut layout.common(), par.message);
|
||||
}
|
||||
|
||||
for entry in par.entries {
|
||||
let mut t_par = HashMap::<Rc<str>, Rc<str>>::new();
|
||||
t_par.insert(Rc::from("icon"), Rc::from(entry.icon));
|
||||
|
||||
let data =
|
||||
parser_state.realize_template(&doc_params(&par.globals), "DialogBoxButton", layout, id_buttons, t_par)?;
|
||||
|
||||
let button = data.fetch_component_as::<ComponentButton>("btn")?;
|
||||
button.set_text(&mut layout.common(), entry.content.clone());
|
||||
button.on_click(tasks.get_button_click_callback(Task::ActionClicked(entry.action)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tasks,
|
||||
parser_state,
|
||||
on_action_click: Some(par.on_action_click),
|
||||
on_close_request: Some(on_close_request),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(popup: PopupHolder<View>, frontend_tasks: FrontendTasks, params: Params) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_raw_text("Info"),
|
||||
Box::new(move |data| {
|
||||
let on_close_request = popup.get_close_callback(data.layout);
|
||||
let view = View::new(data.layout, data.id_content, on_close_request, params)?;
|
||||
|
||||
popup.set_view(data.handle, view, None);
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
networking::http_client::{self, ProgressFuncData},
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
wgui_simple,
|
||||
},
|
||||
views::{ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
use glam::Vec2;
|
||||
use std::path::PathBuf;
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
task::Tasks,
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
use wlx_common::async_executor::AsyncExecutor;
|
||||
|
||||
pub struct Params {
|
||||
pub globals: WguiGlobals,
|
||||
pub executor: AsyncExecutor,
|
||||
pub target_path: PathBuf,
|
||||
pub url: String,
|
||||
pub on_downloaded: Box<dyn FnOnce()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
StartDownload(/*url*/ String, /*target path*/ PathBuf),
|
||||
SetStatusText(String),
|
||||
ShowIconSuccess,
|
||||
ShowIconError,
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
globals: WguiGlobals,
|
||||
tasks: Tasks<Task>,
|
||||
executor: AsyncExecutor,
|
||||
|
||||
#[allow(dead_code)]
|
||||
parser_state: ParserState,
|
||||
|
||||
id_label_status: WidgetID,
|
||||
id_loading_parent: WidgetID,
|
||||
id_content: WidgetID,
|
||||
on_close_request: Option<Box<dyn FnOnce()>>,
|
||||
on_downloaded: Option<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
|
||||
ParseDocumentParams {
|
||||
globals: globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/download_file.xml"),
|
||||
extra: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::StartDownload(url, path) => {
|
||||
if let Some(on_downloaded) = self.on_downloaded.take() {
|
||||
self
|
||||
.executor
|
||||
.spawn(View::download(
|
||||
self.tasks.clone(),
|
||||
self.executor.clone(),
|
||||
url,
|
||||
path,
|
||||
on_downloaded,
|
||||
))
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
Task::SetStatusText(text) => {
|
||||
let widgets = &mut par.layout.state.widgets;
|
||||
widgets
|
||||
.fetch(self.id_label_status)?
|
||||
.cast::<WidgetLabel>()?
|
||||
.set_text(&mut par.layout.common(), Translation::from_raw_text_string(text));
|
||||
}
|
||||
Task::ShowIconSuccess => {
|
||||
par.layout.remove_children(self.id_loading_parent);
|
||||
wgui_simple::create_icon(
|
||||
par.layout,
|
||||
self.id_loading_parent,
|
||||
Vec2::splat(32.0),
|
||||
AssetPath::BuiltIn("dashboard/check.svg"),
|
||||
)?;
|
||||
|
||||
// "Close window" button
|
||||
self
|
||||
.parser_state
|
||||
.realize_template(
|
||||
&doc_params(&self.globals),
|
||||
"btn_close",
|
||||
par.layout,
|
||||
self.id_content,
|
||||
Default::default(),
|
||||
)?
|
||||
.fetch_component_as::<ComponentButton>("btn")?
|
||||
.on_click(self.tasks.get_button_click_callback(Task::Close));
|
||||
}
|
||||
Task::ShowIconError => {
|
||||
par.layout.remove_children(self.id_loading_parent);
|
||||
wgui_simple::create_icon(
|
||||
par.layout,
|
||||
self.id_loading_parent,
|
||||
Vec2::splat(32.0),
|
||||
AssetPath::BuiltIn("dashboard/error.svg"),
|
||||
)?;
|
||||
}
|
||||
Task::Close => {
|
||||
if let Some(on_close) = self.on_close_request.take() {
|
||||
on_close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_async_result<T, E>(error_reason: &'static str, tasks: &Tasks<Task>, result: anyhow::Result<T, E>) -> Option<T>
|
||||
where
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
match result {
|
||||
Ok(res) => Some(res),
|
||||
Err(e) => {
|
||||
tasks.push(Task::ShowIconError);
|
||||
tasks.push(Task::SetStatusText(format!("{}: {:?}", error_reason, e)));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(
|
||||
layout: &mut Layout,
|
||||
id_parent: WidgetID,
|
||||
on_close_request: Box<dyn FnOnce()>,
|
||||
par: Params,
|
||||
) -> anyhow::Result<Self> {
|
||||
let tasks = Tasks::<Task>::new();
|
||||
|
||||
let parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), layout, id_parent)?;
|
||||
let id_label_status = parser_state.get_widget_id("label_status")?;
|
||||
let id_content = parser_state.get_widget_id("content")?;
|
||||
let id_loading_parent = parser_state.get_widget_id("loading_parent")?;
|
||||
|
||||
wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
|
||||
parent_id: id_loading_parent,
|
||||
layout: layout,
|
||||
with_text: false,
|
||||
})?;
|
||||
|
||||
let str_target_path = par.globals.i18n().translate("TARGET_PATH");
|
||||
|
||||
{
|
||||
let label_target_path = parser_state.fetch_widget(&layout.state, "label_target_path")?.widget;
|
||||
label_target_path.cast::<WidgetLabel>()?.set_text(
|
||||
&mut layout.common(),
|
||||
Translation::from_raw_text_string(format!("{}: {}", str_target_path, par.target_path.display())),
|
||||
);
|
||||
}
|
||||
|
||||
tasks.push(Task::StartDownload(par.url, par.target_path));
|
||||
|
||||
Ok(Self {
|
||||
tasks,
|
||||
globals: par.globals.clone(),
|
||||
executor: par.executor.clone(),
|
||||
parser_state,
|
||||
id_label_status,
|
||||
id_loading_parent,
|
||||
id_content,
|
||||
on_close_request: Some(on_close_request),
|
||||
on_downloaded: Some(par.on_downloaded),
|
||||
})
|
||||
}
|
||||
|
||||
async fn download(
|
||||
tasks: Tasks<Task>,
|
||||
executor: AsyncExecutor,
|
||||
url: String,
|
||||
target_path: PathBuf,
|
||||
on_downloaded: Box<dyn FnOnce()>,
|
||||
) -> Option<()> {
|
||||
tasks.push(Task::SetStatusText(String::from("Connecting to the server...")));
|
||||
|
||||
// start downloading from the server with progress reporting
|
||||
let res = handle_async_result(
|
||||
"Download failed",
|
||||
&tasks,
|
||||
http_client::get(http_client::GetParams {
|
||||
executor: &executor,
|
||||
url: &url,
|
||||
on_progress: Some(Box::new({
|
||||
let tasks = tasks.clone();
|
||||
move |data: ProgressFuncData| {
|
||||
tasks.push(Task::SetStatusText(format!(
|
||||
"{}/{} KiB ({}%)",
|
||||
data.bytes_downloaded / 1024,
|
||||
data.file_size / 1024,
|
||||
(data.bytes_downloaded as f32 / data.file_size as f32 * 100.0).round()
|
||||
)))
|
||||
}
|
||||
})),
|
||||
})
|
||||
.await,
|
||||
)?;
|
||||
|
||||
tasks.push(Task::SetStatusText(String::from("Writing to file...")));
|
||||
|
||||
// create skymaps directory if it doesn't exist yet
|
||||
if let Some(parent) = target_path.parent() {
|
||||
handle_async_result(
|
||||
"Directory creation failed",
|
||||
&tasks,
|
||||
smol::fs::create_dir_all(parent).await,
|
||||
)?;
|
||||
}
|
||||
|
||||
handle_async_result(
|
||||
"File write failed",
|
||||
&tasks,
|
||||
smol::fs::write(target_path, res.data).await,
|
||||
)?;
|
||||
|
||||
tasks.push(Task::SetStatusText(String::from("Download finished")));
|
||||
tasks.push(Task::ShowIconSuccess);
|
||||
|
||||
on_downloaded();
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(
|
||||
popup: PopupHolder<View>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
on_view_close: Box<dyn FnOnce()>,
|
||||
params: Params,
|
||||
) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_translation_key("DOWNLOADER"),
|
||||
Box::new(move |data| {
|
||||
let on_close_request = popup.get_close_callback(data.layout);
|
||||
let view = View::new(data.layout, data.id_content, on_close_request, params)?;
|
||||
|
||||
popup.set_view(data.handle, view, Some(on_view_close));
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ use wlx_common::async_executor::AsyncExecutor;
|
|||
use crate::util::{
|
||||
cached_fetcher::{self, CoverArt},
|
||||
steam_utils::{self, AppID},
|
||||
wgui_simple,
|
||||
};
|
||||
|
||||
pub struct ViewCommon {
|
||||
|
|
@ -48,6 +49,7 @@ pub struct Params<'a, 'b> {
|
|||
pub struct View {
|
||||
pub button: Rc<ComponentButton>,
|
||||
id_image_parent: WidgetID,
|
||||
id_loading: WidgetID,
|
||||
app_name: String,
|
||||
app_id: AppID,
|
||||
}
|
||||
|
|
@ -143,6 +145,8 @@ impl View {
|
|||
layout: &mut Layout,
|
||||
cover_art: &CoverArt,
|
||||
) -> anyhow::Result<()> {
|
||||
layout.remove_widget(self.id_loading);
|
||||
|
||||
if cover_art.compressed_image_data.is_empty() {
|
||||
// mount placeholder
|
||||
let img = view_common.get_placeholder_image()?.clone();
|
||||
|
|
@ -271,6 +275,12 @@ impl View {
|
|||
rect_gradient_style(taffy::AlignSelf::End, 0.05),
|
||||
)?;
|
||||
|
||||
let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
|
||||
layout: params.ess.layout,
|
||||
parent_id: image_parent.id,
|
||||
with_text: false,
|
||||
})?;
|
||||
|
||||
// request cover image data from the internet or disk cache
|
||||
params
|
||||
.executor
|
||||
|
|
@ -286,6 +296,7 @@ impl View {
|
|||
id_image_parent: image_parent.id,
|
||||
app_name: params.manifest.name.clone(),
|
||||
app_id: params.manifest.app_id.clone(),
|
||||
id_loading,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ use crate::{
|
|||
frontend::{FrontendTask, FrontendTasks, SoundType},
|
||||
util::{
|
||||
cached_fetcher::{self, CoverArt},
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
steam_utils::{self, AppID, AppManifest},
|
||||
},
|
||||
views::game_cover,
|
||||
views::{ViewTrait, ViewUpdateParams, game_cover},
|
||||
};
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
|
|
@ -34,13 +35,14 @@ pub struct Params<'a> {
|
|||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub frontend_tasks: &'a FrontendTasks,
|
||||
pub on_launched: Box<dyn Fn()>,
|
||||
pub on_launched: Box<dyn FnOnce()>,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
state: ParserState,
|
||||
tasks: Tasks<Task>,
|
||||
on_launched: Box<dyn Fn()>,
|
||||
on_launched: Option<Box<dyn FnOnce()>>,
|
||||
frontend_tasks: FrontendTasks,
|
||||
|
||||
game_cover_view_common: game_cover::ViewCommon,
|
||||
|
|
@ -48,6 +50,30 @@ pub struct View {
|
|||
app_id: AppID,
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::FillAppDetails(details) => self.action_fill_app_details(&mut par.layout, details)?,
|
||||
Task::Launch => self.action_launch(),
|
||||
Task::SetCoverArt(cover_art) => {
|
||||
let _ = self
|
||||
.view_cover
|
||||
.set_cover_art(&mut self.game_cover_view_common, &mut par.layout, &cover_art);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
async fn fetch_details(executor: AsyncExecutor, tasks: Tasks<Task>, app_id: AppID) {
|
||||
let Some(details) = cached_fetcher::get_app_details_json(executor, app_id).await else {
|
||||
|
|
@ -104,7 +130,7 @@ impl View {
|
|||
Ok(Self {
|
||||
state,
|
||||
tasks,
|
||||
on_launched: params.on_launched,
|
||||
on_launched: Some(params.on_launched),
|
||||
frontend_tasks: params.frontend_tasks.clone(),
|
||||
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
|
||||
view_cover,
|
||||
|
|
@ -112,43 +138,20 @@ impl View {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::FillAppDetails(details) => self.action_fill_app_details(layout, details)?,
|
||||
Task::Launch => self.action_launch(),
|
||||
Task::SetCoverArt(cover_art) => {
|
||||
let _ = self
|
||||
.view_cover
|
||||
.set_cover_art(&mut self.game_cover_view_common, layout, &cover_art);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_fill_app_details(
|
||||
&mut self,
|
||||
layout: &mut Layout,
|
||||
mut details: cached_fetcher::AppDetailsJSONData,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut c = layout.start_common();
|
||||
|
||||
{
|
||||
let label_author = self.state.fetch_widget(&c.layout.state, "label_author")?.widget;
|
||||
let label_description = self.state.fetch_widget(&c.layout.state, "label_description")?.widget;
|
||||
let mut c = layout.common();
|
||||
let label_author = self.state.fetch_widget(&c.state, "label_author")?.widget;
|
||||
let label_description = self.state.fetch_widget(&c.state, "label_description")?.widget;
|
||||
|
||||
if let Some(developer) = details.developers.pop() {
|
||||
label_author
|
||||
.cast::<WidgetLabel>()?
|
||||
.set_text(&mut c.common(), Translation::from_raw_text_string(developer));
|
||||
.set_text(&mut c, Translation::from_raw_text_string(developer));
|
||||
}
|
||||
|
||||
let desc = if let Some(desc) = &details.short_description {
|
||||
|
|
@ -162,11 +165,10 @@ impl View {
|
|||
if let Some(desc) = desc {
|
||||
label_description
|
||||
.cast::<WidgetLabel>()?
|
||||
.set_text(&mut c.common(), Translation::from_raw_text(desc));
|
||||
.set_text(&mut c, Translation::from_raw_text(desc));
|
||||
}
|
||||
}
|
||||
|
||||
c.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +192,37 @@ impl View {
|
|||
}
|
||||
}
|
||||
|
||||
(*self.on_launched)();
|
||||
if let Some(on_launched) = self.on_launched.take() {
|
||||
on_launched();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(
|
||||
frontend_tasks: FrontendTasks,
|
||||
executor: AsyncExecutor,
|
||||
globals: WguiGlobals,
|
||||
manifest: AppManifest,
|
||||
popup: PopupHolder<View>,
|
||||
) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_raw_text(&manifest.name),
|
||||
Box::new(move |data| {
|
||||
let on_launched = popup.get_close_callback(data.layout);
|
||||
let view = View::new(Params {
|
||||
manifest: manifest.clone(),
|
||||
executor: executor.clone(),
|
||||
globals: &globals,
|
||||
layout: data.layout,
|
||||
parent_id: data.id_content,
|
||||
frontend_tasks: &frontend_tasks,
|
||||
on_launched,
|
||||
})?;
|
||||
|
||||
popup.set_view(data.handle, view, None);
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
|
|
@ -16,20 +16,19 @@ use wgui::{
|
|||
use wlx_common::async_executor::AsyncExecutor;
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
frontend::FrontendTasks,
|
||||
util::{
|
||||
cached_fetcher::CoverArt,
|
||||
popup_manager::{MountPopupParams, PopupHandle},
|
||||
popup_manager::PopupHolder,
|
||||
steam_utils::{self, AppID, SteamUtils},
|
||||
},
|
||||
views::{self, game_cover, game_launcher},
|
||||
views::{self, ViewTrait, ViewUpdateParams, game_cover},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
AppManifestClicked(steam_utils::AppManifest),
|
||||
SetCoverArt(AppID, Rc<CoverArt>),
|
||||
CloseLauncher,
|
||||
LoadManifests,
|
||||
FillPage(u32),
|
||||
PrevPage,
|
||||
|
|
@ -42,6 +41,7 @@ pub struct Params<'a> {
|
|||
pub frontend_tasks: FrontendTasks,
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub steam_utils: &'a SteamUtils,
|
||||
}
|
||||
|
||||
const MAX_GAMES_PER_PAGE: u32 = 30;
|
||||
|
|
@ -50,10 +50,6 @@ pub struct GameCoverCell {
|
|||
view_cover: game_cover::View,
|
||||
}
|
||||
|
||||
struct State {
|
||||
view_launcher: Option<(PopupHandle, views::game_launcher::View)>,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
parser_state: ParserState,
|
||||
|
|
@ -63,12 +59,37 @@ pub struct View {
|
|||
id_list_parent: WidgetID,
|
||||
game_cover_view_common: game_cover::ViewCommon,
|
||||
executor: AsyncExecutor,
|
||||
state: Rc<RefCell<State>>,
|
||||
mounted_game_covers: HashMap<AppID, GameCoverCell>,
|
||||
all_manifests: Vec<steam_utils::AppManifest>,
|
||||
cur_page: u32,
|
||||
page_count: u32,
|
||||
id_label_page: WidgetID,
|
||||
view_launcher: PopupHolder<views::game_launcher::View>,
|
||||
steam_utils: SteamUtils,
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::LoadManifests => self.load_manifests(),
|
||||
Task::FillPage(page_idx) => self.fill_page(&mut par.layout, &mut par.executor, page_idx)?,
|
||||
Task::AppManifestClicked(manifest) => self.action_app_manifest_clicked(manifest)?,
|
||||
Task::SetCoverArt(app_id, cover_art) => self.set_cover_art(&mut par.layout, app_id, cover_art),
|
||||
Task::PrevPage => self.page_prev(),
|
||||
Task::NextPage => self.page_next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.view_launcher.update(par)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
|
|
@ -105,46 +126,15 @@ impl View {
|
|||
id_list_parent: list_parent.id,
|
||||
mounted_game_covers: HashMap::new(),
|
||||
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
|
||||
state: Rc::new(RefCell::new(State { view_launcher: None })),
|
||||
executor: params.executor,
|
||||
all_manifests: Vec::new(),
|
||||
cur_page: 0,
|
||||
page_count: 0,
|
||||
id_label_page,
|
||||
view_launcher: Default::default(),
|
||||
steam_utils: params.steam_utils.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
layout: &mut Layout,
|
||||
steam_utils: &mut SteamUtils,
|
||||
executor: &AsyncExecutor,
|
||||
) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::LoadManifests => self.load_manifests(steam_utils),
|
||||
Task::FillPage(page_idx) => self.fill_page(layout, executor, page_idx)?,
|
||||
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,
|
||||
Task::PrevPage => self.page_prev(),
|
||||
Task::NextPage => self.page_next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some((_, view)) = &mut state.view_launcher {
|
||||
view.update(layout)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_game_list(
|
||||
|
|
@ -187,8 +177,11 @@ fn fill_game_list(
|
|||
}
|
||||
|
||||
impl View {
|
||||
fn load_manifests(&mut self, steam_utils: &mut SteamUtils) {
|
||||
match steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc) {
|
||||
fn load_manifests(&mut self) {
|
||||
match self
|
||||
.steam_utils
|
||||
.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)
|
||||
{
|
||||
Ok(manifests) => {
|
||||
self.page_count = (manifests.len() as u32 + MAX_GAMES_PER_PAGE) / MAX_GAMES_PER_PAGE;
|
||||
self.all_manifests = manifests;
|
||||
|
|
@ -233,16 +226,14 @@ impl View {
|
|||
}
|
||||
|
||||
// set page text
|
||||
let mut c = layout.start_common();
|
||||
{
|
||||
let mut common = c.common();
|
||||
let mut widget = common.state.widgets.cast_as::<WidgetLabel>(self.id_label_page)?;
|
||||
let mut c = layout.common();
|
||||
let mut widget = c.state.widgets.cast_as::<WidgetLabel>(self.id_label_page)?;
|
||||
widget.set_text(
|
||||
&mut common,
|
||||
&mut c,
|
||||
Translation::from_raw_text_string(format!("{}/{}", self.cur_page + 1, self.page_count)),
|
||||
);
|
||||
}
|
||||
c.finish()?;
|
||||
|
||||
fill_game_list(
|
||||
&mut ConstructEssentials {
|
||||
|
|
@ -283,36 +274,13 @@ impl View {
|
|||
}
|
||||
|
||||
fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> {
|
||||
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
|
||||
title: Translation::from_raw_text(&manifest.name),
|
||||
on_content: {
|
||||
let state = self.state.clone();
|
||||
let tasks = self.tasks.clone();
|
||||
let executor = self.executor.clone();
|
||||
let globals = self.globals.clone();
|
||||
let frontend_tasks = self.frontend_tasks.clone();
|
||||
|
||||
Rc::new(move |data| {
|
||||
let on_launched = {
|
||||
let tasks = tasks.clone();
|
||||
Box::new(move || tasks.push(Task::CloseLauncher))
|
||||
};
|
||||
|
||||
let view = game_launcher::View::new(game_launcher::Params {
|
||||
manifest: manifest.clone(),
|
||||
executor: executor.clone(),
|
||||
globals: &globals,
|
||||
layout: data.layout,
|
||||
parent_id: data.id_content,
|
||||
frontend_tasks: &frontend_tasks,
|
||||
on_launched,
|
||||
})?;
|
||||
|
||||
state.borrow_mut().view_launcher = Some((data.handle, view));
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
}));
|
||||
views::game_launcher::mount_popup(
|
||||
self.frontend_tasks.clone(),
|
||||
self.executor.clone(),
|
||||
self.globals.clone(),
|
||||
manifest,
|
||||
self.view_launcher.clone(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, dash_interface::ConfigChangeKind};
|
||||
|
||||
pub mod app_launcher;
|
||||
pub mod audio_settings;
|
||||
pub mod dialog_box;
|
||||
pub mod download_file;
|
||||
pub mod game_cover;
|
||||
pub mod game_launcher;
|
||||
pub mod game_list;
|
||||
pub mod remote_skymap_downloader;
|
||||
pub mod remote_skymap_list;
|
||||
pub mod running_games_list;
|
||||
pub mod skymap_list;
|
||||
pub mod skymap_list_cell;
|
||||
|
||||
pub struct ViewUpdateParams<'a> {
|
||||
pub layout: &'a mut wgui::layout::Layout,
|
||||
pub executor: &'a AsyncExecutor,
|
||||
pub general_config: &'a mut GeneralConfig,
|
||||
pub config_change_kind: &'a mut Option<ConfigChangeKind>,
|
||||
}
|
||||
|
||||
pub trait ViewTrait {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,396 @@
|
|||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
networking::{self, skymap_catalog::SkymapResolution},
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
},
|
||||
views::{self, ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
drawing::Color,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
renderer_vk::text::custom_glyph::CustomGlyphData,
|
||||
task::Tasks,
|
||||
widget::{image::WidgetImage, label::WidgetLabel},
|
||||
};
|
||||
use wlx_common::{async_executor::AsyncExecutor, config_io, dash_interface::ConfigChangeKind};
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: &'a WguiGlobals,
|
||||
pub layout: &'a mut Layout,
|
||||
pub executor: &'a AsyncExecutor,
|
||||
pub frontend_tasks: FrontendTasks,
|
||||
pub parent_id: WidgetID,
|
||||
pub entry: networking::skymap_catalog::SkymapCatalogEntry,
|
||||
pub preview_image: CustomGlyphData,
|
||||
pub preview_image_compressed: Rc<Vec<u8>>,
|
||||
pub on_updated_library: Rc<dyn Fn()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
Refresh,
|
||||
ResolutionClicked(SkymapResolution),
|
||||
DownloadFinished,
|
||||
RunDownload(SkymapResolution),
|
||||
RemoveFile(SkymapResolution),
|
||||
SetSkymap(SkymapResolution),
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
entry: networking::skymap_catalog::SkymapCatalogEntry,
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
tasks: Tasks<Task>,
|
||||
executor: AsyncExecutor,
|
||||
|
||||
id_resolution_buttons: WidgetID,
|
||||
|
||||
#[allow(dead_code)]
|
||||
parser_state: ParserState,
|
||||
|
||||
popup_download: PopupHolder<views::download_file::View>,
|
||||
popup_dialog_box: PopupHolder<views::dialog_box::View>,
|
||||
|
||||
preview_image_compressed: Rc<Vec<u8>>,
|
||||
on_updated_library: Rc<dyn Fn()>,
|
||||
}
|
||||
|
||||
fn mount_resolution_button(
|
||||
layout: &mut Layout,
|
||||
parser_state: &mut ParserState,
|
||||
doc_params: &ParseDocumentParams,
|
||||
parent_id: WidgetID,
|
||||
res: SkymapResolution,
|
||||
tasks: &Tasks<Task>,
|
||||
already_downloaded: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut t = HashMap::<Rc<str>, Rc<str>>::new();
|
||||
t.insert(Rc::from("text"), Rc::from(res.get_display_str()));
|
||||
t.insert(
|
||||
Rc::from("sprite"),
|
||||
Rc::from(match already_downloaded {
|
||||
true => "dashboard/check.svg",
|
||||
false => "dashboard/download.svg",
|
||||
}),
|
||||
);
|
||||
let data = parser_state.realize_template(doc_params, "ResolutionButton", layout, parent_id, t)?;
|
||||
let button = data.fetch_component_as::<ComponentButton>("button")?;
|
||||
|
||||
if already_downloaded {
|
||||
button.set_color(&mut layout.common(), Color::new(0.0, 0.4, 0.0, 1.0)); // green
|
||||
}
|
||||
tasks.handle_button(&button, Task::ResolutionClicked(res));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::ResolutionClicked(resolution) => {
|
||||
self.resolution_clicked(resolution)?;
|
||||
}
|
||||
Task::Refresh => {
|
||||
self.refresh(par.layout)?;
|
||||
}
|
||||
Task::DownloadFinished => {
|
||||
self.download_finished()?;
|
||||
}
|
||||
Task::RunDownload(resolution) => {
|
||||
self.run_download(resolution)?;
|
||||
}
|
||||
Task::RemoveFile(resolution) => {
|
||||
self.remove_file(resolution)?;
|
||||
}
|
||||
Task::SetSkymap(resolution) => {
|
||||
let skymap_file_path = self
|
||||
.entry
|
||||
.get_destination_path(resolution)
|
||||
.context("Skymap not found" /* you shouldn't really see this, like ever. */)?;
|
||||
|
||||
par.general_config.skybox_texture = skymap_file_path
|
||||
.to_str()
|
||||
.context("Skymap filename not valid UTF-8")?
|
||||
.into();
|
||||
*par.config_change_kind = Some(ConfigChangeKind::EnvironmentBlend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.popup_download.update(par)?;
|
||||
self.popup_dialog_box.update(par)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn doc_params(globals: &WguiGlobals) -> ParseDocumentParams<'_> {
|
||||
ParseDocumentParams {
|
||||
globals: globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/remote_skymap_downloader.xml"),
|
||||
extra: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(par: Params) -> anyhow::Result<Self> {
|
||||
let tasks = Tasks::<Task>::new();
|
||||
|
||||
let parser_state = wgui::parser::parse_from_assets(&doc_params(&par.globals), par.layout, par.parent_id)?;
|
||||
let id_resolution_buttons = parser_state.get_widget_id("resolution_buttons")?;
|
||||
|
||||
let str_version = par.globals.i18n().translate("VERSION");
|
||||
let str_creation_date = par.globals.i18n().translate("CREATION_DATE");
|
||||
let str_modification_date = par.globals.i18n().translate("MODIFICATION_DATE");
|
||||
|
||||
let image = parser_state.fetch_widget(&par.layout.state, "image")?.widget;
|
||||
let mut image = image.cast::<WidgetImage>()?;
|
||||
image.set_content(&mut par.layout.alterables, Some(par.preview_image));
|
||||
|
||||
// Set author label
|
||||
parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_author")?
|
||||
.set_text_simple(
|
||||
&mut par.globals.get(),
|
||||
Translation::from_raw_text_string(format!("by {}", par.entry.author)),
|
||||
);
|
||||
|
||||
// Set description label
|
||||
parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_description")?
|
||||
.set_text_simple(
|
||||
&mut par.globals.get(),
|
||||
Translation::from_raw_text(&par.entry.description),
|
||||
);
|
||||
|
||||
// Set version label
|
||||
parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_version")?
|
||||
.set_text_simple(
|
||||
&mut par.globals.get(),
|
||||
Translation::from_raw_text_string(format!("{}: {}", str_version, par.entry.version)),
|
||||
);
|
||||
|
||||
// Set creation date label
|
||||
parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_creation_date")?
|
||||
.set_text_simple(
|
||||
&mut par.globals.get(),
|
||||
Translation::from_raw_text_string(format!("{}: {}", str_creation_date, par.entry.created_at)),
|
||||
);
|
||||
|
||||
// Set modification date label
|
||||
parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_modification_date")?
|
||||
.set_text_simple(
|
||||
&mut par.globals.get(),
|
||||
Translation::from_raw_text_string(format!("{}: {}", str_modification_date, par.entry.created_at)),
|
||||
);
|
||||
|
||||
tasks.push(Task::Refresh);
|
||||
|
||||
Ok(Self {
|
||||
tasks,
|
||||
globals: par.globals.clone(),
|
||||
executor: par.executor.clone(),
|
||||
entry: par.entry,
|
||||
parser_state,
|
||||
frontend_tasks: par.frontend_tasks,
|
||||
popup_download: Default::default(),
|
||||
popup_dialog_box: Default::default(),
|
||||
id_resolution_buttons,
|
||||
preview_image_compressed: par.preview_image_compressed,
|
||||
on_updated_library: par.on_updated_library,
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
layout.remove_children(self.id_resolution_buttons);
|
||||
|
||||
let files = &self.entry.files;
|
||||
let mut mount_res = |res: SkymapResolution| -> anyhow::Result<()> {
|
||||
mount_resolution_button(
|
||||
layout,
|
||||
&mut self.parser_state,
|
||||
&doc_params(&self.globals),
|
||||
self.id_resolution_buttons,
|
||||
res,
|
||||
&self.tasks,
|
||||
self.entry.is_downloaded(res)?,
|
||||
)
|
||||
};
|
||||
|
||||
mount_res(SkymapResolution::Res2k)?;
|
||||
if files.size_4k.is_some() {
|
||||
mount_res(SkymapResolution::Res4k)?;
|
||||
}
|
||||
if files.size_8k.is_some() {
|
||||
mount_res(SkymapResolution::Res8k)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolution_clicked(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
|
||||
let is_downloaded = self.entry.is_downloaded(resolution).unwrap_or(false);
|
||||
if !is_downloaded {
|
||||
self.tasks.push(Task::RunDownload(resolution));
|
||||
} else {
|
||||
self.show_dialog_box_action(resolution)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_dialog_box_action(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
|
||||
const ACTION_REMOVE: &'static str = "remove";
|
||||
const ACTION_DOWNLOAD_AGAIN: &'static str = "download_again";
|
||||
const ACTION_APPLY: &'static str = "apply";
|
||||
|
||||
let tasks = self.tasks.clone();
|
||||
|
||||
views::dialog_box::mount_popup(
|
||||
self.popup_dialog_box.clone(),
|
||||
self.frontend_tasks.clone(),
|
||||
views::dialog_box::Params {
|
||||
globals: self.globals.clone(),
|
||||
message: Translation::from_translation_key("APP_SETTINGS.SKYMAP_ALREADY_DOWNLOADED"),
|
||||
entries: vec![
|
||||
views::dialog_box::ButtonEntry {
|
||||
content: Translation::from_translation_key("REMOVE"),
|
||||
icon: "dashboard/trash.svg",
|
||||
action: ACTION_REMOVE,
|
||||
},
|
||||
views::dialog_box::ButtonEntry {
|
||||
content: Translation::from_translation_key("DOWNLOAD_AGAIN"),
|
||||
icon: "dashboard/download.svg",
|
||||
action: ACTION_DOWNLOAD_AGAIN,
|
||||
},
|
||||
views::dialog_box::ButtonEntry {
|
||||
content: Translation::from_translation_key("APPLY"),
|
||||
icon: "dashboard/check.svg",
|
||||
action: ACTION_APPLY,
|
||||
},
|
||||
],
|
||||
on_action_click: Box::new(move |action| match action {
|
||||
ACTION_REMOVE => {
|
||||
tasks.push(Task::RemoveFile(resolution));
|
||||
tasks.push(Task::Refresh);
|
||||
}
|
||||
ACTION_DOWNLOAD_AGAIN => {
|
||||
tasks.push(Task::RunDownload(resolution));
|
||||
tasks.push(Task::Refresh);
|
||||
}
|
||||
ACTION_APPLY => {
|
||||
tasks.push(Task::SetSkymap(resolution));
|
||||
tasks.push(Task::Refresh);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_finished(&mut self) -> anyhow::Result<()> {
|
||||
self.entry.save_metadata()?;
|
||||
let mut uuids = config_io::get_skymaps_uuids().unwrap_or_default();
|
||||
let uuid_str = self.entry.uuid.to_string();
|
||||
if !uuids.contains(&uuid_str) {
|
||||
uuids.push(uuid_str);
|
||||
}
|
||||
config_io::set_skymaps_uuids(&uuids)?;
|
||||
|
||||
// Save preview image
|
||||
self.entry.files.save_preview_to_file(&self.preview_image_compressed)?;
|
||||
|
||||
(*self.on_updated_library)();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_download(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
|
||||
let Some(url) = self.entry.files.get_url_from_res(resolution) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(target_path) = self.entry.get_destination_path(resolution) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
views::download_file::mount_popup(
|
||||
self.popup_download.clone(),
|
||||
self.frontend_tasks.clone(),
|
||||
self.tasks.make_callback_box(Task::Refresh),
|
||||
views::download_file::Params {
|
||||
globals: self.globals.clone(),
|
||||
executor: self.executor.clone(),
|
||||
target_path,
|
||||
url,
|
||||
on_downloaded: self.tasks.make_callback_box(Task::DownloadFinished),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, resolution: SkymapResolution) -> anyhow::Result<()> {
|
||||
self.entry.remove_file(resolution);
|
||||
|
||||
if !self.entry.has_any_downloaded() {
|
||||
// all skymaps of this uuid are removed, clean-up files
|
||||
self.entry.remove_metadata();
|
||||
|
||||
// remove uuid of this entry from downloaded skymaps uuid and save the file again
|
||||
let mut uuids = config_io::get_skymaps_uuids().unwrap_or_default();
|
||||
uuids.retain(|uuid| *uuid != self.entry.uuid.to_string());
|
||||
config_io::set_skymaps_uuids(&uuids)?;
|
||||
|
||||
// remove "_preview.dds" files from the disk too
|
||||
self.entry.files.remove_preview_file();
|
||||
}
|
||||
|
||||
(*self.on_updated_library)();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(
|
||||
frontend_tasks: FrontendTasks,
|
||||
executor: AsyncExecutor,
|
||||
globals: WguiGlobals,
|
||||
entry: networking::skymap_catalog::SkymapCatalogEntry,
|
||||
preview_image: CustomGlyphData,
|
||||
preview_image_compressed: Rc<Vec<u8>>,
|
||||
on_updated_library: Rc<dyn Fn()>,
|
||||
popup: PopupHolder<View>,
|
||||
) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_raw_text(&entry.name),
|
||||
Box::new(move |data| {
|
||||
let view = View::new(Params {
|
||||
globals: &globals,
|
||||
layout: data.layout,
|
||||
executor: &executor,
|
||||
parent_id: data.id_content,
|
||||
entry,
|
||||
preview_image,
|
||||
frontend_tasks: frontend_tasks.clone(),
|
||||
preview_image_compressed,
|
||||
on_updated_library,
|
||||
})?;
|
||||
|
||||
popup.set_view(data.handle, view, None);
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use uuid::Uuid;
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams},
|
||||
renderer_vk::text::custom_glyph::CustomGlyphData,
|
||||
task::Tasks,
|
||||
};
|
||||
use wlx_common::async_executor::AsyncExecutor;
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
networking::{
|
||||
self,
|
||||
skymap_catalog::{SkymapCatalog, SkymapCatalogEntry, SkymapUuid},
|
||||
},
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
wgui_simple,
|
||||
},
|
||||
views::{self, ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: &'a WguiGlobals,
|
||||
pub layout: &'a mut Layout,
|
||||
pub executor: &'a AsyncExecutor,
|
||||
pub parent_id: WidgetID,
|
||||
pub frontend_tasks: FrontendTasks,
|
||||
pub on_updated_library: Rc<dyn Fn()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
SetSkymapCatalog(Rc<anyhow::Result<networking::skymap_catalog::SkymapCatalog>>),
|
||||
SetSkymapPreview(
|
||||
(
|
||||
SkymapUuid,
|
||||
Option<(
|
||||
CustomGlyphData, /* ready-to-use preview image data */
|
||||
Rc<Vec<u8>>, /* compressed preview image data (should weigh about 10-15 KiB) */
|
||||
)>,
|
||||
),
|
||||
),
|
||||
ShowRemoteSkymapDownloader(SkymapUuid),
|
||||
RefreshCells,
|
||||
}
|
||||
|
||||
struct MountedCell {
|
||||
skymap_uuid: SkymapUuid,
|
||||
view: views::skymap_list_cell::View,
|
||||
preview_image_compressed: Option<Rc<Vec<u8>>>,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
id_parent: WidgetID,
|
||||
id_loading: WidgetID,
|
||||
globals: WguiGlobals,
|
||||
tasks: Tasks<Task>,
|
||||
mounted_cells: Vec<MountedCell>,
|
||||
executor: AsyncExecutor,
|
||||
frontend_tasks: FrontendTasks,
|
||||
catalog: Option<SkymapCatalog>,
|
||||
popup_remote_skymap_downloader: PopupHolder<views::remote_skymap_downloader::View>,
|
||||
on_updated_library: Rc<dyn Fn()>,
|
||||
}
|
||||
|
||||
fn get_entry_by_uuid(catalog: &SkymapCatalog, skymap_uuid: Uuid) -> Option<&SkymapCatalogEntry> {
|
||||
let Some(entry) = catalog.entries.iter().find(|entry| entry.uuid == skymap_uuid) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(entry)
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
self.popup_remote_skymap_downloader.update(par)?;
|
||||
|
||||
for task in self.tasks.drain() {
|
||||
match task {
|
||||
Task::SetSkymapCatalog(skymap_catalog) => {
|
||||
par.layout.remove_widget(self.id_loading);
|
||||
match &*skymap_catalog {
|
||||
Ok(skymap_catalog) => {
|
||||
self.mount_catalog(par.layout, skymap_catalog)?;
|
||||
}
|
||||
Err(e) => wgui_simple::create_label_error(
|
||||
par.layout,
|
||||
self.id_parent,
|
||||
format!("Failed to fetch skymap catalog: {:?}", e),
|
||||
)?,
|
||||
}
|
||||
}
|
||||
Task::SetSkymapPreview((skymap_uuid, opt_preview_image)) => {
|
||||
if let Some(cell) = &mut self
|
||||
.mounted_cells
|
||||
.iter_mut()
|
||||
.find(|cell| cell.skymap_uuid == skymap_uuid)
|
||||
{
|
||||
if let Some((preview_image, preview_image_compressed)) = opt_preview_image {
|
||||
cell.view.set_image(par.layout, Some(preview_image))?;
|
||||
cell.preview_image_compressed = Some(preview_image_compressed);
|
||||
} else {
|
||||
cell.view.set_image(par.layout, None)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::ShowRemoteSkymapDownloader(skymap_uuid) => {
|
||||
if let Some((preview_image, preview_image_compressed)) = self.get_image_preview(skymap_uuid) {
|
||||
self.show_remote_skymap_downloader(skymap_uuid, preview_image, preview_image_compressed)?;
|
||||
} else {
|
||||
log::error!("preview image not present, ignoring request");
|
||||
}
|
||||
}
|
||||
Task::RefreshCells => {
|
||||
self.refresh_cells(par.layout)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
async fn skymap_catalog_request_wrapper(tasks: Tasks<Task>, executor: AsyncExecutor) {
|
||||
let res = networking::skymap_catalog::request_catalog(&executor).await;
|
||||
tasks.push(Task::SetSkymapCatalog(Rc::new(res)))
|
||||
}
|
||||
|
||||
pub fn new(par: Params) -> anyhow::Result<Self> {
|
||||
let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
|
||||
layout: par.layout,
|
||||
parent_id: par.parent_id,
|
||||
with_text: true,
|
||||
})?;
|
||||
let tasks = Tasks::<Task>::new();
|
||||
let fut = View::skymap_catalog_request_wrapper(tasks.clone(), par.executor.clone());
|
||||
par.executor.spawn(fut).detach();
|
||||
Ok(Self {
|
||||
id_parent: par.parent_id,
|
||||
id_loading,
|
||||
tasks,
|
||||
globals: par.globals.clone(),
|
||||
mounted_cells: Vec::new(),
|
||||
executor: par.executor.clone(),
|
||||
frontend_tasks: par.frontend_tasks,
|
||||
catalog: None,
|
||||
popup_remote_skymap_downloader: Default::default(),
|
||||
on_updated_library: par.on_updated_library,
|
||||
})
|
||||
}
|
||||
|
||||
async fn request_skymap_preview(
|
||||
globals: WguiGlobals,
|
||||
executor: AsyncExecutor,
|
||||
entry: SkymapCatalogEntry,
|
||||
tasks: Tasks<Task>,
|
||||
) {
|
||||
tasks.push(Task::SetSkymapPreview((
|
||||
entry.uuid,
|
||||
networking::image_fetch::fetch_to_glyph_data(&globals, &executor, &entry.files.get_url_preview())
|
||||
.await
|
||||
.ok(),
|
||||
)));
|
||||
}
|
||||
|
||||
fn refresh_cells(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
let Some(catalog) = &self.catalog else {
|
||||
debug_assert!(false);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for cell in &mut self.mounted_cells {
|
||||
if let Some(entry) = get_entry_by_uuid(&catalog, cell.skymap_uuid) {
|
||||
cell.view.refresh_resolution_pips(layout, entry)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_catalog(
|
||||
&mut self,
|
||||
layout: &mut Layout,
|
||||
catalog: &networking::skymap_catalog::SkymapCatalog,
|
||||
) -> anyhow::Result<()> {
|
||||
let doc_params = &ParseDocumentParams {
|
||||
globals: self.globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/remote_skymap_list.xml"),
|
||||
extra: Default::default(),
|
||||
};
|
||||
|
||||
let parser_state = wgui::parser::parse_from_assets(&doc_params, layout, self.id_parent)?;
|
||||
|
||||
let id_list = parser_state.fetch_widget(&layout.state, "list")?.id;
|
||||
|
||||
for entry in &catalog.entries {
|
||||
let task = View::request_skymap_preview(
|
||||
self.globals.clone(),
|
||||
self.executor.clone(),
|
||||
entry.clone(),
|
||||
self.tasks.clone(),
|
||||
);
|
||||
|
||||
let skymap_uuid = entry.uuid.clone();
|
||||
|
||||
self.mounted_cells.push(MountedCell {
|
||||
preview_image_compressed: None,
|
||||
skymap_uuid: entry.uuid.clone(),
|
||||
view: views::skymap_list_cell::View::new(views::skymap_list_cell::Params {
|
||||
id_parent: id_list,
|
||||
layout,
|
||||
entry: Some(entry.clone()),
|
||||
on_click: self
|
||||
.tasks
|
||||
.get_button_click_callback(Task::ShowRemoteSkymapDownloader(skymap_uuid)),
|
||||
})?,
|
||||
});
|
||||
|
||||
self.executor.spawn(task).detach();
|
||||
}
|
||||
|
||||
self.catalog = Some(catalog.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_remote_skymap_downloader(
|
||||
&mut self,
|
||||
uuid: SkymapUuid,
|
||||
preview_image: CustomGlyphData,
|
||||
preview_image_compressed: Rc<Vec<u8>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(catalog) = &self.catalog else {
|
||||
debug_assert!(false); // impossible
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(entry) = get_entry_by_uuid(&catalog, uuid) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// call our task before calling underlying on_updated_library callback
|
||||
let on_updated_library = Rc::new({
|
||||
let func = self.on_updated_library.clone();
|
||||
let tasks = self.tasks.clone();
|
||||
move || {
|
||||
tasks.push(Task::RefreshCells);
|
||||
(*func)();
|
||||
}
|
||||
});
|
||||
|
||||
views::remote_skymap_downloader::mount_popup(
|
||||
self.frontend_tasks.clone(),
|
||||
self.executor.clone(),
|
||||
self.globals.clone(),
|
||||
entry.clone(),
|
||||
preview_image,
|
||||
preview_image_compressed,
|
||||
on_updated_library,
|
||||
self.popup_remote_skymap_downloader.clone(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_image_preview(
|
||||
&self,
|
||||
skymap_uuid: SkymapUuid,
|
||||
) -> Option<(CustomGlyphData, Rc<Vec<u8>> /* preview_image_compressed */)> {
|
||||
if let Some(cell) = &self.mounted_cells.iter().find(|mc| mc.skymap_uuid == skymap_uuid) {
|
||||
let Some(image) = cell.view.get_image() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Some(preview_image_compressed) = cell.preview_image_compressed.clone() else {
|
||||
return None;
|
||||
};
|
||||
return Some((image, preview_image_compressed));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(
|
||||
frontend_tasks: FrontendTasks,
|
||||
executor: AsyncExecutor,
|
||||
globals: WguiGlobals,
|
||||
on_updated_library: Rc<dyn Fn()>,
|
||||
popup: PopupHolder<View>,
|
||||
) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_translation_key("APP_SETTINGS.BROWSE_ONLINE_CATALOG"),
|
||||
Box::new(move |data| {
|
||||
let view = View::new(Params {
|
||||
globals: &globals,
|
||||
layout: data.layout,
|
||||
executor: &executor,
|
||||
parent_id: data.id_content,
|
||||
frontend_tasks,
|
||||
on_updated_library,
|
||||
})?;
|
||||
|
||||
popup.set_view(data.handle, view, None);
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
use anyhow::Context;
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
renderer_vk::text::custom_glyph::CustomGlyphData,
|
||||
task::Tasks,
|
||||
};
|
||||
use wlx_common::{async_executor::AsyncExecutor, config::GeneralConfig, config_io, dash_interface::ConfigChangeKind};
|
||||
|
||||
use crate::{
|
||||
frontend::{FrontendTask, FrontendTasks},
|
||||
util::{
|
||||
networking::skymap_catalog::{self, SkymapCatalogEntry, SkymapResolution},
|
||||
popup_manager::{MountPopupOnceParams, PopupHolder},
|
||||
},
|
||||
views::{self, ViewTrait, ViewUpdateParams},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Task {
|
||||
DownloadSkymaps,
|
||||
Refresh,
|
||||
ShowSkymapResolutionSelector(SkymapCatalogEntry),
|
||||
SetSkymap(SkymapCatalogEntry, SkymapResolution),
|
||||
SetSkymapBuiltIn,
|
||||
}
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub globals: WguiGlobals,
|
||||
pub layout: &'a mut Layout,
|
||||
pub parent_id: WidgetID,
|
||||
pub frontend_tasks: &'a FrontendTasks,
|
||||
}
|
||||
|
||||
struct Cell {
|
||||
#[allow(dead_code)]
|
||||
view: views::skymap_list_cell::View,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
parser_state: ParserState,
|
||||
tasks: Tasks<Task>,
|
||||
list_parent: WidgetID,
|
||||
frontend_tasks: FrontendTasks,
|
||||
globals: WguiGlobals,
|
||||
popup_remote_skymap_list: PopupHolder<views::remote_skymap_list::View>,
|
||||
popup_dialog_box: PopupHolder<views::dialog_box::View>,
|
||||
cells: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl ViewTrait for View {
|
||||
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()> {
|
||||
self.popup_remote_skymap_list.update(par)?;
|
||||
self.popup_dialog_box.update(par)?;
|
||||
|
||||
loop {
|
||||
let tasks = self.tasks.drain();
|
||||
if tasks.is_empty() {
|
||||
break;
|
||||
}
|
||||
for task in tasks {
|
||||
match task {
|
||||
Task::DownloadSkymaps => {
|
||||
self.download_skymaps(&par.executor)?;
|
||||
}
|
||||
Task::Refresh => {
|
||||
self.refresh(&mut par.layout)?;
|
||||
}
|
||||
Task::ShowSkymapResolutionSelector(entry) => {
|
||||
self.show_skymap_resolution_selector(entry);
|
||||
}
|
||||
Task::SetSkymap(entry, resolution) => {
|
||||
self.set_skymap(entry, resolution, par.general_config, par.config_change_kind)?;
|
||||
}
|
||||
Task::SetSkymapBuiltIn => {
|
||||
par.general_config.skybox_texture = "".into();
|
||||
*par.config_change_kind = Some(ConfigChangeKind::EnvironmentBlend);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(params: Params) -> anyhow::Result<Self> {
|
||||
let doc_params = &ParseDocumentParams {
|
||||
globals: params.globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/skymap_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")?.id;
|
||||
let tasks = Tasks::new();
|
||||
|
||||
tasks.push(Task::Refresh);
|
||||
|
||||
tasks.handle_button(
|
||||
&parser_state.fetch_component_as::<ComponentButton>("btn_download_skymaps")?,
|
||||
Task::DownloadSkymaps,
|
||||
);
|
||||
|
||||
tasks.handle_button(
|
||||
&parser_state.fetch_component_as::<ComponentButton>("btn_refresh")?,
|
||||
Task::Refresh,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
parser_state,
|
||||
tasks,
|
||||
list_parent,
|
||||
frontend_tasks: params.frontend_tasks.clone(),
|
||||
globals: params.globals.clone(),
|
||||
popup_remote_skymap_list: Default::default(),
|
||||
popup_dialog_box: Default::default(),
|
||||
cells: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn download_skymaps(&mut self, executor: &AsyncExecutor) -> anyhow::Result<()> {
|
||||
views::remote_skymap_list::mount_popup(
|
||||
self.frontend_tasks.clone(),
|
||||
executor.clone(),
|
||||
self.globals.clone(),
|
||||
self.tasks.make_callback_rc(Task::Refresh), /* on_updated_library */
|
||||
self.popup_remote_skymap_list.clone(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_skymap(
|
||||
&mut self,
|
||||
entry: SkymapCatalogEntry,
|
||||
resolution: SkymapResolution,
|
||||
config: &mut GeneralConfig,
|
||||
change_kind: &mut Option<ConfigChangeKind>,
|
||||
) -> anyhow::Result<()> {
|
||||
let skymap_file_path = entry
|
||||
.get_destination_path(resolution)
|
||||
.context("Skymap not found" /* you shouldn't really see this, like ever. */)?;
|
||||
|
||||
config.skybox_texture = config_io::get_skymaps_root()
|
||||
.join(skymap_file_path)
|
||||
.to_str()
|
||||
.context("Skymap filename not valid UTF-8")?
|
||||
.into();
|
||||
*change_kind = Some(ConfigChangeKind::EnvironmentBlend);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_skymap_resolution_selector(&mut self, entry: SkymapCatalogEntry) {
|
||||
let tasks = self.tasks.clone();
|
||||
|
||||
let mut entries = Vec::<views::dialog_box::ButtonEntry>::new();
|
||||
|
||||
let mut add_entry_resolution = |resolution: SkymapResolution| {
|
||||
if entry.is_downloaded(resolution).unwrap_or(false) {
|
||||
entries.push(views::dialog_box::ButtonEntry {
|
||||
content: Translation::from_raw_text(resolution.get_display_str()),
|
||||
icon: "dashboard/globe.svg",
|
||||
action: resolution.get_display_str_simple(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
add_entry_resolution(SkymapResolution::Res2k);
|
||||
add_entry_resolution(SkymapResolution::Res4k);
|
||||
add_entry_resolution(SkymapResolution::Res8k);
|
||||
|
||||
if entries.is_empty() {
|
||||
// if the skymap is not downloaded, how did we get there?!
|
||||
debug_assert!(false);
|
||||
return;
|
||||
}
|
||||
|
||||
views::dialog_box::mount_popup(
|
||||
self.popup_dialog_box.clone(),
|
||||
self.frontend_tasks.clone(),
|
||||
views::dialog_box::Params {
|
||||
globals: self.globals.clone(),
|
||||
entries,
|
||||
message: Translation::from_translation_key("APP_SETTINGS.SELECT_VARIANT"),
|
||||
on_action_click: Box::new(move |action| {
|
||||
let resolution = SkymapResolution::from_display_str_simple(action).unwrap(); // safety: never fails.
|
||||
tasks.push(Task::SetSkymap(entry, resolution))
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh(&mut self, layout: &mut Layout) -> anyhow::Result<()> {
|
||||
let entries = match skymap_catalog::get_entries_from_disk() {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
log::error!("failed to get skymap entries: {}", e);
|
||||
Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
layout.remove_children(self.list_parent);
|
||||
self.cells.clear();
|
||||
|
||||
let skymaps_root = config_io::get_skymaps_root();
|
||||
|
||||
let mut view = views::skymap_list_cell::View::new(views::skymap_list_cell::Params {
|
||||
id_parent: self.list_parent,
|
||||
layout,
|
||||
entry: None,
|
||||
on_click: self.tasks.get_button_click_callback(Task::SetSkymapBuiltIn),
|
||||
})?;
|
||||
// load preview image
|
||||
let data = include_bytes!("../../assets/dashboard/builtin-skybox-preview.jpg");
|
||||
if let Ok(glyph_data) = CustomGlyphData::from_bytes_raster(&self.globals, "builtin-skybox-preview.jpg", data) {
|
||||
view.set_image(layout, Some(glyph_data))?;
|
||||
}
|
||||
|
||||
self.cells.push(Cell { view });
|
||||
|
||||
for entry in &entries {
|
||||
let mut view = views::skymap_list_cell::View::new(views::skymap_list_cell::Params {
|
||||
id_parent: self.list_parent,
|
||||
layout,
|
||||
entry: Some(entry.clone()),
|
||||
on_click: self
|
||||
.tasks
|
||||
.get_button_click_callback(Task::ShowSkymapResolutionSelector(entry.clone())),
|
||||
})?;
|
||||
|
||||
// load preview image
|
||||
if let Ok(data) = std::fs::read(skymaps_root.join(&entry.files.preview)) {
|
||||
if let Ok(glyph_data) = CustomGlyphData::from_bytes_raster(&self.globals, &entry.files.preview, &data) {
|
||||
view.set_image(layout, Some(glyph_data))?;
|
||||
}
|
||||
}
|
||||
|
||||
self.cells.push(Cell { view });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: PopupHolder<View>) {
|
||||
frontend_tasks
|
||||
.clone()
|
||||
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
|
||||
Translation::from_translation_key("APP_SETTINGS.BROWSE_SKYMAPS"),
|
||||
Box::new(move |data| {
|
||||
let view = View::new(Params {
|
||||
globals: globals.clone(),
|
||||
layout: data.layout,
|
||||
parent_id: data.id_content,
|
||||
frontend_tasks: &frontend_tasks,
|
||||
})?;
|
||||
|
||||
popup.set_view(data.handle, view, None);
|
||||
Ok(popup.get_close_callback(data.layout))
|
||||
}),
|
||||
)));
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::{ButtonClickCallback, ComponentButton},
|
||||
event::EventAlterables,
|
||||
globals::WguiGlobals,
|
||||
i18n::Translation,
|
||||
layout::{Layout, WidgetID},
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
renderer_vk::text::custom_glyph::CustomGlyphData,
|
||||
widget::{image::WidgetImage, label::WidgetLabel},
|
||||
};
|
||||
|
||||
use crate::util::{
|
||||
networking::{
|
||||
self,
|
||||
skymap_catalog::{SkymapCatalogEntry, SkymapResolution},
|
||||
},
|
||||
wgui_simple,
|
||||
};
|
||||
|
||||
pub struct Params<'a> {
|
||||
pub id_parent: WidgetID,
|
||||
pub layout: &'a mut Layout,
|
||||
pub entry: Option<networking::skymap_catalog::SkymapCatalogEntry>,
|
||||
pub on_click: ButtonClickCallback,
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
#[allow(dead_code)]
|
||||
parser_state: ParserState,
|
||||
id_loading: WidgetID,
|
||||
id_image_preview: WidgetID,
|
||||
id_resolution_pips: WidgetID,
|
||||
image: Option<CustomGlyphData>,
|
||||
}
|
||||
|
||||
fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
|
||||
ParseDocumentParams {
|
||||
globals: globals.clone(),
|
||||
path: AssetPath::BuiltIn("gui/view/skymap_list_cell.xml"),
|
||||
extra: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_res_pips(
|
||||
layout: &mut Layout,
|
||||
id_parent: WidgetID,
|
||||
parser_state: &mut ParserState,
|
||||
entry: &SkymapCatalogEntry,
|
||||
) -> anyhow::Result<()> {
|
||||
let globals = layout.state.globals.clone();
|
||||
layout.remove_children(id_parent);
|
||||
|
||||
let mut populate_res_pip = |res: SkymapResolution| -> anyhow::Result<()> {
|
||||
let mut tpar = HashMap::<Rc<str>, Rc<str>>::new();
|
||||
let downloaded = entry.is_downloaded(res).unwrap_or(false);
|
||||
tpar.insert(
|
||||
Rc::from("color"),
|
||||
if downloaded {
|
||||
Rc::from("#11aa40")
|
||||
} else {
|
||||
Rc::from("#444444")
|
||||
},
|
||||
);
|
||||
tpar.insert(Rc::from("text"), res.get_display_str_simple().into());
|
||||
parser_state.realize_template(&doc_params(&globals), "ResolutionPip", layout, id_parent, tpar)?;
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
populate_res_pip(SkymapResolution::Res2k)?;
|
||||
if entry.files.size_4k.is_some() {
|
||||
populate_res_pip(SkymapResolution::Res4k)?;
|
||||
}
|
||||
if entry.files.size_8k.is_some() {
|
||||
populate_res_pip(SkymapResolution::Res8k)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn new(par: Params) -> anyhow::Result<Self> {
|
||||
let globals = par.layout.state.globals.clone();
|
||||
|
||||
let mut parser_state = wgui::parser::parse_from_assets(&doc_params(&globals), par.layout, par.id_parent)?;
|
||||
|
||||
let data = parser_state.realize_template(
|
||||
&doc_params(&globals),
|
||||
"Cell",
|
||||
par.layout,
|
||||
par.id_parent,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
let id_image_preview = data.get_widget_id("image_preview")?;
|
||||
let id_resolution_pips = data.get_widget_id("resolution_pips")?;
|
||||
|
||||
data
|
||||
.fetch_component_as::<ComponentButton>("button")?
|
||||
.on_click(par.on_click);
|
||||
|
||||
{
|
||||
let mut label_title = data.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_title")?;
|
||||
let mut label_author = data.fetch_widget_as::<WidgetLabel>(&par.layout.state, "label_author")?;
|
||||
|
||||
label_title.set_text_simple(
|
||||
&mut globals.get(),
|
||||
Translation::from_raw_text_string(
|
||||
par
|
||||
.entry
|
||||
.as_ref()
|
||||
.map(|e| e.name.clone())
|
||||
.unwrap_or_else(|| "Built-in Sky Shader".into()),
|
||||
),
|
||||
);
|
||||
label_author.set_text_simple(
|
||||
&mut globals.get(),
|
||||
Translation::from_raw_text_string(format!(
|
||||
"by {}",
|
||||
par.entry.as_ref().map(|e| e.author.as_str()).unwrap_or("WayVR Team")
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
|
||||
layout: par.layout,
|
||||
parent_id: id_image_preview,
|
||||
with_text: false,
|
||||
})?;
|
||||
|
||||
// Populate resolution pips
|
||||
if let Some(entry) = par.entry.as_ref() {
|
||||
populate_res_pips(par.layout, id_resolution_pips, &mut parser_state, entry)?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
parser_state,
|
||||
id_loading,
|
||||
id_image_preview,
|
||||
image: None,
|
||||
id_resolution_pips,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refresh_resolution_pips(&mut self, layout: &mut Layout, entry: &SkymapCatalogEntry) -> anyhow::Result<()> {
|
||||
populate_res_pips(layout, self.id_resolution_pips, &mut self.parser_state, &entry)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_image(&mut self, layout: &mut Layout, content: Option<CustomGlyphData>) -> anyhow::Result<()> {
|
||||
layout.remove_widget(self.id_loading);
|
||||
let mut alt = EventAlterables::default();
|
||||
{
|
||||
let mut image_preview = layout.state.widgets.cast_as::<WidgetImage>(self.id_image_preview)?;
|
||||
image_preview.set_content(&mut alt, content.clone());
|
||||
}
|
||||
layout.process_alterables(alt)?;
|
||||
self.image = content;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_image(&self) -> Option<CustomGlyphData> {
|
||||
return self.image.clone();
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +93,11 @@
|
|||
<CheckBox text="and me too!" />
|
||||
<CheckBox text="i'm checked by default" checked="1" />
|
||||
</rectangle>
|
||||
|
||||
<rectangle macro="rect" flex_direction="row" gap="16">
|
||||
<label text="range slider test" weight="bold" />
|
||||
<Slider value="0.5" value2="1.0" min_value="-5.0" max_value="20.0" width="200" height="24"/>
|
||||
</rectangle>
|
||||
|
||||
<rectangle macro="rect" flex_direction="row" align_items="start">
|
||||
<div flex_direction="column" gap="8">
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
&wgui::event::Event::MouseWheel(MouseWheelEvent {
|
||||
delta: Vec2::new(x, y),
|
||||
pos: mouse / scale,
|
||||
device: 0,
|
||||
device: wgui::event::DeviceBitmask(0),
|
||||
}),
|
||||
&mut (),
|
||||
&mut (),
|
||||
|
|
@ -157,7 +157,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
&wgui::event::Event::MouseWheel(MouseWheelEvent {
|
||||
delta: Vec2::new(pos.x as f32 / 5.0, pos.y as f32 / 5.0),
|
||||
pos: mouse / scale,
|
||||
device: 0,
|
||||
device: wgui::event::DeviceBitmask(0),
|
||||
}),
|
||||
&mut (),
|
||||
&mut (),
|
||||
|
|
@ -177,7 +177,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
&wgui::event::Event::MouseDown(MouseButtonEvent {
|
||||
pos: mouse / scale,
|
||||
index: MouseButtonIndex::Left,
|
||||
device: 0,
|
||||
device: wgui::event::DeviceBitmask(0),
|
||||
}),
|
||||
&mut (),
|
||||
&mut (),
|
||||
|
|
@ -190,7 +190,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
&wgui::event::Event::MouseUp(MouseButtonEvent {
|
||||
pos: mouse / scale,
|
||||
index: MouseButtonIndex::Left,
|
||||
device: 0,
|
||||
device: wgui::event::DeviceBitmask(0),
|
||||
}),
|
||||
&mut (),
|
||||
&mut (),
|
||||
|
|
@ -209,7 +209,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
.push_event(
|
||||
&wgui::event::Event::MouseMotion(MouseMotionEvent {
|
||||
pos: mouse / scale,
|
||||
device: 0,
|
||||
device: wgui::event::DeviceBitmask(0),
|
||||
}),
|
||||
&mut (),
|
||||
&mut (),
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ pub struct TestbedGeneric {
|
|||
pub parser_state: ParserState,
|
||||
tasks: Tasks<TestbedTask>,
|
||||
|
||||
globals: WguiGlobals,
|
||||
data: Rc<RefCell<Data>>,
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +182,6 @@ impl TestbedGeneric {
|
|||
layout,
|
||||
parser_state,
|
||||
tasks: Default::default(),
|
||||
globals: globals.clone(),
|
||||
data: Rc::new(RefCell::new(Data {
|
||||
popup_window: WguiWindow::default(),
|
||||
context_menu: context_menu::ContextMenu::default(),
|
||||
|
|
|
|||
|
|
@ -83,15 +83,13 @@ sysinfo = { version = "0.37" }
|
|||
thiserror = "2.0"
|
||||
tracing = "0.1.43"
|
||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
uuid = { version = "1.19.0", features = ["fast-rng", "v4"] }
|
||||
uuid = { workspace = true }
|
||||
wayland-client = { workspace = true }
|
||||
winit = { version = "0.30.12", optional = true }
|
||||
xcb = { version = "1.6.0", features = [
|
||||
"as-raw-xcb-connection",
|
||||
], optional = true }
|
||||
xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros
|
||||
arboard = { version="3.6.1", features = ["wayland-data-control", "wl-clipboard-rs"] }
|
||||
super-swipe-type = "0.2.2"
|
||||
|
||||
[build-dependencies]
|
||||
regex.workspace = true
|
||||
|
|
|
|||
|
|
@ -13,11 +13,6 @@
|
|||
width="${width}" height="${height}" min_width="${width}" min_height="${height}" max_width="${width}" max_height="${height}"
|
||||
/>
|
||||
|
||||
<macro name="prediction_rect"
|
||||
margin="2" padding="8 16" overflow="visible" box_sizing="border_box"
|
||||
border_color="~color_accent_translucent" border="2" round="6" color="~color_accent_40" color2="~color_accent_10" gradient="vertical"
|
||||
align_items="center" justify_content="center" />
|
||||
|
||||
<template name="VerticalSeparator">
|
||||
<rectangle width="2" height="100%" color="~color_accent" />
|
||||
</template>
|
||||
|
|
@ -95,13 +90,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Word prediction key for swipe typing -->
|
||||
<template name="KeyPrediction">
|
||||
<rectangle id="${id}" macro="prediction_rect" height="60" min_height="60" flex_grow="1" flex_shrink="1" flex_basis="0">
|
||||
<label text="${text}" size="16" flex_wrap="wrap" />
|
||||
</rectangle>
|
||||
</template>
|
||||
|
||||
<macro name="button_style" border="2" border_color="~color_accent_translucent" color="~color_bg" round="6"
|
||||
align_items="center" justify_content="center" padding="6" width="60" height="60" overflow="visible"/>
|
||||
|
||||
|
|
@ -243,10 +231,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</rectangle>
|
||||
<div width="100%" height="11" interactable="0" />
|
||||
<rectangle id="swipe_predictions_root" macro="bg_rect" padding="10" align_items="center" justify_content="space_between" flex_direction="row" min_height="60">
|
||||
</rectangle>
|
||||
<div width="100%" height="11" interactable="0" />
|
||||
<div width="100%" height="13" interactable="0" />
|
||||
<rectangle id="keyboard_root" macro="bg_rect" flex_direction="column" padding="10">
|
||||
</rectangle>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ pub fn openvr_uninstall() {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
pub fn openvr_run(show_by_default: bool, headless: bool) -> Result<(), BackendError> {
|
||||
pub fn openvr_run(
|
||||
show_by_default: bool,
|
||||
headless: bool,
|
||||
no_autostart: bool,
|
||||
) -> Result<(), BackendError> {
|
||||
let app_type = EVRApplicationType::VRApplication_Overlay;
|
||||
let Ok(context) = ovr_overlay::Context::init(app_type) else {
|
||||
log::warn!("Will not use OpenVR: Context init failed");
|
||||
|
|
@ -91,6 +95,8 @@ pub fn openvr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
|||
AppState::from_graphics(gfx, gfx_extras, XrBackend::OpenVR)?
|
||||
};
|
||||
|
||||
app.session.no_autostart = no_autostart;
|
||||
|
||||
if show_by_default {
|
||||
app.tasks.enqueue_at(
|
||||
TaskType::Overlay(OverlayTask::ShowHide),
|
||||
|
|
|
|||
|
|
@ -212,9 +212,10 @@ pub(super) fn posef_to_transform(pose: &xr::Posef) -> Affine3A {
|
|||
Affine3A::from_rotation_translation(rotation, translation)
|
||||
}
|
||||
|
||||
pub(super) fn reconfigure_chroma_key(app: &AppState) {
|
||||
pub(super) fn try_apply_chroma_key(app: &AppState) -> bool {
|
||||
let Some(monado) = app.monado_state.as_ref() else {
|
||||
return;
|
||||
log::warn!("Could not set Chroma Key: no monado_state");
|
||||
return false;
|
||||
};
|
||||
|
||||
let params = &app.session.config.chroma_key_params;
|
||||
|
|
@ -226,6 +227,9 @@ pub(super) fn reconfigure_chroma_key(app: &AppState) {
|
|||
params.curve,
|
||||
params.despill,
|
||||
) {
|
||||
log::warn!("Could not set Chroma Key: {e:?}")
|
||||
log::warn!("Could not set Chroma Key: {e:?}");
|
||||
return false;
|
||||
}
|
||||
|
||||
params.curve > 0.001
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use crate::{
|
|||
backend::{
|
||||
BackendError, XrBackend,
|
||||
input::interact,
|
||||
openxr::{helpers::reconfigure_chroma_key, lines::LinePool, overlay::OpenXrOverlayData},
|
||||
openxr::{helpers::try_apply_chroma_key, lines::LinePool, overlay::OpenXrOverlayData},
|
||||
task::{OpenXrTask, OverlayTask, TaskType},
|
||||
},
|
||||
config::{save_settings, save_state},
|
||||
|
|
@ -53,7 +53,11 @@ struct XrState {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
|
||||
pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendError> {
|
||||
pub fn openxr_run(
|
||||
show_by_default: bool,
|
||||
headless: bool,
|
||||
no_autostart: bool,
|
||||
) -> Result<(), BackendError> {
|
||||
let (xr_instance, system) = match helpers::init_xr() {
|
||||
Ok((xr_instance, system)) => (xr_instance, system),
|
||||
Err(e) => {
|
||||
|
|
@ -67,6 +71,8 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
|||
AppState::from_graphics(gfx, gfx_extras, XrBackend::OpenXR)?
|
||||
};
|
||||
|
||||
app.session.no_autostart = no_autostart;
|
||||
|
||||
let modes = xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?;
|
||||
|
||||
if show_by_default {
|
||||
|
|
@ -94,8 +100,6 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
|||
.ok()
|
||||
});
|
||||
|
||||
reconfigure_chroma_key(&app);
|
||||
|
||||
let mut blocker = app
|
||||
.monado_state
|
||||
.as_ref()
|
||||
|
|
@ -482,7 +486,7 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
|||
}
|
||||
}
|
||||
TaskType::OpenXR(task) => {
|
||||
if matches!(task, OpenXrTask::SettingsChanged) {
|
||||
if matches!(task, OpenXrTask::EnvironmentChanged) {
|
||||
reconfigure_environment_blend(
|
||||
&app,
|
||||
&xr_state,
|
||||
|
|
@ -491,7 +495,6 @@ pub fn openxr_run(show_by_default: bool, headless: bool) -> Result<(), BackendEr
|
|||
&mut environment_blend_mode,
|
||||
main_session_visible,
|
||||
);
|
||||
reconfigure_chroma_key(&app);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "openvr")]
|
||||
|
|
@ -532,6 +535,9 @@ pub(super) enum CompositionLayer<'a> {
|
|||
Equirect2(xr::CompositionLayerEquirect2KHR<'a, xr::Vulkan>),
|
||||
}
|
||||
|
||||
// applies chroma key settings.
|
||||
// if chroma key or passthrough is enabled, use alpha env blend
|
||||
// if alpha blend is not used and skybox is enabled, use skybox
|
||||
fn reconfigure_environment_blend(
|
||||
app: &AppState,
|
||||
xr_state: &XrState,
|
||||
|
|
@ -540,9 +546,11 @@ fn reconfigure_environment_blend(
|
|||
environment_blend_mode: &mut xr::EnvironmentBlendMode,
|
||||
main_session_visible: bool,
|
||||
) {
|
||||
let has_chroma_key = try_apply_chroma_key(app);
|
||||
|
||||
*environment_blend_mode = {
|
||||
if modes.contains(&xr::EnvironmentBlendMode::ALPHA_BLEND)
|
||||
&& app.session.config.use_passthrough
|
||||
&& (app.session.config.use_passthrough || has_chroma_key)
|
||||
{
|
||||
xr::EnvironmentBlendMode::ALPHA_BLEND
|
||||
} else {
|
||||
|
|
@ -554,13 +562,17 @@ fn reconfigure_environment_blend(
|
|||
&& app.session.config.use_skybox
|
||||
&& !main_session_visible;
|
||||
|
||||
if want_skybox == skybox.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if want_skybox {
|
||||
log::debug!("Allocating skybox.");
|
||||
*skybox = create_skybox(xr_state, app);
|
||||
if let Some(curr_skybox) = skybox.as_ref() {
|
||||
if curr_skybox.needs_recreate(app) {
|
||||
*skybox = None;
|
||||
log::debug!("Allocating skybox.");
|
||||
*skybox = create_skybox(xr_state, app);
|
||||
}
|
||||
} else {
|
||||
log::debug!("Allocating skybox.");
|
||||
*skybox = create_skybox(xr_state, app);
|
||||
}
|
||||
} else {
|
||||
log::debug!("Destroying skybox.");
|
||||
*skybox = None;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ impl MonadoState {
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn update(&mut self) {
|
||||
#[cfg(feature = "feat-monado-metrics")]
|
||||
if let Some(metrics) = &mut self.metrics {
|
||||
|
|
@ -30,6 +32,10 @@ impl MonadoState {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
#[allow(clippy::unused_self)]
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
#[cfg(feature = "feat-monado-metrics")]
|
||||
pub fn set_metrics_enabled(&mut self, enabled: bool) -> anyhow::Result<()> {
|
||||
#[cfg(feature = "feat-monado-metrics")]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,11 +24,12 @@ use super::{
|
|||
};
|
||||
|
||||
pub(super) struct Skybox {
|
||||
view: Arc<ImageView>,
|
||||
view: Option<Arc<ImageView>>,
|
||||
sky: Option<WlxSwapchain>,
|
||||
grid: Option<WlxSwapchain>,
|
||||
grid_pose: xr::Posef,
|
||||
grid_color_scale_bias_khr: Option<Box<xr::sys::CompositionLayerColorScaleBiasKHR>>,
|
||||
current_skybox: Arc<str>,
|
||||
}
|
||||
|
||||
impl Skybox {
|
||||
|
|
@ -66,14 +67,18 @@ impl Skybox {
|
|||
}
|
||||
}
|
||||
|
||||
if maybe_image.is_none() {
|
||||
let p = include_bytes!("../../res/table_mountain_2.dds");
|
||||
maybe_image = Some(command_buffer.upload_image_dds(p.as_slice())?);
|
||||
}
|
||||
let current_skybox = if maybe_image.is_some() {
|
||||
app.session.config.skybox_texture.clone()
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
command_buffer.build_and_execute_now()?;
|
||||
|
||||
let view = ImageView::new_default(maybe_image.unwrap())?; // safe unwrap
|
||||
let view = if let Some(image) = maybe_image {
|
||||
command_buffer.build_and_execute_now()?;
|
||||
Some(ImageView::new_default(image)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let grid_color_scale_bias_khr = xr
|
||||
.instance
|
||||
|
|
@ -99,9 +104,14 @@ impl Skybox {
|
|||
grid: None,
|
||||
grid_pose: translation_rotation_to_posef(Vec3A::ZERO, Quat::from_rotation_x(PI * -0.5)),
|
||||
grid_color_scale_bias_khr,
|
||||
current_skybox,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn needs_recreate(&self, app: &AppState) -> bool {
|
||||
*self.current_skybox != *app.session.config.skybox_texture
|
||||
}
|
||||
|
||||
fn prepare_sky<'a>(
|
||||
&'a mut self,
|
||||
xr: &'a XrState,
|
||||
|
|
@ -113,7 +123,12 @@ impl Skybox {
|
|||
}
|
||||
let opts = SwapchainOpts::new().immutable();
|
||||
|
||||
let extent = self.view.extent_u32arr();
|
||||
let extent = self
|
||||
.view
|
||||
.as_ref()
|
||||
.map(|v| v.extent_u32arr())
|
||||
.unwrap_or([4096, 4096]);
|
||||
|
||||
let mut swapchain = create_swapchain(xr, app.gfx.clone(), extent, 1, opts)?;
|
||||
let tgt = swapchain
|
||||
.acquire_wait_image()?
|
||||
|
|
@ -121,23 +136,42 @@ impl Skybox {
|
|||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
let pipeline = app.gfx.create_pipeline(
|
||||
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
|
||||
app.gfx_extras.shaders.get("frag_srgb").unwrap(), // want panic
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format),
|
||||
)?;
|
||||
|
||||
let set0 = pipeline.uniform_sampler(0, self.view.clone(), app.gfx.texture_filter)?;
|
||||
let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?;
|
||||
let pass = pipeline.create_pass(
|
||||
tgt.extent_f32(),
|
||||
[0.0, 0.0],
|
||||
app.gfx_extras.quad_verts.clone(),
|
||||
0..4,
|
||||
0..1,
|
||||
vec![set0, set1],
|
||||
&Default::default(),
|
||||
)?;
|
||||
let pass = if let Some(view) = self.view.as_ref() {
|
||||
let pipeline = app.gfx.create_pipeline(
|
||||
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
|
||||
app.gfx_extras.shaders.get("frag_srgb").unwrap(), // want panic
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format),
|
||||
)?;
|
||||
|
||||
let set0 = pipeline.uniform_sampler(0, view.clone(), app.gfx.texture_filter)?;
|
||||
let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?;
|
||||
pipeline.create_pass(
|
||||
tgt.extent_f32(),
|
||||
[0.0, 0.0],
|
||||
app.gfx_extras.quad_verts.clone(),
|
||||
0..4,
|
||||
0..1,
|
||||
vec![set0, set1],
|
||||
&Default::default(),
|
||||
)?
|
||||
} else {
|
||||
let pipeline = app.gfx.create_pipeline(
|
||||
app.gfx_extras.shaders.get("vert_quad").unwrap(), // want panic
|
||||
app.gfx_extras.shaders.get("frag_sky").unwrap(), // want panic
|
||||
WPipelineCreateInfo::new(app.gfx.surface_format),
|
||||
)?;
|
||||
|
||||
pipeline.create_pass(
|
||||
tgt.extent_f32(),
|
||||
[0.0, 0.0],
|
||||
app.gfx_extras.quad_verts.clone(),
|
||||
0..4,
|
||||
0..1,
|
||||
vec![],
|
||||
&Default::default(),
|
||||
)?
|
||||
};
|
||||
|
||||
let mut cmd_buffer = app
|
||||
.gfx
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ pub enum OpenVrTask {
|
|||
|
||||
#[cfg(feature = "openxr")]
|
||||
pub enum OpenXrTask {
|
||||
SettingsChanged,
|
||||
EnvironmentChanged,
|
||||
}
|
||||
|
||||
pub enum PlayspaceTask {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
use std::{io::Read, os::unix::net::UnixStream, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use smithay::input::Seat;
|
||||
use smithay::wayland::selection::data_device::set_data_device_selection;
|
||||
use smithay::{
|
||||
backend::input::Keycode,
|
||||
input::{keyboard::KeyboardHandle, pointer::PointerHandle},
|
||||
|
|
@ -11,13 +9,13 @@ use smithay::{
|
|||
};
|
||||
use xkbcommon::xkb;
|
||||
|
||||
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
|
||||
|
||||
use super::{
|
||||
ProcessWayVREnv,
|
||||
comp::{self, ClientState},
|
||||
process,
|
||||
};
|
||||
use crate::backend::wayvr::comp::Application;
|
||||
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
|
||||
|
||||
pub struct WayVRClient {
|
||||
pub client: wayland_server::Client,
|
||||
|
|
@ -28,7 +26,6 @@ pub struct WayVRCompositor {
|
|||
pub state: comp::Application,
|
||||
pub seat_keyboard: KeyboardHandle<comp::Application>,
|
||||
pub seat_pointer: PointerHandle<comp::Application>,
|
||||
pub seat: Seat<comp::Application>,
|
||||
pub serial_counter: SerialCounter,
|
||||
pub wayland_env: super::WaylandEnv,
|
||||
|
||||
|
|
@ -71,7 +68,6 @@ impl WayVRCompositor {
|
|||
display: wayland_server::Display<comp::Application>,
|
||||
seat_keyboard: KeyboardHandle<comp::Application>,
|
||||
seat_pointer: PointerHandle<comp::Application>,
|
||||
seat: Seat<comp::Application>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let (wayland_env, listener) = create_wayland_listener()?;
|
||||
|
||||
|
|
@ -85,7 +81,6 @@ impl WayVRCompositor {
|
|||
serial_counter: SerialCounter::new(),
|
||||
clients: Vec::new(),
|
||||
toplevel_surf_count: 0,
|
||||
seat,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -205,17 +200,6 @@ impl WayVRCompositor {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn set_clipboard_text(&mut self, text: String) {
|
||||
set_data_device_selection::<Application>(
|
||||
&self.state.display_handle,
|
||||
&self.seat,
|
||||
vec![
|
||||
"text/plain;charset=utf-8".to_string(),
|
||||
"text/plain".to_string(),
|
||||
],
|
||||
text.as_bytes().into(),
|
||||
);
|
||||
}
|
||||
pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> {
|
||||
// Smithay only accepts keymaps in a string form due to thread safety concerns
|
||||
self.seat_keyboard
|
||||
|
|
|
|||
|
|
@ -241,13 +241,7 @@ impl WvrServerState {
|
|||
};
|
||||
|
||||
Ok(Self {
|
||||
manager: client::WayVRCompositor::new(
|
||||
state,
|
||||
display,
|
||||
seat_keyboard,
|
||||
seat_pointer,
|
||||
seat,
|
||||
)?,
|
||||
manager: client::WayVRCompositor::new(state, display, seat_keyboard, seat_pointer)?,
|
||||
processes: ProcessVec::new(),
|
||||
wm: window::WindowManager::new(),
|
||||
ticks: 0,
|
||||
|
|
@ -614,9 +608,6 @@ impl WvrServerState {
|
|||
pub fn send_key(&mut self, virtual_key: u32, down: bool) {
|
||||
self.manager.send_key(virtual_key, down);
|
||||
}
|
||||
pub fn set_clipboard_text(&mut self, text: String) {
|
||||
self.manager.set_clipboard_text(text);
|
||||
}
|
||||
|
||||
pub fn set_keymap(&mut self, keymap: &xkb::Keymap) -> anyhow::Result<()> {
|
||||
self.manager.set_keymap(keymap)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use config::{Config, File};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use wayvr_ipc::packet_client::WvrProcessLaunchParams;
|
||||
use wlx_common::{
|
||||
astr_containers::AStrMap,
|
||||
|
|
@ -45,6 +45,32 @@ where
|
|||
panic!("No usable config found.");
|
||||
}
|
||||
|
||||
const SUPPORTED_EXTESIONS: [&'static str; 4] = ["yaml", "yml", "json", "json5"];
|
||||
|
||||
fn is_supported_config_file(path: &Path) -> bool {
|
||||
if path.is_dir() {
|
||||
return false;
|
||||
}
|
||||
match path.extension().and_then(|e| e.to_str()) {
|
||||
Some(ext) => {
|
||||
for sup in SUPPORTED_EXTESIONS {
|
||||
if sup.eq_ignore_ascii_case(ext) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
log::error!(
|
||||
"Unsupported file extension: \".{}\" in {}. Ignoring file.",
|
||||
ext,
|
||||
path.to_string_lossy()
|
||||
);
|
||||
debug_assert!(false);
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config_with_conf_d<ConfigData>(
|
||||
root_config_filename: &str,
|
||||
ctype: config_io::ConfigRoot,
|
||||
|
|
@ -68,7 +94,8 @@ where
|
|||
if let Ok(paths_unsorted) = std::fs::read_dir(path_conf_d) {
|
||||
let mut paths: Vec<_> = paths_unsorted
|
||||
.filter_map(|r| match r {
|
||||
Ok(entry) => Some(entry),
|
||||
Ok(entry) if is_supported_config_file(&entry.path()) => Some(entry),
|
||||
Ok(_other) => None,
|
||||
Err(e) => {
|
||||
error!("Failed to read conf.d directory: {e}");
|
||||
None
|
||||
|
|
@ -140,7 +167,6 @@ pub struct AutoSettings {
|
|||
pub context_menu_hold_and_release: bool,
|
||||
pub capture_method: CaptureMethod,
|
||||
pub keyboard_middle_click_mode: AltModifier,
|
||||
pub keyboard_swipe_to_type_enabled: bool,
|
||||
pub autostart_apps: Vec<WvrProcessLaunchParams>,
|
||||
pub handsfree_pointer: HandsfreePointer,
|
||||
pub language: Option<Language>,
|
||||
|
|
@ -193,7 +219,6 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
|
|||
context_menu_hold_and_release: config.context_menu_hold_and_release,
|
||||
capture_method: config.capture_method,
|
||||
keyboard_middle_click_mode: config.keyboard_middle_click_mode,
|
||||
keyboard_swipe_to_type_enabled: config.keyboard_swipe_to_type_enabled,
|
||||
autostart_apps: config.autostart_apps.clone(),
|
||||
handsfree_pointer: config.handsfree_pointer,
|
||||
language: config.language,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use wgui::gfx::WGfx;
|
|||
use vulkano::instance::InstanceCreateFlags;
|
||||
use wlx_capture::DrmFormat;
|
||||
|
||||
use crate::shaders::{frag_color, frag_grid, frag_screen, frag_srgb, vert_quad};
|
||||
use crate::shaders::{frag_color, frag_grid, frag_screen, frag_sky, frag_srgb, vert_quad};
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
use {ash::vk, std::os::raw::c_void};
|
||||
|
|
@ -74,6 +74,9 @@ impl WGfxExtras {
|
|||
let shader = frag_srgb::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_srgb", shader);
|
||||
|
||||
let shader = frag_sky::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_sky", shader);
|
||||
|
||||
let shader = frag_grid::load(gfx.device.clone())?;
|
||||
shaders.insert("frag_grid", shader);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use super::timer::GuiTimer;
|
||||
use crate::{
|
||||
app_misc,
|
||||
backend::input::{Haptics, HoverResult, PointerHit, PointerMode},
|
||||
backend::task::ModifyPanelCommand,
|
||||
state::AppState,
|
||||
subsystem::hid::WheelDelta,
|
||||
windowing::backend::{
|
||||
FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform,
|
||||
},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use button::setup_custom_button;
|
||||
use glam::{Affine2, Vec2, vec2};
|
||||
use idmap::IdMap;
|
||||
use label::setup_custom_label;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::{
|
||||
|
|
@ -12,9 +22,9 @@ use wgui::{
|
|||
slider::ComponentSlider,
|
||||
},
|
||||
event::{
|
||||
CallbackDataCommon, Event as WguiEvent, EventAlterables, EventCallback, EventListenerID,
|
||||
EventListenerKind, InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex,
|
||||
MouseLeaveEvent, MouseMotionEvent, MouseWheelEvent,
|
||||
DeviceBitmask, Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind,
|
||||
InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent,
|
||||
MouseMotionEvent, MouseWheelEvent,
|
||||
},
|
||||
gfx::cmd::WGfxClearMode,
|
||||
i18n::Translation,
|
||||
|
|
@ -33,19 +43,6 @@ use wgui::{
|
|||
use wlx_common::overlays::{BackendAttrib, BackendAttribValue};
|
||||
use wlx_common::timestep::Timestep;
|
||||
|
||||
use crate::{
|
||||
app_misc,
|
||||
backend::input::{Haptics, HoverResult, PointerHit, PointerMode},
|
||||
backend::task::ModifyPanelCommand,
|
||||
state::AppState,
|
||||
subsystem::hid::WheelDelta,
|
||||
windowing::backend::{
|
||||
FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform,
|
||||
},
|
||||
};
|
||||
|
||||
use super::timer::GuiTimer;
|
||||
|
||||
pub mod button;
|
||||
pub mod device_list;
|
||||
mod label;
|
||||
|
|
@ -371,7 +368,7 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
|
|||
let e = WguiEvent::MouseWheel(MouseWheelEvent {
|
||||
delta: vec2(delta.x, delta.y) / 8.0,
|
||||
pos: hit.uv * self.layout.content_size,
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
});
|
||||
self.push_event(app, &e);
|
||||
}
|
||||
|
|
@ -379,7 +376,7 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
|
|||
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> HoverResult {
|
||||
let e = &WguiEvent::MouseMotion(MouseMotionEvent {
|
||||
pos: hit.uv * self.layout.content_size,
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
});
|
||||
|
||||
self.has_focus[hit.pointer] = true;
|
||||
|
|
@ -400,7 +397,9 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
|
|||
}
|
||||
|
||||
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
|
||||
let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer });
|
||||
let e = WguiEvent::MouseLeave(MouseLeaveEvent {
|
||||
device: DeviceBitmask::from_usize(pointer),
|
||||
});
|
||||
self.has_focus[pointer] = false;
|
||||
self.push_event(app, &e);
|
||||
}
|
||||
|
|
@ -417,13 +416,13 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
|
|||
WguiEvent::MouseDown(MouseButtonEvent {
|
||||
pos: hit.uv * self.layout.content_size,
|
||||
index,
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
})
|
||||
} else {
|
||||
WguiEvent::MouseUp(MouseButtonEvent {
|
||||
pos: hit.uv * self.layout.content_size,
|
||||
index,
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
})
|
||||
};
|
||||
self.push_event(app, &e);
|
||||
|
|
@ -432,11 +431,11 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
|
|||
if !pressed && !self.has_focus[hit.pointer] {
|
||||
let e = WguiEvent::MouseMotion(MouseMotionEvent {
|
||||
pos: vec2(-1., -1.),
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
});
|
||||
self.push_event(app, &e);
|
||||
let e = WguiEvent::MouseLeave(MouseLeaveEvent {
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
});
|
||||
self.push_event(app, &e);
|
||||
}
|
||||
|
|
@ -493,17 +492,13 @@ pub fn apply_custom_command<T>(
|
|||
element: &str,
|
||||
command: &ModifyPanelCommand,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut alterables = EventAlterables::default();
|
||||
let mut com = CallbackDataCommon {
|
||||
alterables: &mut alterables,
|
||||
state: &panel.layout.state,
|
||||
};
|
||||
let mut com = panel.layout.common();
|
||||
|
||||
match command {
|
||||
ModifyPanelCommand::SetText(text) => {
|
||||
if let Ok(mut label) = panel
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&panel.layout.state, element)
|
||||
.fetch_widget_as::<WidgetLabel>(&com.state, element)
|
||||
{
|
||||
label.set_text(&mut com, Translation::from_raw_text(text));
|
||||
} else if let Ok(button) = panel
|
||||
|
|
@ -516,10 +511,7 @@ pub fn apply_custom_command<T>(
|
|||
}
|
||||
}
|
||||
ModifyPanelCommand::SetImage(path) => {
|
||||
if let Ok(pair) = panel
|
||||
.parser_state
|
||||
.fetch_widget(&panel.layout.state, element)
|
||||
{
|
||||
if let Ok(pair) = panel.parser_state.fetch_widget(&com.state, element) {
|
||||
let data = CustomGlyphData::from_assets(
|
||||
&app.wgui_globals,
|
||||
wgui::assets::AssetPath::File(path),
|
||||
|
|
@ -527,9 +519,9 @@ pub fn apply_custom_command<T>(
|
|||
.context("Could not load content from supplied path.")?;
|
||||
|
||||
if let Some(mut sprite) = pair.widget.get_as::<WidgetSprite>() {
|
||||
sprite.set_content(&mut com, Some(data));
|
||||
sprite.set_content(com.alterables, Some(data));
|
||||
} else if let Some(mut image) = pair.widget.get_as::<WidgetImage>() {
|
||||
image.set_content(&mut com, Some(data));
|
||||
image.set_content(com.alterables, Some(data));
|
||||
} else {
|
||||
anyhow::bail!("No <sprite> or <image> with such id.");
|
||||
}
|
||||
|
|
@ -541,10 +533,7 @@ pub fn apply_custom_command<T>(
|
|||
let color = parse_color_hex(color)
|
||||
.context("Invalid color format, must be a html hex color!")?;
|
||||
|
||||
if let Ok(pair) = panel
|
||||
.parser_state
|
||||
.fetch_widget(&panel.layout.state, element)
|
||||
{
|
||||
if let Ok(pair) = panel.parser_state.fetch_widget(&com.state, element) {
|
||||
if let Some(mut rect) = pair.widget.get_as::<WidgetRectangle>() {
|
||||
rect.set_color(&mut com, color);
|
||||
} else if let Some(mut label) = pair.widget.get_as::<WidgetLabel>() {
|
||||
|
|
@ -592,7 +581,7 @@ pub fn apply_custom_command<T>(
|
|||
.fetch_component_as::<ComponentSlider>(element)
|
||||
{
|
||||
let value_f32 = value_str.parse::<f32>().context("Not a valid number")?;
|
||||
slider.set_value(&mut com, value_f32);
|
||||
slider.set_value_primary(&mut com, value_f32);
|
||||
} else if let Ok(radio) = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentRadioGroup>(element)
|
||||
|
|
@ -610,6 +599,5 @@ pub fn apply_custom_command<T>(
|
|||
}
|
||||
}
|
||||
|
||||
panel.layout.process_alterables(alterables)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use crate::windowing::{OverlayID, backend::OverlayEventData, window::OverlayCategory};
|
||||
use slotmap::{Key, SecondaryMap};
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
use wgui::{
|
||||
components::button::ComponentButton,
|
||||
event::{CallbackDataCommon, EventAlterables},
|
||||
layout::Layout,
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
};
|
||||
|
||||
use crate::windowing::{OverlayID, backend::OverlayEventData, window::OverlayCategory};
|
||||
|
||||
#[derive(Default)]
|
||||
/// Helper for managing a list of overlays
|
||||
/// Populates `id="panels_root"` with `<Screen>`, `<Mirror>`, `<Panel>` templates
|
||||
|
|
@ -25,7 +22,6 @@ impl OverlayList {
|
|||
layout: &mut Layout,
|
||||
parser_state: &mut ParserState,
|
||||
event_data: &OverlayEventData,
|
||||
alterables: &mut EventAlterables,
|
||||
doc_params: &ParseDocumentParams,
|
||||
) -> anyhow::Result<bool> {
|
||||
let mut elements_changed = false;
|
||||
|
|
@ -98,11 +94,7 @@ impl OverlayList {
|
|||
};
|
||||
|
||||
if meta.visible {
|
||||
let mut com = CallbackDataCommon {
|
||||
alterables,
|
||||
state: &layout.state,
|
||||
};
|
||||
overlay_button.set_sticky_state(&mut com, true);
|
||||
overlay_button.set_sticky_state(&mut layout.common(), true);
|
||||
}
|
||||
self.overlay_buttons.insert(meta.id, overlay_button);
|
||||
continue;
|
||||
|
|
@ -121,31 +113,23 @@ impl OverlayList {
|
|||
let overlay_button = parser_state
|
||||
.fetch_component_as::<ComponentButton>(&format!("overlay_{i}"))?;
|
||||
if meta.visible {
|
||||
let mut com = CallbackDataCommon {
|
||||
alterables,
|
||||
state: &layout.state,
|
||||
};
|
||||
overlay_button.set_sticky_state(&mut com, true);
|
||||
overlay_button.set_sticky_state(&mut layout.common(), true);
|
||||
}
|
||||
self.overlay_buttons.insert(meta.id, overlay_button);
|
||||
}
|
||||
elements_changed = true;
|
||||
}
|
||||
OverlayEventData::VisibleOverlaysChanged(overlays) => {
|
||||
let mut com = CallbackDataCommon {
|
||||
alterables,
|
||||
state: &layout.state,
|
||||
};
|
||||
let mut overlay_buttons = self.overlay_buttons.clone();
|
||||
|
||||
for visible in overlays.as_ref() {
|
||||
if let Some(btn) = overlay_buttons.remove(*visible) {
|
||||
btn.set_sticky_state(&mut com, true);
|
||||
btn.set_sticky_state(&mut layout.common(), true);
|
||||
}
|
||||
}
|
||||
|
||||
for btn in overlay_buttons.values() {
|
||||
btn.set_sticky_state(&mut com, false);
|
||||
btn.set_sticky_state(&mut layout.common(), false);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ use std::{collections::HashMap, rc::Rc};
|
|||
|
||||
use wgui::{
|
||||
components::button::ComponentButton,
|
||||
event::{CallbackDataCommon, EventAlterables},
|
||||
layout::Layout,
|
||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||
};
|
||||
|
|
@ -23,25 +22,20 @@ impl SetList {
|
|||
layout: &mut Layout,
|
||||
parser_state: &mut ParserState,
|
||||
event_data: &OverlayEventData,
|
||||
alterables: &mut EventAlterables,
|
||||
doc_params: &ParseDocumentParams,
|
||||
) -> anyhow::Result<bool> {
|
||||
let mut elements_changed = false;
|
||||
match event_data {
|
||||
OverlayEventData::ActiveSetChanged(current_set) => {
|
||||
let mut com = CallbackDataCommon {
|
||||
alterables,
|
||||
state: &layout.state,
|
||||
};
|
||||
if let Some(old_set) = self.current_set.take()
|
||||
&& let Some(old_set) = self.set_buttons.get_mut(old_set)
|
||||
{
|
||||
old_set.set_sticky_state(&mut com, false);
|
||||
old_set.set_sticky_state(&mut layout.common(), false);
|
||||
}
|
||||
if let Some(new_set) = current_set
|
||||
&& let Some(new_set) = self.set_buttons.get_mut(*new_set)
|
||||
{
|
||||
new_set.set_sticky_state(&mut com, true);
|
||||
new_set.set_sticky_state(&mut layout.common(), true);
|
||||
}
|
||||
self.current_set = *current_set;
|
||||
}
|
||||
|
|
@ -59,11 +53,7 @@ impl SetList {
|
|||
let set_button =
|
||||
parser_state.fetch_component_as::<ComponentButton>(&format!("set_{i}"))?;
|
||||
if self.current_set == Some(i) {
|
||||
let mut com = CallbackDataCommon {
|
||||
alterables,
|
||||
state: &layout.state,
|
||||
};
|
||||
set_button.set_sticky_state(&mut com, true);
|
||||
set_button.set_sticky_state(&mut layout.common(), true);
|
||||
}
|
||||
self.set_buttons.push(set_button);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
|||
|
|
@ -42,35 +42,40 @@ pub fn send_packet(conn: &mut local_socket::Stream, data: &[u8]) -> anyhow::Resu
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn read_check(expected_size: u32, res: std::io::Result<usize>) -> bool {
|
||||
fn read_check(expected_size: u32, res: std::io::Result<usize>) -> anyhow::Result<bool> {
|
||||
match res {
|
||||
Ok(count) => {
|
||||
if count == 0 {
|
||||
return false;
|
||||
anyhow::bail!("End of stream");
|
||||
}
|
||||
if count as u32 == expected_size {
|
||||
true // read succeeded
|
||||
return Ok(true); // read succeeded
|
||||
} else {
|
||||
log::error!("count {count} is not {expected_size}");
|
||||
false
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Err(_e) => {
|
||||
//log::error!("failed to get packet size: {}", e);
|
||||
false
|
||||
}
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::WouldBlock => {
|
||||
// no incoming data (this socket is non-blocking), try again later
|
||||
return Ok(false);
|
||||
}
|
||||
_ => {
|
||||
anyhow::bail!("Connection error: {:?}", e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Payload = SmallVec<[u8; 64]>;
|
||||
|
||||
fn read_payload(conn: &mut local_socket::Stream, size: u32) -> Option<Payload> {
|
||||
fn read_payload(conn: &mut local_socket::Stream, size: u32) -> anyhow::Result<Option<Payload>> {
|
||||
let mut payload = Payload::new();
|
||||
payload.resize(size as usize, 0);
|
||||
if read_check(size, conn.read(&mut payload)) {
|
||||
Some(payload)
|
||||
if read_check(size, conn.read(&mut payload))? {
|
||||
Ok(Some(payload))
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -433,36 +438,33 @@ impl Connection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn process_check_payload(&mut self, params: &mut TickParams, payload: Payload) -> bool {
|
||||
log::debug!("payload size {}", payload.len());
|
||||
|
||||
fn process_check_payload(
|
||||
&mut self,
|
||||
params: &mut TickParams,
|
||||
payload: Payload,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Err(e) = self.process_payload(params, payload) {
|
||||
log::error!("Invalid payload from the client, closing connection: {e}");
|
||||
// send also error message directly to the client before disconnecting
|
||||
self.kill(format!("{e}").as_str());
|
||||
false
|
||||
} else {
|
||||
true
|
||||
anyhow::bail!("Invalid payload from the client, closing connection: {e}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_packet(&mut self, params: &mut TickParams) -> bool {
|
||||
fn read_packet(&mut self, params: &mut TickParams) -> anyhow::Result<bool> {
|
||||
if let Some(payload_size) = self.next_packet {
|
||||
let Some(payload) = read_payload(&mut self.conn, payload_size) else {
|
||||
// still failed to read payload, try in next tick
|
||||
return false;
|
||||
let Some(payload) = read_payload(&mut self.conn, payload_size)? else {
|
||||
// still failed to read payload, try in the next tick
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if !self.process_check_payload(params, payload) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.process_check_payload(params, payload)?;
|
||||
self.next_packet = None;
|
||||
}
|
||||
|
||||
let mut buf_packet_header: [u8; 4] = [0; 4];
|
||||
if !read_check(4, self.conn.read(&mut buf_packet_header)) {
|
||||
return false;
|
||||
if !read_check(4, self.conn.read(&mut buf_packet_header))? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let payload_size = u32::from_be_bytes(buf_packet_header[0..4].try_into().unwrap()); // 0-3 bytes (u32 size)
|
||||
|
|
@ -475,30 +477,43 @@ impl Connection {
|
|||
"Client sent a packet header with the size over {size_limit} bytes, closing connection."
|
||||
);
|
||||
self.kill("Too big packet received (over 128 KiB)");
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let Some(payload) = read_payload(&mut self.conn, payload_size) else {
|
||||
let Some(payload) = read_payload(&mut self.conn, payload_size)? else {
|
||||
// failed to read payload, try in next tick
|
||||
self.next_packet = Some(payload_size);
|
||||
return false;
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if !self.process_check_payload(params, payload) {
|
||||
return false;
|
||||
}
|
||||
self.process_check_payload(params, payload)?;
|
||||
|
||||
true
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn tick(&mut self, params: &mut TickParams) {
|
||||
while self.read_packet(params) {}
|
||||
loop {
|
||||
match self.read_packet(params) {
|
||||
Ok(loop_further) => {
|
||||
if !loop_further {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("Disconnecting client: {:?}", e);
|
||||
self.alive = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
fn drop(&mut self) {
|
||||
log::info!("Connection closed");
|
||||
if let Some(auth) = &self.auth {
|
||||
log::info!("IPC: Client \"{}\" disconnected.", auth.client_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,10 @@ struct Args {
|
|||
#[arg(long)]
|
||||
headless: bool,
|
||||
|
||||
/// Do not auto-start registered apps
|
||||
#[arg(long)]
|
||||
no_autostart: bool,
|
||||
|
||||
/// Path to write logs to
|
||||
#[arg(short, long, value_name = "FILE_PATH")]
|
||||
log_to: Option<String>,
|
||||
|
|
@ -150,7 +154,7 @@ fn auto_run(args: Args, used_backend: &mut Option<XrBackend>) {
|
|||
if !args_get_openvr(&args) {
|
||||
use crate::backend::{BackendError, openxr::openxr_run};
|
||||
tried_xr = true;
|
||||
match openxr_run(args.show, args.headless) {
|
||||
match openxr_run(args.show, args.headless, args.no_autostart) {
|
||||
Ok(()) => {
|
||||
used_backend.replace(XrBackend::OpenXR);
|
||||
return;
|
||||
|
|
@ -168,7 +172,7 @@ fn auto_run(args: Args, used_backend: &mut Option<XrBackend>) {
|
|||
if !args_get_openxr(&args) {
|
||||
use crate::backend::{BackendError, openvr::openvr_run};
|
||||
tried_vr = true;
|
||||
match openvr_run(args.show, args.headless) {
|
||||
match openvr_run(args.show, args.headless, args.no_autostart) {
|
||||
Ok(()) => {
|
||||
used_backend.replace(XrBackend::OpenVR);
|
||||
return;
|
||||
|
|
@ -281,7 +285,6 @@ fn logging_init(args: &mut Args) {
|
|||
.from_env_lossy()
|
||||
.add_directive("symphonia_core::probe=warn".parse().unwrap())
|
||||
.add_directive("symphonia_bundle_mp3=warn".parse().unwrap())
|
||||
.add_directive("ort=warn".parse().unwrap())
|
||||
.add_directive("zbus=warn".parse().unwrap())
|
||||
.add_directive("usvg=error".parse().unwrap())
|
||||
.add_directive("resvg=error".parse().unwrap())
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ use wayvr_ipc::{
|
|||
};
|
||||
use wgui::{
|
||||
event::{
|
||||
Event as WguiEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, MouseMotionEvent,
|
||||
MouseWheelEvent,
|
||||
DeviceBitmask, Event as WguiEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent,
|
||||
MouseMotionEvent, MouseWheelEvent,
|
||||
},
|
||||
gfx::cmd::WGfxClearMode,
|
||||
renderer_vk::context::Context as WguiContext,
|
||||
widget::EventResult,
|
||||
};
|
||||
use wlx_common::{
|
||||
dash_interface::{self, DashInterface, RecenterMode},
|
||||
dash_interface::{self, ConfigChangeKind, DashInterface, RecenterMode},
|
||||
locale::WayVRLangProvider,
|
||||
overlays::{BackendAttrib, BackendAttribValue},
|
||||
};
|
||||
|
|
@ -73,8 +73,12 @@ impl DashFrontend {
|
|||
fn new(app: &mut AppState) -> anyhow::Result<Self> {
|
||||
let mut interface = DashInterfaceLive::new();
|
||||
|
||||
for p in app.session.config.autostart_apps.clone() {
|
||||
let _ = interface.process_launch(app, false, p);
|
||||
if app.session.no_autostart {
|
||||
log::info!("Not starting apps due to --no-autostart")
|
||||
} else {
|
||||
for p in app.session.config.autostart_apps.clone() {
|
||||
let _ = interface.process_launch(app, false, p);
|
||||
}
|
||||
}
|
||||
|
||||
let frontend = frontend::Frontend::new(frontend::InitParams {
|
||||
|
|
@ -201,7 +205,7 @@ impl OverlayBackend for DashFrontend {
|
|||
let e = WguiEvent::MouseWheel(MouseWheelEvent {
|
||||
delta: vec2(delta.x, delta.y) / 8.0,
|
||||
pos: hit.uv * self.inner.layout.content_size,
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
});
|
||||
self.push_event(&e);
|
||||
}
|
||||
|
|
@ -209,7 +213,7 @@ impl OverlayBackend for DashFrontend {
|
|||
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> HoverResult {
|
||||
let e = &WguiEvent::MouseMotion(MouseMotionEvent {
|
||||
pos: hit.uv * self.inner.layout.content_size,
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
});
|
||||
|
||||
self.has_focus[hit.pointer] = true;
|
||||
|
|
@ -231,7 +235,9 @@ impl OverlayBackend for DashFrontend {
|
|||
}
|
||||
|
||||
fn on_left(&mut self, _app: &mut AppState, pointer: usize) {
|
||||
let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer });
|
||||
let e = WguiEvent::MouseLeave(MouseLeaveEvent {
|
||||
device: DeviceBitmask::from_usize(pointer),
|
||||
});
|
||||
self.has_focus[pointer] = false;
|
||||
self.push_event(&e);
|
||||
}
|
||||
|
|
@ -248,13 +254,13 @@ impl OverlayBackend for DashFrontend {
|
|||
WguiEvent::MouseDown(MouseButtonEvent {
|
||||
pos: hit.uv * self.inner.layout.content_size,
|
||||
index,
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
})
|
||||
} else {
|
||||
WguiEvent::MouseUp(MouseButtonEvent {
|
||||
pos: hit.uv * self.inner.layout.content_size,
|
||||
index,
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
})
|
||||
};
|
||||
self.push_event(&e);
|
||||
|
|
@ -263,11 +269,11 @@ impl OverlayBackend for DashFrontend {
|
|||
if !pressed && !self.has_focus[hit.pointer] {
|
||||
let e = WguiEvent::MouseMotion(MouseMotionEvent {
|
||||
pos: vec2(-1., -1.),
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
});
|
||||
self.push_event(&e);
|
||||
let e = WguiEvent::MouseLeave(MouseLeaveEvent {
|
||||
device: hit.pointer,
|
||||
device: DeviceBitmask::from_usize(hit.pointer),
|
||||
});
|
||||
self.push_event(&e);
|
||||
}
|
||||
|
|
@ -444,16 +450,22 @@ impl DashInterface<AppState> for DashInterfaceLive {
|
|||
&mut data.session.config
|
||||
}
|
||||
|
||||
fn config_changed(&mut self, data: &mut AppState) {
|
||||
fn config_changed(&mut self, data: &mut AppState, kind: ConfigChangeKind) {
|
||||
data.session.config_dirty = true;
|
||||
#[cfg(feature = "openxr")]
|
||||
{
|
||||
use crate::backend::task::OpenXrTask;
|
||||
data.tasks
|
||||
.enqueue(TaskType::OpenXR(OpenXrTask::SettingsChanged));
|
||||
|
||||
match kind {
|
||||
ConfigChangeKind::OverlayConfig => data
|
||||
.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::SettingsChanged)),
|
||||
ConfigChangeKind::EnvironmentBlend => {
|
||||
#[cfg(feature = "openxr")]
|
||||
{
|
||||
use crate::backend::task::OpenXrTask;
|
||||
data.tasks
|
||||
.enqueue(TaskType::OpenXR(OpenXrTask::EnvironmentChanged));
|
||||
}
|
||||
}
|
||||
}
|
||||
data.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::SettingsChanged));
|
||||
}
|
||||
|
||||
fn restart(&mut self, _data: &mut AppState) {
|
||||
|
|
@ -466,6 +478,16 @@ impl DashInterface<AppState> for DashInterfaceLive {
|
|||
.enqueue(TaskType::Overlay(OverlayTask::ToggleDashboard));
|
||||
}
|
||||
|
||||
fn get_feats(&mut self, data: &mut AppState) -> dash_interface::InterfaceFeats {
|
||||
dash_interface::InterfaceFeats {
|
||||
openxr: matches!(data.xr_backend, XrBackend::OpenXR),
|
||||
#[cfg(feature = "openxr")]
|
||||
monado: data.monado_state.is_some(),
|
||||
#[cfg(not(feature = "openxr"))]
|
||||
monado: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openxr")]
|
||||
fn monado_client_list(
|
||||
&mut self,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use glam::vec2;
|
|||
use slotmap::Key;
|
||||
use wgui::{
|
||||
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
|
||||
event::{CallbackDataCommon, EventAlterables, EventCallback},
|
||||
event::EventCallback,
|
||||
i18n::Translation,
|
||||
parser::Fetchable,
|
||||
widget::EventResult,
|
||||
|
|
@ -466,98 +466,82 @@ fn reset_panel(
|
|||
*panel.state.id.borrow_mut() = id;
|
||||
let state = owc.active_state.as_mut().unwrap();
|
||||
|
||||
let mut alterables = EventAlterables::default();
|
||||
let mut common = CallbackDataCommon {
|
||||
alterables: &mut alterables,
|
||||
state: &panel.layout.state,
|
||||
};
|
||||
let mut com = panel.layout.common();
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentButton>("top_grab")?;
|
||||
c.set_sticky_state(&mut common, !state.grabbable);
|
||||
c.set_sticky_state(&mut com, !state.grabbable);
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentSlider>("lerp_slider")?;
|
||||
c.set_value(&mut common, state.positioning.get_lerp().unwrap_or(1.0));
|
||||
c.set_value_primary(&mut com, state.positioning.get_lerp().unwrap_or(1.0));
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentSlider>("alpha_slider")?;
|
||||
c.set_value(&mut common, state.alpha);
|
||||
c.set_value_primary(&mut com, state.alpha);
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentSlider>("curve_slider")?;
|
||||
c.set_value(&mut common, state.curvature.unwrap_or(0.0));
|
||||
c.set_value_primary(&mut com, state.curvature.unwrap_or(0.0));
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentCheckbox>("additive_box")?;
|
||||
c.set_checked(&mut common, state.additive);
|
||||
c.set_checked(&mut com, state.additive);
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentCheckbox>("align_box")?;
|
||||
c.set_checked(&mut common, state.positioning.get_align().unwrap_or(false));
|
||||
c.set_checked(&mut com, state.positioning.get_align().unwrap_or(false));
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentCheckbox>("global_box")?;
|
||||
c.set_checked(&mut common, owc.global);
|
||||
c.set_checked(&mut com, owc.global);
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentCheckbox>("angle_fade_box")?;
|
||||
c.set_checked(&mut common, state.angle_fade);
|
||||
c.set_checked(&mut com, state.angle_fade);
|
||||
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentCheckbox>("block_input_box")?;
|
||||
c.set_checked(&mut common, state.block_input);
|
||||
c.set_checked(&mut com, state.block_input);
|
||||
|
||||
panel
|
||||
.state
|
||||
.pos
|
||||
.reset(&mut common, &state.positioning.into());
|
||||
panel.state.lock.reset(&mut common, state.interactable);
|
||||
panel.state.tabs.reset(&mut common);
|
||||
panel.state.pos.reset(&mut com, &state.positioning.into());
|
||||
panel.state.lock.reset(&mut com, state.interactable);
|
||||
panel.state.tabs.reset(&mut com);
|
||||
|
||||
if let Some(stereo) = attrib_value!(
|
||||
owc.backend.get_attrib(BackendAttrib::Stereo),
|
||||
BackendAttribValue::Stereo
|
||||
) {
|
||||
panel
|
||||
.state
|
||||
.tabs
|
||||
.set_tab_visible(&mut common, "stereo", true);
|
||||
panel.state.stereo.reset(&mut common, &stereo);
|
||||
panel.state.tabs.set_tab_visible(&mut com, "stereo", true);
|
||||
panel.state.stereo.reset(&mut com, &stereo);
|
||||
|
||||
// Set the checkbox label based on stereo mode
|
||||
let translation = get_stereo_full_frame_translation(stereo);
|
||||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentCheckbox>("stereo_full_frame_box")?;
|
||||
c.set_text(&mut common, Translation::from_translation_key(translation));
|
||||
c.set_text(&mut com, Translation::from_translation_key(translation));
|
||||
} else {
|
||||
panel
|
||||
.state
|
||||
.tabs
|
||||
.set_tab_visible(&mut common, "stereo", false);
|
||||
panel.state.tabs.set_tab_visible(&mut com, "stereo", false);
|
||||
}
|
||||
|
||||
if let Some(mouse) = attrib_value!(
|
||||
owc.backend.get_attrib(BackendAttrib::MouseTransform),
|
||||
BackendAttribValue::MouseTransform
|
||||
) {
|
||||
panel.state.tabs.set_tab_visible(&mut common, "mouse", true);
|
||||
panel.state.mouse.reset(&mut common, &mouse);
|
||||
panel.state.tabs.set_tab_visible(&mut com, "mouse", true);
|
||||
panel.state.mouse.reset(&mut com, &mouse);
|
||||
} else {
|
||||
panel
|
||||
.state
|
||||
.tabs
|
||||
.set_tab_visible(&mut common, "mouse", false);
|
||||
panel.state.tabs.set_tab_visible(&mut com, "mouse", false);
|
||||
}
|
||||
|
||||
if let Some(full_frame) = attrib_value!(
|
||||
|
|
@ -567,7 +551,7 @@ fn reset_panel(
|
|||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentCheckbox>("stereo_full_frame_box")?;
|
||||
c.set_checked(&mut common, full_frame);
|
||||
c.set_checked(&mut com, full_frame);
|
||||
}
|
||||
|
||||
if let Some(adjust_mouse) = attrib_value!(
|
||||
|
|
@ -577,11 +561,9 @@ fn reset_panel(
|
|||
let c = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentCheckbox>("stereo_adjust_mouse_box")?;
|
||||
c.set_checked(&mut common, adjust_mouse);
|
||||
c.set_checked(&mut com, adjust_mouse);
|
||||
}
|
||||
|
||||
panel.layout.process_alterables(alterables)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ where
|
|||
.widgets
|
||||
.get_as::<WidgetSprite>(self.top_sprite_id)
|
||||
{
|
||||
sprite.set_content(common, Some(new.sprite.clone()));
|
||||
sprite.set_content(common.alterables, Some(new.sprite.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
use super::{
|
||||
KeyButtonData, KeyState, KeyboardState, handle_mouse_motion, handle_press, handle_release,
|
||||
init_swipe_type_manager,
|
||||
layout::{self, KeyCapType},
|
||||
};
|
||||
use std::{collections::HashMap, rc::Rc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
app_misc,
|
||||
gui::{
|
||||
|
|
@ -14,18 +11,13 @@ use crate::{
|
|||
subsystem::hid::XkbKeymap,
|
||||
windowing::backend::OverlayEventData,
|
||||
};
|
||||
use anyhow::{Context, bail};
|
||||
use anyhow::Context;
|
||||
use glam::{FloatExt, Mat4, Vec2, vec2, vec3};
|
||||
use slotmap::Key;
|
||||
use std::{collections::HashMap, rc::Rc, time::Duration};
|
||||
use wgui::event::StyleSetRequest;
|
||||
use wgui::layout::LayoutTask;
|
||||
use wgui::taffy::Display;
|
||||
use wgui::{
|
||||
animation::{Animation, AnimationEasing},
|
||||
assets::AssetPath,
|
||||
drawing::{self, Color},
|
||||
event::{self, CallbackMetadata, EventAlterables, EventListenerKind},
|
||||
event::{self, CallbackMetadata, EventListenerKind},
|
||||
layout::LayoutUpdateParams,
|
||||
log::LogErr,
|
||||
parser::{Fetchable, ParseDocumentParams},
|
||||
|
|
@ -34,6 +26,11 @@ use wgui::{
|
|||
widget::{EventResult, div::WidgetDiv, rectangle::WidgetRectangle},
|
||||
};
|
||||
|
||||
use super::{
|
||||
KeyButtonData, KeyState, KeyboardState, handle_press, handle_release,
|
||||
layout::{self, KeyCapType},
|
||||
};
|
||||
|
||||
const PIXELS_PER_UNIT: f32 = 60.;
|
||||
|
||||
fn new_doc_params(panel: &mut GuiPanel<KeyboardState>) -> ParseDocumentParams<'static> {
|
||||
|
|
@ -44,134 +41,6 @@ fn new_doc_params(panel: &mut GuiPanel<KeyboardState>) -> ParseDocumentParams<'s
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_swipe_prediction_bar(
|
||||
panel: &mut GuiPanel<KeyboardState>,
|
||||
app: &mut AppState,
|
||||
) -> anyhow::Result<bool> {
|
||||
let mut elements_changed = false;
|
||||
|
||||
let (accent_color, anim_mult) = {
|
||||
let theme = &app.wgui_theme;
|
||||
(theme.accent_color, theme.animation_mult)
|
||||
};
|
||||
|
||||
if let Some(recv) = panel.state.swipe_candidate_receiver.as_mut()
|
||||
&& let Ok(candidates) = recv.try_recv()
|
||||
{
|
||||
let predictions_root = panel
|
||||
.parser_state
|
||||
.get_widget_id("swipe_predictions_root")
|
||||
.unwrap_or_default();
|
||||
|
||||
if predictions_root.is_null() {
|
||||
return Ok(elements_changed);
|
||||
}
|
||||
let doc_params = new_doc_params(panel);
|
||||
|
||||
panel.layout.remove_children(predictions_root);
|
||||
|
||||
let Some(new_suggestions) = candidates else {
|
||||
return Ok(elements_changed);
|
||||
};
|
||||
|
||||
let mut iter = new_suggestions.iter();
|
||||
let Some(best_prediction) = iter.next() else {
|
||||
bail!("not enough swipe predictions");
|
||||
};
|
||||
if let Some(manager) = panel.state.swipe_typing_manager.as_mut() {
|
||||
manager.select_word(best_prediction, app, panel.state.modifiers);
|
||||
}
|
||||
for (i, prediction) in iter.enumerate() {
|
||||
let mut params = HashMap::new();
|
||||
let id: Rc<str> = Rc::from(format!("Prediction-{i}"));
|
||||
params.insert("id".into(), id.clone());
|
||||
params.insert("text".into(), prediction.clone().into());
|
||||
|
||||
panel.parser_state.instantiate_template(
|
||||
&doc_params,
|
||||
"KeyPrediction",
|
||||
&mut panel.layout,
|
||||
predictions_root,
|
||||
params,
|
||||
)?;
|
||||
|
||||
if let Ok(widget_id) = panel.parser_state.get_widget_id(&id) {
|
||||
let key_state = {
|
||||
let rect = panel
|
||||
.layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetRectangle>(widget_id)
|
||||
.unwrap(); // want panic
|
||||
|
||||
Rc::new(KeyState {
|
||||
// fake button state just so we get key state for anims
|
||||
button_state: KeyButtonData::Modifier {
|
||||
modifier: 0,
|
||||
sticky: core::cell::Cell::new(false),
|
||||
},
|
||||
color: rect.params.color,
|
||||
color2: rect.params.color2,
|
||||
base_border_color: rect.params.border_color,
|
||||
cur_border_color: rect.params.border_color.into(),
|
||||
border: rect.params.border,
|
||||
drawn_state: false.into(),
|
||||
})
|
||||
};
|
||||
panel.add_event_listener(
|
||||
widget_id,
|
||||
EventListenerKind::MousePress,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
let pred = prediction.clone();
|
||||
move |common, data, app, state| {
|
||||
if let Some(manager) = state.swipe_typing_manager.as_mut() {
|
||||
manager.select_alternate_prediction(&pred, app, state.modifiers);
|
||||
on_press_anim(k.clone(), common, data)
|
||||
}
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
);
|
||||
panel.add_event_listener(
|
||||
widget_id,
|
||||
EventListenerKind::MouseEnter,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
move |common, data, _app, _state| {
|
||||
on_enter_anim(k.clone(), common, data, accent_color, anim_mult, 0.0);
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
);
|
||||
panel.add_event_listener(
|
||||
widget_id,
|
||||
EventListenerKind::MouseLeave,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
move |common, data, _app, _state| {
|
||||
on_leave_anim(k.clone(), common, data, accent_color, anim_mult, 0.0);
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
);
|
||||
panel.add_event_listener(
|
||||
widget_id,
|
||||
EventListenerKind::MouseRelease,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
move |common, data, _app, _state| {
|
||||
on_release_anim(k.clone(), common, data);
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
elements_changed = true;
|
||||
}
|
||||
Ok(elements_changed)
|
||||
}
|
||||
#[allow(clippy::too_many_lines, clippy::significant_drop_tightening)]
|
||||
pub(super) fn create_keyboard_panel(
|
||||
app: &mut AppState,
|
||||
|
|
@ -244,7 +113,7 @@ pub(super) fn create_keyboard_panel(
|
|||
params.insert(Rc::from("width"), Rc::from(key_width.to_string()));
|
||||
params.insert(Rc::from("height"), Rc::from(key_height.to_string()));
|
||||
|
||||
let mut label = key.label.clone().into_iter();
|
||||
let mut label = key.label.into_iter();
|
||||
label
|
||||
.next()
|
||||
.and_then(|s| params.insert("text".into(), s.into()));
|
||||
|
|
@ -300,9 +169,6 @@ pub(super) fn create_keyboard_panel(
|
|||
})
|
||||
};
|
||||
|
||||
let key_cap_type: Rc<KeyCapType> = Rc::from(key.cap_type);
|
||||
let key_label: Rc<Vec<String>> = Rc::from(key.label);
|
||||
|
||||
let width_mul = 1. / my_size_f32;
|
||||
|
||||
panel.add_event_listener(
|
||||
|
|
@ -320,7 +186,6 @@ pub(super) fn create_keyboard_panel(
|
|||
anim_mult,
|
||||
width_mul,
|
||||
);
|
||||
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
|
|
@ -340,7 +205,6 @@ pub(super) fn create_keyboard_panel(
|
|||
anim_mult,
|
||||
width_mul,
|
||||
);
|
||||
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
|
|
@ -350,69 +214,26 @@ pub(super) fn create_keyboard_panel(
|
|||
EventListenerKind::MousePress,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
let k_label = key_label.clone();
|
||||
let k_cap_type = key_cap_type.clone();
|
||||
move |common, data, app, state| {
|
||||
let CallbackMetadata::MouseButton(button) = data.metadata else {
|
||||
panic!("CallbackMetadata should contain MouseButton!");
|
||||
};
|
||||
let within_key_pos = data
|
||||
.metadata
|
||||
.get_mouse_pos_normalized(&common.alterables.transform_stack);
|
||||
|
||||
handle_press(
|
||||
app,
|
||||
&k,
|
||||
&k_label,
|
||||
&k_cap_type,
|
||||
&within_key_pos,
|
||||
state,
|
||||
button,
|
||||
button.device,
|
||||
);
|
||||
handle_press(app, &k, state, button);
|
||||
on_press_anim(k.clone(), common, data);
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
);
|
||||
panel.add_event_listener(
|
||||
widget_id,
|
||||
EventListenerKind::MouseMotion,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
let k_label = key_label.clone();
|
||||
let k_cap_type = key_cap_type.clone();
|
||||
move |common, data, _app, state| {
|
||||
let within_key_pos = data
|
||||
.metadata
|
||||
.get_mouse_pos_normalized(&common.alterables.transform_stack);
|
||||
let CallbackMetadata::MousePosition(position) = data.metadata else {
|
||||
panic!("CallbackMetadata should contain MousePosition!");
|
||||
};
|
||||
|
||||
handle_mouse_motion(
|
||||
&k,
|
||||
&k_label,
|
||||
&k_cap_type,
|
||||
state,
|
||||
&within_key_pos,
|
||||
position.device,
|
||||
);
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
);
|
||||
panel.add_event_listener(
|
||||
widget_id,
|
||||
EventListenerKind::MouseRelease,
|
||||
Box::new({
|
||||
let k = key_state.clone();
|
||||
let k_cap_type = key_cap_type.clone();
|
||||
move |common, data, app, state| {
|
||||
if handle_release(app, &k, &k_cap_type, state) {
|
||||
if handle_release(app, &k, state) {
|
||||
on_release_anim(k.clone(), common, data);
|
||||
}
|
||||
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
|
|
@ -444,13 +265,10 @@ pub(super) fn create_keyboard_panel(
|
|||
panel.on_notify = Some(Box::new({
|
||||
let name = "kbd";
|
||||
move |panel, app, event_data| {
|
||||
let mut alterables = EventAlterables::default();
|
||||
|
||||
let mut elems_changed = panel.state.overlay_list.on_notify(
|
||||
&mut panel.layout,
|
||||
&mut panel.parser_state,
|
||||
&event_data,
|
||||
&mut alterables,
|
||||
&doc_params,
|
||||
)?;
|
||||
|
||||
|
|
@ -458,7 +276,6 @@ pub(super) fn create_keyboard_panel(
|
|||
&mut panel.layout,
|
||||
&mut panel.parser_state,
|
||||
&event_data,
|
||||
&mut alterables,
|
||||
&doc_params,
|
||||
)?;
|
||||
|
||||
|
|
@ -486,41 +303,6 @@ pub(super) fn create_keyboard_panel(
|
|||
elems_changed = true;
|
||||
}
|
||||
}
|
||||
if !app.session.config.keyboard_swipe_to_type_enabled {
|
||||
panel.state.swipe_typing_manager = None;
|
||||
panel.state.swipe_candidate_receiver = None;
|
||||
|
||||
let predictions_root = panel
|
||||
.parser_state
|
||||
.get_widget_id("swipe_predictions_root")
|
||||
.unwrap_or_default();
|
||||
|
||||
if !predictions_root.is_null() {
|
||||
panel.layout.remove_children(predictions_root);
|
||||
|
||||
panel.layout.tasks.push(LayoutTask::SetWidgetStyle(
|
||||
predictions_root,
|
||||
StyleSetRequest::Display(Display::None),
|
||||
));
|
||||
}
|
||||
}
|
||||
if app.session.config.keyboard_swipe_to_type_enabled
|
||||
&& panel.state.swipe_typing_manager.is_none()
|
||||
{
|
||||
init_swipe_type_manager(&mut panel.state);
|
||||
|
||||
let predictions_root = panel
|
||||
.parser_state
|
||||
.get_widget_id("swipe_predictions_root")
|
||||
.unwrap_or_default();
|
||||
|
||||
if !predictions_root.is_null() {
|
||||
panel.layout.tasks.push(LayoutTask::SetWidgetStyle(
|
||||
predictions_root,
|
||||
StyleSetRequest::Display(Display::Flex),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OverlayEventData::CustomCommand { element, command } => {
|
||||
|
|
@ -536,7 +318,6 @@ pub(super) fn create_keyboard_panel(
|
|||
panel.process_custom_elems(app);
|
||||
}
|
||||
|
||||
panel.layout.process_alterables(alterables)?;
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
use std::{collections::HashMap, str::FromStr, sync::LazyLock};
|
||||
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
config::{ConfigType, load_known_yaml},
|
||||
subsystem::hid::{
|
||||
KEYS_TO_MODS, KeyType, META, NUM_LOCK, SHIFT, VirtualKey, XkbKeymap, get_key_type,
|
||||
},
|
||||
};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::KeyButtonData;
|
||||
|
||||
|
|
@ -229,7 +230,7 @@ pub(super) struct KeyData {
|
|||
pub(super) cap_type: KeyCapType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum KeyCapType {
|
||||
/// Label an SVG
|
||||
Special,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
use crate::overlays::keyboard::builder::update_swipe_prediction_bar;
|
||||
use crate::overlays::keyboard::layout::KeyCapType;
|
||||
use crate::overlays::keyboard::swipe_type::SwipeTypingManager;
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
process::{Child, Command},
|
||||
sync::atomic::Ordering,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
KEYMAP_CHANGE,
|
||||
backend::{
|
||||
|
|
@ -23,20 +27,9 @@ use crate::{
|
|||
},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use glam::{Affine3A, Quat, Vec2, Vec3, vec3};
|
||||
use glam::{Affine3A, Quat, Vec3, vec3};
|
||||
use regex::Regex;
|
||||
use slotmap::{Key, SlotMap, new_key_type};
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
process::{Child, Command},
|
||||
sync::atomic::Ordering,
|
||||
};
|
||||
use wgui::event::StyleSetRequest;
|
||||
use wgui::layout::LayoutTask;
|
||||
use wgui::parser::Fetchable;
|
||||
use wgui::taffy::Display;
|
||||
use slotmap::{SlotMap, new_key_type};
|
||||
use wgui::{
|
||||
drawing,
|
||||
event::{InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex},
|
||||
|
|
@ -49,7 +42,6 @@ use wlx_common::{
|
|||
|
||||
pub mod builder;
|
||||
mod layout;
|
||||
mod swipe_type;
|
||||
|
||||
pub const KEYBOARD_NAME: &str = "kbd";
|
||||
const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, META];
|
||||
|
|
@ -64,8 +56,6 @@ pub fn create_keyboard(app: &mut AppState, wayland: bool) -> anyhow::Result<Over
|
|||
overlay_list: OverlayList::default(),
|
||||
set_list: SetList::default(),
|
||||
clock_12h: app.session.config.clock_12h,
|
||||
swipe_typing_manager: None,
|
||||
swipe_candidate_receiver: None,
|
||||
};
|
||||
|
||||
let auto_labels = layout.auto_labels.unwrap_or(true);
|
||||
|
|
@ -121,36 +111,14 @@ pub fn create_keyboard(app: &mut AppState, wayland: bool) -> anyhow::Result<Over
|
|||
transform: Affine3A::from_scale_rotation_translation(
|
||||
Vec3::ONE * width,
|
||||
Quat::from_rotation_x(-10f32.to_radians()),
|
||||
vec3(0.0, -0.69, -0.5),
|
||||
vec3(0.0, -0.65, -0.5),
|
||||
),
|
||||
..OverlayWindowState::default()
|
||||
},
|
||||
..OverlayWindowConfig::from_backend(Box::new(backend))
|
||||
})
|
||||
}
|
||||
pub(self) fn init_swipe_type_manager(state: &mut KeyboardState) {
|
||||
match SwipeTypingManager::new() {
|
||||
Ok((engine, receiver)) => {
|
||||
state.swipe_typing_manager = Some(engine);
|
||||
state.swipe_candidate_receiver = Some(receiver);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error occurred while trying to load swipe engine: {}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(self) fn hide_swipe_type_manager(panel: &mut GuiPanel<KeyboardState>) {
|
||||
let predictions_root = panel
|
||||
.parser_state
|
||||
.get_widget_id("swipe_predictions_root")
|
||||
.unwrap_or_default();
|
||||
if !predictions_root.is_null() {
|
||||
panel.layout.tasks.push(LayoutTask::SetWidgetStyle(
|
||||
predictions_root,
|
||||
StyleSetRequest::Display(Display::None),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const fn alt_modifier_to_key(m: AltModifier) -> KeyModifier {
|
||||
match m {
|
||||
AltModifier::Shift => SHIFT,
|
||||
|
|
@ -182,18 +150,8 @@ impl KeyboardBackend {
|
|||
keymap: Option<&XkbKeymap>,
|
||||
app: &mut AppState,
|
||||
) -> anyhow::Result<KeyboardPanelKey> {
|
||||
let mut state = self.default_state.take();
|
||||
|
||||
if app.session.config.keyboard_swipe_to_type_enabled {
|
||||
init_swipe_type_manager(&mut state);
|
||||
}
|
||||
|
||||
log::info!("swipe engine created");
|
||||
let mut panel = create_keyboard_panel(app, keymap, state, &self.wlx_layout)?;
|
||||
|
||||
if !app.session.config.keyboard_swipe_to_type_enabled {
|
||||
hide_swipe_type_manager(&mut panel);
|
||||
}
|
||||
let panel =
|
||||
create_keyboard_panel(app, keymap, self.default_state.take(), &self.wlx_layout)?;
|
||||
|
||||
let id = self.layout_panels.insert(panel);
|
||||
if let Some(layout_name) = keymap.and_then(|k| k.get_name()) {
|
||||
|
|
@ -218,38 +176,30 @@ impl KeyboardBackend {
|
|||
if self.active_layout.eq(new_key) {
|
||||
return Ok(false);
|
||||
}
|
||||
self.internal_switch_keymap(*new_key, app);
|
||||
self.internal_switch_keymap(*new_key);
|
||||
} else {
|
||||
let new_key = self.add_new_keymap(Some(keymap), app)?;
|
||||
self.internal_switch_keymap(new_key, app);
|
||||
self.internal_switch_keymap(new_key);
|
||||
}
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::KeyboardChanged));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn internal_switch_keymap(&mut self, new_key: KeyboardPanelKey, app: &AppState) {
|
||||
let mut state_from = self
|
||||
fn internal_switch_keymap(&mut self, new_key: KeyboardPanelKey) {
|
||||
let state_from = self
|
||||
.layout_panels
|
||||
.get_mut(self.active_layout)
|
||||
.unwrap()
|
||||
.state
|
||||
.take();
|
||||
|
||||
if app.session.config.keyboard_swipe_to_type_enabled {
|
||||
init_swipe_type_manager(&mut state_from);
|
||||
}
|
||||
|
||||
self.active_layout = new_key;
|
||||
|
||||
self.layout_panels
|
||||
.get_mut(self.active_layout)
|
||||
.unwrap()
|
||||
.state = state_from;
|
||||
|
||||
if !app.session.config.keyboard_swipe_to_type_enabled {
|
||||
hide_swipe_type_manager(self.layout_panels.get_mut(self.active_layout).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_effective_keymap(&mut self) -> anyhow::Result<XkbKeymap> {
|
||||
|
|
@ -283,13 +233,6 @@ impl KeyboardBackend {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_swipe_prediction_bar(&mut self, app: &mut AppState) -> anyhow::Result<()> {
|
||||
if update_swipe_prediction_bar(self.panel(), app)? {
|
||||
self.panel().process_custom_elems(app);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn auto_switch_keymap(&mut self, app: &mut AppState) -> anyhow::Result<bool> {
|
||||
let keymap = self.get_effective_keymap()?;
|
||||
app.hid_provider
|
||||
|
|
@ -323,7 +266,6 @@ impl OverlayBackend for KeyboardBackend {
|
|||
});
|
||||
}
|
||||
}
|
||||
self.update_swipe_prediction_bar(app)?;
|
||||
self.panel().should_render(app)
|
||||
}
|
||||
fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> {
|
||||
|
|
@ -385,8 +327,6 @@ struct KeyboardState {
|
|||
overlay_list: OverlayList,
|
||||
set_list: SetList,
|
||||
clock_12h: bool,
|
||||
swipe_typing_manager: Option<SwipeTypingManager>,
|
||||
swipe_candidate_receiver: Option<Receiver<Option<Vec<String>>>>,
|
||||
}
|
||||
|
||||
macro_rules! take_and_leave_default {
|
||||
|
|
@ -406,8 +346,6 @@ impl KeyboardState {
|
|||
overlay_list: OverlayList::default(),
|
||||
set_list: SetList::default(),
|
||||
clock_12h: self.clock_12h,
|
||||
swipe_typing_manager: None,
|
||||
swipe_candidate_receiver: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -448,75 +386,26 @@ enum KeyButtonData {
|
|||
},
|
||||
}
|
||||
|
||||
fn handle_mouse_motion(
|
||||
key: &KeyState,
|
||||
key_label: &Vec<String>,
|
||||
key_cap_type: &KeyCapType,
|
||||
keyboard: &mut KeyboardState,
|
||||
within_key_pos: &Option<Vec2>,
|
||||
device: usize,
|
||||
) {
|
||||
if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut()
|
||||
&& matches!(*key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr)
|
||||
{
|
||||
if !swipe_manager.is_current_swipe_empty() {
|
||||
match &key.button_state {
|
||||
KeyButtonData::Key { vk, pressed } => {
|
||||
if let Some(pos) = within_key_pos {
|
||||
// check because mouse motion can trigger despite hover being false
|
||||
if pos.x >= 0.0 && pos.x <= 1.0 && pos.y >= 0.0 && pos.y <= 1.0 {
|
||||
if let Some(label) = key_label.first() {
|
||||
swipe_manager.add_swipe(
|
||||
pos,
|
||||
label.chars().next().unwrap_or_default(),
|
||||
device,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn handle_press(
|
||||
app: &mut AppState,
|
||||
key: &KeyState,
|
||||
key_label: &Vec<String>,
|
||||
key_cap_type: &KeyCapType,
|
||||
within_key_pos: &Option<Vec2>,
|
||||
keyboard: &mut KeyboardState,
|
||||
button: MouseButtonEvent,
|
||||
device: usize,
|
||||
) {
|
||||
match &key.button_state {
|
||||
KeyButtonData::Key { vk, pressed } => {
|
||||
if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut()
|
||||
&& matches!(*key_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr)
|
||||
{
|
||||
if let Some(pos) = within_key_pos {
|
||||
if let Some(label) = key_label.first() {
|
||||
swipe_manager.add_swipe(
|
||||
pos,
|
||||
label.chars().next().unwrap_or_default(),
|
||||
device,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keyboard.modifiers |= match button.index {
|
||||
MouseButtonIndex::Right => SHIFT,
|
||||
MouseButtonIndex::Middle => keyboard.alt_modifier,
|
||||
_ => 0,
|
||||
};
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), *vk, true);
|
||||
pressed.set(true);
|
||||
play_key_click(app);
|
||||
}
|
||||
keyboard.modifiers |= match button.index {
|
||||
MouseButtonIndex::Right => SHIFT,
|
||||
MouseButtonIndex::Middle => keyboard.alt_modifier,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), *vk, true);
|
||||
pressed.set(true);
|
||||
play_key_click(app);
|
||||
}
|
||||
KeyButtonData::Modifier { modifier, sticky } => {
|
||||
sticky.set(keyboard.modifiers & *modifier == 0);
|
||||
|
|
@ -546,51 +435,20 @@ fn handle_press(
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_release(
|
||||
app: &mut AppState,
|
||||
key: &KeyState,
|
||||
k_cap_type: &KeyCapType,
|
||||
keyboard: &mut KeyboardState,
|
||||
) -> bool {
|
||||
fn handle_release(app: &mut AppState, key: &KeyState, keyboard: &mut KeyboardState) -> bool {
|
||||
match &key.button_state {
|
||||
KeyButtonData::Key { vk, pressed } => {
|
||||
if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut()
|
||||
&& matches!(*k_cap_type, KeyCapType::Letter | KeyCapType::LetterAltGr)
|
||||
{
|
||||
if swipe_manager.did_swipe_leave_first_key() {
|
||||
match swipe_manager.predict() {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::error!("{}", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// pointer must have been released on the same key it was pressed on
|
||||
swipe_manager.reset(); // drop swipe tracking that was started on press
|
||||
pressed.set(false);
|
||||
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), *vk, true);
|
||||
pressed.set(true);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), *vk, false);
|
||||
play_key_click(app);
|
||||
for m in &AUTO_RELEASE_MODS {
|
||||
if keyboard.modifiers & *m != 0 {
|
||||
keyboard.modifiers &= !*m;
|
||||
}
|
||||
} else {
|
||||
if let Some(swipe_manager) = keyboard.swipe_typing_manager.as_mut() {
|
||||
swipe_manager.reset();
|
||||
}
|
||||
pressed.set(false);
|
||||
|
||||
for m in &AUTO_RELEASE_MODS {
|
||||
if keyboard.modifiers & *m != 0 {
|
||||
keyboard.modifiers &= !*m;
|
||||
}
|
||||
}
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), *vk, false);
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers);
|
||||
}
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), *vk, false);
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), keyboard.modifiers);
|
||||
true
|
||||
}
|
||||
KeyButtonData::Modifier { modifier, sticky } => {
|
||||
|
|
|
|||
|
|
@ -1,250 +0,0 @@
|
|||
use crate::state::AppState;
|
||||
use crate::subsystem::hid::{CTRL, KeyModifier, VirtualKey};
|
||||
use crate::subsystem::input::KeyboardFocus;
|
||||
use anyhow::bail;
|
||||
use arboard::Clipboard;
|
||||
use glam::Vec2;
|
||||
use std::mem;
|
||||
use std::sync::mpsc::{Receiver, Sender, SyncSender, channel, sync_channel};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Instant;
|
||||
use super_swipe_type::SwipePoint;
|
||||
use super_swipe_type::keyboard_manager::QwertyKeyboardGrid;
|
||||
use super_swipe_type::swipe_orchestrator::SwipeOrchestrator;
|
||||
|
||||
const PREDICTION_SUGGESTION_COUNT: usize = 5;
|
||||
|
||||
enum PredictionTask {
|
||||
Predict {
|
||||
swipe: Vec<SwipePoint>,
|
||||
last_word: Option<String>,
|
||||
},
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct SwipeTypingManager {
|
||||
keyboard_gird: QwertyKeyboardGrid,
|
||||
current_swipe: Vec<SwipePoint>,
|
||||
swipe_candidate_sender: SyncSender<Option<Vec<String>>>,
|
||||
prediction_task_sender: Sender<PredictionTask>,
|
||||
worker_thread: Option<JoinHandle<()>>,
|
||||
swipe_start_time: Option<Instant>,
|
||||
clipboard: Clipboard,
|
||||
swipe_left_first_key: bool,
|
||||
first_swipe_char: char,
|
||||
current_swipe_device: Option<usize>,
|
||||
last_swiped_word: Option<String>,
|
||||
}
|
||||
|
||||
impl SwipeTypingManager {
|
||||
pub fn select_alternate_prediction(
|
||||
&mut self,
|
||||
word: &String,
|
||||
app: &mut AppState,
|
||||
original_keyboard_mods: KeyModifier,
|
||||
) {
|
||||
Self::undo(app, original_keyboard_mods);
|
||||
self.select_word(word, app, original_keyboard_mods);
|
||||
}
|
||||
|
||||
pub fn select_word(
|
||||
&mut self,
|
||||
word: &String,
|
||||
app: &mut AppState,
|
||||
original_keyboard_mods: KeyModifier,
|
||||
) {
|
||||
self.last_swiped_word = Some(word.clone());
|
||||
let text_to_paste = format!("{word} ");
|
||||
|
||||
match app.hid_provider.keyboard_focus {
|
||||
KeyboardFocus::PhysicalScreen => {
|
||||
if let Ok(_) = self.clipboard.set_text(text_to_paste) {
|
||||
Self::paste(app, original_keyboard_mods);
|
||||
}
|
||||
}
|
||||
KeyboardFocus::WayVR => {
|
||||
if let Some(wvr_server) = app.wvr_server.as_mut() {
|
||||
wvr_server.set_clipboard_text(text_to_paste);
|
||||
Self::paste(app, original_keyboard_mods);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn undo(app: &mut AppState, original_keyboard_mods: KeyModifier) {
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), CTRL);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), VirtualKey::Z, true);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), VirtualKey::Z, false);
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods);
|
||||
}
|
||||
|
||||
fn paste(app: &mut AppState, original_keyboard_mods: KeyModifier) {
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), CTRL);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), VirtualKey::V, true);
|
||||
app.hid_provider
|
||||
.send_key_routed(app.wvr_server.as_mut(), VirtualKey::V, false);
|
||||
app.hid_provider
|
||||
.set_modifiers_routed(app.wvr_server.as_mut(), original_keyboard_mods);
|
||||
}
|
||||
pub fn new() -> anyhow::Result<(SwipeTypingManager, Receiver<Option<Vec<String>>>)> {
|
||||
let (candidate_sender, candidate_receiver) = sync_channel(1);
|
||||
let (task_sender, task_receiver) = channel::<PredictionTask>();
|
||||
|
||||
// Spawn persistent worker thread
|
||||
let worker_candidate_sender = candidate_sender.clone();
|
||||
let worker_thread = thread::spawn(move || {
|
||||
let mut swipe_engine = match SwipeOrchestrator::new() {
|
||||
Ok(engine) => engine,
|
||||
Err(e) => {
|
||||
log::error!("Failed to initialize SwipeOrchestrator: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
while let Ok(task) = task_receiver.recv() {
|
||||
match task {
|
||||
PredictionTask::Predict { swipe, last_word } => {
|
||||
match swipe_engine.predict(swipe, &last_word) {
|
||||
Ok(candidates) => {
|
||||
let words: Vec<String> = candidates
|
||||
.into_iter()
|
||||
.take(PREDICTION_SUGGESTION_COUNT)
|
||||
.map(|c| c.word)
|
||||
.collect();
|
||||
|
||||
let _ = worker_candidate_sender.send(Some(words));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Prediction failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
PredictionTask::Shutdown => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
keyboard_gird: QwertyKeyboardGrid::new(),
|
||||
current_swipe: Vec::new(),
|
||||
swipe_candidate_sender: candidate_sender,
|
||||
prediction_task_sender: task_sender,
|
||||
worker_thread: Some(worker_thread),
|
||||
swipe_start_time: None,
|
||||
clipboard: Clipboard::new()?,
|
||||
swipe_left_first_key: false,
|
||||
first_swipe_char: char::default(),
|
||||
current_swipe_device: None,
|
||||
last_swiped_word: None,
|
||||
},
|
||||
candidate_receiver,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn predict(&mut self) -> anyhow::Result<()> {
|
||||
if self.is_current_swipe_empty() {
|
||||
bail!("nothing to predict");
|
||||
}
|
||||
|
||||
let current_swipe = mem::take(&mut self.current_swipe);
|
||||
let last_word = self.last_swiped_word.clone();
|
||||
self.reset_swipe();
|
||||
|
||||
self.prediction_task_sender.send(PredictionTask::Predict {
|
||||
swipe: current_swipe,
|
||||
last_word,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.reset_swipe();
|
||||
let _ = self.swipe_candidate_sender.send(None);
|
||||
self.last_swiped_word = None;
|
||||
}
|
||||
|
||||
fn reset_swipe(&mut self) {
|
||||
self.swipe_start_time = None;
|
||||
self.current_swipe = Vec::new();
|
||||
self.first_swipe_char = char::default();
|
||||
self.swipe_left_first_key = false;
|
||||
self.current_swipe_device = None;
|
||||
}
|
||||
|
||||
fn start_swipe(&mut self, key_label: char, device: usize) -> Instant {
|
||||
let now = Instant::now();
|
||||
self.swipe_start_time = Some(now);
|
||||
self.first_swipe_char = key_label.to_ascii_lowercase();
|
||||
self.current_swipe_device = Some(device);
|
||||
now
|
||||
}
|
||||
|
||||
pub fn did_swipe_leave_first_key(&self) -> bool {
|
||||
self.swipe_left_first_key
|
||||
}
|
||||
|
||||
pub fn is_current_swipe_empty(&self) -> bool {
|
||||
self.current_swipe.is_empty()
|
||||
}
|
||||
|
||||
pub fn add_swipe(&mut self, within_key_pos_normalized: &Vec2, key_label: char, device: usize) {
|
||||
if let Some(pos) = self
|
||||
.keyboard_gird
|
||||
.key_positions
|
||||
.get(&key_label.to_ascii_lowercase())
|
||||
{
|
||||
if let Some(current_device) = self.current_swipe_device {
|
||||
if current_device != device {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.first_swipe_char != char::default()
|
||||
&& self.first_swipe_char != key_label.to_ascii_lowercase()
|
||||
{
|
||||
self.swipe_left_first_key = true;
|
||||
}
|
||||
|
||||
let key_pos = Vec2 {
|
||||
x: pos.x as f32,
|
||||
y: pos.y as f32,
|
||||
};
|
||||
|
||||
let start_time = match self.swipe_start_time {
|
||||
Some(time) => time,
|
||||
None => self.start_swipe(key_label, device),
|
||||
};
|
||||
|
||||
let within_key_pos_from_center = Vec2 {
|
||||
x: within_key_pos_normalized.x - 0.5,
|
||||
y: within_key_pos_normalized.y - 0.5,
|
||||
};
|
||||
let key_dimensions = Vec2 {
|
||||
x: QwertyKeyboardGrid::get_key_width() as f32,
|
||||
y: QwertyKeyboardGrid::get_key_height() as f32,
|
||||
};
|
||||
|
||||
let point = within_key_pos_from_center * key_dimensions + key_pos;
|
||||
let duration = Instant::now().duration_since(start_time).mul_f32(0.8); // multiply by .8 because library is trained on mobile swipes which happen on a smaller keyboard and are faster
|
||||
self.current_swipe
|
||||
.push(SwipePoint::new(point.x.into(), point.y.into(), duration))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SwipeTypingManager {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.prediction_task_sender.send(PredictionTask::Shutdown);
|
||||
if let Some(handle) = self.worker_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ use glam::{Affine3A, Quat, Vec3, vec3};
|
|||
use wgui::{
|
||||
assets::AssetPath,
|
||||
components::button::ComponentButton,
|
||||
event::{CallbackDataCommon, EventAlterables, StyleSetRequest},
|
||||
event::StyleSetRequest,
|
||||
parser::{Fetchable, ParseDocumentParams},
|
||||
taffy,
|
||||
};
|
||||
|
|
@ -48,9 +48,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
|
|||
let mut panel =
|
||||
GuiPanel::new_from_template(app, watch_xml, state, NewGuiPanelParams::default())?;
|
||||
|
||||
let mut alterables = EventAlterables::default();
|
||||
sets_or_overlays(&panel, app, &mut alterables);
|
||||
panel.layout.process_alterables(alterables)?;
|
||||
sets_or_overlays(&mut panel, app);
|
||||
|
||||
let doc_params = ParseDocumentParams {
|
||||
globals: panel.layout.state.globals.clone(),
|
||||
|
|
@ -61,13 +59,10 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
|
|||
panel.on_notify = Some(Box::new({
|
||||
let name = WATCH_NAME;
|
||||
move |panel, app, event_data| {
|
||||
let mut alterables = EventAlterables::default();
|
||||
|
||||
let mut elems_changed = panel.state.overlay_list.on_notify(
|
||||
&mut panel.layout,
|
||||
&mut panel.parser_state,
|
||||
&event_data,
|
||||
&mut alterables,
|
||||
&doc_params,
|
||||
)?;
|
||||
|
||||
|
|
@ -75,7 +70,6 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
|
|||
&mut panel.layout,
|
||||
&mut panel.parser_state,
|
||||
&event_data,
|
||||
&mut alterables,
|
||||
&doc_params,
|
||||
)?;
|
||||
|
||||
|
|
@ -93,16 +87,12 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
|
|||
.parser_state
|
||||
.fetch_component_as::<ComponentButton>("btn_edit_mode")
|
||||
{
|
||||
let mut com = CallbackDataCommon {
|
||||
alterables: &mut alterables,
|
||||
state: &panel.layout.state,
|
||||
};
|
||||
btn_edit_mode.set_sticky_state(&mut com, edit_mode);
|
||||
btn_edit_mode.set_sticky_state(&mut panel.layout.common(), edit_mode);
|
||||
}
|
||||
}
|
||||
OverlayEventData::SettingsChanged => {
|
||||
panel.layout.mark_redraw();
|
||||
sets_or_overlays(panel, app, &mut alterables);
|
||||
sets_or_overlays(panel, app);
|
||||
|
||||
if app.session.config.clock_12h != panel.state.clock_12h {
|
||||
panel.state.clock_12h = app.session.config.clock_12h;
|
||||
|
|
@ -135,7 +125,6 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
|
|||
panel.process_custom_elems(app);
|
||||
}
|
||||
|
||||
panel.layout.process_alterables(alterables)?;
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
|
@ -173,11 +162,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
|
|||
})
|
||||
}
|
||||
|
||||
fn sets_or_overlays(
|
||||
panel: &GuiPanel<WatchState>,
|
||||
app: &mut AppState,
|
||||
alterables: &mut EventAlterables,
|
||||
) {
|
||||
fn sets_or_overlays(panel: &mut GuiPanel<WatchState>, app: &mut AppState) {
|
||||
let display = if app.session.config.sets_on_watch {
|
||||
[taffy::Display::None, taffy::Display::Flex]
|
||||
} else {
|
||||
|
|
@ -196,6 +181,9 @@ fn sets_or_overlays(
|
|||
];
|
||||
|
||||
for i in 0..2 {
|
||||
alterables.set_style(widget[i], StyleSetRequest::Display(display[i]));
|
||||
panel
|
||||
.layout
|
||||
.alterables
|
||||
.set_style(widget[i], StyleSetRequest::Display(display[i]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,37 @@
|
|||
#version 310 es
|
||||
precision highp float;
|
||||
|
||||
layout (location = 0) in vec2 in_uv;
|
||||
layout (location = 0) out vec4 out_color;
|
||||
layout(location = 0) in vec2 in_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
void main()
|
||||
{
|
||||
float fade = max(1.0 - 2.0 * length(in_uv.xy + vec2(-0.5, -0.5)), 0.0);
|
||||
float grid;
|
||||
const float circle_smoothness = 0.0025;
|
||||
const float circle_thickness = 0.01;
|
||||
const float circle_opacity = 0.3;
|
||||
const float circle_size = 0.1;
|
||||
|
||||
if (fract(in_uv.x / 0.0005) < 0.01 || fract(in_uv.y / 0.0005) < 0.01) {
|
||||
grid = 1.0;
|
||||
} else {
|
||||
grid = 0.0;
|
||||
}
|
||||
out_color = vec4(1.0, 1.0, 1.0, grid * fade);
|
||||
float calc_grid(vec2 coord, float m) {
|
||||
vec2 grid = fract(coord * m);
|
||||
return (step(m, grid.x) * step(m, grid.y));
|
||||
}
|
||||
|
||||
float calc_circle(float dist, float size) {
|
||||
float c1 = size;
|
||||
float c2 = size - circle_thickness;
|
||||
|
||||
return smoothstep(c1, c1 - circle_smoothness, dist) *
|
||||
smoothstep(c2 - circle_smoothness, c2, dist);
|
||||
}
|
||||
|
||||
void main() {
|
||||
float dist = length(in_uv.xy + vec2(-0.5, -0.5));
|
||||
float fade = max(1.0 - 2.0 * dist, 0.0);
|
||||
|
||||
float mask = 1.0 - calc_grid(in_uv.xy * 1000.0, 0.02);
|
||||
|
||||
mask = max(mask, (calc_circle(dist, circle_size) +
|
||||
calc_circle(dist, circle_size * 2.0) +
|
||||
calc_circle(dist, circle_size * 3.0)) *
|
||||
circle_opacity);
|
||||
|
||||
out_color = vec4(1.0, 1.0, 1.0, mask * fade);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,3 +32,10 @@ pub mod frag_srgb {
|
|||
path: "src/shaders/srgb.frag",
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_sky {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
path: "src/shaders/sky.frag",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,368 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 vUV;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
float gauss(float x, float sigma)
|
||||
{
|
||||
float t = x / sigma;
|
||||
return exp(-t * t);
|
||||
}
|
||||
|
||||
float smoothBand(float x, float lo, float hi, float featherLo, float featherHi)
|
||||
{
|
||||
float a = smoothstep(lo - featherLo, lo + featherLo, x);
|
||||
float b = 1.0 - smoothstep(hi - featherHi, hi + featherHi, x);
|
||||
return a * b;
|
||||
}
|
||||
|
||||
float thinLine(float x, float halfWidth)
|
||||
{
|
||||
float d = abs(x);
|
||||
float aa = max(fwidth(x) * 1.5, 1e-5);
|
||||
|
||||
float core = 1.0 - smoothstep(0.0, halfWidth + aa, d);
|
||||
float edge = 1.0 - smoothstep(halfWidth, halfWidth + aa, d);
|
||||
|
||||
return core * edge;
|
||||
}
|
||||
|
||||
float hash11(float p)
|
||||
{
|
||||
return fract(sin(p * 127.1 + 311.7) * 43758.5453123);
|
||||
}
|
||||
|
||||
float hash12(vec2 p)
|
||||
{
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise1Periodic(float x, float period)
|
||||
{
|
||||
float i0 = floor(x);
|
||||
float i1 = i0 + 1.0;
|
||||
float f = fract(x);
|
||||
float u = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float a = hash11(mod(i0, period));
|
||||
float b = hash11(mod(i1, period));
|
||||
return mix(a, b, u);
|
||||
}
|
||||
|
||||
float fbm1Periodic(float x, float period)
|
||||
{
|
||||
float sum = 0.0;
|
||||
float amp = 0.5;
|
||||
float norm = 0.0;
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
sum += amp * noise1Periodic(x, period);
|
||||
norm += amp;
|
||||
x *= 2.0;
|
||||
period *= 2.0;
|
||||
amp *= 0.5;
|
||||
}
|
||||
|
||||
return sum / norm;
|
||||
}
|
||||
|
||||
float noise2TileX(vec2 p, float periodX)
|
||||
{
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float x0 = mod(i.x, periodX);
|
||||
float x1 = mod(i.x + 1.0, periodX);
|
||||
float y0 = i.y;
|
||||
float y1 = i.y + 1.0;
|
||||
|
||||
float a = hash12(vec2(x0, y0));
|
||||
float b = hash12(vec2(x1, y0));
|
||||
float c = hash12(vec2(x0, y1));
|
||||
float d = hash12(vec2(x1, y1));
|
||||
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
float fbm2TileX(vec2 p, float periodX)
|
||||
{
|
||||
float sum = 0.0;
|
||||
float amp = 0.5;
|
||||
float norm = 0.0;
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
sum += amp * noise2TileX(p, periodX);
|
||||
norm += amp;
|
||||
p *= 2.0;
|
||||
periodX *= 2.0;
|
||||
amp *= 0.5;
|
||||
}
|
||||
|
||||
return sum / norm;
|
||||
}
|
||||
|
||||
vec3 tonemapACESApprox(vec3 x)
|
||||
{
|
||||
const float a = 2.51;
|
||||
const float b = 0.03;
|
||||
const float c = 2.43;
|
||||
const float d = 0.59;
|
||||
const float e = 0.14;
|
||||
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float independentPath(
|
||||
float u,
|
||||
float baseLat,
|
||||
float warpAmp0, float warpFreq0, float warpPhase0,
|
||||
float warpAmp1, float warpFreq1, float warpPhase1,
|
||||
float amp0, float freq0, float phase0,
|
||||
float amp1, float freq1, float phase1,
|
||||
float amp2, float freq2, float phase2,
|
||||
out float uw)
|
||||
{
|
||||
uw = fract(u
|
||||
+ warpAmp0 * (fbm1Periodic(u * warpFreq0 + warpPhase0, warpFreq0) - 0.5)
|
||||
+ warpAmp1 * (fbm1Periodic(u * warpFreq1 + warpPhase1, warpFreq1) - 0.5));
|
||||
|
||||
return baseLat
|
||||
+ amp0 * (fbm1Periodic(uw * freq0 + phase0, freq0) - 0.5)
|
||||
+ amp1 * (fbm1Periodic(uw * freq1 + phase1, freq1) - 0.5)
|
||||
+ amp2 * (fbm1Periodic(uw * freq2 + phase2, freq2) - 0.5);
|
||||
}
|
||||
|
||||
float independentPositive(
|
||||
float u,
|
||||
float baseValue,
|
||||
float amp0, float freq0, float phase0,
|
||||
float amp1, float freq1, float phase1)
|
||||
{
|
||||
return baseValue
|
||||
+ amp0 * fbm1Periodic(u * freq0 + phase0, freq0)
|
||||
+ amp1 * fbm1Periodic(u * freq1 + phase1, freq1);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
float u = fract(vUV.x);
|
||||
float v = 0.5 - vUV.y;
|
||||
|
||||
// background gradient
|
||||
float skyT = smoothstep(-0.50, 0.50, v);
|
||||
vec3 col = mix(vec3(0.00008, 0.00012, 0.00045),
|
||||
vec3(0.0220, 0.0470, 0.0980),
|
||||
skyT);
|
||||
|
||||
col += gauss(v - 0.24, 0.30) * vec3(0.0045, 0.0100, 0.0220) * 0.28;
|
||||
|
||||
// seamless flow field around 360 degrees
|
||||
float flowA = fbm1Periodic(u * 7.0 + 1.3, 7.0);
|
||||
float flowB = fbm1Periodic(u * 13.0 + 5.1, 13.0);
|
||||
|
||||
float uu = fract(u
|
||||
+ 0.045 * (flowA - 0.5)
|
||||
+ 0.020 * (flowB - 0.5));
|
||||
|
||||
float sA = fbm1Periodic(uu * 5.0 + 2.7, 5.0);
|
||||
float sB = fbm1Periodic(uu * 11.0 + 11.4, 11.0);
|
||||
float sC = fbm1Periodic(uu * 23.0 + 4.2, 23.0);
|
||||
float sD = fbm1Periodic(uu * 9.0 + 17.2, 9.0);
|
||||
float sE = fbm1Periodic(uu * 19.0 + 8.3, 19.0);
|
||||
|
||||
float center = -0.012
|
||||
+ 0.100 * (sA - 0.5)
|
||||
+ 0.045 * (sB - 0.5)
|
||||
+ 0.018 * (sC - 0.5);
|
||||
|
||||
float thickness = 0.072 + 0.040 * sD;
|
||||
float skew = 0.014 * (sE - 0.5);
|
||||
|
||||
float mainLo = center - 0.035 - skew;
|
||||
float mainHi = center + thickness + 0.15 * skew;
|
||||
|
||||
float veilU;
|
||||
float veilCenter = independentPath(
|
||||
u, 0.050,
|
||||
0.028, 7.0, 1.1,
|
||||
-0.015, 13.0, 5.4,
|
||||
0.050, 3.0, 2.3,
|
||||
0.030, 11.0, 8.1,
|
||||
0.010, 29.0, 5.7,
|
||||
veilU);
|
||||
|
||||
float veilThickness = independentPositive(
|
||||
veilU, 0.011,
|
||||
0.010, 9.0, 14.2,
|
||||
0.006, 21.0, 3.6);
|
||||
|
||||
float veilLo = veilCenter - 0.004;
|
||||
float veilHi = veilCenter + veilThickness;
|
||||
|
||||
float auroraU;
|
||||
float auroraCenter = independentPath(
|
||||
u, 0.182,
|
||||
0.033, 5.0, 3.9,
|
||||
0.020, 17.0, 7.7,
|
||||
0.060, 4.0, 1.6,
|
||||
0.040, 9.0, 12.4,
|
||||
0.014, 27.0, 6.9,
|
||||
auroraU);
|
||||
|
||||
float auroraThickness = independentPositive(
|
||||
auroraU, 0.034,
|
||||
0.014, 7.0, 4.1,
|
||||
0.008, 19.0, 15.2);
|
||||
|
||||
float auroraLo = auroraCenter - 0.004;
|
||||
float auroraHi = auroraCenter + auroraThickness;
|
||||
|
||||
float mistU;
|
||||
float mistCenter = independentPath(
|
||||
u, 0.255,
|
||||
0.020, 6.0, 2.8,
|
||||
-0.012, 15.0, 9.1,
|
||||
0.030, 4.0, 1.9,
|
||||
0.020, 10.0, 13.2,
|
||||
0.008, 26.0, 7.4,
|
||||
mistU);
|
||||
|
||||
float electric0U;
|
||||
float electric0Center = independentPath(
|
||||
u, 0.004,
|
||||
0.052, 9.0, 2.4,
|
||||
-0.018, 1.0, 11.6,
|
||||
0.098, 3.0, 4.1,
|
||||
0.068, 1.0, 9.7,
|
||||
0.054, 2.0, 15.3,
|
||||
electric0U);
|
||||
|
||||
float electric1U;
|
||||
float electric1Center = independentPath(
|
||||
u, 0.002,
|
||||
0.046, 7.0, 8.3,
|
||||
0.020, 7.0, 3.1,
|
||||
0.080, 4.0, 6.6,
|
||||
0.071, 2.0, 12.2,
|
||||
0.064, 1.0, 5.8,
|
||||
electric1U);
|
||||
|
||||
float electric2U;
|
||||
float electric2Center = independentPath(
|
||||
u, 0.006,
|
||||
0.044, 3.0, 13.7,
|
||||
-0.017, 4.0, 6.4,
|
||||
0.061, 5.0, 10.9,
|
||||
0.082, 6.0, 4.2,
|
||||
0.075, 5.0, 14.8,
|
||||
electric2U);
|
||||
|
||||
float mainTex = fbm2TileX(vec2(uu * 20.0 + 7.0, (v - center) * 8.0 + 13.0), 20.0);
|
||||
float fineTex = fbm2TileX(vec2(uu * 36.0 + 17.0, (v - center) * 14.0 + 29.0), 36.0);
|
||||
|
||||
float veilTex = fbm2TileX(vec2(veilU * 14.0 + 5.0, (v - veilCenter) * 9.0 + 41.0), 14.0);
|
||||
float auroraTex = fbm2TileX(vec2(auroraU * 16.0 + 21.0, (v - auroraCenter) * 11.0 + 57.0), 16.0);
|
||||
float auroraFine = fbm2TileX(vec2(auroraU * 28.0 + 3.0, (v - auroraCenter) * 18.0 + 71.0), 28.0);
|
||||
|
||||
float bodyMod = 0.82 + 0.28 * mainTex + 0.18 * fineTex;
|
||||
float veilMod = 0.80 + 0.30 * veilTex;
|
||||
float auroraMod = 0.78 + 0.24 * auroraTex + 0.16 * auroraFine;
|
||||
|
||||
// nasks
|
||||
float aura = gauss(v - center, 0.14);
|
||||
|
||||
float mainBody = smoothBand(v, mainLo, mainHi, 0.018, 0.024);
|
||||
float mainMid = gauss(v - mix(mainLo, mainHi, 0.42), 0.55 * thickness);
|
||||
float mainHiEdge = gauss(v - mainHi, 0.017);
|
||||
|
||||
float veilBody = smoothBand(v, veilLo, veilHi, 0.014, 0.020);
|
||||
float veilMid = gauss(v - mix(veilLo, veilHi, 0.52), 0.60 * veilThickness);
|
||||
float veilLoEdge = gauss(v - veilLo, 0.012);
|
||||
float veilHiEdge = gauss(v - veilHi, 0.018);
|
||||
|
||||
float auroraBody = smoothBand(v, auroraLo, auroraHi, 0.016, 0.022);
|
||||
float auroraMid = gauss(v - mix(auroraLo, auroraHi, 0.52), 0.62 * auroraThickness);
|
||||
float auroraLoEdge = gauss(v - auroraLo, 0.013);
|
||||
float auroraHiEdge = gauss(v - auroraHi, 0.020);
|
||||
|
||||
float mist = gauss(v - mistCenter, 0.085);
|
||||
|
||||
float veilLat = v - veilCenter;
|
||||
float auroraLat = v - auroraCenter;
|
||||
|
||||
float veilColorNoise =
|
||||
0.60 * fbm2TileX(vec2(veilU * 6.0 + 2.0, veilLat * 72.0 + 11.0), 6.0)
|
||||
+ 0.40 * fbm2TileX(vec2(veilU * 14.0 + 7.0, veilLat * 128.0 + 31.0), 14.0);
|
||||
|
||||
float auroraColorU = fract(
|
||||
auroraU
|
||||
+ 0.035 * (fbm1Periodic(auroraU * 9.0 + 2.7, 9.0) - 0.5)
|
||||
+ 0.018 * (fbm1Periodic(auroraU * 21.0 + 6.1, 21.0) - 0.5)
|
||||
);
|
||||
|
||||
vec3 auroraColorNoise = clamp(vec3(
|
||||
fbm1Periodic(auroraColorU * 5.0 + 1.3, 5.0), // R
|
||||
fbm1Periodic(auroraColorU * 9.0 + 7.8, 9.0), // G
|
||||
fbm1Periodic(auroraColorU * 17.0 + 12.4, 17.0) // B
|
||||
), 0.0, 1.0);
|
||||
|
||||
veilColorNoise = clamp(veilColorNoise, 0.0, 1.0);
|
||||
auroraColorNoise = clamp(auroraColorNoise, 0.0, 1.0);
|
||||
|
||||
vec3 veilBaseColor = 0.2 * mix(vec3(0.070, 0.160, 0.460),
|
||||
vec3(0.140, 0.620, 0.760),
|
||||
0.55 * veilColorNoise);
|
||||
|
||||
vec3 veilPeakColor = 0.5 * mix(vec3(0.110, 0.240, 0.620),
|
||||
vec3(0.180, 0.900, 0.950),
|
||||
0.65 * veilColorNoise);
|
||||
|
||||
vec3 auroraBaseColor = 0.2 * vec3(0.100, 0.640, 0.720) * auroraColorNoise * auroraColorNoise;
|
||||
vec3 auroraPeakColor = 0.5 * vec3(0.180, 0.980, 1.050) * auroraColorNoise * auroraColorNoise;
|
||||
|
||||
float electric0 = thinLine(v - electric0Center, 0.0001);
|
||||
float electric1 = thinLine(v - electric1Center, 0.00015);
|
||||
float electric2 = thinLine(v - electric2Center, 0.0002);
|
||||
|
||||
float electric0Glow = gauss(v - electric0Center, 0.0040);
|
||||
float electric1Glow = gauss(v - electric1Center, 0.0048);
|
||||
float electric2Glow = gauss(v - electric2Center, 0.0052);
|
||||
|
||||
float e0Energy = 0.82 + 0.28 * fbm1Periodic(electric0U * 41.0 + 6.2, 41.0);
|
||||
float e1Energy = 0.80 + 0.25 * fbm1Periodic(electric1U * 47.0 + 10.4, 47.0);
|
||||
float e2Energy = 0.80 + 0.25 * fbm1Periodic(electric2U * 39.0 + 14.9, 39.0);
|
||||
|
||||
// compose
|
||||
col += aura * (0.85 + 0.25 * mainTex) * vec3(0.014, 0.032, 0.080) * 0.95;
|
||||
|
||||
col += mainBody * bodyMod * vec3(0.070, 0.160, 0.420) * 0.5;
|
||||
col += mainMid * bodyMod * vec3(0.120, 0.280, 0.740) * 0.4;
|
||||
col += mainHiEdge * bodyMod * vec3(0.105, 0.240, 0.620) * 0.1;
|
||||
|
||||
col += veilBody * veilMod * veilBaseColor * 1.05;
|
||||
col += veilMid * veilMod * veilPeakColor * 0.90;
|
||||
col += veilLoEdge * veilMod * mix(veilBaseColor, veilPeakColor, 0.55) * 0.80;
|
||||
col += veilHiEdge * veilMod * vec3(0.100, 0.220, 0.580) * 0.20;
|
||||
|
||||
col += auroraBody * auroraMod * auroraBaseColor * 0.95;
|
||||
col += auroraMid * auroraMod * auroraPeakColor * 0.78;
|
||||
col += auroraLoEdge * auroraMod * mix(auroraBaseColor, auroraPeakColor, 0.45) * 0.55;
|
||||
col += auroraHiEdge * auroraMod * vec3(0.080, 0.180, 0.480) * 0.18;
|
||||
|
||||
col += mist * (0.52 + 0.28 * auroraTex) * vec3(0.035, 0.082, 0.220) * 0.20;
|
||||
|
||||
col += electric0Glow * vec3(0.70, 1.10, 1.90) * 0.55;
|
||||
col += electric1Glow * mix(veilBaseColor, veilPeakColor, 0.60) * 1.20;
|
||||
col += electric2Glow * mix(auroraBaseColor, auroraPeakColor, 0.65) * 1.35;
|
||||
|
||||
col += electric0 * e0Energy * vec3(10.0, 10.3, 10.8);
|
||||
col += electric1 * e1Energy * vec3( 6.2, 6.8, 7.4);
|
||||
col += electric2 * e2Energy * vec3( 5.8, 6.5, 7.2);
|
||||
|
||||
//TODO: skipped if rendering to a HDR swapchain
|
||||
col = tonemapACESApprox(col);
|
||||
|
||||
outColor = vec4(col, 1.0);
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ use wgui::{
|
|||
drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex,
|
||||
renderer_vk::context::SharedContext as WSharedContext,
|
||||
};
|
||||
use wlx_common::async_executor::AsyncExecutor;
|
||||
use wlx_common::locale::WayVRLangProvider;
|
||||
use wlx_common::{
|
||||
audio,
|
||||
|
|
@ -38,7 +37,6 @@ use crate::{
|
|||
pub struct AppState {
|
||||
pub session: AppSession,
|
||||
pub tasks: TaskContainer,
|
||||
pub executor: AsyncExecutor,
|
||||
|
||||
pub gfx: Arc<WGfx>,
|
||||
pub gfx_extras: WGfxExtras,
|
||||
|
|
@ -156,13 +154,11 @@ impl AppState {
|
|||
desktop_finder.refresh();
|
||||
|
||||
let lang_provider = WayVRLangProvider::from_config(&session.config);
|
||||
|
||||
let executor = Rc::new(smol::LocalExecutor::new());
|
||||
|
||||
let mut app_state = Self {
|
||||
session,
|
||||
tasks,
|
||||
executor,
|
||||
gfx,
|
||||
gfx_extras,
|
||||
hid_provider,
|
||||
|
|
@ -223,6 +219,8 @@ pub struct AppSession {
|
|||
pub config: GeneralConfig,
|
||||
pub config_dirty: bool,
|
||||
|
||||
pub no_autostart: bool,
|
||||
|
||||
pub toast_topics: IdMap<ToastTopic, ToastDisplayMethod>,
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +243,7 @@ impl AppSession {
|
|||
Self {
|
||||
config,
|
||||
toast_topics,
|
||||
no_autostart: false,
|
||||
config_dirty: false,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -277,7 +277,6 @@ impl HidProvider for UInputProvider {
|
|||
log::error!("send_key: {res}");
|
||||
}
|
||||
}
|
||||
|
||||
fn set_desktop_extent(&mut self, extent: Vec2) {
|
||||
self.desktop_extent = extent;
|
||||
}
|
||||
|
|
@ -361,7 +360,7 @@ pub const META: KeyModifier = 0x80;
|
|||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, IntegerId, EnumString, EnumIter, Hash)]
|
||||
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Copy, IntegerId, EnumString, EnumIter)]
|
||||
pub enum VirtualKey {
|
||||
Escape = 9,
|
||||
N1, // number row
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use crate::subsystem::monado_metrics::proto;
|
|||
|
||||
pub struct MonadoMetricsFd {
|
||||
stream_reader: UnixStream,
|
||||
#[allow(dead_code)]
|
||||
stream_writer: UnixStream,
|
||||
|
||||
records: VecDeque<proto::Record>,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue