modular ui rework
This commit is contained in:
@@ -9,14 +9,15 @@ use openxr as xr;
|
|||||||
|
|
||||||
use glam::{Affine3A, Vec2, Vec3A};
|
use glam::{Affine3A, Vec2, Vec3A};
|
||||||
use idmap::IdMap;
|
use idmap::IdMap;
|
||||||
|
use serde::Deserialize;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
overlays::{
|
overlays::{
|
||||||
keyboard::create_keyboard,
|
keyboard::create_keyboard,
|
||||||
watch::{create_watch, WATCH_NAME, WATCH_SCALE},
|
watch::{create_watch, WATCH_NAME},
|
||||||
},
|
},
|
||||||
state::AppState,
|
state::{AppState, ScreenMeta},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::overlay::{OverlayBackend, OverlayData, OverlayState};
|
use super::overlay::{OverlayBackend, OverlayData, OverlayState};
|
||||||
@@ -56,8 +57,15 @@ where
|
|||||||
crate::overlays::screen::get_screens_x11(&app.session)?
|
crate::overlays::screen::get_screens_x11(&app.session)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut watch = create_watch::<T>(app, &screens)?;
|
app.screens.clear();
|
||||||
log::info!("Watch Rotation: {:?}", watch.state.spawn_rotation);
|
for screen in screens.iter() {
|
||||||
|
app.screens.push(ScreenMeta {
|
||||||
|
name: screen.state.name.clone(),
|
||||||
|
id: screen.state.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut watch = create_watch::<T>(app)?;
|
||||||
watch.state.want_visible = true;
|
watch.state.want_visible = true;
|
||||||
overlays.insert(watch.state.id, watch);
|
overlays.insert(watch.state.id, watch);
|
||||||
|
|
||||||
@@ -147,13 +155,14 @@ where
|
|||||||
}
|
}
|
||||||
// toggle watch back on if it was hidden
|
// toggle watch back on if it was hidden
|
||||||
if !any_shown && *o.state.name == *WATCH_NAME {
|
if !any_shown && *o.state.name == *WATCH_NAME {
|
||||||
o.state.spawn_scale = WATCH_SCALE * app.session.config.watch_scale;
|
o.state.reset(app, true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
pub enum OverlaySelector {
|
pub enum OverlaySelector {
|
||||||
Id(usize),
|
Id(usize),
|
||||||
Name(Arc<str>),
|
Name(Arc<str>),
|
||||||
@@ -181,17 +190,30 @@ impl Ord for AppTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum SystemTask {
|
||||||
|
ColorGain(ColorChannel, f32),
|
||||||
|
ResetPlayspace,
|
||||||
|
FixFloor,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send;
|
||||||
|
pub type CreateOverlayTask =
|
||||||
|
dyn FnOnce(&mut AppState) -> Option<(OverlayState, Box<dyn OverlayBackend>)> + Send;
|
||||||
|
|
||||||
pub enum TaskType {
|
pub enum TaskType {
|
||||||
Global(Box<dyn FnOnce(&mut AppState) + Send>),
|
Global(Box<dyn FnOnce(&mut AppState) + Send>),
|
||||||
Overlay(
|
Overlay(OverlaySelector, Box<OverlayTask>),
|
||||||
OverlaySelector,
|
CreateOverlay(OverlaySelector, Box<CreateOverlayTask>),
|
||||||
Box<dyn FnOnce(&mut AppState, &mut OverlayState) + Send>,
|
|
||||||
),
|
|
||||||
CreateOverlay(
|
|
||||||
OverlaySelector,
|
|
||||||
Box<dyn FnOnce(&mut AppState) -> Option<(OverlayState, Box<dyn OverlayBackend>)> + Send>,
|
|
||||||
),
|
|
||||||
DropOverlay(OverlaySelector),
|
DropOverlay(OverlaySelector),
|
||||||
|
System(SystemTask),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Copy)]
|
||||||
|
pub enum ColorChannel {
|
||||||
|
R,
|
||||||
|
G,
|
||||||
|
B,
|
||||||
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TaskContainer {
|
pub struct TaskContainer {
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ impl NotificationManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
log::info!("Received notification message: {}", json_str);
|
|
||||||
let msg = match serde_json::from_str::<XsoMessage>(json_str) {
|
let msg = match serde_json::from_str::<XsoMessage>(json_str) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use std::ffi::CStr;
|
||||||
|
|
||||||
use glam::Affine3A;
|
use glam::Affine3A;
|
||||||
use ovr_overlay::{pose::Matrix3x4, sys::HmdMatrix34_t};
|
use ovr_overlay::{pose::Matrix3x4, settings::SettingsManager, sys::HmdMatrix34_t};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::backend::common::BackendError;
|
use crate::backend::common::{BackendError, ColorChannel};
|
||||||
|
|
||||||
pub trait Affine3AConvert {
|
pub trait Affine3AConvert {
|
||||||
fn from_affine(affine: Affine3A) -> Self;
|
fn from_affine(affine: Affine3A) -> Self;
|
||||||
@@ -96,3 +98,92 @@ impl From<OVRError> for BackendError {
|
|||||||
BackendError::Fatal(anyhow::Error::new(e))
|
BackendError::Fatal(anyhow::Error::new(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use cstr::cstr;
|
||||||
|
const STEAMVR_SECTION: &CStr = cstr!("steamvr");
|
||||||
|
const COLOR_GAIN_CSTR: [&'static CStr; 3] = [
|
||||||
|
cstr!("hmdDisplayColorGainR"),
|
||||||
|
cstr!("hmdDisplayColorGainG"),
|
||||||
|
cstr!("hmdDisplayColorGainB"),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(super) fn adjust_gain(
|
||||||
|
settings: &mut SettingsManager,
|
||||||
|
ch: ColorChannel,
|
||||||
|
delta: f32,
|
||||||
|
) -> Option<()> {
|
||||||
|
let current = [
|
||||||
|
settings
|
||||||
|
.get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[0])
|
||||||
|
.ok()?,
|
||||||
|
settings
|
||||||
|
.get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[1])
|
||||||
|
.ok()?,
|
||||||
|
settings
|
||||||
|
.get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[2])
|
||||||
|
.ok()?,
|
||||||
|
];
|
||||||
|
|
||||||
|
// prevent user from turning everything black
|
||||||
|
let mut min = if current[0] + current[1] + current[2] < 0.11 {
|
||||||
|
0.1
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
match ch {
|
||||||
|
ColorChannel::R => {
|
||||||
|
settings
|
||||||
|
.set_float(
|
||||||
|
STEAMVR_SECTION,
|
||||||
|
COLOR_GAIN_CSTR[0],
|
||||||
|
(current[0] + delta).clamp(min, 1.0),
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
}
|
||||||
|
ColorChannel::G => {
|
||||||
|
settings
|
||||||
|
.set_float(
|
||||||
|
STEAMVR_SECTION,
|
||||||
|
COLOR_GAIN_CSTR[1],
|
||||||
|
(current[1] + delta).clamp(min, 1.0),
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
}
|
||||||
|
ColorChannel::B => {
|
||||||
|
settings
|
||||||
|
.set_float(
|
||||||
|
STEAMVR_SECTION,
|
||||||
|
COLOR_GAIN_CSTR[2],
|
||||||
|
(current[2] + delta).clamp(min, 1.0),
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
}
|
||||||
|
ColorChannel::All => {
|
||||||
|
min *= 0.3333;
|
||||||
|
settings
|
||||||
|
.set_float(
|
||||||
|
STEAMVR_SECTION,
|
||||||
|
COLOR_GAIN_CSTR[0],
|
||||||
|
(current[0] + delta).clamp(min, 1.0),
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
settings
|
||||||
|
.set_float(
|
||||||
|
STEAMVR_SECTION,
|
||||||
|
COLOR_GAIN_CSTR[1],
|
||||||
|
(current[1] + delta).clamp(min, 1.0),
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
settings
|
||||||
|
.set_float(
|
||||||
|
STEAMVR_SECTION,
|
||||||
|
COLOR_GAIN_CSTR[2],
|
||||||
|
(current[2] + delta).clamp(min, 1.0),
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ use vulkano::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{
|
backend::{
|
||||||
|
common::SystemTask,
|
||||||
input::interact,
|
input::interact,
|
||||||
notifications::NotificationManager,
|
notifications::NotificationManager,
|
||||||
openvr::{
|
openvr::{
|
||||||
|
helpers::adjust_gain,
|
||||||
input::{set_action_manifest, OpenVrInputSource},
|
input::{set_action_manifest, OpenVrInputSource},
|
||||||
lines::LinePool,
|
lines::LinePool,
|
||||||
manifest::{install_manifest, uninstall_manifest},
|
manifest::{install_manifest, uninstall_manifest},
|
||||||
@@ -64,11 +66,11 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
|||||||
|
|
||||||
log::info!("Using OpenVR runtime");
|
log::info!("Using OpenVR runtime");
|
||||||
|
|
||||||
let mut overlay_mngr = context.overlay_mngr();
|
|
||||||
//let mut settings_mngr = context.settings_mngr();
|
|
||||||
let mut app_mgr = context.applications_mngr();
|
let mut app_mgr = context.applications_mngr();
|
||||||
let mut input_mngr = context.input_mngr();
|
let mut input_mngr = context.input_mngr();
|
||||||
let mut system_mngr = context.system_mngr();
|
let mut system_mngr = context.system_mngr();
|
||||||
|
let mut overlay_mngr = context.overlay_mngr();
|
||||||
|
let mut settings_mngr = context.settings_mngr();
|
||||||
let mut chaperone_mgr = context.chaperone_setup_mngr();
|
let mut chaperone_mgr = context.chaperone_setup_mngr();
|
||||||
let mut compositor_mngr = context.compositor_mngr();
|
let mut compositor_mngr = context.compositor_mngr();
|
||||||
|
|
||||||
@@ -185,6 +187,17 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
|||||||
overlays.remove_by_selector(&sel);
|
overlays.remove_by_selector(&sel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TaskType::System(task) => match task {
|
||||||
|
SystemTask::ColorGain(channel, value) => {
|
||||||
|
let _ = adjust_gain(&mut settings_mngr, channel, value);
|
||||||
|
}
|
||||||
|
SystemTask::FixFloor => {
|
||||||
|
space_mover.fix_floor(&mut chaperone_mgr, &state.input_state);
|
||||||
|
}
|
||||||
|
SystemTask::ResetPlayspace => {
|
||||||
|
space_mover.reset_offset(&mut chaperone_mgr);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use glam::Vec3A;
|
use glam::Vec3A;
|
||||||
use ovr_overlay::{chaperone_setup::ChaperoneSetupManager, sys::EChaperoneConfigFile};
|
use ovr_overlay::{chaperone_setup::ChaperoneSetupManager, sys::EChaperoneConfigFile};
|
||||||
|
|
||||||
use crate::{backend::common::OverlayContainer, state::AppState};
|
use crate::{
|
||||||
|
backend::{common::OverlayContainer, input::InputState},
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
use super::overlay::OpenVrOverlayData;
|
use super::overlay::OpenVrOverlayData;
|
||||||
|
|
||||||
@@ -58,6 +61,18 @@ impl PlayspaceMover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) {
|
||||||
|
self.offset = Vec3A::ZERO;
|
||||||
|
self.apply_offset(chaperone_mgr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fix_floor(&mut self, chaperone_mgr: &mut ChaperoneSetupManager, input: &InputState) {
|
||||||
|
let y1 = input.pointers[0].pose.translation.y;
|
||||||
|
let y2 = input.pointers[1].pose.translation.y;
|
||||||
|
self.offset.y += y1.min(y2) - 0.03;
|
||||||
|
self.apply_offset(chaperone_mgr);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.offset = Vec3A::ZERO;
|
self.offset = Vec3A::ZERO;
|
||||||
self.start_position = Vec3A::ZERO;
|
self.start_position = Vec3A::ZERO;
|
||||||
|
|||||||
@@ -330,6 +330,9 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
|
|||||||
// set for deletion after all images are done showing
|
// set for deletion after all images are done showing
|
||||||
delete_queue.push((o, cur_frame + 5));
|
delete_queue.push((o, cur_frame + 5));
|
||||||
}
|
}
|
||||||
|
TaskType::System(_task) => {
|
||||||
|
// Not implemented
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub struct OverlayState {
|
|||||||
pub alpha: f32,
|
pub alpha: f32,
|
||||||
pub transform: Affine3A,
|
pub transform: Affine3A,
|
||||||
pub saved_point: Option<Vec3A>,
|
pub saved_point: Option<Vec3A>,
|
||||||
|
pub saved_scale: Option<f32>,
|
||||||
pub spawn_scale: f32, // aka width
|
pub spawn_scale: f32, // aka width
|
||||||
pub spawn_point: Vec3A,
|
pub spawn_point: Vec3A,
|
||||||
pub spawn_rotation: Quat,
|
pub spawn_rotation: Quat,
|
||||||
@@ -52,6 +53,7 @@ impl Default for OverlayState {
|
|||||||
alpha: 1.0,
|
alpha: 1.0,
|
||||||
relative_to: RelativeTo::None,
|
relative_to: RelativeTo::None,
|
||||||
saved_point: None,
|
saved_point: None,
|
||||||
|
saved_scale: None,
|
||||||
spawn_scale: 1.0,
|
spawn_scale: 1.0,
|
||||||
spawn_point: Vec3A::NEG_Z,
|
spawn_point: Vec3A::NEG_Z,
|
||||||
spawn_rotation: Quat::IDENTITY,
|
spawn_rotation: Quat::IDENTITY,
|
||||||
@@ -97,10 +99,11 @@ impl OverlayState {
|
|||||||
|
|
||||||
pub fn auto_movement(&mut self, app: &mut AppState) {
|
pub fn auto_movement(&mut self, app: &mut AppState) {
|
||||||
if let Some(parent) = self.parent_transform(app) {
|
if let Some(parent) = self.parent_transform(app) {
|
||||||
|
let scale = self.saved_scale.unwrap_or(self.spawn_scale);
|
||||||
let point = self.saved_point.unwrap_or(self.spawn_point);
|
let point = self.saved_point.unwrap_or(self.spawn_point);
|
||||||
self.transform = parent
|
self.transform = parent
|
||||||
* Affine3A::from_scale_rotation_translation(
|
* Affine3A::from_scale_rotation_translation(
|
||||||
Vec3::ONE * self.spawn_scale,
|
Vec3::ONE * scale,
|
||||||
self.spawn_rotation,
|
self.spawn_rotation,
|
||||||
point.into(),
|
point.into(),
|
||||||
);
|
);
|
||||||
@@ -110,13 +113,12 @@ impl OverlayState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self, app: &mut AppState, hard_reset: bool) {
|
pub fn reset(&mut self, app: &mut AppState, hard_reset: bool) {
|
||||||
let scale = if hard_reset {
|
if hard_reset {
|
||||||
self.saved_point = None;
|
self.saved_point = None;
|
||||||
self.spawn_scale
|
self.saved_scale = None;
|
||||||
} else {
|
}
|
||||||
self.transform.x_axis.length()
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let scale = self.saved_scale.unwrap_or(self.spawn_scale);
|
||||||
let point = self.saved_point.unwrap_or(self.spawn_point);
|
let point = self.saved_point.unwrap_or(self.spawn_point);
|
||||||
|
|
||||||
let translation = app.input_state.hmd.transform_point3a(point);
|
let translation = app.input_state.hmd.transform_point3a(point);
|
||||||
@@ -284,17 +286,9 @@ impl InteractionHandler for SplitOverlayBackend {
|
|||||||
|
|
||||||
pub fn ui_transform(extent: &[u32; 2]) -> Affine2 {
|
pub fn ui_transform(extent: &[u32; 2]) -> Affine2 {
|
||||||
let center = Vec2 { x: 0.5, y: 0.5 };
|
let center = Vec2 { x: 0.5, y: 0.5 };
|
||||||
if extent[1] > extent[0] {
|
|
||||||
Affine2::from_cols(
|
|
||||||
Vec2::X * (extent[1] as f32 / extent[0] as f32),
|
|
||||||
Vec2::NEG_Y,
|
|
||||||
center,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Affine2::from_cols(
|
Affine2::from_cols(
|
||||||
Vec2::X,
|
Vec2::X,
|
||||||
Vec2::NEG_Y * (extent[0] as f32 / extent[1] as f32),
|
Vec2::NEG_Y * (extent[0] as f32 / extent[1] as f32),
|
||||||
center,
|
center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,13 +2,26 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::config_io;
|
use crate::config_io;
|
||||||
use crate::config_io::get_conf_d_path;
|
use crate::config_io::get_conf_d_path;
|
||||||
|
use crate::gui::modular::ModularUiConfig;
|
||||||
use crate::load_with_fallback;
|
use crate::load_with_fallback;
|
||||||
use crate::overlays::keyboard;
|
use crate::state::LeftRight;
|
||||||
use crate::overlays::watch::WatchConfig;
|
use anyhow::bail;
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub fn def_watch_pos() -> [f32; 3] {
|
||||||
|
[-0.03, -0.01, 0.125]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn def_watch_rot() -> [f32; 4] {
|
||||||
|
[-0.7071066, 0.0007963618, 0.7071066, 0.0]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn def_left() -> LeftRight {
|
||||||
|
LeftRight::Left
|
||||||
|
}
|
||||||
|
|
||||||
pub fn def_pw_tokens() -> Vec<(String, String)> {
|
pub fn def_pw_tokens() -> Vec<(String, String)> {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
@@ -51,9 +64,24 @@ fn def_auto() -> Arc<str> {
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct GeneralConfig {
|
pub struct GeneralConfig {
|
||||||
|
#[serde(default = "def_watch_pos")]
|
||||||
|
pub watch_pos: [f32; 3],
|
||||||
|
|
||||||
|
#[serde(default = "def_watch_rot")]
|
||||||
|
pub watch_rot: [f32; 4],
|
||||||
|
|
||||||
|
#[serde(default = "def_left")]
|
||||||
|
pub watch_hand: LeftRight,
|
||||||
|
|
||||||
#[serde(default = "def_click_freeze_time_ms")]
|
#[serde(default = "def_click_freeze_time_ms")]
|
||||||
pub click_freeze_time_ms: u32,
|
pub click_freeze_time_ms: u32,
|
||||||
|
|
||||||
|
#[serde(default = "def_true")]
|
||||||
|
pub notifications_enabled: bool,
|
||||||
|
|
||||||
|
#[serde(default = "def_true")]
|
||||||
|
pub notifications_sound_enabled: bool,
|
||||||
|
|
||||||
#[serde(default = "def_true")]
|
#[serde(default = "def_true")]
|
||||||
pub keyboard_sound_enabled: bool,
|
pub keyboard_sound_enabled: bool,
|
||||||
|
|
||||||
@@ -63,15 +91,15 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "def_one")]
|
#[serde(default = "def_one")]
|
||||||
pub desktop_view_scale: f32,
|
pub desktop_view_scale: f32,
|
||||||
|
|
||||||
#[serde(default = "def_one")]
|
|
||||||
pub watch_scale: f32,
|
|
||||||
|
|
||||||
#[serde(default = "def_half")]
|
#[serde(default = "def_half")]
|
||||||
pub watch_view_angle_min: f32,
|
pub watch_view_angle_min: f32,
|
||||||
|
|
||||||
#[serde(default = "def_point7")]
|
#[serde(default = "def_point7")]
|
||||||
pub watch_view_angle_max: f32,
|
pub watch_view_angle_max: f32,
|
||||||
|
|
||||||
|
#[serde(default = "def_one")]
|
||||||
|
pub long_press_duration: f32,
|
||||||
|
|
||||||
#[serde(default = "def_pw_tokens")]
|
#[serde(default = "def_pw_tokens")]
|
||||||
pub pw_tokens: Vec<(String, String)>,
|
pub pw_tokens: Vec<(String, String)>,
|
||||||
|
|
||||||
@@ -110,18 +138,54 @@ impl GeneralConfig {
|
|||||||
fn post_load(&self) {
|
fn post_load(&self) {
|
||||||
GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.0, 5.0);
|
GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.0, 5.0);
|
||||||
GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.0, 5.0);
|
GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.0, 5.0);
|
||||||
GeneralConfig::sanitize_range("watch_scale", self.watch_scale, 0.0, 5.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_keyboard() -> keyboard::Layout {
|
const FALLBACKS: [&str; 3] = [
|
||||||
let yaml_data = load_with_fallback!("keyboard.yaml", "res/keyboard.yaml");
|
include_str!("res/keyboard.yaml"),
|
||||||
serde_yaml::from_str(&yaml_data).expect("Failed to parse keyboard.yaml")
|
include_str!("res/watch.yaml"),
|
||||||
|
include_str!("res/settings.yaml"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const FILES: [&str; 3] = ["keyboard.yaml", "watch.yaml", "settings.yaml"];
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
#[repr(usize)]
|
||||||
|
pub enum ConfigType {
|
||||||
|
Keyboard,
|
||||||
|
Watch,
|
||||||
|
Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_watch() -> WatchConfig {
|
pub fn load_known_yaml<T>(config_type: ConfigType) -> T
|
||||||
let yaml_data = load_with_fallback!("watch.yaml", "res/watch.yaml");
|
where
|
||||||
serde_yaml::from_str(&yaml_data).expect("Failed to parse watch.yaml")
|
T: for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
let fallback = FALLBACKS[config_type as usize];
|
||||||
|
let file_name = FILES[config_type as usize];
|
||||||
|
let maybe_override = config_io::load(file_name);
|
||||||
|
|
||||||
|
for yaml in [maybe_override.as_deref(), Some(fallback)].iter() {
|
||||||
|
if let Some(yaml_data) = yaml {
|
||||||
|
match serde_yaml::from_str::<T>(yaml_data) {
|
||||||
|
Ok(d) => return d,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to parse {}, falling back to defaults.", file_name);
|
||||||
|
error!("{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// can only get here if internal fallback is broken
|
||||||
|
panic!("No usable config found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_custom_ui(name: &str) -> anyhow::Result<ModularUiConfig> {
|
||||||
|
let filename = format!("{}.yaml", name);
|
||||||
|
let Some(yaml_data) = config_io::load(&filename) else {
|
||||||
|
bail!("Could not read file at {}", &filename);
|
||||||
|
};
|
||||||
|
Ok(serde_yaml::from_str(&yaml_data)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_general() -> GeneralConfig {
|
pub fn load_general() -> GeneralConfig {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub mod font;
|
pub mod font;
|
||||||
|
pub mod modular;
|
||||||
|
|
||||||
const RES_DIVIDER: usize = 4;
|
const RES_DIVIDER: usize = 4;
|
||||||
|
|
||||||
|
|||||||
609
src/gui/modular/button.rs
Normal file
609
src/gui/modular/button.rs
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
use std::{
|
||||||
|
f32::consts::PI,
|
||||||
|
io::Cursor,
|
||||||
|
ops::Add,
|
||||||
|
process::{self, Child},
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use glam::{Quat, Vec3A};
|
||||||
|
use rodio::{Decoder, Source};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend::{
|
||||||
|
common::{ColorChannel, OverlaySelector, SystemTask, TaskType},
|
||||||
|
input::PointerMode,
|
||||||
|
overlay::RelativeTo,
|
||||||
|
},
|
||||||
|
overlays::{
|
||||||
|
mirror,
|
||||||
|
toast::Toast,
|
||||||
|
watch::{save_watch, WATCH_NAME},
|
||||||
|
},
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{ExecArgs, ModularControl, ModularData};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Copy)]
|
||||||
|
pub enum ViewAngleKind {
|
||||||
|
/// The cosine of the angle at which the watch becomes fully transparent
|
||||||
|
MinOpacity,
|
||||||
|
/// The cosine of the angle at which the watch becomes fully opaque
|
||||||
|
MaxOpacity,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Copy)]
|
||||||
|
pub enum Axis {
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Z,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub enum SystemAction {
|
||||||
|
PlayspaceResetOffset,
|
||||||
|
PlayspaceFixFloor,
|
||||||
|
RecalculateExtent,
|
||||||
|
PersistConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub enum WatchAction {
|
||||||
|
/// Hide the watch until Show/Hide binding is used
|
||||||
|
Hide,
|
||||||
|
/// Switch the watch to the opposite controller
|
||||||
|
SwitchHands,
|
||||||
|
/// Change the fade behavior of the watch
|
||||||
|
ViewAngle {
|
||||||
|
kind: ViewAngleKind,
|
||||||
|
delta: f32,
|
||||||
|
},
|
||||||
|
Rotation {
|
||||||
|
axis: Axis,
|
||||||
|
delta: f32,
|
||||||
|
},
|
||||||
|
Position {
|
||||||
|
axis: Axis,
|
||||||
|
delta: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub enum OverlayAction {
|
||||||
|
/// Reset the overlay to be in front of the HMD with its original scale
|
||||||
|
Reset,
|
||||||
|
/// Toggle the visibility of the overlay
|
||||||
|
ToggleVisible,
|
||||||
|
/// Toggle the ability to grab and recenter the overlay
|
||||||
|
ToggleImmovable,
|
||||||
|
/// Toggle the ability of the overlay to reacto to laser pointer
|
||||||
|
ToggleInteraction,
|
||||||
|
/// Change the opacity of the overlay
|
||||||
|
Opacity { delta: f32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub enum WindowAction {
|
||||||
|
/// Create a new mirror window, or show/hide an existing one
|
||||||
|
ShowMirror,
|
||||||
|
/// Create a new UI window, or show/hide an existing one
|
||||||
|
ShowUi,
|
||||||
|
/// Destroy a previously created window, if it exists
|
||||||
|
Destroy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum ButtonAction {
|
||||||
|
Exec {
|
||||||
|
command: ExecArgs,
|
||||||
|
toast: Option<Arc<str>>,
|
||||||
|
},
|
||||||
|
Watch {
|
||||||
|
action: WatchAction,
|
||||||
|
},
|
||||||
|
Overlay {
|
||||||
|
target: OverlaySelector,
|
||||||
|
action: OverlayAction,
|
||||||
|
},
|
||||||
|
Window {
|
||||||
|
target: Arc<str>,
|
||||||
|
action: WindowAction,
|
||||||
|
},
|
||||||
|
Toast {
|
||||||
|
message: Arc<str>,
|
||||||
|
body: Option<Arc<str>>,
|
||||||
|
seconds: Option<f32>,
|
||||||
|
},
|
||||||
|
ColorAdjust {
|
||||||
|
channel: ColorChannel,
|
||||||
|
delta: f32,
|
||||||
|
},
|
||||||
|
System {
|
||||||
|
action: SystemAction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct PressData {
|
||||||
|
last_down: Instant,
|
||||||
|
last_mode: PointerMode,
|
||||||
|
child: Option<Child>,
|
||||||
|
}
|
||||||
|
impl Clone for PressData {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
last_down: self.last_down,
|
||||||
|
last_mode: self.last_mode,
|
||||||
|
child: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for PressData {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
last_down: Instant::now(),
|
||||||
|
last_mode: PointerMode::Left,
|
||||||
|
child: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default, Clone)]
|
||||||
|
pub struct ButtonData {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub(super) press: PressData,
|
||||||
|
|
||||||
|
pub(super) click_down: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) click_up: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) long_click_up: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) right_down: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) right_up: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) long_right_up: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) middle_down: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) middle_up: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) long_middle_up: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) scroll_down: Option<Vec<ButtonAction>>,
|
||||||
|
pub(super) scroll_up: Option<Vec<ButtonAction>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
|
||||||
|
button.state = Some(ModularData::Button(data.clone()));
|
||||||
|
button.on_press = Some(modular_button_dn);
|
||||||
|
button.on_release = Some(modular_button_up);
|
||||||
|
button.on_scroll = Some(modular_button_scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modular_button_dn(
|
||||||
|
button: &mut ModularControl,
|
||||||
|
_: &mut (),
|
||||||
|
app: &mut AppState,
|
||||||
|
mode: PointerMode,
|
||||||
|
) {
|
||||||
|
// want panic
|
||||||
|
let ModularData::Button(data) = button.state.as_mut().unwrap() else {
|
||||||
|
panic!("modular_button_dn: button state is not Button");
|
||||||
|
};
|
||||||
|
|
||||||
|
data.press.last_down = Instant::now();
|
||||||
|
data.press.last_mode = mode;
|
||||||
|
|
||||||
|
let actions = match mode {
|
||||||
|
PointerMode::Left => data.click_down.as_ref(),
|
||||||
|
PointerMode::Right => data.right_down.as_ref(),
|
||||||
|
PointerMode::Middle => data.middle_down.as_ref(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(actions) = actions {
|
||||||
|
for action in actions {
|
||||||
|
handle_action(action, &mut data.press, app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modular_button_up(button: &mut ModularControl, _: &mut (), app: &mut AppState) {
|
||||||
|
// want panic
|
||||||
|
let ModularData::Button(data) = button.state.as_mut().unwrap() else {
|
||||||
|
panic!("modular_button_up: button state is not Button");
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let duration = now - data.press.last_down;
|
||||||
|
let long_press = duration.as_secs_f32() > app.session.config.long_press_duration;
|
||||||
|
|
||||||
|
let actions = match data.press.last_mode {
|
||||||
|
PointerMode::Left => {
|
||||||
|
if long_press {
|
||||||
|
data.long_click_up.as_ref()
|
||||||
|
} else {
|
||||||
|
data.click_up.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointerMode::Right => {
|
||||||
|
if long_press {
|
||||||
|
data.long_right_up.as_ref()
|
||||||
|
} else {
|
||||||
|
data.right_up.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointerMode::Middle => {
|
||||||
|
if long_press {
|
||||||
|
data.long_middle_up.as_ref()
|
||||||
|
} else {
|
||||||
|
data.middle_up.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(actions) = actions {
|
||||||
|
for action in actions {
|
||||||
|
handle_action(action, &mut data.press, app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modular_button_scroll(button: &mut ModularControl, _: &mut (), app: &mut AppState, delta: f32) {
|
||||||
|
// want panic
|
||||||
|
let ModularData::Button(data) = button.state.as_mut().unwrap() else {
|
||||||
|
panic!("modular_button_scroll: button state is not Button");
|
||||||
|
};
|
||||||
|
|
||||||
|
let actions = if delta < 0.0 {
|
||||||
|
data.scroll_down.as_ref()
|
||||||
|
} else {
|
||||||
|
data.scroll_up.as_ref()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(actions) = actions {
|
||||||
|
for action in actions {
|
||||||
|
handle_action(action, &mut data.press, app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_action(action: &ButtonAction, press: &mut PressData, app: &mut AppState) {
|
||||||
|
match action {
|
||||||
|
ButtonAction::Exec { command, toast } => run_exec(command, toast, press, app),
|
||||||
|
ButtonAction::Watch { action } => run_watch(action, app),
|
||||||
|
ButtonAction::Overlay { target, action } => run_overlay(target, action, app),
|
||||||
|
ButtonAction::Window { target, action } => run_window(target, action, app),
|
||||||
|
ButtonAction::Toast {
|
||||||
|
message,
|
||||||
|
body,
|
||||||
|
seconds,
|
||||||
|
} => {
|
||||||
|
Toast::new(message.clone(), body.clone().unwrap_or_else(|| "".into()))
|
||||||
|
.with_timeout(seconds.unwrap_or(5.))
|
||||||
|
.submit(app);
|
||||||
|
}
|
||||||
|
ButtonAction::ColorAdjust { channel, delta } => {
|
||||||
|
let channel = *channel;
|
||||||
|
let delta = *delta;
|
||||||
|
app.tasks
|
||||||
|
.enqueue(TaskType::System(SystemTask::ColorGain(channel, delta)));
|
||||||
|
}
|
||||||
|
ButtonAction::System { action } => run_system(action, app),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_system(action: &SystemAction, app: &mut AppState) {
|
||||||
|
match action {
|
||||||
|
SystemAction::PlayspaceResetOffset => {
|
||||||
|
app.tasks
|
||||||
|
.enqueue(TaskType::System(SystemTask::ResetPlayspace));
|
||||||
|
}
|
||||||
|
SystemAction::PlayspaceFixFloor => {
|
||||||
|
let now = Instant::now();
|
||||||
|
let sec = Duration::from_secs(1);
|
||||||
|
for i in 0..5 {
|
||||||
|
let at = now.add(i * sec);
|
||||||
|
let display = 5 - i;
|
||||||
|
Toast::new(
|
||||||
|
format!("Fixing floor in {}", display).into(),
|
||||||
|
"Place either controller on the floor.".into(),
|
||||||
|
)
|
||||||
|
.with_timeout(1.0)
|
||||||
|
.submit_at(app, at);
|
||||||
|
}
|
||||||
|
app.tasks
|
||||||
|
.enqueue_at(TaskType::System(SystemTask::FixFloor), now.add(5 * sec));
|
||||||
|
}
|
||||||
|
SystemAction::RecalculateExtent => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
SystemAction::PersistConfig => {
|
||||||
|
if let Err(e) = save_watch(app) {
|
||||||
|
log::error!("Failed to save watch config: {:?}", e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_exec(args: &ExecArgs, toast: &Option<Arc<str>>, press: &mut PressData, app: &mut AppState) {
|
||||||
|
if let Some(proc) = press.child.as_mut() {
|
||||||
|
match proc.try_wait() {
|
||||||
|
Ok(Some(code)) => {
|
||||||
|
if !code.success() {
|
||||||
|
log::error!("Child process exited with code: {}", code);
|
||||||
|
}
|
||||||
|
press.child = None;
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
log::warn!("Unable to launch child process: previous child not exited yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
press.child = None;
|
||||||
|
log::error!("Error checking child process: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let args = args.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
|
||||||
|
match process::Command::new(args[0]).args(&args[1..]).spawn() {
|
||||||
|
Ok(proc) => {
|
||||||
|
press.child = Some(proc);
|
||||||
|
if let Some(toast) = toast.as_ref() {
|
||||||
|
Toast::new(toast.clone(), "".into()).submit(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to spawn process {:?}: {:?}", args, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_watch(data: &WatchAction, app: &mut AppState) {
|
||||||
|
match data {
|
||||||
|
WatchAction::Hide => {
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
OverlaySelector::Name(WATCH_NAME.into()),
|
||||||
|
Box::new(|app, o| {
|
||||||
|
if o.saved_scale.is_none() {
|
||||||
|
o.saved_scale = Some(o.spawn_scale);
|
||||||
|
o.want_visible = false;
|
||||||
|
o.saved_scale = Some(0.0);
|
||||||
|
Toast::new(
|
||||||
|
"Watch hidden".into(),
|
||||||
|
"Use show/hide binding to restore.".into(),
|
||||||
|
)
|
||||||
|
.with_timeout(3.)
|
||||||
|
.submit(app);
|
||||||
|
} else {
|
||||||
|
o.want_visible = true;
|
||||||
|
o.saved_scale = None;
|
||||||
|
Toast::new("Watch restored".into(), "".into()).submit(app);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
audio_thump(app);
|
||||||
|
}
|
||||||
|
WatchAction::SwitchHands => {
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
OverlaySelector::Name(WATCH_NAME.into()),
|
||||||
|
Box::new(|app, o| {
|
||||||
|
if let RelativeTo::Hand(0) = o.relative_to {
|
||||||
|
o.relative_to = RelativeTo::Hand(1);
|
||||||
|
o.spawn_rotation = Quat::from_slice(&app.session.config.watch_rot)
|
||||||
|
* Quat::from_rotation_x(PI)
|
||||||
|
* Quat::from_rotation_z(PI);
|
||||||
|
o.spawn_point = Vec3A::from_slice(&app.session.config.watch_pos);
|
||||||
|
o.spawn_point.x *= -1.;
|
||||||
|
} else {
|
||||||
|
o.relative_to = RelativeTo::Hand(0);
|
||||||
|
o.spawn_rotation = Quat::from_slice(&app.session.config.watch_rot);
|
||||||
|
o.spawn_point = Vec3A::from_slice(&app.session.config.watch_pos);
|
||||||
|
}
|
||||||
|
o.dirty = true;
|
||||||
|
Toast::new("Watch switched".into(), "Check your other hand".into())
|
||||||
|
.with_timeout(3.)
|
||||||
|
.submit(app);
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
audio_thump(app);
|
||||||
|
}
|
||||||
|
WatchAction::ViewAngle { kind, delta } => match kind {
|
||||||
|
ViewAngleKind::MinOpacity => {
|
||||||
|
let diff = (app.session.config.watch_view_angle_max
|
||||||
|
- app.session.config.watch_view_angle_min)
|
||||||
|
+ delta;
|
||||||
|
|
||||||
|
app.session.config.watch_view_angle_min = (app.session.config.watch_view_angle_max
|
||||||
|
- diff)
|
||||||
|
.clamp(0.0, app.session.config.watch_view_angle_max - 0.05);
|
||||||
|
}
|
||||||
|
ViewAngleKind::MaxOpacity => {
|
||||||
|
let diff = app.session.config.watch_view_angle_max
|
||||||
|
- app.session.config.watch_view_angle_min;
|
||||||
|
|
||||||
|
app.session.config.watch_view_angle_max =
|
||||||
|
(app.session.config.watch_view_angle_max + delta).clamp(0.05, 1.0);
|
||||||
|
|
||||||
|
app.session.config.watch_view_angle_min = (app.session.config.watch_view_angle_max
|
||||||
|
- diff)
|
||||||
|
.clamp(0.0, app.session.config.watch_view_angle_max - 0.05);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WatchAction::Rotation { axis, delta } => {
|
||||||
|
let rot = match axis {
|
||||||
|
Axis::X => Quat::from_rotation_x(delta.to_radians()),
|
||||||
|
Axis::Y => Quat::from_rotation_y(delta.to_radians()),
|
||||||
|
Axis::Z => Quat::from_rotation_z(delta.to_radians()),
|
||||||
|
};
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
OverlaySelector::Name(WATCH_NAME.into()),
|
||||||
|
Box::new(move |app, o| {
|
||||||
|
o.spawn_rotation = o.spawn_rotation * rot;
|
||||||
|
app.session.config.watch_rot = o.spawn_rotation.into();
|
||||||
|
o.dirty = true;
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
WatchAction::Position { axis, delta } => {
|
||||||
|
let delta = *delta;
|
||||||
|
let axis = match axis {
|
||||||
|
Axis::X => 0,
|
||||||
|
Axis::Y => 1,
|
||||||
|
Axis::Z => 2,
|
||||||
|
};
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
OverlaySelector::Name(WATCH_NAME.into()),
|
||||||
|
Box::new(move |app, o| {
|
||||||
|
o.spawn_point[axis] += delta;
|
||||||
|
app.session.config.watch_pos = o.spawn_point.into();
|
||||||
|
o.dirty = true;
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppState) {
|
||||||
|
match action {
|
||||||
|
OverlayAction::Reset => {
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
overlay.clone(),
|
||||||
|
Box::new(|app, o| {
|
||||||
|
o.reset(app, true);
|
||||||
|
Toast::new(format!("{} has been reset!", o.name).into(), "".into()).submit(app);
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
OverlayAction::ToggleVisible => {
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
overlay.clone(),
|
||||||
|
Box::new(|app, o| {
|
||||||
|
o.want_visible = !o.want_visible;
|
||||||
|
if o.recenter {
|
||||||
|
o.show_hide = o.want_visible;
|
||||||
|
o.reset(app, false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
OverlayAction::ToggleImmovable => {
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
overlay.clone(),
|
||||||
|
Box::new(|app, o| {
|
||||||
|
o.recenter = !o.recenter;
|
||||||
|
o.grabbable = o.recenter;
|
||||||
|
o.show_hide = o.recenter;
|
||||||
|
if !o.recenter {
|
||||||
|
Toast::new(
|
||||||
|
format!("{} is now locked in place!", o.name).into(),
|
||||||
|
"".into(),
|
||||||
|
)
|
||||||
|
.submit(app);
|
||||||
|
} else {
|
||||||
|
Toast::new(format!("{} is now unlocked!", o.name).into(), "".into())
|
||||||
|
.submit(app);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
audio_thump(app);
|
||||||
|
}
|
||||||
|
OverlayAction::ToggleInteraction => {
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
overlay.clone(),
|
||||||
|
Box::new(|app, o| {
|
||||||
|
o.interactable = !o.interactable;
|
||||||
|
if !o.interactable {
|
||||||
|
Toast::new(
|
||||||
|
format!("{} is now non-interactable!", o.name).into(),
|
||||||
|
"".into(),
|
||||||
|
)
|
||||||
|
.submit(app);
|
||||||
|
} else {
|
||||||
|
Toast::new(format!("{} is now interactable!", o.name).into(), "".into())
|
||||||
|
.submit(app);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
audio_thump(app);
|
||||||
|
}
|
||||||
|
OverlayAction::Opacity { delta } => {
|
||||||
|
let delta = *delta;
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
overlay.clone(),
|
||||||
|
Box::new(move |_, o| {
|
||||||
|
o.alpha = (o.alpha + delta).clamp(0.1, 1.0);
|
||||||
|
o.dirty = true;
|
||||||
|
log::debug!("{}: alpha {}", o.name, o.alpha);
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wayland")]
|
||||||
|
fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
|
||||||
|
use crate::overlays::custom;
|
||||||
|
let selector = OverlaySelector::Name(window.clone());
|
||||||
|
|
||||||
|
match action {
|
||||||
|
WindowAction::ShowMirror => {
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
selector,
|
||||||
|
Box::new(|app, o| {
|
||||||
|
o.want_visible = !o.want_visible;
|
||||||
|
if o.recenter {
|
||||||
|
o.show_hide = o.want_visible;
|
||||||
|
o.reset(app, false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
app.tasks.enqueue(TaskType::CreateOverlay(
|
||||||
|
OverlaySelector::Name(window.clone()),
|
||||||
|
Box::new({
|
||||||
|
let name = window.clone();
|
||||||
|
move |app| {
|
||||||
|
Toast::new("Check your desktop for popup.".into(), "".into())
|
||||||
|
.with_sound(true)
|
||||||
|
.submit(app);
|
||||||
|
mirror::new_mirror(name.clone(), false, &app.session)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
WindowAction::ShowUi => {
|
||||||
|
app.tasks.enqueue(TaskType::Overlay(
|
||||||
|
selector,
|
||||||
|
Box::new(|app, o| {
|
||||||
|
o.want_visible = !o.want_visible;
|
||||||
|
if o.recenter {
|
||||||
|
o.show_hide = o.want_visible;
|
||||||
|
o.reset(app, false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
app.tasks.enqueue(TaskType::CreateOverlay(
|
||||||
|
OverlaySelector::Name(window.clone()),
|
||||||
|
Box::new({
|
||||||
|
let name = window.clone();
|
||||||
|
move |app| custom::create_custom(app, name)
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
WindowAction::Destroy => {
|
||||||
|
app.tasks
|
||||||
|
.enqueue(TaskType::DropOverlay(OverlaySelector::Name(window.clone())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "wayland"))]
|
||||||
|
fn run_action_window(_: Arc<str>, _: &WindowAction, _: &mut AppState) {
|
||||||
|
log::warn!("Cannot run Window action without Wayland feature.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn audio_thump(app: &mut AppState) {
|
||||||
|
if let Some(handle) = app.audio.get_handle() {
|
||||||
|
let wav = include_bytes!("../../res/380885.wav");
|
||||||
|
let cursor = Cursor::new(wav);
|
||||||
|
let source = Decoder::new_wav(cursor).unwrap();
|
||||||
|
let _ = handle.play_raw(source.convert_samples());
|
||||||
|
}
|
||||||
|
}
|
||||||
223
src/gui/modular/label.rs
Normal file
223
src/gui/modular/label.rs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
use chrono::Local;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
use glam::Vec3;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{
|
||||||
|
io::Read,
|
||||||
|
process::{self, Stdio},
|
||||||
|
sync::Arc,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{gui::modular::FALLBACK_COLOR, state::AppState};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::{color_parse_or_default, ExecArgs, ModularControl, ModularData};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(tag = "source")]
|
||||||
|
pub enum LabelContent {
|
||||||
|
Static {
|
||||||
|
text: Arc<str>,
|
||||||
|
},
|
||||||
|
Exec {
|
||||||
|
exec: ExecArgs,
|
||||||
|
interval: f32,
|
||||||
|
},
|
||||||
|
Clock {
|
||||||
|
format: Arc<str>,
|
||||||
|
timezone: Option<Arc<str>>,
|
||||||
|
},
|
||||||
|
Battery {
|
||||||
|
device: usize,
|
||||||
|
low_threshold: f32,
|
||||||
|
low_color: Arc<str>,
|
||||||
|
charging_color: Arc<str>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum LabelData {
|
||||||
|
Battery {
|
||||||
|
device: usize,
|
||||||
|
low_threshold: f32,
|
||||||
|
normal_color: Vec3,
|
||||||
|
low_color: Vec3,
|
||||||
|
charging_color: Vec3,
|
||||||
|
},
|
||||||
|
Clock {
|
||||||
|
format: Arc<str>,
|
||||||
|
timezone: Option<Tz>,
|
||||||
|
},
|
||||||
|
Exec {
|
||||||
|
last_exec: Instant,
|
||||||
|
interval: f32,
|
||||||
|
exec: Vec<Arc<str>>,
|
||||||
|
child: Option<process::Child>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
|
||||||
|
let state = match content {
|
||||||
|
LabelContent::Battery {
|
||||||
|
device,
|
||||||
|
low_threshold,
|
||||||
|
low_color,
|
||||||
|
charging_color,
|
||||||
|
} => Some(LabelData::Battery {
|
||||||
|
device: *device,
|
||||||
|
low_threshold: *low_threshold,
|
||||||
|
normal_color: label.fg_color,
|
||||||
|
low_color: color_parse_or_default(low_color),
|
||||||
|
charging_color: color_parse_or_default(charging_color),
|
||||||
|
}),
|
||||||
|
LabelContent::Clock { format, timezone } => {
|
||||||
|
let tz: Option<Tz> = timezone.as_ref().map(|tz| {
|
||||||
|
tz.parse().unwrap_or_else(|_| {
|
||||||
|
log::error!("Failed to parse timezone '{}'", &tz);
|
||||||
|
label.set_fg_color(FALLBACK_COLOR);
|
||||||
|
Tz::UTC
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(LabelData::Clock {
|
||||||
|
format: format.clone(),
|
||||||
|
timezone: tz,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
LabelContent::Exec { exec, interval } => Some(LabelData::Exec {
|
||||||
|
last_exec: Instant::now(),
|
||||||
|
interval: *interval,
|
||||||
|
exec: exec.clone(),
|
||||||
|
child: None,
|
||||||
|
}),
|
||||||
|
LabelContent::Static { text } => {
|
||||||
|
label.set_text(&text);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(state) = state {
|
||||||
|
label.state = Some(ModularData::Label(state));
|
||||||
|
label.on_update = Some(label_update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut AppState) {
|
||||||
|
let ModularData::Label(data) = control.state.as_mut().unwrap() else {
|
||||||
|
panic!("Label control has no state");
|
||||||
|
};
|
||||||
|
match data {
|
||||||
|
LabelData::Battery {
|
||||||
|
device,
|
||||||
|
low_threshold,
|
||||||
|
normal_color,
|
||||||
|
low_color,
|
||||||
|
charging_color,
|
||||||
|
} => {
|
||||||
|
let device = app.input_state.devices.get(*device);
|
||||||
|
|
||||||
|
let tags = ["", "H", "L", "R", "T"];
|
||||||
|
|
||||||
|
if let Some(device) = device {
|
||||||
|
let (text, color) = device
|
||||||
|
.soc
|
||||||
|
.map(|soc| {
|
||||||
|
let text = format!(
|
||||||
|
"{}{}",
|
||||||
|
tags[device.role as usize],
|
||||||
|
(soc * 100.).min(99.) as u32
|
||||||
|
);
|
||||||
|
let color = if device.charging {
|
||||||
|
*charging_color
|
||||||
|
} else if soc < *low_threshold {
|
||||||
|
*low_color
|
||||||
|
} else {
|
||||||
|
*normal_color
|
||||||
|
};
|
||||||
|
(text, color)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| ("".into(), Vec3::ZERO));
|
||||||
|
|
||||||
|
control.set_text(&text);
|
||||||
|
control.set_fg_color(color);
|
||||||
|
} else {
|
||||||
|
control.set_text("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LabelData::Clock { format, timezone } => {
|
||||||
|
let format = format.clone();
|
||||||
|
if let Some(tz) = timezone {
|
||||||
|
let date = Local::now().with_timezone(tz);
|
||||||
|
control.set_text(&format!("{}", &date.format(&format)));
|
||||||
|
} else {
|
||||||
|
let date = Local::now();
|
||||||
|
control.set_text(&format!("{}", &date.format(&format)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LabelData::Exec {
|
||||||
|
last_exec,
|
||||||
|
interval,
|
||||||
|
exec,
|
||||||
|
child,
|
||||||
|
} => {
|
||||||
|
if let Some(mut proc) = child.take() {
|
||||||
|
match proc.try_wait() {
|
||||||
|
Ok(Some(code)) => {
|
||||||
|
if !code.success() {
|
||||||
|
log::error!("Child process exited with code: {}", code);
|
||||||
|
} else {
|
||||||
|
if let Some(mut stdout) = proc.stdout.take() {
|
||||||
|
let mut buf = String::new();
|
||||||
|
if stdout.read_to_string(&mut buf).is_ok() {
|
||||||
|
control.set_text(&buf);
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to read stdout for child process");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log::error!("No stdout for child process");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
*child = Some(proc);
|
||||||
|
// not exited yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
*child = None;
|
||||||
|
log::error!("Error checking child process: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Instant::now()
|
||||||
|
.saturating_duration_since(*last_exec)
|
||||||
|
.as_secs_f32()
|
||||||
|
> *interval
|
||||||
|
{
|
||||||
|
*last_exec = Instant::now();
|
||||||
|
let args = exec
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_ref())
|
||||||
|
.collect::<SmallVec<[&str; 8]>>();
|
||||||
|
|
||||||
|
match process::Command::new(args[0])
|
||||||
|
.args(&args[1..])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(proc) => {
|
||||||
|
*child = Some(proc);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to spawn process {:?}: {:?}", args, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
334
src/gui/modular/mod.rs
Normal file
334
src/gui/modular/mod.rs
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
pub mod button;
|
||||||
|
pub mod label;
|
||||||
|
//pub mod slider;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use glam::Vec3;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{backend::common::OverlaySelector, state::AppState};
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
button::{modular_button_init, ButtonAction, ButtonData, OverlayAction},
|
||||||
|
label::{modular_label_init, LabelContent, LabelData},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{color_parse, Canvas, CanvasBuilder, Control};
|
||||||
|
|
||||||
|
type ModularControl = Control<(), ModularData>;
|
||||||
|
type ExecArgs = Vec<Arc<str>>;
|
||||||
|
|
||||||
|
const FALLBACK_COLOR: Vec3 = Vec3 {
|
||||||
|
x: 1.,
|
||||||
|
y: 0.,
|
||||||
|
z: 1.,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ModularUiConfig {
|
||||||
|
pub width: f32,
|
||||||
|
pub size: [u32; 2],
|
||||||
|
pub spawn_pos: Option<[f32; 3]>,
|
||||||
|
pub elements: Vec<ModularElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct OverlayListTemplate {
|
||||||
|
click_down: Option<OverlayAction>,
|
||||||
|
click_up: Option<OverlayAction>,
|
||||||
|
long_click_up: Option<OverlayAction>,
|
||||||
|
right_down: Option<OverlayAction>,
|
||||||
|
right_up: Option<OverlayAction>,
|
||||||
|
long_right_up: Option<OverlayAction>,
|
||||||
|
middle_down: Option<OverlayAction>,
|
||||||
|
middle_up: Option<OverlayAction>,
|
||||||
|
long_middle_up: Option<OverlayAction>,
|
||||||
|
scroll_down: Option<OverlayAction>,
|
||||||
|
scroll_up: Option<OverlayAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum ModularElement {
|
||||||
|
Panel {
|
||||||
|
rect: [f32; 4],
|
||||||
|
bg_color: Arc<str>,
|
||||||
|
},
|
||||||
|
Label {
|
||||||
|
rect: [f32; 4],
|
||||||
|
font_size: isize,
|
||||||
|
fg_color: Arc<str>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
data: LabelContent,
|
||||||
|
},
|
||||||
|
Button {
|
||||||
|
rect: [f32; 4],
|
||||||
|
font_size: isize,
|
||||||
|
fg_color: Arc<str>,
|
||||||
|
bg_color: Arc<str>,
|
||||||
|
text: Arc<str>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
data: ButtonData,
|
||||||
|
},
|
||||||
|
/// Convenience type to save you from having to create a bunch of labels
|
||||||
|
BatteryList {
|
||||||
|
rect: [f32; 4],
|
||||||
|
font_size: isize,
|
||||||
|
fg_color: Arc<str>,
|
||||||
|
fg_color_low: Arc<str>,
|
||||||
|
fg_color_charging: Arc<str>,
|
||||||
|
low_threshold: f32,
|
||||||
|
num_devices: usize,
|
||||||
|
layout: ListLayout,
|
||||||
|
},
|
||||||
|
OverlayList {
|
||||||
|
rect: [f32; 4],
|
||||||
|
font_size: isize,
|
||||||
|
fg_color: Arc<str>,
|
||||||
|
bg_color: Arc<str>,
|
||||||
|
layout: ListLayout,
|
||||||
|
#[serde(flatten)]
|
||||||
|
template: OverlayListTemplate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub enum ButtonFunc {
|
||||||
|
HideWatch,
|
||||||
|
SwitchWatchHand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub enum ListLayout {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ModularData {
|
||||||
|
Label(LabelData),
|
||||||
|
Button(ButtonData),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modular_canvas(
|
||||||
|
size: &[u32; 2],
|
||||||
|
elements: &[ModularElement],
|
||||||
|
state: &AppState,
|
||||||
|
) -> anyhow::Result<Canvas<(), ModularData>> {
|
||||||
|
let mut canvas = CanvasBuilder::new(
|
||||||
|
size[0] as _,
|
||||||
|
size[1] as _,
|
||||||
|
state.graphics.clone(),
|
||||||
|
state.format,
|
||||||
|
(),
|
||||||
|
)?;
|
||||||
|
let empty_str: Arc<str> = Arc::from("");
|
||||||
|
for elem in elements.iter() {
|
||||||
|
match elem {
|
||||||
|
ModularElement::Panel {
|
||||||
|
rect: [x, y, w, h],
|
||||||
|
bg_color,
|
||||||
|
} => {
|
||||||
|
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
||||||
|
canvas.panel(*x, *y, *w, *h);
|
||||||
|
}
|
||||||
|
ModularElement::Label {
|
||||||
|
rect: [x, y, w, h],
|
||||||
|
font_size,
|
||||||
|
fg_color,
|
||||||
|
data,
|
||||||
|
} => {
|
||||||
|
canvas.font_size = *font_size;
|
||||||
|
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
||||||
|
let mut label = canvas.label(*x, *y, *w, *h, empty_str.clone());
|
||||||
|
modular_label_init(&mut label, data);
|
||||||
|
}
|
||||||
|
ModularElement::Button {
|
||||||
|
rect: [x, y, w, h],
|
||||||
|
font_size,
|
||||||
|
bg_color,
|
||||||
|
fg_color,
|
||||||
|
text,
|
||||||
|
data,
|
||||||
|
} => {
|
||||||
|
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
||||||
|
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
||||||
|
canvas.font_size = *font_size;
|
||||||
|
let button = canvas.button(*x, *y, *w, *h, text.clone());
|
||||||
|
modular_button_init(button, data);
|
||||||
|
}
|
||||||
|
ModularElement::BatteryList {
|
||||||
|
rect: [x, y, w, h],
|
||||||
|
font_size,
|
||||||
|
fg_color,
|
||||||
|
fg_color_low,
|
||||||
|
fg_color_charging,
|
||||||
|
low_threshold,
|
||||||
|
num_devices,
|
||||||
|
layout,
|
||||||
|
} => {
|
||||||
|
let num_buttons = *num_devices as f32;
|
||||||
|
let mut button_x = *x;
|
||||||
|
let mut button_y = *y;
|
||||||
|
let low_threshold = low_threshold * 0.01;
|
||||||
|
let (button_w, button_h) = match layout {
|
||||||
|
ListLayout::Horizontal => (*w / num_buttons, *h),
|
||||||
|
ListLayout::Vertical => (*w, *h / num_buttons),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
||||||
|
canvas.font_size = *font_size;
|
||||||
|
canvas.fg_color = fg_color;
|
||||||
|
|
||||||
|
for i in 0..*num_devices {
|
||||||
|
let label = canvas.label_centered(
|
||||||
|
button_x + 2.,
|
||||||
|
button_y + 2.,
|
||||||
|
button_w - 4.,
|
||||||
|
button_h - 4.,
|
||||||
|
empty_str.clone(),
|
||||||
|
);
|
||||||
|
modular_label_init(
|
||||||
|
label,
|
||||||
|
&LabelContent::Battery {
|
||||||
|
device: i,
|
||||||
|
low_threshold,
|
||||||
|
low_color: fg_color_low.clone(),
|
||||||
|
charging_color: fg_color_charging.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
button_x += match layout {
|
||||||
|
ListLayout::Horizontal => button_w,
|
||||||
|
ListLayout::Vertical => 0.,
|
||||||
|
};
|
||||||
|
button_y += match layout {
|
||||||
|
ListLayout::Horizontal => 0.,
|
||||||
|
ListLayout::Vertical => button_h,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ModularElement::OverlayList {
|
||||||
|
rect: [x, y, w, h],
|
||||||
|
font_size,
|
||||||
|
fg_color,
|
||||||
|
bg_color,
|
||||||
|
layout,
|
||||||
|
template,
|
||||||
|
} => {
|
||||||
|
let num_buttons = state.screens.len() as f32;
|
||||||
|
let mut button_x = *x;
|
||||||
|
let mut button_y = *y;
|
||||||
|
let (button_w, button_h) = match layout {
|
||||||
|
ListLayout::Horizontal => (*w / num_buttons, *h),
|
||||||
|
ListLayout::Vertical => (*w, *h / num_buttons),
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
||||||
|
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
||||||
|
canvas.font_size = *font_size;
|
||||||
|
|
||||||
|
for screen in state.screens.iter() {
|
||||||
|
let button = canvas.button(
|
||||||
|
button_x + 2.,
|
||||||
|
button_y + 2.,
|
||||||
|
button_w - 4.,
|
||||||
|
button_h - 4.,
|
||||||
|
screen.name.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// cursed
|
||||||
|
let data = ButtonData {
|
||||||
|
click_down: template.click_down.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
click_up: template.click_up.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
long_click_up: template.long_click_up.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
right_down: template.right_down.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
right_up: template.right_up.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
long_right_up: template.long_right_up.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
middle_down: template.middle_down.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
middle_up: template.middle_up.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
long_middle_up: template.long_middle_up.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
scroll_down: template.scroll_down.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
scroll_up: template.scroll_up.as_ref().map(|f| {
|
||||||
|
vec![ButtonAction::Overlay {
|
||||||
|
target: OverlaySelector::Id(screen.id),
|
||||||
|
action: f.clone(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
modular_button_init(button, &data);
|
||||||
|
|
||||||
|
button_x += match layout {
|
||||||
|
ListLayout::Horizontal => button_w,
|
||||||
|
ListLayout::Vertical => 0.,
|
||||||
|
};
|
||||||
|
button_y += match layout {
|
||||||
|
ListLayout::Horizontal => 0.,
|
||||||
|
ListLayout::Vertical => button_h,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(canvas.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_parse_or_default(color: &str) -> Vec3 {
|
||||||
|
color_parse(color).unwrap_or_else(|e| {
|
||||||
|
log::error!("Failed to parse color '{}': {}", color, e);
|
||||||
|
FALLBACK_COLOR
|
||||||
|
})
|
||||||
|
}
|
||||||
51
src/overlays/custom.rs
Normal file
51
src/overlays/custom.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use glam::Vec3A;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend::overlay::{ui_transform, OverlayBackend, OverlayState},
|
||||||
|
config::{load_custom_ui, load_known_yaml, ConfigType},
|
||||||
|
gui::modular::{modular_canvas, ModularUiConfig},
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SETTINGS_NAME: &str = "settings";
|
||||||
|
|
||||||
|
pub fn create_custom(
|
||||||
|
state: &AppState,
|
||||||
|
name: Arc<str>,
|
||||||
|
) -> Option<(OverlayState, Box<dyn OverlayBackend>)> {
|
||||||
|
let config = if &*name == SETTINGS_NAME {
|
||||||
|
load_known_yaml::<ModularUiConfig>(ConfigType::Settings)
|
||||||
|
} else {
|
||||||
|
match load_custom_ui(&name) {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to load custom UI config for {}: {:?}", name, e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let canvas = match modular_canvas(&config.size, &config.elements, state) {
|
||||||
|
Ok(canvas) => canvas,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to create canvas for {}: {:?}", name, e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = OverlayState {
|
||||||
|
name: name.clone(),
|
||||||
|
want_visible: true,
|
||||||
|
interactable: true,
|
||||||
|
grabbable: true,
|
||||||
|
spawn_scale: config.width,
|
||||||
|
spawn_point: Vec3A::from_array(config.spawn_pos.unwrap_or([0., 0., -0.5])),
|
||||||
|
interaction_transform: ui_transform(&config.size),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let backend = Box::new(canvas);
|
||||||
|
|
||||||
|
Some((state, backend))
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use crate::{
|
|||||||
input::PointerMode,
|
input::PointerMode,
|
||||||
overlay::{OverlayData, OverlayState},
|
overlay::{OverlayData, OverlayState},
|
||||||
},
|
},
|
||||||
config,
|
config::{self, ConfigType},
|
||||||
gui::{color_parse, CanvasBuilder, Control},
|
gui::{color_parse, CanvasBuilder, Control},
|
||||||
hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER},
|
hid::{KeyModifier, VirtualKey, ALT, CTRL, KEYS_TO_MODS, META, SHIFT, SUPER},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@@ -268,7 +268,7 @@ pub struct Layout {
|
|||||||
|
|
||||||
impl Layout {
|
impl Layout {
|
||||||
fn load_from_disk() -> Layout {
|
fn load_from_disk() -> Layout {
|
||||||
let mut layout = config::load_keyboard();
|
let mut layout = config::load_known_yaml::<Layout>(ConfigType::Keyboard);
|
||||||
layout.post_load();
|
layout.post_load();
|
||||||
layout
|
layout
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod custom;
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
#[cfg(feature = "wayland")]
|
#[cfg(feature = "wayland")]
|
||||||
pub mod mirror;
|
pub mod mirror;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::{
|
|||||||
io::Cursor,
|
io::Cursor,
|
||||||
ops::Add,
|
ops::Add,
|
||||||
sync::{atomic::AtomicUsize, Arc},
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rodio::{Decoder, Source};
|
use rodio::{Decoder, Source};
|
||||||
@@ -55,19 +56,24 @@ impl Toast {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn submit(self, app: &mut AppState) {
|
pub fn submit(self, app: &mut AppState) {
|
||||||
|
self.submit_at(app, Instant::now());
|
||||||
|
}
|
||||||
|
pub fn submit_at(self, app: &mut AppState, instant: Instant) {
|
||||||
let auto_increment = AUTO_INCREMENT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
let auto_increment = AUTO_INCREMENT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
let name: Arc<str> = format!("toast-{}", auto_increment).into();
|
let name: Arc<str> = format!("toast-{}", auto_increment).into();
|
||||||
let selector = OverlaySelector::Name(name.clone());
|
let selector = OverlaySelector::Name(name.clone());
|
||||||
|
|
||||||
let destroy_at =
|
let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout));
|
||||||
std::time::Instant::now().add(std::time::Duration::from_secs_f32(self.timeout));
|
|
||||||
|
|
||||||
let has_sound = self.sound;
|
let has_sound = self.sound;
|
||||||
|
|
||||||
app.tasks.enqueue(TaskType::CreateOverlay(
|
app.tasks.enqueue_at(
|
||||||
|
TaskType::CreateOverlay(
|
||||||
selector.clone(),
|
selector.clone(),
|
||||||
Box::new(move |app| new_toast(self, name, app)),
|
Box::new(move |app| new_toast(self, name, app)),
|
||||||
));
|
),
|
||||||
|
instant,
|
||||||
|
);
|
||||||
|
|
||||||
app.tasks
|
app.tasks
|
||||||
.enqueue_at(TaskType::DropOverlay(selector), destroy_at);
|
.enqueue_at(TaskType::DropOverlay(selector), destroy_at);
|
||||||
|
|||||||
@@ -1,907 +1,49 @@
|
|||||||
use std::{
|
use glam::{Quat, Vec3A};
|
||||||
f32::consts::PI,
|
use serde::{Deserialize, Serialize};
|
||||||
io::{Cursor, Read},
|
use std::path::PathBuf;
|
||||||
process::{self, Stdio},
|
|
||||||
sync::Arc,
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
use chrono::Local;
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
use glam::{Quat, Vec3, Vec3A};
|
|
||||||
use rodio::{Decoder, Source};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend::{
|
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
|
||||||
common::{OverlaySelector, TaskType},
|
config::{def_left, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType},
|
||||||
input::PointerMode,
|
config_io,
|
||||||
overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
|
gui::modular::{modular_canvas, ModularUiConfig},
|
||||||
},
|
state::{AppState, LeftRight},
|
||||||
config::load_watch,
|
|
||||||
gui::{color_parse, CanvasBuilder, Control},
|
|
||||||
state::AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{keyboard::KEYBOARD_NAME, toast::Toast};
|
|
||||||
|
|
||||||
const FALLBACK_COLOR: Vec3 = Vec3 {
|
|
||||||
x: 1.,
|
|
||||||
y: 0.,
|
|
||||||
z: 1.,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const WATCH_NAME: &str = "watch";
|
pub const WATCH_NAME: &str = "watch";
|
||||||
pub const WATCH_SCALE: f32 = 0.11;
|
|
||||||
|
|
||||||
pub fn create_watch<O>(
|
pub fn create_watch<O>(state: &AppState) -> anyhow::Result<OverlayData<O>>
|
||||||
state: &AppState,
|
|
||||||
screens: &[OverlayData<O>],
|
|
||||||
) -> anyhow::Result<OverlayData<O>>
|
|
||||||
where
|
where
|
||||||
O: Default,
|
O: Default,
|
||||||
{
|
{
|
||||||
let config = load_watch();
|
let config = load_known_yaml::<ModularUiConfig>(ConfigType::Watch);
|
||||||
|
|
||||||
let mut canvas = CanvasBuilder::new(
|
let canvas = modular_canvas(&config.size, &config.elements, state)?;
|
||||||
config.watch_size[0] as _,
|
|
||||||
config.watch_size[1] as _,
|
|
||||||
state.graphics.clone(),
|
|
||||||
state.format,
|
|
||||||
(),
|
|
||||||
)?;
|
|
||||||
let empty_str: Arc<str> = Arc::from("");
|
|
||||||
|
|
||||||
for elem in config.watch_elements.into_iter() {
|
let relative_to = RelativeTo::Hand(state.session.config.watch_hand as usize);
|
||||||
match elem {
|
|
||||||
WatchElement::Panel {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
bg_color,
|
|
||||||
} => {
|
|
||||||
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.panel(x, y, w, h);
|
|
||||||
}
|
|
||||||
WatchElement::Label {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
fg_color,
|
|
||||||
text,
|
|
||||||
} => {
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.label(x, y, w, h, text);
|
|
||||||
}
|
|
||||||
WatchElement::Clock {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
fg_color,
|
|
||||||
format,
|
|
||||||
timezone,
|
|
||||||
} => {
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
|
|
||||||
let tz: Option<Tz> = timezone.map(|tz| {
|
|
||||||
tz.parse().unwrap_or_else(|_| {
|
|
||||||
log::error!("Failed to parse timezone '{}'", &tz);
|
|
||||||
canvas.fg_color = FALLBACK_COLOR;
|
|
||||||
Tz::UTC
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let label = canvas.label(x, y, w, h, empty_str.clone());
|
|
||||||
label.state = Some(ElemState::Clock {
|
|
||||||
timezone: tz,
|
|
||||||
format,
|
|
||||||
});
|
|
||||||
label.on_update = Some(clock_update);
|
|
||||||
}
|
|
||||||
WatchElement::ExecLabel {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
fg_color,
|
|
||||||
exec,
|
|
||||||
interval,
|
|
||||||
} => {
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
let label = canvas.label(x, y, w, h, empty_str.clone());
|
|
||||||
label.state = Some(ElemState::AutoExec {
|
|
||||||
last_exec: Instant::now(),
|
|
||||||
interval,
|
|
||||||
exec,
|
|
||||||
child: None,
|
|
||||||
});
|
|
||||||
label.on_update = Some(exec_label_update);
|
|
||||||
}
|
|
||||||
WatchElement::ExecButton {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
bg_color,
|
|
||||||
fg_color,
|
|
||||||
exec,
|
|
||||||
text,
|
|
||||||
} => {
|
|
||||||
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
let button = canvas.button(x, y, w, h, text.clone());
|
|
||||||
button.state = Some(ElemState::ExecButton { exec, child: None });
|
|
||||||
button.on_press = Some(exec_button);
|
|
||||||
}
|
|
||||||
WatchElement::FuncButton {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
bg_color,
|
|
||||||
fg_color,
|
|
||||||
func,
|
|
||||||
func_right,
|
|
||||||
func_middle,
|
|
||||||
text,
|
|
||||||
} => {
|
|
||||||
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
let button = canvas.button(x, y, w, h, text.clone());
|
|
||||||
button.state = Some(ElemState::FuncButton {
|
|
||||||
func,
|
|
||||||
func_right,
|
|
||||||
func_middle,
|
|
||||||
});
|
|
||||||
button.on_press = Some(btn_func_dn);
|
|
||||||
}
|
|
||||||
WatchElement::Batteries {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
num_devices,
|
|
||||||
normal_fg_color,
|
|
||||||
low_fg_color,
|
|
||||||
charging_fg_color,
|
|
||||||
low_threshold,
|
|
||||||
layout,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let num_buttons = num_devices as f32;
|
|
||||||
let mut button_x = x;
|
|
||||||
let mut button_y = y;
|
|
||||||
let (button_w, button_h) = match layout {
|
|
||||||
ListLayout::Horizontal => (w / num_buttons, h),
|
|
||||||
ListLayout::Vertical => (w, h / num_buttons),
|
|
||||||
};
|
|
||||||
|
|
||||||
let fg_color = color_parse(&normal_fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
let fg_color_low = color_parse(&low_fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
let fg_color_charging = color_parse(&charging_fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
canvas.fg_color = fg_color;
|
|
||||||
|
|
||||||
for i in 0..num_devices {
|
|
||||||
let label = canvas.label_centered(
|
|
||||||
button_x + 2.,
|
|
||||||
button_y + 2.,
|
|
||||||
button_w - 4.,
|
|
||||||
button_h - 4.,
|
|
||||||
empty_str.clone(),
|
|
||||||
);
|
|
||||||
label.state = Some(ElemState::Battery {
|
|
||||||
device: i as _,
|
|
||||||
low_threshold: low_threshold * 0.01,
|
|
||||||
fg_color,
|
|
||||||
fg_color_low,
|
|
||||||
fg_color_charging,
|
|
||||||
});
|
|
||||||
label.on_update = Some(battery_update);
|
|
||||||
|
|
||||||
button_x += match layout {
|
|
||||||
ListLayout::Horizontal => button_w,
|
|
||||||
ListLayout::Vertical => 0.,
|
|
||||||
};
|
|
||||||
button_y += match layout {
|
|
||||||
ListLayout::Horizontal => 0.,
|
|
||||||
ListLayout::Vertical => button_h,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WatchElement::KeyboardButton {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
fg_color,
|
|
||||||
bg_color,
|
|
||||||
text,
|
|
||||||
} => {
|
|
||||||
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
|
|
||||||
let keyboard = canvas.button(x, y, w, h, text);
|
|
||||||
keyboard.state = Some(ElemState::OverlayButton {
|
|
||||||
pressed_at: Instant::now(),
|
|
||||||
mode: PointerMode::Left,
|
|
||||||
overlay: OverlaySelector::Name(KEYBOARD_NAME.into()),
|
|
||||||
});
|
|
||||||
keyboard.on_press = Some(overlay_button_dn);
|
|
||||||
keyboard.on_release = Some(overlay_button_up);
|
|
||||||
keyboard.on_scroll = Some(overlay_button_scroll);
|
|
||||||
}
|
|
||||||
WatchElement::OverlayList {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
fg_color,
|
|
||||||
bg_color,
|
|
||||||
layout,
|
|
||||||
} => {
|
|
||||||
let num_buttons = screens.len() as f32;
|
|
||||||
let mut button_x = x;
|
|
||||||
let mut button_y = y;
|
|
||||||
let (button_w, button_h) = match layout {
|
|
||||||
ListLayout::Horizontal => (w / num_buttons, h),
|
|
||||||
ListLayout::Vertical => (w, h / num_buttons),
|
|
||||||
};
|
|
||||||
|
|
||||||
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
|
|
||||||
for screen in screens.iter() {
|
|
||||||
let button = canvas.button(
|
|
||||||
button_x + 2.,
|
|
||||||
button_y + 2.,
|
|
||||||
button_w - 4.,
|
|
||||||
button_h - 4.,
|
|
||||||
screen.state.name.clone(),
|
|
||||||
);
|
|
||||||
button.state = Some(ElemState::OverlayButton {
|
|
||||||
pressed_at: Instant::now(),
|
|
||||||
mode: PointerMode::Left,
|
|
||||||
overlay: OverlaySelector::Id(screen.state.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
button.on_press = Some(overlay_button_dn);
|
|
||||||
button.on_release = Some(overlay_button_up);
|
|
||||||
button.on_scroll = Some(overlay_button_scroll);
|
|
||||||
|
|
||||||
button_x += match layout {
|
|
||||||
ListLayout::Horizontal => button_w,
|
|
||||||
ListLayout::Vertical => 0.,
|
|
||||||
};
|
|
||||||
button_y += match layout {
|
|
||||||
ListLayout::Horizontal => 0.,
|
|
||||||
ListLayout::Vertical => button_h,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(feature = "wayland")]
|
|
||||||
WatchElement::MirrorButton {
|
|
||||||
rect: [x, y, w, h],
|
|
||||||
font_size,
|
|
||||||
bg_color,
|
|
||||||
fg_color,
|
|
||||||
text,
|
|
||||||
name,
|
|
||||||
show_hide,
|
|
||||||
} => {
|
|
||||||
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
|
|
||||||
canvas.font_size = font_size;
|
|
||||||
let button = canvas.button(x, y, w, h, text.clone());
|
|
||||||
button.state = Some(ElemState::Mirror { name, show_hide });
|
|
||||||
button.on_press = Some(btn_mirror_dn);
|
|
||||||
button.on_scroll = Some(overlay_button_scroll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative_to = RelativeTo::Hand(state.session.watch_hand);
|
|
||||||
|
|
||||||
Ok(OverlayData {
|
Ok(OverlayData {
|
||||||
state: OverlayState {
|
state: OverlayState {
|
||||||
name: WATCH_NAME.into(),
|
name: WATCH_NAME.into(),
|
||||||
want_visible: true,
|
want_visible: true,
|
||||||
interactable: true,
|
interactable: true,
|
||||||
spawn_scale: WATCH_SCALE * state.session.config.watch_scale,
|
spawn_scale: config.width,
|
||||||
spawn_point: state.session.watch_pos.into(),
|
spawn_point: Vec3A::from_slice(&state.session.config.watch_pos),
|
||||||
spawn_rotation: state.session.watch_rot,
|
spawn_rotation: Quat::from_slice(&state.session.config.watch_rot),
|
||||||
interaction_transform: ui_transform(&config.watch_size),
|
interaction_transform: ui_transform(&config.size),
|
||||||
relative_to,
|
relative_to,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
backend: Box::new(canvas.build()),
|
backend: Box::new(canvas),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ElemState {
|
|
||||||
Battery {
|
|
||||||
device: usize,
|
|
||||||
low_threshold: f32,
|
|
||||||
fg_color: Vec3,
|
|
||||||
fg_color_low: Vec3,
|
|
||||||
fg_color_charging: Vec3,
|
|
||||||
},
|
|
||||||
Clock {
|
|
||||||
timezone: Option<Tz>,
|
|
||||||
format: Arc<str>,
|
|
||||||
},
|
|
||||||
AutoExec {
|
|
||||||
last_exec: Instant,
|
|
||||||
interval: f32,
|
|
||||||
exec: Vec<Arc<str>>,
|
|
||||||
child: Option<process::Child>,
|
|
||||||
},
|
|
||||||
OverlayButton {
|
|
||||||
pressed_at: Instant,
|
|
||||||
mode: PointerMode,
|
|
||||||
overlay: OverlaySelector,
|
|
||||||
},
|
|
||||||
ExecButton {
|
|
||||||
exec: Vec<Arc<str>>,
|
|
||||||
child: Option<process::Child>,
|
|
||||||
},
|
|
||||||
FuncButton {
|
|
||||||
func: ButtonFunc,
|
|
||||||
func_right: Option<ButtonFunc>,
|
|
||||||
func_middle: Option<ButtonFunc>,
|
|
||||||
},
|
|
||||||
#[cfg(feature = "wayland")]
|
|
||||||
Mirror { name: Arc<str>, show_hide: bool },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "wayland")]
|
|
||||||
fn btn_mirror_dn(
|
|
||||||
control: &mut Control<(), ElemState>,
|
|
||||||
_: &mut (),
|
|
||||||
app: &mut AppState,
|
|
||||||
mode: PointerMode,
|
|
||||||
) {
|
|
||||||
let ElemState::Mirror { name, show_hide } = control.state.as_ref().unwrap()
|
|
||||||
// want panic
|
|
||||||
else {
|
|
||||||
log::error!("Mirror state not found");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let selector = OverlaySelector::Name(name.clone());
|
|
||||||
|
|
||||||
match mode {
|
|
||||||
PointerMode::Left => {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
selector.clone(),
|
|
||||||
Box::new(|_app, o| {
|
|
||||||
o.want_visible = !o.want_visible;
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
|
|
||||||
app.tasks.enqueue(TaskType::CreateOverlay(
|
|
||||||
selector,
|
|
||||||
Box::new({
|
|
||||||
let name = name.clone();
|
|
||||||
let show_hide = *show_hide;
|
|
||||||
move |app| super::mirror::new_mirror(name.clone(), show_hide, &app.session)
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
PointerMode::Right => {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
selector,
|
|
||||||
Box::new(|_app, o| {
|
|
||||||
o.grabbable = !o.grabbable;
|
|
||||||
o.interactable = o.grabbable;
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
PointerMode::Middle => {
|
|
||||||
app.tasks.enqueue(TaskType::DropOverlay(selector));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audio_thump(app: &mut AppState) {
|
|
||||||
if let Some(handle) = app.audio.get_handle() {
|
|
||||||
let wav = include_bytes!("../res/380885.wav");
|
|
||||||
let cursor = Cursor::new(wav);
|
|
||||||
let source = Decoder::new_wav(cursor).unwrap();
|
|
||||||
let _ = handle.play_raw(source.convert_samples());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn btn_func_dn(
|
|
||||||
control: &mut Control<(), ElemState>,
|
|
||||||
_: &mut (),
|
|
||||||
app: &mut AppState,
|
|
||||||
mode: PointerMode,
|
|
||||||
) {
|
|
||||||
let ElemState::FuncButton {
|
|
||||||
func,
|
|
||||||
func_right,
|
|
||||||
func_middle,
|
|
||||||
} = control.state.as_ref().unwrap()
|
|
||||||
// want panic
|
|
||||||
else {
|
|
||||||
log::error!("FuncButton state not found");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let func = match mode {
|
|
||||||
PointerMode::Left => func,
|
|
||||||
PointerMode::Right => func_right.as_ref().unwrap_or(func),
|
|
||||||
PointerMode::Middle => func_middle.as_ref().unwrap_or(func),
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
match func {
|
|
||||||
ButtonFunc::HideWatch => {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
OverlaySelector::Name(WATCH_NAME.into()),
|
|
||||||
Box::new(|app, o| {
|
|
||||||
o.want_visible = false;
|
|
||||||
o.spawn_scale = 0.0;
|
|
||||||
Toast::new(
|
|
||||||
"Watch hidden".into(),
|
|
||||||
"Use show/hide button to restore.".into(),
|
|
||||||
)
|
|
||||||
.with_timeout(3.)
|
|
||||||
.submit(app);
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
audio_thump(app);
|
|
||||||
}
|
|
||||||
ButtonFunc::SwitchWatchHand => {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
OverlaySelector::Name(WATCH_NAME.into()),
|
|
||||||
Box::new(|app, o| {
|
|
||||||
if let RelativeTo::Hand(0) = o.relative_to {
|
|
||||||
o.relative_to = RelativeTo::Hand(1);
|
|
||||||
o.spawn_rotation = app.session.watch_rot
|
|
||||||
* Quat::from_rotation_x(PI)
|
|
||||||
* Quat::from_rotation_z(PI);
|
|
||||||
o.spawn_point = app.session.watch_pos.into();
|
|
||||||
o.spawn_point.x *= -1.;
|
|
||||||
} else {
|
|
||||||
o.relative_to = RelativeTo::Hand(0);
|
|
||||||
o.spawn_rotation = app.session.watch_rot;
|
|
||||||
o.spawn_point = app.session.watch_pos.into();
|
|
||||||
}
|
|
||||||
Toast::new("Watch switched".into(), "Check your other hand".into())
|
|
||||||
.with_timeout(3.)
|
|
||||||
.submit(app);
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
audio_thump(app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn battery_update(control: &mut Control<(), ElemState>, _: &mut (), app: &mut AppState) {
|
|
||||||
let ElemState::Battery {
|
|
||||||
device,
|
|
||||||
low_threshold,
|
|
||||||
fg_color,
|
|
||||||
fg_color_low,
|
|
||||||
fg_color_charging,
|
|
||||||
} = control.state.as_ref().unwrap()
|
|
||||||
// want panic
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let device = app.input_state.devices.get(*device);
|
|
||||||
|
|
||||||
let tags = ["", "H", "L", "R", "T"];
|
|
||||||
|
|
||||||
if let Some(device) = device {
|
|
||||||
let (text, color) = device
|
|
||||||
.soc
|
|
||||||
.map(|soc| {
|
|
||||||
let text = format!(
|
|
||||||
"{}{}",
|
|
||||||
tags[device.role as usize],
|
|
||||||
(soc * 100.).min(99.) as u32
|
|
||||||
);
|
|
||||||
let color = if device.charging {
|
|
||||||
*fg_color_charging
|
|
||||||
} else if soc < *low_threshold {
|
|
||||||
*fg_color_low
|
|
||||||
} else {
|
|
||||||
*fg_color
|
|
||||||
};
|
|
||||||
(text, color)
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| ("".into(), Vec3::ZERO));
|
|
||||||
|
|
||||||
control.set_text(&text);
|
|
||||||
control.set_fg_color(color);
|
|
||||||
} else {
|
|
||||||
control.set_text("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exec_button(
|
|
||||||
control: &mut Control<(), ElemState>,
|
|
||||||
_: &mut (),
|
|
||||||
_: &mut AppState,
|
|
||||||
_mode: PointerMode,
|
|
||||||
) {
|
|
||||||
let ElemState::ExecButton {
|
|
||||||
exec,
|
|
||||||
ref mut child,
|
|
||||||
..
|
|
||||||
} = control.state.as_mut().unwrap()
|
|
||||||
// want panic
|
|
||||||
else {
|
|
||||||
log::error!("ExecButton state not found");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if let Some(proc) = child {
|
|
||||||
match proc.try_wait() {
|
|
||||||
Ok(Some(code)) => {
|
|
||||||
if !code.success() {
|
|
||||||
log::error!("Child process exited with code: {}", code);
|
|
||||||
}
|
|
||||||
*child = None;
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
log::warn!("Unable to launch child process: previous child not exited yet");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
*child = None;
|
|
||||||
log::error!("Error checking child process: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let args = exec.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
|
|
||||||
match process::Command::new(args[0]).args(&args[1..]).spawn() {
|
|
||||||
Ok(proc) => {
|
|
||||||
*child = Some(proc);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to spawn process {:?}: {:?}", args, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exec_label_update(control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState) {
|
|
||||||
let ElemState::AutoExec {
|
|
||||||
ref mut last_exec,
|
|
||||||
interval,
|
|
||||||
exec,
|
|
||||||
ref mut child,
|
|
||||||
} = control.state.as_mut().unwrap()
|
|
||||||
// want panic
|
|
||||||
else {
|
|
||||||
log::error!("AutoExec state not found");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(mut proc) = child.take() {
|
|
||||||
match proc.try_wait() {
|
|
||||||
Ok(Some(code)) => {
|
|
||||||
if !code.success() {
|
|
||||||
log::error!("Child process exited with code: {}", code);
|
|
||||||
} else {
|
|
||||||
if let Some(mut stdout) = proc.stdout.take() {
|
|
||||||
let mut buf = String::new();
|
|
||||||
if stdout.read_to_string(&mut buf).is_ok() {
|
|
||||||
control.set_text(&buf);
|
|
||||||
} else {
|
|
||||||
log::error!("Failed to read stdout for child process");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log::error!("No stdout for child process");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
*child = Some(proc);
|
|
||||||
// not exited yet
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
*child = None;
|
|
||||||
log::error!("Error checking child process: {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if Instant::now()
|
|
||||||
.saturating_duration_since(*last_exec)
|
|
||||||
.as_secs_f32()
|
|
||||||
> *interval
|
|
||||||
{
|
|
||||||
*last_exec = Instant::now();
|
|
||||||
let args = exec.iter().map(|s| s.as_ref()).collect::<Vec<&str>>();
|
|
||||||
|
|
||||||
match process::Command::new(args[0])
|
|
||||||
.args(&args[1..])
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
{
|
|
||||||
Ok(proc) => {
|
|
||||||
*child = Some(proc);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to spawn process {:?}: {:?}", args, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clock_update(control: &mut Control<(), ElemState>, _: &mut (), _: &mut AppState) {
|
|
||||||
// want panic
|
|
||||||
let ElemState::Clock { timezone, format } = control.state.as_ref().unwrap() else {
|
|
||||||
log::error!("Clock state not found");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(tz) = timezone {
|
|
||||||
let date = Local::now().with_timezone(tz);
|
|
||||||
control.set_text(&format!("{}", &date.format(format)));
|
|
||||||
} else {
|
|
||||||
let date = Local::now();
|
|
||||||
control.set_text(&format!("{}", &date.format(format)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overlay_button_scroll(
|
|
||||||
control: &mut Control<(), ElemState>,
|
|
||||||
_: &mut (),
|
|
||||||
app: &mut AppState,
|
|
||||||
delta: f32,
|
|
||||||
) {
|
|
||||||
// want panic
|
|
||||||
let overlay = match &mut control.state.as_mut().unwrap() {
|
|
||||||
ElemState::OverlayButton { overlay, .. } => overlay.clone(),
|
|
||||||
ElemState::Mirror { name, .. } => OverlaySelector::Name(name.clone()),
|
|
||||||
_ => {
|
|
||||||
log::error!("OverlayButton state not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if delta > 0. {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
overlay,
|
|
||||||
Box::new(|_, o| {
|
|
||||||
o.alpha = (o.alpha + 0.025).min(1.);
|
|
||||||
o.dirty = true;
|
|
||||||
log::debug!("{}: alpha {}", o.name, o.alpha);
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
overlay,
|
|
||||||
Box::new(|_, o| {
|
|
||||||
o.alpha = (o.alpha - 0.025).max(0.1);
|
|
||||||
o.dirty = true;
|
|
||||||
log::debug!("{}: alpha {}", o.name, o.alpha);
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overlay_button_dn(
|
|
||||||
control: &mut Control<(), ElemState>,
|
|
||||||
_: &mut (),
|
|
||||||
_: &mut AppState,
|
|
||||||
ptr_mode: PointerMode,
|
|
||||||
) {
|
|
||||||
let ElemState::OverlayButton {
|
|
||||||
ref mut pressed_at,
|
|
||||||
ref mut mode,
|
|
||||||
..
|
|
||||||
} = control.state.as_mut().unwrap()
|
|
||||||
// want panic
|
|
||||||
else {
|
|
||||||
log::error!("OverlayButton state not found");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
*pressed_at = Instant::now();
|
|
||||||
*mode = ptr_mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn overlay_button_up(control: &mut Control<(), ElemState>, _: &mut (), app: &mut AppState) {
|
|
||||||
let ElemState::OverlayButton {
|
|
||||||
pressed_at,
|
|
||||||
mode,
|
|
||||||
overlay,
|
|
||||||
} = control.state.as_ref().unwrap()
|
|
||||||
// want panic
|
|
||||||
else {
|
|
||||||
log::error!("OverlayButton state not found");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if Instant::now()
|
|
||||||
.saturating_duration_since(*pressed_at)
|
|
||||||
.as_millis()
|
|
||||||
< 2000
|
|
||||||
{
|
|
||||||
match mode {
|
|
||||||
PointerMode::Left => {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
overlay.clone(),
|
|
||||||
Box::new(|app, o| {
|
|
||||||
o.want_visible = !o.want_visible;
|
|
||||||
if o.recenter {
|
|
||||||
o.show_hide = o.want_visible;
|
|
||||||
o.reset(app, false);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
PointerMode::Right => {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
overlay.clone(),
|
|
||||||
Box::new(|app, o| {
|
|
||||||
o.recenter = !o.recenter;
|
|
||||||
o.grabbable = o.recenter;
|
|
||||||
o.show_hide = o.recenter;
|
|
||||||
if !o.recenter {
|
|
||||||
Toast::new(
|
|
||||||
format!("{} is now locked in place!", o.name).into(),
|
|
||||||
"Right-click again to toggle.".into(),
|
|
||||||
)
|
|
||||||
.submit(app);
|
|
||||||
} else {
|
|
||||||
Toast::new(format!("{} is now unlocked!", o.name).into(), "".into())
|
|
||||||
.submit(app);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
audio_thump(app);
|
|
||||||
}
|
|
||||||
PointerMode::Middle => {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
overlay.clone(),
|
|
||||||
Box::new(|app, o| {
|
|
||||||
o.interactable = !o.interactable;
|
|
||||||
if !o.interactable {
|
|
||||||
Toast::new(
|
|
||||||
format!("{} is now non-interactable!", o.name).into(),
|
|
||||||
"Middle-click again to toggle.".into(),
|
|
||||||
)
|
|
||||||
.submit(app);
|
|
||||||
} else {
|
|
||||||
Toast::new(
|
|
||||||
format!("{} is now interactable!", o.name).into(),
|
|
||||||
"".into(),
|
|
||||||
)
|
|
||||||
.submit(app);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
audio_thump(app);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
app.tasks.enqueue(TaskType::Overlay(
|
|
||||||
overlay.clone(),
|
|
||||||
Box::new(|app, o| {
|
|
||||||
o.reset(app, true);
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct WatchConfig {
|
|
||||||
watch_hand: LeftRight,
|
|
||||||
watch_size: [u32; 2],
|
|
||||||
watch_elements: Vec<WatchElement>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
enum WatchElement {
|
|
||||||
Panel {
|
|
||||||
rect: [f32; 4],
|
|
||||||
bg_color: Arc<str>,
|
|
||||||
},
|
|
||||||
Label {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
fg_color: Arc<str>,
|
|
||||||
text: Arc<str>,
|
|
||||||
},
|
|
||||||
Clock {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
fg_color: Arc<str>,
|
|
||||||
format: Arc<str>,
|
|
||||||
timezone: Option<Arc<str>>,
|
|
||||||
},
|
|
||||||
ExecLabel {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
fg_color: Arc<str>,
|
|
||||||
exec: Vec<Arc<str>>,
|
|
||||||
interval: f32,
|
|
||||||
},
|
|
||||||
ExecButton {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
bg_color: Arc<str>,
|
|
||||||
fg_color: Arc<str>,
|
|
||||||
exec: Vec<Arc<str>>,
|
|
||||||
text: Arc<str>,
|
|
||||||
},
|
|
||||||
Batteries {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
low_threshold: f32,
|
|
||||||
num_devices: u16,
|
|
||||||
normal_fg_color: Arc<str>,
|
|
||||||
normal_bg_color: Arc<str>,
|
|
||||||
low_fg_color: Arc<str>,
|
|
||||||
low_bg_color: Arc<str>,
|
|
||||||
charging_fg_color: Arc<str>,
|
|
||||||
charging_bg_color: Arc<str>,
|
|
||||||
layout: ListLayout,
|
|
||||||
},
|
|
||||||
KeyboardButton {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
fg_color: Arc<str>,
|
|
||||||
bg_color: Arc<str>,
|
|
||||||
text: Arc<str>,
|
|
||||||
},
|
|
||||||
OverlayList {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
fg_color: Arc<str>,
|
|
||||||
bg_color: Arc<str>,
|
|
||||||
layout: ListLayout,
|
|
||||||
},
|
|
||||||
FuncButton {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
bg_color: Arc<str>,
|
|
||||||
fg_color: Arc<str>,
|
|
||||||
func: ButtonFunc,
|
|
||||||
func_right: Option<ButtonFunc>,
|
|
||||||
func_middle: Option<ButtonFunc>,
|
|
||||||
text: Arc<str>,
|
|
||||||
},
|
|
||||||
#[cfg(feature = "wayland")]
|
|
||||||
MirrorButton {
|
|
||||||
rect: [f32; 4],
|
|
||||||
font_size: isize,
|
|
||||||
bg_color: Arc<str>,
|
|
||||||
fg_color: Arc<str>,
|
|
||||||
name: Arc<str>,
|
|
||||||
text: Arc<str>,
|
|
||||||
show_hide: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
enum ButtonFunc {
|
|
||||||
HideWatch,
|
|
||||||
SwitchWatchHand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
enum ListLayout {
|
|
||||||
Horizontal,
|
|
||||||
Vertical,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
enum LeftRight {
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn watch_fade<D>(app: &mut AppState, watch: &mut OverlayData<D>)
|
pub fn watch_fade<D>(app: &mut AppState, watch: &mut OverlayData<D>)
|
||||||
where
|
where
|
||||||
D: Default,
|
D: Default,
|
||||||
{
|
{
|
||||||
if watch.state.spawn_scale < f32::EPSILON {
|
if watch.state.saved_scale.is_some_and(|s| s < f32::EPSILON) {
|
||||||
watch.state.want_visible = false;
|
watch.state.want_visible = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -925,3 +67,33 @@ where
|
|||||||
watch.state.alpha = watch.state.alpha.clamp(0., 1.);
|
watch.state.alpha = watch.state.alpha.clamp(0., 1.);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct WatchConf {
|
||||||
|
#[serde(default = "def_watch_pos")]
|
||||||
|
pub watch_pos: [f32; 3],
|
||||||
|
|
||||||
|
#[serde(default = "def_watch_rot")]
|
||||||
|
pub watch_rot: [f32; 4],
|
||||||
|
|
||||||
|
#[serde(default = "def_left")]
|
||||||
|
pub watch_hand: LeftRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_path() -> PathBuf {
|
||||||
|
let mut path = config_io::get_conf_d_path();
|
||||||
|
path.push("watch_state.yaml");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
pub fn save_watch(app: &mut AppState) -> anyhow::Result<()> {
|
||||||
|
let conf = WatchConf {
|
||||||
|
watch_pos: app.session.config.watch_pos.clone(),
|
||||||
|
watch_rot: app.session.config.watch_rot.clone(),
|
||||||
|
watch_hand: app.session.config.watch_hand,
|
||||||
|
};
|
||||||
|
|
||||||
|
let yaml = serde_yaml::to_string(&conf)?;
|
||||||
|
std::fs::write(get_config_path(), yaml)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
519
src/res/settings.yaml
Normal file
519
src/res/settings.yaml
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
# lookng to make changes?
|
||||||
|
# drop me in ~/.config/wlxoverlay.settings.yaml
|
||||||
|
#
|
||||||
|
# i will likely change this later. edit with that in mind
|
||||||
|
|
||||||
|
width: 0.3
|
||||||
|
|
||||||
|
size: [600, 520]
|
||||||
|
|
||||||
|
# +X: right, +Y: up, +Z: back
|
||||||
|
spawn_pos: [0, -0.1, -0.5]
|
||||||
|
|
||||||
|
elements:
|
||||||
|
- type: Panel
|
||||||
|
rect: [0, 0, 600, 800]
|
||||||
|
bg_color: "#102030"
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [15, 35, 600, 70]
|
||||||
|
font_size: 24
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Settings
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [560, 0, 40, 40]
|
||||||
|
font_size: 16
|
||||||
|
bg_color: "#880000"
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
text: X
|
||||||
|
click_down:
|
||||||
|
- type: Window
|
||||||
|
target: "settings"
|
||||||
|
action: Destroy
|
||||||
|
|
||||||
|
- type: Panel
|
||||||
|
rect: [50, 53, 500, 1]
|
||||||
|
bg_color: "#c0c0c0"
|
||||||
|
|
||||||
|
####### Watch Section #######
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [15, 85, 570, 24]
|
||||||
|
font_size: 18
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Watch
|
||||||
|
|
||||||
|
- type: Panel
|
||||||
|
rect: [250, 105, 1, 100]
|
||||||
|
bg_color: "#c0c0c0"
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [288, 105, 100, 24]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Visibility
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [270, 120, 100, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "Hide"
|
||||||
|
click_down:
|
||||||
|
- type: Watch
|
||||||
|
action: Hide
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [270, 170, 100, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "Swap Hand"
|
||||||
|
click_down:
|
||||||
|
- type: Watch
|
||||||
|
action: SwitchHands
|
||||||
|
|
||||||
|
- type: Panel
|
||||||
|
rect: [390, 105, 1, 100]
|
||||||
|
bg_color: "#c0c0c0"
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [430, 105, 120, 24]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Watch Fade
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [410, 120, 140, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "Cutoff Point"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
ViewAngle: {kind: "MaxOpacity", delta: 0.01}
|
||||||
|
scroll_down:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
ViewAngle: {kind: "MaxOpacity", delta: -0.01}
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [410, 170, 140, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "Cutoff Strength"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
ViewAngle: {kind: "MinOpacity", delta: 0.01}
|
||||||
|
scroll_down:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
ViewAngle: {kind: "MinOpacity", delta: -0.01}
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [25, 140, 90, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Rotation
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [108, 120, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "X"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Rotation: {axis: "X", delta: 0.25}
|
||||||
|
scroll_down:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Rotation: {axis: "X", delta: -0.25}
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [153, 120, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "Y"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Rotation: {axis: "Y", delta: 0.25}
|
||||||
|
scroll_down:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Rotation: {axis: "Y", delta: -0.25}
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [198, 120, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "Z"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Rotation: {axis: "Z", delta: 0.25}
|
||||||
|
scroll_down:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Rotation: {axis: "Z", delta: -0.25}
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [25, 190, 90, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Position
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [108, 170, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "X"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Position: {axis: "X", delta: 0.001}
|
||||||
|
scroll_down:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Position: {axis: "X", delta: -0.001}
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [153, 170, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "Y"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Position: {axis: "Y", delta: 0.001}
|
||||||
|
scroll_down:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Position: {axis: "Y", delta: -0.001}
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [198, 170, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#306060"
|
||||||
|
text: "Z"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Position: {axis: "Z", delta: 0.001}
|
||||||
|
scroll_down:
|
||||||
|
- type: Watch
|
||||||
|
action:
|
||||||
|
Position: {axis: "Z", delta: -0.001}
|
||||||
|
|
||||||
|
- type: Panel
|
||||||
|
rect: [50, 220, 500, 1]
|
||||||
|
bg_color: "#c0c0c0"
|
||||||
|
|
||||||
|
####### Mirror Section #######
|
||||||
|
- type: Label
|
||||||
|
rect: [15, 255, 570, 24]
|
||||||
|
font_size: 18
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Mirrors
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [25, 290, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: M1
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [60, 270, 110, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#707070"
|
||||||
|
text: "Show/Hide"
|
||||||
|
click_down:
|
||||||
|
- type: Window
|
||||||
|
target: M1
|
||||||
|
action: ShowMirror
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [185, 270, 60, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#707070"
|
||||||
|
text: "Lock"
|
||||||
|
click_down:
|
||||||
|
- type: Overlay
|
||||||
|
target: M1
|
||||||
|
action: ToggleInteraction
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [258, 270, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#880000"
|
||||||
|
text: "X"
|
||||||
|
click_down:
|
||||||
|
- type: Window
|
||||||
|
target: M1
|
||||||
|
action: Destroy
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [25, 340, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: M2
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [60, 320, 110, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#707070"
|
||||||
|
text: "Show/Hide"
|
||||||
|
click_down:
|
||||||
|
- type: Window
|
||||||
|
target: M2
|
||||||
|
action: ShowMirror
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [185, 320, 60, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#707070"
|
||||||
|
text: "Lock"
|
||||||
|
click_down:
|
||||||
|
- type: Overlay
|
||||||
|
target: M2
|
||||||
|
action: ToggleInteraction
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [258, 320, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#880000"
|
||||||
|
text: "X"
|
||||||
|
click_down:
|
||||||
|
- type: Window
|
||||||
|
target: M2
|
||||||
|
action: Destroy
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [25, 390, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: M3
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [60, 370, 110, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#707070"
|
||||||
|
text: "Show/Hide"
|
||||||
|
click_down:
|
||||||
|
- type: Window
|
||||||
|
target: M3
|
||||||
|
action: ShowMirror
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [185, 370, 60, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#707070"
|
||||||
|
text: "Lock"
|
||||||
|
click_down:
|
||||||
|
- type: Overlay
|
||||||
|
target: M3
|
||||||
|
action: ToggleInteraction
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [258, 370, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#880000"
|
||||||
|
text: "X"
|
||||||
|
click_down:
|
||||||
|
- type: Window
|
||||||
|
target: M3
|
||||||
|
action: Destroy
|
||||||
|
|
||||||
|
- type: Panel
|
||||||
|
rect: [300, 240, 1, 200]
|
||||||
|
bg_color: "#c0c0c0"
|
||||||
|
|
||||||
|
####### Color Gain Section #######
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [325, 255, 90, 24]
|
||||||
|
font_size: 18
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Color Gain
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [470, 255, 90, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: (SteamVR)
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [330, 270, 60, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#707070"
|
||||||
|
text: "All"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: ColorAdjust
|
||||||
|
channel: All
|
||||||
|
delta: 0.01
|
||||||
|
scroll_down:
|
||||||
|
- type: ColorAdjust
|
||||||
|
channel: All
|
||||||
|
delta: -0.01
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [405, 270, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#701010"
|
||||||
|
text: "R"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: ColorAdjust
|
||||||
|
channel: R
|
||||||
|
delta: 0.01
|
||||||
|
scroll_down:
|
||||||
|
- type: ColorAdjust
|
||||||
|
channel: R
|
||||||
|
delta: -0.01
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [450, 270, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#107010"
|
||||||
|
text: "G"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: ColorAdjust
|
||||||
|
channel: G
|
||||||
|
delta: 0.01
|
||||||
|
scroll_down:
|
||||||
|
- type: ColorAdjust
|
||||||
|
channel: G
|
||||||
|
delta: -0.01
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [495, 270, 30, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#101070"
|
||||||
|
text: "B"
|
||||||
|
click_down:
|
||||||
|
- type: Toast
|
||||||
|
message: Use stick up/down while hovering the button!
|
||||||
|
scroll_up:
|
||||||
|
- type: ColorAdjust
|
||||||
|
channel: B
|
||||||
|
delta: 0.01
|
||||||
|
scroll_down:
|
||||||
|
- type: ColorAdjust
|
||||||
|
channel: B
|
||||||
|
delta: -0.01
|
||||||
|
|
||||||
|
- type: Panel
|
||||||
|
rect: [325, 315, 225, 1]
|
||||||
|
bg_color: "#c0c0c0"
|
||||||
|
|
||||||
|
####### Playspace Section #######
|
||||||
|
|
||||||
|
- type: Label
|
||||||
|
rect: [325, 345, 90, 24]
|
||||||
|
font_size: 18
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Static
|
||||||
|
text: Playspace
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [330, 360, 220, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#206060"
|
||||||
|
text: "Fix Floor"
|
||||||
|
click_down:
|
||||||
|
- type: System
|
||||||
|
action: PlayspaceFixFloor
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [330, 410, 220, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#206060"
|
||||||
|
text: "Reset Offset"
|
||||||
|
click_down:
|
||||||
|
- type: System
|
||||||
|
action: PlayspaceResetOffset
|
||||||
|
|
||||||
|
- type: Panel
|
||||||
|
rect: [50, 460, 500, 1]
|
||||||
|
bg_color: "#c0c0c0"
|
||||||
|
|
||||||
|
- type: Button
|
||||||
|
rect: [330, 480, 220, 30]
|
||||||
|
font_size: 12
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
bg_color: "#206060"
|
||||||
|
text: "Save Config"
|
||||||
|
click_down:
|
||||||
|
- type: System
|
||||||
|
action: PersistConfig
|
||||||
|
|
||||||
@@ -1,61 +1,61 @@
|
|||||||
# Tips for optimization:
|
# lookng to make changes?
|
||||||
# - try to re-use font sizes, every loaded font size uses additional VRAM.
|
# drop me in ~/.config/wlxoverlay.settings.yaml
|
||||||
|
|
||||||
# common properties:
|
|
||||||
# - rect: bounding rectangle [x, y, width, height]
|
|
||||||
# (x, y) = (0, 0) is top-left
|
|
||||||
# - bg_color: background color of panels and buttons
|
|
||||||
# - fg_color: color of text
|
|
||||||
#
|
#
|
||||||
# element types:
|
|
||||||
# panel - simple colored rectangle
|
|
||||||
# rect: bounding rectangle
|
|
||||||
# bg_color: color of rectangle. quotation marks mandatory.
|
|
||||||
#
|
|
||||||
# clock - date/time display with custom formatting
|
|
||||||
# rect: bounding rectangle
|
|
||||||
# fg_color: color of text. quotation marks mandatory.
|
|
||||||
# format: chrono format to print, see https://docs.rs/chrono/latest/chrono/format/strftime/index.html
|
|
||||||
# timezone: timezone to use, leave empty for local
|
|
||||||
|
|
||||||
# render resolution of the watch
|
width: 0.115
|
||||||
|
|
||||||
watch_view_angle: 0.5 # 0 = 90 deg, 1 = 0 deg
|
size: [400, 200]
|
||||||
|
|
||||||
# TODO
|
elements:
|
||||||
watch_hand: Left
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
watch_offset: []
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
watch_rotation: []
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
watch_size: [400, 200]
|
|
||||||
|
|
||||||
watch_elements:
|
|
||||||
# background panel
|
# background panel
|
||||||
- type: Panel
|
- type: Panel
|
||||||
rect: [0, 0, 400, 200]
|
rect: [0, 0, 400, 200]
|
||||||
bg_color: "#353535"
|
bg_color: "#353535"
|
||||||
|
|
||||||
- type: FuncButton
|
- type: Button
|
||||||
rect: [2, 162, 26, 36]
|
rect: [2, 162, 26, 36]
|
||||||
font_size: 14
|
font_size: 14
|
||||||
bg_color: "#808040"
|
bg_color: "#808040"
|
||||||
fg_color: "#ffffff"
|
fg_color: "#ffffff"
|
||||||
func: SwitchWatchHand
|
text: "C"
|
||||||
func_right: HideWatch
|
click_up:
|
||||||
func_middle: ~
|
- type: Window
|
||||||
text: "W"
|
target: settings
|
||||||
|
action: ShowUi
|
||||||
|
|
||||||
- type: KeyboardButton
|
# Keyboard button
|
||||||
|
- type: Button
|
||||||
rect: [32, 162, 60, 36]
|
rect: [32, 162, 60, 36]
|
||||||
font_size: 14
|
font_size: 14
|
||||||
fg_color: "#FFFFFF"
|
fg_color: "#FFFFFF"
|
||||||
bg_color: "#406050"
|
bg_color: "#406050"
|
||||||
text: "Kbd"
|
text: Kbd
|
||||||
|
click_up:
|
||||||
|
- type: Overlay
|
||||||
|
target: "kbd"
|
||||||
|
action: ToggleVisible
|
||||||
|
long_click_up:
|
||||||
|
- type: Overlay
|
||||||
|
target: "kbd"
|
||||||
|
action: Reset
|
||||||
|
right_up:
|
||||||
|
- type: Overlay
|
||||||
|
target: "kbd"
|
||||||
|
action: ToggleImmovable
|
||||||
|
middle_up:
|
||||||
|
- type: Overlay
|
||||||
|
target: "kbd"
|
||||||
|
action: ToggleInteraction
|
||||||
|
scroll_up:
|
||||||
|
- type: Overlay
|
||||||
|
target: "kbd"
|
||||||
|
action:
|
||||||
|
Opacity: { delta: 0.025 }
|
||||||
|
scroll_down:
|
||||||
|
- type: Overlay
|
||||||
|
target: "kbd"
|
||||||
|
action:
|
||||||
|
Opacity: { delta: -0.025 }
|
||||||
|
|
||||||
# bottom row, of keyboard + overlays
|
# bottom row, of keyboard + overlays
|
||||||
- type: OverlayList
|
- type: OverlayList
|
||||||
@@ -64,109 +64,100 @@ watch_elements:
|
|||||||
fg_color: "#FFFFFF"
|
fg_color: "#FFFFFF"
|
||||||
bg_color: "#405060"
|
bg_color: "#405060"
|
||||||
layout: Horizontal
|
layout: Horizontal
|
||||||
|
click_up: ToggleVisible
|
||||||
|
long_click_up: Reset
|
||||||
|
right_up: ToggleImmovable
|
||||||
|
middle_up: ToggleInteraction
|
||||||
|
scroll_up:
|
||||||
|
Opacity: { delta: 0.025 }
|
||||||
|
scroll_down:
|
||||||
|
Opacity: { delta: -0.025 }
|
||||||
|
|
||||||
# main clock with date and day-of-week
|
# local clock
|
||||||
- type: Clock
|
- type: Label
|
||||||
rect: [19, 90, 200, 50]
|
rect: [19, 90, 200, 50]
|
||||||
#format: "%h:%M %p" # 11:59 PM
|
|
||||||
format: "%H:%M" # 23:59
|
|
||||||
font_size: 46
|
font_size: 46
|
||||||
fg_color: "#ffffff"
|
fg_color: "#ffffff"
|
||||||
- type: Clock
|
source: Clock
|
||||||
|
format: "%H:%M" # 23:59
|
||||||
|
#format: "%h:%M %p" # 11:59 PM
|
||||||
|
|
||||||
|
# local date
|
||||||
|
- type: Label
|
||||||
rect: [20, 117, 200, 20]
|
rect: [20, 117, 200, 20]
|
||||||
|
font_size: 14
|
||||||
|
fg_color: "#ffffff"
|
||||||
|
source: Clock
|
||||||
format: "%x" # local date representation
|
format: "%x" # local date representation
|
||||||
font_size: 14
|
|
||||||
fg_color: "#ffffff"
|
# local day-of-week
|
||||||
- type: Clock
|
- type: Label
|
||||||
rect: [20, 137, 200, 50]
|
rect: [20, 137, 200, 50]
|
||||||
#format: "%a" # Tue
|
|
||||||
format: "%A" # Tuesday
|
|
||||||
font_size: 14
|
font_size: 14
|
||||||
fg_color: "#ffffff"
|
fg_color: "#ffffff"
|
||||||
|
source: Clock
|
||||||
|
format: "%A" # Tuesday
|
||||||
|
#format: "%a" # Tue
|
||||||
|
|
||||||
# alt clock 1
|
# alt clock 1
|
||||||
- type: Clock
|
- type: Label
|
||||||
rect: [210, 90, 200, 50]
|
rect: [210, 90, 200, 50]
|
||||||
timezone: "Asia/Tokyo" # change TZ1 here
|
|
||||||
format: "%H:%M"
|
|
||||||
font_size: 24
|
font_size: 24
|
||||||
fg_color: "#99BBAA"
|
fg_color: "#99BBAA"
|
||||||
|
source: Clock
|
||||||
|
timezone: "Asia/Tokyo" # change TZ1 here
|
||||||
|
format: "%H:%M" # 23:59
|
||||||
|
#format: "%h:%M %p" # 11:59 PM
|
||||||
- type: Label
|
- type: Label
|
||||||
rect: [210, 60, 200, 50]
|
rect: [210, 60, 200, 50]
|
||||||
font_size: 14
|
font_size: 14
|
||||||
fg_color: "#99BBAA"
|
fg_color: "#99BBAA"
|
||||||
|
source: Static
|
||||||
text: "Tokyo" # change TZ1 label here
|
text: "Tokyo" # change TZ1 label here
|
||||||
|
|
||||||
# alt clock 2
|
# alt clock 2
|
||||||
- type: Clock
|
- type: Label
|
||||||
rect: [210, 150, 200, 50]
|
rect: [210, 150, 200, 50]
|
||||||
timezone: "America/Chicago" # change TZ2 here
|
|
||||||
format: "%H:%M"
|
|
||||||
font_size: 24
|
font_size: 24
|
||||||
fg_color: "#AA99BB"
|
fg_color: "#AA99BB"
|
||||||
|
source: Clock
|
||||||
|
timezone: "America/Chicago" # change TZ2 here
|
||||||
|
format: "%H:%M" # 23:59
|
||||||
|
#format: "%h:%M %p" # 11:59 PM
|
||||||
- type: Label
|
- type: Label
|
||||||
rect: [210, 120, 200, 50]
|
rect: [210, 120, 200, 50]
|
||||||
font_size: 14
|
font_size: 14
|
||||||
fg_color: "#AA99BB"
|
fg_color: "#AA99BB"
|
||||||
|
source: Static
|
||||||
text: "Chicago" # change TZ2 label here
|
text: "Chicago" # change TZ2 label here
|
||||||
|
|
||||||
- type: Batteries
|
# batteries
|
||||||
|
- type: BatteryList
|
||||||
rect: [0, 0, 400, 30]
|
rect: [0, 0, 400, 30]
|
||||||
font_size: 14
|
font_size: 14
|
||||||
|
fg_color: "#99BBAA"
|
||||||
|
fg_color_low: "#B06060"
|
||||||
|
fg_color_charging: "#6080A0"
|
||||||
num_devices: 9
|
num_devices: 9
|
||||||
low_threshold: 20
|
|
||||||
layout: Horizontal
|
layout: Horizontal
|
||||||
normal_fg_color: "#99BBAA"
|
low_threshold: 20
|
||||||
# below is not yet implemented
|
|
||||||
normal_bg_color: "#353535"
|
|
||||||
low_fg_color: "#B06060"
|
|
||||||
low_bg_color: "#353535"
|
|
||||||
charging_fg_color: "#6080A0"
|
|
||||||
charging_bg_color: "#353535"
|
|
||||||
|
|
||||||
# sample
|
|
||||||
# - type: ExecLabel
|
|
||||||
# rect: [50, 20, 200, 50]
|
|
||||||
# font_size: 14
|
|
||||||
# fg_color: "#FFFFFF"
|
|
||||||
# exec: ["echo", "customize me! see watch.yaml"]
|
|
||||||
# interval: 0 # seconds
|
|
||||||
|
|
||||||
### MirrorButton
|
|
||||||
# Bring an additional PipeWire screen, window, region or virtual screen share into VR.
|
|
||||||
# These are view-only, and will not respond to pointers by moving your mouse.
|
|
||||||
# You may have as many as you like, but the `name` must be unique for each.
|
|
||||||
# Controls:
|
|
||||||
# - Blue Click: Show/hide. Shows pipewire prompt on first show.
|
|
||||||
# - Orange Click: Toggle lock in place
|
|
||||||
# - Purple Click: Stop capture. After doing this, you may Blue-click again to select a different source.
|
|
||||||
# - Scroll: Adjust opacity
|
|
||||||
# Warning:
|
|
||||||
# - Window shares may stop updating if the window goes off-screen or is on an inactive workspace
|
|
||||||
# - Resizing, minimizing, maximizing windows may break stuff. Complain to your xdg-desktop-portal implementation.
|
|
||||||
# - Selections are not saved across sessions
|
|
||||||
|
|
||||||
#- type: MirrorButton
|
|
||||||
# rect: [354, 0, 46, 32]
|
|
||||||
# font_size: 14
|
|
||||||
# fg_color: "#FFFFFF"
|
|
||||||
# bg_color: "#B05050"
|
|
||||||
# name: "M1"
|
|
||||||
# text: "M1"
|
|
||||||
# show_hide: false # should it respond to show/hide binding?
|
|
||||||
|
|
||||||
# volume buttons
|
# volume buttons
|
||||||
- type: ExecButton
|
- type: Button
|
||||||
rect: [327, 52, 46, 32]
|
rect: [327, 52, 46, 32]
|
||||||
font_size: 14
|
font_size: 14
|
||||||
fg_color: "#FFFFFF"
|
fg_color: "#FFFFFF"
|
||||||
bg_color: "#505050"
|
bg_color: "#505050"
|
||||||
text: "+"
|
text: "+"
|
||||||
exec: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ]
|
click_down:
|
||||||
- type: ExecButton
|
- type: Exec
|
||||||
|
command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%" ]
|
||||||
|
- type: Button
|
||||||
rect: [327, 116, 46, 32]
|
rect: [327, 116, 46, 32]
|
||||||
font_size: 14
|
font_size: 14
|
||||||
fg_color: "#FFFFFF"
|
fg_color: "#FFFFFF"
|
||||||
bg_color: "#505050"
|
bg_color: "#505050"
|
||||||
text: "-"
|
text: "-"
|
||||||
exec: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%" ]
|
click_down:
|
||||||
|
- type: Exec
|
||||||
|
command: [ "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%" ]
|
||||||
|
|||||||
32
src/state.rs
32
src/state.rs
@@ -1,8 +1,10 @@
|
|||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use glam::{Quat, Vec3};
|
use glam::Vec3;
|
||||||
use rodio::{OutputStream, OutputStreamHandle};
|
use rodio::{OutputStream, OutputStreamHandle};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
use vulkano::format::Format;
|
use vulkano::format::Format;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -15,9 +17,6 @@ use crate::{
|
|||||||
shaders::{frag_color, frag_glyph, frag_sprite, frag_srgb, vert_common},
|
shaders::{frag_color, frag_glyph, frag_sprite, frag_srgb, vert_common},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const WATCH_DEFAULT_POS: Vec3 = Vec3::new(-0.03, -0.01, 0.125);
|
|
||||||
pub const WATCH_DEFAULT_ROT: Quat = Quat::from_xyzw(-0.7071066, 0.0007963618, 0.7071066, 0.0);
|
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub fc: FontCache,
|
pub fc: FontCache,
|
||||||
pub session: AppSession,
|
pub session: AppSession,
|
||||||
@@ -27,6 +26,7 @@ pub struct AppState {
|
|||||||
pub input_state: InputState,
|
pub input_state: InputState,
|
||||||
pub hid_provider: Box<dyn HidProvider>,
|
pub hid_provider: Box<dyn HidProvider>,
|
||||||
pub audio: AudioOutput,
|
pub audio: AudioOutput,
|
||||||
|
pub screens: SmallVec<[ScreenMeta; 8]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -62,6 +62,7 @@ impl AppState {
|
|||||||
input_state: InputState::new(),
|
input_state: InputState::new(),
|
||||||
hid_provider: crate::hid::initialize(),
|
hid_provider: crate::hid::initialize(),
|
||||||
audio: AudioOutput::new(),
|
audio: AudioOutput::new(),
|
||||||
|
screens: smallvec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,12 +71,6 @@ pub struct AppSession {
|
|||||||
pub config_root_path: PathBuf,
|
pub config_root_path: PathBuf,
|
||||||
pub config: GeneralConfig,
|
pub config: GeneralConfig,
|
||||||
|
|
||||||
pub watch_hand: usize,
|
|
||||||
pub watch_pos: Vec3,
|
|
||||||
pub watch_rot: Quat,
|
|
||||||
|
|
||||||
pub primary_hand: usize,
|
|
||||||
|
|
||||||
pub color_norm: Vec3,
|
pub color_norm: Vec3,
|
||||||
pub color_shift: Vec3,
|
pub color_shift: Vec3,
|
||||||
pub color_alt: Vec3,
|
pub color_alt: Vec3,
|
||||||
@@ -91,10 +86,6 @@ impl AppSession {
|
|||||||
AppSession {
|
AppSession {
|
||||||
config_root_path,
|
config_root_path,
|
||||||
config,
|
config,
|
||||||
primary_hand: 1,
|
|
||||||
watch_hand: 0,
|
|
||||||
watch_pos: WATCH_DEFAULT_POS,
|
|
||||||
watch_rot: WATCH_DEFAULT_ROT,
|
|
||||||
color_norm: Vec3 {
|
color_norm: Vec3 {
|
||||||
x: 0.,
|
x: 0.,
|
||||||
y: 1.,
|
y: 1.,
|
||||||
@@ -145,3 +136,16 @@ impl AudioOutput {
|
|||||||
self.audio_stream.as_ref().map(|(_, h)| h)
|
self.audio_stream.as_ref().map(|(_, h)| h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ScreenMeta {
|
||||||
|
pub name: Arc<str>,
|
||||||
|
pub id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum LeftRight {
|
||||||
|
#[default]
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user