sorry about monster commit
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
io::BufReader,
|
||||
process::{Child, ChildStdout},
|
||||
};
|
||||
|
||||
use wgui::{
|
||||
event::{self, EventCallback, EventListenerCollection, EventListenerKind, ListenerHandleVec},
|
||||
parser::CustomAttribsInfoOwned,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{common::OverlaySelector, overlay::OverlayID, task::TaskType, wayvr::WayVRAction},
|
||||
config::{save_layout, AStrSetExt},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::helper::read_label_from_pipe;
|
||||
|
||||
pub(super) fn setup_custom_button<S>(
|
||||
attribs: &CustomAttribsInfoOwned,
|
||||
listeners: &mut EventListenerCollection<AppState, S>,
|
||||
listener_handles: &mut ListenerHandleVec,
|
||||
app: &AppState,
|
||||
) {
|
||||
const EVENTS: [(&str, EventListenerKind); 2] = [
|
||||
("press", EventListenerKind::MousePress),
|
||||
("release", EventListenerKind::MouseRelease),
|
||||
];
|
||||
|
||||
for (name, kind) in EVENTS.iter() {
|
||||
let Some(action) = attribs.get_value(name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut args = action.split_whitespace();
|
||||
|
||||
let Some(command) = args.next() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let callback: EventCallback<AppState, S> = match command {
|
||||
"::DashToggle" => Box::new(move |_common, _data, app, _| {
|
||||
app.tasks
|
||||
.enqueue(TaskType::WayVR(WayVRAction::ToggleDashboard));
|
||||
Ok(())
|
||||
}),
|
||||
"::OverlayToggle" => {
|
||||
let Some(selector) = args.next() else {
|
||||
log::warn!("Missing argument for {}", command);
|
||||
continue;
|
||||
};
|
||||
|
||||
let selector = selector
|
||||
.parse::<usize>()
|
||||
.map(|id| OverlaySelector::Id(OverlayID { 0: id }))
|
||||
.unwrap_or_else(|_| OverlaySelector::Name(selector.into()));
|
||||
|
||||
Box::new(move |_common, _data, app, _| {
|
||||
app.tasks.enqueue(TaskType::Overlay(
|
||||
selector.clone(),
|
||||
Box::new(|app, o| {
|
||||
o.want_visible = !o.want_visible;
|
||||
if o.recenter {
|
||||
o.show_hide = o.want_visible;
|
||||
o.reset(app, false);
|
||||
}
|
||||
|
||||
let mut state_dirty = false;
|
||||
if !o.want_visible {
|
||||
state_dirty |=
|
||||
app.session.config.show_screens.arc_rm(o.name.as_ref());
|
||||
} else if o.want_visible {
|
||||
state_dirty |=
|
||||
app.session.config.show_screens.arc_set(o.name.clone());
|
||||
}
|
||||
|
||||
if state_dirty {
|
||||
match save_layout(&app.session.config) {
|
||||
Ok(()) => log::debug!("Saved state"),
|
||||
Err(e) => {
|
||||
log::error!("Failed to save state: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"::WatchHide" => todo!(),
|
||||
"::WatchSwapHand" => todo!(),
|
||||
"::EditToggle" => return,
|
||||
"::OscSend" => return,
|
||||
// shell
|
||||
_ => todo!(),
|
||||
};
|
||||
|
||||
listeners.register(listener_handles, attribs.widget_id, *kind, callback);
|
||||
}
|
||||
}
|
||||
struct ShellButtonMutableState {
|
||||
child: Option<Child>,
|
||||
reader: Option<BufReader<ChildStdout>>,
|
||||
}
|
||||
|
||||
struct ShellButtonState {
|
||||
exec: String,
|
||||
mut_state: RefCell<ShellButtonMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
fn shell_on_action(
|
||||
state: &ShellButtonState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
}
|
||||
|
||||
fn shell_on_tick(
|
||||
state: &ShellButtonState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
if let Some(mut child) = mut_state.child.take() {
|
||||
match child.try_wait() {
|
||||
// not exited yet
|
||||
Ok(None) => {
|
||||
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
|
||||
read_label_from_pipe("child process", r, &mut *state.carry_over.borrow_mut())
|
||||
}) {
|
||||
//TODO update label
|
||||
}
|
||||
mut_state.child = Some(child);
|
||||
}
|
||||
// exited successfully
|
||||
Ok(Some(code)) if code.success() => {
|
||||
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
|
||||
read_label_from_pipe("child process", r, &mut *state.carry_over.borrow_mut())
|
||||
}) {
|
||||
//TODO update label
|
||||
}
|
||||
mut_state.child = None;
|
||||
}
|
||||
// exited with failure
|
||||
Ok(Some(code)) => {
|
||||
mut_state.child = None;
|
||||
log::warn!("Label process exited with code {}", code);
|
||||
}
|
||||
// lost
|
||||
Err(_) => {
|
||||
mut_state.child = None;
|
||||
log::warn!("Label child process lost.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
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(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) fn read_label_from_pipe<R>(
|
||||
path: &str,
|
||||
reader: &mut BufReader<R>,
|
||||
carry_over: &mut Option<String>,
|
||||
) -> Option<String>
|
||||
where
|
||||
R: Read + Sized,
|
||||
{
|
||||
let mut prev = String::new();
|
||||
let mut cur = String::new();
|
||||
|
||||
for r in reader.lines() {
|
||||
match r {
|
||||
Ok(line) => {
|
||||
prev = cur;
|
||||
cur = carry_over.take().map(|s| s + &line).unwrap_or(line);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("pipe read error on {path}: {e:?}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
carry_over.replace(cur);
|
||||
|
||||
if prev.len() > 0 {
|
||||
Some(prev)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
fs, io,
|
||||
os::unix::fs::FileTypeExt,
|
||||
process::{Child, ChildStdout, Command, Stdio},
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use chrono_tz::Tz;
|
||||
use interprocess::os::unix::fifo_file::create_fifo;
|
||||
use wgui::{
|
||||
drawing,
|
||||
event::{self, EventCallback, EventListenerCollection, ListenerHandleVec},
|
||||
i18n::Translation,
|
||||
layout::Layout,
|
||||
parser::{parse_color_hex, CustomAttribsInfoOwned},
|
||||
widget::label::WidgetLabel,
|
||||
};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::helper::{expand_env_vars, read_label_from_pipe};
|
||||
|
||||
pub(super) fn setup_custom_label<S>(
|
||||
layout: &mut Layout,
|
||||
attribs: &CustomAttribsInfoOwned,
|
||||
listeners: &mut EventListenerCollection<AppState, S>,
|
||||
listener_handles: &mut ListenerHandleVec,
|
||||
app: &AppState,
|
||||
) {
|
||||
let Some(source) = attribs.get_value("source") else {
|
||||
log::warn!("custom label with no source!");
|
||||
return;
|
||||
};
|
||||
|
||||
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(ShellLabelMutableState {
|
||||
child: None,
|
||||
reader: None,
|
||||
next_try: Instant::now(),
|
||||
}),
|
||||
carry_over: RefCell::new(None),
|
||||
};
|
||||
Box::new(move |common, data, _app, _| {
|
||||
shell_on_tick(&state, common, data);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"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),
|
||||
carry_over: RefCell::new(None),
|
||||
mut_state: RefCell::new(FifoLabelMutableState {
|
||||
reader: None,
|
||||
next_try: Instant::now(),
|
||||
}),
|
||||
};
|
||||
Box::new(move |common, data, _app, _| {
|
||||
pipe_on_tick(&state, common, data);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"battery" => {
|
||||
let Some(device) = attribs
|
||||
.get_value("device")
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
else {
|
||||
log::warn!("label with battery source but no device attribute!");
|
||||
return;
|
||||
};
|
||||
|
||||
let state = BatteryLabelState {
|
||||
low_color: attribs
|
||||
.get_value("low_color")
|
||||
.and_then(|s| parse_color_hex(s))
|
||||
.unwrap_or(BAT_LOW),
|
||||
normal_color: attribs
|
||||
.get_value("normal_color")
|
||||
.and_then(|s| parse_color_hex(s))
|
||||
.unwrap_or(BAT_NORMAL),
|
||||
charging_color: attribs
|
||||
.get_value("charging_color")
|
||||
.and_then(|s| parse_color_hex(s))
|
||||
.unwrap_or(BAT_CHARGING),
|
||||
low_threshold: attribs
|
||||
.get_value("low_threshold")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(BAT_LOW_THRESHOLD),
|
||||
device,
|
||||
};
|
||||
Box::new(move |common, data, app, _| {
|
||||
battery_on_tick(&state, common, data, app);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"clock" => {
|
||||
let Some(display) = attribs.get_value("display") else {
|
||||
log::warn!("label with clock source but no display attribute!");
|
||||
return;
|
||||
};
|
||||
|
||||
let format = match display {
|
||||
"name" => {
|
||||
let maybe_pretty_tz = attribs
|
||||
.get_value("timezone")
|
||||
.and_then(|tz| tz.parse::<usize>().ok())
|
||||
.and_then(|tz_idx| app.session.config.timezones.get(tz_idx))
|
||||
.and_then(|tz_name| {
|
||||
tz_name.split('/').next_back().map(|x| x.replace('_', " "))
|
||||
});
|
||||
|
||||
let pretty_tz = match maybe_pretty_tz.as_ref() {
|
||||
Some(x) => x.as_str(),
|
||||
None => "Local",
|
||||
};
|
||||
|
||||
let mut i18n = layout.state.globals.i18n();
|
||||
layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetLabel>(attribs.widget_id)
|
||||
.unwrap()
|
||||
.set_text_simple(&mut *i18n, Translation::from_raw_text(&pretty_tz));
|
||||
|
||||
// does not need to be dynamic
|
||||
return;
|
||||
}
|
||||
"date" => "%x",
|
||||
"dow" => "%A",
|
||||
"time" => {
|
||||
if app.session.config.clock_12h {
|
||||
"%I:%M %p"
|
||||
} else {
|
||||
"%H:%M"
|
||||
}
|
||||
}
|
||||
unk => {
|
||||
log::warn!("Unknown display value for clock label source: {unk}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let tz_str = attribs
|
||||
.get_value("timezone")
|
||||
.and_then(|tz| tz.parse::<usize>().ok())
|
||||
.and_then(|tz_idx| app.session.config.timezones.get(tz_idx));
|
||||
|
||||
let state = ClockLabelState {
|
||||
timezone: tz_str.and_then(|tz| {
|
||||
tz.parse()
|
||||
.inspect_err(|e| log::warn!("Invalid timezone: {e:?}"))
|
||||
.ok()
|
||||
}),
|
||||
format: format.into(),
|
||||
};
|
||||
|
||||
Box::new(move |common, data, _app, _| {
|
||||
clock_on_tick(&state, common, data);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
"ipd" => Box::new(|common, data, app, _| {
|
||||
ipd_on_tick(common, data, app);
|
||||
Ok(())
|
||||
}),
|
||||
unk => {
|
||||
log::warn!("Unknown source value for label: {unk}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
listeners.register(
|
||||
listener_handles,
|
||||
attribs.widget_id,
|
||||
wgui::event::EventListenerKind::InternalStateChange,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
struct ShellLabelMutableState {
|
||||
child: Option<Child>,
|
||||
reader: Option<io::BufReader<ChildStdout>>,
|
||||
next_try: Instant,
|
||||
}
|
||||
|
||||
struct ShellLabelState {
|
||||
exec: String,
|
||||
mut_state: RefCell<ShellLabelMutableState>,
|
||||
carry_over: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
fn shell_on_tick(
|
||||
state: &ShellLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
if let Some(mut child) = mut_state.child.take() {
|
||||
match child.try_wait() {
|
||||
// not exited yet
|
||||
Ok(None) => {
|
||||
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
|
||||
read_label_from_pipe("child process", r, &mut *state.carry_over.borrow_mut())
|
||||
}) {
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
mut_state.child = Some(child);
|
||||
return;
|
||||
}
|
||||
// exited successfully
|
||||
Ok(Some(code)) if code.success() => {
|
||||
if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
|
||||
read_label_from_pipe("child process", r, &mut *state.carry_over.borrow_mut())
|
||||
}) {
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
mut_state.child = None;
|
||||
return;
|
||||
}
|
||||
// exited with failure
|
||||
Ok(Some(code)) => {
|
||||
mut_state.child = None;
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
log::warn!("Label process exited with code {}", code);
|
||||
return;
|
||||
}
|
||||
// lost
|
||||
Err(_) => {
|
||||
mut_state.child = None;
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
log::warn!("Label child process lost.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if mut_state.next_try > Instant::now() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&state.exec)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(mut child) => {
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
mut_state.child = Some(child);
|
||||
mut_state.reader = Some(io::BufReader::new(stdout));
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to run shell script '{}': {e:?}", &state.exec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FifoLabelMutableState {
|
||||
reader: Option<io::BufReader<fs::File>>,
|
||||
next_try: Instant,
|
||||
}
|
||||
|
||||
struct FifoLabelState {
|
||||
path: String,
|
||||
mut_state: RefCell<FifoLabelMutableState>,
|
||||
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 pipe_on_tick(
|
||||
state: &FifoLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let mut mut_state = state.mut_state.borrow_mut();
|
||||
|
||||
let reader = match mut_state.reader.as_mut() {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
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 = fs::File::open(&state.path)
|
||||
.inspect_err(|e| {
|
||||
log::warn!("Failed to open FIFO: {e:?}");
|
||||
mut_state.next_try = Instant::now() + Duration::from_secs(15);
|
||||
})
|
||||
.map(|f| io::BufReader::new(f))
|
||||
.ok();
|
||||
|
||||
mut_state.reader.as_mut().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(text) =
|
||||
read_label_from_pipe(&state.path, reader, &mut *state.carry_over.borrow_mut())
|
||||
{
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
}
|
||||
|
||||
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.);
|
||||
const BAT_LOW_THRESHOLD: u32 = 30;
|
||||
|
||||
struct BatteryLabelState {
|
||||
device: usize,
|
||||
low_color: drawing::Color,
|
||||
normal_color: drawing::Color,
|
||||
charging_color: drawing::Color,
|
||||
low_threshold: u32,
|
||||
}
|
||||
|
||||
fn battery_on_tick(
|
||||
state: &BatteryLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
app: &AppState,
|
||||
) {
|
||||
let device = app.input_state.devices.get(state.device);
|
||||
|
||||
let tags = ["", "H", "L", "R", "T"];
|
||||
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
|
||||
if let Some(device) = device {
|
||||
if let Some(soc) = device.soc {
|
||||
let soc = (soc * 100.).min(99.) as u32;
|
||||
let text = format!("{}{}", tags[device.role as usize], soc);
|
||||
let color = if device.charging {
|
||||
state.charging_color
|
||||
} else if soc < state.low_threshold {
|
||||
state.low_color
|
||||
} else {
|
||||
state.normal_color
|
||||
};
|
||||
label.set_color(common, color, false);
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
return;
|
||||
}
|
||||
}
|
||||
label.set_text(common, Translation::default());
|
||||
}
|
||||
|
||||
struct ClockLabelState {
|
||||
timezone: Option<Tz>,
|
||||
format: Rc<str>,
|
||||
}
|
||||
|
||||
fn clock_on_tick(
|
||||
state: &ClockLabelState,
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
) {
|
||||
let date_time = state.timezone.as_ref().map_or_else(
|
||||
|| format!("{}", Local::now().format(&state.format)),
|
||||
|tz| format!("{}", Local::now().with_timezone(tz).format(&state.format)),
|
||||
);
|
||||
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&date_time));
|
||||
}
|
||||
|
||||
fn ipd_on_tick(
|
||||
common: &mut event::CallbackDataCommon,
|
||||
data: &mut event::CallbackData,
|
||||
app: &AppState,
|
||||
) {
|
||||
let text = app.input_state.ipd.to_string();
|
||||
let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
|
||||
label.set_text(common, Translation::from_raw_text(&text));
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
|
||||
use button::setup_custom_button;
|
||||
use glam::{vec2, Affine2, Vec2};
|
||||
use label::setup_custom_label;
|
||||
use vulkano::{command_buffer::CommandBufferUsage, image::view::ImageView};
|
||||
use wgui::{
|
||||
drawing,
|
||||
event::{
|
||||
Event as WguiEvent, EventListenerCollection, InternalStateChangeEvent, ListenerHandleVec,
|
||||
MouseButtonIndex, MouseDownEvent, MouseLeaveEvent, MouseMotionEvent, MouseUpEvent,
|
||||
MouseWheelEvent,
|
||||
},
|
||||
layout::{Layout, LayoutParams, WidgetID},
|
||||
parser::ParserState,
|
||||
renderer_vk::context::Context as WguiContext,
|
||||
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::{
|
||||
input::{Haptics, PointerHit, PointerMode},
|
||||
overlay::{ui_transform, FrameMeta, OverlayBackend, ShouldRender},
|
||||
},
|
||||
graphics::{CommandBuffers, ExtentExt},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use super::{timer::GuiTimer, timestep::Timestep};
|
||||
|
||||
mod button;
|
||||
mod helper;
|
||||
mod label;
|
||||
|
||||
const MAX_SIZE: u32 = 2048;
|
||||
const MAX_SIZE_VEC2: Vec2 = vec2(MAX_SIZE as _, MAX_SIZE as _);
|
||||
|
||||
const COLOR_ERR: drawing::Color = drawing::Color::new(1., 0., 1., 1.);
|
||||
|
||||
pub struct GuiPanel<S> {
|
||||
pub layout: Layout,
|
||||
pub state: S,
|
||||
pub timers: Vec<GuiTimer>,
|
||||
pub listeners: EventListenerCollection<AppState, S>,
|
||||
pub listener_handles: ListenerHandleVec,
|
||||
pub parser_state: ParserState,
|
||||
interaction_transform: Option<Affine2>,
|
||||
context: WguiContext,
|
||||
timestep: Timestep,
|
||||
}
|
||||
|
||||
pub type OnCustomIdFunc<S> = Box<
|
||||
dyn Fn(
|
||||
Rc<str>,
|
||||
WidgetID,
|
||||
&wgui::parser::ParseDocumentParams,
|
||||
&mut Layout,
|
||||
&mut ParserState,
|
||||
&mut EventListenerCollection<AppState, S>,
|
||||
) -> anyhow::Result<()>,
|
||||
>;
|
||||
|
||||
impl<S> GuiPanel<S> {
|
||||
pub fn new_from_template(
|
||||
app: &mut AppState,
|
||||
path: &str,
|
||||
state: S,
|
||||
on_custom_id: Option<OnCustomIdFunc<S>>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut listeners = EventListenerCollection::<AppState, S>::default();
|
||||
let mut listener_handles = ListenerHandleVec::default();
|
||||
|
||||
let custom_elems = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let doc_params = wgui::parser::ParseDocumentParams {
|
||||
globals: app.wgui_globals.clone(),
|
||||
path,
|
||||
extra: wgui::parser::ParseDocumentExtra {
|
||||
on_custom_attribs: Some(Box::new({
|
||||
let custom_elems = custom_elems.clone();
|
||||
move |attribs| {
|
||||
custom_elems.borrow_mut().push(attribs.to_owned());
|
||||
}
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
let (mut layout, mut parser_state) = wgui::parser::new_layout_from_assets(
|
||||
&mut listeners,
|
||||
&doc_params,
|
||||
&LayoutParams::default(),
|
||||
)?;
|
||||
|
||||
if let Some(on_element_id) = on_custom_id {
|
||||
let ids = parser_state.ids.clone();
|
||||
|
||||
for (id, widget) in ids {
|
||||
on_element_id(
|
||||
id.clone(),
|
||||
widget,
|
||||
&doc_params,
|
||||
&mut layout,
|
||||
&mut parser_state,
|
||||
&mut listeners,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for elem in custom_elems.borrow().iter() {
|
||||
if layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetLabel>(elem.widget_id)
|
||||
.is_some()
|
||||
{
|
||||
setup_custom_label(
|
||||
&mut layout,
|
||||
elem,
|
||||
&mut listeners,
|
||||
&mut listener_handles,
|
||||
app,
|
||||
);
|
||||
} else if layout
|
||||
.state
|
||||
.widgets
|
||||
.get_as::<WidgetRectangle>(elem.widget_id)
|
||||
.is_some()
|
||||
{
|
||||
setup_custom_button(elem, &mut listeners, &mut listener_handles, app);
|
||||
}
|
||||
}
|
||||
|
||||
let context = WguiContext::new(&mut app.wgui_shared, 1.0)?;
|
||||
let mut timestep = Timestep::new();
|
||||
timestep.set_tps(60.0);
|
||||
|
||||
Ok(Self {
|
||||
layout,
|
||||
context,
|
||||
timestep,
|
||||
state,
|
||||
listener_handles,
|
||||
parser_state,
|
||||
timers: vec![],
|
||||
listeners,
|
||||
interaction_transform: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_blank(app: &mut AppState, state: S) -> anyhow::Result<Self> {
|
||||
let layout = Layout::new(app.wgui_globals.clone(), &LayoutParams::default())?;
|
||||
let context = WguiContext::new(&mut app.wgui_shared, 1.0)?;
|
||||
let mut timestep = Timestep::new();
|
||||
timestep.set_tps(60.0);
|
||||
|
||||
Ok(Self {
|
||||
layout,
|
||||
context,
|
||||
timestep,
|
||||
state,
|
||||
parser_state: ParserState::default(),
|
||||
listener_handles: ListenerHandleVec::default(),
|
||||
timers: vec![],
|
||||
listeners: EventListenerCollection::default(),
|
||||
interaction_transform: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_layout(&mut self) -> anyhow::Result<()> {
|
||||
self.layout.update(MAX_SIZE_VEC2, 0.0)
|
||||
}
|
||||
|
||||
pub fn push_event(&mut self, app: &mut AppState, event: &WguiEvent) {
|
||||
if let Err(e) = self
|
||||
.layout
|
||||
.push_event(&mut self.listeners, event, (app, &mut self.state))
|
||||
{
|
||||
log::error!("Failed to push event: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> OverlayBackend for GuiPanel<S> {
|
||||
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
if self.layout.content_size.x * self.layout.content_size.y != 0.0 {
|
||||
self.update_layout()?;
|
||||
self.interaction_transform = Some(ui_transform([
|
||||
//TODO: dynamic
|
||||
self.layout.content_size.x as _,
|
||||
self.layout.content_size.y as _,
|
||||
]));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
|
||||
self.timestep.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
|
||||
//TODO: this only executes one timer event per frame
|
||||
if let Some(signal) = self.timers.iter_mut().find_map(GuiTimer::check_tick) {
|
||||
self.push_event(
|
||||
app,
|
||||
&WguiEvent::InternalStateChange(InternalStateChangeEvent { metadata: signal }),
|
||||
);
|
||||
}
|
||||
|
||||
while self.timestep.on_tick() {
|
||||
self.layout.tick()?;
|
||||
}
|
||||
|
||||
if self.layout.content_size.x * self.layout.content_size.y == 0.0 {
|
||||
return Ok(ShouldRender::Unable);
|
||||
}
|
||||
|
||||
Ok(if self.layout.check_toggle_needs_redraw() {
|
||||
ShouldRender::Should
|
||||
} else {
|
||||
ShouldRender::Can
|
||||
})
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
app: &mut AppState,
|
||||
tgt: Arc<ImageView>,
|
||||
buf: &mut CommandBuffers,
|
||||
_alpha: f32,
|
||||
) -> anyhow::Result<bool> {
|
||||
self.context
|
||||
.update_viewport(&mut app.wgui_shared, tgt.extent_u32arr(), 1.0)?;
|
||||
self.layout.update(MAX_SIZE_VEC2, self.timestep.alpha)?;
|
||||
|
||||
let mut cmd_buf = app
|
||||
.gfx
|
||||
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)
|
||||
.unwrap(); // want panic
|
||||
|
||||
cmd_buf.begin_rendering(tgt)?;
|
||||
let primitives = wgui::drawing::draw(&self.layout)?;
|
||||
self.context
|
||||
.draw(&mut app.wgui_shared, &mut cmd_buf, &primitives)?;
|
||||
cmd_buf.end_rendering()?;
|
||||
buf.push(cmd_buf.build()?);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn frame_meta(&mut self) -> Option<FrameMeta> {
|
||||
Some(FrameMeta {
|
||||
extent: [
|
||||
MAX_SIZE.min(self.layout.content_size.x as _),
|
||||
MAX_SIZE.min(self.layout.content_size.y as _),
|
||||
1,
|
||||
],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta_y: f32, delta_x: f32) {
|
||||
self.layout
|
||||
.push_event(
|
||||
&mut self.listeners,
|
||||
&WguiEvent::MouseWheel(MouseWheelEvent {
|
||||
shift: vec2(delta_x, delta_y),
|
||||
pos: hit.uv * self.layout.content_size,
|
||||
device: hit.pointer,
|
||||
}),
|
||||
(app, &mut self.state),
|
||||
)
|
||||
.unwrap(); // want panic
|
||||
}
|
||||
|
||||
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
|
||||
self.push_event(
|
||||
app,
|
||||
&WguiEvent::MouseMotion(MouseMotionEvent {
|
||||
pos: hit.uv * self.layout.content_size,
|
||||
device: hit.pointer,
|
||||
}),
|
||||
);
|
||||
|
||||
self.layout
|
||||
.check_toggle_haptics_triggered()
|
||||
.then_some(Haptics {
|
||||
intensity: 0.1,
|
||||
duration: 0.01,
|
||||
frequency: 5.0,
|
||||
})
|
||||
}
|
||||
|
||||
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
|
||||
self.push_event(
|
||||
app,
|
||||
&WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }),
|
||||
);
|
||||
}
|
||||
|
||||
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
|
||||
let index = match hit.mode {
|
||||
PointerMode::Left => MouseButtonIndex::Left,
|
||||
PointerMode::Right => MouseButtonIndex::Right,
|
||||
PointerMode::Middle => MouseButtonIndex::Middle,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if pressed {
|
||||
self.push_event(
|
||||
app,
|
||||
&WguiEvent::MouseDown(MouseDownEvent {
|
||||
pos: hit.uv * self.layout.content_size,
|
||||
index,
|
||||
device: hit.pointer,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
self.push_event(
|
||||
app,
|
||||
&WguiEvent::MouseUp(MouseUpEvent {
|
||||
pos: hit.uv * self.layout.content_size,
|
||||
index,
|
||||
device: hit.pointer,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_interaction_transform(&mut self) -> Option<Affine2> {
|
||||
self.interaction_transform
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user