Merge remote-tracking branch 'origin/main' into next-dash-interface
[skip ci]
This commit is contained in:
@@ -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