Merge branch 'main' into uinput-toast

This commit is contained in:
galister 2026-05-12 22:52:25 +09:00 committed by GitHub
commit f6cd9ad90c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
123 changed files with 4636 additions and 2998 deletions

View File

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

View File

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

View File

@ -1,7 +1,6 @@
name: Build AppImage
on:
workflow_dispatch:
push:
branches:
- 'main'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1220
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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

View File

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

After

Width:  |  Height:  |  Size: 295 B

View File

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

After

Width:  |  Height:  |  Size: 360 B

View File

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

After

Width:  |  Height:  |  Size: 574 B

View File

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

After

Width:  |  Height:  |  Size: 286 B

View File

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

View File

@ -86,7 +86,8 @@
round="8"
flex_grow="1"
width="100%"
overflow_y="scroll"
overflow="scroll"
position="relative"
>
<!-- radial gradient -->
<rectangle

View File

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

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}

View File

@ -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(())
}

View File

@ -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;

View File

@ -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,
})
}
}

View File

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

View File

@ -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;

View File

@ -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(),
})
}
}

View File

@ -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(())
}

View File

@ -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 {})
}
}

View File

@ -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 {})
}
}

View File

@ -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 {})
}
}

View File

@ -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 {})
}
}

View File

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

View File

@ -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 {})
}
}

View File

@ -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")?;

View File

@ -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;

View File

@ -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(

View File

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

View File

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

View File

@ -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)
}

View File

@ -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, &params.title)?;
// mount user-set popup content
(*params.on_content)(PopupContentFuncData {
let closed_callback = on_content_func(PopupContentFuncData {
layout,
handle: popup_handle.clone(),
id_content,
config,
})?;
popup_handle.state.borrow_mut().closed_callback = Some(closed_callback);
Ok(())
}
}

View File

@ -2,6 +2,7 @@ use keyvalues_parser::{Obj, Vdf};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Clone)]
pub struct SteamUtils {
steam_root: PathBuf,
}

View File

@ -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)
}

View File

@ -14,7 +14,11 @@ use wgui::{
};
use wlx_common::{config::GeneralConfig, dash_interface::BoxDashInterface, desktop_finder::DesktopEntry};
use crate::frontend::{FrontendTask, FrontendTasks, SoundType};
use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType},
util::popup_manager::{MountPopupOnceParams, PopupHolder},
views::{ViewTrait, ViewUpdateParams},
};
#[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum PosMode {
@ -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))
}),
)));
}

View File

@ -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(())
}

View File

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

View File

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

View File

@ -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,
})
}
}

View File

@ -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))
}),
)));
}

View File

@ -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(())
}

View File

@ -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<()>;
}

View File

@ -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))
}),
)));
}

View File

@ -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))
}),
)));
}

View File

@ -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(&params.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))
}),
)));
}

View File

@ -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();
}
}

View File

@ -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">

View File

@ -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 (),

View File

@ -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(),

View File

@ -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

View File

@ -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>

View File

@ -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),

View File

@ -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
}

View File

@ -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;

View File

@ -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")]
{

View File

@ -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

View File

@ -56,7 +56,7 @@ pub enum OpenVrTask {
#[cfg(feature = "openxr")]
pub enum OpenXrTask {
SettingsChanged,
EnvironmentChanged,
}
pub enum PlayspaceTask {

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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);

View File

@ -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(())
}

View File

@ -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);
}
}
_ => {}

View File

@ -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);
}

View File

@ -1,5 +1,3 @@
#![allow(dead_code)]
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
#[derive(Debug)]

View File

@ -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);
}
}
}

View File

@ -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())

View File

@ -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,

View File

@ -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(())
}

View File

@ -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()));
}
}

View File

@ -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(())
}
}));

View File

@ -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,

View File

@ -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 } => {

View File

@ -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();
}
}
}

View File

@ -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]));
}
}

View File

@ -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);
}

View File

@ -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",
}
}

368
wayvr/src/shaders/sky.frag Normal file
View File

@ -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);
}

View File

@ -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,
}
}

View File

@ -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

View File

@ -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