Merge remote-tracking branch 'origin/feat-skybox-catalog'

This commit is contained in:
galister 2026-04-22 18:24:12 +09:00
commit 16446f7808
83 changed files with 3089 additions and 734 deletions

1
Cargo.lock generated
View File

@ -1423,6 +1423,7 @@ dependencies = [
"smol", "smol",
"smol-hyper", "smol-hyper",
"strum", "strum",
"uuid",
"wayvr-ipc", "wayvr-ipc",
"wgui", "wgui",
"wlx-common", "wlx-common",

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] [profile.dev]
opt-level = 1 opt-level = 1
debug = true debug = true
@ -19,41 +58,3 @@ incremental = true
[profile.release-with-debug] [profile.release-with-debug]
inherits = "release" inherits = "release"
debug = true 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" repository = "https://github.com/wlx-team/wayvr"
[dependencies] [dependencies]
wayvr-ipc = { path = "../wayvr-ipc", default-features = false }
wgui = { path = "../wgui/" }
wlx-common = { path = "../wlx-common" }
anyhow.workspace = true anyhow.workspace = true
async-native-tls = "0.5.0"
chrono = "0.4.42"
glam = { workspace = true, features = ["mint", "serde"] } 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 log.workspace = true
xdg.workspace = true
rust-embed.workspace = true rust-embed.workspace = true
serde = { workspace = true, features = ["rc"] } serde = { workspace = true, features = ["rc"] }
serde_json.workspace = true 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 } 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" 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] [features]
default = ["monado"] default = ["monado"]

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

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

@ -48,6 +48,7 @@
<div gap="4"> <div gap="4">
<Tabs id="tabs"> <Tabs id="tabs">
<Tab name="look_and_feel" translation="APP_SETTINGS.LOOK_AND_FEEL" sprite_src_builtin="dashboard/palette.svg" /> <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="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="controls" translation="APP_SETTINGS.CONTROLS" sprite_src_builtin="dashboard/controller.svg" />
<Tab name="misc" translation="APP_SETTINGS.MISC" sprite_src_builtin="dashboard/blocks.svg" /> <Tab name="misc" translation="APP_SETTINGS.MISC" sprite_src_builtin="dashboard/blocks.svg" />

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

@ -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">
<label id="label_author" weight="bold"/>
<label id="label_description" wrap="1"/>
<Separator/>
<div gap="24" justify_content="center">
<label id="label_creation_date"/>
<label id="label_modification_date"/>
<label id="label_version"/>
</div>
<div gap="8" flex_direction="column" align_items="center">
<image id="image" width="500" height="250" round="8" border="2" border_color="~color_accent"/>
<div id="resolution_buttons" gap="8" flex_direction="row">
<!-- filled-in at runtime -->
</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_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": "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", "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": "Wayland screen capture",
"CAPTURE_METHOD_HELP": "Try changing this if you are\nexperiencing black or glitchy screens", "CAPTURE_METHOD_HELP": "Try changing this if you are\nexperiencing black or glitchy screens",
"COLOR_KEYING": "Color keying", "COLOR_KEYING": "Color keying",
@ -70,6 +72,7 @@
"LONG_PRESS_DURATION": "Long press duration", "LONG_PRESS_DURATION": "Long press duration",
"LOOK_AND_FEEL": "Look & Feel", "LOOK_AND_FEEL": "Look & Feel",
"MISC": "Miscellaneous", "MISC": "Miscellaneous",
"NO_SKYMAPS_FOUND": "No skymaps found",
"NOTIFICATIONS_ENABLED": "Enable notifications", "NOTIFICATIONS_ENABLED": "Enable notifications",
"NOTIFICATIONS_SOUND_ENABLED": "Notification sounds", "NOTIFICATIONS_SOUND_ENABLED": "Notification sounds",
"OPAQUE_BACKGROUND": "Opaque background", "OPAQUE_BACKGROUND": "Opaque background",
@ -96,7 +99,10 @@
"SCREEN_RENDER_DOWN": "Render screen at lower resolution", "SCREEN_RENDER_DOWN": "Render screen at lower resolution",
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens", "SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
"SCROLL_SPEED": "Scroll speed", "SCROLL_SPEED": "Scroll speed",
"SELECT_VARIANT": "Select variant",
"SETS_ON_WATCH": "Sets on watch", "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_MULTIPLIER": "Space drag multiplier",
"SPACE_DRAG_UNLOCKED": "Allow space drag on all axes", "SPACE_DRAG_UNLOCKED": "Allow space drag on all axes",
"SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes", "SPACE_ROTATE_UNLOCKED": "Allow space rotate on all axes",
@ -134,8 +140,12 @@
"VOLUME": "Volume" "VOLUME": "Volume"
}, },
"CLOSE_WINDOW": "Close window", "CLOSE_WINDOW": "Close window",
"CREATION_DATE": "Creation date",
"DEBUG_INFO": "Debug info", "DEBUG_INFO": "Debug info",
"DISPLAY_BRIGHTNESS": "Display brightness", "DISPLAY_BRIGHTNESS": "Display brightness",
"DOWNLOADER": "Downloader",
"DOWNLOAD_AGAIN": "Download again",
"DOWNLOADING_FILE": "Downloading file...",
"FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:", "FAILED_TO_LAUNCH_APPLICATION": "Failed to launch a application:",
"GAME_LAUNCHED": "Game launched", "GAME_LAUNCHED": "Game launched",
"GAME_LIST": { "GAME_LIST": {
@ -149,7 +159,9 @@
"HELLO_USER": "Hello, {USER}!", "HELLO_USER": "Hello, {USER}!",
"HIDE": "Hide", "HIDE": "Hide",
"HOME_SCREEN": "Home", "HOME_SCREEN": "Home",
"MODIFICATION_DATE": "Modification date",
"MONADO_RUNTIME": "Monado runtime", "MONADO_RUNTIME": "Monado runtime",
"LOADING": "Loading...",
"POPUP_ADD_DISPLAY": { "POPUP_ADD_DISPLAY": {
"RESOLUTION": "Resolution" "RESOLUTION": "Resolution"
}, },
@ -160,8 +172,11 @@
"PROCESS_LIST": "Process list", "PROCESS_LIST": "Process list",
"REFRESH": "Refresh", "REFRESH": "Refresh",
"REMOVE": "Remove", "REMOVE": "Remove",
"RELOAD_FROM_DISK": "Reload from disk",
"SETTINGS": "Settings", "SETTINGS": "Settings",
"SHOW": "Show", "SHOW": "Show",
"TARGET_PATH": "Target path",
"TERMINATE_PROCESS": "Terminate process", "TERMINATE_PROCESS": "Terminate process",
"VERSION": "Version",
"WIDTH": "Width" "WIDTH": "Width"
} }

View File

@ -28,7 +28,7 @@ use crate::{
assets, assets,
tab::{Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, settings::TabSettings}, tab::{Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, settings::TabSettings},
util::{ util::{
popup_manager::{MountPopupParams, PopupManager, PopupManagerParams}, popup_manager::{MountPopupOnceParams, PopupManager, PopupManagerParams},
toast_manager::ToastManager, toast_manager::ToastManager,
}, },
views, views,
@ -101,7 +101,7 @@ pub enum FrontendTask {
SetTab(TabType), SetTab(TabType),
RefreshClock, RefreshClock,
RefreshBackground, RefreshBackground,
MountPopup(MountPopupParams), MountPopupOnce(MountPopupOnceParams),
RefreshPopupManager, RefreshPopupManager,
ShowAudioSettings, ShowAudioSettings,
UpdateAudioSettingsView, UpdateAudioSettingsView,
@ -279,10 +279,8 @@ impl<T: 'static> Frontend<T> {
} }
fn update_time(&mut self, data: &mut T) -> anyhow::Result<()> { 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 let mut label = common
.state .state
.widgets .widgets
@ -303,27 +301,20 @@ impl<T: 'static> Frontend<T> {
label.set_text(&mut common, Translation::from_raw_text(&text)); label.set_text(&mut common, Translation::from_raw_text(&text));
} }
c.finish()?;
Ok(()) 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); let config = self.interface.general_config(data);
self.popup_manager.mount_popup( self
self.globals.clone(), .popup_manager
&mut self.layout, .mount_popup_once(&self.globals, &mut self.layout, &self.tasks, params, config)?;
self.tasks.clone(),
params,
config,
)?;
Ok(()) Ok(())
} }
fn refresh_popup_manager(&mut self) -> anyhow::Result<()> { fn refresh_popup_manager(&mut self) -> anyhow::Result<()> {
let mut c = self.layout.start_common(); self.popup_manager.refresh(&mut self.layout.alterables);
self.popup_manager.refresh(c.common().alterables);
c.finish()?;
Ok(()) Ok(())
} }
@ -356,7 +347,7 @@ impl<T: 'static> Frontend<T> {
FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?, FrontendTask::SetTab(tab_type) => self.set_tab(params.data, tab_type)?,
FrontendTask::RefreshClock => self.update_time(params.data)?, FrontendTask::RefreshClock => self.update_time(params.data)?,
FrontendTask::RefreshBackground => self.update_background(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::RefreshPopupManager => self.refresh_popup_manager()?,
FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?, FrontendTask::ShowAudioSettings => self.action_show_audio_settings()?,
FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?, FrontendTask::UpdateAudioSettingsView => self.action_update_audio_settings()?,
@ -369,8 +360,7 @@ impl<T: 'static> Frontend<T> {
} }
fn set_tab_title(&mut self, translation: &str, icon: &str) -> anyhow::Result<()> { fn set_tab_title(&mut self, translation: &str, icon: &str) -> anyhow::Result<()> {
let mut c = self.layout.start_common(); let mut common = self.layout.common();
let mut common = c.common();
{ {
let mut label = common let mut label = common
@ -386,12 +376,11 @@ impl<T: 'static> Frontend<T> {
.widgets .widgets
.cast_as::<WidgetSprite>(self.widgets.id_sprite_titlebar_icon)?; .cast_as::<WidgetSprite>(self.widgets.id_sprite_titlebar_icon)?;
sprite.set_content( sprite.set_content(
&mut common, common.alterables,
Some(CustomGlyphData::from_assets(&self.globals, AssetPath::BuiltIn(icon))?), Some(CustomGlyphData::from_assets(&self.globals, AssetPath::BuiltIn(icon))?),
); );
} }
c.finish()?;
Ok(()) Ok(())
} }

View File

@ -9,7 +9,6 @@ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::{ButtonClickCallback, ComponentButton}, components::button::{ButtonClickCallback, ComponentButton},
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation,
layout::{WidgetID, WidgetPair}, layout::{WidgetID, WidgetPair},
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
task::Tasks, task::Tasks,
@ -17,18 +16,17 @@ use wgui::{
use wlx_common::desktop_finder::DesktopEntry; use wlx_common::desktop_finder::DesktopEntry;
use crate::{ use crate::{
frontend::{Frontend, FrontendTask, FrontendTasks}, frontend::{Frontend, FrontendTasks},
tab::{Tab, TabType}, tab::{Tab, TabType},
util::popup_manager::{MountPopupParams, PopupHandle}, util::popup_manager::PopupHolder,
views::{self, app_launcher}, views::{self},
}; };
enum Task { #[derive(Clone)]
CloseLauncher, enum Task {}
}
struct State { struct State {
view_launcher: Option<(PopupHandle, views::app_launcher::View)>, view_launcher: PopupHolder<views::app_launcher::View>,
} }
pub struct TabApps<T> { 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<()> { 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() { for task in self.tasks.drain() {
match task { match task {}
Task::CloseLauncher => state.view_launcher = None,
}
} }
self self.app_list.tick(frontend, &self.state, &mut self.parser_state)?;
.app_list
.tick(frontend, &self.state, &self.tasks, &mut self.parser_state)?;
if let Some((_, launcher)) = &mut state.view_launcher { state
launcher.update(&mut frontend.interface, data)?; .view_launcher
} .with_view_res(|view| view.update(&mut frontend.interface, data))?;
Ok(()) Ok(())
} }
} }
@ -79,40 +73,14 @@ fn on_app_click(
globals: WguiGlobals, globals: WguiGlobals,
entry: DesktopEntry, entry: DesktopEntry,
state: Rc<RefCell<State>>, state: Rc<RefCell<State>>,
tasks: Tasks<Task>,
) -> ButtonClickCallback { ) -> ButtonClickCallback {
Rc::new(move |_common, _evt| { Rc::new(move |_common, _evt| {
frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { views::app_launcher::mount_popup(
title: Translation::from_raw_text(&entry.app_name), frontend_tasks.clone(),
on_content: { globals.clone(),
// this is awful entry.clone(),
let state = state.clone(); state.borrow_mut().view_launcher.clone(),
let entry = entry.clone(); );
let globals = globals.clone();
let frontend_tasks = frontend_tasks.clone();
let tasks = tasks.clone();
Rc::new(move |data| {
let on_launched = {
let tasks = tasks.clone();
Box::new(move || tasks.push(Task::CloseLauncher))
};
let view = app_launcher::View::new(app_launcher::Params {
entry: entry.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
config: data.config,
on_launched,
})?;
state.borrow_mut().view_launcher = Some((data.handle, view));
Ok(())
})
},
}));
Ok(()) 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> { pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> {
let globals = frontend.layout.state.globals.clone(); let globals = frontend.layout.state.globals.clone();
let tasks = Tasks::new(); 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 parser_state = wgui::parser::parse_from_assets(&doc_params(globals.clone()), &mut frontend.layout, parent_id)?;
let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?; let app_list_parent = parser_state.fetch_widget(&frontend.layout.state, "app_list_parent")?;
@ -334,7 +304,6 @@ impl AppList {
&mut self, &mut self,
frontend: &mut Frontend<T>, frontend: &mut Frontend<T>,
state: &Rc<RefCell<State>>, state: &Rc<RefCell<State>>,
tasks: &Tasks<Task>,
parser_state: &mut ParserState, parser_state: &mut ParserState,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// load 4 entries for a single frame at most // load 4 entries for a single frame at most
@ -348,7 +317,6 @@ impl AppList {
globals.clone(), globals.clone(),
entry.clone(), entry.clone(),
state.clone(), state.clone(),
tasks.clone(),
)); ));
} else { } else {
break; break;

View File

@ -10,7 +10,7 @@ use crate::{
frontend::Frontend, frontend::Frontend,
tab::{Tab, TabType}, tab::{Tab, TabType},
util::steam_utils::SteamUtils, util::steam_utils::SteamUtils,
views::{game_list, running_games_list}, views::{ViewTrait, ViewUpdateParams, game_list, running_games_list},
}; };
pub struct TabGames<T> { pub struct TabGames<T> {
@ -19,7 +19,6 @@ pub struct TabGames<T> {
view_game_list: game_list::View, view_game_list: game_list::View,
view_running_games_list: running_games_list::View, view_running_games_list: running_games_list::View,
steam_utils: SteamUtils,
marker: PhantomData<T>, marker: PhantomData<T>,
} }
@ -29,9 +28,11 @@ impl<T> Tab<T> for TabGames<T> {
} }
fn update(&mut self, frontend: &mut Frontend<T>, time_ms: u32, _data: &mut T) -> anyhow::Result<()> { fn update(&mut self, frontend: &mut Frontend<T>, time_ms: u32, _data: &mut T) -> anyhow::Result<()> {
self self.view_game_list.update(&mut ViewUpdateParams {
.view_game_list layout: &mut frontend.layout,
.update(&mut frontend.layout, &mut self.steam_utils, &frontend.executor)?; executor: &mut frontend.executor,
})?;
self.view_running_games_list.update(&mut frontend.layout, time_ms)?; self.view_running_games_list.update(&mut frontend.layout, time_ms)?;
Ok(()) Ok(())
} }
@ -54,16 +55,17 @@ impl<T> TabGames<T> {
let game_list_parent = state.get_widget_id("game_list_parent")?; 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 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 { let view_game_list = game_list::View::new(game_list::Params {
executor: frontend.executor.clone(), executor: frontend.executor.clone(),
frontend_tasks: frontend.tasks.clone(), frontend_tasks: frontend.tasks.clone(),
globals: globals.clone(), globals: globals.clone(),
layout: &mut frontend.layout, layout: &mut frontend.layout,
parent_id: game_list_parent, 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 { let view_running_games_list = running_games_list::View::new(running_games_list::Params {
globals: globals.clone(), globals: globals.clone(),
layout: &mut frontend.layout, layout: &mut frontend.layout,
@ -77,7 +79,6 @@ impl<T> TabGames<T> {
view_game_list, view_game_list,
view_running_games_list, view_running_games_list,
marker: PhantomData, marker: PhantomData,
steam_utils,
}) })
} }
} }

View File

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

View File

@ -18,7 +18,7 @@ use wgui::{
}; };
use wlx_common::{ use wlx_common::{
config::GeneralConfig, config::GeneralConfig,
dash_interface::{self, MonadoDumpSessionFrame}, dash_interface::{self, ConfigChangeKind, MonadoDumpSessionFrame},
}; };
use crate::{ use crate::{
@ -175,7 +175,7 @@ impl<T> Tab<T> for TabMonado<T> {
Task::GeneralSettingsChromaUpdate => { Task::GeneralSettingsChromaUpdate => {
if let Subtab::GeneralSettings(tab) = &mut self.subtab { if let Subtab::GeneralSettings(tab) = &mut self.subtab {
tab.chroma_update(frontend.interface.general_config(data)); 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), Task::SetBrightness(brightness) => self.set_brightness(frontend, data, brightness),
@ -281,9 +281,7 @@ impl SubtabGeneralSettings {
// get brightness // get brightness
let slider_brightness = state.fetch_component_as::<ComponentSlider>("slider_brightness")?; let slider_brightness = state.fetch_component_as::<ComponentSlider>("slider_brightness")?;
if let Some(brightness) = frontend.interface.monado_brightness_get(data) { if let Some(brightness) = frontend.interface.monado_brightness_get(data) {
let mut c = frontend.layout.start_common(); slider_brightness.set_value(&mut frontend.layout.common(), brightness * 100.0);
slider_brightness.set_value(&mut c.common(), brightness * 100.0);
c.finish()?;
slider_brightness.on_value_changed({ slider_brightness.on_value_changed({
let tasks = tasks.clone(); let tasks = tasks.clone();
@ -304,8 +302,7 @@ impl SubtabGeneralSettings {
let slider_keying_value_range = state.fetch_component_as::<ComponentSlider>("slider_keying_value_range")?; let slider_keying_value_range = state.fetch_component_as::<ComponentSlider>("slider_keying_value_range")?;
{ {
let mut lc = frontend.layout.start_common(); let mut common = frontend.layout.common();
let mut common = lc.common();
// set initial values // set initial values
let (rgb, range_h, range_s, range_v) = config.chroma_key_params.get_rgb_and_hsv_ranges(); let (rgb, range_h, range_s, range_v) = config.chroma_key_params.get_rgb_and_hsv_ranges();
@ -340,8 +337,6 @@ impl SubtabGeneralSettings {
slider_keying_saturation_range.on_value_changed(get_slider_callback(&tasks)); slider_keying_saturation_range.on_value_changed(get_slider_callback(&tasks));
slider_keying_value_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)); cs_keying.on_changed(get_color_selector_callback(&tasks));
lc.finish()?;
} }
Ok(Self { Ok(Self {

View File

@ -5,7 +5,6 @@ use wgui::{
assets::AssetPath, assets::AssetPath,
components::tabs::ComponentTabs, components::tabs::ComponentTabs,
drawing, drawing,
event::{CallbackDataCommon, EventAlterables},
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
@ -20,11 +19,12 @@ use wgui::{
}, },
windowing::context_menu::{self, Blueprint, ContextMenu, TickResult}, 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::{ use crate::{
frontend::{Frontend, FrontendTask}, frontend::{Frontend, FrontendTask, FrontendTasks},
tab::{Tab, TabType, settings::macros::MacroParams}, tab::{Tab, TabType, settings::macros::MacroParams},
views::ViewUpdateParams,
}; };
mod macros; mod macros;
@ -33,6 +33,7 @@ mod tab_controls;
mod tab_features; mod tab_features;
mod tab_look_and_feel; mod tab_look_and_feel;
mod tab_misc; mod tab_misc;
mod tab_skybox;
mod tab_troubleshooting; mod tab_troubleshooting;
#[derive(Clone)] #[derive(Clone)]
@ -43,6 +44,7 @@ enum TabNameEnum {
Misc, Misc,
AutostartApps, AutostartApps,
Troubleshooting, Troubleshooting,
Skybox,
} }
impl TabNameEnum { impl TabNameEnum {
@ -54,6 +56,7 @@ impl TabNameEnum {
"misc" => Some(TabNameEnum::Misc), "misc" => Some(TabNameEnum::Misc),
"autostart_apps" => Some(TabNameEnum::AutostartApps), "autostart_apps" => Some(TabNameEnum::AutostartApps),
"troubleshooting" => Some(TabNameEnum::Troubleshooting), "troubleshooting" => Some(TabNameEnum::Troubleshooting),
"skybox" => Some(TabNameEnum::Skybox),
_ => None, _ => None,
} }
} }
@ -75,14 +78,29 @@ enum Task {
SetTab(TabNameEnum), 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 struct TabSettings<T> {
pub state: ParserState, pub state: ParserState,
app_button_ids: Vec<Rc<str>>, app_button_ids: Vec<Rc<str>>,
context_menu: ContextMenu, context_menu: ContextMenu,
current_tab: Option<Box<dyn SettingsTab>>,
tasks: Tasks<Task>, tasks: Tasks<Task>,
marker: PhantomData<T>, marker: PhantomData<T>,
frontend_tasks: FrontendTasks,
} }
impl<T> Tab<T> for TabSettings<T> { impl<T> Tab<T> for TabSettings<T> {
@ -91,6 +109,13 @@ impl<T> Tab<T> for TabSettings<T> {
} }
fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> { fn update(&mut self, frontend: &mut Frontend<T>, _time_ms: u32, data: &mut T) -> anyhow::Result<()> {
if let Some(tab) = &mut self.current_tab {
tab.update(&mut ViewUpdateParams {
layout: &mut frontend.layout,
executor: &frontend.executor,
})?;
}
let mut changed = false; let mut changed = false;
for task in self.tasks.drain() { for task in self.tasks.drain() {
match task { match task {
@ -179,15 +204,10 @@ impl<T> Tab<T> for TabSettings<T> {
let mut s = name.splitn(5, ';'); let mut s = name.splitn(5, ';');
(s.next(), s.next(), s.next(), s.next(), s.next()) (s.next(), s.next(), s.next(), s.next(), s.next())
} { } {
let mut common = frontend.layout.common();
let mut label = self let mut label = self
.state .state
.fetch_widget_as::<WidgetLabel>(&frontend.layout.state, &format!("{id}_value"))?; .fetch_widget_as::<WidgetLabel>(&common.state, &format!("{id}_value"))?;
let mut alterables = EventAlterables::default();
let mut common = CallbackDataCommon {
alterables: &mut alterables,
state: &frontend.layout.state,
};
let translation = Translation { let translation = Translation {
text: text.into(), text: text.into(),
@ -204,7 +224,7 @@ impl<T> Tab<T> for TabSettings<T> {
// Notify overlays of the change // Notify overlays of the change
if changed { if changed {
frontend.interface.config_changed(data); frontend.interface.config_changed(data, ConfigChangeKind::OverlayConfig);
} }
Ok(()) Ok(())
@ -500,6 +520,7 @@ impl<T> TabSettings<T> {
let root = self.state.get_widget_id("settings_root")?; let root = self.state.get_widget_id("settings_root")?;
frontend.layout.remove_children(root); frontend.layout.remove_children(root);
let globals = frontend.layout.state.globals.clone(); let globals = frontend.layout.state.globals.clone();
self.current_tab = None;
let mut mp = MacroParams { let mut mp = MacroParams {
layout: &mut frontend.layout, layout: &mut frontend.layout,
@ -510,24 +531,36 @@ impl<T> TabSettings<T> {
idx: 9001, idx: 9001,
}; };
let settings_mount_params = SettingsMountParams {
mp: &mut mp,
id_parent: root,
frontend_tasks: &self.frontend_tasks,
};
match name { match name {
TabNameEnum::LookAndFeel => { 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 => { TabNameEnum::Features => {
tab_features::mount(&mut mp, root)?; self.current_tab = Some(Box::new(tab_features::State::mount(settings_mount_params)?));
} }
TabNameEnum::Controls => { TabNameEnum::Controls => {
tab_controls::mount(&mut mp, root)?; self.current_tab = Some(Box::new(tab_controls::State::mount(settings_mount_params)?));
} }
TabNameEnum::Misc => { TabNameEnum::Misc => {
tab_misc::mount(&mut mp, root)?; self.current_tab = Some(Box::new(tab_misc::State::mount(settings_mount_params)?));
} }
TabNameEnum::AutostartApps => { 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 => { 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)?));
} }
} }
@ -562,6 +595,8 @@ impl<T> TabSettings<T> {
state: parser_state, state: parser_state,
marker: PhantomData, marker: PhantomData,
context_menu: ContextMenu::default(), context_menu: ContextMenu::default(),
current_tab: None,
frontend_tasks: frontend.tasks.clone(),
}) })
} }
} }

View File

@ -1,19 +1,33 @@
use std::rc::Rc; use std::rc::Rc;
use crate::tab::settings::macros::{MacroParams, options_autostart_app, options_category}; use crate::tab::settings::{
use wgui::layout::WidgetID; 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<()> { pub struct State {}
*app_button_ids = Vec::new();
if !mp.config.autostart_apps.is_empty() { impl SettingsTab for State {}
let c = options_category(mp, parent, "APP_SETTINGS.AUTOSTART_APPS", "dashboard/apps.svg")?;
// todo: prevent clone impl State {
let autostart_apps = mp.config.autostart_apps.clone(); pub fn mount(par: SettingsMountParams, app_button_ids: &mut Vec<Rc<str>>) -> anyhow::Result<State> {
for app in autostart_apps { *app_button_ids = Vec::new();
options_autostart_app(mp, c, &app.name, app_button_ids)?;
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::{ use crate::tab::settings::{
SettingType, SettingType, SettingsMountParams, SettingsTab,
macros::{MacroParams, options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32}, 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<()> { pub struct State {}
let c = options_category(mp, parent, "APP_SETTINGS.CONTROLS", "dashboard/controller.svg")?;
options_dropdown::<wlx_common::config::AltModifier>(mp, c, &SettingType::KeyboardMiddleClick)?; impl SettingsTab for State {}
options_dropdown::<wlx_common::config::HandsfreePointer>(mp, c, &SettingType::HandsfreePointer)?;
options_checkbox(mp, c, SettingType::FocusFollowsMouseMode)?; impl State {
options_checkbox(mp, c, SettingType::LeftHandedMouse)?; pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
options_checkbox(mp, c, SettingType::AllowSliding)?; let c = options_category(
options_checkbox(mp, c, SettingType::InvertScrollDirectionX)?; par.mp,
options_checkbox(mp, c, SettingType::InvertScrollDirectionY)?; par.id_parent,
options_slider_f32(mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?; "APP_SETTINGS.CONTROLS",
options_slider_f32(mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?; "dashboard/controller.svg",
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_dropdown::<wlx_common::config::AltModifier>(par.mp, c, &SettingType::KeyboardMiddleClick)?;
options_slider_f32(mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1)?; options_dropdown::<wlx_common::config::HandsfreePointer>(par.mp, c, &SettingType::HandsfreePointer)?;
options_slider_i32(mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?; options_checkbox(par.mp, c, SettingType::FocusFollowsMouseMode)?;
Ok(()) 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,19 +1,24 @@
use crate::tab::settings::{ use crate::tab::settings::{
SettingType, macros::{options_category, options_checkbox, options_slider_f32},
macros::{MacroParams, options_category, options_checkbox, options_slider_f32}, SettingType, SettingsMountParams, SettingsTab,
}; };
use wgui::layout::WidgetID;
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { pub struct State {}
let c = options_category(mp, parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
options_checkbox(mp, c, SettingType::NotificationsEnabled)?; impl SettingsTab for State {}
options_checkbox(mp, c, SettingType::NotificationsSoundEnabled)?;
options_checkbox(mp, c, SettingType::KeyboardSoundEnabled)?; impl State {
options_checkbox(mp, c, SettingType::SpaceDragUnlocked)?; pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
options_checkbox(mp, c, SettingType::SpaceRotateUnlocked)?; let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
options_slider_f32(mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5)?; options_checkbox(par.mp, c, SettingType::NotificationsEnabled)?;
options_checkbox(mp, c, SettingType::BlockGameInput)?; options_checkbox(par.mp, c, SettingType::NotificationsSoundEnabled)?;
options_checkbox(mp, c, SettingType::BlockGameInputIgnoreWatch)?; options_checkbox(par.mp, c, SettingType::KeyboardSoundEnabled)?;
options_checkbox(mp, c, SettingType::BlockPosesOnKbdInteraction)?; options_checkbox(par.mp, c, SettingType::SpaceDragUnlocked)?;
Ok(()) 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,30 @@
use crate::tab::settings::{ use crate::tab::settings::{
SettingType, SettingType, SettingsMountParams, SettingsTab,
macros::{MacroParams, options_category, options_checkbox, options_dropdown, options_slider_f32}, macros::{options_category, options_checkbox, options_dropdown, options_slider_f32},
}; };
use wgui::layout::WidgetID;
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { pub struct State {}
let c = options_category(mp, parent, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?;
options_dropdown::<wlx_common::locale::Language>(mp, c, &SettingType::Language)?; impl SettingsTab for State {}
options_checkbox(mp, c, SettingType::OpaqueBackground)?;
options_checkbox(mp, c, SettingType::HideUsername)?; impl State {
options_checkbox(mp, c, SettingType::HideGrabHelp)?; pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
options_slider_f32(mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1)?; // min, max, step let c = options_category(
options_slider_f32(mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05)?; // min, max, step par.mp,
options_slider_f32(mp, c, SettingType::UiRoundMultiplier, 0.5, 5.0, 0.1)?; par.id_parent,
options_checkbox(mp, c, SettingType::SetsOnWatch)?; "APP_SETTINGS.LOOK_AND_FEEL",
options_checkbox(mp, c, SettingType::UseSkybox)?; "dashboard/palette.svg",
options_slider_f32(mp, c, SettingType::GridOpacity, 0.0, 1.0, 0.05)?; // min, max, step )?;
options_checkbox(mp, c, SettingType::UsePassthrough)?; options_dropdown::<wlx_common::locale::Language>(par.mp, c, &SettingType::Language)?;
options_checkbox(mp, c, SettingType::Clock12h)?; options_checkbox(par.mp, c, SettingType::HideUsername)?;
Ok(()) 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_slider_f32(par.mp, c, SettingType::GridOpacity, 0.0, 1.0, 0.05)?; // min, max, step
options_checkbox(par.mp, c, SettingType::UsePassthrough)?;
options_checkbox(par.mp, c, SettingType::Clock12h)?;
Ok(State {})
}
} }

View File

@ -1,15 +1,20 @@
use crate::tab::settings::{ use crate::tab::settings::{
SettingType, SettingType, SettingsMountParams, SettingsTab,
macros::{MacroParams, options_category, options_checkbox, options_dropdown}, macros::{options_category, options_checkbox, options_dropdown},
}; };
use wgui::layout::WidgetID;
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { pub struct State {}
let c = options_category(mp, parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
options_dropdown::<wlx_common::config::CaptureMethod>(mp, c, &SettingType::CaptureMethod)?; impl SettingsTab for State {}
options_checkbox(mp, c, SettingType::XwaylandByDefault)?;
options_checkbox(mp, c, SettingType::UprightScreenFix)?; impl State {
options_checkbox(mp, c, SettingType::DoubleCursorFix)?; pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
options_checkbox(mp, c, SettingType::ScreenRenderDown)?; let c = options_category(par.mp, par.id_parent, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
Ok(()) 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,69 @@
use wgui::{assets::AssetPath, i18n::Translation, layout::Layout, task::Tasks};
use crate::{
frontend::FrontendTasks,
tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox},
},
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::OpaqueBackground)?;
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::{ use crate::tab::settings::{
Task, SettingsMountParams, SettingsTab, Task,
macros::{MacroParams, options_category, options_danger_button}, macros::{options_category, options_danger_button},
}; };
use wgui::layout::WidgetID;
pub fn mount(mp: &mut MacroParams, parent: WidgetID) -> anyhow::Result<()> { pub struct State {}
let c = options_category(mp, parent, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/cpu.svg")?;
options_danger_button( impl SettingsTab for State {}
mp,
c, impl State {
"APP_SETTINGS.RESET_PLAYSPACE", pub fn mount(par: SettingsMountParams) -> anyhow::Result<Self> {
"dashboard/recenter.svg", let c = options_category(
Task::ResetPlayspace, par.mp,
)?; par.id_parent,
options_danger_button( "APP_SETTINGS.TROUBLESHOOTING",
mp, "dashboard/cpu.svg",
c, )?;
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS", options_danger_button(
"dashboard/display.svg", par.mp,
Task::ClearPipewireTokens, c,
)?; "APP_SETTINGS.RESET_PLAYSPACE",
options_danger_button( "dashboard/recenter.svg",
mp, Task::ResetPlayspace,
c, )?;
"APP_SETTINGS.CLEAR_SAVED_STATE", options_danger_button(
"dashboard/binary.svg", par.mp,
Task::ClearSavedState, c,
)?; "APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
options_danger_button( "dashboard/display.svg",
mp, Task::ClearPipewireTokens,
c, )?;
"APP_SETTINGS.DELETE_ALL_CONFIGS", options_danger_button(
"dashboard/circle.svg", par.mp,
Task::DeleteAllConfigs, c,
)?; "APP_SETTINGS.CLEAR_SAVED_STATE",
options_danger_button( "dashboard/binary.svg",
mp, Task::ClearSavedState,
c, )?;
"APP_SETTINGS.RESTART_SOFTWARE", options_danger_button(
"dashboard/refresh.svg", par.mp,
Task::RestartSoftware, c,
)?; "APP_SETTINGS.DELETE_ALL_CONFIGS",
Ok(()) "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 anyhow::Context;
use serde::Deserialize; use serde::Deserialize;
use wlx_common::{async_executor::AsyncExecutor, cache_dir}; use wlx_common::{async_executor::AsyncExecutor, cache_dir};
use crate::util::{http_client, steam_utils::AppID};
pub struct CoverArt { pub struct CoverArt {
// can be empty in case if data couldn't be fetched (use a fallback image then) // can be empty in case if data couldn't be fetched (use a fallback image then)
pub compressed_image_data: Vec<u8>, pub compressed_image_data: Vec<u8>,
@ -24,7 +23,7 @@ pub async fn request_image(executor: AsyncExecutor, app_id: AppID) -> anyhow::Re
app_id app_id
); );
match http_client::get(&executor, &url).await { match http_client::get_simple(&executor, &url).await {
Ok(response) => { Ok(response) => {
log::info!("Success"); log::info!("Success");
cache_dir::set_data(&cache_file_path, &response.data).await?; 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 // Fetch from Steam API
log::info!("Fetching app detail ID {}", app_id); log::info!("Fetching app detail ID {}", app_id);
let url = format!("https://store.steampowered.com/api/appdetails?appids={}", 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 res_utf8 = String::from_utf8(response.data)?;
let root = serde_json::from_str::<serde_json::Value>(&res_utf8)?; let root = serde_json::from_str::<serde_json::Value>(&res_utf8)?;
let body = root.get(&app_id).context("invalid body")?; let body = root.get(&app_id).context("invalid body")?;

View File

@ -1,5 +1,5 @@
pub mod cached_fetcher; pub mod cached_fetcher;
pub mod http_client; pub mod networking;
pub mod pactl_wrapper; pub mod pactl_wrapper;
pub mod popup_manager; pub mod popup_manager;
pub mod steam_utils; pub mod steam_utils;

View File

@ -18,10 +18,33 @@ pub struct HttpClientResponse {
pub data: Vec<u8>, pub data: Vec<u8>,
} }
pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClientResponse> { impl HttpClientResponse {
log::info!("fetching URL \"{}\"", url); 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() let req = Request::builder()
.header( .header(
hyper::header::HOST, hyper::header::HOST,
@ -30,23 +53,56 @@ pub async fn get(executor: &AsyncExecutor, url: &str) -> anyhow::Result<HttpClie
.uri(url) .uri(url)
.body(Empty::new())?; .body(Empty::new())?;
let resp = fetch(executor, req).await?; let resp = fetch(params.executor, req).await?;
if !resp.status().is_success() { if !resp.status().is_success() {
// non-200 HTTP response // non-200 HTTP response
anyhow::bail!("non-200 HTTP response: {}", resp.status().as_str()); 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| { .try_fold(Vec::new(), |mut body, chunk| {
if let Some(chunk) = chunk.data_ref() { if let Some(chunk) = chunk.data_ref() {
bytes_downloaded += chunk.len() as u64;
body.extend_from_slice(chunk); body.extend_from_slice(chunk);
if let Some(on_progress) = &mut on_progress {
on_progress(ProgressFuncData {
bytes_downloaded,
file_size,
})
}
} }
Ok(body) Ok(body)
}) })
.await?; .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( 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,200 @@
#![allow(dead_code)]
use std::path::PathBuf;
// TODO: Remove later
use serde::{Deserialize, Serialize};
use wlx_common::{async_executor::AsyncExecutor, config_io};
use crate::util::networking::{self, WAYVR_SKYMAPS_ROOT, http_client};
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 wlx_common::config::GeneralConfig;
use crate::frontend::{FrontendTask, FrontendTasks}; use crate::{
frontend::{FrontendTask, FrontendTasks},
views::{ViewTrait, ViewUpdateParams},
};
pub struct PopupManagerParams { pub struct PopupManagerParams {
pub parent_id: WidgetID, pub parent_id: WidgetID,
@ -34,15 +37,140 @@ pub struct MountedPopup {
frontend_tasks: FrontendTasks, frontend_tasks: FrontendTasks,
} }
#[derive(Default)]
struct MountedPopupState { struct MountedPopupState {
mounted_popup: Option<MountedPopup>, mounted_popup: Option<MountedPopup>,
closed_callback: Option<PopupClosedCallback>,
} }
#[derive(Clone)] #[derive(Default, Clone)]
pub struct PopupHandle { pub struct PopupHandle {
state: Rc<RefCell<MountedPopupState>>, 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 { impl PopupHandle {
pub fn close(&self) { pub fn close(&self) {
self.state.borrow_mut().mounted_popup = None; // Drop will be called self.state.borrow_mut().mounted_popup = None; // Drop will be called
@ -61,10 +189,26 @@ pub struct PopupContentFuncData<'a> {
pub id_content: WidgetID, 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)] #[derive(Clone)]
pub struct MountPopupParams { pub struct MountPopupOnceParams {
pub title: Translation, title: Translation,
pub on_content: Rc<dyn Fn(PopupContentFuncData) -> anyhow::Result<()>>, 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 { impl Drop for MountedPopup {
@ -78,10 +222,16 @@ impl State {
fn refresh_stack(&mut self, alterables: &mut EventAlterables) { fn refresh_stack(&mut self, alterables: &mut EventAlterables) {
// show only the topmost popup // show only the topmost popup
self.popup_stack.retain(|weak| { self.popup_stack.retain(|weak| {
let Some(popup) = weak.upgrade() else { let retain = {
return false; 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() { for (idx, popup) in self.popup_stack.iter().enumerate() {
@ -116,16 +266,13 @@ impl PopupManager {
state.refresh_stack(alterables); state.refresh_stack(alterables);
} }
/// Mount a new popup on top of the existing popup stack. fn mount_popup_prepare(
/// Only the topmost popup is visible. &self,
pub fn mount_popup( globals: &WguiGlobals,
&mut self,
globals: WguiGlobals,
layout: &mut Layout, layout: &mut Layout,
frontend_tasks: FrontendTasks, frontend_tasks: &FrontendTasks,
params: MountPopupParams, popup_title: &Translation,
config: &GeneralConfig, ) -> anyhow::Result<(PopupHandle, WidgetID /* content widget ID */)> {
) -> anyhow::Result<()> {
let doc_params = &ParseDocumentParams { let doc_params = &ParseDocumentParams {
globals: globals.clone(), globals: globals.clone(),
path: AssetPath::BuiltIn("gui/view/popup_window.xml"), 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")?; 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")?; let but_back = state.fetch_component_as::<ComponentButton>("but_back")?;
@ -152,6 +299,7 @@ impl PopupManager {
let mounted_popup_state = MountedPopupState { let mounted_popup_state = MountedPopupState {
mounted_popup: Some(mounted_popup), mounted_popup: Some(mounted_popup),
closed_callback: None,
}; };
let popup_handle = PopupHandle { let popup_handle = PopupHandle {
@ -159,28 +307,57 @@ impl PopupManager {
}; };
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
log::debug!("pushing popup to popup_stack");
state.popup_stack.push(Rc::downgrade(&popup_handle.state)); state.popup_stack.push(Rc::downgrade(&popup_handle.state));
but_back.on_click({ but_back.on_click({
let popup_handle = Rc::downgrade(&popup_handle.state); let popup_handle = Rc::downgrade(&popup_handle.state);
Rc::new(move |_common, _evt| { Rc::new(move |_common, _evt| {
if let Some(popup_handle) = popup_handle.upgrade() { 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(()) Ok(())
}) })
}); });
frontend_tasks.push(FrontendTask::RefreshPopupManager); 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 // mount user-set popup content
(*params.on_content)(PopupContentFuncData { let closed_callback = on_content_func(PopupContentFuncData {
layout, layout,
handle: popup_handle.clone(), handle: popup_handle.clone(),
id_content, id_content,
config, config,
})?; })?;
popup_handle.state.borrow_mut().closed_callback = Some(closed_callback);
Ok(()) Ok(())
} }
} }

View File

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

View File

@ -1,12 +1,51 @@
use glam::{Mat4, Vec2};
use wgui::{ use wgui::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
components::{self, button::ButtonClickCallback},
drawing,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, LayoutTask, WidgetID},
renderer_vk::text::TextStyle, parser::{Fetchable, ParseDocumentParams},
widget::label::{WidgetLabel, WidgetLabelParams}, renderer_vk::{
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
util::centered_matrix,
},
taffy::{self, prelude::length},
widget::{
ConstructEssentials,
label::{WidgetLabel, WidgetLabelParams},
sprite::{WidgetSprite, WidgetSpriteParams},
},
}; };
#[allow(dead_code)] pub struct CreateButtonParams<'a> {
pub fn create_label(layout: &mut Layout, parent: WidgetID, content: Translation) -> anyhow::Result<()> { 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(())
}
pub fn create_label(layout: &mut Layout, id_parent: WidgetID, content: Translation) -> anyhow::Result<()> {
let label = WidgetLabel::create( let label = WidgetLabel::create(
&mut layout.state, &mut layout.state,
WidgetLabelParams { WidgetLabelParams {
@ -18,7 +57,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())?; layout.add_child(parent, label, Default::default())?;
Ok(()) 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 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)] #[derive(Clone, Copy, Eq, PartialEq, EnumString, VariantNames, AsRefStr)]
enum PosMode { enum PosMode {
@ -66,7 +70,7 @@ struct LaunchParams<'a, T> {
interface: &'a mut BoxDashInterface<T>, interface: &'a mut BoxDashInterface<T>,
auto_start: bool, auto_start: bool,
data: &'a mut T, data: &'a mut T,
on_launched: &'a dyn Fn(), on_launched: Option<Box<dyn FnOnce()>>,
} }
pub struct View { pub struct View {
@ -91,7 +95,7 @@ pub struct View {
auto_start: bool, auto_start: bool,
on_launched: Box<dyn Fn()>, on_launched: Option<Box<dyn FnOnce()>>,
} }
pub struct Params<'a> { pub struct Params<'a> {
@ -101,7 +105,13 @@ pub struct Params<'a> {
pub parent_id: WidgetID, pub parent_id: WidgetID,
pub config: &'a GeneralConfig, pub config: &'a GeneralConfig,
pub frontend_tasks: &'a FrontendTasks, 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 { impl View {
@ -280,7 +290,7 @@ impl View {
entry: params.entry, entry: params.entry,
frontend_tasks: params.frontend_tasks.clone(), frontend_tasks: params.frontend_tasks.clone(),
globals: params.globals.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, auto_start: self.auto_start,
interface, interface,
data, 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(); let mut env = Vec::<String>::new();
if params.compositor_mode == CompositorMode::Native { if params.compositor_mode == CompositorMode::Native {
@ -390,7 +400,9 @@ impl View {
params.frontend_tasks.push(FrontendTask::PlaySound(SoundType::Launch)); 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! // we're done!
Ok(()) Ok(())
@ -420,3 +432,26 @@ impl View {
[width as u32, height as u32] [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<()> { 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 { let num: u8 = match &self.mode {
CurrentMode::Sinks => 0, CurrentMode::Sinks => 0,
CurrentMode::Sources => 1, CurrentMode::Sources => 1,
@ -663,20 +660,21 @@ impl View {
CurrentMode::CardProfileSelector(_) => 255, CurrentMode::CardProfileSelector(_) => 255,
}; };
let mut com = layout.common();
let mut perform = |btn_num: u8, btn: &Rc<ComponentButton>| { let mut perform = |btn_num: u8, btn: &Rc<ComponentButton>| {
let color = if num == btn_num { let color = if num == btn_num {
common.state.theme.accent_color com.state.theme.accent_color
} else { } 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(0, &self.btn_sinks);
perform(1, &self.btn_sources); perform(1, &self.btn_sources);
perform(2, &self.btn_cards); perform(2, &self.btn_cards);
c.finish()?;
Ok(()) Ok(())
} }
@ -801,9 +799,7 @@ impl View {
par, par,
)?; )?;
let mut c = params.layout.start_common(); let mut common = params.layout.common();
let mut common = c.common();
let checkbox = data.fetch_component_as::<ComponentCheckbox>("checkbox")?; let checkbox = data.fetch_component_as::<ComponentCheckbox>("checkbox")?;
let btn_mute = data.fetch_component_as::<ComponentButton>("btn_mute")?; let btn_mute = data.fetch_component_as::<ComponentButton>("btn_mute")?;
let slider = data.fetch_component_as::<ComponentSlider>("slider")?; let slider = data.fetch_component_as::<ComponentSlider>("slider")?;
@ -838,8 +834,6 @@ impl View {
}) })
}); });
c.finish()?;
Ok(()) 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::{ use crate::util::{
cached_fetcher::{self, CoverArt}, cached_fetcher::{self, CoverArt},
steam_utils::{self, AppID}, steam_utils::{self, AppID},
wgui_simple,
}; };
pub struct ViewCommon { pub struct ViewCommon {
@ -48,6 +49,7 @@ pub struct Params<'a, 'b> {
pub struct View { pub struct View {
pub button: Rc<ComponentButton>, pub button: Rc<ComponentButton>,
id_image_parent: WidgetID, id_image_parent: WidgetID,
id_loading: WidgetID,
app_name: String, app_name: String,
app_id: AppID, app_id: AppID,
} }
@ -143,6 +145,8 @@ impl View {
layout: &mut Layout, layout: &mut Layout,
cover_art: &CoverArt, cover_art: &CoverArt,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
layout.remove_widget(self.id_loading);
if cover_art.compressed_image_data.is_empty() { if cover_art.compressed_image_data.is_empty() {
// mount placeholder // mount placeholder
let img = view_common.get_placeholder_image()?.clone(); let img = view_common.get_placeholder_image()?.clone();
@ -271,6 +275,12 @@ impl View {
rect_gradient_style(taffy::AlignSelf::End, 0.05), 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 // request cover image data from the internet or disk cache
params params
.executor .executor
@ -286,6 +296,7 @@ impl View {
id_image_parent: image_parent.id, id_image_parent: image_parent.id,
app_name: params.manifest.name.clone(), app_name: params.manifest.name.clone(),
app_id: params.manifest.app_id.clone(), app_id: params.manifest.app_id.clone(),
id_loading,
}) })
} }
} }

View File

@ -4,9 +4,10 @@ use crate::{
frontend::{FrontendTask, FrontendTasks, SoundType}, frontend::{FrontendTask, FrontendTasks, SoundType},
util::{ util::{
cached_fetcher::{self, CoverArt}, cached_fetcher::{self, CoverArt},
popup_manager::{MountPopupOnceParams, PopupHolder},
steam_utils::{self, AppID, AppManifest}, steam_utils::{self, AppID, AppManifest},
}, },
views::game_cover, views::{ViewTrait, ViewUpdateParams, game_cover},
}; };
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
@ -34,13 +35,14 @@ pub struct Params<'a> {
pub layout: &'a mut Layout, pub layout: &'a mut Layout,
pub parent_id: WidgetID, pub parent_id: WidgetID,
pub frontend_tasks: &'a FrontendTasks, pub frontend_tasks: &'a FrontendTasks,
pub on_launched: Box<dyn Fn()>, pub on_launched: Box<dyn FnOnce()>,
} }
pub struct View { pub struct View {
#[allow(dead_code)] #[allow(dead_code)]
state: ParserState, state: ParserState,
tasks: Tasks<Task>, tasks: Tasks<Task>,
on_launched: Box<dyn Fn()>, on_launched: Option<Box<dyn FnOnce()>>,
frontend_tasks: FrontendTasks, frontend_tasks: FrontendTasks,
game_cover_view_common: game_cover::ViewCommon, game_cover_view_common: game_cover::ViewCommon,
@ -48,6 +50,30 @@ pub struct View {
app_id: AppID, 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 { impl View {
async fn fetch_details(executor: AsyncExecutor, tasks: Tasks<Task>, app_id: AppID) { 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 { let Some(details) = cached_fetcher::get_app_details_json(executor, app_id).await else {
@ -104,7 +130,7 @@ impl View {
Ok(Self { Ok(Self {
state, state,
tasks, tasks,
on_launched: params.on_launched, on_launched: Some(params.on_launched),
frontend_tasks: params.frontend_tasks.clone(), frontend_tasks: params.frontend_tasks.clone(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()), game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
view_cover, 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( fn action_fill_app_details(
&mut self, &mut self,
layout: &mut Layout, layout: &mut Layout,
mut details: cached_fetcher::AppDetailsJSONData, mut details: cached_fetcher::AppDetailsJSONData,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut c = layout.start_common();
{ {
let label_author = self.state.fetch_widget(&c.layout.state, "label_author")?.widget; let mut c = layout.common();
let label_description = self.state.fetch_widget(&c.layout.state, "label_description")?.widget; 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() { if let Some(developer) = details.developers.pop() {
label_author label_author
.cast::<WidgetLabel>()? .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 { let desc = if let Some(desc) = &details.short_description {
@ -162,11 +165,10 @@ impl View {
if let Some(desc) = desc { if let Some(desc) = desc {
label_description label_description
.cast::<WidgetLabel>()? .cast::<WidgetLabel>()?
.set_text(&mut c.common(), Translation::from_raw_text(desc)); .set_text(&mut c, Translation::from_raw_text(desc));
} }
} }
c.finish()?;
Ok(()) 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::{ use wgui::{
assets::AssetPath, assets::AssetPath,
@ -16,20 +16,19 @@ use wgui::{
use wlx_common::async_executor::AsyncExecutor; use wlx_common::async_executor::AsyncExecutor;
use crate::{ use crate::{
frontend::{FrontendTask, FrontendTasks}, frontend::FrontendTasks,
util::{ util::{
cached_fetcher::CoverArt, cached_fetcher::CoverArt,
popup_manager::{MountPopupParams, PopupHandle}, popup_manager::PopupHolder,
steam_utils::{self, AppID, SteamUtils}, steam_utils::{self, AppID, SteamUtils},
}, },
views::{self, game_cover, game_launcher}, views::{self, ViewTrait, ViewUpdateParams, game_cover},
}; };
#[derive(Clone)] #[derive(Clone)]
enum Task { enum Task {
AppManifestClicked(steam_utils::AppManifest), AppManifestClicked(steam_utils::AppManifest),
SetCoverArt(AppID, Rc<CoverArt>), SetCoverArt(AppID, Rc<CoverArt>),
CloseLauncher,
LoadManifests, LoadManifests,
FillPage(u32), FillPage(u32),
PrevPage, PrevPage,
@ -42,6 +41,7 @@ pub struct Params<'a> {
pub frontend_tasks: FrontendTasks, pub frontend_tasks: FrontendTasks,
pub layout: &'a mut Layout, pub layout: &'a mut Layout,
pub parent_id: WidgetID, pub parent_id: WidgetID,
pub steam_utils: &'a SteamUtils,
} }
const MAX_GAMES_PER_PAGE: u32 = 30; const MAX_GAMES_PER_PAGE: u32 = 30;
@ -50,10 +50,6 @@ pub struct GameCoverCell {
view_cover: game_cover::View, view_cover: game_cover::View,
} }
struct State {
view_launcher: Option<(PopupHandle, views::game_launcher::View)>,
}
pub struct View { pub struct View {
#[allow(dead_code)] #[allow(dead_code)]
parser_state: ParserState, parser_state: ParserState,
@ -63,12 +59,37 @@ pub struct View {
id_list_parent: WidgetID, id_list_parent: WidgetID,
game_cover_view_common: game_cover::ViewCommon, game_cover_view_common: game_cover::ViewCommon,
executor: AsyncExecutor, executor: AsyncExecutor,
state: Rc<RefCell<State>>,
mounted_game_covers: HashMap<AppID, GameCoverCell>, mounted_game_covers: HashMap<AppID, GameCoverCell>,
all_manifests: Vec<steam_utils::AppManifest>, all_manifests: Vec<steam_utils::AppManifest>,
cur_page: u32, cur_page: u32,
page_count: u32, page_count: u32,
id_label_page: WidgetID, 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 { impl View {
@ -105,46 +126,15 @@ impl View {
id_list_parent: list_parent.id, id_list_parent: list_parent.id,
mounted_game_covers: HashMap::new(), mounted_game_covers: HashMap::new(),
game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()), game_cover_view_common: game_cover::ViewCommon::new(params.globals.clone()),
state: Rc::new(RefCell::new(State { view_launcher: None })),
executor: params.executor, executor: params.executor,
all_manifests: Vec::new(), all_manifests: Vec::new(),
cur_page: 0, cur_page: 0,
page_count: 0, page_count: 0,
id_label_page, 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( fn fill_game_list(
@ -187,8 +177,11 @@ fn fill_game_list(
} }
impl View { impl View {
fn load_manifests(&mut self, steam_utils: &mut SteamUtils) { fn load_manifests(&mut self) {
match steam_utils.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc) { match self
.steam_utils
.list_installed_games(steam_utils::GameSortMethod::PlayDateDesc)
{
Ok(manifests) => { Ok(manifests) => {
self.page_count = (manifests.len() as u32 + MAX_GAMES_PER_PAGE) / MAX_GAMES_PER_PAGE; self.page_count = (manifests.len() as u32 + MAX_GAMES_PER_PAGE) / MAX_GAMES_PER_PAGE;
self.all_manifests = manifests; self.all_manifests = manifests;
@ -233,16 +226,14 @@ impl View {
} }
// set page text // set page text
let mut c = layout.start_common();
{ {
let mut common = c.common(); let mut c = layout.common();
let mut widget = common.state.widgets.cast_as::<WidgetLabel>(self.id_label_page)?; let mut widget = c.state.widgets.cast_as::<WidgetLabel>(self.id_label_page)?;
widget.set_text( widget.set_text(
&mut common, &mut c,
Translation::from_raw_text_string(format!("{}/{}", self.cur_page + 1, self.page_count)), Translation::from_raw_text_string(format!("{}/{}", self.cur_page + 1, self.page_count)),
); );
} }
c.finish()?;
fill_game_list( fill_game_list(
&mut ConstructEssentials { &mut ConstructEssentials {
@ -283,36 +274,13 @@ impl View {
} }
fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> { fn action_app_manifest_clicked(&mut self, manifest: steam_utils::AppManifest) -> anyhow::Result<()> {
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { views::game_launcher::mount_popup(
title: Translation::from_raw_text(&manifest.name), self.frontend_tasks.clone(),
on_content: { self.executor.clone(),
let state = self.state.clone(); self.globals.clone(),
let tasks = self.tasks.clone(); manifest,
let executor = self.executor.clone(); self.view_launcher.clone(),
let globals = self.globals.clone(); );
let frontend_tasks = self.frontend_tasks.clone();
Rc::new(move |data| {
let on_launched = {
let tasks = tasks.clone();
Box::new(move || tasks.push(Task::CloseLauncher))
};
let view = game_launcher::View::new(game_launcher::Params {
manifest: manifest.clone(),
executor: executor.clone(),
globals: &globals,
layout: data.layout,
parent_id: data.id_content,
frontend_tasks: &frontend_tasks,
on_launched,
})?;
state.borrow_mut().view_launcher = Some((data.handle, view));
Ok(())
})
},
}));
Ok(()) Ok(())
} }

View File

@ -1,6 +1,23 @@
use wlx_common::async_executor::AsyncExecutor;
pub mod app_launcher; pub mod app_launcher;
pub mod audio_settings; pub mod audio_settings;
pub mod dialog_box;
pub mod download_file;
pub mod game_cover; pub mod game_cover;
pub mod game_launcher; pub mod game_launcher;
pub mod game_list; pub mod game_list;
pub mod remote_skymap_downloader;
pub mod remote_skymap_list;
pub mod running_games_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 trait ViewTrait {
fn update(&mut self, par: &mut ViewUpdateParams) -> anyhow::Result<()>;
}

View File

@ -0,0 +1,372 @@
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 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};
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),
}
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)?;
}
}
}
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";
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,
},
],
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);
}
_ => 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: 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,253 @@
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_io};
use crate::{
frontend::{FrontendTask, FrontendTasks},
util::{
networking::skymap_catalog::{self, SkymapCatalogEntry, SkymapResolution},
popup_manager::{MountPopupOnceParams, PopupHolder},
wgui_simple,
},
views::{self, ViewTrait, ViewUpdateParams},
};
#[derive(Clone)]
enum Task {
DownloadSkymaps,
Refresh,
ShowSkymapResolutionSelector(SkymapCatalogEntry),
SetSkymap(SkymapCatalogEntry, SkymapResolution),
}
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)?;
}
}
}
}
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) -> anyhow::Result<()> {
let skymap_file_path = entry
.get_destination_path(resolution)
.context("Skymap not found" /* you shouldn't really see this, like ever. */)?;
log::error!(
"not implemented (skymap path to be loaded: {} with resolution {:?})",
skymap_file_path.to_string_lossy(),
resolution
);
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();
if entries.is_empty() {
wgui_simple::create_label(
layout,
self.list_parent,
Translation::from_translation_key("APP_SETTINGS.NO_SKYMAPS_FOUND"),
)?;
return Ok(());
}
let skymaps_root = config_io::get_skymaps_root();
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: 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,157 @@
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: 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.name.clone()),
);
label_author.set_text_simple(
&mut globals.get(),
Translation::from_raw_text_string(format!("by {}", par.entry.author)),
);
}
let id_loading = wgui_simple::create_loading(wgui_simple::CreateLoadingParams {
layout: par.layout,
parent_id: id_image_preview,
with_text: false,
})?;
// Populate resolution pips
populate_res_pips(par.layout, id_resolution_pips, &mut parser_state, &par.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

@ -46,7 +46,6 @@ pub struct TestbedGeneric {
pub parser_state: ParserState, pub parser_state: ParserState,
tasks: Tasks<TestbedTask>, tasks: Tasks<TestbedTask>,
globals: WguiGlobals,
data: Rc<RefCell<Data>>, data: Rc<RefCell<Data>>,
} }
@ -183,7 +182,6 @@ impl TestbedGeneric {
layout, layout,
parser_state, parser_state,
tasks: Default::default(), tasks: Default::default(),
globals: globals.clone(),
data: Rc::new(RefCell::new(Data { data: Rc::new(RefCell::new(Data {
popup_window: WguiWindow::default(), popup_window: WguiWindow::default(),
context_menu: context_menu::ContextMenu::default(), context_menu: context_menu::ContextMenu::default(),

View File

@ -83,7 +83,7 @@ sysinfo = { version = "0.37" }
thiserror = "2.0" thiserror = "2.0"
tracing = "0.1.43" tracing = "0.1.43"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } 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 } wayland-client = { workspace = true }
winit = { version = "0.30.12", optional = true } winit = { version = "0.30.12", optional = true }
xcb = { version = "1.6.0", features = [ xcb = { version = "1.6.0", features = [

View File

@ -212,9 +212,9 @@ pub(super) fn posef_to_transform(pose: &xr::Posef) -> Affine3A {
Affine3A::from_rotation_translation(rotation, translation) 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 { let Some(monado) = app.monado_state.as_ref() else {
return; return false;
}; };
let params = &app.session.config.chroma_key_params; let params = &app.session.config.chroma_key_params;
@ -226,6 +226,10 @@ pub(super) fn reconfigure_chroma_key(app: &AppState) {
params.curve, params.curve,
params.despill, params.despill,
) { ) {
log::warn!("Could not set Chroma Key: {e:?}") log::warn!("Could not set Chroma Key: {e:?}");
return false;
} }
// if all values were non-0, assume we're in chroma key mode
params.hsv_max[0] * params.hsv_max[1] * params.hsv_max[2] * params.curve > 0.001
} }

View File

@ -17,7 +17,7 @@ use crate::{
backend::{ backend::{
BackendError, XrBackend, BackendError, XrBackend,
input::interact, 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}, task::{OpenXrTask, OverlayTask, TaskType},
}, },
config::{save_settings, save_state}, config::{save_settings, save_state},
@ -100,8 +100,6 @@ pub fn openxr_run(
.ok() .ok()
}); });
reconfigure_chroma_key(&app);
let mut blocker = app let mut blocker = app
.monado_state .monado_state
.as_ref() .as_ref()
@ -488,7 +486,7 @@ pub fn openxr_run(
} }
} }
TaskType::OpenXR(task) => { TaskType::OpenXR(task) => {
if matches!(task, OpenXrTask::SettingsChanged) { if matches!(task, OpenXrTask::EnvironmentChanged) {
reconfigure_environment_blend( reconfigure_environment_blend(
&app, &app,
&xr_state, &xr_state,
@ -497,7 +495,6 @@ pub fn openxr_run(
&mut environment_blend_mode, &mut environment_blend_mode,
main_session_visible, main_session_visible,
); );
reconfigure_chroma_key(&app);
} }
} }
#[cfg(feature = "openvr")] #[cfg(feature = "openvr")]
@ -538,6 +535,9 @@ pub(super) enum CompositionLayer<'a> {
Equirect2(xr::CompositionLayerEquirect2KHR<'a, xr::Vulkan>), 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( fn reconfigure_environment_blend(
app: &AppState, app: &AppState,
xr_state: &XrState, xr_state: &XrState,
@ -546,9 +546,11 @@ fn reconfigure_environment_blend(
environment_blend_mode: &mut xr::EnvironmentBlendMode, environment_blend_mode: &mut xr::EnvironmentBlendMode,
main_session_visible: bool, main_session_visible: bool,
) { ) {
let has_chroma_key = try_apply_chroma_key(app);
*environment_blend_mode = { *environment_blend_mode = {
if modes.contains(&xr::EnvironmentBlendMode::ALPHA_BLEND) if modes.contains(&xr::EnvironmentBlendMode::ALPHA_BLEND)
&& app.session.config.use_passthrough && (app.session.config.use_passthrough || has_chroma_key)
{ {
xr::EnvironmentBlendMode::ALPHA_BLEND xr::EnvironmentBlendMode::ALPHA_BLEND
} else { } else {
@ -560,13 +562,14 @@ fn reconfigure_environment_blend(
&& app.session.config.use_skybox && app.session.config.use_skybox
&& !main_session_visible; && !main_session_visible;
if want_skybox == skybox.is_some() {
return;
}
if want_skybox { if want_skybox {
log::debug!("Allocating skybox."); if let Some(curr_skybox) = skybox.as_ref() {
*skybox = create_skybox(xr_state, app); if curr_skybox.needs_recreate(app) {
*skybox = None;
log::debug!("Allocating skybox.");
*skybox = create_skybox(xr_state, app);
}
}
} else { } else {
log::debug!("Destroying skybox."); log::debug!("Destroying skybox.");
*skybox = None; *skybox = None;

View File

@ -19,6 +19,8 @@ impl MonadoState {
Ok(res) Ok(res)
} }
#[allow(clippy::missing_const_for_fn)]
#[allow(clippy::unused_self)]
pub fn update(&mut self) { pub fn update(&mut self) {
#[cfg(feature = "feat-monado-metrics")] #[cfg(feature = "feat-monado-metrics")]
if let Some(metrics) = &mut self.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<()> { pub fn set_metrics_enabled(&mut self, enabled: bool) -> anyhow::Result<()> {
#[cfg(feature = "feat-monado-metrics")] #[cfg(feature = "feat-monado-metrics")]
{ {

View File

@ -29,6 +29,7 @@ pub(super) struct Skybox {
grid: Option<WlxSwapchain>, grid: Option<WlxSwapchain>,
grid_pose: xr::Posef, grid_pose: xr::Posef,
grid_color_scale_bias_khr: Option<Box<xr::sys::CompositionLayerColorScaleBiasKHR>>, grid_color_scale_bias_khr: Option<Box<xr::sys::CompositionLayerColorScaleBiasKHR>>,
current_skybox: Arc<str>,
} }
impl Skybox { impl Skybox {
@ -66,6 +67,12 @@ impl Skybox {
} }
} }
let current_skybox = if maybe_image.is_some() {
app.session.config.skybox_texture.clone()
} else {
"".into()
};
if maybe_image.is_none() { if maybe_image.is_none() {
let p = include_bytes!("../../res/table_mountain_2.dds"); let p = include_bytes!("../../res/table_mountain_2.dds");
maybe_image = Some(command_buffer.upload_image_dds(p.as_slice())?); maybe_image = Some(command_buffer.upload_image_dds(p.as_slice())?);
@ -99,9 +106,14 @@ impl Skybox {
grid: None, grid: None,
grid_pose: translation_rotation_to_posef(Vec3A::ZERO, Quat::from_rotation_x(PI * -0.5)), grid_pose: translation_rotation_to_posef(Vec3A::ZERO, Quat::from_rotation_x(PI * -0.5)),
grid_color_scale_bias_khr, 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>( fn prepare_sky<'a>(
&'a mut self, &'a mut self,
xr: &'a XrState, xr: &'a XrState,

View File

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

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 anyhow::Context;
use button::setup_custom_button; use button::setup_custom_button;
use glam::{Affine2, Vec2, vec2}; use glam::{Affine2, Vec2, vec2};
use idmap::IdMap; use idmap::IdMap;
use label::setup_custom_label; use label::setup_custom_label;
use std::{cell::RefCell, rc::Rc};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::{ components::{
@ -12,9 +22,9 @@ use wgui::{
slider::ComponentSlider, slider::ComponentSlider,
}, },
event::{ event::{
CallbackDataCommon, Event as WguiEvent, EventAlterables, EventCallback, EventListenerID, Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind,
EventListenerKind, InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent,
MouseLeaveEvent, MouseMotionEvent, MouseWheelEvent, MouseMotionEvent, MouseWheelEvent,
}, },
gfx::cmd::WGfxClearMode, gfx::cmd::WGfxClearMode,
i18n::Translation, i18n::Translation,
@ -33,19 +43,6 @@ use wgui::{
use wlx_common::overlays::{BackendAttrib, BackendAttribValue}; use wlx_common::overlays::{BackendAttrib, BackendAttribValue};
use wlx_common::timestep::Timestep; 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 button;
pub mod device_list; pub mod device_list;
mod label; mod label;
@ -493,17 +490,13 @@ pub fn apply_custom_command<T>(
element: &str, element: &str,
command: &ModifyPanelCommand, command: &ModifyPanelCommand,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default(); let mut com = panel.layout.common();
let mut com = CallbackDataCommon {
alterables: &mut alterables,
state: &panel.layout.state,
};
match command { match command {
ModifyPanelCommand::SetText(text) => { ModifyPanelCommand::SetText(text) => {
if let Ok(mut label) = panel if let Ok(mut label) = panel
.parser_state .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)); label.set_text(&mut com, Translation::from_raw_text(text));
} else if let Ok(button) = panel } else if let Ok(button) = panel
@ -516,10 +509,7 @@ pub fn apply_custom_command<T>(
} }
} }
ModifyPanelCommand::SetImage(path) => { ModifyPanelCommand::SetImage(path) => {
if let Ok(pair) = panel if let Ok(pair) = panel.parser_state.fetch_widget(&com.state, element) {
.parser_state
.fetch_widget(&panel.layout.state, element)
{
let data = CustomGlyphData::from_assets( let data = CustomGlyphData::from_assets(
&app.wgui_globals, &app.wgui_globals,
wgui::assets::AssetPath::File(path), wgui::assets::AssetPath::File(path),
@ -527,9 +517,9 @@ pub fn apply_custom_command<T>(
.context("Could not load content from supplied path.")?; .context("Could not load content from supplied path.")?;
if let Some(mut sprite) = pair.widget.get_as::<WidgetSprite>() { 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>() { } 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 { } else {
anyhow::bail!("No <sprite> or <image> with such id."); anyhow::bail!("No <sprite> or <image> with such id.");
} }
@ -541,10 +531,7 @@ pub fn apply_custom_command<T>(
let color = parse_color_hex(color) let color = parse_color_hex(color)
.context("Invalid color format, must be a html hex color!")?; .context("Invalid color format, must be a html hex color!")?;
if let Ok(pair) = panel if let Ok(pair) = panel.parser_state.fetch_widget(&com.state, element) {
.parser_state
.fetch_widget(&panel.layout.state, element)
{
if let Some(mut rect) = pair.widget.get_as::<WidgetRectangle>() { if let Some(mut rect) = pair.widget.get_as::<WidgetRectangle>() {
rect.set_color(&mut com, color); rect.set_color(&mut com, color);
} else if let Some(mut label) = pair.widget.get_as::<WidgetLabel>() { } else if let Some(mut label) = pair.widget.get_as::<WidgetLabel>() {
@ -610,6 +597,5 @@ pub fn apply_custom_command<T>(
} }
} }
panel.layout.process_alterables(alterables)?;
Ok(()) 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 slotmap::{Key, SecondaryMap};
use std::{collections::HashMap, rc::Rc};
use wgui::{ use wgui::{
components::button::ComponentButton, components::button::ComponentButton,
event::{CallbackDataCommon, EventAlterables},
layout::Layout, layout::Layout,
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
}; };
use crate::windowing::{OverlayID, backend::OverlayEventData, window::OverlayCategory};
#[derive(Default)] #[derive(Default)]
/// Helper for managing a list of overlays /// Helper for managing a list of overlays
/// Populates `id="panels_root"` with `<Screen>`, `<Mirror>`, `<Panel>` templates /// Populates `id="panels_root"` with `<Screen>`, `<Mirror>`, `<Panel>` templates
@ -25,7 +22,6 @@ impl OverlayList {
layout: &mut Layout, layout: &mut Layout,
parser_state: &mut ParserState, parser_state: &mut ParserState,
event_data: &OverlayEventData, event_data: &OverlayEventData,
alterables: &mut EventAlterables,
doc_params: &ParseDocumentParams, doc_params: &ParseDocumentParams,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
let mut elements_changed = false; let mut elements_changed = false;
@ -98,11 +94,7 @@ impl OverlayList {
}; };
if meta.visible { if meta.visible {
let mut com = CallbackDataCommon { overlay_button.set_sticky_state(&mut layout.common(), true);
alterables,
state: &layout.state,
};
overlay_button.set_sticky_state(&mut com, true);
} }
self.overlay_buttons.insert(meta.id, overlay_button); self.overlay_buttons.insert(meta.id, overlay_button);
continue; continue;
@ -121,31 +113,23 @@ impl OverlayList {
let overlay_button = parser_state let overlay_button = parser_state
.fetch_component_as::<ComponentButton>(&format!("overlay_{i}"))?; .fetch_component_as::<ComponentButton>(&format!("overlay_{i}"))?;
if meta.visible { if meta.visible {
let mut com = CallbackDataCommon { overlay_button.set_sticky_state(&mut layout.common(), true);
alterables,
state: &layout.state,
};
overlay_button.set_sticky_state(&mut com, true);
} }
self.overlay_buttons.insert(meta.id, overlay_button); self.overlay_buttons.insert(meta.id, overlay_button);
} }
elements_changed = true; elements_changed = true;
} }
OverlayEventData::VisibleOverlaysChanged(overlays) => { OverlayEventData::VisibleOverlaysChanged(overlays) => {
let mut com = CallbackDataCommon {
alterables,
state: &layout.state,
};
let mut overlay_buttons = self.overlay_buttons.clone(); let mut overlay_buttons = self.overlay_buttons.clone();
for visible in overlays.as_ref() { for visible in overlays.as_ref() {
if let Some(btn) = overlay_buttons.remove(*visible) { 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() { 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::{ use wgui::{
components::button::ComponentButton, components::button::ComponentButton,
event::{CallbackDataCommon, EventAlterables},
layout::Layout, layout::Layout,
parser::{Fetchable, ParseDocumentParams, ParserState}, parser::{Fetchable, ParseDocumentParams, ParserState},
}; };
@ -23,25 +22,20 @@ impl SetList {
layout: &mut Layout, layout: &mut Layout,
parser_state: &mut ParserState, parser_state: &mut ParserState,
event_data: &OverlayEventData, event_data: &OverlayEventData,
alterables: &mut EventAlterables,
doc_params: &ParseDocumentParams, doc_params: &ParseDocumentParams,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
let mut elements_changed = false; let mut elements_changed = false;
match event_data { match event_data {
OverlayEventData::ActiveSetChanged(current_set) => { OverlayEventData::ActiveSetChanged(current_set) => {
let mut com = CallbackDataCommon {
alterables,
state: &layout.state,
};
if let Some(old_set) = self.current_set.take() if let Some(old_set) = self.current_set.take()
&& let Some(old_set) = self.set_buttons.get_mut(old_set) && 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 if let Some(new_set) = current_set
&& let Some(new_set) = self.set_buttons.get_mut(*new_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; self.current_set = *current_set;
} }
@ -59,11 +53,7 @@ impl SetList {
let set_button = let set_button =
parser_state.fetch_component_as::<ComponentButton>(&format!("set_{i}"))?; parser_state.fetch_component_as::<ComponentButton>(&format!("set_{i}"))?;
if self.current_set == Some(i) { if self.current_set == Some(i) {
let mut com = CallbackDataCommon { set_button.set_sticky_state(&mut layout.common(), true);
alterables,
state: &layout.state,
};
set_button.set_sticky_state(&mut com, true);
} }
self.set_buttons.push(set_button); self.set_buttons.push(set_button);
} }

View File

@ -16,7 +16,7 @@ use wgui::{
widget::EventResult, widget::EventResult,
}; };
use wlx_common::{ use wlx_common::{
dash_interface::{self, DashInterface, RecenterMode}, dash_interface::{self, ConfigChangeKind, DashInterface, RecenterMode},
locale::WayVRLangProvider, locale::WayVRLangProvider,
overlays::{BackendAttrib, BackendAttribValue}, overlays::{BackendAttrib, BackendAttribValue},
}; };
@ -448,16 +448,22 @@ impl DashInterface<AppState> for DashInterfaceLive {
&mut data.session.config &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; data.session.config_dirty = true;
#[cfg(feature = "openxr")]
{ match kind {
use crate::backend::task::OpenXrTask; ConfigChangeKind::OverlayConfig => data
data.tasks .tasks
.enqueue(TaskType::OpenXR(OpenXrTask::SettingsChanged)); .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) { fn restart(&mut self, _data: &mut AppState) {

View File

@ -10,7 +10,7 @@ use glam::vec2;
use slotmap::Key; use slotmap::Key;
use wgui::{ use wgui::{
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider}, components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
event::{CallbackDataCommon, EventAlterables, EventCallback}, event::EventCallback,
i18n::Translation, i18n::Translation,
parser::Fetchable, parser::Fetchable,
widget::EventResult, widget::EventResult,
@ -466,98 +466,82 @@ fn reset_panel(
*panel.state.id.borrow_mut() = id; *panel.state.id.borrow_mut() = id;
let state = owc.active_state.as_mut().unwrap(); let state = owc.active_state.as_mut().unwrap();
let mut alterables = EventAlterables::default(); let mut com = panel.layout.common();
let mut common = CallbackDataCommon {
alterables: &mut alterables,
state: &panel.layout.state,
};
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentButton>("top_grab")?; .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 let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentSlider>("lerp_slider")?; .fetch_component_as::<ComponentSlider>("lerp_slider")?;
c.set_value(&mut common, state.positioning.get_lerp().unwrap_or(1.0)); c.set_value(&mut com, state.positioning.get_lerp().unwrap_or(1.0));
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentSlider>("alpha_slider")?; .fetch_component_as::<ComponentSlider>("alpha_slider")?;
c.set_value(&mut common, state.alpha); c.set_value(&mut com, state.alpha);
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentSlider>("curve_slider")?; .fetch_component_as::<ComponentSlider>("curve_slider")?;
c.set_value(&mut common, state.curvature.unwrap_or(0.0)); c.set_value(&mut com, state.curvature.unwrap_or(0.0));
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentCheckbox>("additive_box")?; .fetch_component_as::<ComponentCheckbox>("additive_box")?;
c.set_checked(&mut common, state.additive); c.set_checked(&mut com, state.additive);
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentCheckbox>("align_box")?; .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 let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentCheckbox>("global_box")?; .fetch_component_as::<ComponentCheckbox>("global_box")?;
c.set_checked(&mut common, owc.global); c.set_checked(&mut com, owc.global);
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentCheckbox>("angle_fade_box")?; .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 let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentCheckbox>("block_input_box")?; .fetch_component_as::<ComponentCheckbox>("block_input_box")?;
c.set_checked(&mut common, state.block_input); c.set_checked(&mut com, state.block_input);
panel panel.state.pos.reset(&mut com, &state.positioning.into());
.state panel.state.lock.reset(&mut com, state.interactable);
.pos panel.state.tabs.reset(&mut com);
.reset(&mut common, &state.positioning.into());
panel.state.lock.reset(&mut common, state.interactable);
panel.state.tabs.reset(&mut common);
if let Some(stereo) = attrib_value!( if let Some(stereo) = attrib_value!(
owc.backend.get_attrib(BackendAttrib::Stereo), owc.backend.get_attrib(BackendAttrib::Stereo),
BackendAttribValue::Stereo BackendAttribValue::Stereo
) { ) {
panel panel.state.tabs.set_tab_visible(&mut com, "stereo", true);
.state panel.state.stereo.reset(&mut com, &stereo);
.tabs
.set_tab_visible(&mut common, "stereo", true);
panel.state.stereo.reset(&mut common, &stereo);
// Set the checkbox label based on stereo mode // Set the checkbox label based on stereo mode
let translation = get_stereo_full_frame_translation(stereo); let translation = get_stereo_full_frame_translation(stereo);
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentCheckbox>("stereo_full_frame_box")?; .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 { } else {
panel panel.state.tabs.set_tab_visible(&mut com, "stereo", false);
.state
.tabs
.set_tab_visible(&mut common, "stereo", false);
} }
if let Some(mouse) = attrib_value!( if let Some(mouse) = attrib_value!(
owc.backend.get_attrib(BackendAttrib::MouseTransform), owc.backend.get_attrib(BackendAttrib::MouseTransform),
BackendAttribValue::MouseTransform BackendAttribValue::MouseTransform
) { ) {
panel.state.tabs.set_tab_visible(&mut common, "mouse", true); panel.state.tabs.set_tab_visible(&mut com, "mouse", true);
panel.state.mouse.reset(&mut common, &mouse); panel.state.mouse.reset(&mut com, &mouse);
} else { } else {
panel panel.state.tabs.set_tab_visible(&mut com, "mouse", false);
.state
.tabs
.set_tab_visible(&mut common, "mouse", false);
} }
if let Some(full_frame) = attrib_value!( if let Some(full_frame) = attrib_value!(
@ -567,7 +551,7 @@ fn reset_panel(
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentCheckbox>("stereo_full_frame_box")?; .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!( if let Some(adjust_mouse) = attrib_value!(
@ -577,11 +561,9 @@ fn reset_panel(
let c = panel let c = panel
.parser_state .parser_state
.fetch_component_as::<ComponentCheckbox>("stereo_adjust_mouse_box")?; .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(()) Ok(())
} }

View File

@ -105,7 +105,7 @@ where
.widgets .widgets
.get_as::<WidgetSprite>(self.top_sprite_id) .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

@ -17,7 +17,7 @@ use wgui::{
animation::{Animation, AnimationEasing}, animation::{Animation, AnimationEasing},
assets::AssetPath, assets::AssetPath,
drawing::{self, Color}, drawing::{self, Color},
event::{self, CallbackMetadata, EventAlterables, EventListenerKind}, event::{self, CallbackMetadata, EventListenerKind},
layout::LayoutUpdateParams, layout::LayoutUpdateParams,
log::LogErr, log::LogErr,
parser::{Fetchable, ParseDocumentParams}, parser::{Fetchable, ParseDocumentParams},
@ -265,13 +265,10 @@ pub(super) fn create_keyboard_panel(
panel.on_notify = Some(Box::new({ panel.on_notify = Some(Box::new({
let name = "kbd"; let name = "kbd";
move |panel, app, event_data| { move |panel, app, event_data| {
let mut alterables = EventAlterables::default();
let mut elems_changed = panel.state.overlay_list.on_notify( let mut elems_changed = panel.state.overlay_list.on_notify(
&mut panel.layout, &mut panel.layout,
&mut panel.parser_state, &mut panel.parser_state,
&event_data, &event_data,
&mut alterables,
&doc_params, &doc_params,
)?; )?;
@ -279,7 +276,6 @@ pub(super) fn create_keyboard_panel(
&mut panel.layout, &mut panel.layout,
&mut panel.parser_state, &mut panel.parser_state,
&event_data, &event_data,
&mut alterables,
&doc_params, &doc_params,
)?; )?;
@ -322,7 +318,6 @@ pub(super) fn create_keyboard_panel(
panel.process_custom_elems(app); panel.process_custom_elems(app);
} }
panel.layout.process_alterables(alterables)?;
Ok(()) Ok(())
} }
})); }));

View File

@ -4,7 +4,7 @@ use glam::{Affine3A, Quat, Vec3, vec3};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton, components::button::ComponentButton,
event::{CallbackDataCommon, EventAlterables, StyleSetRequest}, event::StyleSetRequest,
parser::{Fetchable, ParseDocumentParams}, parser::{Fetchable, ParseDocumentParams},
taffy, taffy,
}; };
@ -48,9 +48,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
let mut panel = let mut panel =
GuiPanel::new_from_template(app, watch_xml, state, NewGuiPanelParams::default())?; GuiPanel::new_from_template(app, watch_xml, state, NewGuiPanelParams::default())?;
let mut alterables = EventAlterables::default(); sets_or_overlays(&mut panel, app);
sets_or_overlays(&panel, app, &mut alterables);
panel.layout.process_alterables(alterables)?;
let doc_params = ParseDocumentParams { let doc_params = ParseDocumentParams {
globals: panel.layout.state.globals.clone(), 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({ panel.on_notify = Some(Box::new({
let name = WATCH_NAME; let name = WATCH_NAME;
move |panel, app, event_data| { move |panel, app, event_data| {
let mut alterables = EventAlterables::default();
let mut elems_changed = panel.state.overlay_list.on_notify( let mut elems_changed = panel.state.overlay_list.on_notify(
&mut panel.layout, &mut panel.layout,
&mut panel.parser_state, &mut panel.parser_state,
&event_data, &event_data,
&mut alterables,
&doc_params, &doc_params,
)?; )?;
@ -75,7 +70,6 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
&mut panel.layout, &mut panel.layout,
&mut panel.parser_state, &mut panel.parser_state,
&event_data, &event_data,
&mut alterables,
&doc_params, &doc_params,
)?; )?;
@ -93,16 +87,12 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
.parser_state .parser_state
.fetch_component_as::<ComponentButton>("btn_edit_mode") .fetch_component_as::<ComponentButton>("btn_edit_mode")
{ {
let mut com = CallbackDataCommon { btn_edit_mode.set_sticky_state(&mut panel.layout.common(), edit_mode);
alterables: &mut alterables,
state: &panel.layout.state,
};
btn_edit_mode.set_sticky_state(&mut com, edit_mode);
} }
} }
OverlayEventData::SettingsChanged => { OverlayEventData::SettingsChanged => {
panel.layout.mark_redraw(); 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 { if app.session.config.clock_12h != panel.state.clock_12h {
panel.state.clock_12h = app.session.config.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.process_custom_elems(app);
} }
panel.layout.process_alterables(alterables)?;
Ok(()) Ok(())
} }
})); }));
@ -173,11 +162,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
}) })
} }
fn sets_or_overlays( fn sets_or_overlays(panel: &mut GuiPanel<WatchState>, app: &mut AppState) {
panel: &GuiPanel<WatchState>,
app: &mut AppState,
alterables: &mut EventAlterables,
) {
let display = if app.session.config.sets_on_watch { let display = if app.session.config.sets_on_watch {
[taffy::Display::None, taffy::Display::Flex] [taffy::Display::None, taffy::Display::Flex]
} else { } else {
@ -196,6 +181,9 @@ fn sets_or_overlays(
]; ];
for i in 0..2 { 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

@ -9,7 +9,6 @@ use wgui::{
drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex, drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex,
renderer_vk::context::SharedContext as WSharedContext, renderer_vk::context::SharedContext as WSharedContext,
}; };
use wlx_common::async_executor::AsyncExecutor;
use wlx_common::locale::WayVRLangProvider; use wlx_common::locale::WayVRLangProvider;
use wlx_common::{ use wlx_common::{
audio, audio,
@ -38,7 +37,6 @@ use crate::{
pub struct AppState { pub struct AppState {
pub session: AppSession, pub session: AppSession,
pub tasks: TaskContainer, pub tasks: TaskContainer,
pub executor: AsyncExecutor,
pub gfx: Arc<WGfx>, pub gfx: Arc<WGfx>,
pub gfx_extras: WGfxExtras, pub gfx_extras: WGfxExtras,
@ -157,12 +155,9 @@ impl AppState {
let lang_provider = WayVRLangProvider::from_config(&session.config); let lang_provider = WayVRLangProvider::from_config(&session.config);
let executor = Rc::new(smol::LocalExecutor::new());
Ok(Self { Ok(Self {
session, session,
tasks, tasks,
executor,
gfx, gfx,
gfx_extras, gfx_extras,
hid_provider, hid_provider,

View File

@ -8,6 +8,7 @@ use crate::subsystem::monado_metrics::proto;
pub struct MonadoMetricsFd { pub struct MonadoMetricsFd {
stream_reader: UnixStream, stream_reader: UnixStream,
#[allow(dead_code)]
stream_writer: UnixStream, stream_writer: UnixStream,
records: VecDeque<proto::Record>, records: VecDeque<proto::Record>,

View File

@ -133,16 +133,16 @@ impl Animations {
anim.pos_prev = anim.pos; anim.pos_prev = anim.pos;
anim.pos = pos; anim.pos = pos;
anim.call(state, alterables, 1.0);
if anim.last_tick { if anim.last_tick {
anim.call(state, alterables, 1.0);
alterables.needs_redraw = true; alterables.needs_redraw = true;
} else {
anim.ticks_remaining -= 1;
} }
anim.ticks_remaining -= 1;
} }
self.running_animations.retain(|anim| anim.ticks_remaining > 0); self.running_animations.retain(|anim| !anim.last_tick);
} }
pub fn process(&mut self, state: &LayoutState, alterables: &mut EventAlterables, alpha: f32) { pub fn process(&mut self, state: &LayoutState, alterables: &mut EventAlterables, alpha: f32) {

View File

@ -70,11 +70,7 @@ impl ComponentTrait for ComponentBarGraph {
fn refresh(&self, data: &mut RefreshData) { fn refresh(&self, data: &mut RefreshData) {
let state = self.state.borrow(); let state = self.state.borrow();
self.update_limits_text(&state, &mut data.layout.common());
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
self.update_limits_text(&state, &mut lc.common());
let _ = lc.finish();
} }
} }

View File

@ -123,21 +123,16 @@ impl ComponentTrait for ComponentButton {
fn refresh(&self, data: &mut RefreshData) { fn refresh(&self, data: &mut RefreshData) {
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
if state.active_tooltip.is_some() { if state.active_tooltip.is_some() {
let common = lc.common(); let l_state = &data.layout.state;
if let Some(node_id) = common.state.nodes.get(self.base.get_id()) { if let Some(node_id) = l_state.nodes.get(self.base.get_id()) {
if !widget::is_node_visible(&common.state.tree, *node_id) { if !widget::is_node_visible(&l_state.tree, *node_id) {
state.active_tooltip = None; // destroy the tooltip, this button is now hidden state.active_tooltip = None; // destroy the tooltip, this button is now hidden
} }
} else { } else {
debug_assert!(false); debug_assert!(false);
} }
} }
let _ = lc.finish();
} }
} }

View File

@ -7,10 +7,9 @@ use crate::{
}, },
drawing::{self}, drawing::{self},
event::CallbackDataCommon, event::CallbackDataCommon,
globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID, WidgetPair}, layout::{Layout, WidgetID, WidgetPair},
parser::{self, Fetchable, ParseDocumentParams, ParserState}, parser::{self, Fetchable, ParseDocumentParams},
widget::{ConstructEssentials, rectangle::WidgetRectangle, util::WLength}, widget::{ConstructEssentials, rectangle::WidgetRectangle, util::WLength},
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra}, windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra},
}; };
@ -91,18 +90,12 @@ impl ComponentTrait for ComponentColorSelector {
} }
} }
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
let mut common = lc.common();
self.data.button.set_text( self.data.button.set_text(
&mut common, &mut data.layout.common(),
Translation::from_raw_text_string(format!("{}", state.color.to_hex_rgb())), Translation::from_raw_text_string(format!("{}", state.color.to_hex_rgb())),
); );
self.data.button.set_color(&mut common, state.color); self.data.button.set_color(&mut data.layout.common(), state.color);
let _ = lc.finish();
} }
} }
@ -169,12 +162,10 @@ impl ComponentColorSelector {
let slider_b = parser_state.fetch_component_as::<ComponentSlider>("slider_b")?; let slider_b = parser_state.fetch_component_as::<ComponentSlider>("slider_b")?;
{ {
let mut lc = layout.start_common(); let mut common = layout.common();
let common = &mut lc.common(); slider_r.set_value(&mut common, state.color.r * 255.0);
slider_g.set_value(&mut common, state.color.g * 255.0);
slider_r.set_value(common, state.color.r * 255.0); slider_b.set_value(&mut common, state.color.b * 255.0);
slider_g.set_value(common, state.color.g * 255.0);
slider_b.set_value(common, state.color.b * 255.0);
} }
slider_r.on_value_changed(self.gen_slider_callback(ColorIndex::Red)); slider_r.on_value_changed(self.gen_slider_callback(ColorIndex::Red));

View File

@ -147,15 +147,9 @@ impl ComponentTrait for ComponentEditBox {
} }
fn refresh(&self, data: &mut RefreshData) { fn refresh(&self, data: &mut RefreshData) {
// FIXME: refactor this after merging feat-skybox-catalog branch
let mut lc = data.layout.start_common();
let mut common = lc.common();
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
let res = refresh_all(&mut common, &self.data, &mut state); let res = refresh_all(&mut data.layout.common(), &self.data, &mut state);
debug_assert!(res.is_some()); debug_assert!(res.is_some());
let _ = lc.finish();
} }
fn on_focus_change(&self, data: &mut FocusChangeData) { fn on_focus_change(&self, data: &mut FocusChangeData) {

View File

@ -111,13 +111,10 @@ pub struct ComponentSlider {
impl ComponentTrait for ComponentSlider { impl ComponentTrait for ComponentSlider {
fn refresh(&self, data: &mut RefreshData) { fn refresh(&self, data: &mut RefreshData) {
// FIXME: refactor this after merging feat-skybox-catalog branch let mut common = data.layout.common();
let mut lc = data.layout.start_common();
let mut common = lc.common();
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
let value = state.values.value; let value = state.values.value;
state.set_value(&mut common, &self.data, value); state.set_value(&mut common, &self.data, value);
let _ = lc.finish();
} }
fn base(&self) -> &ComponentBase { fn base(&self) -> &ComponentBase {

View File

@ -129,6 +129,11 @@ impl EventAlterables {
self.dirty_widgets.push(widget_id); self.dirty_widgets.push(widget_id);
} }
pub fn mark_dirty_and_redraw(&mut self, widget_id: WidgetID) {
self.mark_dirty(widget_id);
self.mark_redraw();
}
pub fn mark_tick(&mut self, widget_id: WidgetID) { pub fn mark_tick(&mut self, widget_id: WidgetID) {
self.widgets_to_tick.insert(widget_id); self.widgets_to_tick.insert(widget_id);
} }
@ -174,8 +179,7 @@ impl CallbackDataCommon<'_> {
// helper functions // helper functions
pub fn mark_widget_dirty(&mut self, id: WidgetID) { pub fn mark_widget_dirty(&mut self, id: WidgetID) {
self.alterables.mark_dirty(id); self.alterables.mark_dirty_and_redraw(id);
self.alterables.mark_redraw();
} }
pub fn globals(&self) -> RefMut<'_, globals::Globals> { pub fn globals(&self) -> RefMut<'_, globals::Globals> {

View File

@ -86,6 +86,11 @@ impl WidgetMap {
self.0.get(handle) self.0.get(handle)
} }
// same as get(), but with error message
pub fn fetch(&self, handle: WidgetID) -> anyhow::Result<Widget> {
self.get(handle).cloned().context("Failed to fetch widget")
}
pub fn insert(&mut self, obj: Widget) -> WidgetID { pub fn insert(&mut self, obj: Widget) -> WidgetID {
self self
.0 .0
@ -163,6 +168,9 @@ pub struct Layout {
sounds_to_play_once: Vec<WguiSoundType>, sounds_to_play_once: Vec<WguiSoundType>,
focused_component: Option<ComponentWeak>, focused_component: Option<ComponentWeak>,
// Global EventAlterables queue, always processed in update() call at the end
pub alterables: EventAlterables,
pub widgets_to_tick: Vec<WidgetID>, pub widgets_to_tick: Vec<WidgetID>,
// *Main root* // *Main root*
@ -218,31 +226,11 @@ fn add_child_internal(
)) ))
} }
pub struct LayoutCommon<'a> { impl Layout {
alterables: EventAlterables, pub fn common(&mut self) -> CallbackDataCommon<'_> {
pub layout: &'a mut Layout,
}
impl LayoutCommon<'_> {
pub const fn common(&mut self) -> CallbackDataCommon<'_> {
CallbackDataCommon { CallbackDataCommon {
alterables: &mut self.alterables, alterables: &mut self.alterables,
state: &self.layout.state, state: &self.state,
}
}
pub fn finish(self) -> anyhow::Result<()> {
self.layout.process_alterables(self.alterables)?;
Ok(())
}
}
impl Layout {
// helper function
pub fn start_common(&mut self) -> LayoutCommon<'_> {
LayoutCommon {
alterables: EventAlterables::default(),
layout: self,
} }
} }
@ -338,13 +326,8 @@ impl Layout {
self.needs_redraw = true; self.needs_redraw = true;
} }
fn process_pending_components(&mut self, alterables: &mut EventAlterables) { fn process_pending_components(&mut self) {
for comp in std::mem::take(&mut self.components_to_refresh_once) { for comp in std::mem::take(&mut self.components_to_refresh_once) {
let mut common = CallbackDataCommon {
state: &self.state,
alterables,
};
comp.0.refresh(&mut RefreshData { layout: self }); comp.0.refresh(&mut RefreshData { layout: self });
} }
} }
@ -596,6 +579,7 @@ impl Layout {
tasks: LayoutTasks::new(), tasks: LayoutTasks::new(),
sounds_to_play_once: Vec::new(), sounds_to_play_once: Vec::new(),
focused_component: None, focused_component: None,
alterables: Default::default(),
}) })
} }
@ -683,10 +667,12 @@ impl Layout {
} }
pub fn update(&mut self, params: &mut LayoutUpdateParams) -> anyhow::Result<LayoutUpdateResult> { pub fn update(&mut self, params: &mut LayoutUpdateParams) -> anyhow::Result<LayoutUpdateResult> {
let mut alterables = EventAlterables::default(); // get all queued alterables and process them
let alterables = std::mem::take(&mut self.alterables);
self self
.animations .animations
.process(&self.state, &mut alterables, params.timestep_alpha); .process(&self.state, &mut self.alterables, params.timestep_alpha);
self.process_alterables(alterables)?; self.process_alterables(alterables)?;
self.try_recompute_layout(params.size)?; self.try_recompute_layout(params.size)?;
@ -698,7 +684,7 @@ impl Layout {
pub fn tick(&mut self) -> anyhow::Result<()> { pub fn tick(&mut self) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default(); let mut alterables = EventAlterables::default();
self.animations.tick(&self.state, &mut alterables); self.animations.tick(&self.state, &mut alterables);
self.process_pending_components(&mut alterables); self.process_pending_components();
self.process_pending_widget_ticks(&mut alterables); self.process_pending_widget_ticks(&mut alterables);
self.process_alterables(alterables)?; self.process_alterables(alterables)?;
Ok(()) Ok(())
@ -720,9 +706,7 @@ impl Layout {
} }
} }
LayoutTask::Dispatch(func) => { LayoutTask::Dispatch(func) => {
let mut c = self.start_common(); func(&mut self.common())?;
func(&mut c.common())?;
c.finish()?;
} }
LayoutTask::SetWidgetStyle(widget_id, style_request) => { LayoutTask::SetWidgetStyle(widget_id, style_request) => {
self.set_style_request(widget_id, &style_request); self.set_style_request(widget_id, &style_request);
@ -740,29 +724,26 @@ impl Layout {
} }
pub fn set_focus(&mut self, to_focus: Option<&Component>) -> anyhow::Result<()> { pub fn set_focus(&mut self, to_focus: Option<&Component>) -> anyhow::Result<()> {
let mut c = self.start_common(); if let Some(focused) = &self.focused_component
if let Some(focused) = &c.layout.focused_component
&& let Some(focused) = focused.upgrade() && let Some(focused) = focused.upgrade()
{ {
// Unfocus // Unfocus
focused.on_focus_change(&mut FocusChangeData { focused.on_focus_change(&mut FocusChangeData {
common: &mut c.common(), common: &mut self.common(),
focused: false, focused: false,
}); });
c.layout.focused_component = None; self.focused_component = None;
} }
if let Some(to_focus) = to_focus { if let Some(to_focus) = to_focus {
to_focus.0.on_focus_change(&mut FocusChangeData { to_focus.0.on_focus_change(&mut FocusChangeData {
common: &mut c.common(), common: &mut self.common(),
focused: true, focused: true,
}); });
c.layout.focused_component = Some(to_focus.weak()); self.focused_component = Some(to_focus.weak());
} }
c.finish()?;
Ok(()) Ok(())
} }

View File

@ -1,4 +1,4 @@
use crate::components::button::ComponentButton; use crate::components::button::{ButtonClickCallback, ComponentButton};
use std::{cell::RefCell, collections::VecDeque, rc::Rc}; use std::{cell::RefCell, collections::VecDeque, rc::Rc};
pub struct Tasks<TaskType>(Rc<RefCell<VecDeque<TaskType>>>); pub struct Tasks<TaskType>(Rc<RefCell<VecDeque<TaskType>>>);
@ -30,21 +30,31 @@ impl<TaskType: 'static> Default for Tasks<TaskType> {
} }
} }
// copyable tasks only!
impl<TaskType: Clone + 'static> Tasks<TaskType> { impl<TaskType: Clone + 'static> Tasks<TaskType> {
pub fn handle_button(&self, button: &Rc<ComponentButton>, task: TaskType) { pub fn get_button_click_callback(&self, task: TaskType) -> ButtonClickCallback {
button.on_click({ let this = self.clone();
let this = self.clone(); Rc::new(move |_, _| {
Rc::new(move |_, _| { this.push(task.clone());
this.push(task.clone()); Ok(())
Ok(()) })
})
});
} }
pub fn make_callback(&self, task: TaskType) -> Rc<dyn Fn()> { pub fn handle_button(&self, button: &Rc<ComponentButton>, task: TaskType) {
button.on_click(self.get_button_click_callback(task));
}
pub fn make_callback_rc(&self, task: TaskType) -> Rc<dyn Fn()> {
let this = self.clone(); let this = self.clone();
Rc::new(move || { Rc::new(move || {
this.push(task.clone()); this.push(task.clone());
}) })
} }
pub fn make_callback_box(&self, task: TaskType) -> Box<dyn Fn()> {
let this = self.clone();
Box::new(move || {
this.push(task.clone());
})
}
} }

View File

@ -4,7 +4,7 @@ use slotmap::Key;
use crate::{ use crate::{
drawing::{self, ImagePrimitive, PrimitiveExtent}, drawing::{self, ImagePrimitive, PrimitiveExtent},
event::CallbackDataCommon, event::EventAlterables,
globals::Globals, globals::Globals,
layout::WidgetID, layout::WidgetID,
renderer_vk::text::custom_glyph::CustomGlyphData, renderer_vk::text::custom_glyph::CustomGlyphData,
@ -44,13 +44,13 @@ impl WidgetImage {
) )
} }
pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option<CustomGlyphData>) { pub fn set_content(&mut self, alterables: &mut EventAlterables, content: Option<CustomGlyphData>) {
if self.params.glyph_data == content { if self.params.glyph_data == content {
return; return;
} }
self.params.glyph_data = content; self.params.glyph_data = content;
common.mark_widget_dirty(self.id); alterables.mark_dirty_and_redraw(self.id);
} }
pub fn get_content(&self) -> Option<CustomGlyphData> { pub fn get_content(&self) -> Option<CustomGlyphData> {

View File

@ -5,7 +5,7 @@ use slotmap::Key;
use crate::{ use crate::{
drawing::{self, PrimitiveExtent}, drawing::{self, PrimitiveExtent},
event::CallbackDataCommon, event::{CallbackDataCommon, EventAlterables},
globals::Globals, globals::Globals,
layout::WidgetID, layout::WidgetID,
renderer_vk::text::{ renderer_vk::text::{
@ -49,13 +49,13 @@ impl WidgetSprite {
self.params.color self.params.color
} }
pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option<CustomGlyphData>) { pub fn set_content(&mut self, alterables: &mut EventAlterables, content: Option<CustomGlyphData>) {
if self.params.glyph_data == content { if self.params.glyph_data == content {
return; return;
} }
self.params.glyph_data = content; self.params.glyph_data = content;
common.mark_widget_dirty(self.id); alterables.mark_dirty_and_redraw(self.id);
} }
pub fn get_content(&self) -> Option<CustomGlyphData> { pub fn get_content(&self) -> Option<CustomGlyphData> {

View File

@ -9,7 +9,6 @@ use crate::{
components::button::ComponentButton, components::button::ComponentButton,
drawing, drawing,
event::{EventListenerKind, StyleSetRequest}, event::{EventListenerKind, StyleSetRequest},
globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, LayoutTask, LayoutTasks, WidgetPair}, layout::{Layout, LayoutTask, LayoutTasks, WidgetPair},
parser::{self, Fetchable, ParserState}, parser::{self, Fetchable, ParserState},
@ -271,21 +270,20 @@ impl WguiWindow {
content.id content.id
}; };
let mut c = params.layout.start_common();
if let Some(width) = params.extra.fixed_width { if let Some(width) = params.extra.fixed_width {
c.common() params
.layout
.alterables .alterables
.set_style(content_id, StyleSetRequest::Width(length(width))); .set_style(content_id, StyleSetRequest::Width(length(width)));
} }
if let Some(height) = params.extra.fixed_height { if let Some(height) = params.extra.fixed_height {
c.common() params
.layout
.alterables .alterables
.set_style(content_id, StyleSetRequest::Height(length(height))); .set_style(content_id, StyleSetRequest::Height(length(height)));
} }
c.finish()?;
Ok(()) Ok(())
} }

View File

@ -21,6 +21,23 @@ pub fn get_config_root() -> PathBuf {
CONFIG_ROOT_PATH.clone() CONFIG_ROOT_PATH.clone()
} }
pub fn get_skymaps_root() -> PathBuf {
get_config_root().join("skymaps")
}
pub fn get_skymaps_uuids() -> anyhow::Result<Vec<String>> {
let data = std::fs::read_to_string(get_skymaps_root().join("skymaps.txt"))?;
Ok(data.lines().filter(|line| !line.is_empty()).map(String::from).collect())
}
pub fn set_skymaps_uuids(uuids: &[String]) -> anyhow::Result<()> {
let skymaps_root = get_skymaps_root();
let _ = std::fs::create_dir_all(&skymaps_root);
let data = String::from_iter(uuids.iter().map(|uuid| format!("{}\n", uuid)));
std::fs::write(skymaps_root.join("skymaps.txt"), data)?;
Ok(())
}
impl ConfigRoot { impl ConfigRoot {
pub fn get_conf_d_path(&self) -> PathBuf { pub fn get_conf_d_path(&self) -> PathBuf {
get_config_root().join(match self { get_config_root().join(match self {

View File

@ -65,9 +65,15 @@ pub trait DashInterface<T> {
fn recenter_playspace(&mut self, data: &mut T, mode: RecenterMode) -> anyhow::Result<()>; fn recenter_playspace(&mut self, data: &mut T, mode: RecenterMode) -> anyhow::Result<()>;
fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder; fn desktop_finder<'a>(&'a mut self, data: &'a mut T) -> &'a mut DesktopFinder;
fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig; fn general_config<'a>(&'a mut self, data: &'a mut T) -> &'a mut GeneralConfig;
fn config_changed(&mut self, data: &mut T); fn config_changed(&mut self, data: &mut T, kind: ConfigChangeKind);
fn restart(&mut self, data: &mut T); fn restart(&mut self, data: &mut T);
fn toggle_dashboard(&mut self, data: &mut T); fn toggle_dashboard(&mut self, data: &mut T);
} }
#[derive(Clone, Copy)]
pub enum ConfigChangeKind {
OverlayConfig,
EnvironmentBlend,
}
pub type BoxDashInterface<T> = Box<dyn DashInterface<T>>; pub type BoxDashInterface<T> = Box<dyn DashInterface<T>>;

View File

@ -5,7 +5,7 @@ use wayvr_ipc::{
use crate::{ use crate::{
config::GeneralConfig, config::GeneralConfig,
dash_interface::{self, DashInterface, RecenterMode}, dash_interface::{self, ConfigChangeKind, DashInterface, RecenterMode},
desktop_finder::DesktopFinder, desktop_finder::DesktopFinder,
gen_id, gen_id,
}; };
@ -230,7 +230,7 @@ impl DashInterface<()> for DashInterfaceEmulated {
&mut self.general_config &mut self.general_config
} }
fn config_changed(&mut self, _: &mut ()) {} fn config_changed(&mut self, _: &mut (), _: ConfigChangeKind) {}
fn restart(&mut self, _data: &mut ()) {} fn restart(&mut self, _data: &mut ()) {}