Merge remote-tracking branch 'origin/main' into next-dash-interface
[skip ci]
This commit is contained in:
81
Cargo.lock
generated
81
Cargo.lock
generated
@@ -557,7 +557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d123397e75f904758fef490775a00b0ada545ab409cb0163d919799e5a30119b"
|
||||
dependencies = [
|
||||
"autocxx-engine",
|
||||
"env_logger",
|
||||
"env_logger 0.9.3",
|
||||
"indexmap 1.9.3",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
@@ -1751,6 +1751,16 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.3"
|
||||
@@ -1764,6 +1774,19 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equator"
|
||||
version = "0.4.2"
|
||||
@@ -2955,6 +2978,30 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -4272,6 +4319,21 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
@@ -5048,6 +5110,12 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -6376,16 +6444,18 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayvr_ipc"
|
||||
name = "wayvrctl"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/olekolek1000/wayvr-ipc.git?rev=6d253ef9e36db0f181566030a4990454ecb60395#6d253ef9e36db0f181566030a4990454ecb60395"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"clap",
|
||||
"env_logger 0.11.8",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"shell-words",
|
||||
"tokio",
|
||||
"wayvr-ipc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7055,7 +7125,6 @@ dependencies = [
|
||||
"wayland-client",
|
||||
"wayland-egl",
|
||||
"wayvr-ipc",
|
||||
"wayvr_ipc",
|
||||
"wgui",
|
||||
"winit",
|
||||
"wlx-capture",
|
||||
|
||||
@@ -18,17 +18,21 @@ members = [
|
||||
"wlx-capture",
|
||||
"dash-frontend",
|
||||
"wayvr-ipc",
|
||||
"wayvrctl",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
glam = { version = "0.30.9", features = ["mint", "serde"] }
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
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"
|
||||
vulkano = { version = "0.35.2", default-features = false, features = [
|
||||
"macros",
|
||||
|
||||
@@ -7,12 +7,12 @@ edition = "2024"
|
||||
anyhow.workspace = true
|
||||
wgui = { path = "../wgui/" }
|
||||
glam = { workspace = true, features = ["mint", "serde"] }
|
||||
log = { workspace = true }
|
||||
rust-embed = { workspace = true }
|
||||
log.workspace = true
|
||||
rust-embed.workspace = true
|
||||
chrono = "0.4.42"
|
||||
gio = "0.21.5"
|
||||
gtk = "0.18.2"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
wlx-common = { path = "../wlx-common" }
|
||||
wayvr_ipc = { workspace = true }
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
{
|
||||
"HOME_SCREEN": "ホーム",
|
||||
"MONADO_RUNTIME": "「Monado」ランタイム",
|
||||
"APPLICATIONS": "アプリケーション",
|
||||
"GAMES": "ゲーム",
|
||||
"SETTINGS": "設定",
|
||||
"PROCESSES": "プロセス",
|
||||
"HELLO_USER": "こんにちは、{USER}!",
|
||||
"GENERAL_SETTINGS": "全般設定",
|
||||
"APPLICATION_LAUNCHER": "アプリケーションランチャー",
|
||||
"APP_SETTINGS": {
|
||||
"HIDE_USERNAME": "ユーザー名を表示しない",
|
||||
"OPAQUE_BACKGROUND": "不透明な背景",
|
||||
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "XWaylandモードでデフォルトで実行する",
|
||||
"WLX_OVERLAY_S_SETTINGS": "WlxOverlay-Sの設定",
|
||||
"HEADSET_SETTINGS": "ヘッドセット設定",
|
||||
"BRIGHTNESS": "明るさ",
|
||||
"WLX": {
|
||||
"NOTIFICATIONS_ENABLED": "通知",
|
||||
"NOTIFICATIONS_SOUND_ENABLED": "通知音",
|
||||
"KEYBOARD_SOUND_ENABLED": "キーボード音",
|
||||
"BLOCK_GAME_INPUT": "ゲーム入力をブロック",
|
||||
"SPACE_DRAG_MULTIPLIER": "スペースドラッグ乗数",
|
||||
"SPACE_DRAG_ROTATION_ENABLED": "スペースドラッグでの回転",
|
||||
"SHOW_SKYBOX": "スカイボックス",
|
||||
"ENABLE_PASSTHROUGH": "パススルー"
|
||||
},
|
||||
"RESTART_SOFTWARE": "ソフトウェアを再起動"
|
||||
},
|
||||
"HELLO": "こんにちは!",
|
||||
"AUDIO": {
|
||||
"VOLUME": "音量",
|
||||
"SETTINGS": "オーディオ設定",
|
||||
"AUTO_SWITCH_TO_VR_AUDIO": "VRオーディオに自動切り替え",
|
||||
"SPEAKERS": "スピーカー",
|
||||
"MICROPHONES": "マイク",
|
||||
"CARDS": "カード",
|
||||
"SELECT_AUDIO_CARD_PROFILE": "オーディオカードプロファイルを選択",
|
||||
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "VRスピーカーが見つかりませんでした。手動で切り替えてください。",
|
||||
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "VRマイクが見つかりませんでした。手動で切り替えてください。",
|
||||
"FAILED_TO_SWITCH_MICROPHONE": "マイクの切り替えに失敗しました",
|
||||
"MICROPHONE_SET_SUCCESSFULLY": "マイクの設定が完了しました",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定しました",
|
||||
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "デバイスが見つかり、初期化されましたが、切り替えられていません"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"RECENTER_PLAYSPACE": "プレイスペースを再中央"
|
||||
},
|
||||
"LIST_OF_DISPLAYS": "ディスプレイリスト",
|
||||
"LIST_OF_PROCESSES": "プロセスのリスト",
|
||||
"NO_DISPLAYS_FOUND": "ディスプレイが見つかりません",
|
||||
"ADD_DISPLAY": "ディスプレイを追加",
|
||||
"POPUP_ADD_DISPLAY": {
|
||||
"RESOLUTION": "解像度"
|
||||
},
|
||||
"WIDTH": "幅",
|
||||
"HEIGHT": "高さ",
|
||||
"DISPLAY_PORTRAIT_MODE": "縦向きモード",
|
||||
"HIDE": "隠す",
|
||||
"REMOVE": "削除",
|
||||
"SHOW": "表示",
|
||||
"DISPLAY_OPTIONS": "表示オプション",
|
||||
"PROCESS_LIST": {
|
||||
"NO_PROCESSES_FOUND": "プロセスが見つかりませんでした",
|
||||
"LOCATED_ON": "に",
|
||||
"TERMINATE_PROCESS_NAMED_X": "プロセス \"{PROCESS_NAME}\" を終了します"
|
||||
},
|
||||
"FAILED_TO_LAUNCH_APPLICATION": "アプリケーションの起動に失敗しました:",
|
||||
"APPLICATION_LAUNCHED_ON": "{DISPLAY_NAME}でアプリケーションが起動しました。"
|
||||
}
|
||||
"HOME_SCREEN": "ホーム",
|
||||
"MONADO_RUNTIME": "「Monado」ランタイム",
|
||||
"APPLICATIONS": "アプリケーション",
|
||||
"GAMES": "ゲーム",
|
||||
"SETTINGS": "設定",
|
||||
"PROCESSES": "プロセス",
|
||||
"HELLO_USER": "こんにちは、{USER}!",
|
||||
"GENERAL_SETTINGS": "全般設定",
|
||||
"APPLICATION_LAUNCHER": "アプリケーションランチャー",
|
||||
"APP_SETTINGS": {
|
||||
"HIDE_USERNAME": "ユーザー名を表示しない",
|
||||
"OPAQUE_BACKGROUND": "不透明な背景",
|
||||
"RUN_IN_XWAYLAND_MODE_BY_DEFAULT": "XWaylandモードでデフォルトで実行する",
|
||||
"WLX_OVERLAY_S_SETTINGS": "WlxOverlay-Sの設定",
|
||||
"HEADSET_SETTINGS": "ヘッドセット設定",
|
||||
"BRIGHTNESS": "明るさ",
|
||||
"WLX": {
|
||||
"NOTIFICATIONS_ENABLED": "通知",
|
||||
"NOTIFICATIONS_SOUND_ENABLED": "通知音",
|
||||
"KEYBOARD_SOUND_ENABLED": "キーボード音",
|
||||
"BLOCK_GAME_INPUT": "ゲーム入力をブロック",
|
||||
"SPACE_DRAG_MULTIPLIER": "スペースドラッグ乗数",
|
||||
"SPACE_DRAG_ROTATION_ENABLED": "スペースドラッグでの回転",
|
||||
"SHOW_SKYBOX": "スカイボックス",
|
||||
"ENABLE_PASSTHROUGH": "パススルー"
|
||||
},
|
||||
"RESTART_SOFTWARE": "ソフトウェアを再起動"
|
||||
},
|
||||
"HELLO": "こんにちは!",
|
||||
"AUDIO": {
|
||||
"VOLUME": "音量",
|
||||
"SETTINGS": "オーディオ設定",
|
||||
"AUTO_SWITCH_TO_VR_AUDIO": "VRオーディオに自動切り替え",
|
||||
"SPEAKERS": "スピーカー",
|
||||
"MICROPHONES": "マイク",
|
||||
"CARDS": "カード",
|
||||
"SELECT_AUDIO_CARD_PROFILE": "オーディオカードプロファイルを選択",
|
||||
"NO_VR_SPEAKERS_FOUND_SWITCH_MANUALLY": "VRスピーカーが見つかりませんでした。手動で切り替えてください。",
|
||||
"NO_VR_MICROPHONE_SWITCH_MANUALLY": "VRマイクが見つかりませんでした。手動で切り替えてください。",
|
||||
"FAILED_TO_SWITCH_MICROPHONE": "マイクの切り替えに失敗しました",
|
||||
"MICROPHONE_SET_SUCCESSFULLY": "マイクの設定が完了しました",
|
||||
"SPEAKERS_SET_SUCCESSFULLY": "スピーカーを設定しました",
|
||||
"DEVICE_FOUND_AND_INITIALIZED_BUT_NOT_SWITCHED": "デバイスが見つかり、初期化されましたが、切り替えられていません"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"RECENTER_PLAYSPACE": "プレイスペースを再中央"
|
||||
},
|
||||
"LIST_OF_DISPLAYS": "ディスプレイリスト",
|
||||
"LIST_OF_PROCESSES": "プロセスのリスト",
|
||||
"NO_DISPLAYS_FOUND": "ディスプレイが見つかりません",
|
||||
"ADD_DISPLAY": "ディスプレイを追加",
|
||||
"POPUP_ADD_DISPLAY": {
|
||||
"RESOLUTION": "解像度"
|
||||
},
|
||||
"WIDTH": "幅",
|
||||
"HEIGHT": "高さ",
|
||||
"DISPLAY_PORTRAIT_MODE": "縦向きモード",
|
||||
"HIDE": "隠す",
|
||||
"REMOVE": "削除",
|
||||
"SHOW": "表示",
|
||||
"DISPLAY_OPTIONS": "表示オプション",
|
||||
"PROCESS_LIST": {
|
||||
"NO_PROCESSES_FOUND": "プロセスが見つかりませんでした",
|
||||
"LOCATED_ON": "に",
|
||||
"TERMINATE_PROCESS_NAMED_X": "プロセス \"{PROCESS_NAME}\" を終了します"
|
||||
},
|
||||
"FAILED_TO_LAUNCH_APPLICATION": "アプリケーションの起動に失敗しました:",
|
||||
"APPLICATION_LAUNCHED_ON": "{DISPLAY_NAME}でアプリケーションが起動しました。"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
bytes = "1.9.0"
|
||||
smallvec = "1.13.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde.workspace = true
|
||||
anyhow = "1.0.93"
|
||||
log = "0.4.22"
|
||||
|
||||
@@ -14,7 +14,7 @@ log = "0.4.22"
|
||||
interprocess = { version = "2.2.2", features = ["tokio"], optional = true }
|
||||
tokio = { version = "1.43.1", features = ["macros"], optional = true }
|
||||
tokio-util = { version = "0.7.13", optional = true }
|
||||
serde_json = "1.0.135"
|
||||
serde_json.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["client"]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use bytes::BufMut;
|
||||
use interprocess::local_socket::{
|
||||
self,
|
||||
tokio::{prelude::*, Stream},
|
||||
GenericNamespaced,
|
||||
self, GenericNamespaced,
|
||||
tokio::{Stream, prelude::*},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use smallvec::SmallVec;
|
||||
@@ -486,6 +485,14 @@ impl WayVRClient {
|
||||
send_only!(client, &PacketClient::WlxHaptics(params));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fn_wlx_modify_panel(
|
||||
client: WayVRClientMutex,
|
||||
params: packet_client::WlxModifyPanelParams,
|
||||
) -> anyhow::Result<()> {
|
||||
send_only!(client, &PacketClient::WlxModifyPanel(params));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WayVRClient {
|
||||
|
||||
@@ -48,6 +48,22 @@ pub struct WlxHapticsParams {
|
||||
pub frequency: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum WlxModifyPanelCommand {
|
||||
SetText(String),
|
||||
SetColor(String),
|
||||
SetImage(String),
|
||||
SetVisible(bool),
|
||||
SetStickyState(bool),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct WlxModifyPanelParams {
|
||||
pub overlay: String,
|
||||
pub element: String,
|
||||
pub command: WlxModifyPanelCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum PacketClient {
|
||||
Handshake(Handshake),
|
||||
@@ -68,4 +84,5 @@ pub enum PacketClient {
|
||||
WvrProcessTerminate(packet_server::WvrProcessHandle),
|
||||
WlxHaptics(WlxHapticsParams),
|
||||
WlxInputState(Serial),
|
||||
WlxModifyPanel(WlxModifyPanelParams),
|
||||
}
|
||||
|
||||
@@ -1,167 +1,164 @@
|
||||
#[macro_export]
|
||||
macro_rules! gen_id {
|
||||
(
|
||||
(
|
||||
$container_name:ident,
|
||||
$instance_name:ident,
|
||||
$cell_name:ident,
|
||||
$handle_name:ident) => {
|
||||
//ThingCell
|
||||
pub struct $cell_name {
|
||||
pub obj: $instance_name,
|
||||
pub generation: u64,
|
||||
}
|
||||
//ThingCell
|
||||
pub struct $cell_name {
|
||||
pub obj: $instance_name,
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
//ThingVec
|
||||
pub struct $container_name {
|
||||
// Vec<Option<ThingCell>>
|
||||
pub vec: Vec<Option<$cell_name>>,
|
||||
//ThingVec
|
||||
pub struct $container_name {
|
||||
// Vec<Option<ThingCell>>
|
||||
pub vec: Vec<Option<$cell_name>>,
|
||||
|
||||
cur_generation: u64,
|
||||
}
|
||||
cur_generation: u64,
|
||||
}
|
||||
|
||||
//ThingHandle
|
||||
#[derive(Default, Clone, Copy, PartialEq, Hash, Eq)]
|
||||
pub struct $handle_name {
|
||||
idx: u32,
|
||||
generation: u64,
|
||||
}
|
||||
//ThingHandle
|
||||
#[derive(Default, Clone, Copy, PartialEq, Hash, Eq)]
|
||||
pub struct $handle_name {
|
||||
idx: u32,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl $handle_name {
|
||||
pub fn reset(&mut self) {
|
||||
self.generation = 0;
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
impl $handle_name {
|
||||
pub fn reset(&mut self) {
|
||||
self.generation = 0;
|
||||
}
|
||||
|
||||
pub fn is_set(&self) -> bool {
|
||||
self.generation > 0
|
||||
}
|
||||
pub fn is_set(&self) -> bool {
|
||||
self.generation > 0
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.idx
|
||||
}
|
||||
pub fn id(&self) -> u32 {
|
||||
self.idx
|
||||
}
|
||||
|
||||
pub fn new(idx: u32, generation: u64) -> Self {
|
||||
Self { idx, generation }
|
||||
}
|
||||
}
|
||||
pub fn new(idx: u32, generation: u64) -> Self {
|
||||
Self { idx, generation }
|
||||
}
|
||||
}
|
||||
|
||||
//ThingVec
|
||||
#[allow(dead_code)]
|
||||
impl $container_name {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vec: Vec::new(),
|
||||
cur_generation: 0,
|
||||
}
|
||||
}
|
||||
//ThingVec
|
||||
#[allow(dead_code)]
|
||||
impl $container_name {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vec: Vec::new(),
|
||||
cur_generation: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self, callback: &dyn Fn($handle_name, &$instance_name)) {
|
||||
for (idx, opt_cell) in self.vec.iter().enumerate() {
|
||||
if let Some(cell) = opt_cell {
|
||||
let handle = $container_name::get_handle(&cell, idx);
|
||||
callback(handle, &cell.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn iter(&self, callback: &dyn Fn($handle_name, &$instance_name)) {
|
||||
for (idx, opt_cell) in self.vec.iter().enumerate() {
|
||||
if let Some(cell) = opt_cell {
|
||||
let handle = $container_name::get_handle(&cell, idx);
|
||||
callback(handle, &cell.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_mut(
|
||||
&mut self,
|
||||
callback: &mut dyn FnMut($handle_name, &mut $instance_name),
|
||||
) {
|
||||
for (idx, opt_cell) in self.vec.iter_mut().enumerate() {
|
||||
if let Some(cell) = opt_cell {
|
||||
let handle = $container_name::get_handle(&cell, idx);
|
||||
callback(handle, &mut cell.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn iter_mut(&mut self, callback: &mut dyn FnMut($handle_name, &mut $instance_name)) {
|
||||
for (idx, opt_cell) in self.vec.iter_mut().enumerate() {
|
||||
if let Some(cell) = opt_cell {
|
||||
let handle = $container_name::get_handle(&cell, idx);
|
||||
callback(handle, &mut cell.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_handle(cell: &$cell_name, idx: usize) -> $handle_name {
|
||||
$handle_name {
|
||||
idx: idx as u32,
|
||||
generation: cell.generation,
|
||||
}
|
||||
}
|
||||
pub fn get_handle(cell: &$cell_name, idx: usize) -> $handle_name {
|
||||
$handle_name {
|
||||
idx: idx as u32,
|
||||
generation: cell.generation,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_unused_idx(&mut self) -> Option<u32> {
|
||||
for (num, obj) in self.vec.iter().enumerate() {
|
||||
if obj.is_none() {
|
||||
return Some(num as u32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
fn find_unused_idx(&mut self) -> Option<u32> {
|
||||
for (num, obj) in self.vec.iter().enumerate() {
|
||||
if obj.is_none() {
|
||||
return Some(num as u32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn add(&mut self, obj: $instance_name) -> $handle_name {
|
||||
self.cur_generation += 1;
|
||||
let generation = self.cur_generation;
|
||||
pub fn add(&mut self, obj: $instance_name) -> $handle_name {
|
||||
self.cur_generation += 1;
|
||||
let generation = self.cur_generation;
|
||||
|
||||
let unused_idx = self.find_unused_idx();
|
||||
let unused_idx = self.find_unused_idx();
|
||||
|
||||
let idx = if let Some(idx) = unused_idx {
|
||||
idx
|
||||
} else {
|
||||
self.vec.len() as u32
|
||||
};
|
||||
let idx = if let Some(idx) = unused_idx {
|
||||
idx
|
||||
} else {
|
||||
self.vec.len() as u32
|
||||
};
|
||||
|
||||
let handle = $handle_name { idx, generation };
|
||||
let handle = $handle_name { idx, generation };
|
||||
|
||||
let cell = $cell_name { obj, generation };
|
||||
let cell = $cell_name { obj, generation };
|
||||
|
||||
if let Some(idx) = unused_idx {
|
||||
self.vec[idx as usize] = Some(cell);
|
||||
} else {
|
||||
self.vec.push(Some(cell))
|
||||
}
|
||||
if let Some(idx) = unused_idx {
|
||||
self.vec[idx as usize] = Some(cell);
|
||||
} else {
|
||||
self.vec.push(Some(cell))
|
||||
}
|
||||
|
||||
handle
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, handle: &$handle_name) {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return;
|
||||
}
|
||||
pub fn remove(&mut self, handle: &$handle_name) {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove only if the generation matches
|
||||
if let Some(cell) = &self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
self.vec[handle.idx as usize] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove only if the generation matches
|
||||
if let Some(cell) = &self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
self.vec[handle.idx as usize] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, handle: &$handle_name) -> Option<&$instance_name> {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return None;
|
||||
}
|
||||
pub fn get(&self, handle: &$handle_name) -> Option<&$instance_name> {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(cell) = &self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
return Some(&cell.obj);
|
||||
}
|
||||
}
|
||||
if let Some(cell) = &self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
return Some(&cell.obj);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, handle: &$handle_name) -> Option<&mut $instance_name> {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return None;
|
||||
}
|
||||
pub fn get_mut(&mut self, handle: &$handle_name) -> Option<&mut $instance_name> {
|
||||
// Out of bounds, ignore
|
||||
if handle.idx as usize >= self.vec.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(cell) = &mut self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
return Some(&mut cell.obj);
|
||||
}
|
||||
}
|
||||
if let Some(cell) = &mut self.vec[handle.idx as usize] {
|
||||
if cell.generation == handle.generation {
|
||||
return Some(&mut cell.obj);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* Example usage:
|
||||
|
||||
15
wayvrctl/Cargo.toml
Normal file
15
wayvrctl/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "wayvrctl"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
clap.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
env_logger = "0.11.8"
|
||||
tokio = "1.48.0"
|
||||
wayvr-ipc = { path = "../wayvr-ipc" }
|
||||
shell-words = "1.1.1"
|
||||
267
wayvrctl/src/helper.rs
Normal file
267
wayvrctl/src/helper.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::Serialize;
|
||||
use wayvr_ipc::{
|
||||
client::{WayVRClient, WayVRClientMutex},
|
||||
ipc, packet_client, packet_server,
|
||||
};
|
||||
|
||||
pub struct WayVRClientState {
|
||||
pub wayvr_client: WayVRClientMutex,
|
||||
pub serial_generator: ipc::SerialGenerator,
|
||||
pub pretty_print: bool,
|
||||
}
|
||||
|
||||
fn handle_empty_result(result: anyhow::Result<()>) {
|
||||
if let Err(e) = result {
|
||||
log::error!("{e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_result<T: Serialize>(pretty_print: bool, result: anyhow::Result<T>) {
|
||||
match result {
|
||||
Ok(t) => {
|
||||
let maybe_json = if pretty_print {
|
||||
serde_json::to_string_pretty(&t)
|
||||
} else {
|
||||
serde_json::to_string(&t)
|
||||
};
|
||||
|
||||
match maybe_json {
|
||||
Ok(json_string) => println!("{}", json_string),
|
||||
Err(e) => log::error!("Failed to serialize JSON: {e:?}"),
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("{e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wvr_display_create(
|
||||
state: &mut WayVRClientState,
|
||||
width: u16,
|
||||
height: u16,
|
||||
name: String,
|
||||
scale: Option<f32>,
|
||||
attach_to: packet_client::AttachTo,
|
||||
) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wvr_display_create(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
packet_client::WvrDisplayCreateParams {
|
||||
width,
|
||||
height,
|
||||
name,
|
||||
scale,
|
||||
attach_to,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to create display"),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn wvr_display_list(state: &mut WayVRClientState) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wvr_display_list(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
)
|
||||
.await
|
||||
.context("failed to fetch displays"),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn wvr_display_get(
|
||||
state: &mut WayVRClientState,
|
||||
handle: packet_server::WvrDisplayHandle,
|
||||
) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wvr_display_get(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
handle,
|
||||
)
|
||||
.await
|
||||
.context("failed to fetch display"),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn wvr_display_window_list(
|
||||
state: &mut WayVRClientState,
|
||||
handle: packet_server::WvrDisplayHandle,
|
||||
) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wvr_display_window_list(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
handle,
|
||||
)
|
||||
.await
|
||||
.context("failed to list window displays"),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn wvr_display_remove(
|
||||
state: &mut WayVRClientState,
|
||||
handle: packet_server::WvrDisplayHandle,
|
||||
) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wvr_display_remove(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
handle,
|
||||
)
|
||||
.await
|
||||
.context("failed to remove display"),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn wvr_display_set_visible(
|
||||
state: &mut WayVRClientState,
|
||||
handle: packet_server::WvrDisplayHandle,
|
||||
visible: bool,
|
||||
) {
|
||||
handle_empty_result(
|
||||
WayVRClient::fn_wvr_display_set_visible(state.wayvr_client.clone(), handle, visible)
|
||||
.await
|
||||
.context("failed to set display visibility"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn wvr_window_set_visible(
|
||||
state: &mut WayVRClientState,
|
||||
handle: packet_server::WvrWindowHandle,
|
||||
visible: bool,
|
||||
) {
|
||||
handle_empty_result(
|
||||
WayVRClient::fn_wvr_window_set_visible(state.wayvr_client.clone(), handle, visible)
|
||||
.await
|
||||
.context("failed to set window visibility"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn wvr_process_get(
|
||||
state: &mut WayVRClientState,
|
||||
handle: packet_server::WvrProcessHandle,
|
||||
) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wvr_process_get(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
handle,
|
||||
)
|
||||
.await
|
||||
.context("failed to get process"),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn wvr_process_list(state: &mut WayVRClientState) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wvr_process_list(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
)
|
||||
.await
|
||||
.context("failed to list processes"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn wvr_process_terminate(
|
||||
state: &mut WayVRClientState,
|
||||
handle: packet_server::WvrProcessHandle,
|
||||
) {
|
||||
handle_empty_result(
|
||||
WayVRClient::fn_wvr_process_terminate(state.wayvr_client.clone(), handle)
|
||||
.await
|
||||
.context("failed to terminate process"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn wvr_process_launch(
|
||||
state: &mut WayVRClientState,
|
||||
exec: String,
|
||||
name: String,
|
||||
env: Vec<String>,
|
||||
target_display: packet_server::WvrDisplayHandle,
|
||||
args: String,
|
||||
userdata: HashMap<String, String>,
|
||||
) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wvr_process_launch(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
packet_client::WvrProcessLaunchParams {
|
||||
env,
|
||||
exec,
|
||||
name,
|
||||
target_display,
|
||||
args,
|
||||
userdata,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to launch process"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn wlx_haptics(
|
||||
state: &mut WayVRClientState,
|
||||
intensity: f32,
|
||||
duration: f32,
|
||||
frequency: f32,
|
||||
) {
|
||||
handle_empty_result(
|
||||
WayVRClient::fn_wlx_haptics(
|
||||
state.wayvr_client.clone(),
|
||||
packet_client::WlxHapticsParams {
|
||||
intensity,
|
||||
duration,
|
||||
frequency,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to trigger haptics"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn wlx_panel_modify(
|
||||
state: &mut WayVRClientState,
|
||||
overlay: String,
|
||||
element: String,
|
||||
command: packet_client::WlxModifyPanelCommand,
|
||||
) {
|
||||
handle_empty_result(
|
||||
WayVRClient::fn_wlx_modify_panel(
|
||||
state.wayvr_client.clone(),
|
||||
packet_client::WlxModifyPanelParams {
|
||||
overlay,
|
||||
element,
|
||||
command,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to modify panel"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn wlx_input_state(state: &mut WayVRClientState) {
|
||||
handle_result(
|
||||
state.pretty_print,
|
||||
WayVRClient::fn_wlx_input_state(
|
||||
state.wayvr_client.clone(),
|
||||
state.serial_generator.increment_get(),
|
||||
)
|
||||
.await
|
||||
.context("failed to get input state"),
|
||||
)
|
||||
}
|
||||
327
wayvrctl/src/main.rs
Normal file
327
wayvrctl/src/main.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
process::{self, ExitCode},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use env_logger::Env;
|
||||
use wayvr_ipc::{client::WayVRClient, ipc, packet_client};
|
||||
|
||||
use crate::helper::{
|
||||
WayVRClientState, wlx_haptics, wlx_input_state, wlx_panel_modify, wvr_display_create,
|
||||
wvr_display_get, wvr_display_list, wvr_display_remove, wvr_display_set_visible,
|
||||
wvr_display_window_list, wvr_process_get, wvr_process_launch, wvr_process_list,
|
||||
wvr_process_terminate, wvr_window_set_visible,
|
||||
};
|
||||
|
||||
mod helper;
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> ExitCode {
|
||||
env_logger::init_from_env(Env::default().default_filter_or("info"));
|
||||
let args = Args::parse();
|
||||
|
||||
let mut state = WayVRClientState {
|
||||
wayvr_client: WayVRClient::new(&format!("wayvrctl-{}", process::id()))
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to initialize WayVR connection: {e:?}");
|
||||
process::exit(1);
|
||||
})
|
||||
.unwrap(),
|
||||
serial_generator: ipc::SerialGenerator::new(),
|
||||
pretty_print: args.pretty,
|
||||
};
|
||||
|
||||
let maybe_err = if let Subcommands::Batch { fail_fast } = args.command {
|
||||
run_batch(&mut state, fail_fast).await
|
||||
} else {
|
||||
run_once(&mut state, args).await
|
||||
};
|
||||
|
||||
if let Err(e) = maybe_err {
|
||||
log::error!("{e:?}");
|
||||
return ExitCode::FAILURE;
|
||||
} else {
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
async fn run_batch(state: &mut WayVRClientState, fail_fast: bool) -> anyhow::Result<()> {
|
||||
let stdin = std::io::stdin();
|
||||
|
||||
for (line_no, line) in stdin.lines().enumerate() {
|
||||
let line = line.context("error reading stdin")?;
|
||||
|
||||
if line.trim().is_empty() || line.trim_start().starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = parse_run_line(state, &line)
|
||||
.await
|
||||
.with_context(|| format!("error on line {}", line_no + 1))
|
||||
{
|
||||
if fail_fast {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn parse_run_line(state: &mut WayVRClientState, line: &str) -> anyhow::Result<()> {
|
||||
let mut argv = shell_words::split(&line).with_context(|| format!("parse error"))?;
|
||||
|
||||
// clap expects argv[0] to be the binary name
|
||||
argv.insert(0, env!("CARGO_PKG_NAME").to_string());
|
||||
|
||||
let args = Args::try_parse_from(argv).with_context(|| format!("invalid arguments"))?;
|
||||
run_once(state, args).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_once(state: &mut WayVRClientState, args: Args) -> anyhow::Result<()> {
|
||||
match args.command {
|
||||
Subcommands::Batch { .. } => {
|
||||
log::warn!("Ignoring recursive batch command");
|
||||
}
|
||||
Subcommands::InputState => {
|
||||
wlx_input_state(state).await;
|
||||
}
|
||||
Subcommands::DisplayCreate {
|
||||
width,
|
||||
height,
|
||||
name,
|
||||
scale,
|
||||
} => {
|
||||
wvr_display_create(
|
||||
state,
|
||||
width,
|
||||
height,
|
||||
name,
|
||||
scale,
|
||||
packet_client::AttachTo::None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Subcommands::DisplayList => {
|
||||
wvr_display_list(state).await;
|
||||
}
|
||||
Subcommands::DisplayGet { handle } => {
|
||||
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
|
||||
wvr_display_get(state, handle).await;
|
||||
}
|
||||
Subcommands::DisplayWindowList { handle } => {
|
||||
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
|
||||
wvr_display_window_list(state, handle).await;
|
||||
}
|
||||
Subcommands::DisplayRemove { handle } => {
|
||||
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
|
||||
wvr_display_remove(state, handle).await;
|
||||
}
|
||||
Subcommands::DisplaySetVisible {
|
||||
handle,
|
||||
visible_0_or_1,
|
||||
} => {
|
||||
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
|
||||
wvr_display_set_visible(state, handle, visible_0_or_1 != 0).await;
|
||||
}
|
||||
Subcommands::WindowSetVisible {
|
||||
handle,
|
||||
visible_0_or_1,
|
||||
} => {
|
||||
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
|
||||
wvr_window_set_visible(state, handle, visible_0_or_1 != 0).await;
|
||||
}
|
||||
Subcommands::ProcessGet { handle } => {
|
||||
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
|
||||
wvr_process_get(state, handle).await;
|
||||
}
|
||||
Subcommands::ProcessList => {
|
||||
wvr_process_list(state).await;
|
||||
}
|
||||
Subcommands::ProcessTerminate { handle } => {
|
||||
let handle = serde_json::from_str(&handle).context("Invalid handle")?;
|
||||
wvr_process_terminate(state, handle).await;
|
||||
}
|
||||
Subcommands::ProcessLaunch {
|
||||
exec,
|
||||
name,
|
||||
env,
|
||||
target_display,
|
||||
args,
|
||||
} => {
|
||||
let handle = serde_json::from_str(&target_display).context("Invalid target_display")?;
|
||||
wvr_process_launch(state, exec, name, env, handle, args, HashMap::new()).await;
|
||||
}
|
||||
Subcommands::Haptics {
|
||||
intensity,
|
||||
duration,
|
||||
frequency,
|
||||
} => {
|
||||
wlx_haptics(state, intensity, duration, frequency).await;
|
||||
}
|
||||
Subcommands::PanelModify {
|
||||
overlay,
|
||||
element,
|
||||
command,
|
||||
} => {
|
||||
let command = match command {
|
||||
SubcommandPanelModify::SetText { text } => {
|
||||
packet_client::WlxModifyPanelCommand::SetText(text.join(" "))
|
||||
}
|
||||
SubcommandPanelModify::SetColor { hex_color } => {
|
||||
packet_client::WlxModifyPanelCommand::SetColor(hex_color)
|
||||
}
|
||||
SubcommandPanelModify::SetImage { absolute_path } => {
|
||||
packet_client::WlxModifyPanelCommand::SetImage(absolute_path)
|
||||
}
|
||||
SubcommandPanelModify::SetVisible { visible_0_or_1 } => {
|
||||
packet_client::WlxModifyPanelCommand::SetVisible(visible_0_or_1 != 0)
|
||||
}
|
||||
SubcommandPanelModify::SetStickyState {
|
||||
sticky_state_0_or_1,
|
||||
} => packet_client::WlxModifyPanelCommand::SetStickyState(sticky_state_0_or_1 != 0),
|
||||
};
|
||||
|
||||
wlx_panel_modify(state, overlay, element, command).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A command-line interface for WayVR IPC
|
||||
#[derive(clap::Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// The command to run
|
||||
#[command(subcommand)]
|
||||
command: Subcommands,
|
||||
|
||||
/// Pretty-print JSON output
|
||||
#[arg(short, long)]
|
||||
pretty: bool,
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
enum Subcommands {
|
||||
/// Read commands from stdout, one per line.
|
||||
Batch {
|
||||
/// Stop on the first error
|
||||
#[arg(short, long)]
|
||||
fail_fast: bool,
|
||||
},
|
||||
/// Get the positions of HMD & controllers
|
||||
InputState,
|
||||
/// Create a new WayVR display
|
||||
DisplayCreate {
|
||||
width: u16,
|
||||
height: u16,
|
||||
name: String,
|
||||
#[arg(short, long)]
|
||||
scale: Option<f32>,
|
||||
//attach_to: packet_client::AttachTo,
|
||||
},
|
||||
/// List WayVR displays
|
||||
DisplayList,
|
||||
/// Retrieve information about a single WayVR display
|
||||
DisplayGet {
|
||||
/// A display handle JSON returned by DisplayList or DisplayCreate
|
||||
handle: String,
|
||||
},
|
||||
/// List windows attached to a WayVR display
|
||||
DisplayWindowList {
|
||||
/// A display handle JSON returned by DisplayList or DisplayCreate
|
||||
handle: String,
|
||||
},
|
||||
/// Delete a WayVR display
|
||||
DisplayRemove {
|
||||
/// A display handle JSON returned by DisplayList or DisplayCreate
|
||||
handle: String,
|
||||
},
|
||||
/// Change the visibility of a WayVR display
|
||||
DisplaySetVisible {
|
||||
/// A display handle JSON returned by DisplayList or DisplayCreate
|
||||
handle: String,
|
||||
visible_0_or_1: u8,
|
||||
},
|
||||
|
||||
// DisplaySetLayout skipped
|
||||
/// Change the visibility of a window on a WayVR display
|
||||
WindowSetVisible {
|
||||
/// A JSON window handle returned by DisplayWindowList
|
||||
handle: String,
|
||||
visible_0_or_1: u8,
|
||||
},
|
||||
/// Retrieve information about a WayVR-managed process
|
||||
ProcessGet {
|
||||
/// A JSON process handle returned by ProcessList or ProcessLaunch
|
||||
handle: String,
|
||||
},
|
||||
/// List all processes managed by WayVR
|
||||
ProcessList,
|
||||
/// Terminate a WayVR-managed process
|
||||
ProcessTerminate {
|
||||
/// A JSON process handle returned by ProcessList or ProcessLaunch
|
||||
handle: String,
|
||||
},
|
||||
/// Launch a new process inside WayVR
|
||||
ProcessLaunch {
|
||||
exec: String,
|
||||
name: String,
|
||||
env: Vec<String>,
|
||||
/// A display handle JSON returned by DisplayList or DisplayCreate
|
||||
target_display: String,
|
||||
args: String,
|
||||
},
|
||||
/// Trigger haptics on the user's controller
|
||||
Haptics {
|
||||
#[arg(short, long, default_value = "0.25")]
|
||||
intensity: f32,
|
||||
#[arg(short, long, default_value = "0.1")]
|
||||
duration: f32,
|
||||
#[arg(short, long, default_value = "0.1")]
|
||||
frequency: f32,
|
||||
},
|
||||
/// Apply a modification to a panel element
|
||||
PanelModify {
|
||||
/// The name of the overlay (XML file name without extension)
|
||||
overlay: String,
|
||||
/// The id of the element to modify, as set in the XML
|
||||
element: String,
|
||||
/// Command to execute
|
||||
#[command(subcommand)]
|
||||
command: SubcommandPanelModify,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
enum SubcommandPanelModify {
|
||||
/// Set the text of a <label> or <Button>
|
||||
SetText {
|
||||
/// Text that needs to be set
|
||||
#[arg(num_args = 1.., action = clap::ArgAction::Append)]
|
||||
text: Vec<String>,
|
||||
},
|
||||
/// Set the color of a <rectangle> or <label> or monochrome <sprite>
|
||||
SetColor {
|
||||
/// Color in HTML hex format (#rrggbb or #rrggbbaa)
|
||||
hex_color: String,
|
||||
},
|
||||
/// Set the content of a <sprite> or <image>. Max size for <sprite> is 256x256.
|
||||
SetImage {
|
||||
/// Absolute path to a svg, gif, png, jpeg or webp image.
|
||||
absolute_path: String,
|
||||
},
|
||||
/// Set the visibility of a <div>, <rectangle>, <label>, <sprite> or <image>
|
||||
SetVisible { visible_0_or_1: u8 },
|
||||
/// Set the sticky state of a <Button>. Intended for buttons without `sticky="1"`.
|
||||
SetStickyState { sticky_state_0_or_1: u8 },
|
||||
}
|
||||
@@ -4,10 +4,10 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
cosmic-text = "0.15.0"
|
||||
etagere = "0.2.15"
|
||||
glam = { workspace = true }
|
||||
glam.workspace = true
|
||||
image = { version = "0.25.9", default-features = false, features = [
|
||||
"gif",
|
||||
"jpeg",
|
||||
@@ -15,19 +15,19 @@ image = { version = "0.25.9", default-features = false, features = [
|
||||
"rayon",
|
||||
"webp",
|
||||
] }
|
||||
log = { workspace = true }
|
||||
log.workspace = true
|
||||
lru = "0.16.2"
|
||||
ouroboros = "0.18.5"
|
||||
parking_lot = "0.12.5"
|
||||
regex = { workspace = true }
|
||||
regex.workspace = true
|
||||
resvg = { version = "0.45.1", default-features = false }
|
||||
roxmltree = "0.21.1"
|
||||
rustc-hash = "2.1.1"
|
||||
serde_json = "1.0.145"
|
||||
slotmap = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
slotmap.workspace = true
|
||||
smallvec = "1.15.1"
|
||||
taffy = "0.9.2"
|
||||
vulkano = { workspace = true }
|
||||
vulkano-shaders = { workspace = true }
|
||||
rust-embed = { workspace = true }
|
||||
vulkano.workspace = true
|
||||
vulkano-shaders.workspace = true
|
||||
rust-embed.workspace = true
|
||||
flate2 = "1.1.5"
|
||||
|
||||
@@ -160,7 +160,17 @@ _2nd gradient color_
|
||||
|
||||
### `<sprite>`
|
||||
|
||||
### Image widget, supports raster and svg vector
|
||||
### Image widget for small images,
|
||||
|
||||
Supported formats: svg, png, jpeg, gif, webp
|
||||
|
||||
Maximum image size: 256x256 pixels
|
||||
|
||||
For large or frequently changing images (e.g. album art), or if borders/rounding is desired, consider the `<image>` tag instead.
|
||||
|
||||
Sprite images are atlassed, making them very efficient to render.
|
||||
|
||||
Adding large sprites permanently increases the atlas size (and thus VRAM usage) for the session. (Atlas space can be re-used, but the atlas won't shrink back.)
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -180,6 +190,44 @@ _Internal (assets) image path_
|
||||
|
||||
_wgui internal image path. Do not use directly unless it's related to the core wgui assets._
|
||||
|
||||
## image widget
|
||||
|
||||
### `<image>`
|
||||
|
||||
### Image widget for large images
|
||||
|
||||
Supported formats: svg, png, jpeg, gif, webp
|
||||
|
||||
Maximum image size: Max texture size for the GPU (usually 8K+)
|
||||
|
||||
For small images such as icons, consider using the `<sprite>` tag instead.
|
||||
|
||||
`<image>` requires a single draw call per widget, while `<sprite>` widgets all share a single draw call per panel.
|
||||
|
||||
#### Parameters
|
||||
|
||||
`src`: **string**
|
||||
|
||||
_External (filesystem) image path. Falls back to Internal (assets) if not found._
|
||||
|
||||
`src_ext`: **string**
|
||||
|
||||
_External (filesystem) image path_
|
||||
|
||||
`src_builtin`: **string**
|
||||
|
||||
_Internal (assets) image path_
|
||||
|
||||
`src_internal`: **string**
|
||||
|
||||
_wgui internal image path. Do not use directly unless it's related to the core wgui assets._
|
||||
|
||||
`round`: **float** (default: 0) | **percent** (0-100%)
|
||||
|
||||
`border`: **float**
|
||||
|
||||
`border_color`: #FFAABB | #FFAABBCC
|
||||
|
||||
---
|
||||
|
||||
# Components
|
||||
|
||||
@@ -9,7 +9,10 @@ use crate::{
|
||||
event::EventAlterables,
|
||||
globals::Globals,
|
||||
layout::Widget,
|
||||
renderer_vk::text::{TextShadow, custom_glyph::CustomGlyph},
|
||||
renderer_vk::text::{
|
||||
TextShadow,
|
||||
custom_glyph::{CustomGlyph, CustomGlyphData},
|
||||
},
|
||||
stack::{self, ScissorBoundary, ScissorStack, TransformStack},
|
||||
widget::{self, ScrollbarInfo, WidgetState},
|
||||
};
|
||||
@@ -175,6 +178,17 @@ pub struct Rectangle {
|
||||
pub round_units: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImagePrimitive {
|
||||
pub content: CustomGlyphData,
|
||||
pub content_key: usize,
|
||||
|
||||
pub border: f32, // width in pixels
|
||||
pub border_color: Color,
|
||||
|
||||
pub round_units: u8,
|
||||
}
|
||||
|
||||
pub struct PrimitiveExtent {
|
||||
pub(super) boundary: Boundary,
|
||||
pub(super) transform: Mat4,
|
||||
@@ -185,6 +199,7 @@ pub enum RenderPrimitive {
|
||||
Rectangle(PrimitiveExtent, Rectangle),
|
||||
Text(PrimitiveExtent, Rc<RefCell<Buffer>>, Option<TextShadow>),
|
||||
Sprite(PrimitiveExtent, Option<CustomGlyph>), //option because we want as_slice
|
||||
Image(PrimitiveExtent, ImagePrimitive),
|
||||
ScissorSet(ScissorBoundary),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
assets::AssetPath,
|
||||
components::{button, tooltip, Component},
|
||||
components::{Component, button, tooltip},
|
||||
drawing::Color,
|
||||
i18n::Translation,
|
||||
layout::WidgetID,
|
||||
parser::{
|
||||
parse_check_f32, parse_check_i32, parse_children, parse_f32, print_invalid_attrib, process_component,
|
||||
AttribPair, ParserContext, ParserFile, parse_check_f32, parse_check_i32, parse_children, parse_f32,
|
||||
print_invalid_attrib, process_component,
|
||||
style::{parse_color_opt, parse_round, parse_style, parse_text_style},
|
||||
AttribPair, ParserContext, ParserFile,
|
||||
},
|
||||
widget::util::WLength,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ mod component_checkbox;
|
||||
mod component_slider;
|
||||
mod style;
|
||||
mod widget_div;
|
||||
mod widget_image;
|
||||
mod widget_label;
|
||||
mod widget_rectangle;
|
||||
mod widget_sprite;
|
||||
@@ -15,8 +16,8 @@ use crate::{
|
||||
layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair},
|
||||
parser::{
|
||||
component_button::parse_component_button, component_checkbox::parse_component_checkbox,
|
||||
component_slider::parse_component_slider, widget_div::parse_widget_div, widget_label::parse_widget_label,
|
||||
widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite,
|
||||
component_slider::parse_component_slider, widget_div::parse_widget_div, widget_image::parse_widget_image,
|
||||
widget_label::parse_widget_label, widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite,
|
||||
},
|
||||
widget::ConstructEssentials,
|
||||
};
|
||||
@@ -898,6 +899,9 @@ fn parse_child<'a>(
|
||||
"sprite" => {
|
||||
new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id, &attribs)?);
|
||||
}
|
||||
"image" => {
|
||||
new_widget_id = Some(parse_widget_image(file, ctx, child_node, parent_id, &attribs)?);
|
||||
}
|
||||
"Button" => {
|
||||
new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id, &attribs)?);
|
||||
}
|
||||
|
||||
77
wgui/src/parser/widget_image.rs
Normal file
77
wgui/src/parser/widget_image.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::{
|
||||
assets::AssetPath,
|
||||
layout::WidgetID,
|
||||
parser::{
|
||||
AttribPair, ParserContext, ParserFile, parse_children, parse_widget_universal, print_invalid_attrib,
|
||||
style::{parse_color, parse_round, parse_style},
|
||||
},
|
||||
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData},
|
||||
widget::image::{WidgetImage, WidgetImageParams},
|
||||
};
|
||||
|
||||
pub fn parse_widget_image<'a>(
|
||||
file: &ParserFile,
|
||||
ctx: &mut ParserContext,
|
||||
node: roxmltree::Node<'a, 'a>,
|
||||
parent_id: WidgetID,
|
||||
attribs: &[AttribPair],
|
||||
) -> anyhow::Result<WidgetID> {
|
||||
let mut params = WidgetImageParams::default();
|
||||
let style = parse_style(attribs);
|
||||
let mut glyph = None;
|
||||
|
||||
for pair in attribs {
|
||||
let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref());
|
||||
match key {
|
||||
"src" | "src_ext" | "src_builtin" | "src_internal" => {
|
||||
let asset_path = match key {
|
||||
"src" => AssetPath::FileOrBuiltIn(value),
|
||||
"src_ext" => AssetPath::File(value),
|
||||
"src_builtin" => AssetPath::BuiltIn(value),
|
||||
"src_internal" => AssetPath::WguiInternal(value),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if !value.is_empty() {
|
||||
glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals, asset_path) {
|
||||
Ok(glyph) => Some(glyph),
|
||||
Err(e) => {
|
||||
log::warn!("failed to load {value}: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"round" => {
|
||||
parse_round(
|
||||
value,
|
||||
&mut params.round,
|
||||
ctx.doc_params.globals.get().defaults.rounding_mult,
|
||||
);
|
||||
}
|
||||
"border" => {
|
||||
params.border = value.parse().unwrap_or_else(|_| {
|
||||
print_invalid_attrib(key, value);
|
||||
0.0
|
||||
});
|
||||
}
|
||||
"border_color" => {
|
||||
parse_color(value, &mut params.border_color);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(glyph) = glyph {
|
||||
params.glyph_data = Some(CustomGlyphData::new(glyph));
|
||||
} else {
|
||||
log::warn!("No source for image node!");
|
||||
}
|
||||
|
||||
let (widget, _) = ctx.layout.add_child(parent_id, WidgetImage::create(params), style)?;
|
||||
|
||||
parse_widget_universal(ctx, &widget, attribs);
|
||||
parse_children(file, ctx, node, widget.id)?;
|
||||
|
||||
Ok(widget.id)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
drawing::{self},
|
||||
font_config,
|
||||
gfx::{WGfx, cmd::GfxCommandBuffer},
|
||||
renderer_vk::image::{ImagePipeline, ImageRenderer},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -26,6 +27,7 @@ struct RendererPass<'a> {
|
||||
text_areas: Vec<TextArea<'a>>,
|
||||
text_renderer: TextRenderer,
|
||||
rect_renderer: RectRenderer,
|
||||
image_renderer: ImageRenderer,
|
||||
scissor: Option<drawing::Boundary>,
|
||||
pixel_scale: f32,
|
||||
}
|
||||
@@ -34,16 +36,19 @@ impl RendererPass<'_> {
|
||||
fn new(
|
||||
text_atlas: &mut TextAtlas,
|
||||
rect_pipeline: RectPipeline,
|
||||
image_pipeline: ImagePipeline,
|
||||
scissor: Option<drawing::Boundary>,
|
||||
pixel_scale: f32,
|
||||
) -> anyhow::Result<Self> {
|
||||
let text_renderer = TextRenderer::new(text_atlas)?;
|
||||
let rect_renderer = RectRenderer::new(rect_pipeline)?;
|
||||
let image_renderer = ImageRenderer::new(image_pipeline)?;
|
||||
|
||||
Ok(Self {
|
||||
submitted: false,
|
||||
text_renderer,
|
||||
rect_renderer,
|
||||
image_renderer,
|
||||
text_areas: Vec::new(),
|
||||
scissor,
|
||||
pixel_scale,
|
||||
@@ -90,6 +95,7 @@ impl RendererPass<'_> {
|
||||
|
||||
self.submitted = true;
|
||||
self.rect_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?;
|
||||
self.image_renderer.render(gfx, viewport, &vk_scissor, cmd_buf)?;
|
||||
|
||||
{
|
||||
let mut font_system = font_system.system.lock();
|
||||
@@ -119,18 +125,21 @@ pub struct SharedContext {
|
||||
atlas_map: SlotMap<SharedContextKey, SharedAtlas>,
|
||||
rect_pipeline: RectPipeline,
|
||||
text_pipeline: TextPipeline,
|
||||
image_pipeline: ImagePipeline,
|
||||
}
|
||||
|
||||
impl SharedContext {
|
||||
pub fn new(gfx: Arc<WGfx>) -> anyhow::Result<Self> {
|
||||
let rect_pipeline = RectPipeline::new(gfx.clone(), gfx.surface_format)?;
|
||||
let text_pipeline = TextPipeline::new(gfx.clone(), gfx.surface_format)?;
|
||||
let image_pipeline = ImagePipeline::new(gfx.clone(), gfx.surface_format)?;
|
||||
|
||||
Ok(Self {
|
||||
gfx,
|
||||
atlas_map: SlotMap::with_key(),
|
||||
rect_pipeline,
|
||||
text_pipeline,
|
||||
image_pipeline,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -237,6 +246,7 @@ impl Context {
|
||||
passes.push(RendererPass::new(
|
||||
&mut atlas.text_atlas,
|
||||
shared.rect_pipeline.clone(),
|
||||
shared.image_pipeline.clone(),
|
||||
next_scissor,
|
||||
self.pixel_scale,
|
||||
)?);
|
||||
@@ -293,6 +303,11 @@ impl Context {
|
||||
transform: extent.transform,
|
||||
});
|
||||
}
|
||||
drawing::RenderPrimitive::Image(extent, image) => {
|
||||
pass
|
||||
.image_renderer
|
||||
.add_image(extent.boundary, image.clone(), &extent.transform);
|
||||
}
|
||||
drawing::RenderPrimitive::ScissorSet(boundary) => {
|
||||
next_scissor = Some(boundary.0);
|
||||
needs_new_pass = true;
|
||||
|
||||
244
wgui/src/renderer_vk/image.rs
Normal file
244
wgui/src/renderer_vk/image.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use cosmic_text::SubpixelBin;
|
||||
use glam::Mat4;
|
||||
use smallvec::smallvec;
|
||||
use vulkano::{
|
||||
buffer::{BufferContents, BufferUsage, Subbuffer},
|
||||
command_buffer::CommandBufferUsage,
|
||||
format::Format,
|
||||
image::view::ImageView,
|
||||
pipeline::graphics::{self, vertex_input::Vertex},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
drawing::{Boundary, ImagePrimitive},
|
||||
gfx::{
|
||||
BLEND_ALPHA, WGfx,
|
||||
cmd::GfxCommandBuffer,
|
||||
pass::WGfxPass,
|
||||
pipeline::{WGfxPipeline, WPipelineCreateInfo},
|
||||
},
|
||||
renderer_vk::{
|
||||
model_buffer::ModelBuffer,
|
||||
text::custom_glyph::{CustomGlyphData, RasterizeCustomGlyphRequest, RasterizedCustomGlyph},
|
||||
},
|
||||
};
|
||||
|
||||
use super::viewport::Viewport;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(BufferContents, Vertex, Copy, Clone, Debug)]
|
||||
pub struct ImageVertex {
|
||||
#[format(R32_UINT)]
|
||||
pub in_model_idx: u32,
|
||||
#[format(R32_UINT)]
|
||||
pub in_rect_dim: [u16; 2],
|
||||
#[format(R32_UINT)]
|
||||
pub in_border_color: u32,
|
||||
#[format(R32_UINT)]
|
||||
pub round_border: [u8; 4],
|
||||
}
|
||||
|
||||
/// Cloneable pipeline & shaders to be shared between `RectRenderer` instances.
|
||||
#[derive(Clone)]
|
||||
pub struct ImagePipeline {
|
||||
gfx: Arc<WGfx>,
|
||||
pub(super) inner: Arc<WGfxPipeline<ImageVertex>>,
|
||||
}
|
||||
|
||||
impl ImagePipeline {
|
||||
pub fn new(gfx: Arc<WGfx>, format: Format) -> anyhow::Result<Self> {
|
||||
let vert = vert_image::load(gfx.device.clone())?;
|
||||
let frag = frag_image::load(gfx.device.clone())?;
|
||||
|
||||
let pipeline = gfx.create_pipeline::<ImageVertex>(
|
||||
&vert,
|
||||
&frag,
|
||||
WPipelineCreateInfo::new(format)
|
||||
.use_blend(BLEND_ALPHA)
|
||||
.use_instanced()
|
||||
.use_updatable_descriptors(smallvec![2]),
|
||||
)?;
|
||||
|
||||
Ok(Self { gfx, inner: pipeline })
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageVertexWithContent {
|
||||
vert: ImageVertex,
|
||||
content: CustomGlyphData,
|
||||
content_key: usize, // identifies an image tag.
|
||||
}
|
||||
|
||||
struct CachedPass {
|
||||
content_id: usize,
|
||||
vert_buffer: Subbuffer<[ImageVertex]>,
|
||||
inner: WGfxPass<ImageVertex>,
|
||||
res: [u32; 2],
|
||||
}
|
||||
|
||||
pub struct ImageRenderer {
|
||||
pipeline: ImagePipeline,
|
||||
image_verts: Vec<ImageVertexWithContent>,
|
||||
model_buffer: ModelBuffer,
|
||||
cached_passes: HashMap<usize, CachedPass>,
|
||||
}
|
||||
|
||||
impl ImageRenderer {
|
||||
pub fn new(pipeline: ImagePipeline) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
model_buffer: ModelBuffer::new(&pipeline.gfx)?,
|
||||
pipeline,
|
||||
image_verts: vec![],
|
||||
cached_passes: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn begin(&mut self) {
|
||||
self.image_verts.clear();
|
||||
self.model_buffer.begin();
|
||||
}
|
||||
|
||||
pub fn add_image(&mut self, boundary: Boundary, image: ImagePrimitive, transform: &Mat4) {
|
||||
let in_model_idx = self
|
||||
.model_buffer
|
||||
.register_pos_size(&boundary.pos, &boundary.size, transform);
|
||||
|
||||
self.image_verts.push(ImageVertexWithContent {
|
||||
vert: ImageVertex {
|
||||
in_model_idx,
|
||||
in_rect_dim: [boundary.size.x as u16, boundary.size.y as u16],
|
||||
in_border_color: cosmic_text::Color::from(image.border_color).0,
|
||||
round_border: [
|
||||
image.round_units,
|
||||
(image.border) as u8,
|
||||
0, // unused
|
||||
0,
|
||||
],
|
||||
},
|
||||
content: image.content,
|
||||
content_key: image.content_key,
|
||||
});
|
||||
}
|
||||
|
||||
fn upload_image(
|
||||
gfx: Arc<WGfx>,
|
||||
res: [u32; 2],
|
||||
img: &ImageVertexWithContent,
|
||||
) -> anyhow::Result<Option<Arc<ImageView>>> {
|
||||
let raster = match RasterizedCustomGlyph::try_from(&RasterizeCustomGlyphRequest {
|
||||
data: img.content.clone(),
|
||||
width: res[0] as _,
|
||||
height: res[1] as _,
|
||||
x_bin: SubpixelBin::Zero,
|
||||
y_bin: SubpixelBin::Zero,
|
||||
scale: 1.0, // unused
|
||||
}) {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
log::error!("Unable to rasterize custom image");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let mut cmd_buf = gfx.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
|
||||
|
||||
let image = cmd_buf.upload_image(
|
||||
raster.width as _,
|
||||
raster.height as _,
|
||||
Format::R8G8B8A8_UNORM,
|
||||
&raster.data,
|
||||
)?;
|
||||
let image_view = ImageView::new_default(image)?;
|
||||
|
||||
cmd_buf.build_and_execute_now()?;
|
||||
|
||||
Ok(Some(image_view))
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
gfx: &Arc<WGfx>,
|
||||
viewport: &mut Viewport,
|
||||
vk_scissor: &graphics::viewport::Scissor,
|
||||
cmd_buf: &mut GfxCommandBuffer,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = viewport.resolution();
|
||||
self.model_buffer.upload(gfx)?;
|
||||
|
||||
for img in self.image_verts.iter() {
|
||||
let pass = match self.cached_passes.get_mut(&img.content_key) {
|
||||
Some(x) => {
|
||||
if x.content_id != img.content.id || x.res != res {
|
||||
// image changed
|
||||
let Some(image_view) = Self::upload_image(self.pipeline.gfx.clone(), res, img)? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
x.inner
|
||||
.update_sampler(2, image_view, self.pipeline.gfx.texture_filter)?;
|
||||
}
|
||||
|
||||
x
|
||||
}
|
||||
None => {
|
||||
let vert_buffer = self.pipeline.gfx.empty_buffer(
|
||||
BufferUsage::VERTEX_BUFFER | BufferUsage::TRANSFER_DST,
|
||||
(std::mem::size_of::<ImageVertex>()) as _,
|
||||
)?;
|
||||
|
||||
let Some(image_view) = Self::upload_image(self.pipeline.gfx.clone(), res, img)? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let set0 = viewport.get_image_descriptor(&self.pipeline);
|
||||
let set1 = self.model_buffer.get_image_descriptor(&self.pipeline);
|
||||
let set2 = self
|
||||
.pipeline
|
||||
.inner
|
||||
.uniform_sampler(2, image_view, self.pipeline.gfx.texture_filter)?;
|
||||
|
||||
let pass = self.pipeline.inner.create_pass(
|
||||
[res[0] as _, res[1] as _],
|
||||
vert_buffer.clone(),
|
||||
0..4,
|
||||
0..self.image_verts.len() as _,
|
||||
vec![set0, set1, set2],
|
||||
vk_scissor,
|
||||
)?;
|
||||
|
||||
self.cached_passes.insert(
|
||||
img.content_key,
|
||||
CachedPass {
|
||||
content_id: img.content.id,
|
||||
vert_buffer,
|
||||
inner: pass,
|
||||
res,
|
||||
},
|
||||
);
|
||||
self.cached_passes.get_mut(&img.content_key).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
pass.vert_buffer.write()?[0..1].clone_from_slice(&[img.vert]);
|
||||
|
||||
cmd_buf.run_ref(&pass.inner)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod vert_image {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "vertex",
|
||||
path: "src/renderer_vk/shaders/image.vert",
|
||||
}
|
||||
}
|
||||
|
||||
pub mod frag_image {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
path: "src/renderer_vk/shaders/image.frag",
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod context;
|
||||
pub mod image;
|
||||
pub mod model_buffer;
|
||||
pub mod rect;
|
||||
pub mod text;
|
||||
|
||||
@@ -8,7 +8,7 @@ use vulkano::{
|
||||
|
||||
use crate::{
|
||||
gfx,
|
||||
renderer_vk::{rect::RectPipeline, text::text_atlas::TextPipeline},
|
||||
renderer_vk::{image::ImagePipeline, rect::RectPipeline, text::text_atlas::TextPipeline},
|
||||
};
|
||||
|
||||
pub struct ModelBuffer {
|
||||
@@ -19,6 +19,8 @@ pub struct ModelBuffer {
|
||||
buffer_capacity_f32: u32,
|
||||
|
||||
rect_descriptor: Option<Arc<DescriptorSet>>,
|
||||
text_descriptor: Option<Arc<DescriptorSet>>,
|
||||
image_descriptor: Option<Arc<DescriptorSet>>,
|
||||
}
|
||||
|
||||
impl ModelBuffer {
|
||||
@@ -40,6 +42,8 @@ impl ModelBuffer {
|
||||
buffer,
|
||||
buffer_capacity_f32: INITIAL_CAPACITY_F32,
|
||||
rect_descriptor: None,
|
||||
text_descriptor: None,
|
||||
image_descriptor: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,8 +113,15 @@ impl ModelBuffer {
|
||||
|
||||
pub fn get_text_descriptor(&mut self, pipeline: &TextPipeline) -> Arc<DescriptorSet> {
|
||||
self
|
||||
.rect_descriptor
|
||||
.text_descriptor
|
||||
.get_or_insert_with(|| pipeline.inner.buffer(3, self.buffer.clone()).unwrap())
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn get_image_descriptor(&mut self, pipeline: &ImagePipeline) -> Arc<DescriptorSet> {
|
||||
self
|
||||
.image_descriptor
|
||||
.get_or_insert_with(|| pipeline.inner.buffer(1, self.buffer.clone()).unwrap())
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
55
wgui/src/renderer_vk/shaders/image.frag
Normal file
55
wgui/src/renderer_vk/shaders/image.frag
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : enable
|
||||
|
||||
precision highp float;
|
||||
|
||||
layout(location = 0) in vec2 in_uv;
|
||||
layout(location = 1) in vec4 in_border_color;
|
||||
layout(location = 2) in float in_border_size; // in units
|
||||
layout(location = 3) in float in_radius; // in units
|
||||
layout(location = 4) in vec2 in_rect_size;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
#define UNIFORM_PARAMS_SET 0
|
||||
#include "uniform.glsl"
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D image;
|
||||
|
||||
void main() {
|
||||
float rect_aspect = in_rect_size.x / in_rect_size.y;
|
||||
|
||||
vec2 center = in_rect_size / 2.0;
|
||||
vec2 coords = in_uv * in_rect_size;
|
||||
|
||||
|
||||
float radius = in_radius;
|
||||
|
||||
vec2 sdf_rect_dim = center - vec2(radius);
|
||||
float sdf = length(max(abs(coords - center), sdf_rect_dim) - sdf_rect_dim) -
|
||||
in_radius;
|
||||
|
||||
vec4 color = texture(image, in_uv);
|
||||
|
||||
float pixel_size = 1.0 / uniforms.pixel_scale;
|
||||
|
||||
if (in_border_size < in_radius) {
|
||||
// rounded border
|
||||
float f = in_border_size > 0.0 ? smoothstep(in_border_size + pixel_size,
|
||||
in_border_size, -sdf) *
|
||||
in_border_color.a
|
||||
: 0.0;
|
||||
out_color = mix(color, in_border_color, f);
|
||||
} else {
|
||||
// square border
|
||||
vec2 a = abs(coords - center);
|
||||
float aa = center.x - in_border_size;
|
||||
float bb = center.y - in_border_size;
|
||||
out_color = (a.x > aa || a.y > bb) ? in_border_color : color;
|
||||
}
|
||||
|
||||
if (in_radius > 0.0) {
|
||||
// rounding cutout alpha
|
||||
out_color.a *= 1.0 - smoothstep(-pixel_size, 0.0, sdf);
|
||||
}
|
||||
}
|
||||
52
wgui/src/renderer_vk/shaders/image.vert
Normal file
52
wgui/src/renderer_vk/shaders/image.vert
Normal file
@@ -0,0 +1,52 @@
|
||||
#version 450
|
||||
#extension GL_GOOGLE_include_directive : enable
|
||||
|
||||
precision highp float;
|
||||
|
||||
layout(location = 0) in uint in_model_idx;
|
||||
layout(location = 1) in uint in_rect_dim;
|
||||
layout(location = 2) in uint in_border_color;
|
||||
layout(location = 3) in uint round_border;
|
||||
|
||||
layout(location = 0) out vec2 out_uv;
|
||||
layout(location = 1) out vec4 out_border_color;
|
||||
layout(location = 2) out float out_border_size;
|
||||
layout(location = 3) out float out_radius;
|
||||
layout(location = 4) out vec2 out_rect_size;
|
||||
|
||||
#define UNIFORM_PARAMS_SET 0
|
||||
#define MODEL_BUFFER_SET 1
|
||||
|
||||
#include "model_buffer.glsl"
|
||||
#include "uniform.glsl"
|
||||
|
||||
void main() {
|
||||
uint v = uint(gl_VertexIndex); // 0-3
|
||||
uint rect_width = in_rect_dim & 0xffffu;
|
||||
uint rect_height = (in_rect_dim & 0xffff0000u) >> 16u;
|
||||
vec2 rect_size = vec2(float(rect_width), float(rect_height));
|
||||
float rect_aspect = rect_size.x / rect_size.y;
|
||||
|
||||
// 0.0 - 1.0 normalized
|
||||
uvec2 corner_pos_u = uvec2(v & 1u, (v >> 1u) & 1u);
|
||||
vec2 corner_pos = vec2(corner_pos_u);
|
||||
out_uv = corner_pos;
|
||||
|
||||
mat4 model_matrix = model_buffer.models[in_model_idx];
|
||||
|
||||
out_rect_size = rect_size;
|
||||
|
||||
gl_Position = uniforms.projection * model_matrix * vec4(corner_pos, 0.0, 1.0);
|
||||
|
||||
out_border_color =
|
||||
vec4(float((in_border_color & 0x00ff0000u) >> 16u) / 255.0,
|
||||
float((in_border_color & 0x0000ff00u) >> 8u) / 255.0,
|
||||
float(in_border_color & 0x000000ffu) / 255.0,
|
||||
float((in_border_color & 0xff000000u) >> 24u) / 255.0);
|
||||
|
||||
float radius = float(round_border & 0xffu);
|
||||
out_radius = radius;
|
||||
|
||||
float border_size = float((round_border & 0xff00u) >> 8);
|
||||
out_border_size = border_size;
|
||||
}
|
||||
@@ -53,8 +53,8 @@ impl CustomGlyphContent {
|
||||
/// Clone and reuse this to avoid atlasing the same content twice.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CustomGlyphData {
|
||||
pub(super) id: usize,
|
||||
pub(super) content: Arc<CustomGlyphContent>,
|
||||
pub(crate) id: usize,
|
||||
pub(crate) content: Arc<CustomGlyphContent>,
|
||||
}
|
||||
|
||||
impl CustomGlyphData {
|
||||
@@ -157,7 +157,7 @@ pub struct RasterizedCustomGlyph {
|
||||
}
|
||||
|
||||
impl RasterizedCustomGlyph {
|
||||
pub(super) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<Self> {
|
||||
pub(crate) fn try_from(input: &RasterizeCustomGlyphRequest) -> Option<Self> {
|
||||
match input.data.content.as_ref() {
|
||||
CustomGlyphContent::Svg(tree) => rasterize_svg(tree, input),
|
||||
CustomGlyphContent::Image(data) => Some(rasterize_image(data)),
|
||||
|
||||
@@ -5,7 +5,10 @@ use vulkano::{
|
||||
descriptor_set::DescriptorSet,
|
||||
};
|
||||
|
||||
use crate::{gfx::WGfx, renderer_vk::util::WMat4};
|
||||
use crate::{
|
||||
gfx::WGfx,
|
||||
renderer_vk::{image::ImagePipeline, util::WMat4},
|
||||
};
|
||||
|
||||
use super::{rect::RectPipeline, text::text_atlas::TextPipeline};
|
||||
|
||||
@@ -16,6 +19,7 @@ pub struct Viewport {
|
||||
params_buffer: Subbuffer<[Params]>,
|
||||
text_descriptor: Option<Arc<DescriptorSet>>,
|
||||
rect_descriptor: Option<Arc<DescriptorSet>>,
|
||||
image_descriptor: Option<Arc<DescriptorSet>>,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
@@ -36,6 +40,7 @@ impl Viewport {
|
||||
params_buffer,
|
||||
text_descriptor: None,
|
||||
rect_descriptor: None,
|
||||
image_descriptor: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,6 +62,15 @@ impl Viewport {
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn get_image_descriptor(&mut self, pipeline: &ImagePipeline) -> Arc<DescriptorSet> {
|
||||
self
|
||||
.image_descriptor
|
||||
.get_or_insert_with(|| {
|
||||
pipeline.inner.buffer(0, self.params_buffer.clone()).unwrap() // safe unwrap
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Updates the `Viewport` with the given `resolution` and `projection`.
|
||||
pub fn update(&mut self, resolution: [u32; 2], projection: &glam::Mat4, pixel_scale: f32) -> anyhow::Result<()> {
|
||||
if self.params.screen_resolution == resolution
|
||||
|
||||
113
wgui/src/widget/image.rs
Normal file
113
wgui/src/widget/image.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use slotmap::Key;
|
||||
|
||||
use crate::{
|
||||
drawing::{self, ImagePrimitive, PrimitiveExtent},
|
||||
event::CallbackDataCommon,
|
||||
globals::Globals,
|
||||
layout::WidgetID,
|
||||
renderer_vk::text::custom_glyph::CustomGlyphData,
|
||||
widget::{WidgetStateFlags, util::WLength},
|
||||
};
|
||||
|
||||
use super::{WidgetObj, WidgetState};
|
||||
|
||||
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WidgetImageParams {
|
||||
pub glyph_data: Option<CustomGlyphData>,
|
||||
|
||||
pub border: f32,
|
||||
pub border_color: drawing::Color,
|
||||
|
||||
pub round: WLength,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WidgetImage {
|
||||
params: WidgetImageParams,
|
||||
id: WidgetID,
|
||||
content_key: usize,
|
||||
}
|
||||
|
||||
impl WidgetImage {
|
||||
pub fn create(params: WidgetImageParams) -> WidgetState {
|
||||
WidgetState::new(
|
||||
WidgetStateFlags::default(),
|
||||
Box::new(Self {
|
||||
params,
|
||||
id: WidgetID::null(),
|
||||
content_key: AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option<CustomGlyphData>) {
|
||||
if self.params.glyph_data == content {
|
||||
return;
|
||||
}
|
||||
|
||||
self.params.glyph_data = content;
|
||||
common.mark_widget_dirty(self.id);
|
||||
}
|
||||
|
||||
pub fn get_content(&self) -> Option<CustomGlyphData> {
|
||||
self.params.glyph_data.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetObj for WidgetImage {
|
||||
fn draw(&mut self, state: &mut super::DrawState, _params: &super::DrawParams) {
|
||||
let boundary = drawing::Boundary::construct_relative(state.transform_stack);
|
||||
|
||||
let Some(content) = self.params.glyph_data.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let round_units = match self.params.round {
|
||||
WLength::Units(units) => units as u8,
|
||||
WLength::Percent(percent) => (f32::min(boundary.size.x, boundary.size.y) * percent / 2.0) as u8,
|
||||
};
|
||||
|
||||
state.primitives.push(drawing::RenderPrimitive::Image(
|
||||
PrimitiveExtent {
|
||||
boundary,
|
||||
transform: state.transform_stack.get().transform,
|
||||
},
|
||||
ImagePrimitive {
|
||||
content,
|
||||
content_key: self.content_key,
|
||||
border: self.params.border,
|
||||
border_color: self.params.border_color,
|
||||
round_units,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn measure(
|
||||
&mut self,
|
||||
_globals: &Globals,
|
||||
_known_dimensions: taffy::Size<Option<f32>>,
|
||||
_available_space: taffy::Size<taffy::AvailableSpace>,
|
||||
) -> taffy::Size<f32> {
|
||||
taffy::Size::ZERO
|
||||
}
|
||||
|
||||
fn get_id(&self) -> WidgetID {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: WidgetID) {
|
||||
self.id = id;
|
||||
}
|
||||
|
||||
fn get_type(&self) -> super::WidgetType {
|
||||
super::WidgetType::Sprite
|
||||
}
|
||||
|
||||
fn debug_print(&self) -> String {
|
||||
String::default()
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod div;
|
||||
pub mod image;
|
||||
pub mod label;
|
||||
pub mod rectangle;
|
||||
pub mod sprite;
|
||||
|
||||
@@ -2,6 +2,7 @@ use slotmap::Key;
|
||||
|
||||
use crate::{
|
||||
drawing::{self, GradientMode, PrimitiveExtent},
|
||||
event::CallbackDataCommon,
|
||||
layout::WidgetID,
|
||||
widget::{WidgetStateFlags, util::WLength},
|
||||
};
|
||||
@@ -35,6 +36,10 @@ impl WidgetRectangle {
|
||||
}),
|
||||
)
|
||||
}
|
||||
pub fn set_color(&mut self, common: &mut CallbackDataCommon, color: drawing::Color) {
|
||||
self.params.color = color;
|
||||
common.mark_widget_dirty(self.id);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetObj for WidgetRectangle {
|
||||
|
||||
@@ -5,6 +5,7 @@ use slotmap::Key;
|
||||
|
||||
use crate::{
|
||||
drawing::{self, PrimitiveExtent},
|
||||
event::CallbackDataCommon,
|
||||
globals::Globals,
|
||||
layout::WidgetID,
|
||||
renderer_vk::text::{
|
||||
@@ -39,16 +40,22 @@ impl WidgetSprite {
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn set_color(&mut self, color: drawing::Color) {
|
||||
pub fn set_color(&mut self, common: &mut CallbackDataCommon, color: drawing::Color) {
|
||||
self.params.color = Some(color);
|
||||
common.mark_widget_dirty(self.id);
|
||||
}
|
||||
|
||||
pub const fn get_color(&self) -> Option<drawing::Color> {
|
||||
self.params.color
|
||||
}
|
||||
|
||||
pub fn set_content(&mut self, content: Option<CustomGlyphData>) {
|
||||
pub fn set_content(&mut self, common: &mut CallbackDataCommon, content: Option<CustomGlyphData>) {
|
||||
if self.params.glyph_data == content {
|
||||
return;
|
||||
}
|
||||
|
||||
self.params.glyph_data = content;
|
||||
common.mark_widget_dirty(self.id);
|
||||
}
|
||||
|
||||
pub fn get_content(&self) -> Option<CustomGlyphData> {
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.228", features = ["derive", "rc"] }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
glam = { workspace = true }
|
||||
chrono = "0.4.42"
|
||||
idmap = { workspace = true, features = ["serde"] }
|
||||
|
||||
@@ -20,11 +20,16 @@ categories = ["games"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
log.workspace = true
|
||||
slotmap.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
ash = "^0.38.0" # must match vulkano
|
||||
chrono = "0.4.42"
|
||||
chrono-tz = "0.10.4"
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
config = "0.15.19"
|
||||
dbus = { version = "0.9.9" }
|
||||
futures = "0.3.31"
|
||||
@@ -35,7 +40,6 @@ input-linux = "0.7.1"
|
||||
json = { version = "0.12.4", optional = true }
|
||||
json5 = "1.3.0"
|
||||
libc = "0.2.178"
|
||||
log = { workspace = true }
|
||||
openxr = { git = "https://github.com/Ralith/openxrs", rev = "d0afdd3365bc1e14de28f6a3a21f457e788a702e", features = [
|
||||
"linked",
|
||||
"mint",
|
||||
@@ -51,10 +55,7 @@ rodio = { version = "0.21.1", default-features = false, features = [
|
||||
"hound",
|
||||
] }
|
||||
rosc = { version = "0.11.4", optional = true }
|
||||
serde = { version = "1.0.228", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.145"
|
||||
serde_yaml = "0.9.34"
|
||||
slotmap = { workspace = true }
|
||||
smallvec = "1.15.1"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
sysinfo = { version = "0.37" }
|
||||
|
||||
@@ -54,6 +54,22 @@ pub enum PlayspaceTask {
|
||||
FixFloor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ModifyPanelCommand {
|
||||
SetText(String),
|
||||
SetColor(String),
|
||||
SetImage(String),
|
||||
SetVisible(bool),
|
||||
SetStickyState(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModifyPanelTask {
|
||||
pub overlay: String,
|
||||
pub element: String,
|
||||
pub command: ModifyPanelCommand,
|
||||
}
|
||||
|
||||
pub type ModifyOverlayTask = dyn FnOnce(&mut AppState, &mut OverlayWindowConfig) + Send;
|
||||
pub type CreateOverlayTask = dyn FnOnce(&mut AppState) -> Option<OverlayWindowConfig> + Send;
|
||||
pub enum OverlayTask {
|
||||
@@ -66,6 +82,7 @@ pub enum OverlayTask {
|
||||
CleanupMirrors,
|
||||
Modify(OverlaySelector, Box<ModifyOverlayTask>),
|
||||
Create(OverlaySelector, Box<CreateOverlayTask>),
|
||||
ModifyPanel(ModifyPanelTask),
|
||||
Drop(OverlaySelector),
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,10 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use time::get_millis;
|
||||
use wayvr_ipc::{packet_client, packet_server};
|
||||
use wayvr_ipc::{
|
||||
packet_client::{self},
|
||||
packet_server,
|
||||
};
|
||||
use xkbcommon::xkb;
|
||||
|
||||
use crate::{
|
||||
@@ -94,6 +97,7 @@ pub enum WayVRSignal {
|
||||
BroadcastStateChanged(packet_server::WvrStateChanged),
|
||||
DropOverlay(crate::windowing::OverlayID),
|
||||
Haptics(super::input::Haptics),
|
||||
CustomTask(crate::backend::task::ModifyPanelTask),
|
||||
}
|
||||
|
||||
pub enum BlitMethod {
|
||||
|
||||
@@ -479,6 +479,38 @@ impl Connection {
|
||||
));
|
||||
}
|
||||
|
||||
fn handle_wlx_panel(
|
||||
params: &mut TickParams,
|
||||
custom_params: packet_client::WlxModifyPanelParams,
|
||||
) {
|
||||
use crate::backend::task::{ModifyPanelCommand, ModifyPanelTask};
|
||||
|
||||
params
|
||||
.state
|
||||
.signals
|
||||
.send(super::WayVRSignal::CustomTask(ModifyPanelTask {
|
||||
overlay: custom_params.overlay,
|
||||
element: custom_params.element,
|
||||
command: match custom_params.command {
|
||||
packet_client::WlxModifyPanelCommand::SetText(text) => {
|
||||
ModifyPanelCommand::SetText(text)
|
||||
}
|
||||
packet_client::WlxModifyPanelCommand::SetImage(sprite) => {
|
||||
ModifyPanelCommand::SetImage(sprite)
|
||||
}
|
||||
packet_client::WlxModifyPanelCommand::SetStickyState(sticky) => {
|
||||
ModifyPanelCommand::SetStickyState(sticky)
|
||||
}
|
||||
packet_client::WlxModifyPanelCommand::SetVisible(visible) => {
|
||||
ModifyPanelCommand::SetVisible(visible)
|
||||
}
|
||||
packet_client::WlxModifyPanelCommand::SetColor(color) => {
|
||||
ModifyPanelCommand::SetColor(color)
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
fn process_payload(&mut self, params: &mut TickParams, payload: Payload) -> anyhow::Result<()> {
|
||||
let packet: PacketClient = ipc::data_decode(&payload)?;
|
||||
|
||||
@@ -531,6 +563,9 @@ impl Connection {
|
||||
PacketClient::WlxHaptics(haptics_params) => {
|
||||
Self::handle_wlx_haptics(params, haptics_params);
|
||||
}
|
||||
PacketClient::WlxModifyPanel(custom_params) => {
|
||||
Self::handle_wlx_panel(params, custom_params);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -135,7 +135,7 @@ pub struct WayVRDashboard {
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct WayVRConfig {
|
||||
#[serde(default = "def_false")]
|
||||
#[serde(default = "def_true")]
|
||||
pub run_compositor_at_start: bool,
|
||||
|
||||
#[serde(default = "Default::default")]
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Custom UI Elements
|
||||
|
||||
Elements on custom panels may be modified at runtime using `wayvrctl`.
|
||||
|
||||
For more, refer to: `wayvrctl panel-modify --help`
|
||||
|
||||
### Labels
|
||||
|
||||
#### Clock label
|
||||
@@ -14,43 +18,6 @@ See the Custom Timezones section for more info on timezones. Skip `_timezone` to
|
||||
<label _source="clock" _display="time" _timezone="0" [...] />
|
||||
```
|
||||
|
||||
#### Fifo label
|
||||
|
||||
Fifo label creates a fifo on your system that other programs can pipe output into.
|
||||
|
||||
- The label will look for the last complete line to use as its text.
|
||||
- If the pipe breaks due to an IO error, re-creation is attempted after 15 seconds.
|
||||
- `_path` supports environment variables, but not `~`!
|
||||
|
||||
```xml
|
||||
<label _source="fifo" _path="$XDG_RUNTIME_DIR/my-test-label" [...] />
|
||||
```
|
||||
|
||||
Example script to test with:
|
||||
```bash
|
||||
for i in {0..99}; do echo "i is $i" > $XDG_RUNTIME_DIR/my-test-label; sleep 1; done
|
||||
```
|
||||
|
||||
#### Shell Exec label
|
||||
|
||||
This label executes a shell script using the `sh` shell.
|
||||
|
||||
- Write lines to the script's stdout to update the label text.
|
||||
- The label will look for the last complete line to use as its text.
|
||||
- Long-running scripts are allowed, but the label is only updated while the HMD is active.
|
||||
- If the script exits successfully (code 0), it will be re-ran on the next frame. Otherwise, it will be re-ran in 15s.
|
||||
- Control the pacing from inside the script itself. For example, adding a sleep 5 will make the script execute at most once per 5 seconds.
|
||||
- `_exec` supports everything that `sh` supports!
|
||||
|
||||
```xml
|
||||
<label _source="shell" _exec="$HOME/.local/bin/my-test-script.sh" [...] />
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/usr/bin/bash
|
||||
echo "This is my script's output!"
|
||||
```
|
||||
|
||||
#### Battery label
|
||||
|
||||
This is a label type that's used internally to display battery states.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
process::{Command, Stdio},
|
||||
process::{Child, Command, Stdio},
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
@@ -10,10 +10,7 @@ use std::{
|
||||
use anyhow::Context;
|
||||
use wgui::{
|
||||
components::button::ComponentButton,
|
||||
event::{
|
||||
self, CallbackData, CallbackMetadata, EventCallback, EventListenerKind, MouseButtonIndex,
|
||||
},
|
||||
i18n::Translation,
|
||||
event::{CallbackData, CallbackMetadata, EventCallback, EventListenerKind, MouseButtonIndex},
|
||||
layout::Layout,
|
||||
parser::CustomAttribsInfoOwned,
|
||||
widget::EventResult,
|
||||
@@ -23,7 +20,6 @@ use wlx_common::overlays::ToastTopic;
|
||||
use crate::{
|
||||
RUNNING,
|
||||
backend::task::{OverlayTask, PlayspaceTask, TaskType},
|
||||
gui::panel::helper::PipeReaderThread,
|
||||
overlays::toast::Toast,
|
||||
state::AppState,
|
||||
subsystem::hid::VirtualKey,
|
||||
@@ -359,15 +355,13 @@ pub(super) fn setup_custom_button<S: 'static>(
|
||||
carry_over: RefCell::new(None),
|
||||
});
|
||||
|
||||
let piped = attribs.get_value("_update_label").is_some_and(|s| s == "1");
|
||||
|
||||
layout.add_event_listener::<AppState, S>(
|
||||
attribs.widget_id,
|
||||
EventListenerKind::InternalStateChange,
|
||||
Box::new({
|
||||
let state = state.clone();
|
||||
move |common, _data, _, _| {
|
||||
shell_on_tick(&state, common, piped);
|
||||
move |_, _, _, _| {
|
||||
shell_on_tick(&state);
|
||||
Ok(EventResult::Pass)
|
||||
}
|
||||
}),
|
||||
@@ -429,8 +423,7 @@ pub(super) fn setup_custom_button<S: 'static>(
|
||||
|
||||
#[derive(Default)]
|
||||
struct ShellButtonMutableState {
|
||||
reader: Option<PipeReaderThread>,
|
||||
pid: Option<u32>,
|
||||
child: Option<Child>,
|
||||
}
|
||||
|
||||
struct ShellButtonState {
|
||||
@@ -443,17 +436,18 @@ struct ShellButtonState {
|
||||
fn shell_on_action(state: &ShellButtonState) -> anyhow::Result<()> {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
if mut_state.reader.as_ref().is_some_and(|r| !r.is_finished())
|
||||
&& let Some(pid) = mut_state.pid.as_ref()
|
||||
if let Some(child) = mut_state.child.as_mut()
|
||||
&& let Ok(None) = child.try_wait()
|
||||
{
|
||||
log::info!("ShellExec triggered while child is still running; sending SIGUSR1");
|
||||
let _ = Command::new("kill")
|
||||
.arg("-s")
|
||||
.arg("USR1")
|
||||
.arg(pid.to_string())
|
||||
.arg(child.id().to_string())
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait();
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -464,26 +458,19 @@ fn shell_on_action(state: &ShellButtonState) -> anyhow::Result<()> {
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to run shell script: '{}'", &state.exec))?;
|
||||
|
||||
mut_state.pid = Some(child.id());
|
||||
mut_state.reader = Some(PipeReaderThread::new_from_child(child));
|
||||
mut_state.child = Some(child);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shell_on_tick(state: &ShellButtonState, common: &mut event::CallbackDataCommon, piped: bool) {
|
||||
fn shell_on_tick(state: &ShellButtonState) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
let Some(reader) = mut_state.reader.as_mut() else {
|
||||
let Some(child) = mut_state.child.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if piped && let Some(text) = reader.get_last_line() {
|
||||
state
|
||||
.button
|
||||
.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
|
||||
if reader.is_finished() {
|
||||
mut_state.reader = None;
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
mut_state.child = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufRead, BufReader},
|
||||
process::Child,
|
||||
sync::{
|
||||
Arc, LazyLock,
|
||||
mpsc::{self, Receiver},
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
static ENV_VAR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)}|\$([A-Z_][A-Z0-9_]*)").unwrap() // want panic
|
||||
});
|
||||
|
||||
pub(super) fn expand_env_vars(template: &str) -> String {
|
||||
ENV_VAR_REGEX
|
||||
.replace_all(template, |caps: ®ex::Captures| {
|
||||
let var_name = caps.get(1).or_else(|| caps.get(2)).unwrap().as_str();
|
||||
std::env::var(var_name)
|
||||
.inspect_err(|e| log::warn!("Unable to substitute env var {var_name}: {e:?}"))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
pub(super) struct PipeReaderThread {
|
||||
receiver: Receiver<String>,
|
||||
handle: JoinHandle<bool>,
|
||||
}
|
||||
|
||||
impl PipeReaderThread {
|
||||
pub fn new_from_child(mut c: Child) -> Self {
|
||||
const BUF_LEN: usize = 128;
|
||||
let (sender, receiver) = mpsc::sync_channel::<String>(4);
|
||||
|
||||
let handle = std::thread::spawn({
|
||||
move || {
|
||||
let stdout = c.stdout.take().unwrap();
|
||||
let mut reader = BufReader::new(stdout);
|
||||
|
||||
loop {
|
||||
let mut buf = String::with_capacity(BUF_LEN);
|
||||
match reader.read_line(&mut buf) {
|
||||
Ok(0) => {
|
||||
// EOF reached
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
let _ = sender.try_send(buf);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error reading pipe: {e:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
c.wait()
|
||||
.inspect_err(|e| log::error!("Failed to wait for child process: {e:?}"))
|
||||
.is_ok_and(|c| c.success())
|
||||
}
|
||||
});
|
||||
|
||||
Self { receiver, handle }
|
||||
}
|
||||
|
||||
pub fn new_from_fifo(path: Arc<str>) -> Self {
|
||||
const BUF_LEN: usize = 128;
|
||||
let (sender, receiver) = mpsc::sync_channel::<String>(4);
|
||||
|
||||
let handle = std::thread::spawn({
|
||||
move || {
|
||||
let Ok(mut reader) = fs::File::open(&*path)
|
||||
.inspect_err(|e| {
|
||||
log::warn!("Failed to open fifo: {e:?}");
|
||||
})
|
||||
.map(|r| BufReader::new(r))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
loop {
|
||||
let mut buf = String::with_capacity(BUF_LEN);
|
||||
match reader.read_line(&mut buf) {
|
||||
Ok(0) => {
|
||||
// EOF reached
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
let _ = sender.try_send(buf);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error reading fifo: {e:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
Self { receiver, handle }
|
||||
}
|
||||
|
||||
pub fn get_last_line(&mut self) -> Option<String> {
|
||||
self.receiver.try_iter().last()
|
||||
}
|
||||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.handle.is_finished()
|
||||
}
|
||||
|
||||
pub fn check_success(self) -> bool {
|
||||
self.handle.join().unwrap_or(false)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,7 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs,
|
||||
os::unix::fs::FileTypeExt,
|
||||
process::{Command, Stdio},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Context;
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use interprocess::os::unix::fifo_file::create_fifo;
|
||||
use wgui::{
|
||||
drawing,
|
||||
event::{self, EventCallback},
|
||||
@@ -21,9 +11,7 @@ use wgui::{
|
||||
widget::{EventResult, label::WidgetLabel},
|
||||
};
|
||||
|
||||
use crate::{gui::panel::helper::PipeReaderThread, state::AppState};
|
||||
|
||||
use super::helper::expand_env_vars;
|
||||
use crate::state::AppState;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(super) fn setup_custom_label<S: 'static>(
|
||||
@@ -37,42 +25,6 @@ pub(super) fn setup_custom_label<S: 'static>(
|
||||
};
|
||||
|
||||
let callback: EventCallback<AppState, S> = match source {
|
||||
"shell" => {
|
||||
let Some(exec) = attribs.get_value("_exec") else {
|
||||
log::warn!("label with shell source but no exec attribute!");
|
||||
return;
|
||||
};
|
||||
let state = ShellLabelState {
|
||||
exec: exec.to_string(),
|
||||
mut_state: RefCell::new(PipeLabelMutableState {
|
||||
reader: None,
|
||||
next_try: Instant::now(),
|
||||
}),
|
||||
carry_over: RefCell::new(None),
|
||||
};
|
||||
Box::new(move |common, data, _, _| {
|
||||
let _ = shell_on_tick(&state, common, data).inspect_err(|e| log::error!("{e:?}"));
|
||||
Ok(EventResult::Pass)
|
||||
})
|
||||
}
|
||||
"fifo" => {
|
||||
let Some(path) = attribs.get_value("_path") else {
|
||||
log::warn!("label with fifo source but no path attribute!");
|
||||
return;
|
||||
};
|
||||
let state = FifoLabelState {
|
||||
path: expand_env_vars(path).into(),
|
||||
carry_over: RefCell::new(None),
|
||||
mut_state: RefCell::new(PipeLabelMutableState {
|
||||
reader: None,
|
||||
next_try: Instant::now(),
|
||||
}),
|
||||
};
|
||||
Box::new(move |common, data, _, _| {
|
||||
fifo_on_tick(&state, common, data);
|
||||
Ok(EventResult::Pass)
|
||||
})
|
||||
}
|
||||
"battery" => {
|
||||
let Some(device) = attribs
|
||||
.get_value("_device")
|
||||
@@ -186,126 +138,6 @@ pub(super) fn setup_custom_label<S: 'static>(
|
||||
);
|
||||
}
|
||||
|
||||
struct PipeLabelMutableState {
|
||||
reader: Option<PipeReaderThread>,
|
||||
next_try: Instant,
|
||||
}
|
||||
|
||||
struct ShellLabelState {
|
||||
exec: String,
|
||||
mut_state: RefCell<PipeLabelMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
fn shell_on_tick(
|
||||
state: &ShellLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
if let Some(reader) = mut_state.reader.as_mut() {
|
||||
if let Some(text) = reader.get_last_line() {
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
|
||||
if reader.is_finished() && !mut_state.reader.take().unwrap().check_success() {
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
}
|
||||
return Ok(());
|
||||
} else if mut_state.next_try > Instant::now() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let child = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&state.exec)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("Failed to run shell script: '{}'", &state.exec))?;
|
||||
|
||||
mut_state.reader = Some(PipeReaderThread::new_from_child(child));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct FifoLabelState {
|
||||
path: Arc<str>,
|
||||
mut_state: RefCell<PipeLabelMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl FifoLabelState {
|
||||
fn try_remove_fifo(&self) -> anyhow::Result<()> {
|
||||
let meta = match fs::metadata(&*self.path) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
if fs::exists(&*self.path).unwrap_or(true) {
|
||||
anyhow::bail!("Could not stat existing file at {}: {e:?}", &self.path);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if !meta.file_type().is_fifo() {
|
||||
anyhow::bail!("Existing file at {} is not a FIFO", &self.path);
|
||||
}
|
||||
|
||||
if let Err(e) = fs::remove_file(&*self.path) {
|
||||
anyhow::bail!("Unable to remove existing FIFO at {}: {e:?}", &self.path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FifoLabelState {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.try_remove_fifo() {
|
||||
log::debug!("{e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fifo_on_tick(
|
||||
state: &FifoLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
let Some(reader) = mut_state.reader.as_mut() else {
|
||||
if mut_state.next_try > Instant::now() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = state.try_remove_fifo() {
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
log::warn!("Requested FIFO path is taken: {e:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = create_fifo(&*state.path, 0o777) {
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
log::warn!("Failed to create FIFO: {e:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
mut_state.reader = Some(PipeReaderThread::new_from_fifo(state.path.clone()));
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(text) = reader.get_last_line() {
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
|
||||
if reader.is_finished() && !mut_state.reader.take().unwrap().check_success() {
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
}
|
||||
}
|
||||
|
||||
const BAT_LOW: drawing::Color = drawing::Color::new(0.69, 0.38, 0.38, 1.);
|
||||
const BAT_NORMAL: drawing::Color = drawing::Color::new(0.55, 0.84, 0.79, 1.);
|
||||
const BAT_CHARGING: drawing::Color = drawing::Color::new(0.38, 0.50, 0.62, 1.);
|
||||
|
||||
@@ -33,7 +33,6 @@ use crate::{
|
||||
use super::timer::GuiTimer;
|
||||
|
||||
pub mod button;
|
||||
mod helper;
|
||||
mod label;
|
||||
|
||||
const DEFAULT_MAX_SIZE: f32 = 2048.0;
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use glam::{Affine3A, Quat, Vec3, vec3};
|
||||
use wgui::{
|
||||
components::button::ComponentButton,
|
||||
event::{CallbackDataCommon, EventAlterables},
|
||||
i18n::Translation,
|
||||
parser::{Fetchable, parse_color_hex},
|
||||
renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData},
|
||||
taffy,
|
||||
widget::{
|
||||
image::WidgetImage, label::WidgetLabel, rectangle::WidgetRectangle, sprite::WidgetSprite,
|
||||
},
|
||||
};
|
||||
use wlx_common::windowing::OverlayWindowState;
|
||||
|
||||
use crate::{
|
||||
backend::task::ModifyPanelCommand,
|
||||
gui::{
|
||||
panel::{GuiPanel, NewGuiPanelParams},
|
||||
timer::GuiTimer,
|
||||
},
|
||||
state::AppState,
|
||||
windowing::window::{OverlayCategory, OverlayWindowConfig},
|
||||
windowing::{
|
||||
backend::OverlayEventData,
|
||||
window::{OverlayCategory, OverlayWindowConfig},
|
||||
},
|
||||
};
|
||||
|
||||
struct CustomPanelState {}
|
||||
@@ -36,6 +52,21 @@ pub fn create_custom(app: &mut AppState, name: Arc<str>) -> Option<OverlayWindow
|
||||
|
||||
let scale = panel.layout.content_size.x / 40.0 * 0.05;
|
||||
|
||||
panel.on_notify = Some(Box::new({
|
||||
let name = name.clone();
|
||||
move |panel, app, event_data| {
|
||||
let OverlayEventData::CustomCommand { element, command } = event_data else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Err(e) = apply_custom_command(panel, app, &element, &command) {
|
||||
log::warn!("Could not apply {command:?} on {name}/{element}: {e:?}");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
Some(OverlayWindowConfig {
|
||||
name,
|
||||
category: OverlayCategory::Panel,
|
||||
@@ -52,3 +83,103 @@ pub fn create_custom(app: &mut AppState, name: Arc<str>) -> Option<OverlayWindow
|
||||
..OverlayWindowConfig::from_backend(Box::new(panel))
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_custom_command(
|
||||
panel: &mut GuiPanel<CustomPanelState>,
|
||||
app: &mut AppState,
|
||||
element: &str,
|
||||
command: &ModifyPanelCommand,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut alterables = EventAlterables::default();
|
||||
let mut com = CallbackDataCommon {
|
||||
alterables: &mut alterables,
|
||||
state: &panel.layout.state,
|
||||
};
|
||||
|
||||
match command {
|
||||
ModifyPanelCommand::SetText(text) => {
|
||||
if let Ok(mut label) = panel
|
||||
.parser_state
|
||||
.fetch_widget_as::<WidgetLabel>(&panel.layout.state, element)
|
||||
{
|
||||
label.set_text(&mut com, Translation::from_raw_text(text));
|
||||
} else if let Ok(button) = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentButton>(&element)
|
||||
{
|
||||
button.set_text(&mut com, Translation::from_raw_text(text));
|
||||
} else {
|
||||
anyhow::bail!("No <label> or <Button> with such id.");
|
||||
}
|
||||
}
|
||||
ModifyPanelCommand::SetImage(path) => {
|
||||
if let Ok(pair) = panel
|
||||
.parser_state
|
||||
.fetch_widget(&panel.layout.state, element)
|
||||
{
|
||||
let content = CustomGlyphContent::from_assets(
|
||||
&mut app.wgui_globals,
|
||||
wgui::assets::AssetPath::File(&path),
|
||||
)
|
||||
.context("Could not load content from supplied path.")?;
|
||||
let data = CustomGlyphData::new(content);
|
||||
|
||||
if let Some(mut sprite) = pair.widget.get_as_mut::<WidgetSprite>() {
|
||||
sprite.set_content(&mut com, Some(data));
|
||||
} else if let Some(mut image) = pair.widget.get_as_mut::<WidgetImage>() {
|
||||
image.set_content(&mut com, Some(data));
|
||||
} else {
|
||||
anyhow::bail!("No <sprite> or <image> with such id.");
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("No <sprite> or <image> with such id.");
|
||||
}
|
||||
}
|
||||
ModifyPanelCommand::SetColor(color) => {
|
||||
let color = parse_color_hex(&color)
|
||||
.context("Invalid color format, must be a html hex color!")?;
|
||||
|
||||
if let Ok(pair) = panel
|
||||
.parser_state
|
||||
.fetch_widget(&panel.layout.state, element)
|
||||
{
|
||||
if let Some(mut rect) = pair.widget.get_as_mut::<WidgetRectangle>() {
|
||||
rect.set_color(&mut com, color);
|
||||
} else if let Some(mut label) = pair.widget.get_as_mut::<WidgetLabel>() {
|
||||
label.set_color(&mut com, color, true);
|
||||
} else if let Some(mut sprite) = pair.widget.get_as_mut::<WidgetSprite>() {
|
||||
sprite.set_color(&mut com, color);
|
||||
} else {
|
||||
anyhow::bail!("No <rectangle> or <label> or <sprite> with such id.");
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("No <rectangle> or <label> or <sprite> with such id.");
|
||||
}
|
||||
}
|
||||
ModifyPanelCommand::SetVisible(visible) => {
|
||||
let wid = panel
|
||||
.parser_state
|
||||
.get_widget_id(&element)
|
||||
.context("No widget with such id.")?;
|
||||
|
||||
let display = if *visible {
|
||||
taffy::Display::Flex
|
||||
} else {
|
||||
taffy::Display::None
|
||||
};
|
||||
|
||||
com.alterables
|
||||
.set_style(wid, wgui::event::StyleSetRequest::Display(display));
|
||||
}
|
||||
ModifyPanelCommand::SetStickyState(sticky_down) => {
|
||||
let button = panel
|
||||
.parser_state
|
||||
.fetch_component_as::<ComponentButton>(element)
|
||||
.context("No <Button> with such id.")?;
|
||||
button.set_sticky_state(&mut com, *sticky_down);
|
||||
}
|
||||
}
|
||||
|
||||
panel.layout.process_alterables(alterables)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ where
|
||||
.widgets
|
||||
.get_as::<WidgetSprite>(self.top_sprite_id)
|
||||
{
|
||||
sprite.set_content(Some(new.sprite.clone()));
|
||||
sprite.set_content(common, Some(new.sprite.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -473,7 +473,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
|
||||
.get_as::<WidgetSprite>(btn.sprite)
|
||||
&& let Some(glyph) = panel.state.overlay_cat_icons.get(meta.category)
|
||||
{
|
||||
sprite.set_content(Some(glyph.clone()));
|
||||
sprite.set_content(&mut com, Some(glyph.clone()));
|
||||
}
|
||||
|
||||
btn.button.set_sticky_state(&mut com, meta.visible);
|
||||
@@ -525,7 +525,7 @@ pub fn create_watch(app: &mut AppState) -> anyhow::Result<OverlayWindowConfig> {
|
||||
&& let Some(glyph) = panel.state.device_role_icons.get(dev.role)
|
||||
&& let Some(mut s) = panel.layout.state.widgets.get_as::<WidgetSprite>(*s)
|
||||
{
|
||||
s.set_content(Some(glyph.clone()));
|
||||
s.set_content(&mut com, Some(glyph.clone()));
|
||||
com.alterables
|
||||
.set_style(*div, StyleSetRequest::Display(taffy::Display::Flex));
|
||||
} else {
|
||||
|
||||
@@ -461,6 +461,10 @@ where
|
||||
wayvr::WayVRSignal::Haptics(haptics) => {
|
||||
wayvr.pending_haptics = Some(haptics);
|
||||
}
|
||||
wayvr::WayVRSignal::CustomTask(custom_task) => {
|
||||
app.tasks
|
||||
.enqueue(TaskType::Overlay(OverlayTask::ModifyPanel(custom_task)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@ use wlx_common::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::input::{HoverResult, PointerHit},
|
||||
backend::{
|
||||
input::{HoverResult, PointerHit},
|
||||
task::ModifyPanelCommand,
|
||||
},
|
||||
graphics::{ExtentExt, RenderResult},
|
||||
state::AppState,
|
||||
subsystem::hid::WheelDelta,
|
||||
@@ -123,6 +126,10 @@ pub enum OverlayEventData {
|
||||
pos: Positioning,
|
||||
editing: bool,
|
||||
},
|
||||
CustomCommand {
|
||||
element: String,
|
||||
command: ModifyPanelCommand,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait OverlayBackend: Any {
|
||||
|
||||
@@ -290,6 +290,27 @@ where
|
||||
self.dropped_overlays.push_back(o);
|
||||
}
|
||||
}
|
||||
OverlayTask::ModifyPanel(task) => {
|
||||
if let Some(oid) = self.lookup(&task.overlay)
|
||||
&& let Some(o) = self.mut_by_id(oid)
|
||||
{
|
||||
if !matches!(o.config.category, OverlayCategory::Panel) {
|
||||
log::warn!(
|
||||
"Received command for '{}', but this overlay does not support commands",
|
||||
&task.overlay
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
o.config.backend.notify(
|
||||
app,
|
||||
OverlayEventData::CustomCommand {
|
||||
element: task.element,
|
||||
command: task.command,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user